diff --git a/crates/codegen/src/sonatina/lower_runtime.rs b/crates/codegen/src/sonatina/lower_runtime.rs index 512de8a293..2617bbbe81 100644 --- a/crates/codegen/src/sonatina/lower_runtime.rs +++ b/crates/codegen/src/sonatina/lower_runtime.rs @@ -59,6 +59,9 @@ use sonatina_ir::{ use super::{LowerError, create_module_ctx}; use crate::function_symbols::{FunctionSymbolInput, assign_function_symbols}; +// Sonatina's EVM calling convention can carry at most 16 arguments. +const MAX_DIRECT_CALL_ARGS: usize = 16; + const PANIC_OVERFLOW: u64 = 0x11; const PANIC_DIVISION_BY_ZERO: u64 = 0x12; @@ -88,6 +91,7 @@ struct ModuleLowerer<'db, 'a> { package: &'a RuntimePackage<'db>, func_map: FxHashMap, FuncRef>, func_symbols: FxHashMap, String>, + argument_packs: FxHashMap, Type>, section_membership: FxHashMap, Vec>, type_cache: FxHashMap, Type>, layout_names: FxHashMap, String>, @@ -109,6 +113,7 @@ impl<'db, 'a> ModuleLowerer<'db, 'a> { isa, package, func_map: FxHashMap::default(), + argument_packs: FxHashMap::default(), func_symbols: assign_sonatina_function_symbols(db, package), section_membership: compute_section_membership(db, package), type_cache: FxHashMap::default(), @@ -162,7 +167,7 @@ impl<'db, 'a> ModuleLowerer<'db, 'a> { fn lower_signature(&mut self, function: RuntimeFunction<'db>) -> Result { let body = function.instance(self.db).body(self.db); - let args = body + let mut args = body .signature .params .iter() @@ -174,7 +179,20 @@ impl<'db, 'a> ModuleLowerer<'db, 'a> { .as_ref() .map(|class| self.ty_for_class(class)) .transpose()?; - let symbol = self.function_symbol(function.instance(self.db)); + let instance = function.instance(self.db); + let symbol = self.function_symbol(instance); + // Sonatina may add an out pointer for a compound return value. Reserve + // that slot before its aggregate ABI legalization runs. + let return_slots = usize::from(matches!(ret, Some(Type::Compound(_)))); + if args.len() + return_slots > MAX_DIRECT_CALL_ARGS { + // Keep the fields typed, including object references and aggregates. + // A fresh object at each call also keeps recursive calls independent. + let pack = self + .builder + .declare_struct_type(&format!("{symbol}__args"), &args, false); + self.argument_packs.insert(instance, pack); + args = vec![self.builder.objref_type(pack)]; + } Ok(match ret { Some(ret) => Signature::new_single( &symbol, @@ -909,7 +927,16 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { Ok(()) } - fn body_signature_arg(&self, idx: usize) -> Result { + fn body_signature_arg(&mut self, idx: usize) -> Result { + if self.module.argument_packs.contains_key(&self.body.owner) { + let pack = self.fb.func.arg_values[0]; + let class = self.body.signature.params[idx].class.clone(); + let ty = self.module.ty_for_class(&class)?; + let field = self.argument_pack_field(pack, idx, ty); + return Ok(self + .fb + .insert_inst(ObjLoad::new(self.module.inst_set(), field), ty)); + } self.fb .func .arg_values @@ -918,6 +945,37 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { .ok_or_else(|| LowerError::Internal(format!("missing arg value {idx}"))) } + fn argument_pack_field(&mut self, pack: ValueId, idx: usize, ty: Type) -> ValueId { + let index = self.index_value(idx as u64); + let field_ty = self.fb.module_builder.objref_type(ty); + self.fb.insert_inst( + ObjProj::new(self.module.inst_set(), smallvec![pack, index]), + field_ty, + ) + } + + fn lower_call_args( + &mut self, + callee: RuntimeInstance<'db>, + args: &[RLocalId], + ) -> Result, LowerError> { + let Some(&pack_ty) = self.module.argument_packs.get(&callee) else { + return args.iter().map(|arg| self.local_value(*arg)).collect(); + }; + let ref_ty = self.fb.module_builder.objref_type(pack_ty); + let pack = self + .fb + .insert_inst(ObjAlloc::new(self.module.inst_set(), pack_ty), ref_ty); + for (idx, arg) in args.iter().enumerate() { + let value = self.local_value(*arg)?; + let ty = self.fb.func.dfg.value_ty(value); + let field = self.argument_pack_field(pack, idx, ty); + self.fb + .insert_inst_no_result(ObjStore::new(self.module.inst_set(), field, value)); + } + Ok(smallvec![pack]) + } + fn lower_stmt(&mut self, stmt: &RStmt<'db>) -> Result, LowerError> { match stmt { RStmt::Assign { dst, expr } => { @@ -1165,10 +1223,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { } => self.lower_layout_map_patch(map, *source, *index, *replacement)?, RExpr::Call { callee, args } => { let callee_ref = self.module.func_ref(*callee)?; - let args = args - .iter() - .map(|arg| self.local_value(*arg)) - .collect::, _>>()?; + let args = self.lower_call_args(*callee, args)?; let ret = callee.body(self.module.db).signature.ret.clone(); match ret { Some(class) => { @@ -2313,10 +2368,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { )); } RTerminator::TerminalCall { callee, args } => { - let args = args - .iter() - .map(|arg| self.local_value(*arg)) - .collect::, _>>()?; + let args = self.lower_call_args(*callee, args)?; self.fb.insert_inst_no_result(Call::new( self.module.inst_set(), self.module.func_ref(*callee)?, diff --git a/crates/fe/tests/cli_output.rs b/crates/fe/tests/cli_output.rs index 5051359e8d..b6dd1b7b59 100644 --- a/crates/fe/tests/cli_output.rs +++ b/crates/fe/tests/cli_output.rs @@ -3814,3 +3814,21 @@ fn unsupported_macro_calls_are_cli_errors() { } } } + +#[test] +fn wide_function_arguments_at_all_optimization_levels() { + let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/fe_test/wide_function_arguments.fe"); + // The fixture test covers the default level (1). + for level in ["0", "2", "s"] { + let (output, exit_code) = run_fe_main(&[ + "test", + "--jobs", + "1", + "--optimize", + level, + fixture.to_str().expect("fixture path utf8"), + ]); + assert_eq!(exit_code, 0, "wide calls failed at -O {level}:\n{output}"); + } +} diff --git a/crates/fe/tests/fixtures/fe_test/wide_function_arguments.fe b/crates/fe/tests/fixtures/fe_test/wide_function_arguments.fe new file mode 100644 index 0000000000..34e5ea4e93 --- /dev/null +++ b/crates/fe/tests/fixtures/fe_test/wide_function_arguments.fe @@ -0,0 +1,589 @@ +// Regression for #1500: wide calls must work without inlining. +use std::evm::Evm + +#[inline(never)] +fn weighted16( + _ a00: u256, + _ a01: u256, + _ a02: u256, + _ a03: u256, + _ a04: u256, + _ a05: u256, + _ a06: u256, + _ a07: u256, + _ a08: u256, + _ a09: u256, + _ a10: u256, + _ a11: u256, + _ a12: u256, + _ a13: u256, + _ a14: u256, + _ a15: u256, +) -> u256 { + a00 * 1 + + a01 * 2 + + a02 * 3 + + a03 * 4 + + a04 * 5 + + a05 * 6 + + a06 * 7 + + a07 * 8 + + a08 * 9 + + a09 * 10 + + a10 * 11 + + a11 * 12 + + a12 * 13 + + a13 * 14 + + a14 * 15 + + a15 * 16 +} + +#[inline(never)] +fn weighted17( + _ a00: u256, + _ a01: u256, + _ a02: u256, + _ a03: u256, + _ a04: u256, + _ a05: u256, + _ a06: u256, + _ a07: u256, + _ a08: u256, + _ a09: u256, + _ a10: u256, + _ a11: u256, + _ a12: u256, + _ a13: u256, + _ a14: u256, + _ a15: u256, + _ a16: u256, +) -> u256 { + a00 * 1 + + a01 * 2 + + a02 * 3 + + a03 * 4 + + a04 * 5 + + a05 * 6 + + a06 * 7 + + a07 * 8 + + a08 * 9 + + a09 * 10 + + a10 * 11 + + a11 * 12 + + a12 * 13 + + a13 * 14 + + a14 * 15 + + a15 * 16 + + a16 * 17 +} + +#[inline(never)] +fn weighted32( + _ a00: u256, + _ a01: u256, + _ a02: u256, + _ a03: u256, + _ a04: u256, + _ a05: u256, + _ a06: u256, + _ a07: u256, + _ a08: u256, + _ a09: u256, + _ a10: u256, + _ a11: u256, + _ a12: u256, + _ a13: u256, + _ a14: u256, + _ a15: u256, + _ a16: u256, + _ a17: u256, + _ a18: u256, + _ a19: u256, + _ a20: u256, + _ a21: u256, + _ a22: u256, + _ a23: u256, + _ a24: u256, + _ a25: u256, + _ a26: u256, + _ a27: u256, + _ a28: u256, + _ a29: u256, + _ a30: u256, + _ a31: u256, +) -> u256 { + a00 * 1 + + a01 * 2 + + a02 * 3 + + a03 * 4 + + a04 * 5 + + a05 * 6 + + a06 * 7 + + a07 * 8 + + a08 * 9 + + a09 * 10 + + a10 * 11 + + a11 * 12 + + a12 * 13 + + a13 * 14 + + a14 * 15 + + a15 * 16 + + a16 * 17 + + a17 * 18 + + a18 * 19 + + a19 * 20 + + a20 * 21 + + a21 * 22 + + a22 * 23 + + a23 * 24 + + a24 * 25 + + a25 * 26 + + a26 * 27 + + a27 * 28 + + a28 * 29 + + a29 * 30 + + a30 * 31 + + a31 * 32 +} + +msg WideMsg { + #[selector = sol( + "sum(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)", + )] + Sum { + a00: u256, + a01: u256, + a02: u256, + a03: u256, + a04: u256, + a05: u256, + a06: u256, + a07: u256, + a08: u256, + a09: u256, + a10: u256, + a11: u256, + a12: u256, + a13: u256, + a14: u256, + a15: u256, + a16: u256, + } -> u256, +} + +pub contract Wide { + init() {} + recv WideMsg { + Sum { + a00, + a01, + a02, + a03, + a04, + a05, + a06, + a07, + a08, + a09, + a10, + a11, + a12, + a13, + a14, + a15, + a16, + } -> u256 { + weighted17( + a00, + a01, + a02, + a03, + a04, + a05, + a06, + a07, + a08, + a09, + a10, + a11, + a12, + a13, + a14, + a15, + a16, + ) + } + } +} + +#[test] +fn wide_recv() uses (evm: mut Evm) { + let addr = evm.create2(value: 0, args: (), salt: 0) + let value: u256 = evm + .call( + addr, + gas: 10000000, + value: 0, + message: WideMsg::Sum { + a00: 1, + a01: 2, + a02: 3, + a03: 4, + a04: 5, + a05: 6, + a06: 7, + a07: 8, + a08: 9, + a09: 10, + a10: 11, + a11: 12, + a12: 13, + a13: 14, + a14: 15, + a15: 16, + a16: 17, + }, + ) + assert!(value == 1785) +} + +#[test] +fn argument_boundaries() { + assert!( + weighted16(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16) == 1496, + ) + assert!( + weighted17(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17) == 1785, + ) + assert!( + weighted32( + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + ) + == 11440, + ) +} + +#[inline(never)] +fn recursive( + _ depth: u256, + _ a00: u256, + _ a01: u256, + _ a02: u256, + _ a03: u256, + _ a04: u256, + _ a05: u256, + _ a06: u256, + _ a07: u256, + _ a08: u256, + _ a09: u256, + _ a10: u256, + _ a11: u256, + _ a12: u256, + _ a13: u256, + _ a14: u256, + _ a15: u256, +) -> u256 { + if depth == 0 { + return weighted16( + a00, + a01, + a02, + a03, + a04, + a05, + a06, + a07, + a08, + a09, + a10, + a11, + a12, + a13, + a14, + a15, + ) + } + let inner = recursive( + depth - 1, + a00 + 1, + a01 + 1, + a02 + 1, + a03 + 1, + a04 + 1, + a05 + 1, + a06 + 1, + a07 + 1, + a08 + 1, + a09 + 1, + a10 + 1, + a11 + 1, + a12 + 1, + a13 + 1, + a14 + 1, + a15 + 1, + ) + inner + + weighted16( + a00, + a01, + a02, + a03, + a04, + a05, + a06, + a07, + a08, + a09, + a10, + a11, + a12, + a13, + a14, + a15, + ) +} + +#[test] +fn recursive_calls_preserve_arguments() { + assert!( + recursive(2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16) == 4896, + ) +} + +struct Counter { value: u256 } + +#[inline(never)] +fn with_effect( + _ a00: u256, + _ a01: u256, + _ a02: u256, + _ a03: u256, + _ a04: u256, + _ a05: u256, + _ a06: u256, + _ a07: u256, + _ a08: u256, + _ a09: u256, + _ a10: u256, + _ a11: u256, + _ a12: u256, + _ a13: u256, + _ a14: u256, + _ a15: u256, +) -> (u256, u8) +uses (counter: mut Counter) +{ + counter.value += weighted16( + a00, + a01, + a02, + a03, + a04, + a05, + a06, + a07, + a08, + a09, + a10, + a11, + a12, + a13, + a14, + a15, + ) + (counter.value, 7) +} + +#[test] +fn wide_call_preserves_mutable_effect() { + let mut counter = Counter { value: 10 } + with (Counter = counter) { + let pair = with_effect(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16) + assert!(pair.0 == 1506) + assert!(pair.1 == 7) + let again = with_effect(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16) + assert!(again.0 == 3002) + } +} + +#[inline(never)] +fn wide_result( + _ a00: u256, + _ a01: u256, + _ a02: u256, + _ a03: u256, + _ a04: u256, + _ a05: u256, + _ a06: u256, + _ a07: u256, + _ a08: u256, + _ a09: u256, + _ a10: u256, + _ a11: u256, + _ a12: u256, + _ a13: u256, + _ a14: u256, + _ a15: u256, +) -> [u256; 17] { + [a00, a01, a02, a03, a04, a05, a06, a07, a08, a09, a10, a11, a12, a13, a14, a15, 99] +} + +#[test] +fn wide_result_with_sixteen_arguments() { + let result = wide_result(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16) + let mut i: usize = 0 + while i < 16 { + assert!(result[i] == (i as u256) + 1) + i += 1 + } + assert!(result[16] == 99) +} + +#[inline(never)] +fn mixed( + _ pair: own (u256, u8), + _ signed: i8, + _ flag: bool, + _ a00: u256, + _ a01: u256, + _ a02: u256, + _ a03: u256, + _ a04: u256, + _ a05: u256, + _ a06: u256, + _ a07: u256, + _ a08: u256, + _ a09: u256, + _ a10: u256, + _ a11: u256, + _ a12: u256, + _ a13: u256, +) -> u256 { + assert!(pair.0 == 42) + assert!(pair.1 == 255) + assert!(signed == -7) + assert!(flag) + weighted16( + a00, + a01, + a02, + a03, + a04, + a05, + a06, + a07, + a08, + a09, + a10, + a11, + a12, + a13, + pair.0, + pair.1 as u256, + ) +} + +#[test] +fn packed_arguments_keep_their_types() { + assert!( + mixed( + (42, 255), + -7, + true, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + ) == 5725, + ) +} + +#[inline(never)] +fn terminal( + _ a00: u256, + _ a01: u256, + _ a02: u256, + _ a03: u256, + _ a04: u256, + _ a05: u256, + _ a06: u256, + _ a07: u256, + _ a08: u256, + _ a09: u256, + _ a10: u256, + _ a11: u256, + _ a12: u256, + _ a13: u256, + _ a14: u256, + _ a15: u256, + _ a16: u256, +) -> ! { + let value = weighted17( + a00, + a01, + a02, + a03, + a04, + a05, + a06, + a07, + a08, + a09, + a10, + a11, + a12, + a13, + a14, + a15, + a16, + ) + assert!(value == 1785) + let _ = 1 / (value - 1785) + core::panic() +} + +#[test(should_revert, panic = 0x12)] +fn terminal_calls_pack_arguments() { + terminal(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17) +} diff --git a/newsfragments/1500.bugfix.md b/newsfragments/1500.bugfix.md new file mode 100644 index 0000000000..09330799a5 --- /dev/null +++ b/newsfragments/1500.bugfix.md @@ -0,0 +1 @@ +Support functions and recv arms with more than 16 arguments in the Sonatina backend.