Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6fa4d10
Mark ABI decode/encode helpers as always-inline
micahscopes May 14, 2026
ca0e792
mir(contracts): binary-search runtime selector dispatch
May 5, 2026
a9c242f
mir(contracts): make dispatch strategy configurable
micahscopes May 14, 2026
b3ec2dd
Update ERC20 snapshot for auto binary-search dispatch
micahscopes May 14, 2026
2d3bb37
mir(runtime): type-aware recv wrapper codegen
micahscopes May 14, 2026
9ae5591
Fix direct calldataload/return specialization for recv wrappers
micahscopes May 14, 2026
f6885c3
mir(runtime): query const trait values via CTFE for recv specialization
micahscopes May 15, 2026
d07ea3d
Add ArrayView<T, N, I> for zero-copy array decoding
micahscopes May 14, 2026
f37ffdc
Add SkippedArray<T, N> for zero-copy ABI array decoding
micahscopes May 15, 2026
62688c7
Add SkippedBytes and SkippedString for zero-copy dynamic field decoding
micahscopes May 15, 2026
a0a25c1
implement lazy calldata load for mixed recv parameters
micahscopes May 15, 2026
b597213
prototype const predicates in where clauses
micahscopes May 15, 2026
6df4e17
Unify View types with default type parameters
micahscopes May 15, 2026
aee38ee
Add BytesView<()> msg field test: zero-copy bytes decoding validated
micahscopes May 15, 2026
fd6159c
Auto lazy decode for BytesView and StringView msg fields
micahscopes May 15, 2026
ea17829
Implement #[abi] attribute for auto-generated ABI trait impls on structs
micahscopes May 15, 2026
b907b41
Move len/is_empty to unconstrained impl for BytesView
micahscopes May 15, 2026
f2d7107
Accept snapshot updates from View type and #[abi] struct additions
micahscopes May 15, 2026
9858ce6
Add #[abi] struct end-to-end test: G1Point ABI roundtrip validated
micahscopes May 15, 2026
cfe9dd6
Fix StringView<()>::over() to construct BytesView via from_parts
micahscopes May 15, 2026
69d5102
Document known issue: method generics on default-type-param structs
micahscopes May 15, 2026
5bf1609
Fix method generic clobbered by struct default type param
micahscopes May 15, 2026
fae7880
Style cleanup: import abi_struct, remove em-dashes
micahscopes May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

893 changes: 68 additions & 825 deletions crates/codegen/tests/fixtures/sonatina_ir/create_contract.snap

Large diffs are not rendered by default.

1,542 changes: 56 additions & 1,486 deletions crates/codegen/tests/fixtures/sonatina_ir/effect_handle_field_deref.snap

Large diffs are not rendered by default.

1,910 changes: 670 additions & 1,240 deletions crates/codegen/tests/fixtures/sonatina_ir/erc20.snap

Large diffs are not rendered by default.

752 changes: 35 additions & 717 deletions crates/codegen/tests/fixtures/sonatina_ir/high_level_contract.snap

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions crates/common/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ pub enum DiagnosticPass {
MsgLower,
EventLower,
ErrorLower,
AbiStructLower,
ArithmeticAttr,
PayableAttr,

Expand Down Expand Up @@ -215,6 +216,7 @@ impl DiagnosticPass {
Self::MsgLower => 9,
Self::EventLower => 10,
Self::ErrorLower => 16,
Self::AbiStructLower => 17,
Self::ArithmeticAttr => 12,
Self::PayableAttr => 13,
Self::InlineAttr => 14,
Expand Down
89 changes: 89 additions & 0 deletions crates/fe/src/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,20 @@ fn semantic_ty_to_abi_desc(db: &DriverDataBase, ty: TyId<'_>) -> Result<AbiTypeD
return Ok(AbiTypeDesc::simple("bytes"));
}

if let Some((elem_ty, len_ty)) = core_unbound_array_view_parts(db, ty) {
let elem_desc = semantic_ty_to_abi_desc(db, elem_ty)?;
let len = array_len_to_string(db, len_ty)?;
return Ok(elem_desc.array(&len));
}

if is_core_unbound_bytes_view_ty(db, ty) {
return Ok(AbiTypeDesc::simple("bytes"));
}

if is_core_unbound_string_view_ty(db, ty) {
return Ok(AbiTypeDesc::simple("string"));
}

if ty.is_string(db) {
return Ok(AbiTypeDesc::simple("string"));
}
Expand Down Expand Up @@ -806,6 +820,37 @@ fn core_dyn_array_elem_ty<'db>(db: &'db DriverDataBase, ty: TyId<'db>) -> Option
args.first().copied()
}

/// Recognise an unbound `core::abi::ArrayView<T, N>` (I defaulted to `()`)
/// and return `(elem_ty, len_ty)`.
fn core_unbound_array_view_parts<'db>(
db: &'db DriverDataBase,
ty: TyId<'db>,
) -> Option<(TyId<'db>, TyId<'db>)> {
if let Some((_, inner)) = ty.as_capability(db) {
return core_unbound_array_view_parts(db, inner);
}

let (base, args) = ty.decompose_ty_app(db);
let TyData::TyBase(TyBase::Adt(adt)) = base.data(db) else {
return None;
};
let adt_ref = adt.adt_ref(db);
let name = adt_ref.name(db)?;
if name.data(db) != "ArrayView" {
return None;
}
if !base
.ingot(db)
.is_some_and(|ingot| ingot.kind(db) == IngotKind::Core)
{
return None;
}

let elem_ty = args.first().copied()?;
let len_ty = args.get(1).copied()?;
Some((elem_ty, len_ty))
}

