From 672b154535315e36f06d7cc9d482d1bb4f50d07e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 06:16:30 +0000 Subject: [PATCH 1/3] RUE-662 phase 0-1 groundwork: borrow-accessor syntax, statics, and epoch-engine inlining Place-returning borrow accessors (ADR-0062, S2 surface) through the epoch semantic engine, behind the borrow_accessors preview gate: - yield keyword; -> borrow T result position on fn/method declarations (grammar-side only; free functions are rejected in sema, E0257) - RIR: FnDecl.returns_borrow and InstData::Yield - Sema statics: the (Accessor-Call) rule as an expression-scoped shared loan on the receiver root (ctx.expression_loans, truncated per full expression) checked against inout loans, moves, and assignments (E0259); dedicated escape diagnostics for return/store/let/capture (E0250-E0253); accessor-body well-formedness (single trailing yield, receiver-rooted place; E0254-E0256); declaration shape checks (borrow self receiver, by-value params; E0257, E0260); drop-glue value reads out of a result rejected (E0258); everything behind require_preview(borrow_accessors) (E1100 when off) - Mandatory inlining at call sites in the shared body engine: an accessor call expands to its guards plus the yielded place, traced as a caller-rooted PlaceTrace (no call emitted, no ABI); the standalone accessor compiles its guards plus an unreachable trap so downstream stages see no new shapes - Accessor-body inference runs on demand at expansion and overlays the caller's resolved types - rue-air unit coverage for inlining, gating, every escape shape, exclusivity conflict, and body well-formedness Known limitation, detailed in the PR: the incremental provider body path prunes callee declarations from each body request's RIR, so call-site expansion cannot see accessor bodies there; completing the feature needs body-input changes in the query nucleus, which this change deliberately does not touch. Toward RUE-662. Part of RUE-1015. --- crates/rue-air/src/inference/generate.rs | 13 + crates/rue-air/src/sema/aggregates.rs | 28 +- crates/rue-air/src/sema/analysis.rs | 14 +- .../rue-air/src/sema/analysis/anon_methods.rs | 29 + .../rue-air/src/sema/analysis/builtin_ops.rs | 21 + crates/rue-air/src/sema/analysis/calls.rs | 62 +- crates/rue-air/src/sema/analysis/functions.rs | 8 + .../rue-air/src/sema/analysis/instructions.rs | 1 + .../rue-air/src/sema/analysis/intrinsics.rs | 4 +- crates/rue-air/src/sema/analysis/ownership.rs | 659 +++++++++++++++++- crates/rue-air/src/sema/analysis/pointers.rs | 4 +- .../src/sema/analysis/type_inference.rs | 14 +- crates/rue-air/src/sema/binding_manifest.rs | 5 + crates/rue-air/src/sema/body_identity.rs | 17 +- crates/rue-air/src/sema/call_resolution.rs | 26 +- crates/rue-air/src/sema/consistency_tests.rs | 1 + crates/rue-air/src/sema/context.rs | 78 +++ crates/rue-air/src/sema/control_flow.rs | 162 +++++ crates/rue-air/src/sema/declarations.rs | 89 +++ crates/rue-air/src/sema/fact_mode.rs | 3 + crates/rue-air/src/sema/info.rs | 27 + crates/rue-air/src/sema/ordinary_engine.rs | 69 +- crates/rue-air/src/sema/provider_body_host.rs | 93 ++- crates/rue-air/src/sema/tests.rs | 339 +++++++++ crates/rue-compiler/src/artifact_views.rs | 10 + crates/rue-compiler/src/parsed_modules.rs | 1 + crates/rue-error/src/lib.rs | 141 +++- crates/rue-frontend-diff/src/main.rs | 8 + crates/rue-lexer/src/lib.rs | 3 + crates/rue-lexer/src/logos_lexer.rs | 3 + crates/rue-parser/src/ast.rs | 31 + crates/rue-parser/src/parser/declarations.rs | 22 +- crates/rue-parser/src/parser/expressions.rs | 10 + crates/rue-parser/src/parser/statements.rs | 3 +- crates/rue-parser/src/parser/types.rs | 8 +- crates/rue-parser/src/validate.rs | 1 + crates/rue-rir/src/anonymous_sites.rs | 3 + crates/rue-rir/src/astgen.rs | 13 + crates/rue-rir/src/inst.rs | 28 +- 39 files changed, 2000 insertions(+), 51 deletions(-) diff --git a/crates/rue-air/src/inference/generate.rs b/crates/rue-air/src/inference/generate.rs index 785edbc57..7dc349459 100644 --- a/crates/rue-air/src/inference/generate.rs +++ b/crates/rue-air/src/inference/generate.rs @@ -1454,6 +1454,19 @@ impl<'a> ConstraintGenerator<'a> { InferType::Concrete(Type::NEVER) } + // Accessor yield (ADR-0062): the yielded place must have the + // accessor's declared element type `T` (the function's return + // type); the `yield` itself diverges like `return`. + InstData::Yield(value) => { + let value_info = self.generate(*value, ctx); + self.add_constraint(Constraint::equal( + value_info.ty, + InferType::Concrete(ctx.return_type), + span, + )); + InferType::Concrete(Type::NEVER) + } + // Function call InstData::Call { name, args } => { let alias_target = self.const_function_alias((span.file_id, *name)); diff --git a/crates/rue-air/src/sema/aggregates.rs b/crates/rue-air/src/sema/aggregates.rs index e368f8a07..418c8fe44 100644 --- a/crates/rue-air/src/sema/aggregates.rs +++ b/crates/rue-air/src/sema/aggregates.rs @@ -534,6 +534,15 @@ impl OrdinaryBodyEngine<'_, H> { self.analyze_inst(air, field_value, ctx)? }; + // An accessor result is a second-class borrowed place (ADR-0062): + // capturing it as an aggregate member would store the borrow. + self.reject_accessor_result_escape( + field_value, + super::analysis::AccessorEscapeSite::Capture, + span, + ctx, + )?; + // Two-types model (ADR-0043, RUE-386): storing into a first-class // `str` field must not smuggle a borrowed `str` view (a // `borrow`/`inout str` parameter) into the aggregate — the view @@ -1106,7 +1115,7 @@ impl OrdinaryBodyEngine<'_, H> { // the comptime-only `type`, would reach the intern pool and panic // (RUE-253). for elem_ref in &elem_refs { - if let Some(elem_ty) = ctx.resolved_types.get(elem_ref).copied() { + if let Some(elem_ty) = ctx.resolved_type_of(*elem_ref) { self.reject_non_runtime_array_element(elem_ty, span)?; } } @@ -1168,6 +1177,14 @@ impl OrdinaryBodyEngine<'_, H> { let mut air_elems = Vec::with_capacity(elem_refs.len()); for elem_ref in elem_refs { let elem_result = self.analyze_inst(air, elem_ref, ctx)?; + // An accessor result cannot be captured as an array element + // (ADR-0062): the member would store a second-class borrow. + self.reject_accessor_result_escape( + elem_ref, + super::analysis::AccessorEscapeSite::Capture, + span, + ctx, + )?; air_elems.push(elem_result.air_ref); } @@ -1201,7 +1218,7 @@ impl OrdinaryBodyEngine<'_, H> { // runtime representation. Reject it (E1200 / E0206) before the preview // gate below and before the comptime-only/module element type would // reach the intern pool and panic (RUE-253, RUE-265). - if let Some(value_ty) = ctx.resolved_types.get(&value_ref).copied() { + if let Some(value_ty) = ctx.resolved_type_of(value_ref) { self.reject_non_runtime_array_element(value_ty, span)?; } @@ -1249,6 +1266,13 @@ impl OrdinaryBodyEngine<'_, H> { // Evaluate the repeated value exactly once. let value_result = self.analyze_inst(air, value_ref, ctx)?; + // An accessor result cannot seed an array-repeat literal (ADR-0062). + self.reject_accessor_result_escape( + value_ref, + super::analysis::AccessorEscapeSite::Capture, + span, + ctx, + )?; // Desugar to ArrayInit: `length` elements, each the single value. let elem_refs = vec![value_result.air_ref; length as usize]; diff --git a/crates/rue-air/src/sema/analysis.rs b/crates/rue-air/src/sema/analysis.rs index 4078050a0..c75c45ff1 100644 --- a/crates/rue-air/src/sema/analysis.rs +++ b/crates/rue-air/src/sema/analysis.rs @@ -2642,6 +2642,7 @@ fn analyze_function_bodies_lazy(sema: &mut BodySema<'_>) -> MultiErrorResult( interner: &ThreadedRodeo, args: A, call_span: Span, + resolve_borrow_root: &dyn Fn(InstRef) -> Option, ) -> CompileResult<()> where A: IntoIterator, @@ -3440,7 +3442,15 @@ where for arg in args { let arg = &*arg; - let maybe_var_symbol = root_variable_of(rir, arg.value); + // A `-> borrow T` accessor call is a place for `borrow` arguments + // (ADR-0062): it roots at its receiver's root and joins the shared + // set. `inout` accessor results stay rejected as non-lvalues (the + // exclusive form is the RUE-1016 phase). + let maybe_var_symbol = root_variable_of(rir, arg.value).or_else(|| { + arg.is_borrow() + .then(|| resolve_borrow_root(arg.value)) + .flatten() + }); // Check that inout/borrow arguments are lvalues if arg.is_inout() && maybe_var_symbol.is_none() { @@ -3528,7 +3538,7 @@ mod functions; mod instructions; mod intrinsics; mod ownership; -pub(crate) use ownership::FirstClassStrSite; +pub(crate) use ownership::{AccessorEscapeSite, FirstClassStrSite}; mod pointers; mod type_inference; diff --git a/crates/rue-air/src/sema/analysis/anon_methods.rs b/crates/rue-air/src/sema/analysis/anon_methods.rs index a425e3fdf..7bab6a3a1 100644 --- a/crates/rue-air/src/sema/analysis/anon_methods.rs +++ b/crates/rue-air/src/sema/analysis/anon_methods.rs @@ -35,11 +35,26 @@ impl<'a, D: crate::sema::DeclarationPhase> crate::sema::Sema<'a, D> { has_self, self_mode, self_is_mut, + returns_borrow, .. } = &method_inst.data { let key = (struct_id, *method_name); + // Place-returning accessors (ADR-0062) are phase-1 restricted + // to named structs: anonymous-struct method identity is + // signature-structural and the provider path registers these + // methods without body handles, so an accessor here could not + // inline. Rejected until a later phase widens the surface. + if *returns_borrow { + return Err(CompileError::new( + ErrorKind::AccessorRequiresBorrowSelf { + found: "a method on an anonymous struct type".to_string(), + }, + method_inst.span, + )); + } + // Check for duplicate methods if self.has_method(key) { let struct_def = self.type_pool.struct_def(struct_id); @@ -87,6 +102,7 @@ impl<'a, D: crate::sema::DeclarationPhase> crate::sema::Sema<'a, D> { return_type: ret_type, body: *body, span: method_inst.span, + returns_borrow: false, }, ); self.index_anonymous_callable_method(struct_id, *method_name, *has_self); @@ -126,11 +142,18 @@ impl<'a, D: crate::sema::DeclarationPhase> crate::sema::Sema<'a, D> { has_self, self_mode, self_is_mut, + returns_borrow, .. } = &method_inst.data { let key = (struct_id, *method_name); + // Accessors are not supported on anonymous structs (ADR-0062 + // phase 1); fall back so the compile-time path reports it. + if *returns_borrow { + return None; + } + // Check for duplicate methods - return None in comptime context if self.has_method(key) { return None; @@ -181,6 +204,7 @@ impl<'a, D: crate::sema::DeclarationPhase> crate::sema::Sema<'a, D> { return_type: ret_type, body: *body, span: method_inst.span, + returns_borrow: false, }, ); self.index_anonymous_callable_method(struct_id, *method_name, *has_self); @@ -227,6 +251,7 @@ impl<'a, D: crate::sema::DeclarationPhase> crate::sema::Sema<'a, D> { has_self, self_mode, self_is_mut, + returns_borrow, .. } = &instruction.data else { @@ -247,6 +272,9 @@ impl<'a, D: crate::sema::DeclarationPhase> crate::sema::Sema<'a, D> { .all(|(parameter, comptime)| parameter.is_comptime == *comptime) || !seen.insert(*name) || self.has_method((struct_id, *name)) + // Accessors are not supported on anonymous structs (ADR-0062 + // phase 1). + || *returns_borrow { return None; } @@ -272,6 +300,7 @@ impl<'a, D: crate::sema::DeclarationPhase> crate::sema::Sema<'a, D> { return_type: materialize(&signature.return_type, struct_type)?, body: *body, span: instruction.span, + returns_borrow: false, }, )); } diff --git a/crates/rue-air/src/sema/analysis/builtin_ops.rs b/crates/rue-air/src/sema/analysis/builtin_ops.rs index 63606568a..4f90ca7fc 100644 --- a/crates/rue-air/src/sema/analysis/builtin_ops.rs +++ b/crates/rue-air/src/sema/analysis/builtin_ops.rs @@ -467,11 +467,32 @@ impl OrdinaryBodyEngine<'_, H> { pub(crate) fn peek_place_type(&self, inst_ref: InstRef, ctx: &AnalysisContext) -> Option { match &self.body_rir_ref().get(inst_ref).data { InstData::VarRef { name, .. } => { + // An accessor-inline place alias (`self` inside an inlined + // accessor body, ADR-0062) shadows caller bindings. + if let Some(alias) = ctx.place_aliases.get(name) { + return Some( + alias + .projections + .last() + .map(|p| p.result_type) + .unwrap_or(alias.base_type), + ); + } if let Some(local) = ctx.locals.get(name) { return Some(local.ty); } ctx.params.iter().find(|p| p.name == *name).map(|p| p.ty) } + // A `-> borrow T` accessor call is a place of its element type + // (ADR-0062); any other method call is not a place. + InstData::MethodCall { + receiver, method, .. + } => { + let base_ty = self.peek_place_type(*receiver, ctx)?; + let struct_id = base_ty.as_struct()?; + let info = self.call_facts().method_info(struct_id, *method)?; + info.returns_borrow.then_some(info.return_type) + } InstData::FieldGet { base, field } => { let base_ty = self.peek_place_type(*base, ctx)?; let struct_id = base_ty.as_struct()?; diff --git a/crates/rue-air/src/sema/analysis/calls.rs b/crates/rue-air/src/sema/analysis/calls.rs index 8bc4928a7..ee511f122 100644 --- a/crates/rue-air/src/sema/analysis/calls.rs +++ b/crates/rue-air/src/sema/analysis/calls.rs @@ -76,6 +76,7 @@ impl OrdinaryBodyEngine<'_, H> { param_modes: &[RirParamMode], span: Span, check_exclusive: bool, + ctx: &AnalysisContext, ) -> CompileResult<()> { let args = self.body_rir_ref().call_args(args_range).to_vec(); if args.len() != param_types.len() { @@ -90,7 +91,7 @@ impl OrdinaryBodyEngine<'_, H> { debug_assert_eq!(param_types.len(), param_modes.len()); self.validate_explicit_call_modes(&args, param_modes.iter().copied())?; if check_exclusive { - self.check_exclusive_access(&args, span)?; + self.check_exclusive_access(&args, span, ctx)?; } Ok(()) } @@ -100,7 +101,7 @@ impl OrdinaryBodyEngine<'_, H> { /// check because inference cannot recover their defining file; ordinary /// and specialized calls preserve their inferred/comptime and physical /// view types here. - fn analyze_call_operands( + pub(super) fn analyze_call_operands( &mut self, air: &mut Air, args_range: &rue_rir::RirCallArgsRange, @@ -217,7 +218,25 @@ impl OrdinaryBodyEngine<'_, H> { receiver, method, args, - } => self.analyze_method_call(air, *receiver, *method, args, inst.span, ctx), + } => { + // A `-> borrow T` accessor call inlines to its guards plus + // the yielded place (ADR-0062); in value position the place + // is read. Intercepted before ordinary method dispatch so + // the receiver is traced as a place, never read as a value. + if let Some(struct_id) = self + .peek_place_type(*receiver, ctx) + .and_then(|ty| ty.as_struct()) + && self + .call_facts() + .method_info(struct_id, *method) + .is_some_and(|info| info.returns_borrow) + { + return self.analyze_accessor_call_value( + air, inst_ref, *receiver, struct_id, *method, args, inst.span, ctx, + ); + } + self.analyze_method_call(air, *receiver, *method, args, inst.span, ctx) + } _ => Err(CompileError::new( ErrorKind::InternalError(format!( @@ -380,7 +399,7 @@ impl OrdinaryBodyEngine<'_, H> { let param_comptime = param_data.comptime().to_vec(); let param_names = param_data.names().to_vec(); - self.validate_call_contract(args_range, ¶m_types, ¶m_modes, span, true)?; + self.validate_call_contract(args_range, ¶m_types, ¶m_modes, span, true, ctx)?; // The declaration, visibility, checked-call policy, and explicit call // contract have all selected this exact callable. Record before // operand analysis so a later argument diagnostic retains the edge. @@ -768,7 +787,11 @@ impl OrdinaryBodyEngine<'_, H> { ctx: &mut AnalysisContext, ) -> CompileResult { let args = self.body_rir_ref().call_args(args_range).to_vec(); - let receiver_var = self.extract_root_variable(receiver); + // An accessor-call receiver chain (`v.get_ref(i).len()`, ADR-0062) + // roots at the accessor's own receiver root: the chain is a place. + let receiver_var = self + .extract_root_variable(receiver) + .or_else(|| self.place_root_with_accessors(receiver, ctx)); let method_name_str = self.body_interner().resolve(&method).to_string(); // `Type.function(args)` is an associated-function call / enum @@ -815,7 +838,7 @@ impl OrdinaryBodyEngine<'_, H> { // inferred type — otherwise `str.len()` would miss this route. let receiver_slice_ty = receiver_var .and_then(|_| self.peek_place_type(receiver, ctx)) - .or_else(|| ctx.resolved_types.get(&receiver).copied()); + .or_else(|| ctx.resolved_type_of(receiver)); if receiver_slice_ty.is_some_and(|ty| self.slice_element_type(ty).is_some()) { return self.analyze_slice_method( air, @@ -1003,6 +1026,7 @@ impl OrdinaryBodyEngine<'_, H> { &method_param_modes, span, false, + ctx, )?; self.record_body_method_dependency(method_key); @@ -1039,9 +1063,18 @@ impl OrdinaryBodyEngine<'_, H> { receiver_result = AnalysisResult::new(borrowed, receiver_result.ty); receiver_temp_scope = temp_scope; } + // An inlined accessor receiver arrives as a guards block whose + // tail is the place read (ADR-0062); peel it for the address + // check, exactly like the `inout str` view materialization. + let receiver_addressable_probe = if ctx.accessor_call_insts.contains_key(&receiver) { + self.peel_projected_rvalue_scope(air, receiver_result.air_ref) + .0 + } else { + receiver_result.air_ref + }; self.require_addressable_read( air, - receiver_result.air_ref, + receiver_addressable_probe, receiver_mode == AirArgMode::Inout, self.body_rir_ref().get(receiver).span, )?; @@ -1101,7 +1134,7 @@ impl OrdinaryBodyEngine<'_, H> { }, }); excl_args.extend(args.iter().map(|arg| *arg)); - self.check_exclusive_access(&excl_args, span)?; + self.check_exclusive_access(&excl_args, span, ctx)?; // By-ref receivers are borrows, not moves. The receiver was // already analyzed under `byref_arg_root` above (RUE-254), so no @@ -1119,7 +1152,7 @@ impl OrdinaryBodyEngine<'_, H> { } } else { // Check for exclusive access violation (by-value receiver) - self.check_exclusive_access(&args, span)?; + self.check_exclusive_access(&args, span, ctx)?; } // Analyze arguments - receiver first, then remaining args. @@ -1133,7 +1166,13 @@ impl OrdinaryBodyEngine<'_, H> { mode: receiver_mode, }]; let receiver_frame = match (receiver_mode, receiver_var) { - (AirArgMode::Inout, Some(root)) => Some(vec![(root, CallLoanKind::Inout)]), + (AirArgMode::Inout, Some(root)) => { + // An `inout self` receiver on a root an accessor result + // borrows in the same full expression violates exclusivity + // (ADR-0062, E0259). + self.reject_accessor_loan_conflict(root, "as an `inout self` receiver", span, ctx)?; + Some(vec![(root, CallLoanKind::Inout)]) + } (AirArgMode::Borrow, Some(root)) => Some(vec![(root, CallLoanKind::Borrow)]), _ => None, }; @@ -1289,7 +1328,7 @@ impl OrdinaryBodyEngine<'_, H> { ); } - self.validate_call_contract(args_range, ¶m_types, ¶m_modes, span, true)?; + self.validate_call_contract(args_range, ¶m_types, ¶m_modes, span, true, ctx)?; self.record_body_callable_dependency(function_key); // Analyze arguments (the per-pipeline recursion seam). Module-qualified @@ -1436,6 +1475,7 @@ impl OrdinaryBodyEngine<'_, H> { &method_param_modes, span, true, + ctx, )?; self.record_body_method_dependency(method_key); diff --git a/crates/rue-air/src/sema/analysis/functions.rs b/crates/rue-air/src/sema/analysis/functions.rs index 4289dc3b4..400dc6c58 100644 --- a/crates/rue-air/src/sema/analysis/functions.rs +++ b/crates/rue-air/src/sema/analysis/functions.rs @@ -55,6 +55,7 @@ impl<'a> BodySema<'a> { has_self: bool, self_mode: RirParamMode, self_is_mut: bool, + returns_borrow: bool, ) -> CompileResult<( AnalyzedFunction, Vec, @@ -122,6 +123,7 @@ impl<'a> BodySema<'a> { false, false, self_is_mut, + returns_borrow, ); self.active_anonymous_producer = previous_producer; let ( @@ -226,6 +228,7 @@ impl<'a> BodySema<'a> { /* is_destructor */ true, false, false, + false, ); self.active_anonymous_producer = previous_producer; let ( @@ -302,6 +305,7 @@ impl<'a> BodySema<'a> { is_destructor: bool, allow_unused_variable: bool, self_is_mut: bool, + is_accessor: bool, ) -> CompileResult<( Air, u32, @@ -323,6 +327,7 @@ impl<'a> BodySema<'a> { is_destructor, allow_unused_variable, self_is_mut, + is_accessor, ) } @@ -362,6 +367,7 @@ impl<'a> BodySema<'a> { false, false, self_is_mut, + false, ) } @@ -454,6 +460,7 @@ impl<'a> BodySema<'a> { false, false, self_is_mut, + false, ); self.active_anonymous_producer = previous_producer; analysis @@ -543,6 +550,7 @@ impl<'a> BodySema<'a> { /* is_destructor */ true, false, false, + false, ); self.active_anonymous_producer = previous_producer; let ( diff --git a/crates/rue-air/src/sema/analysis/instructions.rs b/crates/rue-air/src/sema/analysis/instructions.rs index 9f26d402b..9c22909d9 100644 --- a/crates/rue-air/src/sema/analysis/instructions.rs +++ b/crates/rue-air/src/sema/analysis/instructions.rs @@ -174,6 +174,7 @@ impl OrdinaryBodyEngine<'_, H> { | InstData::Break { .. } | InstData::Continue | InstData::Ret(_) + | InstData::Yield(_) | InstData::Block { .. } => self.analyze_control_flow(air, inst_ref, ctx), // Variable operations diff --git a/crates/rue-air/src/sema/analysis/intrinsics.rs b/crates/rue-air/src/sema/analysis/intrinsics.rs index 2e210420a..6682e0b7e 100644 --- a/crates/rue-air/src/sema/analysis/intrinsics.rs +++ b/crates/rue-air/src/sema/analysis/intrinsics.rs @@ -1117,7 +1117,7 @@ impl OrdinaryBodyEngine<'_, H> { } // Get the target type from HM inference - let target_ty = match ctx.resolved_types.get(&inst_ref).copied() { + let target_ty = match ctx.resolved_type_of(inst_ref) { Some(ty) if ty.is_integer() => ty, Some(Type::ERROR) => { // The target type variable decayed to `` with no @@ -1667,7 +1667,7 @@ impl OrdinaryBodyEngine<'_, H> { let index = self.analyze_inst(air, args[0].value, ctx)?; self.require_process_index_type(display, index.ty, span)?; let result_ty = Type::new_ptr_mut(self.body_type_pool().intern_ptr_mut_from_type(Type::U8)); - if let Some(&expected) = ctx.resolved_types.get(&inst_ref) + if let Some(expected) = ctx.resolved_type_of(inst_ref) && !self.types_equivalent(expected, result_ty) && !expected.is_error() && !expected.is_never() diff --git a/crates/rue-air/src/sema/analysis/ownership.rs b/crates/rue-air/src/sema/analysis/ownership.rs index a507fded5..247f9e3e9 100644 --- a/crates/rue-air/src/sema/analysis/ownership.rs +++ b/crates/rue-air/src/sema/analysis/ownership.rs @@ -39,6 +39,18 @@ impl FirstClassStrSite { } } +/// The escape shape a `-> borrow T` accessor result was caught in +/// (ADR-0062): the result is a second-class borrowed place scoped to its +/// enclosing full expression, so returning, storing, `let`-binding, or +/// capturing it in an aggregate is rejected with a dedicated diagnostic. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum AccessorEscapeSite { + Return, + Store, + Let, + Capture, +} + // Place Building // ============================================================================ @@ -87,6 +99,16 @@ pub(super) struct PlaceTrace { is_root_mutable: bool, /// Whether this is a borrow parameter (for error messages) is_borrow_param: bool, + /// Guard statements an inlined accessor call contributed while this + /// place was traced (ADR-0062). They must execute before the place is + /// read: every consumer that turns the trace into a value wraps these + /// around it as a block prefix (see `finish_traced_value`). + pending_stmts: Vec, + /// Whether the trace passed through a `-> borrow T` accessor call. Such + /// a place is a second-class shared borrow: it may be read, projected, + /// re-borrowed, and compared, but never written through or moved out of + /// (write paths reject with E0428, drop-glue value reads with E0258). + via_accessor: bool, } #[derive(Clone)] @@ -294,7 +316,7 @@ impl OrdinaryBodyEngine<'_, H> { span: Span, ctx: &mut AnalysisContext, ) -> CompileResult> { - let Some(trace) = self.try_trace_place(value, air, ctx)? else { + let Some(mut trace) = self.try_trace_place(value, air, ctx)? else { return Ok(None); }; let ty = trace.result_type(); @@ -304,6 +326,7 @@ impl OrdinaryBodyEngine<'_, H> { ty, span, }); + let value = Self::finish_traced_value(air, &mut trace, value, ty, span)?; Ok(Some(AnalysisResult::new(value, ty))) } @@ -635,6 +658,35 @@ impl OrdinaryBodyEngine<'_, H> { match &inst.data { // Base case: local variable reference InstData::VarRef { name, .. } => { + // An accessor-inline place alias (`self` inside an inlined + // accessor body, ADR-0062) substitutes the caller's receiver + // place: the trace roots at the caller's own root variable, + // and the alias is a shared borrow — read-only, never moved + // out of. Aliases are installed only for the inline scope and + // take precedence over any same-named caller binding. + if let Some(alias) = ctx.place_aliases.get(name) { + return Ok(Some(PlaceTrace { + base: alias.base, + base_type: alias.base_type, + projections: alias + .projections + .iter() + .map(|p| ProjectionInfo { + 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: alias.root_var, + is_root_mutable: false, + is_borrow_param: true, + pending_stmts: Vec::new(), + via_accessor: true, + })); + } + // Locals shadow parameters (spec 5.1:10): a `let` that rebinds a // parameter name makes every later reference resolve to the new // local, not the parameter (RUE-278). A local with a param's @@ -647,6 +699,8 @@ impl OrdinaryBodyEngine<'_, H> { root_var: *name, is_root_mutable: local.is_mut, is_borrow_param: false, + pending_stmts: Vec::new(), + via_accessor: false, })); } @@ -663,6 +717,8 @@ impl OrdinaryBodyEngine<'_, H> { is_root_mutable: matches!(param_info.mode, RirParamMode::Inout) || param_info.is_mut, is_borrow_param: matches!(param_info.mode, RirParamMode::Borrow), + pending_stmts: Vec::new(), + via_accessor: false, })); } @@ -778,11 +834,425 @@ impl OrdinaryBodyEngine<'_, H> { } } + // A `-> borrow T` accessor call composes as a place (ADR-0062): + // `v.get_ref(i).name` traces through the inlined accessor result. + // A non-accessor method call is not a place. + InstData::MethodCall { + receiver, + method, + args, + } => { + let args = args.clone(); + self.try_trace_accessor_call(inst_ref, *receiver, *method, &args, air, ctx) + } + // Not a place expression _ => Ok(None), } } + /// Trace a method call as a place when — and only when — it resolves to a + /// `-> borrow T` accessor (ADR-0062). Returns `Ok(None)` for every other + /// method call so value analysis handles it. + fn try_trace_accessor_call( + &mut self, + inst_ref: InstRef, + receiver: InstRef, + method: Spur, + args_range: &rue_rir::RirCallArgsRange, + air: &mut Air, + ctx: &mut AnalysisContext, + ) -> CompileResult> { + // Peek the receiver type without emitting anything: only proceed when + // the method is a registered accessor. + let Some(struct_id) = self + .peek_place_type(receiver, ctx) + .and_then(|ty| ty.as_struct()) + else { + return Ok(None); + }; + let Some(info) = self.call_facts().method_info(struct_id, method) else { + return Ok(None); + }; + if !info.returns_borrow { + return Ok(None); + } + let span = self.body_rir_ref().get(inst_ref).span; + self.expand_accessor_call( + air, inst_ref, receiver, struct_id, method, args_range, span, ctx, + ) + .map(Some) + } + + /// Inline a `-> borrow T` accessor call at its call site (ADR-0062 §3). + /// + /// 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 + /// (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. + #[allow(clippy::too_many_arguments)] + fn expand_accessor_call( + &mut self, + air: &mut Air, + inst_ref: InstRef, + receiver: InstRef, + struct_id: StructId, + method: Spur, + args_range: &rue_rir::RirCallArgsRange, + span: Span, + ctx: &mut AnalysisContext, + ) -> CompileResult { + 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, exactly as a + // `borrow` argument must (E0427); it is read through a shared borrow, + // never moved. + let receiver_span = self.body_rir_ref().get(receiver).span; + let 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() + .any(|(r, kind)| *r == root && *kind == CallLoanKind::Inout) + { + return Err(CompileError::new( + ErrorKind::AccessorLoanConflict { + variable: self.body_interner().resolve(&root).to_string(), + conflict: "while it is passed `inout`", + }, + span, + )); + } + } + let receiver_ty = receiver_trace.result_type(); + 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)); + + // 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)?; + let air_args = + self.analyze_call_operands(air, args_range, ¶m_types, ¶m_modes, false, ctx)?; + + // 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. + 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. + 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 + } + + /// Analyze a `-> borrow T` accessor call in value position (ADR-0062): + /// expand it to guards plus the yielded place, then read the place. The + /// read is a shared-borrow read, so a drop-glue element may only flow to + /// a by-ref consumer (a `borrow` argument or by-ref receiver, which take + /// the place's address); reading it out by value would mint an aliasing + /// owner (E0258) — exactly the RUE-651 double-free the E0711 gate closes. + #[allow(clippy::too_many_arguments)] + pub(super) fn analyze_accessor_call_value( + &mut self, + air: &mut Air, + inst_ref: InstRef, + receiver: InstRef, + struct_id: StructId, + method: Spur, + args_range: &rue_rir::RirCallArgsRange, + span: Span, + ctx: &mut AnalysisContext, + ) -> CompileResult { + let mut trace = self.expand_accessor_call( + air, inst_ref, receiver, struct_id, method, args_range, span, ctx, + )?; + let ty = trace.result_type(); + let byref_consumer = ctx + .byref_arg_root + .is_some_and(|root| root == trace.root_var); + if !byref_consumer && self.type_has_drop_glue(ty) { + return Err(CompileError::new( + ErrorKind::AccessorResultMoved { + ty: ty.safe_name_with_pool(Some(self.body_type_pool())), + }, + span, + ) + .with_help( + "project a field, call a `borrow self` method, compare, or pass the \ + result as a `borrow` argument instead of copying it out", + )); + } + let place = Self::build_place_ref(air, &trace)?; + let value = air.add_inst(AirInst { + data: AirInstData::PlaceRead { place }, + ty, + span, + }); + let value = Self::finish_traced_value(air, &mut trace, value, ty, span)?; + Ok(AnalysisResult::new(value, ty)) + } + + /// The syntactic root of a place expression that may pass through + /// `-> borrow T` accessor calls (ADR-0062): `v.get_ref(i).name` roots at + /// `v`. Extends `root_variable_of` with accessor-call links; `None` when + /// the expression is not a place chain. + pub(crate) fn place_root_with_accessors( + &self, + inst_ref: InstRef, + ctx: &AnalysisContext, + ) -> Option { + match &self.body_rir_ref().get(inst_ref).data { + InstData::VarRef { name, .. } => Some(*name), + InstData::FieldGet { base, .. } | InstData::IndexGet { base, .. } => { + self.place_root_with_accessors(*base, ctx) + } + InstData::MethodCall { + receiver, method, .. + } => { + let base_ty = self.peek_place_type(*receiver, ctx)?; + let base_struct = base_ty.as_struct()?; + let info = self.call_facts().method_info(base_struct, *method)?; + if !info.returns_borrow { + return None; + } + self.place_root_with_accessors(*receiver, ctx) + } + _ => None, + } + } + + /// The accessor variant of `validate_call_contract`: checks arity and + /// explicit argument modes without the by-ref exclusivity pass (accessor + /// parameters are all by-value). + fn validate_call_contract_for_accessor( + &self, + args_range: &rue_rir::RirCallArgsRange, + param_types: &[Type], + param_modes: &[RirParamMode], + span: Span, + ) -> CompileResult<()> { + let args = self.body_rir_ref().call_args(args_range).to_vec(); + if args.len() != param_types.len() { + return Err(CompileError::new( + ErrorKind::WrongArgumentCount { + expected: param_types.len(), + found: args.len(), + }, + span, + )); + } + self.validate_explicit_call_modes(&args, param_modes.iter().copied()) + } + + /// Turn a traced place into its readable AIR value, prefixing any pending + /// accessor guard statements so they execute before the read. + pub(super) fn finish_traced_value( + air: &mut Air, + trace: &mut PlaceTrace, + value: AirRef, + ty: Type, + span: Span, + ) -> CompileResult { + let pending = std::mem::take(&mut trace.pending_stmts); + if pending.is_empty() { + Ok(value) + } else { + Ok(air.add_block(&pending, value, ty, span)?) + } + } + /// Build an AirPlaceRef from a PlaceTrace, adding projections to the Air. pub(super) fn build_place_ref(air: &mut Air, trace: &PlaceTrace) -> CompileResult { let projs = trace.projections.iter().map(|p| p.proj); @@ -859,7 +1329,7 @@ impl OrdinaryBodyEngine<'_, H> { ), InstData::VarRef { name, anchor } => { - let resolved_ty = ctx.resolved_types.get(&inst_ref).copied(); + let resolved_ty = ctx.resolved_type_of(inst_ref); self.analyze_var_ref(air, *name, anchor.clone(), inst.span, resolved_ty, ctx) } @@ -937,7 +1407,7 @@ impl OrdinaryBodyEngine<'_, H> { if annot.is_enum() || self.is_str_like(annot) || self.is_strbuf(annot) { ctx.expected_type = Some(annot); } - } else if let Some(inferred) = ctx.resolved_types.get(&init).copied() + } else if let Some(inferred) = ctx.resolved_type_of(init) && self.is_str_struct(inferred) { // An unannotated literal-derived initializer @@ -981,6 +1451,14 @@ impl OrdinaryBodyEngine<'_, H> { self.reject_non_first_class_str(init, var_type, FirstClassStrSite::Binding, span, ctx)?; } + // A plain `let` cannot bind an accessor result (ADR-0062): the + // borrowed place's extent is the enclosing full expression, so a + // binding would outlive the loan. A wildcard `let _` does not bind + // and is handled as an ordinary (rejected-if-owning) read above. + if name.is_some() { + self.reject_accessor_result_escape(init, AccessorEscapeSite::Let, span, ctx)?; + } + // If name is None, this is a wildcard pattern `_` that discards the value. // `let _ = ;` (and `let _: T = ;`) is a discard site (spec // 3.9:18): discarding a value that carries a linear value would @@ -1619,6 +2097,8 @@ impl OrdinaryBodyEngine<'_, H> { } else { self.analyze_inst(air, value, ctx)? }; + self.reject_accessor_result_escape(value, AccessorEscapeSite::Store, span, ctx)?; + self.reject_accessor_loan_conflict(name, "by assignment", span, ctx)?; // ParamStore has no coercion or conversion step: codegen drops // and copies according to the RHS type. Require semantic type @@ -1698,6 +2178,8 @@ impl OrdinaryBodyEngine<'_, H> { } else { self.analyze_inst(air, value, ctx)? }; + self.reject_accessor_result_escape(value, AccessorEscapeSite::Store, span, ctx)?; + self.reject_accessor_loan_conflict(name, "by assignment", span, ctx)?; // RUE-387: overwriting a local that still holds a live linear value // would drop it implicitly. Legal only when the whole variable was @@ -1954,7 +2436,7 @@ impl OrdinaryBodyEngine<'_, H> { } // Try to trace this expression to a place (lvalue) - if let Some(trace) = self.try_trace_place(inst_ref, air, ctx)? { + if let Some(mut trace) = self.try_trace_place(inst_ref, air, ctx)? { let field_type = trace.result_type(); // Check if the root variable was fully moved (applies regardless of field type) @@ -2034,6 +2516,7 @@ impl OrdinaryBodyEngine<'_, H> { } } else if is_linear { // For linear types, field access consumes the entire struct + self.reject_accessor_place_move(&trace, field_type, span)?; self.reject_linear_destructure_dropping_linear_field(&trace, span)?; self.reject_move_out_of_byref_param(trace.root_var, ctx, span)?; self.reject_move_of_call_loaned_root(trace.root_var, span, ctx)?; @@ -2044,6 +2527,7 @@ impl OrdinaryBodyEngine<'_, H> { emit_move_marker = true; } else if !self.is_type_copy(field_type) { // For non-linear types, check if accessing a non-Copy field + self.reject_accessor_place_move(&trace, field_type, span)?; self.reject_move_out_of_byref_param(trace.root_var, ctx, span)?; self.reject_field_move_out_of_destructor_type(&trace, span)?; @@ -2156,6 +2640,8 @@ impl OrdinaryBodyEngine<'_, H> { span, }); } + // Accessor guard statements (ADR-0062) run before the read. + let air_ref = Self::finish_traced_value(air, &mut trace, air_ref, field_type, span)?; return Ok(AnalysisResult::new(air_ref, field_type)); } @@ -2286,7 +2772,7 @@ impl OrdinaryBodyEngine<'_, H> { let _base_inst = self.body_rir_ref().get(base); // Try to trace this expression to a place (lvalue) - if let Some(trace) = self.try_trace_place(inst_ref, air, ctx)? { + if let Some(mut trace) = self.try_trace_place(inst_ref, air, ctx)? { let elem_type = trace.result_type(); // Reading through an index expression whose base place has been @@ -2380,6 +2866,7 @@ impl OrdinaryBodyEngine<'_, H> { } self.check_read_through_moved_element(&trace, ctx, span)?; } else if !self.is_type_copy(elem_type) { + self.reject_accessor_place_move(&trace, elem_type, span)?; // A CONSTANT index directly into an array variable moves // just that element out (per-element tracking, RUE-186, // spec 3.8:68). Everything else — dynamic index, or an @@ -2423,6 +2910,8 @@ impl OrdinaryBodyEngine<'_, H> { span, )?; } + // Accessor guard statements (ADR-0062) run before the read. + let air_ref = Self::finish_traced_value(air, &mut trace, air_ref, elem_type, span)?; return Ok(AnalysisResult::new(air_ref, elem_type)); } @@ -3017,6 +3506,11 @@ impl OrdinaryBodyEngine<'_, H> { // RUE-233) — E0428, like an explicit `borrow` parameter. self.reject_mutate_iter_borrowed(trace.root_var, span, ctx)?; + // Writing to a root an accessor result borrows in the same full + // expression is an exclusive use inside a shared loan's extent + // (ADR-0062, E0259). + self.reject_accessor_loan_conflict(trace.root_var, "by assignment", span, ctx)?; + // Check mutability let root_name = self.body_interner().resolve(&trace.root_var).to_string(); if !trace.is_root_mutable { @@ -3092,6 +3586,7 @@ impl OrdinaryBodyEngine<'_, H> { // Analyze the value let value_result = self.analyze_inst(air, value, ctx)?; + self.reject_accessor_result_escape(value, AccessorEscapeSite::Store, span, ctx)?; // RUE-387: writing a live linear value's field would silently drop // the old field value. Legal only when that exact field path was @@ -3178,6 +3673,11 @@ impl OrdinaryBodyEngine<'_, H> { // RUE-233) — E0428, like an explicit `borrow` parameter. self.reject_mutate_iter_borrowed(trace.root_var, span, ctx)?; + // Writing to a root an accessor result borrows in the same full + // expression is an exclusive use inside a shared loan's extent + // (ADR-0062, E0259). + self.reject_accessor_loan_conflict(trace.root_var, "by assignment", span, ctx)?; + // Check mutability let root_name = self.body_interner().resolve(&trace.root_var).to_string(); if !trace.is_root_mutable { @@ -3277,6 +3777,7 @@ impl OrdinaryBodyEngine<'_, H> { // Analyze the value let value_result = self.analyze_inst(air, value, ctx)?; + self.reject_accessor_result_escape(value, AccessorEscapeSite::Store, span, ctx)?; // RUE-387: writing a live linear value into an array element would // silently drop the old element. Legal only when that exact @@ -3943,12 +4444,23 @@ impl OrdinaryBodyEngine<'_, H> { /// Check exclusivity rules for inout and borrow parameters in a call /// (adapter over the shared [`check_exclusive_access_in`], RUE-141). - pub(crate) fn check_exclusive_access(&self, args: A, call_span: Span) -> CompileResult<()> + pub(crate) fn check_exclusive_access( + &self, + args: A, + call_span: Span, + ctx: &AnalysisContext, + ) -> CompileResult<()> where A: IntoIterator, A::Item: std::ops::Deref, { - check_exclusive_access_in(self.body_rir_ref(), self.body_interner(), args, call_span) + check_exclusive_access_in( + self.body_rir_ref(), + self.body_interner(), + args, + call_span, + &|inst_ref| self.place_root_with_accessors(inst_ref, ctx), + ) } /// Reject recording a MOVE of `root` while an enclosing call's argument @@ -3988,9 +4500,86 @@ impl OrdinaryBodyEngine<'_, H> { ))); } } + // An accessor-result loan on this root (ADR-0062) spans the enclosing + // full expression; a move within that extent would leave the borrowed + // place aliasing moved-from storage. + self.reject_accessor_loan_conflict(root, "by value (move)", span, ctx) + } + + /// Reject an exclusive use — `inout`, mutation, or move — of a root that + /// an accessor result borrows within the same full expression (ADR-0062, + /// E0259). The accessor loan is shared and extends to the end of the + /// enclosing full expression, so any exclusive access within that extent + /// violates the law of exclusivity. + pub(crate) fn reject_accessor_loan_conflict( + &self, + root: Spur, + conflict: &'static str, + span: Span, + ctx: &AnalysisContext, + ) -> CompileResult<()> { + if let Some((_, loan_span)) = ctx.expression_loans.iter().find(|(r, _)| *r == root) { + return Err(CompileError::new( + ErrorKind::AccessorLoanConflict { + variable: self.body_interner().resolve(&root).to_string(), + conflict, + }, + span, + ) + .with_label("accessor result borrows the value here", *loan_span)); + } Ok(()) } + /// Reject a by-value move of a non-trivially-droppable value out of a + /// place reached through an accessor result (ADR-0062, E0258): the + /// result is a shared borrow, not an owner, so moving an owning value + /// out of it would mint an aliasing second owner. + pub(super) fn reject_accessor_place_move( + &self, + trace: &PlaceTrace, + moved_type: Type, + span: Span, + ) -> CompileResult<()> { + if trace.via_accessor { + return Err(CompileError::new( + ErrorKind::AccessorResultMoved { + ty: moved_type.safe_name_with_pool(Some(self.body_type_pool())), + }, + span, + )); + } + Ok(()) + } + + /// Reject the direct escape of an accessor result (ADR-0062): `operand` + /// is the RIR expression consumed at an escape-shaped site, and if it was + /// expanded as an accessor call, the borrowed place would outlive its + /// enclosing full expression there. + pub(crate) fn reject_accessor_result_escape( + &self, + operand: InstRef, + site: AccessorEscapeSite, + span: Span, + ctx: &AnalysisContext, + ) -> CompileResult<()> { + let Some((method, root)) = ctx.accessor_call_insts.get(&operand) else { + return Ok(()); + }; + let method = self.body_interner().resolve(method).to_string(); + let root = self.body_interner().resolve(root).to_string(); + let kind = match site { + AccessorEscapeSite::Return => ErrorKind::AccessorResultReturned { method, root }, + AccessorEscapeSite::Store => ErrorKind::AccessorResultStored { method, root }, + AccessorEscapeSite::Let => ErrorKind::AccessorResultBound { method, root }, + AccessorEscapeSite::Capture => ErrorKind::AccessorResultCaptured { method, root }, + }; + Err(CompileError::new(kind, span).with_help( + "read the borrowed place, project a field from it, pass it as a `borrow` \ + argument, or compare it — all within the expression that calls the accessor", + )) + } + /// Is `operand` a *direct* reference to a `borrow str` / `inout str` /// parameter — i.e. a borrowed `str` *view* value (ADR-0043 two-types /// model, RUE-386)? @@ -4174,9 +4763,35 @@ impl OrdinaryBodyEngine<'_, H> { } else { return None; }; - root_variable_of(self.body_rir_ref(), arg.value).map(|root| (root, kind)) + root_variable_of(self.body_rir_ref(), arg.value) + .or_else(|| { + // Accessor-call place chains join the shared set + // (ADR-0062); exclusive accessor results do not exist + // in this phase. + (kind == CallLoanKind::Borrow) + .then(|| self.place_root_with_accessors(arg.value, ctx)) + .flatten() + }) + .map(|root| (root, kind)) }) .collect(); + // An `inout` loan on a root an accessor result borrows in the same + // full expression violates exclusivity (ADR-0062, E0259). Checked at + // frame construction (loans taken by earlier sibling arguments) and + // again by accessor expansion itself (the reverse nesting order). + for arg in args.clone() { + if !arg.is_inout() { + continue; + } + if let Some(root) = root_variable_of(self.body_rir_ref(), arg.value) { + self.reject_accessor_loan_conflict( + root, + "as `inout`", + self.body_rir_ref().get(arg.value).span, + ctx, + )?; + } + } let pushed = !frame.is_empty(); if pushed { ctx.call_loaned_roots.push(frame); @@ -4302,7 +4917,23 @@ impl OrdinaryBodyEngine<'_, H> { } let byref_root = if arg.is_inout() || arg.is_borrow() { - let root = require_byref_place_arg(self.body_rir_ref(), &arg)?; + // A `borrow` argument may be an accessor-call place chain + // (ADR-0062): it borrows the accessor receiver's root. + let root = match root_variable_of(self.body_rir_ref(), arg.value) { + Some(root) => root, + None => match self + .place_root_with_accessors(arg.value, ctx) + .filter(|_| arg.is_borrow()) + { + Some(root) => root, + None => { + return Err(require_byref_place_arg(self.body_rir_ref(), &arg) + .expect_err( + "argument without a place root fails the byref check", + )); + } + }, + }; if arg.is_inout() && !ctx.locals.contains_key(&root) && ctx @@ -4326,9 +4957,17 @@ impl OrdinaryBodyEngine<'_, H> { ctx.byref_arg_root = prev_byref_root; let arg_result = arg_result?; if arg.is_inout() || arg.is_borrow() { + // An inlined accessor argument arrives as a guards block whose + // tail is the place read (ADR-0062); peel it for the address + // check, like the `inout str` view materialization below. + let addressable_probe = if ctx.accessor_call_insts.contains_key(&arg.value) { + self.peel_projected_rvalue_scope(air, arg_result.air_ref).0 + } else { + arg_result.air_ref + }; self.require_addressable_read( air, - arg_result.air_ref, + addressable_probe, arg.is_inout(), self.body_rir_ref().get(arg.value).span, )?; diff --git a/crates/rue-air/src/sema/analysis/pointers.rs b/crates/rue-air/src/sema/analysis/pointers.rs index 81112bc33..729ba584d 100644 --- a/crates/rue-air/src/sema/analysis/pointers.rs +++ b/crates/rue-air/src/sema/analysis/pointers.rs @@ -68,7 +68,7 @@ impl OrdinaryBodyEngine<'_, H> { // other path, instead of silently truncating (RUE-244). Skip when the // resolved type is unconstrained (`` — e.g. no annotation) or // never; those carry no expectation to check against. - if let Some(&expected) = ctx.resolved_types.get(&inst_ref) + if let Some(expected) = ctx.resolved_type_of(inst_ref) && !self.types_equivalent(expected, pointee_type) && !expected.is_error() && !expected.is_never() @@ -599,7 +599,7 @@ impl OrdinaryBodyEngine<'_, H> { self.require_intrinsic_type("alloc_bytes", align.ty, Type::U64, span)?; self.require_power_of_two_align("alloc_bytes", args[1].value, span, ctx)?; let result_ty = Type::new_ptr_mut(self.body_type_pool().intern_ptr_mut_from_type(Type::U8)); - if let Some(&expected) = ctx.resolved_types.get(&inst_ref) + if let Some(expected) = ctx.resolved_type_of(inst_ref) && !self.types_equivalent(expected, result_ty) && !expected.is_error() && !expected.is_never() diff --git a/crates/rue-air/src/sema/analysis/type_inference.rs b/crates/rue-air/src/sema/analysis/type_inference.rs index 7e0b7d4f3..db85c6a9e 100644 --- a/crates/rue-air/src/sema/analysis/type_inference.rs +++ b/crates/rue-air/src/sema/analysis/type_inference.rs @@ -644,7 +644,7 @@ impl OrdinaryBodyEngine<'_, H> { // operands through here (RUE-165). Constants inline a fresh // value, so there is no move state to preserve, and unknown // names still get E0201 from the fallback. - let resolved_ty = ctx.resolved_types.get(&inst_ref).copied(); + let resolved_ty = ctx.resolved_type_of(inst_ref); return self.analyze_var_ref( air, *name, @@ -902,6 +902,16 @@ impl OrdinaryBodyEngine<'_, H> { return Ok(AnalysisResult::new(air_ref, element_type)); } + // A `-> borrow T` accessor call is a place in projection position + // (ADR-0062): read it through the traced place, so comparing or + // projecting a drop-glue element borrows rather than copies it. + if matches!(&inst.data, InstData::MethodCall { .. }) + && self.place_root_with_accessors(inst_ref, ctx).is_some() + && let Some(result) = self.try_read_traced_place(air, inst_ref, inst.span, ctx)? + { + return Ok(result); + } + // For other expressions, use the normal analyze_inst // (they will trigger move semantics as expected) self.analyze_inst(air, inst_ref, ctx) @@ -918,7 +928,7 @@ impl OrdinaryBodyEngine<'_, H> { span: Span, context: &str, ) -> CompileResult { - ctx.resolved_types.get(&inst_ref).copied().ok_or_else(|| { + ctx.resolved_type_of(inst_ref).ok_or_else(|| { CompileError::new( ErrorKind::InternalError(format!( "type inference did not resolve type for {} (instruction {:?})", diff --git a/crates/rue-air/src/sema/binding_manifest.rs b/crates/rue-air/src/sema/binding_manifest.rs index 29eb39a34..a2c55074d 100644 --- a/crates/rue-air/src/sema/binding_manifest.rs +++ b/crates/rue-air/src/sema/binding_manifest.rs @@ -1388,6 +1388,10 @@ impl<'a> DeclarationShells<'a> { .structs_by_file_name .get(&(pending.shell.declaration_span.file_id, owner)) .ok_or(DeclarationInstallFailure::MissingNominal)?; + // The shell carries no accessor flag; derive it from + // the body's trailing `yield`, which coincides on + // every accepted program. + let returns_borrow = super::info::body_ends_in_yield(self.sema.rir, body); self.sema.methods.insert( (id, *name), super::MethodInfo { @@ -1399,6 +1403,7 @@ impl<'a> DeclarationShells<'a> { return_type: return_type_value, body, span: pending.shell.declaration_span, + returns_borrow, }, ); self.sema diff --git a/crates/rue-air/src/sema/body_identity.rs b/crates/rue-air/src/sema/body_identity.rs index a11cbabb4..4349d1404 100644 --- a/crates/rue-air/src/sema/body_identity.rs +++ b/crates/rue-air/src/sema/body_identity.rs @@ -2192,6 +2192,9 @@ pub(in crate::sema) struct MethodIdentityHandle { pub body: InstRef, pub span: Span, pub self_is_mut: bool, + /// Whether the declaration is a `-> borrow T` accessor (ADR-0062); + /// carried from the RIR `FnDecl` like `self_is_mut`. + pub returns_borrow: bool, } impl BodyIdentityPool @@ -2264,6 +2267,7 @@ where return_type: signature.return_type, body: handle.body, span: handle.span, + returns_borrow: handle.returns_borrow, }) } @@ -2278,6 +2282,11 @@ 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, }) } @@ -2871,6 +2880,7 @@ mod tests { return_type: Type::I32, body: InstRef::from_raw(body), span: Span::with_file(owner.0, 1, 2), + returns_borrow: false, }; let named = info(10); let anonymous = info(11); @@ -3388,6 +3398,7 @@ mod tests { body: InstRef::from_raw(201), span: Span::with_file(FileId::new(3), 1, 4), self_is_mut: true, + returns_borrow: false, } } @@ -5569,7 +5580,10 @@ mod tests { fn method_handle_from_rir(sema: &BodySema<'_>, declaration: InstRef) -> MethodIdentityHandle { let inst = sema.rir.get(declaration); let InstData::FnDecl { - body, self_is_mut, .. + body, + self_is_mut, + returns_borrow, + .. } = &inst.data else { panic!("method declaration must be a FnDecl"); @@ -5578,6 +5592,7 @@ mod tests { body: *body, span: inst.span, self_is_mut: *self_is_mut, + returns_borrow: *returns_borrow, } } diff --git a/crates/rue-air/src/sema/call_resolution.rs b/crates/rue-air/src/sema/call_resolution.rs index 9d8e3aebb..8b818fe9c 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; +use rue_span::{FileId, Span}; use super::body_identity::{ BodyRirView, DurableCallableSource, DurableNominalSource, FunctionIdentityHandle, @@ -69,6 +69,14 @@ 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`. @@ -94,6 +102,7 @@ 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, @@ -142,6 +151,12 @@ 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, @@ -197,6 +212,9 @@ 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) } @@ -558,7 +576,10 @@ where fn method_handle(&self, declaration: InstRef) -> Option { let inst = self.rir.rir().get(declaration); let InstData::FnDecl { - body, self_is_mut, .. + body, + self_is_mut, + returns_borrow, + .. } = &inst.data else { return None; @@ -567,6 +588,7 @@ where body: *body, span: inst.span, self_is_mut: *self_is_mut, + returns_borrow: *returns_borrow, }) } diff --git a/crates/rue-air/src/sema/consistency_tests.rs b/crates/rue-air/src/sema/consistency_tests.rs index f38c9fee8..ccfe29f6e 100644 --- a/crates/rue-air/src/sema/consistency_tests.rs +++ b/crates/rue-air/src/sema/consistency_tests.rs @@ -336,6 +336,7 @@ mod tests { is_destructor, allow_unused_variable, self_is_mut, + is_accessor, )" ), "analyze_function_internal must be exactly one forwarding expression" diff --git a/crates/rue-air/src/sema/context.rs b/crates/rue-air/src/sema/context.rs index 854a61179..4877e288e 100644 --- a/crates/rue-air/src/sema/context.rs +++ b/crates/rue-air/src/sema/context.rs @@ -5,12 +5,14 @@ //! state tracking for affine types. use std::collections::{HashMap, HashSet}; +use std::sync::Arc; use lasso::Spur; use rue_error::CompileWarning; use rue_rir::RirParamMode; use rue_span::{FileId, Span}; +use crate::inst::{AirPlaceBase, AirProjection}; use crate::scope::ScopedContext; use crate::types::{StructId, Type}; @@ -330,6 +332,34 @@ impl CallLoanKind { } } +/// One projection step of a [`PlaceAlias`], mirroring the metadata the place +/// tracer collects (see `analysis::ownership::ProjectionInfo`) in an owned, +/// clonable form. +#[derive(Debug, Clone)] +pub(crate) struct AliasProjection { + pub proj: AirProjection, + pub result_type: Type, + pub field_name: Option, + pub const_index: Option, + pub index_segment: Option, +} + +/// A name bound to a caller place during accessor inlining (ADR-0062). +/// +/// While a `-> borrow T` accessor call is expanded at its call site, the +/// accessor body's `self` resolves to the *caller's* receiver place: the +/// place tracer substitutes this alias where the body says `self`, so +/// `self.f` composes to `.f` rooted at the caller's own +/// root variable. The alias is a shared borrow: places reached through it +/// are read-only and never moved out of. +#[derive(Debug, Clone)] +pub(crate) struct PlaceAlias { + pub base: AirPlaceBase, + pub base_type: Type, + pub projections: Vec, + pub root_var: Spur, +} + /// Context for analyzing instructions within a function. /// /// Bundles together the mutable state that needs to be threaded through @@ -476,6 +506,37 @@ pub(crate) struct AnalysisContext<'a> { /// `Option(T)` (RUE-6, ADR-0038). Context never selects that nominal. Left /// `None` everywhere else, so no other analysis is affected. pub expected_type: Option, + /// The shared inference context for this body, threaded here so accessor + /// call expansion (ADR-0062) can run type inference for the accessor's + /// body on demand before splicing it into the caller. + pub infer_ctx: &'a super::inference_ctx::InferenceContext, + /// When analyzing a `-> borrow T` accessor body, the body block's single + /// trailing `yield` instruction — the only `yield` the body may contain. + /// `None` outside accessor bodies; a `yield` analyzed while this is + /// `None` is E0256, and one that is not this exact instruction is E0254. + pub accessor_trailing_yield: Option, + /// RIR method-call instructions that expanded as accessor calls in this + /// body (ADR-0062), mapped to the accessor's method name and receiver + /// root. Escape-shape checks (`return`/`let`/store/aggregate capture) + /// 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, + /// 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 + /// truncates this to its pre-statement length after every statement, so + /// an entry's extent is exactly the enclosing full expression. An + /// exclusive use of a listed root within that extent is E0259. + pub expression_loans: Vec<(Spur, Span)>, + /// Resolved-type overlays for accessor bodies currently being inlined, + /// innermost last. `resolved_type_of` consults these before the body's + /// own `resolved_types`, letting the caller's analysis walk accessor-body + /// instructions that the caller's inference never visited. + pub inline_resolved_types: Vec>>, + /// Names bound to caller places during accessor inlining (`self` inside + /// an inlined accessor body). Scoped save/restore is handled by the + /// expansion itself. + pub place_aliases: HashMap, /// True only while analyzing the operand of a `?` expression (RUE-318). The /// `?` site cannot supply an `expected_type` for a *bare* fallible intrinsic /// (`@read_line()?` / `@parse_i64(s)?`): the enclosing function's `Option(U)` @@ -684,10 +745,27 @@ impl<'a> AnalysisContext<'a> { in_loop_move_recheck: true, iter_borrows: self.iter_borrows.clone(), expected_type: None, + infer_ctx: self.infer_ctx, + accessor_trailing_yield: self.accessor_trailing_yield, + accessor_call_insts: self.accessor_call_insts.clone(), + expression_loans: self.expression_loans.clone(), + inline_resolved_types: self.inline_resolved_types.clone(), + place_aliases: self.place_aliases.clone(), try_operand: false, } } + /// Look up an instruction's inferred type, consulting the accessor-inline + /// overlays (innermost first) before the body's own inference results. + pub fn resolved_type_of(&self, inst_ref: InstRef) -> Option { + for overlay in self.inline_resolved_types.iter().rev() { + if let Some(ty) = overlay.get(&inst_ref) { + return Some(*ty); + } + } + self.resolved_types.get(&inst_ref).copied() + } + /// Merge move states from two branches. /// /// For if-else expressions, a variable is considered moved after the expression diff --git a/crates/rue-air/src/sema/control_flow.rs b/crates/rue-air/src/sema/control_flow.rs index 197b09b9c..e9d0d9a8f 100644 --- a/crates/rue-air/src/sema/control_flow.rs +++ b/crates/rue-air/src/sema/control_flow.rs @@ -14,6 +14,7 @@ use rue_span::Span; use super::analysis::FirstClassStrSite; use super::anon_structs::TrustedTryProducer; +use super::call_resolution::CallResolutionFacts; use super::context::{AnalysisContext, AnalysisResult, ConstValue, LocalVar}; use crate::inst::{Air, AirInst, AirInstData, AirPattern, AirRef}; use crate::scope::ScopedContext; @@ -103,6 +104,8 @@ impl OrdinaryBodyEngine<'_, H> { self.analyze_return(air, inner.as_ref().copied(), inst.span, ctx) } + InstData::Yield(operand) => self.analyze_yield(air, *operand, inst_ref, inst.span, ctx), + InstData::Block { instructions } => { self.analyze_block(air, instructions, inst.span, ctx) } @@ -1346,6 +1349,12 @@ impl OrdinaryBodyEngine<'_, H> { span: Span, ctx: &mut AnalysisContext, ) -> CompileResult { + // A `?` early return is a non-diverging exit that bypasses an + // accessor body's single trailing `yield` (ADR-0062 phase 1). + if ctx.accessor_trailing_yield.is_some() { + return Err(CompileError::new(ErrorKind::AccessorBodyMissingYield, span) + .with_note("an accessor body cannot contain `?`; guards may only diverge or fall through to the trailing `yield`")); + } let return_type = ctx.return_type; // Analyze the operand first, then dispatch on ITS shape (Option vs @@ -1765,6 +1774,132 @@ impl OrdinaryBodyEngine<'_, H> { } /// Analyze a return statement. + /// Analyze a `yield` — the exit form of a `-> borrow T` accessor body + /// (ADR-0062). Outside an accessor body it is E0256; inside one, only the + /// body's single trailing `yield` is legal (E0254 otherwise). The + /// trailing `yield` of an *inlined* accessor body never reaches this + /// dispatch — call-site expansion consumes it directly as a place — so + /// this arm covers the standalone compilation of the accessor itself, + /// where the yielded place is validated and the exit lowers to an + /// unreachable trap (every real call site inlines the body; no + /// out-of-line accessor is ever invoked, which is what keeps this + /// construct free of any new call shape or ABI). + fn analyze_yield( + &mut self, + air: &mut Air, + operand: InstRef, + inst_ref: InstRef, + span: Span, + ctx: &mut AnalysisContext, + ) -> CompileResult { + self.require_preview( + rue_error::PreviewFeature::BorrowAccessors, + "a `yield` accessor exit", + span, + )?; + let Some(trailing) = ctx.accessor_trailing_yield else { + return Err(CompileError::new(ErrorKind::YieldOutsideAccessor, span)); + }; + if trailing != inst_ref { + return Err(CompileError::new(ErrorKind::AccessorBodyMissingYield, span) + .with_note("this `yield` is not the single trailing exit of the accessor body")); + } + + // The yielded place must be a projection chain rooted at the receiver + // parameter (E0255). Checked syntactically before the operand is read + // so a local or temporary is named as such rather than surfacing as a + // 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)?; + if !ctx.return_type.is_error() + && !read.ty.is_error() + && !self.types_compatible(read.ty, ctx.return_type) + { + return Err(CompileError::new( + ErrorKind::TypeMismatch { + expected: ctx + .return_type + .safe_name_with_pool(Some(self.body_type_pool())), + found: read.ty.safe_name_with_pool(Some(self.body_type_pool())), + }, + span, + )); + } + + 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)) + } + + /// Walk a yield operand's projection chain to its root and require that + /// root to be the receiver parameter `self` (ADR-0062, E0255). A nested + /// method-call link is legal only when it is itself an accessor. + fn check_yield_rooted_at_receiver( + &mut self, + operand: InstRef, + ctx: &mut AnalysisContext, + ) -> CompileResult<()> { + let self_sym = self.body_interner().get_or_intern("self"); + let mut current = operand; + loop { + let inst = self.body_rir_ref().get(current); + let span = inst.span; + match &inst.data { + InstData::VarRef { name, .. } => { + if *name == self_sym { + return Ok(()); + } + let found = + format!("a place rooted at `{}`", self.body_interner().resolve(name)); + return Err(CompileError::new( + ErrorKind::AccessorYieldNotReceiverRooted { found }, + span, + )); + } + InstData::FieldGet { base, .. } => current = *base, + InstData::IndexGet { base, .. } => current = *base, + InstData::MethodCall { + receiver, method, .. + } => { + let is_accessor = ctx + .resolved_type_of(*receiver) + .and_then(|ty| ty.as_struct()) + .and_then(|struct_id| self.call_facts().method_info(struct_id, *method)) + .is_some_and(|info| info.returns_borrow); + if !is_accessor { + return Err(CompileError::new( + ErrorKind::AccessorYieldNotReceiverRooted { + found: "a method-call result that is not an accessor projection" + .to_string(), + }, + span, + )); + } + current = *receiver; + } + _ => { + return Err(CompileError::new( + ErrorKind::AccessorYieldNotReceiverRooted { + found: "a value expression".to_string(), + }, + span, + )); + } + } + } + } + fn analyze_return( &mut self, air: &mut Air, @@ -1772,6 +1907,13 @@ impl OrdinaryBodyEngine<'_, H> { span: Span, ctx: &mut AnalysisContext, ) -> CompileResult { + // An accessor body has exactly one exit form — its trailing `yield` + // (ADR-0062 phase 1); an early `return` would be a non-diverging exit + // that bypasses it. + if ctx.accessor_trailing_yield.is_some() { + return Err(CompileError::new(ErrorKind::AccessorBodyMissingYield, span) + .with_note("an accessor body cannot contain `return`; guards may only diverge or fall through to the trailing `yield`")); + } let inner_air_ref = if let Some(inner) = inner { // Explicit return with value. A `str`-returning function // (ADR-0043 Phase 3, RUE-324) supplies `str` as the expected type so @@ -1803,6 +1945,16 @@ impl OrdinaryBodyEngine<'_, H> { )?; } + // An accessor result is a second-class borrowed place scoped to + // its full expression (ADR-0062); returning it would let the + // loan escape the receiver access that justifies it. + self.reject_accessor_result_escape( + inner, + super::analysis::AccessorEscapeSite::Return, + span, + ctx, + )?; + // Type check: returned value must match function's return type. if !ctx.return_type.is_error() && !inner_ty.is_error() @@ -1870,11 +2022,21 @@ impl OrdinaryBodyEngine<'_, H> { let recovery_checkpoint = self .body_analysis_error_recovery() .then(|| (air.checkpoint(), ctx.clone())); + // Each non-tail statement is a full expression: accessor-result + // loans taken inside it (ADR-0062) end when it does. Loans taken + // by an *enclosing* statement (this block nested inside an + // expression) stay live across it, so this truncates rather than + // clears. The tail expression's loans are part of the enclosing + // full expression and survive the block. + let expression_loans_before = ctx.expression_loans.len(); let outcome = if is_last { self.analyze_inst(air, inst_ref, ctx) } else { ctx.with_expected_type(None, |ctx| self.analyze_inst(air, inst_ref, ctx)) }; + if !is_last { + ctx.expression_loans.truncate(expression_loans_before); + } let result = match outcome { Ok(result) => result, Err(error) if self.body_analysis_error_recovery() => { diff --git a/crates/rue-air/src/sema/declarations.rs b/crates/rue-air/src/sema/declarations.rs index 6ce41a33d..03eda70a2 100644 --- a/crates/rue-air/src/sema/declarations.rs +++ b/crates/rue-air/src/sema/declarations.rs @@ -1370,11 +1370,29 @@ impl<'a> Sema<'a, super::MutableDeclarations> { let InstData::FnDecl { params: rir_params_range, directives: directives_range, + returns_borrow, .. } = &self.rir.get(declaration).data else { unreachable!() }; + // A `-> borrow T` result is the accessor form (ADR-0062) and exists + // only on `borrow self` methods; a free or associated function cannot + // hand out a receiver projection. Gate first so an ungated program + // reports E1100 rather than the shape error. + if *returns_borrow { + self.require_preview( + PreviewFeature::BorrowAccessors, + "a `-> borrow` accessor", + span, + )?; + return Err(CompileError::new( + ErrorKind::AccessorRequiresBorrowSelf { + found: "a free function".to_string(), + }, + span, + )); + } let params = self.rir.params(rir_params_range); let directives = self.rir.directives(directives_range); let allow_unused_function = self.has_allow_directive(directives.iter(), "unused_function"); @@ -1641,6 +1659,7 @@ impl<'a> Sema<'a, super::MutableDeclarations> { has_self, self_mode, self_is_mut, + returns_borrow, .. } = &method_inst.data { @@ -1675,6 +1694,75 @@ impl<'a> Sema<'a, super::MutableDeclarations> { let param_names: Vec = params.iter().map(|p| p.name).collect(); let param_modes: Vec = params.iter().map(|p| p.mode).collect(); let param_comptime: Vec = params.iter().map(|p| p.is_comptime).collect(); + + // Place-returning borrow accessors (ADR-0062, RUE-662): + // `-> borrow T` is gated behind the `borrow_accessors` preview + // and requires a shared `borrow self` receiver (phase 1; + // `inout self` accessors are RUE-1016). Accessor parameters + // are by-value guard inputs. + if *returns_borrow { + self.require_preview( + PreviewFeature::BorrowAccessors, + "a `-> borrow` accessor", + method_inst.span, + )?; + if !*has_self { + return Err(CompileError::new( + ErrorKind::AccessorRequiresBorrowSelf { + found: "an associated function with no receiver".to_string(), + }, + method_inst.span, + )); + } + match self_mode { + RirParamMode::Borrow => {} + RirParamMode::Inout => { + return Err(CompileError::new( + ErrorKind::AccessorRequiresBorrowSelf { + found: "an `inout self` receiver".to_string(), + }, + method_inst.span, + ) + .with_note( + "mutable accessors (`inout self` -> exclusive result) are a \ + later phase (RUE-1016)", + )); + } + RirParamMode::Normal => { + return Err(CompileError::new( + ErrorKind::AccessorRequiresBorrowSelf { + found: "a by-value `self` receiver".to_string(), + }, + method_inst.span, + )); + } + } + for param in params.iter() { + if param.mode != RirParamMode::Normal { + return Err(CompileError::new( + ErrorKind::AccessorParamModeUnsupported { + mode: format!( + "`{}`", + match param.mode { + RirParamMode::Inout => "inout", + RirParamMode::Borrow => "borrow", + RirParamMode::Normal => unreachable!(), + } + ), + }, + param.span, + )); + } + if param.is_comptime { + return Err(CompileError::new( + ErrorKind::AccessorParamModeUnsupported { + mode: "`comptime`".to_string(), + }, + param.span, + )); + } + } + } // `Self` in a method signature (parameter or return position) // resolves to the enclosing struct's type, just like the // receiver does. Named-struct inline methods reach this path; @@ -1705,6 +1793,7 @@ impl<'a> Sema<'a, super::MutableDeclarations> { return_type: ret_type, body: *body, span: method_inst.span, + returns_borrow: *returns_borrow, }, ); self.named_method_declarations.insert(key, method_ref); diff --git a/crates/rue-air/src/sema/fact_mode.rs b/crates/rue-air/src/sema/fact_mode.rs index d526a5267..60a2917db 100644 --- a/crates/rue-air/src/sema/fact_mode.rs +++ b/crates/rue-air/src/sema/fact_mode.rs @@ -334,6 +334,9 @@ 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/info.rs b/crates/rue-air/src/sema/info.rs index 086fedabd..f9d064359 100644 --- a/crates/rue-air/src/sema/info.rs +++ b/crates/rue-air/src/sema/info.rs @@ -94,6 +94,28 @@ pub struct MethodInfo { pub body: rue_rir::InstRef, /// Span of the method declaration pub span: Span, + /// Whether the result position is `-> borrow T` (ADR-0062): the method + /// is a place-returning accessor whose calls inline to guards plus the + /// yielded receiver projection. `return_type` holds the element type `T`. + pub returns_borrow: bool, +} + +/// Whether a function body block ends in a `yield` — the derived marker of a +/// `-> borrow T` accessor body (ADR-0062) for install paths that carry a body +/// handle but not the declaring `FnDecl`. A non-accessor body ending in +/// `yield` is ill-formed (E0256) and never completes compilation, so the +/// derivation agrees with the declaration flag on every accepted program. +pub(crate) fn body_ends_in_yield(rir: &rue_rir::Rir, body: rue_rir::InstRef) -> bool { + match &rir.get(body).data { + rue_rir::InstData::Block { instructions } => rir + .block_insts(instructions) + .values() + .last() + .is_some_and(|last| matches!(rir.get(last).data, rue_rir::InstData::Yield(_))), + // A single-statement body lowers to the instruction itself. + rue_rir::InstData::Yield(_) => true, + _ => false, + } } /// Signature-only callable metadata consumed at a call site. Imported @@ -134,6 +156,10 @@ pub(crate) struct MethodCallInfo { pub self_mode: rue_rir::RirParamMode, pub params: ParamRange, pub return_type: Type, + /// Whether the method is a `-> borrow T` accessor (ADR-0062). Accessor + /// calls do not dispatch as ordinary calls: they inline the accessor + /// body at the call site via the dedicated accessor-body fact. + pub returns_borrow: bool, } impl MethodCallInfo { @@ -144,6 +170,7 @@ impl MethodCallInfo { self_mode: info.self_mode, params: info.params, return_type: info.return_type, + returns_borrow: info.returns_borrow, } } } diff --git a/crates/rue-air/src/sema/ordinary_engine.rs b/crates/rue-air/src/sema/ordinary_engine.rs index 67c51a33c..d9cc73179 100644 --- a/crates/rue-air/src/sema/ordinary_engine.rs +++ b/crates/rue-air/src/sema/ordinary_engine.rs @@ -597,13 +597,16 @@ impl<'h, H: OrdinaryBodyAnalysisHost> OrdinaryBodyEngine<'h, H> { has_self, self_mode, self_is_mut, + returns_borrow, .. } = method_inst.data else { continue; }; let key = (struct_id, method_name); - if !seen_methods.insert(method_name) || self.has_method(key) { + // Accessors are not supported on anonymous structs (ADR-0062 + // phase 1). + if !seen_methods.insert(method_name) || self.has_method(key) || returns_borrow { return None; } let parameters = self.body_rir_ref().params(¶ms).to_vec(); @@ -649,6 +652,7 @@ impl<'h, H: OrdinaryBodyAnalysisHost> OrdinaryBodyEngine<'h, H> { return_type: return_ty, body, span: method_inst.span, + returns_borrow: false, }, )); } @@ -1517,6 +1521,7 @@ impl<'h, H: OrdinaryBodyAnalysisHost> OrdinaryBodyEngine<'h, H> { has_self: bool, self_mode: RirParamMode, self_is_mut: bool, + returns_borrow: bool, ) -> CompileResult<( AnalyzedFunction, Vec, @@ -1538,7 +1543,7 @@ impl<'h, H: OrdinaryBodyAnalysisHost> OrdinaryBodyEngine<'h, H> { ) })?, ); - self.analyze_method_with_identity( + self.analyze_method_with_identity_kind( infer_ctx, identity, full_name, @@ -1550,6 +1555,8 @@ impl<'h, H: OrdinaryBodyAnalysisHost> OrdinaryBodyEngine<'h, H> { has_self, self_mode, self_is_mut, + false, + returns_borrow, ) } @@ -1592,9 +1599,11 @@ impl<'h, H: OrdinaryBodyAnalysisHost> OrdinaryBodyEngine<'h, H> { self_mode, self_is_mut, false, + false, ) } + #[allow(clippy::too_many_arguments)] fn analyze_method_with_identity_kind

