Skip to main content

huff_neo_codegen/irgen/
builtin_function.rs

1use crate::Codegen;
2use crate::irgen::constants::{constant_gen, evaluate_constant_value, lookup_constant};
3use crate::irgen::statements::evaluate_expression_with_context;
4use alloy_primitives::{B256, U256, hex, keccak256};
5use huff_neo_utils::ast::huff::RUNTIME_CODESIZE_ARG;
6use huff_neo_utils::builtin_eval::{PadDirection, eval_builtin_bytes, eval_event_hash, eval_function_signature};
7use huff_neo_utils::bytecode::{
8    AssertPcPlaceholderData, BytecodeRes, BytecodeSegments, Bytes, CircularCodesizePlaceholderData, Jump, JumpPlaceholderData, Jumps,
9    PushOpcode,
10};
11use huff_neo_utils::bytes_util::{bytes32_to_hex_string, format_even_bytes};
12use huff_neo_utils::error::{CodegenError, CodegenErrorKind};
13use huff_neo_utils::evm_version::EVMVersion;
14use huff_neo_utils::prelude::{
15    AstSpan, BuiltinFunctionArg, BuiltinFunctionCall, BuiltinFunctionKind, ConstVal, Contract, MacroDefinition, PushValue, TableDefinition,
16    TableKind,
17};
18use huff_neo_utils::scope::ScopeManager;
19use std::collections::BTreeMap;
20
21/// Creates a CodegenError for invalid arguments
22fn invalid_arguments_error(msg: impl Into<String>, span: &AstSpan) -> CodegenError {
23    CodegenError { kind: CodegenErrorKind::InvalidArguments(msg.into()), span: span.clone_box(), token: None }
24}
25
26/// Creates a CodegenError for invalid hex strings
27fn invalid_hex_error(hex_str: impl Into<String>, span: &AstSpan) -> CodegenError {
28    CodegenError { kind: CodegenErrorKind::InvalidHex(hex_str.into()), span: span.clone_box(), token: None }
29}
30
31/// Validates that the builtin function call has exactly the expected number of arguments
32fn validate_arg_count(bf: &BuiltinFunctionCall, expected: usize, fn_name: &str) -> Result<(), CodegenError> {
33    if bf.args.len() != expected {
34        tracing::error!(
35            target: "codegen",
36            "Incorrect number of arguments passed to {}, should be {}: {}",
37            fn_name,
38            expected,
39            bf.args.len()
40        );
41        return Err(invalid_arguments_error(
42            format!("Incorrect number of arguments passed to {}, should be {}: {}", fn_name, expected, bf.args.len()),
43            &bf.span,
44        ));
45    }
46    Ok(())
47}
48
49/// Validates that a string contains only hexadecimal characters
50fn validate_hex_string(s: &str, span: &AstSpan) -> Result<(), CodegenError> {
51    if s.chars().all(|c| c.is_ascii_hexdigit()) { Ok(()) } else { Err(invalid_hex_error(s, span)) }
52}
53
54/// Generates bytecode for all builtin function calls
55///
56/// This is the main dispatcher that handles all builtin function types:
57/// - __codesize, __tablesize, __tablestart - Size and location utilities
58/// - __FUNC_SIG, __EVENT_HASH, __ERROR - ABI encoding helpers
59/// - __RIGHTPAD, __LEFTPAD - Byte padding (code tables only)
60/// - __BYTES - Raw bytes insertion
61/// - __VERBATIM - Direct hex injection
62/// - __argsstart - Offset of the constructor arguments in the deployment bytecode
63/// - __ASSERT_PC - Compile-time program counter assertions
64///
65/// TODO: First step to refactor and split the function into smaller functions
66#[allow(clippy::too_many_arguments)]
67pub fn builtin_function_gen<'a>(
68    evm_version: &EVMVersion,
69    contract: &'a Contract,
70    macro_def: &MacroDefinition,
71    scope_mgr: &mut ScopeManager<'a>,
72    offset: &mut usize,
73    table_instances: &mut Jumps,
74    utilized_tables: &mut Vec<TableDefinition>,
75    embedded_tables: &mut BTreeMap<String, usize>,
76    starting_offset: usize,
77    bytes: &mut BytecodeSegments,
78    bf: &BuiltinFunctionCall,
79    relax_jumps: bool,
80) -> Result<(), CodegenError> {
81    tracing::info!(target: "codegen", "RECURSE BYTECODE GOT BUILTIN FUNCTION CALL: {:?}", bf);
82    match bf.kind {
83        BuiltinFunctionKind::Codesize => {
84            let (codesize_offset, bytes_variant) = codesize(evm_version, contract, macro_def, scope_mgr, offset, bf, relax_jumps)?;
85
86            *offset += codesize_offset;
87            bytes.push_with_offset(starting_offset, bytes_variant);
88        }
89        BuiltinFunctionKind::Tablesize => {
90            let (ir_table, push_bytes) = tablesize(contract, bf)?;
91
92            if !utilized_tables.contains(&ir_table) {
93                utilized_tables.push(ir_table);
94            }
95
96            *offset += push_bytes.len() / 2;
97            bytes.push_with_offset(starting_offset, Bytes::Raw(push_bytes));
98        }
99        BuiltinFunctionKind::Tablestart => {
100            validate_arg_count(bf, 1, "__tablestart")?;
101
102            let table_name = match &bf.args[0] {
103                BuiltinFunctionArg::Identifier(name, _) => name,
104                _ => {
105                    return Err(invalid_arguments_error("Expected identifier as argument to __tablestart", &bf.span));
106                }
107            };
108
109            // Make sure the table exists
110            if let Some(t) = contract.find_table_by_name(table_name) {
111                // Check if the table was embedded
112                if let Some(&embedded_offset) = embedded_tables.get(table_name) {
113                    // Table was embedded - push the actual embedding offset directly
114                    let hex_offset = format!("{embedded_offset:04x}");
115                    let push_bytes = format!("61{hex_offset}"); // PUSH2
116
117                    tracing::debug!(
118                        target: "codegen",
119                        "__tablestart for embedded table \"{}\" at embedded offset 0x{:04x}",
120                        table_name,
121                        embedded_offset
122                    );
123
124                    bytes.push_with_offset(*offset, Bytes::Raw(push_bytes));
125                    *offset += 3;
126                } else {
127                    // Table not embedded - use placeholder for end-placement
128                    tracing::debug!(target: "codegen", "Creating table instance for {} at offset {}", table_name, *offset);
129                    let scope_id = scope_mgr.current_scope();
130                    table_instances.push(Jump { label: table_name.to_owned(), bytecode_index: *offset, span: bf.span.clone(), scope_id });
131
132                    bytes.push_with_offset(
133                        *offset,
134                        Bytes::JumpPlaceholder(JumpPlaceholderData::new(table_name.to_owned(), PushOpcode::Push2, String::new())),
135                    );
136                    *offset += 3;
137                }
138
139                if !utilized_tables.contains(&t) {
140                    utilized_tables.push(t);
141                }
142            } else {
143                tracing::error!(
144                    target: "codegen",
145                    "MISSING TABLE PASSED TO __tablestart \"{}\"",
146                    table_name
147                );
148                return Err(CodegenError {
149                    kind: CodegenErrorKind::InvalidMacroInvocation(table_name.to_string()),
150                    span: bf.span.clone_box(),
151                    token: None,
152                });
153            }
154        }
155        BuiltinFunctionKind::EmbedTable => {
156            validate_arg_count(bf, 1, "__EMBED_TABLE")?;
157
158            let table_name = match &bf.args[0] {
159                BuiltinFunctionArg::Identifier(name, _) => name,
160                _ => {
161                    return Err(invalid_arguments_error("Expected identifier as argument to __EMBED_TABLE", &bf.span));
162                }
163            };
164
165            // Find the table definition
166            let table_def = if let Some(t) = contract.find_table_by_name(table_name) {
167                t
168            } else {
169                tracing::error!(
170                    target: "codegen",
171                    "MISSING TABLE PASSED TO __EMBED_TABLE \"{}\"",
172                    table_name
173                );
174                return Err(CodegenError {
175                    kind: CodegenErrorKind::InvalidMacroInvocation(table_name.to_string()),
176                    span: bf.span.clone_box(),
177                    token: None,
178                });
179            };
180
181            // Check if table was already embedded (we only allow one embedding per table)
182            if embedded_tables.contains_key(table_name) {
183                return Err(CodegenError {
184                    kind: CodegenErrorKind::DuplicateTableEmbedding(table_name.to_string()),
185                    span: bf.span.clone_box(),
186                    token: None,
187                });
188            }
189
190            // Track the embedding position (for __tablestart)
191            embedded_tables.insert(table_name.to_string(), *offset);
192
193            // Add table to utilized tables if not already there
194            if !utilized_tables.contains(&table_def) {
195                utilized_tables.push(table_def.clone());
196            }
197
198            // Generate the table bytecode inline
199            let table_code = match table_def.kind {
200                TableKind::CodeTable => {
201                    // Use the existing gen_table_bytecode_builtin function for code tables
202                    Codegen::gen_table_bytecode_builtin(contract, &table_def)?
203                }
204                TableKind::JumpTable | TableKind::JumpTablePacked => {
205                    // Generate jump table bytecode inline
206                    // We need access to label_indices from BytecodeRes, but we don't have it here
207                    // For now we only support code tables with __EMBED_TABLE
208                    return Err(CodegenError {
209                        kind: CodegenErrorKind::InvalidArguments(
210                            "Jump tables cannot be embedded inline with __EMBED_TABLE yet. Only code tables are supported.".to_string(),
211                        ),
212                        span: bf.span.clone_box(),
213                        token: None,
214                    });
215                }
216            };
217
218            // Insert the table bytecode at the current position
219            let table_size_bytes = table_code.len() / 2;
220            *offset += table_size_bytes;
221            bytes.push_with_offset(starting_offset, Bytes::Raw(table_code));
222
223            tracing::debug!(
224                target: "codegen",
225                "Embedded table \"{}\" at offset {}, size {} bytes",
226                table_name,
227                starting_offset,
228                table_size_bytes
229            );
230        }
231        BuiltinFunctionKind::FunctionSignature => {
232            let push_value = eval_function_signature(contract, bf)?;
233            let push_bytes = push_value.to_hex_with_opcode(evm_version);
234            *offset += push_bytes.len() / 2;
235            bytes.push_with_offset(starting_offset, Bytes::Raw(push_bytes));
236        }
237        BuiltinFunctionKind::EventHash => {
238            let push_value = eval_event_hash(contract, bf)?;
239            let push_bytes = push_value.to_hex_with_opcode(evm_version);
240            *offset += push_bytes.len() / 2;
241            bytes.push_with_offset(starting_offset, Bytes::Raw(push_bytes));
242        }
243        BuiltinFunctionKind::Error => {
244            let push_value = error(contract, bf)?;
245            let push_bytes = push_value.to_hex_with_opcode(evm_version);
246            *offset += push_bytes.len() / 2;
247            bytes.push_with_offset(starting_offset, Bytes::Raw(push_bytes));
248        }
249        BuiltinFunctionKind::RightPad => {
250            let push_value = builtin_pad(contract, bf, PadDirection::Right)?;
251            // For padding, always use PUSH32 with full 32 bytes (no optimization)
252            let push_bytes = push_value.to_hex_full_with_opcode();
253            *offset += 33; // PUSH32 opcode (1 byte) + 32 bytes of data
254            bytes.push_with_offset(starting_offset, Bytes::Raw(push_bytes));
255        }
256        BuiltinFunctionKind::LeftPad => {
257            // __LEFTPAD is only available in constant definitions and code tables, not in macro bodies
258            return Err(invalid_arguments_error("__LEFTPAD is only available in constant definitions and code tables", &bf.span));
259        }
260        BuiltinFunctionKind::ArgsStart => {
261            validate_arg_count(bf, 0, "__argsstart")?;
262            // Filled with the deployment offset of the arguments; rejected outside the constructor
263            let placeholder = Bytes::ArgsStartPlaceholder(bf.span.clone());
264            *offset += placeholder.len();
265            bytes.push_with_offset(starting_offset, placeholder);
266        }
267        BuiltinFunctionKind::Verbatim => {
268            validate_arg_count(bf, 1, "__VERBATIM")?;
269
270            let verbatim_str = match &bf.args[0] {
271                BuiltinFunctionArg::HexLiteral(hex, _) => hex.clone(),
272                BuiltinFunctionArg::Constant(name, span) => {
273                    // Constant reference - resolve it
274                    constant_gen(name, contract, span)?.map(|pv| pv.to_hex_trimmed()).unwrap_or_default()
275                }
276                BuiltinFunctionArg::BuiltinFunctionCall(inner_call) => {
277                    // Nested builtin function - evaluate and strip PUSH opcode
278                    match inner_call.kind {
279                        BuiltinFunctionKind::FunctionSignature => eval_function_signature(contract, inner_call)?.to_hex_trimmed(),
280                        BuiltinFunctionKind::Bytes => eval_builtin_bytes(inner_call)?.to_hex_trimmed(),
281                        BuiltinFunctionKind::EventHash => eval_event_hash(contract, inner_call)?.to_hex_trimmed(),
282                        BuiltinFunctionKind::Error => error(contract, inner_call)?.to_hex_trimmed(),
283                        // Padding functions always produce 32 bytes - use to_hex_full() to preserve
284                        BuiltinFunctionKind::RightPad => builtin_pad(contract, inner_call, PadDirection::Right)?.to_hex_full(),
285                        BuiltinFunctionKind::LeftPad => builtin_pad(contract, inner_call, PadDirection::Left)?.to_hex_full(),
286                        _ => {
287                            return Err(invalid_arguments_error(
288                                "Invalid builtin function passed to __VERBATIM. Supported: __FUNC_SIG, __EVENT_HASH, __BYTES, __ERROR, __RIGHTPAD, __LEFTPAD",
289                                &bf.span,
290                            ));
291                        }
292                    }
293                }
294                _ => {
295                    return Err(invalid_arguments_error(
296                        "Expected hex literal, constant reference, or builtin function as argument to __VERBATIM",
297                        &bf.span,
298                    ));
299                }
300            };
301
302            // check if verbatim was passed a hex string
303            if let Err(e) = validate_hex_string(&verbatim_str, &bf.span) {
304                tracing::error!(
305                    target: "codegen",
306                    "INVALID HEX STRING PASSED TO __VERBATIM: \"{}\"",
307                    verbatim_str
308                );
309                return Err(e);
310            }
311
312            tracing::debug!(target: "codegen", "INJECTING as verbatim: {}", verbatim_str);
313            let hex = format_even_bytes(verbatim_str);
314            let push_bytes = hex.to_string();
315            *offset += hex.len() / 2;
316
317            bytes.push_with_offset(starting_offset, Bytes::Raw(push_bytes));
318        }
319        BuiltinFunctionKind::Bytes => {
320            validate_arg_count(bf, 1, "__BYTES")?;
321
322            let string_value = match &bf.args[0] {
323                BuiltinFunctionArg::StringLiteral(s, _) => s.clone(),
324                BuiltinFunctionArg::Constant(name, span) => {
325                    // Constant reference - check what type it is
326                    let constant = lookup_constant(name, contract, span)?;
327                    match &constant.value {
328                        ConstVal::String(s) => s.clone(),
329                        ConstVal::BuiltinFunctionCall(inner_bf) if matches!(inner_bf.kind, BuiltinFunctionKind::Bytes) => {
330                            // Allow chained __BYTES constants
331                            eval_builtin_bytes(inner_bf)?.to_hex_trimmed()
332                        }
333                        _ => {
334                            return Err(invalid_arguments_error(
335                                format!("__BYTES requires a string literal or string constant. Constant [{}] is not a string.", name),
336                                &bf.span,
337                            ));
338                        }
339                    }
340                }
341                _ => {
342                    return Err(invalid_arguments_error("Expected string literal or constant reference as argument to __BYTES", &bf.span));
343                }
344            };
345
346            if string_value.is_empty() {
347                return Err(invalid_arguments_error("Empty string passed to __BYTES", &bf.span));
348            }
349
350            // Convert string to bytes and left-pad to 32 bytes
351            let bytes_slice = string_value.as_bytes();
352            if bytes_slice.len() > 32 {
353                return Err(invalid_arguments_error("Encoded bytes length exceeds 32 bytes", &bf.span));
354            }
355
356            let mut bytes_array = [0u8; 32];
357            bytes_array[32 - bytes_slice.len()..].copy_from_slice(bytes_slice);
358            let push_value = PushValue::from(bytes_array);
359
360            let push_bytes = push_value.to_hex_with_opcode(evm_version);
361            *offset += push_bytes.len() / 2;
362            bytes.push_with_offset(starting_offset, Bytes::Raw(push_bytes));
363        }
364        BuiltinFunctionKind::AssertPc => {
365            validate_arg_count(bf, 1, "__ASSERT_PC")?;
366
367            // Extract the expected position from the argument
368            let expected_position = match &bf.args[0] {
369                BuiltinFunctionArg::HexLiteral(hex_str, _) => usize::from_str_radix(hex_str, 16).map_err(|_| {
370                    invalid_arguments_error(format!("Invalid hex literal value passed to __ASSERT_PC: 0x{}", hex_str), &bf.span)
371                })?,
372                BuiltinFunctionArg::Constant(name, span) => {
373                    // Evaluate constant directly to numeric value without generating bytecode
374                    evaluate_constant_value(name, contract, span)?
375                }
376                BuiltinFunctionArg::Expression(expr) => {
377                    let value = U256::from_be_bytes(evaluate_expression_with_context(expr, contract, scope_mgr)?);
378                    usize::try_from(value)
379                        .map_err(|_| invalid_arguments_error(format!("__ASSERT_PC position {value} is too large"), &bf.span))?
380                }
381                _ => {
382                    return Err(invalid_arguments_error(
383                        "Expected hex literal, constant, or expression as argument to __ASSERT_PC",
384                        &bf.span,
385                    ));
386                }
387            };
388
389            // Create a placeholder for PC assertion validation
390            // This will be checked after jump relaxation in fill_unmatched()
391            // to ensure the assertion uses post-relaxation offsets
392            bytes.push_with_offset(
393                starting_offset,
394                Bytes::AssertPcPlaceholder(AssertPcPlaceholderData::new(expected_position, bf.span.clone())),
395            );
396
397            tracing::debug!(
398                target: "codegen",
399                "Created __ASSERT_PC placeholder: expecting offset 0x{:x} (will validate after relaxation)",
400                expected_position
401            );
402
403            // No bytecode is generated - this is a compile-time validation placeholder
404        }
405    }
406    Ok(())
407}
408
409/// Generates bytecode for the __codesize builtin function
410///
411/// Returns a tuple of (byte_offset, Bytes variant) containing the size of the specified macro.
412/// Handles circular references by creating a CircularCodesizePlaceholder that's filled in later.
413#[allow(clippy::too_many_arguments)]
414fn codesize<'a>(
415    evm_version: &EVMVersion,
416    contract: &'a Contract,
417    macro_def: &MacroDefinition,
418    scope_mgr: &mut ScopeManager<'a>,
419    offset: &mut usize,
420    bf: &BuiltinFunctionCall,
421    relax_jumps: bool,
422) -> Result<(usize, Bytes), CodegenError> {
423    validate_arg_count(bf, 1, "__codesize")?;
424
425    let macro_name = match &bf.args[0] {
426        BuiltinFunctionArg::Identifier(name, _) => name,
427        _ => {
428            return Err(invalid_arguments_error("Expected identifier as argument to __codesize", &bf.span));
429        }
430    };
431
432    // Magic argument: RUNTIME resolves to the byte length of the runtime section
433    // (MAIN body + appended runtime tables). MAIN-side use converges via iterative MAIN
434    // codegen; constructor-side use sees the converged value.
435    if macro_name == RUNTIME_CODESIZE_ARG {
436        let size = contract.runtime_size.ok_or_else(|| CodegenError {
437            kind: CodegenErrorKind::RuntimeSizeNotComputed,
438            span: bf.span.clone_box(),
439            token: None,
440        })?;
441        let push_value = PushValue::from(U256::from(size).to_be_bytes::<32>());
442        let push_bytes = push_value.to_hex_with_opcode(evm_version);
443        let offset = push_bytes.len() / 2;
444        return Ok((offset, Bytes::Raw(push_bytes)));
445    }
446
447    let ir_macro = if let Some(m) = contract.find_macro_by_name(macro_name) {
448        m
449    } else {
450        tracing::error!(
451            target: "codegen",
452            "MISSING MACRO PASSED TO __codesize \"{}\"",
453            macro_name
454        );
455        return Err(CodegenError {
456            kind: CodegenErrorKind::MissingMacroDefinition(macro_name.to_string()),
457            span: bf.span.clone_box(),
458            token: None,
459        });
460    };
461
462    // Get the name of the macro being passed to __codesize
463    let codesize_arg = macro_name;
464    let is_previous_parent = scope_mgr.macro_stack().iter().any(|def| def.name == *codesize_arg);
465
466    // Special case:
467    // If the macro provided to __codesize is the current macro, we need to avoid a
468    // circular reference If this is the case we will store a
469    // placeholder inside the bytecode and fill it in later when
470    // we have adequate information about the macros eventual size.
471    // We also need to avoid if the codesize arg is any of the previous macros to
472    // avoid a circular reference
473    let (codesize_offset, bytes_variant) = if is_previous_parent || macro_def.name.eq(codesize_arg) {
474        tracing::debug!(target: "codegen", "CIRCULAR CODESIZE INVOCATION DETECTED INJECTING PLACEHOLDER | macro: {}", ir_macro.name);
475
476        // Create a CircularCodesizePlaceholder variant (starts as PUSH1, 2 bytes)
477        let placeholder = Bytes::CircularCodesizePlaceholder(CircularCodesizePlaceholderData::new(
478            codesize_arg.to_string(),
479            scope_mgr.codesize_hint(codesize_arg),
480        ));
481        (2, placeholder)
482    } else {
483        // We will still need to recurse to get accurate values. The target macro gets its own
484        // scope frame; `macro_to_bytecode` pops it when done, leaving the caller's frame intact.
485        scope_mgr.start_measurement();
486        scope_mgr.push_macro(ir_macro, *offset);
487        let res = Codegen::macro_to_bytecode(evm_version, ir_macro, contract, scope_mgr, *offset, relax_jumps);
488        scope_mgr.end_measurement();
489        let res: BytecodeRes = match res {
490            Ok(r) => r,
491            Err(e) => {
492                tracing::error!(
493                    target: "codegen",
494                    "FAILED TO RECURSE INTO MACRO \"{}\"",
495                    ir_macro.name
496                );
497                return Err(e);
498            }
499        };
500
501        let size = format_even_bytes(format!("{:02x}", res.bytes.iter().map(|seg| seg.bytes.len()).sum::<usize>()));
502        let push_bytes = format!("{:02x}{size}", 95 + size.len() / 2);
503        let offset = push_bytes.len() / 2;
504        (offset, Bytes::Raw(push_bytes))
505    };
506    Ok((codesize_offset, bytes_variant))
507}
508
509/// Generates a PushValue for the __ERROR builtin function
510///
511/// Returns a PushValue containing the error selector.
512/// For error definitions: selector at start, right-padded with zeros (PUSH32 equivalent).
513/// For raw signatures: left-padded selector (PUSH4 equivalent).
514pub fn error(contract: &Contract, bf: &BuiltinFunctionCall) -> Result<PushValue, CodegenError> {
515    validate_arg_count(bf, 1, "__ERROR")?;
516
517    let arg_str = match &bf.args[0] {
518        BuiltinFunctionArg::StringLiteral(s, _) => s.clone(),
519        BuiltinFunctionArg::Identifier(name, _) => name.clone(),
520        _ => {
521            return Err(invalid_arguments_error("Expected string literal or identifier as argument to __ERROR", &bf.span));
522        }
523    };
524
525    let mut bytes = [0u8; 32];
526    if let Some(error) = contract.errors.iter().find(|e| e.name.eq(&arg_str)) {
527        // Put selector at start, right-padded with zeros (rest is already zero)
528        bytes[0..4].copy_from_slice(&error.selector);
529    } else {
530        let selector: [u8; 4] = keccak256(&arg_str)[..4].try_into().unwrap();
531        // Left-pad the selector (put at end)
532        bytes[28..32].copy_from_slice(&selector);
533    }
534
535    Ok(PushValue::from(bytes))
536}
537
538/// Generates bytecode for the __tablesize builtin function
539///
540/// Returns a tuple of (TableDefinition, bytecode_string) with the size of the specified jump table.
541/// The bytecode is a PUSH instruction with the table size in bytes.
542pub fn tablesize(contract: &Contract, bf: &BuiltinFunctionCall) -> Result<(TableDefinition, String), CodegenError> {
543    validate_arg_count(bf, 1, "__tablesize")?;
544
545    let table_name = match &bf.args[0] {
546        BuiltinFunctionArg::Identifier(name, _) => name.clone(),
547        _ => {
548            return Err(invalid_arguments_error("Expected identifier as argument to __tablesize", &bf.span));
549        }
550    };
551
552    let ir_table = if let Some(t) = contract.find_table_by_name(&table_name) {
553        t
554    } else {
555        tracing::error!(
556            target: "codegen",
557            "MISSING TABLE PASSED TO __tablesize \"{}\"",
558            table_name
559        );
560        return Err(CodegenError { kind: CodegenErrorKind::InvalidMacroInvocation(table_name), span: bf.span.clone_box(), token: None });
561    };
562    let Some(table_size) = ir_table.size else {
563        return Err(CodegenError {
564            kind: CodegenErrorKind::MissingTableSize(ir_table.name.clone()),
565            span: bf.span.clone_box(),
566            token: None,
567        });
568    };
569    let size = bytes32_to_hex_string(&table_size, false);
570    let push_bytes = format!("{:02x}{size}", 95 + size.len() / 2);
571    Ok((ir_table, push_bytes))
572}
573
574/// Generates a PushValue for left or right padding a byte string to 32 bytes
575pub fn builtin_pad(contract: &Contract, bf: &BuiltinFunctionCall, direction: PadDirection) -> Result<PushValue, CodegenError> {
576    validate_arg_count(bf, 1, &direction.to_string())?;
577    let first_arg = match &bf.args[0] {
578        BuiltinFunctionArg::HexLiteral(hex, _) => hex.clone(),
579        BuiltinFunctionArg::BuiltinFunctionCall(inner_call) => match inner_call.kind {
580            BuiltinFunctionKind::FunctionSignature => eval_function_signature(contract, inner_call)?.to_hex_trimmed(),
581            BuiltinFunctionKind::Bytes => eval_builtin_bytes(inner_call)?.to_hex_trimmed(),
582            BuiltinFunctionKind::EventHash => eval_event_hash(contract, inner_call)?.to_hex_trimmed(),
583            _ => {
584                tracing::error!(target: "codegen", "Invalid function call argument type passed to {direction}");
585                return Err(invalid_arguments_error(format!("Invalid argument type passed to {direction}"), &bf.span));
586            }
587        },
588        BuiltinFunctionArg::Constant(name, span) => {
589            constant_gen(name, contract, span)?.map(|pv| pv.to_hex_trimmed()).unwrap_or_default() // __NOOP returns empty string
590        }
591        _ => {
592            tracing::error!(target: "codegen", "Invalid argument type passed to {direction}: {:?}", bf.args[0]);
593            return Err(invalid_arguments_error(format!("Invalid argument type passed to {direction}"), &bf.span));
594        }
595    };
596
597    // Ensure hex string has even length for decoding
598    // For RIGHTPAD with odd-length hex, append "0" on the right to preserve visual appearance
599    // For LEFTPAD with odd-length hex, prepend "0" on the left
600    let hex = if first_arg.len() % 2 == 1 {
601        if direction == PadDirection::Right { format!("{first_arg}0") } else { format!("0{first_arg}") }
602    } else {
603        first_arg
604    };
605
606    // Parse hex string to bytes
607    let hex_bytes = hex::decode(&hex).map_err(|_| invalid_hex_error(&hex, &bf.span))?;
608
609    if hex_bytes.len() > 32 {
610        return Err(invalid_arguments_error("Hex string exceeds 32 bytes", &bf.span));
611    }
612
613    let mut bytes_array = [0u8; 32];
614    if direction == PadDirection::Left {
615        // Left pad: zeros on the left, data on the right
616        let start_idx = 32 - hex_bytes.len();
617        bytes_array[start_idx..].copy_from_slice(&hex_bytes);
618    } else {
619        // Right pad: data on the left, zeros on the right
620        bytes_array[..hex_bytes.len()].copy_from_slice(&hex_bytes);
621    }
622
623    Ok(PushValue::new(B256::from(bytes_array)))
624}