Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Regression: a `mut own` aggregate parameter is passed by value (a direct
// aggregate carrier fixed by the calling convention), but its body both
// projects into it and reassigns it whole, which demands projectable owned
// storage. Carrier inference used to "upgrade" the parameter local to an
// object-ref carrier to satisfy that demand, contradicting the signature's
// by-value param class and tripping the runtime verifier with
// `SlotCarrierMismatch`. The owned storage must instead come from a slot root
// while the parameter carrier stays the signature aggregate.

struct Pt {
x: u256,
y: u256,
}

// Array: projected read, whole reassignment, and returned.
pub fn permute(mut _ s: own [u256; 3]) -> [u256; 3] {
let x: u256 = s[0]
s = [x, s[1], s[2]]
s
}

// Struct: field read, whole reassignment, and returned.
pub fn bump(mut _ p: own Pt) -> Pt {
let old_x: u256 = p.x
p = Pt { x: old_x + 1, y: p.y }
p
}

// Index mutation only (no whole reassignment) still needs owned storage.
pub fn set_first(mut _ s: own [u256; 3], _ v: u256) -> [u256; 3] {
s[0] = v
s
}

// Nested aggregate owned parameter.
pub fn rotate(mut _ s: own [[u256; 2]; 2]) -> [[u256; 2]; 2] {
let head: [u256; 2] = s[0]
s = [s[1], head]
s
}

#[test]
fn test_permute() {
let s: [u256; 3] = [1, 2, 3]
let out: [u256; 3] = permute(s)
assert(out[0] == 1)
assert(out[1] == 2)
assert(out[2] == 3)
}

#[test]
fn test_bump() {
let p: Pt = Pt { x: 10, y: 20 }
let q: Pt = bump(p)
assert(q.x == 11)
assert(q.y == 20)
}

#[test]
fn test_set_first() {
let s: [u256; 3] = [1, 2, 3]
let out: [u256; 3] = set_first(s, 99)
assert(out[0] == 99)
assert(out[1] == 2)
assert(out[2] == 3)
}

#[test]
fn test_rotate() {
let s: [[u256; 2]; 2] = [[1, 2], [3, 4]]
let out: [[u256; 2]; 2] = rotate(s)
assert(out[0][0] == 3)
assert(out[0][1] == 4)
assert(out[1][0] == 1)
assert(out[1][1] == 2)
}
70 changes: 70 additions & 0 deletions crates/mir/src/runtime/lower/classify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3003,6 +3003,76 @@ mod tests {
);
}

#[test]
fn runtime_handle_preservation_keeps_mut_owned_aggregate_param_by_value() {
let mut db = DriverDataBase::default();
let file_url = Url::parse(
"file:///runtime_handle_preservation_keeps_mut_owned_aggregate_param_by_value.fe",
)
.unwrap();
db.workspace().touch(
&mut db,
file_url.clone(),
Some(
include_str!(
"../../../../fe/tests/fixtures/fe_test/mut_owned_aggregate_param_slot_carrier.fe"
)
.to_string(),
),
);
let file = db
.workspace()
.get(&db, &file_url)
.expect("file should be loaded");
let top_mod = db.top_mod(file);

// `permute` takes `mut _ s: own [u256; 3]`, projects it (`s[0]`), reassigns it whole
// (`s = [..]`), and returns it. That demands projectable owned storage, which must be
// supplied by a slot root rather than by widening the by-value parameter into an
// object reference. A "fix" that widened the ABI would keep the behavioral fe_test
// fixture green while silently changing the calling convention; these assertions fail
// loudly instead.
let semantic = semantic_instance_for_named_func(&db, top_mod, "permute");
let instance = runtime_instance_for_semantic(&db, semantic);
let signature = instance.interface_signature(&db);
let body = instance.body(&db);

let param = signature
.params
.first()
.expect("permute has one runtime-visible parameter");

// The interface contract stays a by-value aggregate: callers pass `[u256; 3]`
// directly, never an object handle.
assert!(
matches!(param.class, RuntimeClass::AggregateValue { .. }),
"mut-owned aggregate param must keep a by-value aggregate ABI class, got:\n{:#?}",
param.class,
);

// The parameter local's carrier is the calling-convention handle; it must stay
// exactly equal to the signature class (the equality the runtime verifier enforces,
// whose failure surfaces as SlotCarrierMismatch).
let carrier = body
.value_class(param.local)
.unwrap_or_else(|| panic!("param local {:?} should keep a value carrier", param.local));
assert_eq!(
carrier, &param.class,
"param carrier must equal the signature param class, not be upgraded to an object ref",
);

// Owned storage for the mutation/reassignment comes from a slot root over the same
// aggregate, confirming the demand is met without touching the carrier.
let root = &body.local(param.local).expect("param local exists").root;
assert!(
matches!(
root,
RuntimeLocalRoot::Slot(RuntimeClass::AggregateValue { .. })
),
"projectable owned storage must come from an aggregate slot root, got:\n{root:#?}",
);
}