fn is_core_bytes_ty(db: &DriverDataBase, ty: TyId<'_>) -> bool {
if let Some((_, inner)) = ty.as_capability(db) {
return is_core_bytes_ty(db, inner);
Expand Down Expand Up @@ -848,6 +893,50 @@ fn is_core_dyn_string_ty(db: &DriverDataBase, ty: TyId<'_>) -> bool {
.is_some_and(|ingot| ingot.kind(db) == IngotKind::Core)
}

/// Recognise an unbound `core::abi::BytesView` (I defaulted to `()`).
fn is_core_unbound_bytes_view_ty(db: &DriverDataBase, ty: TyId<'_>) -> bool {
if let Some((_, inner)) = ty.as_capability(db) {
return is_core_unbound_bytes_view_ty(db, inner);
}

let base = ty.base_ty(db);
let TyData::TyBase(TyBase::Adt(adt)) = base.data(db) else {
return false;
};
let adt_ref = adt.adt_ref(db);
let Some(name) = adt_ref.name(db) else {
return false;
};
if name.data(db) != "BytesView" {
return false;
}

base.ingot(db)
.is_some_and(|ingot| ingot.kind(db) == IngotKind::Core)
}

/// Recognise an unbound `core::abi::StringView` (I defaulted to `()`).
fn is_core_unbound_string_view_ty(db: &DriverDataBase, ty: TyId<'_>) -> bool {
if let Some((_, inner)) = ty.as_capability(db) {
return is_core_unbound_string_view_ty(db, inner);
}

let base = ty.base_ty(db);
let TyData::TyBase(TyBase::Adt(adt)) = base.data(db) else {
return false;
};
let adt_ref = adt.adt_ref(db);
let Some(name) = adt_ref.name(db) else {
return false;
};
if name.data(db) != "StringView" {
return false;
}

base.ingot(db)
.is_some_and(|ingot| ingot.kind(db) == IngotKind::Core)
}

/// Recognise `std::abi::sol` SolCompat wrapper types like `Uint160` / `Int24`
/// and return their Solidity ABI type string (e.g. `"uint160"`, `"int24"`).
fn std_sol_compat_abi_type(
Expand Down
94 changes: 94 additions & 0 deletions crates/fe/tests/fixtures/fe_test/abi_dynamic_payload_view.fe
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use std::abi::DynString
use std::evm::{Evm, encode_abi_payload, mem}
use core::abi::BytesView
use std::evm::CallData

/// Test that BytesView (unbound) works as a msg field type for zero-copy
/// bytes decoding. The handler receives BytesView<()> and binds it to
/// CallData for zero-copy access.
///
/// Uses the same ABI encoding as Bytes, but the receiver decodes without
/// copying the bytes payload to memory.

msg DynamicPayloadViewMsg {
#[selector = 0x02]
Check { first: BytesView, marker: u256, second: BytesView } -> u256,
}

pub contract DynamicPayloadViewReceiver {
recv DynamicPayloadViewMsg {
Check { first, marker, second } -> u256 {
assert(marker == 0xabcdef)
assert(first.len == 33)
assert(second.len == 1)
first.len + second.len
}
}
}

/// Test the contract by sending raw ABI-encoded calldata that matches
/// the Bytes encoding (since BytesView uses the same wire format).
/// We reuse the DynamicPayloadReceiver from the existing test to
/// produce the calldata, then call our contract with it.

msg OriginalMsg {
#[selector = 0x02]
Check { first: Bytes, marker: u256, second: Bytes } -> u256,
}

fn two_word_bytes() -> Bytes
uses (evm: mut Evm)
{
let payload_len: u256 = 33
let payload_size: u256 = 96
let data_ptr = mem::alloc(payload_size)

evm.mstore(addr: data_ptr, value: payload_len)
evm.mstore(
addr: data_ptr + 32,
value: 0x1111111111111111111111111111111111111111111111111111111111111111,
)
evm.mstore(
addr: data_ptr + 64,
value: 0x2200000000000000000000000000000000000000000000000000000000000000,
)

Bytes { data: data_ptr, len: payload_len, size: payload_size }
}

fn one_byte_bytes() -> Bytes
uses (evm: mut Evm)
{
let payload_len: u256 = 1
let payload_size: u256 = 64
let data_ptr = mem::alloc(payload_size)

evm.mstore(addr: data_ptr, value: payload_len)
evm.mstore(
addr: data_ptr + 32,
value: 0x3300000000000000000000000000000000000000000000000000000000000000,
)

Bytes { data: data_ptr, len: payload_len, size: payload_size }
}

#[test]
fn test_msg_dynamic_payload_view_decoding() uses (evm: mut Evm) {
let receiver = evm.create2<DynamicPayloadViewReceiver>(value: 0, args: (), salt: 0)
assert(receiver.inner != 0)

// Send using OriginalMsg (which uses Bytes, encodable) to the receiver
// that decodes as BytesView. Same ABI wire format.
let result: u256 = evm.call(
addr: receiver,
gas: 1000000,
value: 0,
message: OriginalMsg::Check {
first: two_word_bytes(),
marker: 0xabcdef,
second: one_byte_bytes(),
},
)

assert(result == 34)
}
41 changes: 41 additions & 0 deletions crates/fe/tests/fixtures/fe_test/abi_struct_g1point.fe
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use std::abi::sol
use std::evm::Evm

#[abi]
pub struct G1Point {
pub x: u256,
pub y: u256,
}

msg PointMsg {
#[selector = sol("add((uint256,uint256),(uint256,uint256))")]
Add { a: G1Point, b: G1Point } -> (u256, u256),
}

pub contract PointAdder {
init() {}
recv PointMsg {
Add { a, b } -> (u256, u256) {
(a.x + b.x, a.y + b.y)
}
}
}

#[test]
fn test_abi_struct_roundtrip() uses (evm: mut Evm) {
let c = evm.create2<PointAdder>(value: 0, args: (), salt: 0)
assert(c.inner != 0)

let (rx, ry): (u256, u256) = evm.call(
addr: c,
gas: 1000000,
value: 0,
message: PointMsg::Add {
a: G1Point { x: 10, y: 20 },
b: G1Point { x: 30, y: 40 },
},
)

assert(rx == 40)
assert(ry == 60)
}
20 changes: 18 additions & 2 deletions crates/hir/src/analysis/analysis_pass.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::analysis::{HirAnalysisDb, diagnostics::DiagnosticVoucher};
use crate::{
ArithmeticAttrError, ErrorDiagnostic, EventError, InlineAttrError, LoopUnrollAttrError,
ParserError, PayableError, SelectorError,
AbiStructError, ArithmeticAttrError, ErrorDiagnostic, EventError, InlineAttrError,
LoopUnrollAttrError, ParserError, PayableError, SelectorError,
hir_def::{ModuleTree, TopLevelMod},
lower::{parse_file_impl, scope_graph_impl},
};
Expand Down Expand Up @@ -131,6 +131,22 @@ impl ModuleAnalysisPass for ErrorLowerPass {
}
}

/// Analysis pass that collects abi struct lowering diagnostics from `#[abi]` struct desugaring.
pub struct AbiStructLowerPass {}

impl ModuleAnalysisPass for AbiStructLowerPass {
fn run_on_module<'db>(
&mut self,
db: &'db dyn HirAnalysisDb,
top_mod: TopLevelMod<'db>,
) -> Vec<Box<dyn DiagnosticVoucher>> {
scope_graph_impl::accumulated::<AbiStructError>(db, top_mod)
.into_iter()
.map(|d| Box::new(d.clone()) as _)
.collect::<Vec<_>>()
}
}

/// Analysis pass that collects arithmetic attribute validation errors.
pub struct ArithmeticAttrPass {}

Expand Down
55 changes: 55 additions & 0 deletions crates/hir/src/analysis/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,49 @@ impl DiagnosticVoucher for crate::ErrorDiagnostic {
}
}

