From 39487f1df8aff9fc947c573a36300de707efc2b7 Mon Sep 17 00:00:00 2001 From: jasisz Date: Fri, 25 Sep 2026 03:32:34 +0200 Subject: [PATCH 1/2] Move record fields out at the record's last use in generated Rust A field of a record local was always cloned, even when nothing read that part of the record again. Two costs followed: - f(s.window.created, s.window.spent) at the last use of s left a second reference to each Map in s, so every in-place insert inside f copied the whole Map. - T.update(s, window = v) at the last use of s was emitted as T { window: v, ..s }, which leaves the old window in the partially moved s until the function returns. A new MIR analysis (field_moves) names the field reads that may move: every other read of the same local either runs in another branch, finished in an earlier let, or reads a disjoint part of the record, and one of them is the local's last use. The Rust backend moves such a read when its root is an owned value. A Map or Vector param that receives such a read graduates to the owned ABI when the callee updates it in place, and a record param that gives a field to such a param, or to the target of Map.set, Map.remove or Vector.set, is taken by value. An update whose base is the record's last use, and whose fields were not moved out earlier, now moves the record and assigns the new fields, so the replaced value drops at once. When the new field values move fields the update replaces, the base moves after them instead of being cloned. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 1 + src/codegen/rust/from_mir.rs | 205 ++++++-- src/codegen/rust/ownership.rs | 29 +- src/ir/mir/expr.rs | 2 +- src/ir/mir/field_moves.rs | 465 ++++++++++++++++++ src/ir/mir/mod.rs | 1 + src/ir/mir/optimize/own_param.rs | 94 +++- .../fixtures/rust_record_field_moves/main.av | 77 +++ tests/rust_work_spec.rs | 48 ++ 9 files changed, 858 insertions(+), 64 deletions(-) create mode 100644 src/ir/mir/field_moves.rs create mode 100644 tests/fixtures/rust_record_field_moves/main.av diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e21def1d..c97fda9e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ The generated loop is now written from the program's source alone, and the manif - **A missing `depends` edge is reported as one.** Naming `Module.Type` from a module this module does not depend on used to advise adding the type to the other module's `exposes`. - **A module checked or verified as one unit of a program is lowered against the program's answer modules**, not only those its own dependency cone reaches. - **A generated loop updates an answer module's state in place on the Rust backend.** The loop now hands the state out of the run for the length of one answer, and every function on the way to the answer takes the run by value, so a Map or Vector the state holds is no longer copied whole on every request. A process answering 2,000 requests from a module holding a 100,000-entry Map went from 0.76 s to 0.01 s. +- **A record gives up its fields at its last use on the Rust backend.** `f(s.window.created, s.window.spent)`, where nothing reads that part of `s` afterwards, moves both Maps into `f` instead of cloning them, so `f` updates them in place instead of copying each one whole; a function that does this takes its record by value. `T.update(s, window = v)` at the last use of `s` drops the old `window` at once, not when the function returns, so a helper function that only empties a field is no longer needed to let it go. Moving 1,000 times two 100,000-entry Maps out of a record into a function that adds a key to each went from 0.77 s to 0.01 s; emptying and refilling such a window 10 times at a million entries peaked at 202 MiB instead of 334 MiB. - **`aver check .` checks a process that calls an operation of a capability under a subdirectory.** An answer module walked as the entry of its own program lent the batch its bare `module` name, which no program can import, and every process lost the capability's namespace (`unknown-ident 'Lib'`). - **`aver effects --write` adds the in-place effects a process is missing.** A yielding function's list is still left alone for what it reaches through its stops, but what its body performs where it is written is added, as `aver check` requires. - **A wasm-gc build no longer asks for equality on an answer module's state.** The list of answer tuples every tuple gets in case `List.zip` builds one demanded an equality helper for a state that has none (one holding a provider's resource, for example), and the build failed with `carrier eq inner type ... has no eq dispatch`. diff --git a/src/codegen/rust/from_mir.rs b/src/codegen/rust/from_mir.rs index f83ed8188..2eaab2c54 100644 --- a/src/codegen/rust/from_mir.rs +++ b/src/codegen/rust/from_mir.rs @@ -163,6 +163,14 @@ pub struct MirEmitCtx<'a> { /// derived record/sum types), which makes the Debug-format conversion /// total. False everywhere except `emit_mir_verify_expr`. pub try_err_to_string: bool, + /// Projection nodes (by address) that may move their field out of + /// their root local — see [`crate::ir::mir::field_moves`]. A move + /// happens only where the root local is also an owned Rust value. + /// Empty on every path without per-fn facts. + pub movable_projections: &'a HashSet, + /// Locals some movable projection takes a field out of; see + /// [`crate::ir::mir::field_moves::moved_roots`]. + pub moved_roots: &'a HashSet, } impl<'a> MirEmitCtx<'a> { @@ -194,6 +202,8 @@ impl<'a> MirEmitCtx<'a> { mir_builtins: &[], bare: empty_bare_facts(), try_err_to_string: false, + movable_projections: empty_addr_set(), + moved_roots: empty_slot_set(), } } @@ -239,6 +249,8 @@ impl<'a> MirEmitCtx<'a> { mir_builtins, bare: &policy.bare, try_err_to_string: false, + movable_projections: &policy.movable_projections, + moved_roots: &policy.moved_roots, } } @@ -271,6 +283,8 @@ impl<'a> MirEmitCtx<'a> { .unwrap_or(&[]), bare: &policy.bare, try_err_to_string: false, + movable_projections: &policy.movable_projections, + moved_roots: &policy.moved_roots, } } @@ -296,6 +310,17 @@ impl<'a> MirEmitCtx<'a> { } } +fn empty_slot_set() -> &'static HashSet { + static EMPTY: std::sync::OnceLock> = + std::sync::OnceLock::new(); + EMPTY.get_or_init(HashSet::new) +} + +fn empty_addr_set() -> &'static HashSet { + static EMPTY: std::sync::OnceLock> = std::sync::OnceLock::new(); + EMPTY.get_or_init(HashSet::new) +} + fn empty_string_set() -> &'static HashSet { static EMPTY: std::sync::OnceLock> = std::sync::OnceLock::new(); EMPTY.get_or_init(HashSet::new) @@ -396,6 +421,12 @@ pub(super) struct MirFnEmitPolicy { /// `BareI64Facts`. pub bare: crate::ir::mir::FnBareFacts, pub current_module_scope: Option, + /// The body's movable projections — see + /// [`MirEmitCtx::movable_projections`]. Empty until + /// [`Self::apply_field_moves`] runs. + pub movable_projections: HashSet, + /// See [`MirEmitCtx::moved_roots`]. + pub moved_roots: HashSet, } impl MirFnEmitPolicy { @@ -411,6 +442,8 @@ impl MirFnEmitPolicy { owned_params: HashSet::new(), bare: crate::ir::mir::FnBareFacts::default(), current_module_scope: None, + movable_projections: HashSet::new(), + moved_roots: HashSet::new(), } } @@ -446,9 +479,21 @@ impl MirFnEmitPolicy { owned_params: HashSet::new(), bare: crate::ir::mir::FnBareFacts::default(), current_module_scope: scope.map(String::from), + movable_projections: HashSet::new(), + moved_roots: HashSet::new(), } } + /// Record which field reads in `mir_fn`'s body may move their field + /// out of the record local they read. The facts are addresses into + /// this very body, so the policy must emit `mir_fn.body` itself. + pub(super) fn apply_field_moves(&mut self, mir_fn: &crate::ir::mir::MirFn) { + self.movable_projections = + crate::ir::mir::field_moves::movable_projections(&mir_fn.body.node); + self.moved_roots = + crate::ir::mir::field_moves::moved_roots(&mir_fn.body.node, &self.movable_projections); + } + /// Apply the Int "unboxing" facts to this policy: clone the per-fn /// `FnBareFacts` slice out of the program-wide `BareI64Facts` so the /// body emit and the signature emit read the SAME per-`LocalId` @@ -637,8 +682,10 @@ pub(super) fn owned_collection_param_names( /// clones it first, and while that clone is alive the caller's copy still /// holds every Map and Vector the record carries. A later in-place update of /// one of them then copies it whole. A function consumes a param when it -/// updates it at its last use or hands it at its last use to a callee that -/// takes it by value; taking such a param by value lets a chain +/// updates it at its last use, hands it at its last use to a callee that +/// takes it by value, or moves a field out of it into such a callee or into +/// an in-place collection update (see [`consumes_local`]); taking such a +/// param by value lets a chain /// of calls move one record from caller to callee, which is what keeps the /// state a generated loop hands an answer module uniquely owned. /// @@ -681,6 +728,18 @@ pub(super) fn compute_owned_record_params( candidates.push((*id, open)); } } + let movable: HashMap> = candidates + .iter() + .filter_map(|(id, _)| { + program.fn_by_id(*id).map(|mir_fn| { + ( + *id, + crate::ir::mir::field_moves::movable_projections(&mir_fn.body.node), + ) + }) + }) + .collect(); + let in_place = crate::ir::mir::field_moves::in_place_collection_params(program); loop { let mut graduated = Vec::new(); for (id, open) in &candidates { @@ -691,8 +750,14 @@ pub(super) fn compute_owned_record_params( if owned[id][i] { continue; } - let slot = mir_fn.params[i].local; - if consumes_local(&mir_fn.body.node, slot, &owned) { + let consumption = Consumption { + slot: mir_fn.params[i].local, + owned: &owned, + movable: &movable[id], + in_place: &in_place, + builtins: &program.builtins, + }; + if consumes_local(&mir_fn.body.node, &consumption) { graduated.push((*id, i)); } } @@ -709,66 +774,98 @@ pub(super) fn compute_owned_record_params( owned } -/// Whether `expr` consumes the local in `slot`: updates it as the base of a -/// record update at its last use, or passes it at its last use to a callee -/// position `owned` says is by value. Returning it bare is not a consumption: -/// a function that only hands its param back keeps borrowing it. -fn consumes_local( - expr: &MirExpr, +/// What [`consumes_local`] reads besides the expression: the by-value +/// callee positions so far, the body's movable projections, the Map/Vector +/// params updated in place and the builtin names. +struct Consumption<'a> { slot: LocalId, - owned: &HashMap>, -) -> bool { + owned: &'a HashMap>, + movable: &'a HashSet, + in_place: &'a HashMap>, + builtins: &'a [String], +} + +/// Whether `expr` consumes the local in `slot`: updates it as the base of a +/// record update that reads it for the last time, passes it at its last use +/// to a callee position `owned` says is by value, or moves a field out of it +/// into a by-value record param, a Map/Vector param updated in place, or the +/// target of `Map.set`, `Map.remove` or `Vector.set`. Returning it bare is not a +/// consumption: a function that only hands its param back keeps borrowing +/// it. +fn consumes_local(expr: &MirExpr, cx: &Consumption<'_>) -> bool { + let slot = cx.slot; let last_use_of = |arg: &MirExpr| local_of(arg).is_some_and(|local| local.slot == slot && local.last_use); - let by_value = |callee: crate::ir::FnId, index: usize| { - owned + let moves_field = |arg: &Spanned| { + matches!(arg.node, MirExpr::Project(_)) + && cx.movable.contains(&(&arg.node as *const MirExpr as usize)) + && super::ownership::projection_root_local(&arg.node) + .is_some_and(|root| root.slot == slot) + }; + let fact = |facts: &HashMap>, callee, index: usize| { + facts .get(&callee) .and_then(|abi| abi.get(index)) .copied() .unwrap_or(false) }; + // A field is worth moving into a record param the callee consumes, or + // into a Map/Vector param it updates in place; a param that only reads + // the field gains nothing from owning it. + let takes_field = |callee, index: usize, arg: &Spanned| match arg.ty() { + Some(Type::Named { .. }) => fact(cx.owned, callee, index), + Some(Type::Map(..) | Type::Vector(_)) => { + fact(cx.owned, callee, index) && fact(cx.in_place, callee, index) + } + _ => false, + }; + let hands_over = |callee: crate::ir::FnId, args: &[Spanned]| { + args.iter().enumerate().any(|(index, arg)| { + (last_use_of(&arg.node) && fact(cx.owned, callee, index)) + || (moves_field(arg) && takes_field(callee, index, arg)) + }) + }; match expr { MirExpr::Call(call) => { - if let MirCallee::Fn(callee) = call.node.callee - && call + let handed = match call.node.callee { + MirCallee::Fn(callee) => hands_over(callee, &call.node.args), + MirCallee::Builtin(id) => { + matches!( + cx.builtins.get(id.0 as usize).map(String::as_str), + Some("Map.set" | "Map.remove" | "Vector.set") + ) && call.node.args.first().is_some_and(moves_field) + } + _ => false, + }; + handed + || call .node .args .iter() - .enumerate() - .any(|(index, arg)| last_use_of(&arg.node) && by_value(callee, index)) - { - return true; - } - call.node - .args - .iter() - .any(|arg| consumes_local(&arg.node, slot, owned)) + .any(|arg| consumes_local(&arg.node, cx)) } MirExpr::TailCall(call) => { - call.node - .args - .iter() - .enumerate() - .any(|(index, arg)| last_use_of(&arg.node) && by_value(call.node.target, index)) + hands_over(call.node.target, &call.node.args) || call .node .args .iter() - .any(|arg| consumes_local(&arg.node, slot, owned)) + .any(|arg| consumes_local(&arg.node, cx)) } MirExpr::RecordUpdate(update) => { - last_use_of(&update.node.base.node) - || consumes_local(&update.node.base.node, slot, owned) + (local_of(&update.node.base.node).is_some_and(|base| base.slot == slot) + && crate::ir::mir::field_moves::update_base_is_final(&update.node)) + || consumes_local(&update.node.base.node, cx) || update .node .updates .iter() - .any(|field| consumes_local(&field.value.node, slot, owned)) + .any(|field| consumes_local(&field.value.node, cx)) } _ => { let mut found = false; crate::ir::mir::expr::walk_children(expr, &mut |child| { - found = found || consumes_local(child, slot, owned); + found = found || consumes_local(child, cx); }); found } @@ -1711,18 +1808,46 @@ pub(super) fn emit_mir_expr(expr: &Spanned, emit_ctx: &MirEmitCtx<'_>) } }; let val = if packed_u8 { pack_u8_list(val) } else { val }; - parts.push(format!("{}: {}", aver_name_to_rust(&f.name), val)); + parts.push((aver_name_to_rust(&f.name), val)); } // A specialized field successor partially moves the replaced // field from an owned record. The `..base` update then moves only // the remaining fields, which Rust permits. Cloning `base` here // would both be unnecessary and fail after the partial move. let emitted_base = emit_mir_expr(&upd.base, emit_ctx)?; - let base = if moved_replaced_field { + let owned_final_base = local_of(&upd.base.node) + .is_some_and(|base| super::ownership::root_is_owned(base, emit_ctx)) + && crate::ir::mir::field_moves::update_base_is_final(upd); + if owned_final_base + && !moved_replaced_field + && local_of(&upd.base.node) + .is_some_and(|base| base.last_use && !emit_ctx.moved_roots.contains(&base.slot)) + { + // The base read is the record's last use, so no new field + // value reads it, and no field read moved any of it. Move + // the record, then assign each field: the replaced value + // drops at once instead of staying in a partially moved + // local until the end of the function. + let assigns: String = parts + .iter() + .map(|(field, value)| format!("__updated.{field} = {value}; ")) + .collect(); + return Some(format!( + "{{ let mut __updated = {emitted_base}; {assigns}__updated }}" + )); + } + // A final base whose new field values move fields out of it + // (`T.update(s, window = f(s.window.created))`) moves after them; + // those values only ever move fields this update replaces. + let base = if moved_replaced_field || owned_final_base { emitted_base } else { mir_clone_arg(emitted_base, &upd.base.node, emit_ctx) }; + let parts: Vec = parts + .iter() + .map(|(field, value)| format!("{field}: {value}")) + .collect(); Some(format!( "{} {{ {}, ..{} }}", rust_type, @@ -3727,6 +3852,7 @@ pub(super) fn emit_mir_fn_body_routed( // `bare_fn_facts`), so body and signature agree on which params / // return are bare. policy.apply_bare_i64(mir_fn.fn_id, ctx); + policy.apply_field_moves(mir_fn); let emit_ctx = MirEmitCtx::for_fn(ctx, &policy); let body = emit_mir_fn_body(&mir_fn.body, &emit_ctx)?; let Some(prologue) = post_checkpoint_prologue else { @@ -3835,6 +3961,7 @@ pub(super) fn emit_mir_tco_fn( for n in &rc_names { policy.owned_params.remove(n); } + policy.apply_field_moves(mir_fn); let emit_ctx = MirEmitCtx::for_fn(ctx, &policy); // Render the body in tail position FIRST — bail before emitting any @@ -5990,6 +6117,8 @@ mod tests { mir_builtins: BUILTINS.get_or_init(Vec::new), bare: &policy.bare, try_err_to_string: false, + movable_projections: &policy.movable_projections, + moved_roots: &policy.moved_roots, }; let lit = span(MirExpr::Literal(span(crate::ast::Literal::Int(7)))); assert_eq!( @@ -7092,6 +7221,8 @@ mod tests { mir_builtins: BUILTINS.get_or_init(Vec::new), bare: &policy.bare, try_err_to_string: false, + movable_projections: &policy.movable_projections, + moved_roots: &policy.moved_roots, } } diff --git a/src/codegen/rust/ownership.rs b/src/codegen/rust/ownership.rs index 7f2c9d7d3..59318add5 100644 --- a/src/codegen/rust/ownership.rs +++ b/src/codegen/rust/ownership.rs @@ -82,9 +82,14 @@ pub(super) fn value_facts(expr: &MirExpr, ctx: &MirEmitCtx<'_>) -> RustValueFact }, borrow_shape: BorrowShape::Direct, // A field of a fresh temporary can move. A local-rooted - // projection stays conservative even when the root is at its - // final use; proving partial moves belongs in a later MIR pass. - can_move: copy || projection_root_local(&project.node.base.node).is_none(), + // projection moves only where `field_moves` proved no later or + // still-borrowed read overlaps it and the root is an owned + // Rust value. + can_move: copy + || match projection_root_local(&project.node.base.node) { + None => true, + Some(root) => projection_moves(expr, root, ctx), + }, provider_resource: false, }; } @@ -231,7 +236,23 @@ fn clone_borrowed(code: String, shape: BorrowShape) -> String { } } -fn projection_root_local(expr: &MirExpr) -> Option<&MirLocal> { +/// Whether this field read may move its field out of `root`: the read is a +/// movable projection and `root` is an owned Rust value (not a borrowed +/// or wrapped parameter, and not carried unchanged into the next loop +/// iteration). +pub(super) fn projection_moves(expr: &MirExpr, root: &MirLocal, ctx: &MirEmitCtx<'_>) -> bool { + ctx.movable_projections + .contains(&(expr as *const MirExpr as usize)) + && root_is_owned(root, ctx) +} + +/// Whether `local` is an owned Rust value that may give up its fields. +pub(super) fn root_is_owned(local: &MirLocal, ctx: &MirEmitCtx<'_>) -> bool { + local_value_facts(local, ctx).mode == RustValueMode::Owned + && !ctx.loop_carried_params.contains(local.name.as_str()) +} + +pub(super) fn projection_root_local(expr: &MirExpr) -> Option<&MirLocal> { match expr { MirExpr::Local(_) => local_of(expr), MirExpr::Project(project) => projection_root_local(&project.node.base.node), diff --git a/src/ir/mir/expr.rs b/src/ir/mir/expr.rs index 9b29dcb02..43b98b58a 100644 --- a/src/ir/mir/expr.rs +++ b/src/ir/mir/expr.rs @@ -214,7 +214,7 @@ pub enum MirExpr { /// /// The exhaustive match makes adding a `MirExpr` variant a compile error here, /// keeping read-only MIR traversals on one canonical child enumeration. -pub(crate) fn walk_children(e: &MirExpr, f: &mut dyn FnMut(&MirExpr)) { +pub(crate) fn walk_children<'a>(e: &'a MirExpr, f: &mut dyn FnMut(&'a MirExpr)) { match e { MirExpr::Literal(_) | MirExpr::Local(_) | MirExpr::FnValue(_) => {} MirExpr::Let(l) => { diff --git a/src/ir/mir/field_moves.rs b/src/ir/mir/field_moves.rs new file mode 100644 index 000000000..aebe77d67 --- /dev/null +++ b/src/ir/mir/field_moves.rs @@ -0,0 +1,465 @@ +//! Which field reads may move their field out of a record local. +//! +//! A read `s.window.created` normally clones the field: the record `s` +//! may be read again. When nothing reads that part of `s` afterwards, the +//! field can move instead, which keeps a Map or Vector inside it uniquely +//! owned by whoever receives it. +//! +//! The answer here is structural and backend-neutral: it names the +//! projection nodes that no later or still-borrowed read of the same local +//! overlaps. A backend still decides whether the root local is an owned +//! value it may partially move (a borrowed parameter never is). +//! +//! A projection `q` rooted at local `s` is movable when every other read +//! `o` of `s` in the body either +//! +//! - cannot run together with `q` (the two sit in different arms of one +//! `match`, or in the two branches of one `if`), +//! - finished before `q` began and holds nothing of `s` afterwards (`o` is +//! in a `let` value and `q` in that `let`'s body), or +//! - reads a disjoint part of `s` (`s.window.spent` beside +//! `s.window.created`; the base of `T.update(s, window = ...)` at its +//! last use beside anything under `s.window`). +//! +//! and one of the reads that can run with `q` is the local's last use, so +//! nothing reads `s` after them. A read inside an independent product is +//! never movable: its branches run on their own threads. + +use std::collections::{HashMap, HashSet}; + +use super::expr::{MirExpr, MirRecordUpdate, walk_children}; +use super::program::LocalId; + +/// The part of a local one read observes. +#[derive(Debug, Clone)] +enum Part { + /// `s` (empty path) or `s.a.b`. + Path(Vec), + /// The base of `T.update(s, a = ...)`: every field of `s` except the + /// replaced ones. + AllExcept(Vec), +} + +#[derive(Debug)] +struct Read { + part: Part, + /// Address of the read's outermost node (the projection chain or the + /// local itself). + addr: usize, + last_use: bool, + in_product: bool, + /// Each ancestor on the way down from the body: its address and the + /// index of the child the read sits under. + trail: Vec<(usize, usize, Branching)>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Branching { + /// Children are alternatives from index 1 on (`match` arms, `if` + /// branches); index 0 is the subject or condition. + Alternatives, + /// `let`: child 0 is the value, child 1 the body. + Sequence, + Other, +} + +/// Addresses (`&MirExpr as *const _ as usize`) of the projection nodes in +/// `body` that may move their field out of their root local. +pub fn movable_projections(body: &MirExpr) -> HashSet { + let mut reads: HashMap> = HashMap::new(); + let mut trail = Vec::new(); + collect(body, &mut trail, false, &mut reads); + let mut out = HashSet::new(); + for group in reads.values() { + for (qi, q) in group.iter().enumerate() { + let Part::Path(path) = &q.part else { + continue; + }; + if path.is_empty() || q.in_product { + continue; + } + let mut ends_here = q.last_use; + let mut blocked = false; + for (oi, o) in group.iter().enumerate() { + if oi == qi || apart(o, q) { + continue; + } + if !disjoint(&o.part, o.last_use, path) { + blocked = true; + break; + } + ends_here |= o.last_use; + } + if !blocked && ends_here { + out.insert(q.addr); + } + } + } + out +} + +/// The locals under `body` that give up a field through one of the +/// `movable` projections. Such a local may be partially moved by the time +/// it is read again, so it can only be read by fields it still holds. +pub fn moved_roots(body: &MirExpr, movable: &HashSet) -> HashSet { + let mut roots = HashSet::new(); + if !movable.is_empty() { + gather_moved_roots(body, movable, &mut roots); + } + roots +} + +fn gather_moved_roots(expr: &MirExpr, movable: &HashSet, roots: &mut HashSet) { + if movable.contains(&(expr as *const MirExpr as usize)) + && let Some((slot, _, _)) = projection_root(expr) + { + roots.insert(slot); + return; + } + walk_children(expr, &mut |child| gather_moved_roots(child, movable, roots)); +} + +/// The field path of a projection chain rooted at a local, outermost last. +fn projection_root(expr: &MirExpr) -> Option<(LocalId, bool, Vec)> { + let mut fields = Vec::new(); + let mut cursor = expr; + loop { + match cursor { + MirExpr::Project(project) => { + fields.push(project.node.field.clone()); + cursor = &project.node.base.node; + } + MirExpr::Local(local) => { + fields.reverse(); + return Some((local.node.slot, local.node.last_use, fields)); + } + _ => return None, + } + } +} + +fn collect( + expr: &MirExpr, + trail: &mut Vec<(usize, usize, Branching)>, + in_product: bool, + reads: &mut HashMap>, +) { + let addr = expr as *const MirExpr as usize; + match expr { + MirExpr::Local(local) => { + reads.entry(local.node.slot).or_default().push(Read { + part: Part::Path(Vec::new()), + addr, + last_use: local.node.last_use, + in_product, + trail: trail.clone(), + }); + return; + } + MirExpr::Project(_) => { + if let Some((slot, last_use, path)) = projection_root(expr) { + reads.entry(slot).or_default().push(Read { + part: Part::Path(path), + addr, + last_use, + in_product, + trail: trail.clone(), + }); + return; + } + } + MirExpr::RecordUpdate(update) => { + if let MirExpr::Local(local) = &update.node.base.node { + let replaced = update + .node + .updates + .iter() + .map(|field| field.name.clone()) + .collect(); + reads.entry(local.node.slot).or_default().push(Read { + part: Part::AllExcept(replaced), + addr: &update.node.base.node as *const MirExpr as usize, + last_use: update_base_is_final(&update.node), + in_product, + trail: { + let mut t = trail.clone(); + t.push((addr, 0, Branching::Other)); + t + }, + }); + for (index, field) in update.node.updates.iter().enumerate() { + trail.push((addr, index + 1, Branching::Other)); + collect(&field.value.node, trail, in_product, reads); + trail.pop(); + } + return; + } + } + _ => {} + } + let branching = match expr { + MirExpr::Match(_) | MirExpr::IfThenElse(_) => Branching::Alternatives, + MirExpr::Let(_) => Branching::Sequence, + _ => Branching::Other, + }; + let in_product = in_product || matches!(expr, MirExpr::IndependentProduct(_)); + let mut index = 0; + walk_children(expr, &mut |child| { + trail.push((addr, index, branching)); + collect(child, trail, in_product, reads); + trail.pop(); + index += 1; + }); +} + +/// Which Map and Vector params of every function are updated in place: +/// the param is the target of `Map.set`, `Map.remove` or `Vector.set`, or +/// is handed to a callee param that is. Moving a field into a param that +/// only reads it saves nothing, so only these params make a field move +/// worth taking a record by value for. +pub fn in_place_collection_params( + program: &crate::ir::mir::program::MirProgram, +) -> HashMap> { + let mut updated: HashMap> = program + .iter() + .map(|(id, f)| (*id, vec![false; f.params.len()])) + .collect(); + loop { + let mut changed = Vec::new(); + for (id, f) in program.iter() { + for (index, param) in f.params.iter().enumerate() { + if !updated[id][index] + && updates_in_place(&f.body.node, param.local, &updated, &program.builtins) + { + changed.push((*id, index)); + } + } + } + if changed.is_empty() { + return updated; + } + for (id, index) in changed { + if let Some(params) = updated.get_mut(&id) { + params[index] = true; + } + } + } +} + +fn updates_in_place( + expr: &MirExpr, + slot: LocalId, + updated: &HashMap>, + builtins: &[String], +) -> bool { + let is_slot = |arg: &MirExpr| matches!(arg, MirExpr::Local(local) if local.node.slot == slot); + let hands_on = |callee: crate::ir::FnId, args: &[crate::ast::Spanned]| { + args.iter().enumerate().any(|(index, arg)| { + is_slot(&arg.node) + && updated + .get(&callee) + .and_then(|params| params.get(index)) + .copied() + .unwrap_or(false) + }) + }; + let found = match expr { + MirExpr::Call(call) => match call.node.callee { + super::expr::MirCallee::Builtin(id) => { + matches!( + builtins.get(id.0 as usize).map(String::as_str), + Some("Map.set" | "Map.remove" | "Vector.set") + ) && call.node.args.first().is_some_and(|arg| is_slot(&arg.node)) + } + super::expr::MirCallee::Fn(callee) => hands_on(callee, &call.node.args), + _ => false, + }, + MirExpr::TailCall(call) => hands_on(call.node.target, &call.node.args), + _ => false, + }; + if found { + return true; + } + let mut below = false; + walk_children(expr, &mut |child| { + below = below || updates_in_place(child, slot, updated, builtins); + }); + below +} + +/// Whether this update reads its base local for the last time: the base +/// read is the local's last use, or every last use of the local inside the +/// new field values reads a field the update replaces +/// (`T.update(s, window = f(s.window.created))`). Nothing reads the local +/// after such an update, and a field value can only move a field the update +/// replaces, so the base may move after the field values run. +pub fn update_base_is_final(update: &MirRecordUpdate) -> bool { + let MirExpr::Local(base) = &update.base.node else { + return false; + }; + if base.node.last_use { + return true; + } + let slot = base.node.slot; + let replaced: Vec<&str> = update.updates.iter().map(|f| f.name.as_str()).collect(); + let mut finals = 0usize; + let mut stray = false; + for field in &update.updates { + final_reads(&field.value.node, slot, &replaced, &mut finals, &mut stray); + } + finals > 0 && !stray +} + +/// Count the last-use reads of `slot` under `expr` that read a replaced +/// field, and note any last-use read that does not. +fn final_reads( + expr: &MirExpr, + slot: LocalId, + replaced: &[&str], + finals: &mut usize, + stray: &mut bool, +) { + if let Some((root, last_use, path)) = projection_root(expr) + && root == slot + { + if last_use { + match path.first() { + Some(first) if replaced.contains(&first.as_str()) => *finals += 1, + _ => *stray = true, + } + } + return; + } + walk_children(expr, &mut |child| { + final_reads(child, slot, replaced, finals, stray) + }); +} + +/// Whether `o` never runs together with `q`, or finishes before `q` starts +/// without holding a borrow of the local. +fn apart(o: &Read, q: &Read) -> bool { + for (a, b) in o.trail.iter().zip(q.trail.iter()) { + if a == b { + continue; + } + let (_, o_child, branching) = *a; + let q_child = b.1; + return match branching { + Branching::Alternatives => o_child >= 1 && q_child >= 1, + Branching::Sequence => o_child == 0 && q_child == 1, + Branching::Other => false, + }; + } + false +} + +/// Whether a read of `part` leaves `path` untouched. The base of an update +/// that is not the local's last use is cloned whole, so it overlaps +/// everything. +fn disjoint(part: &Part, last_use: bool, path: &[String]) -> bool { + match part { + Part::Path(other) => { + let common = other.len().min(path.len()); + other[..common] != path[..common] + } + Part::AllExcept(replaced) => last_use && replaced.iter().any(|field| *field == path[0]), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::Spanned; + use crate::ir::mir::expr::{ + MirCall, MirCallee, MirLet, MirLocal, MirProject, MirRecordField, MirRecordUpdate, + }; + use crate::ir::{FnId, TypeId}; + + fn sp(expr: MirExpr) -> Spanned { + Spanned::bare(expr) + } + + fn local(slot: u32, last_use: bool) -> Spanned { + sp(MirExpr::Local(Spanned::bare(MirLocal { + slot: LocalId(slot), + last_use, + name: "s".to_string(), + }))) + } + + fn project(base: Spanned, field: &str) -> Spanned { + sp(MirExpr::Project(Spanned::bare(MirProject { + base: Box::new(base), + field: field.to_string(), + }))) + } + + fn call(args: Vec>) -> Spanned { + sp(MirExpr::Call(Spanned::bare(MirCall { + callee: MirCallee::Fn(FnId(0)), + args, + }))) + } + + fn addr(expr: &Spanned) -> usize { + &expr.node as *const MirExpr as usize + } + + fn args(expr: &Spanned) -> &[Spanned] { + match &expr.node { + MirExpr::Call(call) => &call.node.args, + _ => panic!("not a call"), + } + } + + #[test] + fn disjoint_fields_at_the_last_use_both_move() { + let body = call(vec![ + project(project(local(0, false), "window"), "created"), + project(project(local(0, true), "window"), "spent"), + ]); + let movable = movable_projections(&body.node); + assert!(movable.contains(&addr(&args(&body)[0]))); + assert!(movable.contains(&addr(&args(&body)[1]))); + } + + #[test] + fn an_overlapping_read_blocks_the_move() { + let body = call(vec![ + project(project(local(0, false), "window"), "created"), + project(local(0, true), "window"), + ]); + let movable = movable_projections(&body.node); + assert!(movable.is_empty()); + } + + #[test] + fn a_read_before_the_last_use_does_not_move() { + let body = sp(MirExpr::Let(Spanned::bare(MirLet { + binding: LocalId(1), + binding_name: "a".to_string(), + value: Box::new(call(vec![project(local(0, false), "window")])), + body: Box::new(call(vec![project(local(0, true), "window")])), + }))); + let movable = movable_projections(&body.node); + let MirExpr::Let(chain) = &body.node else { + unreachable!() + }; + assert_eq!(movable.len(), 1); + assert!(movable.contains(&addr(&args(&chain.node.body)[0]))); + } + + #[test] + fn a_field_moves_beside_the_base_of_its_own_update() { + let body = sp(MirExpr::RecordUpdate(Spanned::bare(MirRecordUpdate { + type_id: Some(TypeId(0)), + type_name: "Setting".to_string(), + base: Box::new(local(0, false)), + updates: vec![MirRecordField { + name: "window".to_string(), + value: call(vec![project(project(local(0, true), "window"), "created")]), + }], + }))); + assert_eq!(movable_projections(&body.node).len(), 1); + } +} diff --git a/src/ir/mir/mod.rs b/src/ir/mir/mod.rs index 746ed76bf..74909abff 100644 --- a/src/ir/mir/mod.rs +++ b/src/ir/mir/mod.rs @@ -75,6 +75,7 @@ pub mod dump; pub mod expr; +pub mod field_moves; pub mod instantiations; pub mod lower; pub mod optimize; diff --git a/src/ir/mir/optimize/own_param.rs b/src/ir/mir/optimize/own_param.rs index c049e4d1b..e81988eb5 100644 --- a/src/ir/mir/optimize/own_param.rs +++ b/src/ir/mir/optimize/own_param.rs @@ -138,11 +138,24 @@ fn is_target_consuming_builtin(name: &str) -> bool { ) } +/// Facts that let a generated-Rust call site supply an owned carrier. +#[derive(Default)] +struct RustOwned { + /// Locals bound by a match pattern; see `own_param_refine_for_model`. + pattern_slots: HashMap>, + /// Field reads that move their field out of a record local, by node + /// address (`field_moves::movable_projections`). + movable_projections: HashMap>, + /// Map/Vector params each fn updates in place + /// (`field_moves::in_place_collection_params`). + in_place_params: HashMap>, +} + /// A single visible call edge: `target(args…)` made from `caller`. -struct CallSite { +struct CallSite<'a> { target: FnId, caller: FnId, - args: Vec>, + args: &'a [Spanned], } #[derive(Clone, Copy, PartialEq, Eq)] @@ -230,13 +243,13 @@ fn own_param_refine_for_model(mut program: MirProgram, model: OwnershipModel) -> // movable into an owned callee parameter without manufacturing the extra // handle that today's borrowed ABI creates. Arena backends cannot use this // fact: their destructured wrapper/tuple entries remain observable holders. - let mut rust_owned_pattern_slots: HashMap> = HashMap::new(); + let mut rust_owned = RustOwned::default(); if model.owned_carriers_are_cow_protected() { for (id, f) in program.iter() { let mut slots = HashSet::new(); collect_pattern_bound_slots(&f.body.node, &mut slots); if !slots.is_empty() { - rust_owned_pattern_slots.insert(*id, slots); + rust_owned.pattern_slots.insert(*id, slots); } } } @@ -361,6 +374,25 @@ fn own_param_refine_for_model(mut program: MirProgram, model: OwnershipModel) -> collect_call_sites(*caller, &f.body.node, &mut call_sites); } + // Generated Rust moves a field out of a record local where no later or + // still-borrowed read overlaps it (`field_moves`), so such a field read + // supplies an owned carrier just like a last-use local. Its root may be + // a record param the backend still borrows; the read then clones, which + // costs what the borrowed ABI cost before and stays correct under COW. + // Moving a field into a param that only reads it saves nothing and + // makes every caller whose record is borrowed clone the field, so a + // field read counts only for a param updated in place. + if model.owned_carriers_are_cow_protected() { + for (id, f) in program.iter() { + let movable = crate::ir::mir::field_moves::movable_projections(&f.body.node); + if !movable.is_empty() { + rust_owned.movable_projections.insert(*id, movable); + } + } + rust_owned.in_place_params = + crate::ir::mir::field_moves::in_place_collection_params(&program); + } + // Per-fn set of let-bound slots that have a still-live RENAME alias // of another slot — used by `uniquely_owned` to reject a call-site // arg whose slot the CALLER still observes through a live alias (the @@ -445,13 +477,20 @@ fn own_param_refine_for_model(mut program: MirProgram, model: OwnershipModel) -> &program, &owned, &provenance, - &rust_owned_pattern_slots, + &rust_owned, &return_aliases, model, &builtins, 0, ); - let ok = !dup && !caller_aliased && argument_owned; + let read_only_field = matches!(&arg.node, MirExpr::Project(_)) + && !rust_owned + .in_place_params + .get(&cs.target) + .and_then(|params| params.get(i)) + .copied() + .unwrap_or(false); + let ok = !dup && !caller_aliased && argument_owned && !read_only_field; if !ok && owned.insert(key, false) != Some(false) { changed = true; } @@ -490,7 +529,7 @@ fn uniquely_owned( program: &MirProgram, owned: &HashMap<(FnId, usize), bool>, provenance: &HashMap>>, - rust_owned_pattern_slots: &HashMap>, + rust_owned: &RustOwned, return_aliases: &ReturnAliasSummary, model: OwnershipModel, builtins: &[String], @@ -543,7 +582,7 @@ fn uniquely_owned( program, owned, provenance, - rust_owned_pattern_slots, + rust_owned, return_aliases, model, builtins, @@ -582,7 +621,7 @@ fn uniquely_owned( program, owned, provenance, - rust_owned_pattern_slots, + rust_owned, return_aliases, model, builtins, @@ -597,7 +636,7 @@ fn uniquely_owned( program, owned, provenance, - rust_owned_pattern_slots, + rust_owned, return_aliases, model, builtins, @@ -608,7 +647,7 @@ fn uniquely_owned( program, owned, provenance, - rust_owned_pattern_slots, + rust_owned, return_aliases, model, builtins, @@ -634,7 +673,7 @@ fn uniquely_owned( program, owned, provenance, - rust_owned_pattern_slots, + rust_owned, return_aliases, model, builtins, @@ -665,20 +704,25 @@ fn uniquely_owned( program, owned, provenance, - rust_owned_pattern_slots, + rust_owned, return_aliases, model, builtins, depth + 1, ) } + // A field read generated Rust moves out of its record local. + MirExpr::Project(_) if model.owned_carriers_are_cow_protected() => rust_owned + .movable_projections + .get(&caller) + .is_some_and(|movable| movable.contains(&(e as *const MirExpr as usize))), MirExpr::Try(inner) if model.returned_aggregates_are_consumed() => uniquely_owned( &inner.node, caller, program, owned, provenance, - rust_owned_pattern_slots, + rust_owned, return_aliases, model, builtins, @@ -699,7 +743,7 @@ fn slot_owned( program: &MirProgram, owned: &HashMap<(FnId, usize), bool>, provenance: &HashMap>>, - rust_owned_pattern_slots: &HashMap>, + rust_owned: &RustOwned, return_aliases: &ReturnAliasSummary, model: OwnershipModel, builtins: &[String], @@ -715,7 +759,8 @@ fn slot_owned( let is_param = (slot as usize) < caller_fn.params.len(); if !is_param && model.owned_carriers_are_cow_protected() - && rust_owned_pattern_slots + && rust_owned + .pattern_slots .get(&caller) .is_some_and(|slots| slots.contains(&slot)) { @@ -752,7 +797,7 @@ fn slot_owned( program, owned, provenance, - rust_owned_pattern_slots, + rust_owned, return_aliases, model, builtins, @@ -1534,27 +1579,32 @@ fn compute_capture_summary( } /// Collect visible `Call(Fn)` / `TailCall` edges made from `caller`. -fn collect_call_sites(caller: FnId, e: &MirExpr, out: &mut Vec) { +fn collect_call_sites<'a>(caller: FnId, e: &'a MirExpr, out: &mut Vec>) { match e { MirExpr::Call(c) => { if let MirCallee::Fn(target) = c.node.callee { out.push(CallSite { target, caller, - args: c.node.args.clone(), + args: &c.node.args, }); } + for arg in &c.node.args { + collect_call_sites(caller, &arg.node, out); + } } MirExpr::TailCall(tc) => { out.push(CallSite { target: tc.node.target, caller, - args: tc.node.args.clone(), + args: &tc.node.args, }); + for arg in &tc.node.args { + collect_call_sites(caller, &arg.node, out); + } } - _ => {} + _ => walk_children(e, &mut |c| collect_call_sites(caller, c, out)), } - walk_children(e, &mut |c| collect_call_sites(caller, c, out)); } #[cfg(test)] diff --git a/tests/fixtures/rust_record_field_moves/main.av b/tests/fixtures/rust_record_field_moves/main.av new file mode 100644 index 000000000..d30daa2e9 --- /dev/null +++ b/tests/fixtures/rust_record_field_moves/main.av @@ -0,0 +1,77 @@ +module Main + intent = "A record whose Maps move out through field reads at the record's last use." + effects [Args.get, Console.print] + +record Window + created: Map + spent: Map + +record Setting + window: Window + rounds: Int + +fn fill(counts: Map, left: Int) -> Map + ? "Keys 1 to left, each mapped to itself." + match left <= 0 + true -> counts + false -> fill(Map.set(counts, left, left), left - 1) + +verify fill + fill({}, 0) => {} + fill({}, 2) => {1 => 1, 2 => 2} + +fn absorbed(created: Map, spent: Map, key: Int) -> Window + ? "Both maps with one more key." + Window(created = Map.set(created, key, 1), spent = Map.set(spent, key, 2)) + +verify absorbed + absorbed({}, {}, 3) => Window(created = {3 => 1}, spent = {3 => 2}) + +fn step(setting: Setting, key: Int) -> Setting + ? "One round: the window's maps go to absorbed at the setting's last use." + Setting(window = absorbed(setting.window.created, setting.window.spent, key), rounds = setting.rounds + 1) + +verify step + step(Setting(window = Window(created = {}, spent = {}), rounds = 0), 1) => Setting(window = Window(created = {1 => 1}, spent = {1 => 2}), rounds = 1) + +fn rounds(setting: Setting, left: Int) -> Setting + ? "Runs step left times." + match left <= 0 + true -> setting + false -> rounds(step(setting, left), left - 1) + +verify rounds + rounds(Setting(window = Window(created = {}, spent = {}), rounds = 0), 0) => Setting(window = Window(created = {}, spent = {}), rounds = 0) + +fn refilled(setting: Setting, size: Int) -> Setting + ? "The window rebuilt from nothing." + Setting.update(setting, window = Window(created = fill({}, size), spent = fill({}, size))) + +verify refilled + refilled(Setting(window = Window(created = {9 => 9}, spent = {}), rounds = 0), 1) => Setting(window = Window(created = {1 => 1}, spent = {1 => 1}), rounds = 0) + +fn restarts(setting: Setting, size: Int, left: Int) -> Setting + ? "Rebuilds the window left times, after emptying it at the setting's last use." + match left <= 0 + true -> setting + false -> restarts(refilled(Setting.update(setting, window = Window(created = {}, spent = {})), size), size, left - 1) + +verify restarts + restarts(Setting(window = Window(created = {}, spent = {}), rounds = 0), 1, 0) => Setting(window = Window(created = {}, spent = {}), rounds = 0) + +fn arg(index: Int, fallback: Int) -> Int + ? "The index-th argument as an Int, or the fallback." + ! [Args.get] + match Vector.get(Vector.fromList(Args.get()), index) + Option.Some(text) -> Result.withDefault(Int.fromString(text), fallback) + Option.None -> fallback + +fn main() -> Unit + ! [Args.get, Console.print] + size = arg(0, 100000) + moves = arg(1, 1000) + start = Setting(window = Window(created = fill({}, size), spent = fill({}, size)), rounds = 0) + done = rounds(start, moves) + Console.print("rounds {done.rounds} created {Map.len(done.window.created)} spent {Map.len(done.window.spent)}") + again = restarts(done, size, arg(2, 0)) + Console.print("restarted {Map.len(again.window.created)}") diff --git a/tests/rust_work_spec.rs b/tests/rust_work_spec.rs index 6b35ad1e0..4acffb00f 100644 --- a/tests/rust_work_spec.rs +++ b/tests/rust_work_spec.rs @@ -469,6 +469,54 @@ fn an_answer_modules_state_reaches_its_answer_function_uniquely_owned() { result.unwrap_or_else(|error| panic!("{error}")); } +/// A field read at its record's last use moves the field instead of cloning +/// it, and an update at the record's last use drops the replaced field at +/// once. +/// +/// `step` hands both Maps of `setting.window` to `absorbed`, which updates +/// them in place only if it holds their sole reference; a clone would leave +/// the record's copy alive and make each update copy the whole Map. `restarts` +/// empties the window of a record it is done with; spelled `..setting`, the +/// old window would stay in the partially moved local until the function +/// returned. +#[test] +fn a_record_gives_up_its_fields_at_its_last_use() { + let name = "rust_record_field_moves"; + let ws = temp_dir(name); + let project = ws.join("project"); + fs::create_dir_all(&project).expect("create project dir"); + let args = ["1000", "10", "2"]; + let result = (|| { + compile_rust(name, &project, name, &[])?; + let entry = fs::read_to_string(project.join("src/aver_generated/entry/mod.rs")) + .map_err(|error| format!("read the generated entry module: {error}"))?; + for moved in [ + "pub fn absorbed(mut created @ _: aver_rt::AverMap<", + "pub fn step(mut setting @ _: Setting,", + "absorbed(setting.window.created, setting.window.spent, key)", + "step(setting, left.clone())", + "{ let mut __updated = setting; __updated.window = Window {", + ] { + if !entry.contains(moved) { + return Err(format!( + "{name}: the record no longer gives up its fields at its last use; missing `{moved}` in:\n{entry}" + )); + } + } + let vm = run_vm_with(name, &args)?; + let bin = cargo_build(&project, name)?; + let rust = run_binary_with(&bin, &args)?; + if vm != rust { + return Err(format!( + "{name}: stdout mismatch\n--- VM ---\n{vm}\n--- Rust ---\n{rust}" + )); + } + Ok(()) + })(); + let _ = fs::remove_dir_all(&ws); + result.unwrap_or_else(|error| panic!("{error}")); +} + /// Runs one backend against a loopback peer, on a port nobody else holds. fn with_peer(run: impl FnOnce(&str) -> Result) -> Result { let port = free_port(); From 724723b05e22a0561bc6516b1a70e86080930bc6 Mon Sep 17 00:00:00 2001 From: jasisz Date: Fri, 25 Sep 2026 03:32:34 +0200 Subject: [PATCH 2/2] Regenerate the self-host for record fields moved at the last use Co-Authored-By: Claude Opus 5.5 (1M context) --- src/self_host/aver_generated/bytes/mod.rs | 2 +- src/self_host/aver_generated/domain/builtins/mod.rs | 8 ++++---- .../aver_generated/domain/eval/common/mod.rs | 4 +--- .../aver_generated/domain/eval/store/mod.rs | 2 +- .../aver_generated/domain/resolver/calls/mod.rs | 6 +++--- .../aver_generated/domain/resolver/fast/mod.rs | 2 +- src/self_host/aver_generated/domain/resolver/mod.rs | 6 +++--- .../aver_generated/domain/resolver/rewrite/mod.rs | 4 ++-- src/self_host/aver_generated/entry/mod.rs | 12 ++++++------ 9 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/self_host/aver_generated/bytes/mod.rs b/src/self_host/aver_generated/bytes/mod.rs index 7b166ae2d..17160fd29 100644 --- a/src/self_host/aver_generated/bytes/mod.rs +++ b/src/self_host/aver_generated/bytes/mod.rs @@ -208,7 +208,7 @@ pub fn concat(mut left @ _: Bytes, right @ _: &Bytes) -> Bytes { crate::cancel_checkpoint(); crate::aver_generated::bytes::Bytes { values: aver_rt::into_packed_u8(aver_rt::AverIntList::concat( - &(left.values).to_int_list().clone(), + &(left.values).to_int_list(), &(right.values).to_int_list().clone(), )) .expect("proof-packed U8 construction escaped its refinement gate"), diff --git a/src/self_host/aver_generated/domain/builtins/mod.rs b/src/self_host/aver_generated/domain/builtins/mod.rs index b94aeb0c3..8bcbfe063 100644 --- a/src/self_host/aver_generated/domain/builtins/mod.rs +++ b/src/self_host/aver_generated/domain/builtins/mod.rs @@ -1067,11 +1067,11 @@ pub fn builtinTerminalSize( aver_rt::AverList::from_vec(vec![ ( AverStr::from("width"), - crate::aver_generated::domain::value::Val::ValInt(sz.width.clone()), + crate::aver_generated::domain::value::Val::ValInt(sz.width), ), ( AverStr::from("height"), - crate::aver_generated::domain::value::Val::ValInt(sz.height.clone()), + crate::aver_generated::domain::value::Val::ValInt(sz.height), ), ]), )), @@ -2091,11 +2091,11 @@ pub fn httpResponseToVal( aver_rt::AverList::from_vec(vec![ ( AverStr::from("status"), - crate::aver_generated::domain::value::Val::ValInt(resp.status.clone()), + crate::aver_generated::domain::value::Val::ValInt(resp.status), ), ( AverStr::from("body"), - crate::aver_generated::domain::value::Val::ValStr(resp.body.clone()), + crate::aver_generated::domain::value::Val::ValStr(resp.body), ), ( AverStr::from("headers"), diff --git a/src/self_host/aver_generated/domain/eval/common/mod.rs b/src/self_host/aver_generated/domain/eval/common/mod.rs index ab89d5231..a5cb4aad7 100644 --- a/src/self_host/aver_generated/domain/eval/common/mod.rs +++ b/src/self_host/aver_generated/domain/eval/common/mod.rs @@ -22,9 +22,7 @@ pub fn evalVarFallbackNamed( ) -> Result { crate::cancel_checkpoint(); match crate::aver_generated::domain::eval::store::lookupFnOption(fns, name.clone()) { - Some(fd @ _) => Ok(crate::aver_generated::domain::value::Val::ValFnRef( - fd.name.clone(), - )), + Some(fd @ _) => Ok(crate::aver_generated::domain::value::Val::ValFnRef(fd.name)), None => match crate::aver_generated::domain::builtins::splitDotted(name.clone()) { Some(_) => Ok(crate::aver_generated::domain::value::Val::ValVariant( crate::aver_generated::domain::ast::ctorNameToTag(name.clone()), diff --git a/src/self_host/aver_generated/domain/eval/store/mod.rs b/src/self_host/aver_generated/domain/eval/store/mod.rs index 5296594c7..c2fe8ff57 100644 --- a/src/self_host/aver_generated/domain/eval/store/mod.rs +++ b/src/self_host/aver_generated/domain/eval/store/mod.rs @@ -247,7 +247,7 @@ pub fn fnsToIdMap( crate::cancel_checkpoint(); aver_list_match!(fns, [] => { return acc; }, [f, rest] => { { let __tco0 = rest; - let __tco1 = acc.insert_owned(f.name.clone(), idx.clone()); + let __tco1 = acc.insert_owned(f.name, idx.clone()); let __tco2 = idx.add(&aver_rt::AverInt::from_i64(1)); fns = __tco0; acc = __tco1; diff --git a/src/self_host/aver_generated/domain/resolver/calls/mod.rs b/src/self_host/aver_generated/domain/resolver/calls/mod.rs index b26214e92..35f22a99d 100644 --- a/src/self_host/aver_generated/domain/resolver/calls/mod.rs +++ b/src/self_host/aver_generated/domain/resolver/calls/mod.rs @@ -12,7 +12,7 @@ pub fn buildFnMap( crate::cancel_checkpoint(); aver_list_match!(fns, [] => { return acc; }, [f, rest] => { { let __tco0 = rest; - let __tco1 = acc.insert_owned(f.name.clone(), idx.clone()); + let __tco1 = acc.insert_owned(f.name, idx.clone()); let __tco2 = idx.add(&aver_rt::AverInt::from_i64(1)); fns = __tco0; acc = __tco1; @@ -787,7 +787,7 @@ pub fn resolveCallsInArms( crate::cancel_checkpoint(); aver_list_match!(arms, [] => { return acc.reverse(); }, [arm, rest] => { { let __tco0 = rest; - let __tco2 = aver_rt::AverList::prepend(crate::aver_generated::domain::ast::MatchArm { pattern: arm.pattern.clone(), body: crate::aver_generated::domain::resolver::calls::resolveCallsInExpr(&arm.body, &*fnMap), bindingSlots: arm.bindingSlots.clone() }, &acc); + let __tco2 = aver_rt::AverList::prepend(crate::aver_generated::domain::ast::MatchArm { pattern: arm.pattern, body: crate::aver_generated::domain::resolver::calls::resolveCallsInExpr(&arm.body, &*fnMap), bindingSlots: arm.bindingSlots }, &acc); arms = __tco0; acc = __tco2; continue; @@ -867,7 +867,7 @@ pub fn resolveCallsInArms__collected( crate::cancel_checkpoint(); aver_list_match!(arms, [] => { return aver_rt::list_builder_finalize(acc); }, [arm, rest] => { { let __tco0 = rest; - let __tco2 = aver_rt::list_builder_push(acc, crate::aver_generated::domain::ast::MatchArm { pattern: arm.pattern.clone(), body: crate::aver_generated::domain::resolver::calls::resolveCallsInExpr(&arm.body, &*fnMap), bindingSlots: arm.bindingSlots.clone() }); + let __tco2 = aver_rt::list_builder_push(acc, crate::aver_generated::domain::ast::MatchArm { pattern: arm.pattern, body: crate::aver_generated::domain::resolver::calls::resolveCallsInExpr(&arm.body, &*fnMap), bindingSlots: arm.bindingSlots }); arms = __tco0; acc = __tco2; continue; diff --git a/src/self_host/aver_generated/domain/resolver/fast/mod.rs b/src/self_host/aver_generated/domain/resolver/fast/mod.rs index 60da94e9e..d419a932c 100644 --- a/src/self_host/aver_generated/domain/resolver/fast/mod.rs +++ b/src/self_host/aver_generated/domain/resolver/fast/mod.rs @@ -122,7 +122,7 @@ pub fn armsNeedTailLoop( ) -> bool { loop { crate::cancel_checkpoint(); - aver_list_match!(arms, [] => { return false; }, [arm, rest] => { if crate::aver_generated::domain::resolver::fast::exprNeedsTailLoop(selfId.clone(), arm.body.clone()) { return true; } else { { + aver_list_match!(arms, [] => { return false; }, [arm, rest] => { if crate::aver_generated::domain::resolver::fast::exprNeedsTailLoop(selfId.clone(), arm.body) { return true; } else { { let __tco1 = rest; arms = __tco1; continue; diff --git a/src/self_host/aver_generated/domain/resolver/mod.rs b/src/self_host/aver_generated/domain/resolver/mod.rs index d755922b2..e67151d90 100644 --- a/src/self_host/aver_generated/domain/resolver/mod.rs +++ b/src/self_host/aver_generated/domain/resolver/mod.rs @@ -7,7 +7,7 @@ pub fn resolveProgram( ) -> crate::aver_generated::domain::ast::Program { crate::cancel_checkpoint(); let resolvedFns @ _ = crate::aver_generated::domain::resolver::core::resolveFns( - prog.fns.clone(), + prog.fns, aver_rt::AverList::empty(), ); let fnMap @ _ = crate::aver_generated::domain::resolver::calls::buildFnMap( @@ -26,13 +26,13 @@ pub fn resolveProgram( aver_rt::AverList::empty(), ); crate::aver_generated::domain::ast::Program { - deps: prog.deps.clone(), + deps: prog.deps, fns: crate::aver_generated::domain::resolver::rewrite::rewriteInternalFns( annotatedFns, aver_rt::AverList::empty(), ), stmts: crate::aver_generated::domain::resolver::rewrite::rewriteInternalStmts( - prog.stmts.clone(), + prog.stmts, aver_rt::AverList::empty(), ), } diff --git a/src/self_host/aver_generated/domain/resolver/rewrite/mod.rs b/src/self_host/aver_generated/domain/resolver/rewrite/mod.rs index 20613d722..15b7e1560 100644 --- a/src/self_host/aver_generated/domain/resolver/rewrite/mod.rs +++ b/src/self_host/aver_generated/domain/resolver/rewrite/mod.rs @@ -436,7 +436,7 @@ pub fn rewriteInternalArms( crate::cancel_checkpoint(); aver_list_match!(arms, [] => { return acc.reverse(); }, [arm, rest] => { { let __tco0 = rest; - let __tco1 = aver_rt::AverList::prepend(crate::aver_generated::domain::ast::MatchArm { pattern: crate::aver_generated::domain::resolver::rewrite::rewritePattern(&arm.pattern), body: crate::aver_generated::domain::resolver::rewrite::rewriteInternalExpr(&arm.body), bindingSlots: arm.bindingSlots.clone() }, &acc); + let __tco1 = aver_rt::AverList::prepend(crate::aver_generated::domain::ast::MatchArm { pattern: crate::aver_generated::domain::resolver::rewrite::rewritePattern(&arm.pattern), body: crate::aver_generated::domain::resolver::rewrite::rewriteInternalExpr(&arm.body), bindingSlots: arm.bindingSlots }, &acc); arms = __tco0; acc = __tco1; continue; @@ -1271,7 +1271,7 @@ pub fn rewriteInternalArms__collected( crate::cancel_checkpoint(); aver_list_match!(arms, [] => { return aver_rt::list_builder_finalize(acc); }, [arm, rest] => { { let __tco0 = rest; - let __tco1 = aver_rt::list_builder_push(acc, crate::aver_generated::domain::ast::MatchArm { pattern: crate::aver_generated::domain::resolver::rewrite::rewritePattern(&arm.pattern), body: crate::aver_generated::domain::resolver::rewrite::rewriteInternalExpr(&arm.body), bindingSlots: arm.bindingSlots.clone() }); + let __tco1 = aver_rt::list_builder_push(acc, crate::aver_generated::domain::ast::MatchArm { pattern: crate::aver_generated::domain::resolver::rewrite::rewritePattern(&arm.pattern), body: crate::aver_generated::domain::resolver::rewrite::rewriteInternalExpr(&arm.body), bindingSlots: arm.bindingSlots }); arms = __tco0; acc = __tco1; continue; diff --git a/src/self_host/aver_generated/entry/mod.rs b/src/self_host/aver_generated/entry/mod.rs index d231b493b..31bdb59c3 100644 --- a/src/self_host/aver_generated/entry/mod.rs +++ b/src/self_host/aver_generated/entry/mod.rs @@ -324,14 +324,14 @@ pub fn shiftFnIdsInProgram( ) -> crate::aver_generated::domain::ast::Program { crate::cancel_checkpoint(); crate::aver_generated::domain::ast::Program { - deps: prog.deps.clone(), + deps: prog.deps, fns: shiftFnIdsInFns__collected( - prog.fns.clone(), + prog.fns, offset.clone(), aver_rt::list_builder_new((aver_rt::AverInt::from_i64(0)).to_usize().unwrap_or(0)), ), stmts: shiftFnIdsInStmts__collected( - prog.stmts.clone(), + prog.stmts, offset, aver_rt::list_builder_new((aver_rt::AverInt::from_i64(0)).to_usize().unwrap_or(0)), ), @@ -733,7 +733,7 @@ pub fn shiftFnIdsInArms( crate::cancel_checkpoint(); aver_list_match!(arms, [] => { return acc.reverse(); }, [arm, rest] => { { let __tco0 = rest; - let __tco2 = aver_rt::AverList::prepend(crate::aver_generated::domain::ast::MatchArm { pattern: arm.pattern.clone(), body: shiftFnIdsInExpr(&arm.body, offset.clone()), bindingSlots: arm.bindingSlots.clone() }, &acc); + let __tco2 = aver_rt::AverList::prepend(crate::aver_generated::domain::ast::MatchArm { pattern: arm.pattern, body: shiftFnIdsInExpr(&arm.body, offset.clone()), bindingSlots: arm.bindingSlots }, &acc); arms = __tco0; acc = __tco2; continue; @@ -1211,7 +1211,7 @@ pub fn loadOneModule__indexed( let prog @ _ = crate::aver_generated::domain::parser::parse(&tokens)?; let moduleFns @ _ = resolveQualifiedModuleFns__indexed(&prog, dep.clone(), __str_index); let loaded2 @ _ = loaded.clone().insert_owned(dep, true); - let innerResult @ _ = loadModules(prog.deps.clone(), moduleRoot.clone(), acc.clone(), loaded2)?; + let innerResult @ _ = loadModules(prog.deps, moduleRoot.clone(), acc.clone(), loaded2)?; { let (accWithInner, loaded3) = innerResult; loadModules( @@ -1447,7 +1447,7 @@ pub fn shiftFnIdsInArms__collected( crate::cancel_checkpoint(); aver_list_match!(arms, [] => { return aver_rt::list_builder_finalize(acc); }, [arm, rest] => { { let __tco0 = rest; - let __tco2 = aver_rt::list_builder_push(acc, crate::aver_generated::domain::ast::MatchArm { pattern: arm.pattern.clone(), body: shiftFnIdsInExpr(&arm.body, offset.clone()), bindingSlots: arm.bindingSlots.clone() }); + let __tco2 = aver_rt::list_builder_push(acc, crate::aver_generated::domain::ast::MatchArm { pattern: arm.pattern, body: shiftFnIdsInExpr(&arm.body, offset.clone()), bindingSlots: arm.bindingSlots }); arms = __tco0; acc = __tco2; continue;