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
21fn 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
26fn 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
31fn 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
49fn 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#[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 if let Some(t) = contract.find_table_by_name(table_name) {
111 if let Some(&embedded_offset) = embedded_tables.get(table_name) {
113 let hex_offset = format!("{embedded_offset:04x}");
115 let push_bytes = format!("61{hex_offset}"); 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 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 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 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 embedded_tables.insert(table_name.to_string(), *offset);
192
193 if !utilized_tables.contains(&table_def) {
195 utilized_tables.push(table_def.clone());
196 }
197
198 let table_code = match table_def.kind {
200 TableKind::CodeTable => {
201 Codegen::gen_table_bytecode_builtin(contract, &table_def)?
203 }
204 TableKind::JumpTable | TableKind::JumpTablePacked => {
205 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 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 let push_bytes = push_value.to_hex_full_with_opcode();
253 *offset += 33; bytes.push_with_offset(starting_offset, Bytes::Raw(push_bytes));
255 }
256 BuiltinFunctionKind::LeftPad => {
257 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 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_gen(name, contract, span)?.map(|pv| pv.to_hex_trimmed()).unwrap_or_default()
275 }
276 BuiltinFunctionArg::BuiltinFunctionCall(inner_call) => {
277 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 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 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 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 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 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 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_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 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 }
405 }
406 Ok(())
407}
408
409#[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 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 let codesize_arg = macro_name;
464 let is_previous_parent = scope_mgr.macro_stack().iter().any(|def| def.name == *codesize_arg);
465
466 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 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 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
509pub 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 bytes[0..4].copy_from_slice(&error.selector);
529 } else {
530 let selector: [u8; 4] = keccak256(&arg_str)[..4].try_into().unwrap();
531 bytes[28..32].copy_from_slice(&selector);
533 }
534
535 Ok(PushValue::from(bytes))
536}
537
538pub 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
574pub 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() }
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 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 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 let start_idx = 32 - hex_bytes.len();
617 bytes_array[start_idx..].copy_from_slice(&hex_bytes);
618 } else {
619 bytes_array[..hex_bytes.len()].copy_from_slice(&hex_bytes);
621 }
622
623 Ok(PushValue::new(B256::from(bytes_array)))
624}