impl DiagnosticVoucher for crate::AbiStructError {
fn to_complete(&self, _db: &dyn SpannedHirAnalysisDb) -> CompleteDiagnostic {
use crate::AbiStructErrorKind;

let primary_span = Span::new(self.file, self.primary_range, SpanKind::Original);

let (code, message, label, notes) = match &self.kind {
AbiStructErrorKind::AbiAttrOnNonStruct { item_kind } => (
1,
format!("`#[abi]` is only valid on structs (found on {item_kind})"),
"`#[abi]` must be placed on a `struct` item".to_string(),
vec!["move `#[abi]` to a struct declaration".to_string()],
),
AbiStructErrorKind::InvalidAbiAttrForm => (
2,
"invalid `#[abi]` attribute form".to_string(),
"expected `#[abi]` without arguments".to_string(),
vec!["remove arguments and use `#[abi]`".to_string()],
),
AbiStructErrorKind::GenericAbiStruct => (
3,
"`#[abi]` structs must be non-generic".to_string(),
"generics are not supported on `#[abi]` structs".to_string(),
vec!["remove generic parameters from the abi struct".to_string()],
),
};

let error_code = GlobalErrorCode::new(DiagnosticPass::AbiStructLower, code);

CompleteDiagnostic::new(
Severity::Error,
message,
vec![SubDiagnostic::new(
LabelStyle::Primary,
label,
Some(primary_span),
)],
notes,
error_code,
)
}
}

