From 5c6947f15fbf54079340dac1b1e0e5620affa170 Mon Sep 17 00:00:00 2001 From: Steve Klabnik Date: Wed, 5 Aug 2026 14:13:08 -0500 Subject: [PATCH] RUE-1208: splice borrow accessors at CFG threshold --- crates/rue-air/src/inst.rs | 72 +++- crates/rue-air/src/sema/analysis/ownership.rs | 294 ++++----------- crates/rue-air/src/sema/body_identity.rs | 11 +- crates/rue-air/src/sema/call_resolution.rs | 20 +- crates/rue-air/src/sema/context.rs | 7 - crates/rue-air/src/sema/control_flow.rs | 37 +- crates/rue-air/src/sema/fact_mode.rs | 3 - crates/rue-air/src/sema/ordinary_engine.rs | 9 +- crates/rue-air/src/sema/output.rs | 1 + crates/rue-air/src/sema/provider_body_host.rs | 23 -- .../rue-air/src/sema/semantic_body_export.rs | 17 +- crates/rue-air/src/sema/tests.rs | 40 +- crates/rue-air/src/semantic_body.rs | 36 +- crates/rue-air/src/semantic_import.rs | 36 ++ crates/rue-cfg/src/build.rs | 26 ++ crates/rue-cfg/src/inline.rs | 300 +++++++++++---- crates/rue-cfg/src/inst.rs | 56 ++- crates/rue-cfg/src/opt/dce.rs | 8 +- crates/rue-cfg/src/opt/forward.rs | 5 +- crates/rue-cfg/src/verify.rs | 25 +- .../rue-cli-tests/cases/borrow_accessors.toml | 74 ++++ crates/rue-codegen/src/cfg_lower.rs | 4 + crates/rue-codegen/src/param_storage.rs | 3 + crates/rue-codegen/src/value_plan.rs | 4 + crates/rue-compiler/src/api_inventory.rs | 2 +- crates/rue-compiler/src/artifact_views.rs | 1 + crates/rue-compiler/src/cfg_query.rs | 344 +++++++++++++++++- crates/rue-compiler/src/drop_glue.rs | 1 + crates/rue-compiler/src/durable_cfg.rs | 275 +++++++++++++- .../src/local_semantic_materialization.rs | 109 +++++- crates/rue-compiler/src/queries.rs | 118 +++++- crates/rue-compiler/src/retained_charge.rs | 3 + .../src/revisioned_query_database.rs | 9 +- .../src/semantic_query_nucleus.rs | 2 + crates/rue-compiler/src/session.rs | 50 ++- crates/rue-compiler/src/unstable.rs | 133 ++++++- crates/rue-oracle/src/lib.rs | 29 ++ .../cases/items/borrow-accessors.toml | 108 +++++- crates/rue-spec/src/traceability.rs | 34 -- .../cases/diagnostics/borrow-accessors.toml | 26 ++ 40 files changed, 1865 insertions(+), 490 deletions(-) create mode 100644 crates/rue-cli-tests/cases/borrow_accessors.toml create mode 100644 crates/rue-ui-tests/cases/diagnostics/borrow-accessors.toml diff --git a/crates/rue-air/src/inst.rs b/crates/rue-air/src/inst.rs index eb0132266..d19816b1b 100644 --- a/crates/rue-air/src/inst.rs +++ b/crates/rue-air/src/inst.rs @@ -441,7 +441,7 @@ impl AirPlace { if self.projections.is_empty() { match self.base { AirPlaceBase::Local(slot) => Some(slot), - AirPlaceBase::Param(_) => None, + AirPlaceBase::Param(_) | AirPlaceBase::Accessor(_) => None, } } else { None @@ -454,7 +454,7 @@ impl AirPlace { if self.projections.is_empty() { match self.base { AirPlaceBase::Param(slot) => Some(slot), - AirPlaceBase::Local(_) => None, + AirPlaceBase::Local(_) | AirPlaceBase::Accessor(_) => None, } } else { None @@ -546,6 +546,10 @@ pub enum AirPlaceBase { Local(u32), /// Parameter slot (for parameters, including inout) Param(u32), + /// Result of a mandatory-inline `-> borrow T` accessor call. This is a + /// second-class place producer, not a value-producing ABI call; CFG + /// construction preserves it only until the accessor CFG splice. + Accessor(AirRef), } /// A projection applied to a place to reach a nested location. @@ -1275,6 +1279,16 @@ impl AirEditor { self.air.add_call(runtime, name, args, ty, span) } + pub fn add_accessor_call( + &mut self, + name: Spur, + args: &[AirCallArg], + ty: Type, + span: Span, + ) -> Result { + self.air.add_accessor_call(name, args, ty, span) + } + pub fn add_call_generic( &mut self, name: Spur, @@ -1552,6 +1566,23 @@ impl Air { format!("place reference {place_ref} is outside the place store"), ) })?; + if let AirPlaceBase::Accessor(call) = place.base { + check_ref(call)?; + if !matches!(self.get(call).data, AirInstData::AccessorCall { .. }) { + return Err(fail( + Some(index), + format!("place {place_ref} has a non-accessor producer {call}"), + )); + } + if self.get(call).ty != place.base_type { + return Err(fail( + Some(index), + format!( + "place {place_ref} accessor base type does not match its producer" + ), + )); + } + } let mut current = place.base_type; for projection in self.get_place_projections(place) { current = match *projection { @@ -1760,7 +1791,7 @@ impl Air { } } } - AirInstData::Call { name, args, .. } => { + AirInstData::Call { name, args, .. } | AirInstData::AccessorCall { name, args } => { context .validate_symbol(*name) .map_err(|reason| fail(Some(index), reason))?; @@ -2193,6 +2224,23 @@ impl Air { })) } + pub(crate) fn add_accessor_call( + &mut self, + name: Spur, + args: &[AirCallArg], + ty: Type, + span: Span, + ) -> Result { + self.preflight_refs("accessor call arguments", args.iter().map(|arg| arg.value))?; + self.reserve_instruction("accessor call arguments")?; + let args = self.add_call_args(args)?; + Ok(self.push_inst(AirInst { + data: AirInstData::AccessorCall { name, args }, + ty, + span, + })) + } + pub(crate) fn add_call_generic( &mut self, name: Spur, @@ -3397,6 +3445,10 @@ pub enum AirInstData { args: AirCallArgs, }, + /// Mandatory-inline place-producing accessor call (ADR-0062/RUE-1208). + /// Its result may only be used as [`AirPlaceBase::Accessor`]. + AccessorCall { name: Spur, args: AirCallArgs }, + /// Generic function call - requires specialization before codegen. /// /// This is emitted when calling a function with `comptime` parameters @@ -3713,6 +3765,19 @@ impl Air { } writeln!(f, ")")?; } + AirInstData::AccessorCall { name, args } => { + match interner { + Some(interner) => write!(f, "accessor_call @{}(", interner.resolve(name))?, + None => write!(f, "accessor_call @{}(", name.into_usize())?, + } + for (i, arg) in self.get_call_args(args).enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", arg)?; + } + writeln!(f, ")")?; + } AirInstData::CallGeneric { name, type_args, @@ -3914,6 +3979,7 @@ impl Air { match place.base { AirPlaceBase::Local(slot) => write!(f, "${}", slot)?, AirPlaceBase::Param(slot) => write!(f, "param%{}", slot)?, + AirPlaceBase::Accessor(call) => write!(f, "accessor%{}", call.as_u32())?, } // Write the projections diff --git a/crates/rue-air/src/sema/analysis/ownership.rs b/crates/rue-air/src/sema/analysis/ownership.rs index ea47e5b6c..b4e59f9eb 100644 --- a/crates/rue-air/src/sema/analysis/ownership.rs +++ b/crates/rue-air/src/sema/analysis/ownership.rs @@ -133,7 +133,7 @@ struct ProjectionInfo { /// This contains all the information needed to build an `AirPlace` and emit /// a `PlaceRead` or `PlaceWrite` instruction. #[derive(Debug)] -pub(super) struct PlaceTrace { +pub(crate) struct PlaceTrace { /// The base of the place (local slot or param slot) base: AirPlaceBase, /// The type of the base (before projections) @@ -166,7 +166,7 @@ pub(super) struct MoveStateSnapshot { impl PlaceTrace { /// Get the final type of the place (after all projections). - pub(super) fn result_type(&self) -> Type { + pub(crate) fn result_type(&self) -> Type { self.projections .last() .map(|p| p.result_type) @@ -867,7 +867,7 @@ impl OrdinaryBodyEngine<'_, H> { /// # Returns /// * `Some(PlaceTrace)` if the expression is a place /// * `None` if it's not (e.g., `get_struct().field` where base is a call) - pub(super) fn try_trace_place( + pub(crate) fn try_trace_place( &mut self, inst_ref: InstRef, air: &mut Air, @@ -1132,25 +1132,16 @@ impl OrdinaryBodyEngine<'_, H> { .map(Some) } - /// Inline a `-> borrow T` accessor call at its call site (ADR-0062 §3). + /// Lower a `-> borrow T` accessor call to a marked place-producing call. /// - /// The call compiles to the accessor body's guards followed by the - /// address computation of the yielded place — no call is emitted, no - /// calling convention for "returning a place" exists (the RUE-1012 - /// forward-compatibility contract). Statically this is the + /// Semantic analysis establishes only the call-site loan and escape + /// contract. The callee body remains an exact CFG-query dependency and is + /// mandatorily spliced before code generation; no calling convention for + /// returning a place exists. Statically this is the /// (Accessor-Call) rule: the receiver must be a place; the result is a /// second-class borrowed place whose loan `(root(receiver), shared)` /// spans the enclosing full expression (registered in - /// `ctx.expression_loans`); and the accessor body must be well-formed — - /// guards may diverge, and the single trailing `yield` names a place - /// rooted at the receiver. - /// - /// Expansion is acyclic (6.6:14, E0261): while an accessor's body is - /// being inlined it is recorded on `ctx.accessor_expansion_stack`, and a - /// call naming an accessor already on that stack is rejected — inlining a - /// cycle has no fixed point. The marker covers the body only; the - /// receiver and argument traces are caller syntax, so a finite chain like - /// `v.get_ref(i).get_ref(j)` is unaffected. + /// `ctx.expression_loans`). #[allow(clippy::too_many_arguments)] fn expand_accessor_call( &mut self, @@ -1163,39 +1154,39 @@ impl OrdinaryBodyEngine<'_, H> { span: Span, ctx: &mut AnalysisContext, ) -> CompileResult { - if ctx.accessor_expansion_stack.contains(&(struct_id, method)) { + let info = self + .call_facts() + .method_info(struct_id, method) + .expect("accessor expansion follows a successful method lookup"); + let method_name_str = self.body_interner().resolve(&method).to_string(); + let call_name = self.method_symbol(struct_id, &method_name_str, true); + let call_name = self.body_interner().get_or_intern(&call_name); + if self.function_identity(call_name).is_ok_and(|identity| { + ctx.canonical_function_identity == crate::FunctionInstanceKey::Definition(identity) + }) { return Err(CompileError::new( ErrorKind::AccessorRecursion { - method: self.body_interner().resolve(&method).to_string(), + method: method_name_str.clone(), }, span, ) .with_note( - "an accessor call is expanded by inlining its body at the call site, so a cycle of accessor calls has no finite expansion", + "an accessor call is expanded by mandatory CFG splicing, so a cycle of accessor calls has no finite expansion", )); } - let info = self - .call_facts() - .method_info(struct_id, method) - .expect("accessor expansion follows a successful method lookup"); - let method_name_str = self.body_interner().resolve(&method).to_string(); - // The receiver of an accessor call must be a place (E0427): the - // accessor's body is inlined against the receiver's storage, so there - // is nothing for borrow-operand elaboration (RUE-953) to loan. It is - // read through a shared borrow, never moved. + // RUE-1208: semantic analysis establishes the accessor's place/loan + // contract, but does not inspect or expand the callee body. The + // marked place-producing call survives through canonical AIR so the + // per-function CFG query can depend on and splice the callee CFG. let receiver_span = self.body_rir_ref().get(receiver).span; - let receiver_trace = { + let mut receiver_trace = { let prev_byref_root = ctx.byref_arg_root.take(); let trace = self.try_trace_place(receiver, air, ctx); ctx.byref_arg_root = prev_byref_root; trace?.ok_or_else(|| CompileError::new(ErrorKind::BorrowNonLvalue, receiver_span))? }; let root = receiver_trace.root_var; - - // (Accessor-Call) exclusivity: the shared loan this call takes on the - // receiver's root conflicts with any exclusive loan already active in - // an enclosing call's argument list (`g(inout v, v.get_ref(i))`). for frame in &ctx.call_loaned_roots { if frame .iter() @@ -1214,203 +1205,40 @@ impl OrdinaryBodyEngine<'_, H> { if receiver_ty.as_struct() != Some(struct_id) { return Err(self.type_mismatch_error(Type::new_struct(struct_id), receiver_ty, span)); } - - // Register the loan for the enclosing full expression and record the - // call for the escape-shape checks. ctx.expression_loans.push((root, span)); ctx.accessor_call_insts.insert(inst_ref, (method, root)); + ctx.referenced_methods.insert((struct_id, method)); + self.record_body_method_dependency((struct_id, method)); - // Guard statements accumulate here, starting with anything the - // receiver trace itself carried (a nested accessor receiver). - let mut guard_stmts = receiver_trace.pending_stmts.clone(); - - // Analyze the explicit arguments as by-value guard inputs (the - // declaration gate rejects every other parameter mode, E0260). let param_data = self.body_param_data(info.params); let param_types = param_data.types().to_vec(); let param_modes = param_data.modes().to_vec(); - let param_names = param_data.names().to_vec(); self.validate_call_contract_for_accessor(args_range, ¶m_types, ¶m_modes, span)?; - // Every accessor parameter is by-value, so no operand here can produce - // a borrow-operand temporary (RUE-953); the guard statements below are - // the accessor's own storage. - let air_args = self - .analyze_call_operands(air, args_range, ¶m_types, ¶m_modes, false, ctx)? - .args; - - // Run inference for the accessor body so the caller's analysis can - // walk instructions the caller's own inference never visited. The - // overlay is popped once expansion completes. - let struct_type = Type::new_struct(struct_id); - let self_sym = self.body_interner().get_or_intern("self"); - let mut infer_params: Vec<(Spur, Type, RirParamMode, bool)> = - vec![(self_sym, struct_type, RirParamMode::Borrow, false)]; - for (index, name) in param_names.iter().enumerate() { - infer_params.push((*name, param_types[index], RirParamMode::Normal, false)); - } - let Some((body, accessor_decl_span)) = self.call_facts().accessor_body(struct_id, method) - else { - return Err(CompileError::new( - ErrorKind::InternalError(format!( - "accessor `{method_name_str}` has no resolvable body to inline" - )), - span, - )); - }; - let overlay = self.run_type_inference( - ctx.infer_ctx, - info.return_type, - &infer_params, - body, - None, - None, - )?; - ctx.inline_resolved_types.push(std::sync::Arc::new(overlay)); - - // Bind the accessor's value parameters as fresh caller locals - // initialized from the analyzed arguments, and `self` as a place - // alias of the receiver. The inline scope shadows caller names; the - // accessor body only names its own bindings (its standalone analysis - // rejects anything else). - ctx.push_scope(); - for (index, name) in param_names.iter().enumerate() { - let ty = param_types[index]; - let (slot, live, alloc) = - self.allocate_local_storage(air, air_args[index].value, ty, span, ctx)?; - guard_stmts.push(live); - guard_stmts.push(alloc); - ctx.insert_local( - *name, - LocalVar { - slot, - ty, - is_mut: false, - span, - allow_unused: true, - }, - ); - } - let alias = super::super::context::PlaceAlias { - base: receiver_trace.base, - base_type: receiver_trace.base_type, - projections: receiver_trace - .projections - .iter() - .map(|p| super::super::context::AliasProjection { - proj: p.proj, - result_type: p.result_type, - field_name: p.field_name, - const_index: p.const_index, - index_segment: p.index_segment, - }) - .collect(), - root_var: root, - }; - let saved_alias = ctx.place_aliases.insert(self_sym, alias); - - // Locate the body's guard statements and trailing yield. The shape - // (single trailing `yield`) is the accessor's well-formedness rule; - // it is re-checked here because a caller may analyze before the - // accessor's own standalone analysis runs. This accessor is on the - // in-progress stack for exactly the body's analysis, so a call it - // makes back into an enclosing accessor is E0261 rather than an - // unbounded re-expansion. - ctx.accessor_expansion_stack.push((struct_id, method)); - let expansion = (|| -> CompileResult { - // A single-statement body lowers to the instruction itself. - let body_insts = match &self.body_rir_ref().get(body).data { - InstData::Block { instructions } => { - self.body_rir_ref().block_insts(instructions).to_vec() - } - _ => vec![body], - }; - let (trailing, guards) = match body_insts.split_last() { - Some((trailing, guards)) - if matches!(self.body_rir_ref().get(*trailing).data, InstData::Yield(_)) => - { - (*trailing, guards.to_vec()) - } - _ => { - return Err(CompileError::new( - ErrorKind::AccessorBodyMissingYield, - accessor_decl_span, - )); - } - }; - - // Analyze the guards as ordinary statements in the caller's AIR. - // A nested `yield` dispatches against the trailing reference and - // is rejected (E0254); a `return` or `?` is likewise rejected by - // the accessor-body checks in control-flow analysis. - let prev_trailing = ctx.accessor_trailing_yield.replace(trailing); - for guard in guards { - let result = - ctx.with_expected_type(None, |ctx| self.analyze_inst(air, guard, ctx))?; - self.reject_discarded_linear_value(result.ty, guard)?; - guard_stmts.push(result.air_ref); - } - ctx.accessor_trailing_yield = prev_trailing; - - // The trailing yield's operand is the place the call becomes. - let InstData::Yield(yield_operand) = self.body_rir_ref().get(trailing).data else { - unreachable!("trailing accessor instruction was checked to be a yield"); - }; - let yield_span = self.body_rir_ref().get(trailing).span; - let mut result_trace = - self.try_trace_place(yield_operand, air, ctx)? - .ok_or_else(|| { - CompileError::new( - ErrorKind::AccessorYieldNotReceiverRooted { - found: "a value expression".to_string(), - }, - yield_span, - ) - })?; - if result_trace.root_var != root { - return Err(CompileError::new( - ErrorKind::AccessorYieldNotReceiverRooted { - found: format!( - "a place rooted at `{}`", - self.body_interner().resolve(&result_trace.root_var) - ), - }, - yield_span, - )); - } - let result_ty = result_trace.result_type(); - if !result_ty.is_error() - && !info.return_type.is_error() - && !self.types_compatible(result_ty, info.return_type) - { - return Err(self.type_mismatch_error(info.return_type, result_ty, yield_span)); - } - - // The composed place is a second-class shared borrow rooted at - // the caller's receiver root, prefixed by the guards. - let mut pending = std::mem::take(&mut guard_stmts); - pending.append(&mut result_trace.pending_stmts); - result_trace.pending_stmts = pending; - result_trace.via_accessor = true; - result_trace.is_borrow_param = true; - result_trace.is_root_mutable = false; - Ok(result_trace) - })(); - - // Unwind the inline scope regardless of outcome. - ctx.accessor_expansion_stack.pop(); - match saved_alias { - Some(alias) => { - ctx.place_aliases.insert(self_sym, alias); - } - None => { - ctx.place_aliases.remove(&self_sym); - } - } - self.check_unused_locals_in_current_scope(ctx); - ctx.pop_scope(); - ctx.inline_resolved_types.pop(); - - expansion + let operands = + self.analyze_call_operands(air, args_range, ¶m_types, ¶m_modes, false, ctx)?; + let receiver_place = Self::build_place_ref(air, &receiver_trace)?; + let receiver_value = air.add_inst(AirInst { + data: AirInstData::PlaceRead { + place: receiver_place, + }, + ty: receiver_ty, + span: receiver_span, + }); + let mut call_args = Vec::with_capacity(operands.args.len() + 1); + call_args.push(AirCallArg { + value: receiver_value, + mode: AirArgMode::Borrow, + }); + call_args.extend(operands.args); + let call = air.add_accessor_call(call_name, &call_args, info.return_type, span)?; + receiver_trace.base = AirPlaceBase::Accessor(call); + receiver_trace.base_type = info.return_type; + receiver_trace.projections.clear(); + receiver_trace.pending_stmts.extend(operands.temp_scope); + receiver_trace.via_accessor = true; + receiver_trace.is_borrow_param = true; + receiver_trace.is_root_mutable = false; + Ok(receiver_trace) } /// Analyze a `-> borrow T` accessor call in value position (ADR-0062): @@ -1530,7 +1358,7 @@ impl OrdinaryBodyEngine<'_, H> { } /// Build an AirPlaceRef from a PlaceTrace, adding projections to the Air. - pub(super) fn build_place_ref(air: &mut Air, trace: &PlaceTrace) -> CompileResult { + pub(crate) fn build_place_ref(air: &mut Air, trace: &PlaceTrace) -> CompileResult { let projs = trace.projections.iter().map(|p| p.proj); Ok(air.make_place(trace.base, trace.base_type, projs)?) } @@ -2862,6 +2690,7 @@ impl OrdinaryBodyEngine<'_, H> { .params .iter() .any(|p| p.name == trace.root_var && p.mode == RirParamMode::Normal), + AirPlaceBase::Accessor(_) => false, }; if is_droppable_param_base { emit_move_marker = true; @@ -2901,6 +2730,9 @@ impl OrdinaryBodyEngine<'_, H> { let (slot, is_param) = match trace.base { AirPlaceBase::Local(slot) => (slot, false), AirPlaceBase::Param(slot) => (slot, true), + AirPlaceBase::Accessor(_) => { + unreachable!("accessor places never receive move markers") + } }; let marker_place = move_is_partial .then(|| Self::build_move_marker_place_ref(air, &trace, span)) @@ -3812,6 +3644,9 @@ impl OrdinaryBodyEngine<'_, H> { span, )); } + AirPlaceBase::Accessor(_) => { + unreachable!("accessor places are classified as borrowed") + } } } @@ -3978,6 +3813,9 @@ impl OrdinaryBodyEngine<'_, H> { span, )); } + AirPlaceBase::Accessor(_) => { + unreachable!("accessor places are classified as borrowed") + } } } @@ -4534,6 +4372,7 @@ impl OrdinaryBodyEngine<'_, H> { .params .iter() .any(|p| p.name == trace.root_var && p.mode == RirParamMode::Normal), + AirPlaceBase::Accessor(_) => false, }; if !droppable_base { return Ok(value); @@ -4541,6 +4380,9 @@ impl OrdinaryBodyEngine<'_, H> { let (slot, is_param) = match trace.base { AirPlaceBase::Local(slot) => (slot, false), AirPlaceBase::Param(slot) => (slot, true), + AirPlaceBase::Accessor(_) => { + unreachable!("accessor places never receive element move markers") + } }; // A dedicated Const instruction keeps the marker's index resolvable // even when the source index expression was a folded constant diff --git a/crates/rue-air/src/sema/body_identity.rs b/crates/rue-air/src/sema/body_identity.rs index 555dd335a..452c74698 100644 --- a/crates/rue-air/src/sema/body_identity.rs +++ b/crates/rue-air/src/sema/body_identity.rs @@ -790,6 +790,7 @@ struct MethodSignature { self_mode: RirParamMode, params: ParamRange, return_type: Type, + returns_borrow: bool, } fn anonymous_nominal_keys_canonically_equal( @@ -2131,6 +2132,7 @@ pub struct DurableMethod { pub result: SemanticImportType, pub has_self: bool, pub self_mode: SemanticParameterMode, + pub is_accessor: bool, } /// The durable callable vocabulary the pool consults to mint callable @@ -2286,11 +2288,7 @@ where self_mode: signature.self_mode, params: signature.params, return_type: signature.return_type, - // The durable callable signature does not carry the accessor - // flag; call sites that need it resolve the full method record - // (with its RIR handle) instead, so this signature-only subset - // stays conservative. - returns_borrow: false, + returns_borrow: signature.returns_borrow, }) } @@ -2349,6 +2347,7 @@ where result, has_self, self_mode, + is_accessor, } = self .source .method(key) @@ -2368,6 +2367,7 @@ where }, params, return_type, + returns_borrow: is_accessor, }; self.method_sigs.insert(key.clone(), signature); Ok(signature) @@ -3375,6 +3375,7 @@ mod tests { result, has_self, self_mode, + is_accessor: false, } } diff --git a/crates/rue-air/src/sema/call_resolution.rs b/crates/rue-air/src/sema/call_resolution.rs index 8b818fe9c..1fba6e5d5 100644 --- a/crates/rue-air/src/sema/call_resolution.rs +++ b/crates/rue-air/src/sema/call_resolution.rs @@ -20,7 +20,7 @@ use std::hash::Hash; use lasso::Spur; use rue_rir::{InstData, InstRef}; -use rue_span::{FileId, Span}; +use rue_span::FileId; use super::body_identity::{ BodyRirView, DurableCallableSource, DurableNominalSource, FunctionIdentityHandle, @@ -69,14 +69,6 @@ pub(crate) trait CallResolutionFacts { /// anonymous table then the named table. Mirrors `Sema::method_info`. fn method_info(&self, struct_id: StructId, name: Spur) -> Option; - /// The body handle and declaration span of a `-> borrow T` accessor - /// method (ADR-0062). Accessor calls are required-inlineable: the call - /// site splices the body's guards and yielded place instead of emitting a - /// call, so — uniquely among call facts — the accessor's RIR body is part - /// of the call-site contract. `None` for non-accessors and for owners - /// whose bodies are not resolvable in this host. - fn accessor_body(&self, struct_id: StructId, name: Spur) -> Option<(InstRef, Span)>; - /// The named-method RIR declaration for the durable-available /// `(owner_file, owner_type_name, method_name)` preimage. Mirrors /// `structs_by_file_name.get` followed by `named_method_declarations.get`. @@ -102,7 +94,6 @@ pub(super) trait CallResolutionFactSource { fn call_value_const(&self, file: FileId, name: Spur) -> Option; fn call_module_binding(&self, file: FileId, name: Spur) -> Option; fn call_method_info(&self, struct_id: StructId, name: Spur) -> Option; - fn call_accessor_body(&self, struct_id: StructId, name: Spur) -> Option<(InstRef, Span)>; fn call_named_method_declaration( &self, file: FileId, @@ -151,12 +142,6 @@ impl CallResolutionFactSource for Sema<'_, D> { .map(MethodCallInfo::from_body) } - fn call_accessor_body(&self, struct_id: StructId, name: Spur) -> Option<(InstRef, Span)> { - self.method_info((struct_id, name)) - .filter(|info| info.returns_borrow) - .map(|info| (info.body, info.span)) - } - fn call_named_method_declaration( &self, owner_file: FileId, @@ -212,9 +197,6 @@ impl CallResolutionFacts for EpochFac fn method_info(&self, struct_id: StructId, name: Spur) -> Option { self.host.call_method_info(struct_id, name) } - fn accessor_body(&self, struct_id: StructId, name: Spur) -> Option<(InstRef, Span)> { - self.host.call_accessor_body(struct_id, name) - } fn named_method_declaration(&self, file: FileId, ty: Spur, method: Spur) -> Option { self.host.call_named_method_declaration(file, ty, method) } diff --git a/crates/rue-air/src/sema/context.rs b/crates/rue-air/src/sema/context.rs index 787019ee1..4877e288e 100644 --- a/crates/rue-air/src/sema/context.rs +++ b/crates/rue-air/src/sema/context.rs @@ -521,12 +521,6 @@ pub(crate) struct AnalysisContext<'a> { /// consult this after analyzing an operand to reject binding a borrowed /// place beyond its full expression, naming the offending accessor. pub accessor_call_insts: HashMap, - /// Accessors whose inline expansion is in progress, outermost first. An - /// accessor call compiles by inlining its body (ADR-0062), so a call that - /// names an accessor already on this stack has no finite expansion and is - /// E0261 — the acyclicity rule that makes required-inlineability - /// well-founded. - pub accessor_expansion_stack: Vec<(StructId, Spur)>, /// Active accessor-result loans for the current full expression /// (ADR-0062): each entry is the receiver root of an expanded accessor /// call, shared mode, together with the call span. The statement loop @@ -754,7 +748,6 @@ impl<'a> AnalysisContext<'a> { infer_ctx: self.infer_ctx, accessor_trailing_yield: self.accessor_trailing_yield, accessor_call_insts: self.accessor_call_insts.clone(), - accessor_expansion_stack: self.accessor_expansion_stack.clone(), expression_loans: self.expression_loans.clone(), inline_resolved_types: self.inline_resolved_types.clone(), place_aliases: self.place_aliases.clone(), diff --git a/crates/rue-air/src/sema/control_flow.rs b/crates/rue-air/src/sema/control_flow.rs index 97a658e20..789c2a21d 100644 --- a/crates/rue-air/src/sema/control_flow.rs +++ b/crates/rue-air/src/sema/control_flow.rs @@ -1811,11 +1811,28 @@ impl OrdinaryBodyEngine<'_, H> { // downstream ownership error. self.check_yield_rooted_at_receiver(operand, ctx)?; - // Read the place non-consumingly: this type-checks the projection - // (including index expressions) and marks its variables used. The - // read is emitted as a statement of the trap block below so the AIR - // stays fully referenced. - let read = self.analyze_inst_for_projection(air, operand, ctx)?; + // Preserve the yielded receiver projection as the accessor CFG's + // distinguished return operand. The mandatory CFG splice consumes + // this `PlaceRead` as a place descriptor before codegen; no accessor + // return ABI exists (RUE-1208). + let trace = self.try_trace_place(operand, air, ctx)?.ok_or_else(|| { + CompileError::new( + ErrorKind::AccessorYieldNotReceiverRooted { + found: "a value expression".to_string(), + }, + span, + ) + })?; + let ty = trace.result_type(); + let place = Self::build_place_ref(air, &trace)?; + let read = AnalysisResult::new( + air.add_inst(crate::AirInst { + data: crate::AirInstData::PlaceRead { place }, + ty, + span, + }), + ty, + ); if !ctx.return_type.is_error() && !read.ty.is_error() && !self.types_compatible(read.ty, ctx.return_type) @@ -1831,15 +1848,7 @@ impl OrdinaryBodyEngine<'_, H> { )); } - let trap = air.add_intrinsic( - Some(crate::RuntimeCallKind::PanicNoMessage), - self.known_symbols().panic, - &[], - Type::NEVER, - span, - )?; - let air_ref = air.add_block(&[read.air_ref], trap, Type::NEVER, span)?; - Ok(AnalysisResult::new(air_ref, Type::NEVER)) + Ok(read) } /// Walk a yield operand's projection chain to its root and require that diff --git a/crates/rue-air/src/sema/fact_mode.rs b/crates/rue-air/src/sema/fact_mode.rs index 47564bff5..fd1ee9184 100644 --- a/crates/rue-air/src/sema/fact_mode.rs +++ b/crates/rue-air/src/sema/fact_mode.rs @@ -335,9 +335,6 @@ mod tests { fn call_method_info(&self, _: StructId, _: Spur) -> Option { None } - fn call_accessor_body(&self, _: StructId, _: Spur) -> Option<(InstRef, rue_span::Span)> { - None - } fn call_named_method_declaration(&self, _: FileId, _: Spur, _: Spur) -> Option { None } diff --git a/crates/rue-air/src/sema/ordinary_engine.rs b/crates/rue-air/src/sema/ordinary_engine.rs index 29b0dd19f..7c8688cfd 100644 --- a/crates/rue-air/src/sema/ordinary_engine.rs +++ b/crates/rue-air/src/sema/ordinary_engine.rs @@ -287,6 +287,12 @@ impl<'h, H: OrdinaryBodyAnalysisHost> OrdinaryBodyEngine<'h, H> { pub(crate) fn function_body_info(&self, name: Spur) -> Option { self.storage.function_body_info(name) } + pub(crate) fn function_identity( + &self, + symbol: Spur, + ) -> Result { + self.storage.function_identity(symbol) + } pub(crate) fn value_const(&self, key: &(FileId, Spur)) -> Option { self.storage.value_const(key.0, key.1) } @@ -1697,6 +1703,8 @@ impl<'h, H: OrdinaryBodyAnalysisHost> OrdinaryBodyEngine<'h, H> { identity, callable_kind: if is_destructor { crate::AnalyzedCallableKind::Destructor + } else if is_accessor { + crate::AnalyzedCallableKind::Accessor } else { crate::AnalyzedCallableKind::Ordinary }, @@ -2129,7 +2137,6 @@ impl<'h, H: OrdinaryBodyAnalysisHost> OrdinaryBodyEngine<'h, H> { infer_ctx, accessor_trailing_yield: None, accessor_call_insts: HashMap::new(), - accessor_expansion_stack: Vec::new(), expression_loans: Vec::new(), inline_resolved_types: Vec::new(), place_aliases: HashMap::new(), diff --git a/crates/rue-air/src/sema/output.rs b/crates/rue-air/src/sema/output.rs index 8087036cb..bbabe9fc2 100644 --- a/crates/rue-air/src/sema/output.rs +++ b/crates/rue-air/src/sema/output.rs @@ -424,6 +424,7 @@ pub struct AnalyzedFunction { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum AnalyzedCallableKind { Ordinary, + Accessor, Destructor, DropGlue, } diff --git a/crates/rue-air/src/sema/provider_body_host.rs b/crates/rue-air/src/sema/provider_body_host.rs index 54e9ee96b..6aca194e6 100644 --- a/crates/rue-air/src/sema/provider_body_host.rs +++ b/crates/rue-air/src/sema/provider_body_host.rs @@ -2386,29 +2386,6 @@ where fn call_method_info(&self, struct_id: StructId, name: Spur) -> Option { self.method_info_for_symbol(struct_id, name) } - fn call_accessor_body(&self, struct_id: StructId, name: Spur) -> Option<(InstRef, Span)> { - // Accessor bodies (ADR-0062) are spliced at the call site, so the - // provider resolves the declaring `FnDecl` from the request-local RIR - // — named methods only; anonymous-struct accessors are rejected at - // declaration. - if let Some(info) = self - .endpoint - .method_info(struct_id, name) - .filter(|info| info.returns_borrow) - { - return Some((info.body, info.span)); - } - let method_ref = self.rir_struct_method_decl(struct_id, name)?; - let inst = self.rir.rir().get(method_ref); - match &inst.data { - InstData::FnDecl { - returns_borrow: true, - body, - .. - } => Some((*body, inst.span)), - _ => None, - } - } fn call_named_method_declaration( &self, file: FileId, diff --git a/crates/rue-air/src/sema/semantic_body_export.rs b/crates/rue-air/src/sema/semantic_body_export.rs index 301396886..f0fc53732 100644 --- a/crates/rue-air/src/sema/semantic_body_export.rs +++ b/crates/rue-air/src/sema/semantic_body_export.rs @@ -261,8 +261,18 @@ pub(crate) fn export_body( } }); } + let base = match source.base { + crate::AirPlaceBase::Local(slot) => crate::AirPlaceBase::Local(slot), + crate::AirPlaceBase::Param(slot) => crate::AirPlaceBase::Param(slot), + crate::AirPlaceBase::Accessor(call) => { + if call.as_u32() as usize >= instruction_count { + return Err(F::InvalidInstructionReference); + } + crate::AirPlaceBase::Accessor(call) + } + }; places.push(SemanticBodyPlace { - base: source.base, + base, base_type: host.export_body_type(source.base_type)?, projections: Arc::from(projections), }); @@ -485,6 +495,10 @@ pub(crate) fn export_body( } } } + AirInstData::AccessorCall { name, args } => SemanticBodyInstData::AccessorCall { + function: host.body_function_identity(*name)?, + args: call_args(args)?, + }, AirInstData::CallGeneric { .. } => return Err(F::UnsupportedGenericCall), AirInstData::Intrinsic { runtime, @@ -626,6 +640,7 @@ pub(crate) fn export_body( Ok(SemanticBodyExport { owner, body: SemanticBody { + is_accessor: analyzed.callable_kind == crate::AnalyzedCallableKind::Accessor, return_type: host.export_body_type(body.return_type())?, instructions: Arc::from(instructions), places: Arc::from(places), diff --git a/crates/rue-air/src/sema/tests.rs b/crates/rue-air/src/sema/tests.rs index d96f19908..46a1d5da4 100644 --- a/crates/rue-air/src/sema/tests.rs +++ b/crates/rue-air/src/sema/tests.rs @@ -4081,10 +4081,9 @@ fn main() -> i32 { } #[test] - fn mutually_recursive_accessors_are_rejected() { - // A cycle through several accessors is the same non-terminating - // expansion as a direct self-call, and is rejected at the point the - // expansion re-enters an accessor already on the stack (6.6:14). + fn mutually_recursive_accessors_retain_marked_calls_for_cfg_cycle_rejection() { + // Cross-body cycle rejection belongs to the canonical CFG dependency + // graph; semantic analysis must preserve both exact accessor calls. let source = " struct P { x: i64, @@ -4101,12 +4100,14 @@ fn main() -> i32 { let p = P { x: 1 }; if p.a() == 1 { 0 } else { 1 } }"; - let errors = compile_with_accessors(source).expect_err("mutually recursive accessors"); - assert!( - errors - .iter() - .any(|error| matches!(&error.kind, ErrorKind::AccessorRecursion { .. })) - ); + let output = compile_with_accessors(source).expect("sema preserves the accessor cycle"); + let calls = output + .functions + .iter() + .flat_map(|function| function.air.iter()) + .filter(|(_, inst)| matches!(inst.data, AirInstData::AccessorCall { .. })) + .count(); + assert_eq!(calls, 3); } #[test] @@ -4358,9 +4359,7 @@ struct P {{ } #[test] - fn accessor_guards_execute_before_the_read() { - // The inlined guards must be part of the caller's AIR: the bounds - // panic from the accessor body appears in main. + fn accessor_calls_remain_marked_for_mandatory_cfg_splicing() { let source = format!( "{GRID_ACCESSOR} fn main() -> i32 {{ @@ -4374,16 +4373,11 @@ fn main() -> i32 {{ .iter() .find(|function| function.name == "main") .expect("main is analyzed"); - let has_panic = main.air.iter().any(|(_, inst)| { - matches!( - &inst.data, - AirInstData::Intrinsic { - runtime: Some(crate::RuntimeCallKind::Panic), - .. - } - ) - }); - assert!(has_panic, "the accessor guard's panic inlines into main"); + assert!( + main.air + .iter() + .any(|(_, inst)| matches!(inst.data, AirInstData::AccessorCall { .. })) + ); } // ========================================================================= diff --git a/crates/rue-air/src/semantic_body.rs b/crates/rue-air/src/semantic_body.rs index 02fa76590..996185ec7 100644 --- a/crates/rue-air/src/semantic_body.rs +++ b/crates/rue-air/src/semantic_body.rs @@ -312,6 +312,10 @@ pub enum SemanticBodyInstData { function: FunctionInstanceKey, args: Arc<[SemanticBodyCallArg]>, }, + AccessorCall { + function: FunctionInstanceKey, + args: Arc<[SemanticBodyCallArg]>, + }, RuntimeCall { runtime: crate::RuntimeCallKind, args: Arc<[SemanticBodyCallArg]>, @@ -448,6 +452,7 @@ macro_rules! semantic_body_inst_schema { StorageLive, SemanticBodyInstData::StorageLive { .. }, 55, "storage_live"; StorageDead, SemanticBodyInstData::StorageDead { .. }, 56, "storage_dead"; MarkMoved, SemanticBodyInstData::MarkMoved { .. }, 57, "mark_moved"; + AccessorCall, SemanticBodyInstData::AccessorCall { .. }, 58, "accessor_call"; } }; } @@ -653,6 +658,10 @@ impl SemanticBodyInstData { function: function.try_map_identities(key, module)?, args: args.clone(), }, + D::AccessorCall { function, args } => D::AccessorCall { + function: function.try_map_identities(key, module)?, + args: args.clone(), + }, D::RuntimeCall { runtime, args } => D::RuntimeCall { runtime: *runtime, args: args.clone(), @@ -858,6 +867,10 @@ impl SemanticBodyInstData { D::Call { function, args: values, + } + | D::AccessorCall { + function, + args: values, } => { visitor(SemanticBodyInstDependency::Function(function)); args(visitor, values); @@ -972,6 +985,8 @@ pub struct SemanticBodyMethodReference { #[derive(Debug, Clone, PartialEq, Eq)] pub struct SemanticBody { + /// Whether this body has the second-class, mandatory-splice accessor ABI. + pub is_accessor: bool, pub return_type: SemanticImportType, pub instructions: Arc<[SemanticBodyInst]>, pub places: Arc<[SemanticBodyPlace]>, @@ -1060,6 +1075,7 @@ impl SemanticBody { }) .collect::, E>>()?; Ok(SemanticBody { + is_accessor: self.is_accessor, return_type: self.return_type.try_map_identities(key, module)?, instructions: instructions.into(), places: places.into(), @@ -1347,12 +1363,16 @@ mod schema_tests { is_param: false, place: Some(4), }, + D::AccessorCall { + function: FunctionInstanceKey::Definition("accessor"), + args: call_args(&[1]), + }, ] } #[test] fn semantic_body_instruction_schema_has_stable_unique_metadata() { - assert_eq!(SEMANTIC_BODY_INST_KINDS.len(), 58); + assert_eq!(SEMANTIC_BODY_INST_KINDS.len(), 59); for (tag, kind) in SEMANTIC_BODY_INST_KINDS.iter().copied().enumerate() { assert_eq!(usize::from(kind.schema_tag()), tag); assert!(!kind.display_name().is_empty()); @@ -1489,6 +1509,13 @@ mod schema_tests { dependencies(&samples[57]), [SeenDependency::Instruction(1), SeenDependency::Place(4)] ); + assert_eq!( + dependencies(&samples[58]), + [ + SeenDependency::Definition("accessor"), + SeenDependency::Instruction(1) + ] + ); } #[test] @@ -1526,6 +1553,13 @@ mod schema_tests { .. } if key == "mapped:cast_type" )); + assert!(matches!( + map(&samples[58]), + SemanticBodyInstData::AccessorCall { + function: FunctionInstanceKey::Definition(key), + .. + } if key == "mapped:accessor" + )); } #[test] diff --git a/crates/rue-air/src/semantic_import.rs b/crates/rue-air/src/semantic_import.rs index e4837fe7b..751d3070c 100644 --- a/crates/rue-air/src/semantic_import.rs +++ b/crates/rue-air/src/semantic_import.rs @@ -547,6 +547,9 @@ pub struct SemanticLocalMaterialization { pub type_pool: crate::FrozenTypeInternPool, pub interner: Arc, pub aggregate_types: std::collections::HashMap>, + /// Exact caller-owned handles explicitly pre-materialized for a mandatory + /// accessor splice, paired with their stable type identities. + pub materialized_types: Vec<(crate::Type, SemanticImportType)>, pub strings: Vec, pub warnings: Arc<[rue_error::CompileWarning]>, pub body_span: Span, @@ -864,6 +867,12 @@ where air.add_call(None, name, &args, ty, span)?; continue; } + SemanticBodyInstData::AccessorCall { function, args } => { + let name = resolve_function(function)?; + let args = call_args(args, current)?; + air.add_accessor_call(name, &args, ty, span)?; + continue; + } SemanticBodyInstData::RuntimeCall { runtime, args } => { let args = call_args(args, current)?; air.add_call( @@ -1447,6 +1456,21 @@ where where K: Eq + Hash, M: Clone + Eq + Hash, + { + self.materialize_local_body_with_types(identity, callable_kind, body, body_span, &[]) + } + + pub fn materialize_local_body_with_types( + self, + identity: FunctionInstanceKey, + callable_kind: crate::AnalyzedCallableKind, + body: &SemanticBody, + body_span: Span, + additional_types: &[SemanticImportType], + ) -> Result, SemanticBodyImportFailure> + where + K: Clone + Eq + Hash, + M: Clone + Eq + Hash, { let completeness = self .local_completeness @@ -1500,6 +1524,16 @@ where }, |value| self.interner.get_or_intern(value), )?; + let materialized_types = additional_types + .iter() + .map(|stable| { + Ok(( + self.import_type_local(stable) + .map_err(SemanticBodyImportFailure::Semantic)?, + stable.clone(), + )) + }) + .collect::, SemanticBodyImportFailure>>()?; let mut aggregate_types = std::collections::HashMap::new(); let type_snapshot = self.type_pool.clone().freeze(); for ty in type_snapshot.all_types() { @@ -1537,6 +1571,7 @@ where type_pool: self.type_pool.freeze(), interner: Arc::new(self.interner), aggregate_types, + materialized_types, strings, warnings, body_span, @@ -2688,6 +2723,7 @@ mod tests { ) -> crate::SemanticBody<&'static str, &'static str> { use crate::{SemanticBody, SemanticBodyAnchor, SemanticBodyInst, SemanticImportType}; SemanticBody { + is_accessor: false, return_type: SemanticImportType::I32, instructions: data .into_iter() diff --git a/crates/rue-cfg/src/build.rs b/crates/rue-cfg/src/build.rs index 58b678c46..533834083 100644 --- a/crates/rue-cfg/src/build.rs +++ b/crates/rue-cfg/src/build.rs @@ -1249,6 +1249,27 @@ impl<'a> CfgBuilder<'a> { } } + AirInstData::AccessorCall { name, args } => { + let mut arg_vals = Vec::new(); + for arg in self.air.get_call_args(args) { + let Some(value) = self.lower_value(arg.value) else { + return Self::diverged(); + }; + arg_vals.push(CfgCallArg { + value, + mode: Self::convert_arg_mode(arg.mode), + }); + } + let args_result = self.cfg.push_call_args(arg_vals); + let args = self.payload_or(args_result, CfgCallArgs::EMPTY, span); + let value = self.emit(CfgInstData::AccessorCall { name: *name, args }, ty, span); + self.cache(air_ref, value); + ExprResult { + value: Some(value), + continuation: Continuation::Continues, + } + } + AirInstData::Intrinsic { runtime, name, @@ -1289,6 +1310,7 @@ impl<'a> CfgBuilder<'a> { // them) — record it so CSE's param keying skips // the slot (RUE-914 hunt finding). PlaceBase::Param(slot) => self.cfg.mark_param_address_taken(slot), + PlaceBase::Accessor(_) => {} }, // A bare scalar parameter lowers directly to a Param // value with no backing local; its address escaping @@ -2101,6 +2123,9 @@ impl<'a> CfgBuilder<'a> { let base_key = match air_place.base { AirPlaceBase::Local(slot) => MovedSlot::Local(slot), AirPlaceBase::Param(slot) => MovedSlot::Param(slot), + AirPlaceBase::Accessor(_) => { + unreachable!("semantic analysis rejects accessor-place writes") + } }; if air_place.projection_count() == 0 { self.emit_overwrite_drop(base_key, val_ty, span); @@ -3353,6 +3378,7 @@ impl<'a> CfgBuilder<'a> { let base = match air_base { AirPlaceBase::Local(slot) => PlaceBase::Local(slot), AirPlaceBase::Param(slot) => PlaceBase::Param(slot), + AirPlaceBase::Accessor(call) => PlaceBase::Accessor(self.lower_value(call)?), }; // Convert projections, lowering any index expressions diff --git a/crates/rue-cfg/src/inline.rs b/crates/rue-cfg/src/inline.rs index 64c7d4681..4cbbee4cb 100644 --- a/crates/rue-cfg/src/inline.rs +++ b/crates/rue-cfg/src/inline.rs @@ -21,8 +21,8 @@ //! included), bracketed by a callee-lifetime `StorageLive` before the //! spliced body and `StorageDead` after the continuation join. //! - Physically by-reference: the argument is a place; parameter accesses are -//! redirected to its root. Only simple local/parameter roots are accepted; -//! projected by-ref arguments are rejected (see [`CfgInlineError`]). +//! redirected to its root and any caller projection prefix is composed with +//! the callee's own projections. //! //! # Worked elaboration example: the materialized-parameter drop //! @@ -112,13 +112,6 @@ pub enum CfgInlineError { call_type: Type, callee_return_type: Type, }, - /// A physically by-reference argument is a projected place (field or - /// index). Redirecting parameter accesses to a projected place needs a - /// proof that address-formation timing and trap behavior are preserved - /// (a projection can bounds-trap when the address is formed), which - /// ADR-0049 §2/§3 defers: Phase 1 accepts only simple local/parameter - /// roots. - ProjectedByRefArgument { arg_index: usize }, /// A physically by-reference argument is not a place read at all — a /// violated sema/CFG invariant at the call site (RUE-760). NonPlaceByRefArgument { arg_index: usize }, @@ -162,10 +155,6 @@ impl std::fmt::Display for CfgInlineError { call_type.name(), callee_return_type.name() ), - Self::ProjectedByRefArgument { arg_index } => write!( - f, - "by-ref argument {arg_index} is a projected place, which Phase 1 inlining excludes" - ), Self::NonPlaceByRefArgument { arg_index } => { write!(f, "by-ref argument {arg_index} is not a place read") } @@ -191,15 +180,17 @@ impl From for CfgInlineError { } /// Where accesses to one callee source parameter land in the caller. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] enum ParamRedirect { /// Physically by-value: the argument was materialized into this caller /// frame slot (the base of a full-ABI-width region). Materialized { slot: u32 }, /// Physically by-reference with a simple caller-local root. - ByRefLocal { slot: u32 }, - /// Physically by-reference with a simple caller-parameter root. - ByRefParam { slot: u32 }, + ByRefPlace { + base: PlaceBase, + base_type: Type, + projections: Vec, + }, } /// One callee source parameter: its start ABI slot and full slot width. @@ -233,7 +224,7 @@ impl Splice<'_> { fn param(&self, slot: u32) -> Result { self.redirects .get(&slot) - .copied() + .cloned() .ok_or(CfgInlineError::UnmappedCalleeParamSlot { slot }) } } @@ -259,15 +250,20 @@ pub fn inline_call( if call.as_u32() as usize >= dst.value_count() { return Err(CfgInlineError::CallSiteNotFound { call }); } - let (call_ty, call_span, call_args) = { + let (call_ty, call_span, call_args, is_accessor) = { let inst = dst.get_inst(call); - let CfgInstData::Call { runtime, args, .. } = &inst.data else { - return Err(CfgInlineError::NotACall { call }); - }; - if runtime.is_some() { - return Err(CfgInlineError::RuntimeCall { call }); + match &inst.data { + CfgInstData::Call { runtime, args, .. } => { + if runtime.is_some() { + return Err(CfgInlineError::RuntimeCall { call }); + } + (inst.ty, inst.span, dst.call_args(args).to_vec(), false) + } + CfgInstData::AccessorCall { args, .. } => { + (inst.ty, inst.span, dst.call_args(args).to_vec(), true) + } + _ => return Err(CfgInlineError::NotACall { call }), } - (inst.ty, inst.span, dst.call_args(args).to_vec()) }; let Some((call_block, call_position)) = dst.blocks().iter().find_map(|block| { block @@ -303,20 +299,26 @@ pub fn inline_call( let redirect = if callee.is_param_by_ref(param.start_slot) { // The argument is a place; only a simple local/parameter root is // redirectable in this phase (module docs, ADR-0049 §2/§3). - let root = byref_argument_root(&dst, arg.value, index)?; - match root { + let place = byref_argument_place(&dst, arg.value, index)?; + match place.base { PlaceBase::Local(slot) => { if callee.is_param_address_taken(param.start_slot) { dst.mark_address_taken(slot); } - ParamRedirect::ByRefLocal { slot } } PlaceBase::Param(slot) => { if callee.is_param_address_taken(param.start_slot) { dst.mark_param_address_taken(slot); } - ParamRedirect::ByRefParam { slot } } + PlaceBase::Accessor(_) => { + return Err(CfgInlineError::NonPlaceByRefArgument { arg_index: index }); + } + } + ParamRedirect::ByRefPlace { + base: place.base, + base_type: place.base_type, + projections: dst.get_place_projections(&place).to_vec(), } } else { let arg_ty = dst.get_inst(arg.value).ty; @@ -363,6 +365,31 @@ pub fn inline_call( block_base, redirects: &redirects, }; + let yielded = if is_accessor { + let returned = callee + .blocks() + .iter() + .find_map(|block| match block.terminator { + Terminator::Return { value: Some(value) } => Some(value), + _ => None, + }) + .ok_or(CfgInlineError::ReturnTypeMismatch { + call_type: call_ty, + callee_return_type: callee.return_type(), + })?; + let CfgInstData::PlaceRead { place } = &callee.get_inst(returned).data else { + return Err(CfgInlineError::ReturnTypeMismatch { + call_type: call_ty, + callee_return_type: callee.return_type(), + }); + }; + Some(( + translate_place(&mut dst, callee, place, &splice)?, + splice.value(returned), + )) + } else { + None + }; for index in 0..callee.value_count() { let source = callee.get_inst(CfgValue::from_raw(index as u32)); let data = translate_data(&mut dst, callee, &source.data, &splice)?; @@ -379,7 +406,9 @@ pub fn inline_call( // The continuation takes the call result as an explicit block parameter; // unit results use a fresh unit constant instead (the builder's join // convention), and never results have no continuation edge (RUE-347). - let replacement = if call_ty != Type::UNIT && call_ty != Type::NEVER { + let replacement = if is_accessor { + None + } else if call_ty != Type::UNIT && call_ty != Type::NEVER { Some(dst.add_block_param(continuation, call_ty)) } else if call_ty == Type::UNIT { Some(dst.add_inst(CfgInst { @@ -390,7 +419,7 @@ pub fn inline_call( } else { None }; - let continuation_takes_value = call_ty != Type::UNIT && call_ty != Type::NEVER; + let continuation_takes_value = !is_accessor && call_ty != Type::UNIT && call_ty != Type::NEVER; // -- Attach the copied blocks. ------------------------------------------ for source in callee.blocks() { @@ -456,14 +485,26 @@ pub fn inline_call( if let (Some(value), true) = (replacement, call_ty == Type::UNIT) { continuation_insts.push(value); } + let mut dead_materialized = Vec::with_capacity(materialized.len()); for &(slot, _, ty) in &materialized { - continuation_insts.push(storage_inst( + dead_materialized.push(storage_inst( &mut dst, CfgInstData::StorageDead { slot, local_ty: ty }, call_span, )); } - continuation_insts.extend(moved_tail); + if is_accessor { + // The yielded place may use a by-value parameter in one of its + // projections (for example `self.items[i]`). Keep those materialized + // parameter slots alive through the caller instructions that consume + // the substituted place; ending them at continuation entry leaves the + // projection index dangling on backends that rematerialize the load. + continuation_insts.extend(moved_tail); + continuation_insts.extend(dead_materialized); + } else { + continuation_insts.extend(dead_materialized); + continuation_insts.extend(moved_tail); + } { let block = dst.get_block_mut(continuation); block.insts = continuation_insts; @@ -475,11 +516,68 @@ pub fn inline_call( if let Some(replacement) = replacement { dst.rewrite_value_uses(|value| if value == call { replacement } else { value })?; } + if let Some((yielded_place, yielded_value)) = yielded { + substitute_accessor_places(&mut dst, call, &yielded_place, yielded_value)?; + } dst.finish_after_optimization(type_pool) .map_err(CfgInlineError::Verification) } +fn substitute_accessor_places( + cfg: &mut Cfg, + call: CfgValue, + yielded: &Place, + yielded_value: CfgValue, +) -> Result<(), CfgInlineError> { + let mut replacements = Vec::new(); + let mut value_replacements = Vec::new(); + for index in 0..cfg.value_count() { + let value = CfgValue::from_raw(index as u32); + let place = match &cfg.get_inst(value).data { + CfgInstData::PlaceRead { place } | CfgInstData::PlaceWrite { place, .. } + if place.base == PlaceBase::Accessor(call) => + { + place + } + _ => continue, + }; + if matches!(cfg.get_inst(value).data, CfgInstData::PlaceRead { .. }) + && cfg.get_place_projections(place).is_empty() + { + value_replacements.push(value); + continue; + } + let mut projections = cfg.get_place_projections(yielded).to_vec(); + projections.extend_from_slice(cfg.get_place_projections(place)); + let composed = cfg.make_place(yielded.base, yielded.base_type, projections)?; + replacements.push((value, composed)); + } + for (value, place) in replacements { + match &mut cfg.get_inst_mut(value).data { + CfgInstData::PlaceRead { place: target } + | CfgInstData::PlaceWrite { place: target, .. } => *target = place, + _ => unreachable!(), + } + } + if !value_replacements.is_empty() { + cfg.rewrite_value_uses(|value| { + if value_replacements.contains(&value) { + yielded_value + } else { + value + } + })?; + for block_index in 0..cfg.block_count() { + let block = BlockId::from_raw(block_index as u32); + cfg.get_block_mut(block) + .insts + .retain(|value| !value_replacements.contains(value)); + } + } + Ok(()) +} + /// The callee's per-source-parameter grouping: the recorded ABI descriptors /// when present, else the synthetic one-slot-per-parameter contract used by /// directly constructed CFGs (see `Cfg::source_param_abi`). @@ -505,25 +603,17 @@ fn callee_params(callee: &Cfg) -> Vec { /// Classify a physically by-reference argument's place root. Sema only /// accepts places here (RUE-760): a plain variable (`Load`/`Param`) or a -/// projection chain (`PlaceRead`). Phase 1 admits the projection-free roots -/// and rejects projected places (see [`CfgInlineError::ProjectedByRefArgument`]). -fn byref_argument_root( +/// projection chain (`PlaceRead`). Projected places retain their complete +/// prefix so callee projections compose onto the caller's exact storage. +fn byref_argument_place( caller: &Cfg, argument: CfgValue, arg_index: usize, -) -> Result { +) -> Result { match &caller.get_inst(argument).data { - CfgInstData::Load { slot } => Ok(PlaceBase::Local(*slot)), - CfgInstData::Param { index } => Ok(PlaceBase::Param(*index)), - CfgInstData::PlaceRead { place } => { - if let Some(slot) = place.as_local() { - Ok(PlaceBase::Local(slot)) - } else if let Some(slot) = place.as_param() { - Ok(PlaceBase::Param(slot)) - } else { - Err(CfgInlineError::ProjectedByRefArgument { arg_index }) - } - } + CfgInstData::Load { slot } => Ok(Place::local(*slot, caller.get_inst(argument).ty)), + CfgInstData::Param { index } => Ok(Place::param(*index, caller.get_inst(argument).ty)), + CfgInstData::PlaceRead { place } => Ok(place.duplicate_with_owner()), _ => Err(CfgInlineError::NonPlaceByRefArgument { arg_index }), } } @@ -552,10 +642,25 @@ fn translate_data( StringConst(index) => StringConst(*index), BlockParam { index } => BlockParam { index: *index }, Param { index } => match splice.param(*index)? { - ParamRedirect::Materialized { slot } | ParamRedirect::ByRefLocal { slot } => { - Load { slot } - } - ParamRedirect::ByRefParam { slot } => Param { index: slot }, + ParamRedirect::Materialized { slot } => Load { slot }, + ParamRedirect::ByRefPlace { + base, + base_type: _, + projections, + } if projections.is_empty() => match base { + PlaceBase::Local(slot) => Load { slot }, + PlaceBase::Param(index) => Param { index }, + PlaceBase::Accessor(_) => { + unreachable!("accessor roots are rejected at classification") + } + }, + ParamRedirect::ByRefPlace { + base, + base_type, + projections, + } => PlaceRead { + place: dst.make_place(base, base_type, projections)?, + }, }, Add(a, b) => Add(splice.value(*a), splice.value(*b)), Sub(a, b) => Sub(splice.value(*a), splice.value(*b)), @@ -593,11 +698,24 @@ fn translate_data( ParamStore { param_slot, value } => { let value = splice.value(*value); match splice.param(*param_slot)? { - ParamRedirect::Materialized { slot } | ParamRedirect::ByRefLocal { slot } => { - Store { slot, value } - } - ParamRedirect::ByRefParam { slot } => ParamStore { - param_slot: slot, + ParamRedirect::Materialized { slot } => Store { slot, value }, + ParamRedirect::ByRefPlace { + base, + base_type: _, + projections, + } if projections.is_empty() => match base { + PlaceBase::Local(slot) => Store { slot, value }, + PlaceBase::Param(param_slot) => ParamStore { param_slot, value }, + PlaceBase::Accessor(_) => { + unreachable!("accessor roots are rejected at classification") + } + }, + ParamRedirect::ByRefPlace { + base, + base_type, + projections, + } => PlaceWrite { + place: dst.make_place(base, base_type, projections)?, value, }, } @@ -628,6 +746,20 @@ fn translate_data( args: dst.push_call_args(args)?, } } + AccessorCall { name, args } => { + let args: Vec = callee + .call_args(args) + .iter() + .map(|arg| CfgCallArg { + value: splice.value(arg.value), + mode: arg.mode, + }) + .collect(); + AccessorCall { + name: *name, + args: dst.push_call_args(args)?, + } + } Intrinsic { runtime, name, @@ -719,16 +851,7 @@ fn translate_place( place: &Place, splice: &Splice<'_>, ) -> Result { - let base = match place.base { - PlaceBase::Local(slot) => PlaceBase::Local(splice.local(slot)), - PlaceBase::Param(slot) => match splice.param(slot)? { - ParamRedirect::Materialized { slot } | ParamRedirect::ByRefLocal { slot } => { - PlaceBase::Local(slot) - } - ParamRedirect::ByRefParam { slot } => PlaceBase::Param(slot), - }, - }; - let projections: Vec = callee + let translated: Vec = callee .get_place_projections(place) .iter() .map(|projection| match projection { @@ -745,7 +868,30 @@ fn translate_place( }, }) .collect(); - dst.make_place(base, place.base_type, projections) + let (base, base_type, mut projections) = match place.base { + PlaceBase::Local(slot) => ( + PlaceBase::Local(splice.local(slot)), + place.base_type, + Vec::new(), + ), + PlaceBase::Param(slot) => match splice.param(slot)? { + ParamRedirect::Materialized { slot } => { + (PlaceBase::Local(slot), place.base_type, Vec::new()) + } + ParamRedirect::ByRefPlace { + base, + base_type, + projections, + } => (base, base_type, projections), + }, + PlaceBase::Accessor(value) => ( + PlaceBase::Accessor(splice.value(value)), + place.base_type, + Vec::new(), + ), + }; + projections.extend(translated); + dst.make_place(base, base_type, projections) .map_err(CfgInlineError::Edit) } @@ -1176,22 +1322,22 @@ mod tests { } #[test] - fn projected_by_ref_argument_is_rejected() { - // Redirecting a projected place needs the address-formation proof - // obligation ADR-0049 records; Phase 1 refuses it. + fn projected_by_ref_argument_composes_onto_caller_place() { let program = compile( "struct Pair { a: i64, b: i64 }\n\ fn bump(inout x: i64) { x = x + 1; }\n\ fn caller() -> i64 { let mut p = Pair { a: 1, b: 2 }; bump(inout p.a); p.a }", ); - let error = program.try_inline("caller", "bump").unwrap_err(); + let inlined = program.inline("caller", "bump"); assert!( - matches!( - error, - CfgInlineError::ProjectedByRefArgument { arg_index: 0 } - ), - "unexpected error: {error}" + attached_values(&inlined).any(|value| match &inlined.get_inst(value).data { + CfgInstData::PlaceWrite { place, .. } => + !inlined.get_place_projections(place).is_empty(), + _ => false, + }), + "the callee write must retain the caller field projection" ); + assert_all_blocks_terminated(&inlined); } #[test] diff --git a/crates/rue-cfg/src/inst.rs b/crates/rue-cfg/src/inst.rs index b9d7c4244..5d1ede2a6 100644 --- a/crates/rue-cfg/src/inst.rs +++ b/crates/rue-cfg/src/inst.rs @@ -115,7 +115,7 @@ impl Place { if self.projections.is_empty() { match self.base { PlaceBase::Local(slot) => Some(slot), - PlaceBase::Param(_) => None, + PlaceBase::Param(_) | PlaceBase::Accessor(_) => None, } } else { None @@ -128,7 +128,7 @@ impl Place { if self.projections.is_empty() { match self.base { PlaceBase::Param(slot) => Some(slot), - PlaceBase::Local(_) => None, + PlaceBase::Local(_) | PlaceBase::Accessor(_) => None, } } else { None @@ -195,6 +195,10 @@ impl CfgInstData { name: *name, args: args.duplicate(), }, + Self::AccessorCall { name, args } => Self::AccessorCall { + name: *name, + args: args.duplicate(), + }, Self::Intrinsic { runtime, name, @@ -253,6 +257,7 @@ impl fmt::Display for Place { match self.base { PlaceBase::Local(slot) => write!(f, "${}", slot)?, PlaceBase::Param(slot) => write!(f, "%{}", slot)?, + PlaceBase::Accessor(value) => write!(f, "accessor%{}", value.as_u32())?, } if !self.projections.is_empty() { write!(f, "[{} projections]", self.projections.extent())?; @@ -268,6 +273,9 @@ pub enum PlaceBase { Local(u32), /// Parameter slot (for parameters, including inout) Param(u32), + /// Mandatory-inline accessor call whose place result has not yet been + /// substituted by the inter-procedural CFG splice. + Accessor(CfgValue), } /// A projection applied to a place to reach a nested location. @@ -493,6 +501,12 @@ pub enum CfgInstData { args: CfgCallArgs, }, + /// Mandatory-inline place-producing accessor call. + AccessorCall { + name: Spur, + args: CfgCallArgs, + }, + /// Intrinsic call (e.g., @dbg). Arguments use the intrinsic-value family. Intrinsic { /// Runtime helper/adaptation selected by semantic analysis. @@ -1162,6 +1176,12 @@ impl Cfg { .push_call_args(self.call_args(args).iter().copied()) .map_err(CfgRemapError::Edit)?, }, + AccessorCall { name, args } => AccessorCall { + name: domain!(symbol(*name)), + args: cfg + .push_call_args(self.call_args(args).iter().copied()) + .map_err(CfgRemapError::Edit)?, + }, Intrinsic { runtime, name, @@ -2131,6 +2151,10 @@ impl Cfg { let base_valid = match base { PlaceBase::Local(slot) => slot < self.num_locals, PlaceBase::Param(slot) => slot < self.num_params, + PlaceBase::Accessor(value) => { + (value.as_u32() as usize) < self.values.len() + && matches!(self.get_inst(value).data, CfgInstData::AccessorCall { .. }) + } }; if !base_valid { return Err(Self::invalid_edit(operation, "place base is out of bounds")); @@ -2633,6 +2657,9 @@ impl Cfg { | IntCast { value: v, .. } | Drop { value: v } => *v = map(*v), PlaceRead { place } => { + if let PlaceBase::Accessor(value) = &mut place.base { + *value = map(*value); + } place.projections = payload::push_projections( &mut self.projections, payload::projections(&old_projections, &place.projections) @@ -2654,6 +2681,9 @@ impl Cfg { } PlaceWrite { place, value } => { *value = map(*value); + if let PlaceBase::Accessor(base) = &mut place.base { + *base = map(*base); + } place.projections = payload::push_projections( &mut self.projections, payload::projections(&old_projections, &place.projections) @@ -2673,7 +2703,7 @@ impl Cfg { }), )?; } - Call { args, .. } => { + Call { args, .. } | AccessorCall { args, .. } => { *args = payload::push_call_args( &mut self.call_args, payload::call_args(&old_call_args, args) @@ -3040,6 +3070,23 @@ impl Cfg { } write!(f, ")") } + CfgInstData::AccessorCall { name, args } => { + match interner { + Some(interner) => write!(f, "accessor_call @{}(", interner.resolve(name))?, + None => write!(f, "accessor_call @{}(", name.into_usize())?, + } + for (i, arg) in self.call_args(args).iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + match arg.mode { + CfgArgMode::Inout => write!(f, "inout {}", arg.value)?, + CfgArgMode::Borrow => write!(f, "borrow {}", arg.value)?, + CfgArgMode::Normal => write!(f, "{}", arg.value)?, + } + } + write!(f, ")") + } CfgInstData::Intrinsic { runtime, name, @@ -3148,6 +3195,9 @@ impl Cfg { PlaceBase::Param(slot) => { let _ = write!(out, "param%{}", slot); } + PlaceBase::Accessor(value) => { + let _ = write!(out, "accessor%{}", value.as_u32()); + } } for proj in self.get_place_projections(place) { match proj { diff --git a/crates/rue-cfg/src/opt/dce.rs b/crates/rue-cfg/src/opt/dce.rs index 6ae8ea2ea..cee19e418 100644 --- a/crates/rue-cfg/src/opt/dce.rs +++ b/crates/rue-cfg/src/opt/dce.rs @@ -243,7 +243,7 @@ pub(super) fn visit_instruction_uses(cfg: &Cfg, value: CfgValue, mut f: impl FnM CfgInstData::ParamStore { value, .. } => f(*value), // Function calls - CfgInstData::Call { args, .. } => { + CfgInstData::Call { args, .. } | CfgInstData::AccessorCall { args, .. } => { for arg in cfg.call_args(args) { f(arg.value); } @@ -285,6 +285,9 @@ pub(super) fn visit_instruction_uses(cfg: &Cfg, value: CfgValue, mut f: impl FnM // Place operations CfgInstData::PlaceRead { place } => { + if let crate::PlaceBase::Accessor(value) = place.base { + f(value); + } // Visit any index values used in projections for proj in cfg.get_place_projections(place) { if let Projection::Index { index, .. } = proj { @@ -294,6 +297,9 @@ pub(super) fn visit_instruction_uses(cfg: &Cfg, value: CfgValue, mut f: impl FnM } CfgInstData::PlaceWrite { place, value } => { f(*value); + if let crate::PlaceBase::Accessor(value) = place.base { + f(value); + } // Visit any index values used in projections for proj in cfg.get_place_projections(place) { if let Projection::Index { index, .. } = proj { diff --git a/crates/rue-cfg/src/opt/forward.rs b/crates/rue-cfg/src/opt/forward.rs index c29597368..c1c4a146c 100644 --- a/crates/rue-cfg/src/opt/forward.rs +++ b/crates/rue-cfg/src/opt/forward.rs @@ -180,7 +180,7 @@ pub fn run(cfg: &mut Cfg) -> Result { record_write(&mut slot_class, slot, None); } } - CfgInstData::Call { args, .. } => { + CfgInstData::Call { args, .. } | CfgInstData::AccessorCall { args, .. } => { for arg in cfg.call_args(args) { if !arg.is_by_ref() { continue; @@ -285,6 +285,9 @@ pub fn run(cfg: &mut Cfg) -> Result { // A parameter base writes through a parameter, // not a local slot: nothing to kill. PlaceBase::Param(_) => {} + PlaceBase::Accessor(_) => { + clear_all = true; + } }, // An unidentifiable by-ref root: be safe and clear // the whole table at this call. diff --git a/crates/rue-cfg/src/verify.rs b/crates/rue-cfg/src/verify.rs index df6a2fd26..7664d6845 100644 --- a/crates/rue-cfg/src/verify.rs +++ b/crates/rue-cfg/src/verify.rs @@ -252,7 +252,7 @@ impl Cfg { out.push(*value); } - CfgInstData::Call { args, .. } => { + CfgInstData::Call { args, .. } | CfgInstData::AccessorCall { args, .. } => { for arg in self.call_args(args) { out.push(arg.value); } @@ -278,6 +278,9 @@ impl Cfg { /// Collect the `CfgValue` operands hiding inside a place's `Index` /// projections. fn collect_place_operands(&self, place: &crate::inst::Place, out: &mut Vec) { + if let PlaceBase::Accessor(value) = place.base { + out.push(value); + } for proj in self.get_place_projections(place) { if let Projection::Index { index, .. } = proj { out.push(*index); @@ -530,7 +533,7 @@ impl<'a> Verifier<'a> { value, }; match data { - CfgInstData::Call { args, .. } => { + CfgInstData::Call { args, .. } | CfgInstData::AccessorCall { args, .. } => { self.cfg .checked_call_args(args) .map_err(|error| self.payload_error(location, error))?; @@ -945,6 +948,16 @@ impl<'a> Verifier<'a> { PlaceBase::Param(slot) => { self.check_param_slot(slot, place.base_type, block, value, "place")? } + PlaceBase::Accessor(producer) => { + let inst = self.cfg.get_inst(producer); + if !matches!(inst.data, CfgInstData::AccessorCall { .. }) + || inst.ty != place.base_type + { + return Err(self.error(format!( + "{block}: {value} has an invalid accessor place producer" + ))); + } + } } let projections = self.cfg.get_place_projections(place); if let Some(pool) = self.type_pool { @@ -1281,6 +1294,9 @@ impl<'a> Verifier<'a> { f(*value, "stored value") } CfgInstData::PlaceRead { place } => { + if let PlaceBase::Accessor(producer) = place.base { + f(producer, "accessor place producer"); + } for projection in self.cfg.get_place_projections(place) { if let Projection::Index { index, .. } = projection { f(*index, "projection index"); @@ -1288,6 +1304,9 @@ impl<'a> Verifier<'a> { } } CfgInstData::PlaceWrite { place, value } => { + if let PlaceBase::Accessor(producer) = place.base { + f(producer, "accessor place producer"); + } for projection in self.cfg.get_place_projections(place) { if let Projection::Index { index, .. } = projection { f(*index, "projection index"); @@ -1295,7 +1314,7 @@ impl<'a> Verifier<'a> { } f(*value, "place-write value"); } - CfgInstData::Call { args, .. } => { + CfgInstData::Call { args, .. } | CfgInstData::AccessorCall { args, .. } => { for arg in self.cfg.call_args(args) { f(arg.value, "call argument"); } diff --git a/crates/rue-cli-tests/cases/borrow_accessors.toml b/crates/rue-cli-tests/cases/borrow_accessors.toml new file mode 100644 index 000000000..67cb8322d --- /dev/null +++ b/crates/rue-cli-tests/cases/borrow_accessors.toml @@ -0,0 +1,74 @@ +[section] +id = "cli.borrow_accessors" +name = "Borrow accessor CFG splicing" +description = "Marked place-returning accessor calls are mandatorily spliced before both backends." + +[[case]] +name = "guarded_splice_aarch64" +files = [{ path = "main.rue", source = """ +struct Grid { + cells: [i64; 4], + + fn at(borrow self, i: u64) -> borrow i64 { + if i >= 4 { @panic("index out of bounds"); } + yield self.cells[i]; + } +} +fn main() -> i32 { + let g = Grid { cells: [10, 20, 30, 40] }; + if g.at(0) + g.at(3) == 50 { 0 } else { 1 } +} +""" }] +args = ["--preview", "borrow_accessors", "--emit", "asm", "--target", "aarch64-linux", "main.rue"] +compile_only = true +compile_stdout_contains = ["=== Assembly (aarch64-linux) ===", "main:"] +compile_stdout_not_contains = [".at:"] + +[[case]] +name = "accessor_local_destructor_is_linked_and_runs" +description = "A destructor required only by a spliced accessor body remains in the caller's codegen dependency closure." +files = [{ path = "main.rue", source = """ +struct D { value: i32 } + +drop fn D(self) { + @dbg(self.value); +} + +struct P { + x: i64, + + fn value(borrow self) -> borrow i64 { + let d = D { value: 77 }; + yield self.x; + } +} + +fn main() -> i32 { + let p = P { x: 9 }; + if p.value() == 9 { 0 } else { 1 } +} +""" }] +args = ["--preview", "borrow_accessors", "main.rue", "-o", "prog"] +stdout = "77\n" +exit_code = 0 + +[[case]] +name = "guarded_splice_x86_64" +files = [{ path = "main.rue", source = """ +struct Grid { + cells: [i64; 4], + + fn at(borrow self, i: u64) -> borrow i64 { + if i >= 4 { @panic("index out of bounds"); } + yield self.cells[i]; + } +} +fn main() -> i32 { + let g = Grid { cells: [10, 20, 30, 40] }; + if g.at(0) + g.at(3) == 50 { 0 } else { 1 } +} +""" }] +args = ["--preview", "borrow_accessors", "--emit", "asm", "--target", "x86-64-linux", "main.rue"] +compile_only = true +compile_stdout_contains = ["=== Assembly (x86-64-linux) ===", "main:"] +compile_stdout_not_contains = [".at:"] diff --git a/crates/rue-codegen/src/cfg_lower.rs b/crates/rue-codegen/src/cfg_lower.rs index fc0c29dc8..8c8e556b5 100644 --- a/crates/rue-codegen/src/cfg_lower.rs +++ b/crates/rue-codegen/src/cfg_lower.rs @@ -238,6 +238,10 @@ fn format_cfg_inst_data_impl( CfgInstData::Drop { value } => format!("drop {}", value), CfgInstData::StorageLive { slot, .. } => format!("storage_live ${}", slot), CfgInstData::StorageDead { slot, .. } => format!("storage_dead ${}", slot), + CfgInstData::AccessorCall { name, .. } => match interner { + Some(interner) => format!("accessor_call @{}", interner.resolve(name)), + None => format!("accessor_call @{}", name.into_usize()), + }, // Place operations CfgInstData::PlaceRead { place } => { format!("place_read {}", cfg.place_to_string(place)) diff --git a/crates/rue-codegen/src/param_storage.rs b/crates/rue-codegen/src/param_storage.rs index c186894ce..57f80b352 100644 --- a/crates/rue-codegen/src/param_storage.rs +++ b/crates/rue-codegen/src/param_storage.rs @@ -304,6 +304,9 @@ fn scan_param_references(cfg: &Cfg, type_pool: &FrozenTypeInternPool) -> ParamRe mark_span(&mut needs_home, index, span); } } + PlaceBase::Accessor(_) => { + panic!("mandatory-inline accessor place reached codegen") + } } } CfgInstData::Alloc { slot, .. } diff --git a/crates/rue-codegen/src/value_plan.rs b/crates/rue-codegen/src/value_plan.rs index e933b71bc..0df77a8a3 100644 --- a/crates/rue-codegen/src/value_plan.rs +++ b/crates/rue-codegen/src/value_plan.rs @@ -712,6 +712,7 @@ fn place_plan( slot, by_ref: ctx.cfg.is_param_by_ref(slot), }, + PlaceBase::Accessor(_) => panic!("mandatory-inline accessor place reached codegen"), }; let projections = ctx .cfg @@ -1885,6 +1886,9 @@ pub(crate) fn lower_value( CfgInstData::PlaceWrite { value, .. } => { lower_residual!(ResidualInput::PlaceWrite { value: *value }) } + CfgInstData::AccessorCall { .. } => { + panic!("mandatory-inline accessor call reached codegen") + } } } diff --git a/crates/rue-compiler/src/api_inventory.rs b/crates/rue-compiler/src/api_inventory.rs index 1756ad9e3..125d7622d 100644 --- a/crates/rue-compiler/src/api_inventory.rs +++ b/crates/rue-compiler/src/api_inventory.rs @@ -87,7 +87,7 @@ fn local_semantic_materialization_is_an_inert_exact_fact_boundary() { "pub(crate) fn identity(&self) -> &StableDefinitionKey", "pub(crate) fn lang_item(&self) -> Option", "rue_air::SemanticImportEpoch::new_local(", - ".materialize_local_body(", + ".materialize_local_body_with_types(", "FunctionInstanceKey::AnonymousMember", ] { assert!( diff --git a/crates/rue-compiler/src/artifact_views.rs b/crates/rue-compiler/src/artifact_views.rs index 7a8d886b6..6660787bf 100644 --- a/crates/rue-compiler/src/artifact_views.rs +++ b/crates/rue-compiler/src/artifact_views.rs @@ -1962,6 +1962,7 @@ fn cfg_instruction_kind(data: &rue_cfg::CfgInstData) -> &'static str { PlaceRead { .. } => "place_read", PlaceWrite { .. } => "place_write", Call { .. } => "call", + AccessorCall { .. } => "accessor_call", Intrinsic { .. } => "intrinsic", StructInit { .. } => "struct_initializer", ArrayInit { .. } => "array_initializer", diff --git a/crates/rue-compiler/src/cfg_query.rs b/crates/rue-compiler/src/cfg_query.rs index 69e1b7210..487ac1048 100644 --- a/crates/rue-compiler/src/cfg_query.rs +++ b/crates/rue-compiler/src/cfg_query.rs @@ -177,21 +177,181 @@ impl QueryKey for CfgQueryKey { } } +#[derive(Debug)] +pub(crate) struct AccessorCfgSubgraph { + pub(crate) roots: std::collections::BTreeMap, + pub(crate) dependencies: + std::collections::BTreeMap>, + pub(crate) accessors: std::collections::BTreeSet, +} + +#[derive(Debug)] +pub(crate) enum AccessorCfgSubgraphFailure { + Missing(crate::FunctionInstanceKey), + Cycle(crate::FunctionInstanceKey), +} + +pub(crate) fn accessor_source_name(identity: &crate::FunctionInstanceKey) -> String { + match identity { + crate::FunctionInstanceKey::Definition(definition) => definition.name().to_owned(), + crate::FunctionInstanceKey::Specialization { base, .. } => accessor_source_name(base), + crate::FunctionInstanceKey::AnonymousMember { member, .. } => member.name.to_string(), + crate::FunctionInstanceKey::DropGlue(_) => "".to_owned(), + } +} + +/// Build the exact accessor dependency closure shared by the session and +/// one-shot query collectors. Dependencies are in callee-before-caller +/// postorder so nested mandatory splices are deterministic. +pub(crate) fn accessor_cfg_subgraph( + keys: std::collections::BTreeMap, +) -> Result { + let direct = keys + .iter() + .map(|(function, key)| { + let callees = match &key.semantic_input { + CfgSemanticInput::Body { input, .. } => canonical_body(&input.canonical) + .instructions + .iter() + .filter_map(|instruction| match &instruction.data { + rue_air::SemanticBodyInstData::AccessorCall { function, .. } => { + Some(function.clone()) + } + _ => None, + }) + .collect(), + CfgSemanticInput::DropGlue { .. } => Vec::new(), + }; + (function.clone(), callees) + }) + .collect::>>(); + let mut accessors = keys + .iter() + .filter_map(|(function, key)| match &key.semantic_input { + CfgSemanticInput::Body { input, .. } + if canonical_body(&input.canonical).is_accessor => + { + Some(function.clone()) + } + _ => None, + }) + .collect::>(); + accessors.extend(direct.values().flatten().cloned()); + + fn postorder( + function: &crate::FunctionInstanceKey, + direct: &std::collections::BTreeMap< + crate::FunctionInstanceKey, + Vec, + >, + keys: &std::collections::BTreeMap, + visiting: &mut std::collections::BTreeSet, + seen: &mut std::collections::BTreeSet, + output: &mut Vec, + ) -> Result<(), AccessorCfgSubgraphFailure> { + for callee in direct.get(function).into_iter().flatten() { + if !keys.contains_key(callee) { + return Err(AccessorCfgSubgraphFailure::Missing(callee.clone())); + } + if !visiting.insert(callee.clone()) { + return Err(AccessorCfgSubgraphFailure::Cycle(callee.clone())); + } + if seen.insert(callee.clone()) { + postorder(callee, direct, keys, visiting, seen, output)?; + output.push(keys[callee].clone()); + } + visiting.remove(callee); + } + Ok(()) + } + + fn facts( + key: &CfgQueryKey, + ) -> Option<&crate::local_semantic_materialization::LocalMaterializationFacts> { + match &key.semantic_input { + CfgSemanticInput::Body { + materialization, .. + } => Some(materialization), + CfgSemanticInput::DropGlue { .. } => None, + } + } + fn with_facts( + key: &CfgQueryKey, + materialization: Arc, + ) -> CfgQueryKey { + let semantic_input = match &key.semantic_input { + CfgSemanticInput::Body { input, .. } => CfgSemanticInput::Body { + input: input.clone(), + materialization, + }, + CfgSemanticInput::DropGlue { .. } => key.semantic_input.clone(), + }; + CfgQueryKey::new( + key.function.clone(), + key.configuration.clone(), + semantic_input, + ) + } + + let mut roots = std::collections::BTreeMap::new(); + let mut dependencies = std::collections::BTreeMap::new(); + for function in keys.keys() { + let mut output = Vec::new(); + postorder( + function, + &direct, + &keys, + &mut std::collections::BTreeSet::from([function.clone()]), + &mut std::collections::BTreeSet::new(), + &mut output, + )?; + let root = &keys[function]; + if output.is_empty() { + roots.insert(function.clone(), root.clone()); + dependencies.insert(function.clone(), Arc::<[CfgQueryKey]>::from([])); + continue; + } + let merged = Arc::new( + crate::local_semantic_materialization::LocalMaterializationFacts::union( + std::iter::once(root).chain(output.iter()).filter_map(facts), + ), + ); + roots.insert(function.clone(), with_facts(root, merged.clone())); + dependencies.insert(function.clone(), output.iter().cloned().collect()); + } + Ok(AccessorCfgSubgraph { + roots, + dependencies, + accessors, + }) +} + #[derive(Debug, Clone)] pub(crate) struct OptimizedCfgQueryKey { pub(crate) cfg: CfgQueryKey, pub(crate) opt_level: rue_cfg::OptLevel, + pub(crate) accessor_dependencies: Arc<[CfgQueryKey]>, } impl OptimizedCfgQueryKey { - pub(crate) fn new(cfg: CfgQueryKey, opt_level: rue_cfg::OptLevel) -> Self { - Self { cfg, opt_level } + pub(crate) fn new( + cfg: CfgQueryKey, + opt_level: rue_cfg::OptLevel, + accessor_dependencies: Arc<[CfgQueryKey]>, + ) -> Self { + Self { + cfg, + opt_level, + accessor_dependencies, + } } } impl PartialEq for OptimizedCfgQueryKey { fn eq(&self, other: &Self) -> bool { - self.cfg == other.cfg && self.opt_level == other.opt_level + self.cfg == other.cfg + && self.opt_level == other.opt_level + && self.accessor_dependencies == other.accessor_dependencies } } @@ -201,12 +361,18 @@ impl std::hash::Hash for OptimizedCfgQueryKey { fn hash(&self, state: &mut H) { self.cfg.hash(state); std::mem::discriminant(&self.opt_level).hash(state); + self.accessor_dependencies.hash(state); } } impl QueryKey for OptimizedCfgQueryKey { fn stable_identity(&self) -> String { - format!("{};opt={:?}", self.cfg.stable_identity(), self.opt_level) + format!( + "{};opt={:?};accessors={}", + self.cfg.stable_identity(), + self.opt_level, + self.accessor_dependencies.len() + ) } } @@ -691,6 +857,7 @@ fn materialize_and_build_cfg( &facts.nominal_metadata, &facts.modules, &builtin_facts, + &facts.required_types, ) } CfgSemanticInput::DropGlue { owner, .. } => { @@ -704,6 +871,7 @@ fn materialize_and_build_cfg( &facts.nominal_metadata, &facts.modules, &builtin_facts, + &facts.required_types, ) } }; @@ -967,7 +1135,147 @@ pub(crate) fn evaluate_optimized_cfg( .with_terminal_kind(rue_query::QueryTerminalKind::Failure)); }; let _span = tracing::info_span!("cfg_optimization", phase = "cfg_and_optimization").entered(); - let current = record.cfg.clone(); + let mut current = record.cfg.clone(); + let mut domains = record.domains.clone(); + let interner = record.interner.clone(); + let mut strings = record.strings.to_vec(); + let mut local_atoms = record.local_atoms.to_vec(); + let mut symbol_mappings = record.codegen.symbol_mappings.as_ref().clone(); + let mut foreign_symbols = record.codegen.foreign_symbols.as_ref().clone(); + let mut materialization_warnings = record.materialization_warnings.to_vec(); + let mut warnings = record.warnings.to_vec(); + let mut implicit_destructor_targets = record + .implicit_destructor_targets + .iter() + .cloned() + .collect::>(); + let mut implicit_destructor_dependencies_complete = + record.implicit_destructor_dependencies_complete; + let mut accessor_cfgs = std::collections::BTreeMap::new(); + for dependency in key.accessor_dependencies.iter() { + let terminal = context.query_registered(cfgs, dependency.clone())?; + let QueryOutcome::Success(value) = terminal.outcome() else { + unreachable!("Cfg publishes typed values") + }; + let CfgValue::Available(callee) = value else { + return Ok(QueryOutput::success(value.clone()) + .with_terminal_kind(rue_query::QueryTerminalKind::Failure)); + }; + let body_span = match &dependency.semantic_input { + CfgSemanticInput::Body { input, .. } => input.body_span, + CfgSemanticInput::DropGlue { body_span, .. } => *body_span, + }; + accessor_cfgs.insert(dependency.function.clone(), (callee.clone(), body_span)); + } + loop { + let call = current.blocks().iter().find_map(|block| { + block.insts.iter().copied().find(|value| { + matches!( + current.get_inst(*value).data, + rue_cfg::CfgInstData::AccessorCall { .. } + ) + }) + }); + let Some(call) = call else { break }; + let rue_cfg::CfgInstData::AccessorCall { name, .. } = current.get_inst(call).data else { + unreachable!() + }; + let source_name = record.interner.resolve(&name); + let Some(identity) = domains.callable_for_symbol(name) else { + return Ok(QueryOutput::success(internal_failure( + format!("accessor call '{source_name}' has no stable callable identity"), + record.body_span, + )) + .with_terminal_kind(rue_query::QueryTerminalKind::Failure)); + }; + let Some((callee, callee_body_span)) = accessor_cfgs.get(&identity) else { + return Ok(QueryOutput::success(internal_failure( + format!("accessor CFG dependency is unavailable for '{source_name}'"), + record.body_span, + )) + .with_terminal_kind(rue_query::QueryTerminalKind::Failure)); + }; + let (callee_cfg, string_map) = match domains.import_accessor_cfg( + &callee.domains, + &callee.cfg, + &callee.interner, + &interner, + &mut strings, + *callee_body_span, + ) { + Ok(value) => value, + Err(error) => { + return Ok(QueryOutput::success(internal_failure( + format!("accessor CFG domain import failed: {error:?}"), + record.body_span, + )) + .with_terminal_kind(rue_query::QueryTerminalKind::Failure)); + } + }; + let callee_cfg = match callee_cfg.finish_after_optimization(&record.type_pool) { + Ok(cfg) => cfg, + Err(error) => { + return Ok(QueryOutput::success(internal_failure( + format!("imported accessor CFG failed verification: {error}"), + record.body_span, + )) + .with_terminal_kind(rue_query::QueryTerminalKind::Failure)); + } + }; + for atom in callee.local_atoms.iter() { + let mut atom = atom.clone(); + let Some(dense_id) = string_map.get(&atom.dense_id).copied() else { + return Ok(QueryOutput::success(internal_failure( + "accessor local atom has no imported string id", + record.body_span, + )) + .with_terminal_kind(rue_query::QueryTerminalKind::Failure)); + }; + atom.dense_id = dense_id; + if !local_atoms + .iter() + .any(|current| current.identity == atom.identity) + { + local_atoms.push(atom); + } + } + symbol_mappings.extend( + callee + .codegen + .symbol_mappings + .iter() + .map(|(k, v)| (k.clone(), v.clone())), + ); + foreign_symbols.extend(callee.codegen.foreign_symbols.iter().cloned()); + for warning in import_warnings( + &callee.materialization_warnings, + callee.body_span, + *callee_body_span, + ) { + if !materialization_warnings.contains(&warning) { + materialization_warnings.push(warning); + } + } + for warning in import_warnings(&callee.warnings, callee.body_span, *callee_body_span) { + if !warnings.contains(&warning) { + warnings.push(warning); + } + } + implicit_destructor_targets.extend(callee.implicit_destructor_targets.iter().cloned()); + implicit_destructor_dependencies_complete &= + callee.implicit_destructor_dependencies_complete; + current = match rue_cfg::inline_call(¤t, call, &callee_cfg, &record.type_pool) { + Ok(cfg) => cfg, + Err(error) => { + return Ok(QueryOutput::success(internal_failure( + format!("mandatory accessor CFG splice failed: {error}"), + record.body_span, + )) + .with_terminal_kind(rue_query::QueryTerminalKind::Failure)); + } + }; + context.record_work(rue_query::WorkItem::new("cfg.accessor-splices", 1)); + } context.record_work(rue_query::WorkItem::new("cfg.optimize.attempts", 1)); context.record_work(rue_query::WorkItem::new( "cfg.optimize.nonzero-level", @@ -981,18 +1289,24 @@ pub(crate) fn evaluate_optimized_cfg( air: record.air.clone(), source_name: record.source_name.clone(), cfg, - domains: record.domains.clone(), + domains, type_pool: record.type_pool.clone(), - interner: record.interner.clone(), - strings: record.strings.clone(), - local_atoms: record.local_atoms.clone(), - codegen: record.codegen.clone(), - materialization_warnings: record.materialization_warnings.clone(), + interner, + strings: strings.into(), + local_atoms: local_atoms.into(), + codegen: Arc::new(CfgCodegenDomain { + defined_symbol: record.codegen.defined_symbol.clone(), + symbol_mappings: Arc::new(symbol_mappings), + foreign_symbols: Arc::new(foreign_symbols), + }), + materialization_warnings: materialization_warnings.into(), body_span: record.body_span, - warnings: record.warnings.clone(), - implicit_destructor_targets: record.implicit_destructor_targets.clone(), - implicit_destructor_dependencies_complete: record - .implicit_destructor_dependencies_complete, + warnings: warnings.into(), + implicit_destructor_targets: implicit_destructor_targets + .into_iter() + .collect::>() + .into(), + implicit_destructor_dependencies_complete, }, )))) } diff --git a/crates/rue-compiler/src/drop_glue.rs b/crates/rue-compiler/src/drop_glue.rs index a18e584b9..960a12e9c 100644 --- a/crates/rue-compiler/src/drop_glue.rs +++ b/crates/rue-compiler/src/drop_glue.rs @@ -223,6 +223,7 @@ pub(crate) fn synthesize_canonical_drop_glue( }; add(SemanticBodyInstData::Ret(Some(value)), Ty::Unit); Ok(Body { + is_accessor: false, return_type: Ty::Unit, instructions: instructions.into(), places: Arc::new([]), diff --git a/crates/rue-compiler/src/durable_cfg.rs b/crates/rue-compiler/src/durable_cfg.rs index e23cba544..5f4550f6f 100644 --- a/crates/rue-compiler/src/durable_cfg.rs +++ b/crates/rue-compiler/src/durable_cfg.rs @@ -74,6 +74,7 @@ fn live_instruction_kind(data: &AirInstData) -> rue_air::SemanticBodyInstKind { runtime: Some(_), .. } => K::RuntimeCall, AirInstData::Call { runtime: None, .. } => K::Call, + AirInstData::AccessorCall { .. } => K::AccessorCall, AirInstData::CallGeneric { .. } => K::CallGeneric, AirInstData::Intrinsic { .. } => K::Intrinsic, AirInstData::Param { .. } => K::Param, @@ -352,13 +353,13 @@ impl RetainedCharge for CfgDomainProjection { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum CfgDomainFailure { Shape, Unsupported, Missing, MissingLiveType(Type), - MissingStableType, + MissingStableType(CanonicalType), MissingSymbol, MissingString, Edit(rue_cfg::CfgEditError), @@ -380,6 +381,214 @@ impl CfgDomainFailure { } impl CfgDomainProjection { + /// Admit stable symbols already owned by a surrounding semantic output. + pub(crate) fn admit_stable_symbols( + &mut self, + old: &Self, + old_interner: &lasso::ThreadedRodeo, + new_interner: &lasso::ThreadedRodeo, + ) -> Result<(), CfgDomainFailure> { + for (old_symbol, stable) in &old.symbols { + if self + .symbols + .iter() + .any(|(_, candidate)| candidate == stable) + { + continue; + } + let symbol = new_interner.get_or_intern(old_interner.resolve(old_symbol)); + self.symbols.push((symbol, stable.clone())); + } + self.symbols.sort_by(|left, right| { + (left.0.into_usize(), &left.1).cmp(&(right.0.into_usize(), &right.1)) + }); + self.symbols.dedup(); + if self.symbols.windows(2).any(|pair| pair[0].0 == pair[1].0) { + return Err(CfgDomainFailure::Shape); + } + Ok(()) + } + + /// Admit stable types already owned by a surrounding semantic output. + pub(crate) fn admit_stable_types( + &mut self, + old: &Self, + type_pool: &rue_air::FrozenTypeInternPool, + aggregates: &HashMap, + ) -> Result<(), CfgDomainFailure> { + for (_, stable) in &old.types { + if self.current_type(stable).is_ok() { + continue; + } + let current = type_pool + .all_types() + .find(|candidate| { + canonical_type_from_live(*candidate, type_pool, aggregates) + .is_ok_and(|value| value == *stable) + }) + .ok_or_else(|| CfgDomainFailure::MissingStableType(stable.clone()))?; + self.types.push((current, stable.clone())); + } + self.types = deduplicate_type_mappings(std::mem::take(&mut self.types))?; + Ok(()) + } + + /// Admit stable strings already owned by a surrounding semantic output. + /// + /// Optimized accessor splicing can add callee literals to a caller CFG. + /// The one-shot semantic adapter owns the merged program string table, so + /// relocation maps those stable literals to that table before importing + /// the optimized terminal. + pub(crate) fn admit_stable_strings( + &mut self, + old: &Self, + strings: &[String], + ) -> Result<(), CfgDomainFailure> { + for (_, stable) in &old.strings { + let index = strings + .iter() + .position(|value| value == stable.as_ref()) + .ok_or(CfgDomainFailure::MissingString) + .and_then(|index| u32::try_from(index).map_err(|_| CfgDomainFailure::Shape))?; + if !self + .strings + .iter() + .any(|(candidate, value)| *candidate == index && value == stable) + { + self.strings.push((index, stable.clone())); + } + } + self.strings.sort_by_key(|(index, _)| *index); + self.strings.dedup(); + if self.strings.windows(2).any(|pair| pair[0].0 == pair[1].0) { + return Err(CfgDomainFailure::Shape); + } + Ok(()) + } + + /// Extend this function-local live domain with the stable values used by + /// an accessor CFG, then remap that CFG into the extended domain. Accessor + /// splicing preserves the callee's source spans and stable local atoms; + /// only dense string ids and live type/symbol ids are relocated. + pub(crate) fn import_accessor_cfg( + &mut self, + old: &Self, + cfg: &rue_cfg::Cfg, + old_interner: &lasso::ThreadedRodeo, + new_interner: &lasso::ThreadedRodeo, + strings: &mut Vec, + new_body_span: Span, + ) -> Result<(rue_cfg::CfgEditor, std::collections::BTreeMap), CfgDomainFailure> { + if old.incomplete_epoch.is_some() || self.incomplete_epoch.is_some() { + return Err(CfgDomainFailure::Missing); + } + for (_, stable) in &old.types { + self.current_type(stable)?; + } + for (live, stable) in &old.symbols { + if !self + .symbols + .iter() + .any(|(_, candidate)| candidate == stable) + { + let symbol = new_interner.get_or_intern(old_interner.resolve(live)); + self.symbols.push((symbol, stable.clone())); + } + } + let mut string_map = std::collections::BTreeMap::new(); + for (old_index, stable) in &old.strings { + let new_index = if let Some(index) = + strings.iter().position(|value| value == stable.as_ref()) + { + u32::try_from(index).map_err(|_| CfgDomainFailure::Shape)? + } else { + let index = u32::try_from(strings.len()).map_err(|_| CfgDomainFailure::Shape)?; + strings.push(stable.to_string()); + index + }; + string_map.insert(*old_index, new_index); + if !self + .strings + .iter() + .any(|(index, value)| *index == new_index && value == stable) + { + self.strings.push((new_index, stable.clone())); + } + } + self.types = deduplicate_type_mappings(std::mem::take(&mut self.types))?; + self.symbols.sort_by(|left, right| { + (left.0.into_usize(), &left.1).cmp(&(right.0.into_usize(), &right.1)) + }); + self.symbols.dedup(); + + let imported = cfg + .try_remap_domains( + |value| self.current_type(&old.stable_type(value)?), + |value| match self + .current_nominal(&old.stable_nominal(Type::new_struct(value))?)? + .kind() + { + TypeKind::Struct(id) => Ok(id), + _ => Err(CfgDomainFailure::Shape), + }, + |value| match self + .current_nominal(&old.stable_nominal(Type::new_enum(value))?)? + .kind() + { + TypeKind::Enum(id) => Ok(id), + _ => Err(CfgDomainFailure::Shape), + }, + |value: Spur| { + let stable = old + .symbols + .iter() + .find(|(symbol, _)| *symbol == value) + .map(|(_, value)| value) + .ok_or(CfgDomainFailure::MissingSymbol)?; + self.symbols + .iter() + .find(|(_, identity)| identity == stable) + .map(|(symbol, _)| *symbol) + .ok_or(CfgDomainFailure::MissingSymbol) + }, + |value| { + string_map + .get(&value) + .copied() + .ok_or(CfgDomainFailure::MissingString) + }, + |value| { + let anchor = old + .spans + .iter() + .find(|(span, _)| *span == value) + .map(|(_, anchor)| *anchor) + .unwrap_or_else(|| StableCfgSpan::new(value, old.body_span)); + anchor.relocate(new_body_span) + }, + ) + .map_err(|error| match error { + rue_cfg::CfgRemapError::Domain(error) => error, + rue_cfg::CfgRemapError::Edit(error) => CfgDomainFailure::Edit(error), + })?; + Ok((imported, string_map)) + } + + pub(crate) fn callable_for_symbol(&self, name: Spur) -> Option { + self.symbols.iter().find_map(|(live, stable)| { + if *live != name { + return None; + } + match stable { + StableCfgSymbol::Callable(callable) => Some(callable.clone()), + StableCfgSymbol::Specialization(identity) => { + crate::semantic_identity::function_instance_from_specialization(identity) + } + StableCfgSymbol::Runtime(_) | StableCfgSymbol::Intrinsic(_) => None, + } + }) + } + pub(crate) fn same_live_domain(&self, other: &Self) -> bool { let complete_or_same_epoch = match (&self.incomplete_epoch, &other.incomplete_epoch) { (None, None) => true, @@ -404,7 +613,7 @@ impl CfgDomainProjection { body_span: Span, strings: &[String], interner: &lasso::ThreadedRodeo, - stable_type: impl Fn(Type) -> Result, + stable_type: impl Fn(Type) -> Result + Copy, stable_callable: impl Fn(lasso::Spur) -> Option, ) -> Result { let mut types = vec![ @@ -454,7 +663,7 @@ impl CfgDomainProjection { AirInstData::IntCast { from_ty, .. } => { types.push((*from_ty, stable_type(*from_ty)?)); } - AirInstData::Call { name, .. } => { + AirInstData::Call { name, .. } | AirInstData::AccessorCall { name, .. } => { let symbol = stable_callable(*name) .map(StableCfgSymbol::Callable) .unwrap_or_else(|| { @@ -761,10 +970,10 @@ impl CfgDomainProjection { crate::ModuleId, >, body: &rue_air::SemanticBody, - stable_type: impl Fn(Type) -> Result, + stable_type: impl Fn(Type) -> Result + Copy, stable_callable: impl Fn(lasso::Spur) -> Option, ) -> Result { - Self::from_body_parts( + let mut projection = Self::from_body_parts( &materialization.air, &materialization.identity, &materialization.local_atoms, @@ -776,7 +985,15 @@ impl CfgDomainProjection { &materialization.interner, stable_type, stable_callable, - ) + )?; + // The caller's materialization includes its transitive accessor + // closure. Preserve the complete reverse map so callee-only stable + // types resolve to caller-owned live handles during mandatory splice. + projection + .types + .extend(materialization.materialized_types.iter().cloned()); + projection.types = deduplicate_type_mappings(projection.types)?; + Ok(projection) } #[allow(clippy::too_many_arguments)] @@ -888,6 +1105,10 @@ impl CfgDomainProjection { }, DurableAirInstData::Call { function, .. }, ) => symbols.push((*name, StableCfgSymbol::Callable(function.clone()))), + ( + AirInstData::AccessorCall { name, .. }, + DurableAirInstData::AccessorCall { function, .. }, + ) => symbols.push((*name, StableCfgSymbol::Callable(function.clone()))), ( AirInstData::Call { runtime: None, @@ -1115,7 +1336,7 @@ impl CfgDomainProjection { .iter() .find(|(_, stable)| stable == value) .map(|(current, _)| *current) - .ok_or(CfgDomainFailure::MissingStableType) + .ok_or_else(|| CfgDomainFailure::MissingStableType(value.clone())) } fn stable_nominal(&self, value: Type) -> Result { self.stable_type(value) @@ -1296,4 +1517,42 @@ mod tests { matches!(imported.get_inst(rue_cfg::CfgValue::from_raw(0)).data, CfgInstData::Call { name, .. } if name == new) ); } + + #[test] + fn accessor_import_reanchors_reused_callee_spans() { + let old_interner = lasso::ThreadedRodeo::new(); + let new_interner = lasso::ThreadedRodeo::new(); + let old_symbol = old_interner.get_or_intern("callee"); + let new_symbol = new_interner.get_or_intern("callee"); + let stable = StableCfgSymbol::Intrinsic(Arc::from("callee")); + let mut old = projection_with(old_symbol, stable.clone()); + old.body_span = Span::new(10, 20); + old.spans = vec![( + Span::new(12, 13), + StableCfgSpan::Relative { start: 2, end: 3 }, + )]; + let mut current = projection_with(new_symbol, stable); + current.body_span = Span::new(30, 40); + current.spans.clear(); + let mut cfg = Cfg::new(Type::I32, 0, 0, "f".into(), Vec::::new()); + let block = cfg.new_block(); + cfg.append_call(block, None, old_symbol, [], Type::I32, Span::new(12, 13)) + .unwrap(); + + let (imported, _) = current + .import_accessor_cfg( + &old, + &cfg, + &old_interner, + &new_interner, + &mut Vec::new(), + Span::new(50, 60), + ) + .unwrap(); + + assert_eq!( + imported.get_inst(rue_cfg::CfgValue::from_raw(0)).span, + Span::new(52, 53) + ); + } } diff --git a/crates/rue-compiler/src/local_semantic_materialization.rs b/crates/rue-compiler/src/local_semantic_materialization.rs index 37a44800a..dc391d42f 100644 --- a/crates/rue-compiler/src/local_semantic_materialization.rs +++ b/crates/rue-compiler/src/local_semantic_materialization.rs @@ -56,6 +56,69 @@ pub(crate) struct LocalMaterializationFacts { pub(crate) nominal_metadata: Arc<[LocalNominalMetadataFact]>, pub(crate) modules: Arc<[ModuleId]>, pub(crate) builtin_nominals: Arc<[LocalBuiltinNominalRequest]>, + /// Exact stable types that must exist in this local epoch even when they + /// originate in an accessor body that will be mandatorily spliced here. + pub(crate) required_types: Arc<[crate::durable_semantics::DurableType]>, +} + +impl LocalMaterializationFacts { + pub(crate) fn union<'a>(facts: impl IntoIterator) -> Self { + let mut declarations = Vec::new(); + let mut anonymous_nominals = Vec::new(); + let mut callables = Vec::new(); + let mut nominal_metadata = Vec::new(); + let mut modules = Vec::new(); + let mut builtin_nominals = Vec::new(); + let mut required_types = Vec::new(); + for facts in facts { + for (source, destination) in [(facts.declarations.as_ref(), &mut declarations)] { + for value in source { + if !destination.contains(value) { + destination.push(value.clone()); + } + } + } + for value in facts.anonymous_nominals.iter() { + if !anonymous_nominals.contains(value) { + anonymous_nominals.push(value.clone()); + } + } + for value in facts.callables.iter() { + if !callables.contains(value) { + callables.push(value.clone()); + } + } + for value in facts.nominal_metadata.iter() { + if !nominal_metadata.contains(value) { + nominal_metadata.push(value.clone()); + } + } + for value in facts.modules.iter() { + if !modules.contains(value) { + modules.push(value.clone()); + } + } + for value in facts.builtin_nominals.iter() { + if !builtin_nominals.contains(value) { + builtin_nominals.push(value.clone()); + } + } + for value in facts.required_types.iter() { + if !required_types.contains(value) { + required_types.push(value.clone()); + } + } + } + Self { + declarations: declarations.into(), + anonymous_nominals: anonymous_nominals.into(), + callables: callables.into(), + nominal_metadata: nominal_metadata.into(), + modules: modules.into(), + builtin_nominals: builtin_nominals.into(), + required_types: required_types.into(), + } + } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -102,6 +165,7 @@ impl RetainedCharge for LocalMaterializationFacts { .saturating_add(self.nominal_metadata.retained_charge()) .saturating_add(self.modules.retained_charge()) .saturating_add(self.builtin_nominals.retained_charge()) + .saturating_add(self.required_types.retained_charge()) } } @@ -378,6 +442,7 @@ pub(crate) fn materialize_canonical_body( nominal_metadata: &[LocalNominalMetadataFact], modules: &[ModuleId], builtin_facts: &[LocalBuiltinNominalFact], + materialization_types: &[crate::durable_semantics::DurableType], ) -> Result { let (identity, body) = match canonical { crate::body_query::CanonicalBody::Ordinary { owner, body } => { @@ -402,6 +467,7 @@ pub(crate) fn materialize_canonical_body( nominal_metadata, modules, builtin_facts, + materialization_types, ) } @@ -416,8 +482,13 @@ pub(crate) fn materialize_semantic_body( nominal_metadata: &[LocalNominalMetadataFact], modules: &[ModuleId], builtin_facts: &[LocalBuiltinNominalFact], + materialization_types: &[crate::durable_semantics::DurableType], ) -> Result { - let callable_kind = callable_kind_for_identity(&identity); + let callable_kind = if body.is_accessor { + rue_air::AnalyzedCallableKind::Accessor + } else { + callable_kind_for_identity(&identity) + }; let mut destructors = std::collections::BTreeMap::new(); for candidate in declarations { @@ -643,7 +714,13 @@ pub(crate) fn materialize_semantic_body( }) .collect(); let epoch = rue_air::SemanticImportEpoch::new_local(nominals, callables, modules.to_vec())?; - Ok(epoch.materialize_local_body(identity, callable_kind, body, body_span)?) + Ok(epoch.materialize_local_body_with_types( + identity, + callable_kind, + body, + body_span, + materialization_types, + )?) } /// Select the transitive nominal and callable closure needed to materialize one @@ -1125,8 +1202,21 @@ pub(crate) fn select_materialization_facts( slice_sources, }; selection.callable(identity); + let mut required_types = Vec::new(); + let mut require_type = |ty: &crate::durable_semantics::DurableType| { + if !required_types.contains(ty) { + required_types.push(ty.clone()); + } + }; + require_type(&body.return_type); + if !body.strings.is_empty() { + require_type(&rue_air::SemanticImportType::PtrConst(Box::new( + rue_air::SemanticImportType::U8, + ))); + } selection.semantic_type(&body.return_type); for instruction in body.instructions.iter() { + require_type(&instruction.ty); selection.semantic_type(&instruction.ty); if let rue_air::SemanticBodyInstData::CallSpecialized { identity, .. } = &instruction.data && let Some(callable) = @@ -1142,11 +1232,15 @@ pub(crate) fn select_materialization_facts( } Dependency::Nominal(nominal) => selection.nominal(nominal), Dependency::Function(function) => selection.callable(function), - Dependency::Type(ty) => selection.semantic_type(ty), + Dependency::Type(ty) => { + require_type(ty); + selection.semantic_type(ty); + } Dependency::Instruction(_) | Dependency::Place(_) | Dependency::String(_) => {} }); } for place in body.places.iter() { + require_type(&place.base_type); selection.semantic_type(&place.base_type); for projection in place.projections.iter() { match projection { @@ -1154,12 +1248,14 @@ pub(crate) fn select_materialization_facts( selection.nominal(struct_key) } rue_air::SemanticBodyProjection::Index { array_type, .. } => { + require_type(array_type); selection.semantic_type(array_type) } } } } for (_, ty) in body.param_drops.iter() { + require_type(ty); selection.semantic_type(ty); } for reference in body.method_references.iter() { @@ -1215,6 +1311,7 @@ pub(crate) fn select_materialization_facts( nominal_metadata: nominal_metadata.into(), modules: selection.modules.into_iter().collect::>().into(), builtin_nominals: selection.builtins.into_iter().collect::>().into(), + required_types: required_types.into(), }) } @@ -1234,6 +1331,7 @@ pub(crate) fn select_drop_glue_materialization_facts( ) })); let body = rue_air::SemanticBody { + is_accessor: false, return_type: rue_air::SemanticImportType::Unit, instructions: Arc::new([]), places: Arc::new([]), @@ -1265,6 +1363,7 @@ mod tests { fn body() -> rue_air::SemanticBody { use rue_air::{SemanticBody, SemanticBodyAnchor, SemanticBodyInst, SemanticBodyInstData}; SemanticBody { + is_accessor: false, return_type: rue_air::SemanticImportType::I32, instructions: vec![ SemanticBodyInst { @@ -1339,6 +1438,7 @@ mod tests { &[], std::slice::from_ref(&module), &[], + &[], ) .err(), Some(LocalMaterializationFailure::MissingNominalMetadata) @@ -1358,6 +1458,7 @@ mod tests { )], std::slice::from_ref(&module), &[], + &[], ) .unwrap(); let ty = output @@ -1429,6 +1530,7 @@ mod tests { &[LocalNominalMetadataFact::new(record, None)], &[module], &[], + &[], ) .err() .expect("duplicate destructor records must not select the first"); @@ -1502,6 +1604,7 @@ mod tests { &facts.nominal_metadata, &facts.modules, &[], + &[], ) .expect("fallback destructor symbols materialize without violating AIR invariants"); } diff --git a/crates/rue-compiler/src/queries.rs b/crates/rue-compiler/src/queries.rs index c39235fa1..763ed6d37 100644 --- a/crates/rue-compiler/src/queries.rs +++ b/crates/rue-compiler/src/queries.rs @@ -526,6 +526,78 @@ pub(crate) fn collect_function_cfg_queries( }) .collect::>(); + // Build the exact accessor subgraph over canonical per-body artifacts. + // These keys become nested `compiler.cfg` reads of each caller's + // optimized-CFG query, so an accessor-body edit invalidates precisely its + // transitive callers without an epoch-wide eligibility switch (RUE-1208). + let mut raw_accessor_keys = std::collections::BTreeMap::new(); + for input in stable_inputs { + let body = match input.canonical.as_ref() { + crate::body_query::CanonicalBody::Ordinary { body, .. } + | crate::body_query::CanonicalBody::Anonymous { body, .. } + | crate::body_query::CanonicalBody::Specialization { body, .. } => body, + }; + let materialization = crate::local_semantic_materialization::select_materialization_facts( + &input.function, + body, + durable_declarations, + durable_anonymous_nominals, + &callable_symbols, + ) + .map_err(|error| CfgConstructionFailure { + errors: CompileError::new( + ErrorKind::InternalError(format!( + "accessor CFG materialization fact selection failed: {error:?}" + )), + input.body_span, + ) + .into(), + work: work.clone(), + })?; + let semantic_input = crate::cfg_query::CfgSemanticInput::Body { + input: std::sync::Arc::new(input.clone()), + materialization: std::sync::Arc::new(materialization), + }; + raw_accessor_keys.insert( + input.function.clone(), + crate::cfg_query::CfgQueryKey::new( + input.function.clone(), + configuration.clone(), + semantic_input, + ), + ); + } + let accessor_subgraph = + crate::cfg_query::accessor_cfg_subgraph(raw_accessor_keys).map_err(|failure| { + let (kind, span) = match failure { + crate::cfg_query::AccessorCfgSubgraphFailure::Missing(identity) => ( + ErrorKind::InternalError(format!( + "accessor CFG dependency is missing: {identity:?}" + )), + rue_span::Span::default(), + ), + crate::cfg_query::AccessorCfgSubgraphFailure::Cycle(identity) => ( + ErrorKind::AccessorRecursion { + method: crate::cfg_query::accessor_source_name(&identity), + }, + stable_inputs + .iter() + .find(|input| input.function == identity) + .map_or(rue_span::Span::default(), |input| input.body_span), + ), + }; + CfgConstructionFailure { + errors: CompileError::new(kind, span).into(), + work: work.clone(), + } + })?; + let accessor_roots = accessor_subgraph.roots; + let accessor_dependencies = accessor_subgraph.dependencies; + // Accessors have no out-of-line ABI. Their raw CFGs are query + // dependencies only; every executable occurrence is consumed by the + // caller's mandatory splice above. + all_functions.retain(|(_, identity, _, _, _)| !accessor_subgraph.accessors.contains(identity)); + let _span = info_span!("cfg_collection", phase = "cfg_query_collection").entered(); let aggregate_types = std::sync::Arc::new(stable_aggregate_types); let results: Vec<_> = all_functions @@ -556,7 +628,9 @@ pub(crate) fn collect_function_cfg_queries( canonical_semantic::CfgConstructionWork::default(), )); }; - let semantic_input = if let Some(input) = current_input { + let semantic_input = if let Some(root) = accessor_roots.get(&semantic_identity) { + root.semantic_input.clone() + } else if let Some(input) = current_input { let body = match input.canonical.as_ref() { crate::body_query::CanonicalBody::Ordinary { body, .. } | crate::body_query::CanonicalBody::Anonymous { body, .. } @@ -632,7 +706,7 @@ pub(crate) fn collect_function_cfg_queries( )); }; let func = std::sync::Arc::new(func); - let domains = match current_input + let mut domains = match current_input .map(|input| { crate::durable_cfg::CfgDomainProjection::from_body( &func, @@ -694,6 +768,10 @@ pub(crate) fn collect_function_cfg_queries( configuration.clone(), semantic_input, opt_level, + accessor_dependencies + .get(&semantic_identity) + .cloned() + .unwrap_or_else(|| std::sync::Arc::new([])), cancellation.clone(), ) .map_err(|abort| { @@ -783,6 +861,42 @@ pub(crate) fn collect_function_cfg_queries( function_work, )), crate::cfg_query::CfgValue::Available(record) => { + domains + .admit_stable_symbols(&record.domains, &record.interner, &interner) + .map_err(|failure| { + ( + CompileError::new( + failure.error_kind("CFG terminal relocation failed"), + body_span, + ) + .into(), + function_work.clone(), + ) + })?; + domains + .admit_stable_types(&record.domains, &type_pool, &aggregate_types) + .map_err(|failure| { + ( + CompileError::new( + failure.error_kind("CFG terminal relocation failed"), + body_span, + ) + .into(), + function_work.clone(), + ) + })?; + domains + .admit_stable_strings(&record.domains, &strings) + .map_err(|failure| { + ( + CompileError::new( + failure.error_kind("CFG terminal relocation failed"), + body_span, + ) + .into(), + function_work.clone(), + ) + })?; let cfg = if record.domains.same_live_domain(&domains) { record.cfg.clone() } else { diff --git a/crates/rue-compiler/src/retained_charge.rs b/crates/rue-compiler/src/retained_charge.rs index a436f5926..93474666d 100644 --- a/crates/rue-compiler/src/retained_charge.rs +++ b/crates/rue-compiler/src/retained_charge.rs @@ -758,6 +758,9 @@ impl RetainedCharge for rue_air::SemanticB Self::Call { function, args } => function .retained_charge() .saturating_add(args.retained_charge()), + Self::AccessorCall { function, args } => function + .retained_charge() + .saturating_add(args.retained_charge()), Self::RuntimeCall { args, .. } => args.retained_charge(), Self::CallSpecialized { identity, args } => identity .retained_charge() diff --git a/crates/rue-compiler/src/revisioned_query_database.rs b/crates/rue-compiler/src/revisioned_query_database.rs index 816f7264a..daee04947 100644 --- a/crates/rue-compiler/src/revisioned_query_database.rs +++ b/crates/rue-compiler/src/revisioned_query_database.rs @@ -10114,6 +10114,7 @@ fn resolve_parsed_semantic_signature( crate::declaration_candidate::DeclarationParameterMode::Borrow => M::Borrow, crate::declaration_candidate::DeclarationParameterMode::Inout => M::Inout, }, + is_accessor: *is_accessor, is_unchecked: *is_unchecked, is_extern: *is_extern, is_c_export: *is_c_export, @@ -15508,6 +15509,7 @@ impl RevisionedQueryDatabase { configuration: crate::semantic_query_nucleus::SemanticQueryConfiguration, semantic_input: crate::cfg_query::CfgSemanticInput, opt_level: rue_cfg::OptLevel, + accessor_dependencies: Arc<[crate::cfg_query::CfgQueryKey]>, cancellation: CancellationToken, ) -> Result< ( @@ -15517,7 +15519,8 @@ impl RevisionedQueryDatabase { QueryAbort, > { let cfg = crate::cfg_query::CfgQueryKey::new(function, configuration, semantic_input); - let optimized = crate::cfg_query::OptimizedCfgQueryKey::new(cfg, opt_level); + let optimized = + crate::cfg_query::OptimizedCfgQueryKey::new(cfg, opt_level, accessor_dependencies); let attempt = self.runtime.request_registered( &self.optimized_cfgs, revision, @@ -20465,6 +20468,7 @@ impl rue_air::DurableCallableSource result, has_self, self_mode, + is_accessor, .. } = signature.signature else { @@ -20496,6 +20500,7 @@ impl rue_air::DurableCallableSource rue_air::SemanticParameterMode::Inout } }, + is_accessor, }) } @@ -23036,6 +23041,7 @@ pub(crate) mod test_support { rue_air::SemanticParameterMode::Inout } }, + is_accessor: false, }) } } @@ -34544,6 +34550,7 @@ fn main() -> i32 { &[], std::slice::from_ref(&module), &[], + &[], ) .expect("durable provider export materializes in a fresh local epoch"); diff --git a/crates/rue-compiler/src/semantic_query_nucleus.rs b/crates/rue-compiler/src/semantic_query_nucleus.rs index 691111cfe..e6143f378 100644 --- a/crates/rue-compiler/src/semantic_query_nucleus.rs +++ b/crates/rue-compiler/src/semantic_query_nucleus.rs @@ -801,6 +801,7 @@ pub(crate) enum DeclarationSignatureProjection { result: DurableType, has_self: bool, self_mode: crate::durable_semantics::DurableParameterMode, + is_accessor: bool, is_unchecked: bool, is_extern: bool, is_c_export: bool, @@ -1048,6 +1049,7 @@ impl DeclarationSemanticValue { result, has_self, self_mode, + is_accessor: _, is_unchecked, is_extern: _, is_c_export: _, diff --git a/crates/rue-compiler/src/session.rs b/crates/rue-compiler/src/session.rs index 6a95090d6..8ad934b57 100644 --- a/crates/rue-compiler/src/session.rs +++ b/crates/rue-compiler/src/session.rs @@ -4831,6 +4831,44 @@ impl CompilerSession { )); } cfg_inputs.sort_by(|left, right| left.0.cmp(&right.0)); + let mut raw_accessor_keys = std::collections::BTreeMap::new(); + for (function, semantic_input, _) in &cfg_inputs { + raw_accessor_keys.insert( + function.clone(), + crate::cfg_query::CfgQueryKey::new( + function.clone(), + graph.configuration.clone(), + semantic_input.clone(), + ), + ); + } + let accessor_subgraph = crate::cfg_query::accessor_cfg_subgraph(raw_accessor_keys) + .map_err(|failure| { + let (kind, span) = match failure { + crate::cfg_query::AccessorCfgSubgraphFailure::Missing(identity) => ( + ErrorKind::InternalError(format!( + "accessor CFG dependency is missing: {identity:?}" + )), + fallback_span, + ), + crate::cfg_query::AccessorCfgSubgraphFailure::Cycle(identity) => { + let span = cfg_inputs + .iter() + .find(|(function, _, _)| function == &identity) + .map_or(fallback_span, |(_, _, body_span)| *body_span); + ( + ErrorKind::AccessorRecursion { + method: crate::cfg_query::accessor_source_name(&identity), + }, + span, + ) + } + }; + CompileError::new(kind, span) + })?; + let accessor_roots = accessor_subgraph.roots; + let accessor_dependencies = accessor_subgraph.dependencies; + let accessor_functions = accessor_subgraph.accessors; let mut cfgs = Vec::with_capacity(cfg_inputs.len()); #[cfg(test)] self.rooted_cfg_executions.clear(); @@ -4838,6 +4876,9 @@ impl CompilerSession { tracing::info_span!("optimized_cfg_collection", phase = "cfg_and_optimization") .entered(); for (function, semantic_input, body_span) in cfg_inputs { + if accessor_functions.contains(&function) { + continue; + } let (optimized_cfg_key, attempt) = self .queries .revisioned @@ -4845,8 +4886,15 @@ impl CompilerSession { graph.revision, function.clone(), graph.configuration.clone(), - semantic_input, + accessor_roots + .get(&function) + .map(|key| key.semantic_input.clone()) + .unwrap_or(semantic_input), options.opt_level, + accessor_dependencies + .get(&function) + .cloned() + .unwrap_or_else(|| Arc::new([])), rue_query::CancellationToken::new(), ) .map_err(|abort| { diff --git a/crates/rue-compiler/src/unstable.rs b/crates/rue-compiler/src/unstable.rs index 9180a6110..a463d9511 100644 --- a/crates/rue-compiler/src/unstable.rs +++ b/crates/rue-compiler/src/unstable.rs @@ -924,7 +924,6 @@ mod codegen_unit_tests { let mut session = crate::CompilerSession::new(); crate::publish_test_snapshot(&mut session, &snapshot).unwrap(); let semantic = session.canonical_semantic(&options).unwrap(); - let errors = crate::codegen_query::with_test_codegen_failure_injection(|| { session .codegen_products( @@ -1054,6 +1053,138 @@ mod codegen_unit_tests { ); } + #[test] + fn accessor_edit_recomputes_caller_without_publishing_accessor_abi() { + let source = |value| { + crate::SourceSnapshot::single( + "main.rue", + format!( + "struct P {{ x: i64, fn value(borrow self) -> borrow i64 {{ if self.x == {value} {{ let bad = 1 / 0; if bad == 0 {{ }} }} yield self.x; }} }} fn helper() -> i64 {{ 1 }} fn main() -> i32 {{ let p = P {{ x: 7 }}; if p.value() + helper() == 8 {{ 0 }} else {{ 1 }} }}" + ), + ) + .unwrap() + }; + let mut options = crate::CompileOptions::default(); + options + .preview_features + .insert(rue_error::PreviewFeature::BorrowAccessors); + let mut session = crate::CompilerSession::new(); + + session.update(&source(7)).into_result().unwrap(); + let semantic = session.canonical_semantic(&options).unwrap(); + let cold = session + .codegen_products( + &semantic, + &options, + rue_codegen::BackendArtifactRequest::default(), + ) + .unwrap(); + assert_eq!(cold.len(), 2, "accessors have no out-of-line ABI unit"); + assert!( + cold.iter() + .all(|unit| !unit.machine_name.contains(".value")) + ); + let cold_rooted = session.rooted_cfg(&options).unwrap(); + let cold_helper_key = cold_rooted + .cfgs + .iter() + .find(|unit| crate::cfg_query::accessor_source_name(&unit.function) == "helper") + .unwrap() + .optimized_cfg_key + .clone(); + + session.update(&source(8)).into_result().unwrap(); + let warm_rooted = session.rooted_cfg(&options).unwrap(); + let warm_helper_key = &warm_rooted + .cfgs + .iter() + .find(|unit| crate::cfg_query::accessor_source_name(&unit.function) == "helper") + .unwrap() + .optimized_cfg_key; + assert_eq!( + cold_helper_key, *warm_helper_key, + "{cold_helper_key:#?}\n{warm_helper_key:#?}" + ); + let execution = |name: &str| { + session + .rooted_cfg_executions() + .iter() + .find_map(|(identity, execution)| { + matches!(identity, crate::FunctionInstanceKey::Definition(definition) if definition.name() == name) + .then_some(*execution) + }) + .unwrap() + }; + assert_eq!(execution("main"), rue_query::RequestExecution::Computed); + assert_eq!( + execution("helper"), + rue_query::RequestExecution::Reused, + "{:#?}", + session.rooted_cfg_executions() + ); + assert!(session.rooted_cfg_executions().iter().all(|(identity, _)| { + !matches!(identity, crate::FunctionInstanceKey::Definition(definition) if definition.name() == "value") + })); + let semantic = session.canonical_semantic(&options).unwrap(); + let warm = session + .codegen_products( + &semantic, + &options, + rue_codegen::BackendArtifactRequest::default(), + ) + .unwrap(); + let mut fresh = crate::CompilerSession::new(); + fresh.update(&source(8)).into_result().unwrap(); + let semantic = fresh.canonical_semantic(&options).unwrap(); + let fresh = fresh + .codegen_products( + &semantic, + &options, + rue_codegen::BackendArtifactRequest::default(), + ) + .unwrap(); + let warm = warm + .iter() + .map(|product| (&product.machine_name, &product.machine_code.code)) + .collect::>(); + let fresh = fresh + .iter() + .map(|product| (&product.machine_name, &product.machine_code.code)) + .collect::>(); + assert_eq!(warm, fresh); + } + + #[test] + fn accessor_raw_cfg_dependency_key_is_shared_across_distinct_callers() { + let snapshot = crate::SourceSnapshot::single( + "main.rue", + "struct P { x: i64, fn value(borrow self) -> borrow i64 { yield self.x; } } \ + struct A { n: i64 } struct B { n: i64 } \ + fn caller_a(borrow p: P) -> i64 { let a = A { n: 1 }; p.value() + a.n } \ + fn caller_b(borrow p: P) -> i64 { let b = B { n: 2 }; p.value() + b.n } \ + fn main() -> i32 { let p = P { x: 3 }; if caller_a(borrow p) + caller_b(borrow p) == 9 { 0 } else { 1 } }", + ) + .unwrap(); + let mut options = crate::CompileOptions::default(); + options + .preview_features + .insert(rue_error::PreviewFeature::BorrowAccessors); + let mut session = crate::CompilerSession::new(); + session.update(&snapshot).into_result().unwrap(); + let rooted = session.rooted_cfg(&options).unwrap(); + let dependency = |name: &str| { + let unit = rooted + .cfgs + .iter() + .find(|unit| crate::cfg_query::accessor_source_name(&unit.function) == name) + .unwrap(); + assert_eq!(unit.optimized_cfg_key.accessor_dependencies.len(), 1); + unit.optimized_cfg_key.accessor_dependencies[0].clone() + }; + + assert_eq!(dependency("caller_a"), dependency("caller_b")); + } + #[test] fn codegen_units_cover_x86_and_aarch64_relocations_deterministically() { let snapshot = crate::SourceSnapshot::single( diff --git a/crates/rue-oracle/src/lib.rs b/crates/rue-oracle/src/lib.rs index 8e6555f73..88623dc99 100644 --- a/crates/rue-oracle/src/lib.rs +++ b/crates/rue-oracle/src/lib.rs @@ -369,6 +369,7 @@ pub enum ContractViolationKind { InoutArgumentNotLvalue, NonIntegerOperationType, UnsupportedDebugType, + UnsplicedAccessor, } /// The closed, machine-readable cause of an oracle execution failure. @@ -1347,6 +1348,7 @@ impl<'a> Interp<'a> { }; (slot, cfg.num_params(), width, Some(slot)) } + PlaceBase::Accessor(_) => return Some(ContractViolationKind::UnsplicedAccessor), }; let out_of_bounds = if width == 0 { // Zero-sized roots consume no logical slot, so the canonical base @@ -2652,6 +2654,12 @@ impl<'a> Interp<'a> { Self::set_param(frame, *param_slot, val); Value::Unit } + CfgInstData::AccessorCall { .. } => { + return Err(unsupported( + UnsupportedKind::ContractViolation(ContractViolationKind::UnsplicedAccessor), + "accessor call reached the oracle", + )); + } CfgInstData::Call { runtime, name, .. } => { let fname = self.interner().resolve(name).to_string(); let call_args = cfg.get_call_args(&inst.data).to_vec(); @@ -2755,6 +2763,14 @@ impl<'a> Interp<'a> { let place = match base { PlaceBase::Local(slot) => Place::local(slot, base_type), PlaceBase::Param(slot) => Place::param(slot, base_type), + PlaceBase::Accessor(_) => { + return Err(unsupported( + UnsupportedKind::ContractViolation( + ContractViolationKind::UnsplicedAccessor, + ), + "accessor place reached call writeback", + )); + } }; self.place_write(cfg, frame, &place, val)?; } @@ -3128,6 +3144,10 @@ impl<'a> Interp<'a> { format!("param place {slot} out of bounds"), )), }, + PlaceBase::Accessor(_) => Err(unsupported( + UnsupportedKind::ContractViolation(ContractViolationKind::UnsplicedAccessor), + "accessor place reached oracle storage", + )), } } @@ -3184,6 +3204,14 @@ impl<'a> Interp<'a> { let (store, slot) = match base { PlaceBase::Local(slot) => (&mut frame.locals, slot as usize), PlaceBase::Param(slot) => (&mut frame.params, slot as usize), + PlaceBase::Accessor(_) => { + return Err(unsupported( + UnsupportedKind::ContractViolation( + ContractViolationKind::UnsplicedAccessor, + ), + "accessor place reached oracle write", + )); + } }; if slot >= store.len() { store.resize(slot + 1, None); @@ -3916,6 +3944,7 @@ fn promotion_key(base: PlaceBase) -> u64 { match base { PlaceBase::Local(slot) => (slot as u64) << 1, PlaceBase::Param(slot) => ((slot as u64) << 1) | 1, + PlaceBase::Accessor(value) => ((value.as_u32() as u64) << 2) | 3, } } diff --git a/crates/rue-spec/cases/items/borrow-accessors.toml b/crates/rue-spec/cases/items/borrow-accessors.toml index 0ef435bd0..bb1cee566 100644 --- a/crates/rue-spec/cases/items/borrow-accessors.toml +++ b/crates/rue-spec/cases/items/borrow-accessors.toml @@ -1,16 +1,9 @@ # Borrow accessors (ADR-0062 phases 0-1, RUE-662), behind the # `borrow_accessors` preview. # -# Coverage note (RUE-662 ruling, 2026-07-29): declaration- and body-level -# rules are enforced today and carry `preview_should_pass = true`, as does the -# guard-trap dynamics case (the guard observably traps whether or not the call -# is inlined) and the expansion-acyclicity rejection (6.6:14, which fires as -# soon as an expansion is entered). The remaining call-site rules (6.6:8-6.6:11) -# are implemented in the semantic engine but do not yet fire through the -# incremental driver, whose per-body RIR pruning cannot see accessor callee -# bodies; they become enforceable with the RUE-662 epoch-eligibility scaffold -# (after RUE-1033) and are tracked as known-uncovered until their cases flip to -# `preview_should_pass = true`. +# Accessor calls survive semantic analysis as marked place-producing calls. +# The optimized-CFG query observes the exact accessor CFG dependencies and +# mandatorily splices their guards and yielded place before code generation. [section] id = "items.borrow-accessors" @@ -43,6 +36,89 @@ fn main() -> i32 { """ exit_code = 0 +[[case]] +name = "nested_accessor_splices_are_repeatable" +spec = ["6.6:8"] +description = "Nested accessor calls form a callee-first splice graph and repeated/diamond-shaped uses do not look recursive." +preview = "borrow_accessors" +preview_should_pass = true +source = """ +struct P { + x: i64, + + fn inner(borrow self) -> borrow i64 { yield self.x; } + fn outer(borrow self) -> borrow i64 { yield self.inner(); } +} +fn main() -> i32 { + let p = P { x: 21 }; + if p.outer() + p.outer() == 42 { 0 } else { 1 } +} +""" +exit_code = 0 + +[[case]] +name = "indirect_accessor_recursion_is_rejected" +spec = ["6.6:8"] +description = "An indirect accessor cycle is diagnosed as E0261 before mandatory splicing." +preview = "borrow_accessors" +preview_should_pass = true +source = """ +struct P { + x: i64, + + fn a(borrow self) -> borrow i64 { yield self.b(); } + fn b(borrow self) -> borrow i64 { yield self.a(); } +} +fn main() -> i32 { + let p = P { x: 1 }; + if p.a() == 1 { 0 } else { 1 } +} +""" +compile_fail = true +error_contains = ["E0261", "directly or through other accessors"] + +[[case]] +name = "projected_receiver_accessor_executes_in_place" +spec = ["6.6:8"] +description = "A projected receiver remains a place when its accessor CFG is spliced." +preview = "borrow_accessors" +preview_should_pass = true +source = """ +struct Inner { + x: i64, + fn value(borrow self) -> borrow i64 { yield self.x; } +} +struct Outer { inner: Inner } +fn main() -> i32 { + let outer = Outer { inner: Inner { x: 7 } }; + if outer.inner.value() == 7 { 0 } else { 1 } +} +""" +exit_code = 0 + +[[case]] +name = "callee_only_nominal_relocates_into_caller_domain" +spec = ["6.6:8"] +description = "Types used only by an accessor body are rematerialized in the caller domain before splicing." +preview = "borrow_accessors" +preview_should_pass = true +source = """ +struct Marker { value: i64 } +struct P { + x: i64, + fn value(borrow self) -> borrow i64 { + let marker = Marker { value: 1 }; + if marker.value != 1 { @panic("bad marker"); } + yield self.x; + } +} +fn main() -> i32 { + let p = P { x: 9 }; + if p.value() == 9 { 0 } else { 1 } +} +""" +exit_code = 0 + [[case]] name = "accessor_requires_preview_gate" spec = ["6.6:3"] @@ -364,9 +440,8 @@ compile_fail = true error_contains = ["E0261", "an accessor may not invoke itself"] # --------------------------------------------------------------------------- -# Call-site semantics: implemented in the semantic engine (see the rue-air -# unit suite), enforceable through the driver once the RUE-662 eligibility -# scaffold lands. Until then these run as preview-allowed-to-fail. +# Call-site semantics are enforced through the canonical semantic-body and CFG +# query path; these cases are required provider-path coverage. # --------------------------------------------------------------------------- [[case]] @@ -374,6 +449,7 @@ name = "accessor_read_executes_in_place" spec = ["6.6:8", "6.6:12"] description = "A guarded accessor call reads the projected element in place; guards run at the call site." preview = "borrow_accessors" +preview_should_pass = true source = """ struct Grid { cells: [i64; 4], @@ -425,6 +501,7 @@ name = "accessor_result_cannot_be_returned" spec = ["6.6:9"] description = "Returning an accessor result escapes its full expression (E0250)." preview = "borrow_accessors" +preview_should_pass = true source = """ struct P { x: i64, @@ -449,6 +526,7 @@ name = "accessor_result_cannot_be_let_bound" spec = ["6.6:9"] description = "A plain `let` binding of an accessor result escapes its full expression (E0252)." preview = "borrow_accessors" +preview_should_pass = true source = """ struct P { x: i64, @@ -471,6 +549,7 @@ name = "accessor_result_cannot_be_stored" spec = ["6.6:9"] description = "Assigning an accessor result into storage escapes its full expression (E0251)." preview = "borrow_accessors" +preview_should_pass = true source = """ struct P { x: i64, @@ -494,6 +573,7 @@ name = "accessor_result_cannot_be_captured" spec = ["6.6:9"] description = "Capturing an accessor result in an array literal escapes its full expression (E0253)." preview = "borrow_accessors" +preview_should_pass = true source = """ struct P { x: i64, @@ -516,6 +596,7 @@ name = "accessor_loan_conflicts_with_inout" spec = ["6.6:10"] description = "An exclusive use of the borrowed root in the same full expression violates exclusivity (E0259)." preview = "borrow_accessors" +preview_should_pass = true source = """ struct Grid { cells: [i64; 4], @@ -543,6 +624,7 @@ name = "accessor_result_drop_glue_read_rejected" spec = ["6.6:11"] description = "Reading an owning (drop-glue) value out of an accessor result by value is rejected (E0258)." preview = "borrow_accessors" +preview_should_pass = true real_std = true source = """ const std = @import("std"); diff --git a/crates/rue-spec/src/traceability.rs b/crates/rue-spec/src/traceability.rs index 4311b3aeb..421ff287c 100644 --- a/crates/rue-spec/src/traceability.rs +++ b/crates/rue-spec/src/traceability.rs @@ -236,40 +236,6 @@ pub const KNOWN_UNCOVERED_NORMATIVE: &[(&str, &str)] = &[ move-without-destructor rule governs `@raw`/`@raw_mut` pointer escapes under \ ADR-0028 programmer responsibility, which is not positively testable.", ), - // ADR-0062 phase 1 (RUE-662) borrow-accessor call-site rules. The call-site - // semantics are implemented in the semantic engine (see the rue-air accessor - // unit suite) but do not yet fire through the incremental driver, whose - // per-body pruned RIR cannot see accessor callee bodies. Per the RUE-662 - // ruling (2026-07-29) they become driver-enforceable via the epoch-engine - // eligibility fallback once RUE-1033 lands; the CFG-threshold splice that - // retires the fallback is tracked as RUE-1208. The corresponding spec cases - // in cases/items/borrow-accessors.toml run as preview-allowed-to-fail and - // flip to `preview_should_pass = true` with the eligibility change, retiring - // these entries. - ( - "6.6:8", - "Accessor call semantics (borrowed-place result, full-expression loan \ - extent): engine-implemented; awaits the RUE-662 provider-eligibility \ - fallback (after RUE-1033) to fire through the driver.", - ), - ( - "6.6:9", - "Accessor-result escape rejections (E0250-E0253): engine-implemented; \ - awaits the RUE-662 provider-eligibility fallback (after RUE-1033) to \ - fire through the driver.", - ), - ( - "6.6:10", - "Exclusivity over the accessor loan extent (E0259): engine-implemented; \ - awaits the RUE-662 provider-eligibility fallback (after RUE-1033) to \ - fire through the driver.", - ), - ( - "6.6:11", - "Drop-glue by-value read out of an accessor result (E0258): \ - engine-implemented; awaits the RUE-662 provider-eligibility fallback \ - (after RUE-1033) to fire through the driver.", - ), ]; impl TraceabilityReport { diff --git a/crates/rue-ui-tests/cases/diagnostics/borrow-accessors.toml b/crates/rue-ui-tests/cases/diagnostics/borrow-accessors.toml new file mode 100644 index 000000000..60544ccd1 --- /dev/null +++ b/crates/rue-ui-tests/cases/diagnostics/borrow-accessors.toml @@ -0,0 +1,26 @@ +[section] +id = "diagnostics.borrow-accessors" +name = "Borrow accessor diagnostics" +description = "Provider-path accessor calls retain their second-class place diagnostics." + +[[case]] +name = "returned_accessor_result_names_method_and_root" +source = """ +struct P { + x: i64, + + fn xr(borrow self) -> borrow i64 { + yield self.x; + } +} +fn read(borrow p: P) -> i64 { + return p.xr(); +} +fn main() -> i32 { + let p = P { x: 1 }; + @intCast(read(borrow p)) +} +""" +preview = "borrow_accessors" +compile_fail = true +error_contains = ["E0250", "cannot return an accessor result", "`xr`", "`p`"]