From 8cfa7c1ce7ec5341d54f38c80c9b6ef5b433183c Mon Sep 17 00:00:00 2001 From: Grant Wuerker Date: Tue, 7 Jul 2026 11:04:20 -0600 Subject: [PATCH 1/8] stdlib: add storage map entry pointers --- .../fe_test/storage_map_struct_values.fe | 109 ++++++++++++++++++ .../ty_check/storage_map_ptr_is_read_only.fe | 15 +++ .../storage_map_ptr_is_read_only.snap | 11 ++ .../ty_check/storage_map_ptr_requires_mut.fe | 15 +++ .../storage_map_ptr_requires_mut.snap | 13 +++ ingots/std/src/evm.fe | 2 +- ingots/std/src/evm/storage_map.fe | 63 +++++++++- 7 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 crates/fe/tests/fixtures/fe_test/storage_map_struct_values.fe create mode 100644 crates/uitest/fixtures/ty_check/storage_map_ptr_is_read_only.fe create mode 100644 crates/uitest/fixtures/ty_check/storage_map_ptr_is_read_only.snap create mode 100644 crates/uitest/fixtures/ty_check/storage_map_ptr_requires_mut.fe create mode 100644 crates/uitest/fixtures/ty_check/storage_map_ptr_requires_mut.snap diff --git a/crates/fe/tests/fixtures/fe_test/storage_map_struct_values.fe b/crates/fe/tests/fixtures/fe_test/storage_map_struct_values.fe new file mode 100644 index 0000000000..dc4165d0d3 --- /dev/null +++ b/crates/fe/tests/fixtures/fe_test/storage_map_struct_values.fe @@ -0,0 +1,109 @@ +msg MapStructMsg { + #[selector = 1] + PutPosition { + account: u256, + market: u256, + owner: u256, + collateral: u256, + debt: u256, + nonce: u256, + } -> u256, + + #[selector = 2] + ReadPosition { account: u256, market: u256 } -> u256, +} + +struct Position { + owner: u256, + collateral: u256, + debt: u256, + nonce: u256, +} + +impl Copy for Position {} + +fn write_position(value: own Position) uses (position: mut Position) { + position = value +} + +fn read_position() -> Position uses (position: Position) { + position +} + +struct Store { + positions: StorageMap<(u256, u256), Position>, +} + +fn pack_position(position: Position) -> u256 { + position.owner * 1000000000 + position.collateral * 1000000 + position.debt * 1000 + position.nonce +} + +pub contract C { + mut store: Store + + init() uses (mut store) {} + + recv MapStructMsg { + PutPosition { account, market, owner, collateral, debt, nonce } -> u256 uses (mut store) { + let position = Position { + owner: owner, + collateral: collateral, + debt: debt, + nonce: nonce, + } + let loaded = with (mut store.positions) { + let position_ptr = store.positions.mut_ptr(key: (account, market)) + with (position_ptr) { + write_position(value: position) + read_position() + } + } + pack_position(position: loaded) + } + + ReadPosition { account, market } -> u256 uses (store) { + let position_ptr = store.positions.ptr(key: (account, market)) + let loaded = with (position_ptr) { + read_position() + } + pack_position(position: loaded) + } + } +} + +#[test] +fn test_storage_map_struct_values() uses (evm: mut Evm) { + let c = evm.create2(value: 0, args: (), salt: 0) + assert!(c.inner != 0) + + let packed: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: MapStructMsg::PutPosition { + account: 7, + market: 8, + owner: 11, + collateral: 22, + debt: 33, + nonce: 44, + }, + ) + assert!(packed == 11022033044) + + let packed: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: MapStructMsg::ReadPosition { account: 7, market: 8 }, + ) + assert!(packed == 11022033044) + + let empty: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: MapStructMsg::ReadPosition { account: 7, market: 9 }, + ) + assert!(empty == 0) +} diff --git a/crates/uitest/fixtures/ty_check/storage_map_ptr_is_read_only.fe b/crates/uitest/fixtures/ty_check/storage_map_ptr_is_read_only.fe new file mode 100644 index 0000000000..430c056c83 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/storage_map_ptr_is_read_only.fe @@ -0,0 +1,15 @@ +msg M { + #[selector = 0] + Ping -> (), +} + +contract C { + mut balances: StorageMap + + recv M { + Ping -> () uses (balances) { + let ptr = balances.ptr(key: 1) + ptr.write(2) + } + } +} diff --git a/crates/uitest/fixtures/ty_check/storage_map_ptr_is_read_only.snap b/crates/uitest/fixtures/ty_check/storage_map_ptr_is_read_only.snap new file mode 100644 index 0000000000..0cda997e47 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/storage_map_ptr_is_read_only.snap @@ -0,0 +1,11 @@ +--- +source: crates/uitest/tests/ty_check.rs +assertion_line: 27 +expression: diags +input_file: fixtures/ty_check/storage_map_ptr_is_read_only.fe +--- +error[2-0010]: no method named `write` found for struct `StoragePtr` + ┌─ storage_map_ptr_is_read_only.fe:12:17 + │ +12 │ ptr.write(2) + │ ^^^^^ method not found in `StoragePtr` diff --git a/crates/uitest/fixtures/ty_check/storage_map_ptr_requires_mut.fe b/crates/uitest/fixtures/ty_check/storage_map_ptr_requires_mut.fe new file mode 100644 index 0000000000..bdc5b629e9 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/storage_map_ptr_requires_mut.fe @@ -0,0 +1,15 @@ +msg M { + #[selector = 0] + Ping -> (), +} + +contract C { + mut balances: StorageMap + + recv M { + Ping -> () uses (balances) { + let ptr = balances.mut_ptr(key: 1) + ptr.write(2) + } + } +} diff --git a/crates/uitest/fixtures/ty_check/storage_map_ptr_requires_mut.snap b/crates/uitest/fixtures/ty_check/storage_map_ptr_requires_mut.snap new file mode 100644 index 0000000000..73917ffec1 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/storage_map_ptr_requires_mut.snap @@ -0,0 +1,13 @@ +--- +source: crates/uitest/tests/ty_check.rs +assertion_line: 27 +expression: diags +input_file: fixtures/ty_check/storage_map_ptr_requires_mut.fe +--- +error[8-0037]: effect `StorageMap` must be mutable when calling `mut_ptr` + ┌─ storage_map_ptr_requires_mut.fe:11:23 + │ +11 │ let ptr = balances.mut_ptr(key: 1) + │ ^^^^^^^^^^^^^^^^^^^^^^^^ `mut_ptr` requires `mut StorageMap` + │ + = use a mutable binding or pass a mutable reference in the `with` block diff --git a/ingots/std/src/evm.fe b/ingots/std/src/evm.fe index 11d50b8520..88f2ab645a 100644 --- a/ingots/std/src/evm.fe +++ b/ingots/std/src/evm.fe @@ -17,7 +17,7 @@ pub use mutex::{self, *} pub use packed::{self, *} pub use ssz::{self, *} pub use storage_bytes::{self, *} -pub use storage_map::{StorageKey, StorageMap} +pub use storage_map::{StorageKey, StorageMap, StoragePtr} pub use storage_packed_array::{PackedBits, StoragePackedArray, ValidPackedBits} pub use units::{self, *} pub use word::{self, *} diff --git a/ingots/std/src/evm/storage_map.fe b/ingots/std/src/evm/storage_map.fe index ba502a716a..4102c384ff 100644 --- a/ingots/std/src/evm/storage_map.fe +++ b/ingots/std/src/evm/storage_map.fe @@ -2,7 +2,7 @@ use super::effects::{Address, RawStorage} use super::mem use ingot::evm::ops::{keccak256, mstore, sload, sstore} use ingot::evm::word::WordRepr -use core::{EffectRef, EffectRefMut} +use core::{AddressSpace, EffectHandle, EffectRef, StorPtr} /// Keys that can be written into the mapping preimage for keccak hashing. pub trait StorageKey { @@ -131,6 +131,41 @@ pub struct StorageMap { impl Copy for StorageMap {} +/// Read-only pointer to a value stored in a `StorageMap`. +pub struct StoragePtr { + slot: u256, +} + +impl Copy for StoragePtr {} + +impl EffectHandle for StoragePtr { + type Target = T + + const SPACE: AddressSpace = AddressSpace::Storage + + fn from_raw(_ raw: u256) -> Self { + Self { slot: raw } + } + + fn raw(self) -> u256 { + self.slot + } +} + +impl EffectRef for StoragePtr {} + +impl StoragePtr { + /// Reads the pointed-at value. + pub fn read(self) -> T + where T: Copy + { + let ptr = self + with (ptr) { + core::effect_ref::read(ptr) + } + } +} + #[inline(always)] fn storagemap_storage_slot_with_salt(key: K, salt: u256) -> u256 where K: StorageKey @@ -160,7 +195,7 @@ fn storagemap_set_word_with_salt(key: K, salt: u256, word: u256) sstore(slot: storage_slot, value: word) } -impl StorageMap { +impl StorageMap { pub(ingot) fn new_unchecked() -> Self { Self { seal: StorageMapSeal {} } } @@ -171,6 +206,30 @@ impl StorageMap { Self::new_unchecked() } + #[inline(always)] + pub(ingot) fn ptr_unchecked(self, key: K) -> StoragePtr { + StoragePtr::from_raw(storagemap_storage_slot_with_salt(key, salt: SALT)) + } + + #[inline(always)] + pub fn ptr(self, key: K) -> StoragePtr { + self.ptr_unchecked(key) + } + + #[inline(always)] + pub(ingot) fn mut_ptr_unchecked(self, key: K) -> StorPtr { + StorPtr::from_raw(storagemap_storage_slot_with_salt(key, salt: SALT)) + } + + #[inline(always)] + pub fn mut_ptr(self, key: K) -> StorPtr + uses (map: mut StorageMap) + { + self.mut_ptr_unchecked(key) + } +} + +impl StorageMap { #[inline(always)] pub(ingot) fn get_unchecked(self, _ key: K) -> V { V::from_word(storagemap_get_word_with_salt(key, salt: SALT)) From 4843586e87c0279bc977e1cb749e7122c53dc535 Mon Sep 17 00:00:00 2001 From: Grant Wuerker Date: Tue, 7 Jul 2026 14:27:15 -0600 Subject: [PATCH 2/8] codegen: pack storage struct fields --- crates/codegen/src/sonatina/lower_runtime.rs | 490 +++++++++++++++--- .../storage_map_packed_struct_values.fe | 210 ++++++++ crates/mir/src/runtime/ir.rs | 177 +++++++ 3 files changed, 811 insertions(+), 66 deletions(-) create mode 100644 crates/fe/tests/fixtures/fe_test/storage_map_packed_struct_values.fe diff --git a/crates/codegen/src/sonatina/lower_runtime.rs b/crates/codegen/src/sonatina/lower_runtime.rs index 5e0a0ab5e0..bbaa67ccd6 100644 --- a/crates/codegen/src/sonatina/lower_runtime.rs +++ b/crates/codegen/src/sonatina/lower_runtime.rs @@ -614,12 +614,24 @@ enum SlotRoot { Object(ValueId, Type), } +#[derive(Clone, Copy)] +struct PackedLane { + bit_offset: u16, + bit_width: u16, +} + enum PlaceTerminal<'db> { Ptr { addr: ValueId, space: AddressSpaceKind, class: RuntimeClass<'db>, }, + PackedPtr { + addr: ValueId, + space: AddressSpaceKind, + class: RuntimeClass<'db>, + lane: PackedLane, + }, Object { value: ValueId, class: RuntimeClass<'db>, @@ -654,6 +666,12 @@ enum CopySource<'db> { space: AddressSpaceKind, class: RuntimeClass<'db>, }, + PackedPtr { + addr: ValueId, + space: AddressSpaceKind, + class: RuntimeClass<'db>, + lane: PackedLane, + }, } struct FunctionLowerer<'ctx, 'db, 'a> { @@ -2498,6 +2516,231 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { } } + fn uses_storage_layout(space: AddressSpaceKind) -> bool { + matches!( + space, + AddressSpaceKind::Storage | AddressSpaceKind::Transient + ) + } + + fn index_stride_words_for_space( + &self, + class: &RuntimeClass<'db>, + space: AddressSpaceKind, + ) -> Option { + if Self::uses_storage_layout(space) { + class.storage_index_stride_words(self.module.db) + } else { + class.index_stride_words(self.module.db) + } + } + + fn span_words_for_space(&self, class: &RuntimeClass<'db>, space: AddressSpaceKind) -> u64 { + if Self::uses_storage_layout(space) { + class.storage_span_words(self.module.db) + } else { + class.span_words(self.module.db) + } + } + + fn ptr_field_terminal( + &mut self, + addr: ValueId, + space: AddressSpaceKind, + base_class: &RuntimeClass<'db>, + field: FieldIndex, + class: RuntimeClass<'db>, + ) -> Result, LowerError> { + if Self::uses_storage_layout(space) { + let placement = base_class + .storage_field_placement(self.module.db, field) + .ok_or_else(|| { + LowerError::Internal("field projection on non-struct class".to_string()) + })?; + let addr = self.offset_address(addr, placement.word_offset, space)?; + if placement.is_packed() { + if !matches!(class, RuntimeClass::Scalar(_)) { + return Err(LowerError::Internal( + "packed storage field placement for non-scalar class".to_string(), + )); + } + return Ok(PlaceTerminal::PackedPtr { + addr, + space, + class, + lane: PackedLane { + bit_offset: placement.bit_offset, + bit_width: placement.bit_width, + }, + }); + } + return Ok(PlaceTerminal::Ptr { addr, space, class }); + } + + let offset = base_class + .field_offset_words(self.module.db, field) + .ok_or_else(|| { + LowerError::Internal("field projection on non-struct class".to_string()) + })?; + Ok(PlaceTerminal::Ptr { + addr: self.offset_address(addr, offset, space)?, + space, + class, + }) + } + + fn ptr_struct_field( + &mut self, + addr: ValueId, + space: AddressSpaceKind, + data: &mir::StructLayout<'db>, + idx: usize, + field: &RuntimeClass<'db>, + ) -> Result<(ValueId, Option), LowerError> { + if Self::uses_storage_layout(space) { + let placement = data + .storage_field_placement(self.module.db, idx) + .ok_or_else(|| { + LowerError::Internal(format!("missing storage field placement for index {idx}")) + })?; + let addr = self.offset_address(addr, placement.word_offset, space)?; + if placement.is_packed() { + if !matches!(field, RuntimeClass::Scalar(_)) { + return Err(LowerError::Internal( + "packed storage field placement for non-scalar class".to_string(), + )); + } + return Ok(( + addr, + Some(PackedLane { + bit_offset: placement.bit_offset, + bit_width: placement.bit_width, + }), + )); + } + return Ok((addr, None)); + } + + Ok(( + self.offset_address(addr, data.field_offset_words(self.module.db, idx), space)?, + None, + )) + } + + fn copy_source_for_ptr_struct_field( + &mut self, + addr: ValueId, + space: AddressSpaceKind, + data: &mir::StructLayout<'db>, + idx: usize, + field: &RuntimeClass<'db>, + ) -> Result, LowerError> { + let (addr, lane) = self.ptr_struct_field(addr, space, data, idx, field)?; + Ok(match lane { + Some(lane) => CopySource::PackedPtr { + addr, + space, + class: field.clone(), + lane, + }, + None => CopySource::Ptr { + addr, + space, + class: field.clone(), + }, + }) + } + + fn load_packed_lane( + &mut self, + addr: ValueId, + space: AddressSpaceKind, + scalar: &ScalarClass<'db>, + lane: PackedLane, + ) -> Result { + let word = self.load_word(addr, space)?; + let shifted = self.shr_word_by_bits(word, lane.bit_offset); + let value = self.mask_word_bits(shifted, lane.bit_width)?; + self.cast_scalar(value, scalar_ty(scalar)) + } + + fn store_packed_lane( + &mut self, + addr: ValueId, + space: AddressSpaceKind, + scalar: &ScalarClass<'db>, + lane: PackedLane, + src: ValueId, + ) -> Result<(), LowerError> { + let value = self.cast_scalar_with_signedness(src, Type::I256, scalar.is_signed_int())?; + let mask = self.word_mask(lane.bit_width)?; + let value = self + .fb + .insert_inst(And::new(self.module.inst_set(), value, mask), Type::I256); + let inserted = self.shl_word_by_bits(value, lane.bit_offset); + let lane_mask = self.shl_word_by_bits(mask, lane.bit_offset); + let word = self.load_word(addr, space)?; + let clear_mask = self + .fb + .insert_inst(Not::new(self.module.inst_set(), lane_mask), Type::I256); + let cleared = self.fb.insert_inst( + And::new(self.module.inst_set(), word, clear_mask), + Type::I256, + ); + let next = self.fb.insert_inst( + Or::new(self.module.inst_set(), cleared, inserted), + Type::I256, + ); + self.store_word(addr, space, next) + } + + fn mask_word_bits(&mut self, value: ValueId, bit_width: u16) -> Result { + if bit_width >= 256 { + return Ok(value); + } + let mask = self.word_mask(bit_width)?; + Ok(self + .fb + .insert_inst(And::new(self.module.inst_set(), value, mask), Type::I256)) + } + + fn word_mask(&mut self, bit_width: u16) -> Result { + if bit_width == 0 || bit_width > 256 { + return Err(LowerError::Internal(format!( + "invalid packed storage bit width {bit_width}" + ))); + } + if bit_width == 256 { + return Ok(self.fb.make_imm_value(I256::all_one())); + } + let one = self.index_value(1); + let width = self.index_value(bit_width.into()); + let shifted = self + .fb + .insert_inst(Shl::new(self.module.inst_set(), width, one), Type::I256); + Ok(self + .fb + .insert_inst(Sub::new(self.module.inst_set(), shifted, one), Type::I256)) + } + + fn shl_word_by_bits(&mut self, value: ValueId, bits: u16) -> ValueId { + if bits == 0 { + return value; + } + let shift = self.index_value(bits.into()); + self.fb + .insert_inst(Shl::new(self.module.inst_set(), shift, value), Type::I256) + } + + fn shr_word_by_bits(&mut self, value: ValueId, bits: u16) -> ValueId { + if bits == 0 { + return value; + } + let shift = self.index_value(bits.into()); + self.fb + .insert_inst(Shr::new(self.module.inst_set(), shift, value), Type::I256) + } + fn load_terminal_value( &mut self, terminal: &PlaceTerminal<'db>, @@ -2513,6 +2756,16 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { self.module.ty_for_class(class)?, )), PlaceTerminal::Ptr { addr, space, .. } => self.load_from_ptr(*addr, *space, class), + PlaceTerminal::PackedPtr { + addr, space, lane, .. + } => { + let RuntimeClass::Scalar(scalar) = class else { + return Err(LowerError::Internal( + "packed ptr terminal must load a scalar class".to_string(), + )); + }; + self.load_packed_lane(*addr, *space, scalar, *lane) + } } } @@ -2667,18 +2920,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { class: base_class, }, ResolvedPlaceElem::Field { field, class }, - ) => { - let offset = base_class - .field_offset_words(self.module.db, *field) - .ok_or_else(|| { - LowerError::Internal("field projection on non-struct class".to_string()) - })?; - PlaceTerminal::Ptr { - addr: self.offset_address(addr, offset, space)?, - space, - class: class.clone(), - } - } + ) => self.ptr_field_terminal(addr, space, &base_class, *field, class.clone())?, ( PlaceTerminal::Ptr { addr, @@ -2687,8 +2929,8 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { }, ResolvedPlaceElem::Index { index, class }, ) => { - let span = base_class - .index_stride_words(self.module.db) + let span = self + .index_stride_words_for_space(&base_class, space) .ok_or_else(|| { LowerError::Internal("index projection on non-array class".to_string()) })?; @@ -2741,6 +2983,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { "unsupported place projection terminal `{terminal_kind}` with `{elem:?}`", terminal_kind = match terminal { PlaceTerminal::Ptr { .. } => "ptr", + PlaceTerminal::PackedPtr { .. } => "packed ptr", PlaceTerminal::Object { .. } => "object", PlaceTerminal::Const { .. } => "const", } @@ -2788,6 +3031,19 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { self.module.ty_for_class(&class)?, ), PlaceTerminal::Ptr { addr, space, class } => self.load_from_ptr(addr, space, &class)?, + PlaceTerminal::PackedPtr { + addr, + space, + class, + lane, + } => { + let RuntimeClass::Scalar(scalar) = &class else { + return Err(LowerError::Internal( + "packed ptr terminal must load a scalar class".to_string(), + )); + }; + self.load_packed_lane(addr, space, scalar, lane)? + } })) } @@ -2871,6 +3127,17 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { PlaceTerminal::Object { value, class } => CopySource::Object { value, class }, PlaceTerminal::Const { value, class } => CopySource::Const { value, class }, PlaceTerminal::Ptr { addr, space, class } => CopySource::Ptr { addr, space, class }, + PlaceTerminal::PackedPtr { + addr, + space, + class, + lane, + } => CopySource::PackedPtr { + addr, + space, + class, + lane, + }, } } @@ -2963,6 +3230,24 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { } self.load_from_ptr(*addr, *space, class)? } + CopySource::PackedPtr { + addr, + space, + class: source_class, + lane, + } => { + if matches!(source_class, RuntimeClass::AggregateValue { .. }) { + return Err(LowerError::Internal(format!( + "leaf packed ptr copy source must not stay aggregate-valued: source={source_class:?} target={class:?}", + ))); + } + let RuntimeClass::Scalar(scalar) = class else { + return Err(LowerError::Internal( + "packed ptr copy source must target a scalar class".to_string(), + )); + }; + self.load_packed_lane(*addr, *space, scalar, *lane)? + } }) } @@ -2971,7 +3256,8 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { CopySource::Value { class, .. } | CopySource::Object { class, .. } | CopySource::Const { class, .. } - | CopySource::Ptr { class, .. } => class, + | CopySource::Ptr { class, .. } + | CopySource::PackedPtr { class, .. } => class, } } @@ -3036,15 +3322,16 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { ), class: src_field.clone(), }, - CopySource::Ptr { addr, space, .. } => CopySource::Ptr { - addr: self.offset_address( - *addr, - src.field_offset_words(self.module.db, idx), - *space, - )?, - space: *space, - class: src_field.clone(), - }, + CopySource::Ptr { addr, space, .. } => { + self.copy_source_for_ptr_struct_field( + *addr, *space, &src, idx, src_field, + )? + } + CopySource::PackedPtr { .. } => { + return Err(LowerError::Internal( + "packed ptr source cannot carry an aggregate struct".to_string(), + )); + } }; self.copy_source_into_object(field_source, dst_field, field_object)?; } @@ -3085,12 +3372,17 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { CopySource::Ptr { addr, space, .. } => CopySource::Ptr { addr: self.offset_address( *addr, - idx as u64 * src.elem.span_words(self.module.db), + idx as u64 * self.span_words_for_space(&src.elem, *space), *space, )?, space: *space, class: src.elem.clone(), }, + CopySource::PackedPtr { .. } => { + return Err(LowerError::Internal( + "packed ptr source cannot carry an aggregate array".to_string(), + )); + } }; self.copy_source_into_object(elem_source, &dst.elem, elem_object)?; } @@ -3148,6 +3440,11 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { value: self.load_aggregate_from_ptr(addr, space, src_layout)?, class: RuntimeClass::AggregateValue { layout: src_layout }, }, + CopySource::PackedPtr { .. } => { + return Err(LowerError::Internal( + "packed ptr source cannot carry an enum aggregate".to_string(), + )); + } source => source, }; @@ -3162,6 +3459,9 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { ), CopySource::Const { .. } => unreachable!("const enum sources are normalized to values"), CopySource::Ptr { .. } => unreachable!("ptr enum sources are normalized to values"), + CopySource::PackedPtr { .. } => { + unreachable!("packed ptr enum sources are rejected before normalization") + } }; let entry = self @@ -3269,6 +3569,9 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { } CopySource::Const { .. } => unreachable!("const enum sources are normalized to values"), CopySource::Ptr { .. } => unreachable!("ptr enum sources are normalized to values"), + CopySource::PackedPtr { .. } => { + unreachable!("packed ptr enum sources are rejected before variant copy") + } }; for (idx, (src_field, dst_field)) in src_fields.iter().zip(dst_fields.iter()).enumerate() { @@ -3316,6 +3619,11 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { .to_string(), )); } + CopySource::PackedPtr { .. } => { + return Err(LowerError::Internal( + "packed ptr source cannot carry enum variant payloads".to_string(), + )); + } }; self.copy_source_into_object(field_source, dst_field, field_object)?; } @@ -3371,6 +3679,9 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { } Ok(Lowered::Value(addr)) } + PlaceTerminal::PackedPtr { .. } => Err(LowerError::Unsupported( + "cannot borrow packed storage fields as raw pointers".to_string(), + )), } } @@ -3387,6 +3698,20 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { self.store_to_ptr(addr, space, &class, src)?; Ok(Lowered::Value(())) } + PlaceTerminal::PackedPtr { + addr, + space, + class, + lane, + } => { + let RuntimeClass::Scalar(scalar) = &class else { + return Err(LowerError::Internal( + "packed ptr terminal must store a scalar class".to_string(), + )); + }; + self.store_packed_lane(addr, space, scalar, lane, src)?; + Ok(Lowered::Value(())) + } PlaceTerminal::Object { value, class } => { if !matches!( class, @@ -3430,6 +3755,17 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { self.copy_to_ptr(addr, space, &dst_class, src_value)?; Ok(Lowered::Value(())) } + PlaceTerminal::PackedPtr { + addr, space, lane, .. + } => { + let RuntimeClass::Scalar(scalar) = &dst_class else { + return Err(LowerError::Internal( + "packed ptr terminal must copy into a scalar class".to_string(), + )); + }; + self.store_packed_lane(addr, space, scalar, lane, src_value)?; + Ok(Lowered::Value(())) + } } } @@ -3456,12 +3792,19 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { self.fb.type_of(src) ))); } - let field_addr = self.offset_address( - addr, - data.field_offset_words(self.module.db, idx), - space, - )?; - self.copy_to_ptr(field_addr, space, field, field_value)?; + let (field_addr, lane) = + self.ptr_struct_field(addr, space, &data, idx, field)?; + if let Some(lane) = lane { + let RuntimeClass::Scalar(scalar) = field else { + return Err(LowerError::Internal( + "packed storage field placement for non-scalar class" + .to_string(), + )); + }; + self.store_packed_lane(field_addr, space, scalar, lane, field_value)?; + } else { + self.copy_to_ptr(field_addr, space, field, field_value)?; + } } Ok(()) } @@ -3479,7 +3822,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { } let elem_addr = self.offset_address( addr, - idx as u64 * data.elem.span_words(self.module.db), + idx as u64 * self.span_words_for_space(&data.elem, space), space, )?; self.copy_to_ptr(elem_addr, space, &data.elem, field_value)?; @@ -3622,12 +3965,18 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { let ty = self.module.ty_for_layout(layout)?; let mut value = self.fb.make_undef_value(ty); for (idx, field) in data.fields.iter().enumerate() { - let field_addr = self.offset_address( - addr, - data.field_offset_words(self.module.db, idx), - space, - )?; - let field_value = self.load_from_ptr(field_addr, space, field)?; + let (field_addr, lane) = + self.ptr_struct_field(addr, space, &data, idx, field)?; + let field_value = if let Some(lane) = lane { + let RuntimeClass::Scalar(scalar) = field else { + return Err(LowerError::Internal( + "packed storage field placement for non-scalar class".to_string(), + )); + }; + self.load_packed_lane(field_addr, space, scalar, lane)? + } else { + self.load_from_ptr(field_addr, space, field)? + }; let expected_ty = self.module.ty_for_class(field)?; let actual_ty = self.fb.type_of(field_value); if actual_ty != expected_ty { @@ -3654,7 +4003,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { for idx in 0..data.len as usize { let elem_addr = self.offset_address( addr, - idx as u64 * data.elem.span_words(self.module.db), + idx as u64 * self.span_words_for_space(&data.elem, space), space, )?; let elem = self.load_from_ptr(elem_addr, space, &data.elem)?; @@ -3792,6 +4141,41 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { } } + fn store_word( + &mut self, + addr: ValueId, + space: AddressSpaceKind, + value: ValueId, + ) -> Result<(), LowerError> { + match space { + AddressSpaceKind::Memory => self.fb.insert_inst_no_result(Mstore::new( + self.module.inst_set(), + addr, + value, + Type::I256, + )), + AddressSpaceKind::Storage => { + self.fb + .insert_inst_no_result(EvmSstore::new(self.module.inst_set(), addr, value)) + } + AddressSpaceKind::Transient => { + self.fb + .insert_inst_no_result(EvmTstore::new(self.module.inst_set(), addr, value)) + } + AddressSpaceKind::Calldata => { + return Err(LowerError::Unsupported( + "storing into calldata-backed providers is not supported".to_string(), + )); + } + AddressSpaceKind::Code => { + return Err(LowerError::Unsupported( + "storing into code-backed providers is not supported".to_string(), + )); + } + } + Ok(()) + } + fn load_scalar( &mut self, addr: ValueId, @@ -3832,33 +4216,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { LowerError::Unsupported("aggregate/handle ptr stores require CopyInto".to_string()), ), }?; - match space { - AddressSpaceKind::Memory => self.fb.insert_inst_no_result(Mstore::new( - self.module.inst_set(), - addr, - value, - Type::I256, - )), - AddressSpaceKind::Storage => { - self.fb - .insert_inst_no_result(EvmSstore::new(self.module.inst_set(), addr, value)) - } - AddressSpaceKind::Transient => { - self.fb - .insert_inst_no_result(EvmTstore::new(self.module.inst_set(), addr, value)) - } - AddressSpaceKind::Calldata => { - return Err(LowerError::Unsupported( - "storing into calldata-backed providers is not supported".to_string(), - )); - } - AddressSpaceKind::Code => { - return Err(LowerError::Unsupported( - "storing into code-backed providers is not supported".to_string(), - )); - } - } - Ok(()) + self.store_word(addr, space, value) } fn extract_aggregate_field( diff --git a/crates/fe/tests/fixtures/fe_test/storage_map_packed_struct_values.fe b/crates/fe/tests/fixtures/fe_test/storage_map_packed_struct_values.fe new file mode 100644 index 0000000000..e187e536e0 --- /dev/null +++ b/crates/fe/tests/fixtures/fe_test/storage_map_packed_struct_values.fe @@ -0,0 +1,210 @@ +use core::EffectHandle +use std::evm::Evm + +msg PackedStructMapMsg { + #[selector = 1] + Put { + a: u8, + b: u16, + c: bool, + d: u256, + e: u8, + } -> u256, + + #[selector = 2] + Read -> u256, + + #[selector = 3] + RawSlot { offset: u256 } -> u256, + + #[selector = 4] + SetA { a: u8 } -> u256, + + #[selector = 5] + SetB { b: u16 } -> u256, + + #[selector = 6] + SetC { c: bool } -> u256, +} + +struct PackedPosition { + a: u8, + b: u16, + c: bool, + d: u256, + e: u8, +} + +impl Copy for PackedPosition {} + +fn write_packed(value: own PackedPosition) uses (position: mut PackedPosition) { + position = value +} + +fn read_packed() -> PackedPosition uses (position: PackedPosition) { + position +} + +fn set_a(value: u8) uses (position: mut PackedPosition) { + position.a = value +} + +fn set_b(value: u16) uses (position: mut PackedPosition) { + position.b = value +} + +fn set_c(value: bool) uses (position: mut PackedPosition) { + position.c = value +} + +fn pack_position(position: PackedPosition) -> u256 { + let mut c: u256 = 0 + if position.c { + c = 1 + } + ((position.a as u256) * 1000000000000) + + ((position.b as u256) * 100000000) + + (c * 10000000) + + (position.d * 1000) + + (position.e as u256) +} + +struct Store { + positions: StorageMap, +} + +pub contract C { + mut store: Store + + init() uses (mut store) {} + + recv PackedStructMapMsg { + Put { a, b, c, d, e } -> u256 uses (mut store) { + let position = PackedPosition { a: a, b: b, c: c, d: d, e: e } + let position_ptr = with (mut store.positions) { + store.positions.mut_ptr(key: 1) + } + with (position_ptr) { + write_packed(value: position) + pack_position(position: read_packed()) + } + } + + Read -> u256 uses (store) { + let position_ptr = store.positions.ptr(key: 1) + with (position_ptr) { + pack_position(position: read_packed()) + } + } + + RawSlot { offset } -> u256 uses (store, evm: Evm) { + let position_ptr = store.positions.ptr(key: 1) + evm.sload(position_ptr.raw() + offset) + } + + SetA { a } -> u256 uses (mut store, evm: Evm) { + let position_ptr = with (mut store.positions) { + store.positions.mut_ptr(key: 1) + } + with (position_ptr) { + set_a(value: a) + } + evm.sload(position_ptr.raw()) + } + + SetB { b } -> u256 uses (mut store, evm: Evm) { + let position_ptr = with (mut store.positions) { + store.positions.mut_ptr(key: 1) + } + with (position_ptr) { + set_b(value: b) + } + evm.sload(position_ptr.raw()) + } + + SetC { c } -> u256 uses (mut store, evm: Evm) { + let position_ptr = with (mut store.positions) { + store.positions.mut_ptr(key: 1) + } + with (position_ptr) { + set_c(value: c) + } + evm.sload(position_ptr.raw()) + } + } +} + +#[test] +fn test_storage_map_packed_struct_values() uses (evm: mut Evm) { + let c = evm.create2(value: 0, args: (), salt: 0) + + let packed: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: PackedStructMapMsg::Put { + a: 0xaa, + b: 0xbeef, + c: true, + d: 7, + e: 0x44, + }, + ) + assert!(packed == 174887910007068) + + let raw0: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: PackedStructMapMsg::RawSlot { offset: 0 }, + ) + assert!(raw0 == 0x1beefaa) + + let raw1: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: PackedStructMapMsg::RawSlot { offset: 1 }, + ) + assert!(raw1 == 7) + + let raw2: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: PackedStructMapMsg::RawSlot { offset: 2 }, + ) + assert!(raw2 == 0x44) + + let raw0: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: PackedStructMapMsg::SetB { b: 0x1234 }, + ) + assert!(raw0 == 0x11234aa) + + let raw0: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: PackedStructMapMsg::SetA { a: 0x55 }, + ) + assert!(raw0 == 0x1123455) + + let raw0: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: PackedStructMapMsg::SetC { c: false }, + ) + assert!(raw0 == 0x123455) + + let packed: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: PackedStructMapMsg::Read {}, + ) + assert!(packed == 85466000007068) +} diff --git a/crates/mir/src/runtime/ir.rs b/crates/mir/src/runtime/ir.rs index 4953d387a9..970ad6cf9e 100644 --- a/crates/mir/src/runtime/ir.rs +++ b/crates/mir/src/runtime/ir.rs @@ -179,6 +179,17 @@ impl<'db> RuntimeClass<'db> { } } + pub fn storage_index_stride_words(&self, db: &'db dyn MirDb) -> Option { + match self { + RuntimeClass::AggregateValue { layout } => match layout.data(db) { + Layout::Array(data) => Some(data.elem.storage_span_words(db)), + Layout::Struct(_) | Layout::Enum(_) => None, + }, + RuntimeClass::Ref { pointee, .. } => pointee.storage_index_stride_words(db), + RuntimeClass::Scalar(_) | RuntimeClass::RawAddr { .. } => None, + } + } + pub fn field_offset_words(&self, db: &'db dyn MirDb, field: FieldIndex) -> Option { if matches!(self, RuntimeClass::Scalar(_) | RuntimeClass::RawAddr { .. }) { return None; @@ -190,6 +201,21 @@ impl<'db> RuntimeClass<'db> { Some(data.field_offset_words(db, field.0 as usize)) } + pub fn storage_field_placement( + &self, + db: &'db dyn MirDb, + field: FieldIndex, + ) -> Option { + if matches!(self, RuntimeClass::Scalar(_) | RuntimeClass::RawAddr { .. }) { + return None; + } + let layout = self.aggregate_layout()?; + let Layout::Struct(data) = layout.data(db) else { + return None; + }; + data.storage_field_placement(db, field.0 as usize) + } + pub fn span_words(&self, db: &'db dyn MirDb) -> u64 { match self { RuntimeClass::Scalar(_) | RuntimeClass::Ref { .. } | RuntimeClass::RawAddr { .. } => 1, @@ -214,6 +240,30 @@ impl<'db> RuntimeClass<'db> { } } + pub fn storage_span_words(&self, db: &'db dyn MirDb) -> u64 { + match self { + RuntimeClass::Scalar(_) | RuntimeClass::Ref { .. } | RuntimeClass::RawAddr { .. } => 1, + RuntimeClass::AggregateValue { layout } => match layout.data(db) { + Layout::Struct(data) => data.storage_span_words(db), + Layout::Array(data) => data.elem.storage_span_words(db) * data.len, + Layout::Enum(data) => { + 1 + data + .variants + .iter() + .map(|variant| { + variant + .fields + .iter() + .map(|field| field.span_words(db)) + .sum::() + }) + .max() + .unwrap_or(0) + } + }, + } + } + pub fn shares_runtime_rep_with(&self, db: &'db dyn MirDb, desired: &RuntimeClass<'db>) -> bool { match (self, desired) { (RuntimeClass::Scalar(actual), RuntimeClass::Scalar(desired)) => actual == desired, @@ -537,6 +587,20 @@ pub struct StructLayout<'db> { pub fields: Box<[RuntimeClass<'db>]>, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Update)] +pub struct FieldPlacement { + pub word_offset: u64, + pub bit_offset: u16, + pub bit_width: u16, + pub packed: bool, +} + +impl FieldPlacement { + pub fn is_packed(self) -> bool { + self.packed + } +} + impl<'db> StructLayout<'db> { pub fn field_offset_words(&self, db: &'db dyn MirDb, idx: usize) -> u64 { self.fields @@ -545,6 +609,119 @@ impl<'db> StructLayout<'db> { .map(|field| field.span_words(db)) .sum() } + + pub fn storage_field_placement( + &self, + db: &'db dyn MirDb, + idx: usize, + ) -> Option { + self.storage_field_placements(db).get(idx).copied() + } + + fn storage_field_placements(&self, db: &'db dyn MirDb) -> Vec { + let mut placements: Vec = Vec::with_capacity(self.fields.len()); + let mut word_offset = 0; + let mut used_bits = 0; + let mut current_word_start = None; + + for field in self.fields.iter() { + if let Some(bit_width) = storage_scalar_bit_width(field) + && bit_width < 256 + { + if used_bits + bit_width > 256 { + word_offset += 1; + used_bits = 0; + current_word_start = None; + } + + let placement_idx = placements.len(); + if used_bits == 0 { + current_word_start = Some(placement_idx); + } else if let Some(start) = current_word_start { + for placement in &mut placements[start..] { + placement.packed = true; + } + } + + placements.push(FieldPlacement { + word_offset, + bit_offset: used_bits, + bit_width, + packed: used_bits > 0, + }); + + used_bits += bit_width; + if used_bits == 256 { + word_offset += 1; + used_bits = 0; + current_word_start = None; + } + continue; + } + + if used_bits > 0 { + word_offset += 1; + used_bits = 0; + current_word_start = None; + } + + placements.push(FieldPlacement { + word_offset, + bit_offset: 0, + bit_width: 256, + packed: false, + }); + word_offset += field.storage_span_words(db); + } + + placements + } + + pub fn storage_span_words(&self, db: &'db dyn MirDb) -> u64 { + let mut word_offset = 0; + let mut used_bits = 0; + + for field in self.fields.iter() { + if let Some(bit_width) = storage_scalar_bit_width(field) + && bit_width < 256 + { + if used_bits + bit_width > 256 { + word_offset += 1; + used_bits = 0; + } + used_bits += bit_width; + if used_bits == 256 { + word_offset += 1; + used_bits = 0; + } + continue; + } + + if used_bits > 0 { + word_offset += 1; + used_bits = 0; + } + word_offset += field.storage_span_words(db); + } + + if used_bits > 0 { + word_offset + 1 + } else { + word_offset + } + } +} + +fn storage_scalar_bit_width(class: &RuntimeClass<'_>) -> Option { + let RuntimeClass::Scalar(scalar) = class else { + return None; + }; + Some(match scalar.repr { + ScalarRepr::Bool => 1, + ScalarRepr::Int { bits, .. } => bits, + ScalarRepr::FixedBytes { len } => len.saturating_mul(8), + ScalarRepr::Address { bits } => bits, + }) } #[derive(Clone, Debug, PartialEq, Eq, Hash, Update)] From 55e959c92679329ea808fa7b105bc0c721237b25 Mon Sep 17 00:00:00 2001 From: Grant Wuerker Date: Wed, 15 Jul 2026 18:42:57 -0600 Subject: [PATCH 3/8] Refine storage map struct value layout --- crates/codegen/src/sonatina/lower_runtime.rs | 226 ++++++++++++++---- .../fixtures/sonatina_ir/erc20_low_level.snap | 101 ++++---- ...contract_storage_struct_fields_unpacked.fe | 90 +++++++ crates/mir/src/runtime/lower/type_info.rs | 18 +- ingots/std/src/evm.fe | 2 +- ingots/std/src/evm/storage_map.fe | 52 +++- 6 files changed, 378 insertions(+), 111 deletions(-) create mode 100644 crates/fe/tests/fixtures/fe_test/contract_storage_struct_fields_unpacked.fe diff --git a/crates/codegen/src/sonatina/lower_runtime.rs b/crates/codegen/src/sonatina/lower_runtime.rs index bbaa67ccd6..5e3ffcc566 100644 --- a/crates/codegen/src/sonatina/lower_runtime.rs +++ b/crates/codegen/src/sonatina/lower_runtime.rs @@ -3,7 +3,10 @@ use std::hash::{Hash, Hasher}; use driver::DriverDataBase; use hir::{ - analysis::{semantic::FieldIndex, ty::ty_check::BodyOwner}, + analysis::{ + semantic::FieldIndex, + ty::{corelib::resolve_lib_type_path, ty_check::BodyOwner, ty_def::TyId}, + }, hir_def::{ArithBinOp, BinOp, CompBinOp, LogicalBinOp, UnOp}, projection::IndexSource, }; @@ -620,10 +623,17 @@ struct PackedLane { bit_width: u16, } +#[derive(Clone, Copy, PartialEq, Eq)] +enum PtrLayoutMode { + Word, + StorageMapEntry, +} + enum PlaceTerminal<'db> { Ptr { addr: ValueId, space: AddressSpaceKind, + layout: PtrLayoutMode, class: RuntimeClass<'db>, }, PackedPtr { @@ -664,6 +674,7 @@ enum CopySource<'db> { Ptr { addr: ValueId, space: AddressSpaceKind, + layout: PtrLayoutMode, class: RuntimeClass<'db>, }, PackedPtr { @@ -2424,11 +2435,12 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { class, }), RuntimeClass::Ref { - kind: RefKind::Provider { space, .. }, + kind: RefKind::Provider { provider_ty, space }, .. } => Ok(PlaceTerminal::Ptr { addr: self.local_value(value)?, space, + layout: self.ptr_layout_mode_for_provider(provider_ty, space), class, }), RuntimeClass::AggregateValue { .. } if allow_value_carrier => { @@ -2440,6 +2452,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { RuntimeClass::RawAddr { space, .. } if allow_value_carrier => Ok(PlaceTerminal::Ptr { addr: self.local_value(value)?, space, + layout: PtrLayoutMode::Word, class, }), RuntimeClass::Scalar(_) @@ -2489,12 +2502,13 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { class: (**pointee).clone(), }), RuntimeClass::Ref { - kind: RefKind::Provider { space, .. }, + kind: RefKind::Provider { provider_ty, space }, pointee, .. } => Ok(PlaceTerminal::Ptr { addr: value, space: *space, + layout: self.ptr_layout_mode_for_provider(*provider_ty, *space), class: (**pointee).clone(), }), RuntimeClass::RawAddr { @@ -2503,6 +2517,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { } => Ok(PlaceTerminal::Ptr { addr: value, space: *space, + layout: PtrLayoutMode::Word, class: RuntimeClass::AggregateValue { layout: *layout }, }), RuntimeClass::RawAddr { target: None, .. } => Err(LowerError::Unsupported( @@ -2516,27 +2531,58 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { } } - fn uses_storage_layout(space: AddressSpaceKind) -> bool { - matches!( - space, - AddressSpaceKind::Storage | AddressSpaceKind::Transient - ) + fn ptr_layout_mode_for_provider( + &self, + provider_ty: TyId<'db>, + space: AddressSpaceKind, + ) -> PtrLayoutMode { + if space == AddressSpaceKind::Storage && self.is_storage_map_entry_provider(provider_ty) { + PtrLayoutMode::StorageMapEntry + } else { + PtrLayoutMode::Word + } } - fn index_stride_words_for_space( + fn is_storage_map_entry_provider(&self, provider_ty: TyId<'db>) -> bool { + let db = self.module.db; + let Some(adt_def) = provider_ty.adt_def(db) else { + return false; + }; + let scope = adt_def.scope(db); + let (provider_base, _) = provider_ty.decompose_ty_app(db); + + [ + "std::evm::storage_map::StoragePtr", + "std::evm::storage_map::StorageMutPtr", + "std::evm::StoragePtr", + "std::evm::StorageMutPtr", + ] + .into_iter() + .filter_map(|path| resolve_lib_type_path(db, scope, path)) + .any(|entry_ptr| { + let (entry_base, _) = entry_ptr.decompose_ty_app(db); + provider_base == entry_base + }) + } + + fn uses_storage_layout(layout: PtrLayoutMode) -> bool { + layout == PtrLayoutMode::StorageMapEntry + } + + fn index_stride_words_for_layout( &self, class: &RuntimeClass<'db>, - space: AddressSpaceKind, + layout: PtrLayoutMode, ) -> Option { - if Self::uses_storage_layout(space) { + if Self::uses_storage_layout(layout) { class.storage_index_stride_words(self.module.db) } else { class.index_stride_words(self.module.db) } } - fn span_words_for_space(&self, class: &RuntimeClass<'db>, space: AddressSpaceKind) -> u64 { - if Self::uses_storage_layout(space) { + fn span_words_for_layout(&self, class: &RuntimeClass<'db>, layout: PtrLayoutMode) -> u64 { + if Self::uses_storage_layout(layout) { class.storage_span_words(self.module.db) } else { class.span_words(self.module.db) @@ -2547,11 +2593,12 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { &mut self, addr: ValueId, space: AddressSpaceKind, + layout: PtrLayoutMode, base_class: &RuntimeClass<'db>, field: FieldIndex, class: RuntimeClass<'db>, ) -> Result, LowerError> { - if Self::uses_storage_layout(space) { + if Self::uses_storage_layout(layout) { let placement = base_class .storage_field_placement(self.module.db, field) .ok_or_else(|| { @@ -2574,7 +2621,12 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { }, }); } - return Ok(PlaceTerminal::Ptr { addr, space, class }); + return Ok(PlaceTerminal::Ptr { + addr, + space, + layout, + class, + }); } let offset = base_class @@ -2585,6 +2637,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { Ok(PlaceTerminal::Ptr { addr: self.offset_address(addr, offset, space)?, space, + layout, class, }) } @@ -2593,11 +2646,12 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { &mut self, addr: ValueId, space: AddressSpaceKind, + layout: PtrLayoutMode, data: &mir::StructLayout<'db>, idx: usize, field: &RuntimeClass<'db>, ) -> Result<(ValueId, Option), LowerError> { - if Self::uses_storage_layout(space) { + if Self::uses_storage_layout(layout) { let placement = data .storage_field_placement(self.module.db, idx) .ok_or_else(|| { @@ -2631,11 +2685,12 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { &mut self, addr: ValueId, space: AddressSpaceKind, + layout: PtrLayoutMode, data: &mir::StructLayout<'db>, idx: usize, field: &RuntimeClass<'db>, ) -> Result, LowerError> { - let (addr, lane) = self.ptr_struct_field(addr, space, data, idx, field)?; + let (addr, lane) = self.ptr_struct_field(addr, space, layout, data, idx, field)?; Ok(match lane { Some(lane) => CopySource::PackedPtr { addr, @@ -2646,6 +2701,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { None => CopySource::Ptr { addr, space, + layout, class: field.clone(), }, }) @@ -2755,7 +2811,12 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { ConstLoad::new(self.module.inst_set(), *value), self.module.ty_for_class(class)?, )), - PlaceTerminal::Ptr { addr, space, .. } => self.load_from_ptr(*addr, *space, class), + PlaceTerminal::Ptr { + addr, + space, + layout, + .. + } => self.load_from_ptr(*addr, *space, *layout, class), PlaceTerminal::PackedPtr { addr, space, lane, .. } => { @@ -2797,6 +2858,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { SlotRoot::Ptr(ptr, _) => PlaceTerminal::Ptr { addr: *ptr, space: AddressSpaceKind::Memory, + layout: PtrLayoutMode::Word, class, }, SlotRoot::Object(value, _) => PlaceTerminal::Object { @@ -2824,6 +2886,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { ResolvedPlaceRootKind::Ptr { addr, space, class } => PlaceTerminal::Ptr { addr: self.local_value(addr)?, space, + layout: PtrLayoutMode::Word, class, }, }; @@ -2917,20 +2980,29 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { PlaceTerminal::Ptr { addr, space, + layout, class: base_class, }, ResolvedPlaceElem::Field { field, class }, - ) => self.ptr_field_terminal(addr, space, &base_class, *field, class.clone())?, + ) => self.ptr_field_terminal( + addr, + space, + layout, + &base_class, + *field, + class.clone(), + )?, ( PlaceTerminal::Ptr { addr, space, + layout, class: base_class, }, ResolvedPlaceElem::Index { index, class }, ) => { let span = self - .index_stride_words_for_space(&base_class, space) + .index_stride_words_for_layout(&base_class, layout) .ok_or_else(|| { LowerError::Internal("index projection on non-array class".to_string()) })?; @@ -2951,11 +3023,17 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { Type::I256, ), space, + layout, class: class.clone(), } } ( - PlaceTerminal::Ptr { addr, space, .. }, + PlaceTerminal::Ptr { + addr, + space, + layout, + .. + }, ResolvedPlaceElem::VariantField { variant, field, @@ -2972,6 +3050,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { space, )?, space, + layout, class: class.clone(), }, (terminal, ResolvedPlaceElem::Deref { carrier_class, .. }) => { @@ -3030,7 +3109,12 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { ConstLoad::new(self.module.inst_set(), value), self.module.ty_for_class(&class)?, ), - PlaceTerminal::Ptr { addr, space, class } => self.load_from_ptr(addr, space, &class)?, + PlaceTerminal::Ptr { + addr, + space, + layout, + class, + } => self.load_from_ptr(addr, space, layout, &class)?, PlaceTerminal::PackedPtr { addr, space, @@ -3116,6 +3200,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { RuntimeClass::RawAddr { space, .. } => CopySource::Ptr { addr: value, space, + layout: PtrLayoutMode::Word, class, }, _ => CopySource::Value { value, class }, @@ -3126,7 +3211,17 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { match terminal { PlaceTerminal::Object { value, class } => CopySource::Object { value, class }, PlaceTerminal::Const { value, class } => CopySource::Const { value, class }, - PlaceTerminal::Ptr { addr, space, class } => CopySource::Ptr { addr, space, class }, + PlaceTerminal::Ptr { + addr, + space, + layout, + class, + } => CopySource::Ptr { + addr, + space, + layout, + class, + }, PlaceTerminal::PackedPtr { addr, space, @@ -3221,6 +3316,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { CopySource::Ptr { addr, space, + layout, class: source_class, } => { if matches!(source_class, RuntimeClass::AggregateValue { .. }) { @@ -3228,7 +3324,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { "leaf ptr copy source must not stay aggregate-valued: source={source_class:?} target={class:?}", ))); } - self.load_from_ptr(*addr, *space, class)? + self.load_from_ptr(*addr, *space, *layout, class)? } CopySource::PackedPtr { addr, @@ -3322,11 +3418,14 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { ), class: src_field.clone(), }, - CopySource::Ptr { addr, space, .. } => { - self.copy_source_for_ptr_struct_field( - *addr, *space, &src, idx, src_field, - )? - } + CopySource::Ptr { + addr, + space, + layout, + .. + } => self.copy_source_for_ptr_struct_field( + *addr, *space, *layout, &src, idx, src_field, + )?, CopySource::PackedPtr { .. } => { return Err(LowerError::Internal( "packed ptr source cannot carry an aggregate struct".to_string(), @@ -3369,13 +3468,19 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { ), class: src.elem.clone(), }, - CopySource::Ptr { addr, space, .. } => CopySource::Ptr { + CopySource::Ptr { + addr, + space, + layout, + .. + } => CopySource::Ptr { addr: self.offset_address( *addr, - idx as u64 * self.span_words_for_space(&src.elem, *space), + idx as u64 * self.span_words_for_layout(&src.elem, *layout), *space, )?, space: *space, + layout: *layout, class: src.elem.clone(), }, CopySource::PackedPtr { .. } => { @@ -3436,9 +3541,14 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { ), class, }, - CopySource::Ptr { addr, space, .. } => CopySource::Value { - value: self.load_aggregate_from_ptr(addr, space, src_layout)?, - class: RuntimeClass::AggregateValue { layout: src_layout }, + CopySource::Ptr { + addr, + space, + layout, + class, + } => CopySource::Value { + value: self.load_aggregate_from_ptr(addr, space, layout, src_layout)?, + class, }, CopySource::PackedPtr { .. } => { return Err(LowerError::Internal( @@ -3694,7 +3804,9 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { return Ok(Lowered::Terminated); }; match terminal { - PlaceTerminal::Ptr { addr, space, class } => { + PlaceTerminal::Ptr { + addr, space, class, .. + } => { self.store_to_ptr(addr, space, &class, src)?; Ok(Lowered::Value(())) } @@ -3751,8 +3863,13 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { PlaceTerminal::Const { .. } => Err(LowerError::Unsupported( "cannot copy into const-backed places".to_string(), )), - PlaceTerminal::Ptr { addr, space, .. } => { - self.copy_to_ptr(addr, space, &dst_class, src_value)?; + PlaceTerminal::Ptr { + addr, + space, + layout, + .. + } => { + self.copy_to_ptr(addr, space, layout, &dst_class, src_value)?; Ok(Lowered::Value(())) } PlaceTerminal::PackedPtr { @@ -3773,6 +3890,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { &mut self, addr: ValueId, space: AddressSpaceKind, + layout_mode: PtrLayoutMode, class: &RuntimeClass<'db>, src: ValueId, ) -> Result<(), LowerError> { @@ -3793,7 +3911,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { ))); } let (field_addr, lane) = - self.ptr_struct_field(addr, space, &data, idx, field)?; + self.ptr_struct_field(addr, space, layout_mode, &data, idx, field)?; if let Some(lane) = lane { let RuntimeClass::Scalar(scalar) = field else { return Err(LowerError::Internal( @@ -3803,7 +3921,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { }; self.store_packed_lane(field_addr, space, scalar, lane, field_value)?; } else { - self.copy_to_ptr(field_addr, space, field, field_value)?; + self.copy_to_ptr(field_addr, space, layout_mode, field, field_value)?; } } Ok(()) @@ -3822,14 +3940,16 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { } let elem_addr = self.offset_address( addr, - idx as u64 * self.span_words_for_space(&data.elem, space), + idx as u64 * self.span_words_for_layout(&data.elem, layout_mode), space, )?; - self.copy_to_ptr(elem_addr, space, &data.elem, field_value)?; + self.copy_to_ptr(elem_addr, space, layout_mode, &data.elem, field_value)?; } Ok(()) } - Layout::Enum(data) => self.copy_enum_to_ptr(addr, space, *layout, &data, src), + Layout::Enum(data) => { + self.copy_enum_to_ptr(addr, space, layout_mode, *layout, &data, src) + } }, } } @@ -3838,6 +3958,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { &mut self, addr: ValueId, space: AddressSpaceKind, + layout_mode: PtrLayoutMode, layout: LayoutId<'db>, data: &mir::runtime::EnumLayout<'db>, src: ValueId, @@ -3904,7 +4025,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { })?, space, )?; - self.copy_to_ptr(field_addr, space, field, field_value)?; + self.copy_to_ptr(field_addr, space, layout_mode, field, field_value)?; } self.fb .insert_inst_no_result(Jump::new(self.module.inst_set(), done)); @@ -3922,6 +4043,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { &mut self, addr: ValueId, space: AddressSpaceKind, + layout_mode: PtrLayoutMode, class: &RuntimeClass<'db>, ) -> Result { match class { @@ -3946,7 +4068,9 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { AddressSpaceKind::Storage | AddressSpaceKind::Transient | AddressSpaceKind::Calldata - | AddressSpaceKind::Code => self.load_aggregate_from_ptr(addr, space, *layout), + | AddressSpaceKind::Code => { + self.load_aggregate_from_ptr(addr, space, layout_mode, *layout) + } }, RuntimeClass::Ref { .. } => Err(LowerError::Unsupported( "loading handle values from raw-address places is not supported".to_string(), @@ -3958,6 +4082,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { &mut self, addr: ValueId, space: AddressSpaceKind, + layout_mode: PtrLayoutMode, layout: LayoutId<'db>, ) -> Result { match layout.data(self.module.db) { @@ -3966,7 +4091,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { let mut value = self.fb.make_undef_value(ty); for (idx, field) in data.fields.iter().enumerate() { let (field_addr, lane) = - self.ptr_struct_field(addr, space, &data, idx, field)?; + self.ptr_struct_field(addr, space, layout_mode, &data, idx, field)?; let field_value = if let Some(lane) = lane { let RuntimeClass::Scalar(scalar) = field else { return Err(LowerError::Internal( @@ -3975,7 +4100,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { }; self.load_packed_lane(field_addr, space, scalar, lane)? } else { - self.load_from_ptr(field_addr, space, field)? + self.load_from_ptr(field_addr, space, layout_mode, field)? }; let expected_ty = self.module.ty_for_class(field)?; let actual_ty = self.fb.type_of(field_value); @@ -4003,10 +4128,10 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { for idx in 0..data.len as usize { let elem_addr = self.offset_address( addr, - idx as u64 * self.span_words_for_space(&data.elem, space), + idx as u64 * self.span_words_for_layout(&data.elem, layout_mode), space, )?; - let elem = self.load_from_ptr(elem_addr, space, &data.elem)?; + let elem = self.load_from_ptr(elem_addr, space, layout_mode, &data.elem)?; let expected_ty = self.module.ty_for_class(&data.elem)?; let actual_ty = self.fb.type_of(elem); if actual_ty != expected_ty { @@ -4028,7 +4153,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { } Ok(value) } - Layout::Enum(data) => self.load_enum_from_ptr(addr, space, layout, &data), + Layout::Enum(data) => self.load_enum_from_ptr(addr, space, layout_mode, layout, &data), } } @@ -4036,6 +4161,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { &mut self, addr: ValueId, space: AddressSpaceKind, + layout_mode: PtrLayoutMode, layout: LayoutId<'db>, data: &mir::runtime::EnumLayout<'db>, ) -> Result { @@ -4081,7 +4207,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { })?, space, )?; - self.load_from_ptr(field_addr, space, field) + self.load_from_ptr(field_addr, space, layout_mode, field) }) .collect::, _>>()?; self.fb.insert_inst_no_result(EnumWriteVariant::new( diff --git a/crates/codegen/tests/fixtures/sonatina_ir/erc20_low_level.snap b/crates/codegen/tests/fixtures/sonatina_ir/erc20_low_level.snap index ffaa6db3eb..0ddc5d46d1 100644 --- a/crates/codegen/tests/fixtures/sonatina_ir/erc20_low_level.snap +++ b/crates/codegen/tests/fixtures/sonatina_ir/erc20_low_level.snap @@ -1,5 +1,6 @@ --- source: crates/codegen/tests/sonatina_ir.rs +assertion_line: 610 expression: output input_file: crates/codegen/tests/fixtures/erc20_low_level.fe --- @@ -33,17 +34,6 @@ func private %abi_encode_u256(v0.i256) { unreachable; } -func private %allowance__g26d5(v0.@layout_0, v1.@layout_0, v2.i256) -> i256 { - block0: - jump block1; - - block1: - v8.@layout_2 = insert_value undef.@layout_2 0.i256 v0; - v9.@layout_2 = insert_value v8 1.i256 v1; - v10.i256 = call %std__lib__evm__storage_map__impl_StorageMap_9f54__get__ga15b_ddb0 v9 1.i256; - return v10; -} - func private %allowance__g35d6(v0.i256, v1.@layout_0, v2.@layout_0, v3.i256, v4.i256) -> i256 { block0: jump block1; @@ -55,14 +45,15 @@ func private %allowance__g35d6(v0.i256, v1.@layout_0, v2.@layout_0, v3.i256, v4. return v13; } -func private %approve__g44bd(v0.@layout_0, v1.i256, v2.i256) -> i256 { +func private %allowance__g26d5(v0.@layout_0, v1.@layout_0, v2.i256) -> i256 { block0: jump block1; block1: - v6.@layout_0 = call %caller; - call %approve__g35d6 v2 v6 v0 v1 0.i256 1.i256; - return 1.i256; + v8.@layout_2 = insert_value undef.@layout_2 0.i256 v0; + v9.@layout_2 = insert_value v8 1.i256 v1; + v10.i256 = call %std__lib__evm__storage_map__impl_StorageMap_9f54__get__ga15b_ddb0 v9 1.i256; + return v10; } func private %approve__g35d6(v0.i256, v1.@layout_0, v2.@layout_0, v3.i256, v4.i256, v5.i256) { @@ -76,7 +67,17 @@ func private %approve__g35d6(v0.i256, v1.@layout_0, v2.@layout_0, v3.i256, v4.i2 return; } -func private %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__balance_of__g35d6_3e9c(v0.i256, v1.@layout_0, v2.i256, v3.i256) -> i256 { +func private %approve__g44bd(v0.@layout_0, v1.i256, v2.i256) -> i256 { + block0: + jump block1; + + block1: + v6.@layout_0 = call %caller; + call %approve__g35d6 v2 v6 v0 v1 0.i256 1.i256; + return 1.i256; +} + +func private %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__balance_of__g35d6_7cc8(v0.i256, v1.@layout_0, v2.i256, v3.i256) -> i256 { block0: jump block1; @@ -85,7 +86,7 @@ func private %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__bala return v6; } -func private %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__balance_of__g35d6_3e9c_0(v0.i256, v1.@layout_0, v2.i256, v3.i256) -> i256 { +func private %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__balance_of__g35d6_7cc8_0(v0.i256, v1.@layout_0, v2.i256, v3.i256) -> i256 { block0: jump block1; @@ -94,7 +95,7 @@ func private %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__bala return v6; } -func private %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__balance_of__g35d6_3e9c_1(v0.i256, v1.@layout_0, v2.i256, v3.i256) -> i256 { +func private %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__balance_of__g35d6_7cc8_1(v0.i256, v1.@layout_0, v2.i256, v3.i256) -> i256 { block0: jump block1; @@ -150,25 +151,25 @@ func private %do_init(v0.i256) { return; } -func private %std__lib__evm__effects__impl_trait_Address_3ffb__eq_519f(v0.@layout_0, v1.i256) -> i1 { +func private %std__lib__evm__effects__impl_trait_Address_3ffb__eq_4d27(v0.i256, v1.@layout_0) -> i1 { block0: jump block1; block1: - v4.i256 = extract_value v0 0.i256; - v6.i256 = evm_sload v1; - v7.i1 = eq v4 v6; + v3.i256 = evm_sload v0; + v6.i256 = extract_value v1 0.i256; + v7.i1 = eq v3 v6; return v7; } -func private %std__lib__evm__effects__impl_trait_Address_3ffb__eq_f155(v0.i256, v1.@layout_0) -> i1 { +func private %std__lib__evm__effects__impl_trait_Address_3ffb__eq_fdc4(v0.@layout_0, v1.i256) -> i1 { block0: jump block1; block1: - v3.i256 = evm_sload v0; - v6.i256 = extract_value v1 0.i256; - v7.i1 = eq v3 v6; + v4.i256 = extract_value v0 0.i256; + v6.i256 = evm_sload v1; + v7.i1 = eq v4 v6; return v7; } @@ -351,7 +352,7 @@ func private %mint__g0502(v0.i256, v1.@layout_0, v2.i256, v3.i256, v4.i256) { block1: v5.@layout_0 = call %caller; v8.i256 = add v0 1.i256; - v9.i1 = call %core__lib__ops__trait_Eq__ne__g7528_2614 v5 v8; + v9.i1 = call %core__lib__ops__trait_Eq__ne__g7528_7974 v5 v8; br v9 block2 block3; block2: @@ -362,7 +363,7 @@ func private %mint__g0502(v0.i256, v1.@layout_0, v2.i256, v3.i256, v4.i256) { jump block4; block4: - v15.i256 = call %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__balance_of__g35d6_3e9c v0 v1 v3 v4; + v15.i256 = call %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__balance_of__g35d6_7cc8 v0 v1 v3 v4; (v17.i256, v18.i1) = uaddo v15 v2; br v18 block5 block6; @@ -400,22 +401,22 @@ func private %mstore(v0.i256, v1.i256) { return; } -func private %core__lib__ops__trait_Eq__ne__g7528_2614(v0.@layout_0, v1.i256) -> i1 { +func private %core__lib__ops__trait_Eq__ne__g7528_7974(v0.@layout_0, v1.i256) -> i1 { block0: jump block1; block1: - v4.i1 = call %std__lib__evm__effects__impl_trait_Address_3ffb__eq_519f v0 v1; + v4.i1 = call %std__lib__evm__effects__impl_trait_Address_3ffb__eq_fdc4 v0 v1; v5.i1 = is_zero v4; return v5; } -func private %core__lib__ops__trait_Eq__ne__g7528_ba99(v0.i256, v1.@layout_0) -> i1 { +func private %core__lib__ops__trait_Eq__ne__g7528_9b98(v0.i256, v1.@layout_0) -> i1 { block0: jump block1; block1: - v4.i1 = call %std__lib__evm__effects__impl_trait_Address_3ffb__eq_f155 v0 v1; + v4.i1 = call %std__lib__evm__effects__impl_trait_Address_3ffb__eq_4d27 v0 v1; v5.i1 = is_zero v4; return v5; } @@ -636,7 +637,7 @@ func private %set_owner_once(v0.i256, v1.@layout_0, v2.i256, v3.i256) { obj.store v13 v14; v15.i256 = add v0 1.i256; v16.@layout_0 = obj.load v4; - v17.i1 = call %core__lib__ops__trait_Eq__ne__g7528_ba99 v15 v16; + v17.i1 = call %core__lib__ops__trait_Eq__ne__g7528_9b98 v15 v16; br v17 block2 block3; block2: @@ -1018,22 +1019,12 @@ func private %to_word(v0.i256) -> i256 { return v0; } -func private %transfer__g2ee9(v0.@layout_0, v1.i256, v2.i256) -> i256 { - block0: - jump block1; - - block1: - v6.@layout_0 = call %caller; - call %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__transfer__g0f37_84af v2 v6 v0 v1 0.i256 1.i256; - return 1.i256; -} - -func private %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__transfer__g0f37_84af(v0.i256, v1.@layout_0, v2.@layout_0, v3.i256, v4.i256, v5.i256) { +func private %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__transfer__g0f37_4ab6(v0.i256, v1.@layout_0, v2.@layout_0, v3.i256, v4.i256, v5.i256) { block0: jump block1; block1: - v10.i256 = call %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__balance_of__g35d6_3e9c_0 v0 v1 v4 v5; + v10.i256 = call %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__balance_of__g35d6_7cc8_0 v0 v1 v4 v5; v12.i1 = lt v10 v3; br v12 block2 block3; @@ -1045,7 +1036,7 @@ func private %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__tran jump block4; block4: - v18.i256 = call %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__balance_of__g35d6_3e9c_0 v0 v2 v4 v5; + v18.i256 = call %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__balance_of__g35d6_7cc8_0 v0 v2 v4 v5; (v22.i256, v23.i1) = usubo v10 v3; br v23 block5 block6; @@ -1064,12 +1055,12 @@ func private %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__tran return; } -func private %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__transfer__g0f37_84af_0(v0.i256, v1.@layout_0, v2.@layout_0, v3.i256, v4.i256, v5.i256) { +func private %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__transfer__g0f37_4ab6_0(v0.i256, v1.@layout_0, v2.@layout_0, v3.i256, v4.i256, v5.i256) { block0: jump block1; block1: - v10.i256 = call %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__balance_of__g35d6_3e9c_1 v0 v1 v4 v5; + v10.i256 = call %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__balance_of__g35d6_7cc8_1 v0 v1 v4 v5; v12.i1 = lt v10 v3; br v12 block2 block3; @@ -1081,7 +1072,7 @@ func private %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__tran jump block4; block4: - v18.i256 = call %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__balance_of__g35d6_3e9c_1 v0 v2 v4 v5; + v18.i256 = call %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__balance_of__g35d6_7cc8_1 v0 v2 v4 v5; (v22.i256, v23.i1) = usubo v10 v3; br v23 block5 block6; @@ -1100,6 +1091,16 @@ func private %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__tran return; } +func private %transfer__g2ee9(v0.@layout_0, v1.i256, v2.i256) -> i256 { + block0: + jump block1; + + block1: + v6.@layout_0 = call %caller; + call %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__transfer__g0f37_4ab6 v2 v6 v0 v1 0.i256 1.i256; + return 1.i256; +} + func private %transfer_from__g2ee9(v0.@layout_0, v1.@layout_0, v2.i256, v3.i256) -> i256 { block0: jump block1; @@ -1127,7 +1128,7 @@ func private %transfer_from__g0502(v0.i256, v1.@layout_0, v2.@layout_0, v3.i256, jump block4; block4: - call %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__transfer__g0f37_84af_0 v0 v1 v2 v3 v4 v5; + call %standalone_erc20_low_level__erc20_low_level__impl_Erc20_ee5d__transfer__g0f37_4ab6_0 v0 v1 v2 v3 v4 v5; v23.@layout_2 = insert_value undef.@layout_2 0.i256 v1; v26.@layout_2 = insert_value v23 1.i256 v6; (v28.i256, v29.i1) = usubo v11 v3; diff --git a/crates/fe/tests/fixtures/fe_test/contract_storage_struct_fields_unpacked.fe b/crates/fe/tests/fixtures/fe_test/contract_storage_struct_fields_unpacked.fe new file mode 100644 index 0000000000..6f1a951d1f --- /dev/null +++ b/crates/fe/tests/fixtures/fe_test/contract_storage_struct_fields_unpacked.fe @@ -0,0 +1,90 @@ +use std::evm::Evm + +msg ContractStructMsg { + #[selector = 1] + Put { a: u8, b: u8, c: u256 } -> u256, + + #[selector = 2] + RawSlot { offset: u256 } -> u256, + + #[selector = 3] + Read -> u256, +} + +struct ContractStruct { + a: u8, + b: u8, + c: u256, +} + +fn pack(value: ContractStruct) -> u256 { + ((value.a as u256) * 1000000) + ((value.b as u256) * 1000) + value.c +} + +pub contract C { + mut value: ContractStruct + + init() uses (mut value) {} + + recv ContractStructMsg { + Put { a, b, c } -> u256 uses (mut value) { + value.a = a + value.b = b + value.c = c + pack(value: value) + } + + RawSlot { offset } -> u256 uses (evm: Evm) { + evm.sload(offset) + } + + Read -> u256 uses (value) { + pack(value: value) + } + } +} + +#[test] +fn test_contract_storage_struct_fields_unpacked() uses (evm: mut Evm) { + let c = evm.create2(value: 0, args: (), salt: 0) + + let packed: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: ContractStructMsg::Put { a: 0xaa, b: 0xbb, c: 0xcc }, + ) + assert!(packed == 170187204) + + let raw0: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: ContractStructMsg::RawSlot { offset: 0 }, + ) + assert!(raw0 == 0xaa) + + let raw1: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: ContractStructMsg::RawSlot { offset: 1 }, + ) + assert!(raw1 == 0xbb) + + let raw2: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: ContractStructMsg::RawSlot { offset: 2 }, + ) + assert!(raw2 == 0xcc) + + let packed: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: ContractStructMsg::Read {}, + ) + assert!(packed == 170187204) +} diff --git a/crates/mir/src/runtime/lower/type_info.rs b/crates/mir/src/runtime/lower/type_info.rs index f9725d53f6..9f67824b31 100644 --- a/crates/mir/src/runtime/lower/type_info.rs +++ b/crates/mir/src/runtime/lower/type_info.rs @@ -50,6 +50,7 @@ impl<'db> RuntimeTypeEnv<'db> { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Update)] pub(crate) struct RuntimeEffectHandleInfo<'db> { + pub(crate) provider_ty: TyId<'db>, pub(crate) target_ty: TyId<'db>, pub(crate) space: AddressSpaceKind, pub(crate) impl_instance: ResolvedImplInstance<'db>, @@ -464,12 +465,16 @@ fn effect_handle_transport_class_for_info<'db>( ), }; } - provider_class_for_target_in_env( - db, - RuntimeTypeEnv::new(Some(effect_scope), assumptions), - Some(info.target_ty), - info.space, - ) + let env = RuntimeTypeEnv::new(Some(effect_scope), assumptions); + let target_ty = runtime_repr_ty_in_env(db, env, info.target_ty); + RuntimeClass::Ref { + pointee: Box::new(stored_class_for_ty_in_env(db, env, target_ty)), + kind: RefKind::Provider { + provider_ty: info.provider_ty, + space: info.space, + }, + view: RefView::Whole, + } } pub(crate) fn effect_handle_transport_class_for_ty_in_env<'db>( @@ -559,6 +564,7 @@ pub(crate) fn runtime_effect_handle_info<'db>( }; let target_ty = semantics.target_ty?; Some(RuntimeEffectHandleInfo { + provider_ty: repr_ty, target_ty, space: provider_address_space_to_runtime(semantics.address_space?), impl_instance, diff --git a/ingots/std/src/evm.fe b/ingots/std/src/evm.fe index 88f2ab645a..8e488e3203 100644 --- a/ingots/std/src/evm.fe +++ b/ingots/std/src/evm.fe @@ -17,7 +17,7 @@ pub use mutex::{self, *} pub use packed::{self, *} pub use ssz::{self, *} pub use storage_bytes::{self, *} -pub use storage_map::{StorageKey, StorageMap, StoragePtr} +pub use storage_map::{StorageKey, StorageMap, StorageMutPtr, StoragePtr} pub use storage_packed_array::{PackedBits, StoragePackedArray, ValidPackedBits} pub use units::{self, *} pub use word::{self, *} diff --git a/ingots/std/src/evm/storage_map.fe b/ingots/std/src/evm/storage_map.fe index 4102c384ff..61890492db 100644 --- a/ingots/std/src/evm/storage_map.fe +++ b/ingots/std/src/evm/storage_map.fe @@ -2,7 +2,7 @@ use super::effects::{Address, RawStorage} use super::mem use ingot::evm::ops::{keccak256, mstore, sload, sstore} use ingot::evm::word::WordRepr -use core::{AddressSpace, EffectHandle, EffectRef, StorPtr} +use core::{AddressSpace, EffectHandle, EffectHandleMut, EffectRef, EffectRefMut} /// Keys that can be written into the mapping preimage for keccak hashing. pub trait StorageKey { @@ -136,7 +136,13 @@ pub struct StoragePtr { slot: u256, } +/// Mutable pointer to a value stored in a `StorageMap`. +pub struct StorageMutPtr { + slot: u256, +} + impl Copy for StoragePtr {} +impl Copy for StorageMutPtr {} impl EffectHandle for StoragePtr { type Target = T @@ -166,6 +172,44 @@ impl StoragePtr { } } +impl EffectHandle for StorageMutPtr { + type Target = T + + const SPACE: AddressSpace = AddressSpace::Storage + + fn from_raw(_ raw: u256) -> Self { + Self { slot: raw } + } + + fn raw(self) -> u256 { + self.slot + } +} + +impl EffectHandleMut for StorageMutPtr {} +impl EffectRef for StorageMutPtr {} +impl EffectRefMut for StorageMutPtr {} + +impl StorageMutPtr { + /// Reads the pointed-at value. + pub fn read(self) -> T + where T: Copy + { + let mut ptr = self + with (ptr) { + core::effect_ref::read(ptr) + } + } + + /// Writes the pointed-at value. + pub fn write(self, _ value: T) { + let mut ptr = self + with (ptr) { + core::effect_ref::write(ptr, value) + } + } +} + #[inline(always)] fn storagemap_storage_slot_with_salt(key: K, salt: u256) -> u256 where K: StorageKey @@ -217,12 +261,12 @@ impl StorageMap { } #[inline(always)] - pub(ingot) fn mut_ptr_unchecked(self, key: K) -> StorPtr { - StorPtr::from_raw(storagemap_storage_slot_with_salt(key, salt: SALT)) + pub(ingot) fn mut_ptr_unchecked(self, key: K) -> StorageMutPtr { + StorageMutPtr::from_raw(storagemap_storage_slot_with_salt(key, salt: SALT)) } #[inline(always)] - pub fn mut_ptr(self, key: K) -> StorPtr + pub fn mut_ptr(self, key: K) -> StorageMutPtr uses (map: mut StorageMap) { self.mut_ptr_unchecked(key) From 4cf2d6964434640b4bdba6cc7528c14433713757 Mon Sep 17 00:00:00 2001 From: Grant Wuerker Date: Fri, 24 Jul 2026 20:25:23 -0600 Subject: [PATCH 4/8] storage: pack struct fields in contract layout --- crates/codegen/src/sonatina/lower_runtime.rs | 37 ++--- ... contract_storage_struct_fields_packed.fe} | 8 +- .../hir/src/core/semantic/storage_layout.rs | 153 +++++++++++++++++- crates/hir/tests/contract_layout_report.rs | 45 ++++++ .../src/functionality/hover.rs | 9 +- crates/mir/src/verify/storage_layout.rs | 9 +- 6 files changed, 226 insertions(+), 35 deletions(-) rename crates/fe/tests/fixtures/fe_test/{contract_storage_struct_fields_unpacked.fe => contract_storage_struct_fields_packed.fe} (92%) diff --git a/crates/codegen/src/sonatina/lower_runtime.rs b/crates/codegen/src/sonatina/lower_runtime.rs index 5e3ffcc566..e375247658 100644 --- a/crates/codegen/src/sonatina/lower_runtime.rs +++ b/crates/codegen/src/sonatina/lower_runtime.rs @@ -5,7 +5,7 @@ use driver::DriverDataBase; use hir::{ analysis::{ semantic::FieldIndex, - ty::{corelib::resolve_lib_type_path, ty_check::BodyOwner, ty_def::TyId}, + ty::{ty_check::BodyOwner, ty_def::TyId}, }, hir_def::{ArithBinOp, BinOp, CompBinOp, LogicalBinOp, UnOp}, projection::IndexSource, @@ -626,7 +626,7 @@ struct PackedLane { #[derive(Clone, Copy, PartialEq, Eq)] enum PtrLayoutMode { Word, - StorageMapEntry, + Storage, } enum PlaceTerminal<'db> { @@ -2533,40 +2533,21 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { fn ptr_layout_mode_for_provider( &self, - provider_ty: TyId<'db>, + _provider_ty: TyId<'db>, space: AddressSpaceKind, ) -> PtrLayoutMode { - if space == AddressSpaceKind::Storage && self.is_storage_map_entry_provider(provider_ty) { - PtrLayoutMode::StorageMapEntry + if matches!( + space, + AddressSpaceKind::Storage | AddressSpaceKind::Transient + ) { + PtrLayoutMode::Storage } else { PtrLayoutMode::Word } } - fn is_storage_map_entry_provider(&self, provider_ty: TyId<'db>) -> bool { - let db = self.module.db; - let Some(adt_def) = provider_ty.adt_def(db) else { - return false; - }; - let scope = adt_def.scope(db); - let (provider_base, _) = provider_ty.decompose_ty_app(db); - - [ - "std::evm::storage_map::StoragePtr", - "std::evm::storage_map::StorageMutPtr", - "std::evm::StoragePtr", - "std::evm::StorageMutPtr", - ] - .into_iter() - .filter_map(|path| resolve_lib_type_path(db, scope, path)) - .any(|entry_ptr| { - let (entry_base, _) = entry_ptr.decompose_ty_app(db); - provider_base == entry_base - }) - } - fn uses_storage_layout(layout: PtrLayoutMode) -> bool { - layout == PtrLayoutMode::StorageMapEntry + layout == PtrLayoutMode::Storage } fn index_stride_words_for_layout( diff --git a/crates/fe/tests/fixtures/fe_test/contract_storage_struct_fields_unpacked.fe b/crates/fe/tests/fixtures/fe_test/contract_storage_struct_fields_packed.fe similarity index 92% rename from crates/fe/tests/fixtures/fe_test/contract_storage_struct_fields_unpacked.fe rename to crates/fe/tests/fixtures/fe_test/contract_storage_struct_fields_packed.fe index 6f1a951d1f..aad5589f66 100644 --- a/crates/fe/tests/fixtures/fe_test/contract_storage_struct_fields_unpacked.fe +++ b/crates/fe/tests/fixtures/fe_test/contract_storage_struct_fields_packed.fe @@ -45,7 +45,7 @@ pub contract C { } #[test] -fn test_contract_storage_struct_fields_unpacked() uses (evm: mut Evm) { +fn test_contract_storage_struct_fields_packed() uses (evm: mut Evm) { let c = evm.create2(value: 0, args: (), salt: 0) let packed: u256 = evm.call( @@ -62,7 +62,7 @@ fn test_contract_storage_struct_fields_unpacked() uses (evm: mut Evm) { value: 0, message: ContractStructMsg::RawSlot { offset: 0 }, ) - assert!(raw0 == 0xaa) + assert!(raw0 == 0xbbaa) let raw1: u256 = evm.call( addr: c, @@ -70,7 +70,7 @@ fn test_contract_storage_struct_fields_unpacked() uses (evm: mut Evm) { value: 0, message: ContractStructMsg::RawSlot { offset: 1 }, ) - assert!(raw1 == 0xbb) + assert!(raw1 == 0xcc) let raw2: u256 = evm.call( addr: c, @@ -78,7 +78,7 @@ fn test_contract_storage_struct_fields_unpacked() uses (evm: mut Evm) { value: 0, message: ContractStructMsg::RawSlot { offset: 2 }, ) - assert!(raw2 == 0xcc) + assert!(raw2 == 0) let packed: u256 = evm.call( addr: c, diff --git a/crates/hir/src/core/semantic/storage_layout.rs b/crates/hir/src/core/semantic/storage_layout.rs index 8e62a46fb0..b57b4a3ebd 100644 --- a/crates/hir/src/core/semantic/storage_layout.rs +++ b/crates/hir/src/core/semantic/storage_layout.rs @@ -178,9 +178,16 @@ pub struct ContractLayoutEntry<'db> { pub ty: TyId<'db>, pub address_space: ProviderAddressSpace, pub value: ContractLayoutValue<'db>, + pub lane: Option, pub kind: ContractLayoutEntryKind, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Update)] +pub struct ContractLayoutLane { + pub bit_offset: u16, + pub bit_width: u16, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Update)] pub enum ContractLayoutEntryKind { InlineField, @@ -1289,6 +1296,37 @@ fn const_ty_to_usize<'db>(db: &'db dyn HirAnalysisDb, ty: TyId<'db>) -> Option bool { + matches!( + space, + ProviderAddressSpace::Storage | ProviderAddressSpace::Transient + ) +} + +fn storage_scalar_bit_width<'db>(db: &'db dyn HirAnalysisDb, ty: TyId<'db>) -> Option { + match ty.base_ty(db).data(db) { + TyData::TyBase(TyBase::Prim(prim)) => match prim { + PrimTy::Bool => Some(1), + PrimTy::U8 | PrimTy::I8 => Some(8), + PrimTy::U16 | PrimTy::I16 => Some(16), + PrimTy::U32 | PrimTy::I32 => Some(32), + PrimTy::U64 | PrimTy::I64 => Some(64), + PrimTy::U128 | PrimTy::I128 => Some(128), + PrimTy::U256 | PrimTy::I256 | PrimTy::Usize | PrimTy::Isize | PrimTy::String => { + Some(256) + } + PrimTy::Array + | PrimTy::Tuple(_) + | PrimTy::Ptr + | PrimTy::View + | PrimTy::BorrowMut + | PrimTy::BorrowRef => None, + }, + TyData::TyBase(TyBase::Contract(_)) => Some(256), + _ => None, + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum WalkMode { Counted, @@ -1340,6 +1378,7 @@ impl<'db> WalkOutput<'db> { place, ty, offset: 0, + lane: None, dimensions: dimensions.to_vec(), strides: vec![0; dimensions.len()], kind: InlineLayoutLeafKind::Field, @@ -1360,6 +1399,7 @@ struct InlineLayoutLeaf<'db> { place: StoragePlace<'db>, ty: TyId<'db>, offset: usize, + lane: Option, dimensions: Vec>, strides: Vec, kind: InlineLayoutLeafKind, @@ -2040,6 +2080,102 @@ impl<'db> FieldCollector<'db> { } } + fn packable_storage_scalar_leaf(&self, output: &WalkOutput<'db>) -> Option { + if output.inline_span != 1 || output.inline_leaves.len() != 1 { + return None; + } + let leaf = &output.inline_leaves[0]; + if leaf.offset != 0 || leaf.lane.is_some() || leaf.kind != InlineLayoutLeafKind::Field { + return None; + } + let bit_width = storage_scalar_bit_width(self.db, leaf.ty)?; + (bit_width < 256).then_some(bit_width) + } + + fn walk_storage_struct_sequence( + &mut self, + items: impl IntoIterator, StoragePlace<'db>)>, + dimensions: &[LayoutIndexDimension<'db>], + mode: WalkMode, + ) -> WalkOutput<'db> { + let mut inline_span = 0usize; + let mut used_bits = 0u16; + let mut inline_leaves = Vec::new(); + let mut events = Vec::new(); + + for (instantiation, place) in items { + let mut output = self.walk_instantiation(&instantiation, place, dimensions, mode); + if let Some(bit_width) = self.packable_storage_scalar_leaf(&output) { + if used_bits + bit_width > 256 { + let Some(next) = inline_span.checked_add(1) else { + self.push_error(ContractLayoutError::LayoutExtentOverflow); + continue; + }; + inline_span = next; + used_bits = 0; + } + + for leaf in &mut output.inline_leaves { + leaf.offset = inline_span; + leaf.lane = Some(ContractLayoutLane { + bit_offset: used_bits, + bit_width, + }); + } + used_bits += bit_width; + if used_bits == 256 { + let Some(next) = inline_span.checked_add(1) else { + self.push_error(ContractLayoutError::LayoutExtentOverflow); + continue; + }; + inline_span = next; + used_bits = 0; + } + inline_leaves.extend(output.inline_leaves); + events.extend(output.events); + continue; + } + + if used_bits > 0 { + let Some(next) = inline_span.checked_add(1) else { + self.push_error(ContractLayoutError::LayoutExtentOverflow); + continue; + }; + inline_span = next; + used_bits = 0; + } + + let Some(next) = inline_span.checked_add(output.inline_span) else { + self.push_error(ContractLayoutError::LayoutExtentOverflow); + continue; + }; + for leaf in &mut output.inline_leaves { + let Some(offset) = leaf.offset.checked_add(inline_span) else { + self.push_error(ContractLayoutError::LayoutExtentOverflow); + continue; + }; + leaf.offset = offset; + } + inline_span = next; + inline_leaves.extend(output.inline_leaves); + events.extend(output.events); + } + + if used_bits > 0 { + if let Some(next) = inline_span.checked_add(1) { + inline_span = next; + } else { + self.push_error(ContractLayoutError::LayoutExtentOverflow); + } + } + + WalkOutput { + inline_span, + inline_leaves, + events, + } + } + fn walk_array( &mut self, ty: TyId<'db>, @@ -2190,7 +2326,11 @@ impl<'db> FieldCollector<'db> { let field_place = place.with_step(PlaceStep::StructField(field_idx as u32)); items.push((inst, field_place)); } - let mut output = self.walk_sequence(items, dimensions, mode); + let mut output = if storage_like_space(self.active_space) { + self.walk_storage_struct_sequence(items, dimensions, mode) + } else { + self.walk_sequence(items, dimensions, mode) + }; direct_events.append(&mut output.events); output.events = direct_events; output @@ -2201,6 +2341,7 @@ impl<'db> FieldCollector<'db> { place: place.clone(), ty, offset: 0, + lane: None, dimensions: dimensions.to_vec(), strides: vec![0; dimensions.len()], kind: InlineLayoutLeafKind::EnumTag, @@ -3737,6 +3878,7 @@ fn inferred_parameter_entry<'db>( ty: occurrence_placeholder_ty(db, occurrence).unwrap_or(occurrence.placeholder), address_space, value, + lane: None, kind: ContractLayoutEntryKind::Parameter(ContractLayoutParameterOrigin::Inferred), } } @@ -3778,6 +3920,7 @@ fn allocated_contract_layout_report<'db>( ty: leaf.ty, address_space: field.address_space, value, + lane: leaf.lane, kind, }); } @@ -3792,6 +3935,7 @@ fn allocated_contract_layout_report<'db>( ty: occurrence.ty, address_space: occurrence.space, value: ContractLayoutValue::Scalar(occurrence.value), + lane: None, kind: ContractLayoutEntryKind::Parameter(ContractLayoutParameterOrigin::Explicit), }); } @@ -3930,8 +4074,15 @@ pub fn validate_allocated_contract_layout<'db>( let Some(extent) = affine_family_extent(&leaf.dimensions, &leaf.strides) else { return Err(LayoutInvariantError::InvalidFieldExtent { field: field.field }); }; + let invalid_lane = leaf.lane.is_some_and(|lane| { + lane.bit_width == 0 + || u32::from(lane.bit_width) > 256 + || u32::from(lane.bit_offset) + u32::from(lane.bit_width) > 256 + }) || (leaf.lane.is_some() + && !storage_like_space(field.address_space)); if leaf.place.field != field.field || leaf.dimensions.iter().any(|dimension| dimension.len == 0) + || invalid_lane || resolve_storage_place_with_dimensions(db, field, &leaf.place, &leaf.dimensions) .is_none() || leaf diff --git a/crates/hir/tests/contract_layout_report.rs b/crates/hir/tests/contract_layout_report.rs index d7ca3ef5a5..3a46a19eb6 100644 --- a/crates/hir/tests/contract_layout_report.rs +++ b/crates/hir/tests/contract_layout_report.rs @@ -29,6 +29,10 @@ fn scalar_value(db: &HirAnalysisTestDb, entry: &ContractLayoutEntry<'_>) -> Stri value.data(db).to_string() } +fn lane(entry: &ContractLayoutEntry<'_>) -> Option<(u16, u16)> { + entry.lane.map(|lane| (lane.bit_offset, lane.bit_width)) +} + #[test] fn report_distinguishes_inline_fields_from_explicit_and_inferred_parameters() { parse_ok!( @@ -117,6 +121,42 @@ pub contract Counter { ); } +#[test] +fn report_includes_packed_inline_field_lanes() { + parse_ok!( + db, + top_mod, + r#" +struct Packed { + a: u8, + b: u16, + c: bool, + d: u256, + e: u8, +} + +contract C { + mut packed: Packed, +} +"#, + ); + let contract = find_contract(&db, top_mod, "C"); + let report = contract.layout_report(&db).unwrap(); + + for (path, value, expected_lane) in [ + ("packed.a", "0", Some((0, 8))), + ("packed.b", "0", Some((8, 16))), + ("packed.c", "0", Some((24, 1))), + ("packed.d", "1", None), + ("packed.e", "2", Some((0, 8))), + ] { + let entry = entry(&db, &report.entries, path); + assert_eq!(scalar_value(&db, entry), value, "{path}"); + assert_eq!(lane(entry), expected_lane, "{path}"); + assert_eq!(entry.kind, ContractLayoutEntryKind::InlineField, "{path}"); + } +} + #[test] fn report_preserves_array_geometry_and_enum_overlays() { parse_ok!( @@ -153,6 +193,11 @@ contract C { assert_eq!(strides, &[2]); assert_eq!(*extent, 3); } + assert_eq!(lane(entry(&db, &report.entries, "values[i0].left")), None); + assert_eq!( + lane(entry(&db, &report.entries, "values[i0].right")), + Some((0, 8)) + ); let roots = entry(&db, &report.entries, "roots[i0][i1].ROOT"); let ContractLayoutValue::Indexed { diff --git a/crates/language-server/src/functionality/hover.rs b/crates/language-server/src/functionality/hover.rs index 6973f168d3..76af39473e 100644 --- a/crates/language-server/src/functionality/hover.rs +++ b/crates/language-server/src/functionality/hover.rs @@ -213,8 +213,15 @@ fn layout_entry_markdown(db: &DriverDataBase, entry: &ContractLayoutEntry<'_>) - "inferred parameter" } }; + let lane = entry.lane.map_or_else(String::new, |lane| { + format!( + " bits {}..{}", + lane.bit_offset, + lane.bit_offset + lane.bit_width + ) + }); format!( - "- `{value}`{}: `{}` ({kind}, `{}`)", + "- `{value}`{lane}{}: `{}` ({kind}, `{}`)", dimensions.unwrap_or_default(), entry.path.display(db), entry.ty.pretty_print(db), diff --git a/crates/mir/src/verify/storage_layout.rs b/crates/mir/src/verify/storage_layout.rs index b4c26ec856..e760b9a232 100644 --- a/crates/mir/src/verify/storage_layout.rs +++ b/crates/mir/src/verify/storage_layout.rs @@ -190,7 +190,14 @@ fn verify_contract_field_binding<'db>( field: binding.field, class: binding.class.clone(), })?; - let mir_span = pointee.span_words(db); + let mir_span = if matches!( + field.address_space, + ProviderAddressSpace::Storage | ProviderAddressSpace::Transient + ) { + pointee.storage_span_words(db) + } else { + pointee.span_words(db) + }; if u64::try_from(field.inline_span).ok() != Some(mir_span) { return Err(VerifyError::ContractFieldSpanMismatch { field: binding.field, From 2f448f0edaef390da118c6a5fbfa645a1bf4ae10 Mon Sep 17 00:00:00 2001 From: Grant Wuerker Date: Mon, 27 Jul 2026 09:52:24 -0600 Subject: [PATCH 5/8] storage: unify packed struct layout --- crates/codegen/src/sonatina/lower_runtime.rs | 112 +++++--- crates/common/src/layout.rs | 255 ++++++++++++++++++ .../storage_packed_aggregate_boundaries.fe | 126 +++++++++ .../hir/src/core/semantic/storage_layout.rs | 112 ++++---- crates/hir/tests/contract_layout_report.rs | 74 ++++- crates/mir/src/runtime/ir.rs | 178 +++++------- crates/mir/src/runtime/lower/abi.rs | 8 +- crates/mir/src/runtime/lower/boundary.rs | 1 + crates/mir/src/runtime/lower/conversion.rs | 1 + crates/mir/src/runtime/lower/infer.rs | 5 +- crates/mir/src/runtime/lower/layout.rs | 26 +- 11 files changed, 681 insertions(+), 217 deletions(-) create mode 100644 crates/fe/tests/fixtures/fe_test/storage_packed_aggregate_boundaries.fe diff --git a/crates/codegen/src/sonatina/lower_runtime.rs b/crates/codegen/src/sonatina/lower_runtime.rs index e375247658..31c41efbf8 100644 --- a/crates/codegen/src/sonatina/lower_runtime.rs +++ b/crates/codegen/src/sonatina/lower_runtime.rs @@ -386,7 +386,15 @@ impl<'db, 'a> ModuleLowerer<'db, 'a> { if let Some(&existing) = self.type_cache.get(&layout) { return Ok(existing); } - + // Storage policy does not change the in-memory representation of a layout. + if let Some(existing) = self.type_cache.iter().find_map(|(candidate, ty)| { + layout + .shares_runtime_rep_with(self.db, *candidate) + .then_some(*ty) + }) { + self.type_cache.insert(layout, existing); + return Ok(existing); + } let ty = match layout.data(self.db) { Layout::Struct(data) => { let fields = data @@ -2586,7 +2594,12 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { LowerError::Internal("field projection on non-struct class".to_string()) })?; let addr = self.offset_address(addr, placement.word_offset, space)?; - if placement.is_packed() { + if placement.requires_read_modify_write { + let lane = placement.lane.ok_or_else(|| { + LowerError::Internal( + "read-modify-write storage placement is missing a bit lane".to_string(), + ) + })?; if !matches!(class, RuntimeClass::Scalar(_)) { return Err(LowerError::Internal( "packed storage field placement for non-scalar class".to_string(), @@ -2597,8 +2610,8 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { space, class, lane: PackedLane { - bit_offset: placement.bit_offset, - bit_width: placement.bit_width, + bit_offset: lane.bit_offset, + bit_width: lane.bit_width, }, }); } @@ -2639,7 +2652,12 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { LowerError::Internal(format!("missing storage field placement for index {idx}")) })?; let addr = self.offset_address(addr, placement.word_offset, space)?; - if placement.is_packed() { + if placement.requires_read_modify_write { + let lane = placement.lane.ok_or_else(|| { + LowerError::Internal( + "read-modify-write storage placement is missing a bit lane".to_string(), + ) + })?; if !matches!(field, RuntimeClass::Scalar(_)) { return Err(LowerError::Internal( "packed storage field placement for non-scalar class".to_string(), @@ -2648,8 +2666,8 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { return Ok(( addr, Some(PackedLane { - bit_offset: placement.bit_offset, - bit_width: placement.bit_width, + bit_offset: lane.bit_offset, + bit_width: lane.bit_width, }), )); } @@ -2688,6 +2706,19 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { }) } + fn variant_field_offset_words_for_layout( + &self, + variant: VariantId<'db>, + field: FieldIndex, + layout: PtrLayoutMode, + ) -> Option { + if Self::uses_storage_layout(layout) { + variant.storage_field_offset_words(self.module.db, field) + } else { + variant.field_offset_words(self.module.db, field) + } + } + fn load_packed_lane( &mut self, addr: ValueId, @@ -3023,8 +3054,7 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { ) => PlaceTerminal::Ptr { addr: self.offset_address( addr, - variant - .field_offset_words(self.module.db, *field) + self.variant_field_offset_words_for_layout(*variant, *field, layout) .ok_or_else(|| { LowerError::Internal("variant field layout missing".to_string()) })?, @@ -3999,11 +4029,14 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { ); let field_addr = self.offset_address( addr, - variant - .field_offset_words(self.module.db, FieldIndex(field_idx as u16)) - .ok_or_else(|| { - LowerError::Internal("variant field layout missing".to_string()) - })?, + self.variant_field_offset_words_for_layout( + variant, + FieldIndex(field_idx as u16), + layout_mode, + ) + .ok_or_else(|| { + LowerError::Internal("variant field layout missing".to_string()) + })?, space, )?; self.copy_to_ptr(field_addr, space, layout_mode, field, field_value)?; @@ -4181,11 +4214,14 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { .map(|(field_idx, field)| { let field_addr = self.offset_address( addr, - variant - .field_offset_words(self.module.db, FieldIndex(field_idx as u16)) - .ok_or_else(|| { - LowerError::Internal("variant field layout missing".to_string()) - })?, + self.variant_field_offset_words_for_layout( + variant, + FieldIndex(field_idx as u16), + layout_mode, + ) + .ok_or_else(|| { + LowerError::Internal("variant field layout missing".to_string()) + })?, space, )?; self.load_from_ptr(field_addr, space, layout_mode, field) @@ -4301,11 +4337,25 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { src: ValueId, ) -> Result<(), LowerError> { let value = match class { - RuntimeClass::Scalar(scalar) => self.cast_scalar_with_signedness( - src, - scalar_word_ty(scalar), - scalar.is_signed_int(), - ), + RuntimeClass::Scalar(scalar) => { + let src_bits = int_bits(self.fb.type_of(src)); + let value = self.cast_scalar_with_signedness( + src, + scalar_word_ty(scalar), + scalar.is_signed_int(), + )?; + let storage_bits = scalar.storage_bit_width(); + if matches!( + space, + AddressSpaceKind::Storage | AddressSpaceKind::Transient + ) && storage_bits < 256 + && (scalar.is_signed_int() || src_bits > storage_bits) + { + self.mask_word_bits(value, storage_bits)? + } else { + value + } + } RuntimeClass::Ref { kind: RefKind::Provider { @@ -4317,12 +4367,14 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { .. }, .. - } => self.coerce_value_to_ty(src, Type::I256), - RuntimeClass::RawAddr { .. } => self.coerce_value_to_ty(src, Type::I256), - RuntimeClass::AggregateValue { .. } | RuntimeClass::Ref { .. } => Err( - LowerError::Unsupported("aggregate/handle ptr stores require CopyInto".to_string()), - ), - }?; + } => self.coerce_value_to_ty(src, Type::I256)?, + RuntimeClass::RawAddr { .. } => self.coerce_value_to_ty(src, Type::I256)?, + RuntimeClass::AggregateValue { .. } | RuntimeClass::Ref { .. } => { + return Err(LowerError::Unsupported( + "aggregate/handle ptr stores require CopyInto".to_string(), + )); + } + }; self.store_word(addr, space, value) } diff --git a/crates/common/src/layout.rs b/crates/common/src/layout.rs index 433a1810ce..8a9dc0c4d6 100644 --- a/crates/common/src/layout.rs +++ b/crates/common/src/layout.rs @@ -15,4 +15,259 @@ impl TargetDataLayout { pub const EVM_LAYOUT: TargetDataLayout = TargetDataLayout::evm(); pub const WORD_SIZE_BYTES: usize = EVM_LAYOUT.word_size_bytes; +pub const WORD_SIZE_BITS: u16 = (WORD_SIZE_BYTES * 8) as u16; pub const DISCRIMINANT_SIZE_BYTES: usize = EVM_LAYOUT.discriminant_size_bytes; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct StorageFieldShape { + pub span_words: u64, + pub bit_width: Option, +} + +impl StorageFieldShape { + pub const fn aggregate(span_words: u64) -> Self { + Self { + span_words, + bit_width: None, + } + } + + pub const fn scalar(bit_width: u16) -> Self { + Self { + span_words: 1, + bit_width: Some(bit_width), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct StorageBitLane { + pub bit_offset: u16, + pub bit_width: u16, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct StorageFieldPlacement { + pub word_offset: u64, + pub lane: Option, + pub requires_read_modify_write: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StorageFieldsLayout { + pub placements: Vec, + pub span_words: u64, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StorageLayoutError { + InvalidBitWidth(u16), + InvalidScalarSpan(u64), + ExtentOverflow, +} + +fn mark_shared_scalar_group(placements: &mut [StorageFieldPlacement], group_start: Option) { + let Some(group_start) = group_start else { + return; + }; + if placements.len().saturating_sub(group_start) > 1 { + for placement in &mut placements[group_start..] { + placement.requires_read_modify_write = true; + } + } +} + +pub fn storage_fields_layout( + fields: impl IntoIterator, +) -> Result { + let fields = fields.into_iter(); + let mut placements = Vec::with_capacity(fields.size_hint().0); + let mut word_offset = 0u64; + let mut used_bits = 0u16; + let mut scalar_group_start = None; + + for field in fields { + match field.bit_width { + Some(0) => return Err(StorageLayoutError::InvalidBitWidth(0)), + Some(bit_width) if bit_width > WORD_SIZE_BITS => { + return Err(StorageLayoutError::InvalidBitWidth(bit_width)); + } + Some(bit_width) => { + if field.span_words != 1 { + return Err(StorageLayoutError::InvalidScalarSpan(field.span_words)); + } + if bit_width == WORD_SIZE_BITS { + if used_bits > 0 { + mark_shared_scalar_group(&mut placements, scalar_group_start); + word_offset = word_offset + .checked_add(1) + .ok_or(StorageLayoutError::ExtentOverflow)?; + used_bits = 0; + scalar_group_start = None; + } + placements.push(StorageFieldPlacement { + word_offset, + lane: None, + requires_read_modify_write: false, + }); + word_offset = word_offset + .checked_add(1) + .ok_or(StorageLayoutError::ExtentOverflow)?; + continue; + } + + if u32::from(used_bits) + u32::from(bit_width) > u32::from(WORD_SIZE_BITS) { + mark_shared_scalar_group(&mut placements, scalar_group_start); + word_offset = word_offset + .checked_add(1) + .ok_or(StorageLayoutError::ExtentOverflow)?; + used_bits = 0; + scalar_group_start = None; + } + scalar_group_start.get_or_insert(placements.len()); + let placement_word_offset = word_offset; + let lane = StorageBitLane { + bit_offset: used_bits, + bit_width, + }; + used_bits += bit_width; + placements.push(StorageFieldPlacement { + word_offset: placement_word_offset, + lane: Some(lane), + requires_read_modify_write: false, + }); + if used_bits == WORD_SIZE_BITS { + mark_shared_scalar_group(&mut placements, scalar_group_start); + word_offset = word_offset + .checked_add(1) + .ok_or(StorageLayoutError::ExtentOverflow)?; + used_bits = 0; + scalar_group_start = None; + } + } + None => { + if used_bits > 0 { + mark_shared_scalar_group(&mut placements, scalar_group_start); + word_offset = word_offset + .checked_add(1) + .ok_or(StorageLayoutError::ExtentOverflow)?; + used_bits = 0; + scalar_group_start = None; + } + placements.push(StorageFieldPlacement { + word_offset, + lane: None, + requires_read_modify_write: false, + }); + word_offset = word_offset + .checked_add(field.span_words) + .ok_or(StorageLayoutError::ExtentOverflow)?; + } + } + } + + if used_bits > 0 { + mark_shared_scalar_group(&mut placements, scalar_group_start); + word_offset = word_offset + .checked_add(1) + .ok_or(StorageLayoutError::ExtentOverflow)?; + } + + Ok(StorageFieldsLayout { + placements, + span_words: word_offset, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn packs_subword_scalars_and_preserves_aggregate_boundaries() { + let layout = storage_fields_layout([ + StorageFieldShape::scalar(8), + StorageFieldShape::scalar(16), + StorageFieldShape::aggregate(2), + StorageFieldShape::scalar(1), + ]) + .unwrap(); + + assert_eq!(layout.span_words, 4); + assert_eq!( + layout.placements, + [ + StorageFieldPlacement { + word_offset: 0, + lane: Some(StorageBitLane { + bit_offset: 0, + bit_width: 8, + }), + requires_read_modify_write: true, + }, + StorageFieldPlacement { + word_offset: 0, + lane: Some(StorageBitLane { + bit_offset: 8, + bit_width: 16, + }), + requires_read_modify_write: true, + }, + StorageFieldPlacement { + word_offset: 1, + lane: None, + requires_read_modify_write: false, + }, + StorageFieldPlacement { + word_offset: 3, + lane: Some(StorageBitLane { + bit_offset: 0, + bit_width: 1, + }), + requires_read_modify_write: false, + }, + ] + ); + } + + #[test] + fn starts_a_new_word_when_the_next_lane_does_not_fit() { + let layout = storage_fields_layout([ + StorageFieldShape::scalar(128), + StorageFieldShape::scalar(128), + StorageFieldShape::scalar(8), + ]) + .unwrap(); + + assert_eq!(layout.span_words, 2); + assert!(layout.placements[0].requires_read_modify_write); + assert!(layout.placements[1].requires_read_modify_write); + assert_eq!( + layout.placements[2], + StorageFieldPlacement { + word_offset: 1, + lane: Some(StorageBitLane { + bit_offset: 0, + bit_width: 8, + }), + requires_read_modify_write: false, + } + ); + } + + #[test] + fn gives_a_lone_subword_scalar_an_explicit_lane() { + let layout = + storage_fields_layout([StorageFieldShape::scalar(8)]).expect("valid storage layout"); + + assert_eq!(layout.span_words, 1); + assert_eq!( + layout.placements[0].lane, + Some(StorageBitLane { + bit_offset: 0, + bit_width: 8, + }) + ); + assert!(!layout.placements[0].requires_read_modify_write); + } +} diff --git a/crates/fe/tests/fixtures/fe_test/storage_packed_aggregate_boundaries.fe b/crates/fe/tests/fixtures/fe_test/storage_packed_aggregate_boundaries.fe new file mode 100644 index 0000000000..337bf2bdf2 --- /dev/null +++ b/crates/fe/tests/fixtures/fe_test/storage_packed_aggregate_boundaries.fe @@ -0,0 +1,126 @@ +use std::evm::Evm + +struct Inner { + first: u8, + second: u8, +} + +struct Outer { + inner: Inner, + tail: u8, +} + +struct SignedTail { + value: i8, + next: u256, +} + +enum Choice { + Packed(Inner, u8), + Unit, +} + +msg M { + #[selector = 1] + Write, + + #[selector = 2] + Raw { slot: u256 } -> u256, + + #[selector = 3] + Read -> u256, +} + +pub contract C { + mut outer: Outer, + mut pair: (u8, u16), + mut signed: SignedTail, + mut choice: Choice, + + recv M { + Write uses (mut outer, mut pair, mut signed, mut choice) { + outer.inner.first = 0xaa + outer.inner.second = 0xbb + outer.tail = 0xcc + pair.0 = 0x11 + pair.1 = 0x2233 + signed.value = -1 + signed.next = 0x44 + choice = Choice::Packed( + Inner { + first: 0x55, + second: 0x66, + }, + 0x77, + ) + } + + Raw { slot } -> u256 uses (evm: Evm) { + evm.sload(slot) + } + + Read -> u256 uses (outer, pair, signed, choice) { + if outer.inner.first != 0xaa { + return 0 + } + if outer.inner.second != 0xbb { + return 0 + } + if outer.tail != 0xcc { + return 0 + } + if pair.0 != 0x11 { + return 0 + } + if pair.1 != 0x2233 { + return 0 + } + if signed.value != -1 { + return 0 + } + if signed.next != 0x44 { + return 0 + } + match choice { + Choice::Packed(inner, tail) => { + if inner.first == 0x55 && inner.second == 0x66 && tail == 0x77 { + 1 + } else { + 0 + } + }, + Choice::Unit => 0, + } + } + } +} + +#[test] +fn test_storage_packed_aggregate_boundaries() uses (evm: mut Evm) { + let c = evm.create2(value: 0, args: (), salt: 0) + evm.call(addr: c, gas: 1000000, value: 0, message: M::Write {}) + + for (slot, expected) in [ + (0, 0xbbaa), + (1, 0xcc), + (2, 0x11), + (3, 0x2233), + (4, 0xff), + (5, 0x44), + (6, 0), + (7, 0x6655), + (8, 0x77), + (9, 0), + ] { + let actual: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: M::Raw { slot: slot }, + ) + assert!(actual == expected) + } + + let valid: u256 = evm.call(addr: c, gas: 1000000, value: 0, message: M::Read {}) + assert!(valid == 1) +} diff --git a/crates/hir/src/core/semantic/storage_layout.rs b/crates/hir/src/core/semantic/storage_layout.rs index b57b4a3ebd..85b30466e7 100644 --- a/crates/hir/src/core/semantic/storage_layout.rs +++ b/crates/hir/src/core/semantic/storage_layout.rs @@ -1,4 +1,7 @@ -use common::indexmap::IndexMap; +use common::{ + indexmap::IndexMap, + layout::{StorageFieldShape, storage_fields_layout}, +}; use num_bigint::BigUint; use num_traits::ToPrimitive; use rustc_hash::{FxHashMap, FxHashSet}; @@ -2080,95 +2083,72 @@ impl<'db> FieldCollector<'db> { } } - fn packable_storage_scalar_leaf(&self, output: &WalkOutput<'db>) -> Option { - if output.inline_span != 1 || output.inline_leaves.len() != 1 { - return None; - } - let leaf = &output.inline_leaves[0]; - if leaf.offset != 0 || leaf.lane.is_some() || leaf.kind != InlineLayoutLeafKind::Field { - return None; - } - let bit_width = storage_scalar_bit_width(self.db, leaf.ty)?; - (bit_width < 256).then_some(bit_width) - } - fn walk_storage_struct_sequence( &mut self, items: impl IntoIterator, StoragePlace<'db>)>, dimensions: &[LayoutIndexDimension<'db>], mode: WalkMode, ) -> WalkOutput<'db> { - let mut inline_span = 0usize; - let mut used_bits = 0u16; - let mut inline_leaves = Vec::new(); - let mut events = Vec::new(); + let mut outputs = Vec::new(); + let mut shapes = Vec::new(); for (instantiation, place) in items { - let mut output = self.walk_instantiation(&instantiation, place, dimensions, mode); - if let Some(bit_width) = self.packable_storage_scalar_leaf(&output) { - if used_bits + bit_width > 256 { - let Some(next) = inline_span.checked_add(1) else { - self.push_error(ContractLayoutError::LayoutExtentOverflow); - continue; - }; - inline_span = next; - used_bits = 0; - } - - for leaf in &mut output.inline_leaves { - leaf.offset = inline_span; - leaf.lane = Some(ContractLayoutLane { - bit_offset: used_bits, - bit_width, - }); - } - used_bits += bit_width; - if used_bits == 256 { - let Some(next) = inline_span.checked_add(1) else { - self.push_error(ContractLayoutError::LayoutExtentOverflow); - continue; - }; - inline_span = next; - used_bits = 0; - } - inline_leaves.extend(output.inline_leaves); - events.extend(output.events); + let direct_bit_width = storage_scalar_bit_width(self.db, instantiation.ty) + .filter(|bit_width| *bit_width < 256); + let output = self.walk_instantiation(&instantiation, place, dimensions, mode); + let Ok(span_words) = u64::try_from(output.inline_span) else { + self.push_error(ContractLayoutError::LayoutExtentOverflow); continue; - } + }; + let shape = if let Some(bit_width) = direct_bit_width { + debug_assert_eq!(output.inline_span, 1); + debug_assert_eq!(output.inline_leaves.len(), 1); + debug_assert_eq!(output.inline_leaves[0].offset, 0); + debug_assert!(output.inline_leaves[0].lane.is_none()); + debug_assert_eq!(output.inline_leaves[0].kind, InlineLayoutLeafKind::Field); + StorageFieldShape::scalar(bit_width) + } else { + StorageFieldShape::aggregate(span_words) + }; + shapes.push(shape); + outputs.push(output); + } - if used_bits > 0 { - let Some(next) = inline_span.checked_add(1) else { - self.push_error(ContractLayoutError::LayoutExtentOverflow); - continue; - }; - inline_span = next; - used_bits = 0; - } + let Ok(layout) = storage_fields_layout(shapes) else { + self.push_error(ContractLayoutError::LayoutExtentOverflow); + return WalkOutput::empty(); + }; + let Ok(inline_span) = usize::try_from(layout.span_words) else { + self.push_error(ContractLayoutError::LayoutExtentOverflow); + return WalkOutput::empty(); + }; - let Some(next) = inline_span.checked_add(output.inline_span) else { + let mut inline_leaves = Vec::new(); + let mut events = Vec::new(); + for (mut output, placement) in outputs.into_iter().zip(layout.placements) { + let Ok(word_offset) = usize::try_from(placement.word_offset) else { self.push_error(ContractLayoutError::LayoutExtentOverflow); continue; }; for leaf in &mut output.inline_leaves { - let Some(offset) = leaf.offset.checked_add(inline_span) else { + let Some(offset) = leaf.offset.checked_add(word_offset) else { self.push_error(ContractLayoutError::LayoutExtentOverflow); continue; }; leaf.offset = offset; + if placement.requires_read_modify_write + && let Some(lane) = placement.lane + { + leaf.lane = Some(ContractLayoutLane { + bit_offset: lane.bit_offset, + bit_width: lane.bit_width, + }); + } } - inline_span = next; inline_leaves.extend(output.inline_leaves); events.extend(output.events); } - if used_bits > 0 { - if let Some(next) = inline_span.checked_add(1) { - inline_span = next; - } else { - self.push_error(ContractLayoutError::LayoutExtentOverflow); - } - } - WalkOutput { inline_span, inline_leaves, diff --git a/crates/hir/tests/contract_layout_report.rs b/crates/hir/tests/contract_layout_report.rs index 3a46a19eb6..f56f1fb5af 100644 --- a/crates/hir/tests/contract_layout_report.rs +++ b/crates/hir/tests/contract_layout_report.rs @@ -148,7 +148,7 @@ contract C { ("packed.b", "0", Some((8, 16))), ("packed.c", "0", Some((24, 1))), ("packed.d", "1", None), - ("packed.e", "2", Some((0, 8))), + ("packed.e", "2", None), ] { let entry = entry(&db, &report.entries, path); assert_eq!(scalar_value(&db, entry), value, "{path}"); @@ -157,6 +157,73 @@ contract C { } } +#[test] +fn report_preserves_aggregate_boundaries_around_packed_structs() { + parse_ok!( + db, + top_mod, + r#" +struct Inner { + first: u8, + second: u8, +} + +struct Outer { + inner: Inner, + tail: u8, + pair: (u8, u16), + one: [u8; 1], + after: u8, +} + +enum Choice { + Nested(Inner, u8), + Unit, +} + +contract C { + mut outer: Outer, + mut choice: Choice, +} +"#, + ); + let contract = find_contract(&db, top_mod, "C"); + let report = contract.layout_report(&db).unwrap(); + + for (path, value, expected_lane) in [ + ("outer.inner.first", "0", Some((0, 8))), + ("outer.inner.second", "0", Some((8, 8))), + ("outer.tail", "1", None), + ("outer.pair.0", "2", None), + ("outer.pair.1", "3", None), + ("outer.after", "5", None), + ("choice.", "6", None), + ("choice::Nested.0.first", "7", Some((0, 8))), + ("choice::Nested.0.second", "7", Some((8, 8))), + ("choice::Nested.1", "8", None), + ] { + let entry = entry(&db, &report.entries, path); + assert_eq!(scalar_value(&db, entry), value, "{path}"); + assert_eq!(lane(entry), expected_lane, "{path}"); + } + + let one = entry(&db, &report.entries, "outer.one[i0]"); + let ContractLayoutValue::Indexed { + base, + dimensions, + strides, + extent, + } = &one.value + else { + panic!("expected indexed inline entry: {one:#?}"); + }; + assert_eq!(base.data(&db).to_string(), "4"); + assert_eq!(dimensions, &[1]); + assert_eq!(strides, &[1]); + assert_eq!(*extent, 1); + assert_eq!(lane(one), None); +} + #[test] fn report_preserves_array_geometry_and_enum_overlays() { parse_ok!( @@ -194,10 +261,7 @@ contract C { assert_eq!(*extent, 3); } assert_eq!(lane(entry(&db, &report.entries, "values[i0].left")), None); - assert_eq!( - lane(entry(&db, &report.entries, "values[i0].right")), - Some((0, 8)) - ); + assert_eq!(lane(entry(&db, &report.entries, "values[i0].right")), None); let roots = entry(&db, &report.entries, "roots[i0][i1].ROOT"); let ContractLayoutValue::Indexed { diff --git a/crates/mir/src/runtime/ir.rs b/crates/mir/src/runtime/ir.rs index 970ad6cf9e..96f55ed0c4 100644 --- a/crates/mir/src/runtime/ir.rs +++ b/crates/mir/src/runtime/ir.rs @@ -1,3 +1,5 @@ +pub use common::layout::StorageFieldPlacement as FieldPlacement; +use common::layout::{StorageFieldShape, StorageFieldsLayout, storage_fields_layout}; use cranelift_entity::{EntityRef, entity_impl}; use hir::analysis::{ semantic::{FieldIndex, LayoutEvidenceConstant, SemanticInstance}, @@ -250,13 +252,7 @@ impl<'db> RuntimeClass<'db> { 1 + data .variants .iter() - .map(|variant| { - variant - .fields - .iter() - .map(|field| field.span_words(db)) - .sum::() - }) + .map(|variant| variant.storage_span_words(db)) .max() .unwrap_or(0) } @@ -454,6 +450,15 @@ impl ScalarClass<'_> { pub fn is_signed_int(&self) -> bool { matches!(self.repr, ScalarRepr::Int { signed: true, .. }) } + + pub fn storage_bit_width(&self) -> u16 { + match self.repr { + ScalarRepr::Bool => 1, + ScalarRepr::Int { bits, .. } => bits, + ScalarRepr::FixedBytes { len } => len.saturating_mul(8), + ScalarRepr::Address { bits } => bits, + } + } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Update)] @@ -566,6 +571,10 @@ impl<'db> LayoutId<'db> { }), } } + + pub fn shares_runtime_rep_with(self, db: &'db dyn MirDb, other: Self) -> bool { + layouts_share_runtime_rep(db, self, other) + } } #[derive(Clone, Debug, PartialEq, Eq, Hash, Update)] @@ -585,19 +594,33 @@ pub enum Layout<'db> { #[derive(Clone, Debug, PartialEq, Eq, Hash, Update)] pub struct StructLayout<'db> { pub fields: Box<[RuntimeClass<'db>]>, + pub storage_field_packing: StorageFieldPacking, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Update)] -pub struct FieldPlacement { - pub word_offset: u64, - pub bit_offset: u16, - pub bit_width: u16, - pub packed: bool, +pub enum StorageFieldPacking { + Packed, + WordAligned, } -impl FieldPlacement { - pub fn is_packed(self) -> bool { - self.packed +impl StorageFieldPacking { + pub fn for_struct_fields<'db>(db: &'db dyn MirDb, fields: &[RuntimeClass<'db>]) -> Self { + let layout = storage_fields_layout(fields.iter().map(|field| { + storage_scalar_bit_width(field).map_or_else( + || StorageFieldShape::aggregate(field.storage_span_words(db)), + StorageFieldShape::scalar, + ) + })) + .expect("runtime storage field layout must be valid"); + if layout + .placements + .iter() + .any(|placement| placement.requires_read_modify_write) + { + Self::Packed + } else { + Self::WordAligned + } } } @@ -615,100 +638,24 @@ impl<'db> StructLayout<'db> { db: &'db dyn MirDb, idx: usize, ) -> Option { - self.storage_field_placements(db).get(idx).copied() + self.storage_layout(db).placements.get(idx).copied() } - fn storage_field_placements(&self, db: &'db dyn MirDb) -> Vec { - let mut placements: Vec = Vec::with_capacity(self.fields.len()); - let mut word_offset = 0; - let mut used_bits = 0; - let mut current_word_start = None; - - for field in self.fields.iter() { - if let Some(bit_width) = storage_scalar_bit_width(field) - && bit_width < 256 + fn storage_layout(&self, db: &'db dyn MirDb) -> StorageFieldsLayout { + storage_fields_layout(self.fields.iter().map(|field| { + if self.storage_field_packing == StorageFieldPacking::Packed + && let Some(bit_width) = storage_scalar_bit_width(field) { - if used_bits + bit_width > 256 { - word_offset += 1; - used_bits = 0; - current_word_start = None; - } - - let placement_idx = placements.len(); - if used_bits == 0 { - current_word_start = Some(placement_idx); - } else if let Some(start) = current_word_start { - for placement in &mut placements[start..] { - placement.packed = true; - } - } - - placements.push(FieldPlacement { - word_offset, - bit_offset: used_bits, - bit_width, - packed: used_bits > 0, - }); - - used_bits += bit_width; - if used_bits == 256 { - word_offset += 1; - used_bits = 0; - current_word_start = None; - } - continue; - } - - if used_bits > 0 { - word_offset += 1; - used_bits = 0; - current_word_start = None; + StorageFieldShape::scalar(bit_width) + } else { + StorageFieldShape::aggregate(field.storage_span_words(db)) } - - placements.push(FieldPlacement { - word_offset, - bit_offset: 0, - bit_width: 256, - packed: false, - }); - word_offset += field.storage_span_words(db); - } - - placements + })) + .expect("runtime storage field layout must be valid") } pub fn storage_span_words(&self, db: &'db dyn MirDb) -> u64 { - let mut word_offset = 0; - let mut used_bits = 0; - - for field in self.fields.iter() { - if let Some(bit_width) = storage_scalar_bit_width(field) - && bit_width < 256 - { - if used_bits + bit_width > 256 { - word_offset += 1; - used_bits = 0; - } - used_bits += bit_width; - if used_bits == 256 { - word_offset += 1; - used_bits = 0; - } - continue; - } - - if used_bits > 0 { - word_offset += 1; - used_bits = 0; - } - word_offset += field.storage_span_words(db); - } - - if used_bits > 0 { - word_offset + 1 - } else { - word_offset - } + self.storage_layout(db).span_words } } @@ -716,12 +663,7 @@ fn storage_scalar_bit_width(class: &RuntimeClass<'_>) -> Option { let RuntimeClass::Scalar(scalar) = class else { return None; }; - Some(match scalar.repr { - ScalarRepr::Bool => 1, - ScalarRepr::Int { bits, .. } => bits, - ScalarRepr::FixedBytes { len } => len.saturating_mul(8), - ScalarRepr::Address { bits } => bits, - }) + Some(scalar.storage_bit_width()) } #[derive(Clone, Debug, PartialEq, Eq, Hash, Update)] @@ -754,6 +696,21 @@ impl<'db> EnumVariantLayout<'db> { .map(|field| field.span_words(db)) .sum() } + + pub fn storage_payload_field_offset_words(&self, db: &'db dyn MirDb, field: FieldIndex) -> u64 { + self.fields + .iter() + .take(field.0 as usize) + .map(|field| field.storage_span_words(db)) + .sum() + } + + pub fn storage_span_words(&self, db: &'db dyn MirDb) -> u64 { + self.fields + .iter() + .map(|field| field.storage_span_words(db)) + .sum() + } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Update)] @@ -774,6 +731,11 @@ impl<'db> VariantId<'db> { let layout = self.layout(db)?; Some(1 + layout.variants[self.index as usize].payload_field_offset_words(db, field)) } + + pub fn storage_field_offset_words(self, db: &'db dyn MirDb, field: FieldIndex) -> Option { + let layout = self.layout(db)?; + Some(1 + layout.variants[self.index as usize].storage_payload_field_offset_words(db, field)) + } } fn enum_tag_repr(variant_count: usize) -> ScalarRepr { diff --git a/crates/mir/src/runtime/lower/abi.rs b/crates/mir/src/runtime/lower/abi.rs index 1e2aeeaac0..fbefdbb35f 100644 --- a/crates/mir/src/runtime/lower/abi.rs +++ b/crates/mir/src/runtime/lower/abi.rs @@ -132,7 +132,13 @@ pub(crate) fn runtime_abi_plan<'db>( .chain(evidence.iter().map(|result| result.class.clone())) .collect::>() .into_boxed_slice(); - let layout = LayoutId::new(db, LayoutKey::Struct(StructLayout { fields })); + let layout = LayoutId::new( + db, + LayoutKey::Struct(StructLayout { + fields, + storage_field_packing: crate::runtime::StorageFieldPacking::WordAligned, + }), + ); (Some(RuntimeClass::AggregateValue { layout }), Some(layout)) }; diff --git a/crates/mir/src/runtime/lower/boundary.rs b/crates/mir/src/runtime/lower/boundary.rs index ec383f3bde..9b70968b7d 100644 --- a/crates/mir/src/runtime/lower/boundary.rs +++ b/crates/mir/src/runtime/lower/boundary.rs @@ -1019,6 +1019,7 @@ mod tests { db, LayoutKey::Struct(StructLayout { fields: vec![word_class()].into(), + storage_field_packing: crate::runtime::StorageFieldPacking::WordAligned, }), ) } diff --git a/crates/mir/src/runtime/lower/conversion.rs b/crates/mir/src/runtime/lower/conversion.rs index 7b51d842dc..d8c6ea5ad6 100644 --- a/crates/mir/src/runtime/lower/conversion.rs +++ b/crates/mir/src/runtime/lower/conversion.rs @@ -700,6 +700,7 @@ mod tests { db, LayoutKey::Struct(StructLayout { fields: vec![word_class()].into(), + storage_field_packing: crate::runtime::StorageFieldPacking::WordAligned, }), ) } diff --git a/crates/mir/src/runtime/lower/infer.rs b/crates/mir/src/runtime/lower/infer.rs index 41a1302235..ca73aaf6b5 100644 --- a/crates/mir/src/runtime/lower/infer.rs +++ b/crates/mir/src/runtime/lower/infer.rs @@ -1364,11 +1364,13 @@ fn merge_layouts<'db>( )) } (Layout::Struct(current), Layout::Struct(desired)) - if current.fields.len() == desired.fields.len() => + if current.fields.len() == desired.fields.len() + && current.storage_field_packing == desired.storage_field_packing => { Some(LayoutId::new( db, LayoutKey::Struct(StructLayout { + storage_field_packing: current.storage_field_packing, fields: current .fields .iter() @@ -1529,6 +1531,7 @@ mod tests { db, LayoutKey::Struct(StructLayout { fields: vec![word.clone(), word].into(), + storage_field_packing: crate::runtime::StorageFieldPacking::WordAligned, }), ) } diff --git a/crates/mir/src/runtime/lower/layout.rs b/crates/mir/src/runtime/lower/layout.rs index 7acdf7dbd4..1725caa2c8 100644 --- a/crates/mir/src/runtime/lower/layout.rs +++ b/crates/mir/src/runtime/lower/layout.rs @@ -7,7 +7,7 @@ use crate::{ db::MirDb, runtime::{ ArrayLayout, EnumLayoutKey, EnumVariantLayout, Layout, LayoutId, LayoutKey, PlaceElem, - RuntimeClass, StructLayout, + RuntimeClass, StorageFieldPacking, StructLayout, }, }; @@ -39,14 +39,22 @@ pub(crate) fn layout_for_ty_in_env<'db>( }), ); } + let fields = ty + .field_types(db) + .into_iter() + .map(|field| stored_class_for_ty_in_env(db, env, field)) + .collect::>() + .into_boxed_slice(); + let storage_field_packing = if ty.is_struct(db) { + StorageFieldPacking::for_struct_fields(db, &fields) + } else { + StorageFieldPacking::WordAligned + }; LayoutId::new( db, LayoutKey::Struct(StructLayout { - fields: ty - .field_types(db) - .into_iter() - .map(|field| stored_class_for_ty_in_env(db, env, field)) - .collect(), + fields, + storage_field_packing, }), ) } @@ -94,9 +102,15 @@ pub(crate) fn layout_for_aggregate_instance_in_env<'db>( "aggregate instance arity mismatch for struct/tuple type {}", ty.pretty_print(db), ); + let storage_field_packing = if ty.is_struct(db) { + StorageFieldPacking::for_struct_fields(db, field_classes) + } else { + StorageFieldPacking::WordAligned + }; LayoutId::new( db, LayoutKey::Struct(StructLayout { + storage_field_packing, fields: field_classes.to_vec().into_boxed_slice(), }), ) From 13b15271c2b8629177f91291f74bdf07b22672e1 Mon Sep 17 00:00:00 2001 From: Grant Wuerker Date: Wed, 29 Jul 2026 17:44:27 -0600 Subject: [PATCH 6/8] storage: preserve packed lanes through effect forwarding --- crates/codegen/src/sonatina/lower_runtime.rs | 80 ++++++++++-- .../contract_storage_struct_fields_packed.fe | 33 +++++ .../storage_map_packed_struct_values.fe | 33 ++++- crates/mir/src/runtime/ir.rs | 9 ++ crates/mir/src/runtime/lower/boundary.rs | 46 ++++++- crates/mir/src/runtime/lower/classify.rs | 118 ++++++++++-------- crates/mir/src/runtime/lower/conversion.rs | 44 +++++++ crates/mir/src/runtime/package.rs | 3 + crates/mir/src/runtime/place.rs | 106 ++++++++++++++-- crates/mir/src/runtime/pretty.rs | 3 + crates/mir/src/verify/layout.rs | 41 ++++-- crates/mir/src/verify/storage_layout.rs | 2 +- 12 files changed, 438 insertions(+), 80 deletions(-) diff --git a/crates/codegen/src/sonatina/lower_runtime.rs b/crates/codegen/src/sonatina/lower_runtime.rs index 31c41efbf8..0015d541f2 100644 --- a/crates/codegen/src/sonatina/lower_runtime.rs +++ b/crates/codegen/src/sonatina/lower_runtime.rs @@ -10,7 +10,7 @@ use hir::{ hir_def::{ArithBinOp, BinOp, CompBinOp, LogicalBinOp, UnOp}, projection::IndexSource, }; -use mir::runtime::RefKind; +use mir::runtime::{RefKind, RefView, StorageBitLane}; use mir::{ AddressSpaceKind, ConstNode, ConstRegionId, ConstScalar, IntrinsicArithBinOp, Layout, LayoutId, RBlockId, RExpr, RLocalId, RStmt, RTerminator, ResolvedPlaceElem, ResolvedPlaceRootKind, @@ -625,11 +625,7 @@ enum SlotRoot { Object(ValueId, Type), } -#[derive(Clone, Copy)] -struct PackedLane { - bit_offset: u16, - bit_width: u16, -} +type PackedLane = StorageBitLane; #[derive(Clone, Copy, PartialEq, Eq)] enum PtrLayoutMode { @@ -2420,6 +2416,27 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { root_kind: &str, ) -> Result, LowerError> { match carrier_class { + RuntimeClass::Ref { + kind: RefKind::Provider { space, .. }, + view: RefView::StorageLane(lane), + .. + } => { + if !matches!( + space, + AddressSpaceKind::Storage | AddressSpaceKind::Transient + ) || !matches!(&class, RuntimeClass::Scalar(_)) + { + return Err(LowerError::Internal( + "storage-lane view requires a scalar storage provider".to_string(), + )); + } + Ok(PlaceTerminal::PackedPtr { + addr: self.local_value(value)?, + space, + class, + lane, + }) + } RuntimeClass::Ref { kind: RefKind::Const, .. @@ -2477,6 +2494,27 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { carrier_class: &RuntimeClass<'db>, ) -> Result, LowerError> { match carrier_class { + RuntimeClass::Ref { + kind: RefKind::Provider { space, .. }, + pointee, + view: RefView::StorageLane(lane), + } => { + if !matches!( + space, + AddressSpaceKind::Storage | AddressSpaceKind::Transient + ) || !matches!(pointee.as_ref(), RuntimeClass::Scalar(_)) + { + return Err(LowerError::Internal( + "storage-lane view requires a scalar storage provider".to_string(), + )); + } + Ok(PlaceTerminal::PackedPtr { + addr: value, + space: *space, + class: (**pointee).clone(), + lane: *lane, + }) + } RuntimeClass::Ref { kind: RefKind::Const, pointee, @@ -3800,9 +3838,33 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { } Ok(Lowered::Value(addr)) } - PlaceTerminal::PackedPtr { .. } => Err(LowerError::Unsupported( - "cannot borrow packed storage fields as raw pointers".to_string(), - )), + PlaceTerminal::PackedPtr { + addr, + space, + class, + lane, + } => { + if let Some(dst) = dst + && let Some(RuntimeClass::Ref { + pointee, + kind: + RefKind::Provider { + space: target_space, + .. + }, + view: RefView::StorageLane(target_lane), + }) = self.body.value_class(dst) + && pointee.as_ref() == &class + && *target_space == space + && *target_lane == lane + { + return Ok(Lowered::Value(addr)); + } + Err(LowerError::Unsupported( + "packed storage field address requires a matching lane-aware provider" + .to_string(), + )) + } } } diff --git a/crates/fe/tests/fixtures/fe_test/contract_storage_struct_fields_packed.fe b/crates/fe/tests/fixtures/fe_test/contract_storage_struct_fields_packed.fe index aad5589f66..be5259c2c4 100644 --- a/crates/fe/tests/fixtures/fe_test/contract_storage_struct_fields_packed.fe +++ b/crates/fe/tests/fixtures/fe_test/contract_storage_struct_fields_packed.fe @@ -9,6 +9,9 @@ msg ContractStructMsg { #[selector = 3] Read -> u256, + + #[selector = 4] + IncrementFields -> u256, } struct ContractStruct { @@ -21,6 +24,10 @@ fn pack(value: ContractStruct) -> u256 { ((value.a as u256) * 1000000) + ((value.b as u256) * 1000) + value.c } +fn increment() uses (field: mut u8) { + field += 1 +} + pub contract C { mut value: ContractStruct @@ -41,6 +48,16 @@ pub contract C { Read -> u256 uses (value) { pack(value: value) } + + IncrementFields -> u256 uses (mut value) { + with (mut value.a) { + increment() + } + with (mut value.b) { + increment() + } + pack(value: value) + } } } @@ -87,4 +104,20 @@ fn test_contract_storage_struct_fields_packed() uses (evm: mut Evm) { message: ContractStructMsg::Read {}, ) assert!(packed == 170187204) + + let packed: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: ContractStructMsg::IncrementFields {}, + ) + assert!(packed == 171188204) + + let raw0: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: ContractStructMsg::RawSlot { offset: 0 }, + ) + assert!(raw0 == 0xbcab) } diff --git a/crates/fe/tests/fixtures/fe_test/storage_map_packed_struct_values.fe b/crates/fe/tests/fixtures/fe_test/storage_map_packed_struct_values.fe index e187e536e0..ad462fd27e 100644 --- a/crates/fe/tests/fixtures/fe_test/storage_map_packed_struct_values.fe +++ b/crates/fe/tests/fixtures/fe_test/storage_map_packed_struct_values.fe @@ -25,6 +25,9 @@ msg PackedStructMapMsg { #[selector = 6] SetC { c: bool } -> u256, + + #[selector = 7] + IncrementB -> u256, } struct PackedPosition { @@ -57,6 +60,16 @@ fn set_c(value: bool) uses (position: mut PackedPosition) { position.c = value } +fn increment_u16() uses (field: mut u16) { + field += 1 +} + +fn increment_b() uses (position: mut PackedPosition) { + with (mut position.b) { + increment_u16() + } +} + fn pack_position(position: PackedPosition) -> u256 { let mut c: u256 = 0 if position.c { @@ -131,6 +144,16 @@ pub contract C { } evm.sload(position_ptr.raw()) } + + IncrementB -> u256 uses (mut store, evm: Evm) { + let position_ptr = with (mut store.positions) { + store.positions.mut_ptr(key: 1) + } + with (position_ptr) { + increment_b() + } + evm.sload(position_ptr.raw()) + } } } @@ -200,11 +223,19 @@ fn test_storage_map_packed_struct_values() uses (evm: mut Evm) { ) assert!(raw0 == 0x123455) + let raw0: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: PackedStructMapMsg::IncrementB {}, + ) + assert!(raw0 == 0x123555) + let packed: u256 = evm.call( addr: c, gas: 1000000, value: 0, message: PackedStructMapMsg::Read {}, ) - assert!(packed == 85466000007068) + assert!(packed == 85466100007068) } diff --git a/crates/mir/src/runtime/ir.rs b/crates/mir/src/runtime/ir.rs index 96f55ed0c4..2bd7da097b 100644 --- a/crates/mir/src/runtime/ir.rs +++ b/crates/mir/src/runtime/ir.rs @@ -342,6 +342,15 @@ pub enum RefKind<'db> { pub enum RefView<'db> { Whole, EnumVariant(VariantId<'db>), + /// The reference carries the containing storage word at runtime; this + /// compile-time view identifies the scalar lane within that word. + StorageLane(StorageBitLane), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Update)] +pub struct StorageBitLane { + pub bit_offset: u16, + pub bit_width: u16, } fn layouts_share_runtime_rep<'db>( diff --git a/crates/mir/src/runtime/lower/boundary.rs b/crates/mir/src/runtime/lower/boundary.rs index 9b70968b7d..53002f6043 100644 --- a/crates/mir/src/runtime/lower/boundary.rs +++ b/crates/mir/src/runtime/lower/boundary.rs @@ -208,13 +208,13 @@ impl<'db> BoundaryShapeMatcher<'db> { RuntimeClassShape::Ref { pointee: actual_pointee, kind: RefShapeKind::Provider(space), - view: RefView::Whole, + view: RefView::Whole | RefView::StorageLane(_), } => provider_spaces.contains(space) && **actual_pointee == *pointee, RuntimeClassShape::RawAddr { .. } => *allow_raw_addr, RuntimeClassShape::Scalar(_) | RuntimeClassShape::AggregateValue { .. } | RuntimeClassShape::Ref { - view: RefView::EnumVariant(_), + view: RefView::EnumVariant(_) | RefView::StorageLane(_), .. } => false, }, @@ -624,10 +624,10 @@ impl BoundaryMatcher { RuntimeClass::Ref { pointee: actual_pointee, kind: RefKind::Provider { space, .. }, - view: RefView::Whole, + view: RefView::Whole | RefView::StorageLane(_), } => allow.provider_spaces.contains(space) && **actual_pointee == *pointee, RuntimeClass::Ref { - view: RefView::EnumVariant(_), + view: RefView::EnumVariant(_) | RefView::StorageLane(_), .. } => false, RuntimeClass::RawAddr { .. } => allow.allow_raw_addr, @@ -946,6 +946,16 @@ mod tests { }) } + fn u8_class<'db>() -> RuntimeClass<'db> { + RuntimeClass::Scalar(ScalarClass { + repr: ScalarRepr::Int { + bits: 8, + signed: false, + }, + role: ScalarRole::Plain, + }) + } + fn raw_addr_class<'db>(space: AddressSpaceKind) -> RuntimeClass<'db> { RuntimeClass::RawAddr { space, @@ -1156,6 +1166,34 @@ mod tests { } } + #[test] + fn borrow_like_boundary_accepts_storage_lane_provider_views() { + let db = DriverDataBase::default(); + let pointee = u8_class(); + let boundary = RuntimeBoundarySpec::BorrowLike { + pointee: pointee.clone(), + access: BorrowAccess::ReadWrite, + allow: default_borrow_transport_set(BorrowAccess::ReadWrite, AddressSpaceKind::Storage), + }; + let class = ref_class( + pointee, + RefKind::Provider { + provider_ty: TyId::unit(&db), + space: AddressSpaceKind::Storage, + }, + RefView::StorageLane(crate::runtime::StorageBitLane { + bit_offset: 8, + bit_width: 8, + }), + ); + + assert!(BoundaryMatcher::class_satisfies_boundary(&class, &boundary)); + assert!( + BoundaryShapeMatcher::for_boundary(&boundary) + .matches_shape(&RuntimeClassShape::from_class(&class)) + ); + } + #[test] fn borrow_like_planner_prefers_compatible_address_then_scalar_slot_materialization() { let db = DriverDataBase::default(); diff --git a/crates/mir/src/runtime/lower/classify.rs b/crates/mir/src/runtime/lower/classify.rs index 04d9d7575a..4478db2901 100644 --- a/crates/mir/src/runtime/lower/classify.rs +++ b/crates/mir/src/runtime/lower/classify.rs @@ -35,12 +35,12 @@ use crate::{ instance::{RuntimeInstanceKey, RuntimeInstanceSource}, runtime::place::{ project_field_class, project_index_class, project_variant_field_class, - ref_class_for_place_result, + projected_field_ref_view, ref_class_for_place_result_with_view, transport_ref_view, }, runtime::{ - AddressSpaceKind, BorrowAccess, Layout, LayoutId, RuntimeBoundarySpec, RuntimeCarrier, - RuntimeClass, RuntimeCodeRegion, RuntimeCodeRegionKey, RuntimeParamPlan, SaturatingBinOp, - ScalarClass, ScalarRepr, ScalarRole, VariantId, + AddressSpaceKind, BorrowAccess, Layout, LayoutId, RefView, RuntimeBoundarySpec, + RuntimeCarrier, RuntimeClass, RuntimeCodeRegion, RuntimeCodeRegionKey, RuntimeParamPlan, + SaturatingBinOp, ScalarClass, ScalarRepr, ScalarRole, VariantId, }, }; @@ -659,64 +659,82 @@ impl<'a, 'db> BodyEnv<'a, 'db> { place: &NSPlace<'db>, ) -> Option> { let root = normalized_place_root_class_in_context(self, place.root.clone(), carriers)?; - Some(self.walk_place_path_classes(root, place).0) + Some(self.walk_place_path_classes(root, None, place).0) } - /// Projects `root` through the place's path, returning the final class and - /// the carrier class crossed by the last `Deref` (if any). + /// Projects `root` through the place's path, retaining the transport and + /// reference view established by the final carrier. fn walk_place_path_classes( self, root: RuntimeClass<'db>, + mut transport: Option>, place: &NSPlace<'db>, - ) -> (RuntimeClass<'db>, Option>) { + ) -> (RuntimeClass<'db>, Option>, RefView<'db>) { let mut current = root; let mut last_deref_carrier = None; + let mut view = transport + .as_ref() + .map(transport_ref_view) + .unwrap_or(RefView::Whole); for (idx, projection) in place.path.iter().enumerate() { - if matches!(projection, Projection::Deref) { - last_deref_carrier = Some(current.clone()); - } current = match projection { - Projection::Field(field) => project_field_class( - self.db, - current, - FieldIndex((*field).try_into().expect("field index fits")), - ), - Projection::Index(_) => project_index_class(self.db, current), - Projection::Deref => current - .deref_target() - .unwrap_or_else(|| panic!("invalid deref projection class")), + Projection::Field(field) => { + let field = FieldIndex((*field).try_into().expect("field index fits")); + view = transport.as_ref().map_or(RefView::Whole, |transport| { + projected_field_ref_view(self.db, transport, ¤t, field) + }); + project_field_class(self.db, current, field) + } + Projection::Index(_) => { + view = RefView::Whole; + project_index_class(self.db, current) + } + Projection::Deref => { + last_deref_carrier = Some(current.clone()); + transport = Some(current.clone()); + view = transport_ref_view(¤t); + current + .deref_target() + .unwrap_or_else(|| panic!("invalid deref projection class")) + } Projection::VariantField { variant, field_idx, .. - } => project_variant_field_place_class( - self.db, - current, - *variant, - FieldIndex((*field_idx).try_into().expect("field index fits")), - ), - Projection::Discriminant => match current { - RuntimeClass::Ref { pointee, .. } => match pointee.aggregate_layout() { - Some(layout) => match layout.data(self.db) { + } => { + view = RefView::Whole; + project_variant_field_place_class( + self.db, + current, + *variant, + FieldIndex((*field_idx).try_into().expect("field index fits")), + ) + } + Projection::Discriminant => { + view = RefView::Whole; + match current { + RuntimeClass::Ref { pointee, .. } => match pointee.aggregate_layout() { + Some(layout) => match layout.data(self.db) { + Layout::Enum(layout) => RuntimeClass::Scalar(layout.tag), + Layout::Struct(_) | Layout::Array(_) => { + panic!("invalid discriminant projection class") + } + }, + None => panic!("invalid discriminant projection class"), + }, + RuntimeClass::AggregateValue { layout } + | RuntimeClass::RawAddr { + target: Some(layout), + .. + } => match layout.data(self.db) { Layout::Enum(layout) => RuntimeClass::Scalar(layout.tag), Layout::Struct(_) | Layout::Array(_) => { panic!("invalid discriminant projection class") } }, - None => panic!("invalid discriminant projection class"), - }, - RuntimeClass::AggregateValue { layout } - | RuntimeClass::RawAddr { - target: Some(layout), - .. - } => match layout.data(self.db) { - Layout::Enum(layout) => RuntimeClass::Scalar(layout.tag), - Layout::Struct(_) | Layout::Array(_) => { + RuntimeClass::Scalar(_) | RuntimeClass::RawAddr { target: None, .. } => { panic!("invalid discriminant projection class") } - }, - RuntimeClass::Scalar(_) | RuntimeClass::RawAddr { target: None, .. } => { - panic!("invalid discriminant projection class") } - }, + } }; // Mirror `try_lower_place`: projecting onward through a // handle-classed element continues in the pointee, which re-roots @@ -725,10 +743,12 @@ impl<'a, 'db> BodyEnv<'a, 'db> { && let Some(target) = current.deref_target() { last_deref_carrier = Some(current.clone()); + transport = Some(current.clone()); + view = transport_ref_view(¤t); current = target; } } - (current, last_deref_carrier) + (current, last_deref_carrier, view) } pub(crate) fn normalized_place_address_class( @@ -736,9 +756,11 @@ impl<'a, 'db> BodyEnv<'a, 'db> { carriers: &[RuntimeCarrier<'db>], place: &NSPlace<'db>, ) -> Option> { - let value_class = self.normalized_place_class(carriers, place)?; let mut root_class = normalized_place_root_transport_class_in_context(self, place.root.clone(), carriers)?; + let root = normalized_place_root_class_in_context(self, place.root.clone(), carriers)?; + let (value_class, last_deref_carrier, view) = + self.walk_place_path_classes(root, Some(root_class.clone()), place); let (mut root_space, mut force_raw) = match place.root { NSPlaceRoot::CarrierDerefLocal(_) => (AddressSpaceKind::Memory, false), NSPlaceRoot::Root(root) => match self.body.root(root)? { @@ -754,19 +776,17 @@ impl<'a, 'db> BodyEnv<'a, 'db> { // carrier's transport (mirroring `resolve_runtime_place_address_class` // over lowered places), so a borrow through a handle-typed field keeps // the handle's transport rather than the place root's. - if let Some(root) = - normalized_place_root_class_in_context(self, place.root.clone(), carriers) - && let (_, Some(carrier)) = self.walk_place_path_classes(root, place) - { + if let Some(carrier) = last_deref_carrier { root_space = carrier.address_space().unwrap_or(root_space); force_raw = matches!(carrier, RuntimeClass::RawAddr { .. }); root_class = carrier; } - Some(ref_class_for_place_result( + Some(ref_class_for_place_result_with_view( &root_class, &value_class, root_space, force_raw, + view, )) } diff --git a/crates/mir/src/runtime/lower/conversion.rs b/crates/mir/src/runtime/lower/conversion.rs index d8c6ea5ad6..c78c6cdb64 100644 --- a/crates/mir/src/runtime/lower/conversion.rs +++ b/crates/mir/src/runtime/lower/conversion.rs @@ -606,6 +606,7 @@ impl<'db> RuntimeConversionPlanner<'db> { ( RuntimeClass::Ref { kind: RefKind::Provider { .. }, + view: RefView::Whole, .. }, RuntimeClass::RawAddr { .. }, @@ -695,6 +696,16 @@ mod tests { }) } + fn u8_class<'db>() -> RuntimeClass<'db> { + RuntimeClass::Scalar(ScalarClass { + repr: ScalarRepr::Int { + bits: 8, + signed: false, + }, + role: ScalarRole::Plain, + }) + } + fn test_struct_layout<'db>(db: &'db dyn MirDb) -> LayoutId<'db> { LayoutId::new( db, @@ -852,6 +863,39 @@ mod tests { )); } + #[test] + fn storage_lane_provider_loads_value_without_erasing_to_raw_address() { + let db = DriverDataBase::default(); + let value_class = u8_class(); + let source = RuntimeClass::Ref { + pointee: Box::new(value_class.clone()), + kind: RefKind::Provider { + provider_ty: TyId::unit(&db), + space: AddressSpaceKind::Storage, + }, + view: RefView::StorageLane(crate::runtime::StorageBitLane { + bit_offset: 8, + bit_width: 8, + }), + }; + + let load = + RuntimeConversionPlanner::plan(&db, source.clone(), value_class.clone()).unwrap(); + assert_eq!( + load.steps.as_ref(), + &[RuntimeConversionStep::LoadRef { class: value_class }] + ); + + let raw = RuntimeClass::RawAddr { + space: AddressSpaceKind::Storage, + target: None, + }; + assert!(matches!( + RuntimeConversionPlanner::plan(&db, source, raw), + Err(RuntimeConversionError::Unsupported { .. }) + )); + } + #[test] fn aggregate_to_object_ref_materializes_without_policy() { let db = DriverDataBase::default(); diff --git a/crates/mir/src/runtime/package.rs b/crates/mir/src/runtime/package.rs index 479a2181a9..c149f08a05 100644 --- a/crates/mir/src/runtime/package.rs +++ b/crates/mir/src/runtime/package.rs @@ -2205,6 +2205,9 @@ fn ref_view_sort_key<'db>(db: &'db dyn MirDb, view: &RefView<'db>) -> String { layout_sort_key(db, variant.enum_layout), variant.index ), + RefView::StorageLane(lane) => { + format!("storage_lane:{}:{}", lane.bit_offset, lane.bit_width) + } } } diff --git a/crates/mir/src/runtime/place.rs b/crates/mir/src/runtime/place.rs index baa02e4819..1f4f001340 100644 --- a/crates/mir/src/runtime/place.rs +++ b/crates/mir/src/runtime/place.rs @@ -17,7 +17,7 @@ use crate::{ AddressSpaceKind, ConstScalar, Layout, LayoutId, PlaceElem, PlaceRoot, RLocalId, RefKind, RefView, ResolvedPlaceElem, ResolvedPlaceRootKind, ResolvedRuntimePlace, RuntimeBody, RuntimeClass, RuntimeLocalRoot, RuntimeProgramView, RuntimeProviderBinding, - RuntimeProviderBindingId, ScalarClass, ScalarRepr, ScalarRole, VariantId, + RuntimeProviderBindingId, ScalarClass, ScalarRepr, ScalarRole, StorageBitLane, VariantId, }, verify::VerifyError, }; @@ -204,18 +204,37 @@ pub fn resolve_runtime_place_address_class<'db>( let resolved = resolve_runtime_place(db, program, body, place)?; let (mut root_class, mut root_space, mut force_raw) = runtime_place_transport_root(body, place)?; + let mut current = resolved_root_class(&resolved.root_kind).clone(); + let mut view = transport_ref_view(&root_class); for elem in resolved.path.iter() { - if let ResolvedPlaceElem::Deref { carrier_class, .. } = elem { - root_class = carrier_class.clone(); - root_space = root_class.address_space().unwrap_or(root_space); - force_raw = matches!(root_class, RuntimeClass::RawAddr { .. }); + match elem { + ResolvedPlaceElem::Field { field, class } => { + view = projected_field_ref_view(db, &root_class, ¤t, *field); + current = class.clone(); + } + ResolvedPlaceElem::Index { class, .. } + | ResolvedPlaceElem::VariantField { class, .. } => { + view = RefView::Whole; + current = class.clone(); + } + ResolvedPlaceElem::Deref { + carrier_class, + class, + } => { + root_class = carrier_class.clone(); + root_space = root_class.address_space().unwrap_or(root_space); + force_raw = matches!(root_class, RuntimeClass::RawAddr { .. }); + view = transport_ref_view(&root_class); + current = class.clone(); + } } } - Ok(ref_class_for_place_result( + Ok(ref_class_for_place_result_with_view( &root_class, &resolved.result_class, root_space, force_raw, + view, )) } @@ -236,6 +255,22 @@ pub(crate) fn ref_class_for_place_result<'db>( value_class: &RuntimeClass<'db>, root_space: AddressSpaceKind, force_raw: bool, +) -> RuntimeClass<'db> { + ref_class_for_place_result_with_view( + root_class, + value_class, + root_space, + force_raw, + RefView::Whole, + ) +} + +pub(crate) fn ref_class_for_place_result_with_view<'db>( + root_class: &RuntimeClass<'db>, + value_class: &RuntimeClass<'db>, + root_space: AddressSpaceKind, + force_raw: bool, + view: RefView<'db>, ) -> RuntimeClass<'db> { if !force_raw { match root_class { @@ -243,14 +278,14 @@ pub(crate) fn ref_class_for_place_result<'db>( return RuntimeClass::Ref { pointee: Box::new(value_class.clone()), kind: kind.clone(), - view: RefView::Whole, + view, }; } RuntimeClass::AggregateValue { .. } => { return RuntimeClass::Ref { pointee: Box::new(value_class.clone()), kind: RefKind::Object, - view: RefView::Whole, + view, }; } RuntimeClass::Scalar(_) | RuntimeClass::RawAddr { .. } => {} @@ -262,6 +297,61 @@ pub(crate) fn ref_class_for_place_result<'db>( } } +pub(crate) fn transport_ref_view<'db>(class: &RuntimeClass<'db>) -> RefView<'db> { + match class { + RuntimeClass::Ref { + view: RefView::StorageLane(lane), + .. + } => RefView::StorageLane(*lane), + RuntimeClass::Scalar(_) + | RuntimeClass::AggregateValue { .. } + | RuntimeClass::Ref { .. } + | RuntimeClass::RawAddr { .. } => RefView::Whole, + } +} + +pub(crate) fn projected_field_ref_view<'db>( + db: &'db dyn MirDb, + transport: &RuntimeClass<'db>, + base: &RuntimeClass<'db>, + field: FieldIndex, +) -> RefView<'db> { + if !matches!( + transport, + RuntimeClass::Ref { + kind: RefKind::Provider { + space: AddressSpaceKind::Storage | AddressSpaceKind::Transient, + .. + }, + .. + } + ) { + return RefView::Whole; + } + let Some(placement) = base.storage_field_placement(db, field) else { + return RefView::Whole; + }; + if !placement.requires_read_modify_write { + return RefView::Whole; + } + let lane = placement + .lane + .expect("read-modify-write storage field must have a bit lane"); + RefView::StorageLane(StorageBitLane { + bit_offset: lane.bit_offset, + bit_width: lane.bit_width, + }) +} + +fn resolved_root_class<'a, 'db>(root: &'a ResolvedPlaceRootKind<'db>) -> &'a RuntimeClass<'db> { + match root { + ResolvedPlaceRootKind::Slot { class, .. } + | ResolvedPlaceRootKind::Ref { class, .. } + | ResolvedPlaceRootKind::Provider { class, .. } + | ResolvedPlaceRootKind::Ptr { class, .. } => class, + } +} + fn runtime_place_transport_root<'db>( body: &impl PlaceClassEnv<'db>, place: &crate::runtime::RuntimePlace<'db>, diff --git a/crates/mir/src/runtime/pretty.rs b/crates/mir/src/runtime/pretty.rs index 8a3323c393..565ad5cbb2 100644 --- a/crates/mir/src/runtime/pretty.rs +++ b/crates/mir/src/runtime/pretty.rs @@ -1032,6 +1032,9 @@ fn format_ref_view<'db>(db: &'db dyn MirDb, view: &RefView<'db>) -> String { match view { RefView::Whole => "whole".to_string(), RefView::EnumVariant(variant) => format!("variant {}", format_variant(db, *variant)), + RefView::StorageLane(lane) => { + format!("storage_lane {}:{}", lane.bit_offset, lane.bit_width) + } } } diff --git a/crates/mir/src/verify/layout.rs b/crates/mir/src/verify/layout.rs index e077ec620f..22834bce57 100644 --- a/crates/mir/src/verify/layout.rs +++ b/crates/mir/src/verify/layout.rs @@ -2,7 +2,10 @@ use rustc_hash::FxHashSet; use crate::{ db::MirDb, - runtime::{Layout, LayoutId, RefView, RuntimeClass, RuntimeProgramView, ScalarRole}, + runtime::{ + AddressSpaceKind, Layout, LayoutId, RefKind, RefView, RuntimeClass, RuntimeProgramView, + ScalarRole, + }, verify::VerifyError, }; @@ -15,13 +18,31 @@ pub(super) fn verify_class_layouts<'db>( match class { RuntimeClass::Scalar(_) | RuntimeClass::RawAddr { .. } => Ok(()), RuntimeClass::AggregateValue { layout } => verify_layout(db, program, *layout, visited), - RuntimeClass::Ref { pointee, view, .. } => { - if !matches!(view, RefView::Whole | RefView::EnumVariant(_)) { - return Err(VerifyError::InvalidLayoutRefView( - pointee.aggregate_layout().unwrap_or_else(|| { - panic!("ref view should only appear on aggregate pointees: {pointee:?}") - }), - )); + RuntimeClass::Ref { + pointee, + kind, + view, + } => { + match view { + RefView::Whole => {} + RefView::EnumVariant(_) if pointee.aggregate_layout().is_some() => {} + RefView::StorageLane(lane) + if matches!( + kind, + RefKind::Provider { + space: AddressSpaceKind::Storage | AddressSpaceKind::Transient, + .. + } + ) && matches!( + pointee.as_ref(), + RuntimeClass::Scalar(scalar) + if scalar.storage_bit_width() == lane.bit_width + ) && lane.bit_width > 0 + && u32::from(lane.bit_offset) + u32::from(lane.bit_width) + <= u32::from(common::layout::WORD_SIZE_BITS) => {} + RefView::EnumVariant(_) | RefView::StorageLane(_) => { + return Err(VerifyError::InvalidPlace(class.clone())); + } } if let Some(layout) = pointee.aggregate_layout() { verify_layout(db, program, layout, visited)?; @@ -87,6 +108,10 @@ fn verify_stored_class<'db>( }), )); } + RuntimeClass::Ref { + view: RefView::StorageLane(_), + .. + } => return Err(VerifyError::InvalidPlace(class.clone())), RuntimeClass::Scalar(_) | RuntimeClass::AggregateValue { .. } | RuntimeClass::Ref { .. } diff --git a/crates/mir/src/verify/storage_layout.rs b/crates/mir/src/verify/storage_layout.rs index e760b9a232..64a41cd6c8 100644 --- a/crates/mir/src/verify/storage_layout.rs +++ b/crates/mir/src/verify/storage_layout.rs @@ -245,7 +245,7 @@ fn contract_field_ref_kind<'db>( RuntimeClass::Scalar(_) | RuntimeClass::AggregateValue { .. } | RuntimeClass::Ref { - view: RefView::EnumVariant(_), + view: RefView::EnumVariant(_) | RefView::StorageLane(_), .. } => None, } From 3aa569005fd018b96b69e0753b8d8901465e5fda Mon Sep 17 00:00:00 2001 From: Grant Wuerker Date: Thu, 30 Jul 2026 12:05:50 -0600 Subject: [PATCH 7/8] storage: preserve scalar map word encoding --- crates/codegen/src/sonatina/lower_runtime.rs | 4 +- .../storage_map_scalar_pointer_encoding.fe | 84 +++++++++++++++++++ .../storage_packed_aggregate_boundaries.fe | 2 +- 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 crates/fe/tests/fixtures/fe_test/storage_map_scalar_pointer_encoding.fe diff --git a/crates/codegen/src/sonatina/lower_runtime.rs b/crates/codegen/src/sonatina/lower_runtime.rs index 0015d541f2..34483c5378 100644 --- a/crates/codegen/src/sonatina/lower_runtime.rs +++ b/crates/codegen/src/sonatina/lower_runtime.rs @@ -4407,11 +4407,13 @@ impl<'ctx, 'db, 'a> FunctionLowerer<'ctx, 'db, 'a> { scalar.is_signed_int(), )?; let storage_bits = scalar.storage_bit_width(); + // Same-width signed scalars keep their whole-word sign extension. + // Packed fields are masked separately by `store_packed_lane`. if matches!( space, AddressSpaceKind::Storage | AddressSpaceKind::Transient ) && storage_bits < 256 - && (scalar.is_signed_int() || src_bits > storage_bits) + && src_bits > storage_bits { self.mask_word_bits(value, storage_bits)? } else { diff --git a/crates/fe/tests/fixtures/fe_test/storage_map_scalar_pointer_encoding.fe b/crates/fe/tests/fixtures/fe_test/storage_map_scalar_pointer_encoding.fe new file mode 100644 index 0000000000..762277077c --- /dev/null +++ b/crates/fe/tests/fixtures/fe_test/storage_map_scalar_pointer_encoding.fe @@ -0,0 +1,84 @@ +use core::EffectHandle +use std::evm::Evm + +msg ScalarMapMsg { + #[selector = 1] + Set { value: i8 } -> u256, + + #[selector = 2] + WritePointer { value: i8 } -> u256, + + #[selector = 3] + ReadBoth { expected: i8 } -> u256, +} + +struct Store { + values: StorageMap, +} + +pub contract C { + mut store: Store + + init() uses (mut store) {} + + recv ScalarMapMsg { + Set { value } -> u256 uses (mut store, evm: Evm) { + with (mut store.values) { + store.values.set(key: 1, value: value) + } + let ptr = store.values.ptr(key: 1) + evm.sload(ptr.raw()) + } + + WritePointer { value } -> u256 uses (mut store, evm: Evm) { + let ptr = with (mut store.values) { + store.values.mut_ptr(key: 1) + } + ptr.write(value) + evm.sload(ptr.raw()) + } + + ReadBoth { expected } -> u256 uses (store) { + let from_get = store.values.get(key: 1) + let from_ptr = store.values.ptr(key: 1).read() + if from_get == expected && from_ptr == expected { 1 } else { 0 } + } + } +} + +#[test] +fn test_storage_map_scalar_pointer_encoding() uses (evm: mut Evm) { + let c = evm.create2(value: 0, args: (), salt: 0) + + let from_set: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: ScalarMapMsg::Set { value: -1 }, + ) + assert!(from_set == u256::max()) + + let reads_set_value: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: ScalarMapMsg::ReadBoth { expected: -1 }, + ) + assert!(reads_set_value == 1) + + let from_pointer: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: ScalarMapMsg::WritePointer { value: -1 }, + ) + assert!(from_pointer == from_set) + + let reads_pointer_value: u256 = evm.call( + addr: c, + gas: 1000000, + value: 0, + message: ScalarMapMsg::ReadBoth { expected: -1 }, + ) + assert!(reads_pointer_value == 1) +} diff --git a/crates/fe/tests/fixtures/fe_test/storage_packed_aggregate_boundaries.fe b/crates/fe/tests/fixtures/fe_test/storage_packed_aggregate_boundaries.fe index 337bf2bdf2..8b632dd3ce 100644 --- a/crates/fe/tests/fixtures/fe_test/storage_packed_aggregate_boundaries.fe +++ b/crates/fe/tests/fixtures/fe_test/storage_packed_aggregate_boundaries.fe @@ -105,7 +105,7 @@ fn test_storage_packed_aggregate_boundaries() uses (evm: mut Evm) { (1, 0xcc), (2, 0x11), (3, 0x2233), - (4, 0xff), + (4, u256::max()), (5, 0x44), (6, 0), (7, 0x6655), From 168787afaeae56757ac147998bedbaa60871ae52 Mon Sep 17 00:00:00 2001 From: Grant Wuerker Date: Thu, 30 Jul 2026 15:49:59 -0600 Subject: [PATCH 8/8] mir: preserve effect transport for one-word targets --- .../fe_test/runtime_handle_preservation.fe | 19 ++++++++++++++++++- crates/mir/src/runtime/lower/arg_selector.rs | 8 ++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/crates/fe/tests/fixtures/fe_test/runtime_handle_preservation.fe b/crates/fe/tests/fixtures/fe_test/runtime_handle_preservation.fe index 4917bd13a4..a356d0c6b3 100644 --- a/crates/fe/tests/fixtures/fe_test/runtime_handle_preservation.fe +++ b/crates/fe/tests/fixtures/fe_test/runtime_handle_preservation.fe @@ -1,6 +1,6 @@ use core::EffectHandle use core::contracts::ContractHost -use std::evm::{Evm, MemPtr, RawMem, RawStorage, StorPtr} +use std::evm::{Evm, MemPtr, RawMem, RawStorage, StorageMutPtr, StorPtr} struct Empty {} @@ -39,6 +39,12 @@ fn check_runtime_value(_ evm: mut Evm, value: u256) -> u256 { value } +struct OneWord { + value: u256, +} + +impl Copy for OneWord {} + #[test] fn test_zst_target_effect_handles_keep_runtime_carriers() uses (evm: Evm) { let mp: MemPtr = evm.mem_ptr(0x120) @@ -65,6 +71,17 @@ fn effect_handle_read_write_helpers_delegate_to_the_free_functions() uses (evm: assert!(sp.read() == 42) } +#[test] +fn one_word_aggregate_effect_handles_keep_provider_transport() uses (evm: Evm) { + let sp: StorPtr = evm.stor_ptr(0xabd) + sp.write(OneWord { value: 43 }) + assert!(sp.read().value == 43) + + let map_ptr: StorageMutPtr = StorageMutPtr::from_raw(0xabe) + map_ptr.write(OneWord { value: 44 }) + assert!(map_ptr.read().value == 44) +} + #[test] fn test_explicit_root_provider_params_keep_runtime_carriers() uses (evm: mut Evm) { let generic_len = host_input_len(mut evm) diff --git a/crates/mir/src/runtime/lower/arg_selector.rs b/crates/mir/src/runtime/lower/arg_selector.rs index 9d221bd875..800703f731 100644 --- a/crates/mir/src/runtime/lower/arg_selector.rs +++ b/crates/mir/src/runtime/lower/arg_selector.rs @@ -748,6 +748,14 @@ impl<'a, 'carriers, 'roots, 'cache, 'db> RuntimeArgSelector<'a, 'carriers, 'root CompiledEffectValuePlan::ErasedPlainValue => { self.select_materialized_operand_value(value.local, value) } + CompiledEffectValuePlan::ByValue( + plan @ CompiledValuePassPlan::BorrowLike(boundary), + ) => { + // A handle and a one-word aggregate target can have the same + // shape, but effect values must retain the handle transport. + self.select_effect_handle_operand_for_boundary(value, boundary) + .or_else(|| self.selected_value_pass_plan(value, plan)) + } CompiledEffectValuePlan::ByValue(plan) => self.selected_value_pass_plan(value, plan), CompiledEffectValuePlan::ByValueFallback(fallback) => self .try_selected_semantic_operand_for_class(value, fallback)