impl DiagnosticVoucher for crate::InlineAttrError {
fn to_complete(&self, _db: &dyn SpannedHirAnalysisDb) -> CompleteDiagnostic {
use crate::hir_def::InlineAttrErrorKind;
Expand Down Expand Up @@ -3045,6 +3088,18 @@ impl DiagnosticVoucher for BodyDiag<'_> {
}
}

Self::WhereConstPredicateFailed { primary } => CompleteDiagnostic {
severity,
message: "where clause const predicate failed".to_string(),
sub_diagnostics: vec![SubDiagnostic {
style: LabelStyle::Primary,
message: "const predicate evaluated to `false`".to_string(),
span: primary.resolve(db),
}],
notes: Vec::new(),
error_code,
},

Self::InvalidCast {
primary,
from,
Expand Down
5 changes: 3 additions & 2 deletions crates/hir/src/analysis/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ pub mod place;
pub mod semantic;

use self::analysis_pass::{
AnalysisPassManager, ArithmeticAttrPass, ErrorLowerPass, EventLowerPass, InlineAttrPass,
LoopUnrollAttrPass, MsgLowerPass, ParsingPass, PayableAttrPass,
AbiStructLowerPass, AnalysisPassManager, ArithmeticAttrPass, ErrorLowerPass, EventLowerPass,
InlineAttrPass, LoopUnrollAttrPass, MsgLowerPass, ParsingPass, PayableAttrPass,
};
use self::name_resolution::ImportAnalysisPass;
use self::ty::{
Expand All @@ -33,6 +33,7 @@ pub fn initialize_analysis_pass() -> AnalysisPassManager {
pass_manager.add_module_pass("MsgLower", Box::new(MsgLowerPass {}));
pass_manager.add_module_pass("EventLower", Box::new(EventLowerPass {}));
pass_manager.add_module_pass("ErrorLower", Box::new(ErrorLowerPass {}));
pass_manager.add_module_pass("AbiStructLower", Box::new(AbiStructLowerPass {}));
pass_manager.add_module_pass("InlineAttr", Box::new(InlineAttrPass {}));
pass_manager.add_module_pass("LoopUnrollAttr", Box::new(LoopUnrollAttrPass {}));
pass_manager.add_module_pass("MsgSelector", Box::new(MsgSelectorAnalysisPass {}));
Expand Down
Loading