#[test]
fn transport_shaped_returns_remain_visible_transport_returns() {
let mut db = DriverDataBase::default();
Expand Down
12 changes: 12 additions & 0 deletions crates/mir/src/runtime/lower/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ pub(super) struct InferenceResult<'db> {
pub(super) struct LocalStateInferer<'a, 'db> {
env: BodyEnv<'a, 'db>,
carriers: Vec<RuntimeCarrier<'db>>,
/// Locals whose carrier is fixed by the interface signature (the runtime-visible
/// parameters). Their carrier is the calling convention: the verifier requires it to
/// stay exactly equal to the signature param class, so inference must never refine it
/// (e.g. upgrading an owned aggregate param to an object ref for internal mutable
/// storage — that is satisfied by a [`RuntimeLocalRoot::Slot`] instead).
signature_pinned: Vec<bool>,
class_cache: InferClassCache<'db>,
pending_dependents: Vec<AssignmentId>,
}
Expand All @@ -65,12 +71,15 @@ impl<'a, 'db> LocalStateInferer<'a, 'db> {
param_locals: &[SLocalId],
) -> Self {
let mut carriers = vec![RuntimeCarrier::Erased; env.body().locals.len()];
let mut signature_pinned = vec![false; env.body().locals.len()];
for (class, local) in params.iter().zip(param_locals.iter().copied()) {
carriers[local.index()] = RuntimeCarrier::Value(class.clone());
signature_pinned[local.index()] = true;
}
Self {
env,
carriers,
signature_pinned,
class_cache: InferClassCache::new(env.body().locals.len()),
pending_dependents: Vec::new(),
}
Expand Down Expand Up @@ -112,6 +121,9 @@ impl<'a, 'db> LocalStateInferer<'a, 'db> {
}

fn set_carrier(&mut self, local: SLocalId, desired: RuntimeCarrier<'db>) -> bool {
if self.signature_pinned[local.index()] {
return false;
}
let current = self
.carriers
.get(local.index())
Expand Down
10 changes: 10 additions & 0 deletions crates/mir/src/runtime/lower/returns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,10 @@ struct ReturnSliceInferer<'summary, 'lookup, 'db> {
db: &'db dyn MirDb,
summary: &'summary RuntimeReturnSummary<'db>,
carriers: Vec<RuntimeCarrier<'db>>,
/// Locals whose carrier is fixed by the interface signature (the runtime-visible
/// parameters); mirrors [`LocalStateInferer`] so both solvers agree on param carriers
/// when a parameter is itself a returned value. See its `signature_pinned` field.
signature_pinned: Vec<bool>,
class_cache: InferClassCache<'db>,
pending_dependents: Vec<SliceAssignmentId>,
lookup: &'lookup mut dyn FnMut(RuntimeInstanceKey<'db>) -> Option<RuntimeClass<'db>>,
Expand All @@ -323,13 +327,16 @@ impl<'summary, 'lookup, 'db> ReturnSliceInferer<'summary, 'lookup, 'db> {
lookup: &'lookup mut dyn FnMut(RuntimeInstanceKey<'db>) -> Option<RuntimeClass<'db>>,
) -> Self {
let mut carriers = vec![RuntimeCarrier::Erased; summary.semantic_body.locals.len()];
let mut signature_pinned = vec![false; summary.semantic_body.locals.len()];
for (class, local) in params.iter().zip(summary.param_locals.iter().copied()) {
carriers[local.index()] = RuntimeCarrier::Value(class.clone());
signature_pinned[local.index()] = true;
}
Self {
db,
summary,
carriers,
signature_pinned,
class_cache: InferClassCache::new(summary.semantic_body.locals.len()),
pending_dependents: Vec::new(),
lookup,
Expand All @@ -343,6 +350,9 @@ impl<'summary, 'lookup, 'db> ReturnSliceInferer<'summary, 'lookup, 'db> {
}

fn set_carrier(&mut self, local: SLocalId, desired: RuntimeCarrier<'db>) -> bool {
if self.signature_pinned[local.index()] {
return false;
}
let current = self
.carriers
.get(local.index())
Expand Down
Loading