( &mut self, infer_ctx: &InferenceContext, @@ -1612,6 +1621,7 @@ impl<'h, H: OrdinaryBodyAnalysisHost> OrdinaryBodyEngine<'h, H> { self_mode: RirParamMode, self_is_mut: bool, is_destructor: bool, + is_accessor: bool, ) -> CompileResult<( AnalyzedFunction, Vec, @@ -1665,6 +1675,7 @@ impl<'h, H: OrdinaryBodyAnalysisHost> OrdinaryBodyEngine<'h, H> { is_destructor, false, self_is_mut, + is_accessor, ); self.storage.replace_active_anonymous_producer(previous); let ( @@ -1750,6 +1761,7 @@ impl<'h, H: OrdinaryBodyAnalysisHost> OrdinaryBodyEngine<'h, H> { self_mode, self_is_mut, true, + false, ) } @@ -1797,6 +1809,7 @@ impl<'h, H: OrdinaryBodyAnalysisHost> OrdinaryBodyEngine<'h, H> { true, false, false, + false, ); self.storage.replace_active_anonymous_producer(previous); let ( @@ -1867,6 +1880,7 @@ impl<'h, H: OrdinaryBodyAnalysisHost> OrdinaryBodyEngine<'h, H> { false, allow_unused_variable, false, + false, ) } @@ -1899,6 +1913,7 @@ impl<'h, H: OrdinaryBodyAnalysisHost> OrdinaryBodyEngine<'h, H> { false, false, false, + false, ) } @@ -1913,6 +1928,7 @@ impl<'h, H: OrdinaryBodyAnalysisHost> OrdinaryBodyEngine<'h, H> { is_destructor: bool, allow_unused_variable: bool, self_is_mut: bool, + is_accessor: bool, ) -> CompileResult<( Air, u32, @@ -2111,9 +2127,47 @@ impl<'h, H: OrdinaryBodyAnalysisHost> OrdinaryBodyEngine<'h, H> { in_loop_move_recheck: false, iter_borrows: Vec::new(), expected_type: None, + infer_ctx, + accessor_trailing_yield: None, + accessor_call_insts: HashMap::new(), + expression_loans: Vec::new(), + inline_resolved_types: Vec::new(), + place_aliases: HashMap::new(), try_operand: false, }; + // Accessor body shape (ADR-0062 phase 1): the body block must end in + // a single trailing `yield`, and only that instruction may be a + // `yield` — every other non-diverging exit is E0254, enforced by the + // per-instruction `yield`/`return`/`?` analysis against the trailing + // reference recorded here. + if is_accessor { + // A single-statement body lowers to the instruction itself; a + // multi-statement body lowers to a block whose last instruction + // is the trailing exit. + let trailing = match &self.body_rir_ref().get(body).data { + rue_rir::InstData::Block { instructions } => self + .body_rir_ref() + .block_insts(instructions) + .values() + .last(), + _ => Some(body), + }; + let trailing_yield = trailing.filter(|inst_ref| { + matches!( + self.body_rir_ref().get(*inst_ref).data, + rue_rir::InstData::Yield(_) + ) + }); + let Some(trailing_yield) = trailing_yield else { + return Err(CompileError::new( + ErrorKind::AccessorBodyMissingYield, + self.body_rir_ref().get(body).span, + )); + }; + ctx.accessor_trailing_yield = Some(trailing_yield); + } + // ====================================================================== // Phase 3: AIR Emission // ====================================================================== @@ -2173,6 +2227,17 @@ impl<'h, H: OrdinaryBodyAnalysisHost> OrdinaryBodyEngine<'h, H> { // Add implicit return only if body doesn't already diverge (e.g., explicit return) if body_result.ty != Type::NEVER { + // An accessor result cannot be the function's tail value: the + // borrowed place is scoped to its full expression (ADR-0062). + { + let tail = self.rir_block_tail_expr(body); + self.reject_accessor_result_escape( + tail, + super::analysis::AccessorEscapeSite::Return, + self.body_rir_ref().get(tail).span, + &ctx, + )?; + } // Two-types model (ADR-0043, RUE-386): a `str`-returning function's // implicit-return (tail) value must be a first-class `str`. A buffer // (`StrBuf`/`Str(N)`) or a borrowed `str` view escaping here dangles diff --git a/crates/rue-air/src/sema/provider_body_host.rs b/crates/rue-air/src/sema/provider_body_host.rs index 4816311f1..e83b8cdc6 100644 --- a/crates/rue-air/src/sema/provider_body_host.rs +++ b/crates/rue-air/src/sema/provider_body_host.rs @@ -1152,7 +1152,18 @@ where (None, Some(key)) => (key, false), _ => return None, }; - let info = self.calls.method_signature_info(&key)?; + // The signature-only durable subset cannot carry the `-> borrow T` + // accessor flag (ADR-0062); recover it from the owning struct's + // request-local RIR declaration when that declaration is present. + // (Provider body requests currently prune type declarations from + // their RIR, so this recovery only fires on hosts whose request + // carries the owner — see the RUE-662 provider-path limitation.) + let mut info = self.calls.method_signature_info(&key)?; + if let Some(method_ref) = self.rir_struct_method_decl(struct_id, symbol) + && let InstData::FnDecl { returns_borrow, .. } = &self.rir.rir().get(method_ref).data + { + info.returns_borrow = *returns_borrow; + } let callable_owner = self.type_pool.struct_symbol_name(struct_id); let full_name = if has_self { format!("{callable_owner}.{name}") @@ -1180,6 +1191,52 @@ where Some(info) } + /// Locate a named method's `FnDecl` by walking the request-local RIR's + /// struct declarations. The provider's request RIR carries the owning + /// `StructDecl` (types are part of every consumer's inputs) even when the + /// method's own body query is a different request, so this is the one + /// resolution path that can recover declaration-level facts — like the + /// `-> borrow T` accessor flag (ADR-0062) — that the durable signature + /// subset does not carry. + fn rir_struct_method_decl(&self, struct_id: StructId, method: Spur) -> Option { + let struct_def = self.type_pool.struct_def(struct_id); + // The RIR names structs by their source name; qualify by declaring + // file below so same-named structs in sibling files cannot collide. + // Translate lookups through strings so a distinct semantic interner + // cannot skew the Spurs. + let owner_file = struct_def.file_id; + // A cross-file-unique pool name is `Source$escaped_file`; the RIR + // declaration carries the bare source name ('$' cannot appear in a + // source identifier). + let source_name = struct_def + .name + .split('$') + .next() + .expect("split yields at least one segment"); + let owner_sym = self.rir.rir_interner().get(source_name)?; + let method_sym = self + .rir + .rir_interner() + .get(self.interner.resolve(&method))?; + let rir = self.rir.rir(); + for index in 0..rir.len() { + let inst_ref = InstRef::from_raw(index as u32); + if let InstData::StructDecl { name, methods, .. } = &rir.get(inst_ref).data + && *name == owner_sym + && rir.get(inst_ref).span.file_id == owner_file + { + for method_ref in rir.struct_methods(methods) { + if let InstData::FnDecl { name, .. } = &rir.get(method_ref).data + && *name == method_sym + { + return Some(method_ref); + } + } + } + } + None + } + fn named_method_definition(&self, struct_id: StructId, symbol: Spur) -> Option { if self .anonymous_methods @@ -2046,6 +2103,9 @@ where self_mode: method.self_mode, params, return_type, + // Anonymous-struct methods cannot be accessors (ADR-0062 + // phase 1 rejects them at declaration). + returns_borrow: false, }, )); signatures.push(super::AnonMethodSig { @@ -2125,6 +2185,9 @@ where struct_type: owner_type, has_self: method.has_self, self_mode: method.self_mode, + // Anonymous-struct methods cannot be accessors (ADR-0062 + // phase 1 rejects them at declaration). + returns_borrow: false, params: self.state.allocate_params( (0..method.parameters.len()) .map(|index| self.interner.get_or_intern(&format!("arg{index}"))), @@ -2308,6 +2371,29 @@ 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, @@ -4107,7 +4193,7 @@ where name.to_owned(), )) })?; - let (params, return_type, body, has_self, self_mode, self_is_mut) = + let (params, return_type, body, has_self, self_mode, self_is_mut, returns_borrow) = match &host.rir.rir().get(declaration).data { InstData::FnDecl { params, @@ -4116,6 +4202,7 @@ where has_self, self_mode, self_is_mut, + returns_borrow, .. } => ( params.clone(), @@ -4124,6 +4211,7 @@ where *has_self, *self_mode, *self_is_mut, + *returns_borrow, ), _ => unreachable!("registered provider member points at FnDecl"), }; @@ -4170,6 +4258,7 @@ where has_self, self_mode, self_is_mut, + returns_borrow, )?, body_span, ) diff --git a/crates/rue-air/src/sema/tests.rs b/crates/rue-air/src/sema/tests.rs index f75ac81ad..08335e68c 100644 --- a/crates/rue-air/src/sema/tests.rs +++ b/crates/rue-air/src/sema/tests.rs @@ -3729,4 +3729,343 @@ fn main() -> i32 { .is_some() ); } + + // ======================================================================== + // Place-returning borrow accessors (ADR-0062, RUE-662) + // ======================================================================== + + fn compile_with_accessors(source: &str) -> MultiErrorResult { + let mut features = PreviewFeatures::new(); + features.insert(PreviewFeature::BorrowAccessors); + compile_to_air_with_preview_features(source, features) + } + + const GRID_ACCESSOR: &str = " +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]; + } +} +"; + + #[test] + fn accessor_call_inlines_with_no_call_shape() { + // ADR-0062 §3: a call `g.at(2)` compiles by inlining — guards plus + // the yielded place — so the caller's AIR must contain NO call to the + // accessor; the read is an ordinary projected `PlaceRead`. + let source = format!( + "{GRID_ACCESSOR} +fn main() -> i32 {{ + let g = Grid {{ cells: [10, 20, 30, 40] }}; + if g.at(2) == 30 {{ 0 }} else {{ 1 }} +}}" + ); + let output = compile_with_accessors(&source).expect("accessor call compiles"); + let main = output + .functions + .iter() + .find(|function| function.name == "main") + .expect("main is analyzed"); + let calls_accessor = main.air.iter().any(|(_, inst)| { + matches!(&inst.data, AirInstData::Call { name, .. } + if output.strings.is_empty() || { + // Call names are interned; compare through the printer-safe path. + let _ = name; + false + }) + }); + assert!( + !calls_accessor, + "an accessor call must not lower to an AIR call" + ); + // The inlined result place is read: main contains a PlaceRead with an + // index projection, which an ordinary method call would never emit. + let has_place_read = main + .air + .iter() + .any(|(_, inst)| matches!(inst.data, AirInstData::PlaceRead { .. })); + assert!(has_place_read, "the yielded place is read in the caller"); + // And no Call instruction at all exists in main (the only callee in + // this program is the accessor). + let has_any_call = main + .air + .iter() + .any(|(_, inst)| matches!(inst.data, AirInstData::Call { .. })); + assert!( + !has_any_call, + "mandatory inlining leaves no call in the caller" + ); + } + + #[test] + fn accessor_declaration_requires_preview_gate() { + let source = format!("{GRID_ACCESSOR}\nfn main() -> i32 {{ 0 }}"); + let errors = compile_to_air(&source).expect_err("the gate is off"); + assert!(errors.iter().any(|error| matches!( + &error.kind, + ErrorKind::PreviewFeatureRequired { feature, .. } + if *feature == PreviewFeature::BorrowAccessors + ))); + } + + #[test] + fn accessor_result_cannot_be_returned() { + let source = format!( + "{GRID_ACCESSOR} +fn read(borrow g: Grid) -> i64 {{ + return g.at(0); +}} +fn main() -> i32 {{ + let g = Grid {{ cells: [1, 2, 3, 4] }}; + read(borrow g); + 0 +}}" + ); + let errors = compile_with_accessors(&source).expect_err("return escape"); + assert!( + errors + .iter() + .any(|error| matches!(&error.kind, ErrorKind::AccessorResultReturned { .. })) + ); + } + + #[test] + fn accessor_result_cannot_be_tail_returned() { + let source = format!( + "{GRID_ACCESSOR} +fn read(borrow g: Grid) -> i64 {{ + g.at(0) +}} +fn main() -> i32 {{ + let g = Grid {{ cells: [1, 2, 3, 4] }}; + read(borrow g); + 0 +}}" + ); + let errors = compile_with_accessors(&source).expect_err("tail-return escape"); + assert!( + errors + .iter() + .any(|error| matches!(&error.kind, ErrorKind::AccessorResultReturned { .. })) + ); + } + + #[test] + fn accessor_result_cannot_be_let_bound() { + let source = format!( + "{GRID_ACCESSOR} +fn main() -> i32 {{ + let g = Grid {{ cells: [1, 2, 3, 4] }}; + let b = g.at(0); + 0 +}}" + ); + let errors = compile_with_accessors(&source).expect_err("let escape"); + assert!( + errors + .iter() + .any(|error| matches!(&error.kind, ErrorKind::AccessorResultBound { .. })) + ); + } + + #[test] + fn accessor_result_cannot_be_stored() { + let source = format!( + "{GRID_ACCESSOR} +fn main() -> i32 {{ + let g = Grid {{ cells: [1, 2, 3, 4] }}; + let mut x = 0; + x = g.at(0); + 0 +}}" + ); + let errors = compile_with_accessors(&source).expect_err("store escape"); + assert!( + errors + .iter() + .any(|error| matches!(&error.kind, ErrorKind::AccessorResultStored { .. })) + ); + } + + #[test] + fn accessor_result_cannot_be_captured_in_aggregate() { + let source = format!( + "{GRID_ACCESSOR} +fn main() -> i32 {{ + let g = Grid {{ cells: [1, 2, 3, 4] }}; + let a = [g.at(0)]; + 0 +}}" + ); + let errors = compile_with_accessors(&source).expect_err("capture escape"); + assert!( + errors + .iter() + .any(|error| matches!(&error.kind, ErrorKind::AccessorResultCaptured { .. })) + ); + } + + #[test] + fn accessor_loan_conflicts_with_inout_in_same_expression() { + // The (Accessor-Call) loan spans the enclosing full expression: + // `use(v.at(0), bump(inout v))` overlaps a shared accessor loan with + // an exclusive `inout` loan on the same root (ADR-0062 §2). + let source = format!( + "{GRID_ACCESSOR} +fn using(a: i64, b: i64) -> i64 {{ a + b }} +fn bump(inout g: Grid) -> i64 {{ g.cells[0] = 9; 0 }} +fn main() -> i32 {{ + let mut g = Grid {{ cells: [1, 2, 3, 4] }}; + using(g.at(0), bump(inout g)); + 0 +}}" + ); + let errors = compile_with_accessors(&source).expect_err("exclusivity conflict"); + assert!( + errors + .iter() + .any(|error| matches!(&error.kind, ErrorKind::AccessorLoanConflict { .. })) + ); + } + + #[test] + fn accessor_body_requires_trailing_yield() { + let source = " +struct P { + x: i64, + + fn xr(borrow self) -> borrow i64 { + self.x + } +} +fn main() -> i32 { + let p = P { x: 1 }; + if p.xr() == 1 { 0 } else { 1 } +}"; + let errors = compile_with_accessors(source).expect_err("missing yield"); + assert!( + errors + .iter() + .any(|error| matches!(&error.kind, ErrorKind::AccessorBodyMissingYield)) + ); + } + + #[test] + fn accessor_yield_must_root_at_receiver() { + let source = " +struct P { + x: i64, + + fn xr(borrow self, other: i64) -> borrow i64 { + yield other; + } +} +fn main() -> i32 { + let p = P { x: 1 }; + if p.xr(2) == 1 { 0 } else { 1 } +}"; + let errors = compile_with_accessors(source).expect_err("non-receiver yield"); + assert!(errors.iter().any(|error| matches!( + &error.kind, + ErrorKind::AccessorYieldNotReceiverRooted { .. } + ))); + } + + #[test] + fn yield_outside_accessor_is_rejected() { + let errors = + compile_with_accessors("fn main() -> i32 { yield 1; }").expect_err("stray yield"); + assert!( + errors + .iter() + .any(|error| matches!(&error.kind, ErrorKind::YieldOutsideAccessor)) + ); + } + + #[test] + fn accessor_requires_borrow_self_receiver() { + let source = " +struct P { + x: i64, + + fn xr(inout self) -> borrow i64 { + yield self.x; + } +} +fn main() -> i32 { 0 }"; + let errors = compile_with_accessors(source).expect_err("inout self accessor"); + assert!( + errors + .iter() + .any(|error| matches!(&error.kind, ErrorKind::AccessorRequiresBorrowSelf { .. })) + ); + } + + #[test] + fn free_function_cannot_be_an_accessor() { + let source = " +fn first(borrow v: i64) -> borrow i64 { + yield v; +} +fn main() -> i32 { 0 }"; + let errors = compile_with_accessors(source).expect_err("free-fn accessor"); + assert!( + errors + .iter() + .any(|error| matches!(&error.kind, ErrorKind::AccessorRequiresBorrowSelf { .. })) + ); + } + + #[test] + fn accessor_params_must_be_by_value() { + let source = " +struct P { + x: i64, + + fn xr(borrow self, borrow k: i64) -> borrow i64 { + yield self.x; + } +} +fn main() -> i32 { 0 }"; + let errors = compile_with_accessors(source).expect_err("borrow accessor param"); + assert!( + errors + .iter() + .any(|error| matches!(&error.kind, ErrorKind::AccessorParamModeUnsupported { .. })) + ); + } + + #[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. + let source = format!( + "{GRID_ACCESSOR} +fn main() -> i32 {{ + let g = Grid {{ cells: [10, 20, 30, 40] }}; + if g.at(3) == 40 {{ 0 }} else {{ 1 }} +}}" + ); + let output = compile_with_accessors(&source).expect("accessor call compiles"); + let main = output + .functions + .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"); + } } diff --git a/crates/rue-compiler/src/artifact_views.rs b/crates/rue-compiler/src/artifact_views.rs index 3f02004ca..78721dfd9 100644 --- a/crates/rue-compiler/src/artifact_views.rs +++ b/crates/rue-compiler/src/artifact_views.rs @@ -178,6 +178,7 @@ impl TokenView { Match => "MATCH", While => "WHILE", Loop => "LOOP", + Yield => "YIELD", For => "FOR", In => "IN", Break => "BREAK", @@ -1666,6 +1667,13 @@ fn expr_record( .map(|value| expr_record(owner, value)) .collect(), ), + Expr::Yield(yield_expression) => syntax_record( + "yield", + span, + None, + None, + vec![expr_record(owner, &yield_expression.value)], + ), Expr::StructLit(literal) => { let mut children = literal .base @@ -2032,6 +2040,7 @@ fn rir_kind(data: &rue_rir::InstData) -> &'static str { TypeIntrinsic { .. } => "type_intrinsic", OffsetOf { .. } => "offset_of", Ret(_) => "return", + Yield(_) => "yield", Block { .. } => "block", Alloc { .. } => "allocate", VarRef { .. } => "variable_reference", @@ -2127,6 +2136,7 @@ fn rir_operands(rir: &rue_rir::Rir, data: &rue_rir::InstData) -> Vec push("value", *value), FnDecl { body, .. } | DropFnDecl { body, .. } => push("body", *body), ConstDecl { init, .. } | Alloc { init, .. } => push("initializer", *init), Assign { value, .. } => push("value", *value), diff --git a/crates/rue-compiler/src/parsed_modules.rs b/crates/rue-compiler/src/parsed_modules.rs index df3181370..26bcd421c 100644 --- a/crates/rue-compiler/src/parsed_modules.rs +++ b/crates/rue-compiler/src/parsed_modules.rs @@ -1675,6 +1675,7 @@ fn walk_expr( walk_expr(value, module, resolver, imports)?; } } + Expr::Yield(value) => walk_expr(&value.value, module, resolver, imports)?, Expr::StructLit(value) => { if let Some(base) = &value.base { walk_expr(base, module, resolver, imports)?; diff --git a/crates/rue-error/src/lib.rs b/crates/rue-error/src/lib.rs index 7ce4ef81a..7398489cb 100644 --- a/crates/rue-error/src/lib.rs +++ b/crates/rue-error/src/lib.rs @@ -129,6 +129,62 @@ impl ErrorCode { /// arguments and consumes either its status code or the unit value, so a /// different source signature would violate the entry ABI. pub const INVALID_MAIN_SIGNATURE: Self = Self(211); + // E0250-E0260 form the borrow-accessor block (ADR-0062, RUE-662). The + // ownership/borrow family's E04xx band is at its ceiling (E0499), so + // accessor diagnostics live here in the semantic band instead. + /// An accessor result (`v.get_ref(i)`) was returned from the enclosing + /// function. The result is a second-class borrowed place scoped to the + /// enclosing full expression (ADR-0062); returning it would let the loan + /// outlive the receiver access that justifies it. + pub const ACCESSOR_RESULT_RETURNED: Self = Self(250); + /// An accessor result was stored — assigned to a variable, field, or + /// element. The result is a second-class borrowed place (ADR-0062); a + /// stored copy would be a stored borrow, which Rue does not have. + pub const ACCESSOR_RESULT_STORED: Self = Self(251); + /// An accessor result was bound by a plain `let`. The result's extent is + /// the enclosing full expression (ADR-0062), so a binding would outlive + /// the loan. Use the result directly within one expression instead. + pub const ACCESSOR_RESULT_BOUND: Self = Self(252); + /// An accessor result was captured into an aggregate (struct literal or + /// array literal). The result is a second-class borrowed place + /// (ADR-0062); an aggregate member holding it would be a stored borrow. + pub const ACCESSOR_RESULT_CAPTURED: Self = Self(253); + /// An accessor body's non-diverging control flow does not end in the + /// single trailing `yield` (ADR-0062 phase 1): the final statement is not + /// a `yield`, a `yield` appears before the end, or the body contains a + /// `return`/`?` exit. Guards before the yield may only diverge (trap, + /// `@panic`) or fall through. + pub const ACCESSOR_BODY_MISSING_YIELD: Self = Self(254); + /// The operand of an accessor's `yield` is not a place rooted at the + /// receiver parameter (`self`). An accessor hands out a projection of its + /// receiver (ADR-0062); yielding a local, temporary, or unrelated place + /// would dangle once the accessor's frame is gone. + pub const ACCESSOR_YIELD_NOT_RECEIVER_ROOTED: Self = Self(255); + /// A `yield` expression appears outside the body of a `-> borrow T` + /// accessor. `yield` is the accessor body's exit form (ADR-0062). + pub const YIELD_OUTSIDE_ACCESSOR: Self = Self(256); + /// A `-> borrow T` result on a declaration that is not a `borrow self` + /// method: a free function, an associated function, or a method with a + /// by-value or `mut self` receiver. Phase 1 accessors are read-only + /// projections of a shared receiver borrow (ADR-0062); `inout self` + /// accessors are the phase-2 follow-up (RUE-1016). + pub const ACCESSOR_REQUIRES_BORROW_SELF: Self = Self(257); + /// A value with drop glue was read out of an accessor result by value. + /// The result is a borrowed place, not an owner (ADR-0062); copying a + /// drop-glue value out of it would mint an aliasing second owner — the + /// same double-free the E0711 gate closes (RUE-651). Only trivially + /// droppable element values may be read out by value. + pub const ACCESSOR_RESULT_MOVED: Self = Self(258); + /// A root was used exclusively (`inout`, mutation, or move) in the same + /// full expression in which an accessor result borrows it. The accessor + /// result's shared loan spans the enclosing full expression (ADR-0062), + /// so the exclusive use violates the law of exclusivity. + pub const ACCESSOR_LOAN_CONFLICT: Self = Self(259); + /// An accessor declares a parameter mode other than by-value (`borrow`, + /// `inout`, or `comptime` on a non-receiver parameter). Phase 1 accessor + /// arguments are by-value guard inputs (ADR-0062); by-ref accessor + /// parameters are deferred with the coroutine form (RUE-1012). + pub const ACCESSOR_PARAM_MODE_UNSUPPORTED: Self = Self(260); // ======================================================================== // Struct/enum errors (E0400-E0499) @@ -555,6 +611,11 @@ pub enum PreviewFeature { /// linking (ADR-0064, RUE-1055). Gated until every phase of the guaranteed /// target-C boundary is proven on both backends. CFfi, + /// Place-returning borrow accessors (ADR-0062, RUE-662): methods with a + /// `-> borrow T` result whose `yield`-body hands out a second-class borrow + /// of a projection of the receiver. Gated until mutable accessors and std + /// adoption complete the rollout (RUE-1015). + BorrowAccessors, } /// Error returned when parsing a preview feature name fails. @@ -577,6 +638,7 @@ impl PreviewFeature { PreviewFeature::TestInfra => "test_infra", PreviewFeature::Slices => "slices", PreviewFeature::CFfi => "c_ffi", + PreviewFeature::BorrowAccessors => "borrow_accessors", } } @@ -587,6 +649,7 @@ impl PreviewFeature { PreviewFeature::TestInfra => "ADR-0005", PreviewFeature::Slices => "ADR-0043", PreviewFeature::CFfi => "ADR-0064", + PreviewFeature::BorrowAccessors => "ADR-0062", } } @@ -596,6 +659,7 @@ impl PreviewFeature { PreviewFeature::TestInfra, PreviewFeature::Slices, PreviewFeature::CFfi, + PreviewFeature::BorrowAccessors, ] } @@ -621,6 +685,7 @@ impl std::str::FromStr for PreviewFeature { "test_infra" => Ok(PreviewFeature::TestInfra), "slices" => Ok(PreviewFeature::Slices), "c_ffi" => Ok(PreviewFeature::CFfi), + "borrow_accessors" => Ok(PreviewFeature::BorrowAccessors), _ => Err(ParsePreviewFeatureError(s.to_string())), } } @@ -1421,6 +1486,63 @@ pub enum ErrorKind { /// variable moved-from) #[error("cannot move out of inout parameter '{variable}'")] MoveOutOfInout { variable: String }, + /// An accessor result was returned from the enclosing function + /// (ADR-0062: the borrowed place is scoped to its full expression). + #[error( + "cannot return an accessor result: `{method}` yields a second-class borrow of `{root}`, valid only within the calling expression" + )] + AccessorResultReturned { method: String, root: String }, + /// An accessor result was assigned into a variable, field, or element. + #[error( + "cannot store an accessor result: `{method}` yields a second-class borrow of `{root}`, valid only within the calling expression" + )] + AccessorResultStored { method: String, root: String }, + /// An accessor result was bound by a plain `let`. + #[error( + "cannot bind an accessor result with `let`: `{method}` yields a second-class borrow of `{root}`, valid only within the calling expression" + )] + AccessorResultBound { method: String, root: String }, + /// An accessor result was captured into a struct or array literal. + #[error( + "cannot capture an accessor result in an aggregate: `{method}` yields a second-class borrow of `{root}`, valid only within the calling expression" + )] + AccessorResultCaptured { method: String, root: String }, + /// An accessor body's non-diverging control flow does not end in the + /// single trailing `yield` (ADR-0062 phase 1). + #[error( + "an accessor body must end in a single trailing `yield`: every non-diverging path must fall through to it, and no code may follow it" + )] + AccessorBodyMissingYield, + /// The operand of an accessor's `yield` is not a place rooted at the + /// receiver parameter. + #[error("an accessor must yield a place rooted at `self`, not {found}")] + AccessorYieldNotReceiverRooted { found: String }, + /// A `yield` expression outside an accessor body. + #[error("`yield` is only valid inside the body of a `-> borrow` accessor")] + YieldOutsideAccessor, + /// A `-> borrow T` result on a declaration that is not a `borrow self` + /// method. + #[error("a `-> borrow` accessor requires a `borrow self` receiver, but this is {found}")] + AccessorRequiresBorrowSelf { found: String }, + /// A drop-glue value read out of an accessor result by value. + #[error( + "cannot copy a value of type `{ty}` out of an accessor result: it owns resources (has drop glue), and the result is a borrow, not an owner" + )] + AccessorResultMoved { ty: String }, + /// Exclusive use of a root while an accessor result borrows it in the + /// same full expression. + #[error( + "cannot use '{variable}' {conflict} while an accessor result borrows it in the same expression" + )] + AccessorLoanConflict { + variable: String, + conflict: &'static str, + }, + /// An accessor parameter with a non-by-value mode. + #[error( + "an accessor parameter must be by-value: `{mode}` accessor parameters are not supported" + )] + AccessorParamModeUnsupported { mode: String }, /// Cannot move `self` out of a destructor body (RUE-139). The compiler /// drops a value by running its destructor and THEN dropping its fields; /// moving `self` to a new owner (a call argument, another binding, ...) @@ -1869,6 +1991,23 @@ impl ErrorKind { ErrorKind::MoveOutOfBorrow { .. } => ErrorCode::MOVE_OUT_OF_BORROW, ErrorKind::BorrowInoutConflict { .. } => ErrorCode::BORROW_INOUT_CONFLICT, ErrorKind::MoveWhileCallLoaned { .. } => ErrorCode::MOVE_WHILE_CALL_LOANED, + ErrorKind::AccessorResultReturned { .. } => ErrorCode::ACCESSOR_RESULT_RETURNED, + ErrorKind::AccessorResultStored { .. } => ErrorCode::ACCESSOR_RESULT_STORED, + ErrorKind::AccessorResultBound { .. } => ErrorCode::ACCESSOR_RESULT_BOUND, + ErrorKind::AccessorResultCaptured { .. } => ErrorCode::ACCESSOR_RESULT_CAPTURED, + ErrorKind::AccessorBodyMissingYield => ErrorCode::ACCESSOR_BODY_MISSING_YIELD, + ErrorKind::AccessorYieldNotReceiverRooted { .. } => { + ErrorCode::ACCESSOR_YIELD_NOT_RECEIVER_ROOTED + } + ErrorKind::YieldOutsideAccessor => ErrorCode::YIELD_OUTSIDE_ACCESSOR, + ErrorKind::AccessorRequiresBorrowSelf { .. } => { + ErrorCode::ACCESSOR_REQUIRES_BORROW_SELF + } + ErrorKind::AccessorResultMoved { .. } => ErrorCode::ACCESSOR_RESULT_MOVED, + ErrorKind::AccessorLoanConflict { .. } => ErrorCode::ACCESSOR_LOAN_CONFLICT, + ErrorKind::AccessorParamModeUnsupported { .. } => { + ErrorCode::ACCESSOR_PARAM_MODE_UNSUPPORTED + } ErrorKind::InoutKeywordMissing => ErrorCode::INOUT_KEYWORD_MISSING, ErrorKind::BorrowKeywordMissing => ErrorCode::BORROW_KEYWORD_MISSING, ErrorKind::UnexpectedCallArgumentMode { .. } => { @@ -2785,7 +2924,7 @@ mod tests { #[test] fn test_preview_feature_all_names() { let names = PreviewFeature::all_names(); - assert_eq!(names, "test_infra, slices, c_ffi"); + assert_eq!(names, "test_infra, slices, c_ffi, borrow_accessors"); } #[test] diff --git a/crates/rue-frontend-diff/src/main.rs b/crates/rue-frontend-diff/src/main.rs index c9af42c10..b5c16acd7 100644 --- a/crates/rue-frontend-diff/src/main.rs +++ b/crates/rue-frontend-diff/src/main.rs @@ -542,6 +542,14 @@ impl Shapes<'_> { "_".into(), "_".into(), ), + Expr::Yield(v) => node( + "yield", + "", + self.expr(&v.value), + "_".into(), + "_".into(), + "_".into(), + ), Expr::StructLit(v) => { let mut head = v.base.as_ref().map_or_else( || { diff --git a/crates/rue-lexer/src/lib.rs b/crates/rue-lexer/src/lib.rs index ed3f5628e..7bc569189 100644 --- a/crates/rue-lexer/src/lib.rs +++ b/crates/rue-lexer/src/lib.rs @@ -44,6 +44,7 @@ pub enum TokenKind { Break, Continue, Return, + Yield, True, False, Struct, @@ -160,6 +161,7 @@ impl TokenKind { TokenKind::Break => "'break'", TokenKind::Continue => "'continue'", TokenKind::Return => "'return'", + TokenKind::Yield => "'yield'", TokenKind::True => "'true'", TokenKind::False => "'false'", TokenKind::Struct => "'struct'", @@ -276,6 +278,7 @@ impl std::fmt::Display for TokenKind { TokenKind::Break => write!(f, "BREAK"), TokenKind::Continue => write!(f, "CONTINUE"), TokenKind::Return => write!(f, "RETURN"), + TokenKind::Yield => write!(f, "YIELD"), TokenKind::True => write!(f, "TRUE"), TokenKind::False => write!(f, "FALSE"), TokenKind::Struct => write!(f, "STRUCT"), diff --git a/crates/rue-lexer/src/logos_lexer.rs b/crates/rue-lexer/src/logos_lexer.rs index 638ee766d..65cdcfed1 100644 --- a/crates/rue-lexer/src/logos_lexer.rs +++ b/crates/rue-lexer/src/logos_lexer.rs @@ -430,6 +430,8 @@ pub enum LogosTokenKind { Continue, #[token("return")] Return, + #[token("yield")] + Yield, #[token("true")] True, #[token("false")] @@ -647,6 +649,7 @@ impl From for TokenKind { LogosTokenKind::Break => TokenKind::Break, LogosTokenKind::Continue => TokenKind::Continue, LogosTokenKind::Return => TokenKind::Return, + LogosTokenKind::Yield => TokenKind::Yield, LogosTokenKind::True => TokenKind::True, LogosTokenKind::False => TokenKind::False, LogosTokenKind::Struct => TokenKind::Struct, diff --git a/crates/rue-parser/src/ast.rs b/crates/rue-parser/src/ast.rs index 07b215994..24016f229 100644 --- a/crates/rue-parser/src/ast.rs +++ b/crates/rue-parser/src/ast.rs @@ -223,6 +223,10 @@ pub struct Method { pub params: Vec, /// Return type (None means implicit unit `()`) pub return_type: Option, + /// When the result position is `-> borrow T`, the span of the `borrow` + /// keyword: the method is a place-returning accessor (ADR-0062) whose + /// body yields a second-class borrow of a receiver projection. + pub borrow_return: Option, /// Method body pub body: Expr, /// Span covering the entire method @@ -274,6 +278,11 @@ pub struct Function { pub params: Vec, /// Return type (None means implicit unit `()`) pub return_type: Option, + /// When the result position is `-> borrow T`, the span of the `borrow` + /// keyword (ADR-0062). Always rejected in sema for free functions — + /// accessors require a `borrow self` receiver — but parsed here so the + /// diagnostic can be semantic rather than a parse error. + pub borrow_return: Option, /// Function body pub body: Expr, /// The C ABI string when this function is a `pub extern "C" fn` export @@ -685,6 +694,9 @@ pub enum Expr { Continue(ContinueExpr), /// Return statement (returns a value from the current function) Return(ReturnExpr), + /// Yield statement (hands out a receiver projection from an accessor + /// body, ADR-0062) + Yield(YieldExpr), /// Struct literal (e.g., `Point { x: 1, y: 2 }`) StructLit(StructLitExpr), /// Field access (e.g., `point.x`) @@ -1264,6 +1276,16 @@ pub struct ReturnExpr { pub span: Span, } +/// A yield expression: the exit form of a `-> borrow T` accessor body +/// (ADR-0062). Its operand is the place the accessor hands out; unlike +/// `return` the operand is mandatory. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct YieldExpr { + /// The place expression the accessor yields. + pub value: Box, + pub span: Span, +} + /// A self expression (the `self` keyword in method bodies). #[derive(Debug, Clone, PartialEq, Eq)] pub struct SelfExpr { @@ -1321,6 +1343,7 @@ impl Expr { Expr::Break(break_expr) => break_expr.span, Expr::Continue(continue_expr) => continue_expr.span, Expr::Return(return_expr) => return_expr.span, + Expr::Yield(yield_expr) => yield_expr.span, Expr::StructLit(struct_lit) => struct_lit.span, Expr::Field(field_expr) => field_expr.span, Expr::MethodCall(method_call) => method_call.span, @@ -1659,6 +1682,10 @@ fn rebind_expr(expr: &mut Expr, file_id: FileId) { } rebind_span(&mut return_expr.span, file_id); } + Expr::Yield(yield_expr) => { + rebind_expr(&mut yield_expr.value, file_id); + rebind_span(&mut yield_expr.span, file_id); + } Expr::StructLit(literal) => { if let Some(base) = &mut literal.base { rebind_expr(base, file_id); @@ -2090,6 +2117,10 @@ fn fmt_expr(f: &mut fmt::Formatter<'_>, expr: &Expr, level: usize) -> fmt::Resul writeln!(f, "Return (unit)") } } + Expr::Yield(yield_expr) => { + writeln!(f, "Yield")?; + fmt_expr(f, &yield_expr.value, level + 1) + } Expr::StructLit(lit) => { writeln!(f, "StructLit sym:{}", lit.name.name.into_usize())?; for field in &lit.fields { diff --git a/crates/rue-parser/src/parser/declarations.rs b/crates/rue-parser/src/parser/declarations.rs index ffa17c62e..413608d29 100644 --- a/crates/rue-parser/src/parser/declarations.rs +++ b/crates/rue-parser/src/parser/declarations.rs @@ -121,11 +121,7 @@ impl Parser { self.expect(TokenKind::Fn)?; let name = self.ident()?; let params = self.params()?; - let return_type = if self.eat(TokenKind::Arrow) { - Some(self.ty()?) - } else { - None - }; + let (return_type, borrow_return) = self.return_type_with_borrow()?; let body = Expr::Block(self.block()?); Ok(Function { directives, @@ -134,12 +130,28 @@ impl Parser { name, params, return_type, + borrow_return, body, export_abi, span: self.span_from(start), }) } + /// Parse an optional `-> [borrow] type` result position. A leading + /// `borrow` marks a place-returning accessor (ADR-0062); the keyword's + /// span is carried so semantic analysis can gate and diagnose the form. + pub(super) fn return_type_with_borrow(&mut self) -> PResult<(Option, Option)> { + if !self.eat(TokenKind::Arrow) { + return Ok((None, None)); + } + let borrow_return = if self.at(TokenKind::Borrow) { + Some(self.bump().span) + } else { + None + }; + Ok((Some(self.ty()?), borrow_return)) + } + /// Parse a foreign-declaration block: `extern "C" { fn name(...) -> T; }`. /// /// The ABI string is captured verbatim (validated in semantic analysis so diff --git a/crates/rue-parser/src/parser/expressions.rs b/crates/rue-parser/src/parser/expressions.rs index 790e31a45..ff9e53a29 100644 --- a/crates/rue-parser/src/parser/expressions.rs +++ b/crates/rue-parser/src/parser/expressions.rs @@ -161,6 +161,16 @@ impl Parser { span: self.span_from(start), })) } + TokenKind::Yield => { + self.bump(); + // Unlike `return`, the operand is mandatory: an accessor + // always hands out a place (ADR-0062). + let value = Box::new(self.expr()?); + Ok(Expr::Yield(YieldExpr { + value, + span: self.span_from(start), + })) + } TokenKind::Comptime => { self.bump(); let inner = Expr::Block(self.block()?); diff --git a/crates/rue-parser/src/parser/statements.rs b/crates/rue-parser/src/parser/statements.rs index 79a40cccd..524113138 100644 --- a/crates/rue-parser/src/parser/statements.rs +++ b/crates/rue-parser/src/parser/statements.rs @@ -554,13 +554,14 @@ fn is_control_flow(expr: &Expr) -> bool { | Expr::Break(_) | Expr::Continue(_) | Expr::Return(_) + | Expr::Yield(_) | Expr::Block(_) ) } fn is_diverging(expr: &Expr) -> bool { matches!( expr, - Expr::Break(_) | Expr::Continue(_) | Expr::Return(_) | Expr::Loop(_) + Expr::Break(_) | Expr::Continue(_) | Expr::Return(_) | Expr::Yield(_) | Expr::Loop(_) ) } diff --git a/crates/rue-parser/src/parser/types.rs b/crates/rue-parser/src/parser/types.rs index 87a59563e..25dfcf419 100644 --- a/crates/rue-parser/src/parser/types.rs +++ b/crates/rue-parser/src/parser/types.rs @@ -289,11 +289,7 @@ impl Parser { } } self.expect(TokenKind::RParen)?; - let return_type = if self.eat(TokenKind::Arrow) { - Some(self.ty()?) - } else { - None - }; + let (return_type, borrow_return) = self.return_type_with_borrow()?; let body = Expr::Block(self.block()?); Ok(Method { directives, @@ -301,6 +297,7 @@ impl Parser { receiver, params, return_type, + borrow_return, body, span: self.span_from(start), }) @@ -328,6 +325,7 @@ impl Parser { }), params: Vec::new(), return_type: None, + borrow_return: None, body, span, }) diff --git a/crates/rue-parser/src/validate.rs b/crates/rue-parser/src/validate.rs index a9487c2a5..ca338517a 100644 --- a/crates/rue-parser/src/validate.rs +++ b/crates/rue-parser/src/validate.rs @@ -310,6 +310,7 @@ impl Validator<'_> { self.check_expr(value); } } + Expr::Yield(y) => self.check_expr(&y.value), Expr::StructLit(s) => { if let Some(base) = &s.base { self.check_expr(base); diff --git a/crates/rue-rir/src/anonymous_sites.rs b/crates/rue-rir/src/anonymous_sites.rs index 3d29ca168..78ffd8264 100644 --- a/crates/rue-rir/src/anonymous_sites.rs +++ b/crates/rue-rir/src/anonymous_sites.rs @@ -203,6 +203,9 @@ impl SiteWalker { self.at(Seg::Operand(0), |this| this.walk_expr(value)); } } + Expr::Yield(yield_expr) => { + self.at(Seg::Operand(0), |this| this.walk_expr(&yield_expr.value)); + } Expr::StructLit(struct_lit) => { if let Some(base) = &struct_lit.base { self.at(Seg::Operand(0), |this| this.walk_expr(base)); diff --git a/crates/rue-rir/src/astgen.rs b/crates/rue-rir/src/astgen.rs index ecd6eac5e..9e0df79a8 100644 --- a/crates/rue-rir/src/astgen.rs +++ b/crates/rue-rir/src/astgen.rs @@ -701,6 +701,7 @@ impl<'a> AstGen<'a> { has_self, self_mode, self_is_mut, + method.borrow_return.is_some(), method.span, ) .record_failure(&mut self.payload_error); @@ -809,6 +810,7 @@ impl<'a> AstGen<'a> { false, RirParamMode::Normal, false, + func.borrow_return.is_some(), func.span, ) .record_failure(&mut self.payload_error); @@ -872,6 +874,7 @@ impl<'a> AstGen<'a> { false, RirParamMode::Normal, false, + false, foreign.span, ) .record_failure(&mut self.payload_error) @@ -1081,6 +1084,16 @@ impl<'a> AstGen<'a> { span: return_expr.span, }) } + Expr::Yield(yield_expr) => { + let value = self.gen_expr_at( + crate::RirStructuralPathSegment::Operand(0), + &yield_expr.value, + ); + self.rir.add_inst(Inst { + data: InstData::Yield(value), + span: yield_expr.span, + }) + } Expr::StructLit(struct_lit) => { // Generate module reference if this is a qualified struct literal let module = struct_lit.base.as_ref().map(|base_expr| { diff --git a/crates/rue-rir/src/inst.rs b/crates/rue-rir/src/inst.rs index 0a9328651..db41f754f 100644 --- a/crates/rue-rir/src/inst.rs +++ b/crates/rue-rir/src/inst.rs @@ -1202,6 +1202,7 @@ impl RirEditor { has_self: bool, self_mode: RirParamMode, self_is_mut: bool, + returns_borrow: bool, span: Span, ) -> Result { self.atomic(|rir| { @@ -1221,6 +1222,7 @@ impl RirEditor { has_self, self_mode, self_is_mut, + returns_borrow, }, span, })) @@ -1665,6 +1667,7 @@ impl RirEditor { has_self, self_mode, self_is_mut, + returns_borrow, } => { let directives = remap_directives(source, directives, &mut symbol, &mut remap_span); @@ -1691,6 +1694,7 @@ impl RirEditor { *has_self, *self_mode, *self_is_mut, + *returns_borrow, span, )? } @@ -1747,6 +1751,9 @@ impl RirEditor { InstData::Ret(value) => { self.add_inst(payload_free(InstData::Ret(value.map(remap_ref)))) } + InstData::Yield(value) => { + self.add_inst(payload_free(InstData::Yield(remap_ref(*value)))) + } InstData::Block { instructions } => { let instructions = source .block_insts(instructions) @@ -2727,6 +2734,7 @@ impl Rir { refs!(*reference); } } + InstData::Yield(value) => refs!(*value), InstData::FnDecl { directives, name, @@ -4216,6 +4224,11 @@ pub enum InstData { /// identity, and is always false unless `has_self` is true with /// `self_mode == Normal`. self_is_mut: bool, + /// Whether the result position is `-> borrow T` (ADR-0062): the + /// declaration is a place-returning accessor whose body yields a + /// second-class borrow of a receiver projection. `return_type` holds + /// the borrowed element type `T`. + returns_borrow: bool, }, /// Constant declaration @@ -4283,6 +4296,11 @@ pub enum InstData { /// Return value from function (None for `return;` in unit-returning functions) Ret(Option), + /// Yield a place from a `-> borrow T` accessor body (ADR-0062). The + /// operand is the place expression the accessor hands out; valid only as + /// the trailing exit of an accessor body (enforced in sema). + Yield(InstRef), + /// Block of instructions (for function bodies) /// The result is the last instruction in the block Block { @@ -4958,6 +4976,7 @@ impl<'a, 'b> RirPrinter<'a, 'b> { has_self, self_mode, self_is_mut, + returns_borrow, } => { let pub_str = if *is_c_export { "pub extern \"C\" " @@ -5005,15 +5024,17 @@ impl<'a, 'b> RirPrinter<'a, 'b> { }) .collect(); let directives_str = self.format_directives(directives); + let borrow_str = if *returns_borrow { "borrow " } else { "" }; writeln!( out, - "{}{}{}fn {}({}{}) -> {} {{", + "{}{}{}fn {}({}{}) -> {}{} {{", directives_str, pub_str, unchecked_str, name_str, self_str, params_str.join(", "), + borrow_str, ret_str ) .unwrap(); @@ -5051,6 +5072,9 @@ impl<'a, 'b> RirPrinter<'a, 'b> { writeln!(out, "ret").unwrap(); } } + InstData::Yield(inner) => { + writeln!(out, "yield {}", self.display_ref(*inner)).unwrap(); + } InstData::Call { name, args } => { let name_str = self.interner.resolve(&*name); let args = self.rir.call_args(args); @@ -5636,6 +5660,7 @@ mod typed_payload_tests { false, RirParamMode::Normal, false, + false, span(), ) .unwrap(); @@ -6367,6 +6392,7 @@ mod typed_payload_tests { has_self: false, self_mode: RirParamMode::Normal, self_is_mut: false, + returns_borrow: false, }, ); From a0413626d21896b2ec29136497bc767b34246ea7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 14:24:48 +0000 Subject: [PATCH 2/3] Toward RUE-662: spec chapter, formal core, and driver-enforceable coverage Pull the documentation and coverage deliverables forward while the provider-eligibility fallback waits on RUE-1033: - Add spec chapter 6.6 (Borrow Accessors) with rules 6.6:1-6.6:13, the `yield` keyword row in 2.4:2, and `result`/`yield_expr` grammar productions in appendix A. - Amend the formal core with the (Accessor-Call) rule, accessor body well-formedness, and the inlined-call dynamics note in section 5.8, plus the section 8 traceability row. - Add spec cases for chapter 6.6: declaration/body rules and the guard-trap dynamics case pass through today's driver and are required coverage; the remaining call-site cases (escapes, exclusivity, drop-glue read, in-place execution) run as preview-allowed-to-fail until the eligibility change lands, with matching KNOWN_UNCOVERED_NORMATIVE entries for 6.6:8-6.6:11. - Re-check the accessor declaration shape (E0257 receiver, E0260 parameter modes) in the shared body engine so those rules hold on every analysis host, including the incremental driver. - Point the ADR-0062 frontmatter at the `borrow_accessors` feature name. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015RwaF7SFHXcBuKdYrnwi5G --- crates/rue-air/src/sema/ordinary_engine.rs | 69 ++++ .../cases/items/borrow-accessors.toml | 367 ++++++++++++++++++ crates/rue-spec/cases/lexical/keywords.toml | 7 + crates/rue-spec/src/traceability.rs | 34 ++ .../0062-place-returning-borrow-accessors.md | 2 +- docs/formal/01-core-calculus.md | 51 +++ .../src/02-lexical-structure/04-keywords.md | 1 + docs/spec/src/06-items/06-borrow-accessors.md | 117 ++++++ docs/spec/src/appendices/A-grammar.md | 14 +- 9 files changed, 659 insertions(+), 3 deletions(-) create mode 100644 crates/rue-spec/cases/items/borrow-accessors.toml create mode 100644 docs/spec/src/06-items/06-borrow-accessors.md diff --git a/crates/rue-air/src/sema/ordinary_engine.rs b/crates/rue-air/src/sema/ordinary_engine.rs index d9cc73179..3bf9f829c 100644 --- a/crates/rue-air/src/sema/ordinary_engine.rs +++ b/crates/rue-air/src/sema/ordinary_engine.rs @@ -2142,6 +2142,75 @@ impl<'h, H: OrdinaryBodyAnalysisHost> OrdinaryBodyEngine<'h, H> { // per-instruction `yield`/`return`/`?` analysis against the trailing // reference recorded here. if is_accessor { + // Declaration shape (ADR-0062 phase 1), re-checked at the engine + // so it holds on every host that analyzes the body: the receiver + // is a shared `borrow self`, and value parameters are plain + // by-value guard inputs. The epoch declaration collector reports + // the same errors earlier when it runs. + let body_span = self.body_rir_ref().get(body).span; + let self_sym_check = self.storage.body_interner().get_or_intern("self"); + match params + .iter() + .find(|(name, _, _, _)| *name == self_sym_check) + { + Some((_, _, RirParamMode::Borrow, _)) => {} + Some((_, _, RirParamMode::Inout, _)) => { + return Err(CompileError::new( + ErrorKind::AccessorRequiresBorrowSelf { + found: "an `inout self` receiver".to_string(), + }, + body_span, + ) + .with_note( + "mutable accessors (`inout self` -> exclusive result) are a later phase (RUE-1016)", + )); + } + Some(_) => { + return Err(CompileError::new( + ErrorKind::AccessorRequiresBorrowSelf { + found: "a by-value `self` receiver".to_string(), + }, + body_span, + )); + } + None => { + return Err(CompileError::new( + ErrorKind::AccessorRequiresBorrowSelf { + found: "a function with no receiver".to_string(), + }, + body_span, + )); + } + } + for (name, _, mode, is_comptime) in params.iter() { + if *name == self_sym_check { + continue; + } + if *is_comptime { + return Err(CompileError::new( + ErrorKind::AccessorParamModeUnsupported { + mode: "`comptime`".to_string(), + }, + body_span, + )); + } + if *mode != RirParamMode::Normal { + return Err(CompileError::new( + ErrorKind::AccessorParamModeUnsupported { + mode: format!( + "`{}`", + match mode { + RirParamMode::Inout => "inout", + RirParamMode::Borrow => "borrow", + RirParamMode::Normal => unreachable!(), + } + ), + }, + body_span, + )); + } + } + // A single-statement body lowers to the instruction itself; a // multi-statement body lowers to a block whose last instruction // is the trailing exit. diff --git a/crates/rue-spec/cases/items/borrow-accessors.toml b/crates/rue-spec/cases/items/borrow-accessors.toml new file mode 100644 index 000000000..d59816cbf --- /dev/null +++ b/crates/rue-spec/cases/items/borrow-accessors.toml @@ -0,0 +1,367 @@ +# 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). 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`. + +[section] +id = "items.borrow-accessors" +spec_chapter = "6.6" +name = "Borrow Accessors" +description = "Place-returning read accessors: `fn (borrow self, ...) -> borrow T` with a trailing-yield body (ADR-0062)." + +[[case]] +name = "accessor_declaration_parses_and_compiles" +spec = ["6.6:1", "6.6:2"] +description = "The S2 surface: an ordinary fn with `-> borrow T` and a trailing-yield body is accepted under the preview." +preview = "borrow_accessors" +preview_should_pass = true +source = """ +struct Grid { + cells: [i64; 4], + + @allow(unused_function) + 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: [1, 2, 3, 4] }; + if g.cells[0] == 1 { 0 } else { 1 } +} +""" +exit_code = 0 + +[[case]] +name = "accessor_requires_preview_gate" +spec = ["6.6:3"] +description = "Using the accessor form without --preview borrow_accessors is E1100." +source = """ +struct P { + x: i64, + + fn xr(borrow self) -> borrow i64 { + yield self.x; + } +} +fn main() -> i32 { + let p = P { x: 1 }; + if p.xr() == 1 { 0 } else { 1 } +} +""" +compile_fail = true +error_contains = ["E1100", "borrow_accessors"] + +[[case]] +name = "accessor_requires_borrow_self" +spec = ["6.6:4"] +description = "An `inout self` accessor is rejected: phase 1 accessors are shared projections (E0257)." +preview = "borrow_accessors" +preview_should_pass = true +source = """ +struct P { + x: i64, + + fn xr(inout self) -> borrow i64 { + yield self.x; + } +} +fn main() -> i32 { + let mut p = P { x: 1 }; + if p.xr() == 1 { 0 } else { 1 } +} +""" +compile_fail = true +error_contains = ["E0257", "requires a `borrow self` receiver"] + +[[case]] +name = "accessor_params_are_by_value" +spec = ["6.6:5"] +description = "A `borrow`-mode accessor parameter is rejected (E0260)." +preview = "borrow_accessors" +preview_should_pass = true +source = """ +struct P { + x: i64, + + fn xr(borrow self, borrow k: i64) -> borrow i64 { + yield self.x; + } +} +fn main() -> i32 { + let p = P { x: 1 }; + let k = 2; + if p.xr(borrow k) == 1 { 0 } else { 1 } +} +""" +compile_fail = true +error_contains = ["E0260", "must be by-value"] + +[[case]] +name = "accessor_body_requires_trailing_yield" +spec = ["6.6:6"] +description = "An accessor body whose final statement is not a yield is rejected (E0254)." +preview = "borrow_accessors" +preview_should_pass = true +source = """ +struct P { + x: i64, + + fn xr(borrow self) -> borrow i64 { + self.x + } +} +fn main() -> i32 { + let p = P { x: 1 }; + if p.xr() == 1 { 0 } else { 1 } +} +""" +compile_fail = true +error_contains = ["E0254", "single trailing `yield`"] + +[[case]] +name = "yield_outside_accessor_rejected" +spec = ["6.6:6"] +description = "A `yield` outside an accessor body is rejected (E0256)." +preview = "borrow_accessors" +preview_should_pass = true +source = """ +fn main() -> i32 { + yield 1; +} +""" +compile_fail = true +error_contains = ["E0256", "only valid inside the body of a `-> borrow` accessor"] + +[[case]] +name = "accessor_yield_must_root_at_receiver" +spec = ["6.6:7"] +description = "Yielding a place not rooted at `self` is rejected (E0255)." +preview = "borrow_accessors" +preview_should_pass = true +source = """ +struct P { + x: i64, + + fn xr(borrow self, other: i64) -> borrow i64 { + yield other; + } +} +fn main() -> i32 { + let p = P { x: 1 }; + if p.xr(2) == 1 { 0 } else { 1 } +} +""" +compile_fail = true +error_contains = ["E0255", "place rooted at `self`"] + +# --------------------------------------------------------------------------- +# 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. +# --------------------------------------------------------------------------- + +[[case]] +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" +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] }; + let sum = g.at(0) + g.at(3); + @dbg(sum); + if sum == 50 { 0 } else { 1 } +} +""" +expected_stdout = "50\n" +exit_code = 0 + +[[case]] +name = "accessor_guard_traps_out_of_bounds" +spec = ["6.6:12"] +description = "The accessor's guard panics at the call site for an out-of-bounds index." +preview = "borrow_accessors" +preview_should_pass = true +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(9) == 0 { 0 } else { 1 } +} +""" +runtime_error = "index out of bounds" + +[[case]] +name = "accessor_result_cannot_be_returned" +spec = ["6.6:9"] +description = "Returning an accessor result escapes its full expression (E0250)." +preview = "borrow_accessors" +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 }; + if read(borrow p) == 1 { 0 } else { 1 } +} +""" +compile_fail = true +error_contains = ["E0250", "cannot return an accessor result"] + +[[case]] +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" +source = """ +struct P { + x: i64, + + fn xr(borrow self) -> borrow i64 { + yield self.x; + } +} +fn main() -> i32 { + let p = P { x: 1 }; + let b = p.xr(); + if b == 1 { 0 } else { 1 } +} +""" +compile_fail = true +error_contains = ["E0252", "cannot bind an accessor result with `let`"] + +[[case]] +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" +source = """ +struct P { + x: i64, + + fn xr(borrow self) -> borrow i64 { + yield self.x; + } +} +fn main() -> i32 { + let p = P { x: 1 }; + let mut slot = 0; + slot = p.xr(); + if slot == 1 { 0 } else { 1 } +} +""" +compile_fail = true +error_contains = ["E0251", "cannot store an accessor result"] + +[[case]] +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" +source = """ +struct P { + x: i64, + + fn xr(borrow self) -> borrow i64 { + yield self.x; + } +} +fn main() -> i32 { + let p = P { x: 1 }; + let a = [p.xr()]; + if a[0] == 1 { 0 } else { 1 } +} +""" +compile_fail = true +error_contains = ["E0253", "cannot capture an accessor result"] + +[[case]] +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" +source = """ +struct Grid { + cells: [i64; 4], + + fn at(borrow self, i: u64) -> borrow i64 { + yield self.cells[i]; + } +} +fn using(a: i64, b: i64) -> i64 { a + b } +fn bump(inout g: Grid) -> i64 { + g.cells[0] = 9; + 0 +} +fn main() -> i32 { + let mut g = Grid { cells: [1, 2, 3, 4] }; + using(g.at(0), bump(inout g)); + 0 +} +""" +compile_fail = true +error_contains = ["E0259", "while an accessor result borrows it"] + +[[case]] +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" +real_std = true +source = """ +const std = @import("std"); +const StrBuf = std.strbuf.StrBuf; +struct Named { + name: StrBuf, + + fn name_ref(borrow self) -> borrow StrBuf { + yield self.name; + } +} +fn take(s: StrBuf) -> u64 { s.len() } +fn main() -> i32 { + let mut s = StrBuf.new(); + s.push_str("hi"); + let n = Named { name: s }; + if take(n.name_ref()) == 2 { 0 } else { 1 } +} +""" +compile_fail = true +error_contains = ["E0258"] diff --git a/crates/rue-spec/cases/lexical/keywords.toml b/crates/rue-spec/cases/lexical/keywords.toml index c354eed0b..2e482d416 100644 --- a/crates/rue-spec/cases/lexical/keywords.toml +++ b/crates/rue-spec/cases/lexical/keywords.toml @@ -71,6 +71,13 @@ spec = ["2.4:1", "2.4:2"] source = "fn main() -> i32 { let return = 1; 0 }" compile_fail = true +[[case]] +name = "keyword_yield_reserved" +error_contains = "[E0100]" +spec = ["2.4:1", "2.4:2"] +source = "fn main() -> i32 { let yield = 1; 0 }" +compile_fail = true + [[case]] name = "keyword_break_reserved" error_contains = "[E0100]" diff --git a/crates/rue-spec/src/traceability.rs b/crates/rue-spec/src/traceability.rs index 421ff287c..4311b3aeb 100644 --- a/crates/rue-spec/src/traceability.rs +++ b/crates/rue-spec/src/traceability.rs @@ -236,6 +236,40 @@ 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/docs/designs/0062-place-returning-borrow-accessors.md b/docs/designs/0062-place-returning-borrow-accessors.md index a0732a8e9..d1fea0512 100644 --- a/docs/designs/0062-place-returning-borrow-accessors.md +++ b/docs/designs/0062-place-returning-borrow-accessors.md @@ -3,7 +3,7 @@ id: 0062 title: "Place-returning borrow accessors: projection reads of owned elements" status: accepted tags: [ownership, borrows, collections, accessors, stdlib, formal-semantics] -feature-flag: borrow-accessors +feature-flag: borrow_accessors created: 2026-07-18 accepted: 2026-07-18 implemented: diff --git a/docs/formal/01-core-calculus.md b/docs/formal/01-core-calculus.md index b08d9d5cc..b3fcbba53 100644 --- a/docs/formal/01-core-calculus.md +++ b/docs/formal/01-core-calculus.md @@ -863,6 +863,56 @@ carries only the moves performed by the by-value arguments. Because the core is fully monomorphic (§1), `g` names a single concrete signature: there is no overload or generic instantiation to resolve at the call. +**Accessor calls (ADR-0062, preview).** A *read accessor* is a method of the +form + +``` + A.f : fn ( borrow self : A, x1:T1, ..., xk:Tk ) -> borrow T { e_guard ; yield p_y } +``` + +whose body is well-formed iff every non-diverging exit is the single trailing +`yield` of a place `p_y` rooted at the receiver parameter — a projection chain +`self.f…[e]…` (possibly through a nested accessor call), whose guards `e_guard` +either diverge (trap, `@panic`) or fall through, with an empty post-`yield` +continuation. Value parameters are by-value (prose `6.6:4`–`6.6:7`). A call +produces a **borrowed place**, not a value: + +``` + Γ;Σ;Λ ⊢ receiver place p ⇒ A fully-owned(Σ, p) + for each i, threading Σ left-to-right (Σ0 = Σ): + Γ;Σ_{i-1};Λ ⊢ e_i ⇒ Ti ⊣ Σi -- by-value guard inputs, §4.2 + A.f is a well-formed read accessor with element type T + add (root(p), shared) to Λ_expr -- extent: the enclosing FULL EXPRESSION, not the call + Λ_expr ∪ Λ_call of every call in that extent is CONSISTENT (law of exclusivity, §5.4) + ─────────────────────────────────────────────────────────────────────── (Accessor-Call) + Γ;Σ;Λ ⊢ p.f(e1, ..., ek) ⇒ borrowed-place T ⊣ Σk +``` + +The result is usable in **place contexts only**: it may be read (a `Copy`-shaped +read; reading out an owning value would mint a second owner and is rejected — +the same argument as the RUE-651 `get` gate), projected further, passed as a +`borrow` argument, or compared (§5.4's compare loan). It may **not** be +returned, stored, bound by a `let`, or captured in an aggregate — each escape +would let the loan outlive its extent (prose `6.6:9`–`6.6:11`). Unlike `(Call)`'s +loans, which are discharged at the call, the accessor loan joins the enclosing +full expression's loan set `Λ_expr` — the same extent generalization the §5.4 +equality-compare paragraph anticipates — so an exclusive use of `root(p)` +anywhere in that extent is inconsistent (`use(v.get_ref(i), g(inout v))` is the +canonical rejection). This is the first construct that makes the dormant +"loaned in Λ" premises of §5.1/§5.2 observable within a single judgment; no new +§7 theorem shapes are introduced — the result's extent is bounded by its +expression, so second-classness, view-intact, loan-extent-nesting, and +handle-uniqueness quantify over it unchanged. + +Dynamically an accessor call is not a `(D-Call)`: the call reduces **by the +accessor's inlined body** — the guards run in the caller (and may trap, §6.12) +and the redex is then replaced by the projected place itself, `(ℓ, π·π_y)` for +a user accessor over §6.9's by-ref place plumbing (a library accessor over the +allocation store would yield `view⟨A | o, k⟩`, §6.13.2 — deferred to the std +phase, RUE-1017). No call frame is pushed and no calling convention for +"returning a place" exists; that absence is the RUE-1012 forward-compatibility +contract. + --- ## 6. Dynamic semantics (small-step) @@ -1930,6 +1980,7 @@ witness of the dynamic semantics (RUE-50), cited inline in each §6 rule group. | §4.1/§5.4 equality borrows its operands | 4.3:3f | | §5.5 match / enum elim + intro | 6.3:17, 3.8:33 (destructure), 4.7 (match) | | §5.8 leaf/operator/aggregate/call statics | 4.1:2/5/7, 4.2:1/6/14, 4.3:1/2/5/6, 4.3a:3/4, 4.4:2, 3.6:5/6/15/16, 3.5:1/2, 4.10:3/4/5/7, 6.1:36 | +| §5.8 (Accessor-Call) + accessor body WF (preview, ADR-0062) | 6.6:2–6.6:12 | | §5.6 enum drop (active payload) | 6.3:20 | | §4.3 expression/return value | 4.5:3 (→ value, not just type), 6.1:4/5, 4.9:1/7 | | §5.2 assignment / reinit | 3.8:55/56, 3.8:72, 3.8:77 | diff --git a/docs/spec/src/02-lexical-structure/04-keywords.md b/docs/spec/src/02-lexical-structure/04-keywords.md index 67cae6ae2..cc88e9954 100644 --- a/docs/spec/src/02-lexical-structure/04-keywords.md +++ b/docs/spec/src/02-lexical-structure/04-keywords.md @@ -48,6 +48,7 @@ The following words are keywords and cannot be used as identifiers: | `unchecked` | Unchecked function modifier | | `ptr` | Pointer type constructor (`ptr const T` / `ptr mut T`) | | `extern` | Foreign declaration block (`extern "C" { … }`, ADR-0064) | +| `yield` | Accessor body exit (`-> borrow T` accessors, ADR-0062) | ## Type Names diff --git a/docs/spec/src/06-items/06-borrow-accessors.md b/docs/spec/src/06-items/06-borrow-accessors.md new file mode 100644 index 000000000..e89c2b905 --- /dev/null +++ b/docs/spec/src/06-items/06-borrow-accessors.md @@ -0,0 +1,117 @@ ++++ +title = "Borrow Accessors" +weight = 6 +template = "spec/page.html" ++++ + +# Borrow Accessors + +{{ preview_feature(feature="borrow_accessors", adr="ADR-0062") }} + +{{ rule(id="6.6:1", cat="informative") }} + +A *borrow accessor* is a method that hands out a second-class borrow of a +projection of its receiver: `v.get_ref(i)` produces a borrowed place naming +element `i` in place — no copy, no move-out — checked by the ordinary +law-of-exclusivity loan machinery and scoped to the enclosing full expression +(core calculus `docs/formal/01-core-calculus.md` §5.8, rule `(Accessor-Call)`). +This is the ADR-0062 read-accessor form; mutable accessors (`inout self` → +exclusive result) are a later phase. + +## Declaration + +{{ rule(id="6.6:2", cat="syntax") }} + +```ebnf +accessor = "fn" IDENT "(" "borrow" "self" [ "," params ] ")" + "->" "borrow" type "{" { statement } yield_expr [ ";" ] "}" ; +yield_expr = "yield" expression ; +``` + +{{ rule(id="6.6:3", cat="legality-rule") }} + +A `-> borrow` result position and the `yield` form require the +`borrow_accessors` preview feature. Without `--preview borrow_accessors`, a +program using either is rejected at compile time (E1100), per 8.4. + +{{ rule(id="6.6:4", cat="legality-rule") }} + +An accessor **MUST** declare a `borrow self` receiver. A `-> borrow` result on +a free function, an associated function, a by-value or `mut self` method, an +`inout self` method, or a method of an anonymous struct type is rejected +(E0257). + +{{ rule(id="6.6:5", cat="legality-rule") }} + +Accessor value parameters **MUST** be plain by-value parameters: `borrow`, +`inout`, and `comptime` parameter modes are rejected on an accessor (E0260). + +## The accessor body + +{{ rule(id="6.6:6", cat="legality-rule") }} + +Every non-diverging path through an accessor body **MUST** fall through to the +body's single trailing `yield`: the final statement of the body is a `yield`, +no other `yield` may appear, no code may follow it, and the body **MUST NOT** +contain `return` or `?` (E0254). Guard code before the `yield` may only +diverge — trap or `@panic` — or fall through. A `yield` outside an accessor +body is rejected (E0256). + +{{ rule(id="6.6:7", cat="legality-rule") }} + +The operand of the `yield` **MUST** be a place rooted at the receiver +parameter: `self`, or a projection chain from `self` through fields, indices, +or nested accessor calls (E0255). Yielding a local, a parameter other than the +receiver, or a computed value would hand out a place that dies with the +accessor's guards. + +## Calls + +{{ rule(id="6.6:8", cat="normative") }} + +A call to an accessor requires its receiver to be a place, exactly as passing +it as a `borrow` argument does (6.4:27), and evaluates its arguments by value. +The result is a *borrowed place*, not a first-class value: a shared loan on +the receiver's root variable whose extent is the enclosing full expression +(core calculus `docs/formal/01-core-calculus.md` §5.8, rule +`(Accessor-Call)`). Within that extent the result may be read, projected +further (`v.get_ref(i).name`), passed as a `borrow` argument, or compared. + +{{ rule(id="6.6:9", cat="legality-rule") }} + +An accessor result **MUST NOT** escape its full expression: returning it +(E0250), storing it by assignment (E0251), binding it with a plain `let` +(E0252), or capturing it in a struct or array literal (E0253) is rejected. + +{{ rule(id="6.6:10", cat="legality-rule") }} + +The law of exclusivity extends over the accessor loan's whole extent: an +exclusive use of the borrowed root — passing it `inout`, an `inout self` +receiver access, assigning to it, or moving it — anywhere within the same full +expression is rejected (E0259). `use(v.get_ref(i), g(inout v))` is ill-formed +even though the read syntactically precedes the exclusive access. + +{{ rule(id="6.6:11", cat="legality-rule") }} + +Reading a value that owns resources (one with drop glue) out of an accessor +result by value is rejected (E0258): the result is a borrow, not an owner, and +a by-value read would mint an aliasing second owner — the same soundness +argument as the by-copy container-read gate (E0711). Only trivially droppable +values may be read out; owning values are used in place through projection, +`borrow` arguments, comparison, or `borrow self` methods. + +## Dynamics and lowering + +{{ rule(id="6.6:12", cat="dynamic-semantics") }} + +An accessor call evaluates by the accessor's inlined body: the guards run in +the calling context — and may trap — and the call's result is then the +yielded place itself, projected from the caller's receiver place (core +calculus §5.8 dynamics note). No function call occurs at runtime. + +{{ rule(id="6.6:13", cat="informative") }} + +Accessors are required-inlineable by design: no calling convention for +"returning a place" exists, which is the forward-compatibility contract that +keeps the future coroutine-accessor generalization (RUE-1012) free to choose +its own call shape. diff --git a/docs/spec/src/appendices/A-grammar.md b/docs/spec/src/appendices/A-grammar.md index 6dfd2cd8e..5fa4f550c 100644 --- a/docs/spec/src/appendices/A-grammar.md +++ b/docs/spec/src/appendices/A-grammar.md @@ -31,7 +31,11 @@ intrinsic_arg = type | expression ; (* Functions *) function = directives [ "pub" ] [ "unchecked" ] - "fn" IDENT "(" [ params ] ")" [ "->" type ] "{" block "}" ; + "fn" IDENT "(" [ params ] ")" [ result ] "{" block "}" ; +result = "->" [ "borrow" ] type ; (* "borrow" marks a place-returning + accessor (ADR-0062, preview); + legal only on `borrow self` + methods — a legality rule *) params = param { "," param } [ "," ] ; param = [ param_mode ] IDENT ":" type ; param_mode = "comptime" | "inout" | "borrow" ; @@ -44,7 +48,7 @@ struct_fields = struct_field { "," struct_field } [ "," ] ; struct_field = IDENT ":" type ; method = directives "fn" IDENT "(" [ [ "inout" | "borrow" | "mut" ] "self" [ "," params ] | params ] ")" - [ "->" type ] "{" block "}" ; + [ result ] "{" block "}" ; (* Enums *) enum_def = [ "pub" ] "enum" IDENT "{" [ enum_variants ] "}" ; @@ -67,6 +71,12 @@ compound_op = "+=" | "-=" | "*=" | "/=" | "%=" expr_stmt = expression ";" | control_flow_expr | block_expr ; (* block-like expressions need no semicolon *) +yield_expr = "yield" expression ; (* the trailing exit of an accessor body + (ADR-0062, preview); parsed as an + expression form, valid only as the + single trailing statement of a + `-> borrow` accessor body — a + legality rule *) (* Place expressions: a variable — or `self`, inside a method — followed by zero or more field/index projections. Used as assignment targets. Assigning From da6a9dc107900388a31c0c0a1c34d24a96f27cab Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 14:45:54 +0000 Subject: [PATCH 3/3] Toward RUE-662: declare the accessor guard trap as a user panic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The oracle-diff audit classifies a spec case's expected trap from its runtime_error string, and "index out of bounds" names the built-in IndexOutOfBounds trap — but the accessor's guard is spelled @panic("index out of bounds"), which traps as UserPanic. Use the established user-panic assertion shape (exit_code = 101 with stderr_contains = "panic: ...") so the expected trap kind matches the one the program actually raises. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015RwaF7SFHXcBuKdYrnwi5G --- crates/rue-spec/cases/items/borrow-accessors.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/rue-spec/cases/items/borrow-accessors.toml b/crates/rue-spec/cases/items/borrow-accessors.toml index d59816cbf..e2af7062e 100644 --- a/crates/rue-spec/cases/items/borrow-accessors.toml +++ b/crates/rue-spec/cases/items/borrow-accessors.toml @@ -219,7 +219,8 @@ fn main() -> i32 { if g.at(9) == 0 { 0 } else { 1 } } """ -runtime_error = "index out of bounds" +exit_code = 101 +stderr_contains = "panic: index out of bounds" [[case]] name = "accessor_result_cannot_be_returned"