diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml index c300bb6c..c1d7d9e8 100644 --- a/.github/workflows/native.yml +++ b/.github/workflows/native.yml @@ -41,6 +41,37 @@ jobs: - run: rustup override set 1.97.1 - name: Check compiler and native conformance run: cargo test --locked --workspace --no-fail-fast --target ${{ matrix.target }} + - name: Check GNU Resource measurement controls + if: matrix.target == 'x86_64-unknown-linux-gnu' + run: python -m unittest tools.tests.test_measure_resource_native_archives + - name: Measure fresh GNU private Resource runtime archives + id: resource_archives + if: matrix.target == 'x86_64-unknown-linux-gnu' + run: >- + python tools/measure_resource_native_archives.py prepare + --output target/native-resource-gnu + --github-output "$GITHUB_OUTPUT" + - name: Execute complete GNU Resource corpus with compiled receipt pin + if: matrix.target == 'x86_64-unknown-linux-gnu' + run: >- + python tools/measure_resource_native_archives.py run + --receipt "${{ steps.resource_archives.outputs.receipt }}" + --expected-sha256 "${{ steps.resource_archives.outputs.sha256 }}" + --target-dir target/native-resource-gnu-gate + - name: Retain GNU Resource measurements and execution evidence + if: always() && matrix.target == 'x86_64-unknown-linux-gnu' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: native-resource-${{ matrix.target }}-${{ github.sha }} + path: | + target/native-resource-gnu/*.json + target/native-resource-gnu/*.jsonl + target/native-resource-gnu/*.log + target/native-resource-gnu/*-crt-probe + target/native-resource-gnu/*-crt-probe.c + target/native-resource-gnu/*-build/x86_64-unknown-linux-gnu/*/libjett_runtime.a + if-no-files-found: warn + retention-days: 7 - name: Package release compiler and both runtime profiles run: python tools/package_native.py --output dist/package - name: Check installed compiler diff --git a/Cargo.lock b/Cargo.lock index 6776288e..93b76af2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -933,6 +933,7 @@ dependencies = [ "cranelift-module", "cranelift-object", "jett_common", + "jett_comptime", "jett_diagnostics", "jett_hir", "jett_mir", @@ -958,7 +959,9 @@ dependencies = [ "jett_diagnostics", "jett_intrinsics", "jett_parser", + "jett_resolve", "jett_runtime", + "jett_typecheck", "jett_types", "rand", "subtle", @@ -1013,6 +1016,7 @@ version = "0.1.0" dependencies = [ "insta", "jett_common", + "jett_comptime", "jett_diagnostics", "jett_intrinsics", "jett_parser", @@ -1053,6 +1057,7 @@ name = "jett_mir" version = "0.1.0" dependencies = [ "jett_common", + "jett_comptime", "jett_diagnostics", "jett_hir", "jett_parser", diff --git a/README.md b/README.md index 3d4211d2..e675ea57 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,25 @@ cargo test -p jett_comptime Compile-fail fixtures live in `tests/compile_fail` and assert specific diagnostics. Run-pass fixtures live in `tests/run_pass` and exercise interpreter behavior. +### Private Resource native gates + +The archive measurement helper builds fresh compiler-test runtimes and runs +pinned Source execution gates. On Linux GNU, its default target measures the +dynamic CRT. On Windows MSVC, use an x64 Visual Studio Developer PowerShell +with `link.exe` on PATH and LLVM's `llvm-nm` and `llvm-readobj` installed: + +```powershell +python tools/measure_resource_native_archives.py prepare --target x86_64-pc-windows-msvc --nm llvm-nm --readobj llvm-readobj --linker link.exe --output target/native/private-resource +python tools/measure_resource_native_archives.py run --target x86_64-pc-windows-msvc --suite occupied-structs --reuse-target-dir --receipt RECEIPT_PATH --expected-sha256 MEASURED_DIGEST --target-dir target/native +``` + +Preparation returns the receipt path and measured digest. The MSVC path checks +static CRT, exact runtime exports, compiler and linker identity, and the SDK LIB +environment. The two occupied-struct gates use both fresh archive profiles; +ordinary Runtime test counts do not establish their linked Source execution. +Reusing the MSVC test target requires a newly compiled gate with that receipt +pin. The default `original` suite retains the complete shared Source gate. + The language intentionally enforces function complexity limits. The current checker caps functions at 100 statements, nesting depth 4, and cyclomatic complexity 10. ## License diff --git a/crates/jett_codegen_cranelift/Cargo.toml b/crates/jett_codegen_cranelift/Cargo.toml index 1476961a..44bf14e9 100644 --- a/crates/jett_codegen_cranelift/Cargo.toml +++ b/crates/jett_codegen_cranelift/Cargo.toml @@ -19,6 +19,7 @@ sha2.workspace = true target-lexicon.workspace = true [dev-dependencies] +jett_comptime.workspace = true jett_diagnostics.workspace = true jett_parser.workspace = true jett_resolve.workspace = true diff --git a/crates/jett_codegen_cranelift/src/emit.rs b/crates/jett_codegen_cranelift/src/emit.rs index 2829f41a..35116176 100644 --- a/crates/jett_codegen_cranelift/src/emit.rs +++ b/crates/jett_codegen_cranelift/src/emit.rs @@ -1,5 +1,8 @@ pub(crate) mod debug; mod graphics; +pub(super) mod ordinary_borrowed_sums; +mod resource_execution; +mod resource_layout; mod values; use jett_mir::move_values::{ MoveValuePlan, is_copy_owned, is_function, is_linear, is_string, representation_type, @@ -215,7 +218,11 @@ fn emit_for_triple( if let Some(entry) = entry { validate_program_entry_contract(program, types, entry)?; } + if !program.resource_manifest.kinds().is_empty() { + return resource_execution::emit(program, types, target, entry, options); + } jett_mir::validate(program).map_err(CodegenError::InvalidMir)?; + ordinary_borrowed_sums::validate_public_program(program, types)?; for function in &program.functions { jett_mir::move_values::validate_local_view_initializers(function, types).map_err( |message| CodegenError::InvalidMirContract { @@ -745,6 +752,28 @@ fn translate_function( types: &TypeInterner, symbol: &str, context: &mut Context, +) -> Result<(), CodegenError> { + translate_function_inner( + module, + declarations, + program, + function, + types, + symbol, + context, + None, + ) +} + +fn translate_function_inner( + module: &mut ObjectModule, + declarations: &DeclaredFunctions, + program: &Program, + function: &Function, + types: &TypeInterner, + symbol: &str, + context: &mut Context, + resource_layout: Option<&resource_layout::EmittedResourceLayout<'_>>, ) -> Result<(), CodegenError> { if crate::verify::descriptor_only_function(function) { let mut builder_context = FunctionBuilderContext::new(); @@ -770,10 +799,44 @@ fn translate_function( let runtime_context = builder.declare_var(module.target_config().pointer_type()); let environment_variable = builder.declare_var(ir::types::I64); - let caller_acquisitions = jett_mir::validate_caller_acquisitions(program, function, types) - .map_err(|message| contract_error(symbol, function.span, message))?; - let ownership = MoveValuePlan::analyze(program, function, types) + let companion = resource_layout + .map(|layout| jett_mir::ResourceCompanionPlan::analyze(layout.plan(), function.id)) + .transpose() .map_err(|message| contract_error(symbol, function.span, message))?; + let ordinary_acquisitions = if companion.is_none() { + Some( + jett_mir::validate_caller_acquisitions(program, function, types) + .map_err(|message| contract_error(symbol, function.span, message))?, + ) + } else { + None + }; + let caller_acquisitions = companion + .as_ref() + .map(|plan| plan.caller_acquisitions()) + .or(ordinary_acquisitions.as_ref()) + .ok_or_else(|| { + contract_error(symbol, function.span, "missing exact Source acquisitions") + })?; + let ordinary_ownership = if companion.is_none() { + Some( + MoveValuePlan::analyze(program, function, types) + .map_err(|message| contract_error(symbol, function.span, message))?, + ) + } else { + None + }; + let ownership = companion + .as_ref() + .map(|plan| plan.storage()) + .or(ordinary_ownership.as_ref()) + .ok_or_else(|| { + contract_error( + symbol, + function.span, + "function has no exact ownership storage proof", + ) + })?; let local_slots = function .locals .iter() @@ -851,17 +914,79 @@ fn translate_function( }); } let failure_block = builder.create_block(); + let resource_failure = resource_layout.map(|_| { + let block = builder.create_block(); + builder.append_block_param(block, ir::types::I32); + block + }); + let resource_scope = builder.declare_var(ir::types::I64); + let resource_slots = resource_layout.map(|_| { + let returned = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 8, + 3, + )); + let finished = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 8, + 3, + )); + (returned, finished) + }); + let resource_status = builder.declare_var(ir::types::I32); + let mut resource_frames = Vec::new(); + let mut resource_owners = Vec::new(); + let mut resource_loans = Vec::new(); + let mut carrier_owners = Vec::new(); + let mut carrier_builders = Vec::new(); + let mut carrier_loans = Vec::new(); + if let Some(layout) = resource_layout { + let plan = layout.plan().function(function.id).ok_or_else(|| { + resource_layout::pending("function is outside its selected Resource family") + })?; + for storage in [ + &mut resource_frames, + &mut resource_owners, + &mut resource_loans, + &mut carrier_owners, + &mut carrier_builders, + &mut carrier_loans, + ] + .into_iter() + .zip([ + (plan.frames().len(), 16), + (plan.owner_slots().len(), 8), + (plan.loans().len(), 8), + (plan.carriers().map_or(0, |plan| plan.slots().len()), 8), + (plan.carriers().map_or(0, |plan| plan.slots().len()), 8), + (plan.carriers().map_or(0, |plan| plan.loans().len()), 8), + ]) { + for _ in 0..storage.1.0 { + storage + .0 + .push(builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + storage.1.1, + 3, + ))); + } + } + } let mut variables = Vec::with_capacity(function.locals.len()); let mut pending_variables = Vec::with_capacity(function.locals.len()); for local in &function.locals { - variables - .push(clif_type(types, local.ty, "function local")?.map(|ty| builder.declare_var(ty))); - pending_variables.push(if is_task_scalar(types, local.ty)? { - Some(builder.declare_var(ir::types::I64)) - } else { - None - }); + variables.push( + resource_execution::clif_type(resource_layout, types, local.ty, "function local")? + .map(|ty| builder.declare_var(ty)), + ); + pending_variables.push( + if resource_execution::task_scalar(resource_layout, types, local.ty)? { + Some(builder.declare_var(ir::types::I64)) + } else { + None + }, + ); } let control_flow = ControlFlowGraph::analyze(function).map_err(|errors| { @@ -911,6 +1036,25 @@ fn translate_function( builder.ins().stack_store(zero, storage.owner_slot, 0); builder.ins().stack_store(zero, storage.retired_slot, 0); } + if let Some((returned, finished)) = resource_slots { + builder.ins().stack_store(zero, returned, 0); + builder.ins().stack_store(zero, finished, 0); + for slot in resource_frames + .iter() + .chain(&resource_owners) + .chain(&resource_loans) + .chain(&carrier_owners) + .chain(&carrier_builders) + .chain(&carrier_loans) + { + builder.ins().stack_store(zero, *slot, 0); + } + for slot in &resource_frames { + builder.ins().stack_store(zero, *slot, 8); + } + let failed = builder.ins().iconst(ir::types::I32, 1); + builder.def_var(resource_status, failed); + } } let mut translator = Translator { @@ -926,8 +1070,9 @@ fn translate_function( types, equality_methods: &program.equality_methods, symbol, + function, local_types: &function.locals, - caller_acquisitions: &caller_acquisitions, + caller_acquisitions, taking_binding: None, test_ownership: matches!( function.identity.declaration.kind, @@ -938,9 +1083,34 @@ fn translate_function( generation_slots: &generation_slots, next_temporary: 0, failure_block, + resource: None, }; + translator.resource = resource_layout.map(|layout| resource_execution::ResourceEmission { + layout, + function: function.id, + scope: resource_scope, + return_companion: resource_slots.expect("selected Resource storage").0, + scope_finished: resource_slots.expect("selected Resource storage").1, + status: resource_status, + frames: &resource_frames, + owners: &resource_owners, + loans: &resource_loans, + carrier_owners: &carrier_owners, + carrier_builders: &carrier_builders, + carrier_loans: &carrier_loans, + failure: resource_failure.expect("selected Resource failure block"), + block: block.id, + position: jett_mir::ResourcePosition::Terminator, + }); if block.id == function.entry { let incoming = translator.builder.block_params(native_block).to_vec(); + if let Some(resource) = translator.resource { + let scope = incoming.get(2).copied().ok_or_else(|| { + resource_layout::pending("dedicated function lost its hidden Scope") + })?; + translator.builder.def_var(resource.scope, scope); + translator.resource_validate_scope()?; + } let environment = incoming.get(1).copied().ok_or_else(|| { contract_error( symbol, @@ -958,7 +1128,11 @@ fn translate_function( format!("cannot bind native actor environment: {error}"), ) })?; - let mut incoming_index = 2_usize; + let mut incoming_index = if resource_layout.is_some() { + 3_usize + } else { + 2_usize + }; for (index, parameter) in function.params.iter().enumerate() { let variable = variable_for(&variables, parameter.local.index(), parameter.span, symbol)?; @@ -1065,6 +1239,9 @@ fn translate_function( })?; } } + if translator.resource.is_some() { + translator.resource_bind_parameters(function)?; + } for parameter in &function.params { if let Some(slot) = local_slots[parameter.local.index() as usize] { let v = translator @@ -1093,13 +1270,25 @@ fn translate_function( } translator.drop_dead_locals(&ownership.live_in[block_index])?; for (index, statement) in block.statements.iter().enumerate() { - translator.statement(statement)?; + if let Some(resource) = &mut translator.resource { + resource.position = jett_mir::ResourcePosition::Statement(index); + } + translator.statement(block.id, index, statement)?; translator.drop_temporaries()?; translator.drop_dead_locals(&ownership.live_after_statement[block_index][index])?; } + if let Some(resource) = &mut translator.resource { + resource.position = jett_mir::ResourcePosition::Terminator; + } translator.terminator(&block.terminator)?; } + if let Some(resource_failure) = resource_failure { + builder.switch_to_block(resource_failure); + let status = builder.block_params(resource_failure)[0]; + builder.def_var(resource_status, status); + builder.ins().jump(failure_block, &[]); + } builder.switch_to_block(failure_block); let mut translator = Translator { builder: &mut builder, @@ -1114,8 +1303,9 @@ fn translate_function( types, equality_methods: &program.equality_methods, symbol, + function, local_types: &function.locals, - caller_acquisitions: &caller_acquisitions, + caller_acquisitions, taking_binding: None, test_ownership: matches!( function.identity.declaration.kind, @@ -1126,15 +1316,39 @@ fn translate_function( generation_slots: &generation_slots, next_temporary: temporary_slots.len(), failure_block, + resource: None, }; + translator.resource = resource_layout.map(|layout| resource_execution::ResourceEmission { + layout, + function: function.id, + scope: resource_scope, + return_companion: resource_slots.expect("selected Resource storage").0, + scope_finished: resource_slots.expect("selected Resource storage").1, + status: resource_status, + frames: &resource_frames, + owners: &resource_owners, + loans: &resource_loans, + carrier_owners: &carrier_owners, + carrier_builders: &carrier_builders, + carrier_loans: &carrier_loans, + failure: resource_failure.expect("selected Resource failure block"), + block: function.entry, + position: jett_mir::ResourcePosition::Terminator, + }); translator.drop_all()?; - let mut results = match clif_type(types, function.return_type, "failure return")? { + translator.resource_failure_cleanup()?; + let mut results = match resource_execution::clif_type( + resource_layout, + types, + function.return_type, + "failure return", + )? { None => vec![], Some(ir::types::F32) => vec![translator.builder.ins().f32const(0.0)], Some(ir::types::F64) => vec![translator.builder.ins().f64const(0.0)], Some(t) => vec![translator.builder.ins().iconst(t, 0)], }; - if is_task_scalar(types, function.return_type)? { + if resource_execution::task_scalar(resource_layout, types, function.return_type)? { results.push(translator.builder.ins().iconst(ir::types::I64, 0)); } translator.builder.ins().return_(&results); @@ -1175,6 +1389,7 @@ struct Translator<'a, 'builder> { actor_state_range: Option<(usize, usize)>, types: &'a TypeInterner, symbol: &'a str, + function: &'a Function, local_types: &'a [jett_mir::Local], caller_acquisitions: &'a jett_mir::CallerAcquisitions<'a>, taking_binding: Option, @@ -1184,11 +1399,28 @@ struct Translator<'a, 'builder> { generation_slots: &'a [NativeCallGenerationStorage], next_temporary: usize, failure_block: ir::Block, + resource: Option>, } impl Translator<'_, '_> { - fn statement(&mut self, statement: &Statement) -> Result<(), CodegenError> { + fn statement( + &mut self, + block: jett_mir::BlockId, + index: usize, + statement: &Statement, + ) -> Result<(), CodegenError> { + if self.ordinary_borrowed_sum_statement(block, index, statement)? { + return Ok(()); + } + if self.resource_statement(statement)? { + return Ok(()); + } match &statement.kind { + StatementKind::ResourceCall(_) + | StatementKind::ResourceLexicalExit(_) + | StatementKind::ResourceConstructor(_) => Err(resource_layout::pending( + "Resource node has no constructor-owned native family", + )), StatementKind::ReflectedContainerReady { source, kind } => { let ty = self.local_types[source.index() as usize].ty; let expression = Expression { @@ -1215,6 +1447,22 @@ impl Translator<'_, '_> { )?; Ok(()) } + StatementKind::ReflectedValueReady { + source, + inspect_pending, + } => { + if !*inspect_pending { + return Ok(()); + } + let ty = self.local_types[source.index() as usize].ty; + let expression = Expression { + kind: ExpressionKind::Local(*source), + ty, + span: statement.span, + }; + let value = self.argument(&expression, true)?; + self.reflected_value_ready(value, ty, statement.span) + } StatementKind::IterationBorrow { .. } => Ok(()), StatementKind::SequenceLength { source, target } | StatementKind::SequenceGet { source, target, .. } => { @@ -1705,6 +1953,9 @@ impl Translator<'_, '_> { } fn return_value(&mut self, value: Option<&Expression>, span: Span) -> Result<(), CodegenError> { + if self.resource.is_some() { + return self.resource_return(value, span); + } let mut result = match value { Some(value) => self.expression(value)?, None => self.nothing(), @@ -1743,6 +1994,7 @@ impl Translator<'_, '_> { } fn terminator(&mut self, terminator: &Terminator) -> Result<(), CodegenError> { + self.carrier_iteration_boundary(terminator.span)?; match &terminator.kind { TerminatorKind::Return(value) => self.return_value(value.as_ref(), terminator.span), TerminatorKind::Goto(target) => { @@ -1787,6 +2039,9 @@ impl Translator<'_, '_> { variants, otherwise, } => { + if self.carrier_switch(scrutinee, variants, *otherwise, terminator.span)? { + return Ok(()); + } let lowered = self.expression(scrutinee)?; let handle = self.scalar(lowered, scrutinee.span)?; self.check_struct_pending_access( @@ -1899,8 +2154,22 @@ impl Translator<'_, '_> { } fn expression_inner(&mut self, expression: &Expression) -> Result { - let kind = scalar_kind(self.types, expression.ty, "native expression")?; + if let Some(value) = self.resource_expression(expression)? { + return Ok(value); + } + let kind = if self.resource_is_named_value(expression)? { + ScalarKind::Function + } else { + scalar_kind(self.types, expression.ty, "native expression")? + }; match &expression.kind { + ExpressionKind::ResourceHookValue { .. } | ExpressionKind::ResourceInvoke { .. } => { + Err(contract_error( + self.symbol, + expression.span, + "pending ResourceOwnershipPlan: native Resource custody and descriptor emission are not admitted", + )) + } ExpressionKind::Int(value) => { let ty = self.required_clif_type(expression.ty, expression.span)?; let immediate = integer_immediate(*value, kind).ok_or_else(|| { @@ -2004,6 +2273,7 @@ impl Translator<'_, '_> { } } ExpressionKind::FunctionRef(function) => { + self.resource_validate_named_producer(expression, *function)?; self.function_descriptor(*function, None, expression.span) } ExpressionKind::ClosureRef { function, captures } => { @@ -2106,6 +2376,19 @@ impl Translator<'_, '_> { right.ty, expression.span, )?; + if operand_kind == ScalarKind::Actor { + let not_equal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(*op == BinaryOp::NotEqual)); + let value = self.leaf( + NativeLeaf::ActorIdentityEqual, + &[left_value, right_value, not_equal], + true, + )?; + let value = self.builder.ins().ireduce(ir::types::I8, value); + return Ok(LoweredValue::Scalar(value)); + } if operand_kind == ScalarKind::String { let leaf = if *op == BinaryOp::NotEqual { NativeLeaf::StringNotEqual @@ -2339,6 +2622,7 @@ impl Translator<'_, '_> { intrinsic, type_arguments, reflection_arguments, + field_validation, args, evaluation_order, .. @@ -2346,6 +2630,7 @@ impl Translator<'_, '_> { *intrinsic, type_arguments, reflection_arguments, + field_validation.as_ref(), args, evaluation_order, expression.ty, @@ -2822,6 +3107,19 @@ impl Translator<'_, '_> { evaluation_order: &[usize], expression: &Expression, ) -> Result { + if let ExpressionKind::Call { + ownership: jett_hir::CallOwnership::Generated(generated), + .. + } = &expression.kind + && matches!( + generated.operation, + jett_hir::GeneratedOperation::InterfaceDispatch { .. } + ) + { + for argument in args { + self.preflight_interface_dispatch_argument(argument)?; + } + } let environment = self.builder.ins().iconst(ir::types::I64, 0); self.call_with_environment( function, @@ -4084,7 +4382,7 @@ function root() returns int64: assert!( matches!(result, Err(CodegenError::InvalidMir(errors)) if errors.len() == 1 && errors[0].message - == "call ownership local metadata is outside its function or interner"), + == "Resource manifest type visitor is outside its interner"), "enclosing metadata corruption {corruption}" ); assert!(matches!( @@ -4575,12 +4873,26 @@ function root() returns nothing: } else { local.ty = wrapped; } + let result = emit_host_object(&program, &types); + if matches!(corruption, 1 | 2) { + assert!( + matches!(result, Err(CodegenError::InvalidMir(errors)) + if errors.len() == 1 && errors[0].message + == "Resource manifest type visitor is outside its interner"), + "debug={debug_type}, corruption={corruption}" + ); + } else { + assert!( + matches!(result, Err(CodegenError::UnsupportedType { .. })), + "debug={debug_type}, corruption={corruption}" + ); + } assert!( matches!( - emit_host_object(&program, &types), + crate::verify::scalar_kind(&types, wrapped, "absent original metadata"), Err(CodegenError::UnsupportedType { .. }) ), - "debug={debug_type}, corruption={corruption}" + "native wrapped metadata debug={debug_type}, corruption={corruption}" ); } } @@ -4857,12 +5169,26 @@ function root() returns int64: local.ty = wrapped; } latent.locals.push(local); + let result = emit_host_object(&program, &types); + if corruption == 1 { + assert!( + matches!(result, Err(CodegenError::InvalidMir(errors)) + if errors.len() == 1 && errors[0].message + == "Resource manifest type visitor is outside its interner"), + "debug={debug_type}, corruption={corruption}" + ); + } else { + assert!( + matches!(result, Err(CodegenError::UnsupportedType { .. })), + "debug={debug_type}, corruption={corruption}" + ); + } assert!( matches!( - emit_host_object(&program, &types), + crate::verify::scalar_kind(&types, wrapped, "latent original metadata"), Err(CodegenError::UnsupportedType { .. }) ), - "debug={debug_type}, corruption={corruption}" + "native wrapped metadata debug={debug_type}, corruption={corruption}" ); } } diff --git a/crates/jett_codegen_cranelift/src/emit/debug.rs b/crates/jett_codegen_cranelift/src/emit/debug.rs index 9a5d7929..b42e27ad 100644 --- a/crates/jett_codegen_cranelift/src/emit/debug.rs +++ b/crates/jett_codegen_cranelift/src/emit/debug.rs @@ -303,7 +303,8 @@ pub(crate) fn equality_layout( | Type::String | Type::Bool | Type::Bytes - | Type::Nothing => true, + | Type::Nothing + | Type::Actor(_) => true, Type::List(element) | Type::Set(element) | Type::Optional(element) => { supported(types, *element, seen, methods) } @@ -336,7 +337,6 @@ pub(crate) fn equality_layout( | Type::TypeConstruction | Type::Never | Type::Interface(_) - | Type::Actor(_) | Type::Resource(_) | Type::Capability(_) | Type::Function { .. } diff --git a/crates/jett_codegen_cranelift/src/emit/ordinary_borrowed_sums.rs b/crates/jett_codegen_cranelift/src/emit/ordinary_borrowed_sums.rs new file mode 100644 index 00000000..ae259983 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/ordinary_borrowed_sums.rs @@ -0,0 +1,239 @@ +//! Nonconsuming ordinary sum payloads require their original checked Handle proof. +use super::*; +use jett_mir::{BlockId, OrdinaryBorrowedSumProjection, OrdinarySumPayloadPath}; + +#[cfg(test)] +mod tests; + +/// Public admission checks the private current graph before any ordinary +/// initializer or consuming sum path can reinterpret a stale projection. +pub(super) fn validate_public_program( + program: &Program, + types: &TypeInterner, +) -> Result<(), CodegenError> { + let mut errors = Vec::new(); + for function in &program.functions { + for block in &function.blocks { + for (index, statement) in block.statements.iter().enumerate() { + let result = validate_statement(function, types, block.id, index, statement) + .and_then(|()| match statement.kind { + StatementKind::SumTag { source, .. } + | StatementKind::SumTake { source, .. } + if function.is_view_local(source) => + { + function + .ordinary_borrowed_sum_projection(block.id, index)? + .ok_or_else(|| { + "borrowed ordinary sum has no original projection proof".into() + }) + .map(|_| ()) + } + _ => Ok(()), + }); + if let Err(message) = result { + errors.push(jett_mir::ValidationError { + span: statement.span, + message, + }); + } + } + } + } + if errors.is_empty() { + Ok(()) + } else { + Err(CodegenError::InvalidMir(errors)) + } +} + +pub(crate) fn validate_statement( + function: &Function, + types: &TypeInterner, + block: BlockId, + index: usize, + statement: &Statement, +) -> Result<(), String> { + match &statement.kind { + StatementKind::SumTag { .. } | StatementKind::SumTake { .. } => { + if let Some(row) = function.ordinary_borrowed_sum_projection(block, index)? { + validate_types(function, types, row)?; + } + } + StatementKind::Let { local, value } | StatementKind::BeginCallView { local, value } => { + if function.ordinary_borrowed_sum_alias(*local, value)? + && function.local(*local).is_none() + { + return Err("ordinary sum alias lost its exact local header".into()); + } + } + _ => {} + } + Ok(()) +} + +fn validate_types( + function: &Function, + types: &TypeInterner, + row: &OrdinaryBorrowedSumProjection, +) -> Result<(), String> { + let source = function + .local(row.source()) + .ok_or("ordinary sum source is absent")?; + let backing = function + .local(row.backing()) + .ok_or("ordinary sum backing is absent")?; + let output = function + .local(row.output()) + .ok_or("ordinary sum output is absent")?; + let tag = function + .local(row.tag()) + .ok_or("ordinary sum tag is absent")?; + if source.ty != row.sum_type() + || backing.ty != row.sum_type() + || output.ty != row.payload_type() + || tag.ty != TypeInterner::BOOL + || !function.is_view_local(row.source()) + || !function.is_view_local(row.output()) + || source.view_source != Some(row.backing()) + || output.view_source != Some(row.backing()) + { + return Err("ordinary sum projection changed its exact types or backing origin".into()); + } + match (types.resolve(row.sum_type()), row.path()) { + (Type::Optional(payload), OrdinarySumPayloadPath::OptionalSome) + if *payload == row.payload_type() && row.error().is_none() => {} + (Type::Result(payload, error), OrdinarySumPayloadPath::ResultOk) + if *payload == row.payload_type() && *error == TypeInterner::STRING => + { + let error = row + .error() + .and_then(|local| function.local(local)) + .ok_or("ordinary Result projection lost its String companion")?; + if error.ty != TypeInterner::STRING || function.is_view_local(error.id) { + return Err("ordinary Result failure must own one String copy".into()); + } + } + _ => return Err("ordinary sum projection changed its selected nominal payload".into()), + } + Ok(()) +} + +impl Translator<'_, '_> { + pub(super) fn ordinary_borrowed_sum_statement( + &mut self, + block: BlockId, + index: usize, + statement: &Statement, + ) -> Result { + let symbol = self.symbol; + let fail = |message| contract_error(symbol, statement.span, message); + match &statement.kind { + StatementKind::Let { local, value } | StatementKind::BeginCallView { local, value } => { + if self + .function + .ordinary_borrowed_sum_alias(*local, value) + .map_err(fail)? + { + // The original header selects either a nonowning alias or + // an ordinary copyable final binding. Projected bits stay + // borrowed until that exact destination is initialized. + if self.local_types[local.index() as usize] + .view_source + .is_some() + && self.local_slots[local.index() as usize].is_some() + { + return Err(fail( + "ordinary borrowed payload gained owning storage".into(), + )); + } + let value = self.argument(value, true)?; + self.define_local(*local, value, statement.span)?; + return Ok(true); + } + return Ok(false); + } + StatementKind::SumTag { .. } | StatementKind::SumTake { .. } => {} + _ => return Ok(false), + } + let Some(row) = self + .function + .ordinary_borrowed_sum_projection(block, index) + .map_err(fail)? + else { + return Ok(false); + }; + validate_types(self.function, self.types, row).map_err(fail)?; + let sum_type = row.sum_type(); + let source = Expression { + kind: ExpressionKind::Local(row.source()), + ty: sum_type, + span: statement.span, + }; + let value = self.argument(&source, true)?; + let value = self.scalar(value, statement.span)?; + match statement.kind { + StatementKind::SumTag { target, .. } => { + let layout = debug::debug_layout(self.types, sum_type) + .ok_or_else(|| self.unsupported(statement.span, "ordinary sum debug layout"))?; + let (pointer, length) = self.static_data(&layout)?; + let tag = self.leaf(NativeLeaf::SumHandleTag, &[value, pointer, length], true)?; + let tag = self.builder.ins().ireduce(ir::types::I8, tag); + self.define_local(target, LoweredValue::Scalar(tag), statement.span)?; + } + StatementKind::SumTake { + target, success, .. + } => { + if success && self.local_slots[target.index() as usize].is_some() { + return Err(fail( + "ordinary success payload gained owning storage".into(), + )); + } + if !success && self.local_slots[target.index() as usize].is_none() { + return Err(fail("ordinary failure String lost owning storage".into())); + } + let tag = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(success)); + let depth = self.leaf(NativeLeaf::SumPayloadPendingDepth, &[value], true)?; + let bits = self.leaf(NativeLeaf::SumPayloadBorrow, &[value, tag], true)?; + let ty = self.local_types[target.index() as usize].ty; + let value = self.ordinary_borrowed_payload(bits, ty, depth, statement.span)?; + self.define_local(target, value, statement.span)?; + } + _ => unreachable!("projection query admits only tag and payload rows"), + } + Ok(true) + } + + /// Keep opaque carriers borrowed; only primitive payload bits need a native + /// bitcast or width reduction. No temporary owner is created here. + fn ordinary_borrowed_payload( + &mut self, + bits: Value, + ty: TypeId, + depth: Value, + span: Span, + ) -> Result { + let native = self.required_clif_type(ty, span)?; + let value = if native == ir::types::F64 { + self.builder + .ins() + .bitcast(native, ir::MemFlags::new(), bits) + } else if native == ir::types::F32 { + let bits = self.builder.ins().ireduce(ir::types::I32, bits); + self.builder + .ins() + .bitcast(native, ir::MemFlags::new(), bits) + } else if native != ir::types::I64 { + self.builder.ins().ireduce(native, bits) + } else { + bits + }; + if is_task_scalar(self.types, ty)? { + Ok(LoweredValue::ScalarTask(value, depth)) + } else { + Ok(LoweredValue::Scalar(value)) + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/ordinary_borrowed_sums/tests.rs b/crates/jett_codegen_cranelift/src/emit/ordinary_borrowed_sums/tests.rs new file mode 100644 index 00000000..e3754b80 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/ordinary_borrowed_sums/tests.rs @@ -0,0 +1,518 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; +use std::collections::HashMap; + +const SOURCE: &str = r#"namespace app +function project_optional(view source: optional[list[int64]]) returns int64: + list[int64] alias = view((view source) handle: + return 0 + ) + list[int64] forwarded = alias + return 1 +function project_result(view source: result[list[int64], string]) returns int64: + list[int64] alias = view((view source) handle error: + return 0 + ) + return 1 +function main() returns nothing: + optional[list[int64]] present = some(list(2, 3)) + optional[list[int64]] absent = none + result[list[int64], string] success = ok(list(5, 7)) + result[list[int64], string] failure = fail("absent") + int64 first = project_optional(view present) + int64 second = project_optional(view absent) + int64 third = project_result(view success) + int64 fourth = project_result(view failure) + int64 combined = first + second + third + fourth + return nothing +"#; + +fn lower(source: &str, release: bool) -> (Program, TypeInterner) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check_with_options( + &parsed.module, + &resolved, + jett_typecheck::CheckOptions { release }, + ); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let mut hir = jett_hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .unwrap(); + jett_hir::complete_value_conversions(&mut hir, &checked.interner).unwrap(); + let mir = jett_mir::lower(&hir, &checked.interner).unwrap(); + (mir, checked.interner) +} + +fn object( + program: &Program, + types: &TypeInterner, + release: bool, +) -> Result { + let entry = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .unwrap() + .id; + crate::emit_host_program_object_with_options( + program, + types, + entry, + CodegenOptions { optimize: release }, + ) +} + +fn projection(program: &Program, name: &str) -> (FunctionId, BlockId, usize) { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + for block in &function.blocks { + for (index, statement) in block.statements.iter().enumerate() { + if matches!(statement.kind, StatementKind::SumTake { success: true, .. }) + && function + .ordinary_borrowed_sum_projection(block.id, index) + .unwrap() + .is_some() + { + return (function.id, block.id, index); + } + } + } + panic!("genuine {name} has no projected success"); +} + +/// Public lowering constructs an ordinary empty seed. Copying editable fields +/// into it cannot mint the private original borrowed-Handle proof. +fn without_proof(original: &Function, release: bool) -> Function { + let (mut seed, _) = lower( + "namespace app\nfunction main() returns nothing:\n return nothing\n", + release, + ); + let mut function = seed.functions.remove(0); + function.id = original.id; + function.identity = original.identity.clone(); + function.debug_kind = original.debug_kind.clone(); + function.params = original.params.clone(); + function.capture_count = original.capture_count; + function.return_type = original.return_type; + function.locals = original.locals.clone(); + function.entry = original.entry; + function.blocks = original.blocks.clone(); + function.span = original.span; + function +} + +#[derive(Debug, Clone, Copy)] +enum Mutation { + Copy, + Retarget, + Source, + OutputOrigin, + PayloadType, + Guard, + MissingProof, +} + +#[test] +fn ordinary_borrowed_sum_public_object_rejects_forged_current_projections() { + for release in [false, true] { + let (baseline, types) = lower(SOURCE, release); + let emitted = object(&baseline, &types, release).expect("genuine checked Source object"); + assert!(!emitted.bytes.is_empty()); + let (function, block, index) = projection(&baseline, "project_optional"); + for mutation in [ + Mutation::Copy, + Mutation::Retarget, + Mutation::Source, + Mutation::OutputOrigin, + Mutation::PayloadType, + Mutation::Guard, + Mutation::MissingProof, + ] { + let mut forged = baseline.clone(); + let function = &mut forged.functions[function.index() as usize]; + let row = function + .ordinary_borrowed_sum_projection(block, index) + .unwrap() + .unwrap(); + let output = row.output(); + let tag = row.tag(); + match mutation { + Mutation::Copy => { + let copy = function.blocks[block.index() as usize].statements[index].clone(); + function.blocks[block.index() as usize] + .statements + .insert(index, copy); + } + Mutation::Retarget => { + let moved = function.blocks[block.index() as usize] + .statements + .remove(index); + function.blocks[function.entry.index() as usize] + .statements + .insert(0, moved); + } + Mutation::Source => { + let StatementKind::SumTake { source, .. } = + &mut function.blocks[block.index() as usize].statements[index].kind + else { + unreachable!() + }; + *source = output; + } + Mutation::OutputOrigin => { + function.locals[output.index() as usize].view_source = None + } + Mutation::PayloadType => { + function.locals[output.index() as usize].ty = TypeInterner::INT64 + } + Mutation::Guard => { + let branch = function.blocks.iter_mut().find(|block| + matches!(&block.terminator.kind, TerminatorKind::Branch { condition, .. } + if matches!(condition.kind, ExpressionKind::Local(local) if local == tag))).unwrap(); + let TerminatorKind::Branch { + then_block, + else_block, + .. + } = &mut branch.terminator.kind + else { + unreachable!() + }; + std::mem::swap(then_block, else_block); + } + Mutation::MissingProof => *function = without_proof(function, release), + } + match object(&forged, &types, release) { + Err(CodegenError::InvalidMir(errors)) => { + assert!(!errors.is_empty(), "{mutation:?} release={release}") + } + result => panic!( + "{mutation:?} release={release}: public emission did not reject private-proof corruption: {result:?}" + ), + } + } + } +} + +fn translated( + program: &Program, + types: &TypeInterner, + name: &str, +) -> (ir::Function, HashMap) { + let mut prepared = program.clone(); + jett_mir::prepare_native_sequences(&mut prepared, types); + jett_mir::prepare_native_uninhabited_sums(&mut prepared, types); + jett_mir::prepare_native_generated_functions(&mut prepared, types); + let verified = verify_program(&prepared, types).unwrap(); + let flags = settings::Flags::new(settings::builder()); + let isa = isa::lookup(HOST).unwrap().finish(flags).unwrap(); + let mut module = ObjectModule::new( + ObjectBuilder::new( + isa, + b"ordinary-projection-test".to_vec(), + default_libcall_names(), + ) + .unwrap(), + ); + let declarations = + declare_reachable_functions(&mut module, &prepared, types, &verified).unwrap(); + let function = prepared + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + let declaration = declarations.get(function.id).unwrap(); + let mut context = module.make_context(); + context.func.signature = declaration.signature.clone(); + translate_function( + &mut module, + &declarations, + &prepared, + function, + types, + &declaration.symbol, + &mut context, + ) + .unwrap(); + let names = context + .func + .dfg + .ext_funcs + .iter() + .map(|(reference, data)| { + let ir::ExternalName::User(user) = data.name else { + panic!("unexpected import name") + }; + let name = &context.func.params.user_named_funcs()[user]; + ( + reference, + module + .declarations() + .get_function_decl(FuncId::from_u32(name.index)) + .name + .clone() + .expect("declared runtime symbol"), + ) + }) + .collect(); + (context.func, names) +} + +fn calls(function: &ir::Function, names: &HashMap) -> Vec<(ir::Inst, String)> { + function + .layout + .blocks() + .flat_map(|block| function.layout.block_insts(block)) + .filter_map(|instruction| { + let ir::InstructionData::Call { func_ref, .. } = function.dfg.insts[instruction] else { + return None; + }; + names + .get(&func_ref) + .cloned() + .map(|name| (instruction, name)) + }) + .collect() +} + +#[test] +fn ordinary_borrowed_sum_emission_keeps_success_borrowed_and_copies_only_failure_string() { + for release in [false, true] { + let (program, types) = lower(SOURCE, release); + object(&program, &types, release).expect("checked public baseline"); + for (name, copies) in [("project_optional", 0), ("project_result", 1)] { + let (function, names) = translated(&program, &types, name); + let calls = calls(&function, &names); + assert!( + calls + .iter() + .any(|(_, name)| name == "jett_rt_v1_sum_handle_tag") + ); + assert!( + calls + .iter() + .any(|(_, name)| name == "jett_rt_v1_sum_payload_borrow") + ); + assert_eq!( + calls + .iter() + .filter(|(_, name)| name == NativeLeaf::Retain.symbol()) + .count(), + copies, + "{name} release={release} calls={calls:?}\n{}", + function.display() + ); + assert!( + !calls.iter().any( + |(_, name)| name == "jett_rt_v1_sum_take" || name == "jett_rt_v1_sum_clone" + ) + ); + // The payload leaf consumes the borrowed reader's opaque bits and + // never clears/stores its source slot; View formals have no slot. + let takes = calls + .iter() + .filter(|(_, name)| name == "jett_rt_v1_sum_payload_borrow") + .count(); + assert_eq!(takes, if copies == 0 { 1 } else { 2 }); + } + } +} + +#[test] +fn ordinary_borrowed_sum_scalar_width_and_pending_metadata_emit_without_owner_adoption() { + for release in [false, true] { + for (ty, literal, opcode) in [ + ("uint8", "7", ir::Opcode::Ireduce), + ("int16", "7", ir::Opcode::Ireduce), + ("float32", "1.5", ir::Opcode::Bitcast), + ("float64", "1.5", ir::Opcode::Bitcast), + ("int64", "7", ir::Opcode::Iconst), + ] { + let source = format!( + "namespace app\nfunction payload(view source: optional[{ty}]) returns {ty}:\n {ty} alias = view((view source) handle:\n return {literal}\n )\n return alias\nfunction main() returns nothing:\n {ty} value = {literal}\n optional[{ty}] source = some(run run value)\n {ty} read = payload(view source)\n return nothing\n" + ); + let (program, types) = lower(&source, release); + object(&program, &types, release) + .unwrap_or_else(|error| panic!("{ty} release={release}: {error:?}")); + let (function, names) = translated(&program, &types, "payload"); + let calls = calls(&function, &names); + assert!( + calls + .iter() + .any(|(_, name)| name == "jett_rt_v1_sum_payload_pending_depth") + ); + assert!( + calls + .iter() + .any(|(_, name)| name == "jett_rt_v1_sum_payload_borrow") + ); + assert!( + !calls + .iter() + .any(|(_, name)| name == NativeLeaf::Retain.symbol() + || name == "jett_rt_v1_sum_take") + ); + assert!( + function + .layout + .blocks() + .flat_map(|block| function.layout.block_insts(block)) + .any(|instruction| function.dfg.insts[instruction].opcode() == opcode) + ); + assert_eq!( + function.signature.returns.len(), + 2, + "primitive pending depth remains in the actual ABI" + ); + let depths = calls + .iter() + .filter(|(_, name)| name == "jett_rt_v1_sum_payload_pending_depth") + .map(|(instruction, _)| function.dfg.inst_results(*instruction)[0]) + .collect::>(); + assert!( + function + .layout + .blocks() + .flat_map(|block| function.layout.block_insts(block)) + .filter(|instruction| function.dfg.insts[*instruction].opcode() + == ir::Opcode::Return) + .any( + |instruction| function.dfg.inst_args(instruction).get(1).is_some_and( + |depth| depths + .iter() + .any(|original| function.dfg.resolve_aliases(*depth) + == function.dfg.resolve_aliases(*original)) + ) + ), + "{ty}: actual payload pending depth reaches the return; it is not reset to zero" + ); + } + } +} + +const STRING_SUCCESS: &str = r#"namespace app +function copied_optional(view source: optional[string]) returns string: + string alias = view((view source) handle: + return "missing" + ) + return alias +function copied_result(view source: result[string, string]) returns string: + string alias = view((view source) handle error: + return error + ) + return alias +function main() returns nothing: + optional[string] optional_source = some("optional") + result[string, string] result_source = ok("result") + string first = copied_optional(view optional_source) + string second = copied_optional(view optional_source) + string third = copied_result(view result_source) + string fourth = copied_result(view result_source) + return nothing +"#; + +fn integer_constant(function: &ir::Function, value: Value) -> Option { + let ir::ValueDef::Result(instruction, _) = + function.dfg.value_def(function.dfg.resolve_aliases(value)) + else { + return None; + }; + match function.dfg.insts[instruction] { + ir::InstructionData::UnaryImm { + opcode: ir::Opcode::Iconst, + imm, + .. + } => Some(imm.bits()), + _ => None, + } +} + +#[test] +fn ordinary_borrowed_sum_string_success_copies_final_binding_once_from_nonowning_projection() { + for release in [false, true] { + let (program, types) = lower(STRING_SUCCESS, release); + let emitted = object(&program, &types, release) + .expect("genuine checked String-success public object"); + assert!(!emitted.bytes.is_empty()); + for name in ["copied_optional", "copied_result"] { + let (id, block, index) = projection(&program, name); + let source = &program.functions[id.index() as usize]; + let row = source + .ordinary_borrowed_sum_projection(block, index) + .unwrap() + .unwrap(); + let final_alias = source.local(row.alias()).unwrap(); + assert_eq!(final_alias.ty, TypeInterner::STRING); + assert_eq!( + final_alias.view_source, None, + "preserve the checked String copy header" + ); + let storage = MoveValuePlan::analyze(&program, source, &types).unwrap(); + assert!( + storage + .owned_locals + .contains(&(row.alias().index() as usize)) + ); + assert!( + !storage + .owned_locals + .contains(&(row.output().index() as usize)), + "projected temporary must remain nonowning" + ); + assert_eq!( + source.local(row.output()).unwrap().view_source, + Some(row.backing()) + ); + + let (function, names) = translated(&program, &types, name); + let calls = calls(&function, &names); + assert!( + !calls.iter().any( + |(_, name)| name == "jett_rt_v1_sum_take" || name == "jett_rt_v1_sum_clone" + ) + ); + let success = calls + .iter() + .filter(|(_, name)| name == "jett_rt_v1_sum_payload_borrow") + .filter(|(instruction, _)| { + integer_constant(&function, function.dfg.inst_args(*instruction)[2]) == Some(1) + }) + .map(|(instruction, _)| function.dfg.inst_results(*instruction)[0]) + .collect::>(); + assert_eq!(success.len(), 1, "one guarded success projection"); + let payload = function.dfg.resolve_aliases(success[0]); + let alias_copies = calls + .iter() + .filter(|(_, name)| name == NativeLeaf::Retain.symbol()) + .filter(|(instruction, _)| { + function + .dfg + .resolve_aliases(function.dfg.inst_args(*instruction)[1]) + == payload + }) + .count(); + assert_eq!( + alias_copies, + 1, + "the final String binding retains exactly once directly from borrowed bits; later Return copies are distinct; calls={calls:?}\n{}", + function.display() + ); + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution.rs new file mode 100644 index 00000000..52ac9fdc --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution.rs @@ -0,0 +1,2254 @@ +//! Dedicated Source family. The layout constructor owns all entry and custody decisions. +use super::resource_layout::{EmittedResourceLayout, pending}; +use super::*; +use jett_mir::{ + ResourceCallOperand as Operand, ResourceCallResult as CallResult, + ResourceFrameRole as FrameRole, ResourceOperationRole as Role, ResourceSlotStorage as Storage, +}; +mod carriers; +mod leaves; +mod lexical_borrows; +mod named_indirect; +mod returned_hooks; +mod sum_views; +#[cfg(test)] +mod tests; +use leaves::Leaf; + +#[derive(Clone, Copy)] +pub(super) struct ResourceEmission<'a> { + pub(super) layout: &'a EmittedResourceLayout<'a>, + pub(super) function: FunctionId, + pub(super) scope: Variable, + pub(super) status: Variable, + pub(super) frames: &'a [ir::StackSlot], + pub(super) owners: &'a [ir::StackSlot], + pub(super) return_companion: ir::StackSlot, + pub(super) scope_finished: ir::StackSlot, + pub(super) loans: &'a [ir::StackSlot], + pub(super) carrier_owners: &'a [ir::StackSlot], + pub(super) carrier_builders: &'a [ir::StackSlot], + pub(super) carrier_loans: &'a [ir::StackSlot], + pub(super) failure: ir::Block, + pub(super) block: jett_mir::BlockId, + pub(super) position: jett_mir::ResourcePosition, +} +impl<'a> ResourceEmission<'a> { + fn function_plan(self) -> Result<&'a jett_mir::ResourceFunctionPlan, CodegenError> { + self.layout + .plan() + .function(self.function) + .ok_or_else(|| pending("native family lost its exact function plan")) + } + fn at_site(self) -> Result, CodegenError> { + Ok(self + .function_plan()? + .operations() + .iter() + .filter(|operation| { + operation.site().block() == self.block + && operation.site().position() == self.position + }) + .collect()) + } + fn ordinal(self, operation: &jett_mir::ResourceOperation) -> Result { + self.layout + .operation(self.function, operation.id()) + .ok_or_else(|| pending("native operation has no exact installed row")) + } + fn local_slot( + self, + local: jett_mir::LocalId, + ) -> Result, CodegenError> { + Ok(self + .function_plan()? + .owner_slots() + .iter() + .find_map(|slot| match slot.storage() { + Storage::Local { header } if header.id == local => Some(slot.id()), + _ => None, + })) + } +} + +pub(super) fn clif_type( + layout: Option<&EmittedResourceLayout<'_>>, + types: &TypeInterner, + ty: TypeId, + role: &str, +) -> Result, CodegenError> { + if layout.is_some_and(|layout| layout.plan().type_requires_custody(ty)) { + Ok(Some(ir::types::I64)) + } else { + super::clif_type(types, ty, role) + } +} +pub(super) fn task_scalar( + layout: Option<&EmittedResourceLayout<'_>>, + types: &TypeInterner, + ty: TypeId, +) -> Result { + if layout.is_some_and(|layout| layout.plan().type_requires_custody(ty)) { + Ok(false) + } else { + is_task_scalar(types, ty) + } +} +fn signature( + module: &ObjectModule, + function: &Function, + types: &TypeInterner, + layout: &EmittedResourceLayout<'_>, +) -> Result { + if layout.plan().function(function.id).is_none() { + return super::signature(module, function, types); + } + let mut signature = module.make_signature(); + signature.params.extend([ + AbiParam::new(ir::types::I64), + AbiParam::new(ir::types::I64), + AbiParam::new(ir::types::I64), + ]); + for parameter in &function.params { + if let Some(ty) = clif_type( + Some(layout), + types, + parameter.ty, + "Resource-family parameter", + )? { + signature.params.push(AbiParam::new(ty)); + if task_scalar(Some(layout), types, parameter.ty)? { + signature.params.push(AbiParam::new(ir::types::I64)); + } + } + } + if let Some(ty) = clif_type( + Some(layout), + types, + function.return_type, + "Resource-family return", + )? { + signature.returns.push(AbiParam::new(ty)); + if task_scalar(Some(layout), types, function.return_type)? { + signature.returns.push(AbiParam::new(ir::types::I64)); + } + } + Ok(signature) +} + +pub(super) fn emit( + program: &Program, + types: &TypeInterner, + target: Triple, + entry: Option, + options: CodegenOptions, +) -> Result { + let entry = entry.ok_or_else(|| { + pending("Resource executable requires the exact driver-selected entry FunctionId") + })?; + jett_mir::validate(program).map_err(CodegenError::InvalidMir)?; + let original_plan = jett_mir::validate_resource_ownership_for_entry(program, types, entry) + .map_err(CodegenError::InvalidMir)?; + for function in &program.functions { + jett_mir::move_values::validate_local_view_initializers(function, types).map_err( + |message| contract_error(&function.identity.declaration.name, function.span, message), + )?; + } + crate::verify::verify_resource_descriptor_bodies(&original_plan)?; + let mut prepared = program.clone(); + jett_mir::prepare_native_sequences(&mut prepared, types); + jett_mir::prepare_native_uninhabited_sums(&mut prepared, types); + jett_mir::prepare_native_generated_functions(&mut prepared, types); + let program = &prepared; + let layout = EmittedResourceLayout::from_program(program, types, entry)?; + let verified = crate::verify::verify_resource_program(layout.plan())?; + let mut flags = settings::builder(); + if options.optimize { + use cranelift_codegen::settings::Configurable; + flags + .set("opt_level", "speed") + .map_err(|error| CodegenError::Backend(error.to_string()))?; + } + let isa = isa::lookup(target.clone()) + .map_err(|error| CodegenError::Backend(error.to_string()))? + .finish(settings::Flags::new(flags)) + .map_err(|error| CodegenError::Backend(error.to_string()))?; + let mut module = ObjectModule::new( + ObjectBuilder::new(isa, b"jett-resource".to_vec(), default_libcall_names()) + .map_err(|error| CodegenError::Backend(error.to_string()))?, + ); + let mut declarations = + DeclaredFunctions::new(program.functions.len(), verified.functions().len()); + for verified in verified.functions() { + let function = program_function(program, verified.mir_id)?; + let signature = signature(&module, function, types, &layout)?; + let native_id = module + .declare_function(&verified.symbol, Linkage::Local, &signature) + .map_err(|error| CodegenError::Backend(error.to_string()))?; + declarations.insert(DeclaredFunction { + mir_id: function.id, + native_id, + symbol: verified.symbol.clone(), + signature, + modes: function.params.iter().map(|param| param.mode).collect(), + parameter_types: function.params.iter().map(|param| param.ty).collect(), + debug_label: debug::function_label(function).map_err(|message| pending(message))?, + })?; + } + for declaration in declarations.iter() { + let function = program_function(program, declaration.mir_id)?; + let mut context = module.make_context(); + context.func.signature = declaration.signature.clone(); + let selected = layout + .plan() + .function(function.id) + .is_some() + .then_some(&layout); + translate_function_inner( + &mut module, + &declarations, + program, + function, + types, + &declaration.symbol, + &mut context, + selected, + )?; + module + .define_function(declaration.native_id, &mut context) + .map_err(|error| { + CodegenError::Backend(format!("Resource-family {}: {error:?}", declaration.symbol)) + })?; + } + layout.define_accessor(&mut module)?; + define_entry(&mut module, &declarations, &layout)?; + let mut symbols = declarations.symbols(); + symbols.push("jett_aot_resource_v1_manifest".into()); + symbols.push(JETT_AOT_ENTRY_SYMBOL_V1.into()); + let bytes = module + .finish() + .emit() + .map_err(|error| CodegenError::Backend(error.to_string()))?; + Ok(ObjectArtifact { + target: target.to_string(), + symbols, + bytes, + }) +} + +impl Translator<'_, '_> { + pub(super) fn resource_validate_scope(&mut self) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("Scope prologue has no selected family"))?; + let scope = self.builder.use_var(resource.scope); + let context = self.builder.use_var(self.runtime_context); + let function = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(resource.function.index())); + let signature = self.builder.ins().iconst( + ir::types::I32, + i64::from( + resource + .layout + .function_signature(resource.function) + .ok_or_else(|| pending("Scope lacks its containing function signature"))?, + ), + ); + let template = self.builder.ins().iconst( + ir::types::I32, + i64::from( + resource + .layout + .frame( + resource.function, + resource.function_plan()?.root_scope().id(), + ) + .ok_or_else(|| pending("Scope template missing"))?, + ), + ); + Leaf::ScopeValidate.checked( + self.module, + self.builder, + &[context, scope, function, signature, template], + resource.failure, + ) + } + pub(super) fn resource_bind_parameters( + &mut self, + function: &Function, + ) -> Result<(), CodegenError> { + let Some(resource) = self.resource else { + return Ok(()); + }; + for parameter in &function.params { + if let Some(slot) = resource.carrier_local_slot(parameter.local)? { + let variable = self.variables[parameter.local.index() as usize] + .ok_or_else(|| pending("carrier formal has no exact native variable"))?; + let value = self.builder.use_var(variable); + self.builder + .ins() + .stack_store(value, resource.carrier_owners[slot.index()], 0); + } + if let Some(slot) = resource.local_slot(parameter.local)? { + let variable = self.variables[parameter.local.index() as usize] + .ok_or_else(|| pending("Resource formal has no carrier variable"))?; + let value = self.builder.use_var(variable); + self.builder + .ins() + .stack_store(value, resource.owners[slot.index()], 0); + } + } + Ok(()) + } + fn resource_context(&mut self) -> Value { + self.builder.use_var(self.runtime_context) + } + fn resource_frame(&mut self, id: jett_mir::ResourceFrameId) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("frame lookup has no fresh native family"))?; + let header = resource + .function_plan()? + .frames() + .get(id.index()) + .ok_or_else(|| pending("frame lookup is outside the fresh plan"))?; + match header.role() { + FrameRole::Scope | FrameRole::Return => Ok(self.builder.use_var(resource.scope)), + FrameRole::Operation => { + Ok(self + .builder + .ins() + .stack_load(ir::types::I64, resource.frames[id.index()], 0)) + } + } + } + fn resource_owner(&mut self, id: jett_mir::ResourceOwnerSlotId) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("owner lookup has no fresh native family"))?; + let slot = resource + .owners + .get(id.index()) + .copied() + .ok_or_else(|| pending("owner storage is outside the exact plan"))?; + Ok(self.builder.ins().stack_load(ir::types::I64, slot, 0)) + } + fn resource_store( + &mut self, + id: jett_mir::ResourceOwnerSlotId, + value: Value, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("owner publication has no fresh native family"))?; + self.builder + .ins() + .stack_store(value, resource.owners[id.index()], 0); + Ok(()) + } + fn resource_operation( + &mut self, + operation: &jett_mir::ResourceOperation, + ) -> Result<(Value, Value, Value), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("operation has no fresh native family"))?; + let context = self.resource_context(); + let frame = self.resource_frame(operation.frame())?; + let ordinal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(resource.ordinal(operation)?)); + Ok((context, frame, ordinal)) + } + fn resource_destination( + &mut self, + operation: &jett_mir::ResourceOperation, + ) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("destination has no fresh native family"))?; + let (context, frame, ordinal) = self.resource_operation(operation)?; + Leaf::DestinationFrame.output( + self.module, + self.builder, + &[context, frame, ordinal], + resource.failure, + 8, + ) + } + fn resource_transfer( + &mut self, + operation: &jett_mir::ResourceOperation, + ) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("transfer has no fresh native family"))?; + let (source, destination) = match operation.role() { + Role::Transfer { + source, + destination, + } => (*source, *destination), + _ => return Err(pending("transfer selected a different exact role")), + }; + let value = self.resource_owner(source)?; + let target = self.resource_destination(operation)?; + let (context, frame, ordinal) = self.resource_operation(operation)?; + let value = Leaf::Transfer.output( + self.module, + self.builder, + &[context, frame, ordinal, value, target], + resource.failure, + 8, + )?; + self.clear_slot(resource.owners[source.index()]); + self.resource_store(destination, value)?; + Ok(value) + } + fn resource_loan(&mut self, id: jett_mir::ResourceLoanId) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("loan lookup has no fresh native family"))?; + match resource.function_plan()?.loans()[id.index()].source() { + jett_mir::ResourceLoanSource::Owner(_) + | jett_mir::ResourceLoanSource::ProjectedSumPayload { .. } + | jett_mir::ResourceLoanSource::CarrierSumProjection { .. } => Ok(self + .builder + .ins() + .stack_load(ir::types::I64, resource.loans[id.index()], 0)), + jett_mir::ResourceLoanSource::IncomingViewFormal { parameter, .. } => { + let header = &resource.function_plan()?.parameters()[parameter]; + let variable = self.variables[header.local.index() as usize] + .ok_or_else(|| pending("resident formal has no exact variable"))?; + Ok(self.builder.use_var(variable)) + } + } + } + fn resource_begin_borrow( + &mut self, + operation: &jett_mir::ResourceOperation, + ) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("borrow has no fresh native family"))?; + let (loan, leaf) = match operation.role() { + Role::Borrow { loan } => (loan, Leaf::BorrowBegin), + Role::BorrowSum { loan } => (loan, Leaf::SumBorrow), + _ => return Err(pending("borrow selected another role")), + }; + let jett_mir::ResourceLoanSource::Owner(source) = + resource.function_plan()?.loans()[loan.index()].source() + else { + return Err(pending( + "resident forwarding cannot create a child core lease", + )); + }; + let owner = self.resource_owner(source)?; + let (context, frame, ordinal) = self.resource_operation(operation)?; + let value = leaf.output( + self.module, + self.builder, + &[context, frame, ordinal, owner], + resource.failure, + 8, + )?; + self.builder + .ins() + .stack_store(value, resource.loans[loan.index()], 0); + Ok(value) + } +} + +impl Translator<'_, '_> { + fn resource_open_operation( + &mut self, + frame: jett_mir::ResourceFrameId, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("operation has no selected family"))?; + let header = &resource.function_plan()?.frames()[frame.index()]; + if header.role() != FrameRole::Operation { + return Err(pending("invocation did not select its Operation frame")); + } + let parent = self.resource_frame( + header + .parent() + .ok_or_else(|| pending("Operation frame has no parent"))?, + )?; + let template = resource + .layout + .frame(resource.function, frame) + .ok_or_else(|| pending("Operation template missing"))?; + let template = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(template)); + let context = self.resource_context(); + let value = Leaf::OperationBegin.output( + self.module, + self.builder, + &[context, parent, template], + resource.failure, + 8, + )?; + self.builder + .ins() + .stack_store(value, resource.frames[frame.index()], 0); + Ok(()) + } + fn resource_drop( + &mut self, + operation: &jett_mir::ResourceOperation, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("drop has no selected family"))?; + let Role::Drop { source, .. } = operation.role() else { + return Err(pending("drop selected another role")); + }; + let owner = self.resource_owner(*source)?; + let (context, frame, ordinal) = self.resource_operation(operation)?; + // A plain owner and a conditional shell have separate runtime protocols. + let leaf = if !matches!( + resource.function_plan()?.owner_slots()[source.index()].shape(), + jett_mir::ResourceShape::Plain { .. } + ) { + Leaf::SumDrop + } else { + Leaf::Close + }; + leaf.checked( + self.module, + self.builder, + &[context, frame, ordinal, owner], + resource.failure, + )?; + self.clear_slot(resource.owners[source.index()]); + Ok(()) + } + fn resource_end_borrow( + &mut self, + operation: &jett_mir::ResourceOperation, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("loan retirement has no selected family"))?; + let (loan, leaf) = match operation.role() { + Role::EndBorrow { loan } => (*loan, Leaf::BorrowEnd), + Role::EndSumBorrow { loan } => (*loan, Leaf::SumBorrowEnd), + _ => { + return Err(pending( + "loan retirement selected a different operation role", + )); + } + }; + let value = self.resource_loan(loan)?; + let (context, frame, ordinal) = self.resource_operation(operation)?; + leaf.checked( + self.module, + self.builder, + &[context, frame, ordinal, value], + resource.failure, + )?; + self.clear_slot(resource.loans[loan.index()]); + Ok(()) + } + fn resource_finish_operation( + &mut self, + operations: &[&jett_mir::ResourceOperation], + frame: jett_mir::ResourceFrameId, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("completion has no selected family"))?; + for operation in operations + .iter() + .copied() + .filter(|operation| operation.frame() == frame) + { + match operation.role() { + Role::EndBorrow { .. } | Role::EndSumBorrow { .. } => { + self.resource_end_borrow(operation)?; + } + Role::Drop { .. } => self.resource_drop(operation)?, + Role::Carrier { .. } => self.carrier_finish_operation(operation)?, + Role::Complete { .. } => { + let (context, active, ordinal) = self.resource_operation(operation)?; + // Clear compiler storage before the fallible retirement. Runtime owns any refusal. + self.clear_slot(resource.frames[frame.index()]); + let zero = self.builder.ins().iconst(ir::types::I64, 0); + self.builder + .ins() + .stack_store(zero, resource.frames[frame.index()], 8); + Leaf::OperationComplete.checked( + self.module, + self.builder, + &[context, active, ordinal], + resource.failure, + )?; + } + _ => {} + } + } + Ok(()) + } + fn resource_stage_actual( + &mut self, + operations: &[&jett_mir::ResourceOperation], + frame: jett_mir::ResourceFrameId, + parameter: usize, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("actual staging has no selected family"))?; + self.carrier_stage_actual(operations, frame, parameter)?; + for operation in operations + .iter() + .copied() + .filter(|operation| operation.frame() == frame) + { + match operation.role() { + Role::Transfer { destination, .. } if matches!(resource.function_plan()?.owner_slots()[destination.index()].storage(), Storage::Argument { parameter: selected, .. } if *selected == parameter) => + { + self.resource_transfer(operation)?; + } + Role::Borrow { loan } | Role::BorrowSum { loan } => { + let for_parameter = resource + .function_plan()? + .loans() + .get(loan.index()) + .is_some_and(|record| record.parameter() == Some(parameter)); + if for_parameter + && !matches!( + resource.function_plan()?.loans()[loan.index()].source(), + jett_mir::ResourceLoanSource::CarrierSumProjection { .. } + ) + { + self.resource_begin_borrow(operation)?; + } + } + _ => {} + } + } + Ok(()) + } + fn resource_operand( + &mut self, + operand: &Operand, + ordinary: &[Option], + _span: Span, + ) -> Result { + match operand { + Operand::CarrierOwned { slot, .. } => self.carrier_owner(*slot), + Operand::CarrierBorrowed { loan, .. } => self.carrier_loan(*loan), + Operand::Owned { slot, .. } => self.resource_owner(*slot), + Operand::Borrowed { loan, .. } => self.resource_loan(*loan), + Operand::Ordinary { parameter, .. } => { + let value = + ordinary.get(*parameter).copied().flatten().ok_or_else(|| { + pending("ordinary actual has not completed in source order") + })?; + Ok(self.payload_bits(value).0) + } + } + } + fn resource_prepared_call( + &mut self, + frame: jett_mir::ResourceFrameId, + ) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("staged call has no selected family"))?; + Ok(self + .builder + .ins() + .stack_load(ir::types::I64, resource.frames[frame.index()], 8)) + } + + fn resource_staged_call( + &mut self, + node: &jett_mir::ResourceCallNode, + span: Span, + operations: &[&jett_mir::ResourceOperation], + ) -> Result<(), CodegenError> { + use jett_mir::ResourceCallNode as Node; + let resource = self + .resource + .ok_or_else(|| pending("staged call has no selected family"))?; + let region_id = match node { + Node::Begin { region } + | Node::Stage { region, .. } + | Node::Invoke { region, .. } + | Node::End { region, .. } => *region, + }; + let current = resource + .layout + .plan() + .program() + .functions + .get(resource.function.index() as usize) + .ok_or_else(|| pending("staged call current function is absent"))?; + let region = current + .resource_call_region(region_id) + .ok_or_else(|| pending("staged call lost its private constructor region"))?; + let mut preparations = resource.function_plan()?.operations().iter().filter(|operation| + matches!(operation.role(), Role::BeginSourceFunction { region, .. } if *region == region_id)); + let begin = preparations + .next() + .ok_or_else(|| pending("staged region has no complete prepared tuple"))?; + if preparations.next().is_some() { + return Err(pending("staged region has duplicate prepared tuples")); + } + let Role::BeginSourceFunction { + function, + formals, + result, + .. + } = begin.role() + else { + unreachable!() + }; + let context = self.resource_context(); + match node { + Node::Begin { .. } => { + if !operations + .iter() + .any(|operation| operation.id() == begin.id()) + { + return Err(pending("staged Begin differs from its exact current site")); + } + self.resource_open_operation(begin.frame())?; + let (context, frame, ordinal) = self.resource_operation(begin)?; + let call = Leaf::SourcePrepare.output( + self.module, + self.builder, + &[context, frame, ordinal], + resource.failure, + 8, + )?; + self.builder + .ins() + .stack_store(call, resource.frames[begin.frame().index()], 8); + Ok(()) + } + Node::Stage { + source_index, + parameter, + value, + ordinary, + .. + } => { + let mut stages = operations.iter().copied().filter(|operation| + matches!(operation.role(), Role::StageSourceActual { region, source_index: index, parameter: selected, ordinary: endpoint, .. } + if *region == region_id && index == source_index && selected == parameter && endpoint == ordinary)); + let stage = stages + .next() + .ok_or_else(|| pending("Stage has no exact reached-prefix role"))?; + if stages.next().is_some() || stage.frame() != begin.frame() { + return Err(pending( + "Stage has ambiguous or foreign operation authority", + )); + } + let Role::StageSourceActual { operand, .. } = stage.role() else { + unreachable!() + }; + let formal = formals + .iter() + .find(|formal| formal.parameter() == *parameter) + .ok_or_else(|| pending("Stage lost its sealed original formal"))?; + if formal.source_index() != *source_index { + return Err(pending("Stage changed its lexical/formal mapping")); + } + let evaluated = if resource.layout.plan().type_requires_custody(value.ty) { + self.expression(value)? + } else { + let borrowed = formal.access() == jett_mir::ParamMode::View + && formal.effect() != jett_mir::ResourceArgumentEffect::RelinquishOwned; + let binding = ordinary + .and_then(|local| self.caller_acquisitions.owner_initializer(local)) + .and_then(|source| source.binding); + let previous = std::mem::replace(&mut self.taking_binding, binding); + let evaluated = self.argument(value, borrowed); + self.taking_binding = previous; + evaluated? + }; + let mut ordinary_values = vec![None; formals.len()]; + if let Some(local) = ordinary { + self.define_local(*local, evaluated, span)?; + let endpoint = Expression { + kind: ExpressionKind::Local(*local), + ty: self.local_types[local.index() as usize].ty, + span, + }; + ordinary_values[*parameter] = Some(self.argument(&endpoint, true)?); + } + self.resource_stage_actual(operations, begin.frame(), *parameter)?; + let bits = self.resource_operand(operand, &ordinary_values, span)?; + let call = self.resource_prepared_call(begin.frame())?; + let index = self + .builder + .ins() + .iconst(ir::types::I32, *source_index as i64); + let parameter = self.builder.ins().iconst(ir::types::I32, *parameter as i64); + Leaf::SourceActual.checked( + self.module, + self.builder, + &[context, call, index, parameter, bits], + resource.failure, + ) + } + Node::Invoke { output, .. } => { + let mut invocations = operations.iter().copied().filter(|operation| + operation.frame() == begin.frame() && matches!(operation.role(), Role::InvokeSourceFunction { function: target, .. } if target == function)); + let invocation = invocations + .next() + .ok_or_else(|| pending("staged Invoke has no exact reached role"))?; + if invocations.next().is_some() + || region.invocation().map(|(_, local)| local) != Some(*output) + { + return Err(pending("staged Invoke changed its constructor output")); + } + // The writer independently joins the full Begin/Invoke tuple to one row. + resource.ordinal(invocation)?; + let call = self.resource_prepared_call(begin.frame())?; + let scope = Leaf::SourceEnter.output( + self.module, + self.builder, + &[context, call], + resource.failure, + 8, + )?; + let environment = self.builder.ins().iconst(ir::types::I64, 0); + let mut native = vec![context, environment, scope]; + for parameter in 0..formals.len() { + let formal = formals + .iter() + .find(|formal| formal.parameter() == parameter) + .ok_or_else(|| pending("staged Invoke lost a complete formal"))?; + let index = self.builder.ins().iconst(ir::types::I32, parameter as i64); + let installed = Leaf::SourceParameter.output( + self.module, + self.builder, + &[context, scope, index], + resource.failure, + 8, + )?; + let ty = formal.parameter_type(); + if resource.layout.plan().type_requires_custody(ty) { + native.push(installed); + } else { + let unpacked = self.unpack_payload(installed, ty, span)?; + native.push(self.scalar(unpacked, span)?); + let local = region + .actuals() + .iter() + .find(|actual| actual.parameter() == parameter) + .and_then(|actual| actual.ordinary()) + .ok_or_else(|| { + pending("staged ordinary formal has no current endpoint") + })?; + let endpoint = Expression { + kind: ExpressionKind::Local(local), + ty: self.local_types[local.index() as usize].ty, + span, + }; + if task_scalar(Some(resource.layout), self.types, ty)? { + let ordinary = self.argument(&endpoint, true)?; + native.push(self.scalar_task(ordinary, span)?.1); + } + if formal.access() == jett_mir::ParamMode::Owned + && is_linear(self.types, ty) + { + let slot = + self.local_slots[local.index() as usize].ok_or_else(|| { + pending("staged ordinary owned argument has no companion slot") + })?; + self.clear_slot(slot); + } + } + } + let declared = self.declarations.get(*function).ok_or_else(|| { + pending("staged Source target is outside its declared family") + })?; + let reference = self + .module + .declare_func_in_func(declared.native_id, self.builder.func); + let native_call = self.builder.ins().call(reference, &native); + let returned = self.builder.inst_results(native_call).to_vec(); + Leaf::SourceStatus.checked( + self.module, + self.builder, + &[context, call], + resource.failure, + )?; + self.check_failure()?; + let bits = returned + .first() + .copied() + .ok_or_else(|| pending("staged Source result is absent"))?; + let opaque_output = resource.function_plan()?.descriptor_local(*output); + if let Some(hook) = opaque_output { + let selected = resource.layout.plan().function(*function).ok_or_else(|| { + pending("staged descriptor target has no exact Source plan") + })?; + if selected.descriptor_return() != Some(hook) + || self.local_types[output.index() as usize].ty != hook.function_type() + || !matches!(result, CallResult::Ordinary { ty } if *ty == hook.function_type()) + { + return Err(pending( + "staged descriptor output changes its exact callee Return or current endpoint", + )); + } + } + let lowered = match *result { + CallResult::Carrier { slot } => { + if resource.carrier_local_slot(*output)? != Some(slot) { + return Err(pending( + "staged carrier output differs from its exact Local slot", + )); + } + self.carrier_store(slot, bits)?; + LoweredValue::Scalar(bits) + } + CallResult::Owned { slot } => { + if resource.local_slot(*output)? != Some(slot) { + return Err(pending( + "staged owned output differs from its exact Local slot", + )); + } + self.resource_store(slot, bits)?; + LoweredValue::Scalar(bits) + } + CallResult::Ordinary { .. } if opaque_output.is_some() => { + LoweredValue::Scalar(bits) + } + CallResult::Ordinary { ty } + if task_scalar(Some(resource.layout), self.types, ty)? => + { + let depth = returned + .get(1) + .copied() + .ok_or_else(|| pending("staged primitive result lost Pending depth"))?; + LoweredValue::ScalarTask(bits, depth) + } + CallResult::Ordinary { ty } if is_linear(self.types, ty) => { + self.own_linear(bits)? + } + CallResult::Ordinary { ty } if is_copy_owned(self.types, ty) => { + self.own(bits)? + } + CallResult::Ordinary { .. } => LoweredValue::Scalar(bits), + }; + self.define_local(*output, lowered, span) + } + Node::End { outcome, .. } => { + let mut completions = operations.iter().copied().filter(|operation| + operation.frame() == begin.frame() && matches!(operation.role(), Role::Complete { outcome: selected } if selected == outcome)); + if completions.next().is_none() || completions.next().is_some() { + return Err(pending( + "staged End lost its exact normal/abandoned completion", + )); + } + self.resource_finish_operation(operations, begin.frame()) + } + } + } + + fn resource_invoke( + &mut self, + expression: &Expression, + args: &[Expression], + invocation: &jett_mir::ResourceOperation, + operations: &[&jett_mir::ResourceOperation], + descriptor: Option<&Expression>, + ) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("invocation has no selected family"))?; + let (order, operands, result, source) = match invocation.role() { + Role::InvokeHook { + evaluation_order, + operands, + result, + source, + .. + } + | Role::InvokeSourceFunction { + evaluation_order, + operands, + result, + source, + .. + } => ( + evaluation_order.as_slice(), + operands.as_slice(), + *result, + source, + ), + _ => return Err(pending("invocation selected another exact role")), + }; + let indirect_hook = + descriptor.is_some() && matches!(invocation.role(), Role::InvokeHook { .. }); + self.resource_validate_named_invocation(invocation, descriptor)?; + self.resource_validate_hook_invocation(invocation, descriptor)?; + let opaque_result = self.resource_is_hook_descriptor(expression)?; + self.resource_open_operation(invocation.frame())?; + let physical = match invocation.role() { + Role::InvokeHook { hook, .. } + if hook.recipe() == jett_types::ResourceKernelRecipe::NetworkFactory => + { + operations + .iter() + .copied() + .find(|operation| { + operation.frame() == invocation.frame() + && matches!(operation.role(), Role::Acquire { .. }) + }) + .ok_or_else(|| pending("Factory has no exact Acquire wire role"))? + } + Role::InvokeHook { hook, .. } + if hook.recipe() == jett_types::ResourceKernelRecipe::Finalize => + { + operations + .iter() + .copied() + .find(|operation| { + operation.frame() == invocation.frame() + && matches!(operation.role(), Role::Close { .. }) + }) + .ok_or_else(|| pending("Finalize has no exact Close wire role"))? + } + _ => invocation, + }; + let (context, frame, ordinal) = self.resource_operation(physical)?; + let call = match invocation.role() { + Role::InvokeSourceFunction { .. } => Some(Leaf::SourcePrepare.output( + self.module, + self.builder, + &[context, frame, ordinal], + resource.failure, + 8, + )?), + Role::InvokeHook { .. } if !indirect_hook => { + let no_descriptor = self.builder.ins().iconst(ir::types::I64, 0); + Some(Leaf::HookPrepare.output( + self.module, + self.builder, + &[context, frame, ordinal, no_descriptor], + resource.failure, + 8, + )?) + } + Role::InvokeHook { .. } => None, + _ => unreachable!(), + }; + let mut evaluated = vec![None; args.len()]; + for ¶meter in order { + let argument = args + .get(parameter) + .ok_or_else(|| pending("exact invocation actual is absent"))?; + let fact = source + .arguments + .iter() + .find(|fact| fact.parameter_index == parameter) + .ok_or_else(|| pending("invocation lost an original Source formal"))?; + let value = if resource.layout.plan().type_requires_custody(argument.ty) { + self.expression(argument)? + } else { + self.argument( + argument, + resource_argument_view(invocation, parameter, self.types)?, + )? + }; + evaluated[parameter] = Some(value); + self.resource_stage_actual(operations, invocation.frame(), parameter)?; + if matches!(invocation.role(), Role::InvokeSourceFunction { .. }) { + let operand = operands + .iter() + .find(|operand| match operand { + Operand::Ordinary { + parameter: selected, + .. + } + | Operand::Owned { + parameter: selected, + .. + } + | Operand::Borrowed { + parameter: selected, + .. + } + | Operand::CarrierOwned { + parameter: selected, + .. + } + | Operand::CarrierBorrowed { + parameter: selected, + .. + } => *selected == parameter, + }) + .ok_or_else(|| pending("Source actual has no exact operand role"))?; + let bits = self.resource_operand(operand, &evaluated, argument.span)?; + let source_index = self + .builder + .ins() + .iconst(ir::types::I32, fact.source_index as i64); + let parameter = self.builder.ins().iconst(ir::types::I32, parameter as i64); + Leaf::SourceActual.checked( + self.module, + self.builder, + &[ + context, + call.expect("Source call was prepared"), + source_index, + parameter, + bits, + ], + resource.failure, + )?; + } + } + // Original indirect semantics evaluate the callable once after all actuals. + // Opaque hook metadata and ordinary named descriptors use distinct checks. + let hook_descriptor = if let Some(callee) = descriptor { + let lowered = self.expression(callee)?; + let descriptor = self.scalar(lowered, callee.span)?; + if !indirect_hook { + self.resource_validate_named_descriptor(descriptor, invocation, callee.span)?; + } + descriptor + } else { + self.builder.ins().iconst(ir::types::I64, 0) + }; + let call = if indirect_hook { + let ordinal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(resource.ordinal(invocation)?)); + Leaf::HookPrepare.output( + self.module, + self.builder, + &[context, frame, ordinal, hook_descriptor], + resource.failure, + 8, + )? + } else { + call.expect("direct hook or Source call was prepared") + }; + let result_value = match invocation.role() { + Role::InvokeHook { hook, .. } => { + let find = |parameter| { + operands + .iter() + .find(|operand| match operand { + Operand::Ordinary { + parameter: selected, + .. + } + | Operand::Owned { + parameter: selected, + .. + } + | Operand::Borrowed { + parameter: selected, + .. + } + | Operand::CarrierOwned { + parameter: selected, + .. + } + | Operand::CarrierBorrowed { + parameter: selected, + .. + } => *selected == parameter, + }) + .ok_or_else(|| pending("hook recipe lost an exact operand")) + }; + match hook.recipe() { + jett_types::ResourceKernelRecipe::NetworkFactory => { + let net = self.resource_operand(find(0)?, &evaluated, expression.span)?; + let label = self.resource_operand(find(1)?, &evaluated, expression.span)?; + let prepared = Leaf::FactoryPrepare.output( + self.module, + self.builder, + &[context, call, net], + resource.failure, + 8, + )?; + Leaf::FactoryCommit.output( + self.module, + self.builder, + &[context, prepared, net, label], + resource.failure, + 16, + )? + } + jett_types::ResourceKernelRecipe::NetworkBorrow => { + let net = self.resource_operand(find(0)?, &evaluated, expression.span)?; + let loan = self.resource_operand(find(1)?, &evaluated, expression.span)?; + Leaf::BorrowCommit.output( + self.module, + self.builder, + &[context, call, net, loan], + resource.failure, + 8, + )? + } + jett_types::ResourceKernelRecipe::Finalize => { + let close = operations + .iter() + .copied() + .find(|operation| { + operation.frame() == invocation.frame() + && matches!(operation.role(), Role::Close { .. }) + }) + .ok_or_else(|| pending("Finalize has no exact consumed owner role"))?; + let Role::Close { source, .. } = close.role() else { + unreachable!(); + }; + let owner = self.resource_owner(*source)?; + if indirect_hook { + Leaf::DescriptorClose.checked( + self.module, + self.builder, + &[context, call, owner], + resource.failure, + )?; + } else { + let (context, frame, ordinal) = self.resource_operation(close)?; + Leaf::Close.checked( + self.module, + self.builder, + &[context, frame, ordinal, owner], + resource.failure, + )?; + } + self.clear_slot(resource.owners[source.index()]); + self.builder.ins().iconst(ir::types::I64, 0) + } + } + } + Role::InvokeSourceFunction { function, .. } => { + let scope = Leaf::SourceEnter.output( + self.module, + self.builder, + &[context, call], + resource.failure, + 8, + )?; + let mut native = vec![context, self.builder.ins().iconst(ir::types::I64, 0), scope]; + for (parameter, argument) in args.iter().enumerate() { + let index = self.builder.ins().iconst(ir::types::I32, parameter as i64); + let installed = Leaf::SourceParameter.output( + self.module, + self.builder, + &[context, scope, index], + resource.failure, + 8, + )?; + if resource.layout.plan().type_requires_custody(argument.ty) { + native.push(installed); + } else { + let unpacked = + self.unpack_payload(installed, argument.ty, argument.span)?; + native.push(self.scalar(unpacked, argument.span)?); + if task_scalar(Some(resource.layout), self.types, argument.ty)? { + native.push( + self.scalar_task( + evaluated[parameter].ok_or_else(|| { + pending("ordinary Source argument is absent") + })?, + argument.span, + )? + .1, + ); + } + if !resource_argument_view(invocation, parameter, self.types)? + && is_linear(self.types, argument.ty) + { + if let Some(LoweredValue::Owned(_, slot)) = evaluated[parameter] { + self.clear_slot(slot); + } + } + } + } + let declared = self + .declarations + .get(*function) + .ok_or_else(|| pending("Source family callee is absent"))?; + let reference = self + .module + .declare_func_in_func(declared.native_id, self.builder.func); + let native_call = self.builder.ins().call(reference, &native); + let results = self.builder.inst_results(native_call).to_vec(); + Leaf::SourceStatus.checked( + self.module, + self.builder, + &[context, call], + resource.failure, + )?; + self.check_failure()?; + let bits = results + .first() + .copied() + .ok_or_else(|| pending("Source family call has no domain result"))?; + // Primitive Pending depth remains part of the ordinary result ABI. + if task_scalar(Some(resource.layout), self.types, expression.ty)? { + let depth = results.get(1).copied().ok_or_else(|| { + pending("ordinary Source primitive lost its Pending depth") + })?; + self.resource_finish_operation(operations, invocation.frame())?; + return Ok(LoweredValue::ScalarTask(bits, depth)); + } + bits + } + _ => unreachable!(), + }; + let lowered = match result { + CallResult::Carrier { slot } => { + self.carrier_store(slot, result_value)?; + LoweredValue::Scalar(result_value) + } + CallResult::Owned { slot } => { + self.resource_store(slot, result_value)?; + LoweredValue::Scalar(result_value) + } + CallResult::Ordinary { .. } if opaque_result => LoweredValue::Scalar(result_value), + CallResult::Ordinary { ty } if is_linear(self.types, ty) => { + self.own_linear(result_value)? + } + CallResult::Ordinary { ty } if is_copy_owned(self.types, ty) => { + self.own(result_value)? + } + CallResult::Ordinary { .. } => LoweredValue::Scalar(result_value), + }; + self.resource_finish_operation(operations, invocation.frame())?; + Ok(lowered) + } + pub(super) fn resource_expression( + &mut self, + expression: &Expression, + ) -> Result, CodegenError> { + let Some(resource) = self.resource else { + return Ok(None); + }; + let operations = resource + .function_plan()? + .operations_for_expression(expression) + .collect::>(); + let invocation = operations.iter().copied().find(|operation| { + matches!( + operation.role(), + Role::InvokeHook { .. } | Role::InvokeSourceFunction { .. } + ) + }); + if let Some(invocation) = invocation { + let (args, descriptor) = match &expression.kind { + ExpressionKind::Call { args, .. } | ExpressionKind::ResourceInvoke { args, .. } => { + (args.as_slice(), None) + } + ExpressionKind::IndirectCall { callee, args, .. } => { + (args.as_slice(), Some(callee.as_ref())) + } + _ => return Err(pending("exact invocation role changed its expression kind")), + }; + return self + .resource_invoke(expression, args, invocation, &operations, descriptor) + .map(Some); + } + if operations.iter().any(|operation| { + operation.is_expression_operation() + && matches!( + operation.role(), + Role::Complete { + outcome: jett_mir::ResourceCompletion::Abort + } + ) + && resource.function_plan().is_ok_and(|plan| { + plan.frames()[operation.frame().index()].role() == FrameRole::Operation + }) + }) { + return Err(pending( + "statically aborted Resource actual needs its exact custody CFG emitter", + )); + } + if let Some(value) = self.carrier_expression(expression)? { + return Ok(Some(value)); + } + if self.resource_is_hook_descriptor(expression)? { + return self.resource_hook_value(expression, &operations).map(Some); + } + if self.resource_is_named_value(expression)? + || !resource.layout.plan().type_requires_custody(expression.ty) + { + return Ok(None); + } + let value = match &expression.kind { + ExpressionKind::Local(local) => { + if let Some(slot) = resource.local_slot(*local)? { + self.resource_owner(slot)? + } else { + let variable = self.variables[local.index() as usize] + .ok_or_else(|| pending("resident Resource has no exact native variable"))?; + self.builder.use_var(variable) + } + } + ExpressionKind::View(inner) => return self.expression(inner).map(Some), + ExpressionKind::ResourceHookValue { .. } => { + return Err(pending("hook value has no exact current descriptor proof")); + } + ExpressionKind::OptionalSome(inner) | ExpressionKind::ResultOk(inner) => { + self.expression(inner)?; + let operation = operations + .iter() + .copied() + .find(|operation| matches!(operation.role(), Role::SumAdopt { .. })) + .ok_or_else(|| pending("occupied Resource sum lost its exact adoption"))?; + let Role::SumAdopt { + source, + destination, + } = operation.role() + else { + unreachable!(); + }; + let owner = self.resource_owner(*source)?; + let target = self.resource_destination(operation)?; + let (context, frame, ordinal) = self.resource_operation(operation)?; + let value = Leaf::SumAdopt.output( + self.module, + self.builder, + &[context, frame, ordinal, owner, target], + resource.failure, + 8, + )?; + self.clear_slot(resource.owners[source.index()]); + self.resource_store(*destination, value)?; + value + } + ExpressionKind::OptionalNone => { + let operation = operations + .iter() + .copied() + .find(|operation| matches!(operation.role(), Role::CreateAbsentSum { .. })) + .ok_or_else(|| pending("absent Resource sum lost its exact constructor"))?; + let Role::CreateAbsentSum { destination } = operation.role() else { + unreachable!(); + }; + let (context, frame, ordinal) = self.resource_operation(operation)?; + let value = Leaf::AbsentSum.output( + self.module, + self.builder, + &[context, frame, ordinal], + resource.failure, + 8, + )?; + self.resource_store(*destination, value)?; + value + } + ExpressionKind::ResultFail(inner) => { + let evaluated = self.expression(inner)?; + let operation = operations + .iter() + .copied() + .find(|operation| matches!(operation.role(), Role::CreateFailureSum { .. })) + .ok_or_else(|| { + pending("Resource Fail lost its exact ordinary companion constructor") + })?; + let Role::CreateFailureSum { destination, .. } = operation.role() else { + unreachable!(); + }; + let (context, frame, ordinal) = self.resource_operation(operation)?; + let owned = self.own_copy_value(evaluated, inner.ty, inner.span)?; + let companion = self.scalar(owned, inner.span)?; + let value = Leaf::FailureSum.output( + self.module, + self.builder, + &[context, frame, ordinal, companion], + resource.failure, + 8, + )?; + if let LoweredValue::Owned(_, slot) = owned { + self.clear_slot(slot); + } + self.resource_store(*destination, value)?; + value + } + _ => { + return Err(pending( + "native Resource expression requires its next finite custody emitter", + )); + } + }; + Ok(Some(LoweredValue::Scalar(value))) + } +} + +fn resource_argument_view( + invocation: &jett_mir::ResourceOperation, + parameter: usize, + types: &TypeInterner, +) -> Result { + match invocation.role() { + Role::InvokeHook { hook, .. } => match types.resolve(hook.function_type()) { + Type::Function { view_params, .. } => view_params + .get(parameter) + .copied() + .ok_or_else(|| pending("hook physical access has no exact formal")), + _ => Err(pending("hook has no exact checked physical signature")), + }, + Role::InvokeSourceFunction { formals, .. } => formals + .iter() + .find(|formal| formal.parameter() == parameter) + .map(|formal| formal.access() == jett_mir::ParamMode::View) + .ok_or_else(|| pending("Source call physical access has no exact original formal")), + _ => Err(pending("formal access requires an exact invocation role")), + } +} + +impl Translator<'_, '_> { + pub(super) fn resource_statement( + &mut self, + statement: &Statement, + ) -> Result { + let Some(resource) = self.resource else { + return Ok(false); + }; + if self.carrier_statement(statement)? { + return Ok(true); + } + let operations = resource.at_site()?; + let site_only = operations + .iter() + .copied() + .filter(|operation| !operation.is_expression_operation()) + .collect::>(); + if matches!(statement.kind, StatementKind::ResourceLexicalExit(_)) { + if !self.resource_lexical_retirement(statement.span)? { + return Err(pending( + "lexical exit node has no exact constructor retirement", + )); + } + return Ok(true); + } + if self.resource_borrowed_sum_statement(statement, &site_only)? { + return Ok(true); + } + match &statement.kind { + StatementKind::ResourceCall(node) => { + self.resource_staged_call(node, statement.span, &site_only)?; + Ok(true) + } + StatementKind::Let { local, value } + if resource.layout.plan().type_requires_custody(value.ty) + && !self.resource_is_named_value(value)? => + { + let evaluated = self.expression(value)?; + let bits = if let Some(destination) = resource.local_slot(*local)? { + let operation = site_only.iter().copied().find(|operation| matches!(operation.role(), Role::Transfer { destination: selected, .. } if *selected == destination)); + if let Some(operation) = operation { + self.resource_transfer(operation)? + } else { + self.scalar(evaluated, value.span)? + } + } else { + if let Some(operation) = site_only.iter().copied().find(|operation| { + matches!( + operation.role(), + Role::Borrow { .. } | Role::BorrowSum { .. } + ) + }) { + self.resource_begin_borrow(operation)? + } else { + self.scalar(evaluated, value.span)? + } + }; + self.define_local(*local, LoweredValue::Scalar(bits), statement.span)?; + Ok(true) + } + StatementKind::Assign { target, value } + if resource.layout.plan().type_requires_custody(target.ty) => + { + let ExpressionKind::Local(local) = target.kind else { + return Err(pending( + "Resource projected assignment transport is pending", + )); + }; + self.expression(value)?; + let destination = resource + .local_slot(local)? + .ok_or_else(|| pending("Resource assignment lost its owning local"))?; + let mut replacements = site_only.iter().copied().filter(|operation| + matches!(operation.role(), Role::Replace { destination: selected, .. } if *selected == destination)); + let mut reseats = site_only.iter().copied().filter(|operation| + matches!(operation.role(), Role::SelfRebind { slot } if *slot == destination)); + let mut transfers = site_only.iter().copied().filter(|operation| + matches!(operation.role(), Role::Transfer { destination: selected, .. } if *selected == destination)); + let replace = replacements.next(); + let reseat = reseats.next(); + let transfer = transfers.next(); + if replacements.next().is_some() + || reseats.next().is_some() + || transfers.next().is_some() + { + return Err(pending( + "Resource assignment has ambiguous exact endpoint roles", + )); + } + let bits = if let Some(replace) = replace { + let Role::Replace { + replacement, + old: jett_mir::ResourceOccupancy::Occupied, + .. + } = replace.role() + else { + return Err(pending( + "Resource replacement does not have its occupied old owner", + )); + }; + if reseat.is_some() + || !matches!(transfer.map(|operation| operation.role()), + Some(Role::Transfer { source, destination: selected }) if source == replacement && *selected == destination) + { + return Err(pending( + "Resource replacement lost its exact evaluated RHS transfer", + )); + } + let old = self.resource_owner(destination)?; + let rhs = self.resource_owner(*replacement)?; + let (context, frame, ordinal) = self.resource_operation(replace)?; + let bits = Leaf::Replace.output( + self.module, + self.builder, + &[context, frame, ordinal, old, rhs], + resource.failure, + 8, + )?; + self.clear_slot(resource.owners[replacement.index()]); + self.resource_store(destination, bits)?; + bits + } else if reseat.is_some() { + if transfer.is_some() + || !matches!(value.kind, ExpressionKind::Local(source) if source == local) + || value.ty != target.ty + { + return Err(pending( + "Resource self-rebind lost its exact current Local occurrence", + )); + } + self.resource_owner(destination)? + } else { + self.resource_transfer(transfer.ok_or_else(|| { + pending("vacant Resource assignment has no exact evaluated RHS transfer") + })?)? + }; + self.define_local(local, LoweredValue::Scalar(bits), statement.span)?; + Ok(true) + } + StatementKind::Evaluate(expression) | StatementKind::HandleDefault(expression) + if resource.layout.plan().type_requires_custody(expression.ty) => + { + self.expression(expression)?; + for operation in site_only { + if matches!(operation.role(), Role::Drop { .. }) { + self.resource_drop(operation)?; + } + } + Ok(true) + } + StatementKind::SumTag { source, target } + if resource + .layout + .plan() + .type_requires_custody(self.local_types[source.index() as usize].ty) => + { + let slot = resource + .local_slot(*source)? + .ok_or_else(|| pending("Resource sum tag has no exact owner"))?; + let value = self.resource_owner(slot)?; + let context = self.resource_context(); + let execution = resource + .function_plan()? + .execution_frame(resource.block, resource.position) + .ok_or_else(|| { + pending("Resource sum tag lost its exact current execution frame") + })?; + let frame = self.resource_frame(execution)?; + let output = self.builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 4, + 2, + )); + let zero = self.builder.ins().iconst(ir::types::I32, 0); + self.builder.ins().stack_store(zero, output, 0); + let address = self.builder.ins().stack_addr(ir::types::I64, output, 0); + Leaf::SumTag.checked( + self.module, + self.builder, + &[context, frame, value, address], + resource.failure, + )?; + let tag = self.builder.ins().stack_load(ir::types::I32, output, 0); + let tag = self.builder.ins().ireduce(ir::types::I8, tag); + self.define_local(*target, LoweredValue::Scalar(tag), statement.span)?; + Ok(true) + } + StatementKind::SumTake { + source, + target, + success, + } if resource + .layout + .plan() + .type_requires_custody(self.local_types[source.index() as usize].ty) => + { + let operation = operations + .iter() + .copied() + .find(|operation| { + if *success { + matches!(operation.role(), Role::SumTake { .. }) + } else { + matches!(operation.role(), Role::TakeFailureCompanion { .. }) + } + }) + .ok_or_else(|| { + pending("Resource sum extraction lost its exact selected-edge role") + })?; + let source_slot = resource + .local_slot(*source)? + .ok_or_else(|| pending("Resource sum extraction lost its shell owner"))?; + let value = self.resource_owner(source_slot)?; + let (context, frame, ordinal) = self.resource_operation(operation)?; + let lowered = if let Role::SumTake { destination, .. } = operation.role() { + let destination_frame = self.resource_destination(operation)?; + let bits = Leaf::SumTake.output( + self.module, + self.builder, + &[context, frame, ordinal, value, destination_frame], + resource.failure, + 8, + )?; + self.resource_store(*destination, bits)?; + LoweredValue::Scalar(bits) + } else { + let bits = Leaf::FailureCompanionTake.output( + self.module, + self.builder, + &[context, frame, ordinal, value], + resource.failure, + 8, + )?; + self.own(bits)? + }; + self.clear_slot(resource.owners[source_slot.index()]); + self.define_local(*target, lowered, statement.span)?; + Ok(true) + } + _ => Ok(false), + } + } + fn resource_scope_completion( + &mut self, + operation: &jett_mir::ResourceOperation, + body: Value, + checked: bool, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("completion has no selected family"))?; + let context = self.resource_context(); + let scope = self.builder.use_var(resource.scope); + let ordinal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(resource.ordinal(operation)?)); + let one = self.builder.ins().iconst(ir::types::I64, 1); + self.builder + .ins() + .stack_store(one, resource.scope_finished, 0); + if checked { + Leaf::ScopeComplete.checked( + self.module, + self.builder, + &[context, scope, ordinal, body], + resource.failure, + ) + } else { + Leaf::ScopeComplete + .call(self.module, self.builder, &[context, scope, ordinal, body]) + .map(|_| ()) + } + } + pub(super) fn resource_return( + &mut self, + expression: Option<&Expression>, + span: Span, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("return has no selected family"))?; + let operations = resource.at_site()?; + let opaque_return = match (resource.function_plan()?.descriptor_return(), expression) { + (Some(hook), Some(value)) => { + if resource.function_plan()?.descriptor_value(value) != Some(hook) + || value.ty != hook.function_type() + { + return Err(pending( + "returned hook metadata changes its sealed Return proof", + )); + } + true + } + (Some(_), None) => { + return Err(pending("hook descriptor Return omitted its exact value")); + } + (None, _) => false, + }; + let mut result = if let Some(expression) = expression { + self.expression(expression)? + } else { + self.nothing() + }; + // The original operand may observe a nested alias. Its evaluation + // precedes certified lexical retirement and provisional publication. + self.resource_lexical_retirement(span)?; + let completion = operations + .iter() + .copied() + .find(|operation| { + operation.frame() + == resource + .function_plan() + .expect("selected plan") + .root_scope() + .id() + && matches!(operation.role(), Role::Complete { .. }) + }) + .ok_or_else(|| pending("return has no exact Scope completion"))?; + let carrier_publication = resource.function_plan()?.carriers().and_then(|plan| { + plan.operations().iter().find(|operation| { + operation.site().block() == resource.block + && operation.site().position() == resource.position + && operation.edge().is_none() + && matches!( + operation.role(), + jett_mir::ResourceCarrierOperationRole::PublishReturn { .. } + ) + }) + }); + if let Some(publication) = carrier_publication { + let jett_mir::ResourceCarrierOperationRole::PublishReturn { source } = + publication.role() + else { + unreachable!(); + }; + let transfer = resource.function_plan()?.carriers().and_then(|plan| plan.operations().iter().find(|operation| + operation.site().block() == resource.block && operation.site().position() == resource.position + && !operation.is_expression_operation() && operation.edge().is_none() + && matches!(operation.role(), jett_mir::ResourceCarrierOperationRole::Transfer { destination, .. } + | jett_mir::ResourceCarrierOperationRole::QualifyMachine { destination, .. } if destination == source))) + .ok_or_else(|| pending("carrier Return lost its exact provisional transfer"))?; + self.carrier_transfer(transfer)?; + } + let publication = operations + .iter() + .copied() + .find(|operation| matches!(operation.role(), Role::CompleteReturnAfterCleanup { .. })); + if let Some(publication) = publication { + let Role::CompleteReturnAfterCleanup { source } = publication.role() else { + unreachable!(); + }; + let transfer = operations.iter().copied().find(|operation| matches!(operation.role(), Role::Transfer { destination, .. } if destination == source)).ok_or_else(|| pending("owned return has no exact provisional transfer"))?; + self.resource_transfer(transfer)?; + } else if carrier_publication.is_none() + && !opaque_return + && expression.is_some_and(|expression| is_copy_owned(self.types, expression.ty)) + { + let value = self.scalar(result, span)?; + let leaf = + if expression.is_some_and(|expression| is_function(self.types, expression.ty)) { + NativeLeaf::StructClone + } else { + NativeLeaf::Retain + }; + let retained = self.leaf(leaf, &[value], true)?; + self.builder + .ins() + .stack_store(retained, resource.return_companion, 0); + result = LoweredValue::Scalar(retained); + } else if let LoweredValue::Owned(value, slot) = result { + // Preserve the ordinary owner through fallible Scope cleanup. + self.builder + .ins() + .stack_store(value, resource.return_companion, 0); + self.clear_slot(slot); + result = LoweredValue::Scalar(value); + } + self.flush_actor_state(span)?; + self.drop_all()?; + let body = self.builder.ins().iconst(ir::types::I32, 0); + self.resource_scope_completion(completion, body, true)?; + if let Some(publication) = carrier_publication { + let jett_mir::ResourceCarrierOperationRole::PublishReturn { source } = + publication.role() + else { + unreachable!(); + }; + let value = self.carrier_owner(*source)?; + let context = self.resource_context(); + let scope = self.builder.use_var(resource.scope); + let ordinal = resource + .layout + .carrier_operation(resource.function, publication.id()) + .ok_or_else(|| pending("carrier Return publication has no exact installed row"))?; + let ordinal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(ordinal)); + let result = Leaf::CarrierPublish.output( + self.module, + self.builder, + &[context, scope, ordinal, value], + resource.failure, + 8, + )?; + self.clear_slot(resource.carrier_owners[source.index()]); + self.builder.ins().return_(&[result]); + } else if let Some(publication) = publication { + let Role::CompleteReturnAfterCleanup { source } = publication.role() else { + unreachable!(); + }; + let value = self.resource_owner(*source)?; + let context = self.resource_context(); + let scope = self.builder.use_var(resource.scope); + let ordinal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(resource.ordinal(publication)?)); + let result = Leaf::ReturnPublish.output( + self.module, + self.builder, + &[context, scope, ordinal, value], + resource.failure, + 8, + )?; + self.clear_slot(resource.owners[source.index()]); + self.builder.ins().return_(&[result]); + } else if expression.is_some_and(|expression| { + task_scalar(Some(resource.layout), self.types, expression.ty).unwrap_or(false) + }) { + let (bits, depth) = self.scalar_task(result, span)?; + self.builder.ins().return_(&[bits, depth]); + } else { + let bits = self.scalar(result, span)?; + self.clear_slot(resource.return_companion); + self.builder.ins().return_(&[bits]); + } + Ok(()) + } + pub(super) fn resource_failure_cleanup(&mut self) -> Result<(), CodegenError> { + let Some(resource) = self.resource else { + return Ok(()); + }; + // Ordinary return custody remains ordinary even when a Resource cleanup fails. + self.drop_slot(resource.return_companion)?; + for header in resource + .function_plan()? + .frames() + .iter() + .rev() + .filter(|header| header.role() == FrameRole::Operation) + { + let active = self.builder.ins().stack_load( + ir::types::I64, + resource.frames[header.id().index()], + 0, + ); + let retire = self.builder.create_block(); + let next = self.builder.create_block(); + let live = self.builder.ins().icmp_imm(IntCC::NotEqual, active, 0); + self.builder.ins().brif(live, retire, &[], next, &[]); + self.builder.switch_to_block(retire); + let operation = resource + .function_plan()? + .operations() + .iter() + .find(|operation| { + operation.frame() == header.id() + && matches!(operation.role(), Role::Complete { .. }) + }) + .ok_or_else(|| pending("active Operation has no sealed completion row"))?; + let context = self.resource_context(); + let ordinal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(resource.ordinal(operation)?)); + self.clear_slot(resource.frames[header.id().index()]); + let zero = self.builder.ins().iconst(ir::types::I64, 0); + self.builder + .ins() + .stack_store(zero, resource.frames[header.id().index()], 8); + // Attempt all enclosing retirement even after the first infrastructure failure. + Leaf::OperationComplete.call(self.module, self.builder, &[context, active, ordinal])?; + self.builder.ins().jump(next, &[]); + self.builder.switch_to_block(next); + } + let finished = self + .builder + .ins() + .stack_load(ir::types::I64, resource.scope_finished, 0); + let retire = self.builder.create_block(); + let next = self.builder.create_block(); + let live = self.builder.ins().icmp_imm(IntCC::Equal, finished, 0); + self.builder.ins().brif(live, retire, &[], next, &[]); + self.builder.switch_to_block(retire); + let operation = resource + .function_plan()? + .operations() + .iter() + .find(|operation| { + operation.frame() + == resource + .function_plan() + .expect("selected plan") + .root_scope() + .id() + && matches!(operation.role(), Role::Complete { .. }) + }) + .ok_or_else(|| pending("failure Scope has no original completion projection"))?; + let body = self.builder.use_var(resource.status); + self.resource_scope_completion(operation, body, false)?; + self.builder.ins().jump(next, &[]); + self.builder.switch_to_block(next); + Ok(()) + } +} + +/// One C entry wrapper owns its exact installation and runtime attempt. +fn define_entry( + module: &mut ObjectModule, + declarations: &DeclaredFunctions, + layout: &EmittedResourceLayout<'_>, +) -> Result<(), CodegenError> { + let (id, mut context) = build_entry(module, declarations, layout)?; + module + .define_function(id, &mut context) + .map_err(|error| CodegenError::Backend(format!("Resource C entry: {error:?}"))) +} + +fn build_entry( + module: &mut ObjectModule, + declarations: &DeclaredFunctions, + layout: &EmittedResourceLayout<'_>, +) -> Result<(FuncId, Context), CodegenError> { + let selected = layout.entry(); + let entry = declarations + .get(FunctionId::new(selected.function)) + .ok_or_else(|| pending("driver entry has no declared native family"))?; + let signature = program_entry_signature(module); + let id = module + .declare_function(JETT_AOT_ENTRY_SYMBOL_V1, Linkage::Export, &signature) + .map_err(|error| CodegenError::Backend(error.to_string()))?; + let mut context = module.make_context(); + context.func.signature = signature; + let mut frontend = FunctionBuilderContext::new(); + let mut builder = FunctionBuilder::new(&mut context.func, &mut frontend); + let initial = builder.create_block(); + builder.append_block_params_for_function_params(initial); + builder.switch_to_block(initial); + let ctx = builder.block_params(initial)[0]; + let startup_refused = builder.create_block(); + builder.append_block_param(startup_refused, ir::types::I32); + let refused = builder.create_block(); + builder.append_block_param(refused, ir::types::I32); + let scope_output = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 8, + 3, + )); + let result_output = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 24, + 3, + )); + let zero = builder.ins().iconst(ir::types::I64, 0); + builder.ins().stack_store(zero, scope_output, 0); + builder.ins().stack_store(zero, result_output, 0); + builder.ins().stack_store(zero, result_output, 8); + builder.ins().stack_store(zero, result_output, 16); + let scope_address = builder.ins().stack_addr(ir::types::I64, scope_output, 0); + let result_address = builder.ins().stack_addr(ir::types::I64, result_output, 0); + let manifest_output = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 32, + 3, + )); + let attempt_output = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 8, + 3, + )); + builder.ins().stack_store(zero, attempt_output, 0); + let manifest_address = builder.ins().stack_addr(ir::types::I64, manifest_output, 0); + let attempt_address = builder.ins().stack_addr(ir::types::I64, attempt_output, 0); + let mut manifest_signature = module.make_signature(); + manifest_signature + .params + .push(AbiParam::new(ir::types::I64)); + manifest_signature + .returns + .push(AbiParam::new(ir::types::I32)); + let manifest = module + .declare_function( + "jett_aot_resource_v1_manifest", + Linkage::Export, + &manifest_signature, + ) + .map_err(|error| CodegenError::Backend(error.to_string()))?; + let manifest = module.declare_func_in_func(manifest, builder.func); + let manifest_call = builder.ins().call(manifest, &[manifest_address]); + let manifest_status = builder.inst_results(manifest_call)[0]; + let manifest_ready = builder.create_block(); + let manifest_ok = builder.ins().icmp_imm(IntCC::Equal, manifest_status, 0); + builder.ins().brif( + manifest_ok, + manifest_ready, + &[], + startup_refused, + &[manifest_status.into()], + ); + builder.switch_to_block(manifest_ready); + Leaf::ProgramBegin.checked( + module, + &mut builder, + &[ctx, manifest_address, attempt_address, result_address], + startup_refused, + )?; + Leaf::EntryScope.checked( + module, + &mut builder, + &[ctx, scope_address, result_address], + refused, + )?; + let root = builder.ins().stack_load(ir::types::I64, scope_output, 0); + let bridge_completion = layout.entry_bridge_completion(); + let mut native = vec![ctx, zero]; + if bridge_completion.is_some() { + if layout.plan().function(entry.mir_id).is_some() { + return Err(pending( + "ordinary entry bridge acquired a Source-family body ABI", + )); + } + let function = builder + .ins() + .iconst(ir::types::I32, i64::from(selected.function)); + let signature = builder + .ins() + .iconst(ir::types::I32, i64::from(selected.signature)); + let template = builder + .ins() + .iconst(ir::types::I32, i64::from(selected.scope)); + Leaf::ScopeValidate.checked( + module, + &mut builder, + &[ctx, root, function, signature, template], + refused, + )?; + } else { + native.push(root); + } + for (parameter, ty) in entry.parameter_types.iter().enumerate() { + if *ty != TypeInterner::NETWORK { + return Err(pending( + "native root capability differs from the exact selected Network entry", + )); + } + let output = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 8, + 3, + )); + builder.ins().stack_store(zero, output, 0); + builder.ins().stack_store(zero, result_output, 0); + builder.ins().stack_store(zero, result_output, 8); + builder.ins().stack_store(zero, result_output, 16); + let address = builder.ins().stack_addr(ir::types::I64, output, 0); + let parameter = builder.ins().iconst(ir::types::I32, parameter as i64); + Leaf::EntryNetwork.checked( + module, + &mut builder, + &[ctx, parameter, address, result_address], + refused, + )?; + native.push(builder.ins().stack_load(ir::types::I64, output, 0)); + } + let reference = module.declare_func_in_func(entry.native_id, builder.func); + builder.ins().call(reference, &native); + if let Some(ordinal) = bridge_completion { + let status = crate::values::declare_leaf(module, NativeLeaf::Status)?; + let status = module.declare_func_in_func(status, builder.func); + let call = builder.ins().call(status, &[ctx]); + let body = builder + .inst_results(call) + .first() + .copied() + .ok_or_else(|| pending("ordinary entry has no original native body status"))?; + let ordinal = builder.ins().iconst(ir::types::I32, i64::from(ordinal)); + // Completion can report body failure after actual retirement. Observe the + // retained root outcome next, rather than replacing the original status. + Leaf::ScopeComplete.call(module, &mut builder, &[ctx, root, ordinal, body])?; + } + let output = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 4, + 2, + )); + let zero32 = builder.ins().iconst(ir::types::I32, 0); + builder.ins().stack_store(zero32, output, 0); + let function = builder + .ins() + .iconst(ir::types::I32, i64::from(selected.function)); + let signature = builder + .ins() + .iconst(ir::types::I32, i64::from(selected.signature)); + let template = builder + .ins() + .iconst(ir::types::I32, i64::from(selected.scope)); + let address = builder.ins().stack_addr(ir::types::I64, output, 0); + Leaf::EntryOutcome.checked( + module, + &mut builder, + &[ctx, root, function, signature, template, address], + refused, + )?; + let body_status = builder.ins().stack_load(ir::types::I32, output, 0); + let finish = builder.create_block(); + builder.append_block_param(finish, ir::types::I32); + builder.ins().jump(finish, &[body_status.into()]); + builder.switch_to_block(refused); + let status = builder.block_params(refused)[0]; + builder.ins().jump(finish, &[status.into()]); + builder.switch_to_block(finish); + let status = builder.block_params(finish)[0]; + let attempt = builder.ins().stack_load(ir::types::I64, attempt_output, 0); + let borrowed = builder.ins().icmp_imm(IntCC::Equal, attempt, 0); + let external_finish = builder.create_block(); + let generated_finish = builder.create_block(); + builder + .ins() + .brif(borrowed, external_finish, &[], generated_finish, &[]); + builder.switch_to_block(external_finish); + builder.ins().return_(&[status]); + builder.switch_to_block(generated_finish); + let terminal = Leaf::ProgramComplete.call(module, &mut builder, &[ctx, attempt, status])?; + builder.ins().return_(&[terminal]); + builder.switch_to_block(startup_refused); + let status = builder.block_params(startup_refused)[0]; + builder.ins().return_(&[status]); + builder.seal_all_blocks(); + builder.finalize(); + Ok((id, context)) +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/carriers.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/carriers.rs new file mode 100644 index 00000000..bca5f00c --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/carriers.rs @@ -0,0 +1,1221 @@ +//! Native execution of constructor-owned typed carrier operations. Ordinary +//! container leaves never receive a carrier root, loan, or Resource sum shell. +use super::*; +use jett_mir::{ + ResourceCarrierEdge as Edge, ResourceCarrierIndex as Index, + ResourceCarrierLoanSource as LoanSource, ResourceCarrierObservation as Observation, + ResourceCarrierOperation as Operation, ResourceCarrierOperationRole as CarrierRole, + ResourceCarrierProjectionPath as Path, ResourceCarrierSlotId as SlotId, + ResourceCarrierValue as CarrierValue, +}; + +impl<'a> ResourceEmission<'a> { + fn carrier_plan(self) -> Result<&'a jett_mir::ResourceCarrierFunctionPlan, CodegenError> { + self.function_plan()? + .carriers() + .ok_or_else(|| pending("carrier execution has no fresh private plan")) + } + fn carrier_at_site(self) -> Result, CodegenError> { + Ok(self + .carrier_plan()? + .operations() + .iter() + .filter(|row| { + row.site().block() == self.block + && row.site().position() == self.position + && row.edge().is_none() + && !row.is_expression_operation() + }) + .collect()) + } + pub(super) fn carrier_local_slot( + self, + local: jett_mir::LocalId, + ) -> Result, CodegenError> { + let Some(plan) = self.function_plan()?.carriers() else { + return Ok(None); + }; + let mut slots = plan.slots().iter().filter( + |slot| matches!(slot.storage(), Storage::Local { header } if header.id == local), + ); + let selected = slots.next().map(|slot| slot.id()); + if slots.next().is_some() { + return Err(pending("carrier local has ambiguous current storage")); + } + Ok(selected) + } +} + +impl Translator<'_, '_> { + fn carrier_staged_constructor( + &mut self, + node: &jett_mir::ResourceConstructorNode, + span: Span, + ) -> Result<(), CodegenError> { + use jett_mir::ResourceConstructorNode as Node; + let resource = self + .resource + .ok_or_else(|| pending("staged carrier constructor has no selected family"))?; + let function = resource + .layout + .plan() + .program() + .functions + .get(resource.function.index() as usize) + .filter(|f| f.id == resource.function) + .ok_or_else(|| pending("staged carrier constructor lost its exact function"))?; + let original = function + .resource_constructor(node.constructor()) + .map_err(|error| pending(&error))?; + let operations = resource.carrier_at_site()?; + let wrappers = resource.at_site()?; + match node { + Node::Begin { .. } => { + let mut rows = operations + .iter() + .copied() + .filter(|row| matches!(row.role(), CarrierRole::BeginConstructor { .. })); + let row = rows + .next() + .ok_or_else(|| pending("staged constructor has no exact Begin"))?; + if rows.next().is_some() || row.site() != original.begin() { + return Err(pending("staged constructor Begin is ambiguous or foreign")); + } + let CarrierRole::BeginConstructor { destination, .. } = row.role() else { + unreachable!() + }; + self.resource_open_operation(row.frame())?; + let (context, frame, ordinal) = self.carrier_operation(row)?; + let builder = Leaf::CarrierConstructBegin.output( + self.module, + self.builder, + &[context, frame, ordinal], + resource.failure, + 8, + )?; + self.builder.ins().stack_store( + builder, + resource.carrier_builders[destination.index()], + 0, + ); + } + Node::Stage { + source_index, + field, + value, + .. + } => { + let mut rows = operations.iter().copied().filter(|row| matches!(row.role(),CarrierRole::ConstructorChild { child, .. } if child.index == *field)); + let row = rows + .next() + .ok_or_else(|| pending("staged constructor lost its exact Child"))?; + if rows.next().is_some() + || original + .fields() + .get(*source_index) + .is_none_or(|f| f.stage() != row.site() || f.field() != *field) + { + return Err(pending( + "staged constructor Child changed its exact lexical occurrence", + )); + } + let CarrierRole::ConstructorChild { destination, child } = row.role() else { + unreachable!() + }; + self.carrier_stage_child(*destination, child.value, *source_index, value, span)?; + } + Node::Commit { output, .. } => { + let mut commits = operations + .iter() + .copied() + .filter(|row| matches!(row.role(), CarrierRole::CommitConstructor { .. })); + let commit = commits + .next() + .ok_or_else(|| pending("staged constructor has no exact Commit"))?; + if commits.next().is_some() + || original + .commit() + .is_none_or(|(site, header)| site != commit.site() || header.id != *output) + { + return Err(pending( + "staged constructor Commit changed its exact output", + )); + } + let CarrierRole::CommitConstructor { destination } = commit.role() else { + unreachable!() + }; + let builder = self.builder.ins().stack_load( + ir::types::I64, + resource.carrier_builders[destination.index()], + 0, + ); + let context = self.resource_context(); + let bits = Leaf::CarrierCommit.output( + self.module, + self.builder, + &[context, builder], + resource.failure, + 8, + )?; + self.clear_slot(resource.carrier_builders[destination.index()]); + self.carrier_store(*destination, bits)?; + let target = resource.carrier_local_slot(*output)?.ok_or_else(|| { + pending("staged constructor output has no exact parent holder") + })?; + let mut transfers = operations.iter().copied().filter(|row| matches!(row.role(),CarrierRole::Transfer { source, destination:to } if source == destination && *to == target)); + let transfer = transfers.next().ok_or_else(|| { + pending("staged constructor Commit did not migrate its output before End") + })?; + if transfers.next().is_some() { + return Err(pending("staged constructor output migration is ambiguous")); + } + let bits = self.carrier_transfer(transfer)?; + self.define_local(*output, LoweredValue::Scalar(bits), span)?; + } + Node::End { outcome, .. } => { + let mut completions = wrappers.iter().copied().filter( + |row| matches!(row.role(),Role::Complete { outcome:found } if found == outcome), + ); + let complete = completions + .next() + .ok_or_else(|| pending("staged constructor has no exact completion"))?; + if completions.next().is_some() + || !original.exits().contains(&(complete.site(), *outcome)) + { + return Err(pending( + "staged constructor End changed its reached exact prefix", + )); + } + // Core already owns every acquired ticket. Clear compiler storage + // before fallible cleanup, including an incomplete builder prefix. + for slot in resource + .carrier_plan()? + .slots() + .iter() + .filter(|slot| slot.frame() == complete.frame()) + { + self.clear_slot(resource.carrier_owners[slot.id().index()]); + self.clear_slot(resource.carrier_builders[slot.id().index()]); + } + self.resource_finish_operation(&wrappers, complete.frame())?; + } + } + Ok(()) + } + + fn carrier_stage_child( + &mut self, + destination: SlotId, + child: CarrierValue, + arrival: usize, + actual: &Expression, + span: Span, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("carrier Child has no selected family"))?; + let evaluated = self.expression(actual)?; + let value = match child { + CarrierValue::Ordinary { ty } if ty == actual.ty => { + if is_copy_owned(self.types, ty) || is_linear(self.types, ty) { + self.own_copy_value(evaluated, ty, span)? + } else { + evaluated + } + } + CarrierValue::Ordinary { .. } => { + return Err(pending("carrier Child changed its exact ordinary type")); + } + _ => evaluated, + }; + let bits = match child { + CarrierValue::Owned { slot } => self.carrier_owner(slot)?, + CarrierValue::LeafOwned { slot } => self.resource_owner(slot)?, + CarrierValue::Ordinary { .. } => self.payload_bits(value).0, + _ => return Err(pending("carrier Child cannot adopt a borrowed endpoint")), + }; + let builder = self.builder.ins().stack_load( + ir::types::I64, + resource.carrier_builders[destination.index()], + 0, + ); + let context = self.resource_context(); + let arrival = u32::try_from(arrival) + .map_err(|_| pending("carrier lexical arrival exceeds wire width"))?; + let arrival = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(arrival)); + Leaf::CarrierChild.checked( + self.module, + self.builder, + &[context, builder, arrival, bits], + resource.failure, + )?; + match child { + CarrierValue::Owned { slot } => self.clear_slot(resource.carrier_owners[slot.index()]), + CarrierValue::LeafOwned { slot } => self.clear_slot(resource.owners[slot.index()]), + CarrierValue::Ordinary { .. } => { + if let LoweredValue::Owned(_, slot) = value { + self.clear_slot(slot); + } + } + _ => unreachable!(), + } + Ok(()) + } + fn carrier_operation( + &mut self, + operation: &Operation, + ) -> Result<(Value, Value, Value), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("carrier operation has no selected family"))?; + let context = self.resource_context(); + let frame = self.resource_frame(operation.frame())?; + let ordinal = resource + .layout + .carrier_operation(resource.function, operation.id()) + .ok_or_else(|| pending("carrier occurrence lost its exact installed row"))?; + let ordinal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(ordinal)); + Ok((context, frame, ordinal)) + } + pub(super) fn carrier_owner(&mut self, slot: SlotId) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("carrier owner has no selected family"))?; + let storage = *resource + .carrier_owners + .get(slot.index()) + .ok_or_else(|| pending("carrier owner lost its exact storage"))?; + Ok(self.builder.ins().stack_load(ir::types::I64, storage, 0)) + } + pub(super) fn carrier_store(&mut self, slot: SlotId, value: Value) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("carrier store has no selected family"))?; + let storage = *resource + .carrier_owners + .get(slot.index()) + .ok_or_else(|| pending("carrier store lost its exact storage"))?; + self.builder.ins().stack_store(value, storage, 0); + Ok(()) + } + pub(super) fn carrier_loan( + &mut self, + loan: jett_mir::ResourceCarrierLoanId, + ) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("carrier loan has no selected family"))?; + let record = resource + .carrier_plan()? + .loans() + .get(loan.index()) + .filter(|row| row.id() == loan) + .ok_or_else(|| pending("carrier loan changed its exact current row"))?; + if let LoanSource::IncomingViewFormal { scope, parameter } = record.source() { + if scope != resource.function_plan()?.root_scope().id() || !record.path().is_empty() { + return Err(pending( + "incoming carrier view changes its original scope/path", + )); + } + let formal = resource + .function_plan()? + .parameters() + .get(parameter) + .ok_or_else(|| pending("incoming carrier view lost its exact formal"))?; + let variable = self.variables[formal.local.index() as usize] + .ok_or_else(|| pending("incoming carrier view has no native parameter"))?; + return Ok(self.builder.use_var(variable)); + } + let storage = *resource + .carrier_loans + .get(loan.index()) + .ok_or_else(|| pending("carrier loan lost its native storage"))?; + Ok(self.builder.ins().stack_load(ir::types::I64, storage, 0)) + } + fn carrier_source(&mut self, source: LoanSource) -> Result { + match source { + LoanSource::Slot(slot) => self.carrier_owner(slot), + LoanSource::Loan(loan) => self.carrier_loan(loan), + LoanSource::IncomingViewFormal { scope, parameter } => { + let resource = self + .resource + .ok_or_else(|| pending("carrier formal source has no family"))?; + if scope != resource.function_plan()?.root_scope().id() { + return Err(pending("carrier formal uses a foreign scope")); + } + let formal = resource + .function_plan()? + .parameters() + .get(parameter) + .ok_or_else(|| pending("carrier formal is absent"))?; + let variable = self.variables[formal.local.index() as usize] + .ok_or_else(|| pending("carrier formal has no native variable"))?; + Ok(self.builder.use_var(variable)) + } + } + } + fn carrier_owned_root( + &self, + loan: jett_mir::ResourceCarrierLoanId, + ) -> Result<(SlotId, Vec), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("carrier extraction has no family"))?; + let plan = resource.carrier_plan()?; + let mut visited = std::collections::BTreeSet::new(); + let mut cursor = loan; + let mut segments = Vec::new(); + loop { + if !visited.insert(cursor.index()) { + return Err(pending("carrier extraction has a cyclic parent loan")); + } + let row = plan + .loans() + .get(cursor.index()) + .filter(|row| row.id() == cursor) + .ok_or_else(|| pending("carrier extraction lost its exact parent loan"))?; + segments.push(row.path().to_vec()); + match row.source() { + LoanSource::Slot(slot) => { + let mut path = Vec::new(); + for segment in segments.iter().rev() { + path.extend_from_slice(segment); + } + return Ok((slot, path)); + } + LoanSource::Loan(parent) => cursor = parent, + LoanSource::IncomingViewFormal { .. } => { + return Err(pending( + "owning projection cannot consume an incoming carrier view", + )); + } + } + } + } + fn carrier_selection(&mut self, path: &[Path]) -> Result { + let mut selected = None; + for step in path { + let index = match step { + Path::Element { index } | Path::MapKey { index } | Path::MapValue { index } => { + *index + } + _ => continue, + }; + let local = match index { + Index::Constant(_) => continue, + Index::Local(local) => local, + Index::CurrentIteration { header } => { + let resource = self + .resource + .ok_or_else(|| pending("iteration selection has no family"))?; + let mut loops = resource + .carrier_plan()? + .iterations() + .iter() + .filter(|row| row.header == header); + let selected = loops.next().ok_or_else(|| { + pending("iteration selection has no original/canonical loop") + })?; + if loops.next().is_some() { + return Err(pending("iteration selection is ambiguous")); + } + selected.cursor.ok_or_else(|| { + pending("raw carrier loop has no authenticated native cursor") + })? + } + }; + if selected.replace(local).is_some() { + return Err(pending( + "carrier path needs its exact multiple-selection transport", + )); + } + } + if let Some(local) = selected { + if self.local_types[local.index() as usize].ty != TypeInterner::INT64 { + return Err(pending("carrier selector changed its exact Int64 local")); + } + let variable = self.variables[local.index() as usize] + .ok_or_else(|| pending("carrier selector has no native variable"))?; + Ok(self.builder.use_var(variable)) + } else { + Ok(self.builder.ins().iconst(ir::types::I64, 0)) + } + } + pub(super) fn carrier_transfer( + &mut self, + operation: &Operation, + ) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("carrier transfer has no family"))?; + let (source, destination, leaf) = match operation.role() { + CarrierRole::Transfer { + source, + destination, + } => (*source, *destination, Leaf::CarrierTransfer), + CarrierRole::QualifyMachine { + source, + destination, + .. + } => (*source, *destination, Leaf::CarrierQualify), + CarrierRole::BeginIteration { + source: CarrierValue::Owned { slot }, + destination, + .. + } => (*slot, *destination, Leaf::CarrierTransfer), + _ => return Err(pending("carrier transfer selected another role")), + }; + let input = self.carrier_owner(source)?; + let (context, frame, ordinal) = self.carrier_operation(operation)?; + let bits = leaf.output( + self.module, + self.builder, + &[context, frame, ordinal, input], + resource.failure, + 8, + )?; + self.clear_slot(resource.carrier_owners[source.index()]); + self.carrier_store(destination, bits)?; + Ok(bits) + } + fn carrier_begin_borrow(&mut self, operation: &Operation) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("carrier borrow has no family"))?; + let loan = match operation.role() { + CarrierRole::Borrow { loan } + | CarrierRole::Project { + destination: loan, .. + } => *loan, + _ => return Err(pending("carrier borrow selected another role")), + }; + let record = resource + .carrier_plan()? + .loans() + .get(loan.index()) + .ok_or_else(|| pending("carrier borrow lost its exact loan"))?; + let input = self.carrier_source(record.source())?; + let selection = self.carrier_selection(record.path())?; + let (context, frame, ordinal) = self.carrier_operation(operation)?; + let bits = Leaf::CarrierBorrow.output( + self.module, + self.builder, + &[context, frame, ordinal, input, selection], + resource.failure, + 8, + )?; + self.builder + .ins() + .stack_store(bits, resource.carrier_loans[loan.index()], 0); + Ok(bits) + } + fn carrier_end_borrow(&mut self, operation: &Operation) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("carrier end borrow has no family"))?; + let CarrierRole::EndBorrow { loan } = operation.role() else { + return Err(pending("carrier end selected another role")); + }; + let input = self.carrier_loan(*loan)?; + let (context, frame, ordinal) = self.carrier_operation(operation)?; + Leaf::CarrierEndBorrow.checked( + self.module, + self.builder, + &[context, frame, ordinal, input], + resource.failure, + )?; + self.clear_slot(resource.carrier_loans[loan.index()]); + Ok(()) + } + /// Execute one exact occurrence; expression child evaluation stays with the + /// constructor emitter and publication stays after ScopeComplete. + fn carrier_effect( + &mut self, + operation: &Operation, + span: Span, + ) -> Result, CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("carrier effect has no family"))?; + match operation.role() { + CarrierRole::Transfer { .. } + | CarrierRole::QualifyMachine { .. } + | CarrierRole::BeginIteration { .. } => self + .carrier_transfer(operation) + .map(|bits| Some(LoweredValue::Scalar(bits))), + CarrierRole::Borrow { .. } | CarrierRole::Project { .. } => self + .carrier_begin_borrow(operation) + .map(|bits| Some(LoweredValue::Scalar(bits))), + CarrierRole::EndBorrow { .. } => { + self.carrier_end_borrow(operation)?; + Ok(None) + } + CarrierRole::Observe { + source, + observation, + target, + } => { + let input = self.carrier_loan(*source)?; + let selected = match observation { + Observation::Ordinary { path, .. } => path.as_slice(), + _ => &[], + }; + let selection = self.carrier_selection(selected)?; + let (context, frame, ordinal) = self.carrier_operation(operation)?; + let bits = Leaf::CarrierObserve.output( + self.module, + self.builder, + &[context, frame, ordinal, input, selection], + resource.failure, + 8, + )?; + let result = match observation { + Observation::Length | Observation::Tag => LoweredValue::Scalar(bits), + Observation::State { state } => { + let selected = i64::try_from(*state) + .map_err(|_| pending("checked carrier state is outside tag width"))?; + LoweredValue::Scalar(self.builder.ins().icmp_imm( + IntCC::Equal, + bits, + selected, + )) + } + Observation::Ordinary { ty, .. } => self.unpack_payload(bits, *ty, span)?, + }; + if let Some(target) = target { + let bits = self.scalar(result, span)?; + let result = if self.local_types[target.index() as usize].ty + == TypeInterner::BOOL + && self.builder.func.dfg.value_type(bits) != ir::types::I8 + { + LoweredValue::Scalar(self.builder.ins().ireduce(ir::types::I8, bits)) + } else { + result + }; + self.define_local(*target, result, span)?; + } + Ok(Some(result)) + } + CarrierRole::Extract { + source, + path, + destination, + } => { + let (root, mut full) = self.carrier_owned_root(*source)?; + full.extend_from_slice(path); + let input = self.carrier_owner(root)?; + let selection = self.carrier_selection(&full)?; + let (context, frame, ordinal) = self.carrier_operation(operation)?; + let bits = Leaf::CarrierExtract.output( + self.module, + self.builder, + &[context, frame, ordinal, input, selection], + resource.failure, + 8, + )?; + self.carrier_store(*destination, bits)?; + if let Storage::Local { header } = + resource.carrier_plan()?.slots()[destination.index()].storage() + { + self.define_local(header.id, LoweredValue::Scalar(bits), span)?; + } + Ok(Some(LoweredValue::Scalar(bits))) + } + CarrierRole::ExtractOrdinary { + source, + path, + target, + ty, + } => { + let (root, mut full) = self.carrier_owned_root(*source)?; + full.extend_from_slice(path); + let input = self.carrier_owner(root)?; + let selection = self.carrier_selection(&full)?; + let (context, frame, ordinal) = self.carrier_operation(operation)?; + let bits = Leaf::CarrierExtract.output( + self.module, + self.builder, + &[context, frame, ordinal, input, selection], + resource.failure, + 8, + )?; + let value = self.unpack_payload(bits, *ty, span)?; + self.define_local(*target, value, span)?; + Ok(Some(value)) + } + CarrierRole::AdaptSum { + source, + path, + destination, + loan, + .. + } => { + let (input, full) = if loan.is_some() { + (self.carrier_loan(*source)?, path.clone()) + } else { + let (root, mut full) = self.carrier_owned_root(*source)?; + full.extend_from_slice(path); + (self.carrier_owner(root)?, full) + }; + let selection = self.carrier_selection(&full)?; + let (context, frame, ordinal) = self.carrier_operation(operation)?; + let bits = Leaf::CarrierAdaptSum.output( + self.module, + self.builder, + &[context, frame, ordinal, input, selection], + resource.failure, + 8, + )?; + if let Some(loan) = loan { + // The adapter returns the exact SumLoan. Its parent-bound + // shell is owned solely by runtime, never an ordinary owner. + self.builder + .ins() + .stack_store(bits, resource.loans[loan.index()], 0); + } else { + self.resource_store(*destination, bits)?; + if let Storage::Local { header } = + resource.function_plan()?.owner_slots()[destination.index()].storage() + { + self.define_local(header.id, LoweredValue::Scalar(bits), span)?; + } + } + Ok(Some(LoweredValue::Scalar(bits))) + } + CarrierRole::Retire { source } => { + let input = self.carrier_owner(*source)?; + let (context, frame, ordinal) = self.carrier_operation(operation)?; + Leaf::CarrierRetire.checked( + self.module, + self.builder, + &[context, frame, ordinal, input], + resource.failure, + )?; + self.clear_slot(resource.carrier_owners[source.index()]); + Ok(None) + } + CarrierRole::RetireIteration { source, binders } => { + let input = self.carrier_owner(*source)?; + let (context, frame, ordinal) = self.carrier_operation(operation)?; + Leaf::CarrierResetIteration.checked( + self.module, + self.builder, + &[context, frame, ordinal, input], + resource.failure, + )?; + for binder in binders { + if let Some(slot) = resource.carrier_local_slot(*binder)? { + self.clear_slot(resource.carrier_owners[slot.index()]); + } + if let Some(slot) = resource.local_slot(*binder)? { + self.clear_slot(resource.owners[slot.index()]); + } + } + Ok(None) + } + CarrierRole::BeginConstructor { .. } + | CarrierRole::ConstructorChild { .. } + | CarrierRole::CommitConstructor { .. } + | CarrierRole::PublishReturn { .. } => Err(pending( + "carrier effect must use its exact constructor/publication phase", + )), + } + } + fn carrier_construct( + &mut self, + expression: &Expression, + operations: &[&Operation], + ) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("carrier constructor has no family"))?; + let mut output = None; + let mut destination = None; + let mut arrival = 0usize; + for operation in operations { + match operation.role() { + CarrierRole::BeginConstructor { + destination: slot, .. + } => { + if destination.replace(*slot).is_some() { + return Err(pending( + "carrier expression contains multiple outer constructors", + )); + } + let (context, frame, ordinal) = self.carrier_operation(operation)?; + let builder = Leaf::CarrierConstructBegin.output( + self.module, + self.builder, + &[context, frame, ordinal], + resource.failure, + 8, + )?; + self.builder.ins().stack_store( + builder, + resource.carrier_builders[slot.index()], + 0, + ); + } + CarrierRole::ConstructorChild { + destination: slot, + child, + } => { + if destination != Some(*slot) { + return Err(pending( + "carrier child changed its exact constructor destination", + )); + } + let actual = constructor_child(expression, child.index)?; + let evaluated = self.expression(actual)?; + let value = match child.value { + CarrierValue::Ordinary { ty } => { + if ty != actual.ty { + return Err(pending( + "carrier ordinary child changed its exact source type", + )); + } + if is_copy_owned(self.types, ty) || is_linear(self.types, ty) { + self.own_copy_value(evaluated, ty, actual.span)? + } else { + evaluated + } + } + _ => evaluated, + }; + let bits = match child.value { + CarrierValue::Owned { slot } => self.carrier_owner(slot)?, + CarrierValue::LeafOwned { slot } => self.resource_owner(slot)?, + CarrierValue::Ordinary { .. } => self.payload_bits(value).0, + CarrierValue::Borrowed { .. } | CarrierValue::LeafBorrowed { .. } => { + return Err(pending( + "carrier constructor cannot adopt a borrowed child", + )); + } + }; + let builder = self.builder.ins().stack_load( + ir::types::I64, + resource.carrier_builders[slot.index()], + 0, + ); + let context = self.resource_context(); + let index = u32::try_from(arrival) + .map_err(|_| pending("carrier child arrival is outside wire width"))?; + arrival = arrival + .checked_add(1) + .ok_or_else(|| pending("carrier child arrival overflow"))?; + let index = self.builder.ins().iconst(ir::types::I32, i64::from(index)); + Leaf::CarrierChild.checked( + self.module, + self.builder, + &[context, builder, index, bits], + resource.failure, + )?; + match child.value { + CarrierValue::Owned { slot } => { + self.clear_slot(resource.carrier_owners[slot.index()]) + } + CarrierValue::LeafOwned { slot } => { + self.clear_slot(resource.owners[slot.index()]) + } + CarrierValue::Ordinary { .. } => { + if let LoweredValue::Owned(_, slot) = value { + self.clear_slot(slot); + } + } + CarrierValue::Borrowed { .. } | CarrierValue::LeafBorrowed { .. } => { + unreachable!() + } + } + } + CarrierRole::CommitConstructor { destination: slot } => { + if destination != Some(*slot) || output.is_some() { + return Err(pending( + "carrier constructor changed its exact commit phase", + )); + } + let builder = self.builder.ins().stack_load( + ir::types::I64, + resource.carrier_builders[slot.index()], + 0, + ); + let context = self.resource_context(); + let bits = Leaf::CarrierCommit.output( + self.module, + self.builder, + &[context, builder], + resource.failure, + 8, + )?; + self.clear_slot(resource.carrier_builders[slot.index()]); + self.carrier_store(*slot, bits)?; + output = Some(LoweredValue::Scalar(bits)); + } + _ => { + return Err(pending( + "carrier constructor selected an unrelated outer expression role", + )); + } + } + } + output.ok_or_else(|| pending("carrier constructor omitted its exact commit")) + } + pub(super) fn carrier_expression( + &mut self, + expression: &Expression, + ) -> Result, CodegenError> { + let Some(resource) = self.resource else { + return Ok(None); + }; + let Some(plan) = resource.function_plan()?.carriers() else { + return Ok(None); + }; + let operations = plan + .operations_for_expression(expression) + .collect::>(); + if operations + .iter() + .any(|row| matches!(row.role(), CarrierRole::BeginConstructor { .. })) + { + return self.carrier_construct(expression, &operations).map(Some); + } + if !operations.is_empty() { + match &expression.kind { + ExpressionKind::Field { base, .. } => { + self.expression(base)?; + } + ExpressionKind::StateIs { value, .. } => { + self.expression(value)?; + } + ExpressionKind::InterfaceCoerce { value, adapters } if adapters.is_empty() => { + self.expression(value)?; + } + ExpressionKind::Intrinsic { args, .. } => { + for argument in args { + self.expression(argument)?; + } + } + _ => {} + } + } + let mut value = None; + for operation in operations { + if let Some(result) = self.carrier_effect(operation, expression.span)? { + value = Some(result); + } + } + if value.is_some() { + return Ok(value); + } + if !super::super::resource_layout::carrier_operations::is_carrier_type( + resource.layout.plan(), + expression.ty, + ) { + return Ok(None); + } + match &expression.kind { + ExpressionKind::Local(local) => { + let bits = if let Some(slot) = resource.carrier_local_slot(*local)? { + self.carrier_owner(slot)? + } else { + let variable = self.variables[local.index() as usize] + .ok_or_else(|| pending("carrier view local has no current variable"))?; + self.builder.use_var(variable) + }; + Ok(Some(LoweredValue::Scalar(bits))) + } + ExpressionKind::View(inner) => self.expression(inner).map(Some), + _ => Err(pending( + "carrier expression has no exact constructor/projection/current-local occurrence", + )), + } + } + pub(super) fn carrier_stage_actual( + &mut self, + operations: &[&jett_mir::ResourceOperation], + frame: jett_mir::ResourceFrameId, + parameter: usize, + ) -> Result<(), CodegenError> { + let Some(resource) = self.resource else { + return Ok(()); + }; + let Some(plan) = resource.function_plan()?.carriers() else { + return Ok(()); + }; + for wrapper in operations + .iter() + .copied() + .filter(|row| row.frame() == frame) + { + let Role::Carrier { operation } = wrapper.role() else { + continue; + }; + let row = plan + .operations() + .get(operation.index()) + .ok_or_else(|| pending("carrier staging lost its exact occurrence"))?; + match row.role() { + CarrierRole::Transfer { destination, .. } + | CarrierRole::QualifyMachine { destination, .. } + if matches!(plan.slots()[destination.index()].storage(), Storage::Argument { parameter: selected, .. } if *selected == parameter) => + { + self.carrier_transfer(row)?; + } + CarrierRole::Borrow { loan } => { + let selected = operations.iter().any(|wrapper| match wrapper.role() { + Role::InvokeSourceFunction { operands, .. } => operands.iter().any(|operand| matches!(operand, Operand::CarrierBorrowed { parameter: actual, loan: selected } if *actual == parameter && selected == loan)), + _ => false, + }); + if selected { + self.carrier_begin_borrow(row)?; + } + } + _ => {} + } + } + Ok(()) + } + pub(super) fn carrier_finish_operation( + &mut self, + wrapper: &jett_mir::ResourceOperation, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("carrier finish has no family"))?; + let Role::Carrier { operation } = wrapper.role() else { + return Ok(()); + }; + let row = resource + .carrier_plan()? + .operations() + .get(operation.index()) + .ok_or_else(|| pending("carrier finish lost its exact occurrence"))?; + if matches!( + row.role(), + CarrierRole::EndBorrow { .. } | CarrierRole::Retire { .. } + ) { + let span = resource + .layout + .plan() + .program() + .functions + .get(resource.function.index() as usize) + .filter(|function| function.id == resource.function) + .ok_or_else(|| pending("carrier completion lost its original function"))? + .span; + self.carrier_effect(row, span)?; + } + Ok(()) + } + pub(super) fn carrier_statement( + &mut self, + statement: &Statement, + ) -> Result { + let Some(resource) = self.resource else { + return Ok(false); + }; + if resource.function_plan()?.carriers().is_none() { + return Ok(false); + } + if let StatementKind::ResourceConstructor(node) = &statement.kind { + self.carrier_staged_constructor(node, statement.span)?; + return Ok(true); + } + let operations = resource.carrier_at_site()?; + let mut claimed = false; + match &statement.kind { + StatementKind::Let { local, value } + if super::super::resource_layout::carrier_operations::is_carrier_type( + resource.layout.plan(), + value.ty, + ) => + { + let mut evaluated = self.expression(value)?; + for operation in &operations { + if let Some(value) = self.carrier_effect(operation, statement.span)? { + evaluated = value; + } + } + self.define_local(*local, evaluated, statement.span)?; + return Ok(true); + } + StatementKind::Evaluate(value) | StatementKind::HandleDefault(value) + if super::super::resource_layout::carrier_operations::is_carrier_type( + resource.layout.plan(), + value.ty, + ) => + { + self.expression(value)?; + claimed = true; + } + StatementKind::SequenceLength { source, .. } + | StatementKind::SequenceGet { source, .. } + if source + .ty(resource + .layout + .plan() + .program() + .functions + .get(resource.function.index() as usize) + .ok_or_else(|| pending("carrier sequence lost its current function"))?) + .is_some_and(|ty| { + super::super::resource_layout::carrier_operations::is_carrier_type( + resource.layout.plan(), + ty, + ) + }) => + { + claimed = true; + } + StatementKind::SumTag { source, .. } | StatementKind::SumTake { source, .. } + if super::super::resource_layout::carrier_operations::is_carrier_type( + resource.layout.plan(), + self.local_types[source.index() as usize].ty, + ) => + { + claimed = true; + } + _ => {} + } + if claimed { + for operation in operations { + self.carrier_effect(operation, statement.span)?; + } + } + Ok(claimed) + } + pub(in crate::emit) fn carrier_iteration_boundary( + &mut self, + span: Span, + ) -> Result<(), CodegenError> { + let Some(resource) = self.resource else { + return Ok(()); + }; + if resource.function_plan()?.carriers().is_none() { + return Ok(()); + } + for operation in resource.carrier_at_site()? { + if matches!(operation.role(), CarrierRole::RetireIteration { .. }) { + self.carrier_effect(operation, span)?; + } + } + Ok(()) + } + pub(in crate::emit) fn carrier_switch( + &mut self, + scrutinee: &Expression, + variants: &[( + jett_hir::VariantId, + jett_mir::BlockId, + Vec, + )], + otherwise: Option, + span: Span, + ) -> Result { + let Some(resource) = self.resource else { + return Ok(false); + }; + if !super::super::resource_layout::carrier_operations::is_carrier_type( + resource.layout.plan(), + scrutinee.ty, + ) { + return Ok(false); + } + self.expression(scrutinee)?; + let plan = resource.carrier_plan()?; + let mut tag = None; + for row in resource.carrier_at_site()? { + let output = self.carrier_effect(row, span)?; + if matches!( + row.role(), + CarrierRole::Observe { + observation: Observation::Tag, + .. + } + ) { + if tag.is_some() { + return Err(pending("carrier Switch has multiple tag selectors")); + } + let output = output.ok_or_else(|| pending("carrier Switch omitted tag output"))?; + tag = Some(self.scalar(output, span)?); + } + } + let tag = tag.ok_or_else(|| pending("carrier Switch has no exact tag observation"))?; + let Type::Enum(owner) = self.types.resolve(scrutinee.ty) else { + return Err(pending("carrier Switch lost its exact enum type")); + }; + let definition = self.types.resolve_enum(*owner); + for (variant, target, _) in variants { + let discriminator = definition + .variants + .get(variant.index() as usize) + .ok_or_else(|| pending("carrier Switch selected a foreign variant"))? + .discriminant; + let matches = self + .builder + .ins() + .icmp_imm(IntCC::Equal, tag, discriminator); + let selected = self.builder.create_block(); + let next = self.builder.create_block(); + self.builder.ins().brif(matches, selected, &[], next, &[]); + self.builder.switch_to_block(selected); + let edge = Edge::SwitchVariant { + source: resource.block, + variant: *variant, + target: *target, + }; + for operation in plan.operations_on_edge(edge) { + self.carrier_effect(operation, span)?; + } + let target = block_for(self.blocks, target.index(), span, self.symbol)?; + self.drop_temporaries()?; + self.builder.ins().jump(target, &[]); + self.builder.switch_to_block(next); + } + if let Some(target) = otherwise { + for operation in plan.operations_on_edge(Edge::SwitchOtherwise { + source: resource.block, + target, + }) { + self.carrier_effect(operation, span)?; + } + self.drop_temporaries()?; + let target = block_for(self.blocks, target.index(), span, self.symbol)?; + self.builder.ins().jump(target, &[]); + } else { + self.builder.ins().trap(TrapCode::unwrap_user(1)); + } + Ok(true) + } +} + +fn constructor_child(expression: &Expression, index: usize) -> Result<&Expression, CodegenError> { + let child = match &expression.kind { + ExpressionKind::ListConstruct { elements } => elements.get(index), + ExpressionKind::MapConstruct { entries } => entries.get(index / 2).map(|entry| { + if index % 2 == 0 { + &entry.key + } else { + &entry.value + } + }), + ExpressionKind::StructConstruct { fields, .. } => fields.get(index), + ExpressionKind::EnumConstruct { payloads, .. } + | ExpressionKind::MachineConstruct { payloads, .. } => payloads.get(index), + ExpressionKind::OptionalSome(inner) + | ExpressionKind::ResultOk(inner) + | ExpressionKind::ResultFail(inner) + if index == 0 => + { + Some(inner.as_ref()) + } + _ => None, + }; + child + .ok_or_else(|| pending("carrier child lost its exact original/current constructor operand")) +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/leaves.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/leaves.rs new file mode 100644 index 00000000..5d161a30 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/leaves.rs @@ -0,0 +1,303 @@ +//! Exact selected039a imports for the dedicated Resource family. +//! No leaf declaration grants Source authority or creates a raw runtime key. +use super::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum Leaf { + ProgramBegin, + ProgramComplete, + EntryScope, + EntryNetwork, + EntryOutcome, + ScopeValidate, + OperationBegin, + OperationComplete, + HookPrepare, + FactoryPrepare, + FactoryCommit, + BorrowBegin, + BorrowCommit, + BorrowEnd, + Close, + DescriptorClose, + Descriptor, + Transfer, + DestinationFrame, + SumAdopt, + SumTag, + SumBorrow, + SumViewTag, + SumViewProject, + SumFailureRead, + SumBorrowEnd, + SumTake, + SumDrop, + FailureCompanionTake, + AbsentSum, + FailureSum, + Replace, + SourcePrepare, + SourceActual, + SourceEnter, + SourceParameter, + SourceStatus, + ScopeComplete, + ReturnPublish, + CarrierConstructBegin, + CarrierChild, + CarrierCommit, + CarrierTransfer, + CarrierBorrow, + CarrierExtract, + CarrierAdaptSum, + CarrierObserve, + CarrierEndBorrow, + CarrierRetire, + CarrierResetIteration, + CarrierQualify, + CarrierPublish, +} +impl Leaf { + fn spec(self) -> (&'static str, &'static [ir::Type]) { + use Leaf::*; + use ir::types::{I32, I64}; + match self { + ProgramBegin => ("jett_rt_v1_resource_program_begin", &[I64, I64, I64, I64]), + ProgramComplete => ("jett_rt_v1_resource_program_complete", &[I64, I64, I32]), + EntryScope => ("jett_rt_v1_resource_entry_scope", &[I64, I64, I64]), + EntryNetwork => ("jett_rt_v1_resource_entry_network", &[I64, I32, I64, I64]), + EntryOutcome => ( + "jett_rt_v1_resource_entry_outcome", + &[I64, I64, I32, I32, I32, I64], + ), + ScopeValidate => ( + "jett_rt_v1_resource_scope_validate", + &[I64, I64, I32, I32, I32], + ), + OperationBegin => ("jett_rt_v1_resource_operation_begin", &[I64, I64, I32, I64]), + OperationComplete => ("jett_rt_v1_resource_operation_complete", &[I64, I64, I32]), + HookPrepare => ( + "jett_rt_v1_resource_hook_prepare", + &[I64, I64, I32, I64, I64], + ), + FactoryPrepare => ("jett_rt_v1_resource_factory_prepare", &[I64, I64, I64, I64]), + FactoryCommit => ( + "jett_rt_v1_resource_factory_commit", + &[I64, I64, I64, I64, I64], + ), + BorrowBegin => ( + "jett_rt_v1_resource_borrow_begin", + &[I64, I64, I32, I64, I64], + ), + BorrowCommit => ( + "jett_rt_v1_resource_borrow_commit", + &[I64, I64, I64, I64, I64], + ), + BorrowEnd => ("jett_rt_v1_resource_borrow_end", &[I64, I64, I32, I64]), + Close => ("jett_rt_v1_resource_close", &[I64, I64, I32, I64]), + DescriptorClose => ("jett_rt_v1_resource_descriptor_close", &[I64, I64, I64]), + Descriptor => ("jett_rt_v1_resource_descriptor", &[I64, I32, I64]), + Transfer => ( + "jett_rt_v1_resource_transfer", + &[I64, I64, I32, I64, I64, I64], + ), + DestinationFrame => ( + "jett_rt_v1_resource_destination_frame", + &[I64, I64, I32, I64], + ), + SumAdopt => ( + "jett_rt_v1_resource_sum_adopt", + &[I64, I64, I32, I64, I64, I64], + ), + SumTag => ("jett_rt_v1_resource_sum_tag", &[I64, I64, I64, I64]), + SumBorrow => ("jett_rt_v1_resource_sum_borrow", &[I64, I64, I32, I64, I64]), + SumViewTag => ( + "jett_rt_v1_resource_sum_view_tag", + &[I64, I64, I32, I64, I64], + ), + SumViewProject => ( + "jett_rt_v1_resource_sum_view_project", + &[I64, I64, I32, I64, I64], + ), + SumFailureRead => ( + "jett_rt_v1_resource_sum_failure_read", + &[I64, I64, I32, I64, I64], + ), + SumBorrowEnd => ("jett_rt_v1_resource_sum_borrow_end", &[I64, I64, I32, I64]), + SumTake => ( + "jett_rt_v1_resource_sum_take", + &[I64, I64, I32, I64, I64, I64], + ), + SumDrop => ("jett_rt_v1_resource_sum_drop", &[I64, I64, I32, I64]), + FailureCompanionTake => ( + "jett_rt_v1_resource_failure_companion_take", + &[I64, I64, I32, I64, I64], + ), + AbsentSum => ("jett_rt_v1_resource_absent_sum", &[I64, I64, I32, I64]), + FailureSum => ( + "jett_rt_v1_resource_failure_sum", + &[I64, I64, I32, I64, I64], + ), + Replace => ( + "jett_rt_v1_resource_replace", + &[I64, I64, I32, I64, I64, I64], + ), + SourcePrepare => ("jett_rt_v1_resource_source_prepare", &[I64, I64, I32, I64]), + SourceActual => ( + "jett_rt_v1_resource_source_actual", + &[I64, I64, I32, I32, I64], + ), + SourceEnter => ("jett_rt_v1_resource_source_enter", &[I64, I64, I64]), + SourceParameter => ( + "jett_rt_v1_resource_source_parameter", + &[I64, I64, I32, I64], + ), + SourceStatus => ("jett_rt_v1_resource_source_status", &[I64, I64]), + ScopeComplete => ("jett_rt_v1_resource_scope_complete", &[I64, I64, I32, I32]), + CarrierConstructBegin => ( + "jett_rt_v1_resource_carrier_construct_begin", + &[I64, I64, I32, I64], + ), + CarrierChild => ("jett_rt_v1_resource_carrier_child", &[I64, I64, I32, I64]), + CarrierCommit => ("jett_rt_v1_resource_carrier_commit", &[I64, I64, I64]), + CarrierTransfer => ( + "jett_rt_v1_resource_carrier_transfer", + &[I64, I64, I32, I64, I64], + ), + CarrierBorrow => ( + "jett_rt_v1_resource_carrier_borrow", + &[I64, I64, I32, I64, I64, I64], + ), + CarrierExtract => ( + "jett_rt_v1_resource_carrier_extract", + &[I64, I64, I32, I64, I64, I64], + ), + CarrierAdaptSum => ( + "jett_rt_v1_resource_carrier_adapt", + &[I64, I64, I32, I64, I64, I64], + ), + CarrierObserve => ( + "jett_rt_v1_resource_carrier_observe", + &[I64, I64, I32, I64, I64, I64], + ), + CarrierEndBorrow => ("jett_rt_v1_resource_carrier_end", &[I64, I64, I32, I64]), + CarrierRetire => ("jett_rt_v1_resource_carrier_retire", &[I64, I64, I32, I64]), + CarrierResetIteration => ( + "jett_rt_v1_resource_carrier_reset_iteration", + &[I64, I64, I32, I64], + ), + CarrierQualify => ( + "jett_rt_v1_resource_carrier_qualify", + &[I64, I64, I32, I64, I64], + ), + CarrierPublish => ( + "jett_rt_v1_resource_carrier_publish", + &[I64, I64, I32, I64, I64], + ), + ReturnPublish => ( + "jett_rt_v1_resource_return_publish", + &[I64, I64, I32, I64, I64], + ), + } + } + pub(super) fn declare( + self, + module: &mut ObjectModule, + ) -> Result { + if module.target_config().pointer_type() != ir::types::I64 { + return Err(pending( + "dedicated Resource leaves require selected64-bit context pointers", + )); + } + let (symbol, params) = self.spec(); + let mut signature = module.make_signature(); + signature + .params + .extend(params.iter().copied().map(AbiParam::new)); + signature.returns.push(AbiParam::new(ir::types::I32)); + module + .declare_function(symbol, Linkage::Import, &signature) + .map_err(|error| CodegenError::Backend(format!("Resource leaf {symbol}: {error}"))) + } + pub(super) fn call( + self, + module: &mut ObjectModule, + builder: &mut FunctionBuilder<'_>, + arguments: &[Value], + ) -> Result { + let (_, params) = self.spec(); + if arguments.len() != params.len() + || arguments + .iter() + .zip(params) + .any(|(value, ty)| builder.func.dfg.value_type(*value) != *ty) + { + return Err(pending( + "Resource leaf actual ABI differs from its selected signature", + )); + } + let id = self.declare(module)?; + let reference = module.declare_func_in_func(id, builder.func); + let call = builder.ins().call(reference, arguments); + builder + .inst_results(call) + .first() + .copied() + .ok_or_else(|| pending("Resource leaf has no exact Status result")) + } + /// The failure block receives this exact Status and runs the full ordinary + /// companion and Resource cleanup path. A success output is read only on0. + pub(super) fn checked( + self, + module: &mut ObjectModule, + builder: &mut FunctionBuilder<'_>, + arguments: &[Value], + failure: ir::Block, + ) -> Result<(), CodegenError> { + let status = self.call(module, builder, arguments)?; + let accepted = builder.create_block(); + let success = builder.ins().icmp_imm(IntCC::Equal, status, 0); + builder + .ins() + .brif(success, accepted, &[], failure, &[status.into()]); + builder.switch_to_block(accepted); + Ok(()) + } + pub(super) fn output( + self, + module: &mut ObjectModule, + builder: &mut FunctionBuilder<'_>, + arguments: &[Value], + failure: ir::Block, + result_bytes: u32, + ) -> Result { + if !matches!(result_bytes, 4 | 8 | 16) || (result_bytes == 4) != (self == Leaf::SumViewTag) + { + return Err(pending( + "Resource out-storage differs from its exact selected leaf layout", + )); + } + let output_type = if result_bytes == 4 { + ir::types::I32 + } else { + ir::types::I64 + }; + let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + result_bytes, + 3, + )); + let zero = builder.ins().iconst(output_type, 0); + builder.ins().stack_store(zero, slot, 0); + if result_bytes == 16 { + builder.ins().stack_store(zero, slot, 8); + } + let address = builder.ins().stack_addr(ir::types::I64, slot, 0); + let mut arguments = arguments.to_vec(); + arguments.push(address); + self.checked(module, builder, &arguments, failure)?; + Ok(builder + .ins() + .stack_load(output_type, slot, if result_bytes == 16 { 8 } else { 0 })) + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/lexical_borrows.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/lexical_borrows.rs new file mode 100644 index 00000000..b4028511 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/lexical_borrows.rs @@ -0,0 +1,73 @@ +//! Emit only constructor-certified lexical loan ends; keep RootScope active. +use super::*; + +impl Translator<'_, '_> { + pub(super) fn resource_lexical_retirement(&mut self, span: Span) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("lexical retirement has no selected Resource family"))?; + let plan = resource.function_plan()?; + let Some(exit) = plan.lexical_exit(resource.block, resource.position) else { + return Ok(false); + }; + let function = resource + .layout + .plan() + .program() + .functions + .get(resource.function.index() as usize) + .filter(|function| function.id == resource.function) + .ok_or_else(|| pending("lexical retirement lost its exact current function"))?; + let original = function + .resource_lexical_exit(resource.block, resource.position) + .map_err(|message| pending(&message))? + .ok_or_else(|| pending("lexical retirement lost its private constructor exit"))?; + if original.kind() != exit.kind() + || (exit.kind() == jett_mir::ResourceLexicalExitKind::Return) + != (resource.position == jett_mir::ResourcePosition::Terminator) + { + return Err(pending("lexical retirement changed its exact exit kind")); + } + for id in exit.operation_ids() { + let operation = plan + .operations() + .get(id.index()) + .filter(|operation| { + operation.id() == *id + && operation.site().function() == resource.function + && operation.site().block() == resource.block + && operation.site().position() == resource.position + && !operation.is_expression_operation() + }) + .ok_or_else(|| pending("lexical retirement lost an exact ordered end row"))?; + self.resource_end_borrow(operation)?; + } + for local in exit.clear_locals() { + let index = local.index() as usize; + let header = self + .local_types + .get(index) + .filter(|header| header.id == *local) + .ok_or_else(|| pending("lexical retirement lost a current alias header"))?; + if !resource.layout.plan().type_requires_custody(header.ty) + || resource.local_slot(*local)?.is_some() + || self.local_slots[index].is_some() + { + return Err(pending( + "lexical alias retirement selected owning or ordinary storage", + )); + } + let variable = variable_for(self.variables, local.index(), span, self.symbol)? + .ok_or_else(|| pending("lexical retirement lost its nonowning scalar local"))?; + let zero = self.builder.ins().iconst(ir::types::I64, 0); + self.builder.try_def_var(variable, zero).map_err(|error| { + contract_error( + self.symbol, + span, + format!("cannot clear retired Resource alias: {error}"), + ) + })?; + } + Ok(true) + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect.rs new file mode 100644 index 00000000..dd58b59d --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect.rs @@ -0,0 +1,231 @@ +//! The ordinary descriptor selects only the constructor-proved Source body. +//! It never supplies a Resource family's hidden Scope through call_indirect. +use super::*; + +fn descriptor_identity( + builder: &mut FunctionBuilder<'_>, + code: Value, + environment: Value, + expected: Value, +) -> Value { + let selected = builder + .ins() + .icmp(ir::condcodes::IntCC::Equal, code, expected); + let empty = builder + .ins() + .icmp_imm(ir::condcodes::IntCC::Equal, environment, 0); + builder.ins().band(selected, empty) +} + +impl Translator<'_, '_> { + pub(in crate::emit) fn resource_is_named_value( + &self, + expression: &Expression, + ) -> Result { + let Some(resource) = self.resource else { + return Ok(false); + }; + let Some(proof) = resource.function_plan()?.named_callable_value(expression) else { + return Ok(false); + }; + let selected = program_function(resource.layout.plan().program(), proof.function())?; + if proof.signature_type() != expression.ty + || proof.identity() != &selected.identity + || !matches!(self.types.resolve(expression.ty), Type::Function { .. }) + { + return Err(pending( + "ordinary named descriptor occurrence changes its sealed proof", + )); + } + Ok(true) + } + + pub(in crate::emit) fn resource_validate_named_producer( + &self, + expression: &Expression, + target: FunctionId, + ) -> Result<(), CodegenError> { + let Some(resource) = self.resource else { + return Ok(()); + }; + if resource.layout.plan().function(target).is_none() { + return Ok(()); + } + let proof = resource + .function_plan()? + .named_callable_producer(expression) + .ok_or_else(|| { + pending("Resource family descriptor has no exact current named producer") + })?; + let selected = program_function(resource.layout.plan().program(), target)?; + if !matches!(expression.kind, ExpressionKind::FunctionRef(function) if function == target) + || proof.function() != target + || proof.signature_type() != expression.ty + || proof.identity() != &selected.identity + { + return Err(pending( + "Resource named descriptor changes its sealed producer", + )); + } + Ok(()) + } + + pub(super) fn resource_validate_named_invocation( + &self, + invocation: &jett_mir::ResourceOperation, + callee: Option<&Expression>, + ) -> Result<(), CodegenError> { + let Role::InvokeSourceFunction { + function, source, .. + } = invocation.role() + else { + return Ok(()); + }; + match (&source.target, callee, invocation.named_indirect_target()) { + (jett_hir::CallTarget::Indirect { signature_type }, Some(callee), Some(proof)) => { + let resource = self + .resource + .ok_or_else(|| pending("named Source call lost its family"))?; + let selected = program_function(resource.layout.plan().program(), *function)?; + if proof.function() != *function + || proof.signature_type() != *signature_type + || callee.ty != *signature_type + || proof.identity() != &selected.identity + || source.bridge != jett_hir::CallBridge::Direct + || !source.generated_operands.is_empty() + { + return Err(pending( + "named indirect Source call changes its sealed body or tuple", + )); + } + Ok(()) + } + (jett_hir::CallTarget::Indirect { .. }, _, _) => Err(pending( + "indirect Source call has no exact current named target proof", + )), + (_, None, None) => Ok(()), + _ => Err(pending( + "named indirect target proof is attached to a different Source call", + )), + } + } + + pub(super) fn resource_validate_named_descriptor( + &mut self, + descriptor: Value, + invocation: &jett_mir::ResourceOperation, + span: Span, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("named descriptor has no current family"))?; + let proof = invocation + .named_indirect_target() + .ok_or_else(|| pending("named descriptor has no exact invocation proof"))?; + self.leaf(NativeLeaf::FunctionCallableCheck, &[descriptor], true)?; + let code_index = self + .builder + .ins() + .iconst(ir::types::I64, NATIVE_FUNCTION_CODE_FIELD as i64); + let code = self.leaf(NativeLeaf::StructField, &[descriptor, code_index], true)?; + let environment_index = self + .builder + .ins() + .iconst(ir::types::I64, NATIVE_FUNCTION_ENVIRONMENT_FIELD as i64); + let environment = self.leaf( + NativeLeaf::StructField, + &[descriptor, environment_index], + true, + )?; + let declaration = self.declarations.get(proof.function()).ok_or_else(|| { + contract_error(self.symbol, span, "named Resource target is not reachable") + })?; + let reference = self + .module + .declare_func_in_func(declaration.native_id, self.builder.func); + let pointer = self.module.target_config().pointer_type(); + let expected = self.builder.ins().func_addr(pointer, reference); + let expected = if pointer == ir::types::I64 { + expected + } else { + self.builder.ins().uextend(ir::types::I64, expected) + }; + let valid = descriptor_identity(self.builder, code, environment, expected); + let accepted = self.builder.create_block(); + let refused = self.builder.ins().iconst(ir::types::I32, 1); + self.builder + .ins() + .brif(valid, accepted, &[], resource.failure, &[refused.into()]); + self.builder.switch_to_block(accepted); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cranelift_codegen::{control::ControlPlane, settings::Configurable}; + + // Optimize the actual emitted predicate, rather than a Rust copy of it. + // Constant inputs expose a code substitution and a nonempty environment + // independently; these controls do not claim native Source execution. + fn optimized_identity(code: i64, environment: i64, expected: i64) -> i64 { + let mut flags = settings::builder(); + flags.set("opt_level", "speed").unwrap(); + let isa = isa::lookup(HOST) + .unwrap() + .finish(settings::Flags::new(flags)) + .unwrap(); + let mut context = Context::new(); + context.func.signature = ir::Signature::new(isa.default_call_conv()); + context + .func + .signature + .returns + .push(AbiParam::new(ir::types::I8)); + let mut frontend = FunctionBuilderContext::new(); + { + let mut builder = FunctionBuilder::new(&mut context.func, &mut frontend); + let entry = builder.create_block(); + builder.switch_to_block(entry); + let code = builder.ins().iconst(ir::types::I64, code); + let environment = builder.ins().iconst(ir::types::I64, environment); + let expected = builder.ins().iconst(ir::types::I64, expected); + let accepted = descriptor_identity(&mut builder, code, environment, expected); + builder.ins().return_(&[accepted]); + builder.seal_all_blocks(); + builder.finalize(); + } + context.verify(isa.as_ref()).unwrap(); + context + .optimize(isa.as_ref(), &mut ControlPlane::default()) + .unwrap(); + context.verify(isa.as_ref()).unwrap(); + let returned = context + .func + .layout + .blocks() + .flat_map(|block| context.func.layout.block_insts(block)) + .find(|instruction| context.func.dfg.insts[*instruction].opcode() == ir::Opcode::Return) + .unwrap(); + let result = context + .func + .dfg + .resolve_aliases(context.func.dfg.inst_args(returned)[0]); + let ir::ValueDef::Result(instruction, _) = context.func.dfg.value_def(result) else { + panic!("identity predicate did not reduce to a constant"); + }; + let ir::InstructionData::UnaryImm { imm, .. } = context.func.dfg.insts[instruction] else { + panic!("identity predicate did not reduce to an immediate"); + }; + imm.bits() + } + + #[test] + fn resource_named_indirect_code_identity_and_environment_are_independent_guards() { + assert_eq!(optimized_identity(701, 0, 701), 1); + assert_eq!(optimized_identity(702, 0, 701), 0); + assert_eq!(optimized_identity(701, 522, 701), 0); + assert_eq!(optimized_identity(702, 522, 701), 0); + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect/18_indirect_named_close.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect/18_indirect_named_close.jett new file mode 100644 index 00000000..b75d5af7 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect/18_indirect_named_close.jett @@ -0,0 +1,16 @@ +namespace app +function wrong_target(token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 marker = resource_probe.terminal_label() + return nothing +function close_selected(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = close_selected + resource_probe.TestHandle token = resource_probe.create(view net, 701) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect/19_indirect_named_terminal_failure.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect/19_indirect_named_terminal_failure.jett new file mode 100644 index 00000000..eaa7e5a8 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect/19_indirect_named_terminal_failure.jett @@ -0,0 +1,16 @@ +namespace app +function wrong_target(token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 marker = resource_probe.terminal_label() + return nothing +function close_selected(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = wrong_target + resource_probe.TestHandle token = resource_probe.create(view net, 701) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect/20_indirect_named_alias.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect/20_indirect_named_alias.jett new file mode 100644 index 00000000..c0aa00e0 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect/20_indirect_named_alias.jett @@ -0,0 +1,17 @@ +namespace app +function wrong_target(token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 marker = resource_probe.terminal_label() + return nothing +function close_selected(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing selected = close_selected + function(resource_probe.TestHandle) returns nothing dispose = selected + resource_probe.TestHandle token = resource_probe.create(view net, 701) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/mutable_assignment.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/mutable_assignment.jett new file mode 100644 index 00000000..8fc9b31a --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/mutable_assignment.jett @@ -0,0 +1,44 @@ +namespace app +function observe_previous(view net: Network, view token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +export function replace_live(view net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2401) handle error: + return nothing + token = resource_probe.create(view net, 2402) handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + resource_probe.close(token) + return nothing +export function rebind_after_close(view net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2411) handle error: + return nothing + resource_probe.close(token) + token = resource_probe.create(view net, 2412) handle error: + return nothing + resource_probe.close(token) + return nothing +export function rebind_self(view net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2421) handle error: + return nothing + token = token + resource_probe.close(token) + return nothing +export function failed_rhs_keeps_owner(view net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2431) handle error: + return nothing + token = resource_probe.create(view net, 2432) handle error: + observe_previous(view net, view token) + return nothing + resource_probe.close(token) + return nothing +export function main(net: Network) returns nothing: + replace_live(view net) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/returned_hooks.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/returned_hooks.rs new file mode 100644 index 00000000..1f27e9f1 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/returned_hooks.rs @@ -0,0 +1,138 @@ +//! Opaque hook metadata uses exact constructor proof, never ordinary callable storage. +use super::*; + +impl Translator<'_, '_> { + pub(in crate::emit) fn resource_is_hook_descriptor( + &self, + expression: &Expression, + ) -> Result { + let Some(resource) = self.resource else { + return Ok(false); + }; + let Some(hook) = resource.function_plan()?.descriptor_value(expression) else { + return Ok(false); + }; + if hook.function_type() != expression.ty + || !resource + .layout + .plan() + .program() + .resource_manifest + .contains_hook(hook) + || !matches!(self.types.resolve(expression.ty), Type::Function { .. }) + { + return Err(pending( + "opaque descriptor occurrence changes its exact hook proof", + )); + } + Ok(true) + } + + pub(super) fn resource_validate_hook_invocation( + &self, + invocation: &jett_mir::ResourceOperation, + callee: Option<&Expression>, + ) -> Result<(), CodegenError> { + let Role::InvokeHook { hook, source, .. } = invocation.role() else { + return Ok(()); + }; + match (&source.target, callee, invocation.indirect_hook_target()) { + (jett_hir::CallTarget::Indirect { signature_type }, Some(callee), Some(proof)) => { + if proof != hook + || proof.function_type() != *signature_type + || callee.ty != *signature_type + || source.bridge != jett_hir::CallBridge::Direct + || !source.generated_operands.is_empty() + || !self.resource_is_hook_descriptor(callee)? + || self + .resource + .expect("selected family") + .function_plan()? + .descriptor_value(callee) + != Some(proof) + { + return Err(pending( + "indirect hook changes its exact descriptor, signature or Source tuple", + )); + } + Ok(()) + } + (jett_hir::CallTarget::Indirect { .. }, _, _) => Err(pending( + "indirect hook has no exact current descriptor target proof", + )), + (_, None, None) => Ok(()), + _ => Err(pending( + "hook descriptor proof is attached to a different original call", + )), + } + } + + pub(super) fn resource_hook_value( + &mut self, + expression: &Expression, + operations: &[&jett_mir::ResourceOperation], + ) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("descriptor has no family"))?; + let hook = resource + .function_plan()? + .descriptor_value(expression) + .ok_or_else(|| pending("descriptor has no exact current occurrence"))?; + let value = match &expression.kind { + ExpressionKind::Local(local) => { + if resource.function_plan()?.descriptor_local(*local) != Some(hook) + || self.local_slots[local.index() as usize].is_some() + { + return Err(pending( + "opaque descriptor Local gained ordinary ownership or changed its header", + )); + } + let variable = self.variables[local.index() as usize] + .ok_or_else(|| pending("opaque descriptor Local has no native variable"))?; + self.builder.try_use_var(variable).map_err(|error| { + pending(&format!( + "opaque descriptor Local is not initialized: {error}" + )) + })? + } + ExpressionKind::Clone(inner) => { + if resource.function_plan()?.descriptor_value(inner) != Some(hook) { + return Err(pending( + "opaque descriptor alias changes its exact producer", + )); + } + let lowered = self.expression(inner)?; + self.scalar(lowered, expression.span)? + } + ExpressionKind::ResourceHookValue { hook: producer } if producer == hook => { + let mut selected = operations.iter().copied().filter(|operation| + matches!(operation.role(), Role::Descriptor { hook: current } if current == hook)); + let operation = selected + .next() + .ok_or_else(|| pending("hook value lost its exact descriptor row"))?; + if selected.next().is_some() { + return Err(pending("hook value has duplicate descriptor rows")); + } + let context = self.resource_context(); + let ordinal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(resource.ordinal(operation)?)); + Leaf::Descriptor.output( + self.module, + self.builder, + &[context, ordinal], + resource.failure, + 8, + )? + } + _ => { + return Err(pending( + "opaque descriptor transport has no exact native carrier route", + )); + } + }; + Ok(LoweredValue::Scalar(value)) + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/09_later_handle_minimal.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/09_later_handle_minimal.jett new file mode 100644 index 00000000..97660978 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/09_later_handle_minimal.jett @@ -0,0 +1,16 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 502) handle error: + return nothing + accept_staged(token: token, marker: fail_marker() handle error: + return nothing + , net: view net) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/10_later_handle_original.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/10_later_handle_original.jett new file mode 100644 index 00000000..a56e1c27 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/10_later_handle_original.jett @@ -0,0 +1,24 @@ +namespace app +function stage_owned(view net: Network, token: resource_probe.TestHandle) returns resource_probe.TestHandle: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return token + return token +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 501) handle error: + return nothing + accept_staged(token: stage_owned(view net, token), marker: fail_marker() handle error: + return nothing + , net: view net) + return nothing +export function main(net: Network) returns nothing: + scenario(view net) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/11_later_handle_success.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/11_later_handle_success.jett new file mode 100644 index 00000000..0e951718 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/11_later_handle_success.jett @@ -0,0 +1,16 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function successful_marker() returns result[int64, string]: + return ok(7) +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 503) handle error: + return nothing + accept_staged(token: token, marker: successful_marker() handle error: + return nothing + , net: view net) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/12_later_handle_written_view.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/12_later_handle_written_view.jett new file mode 100644 index 00000000..b0e4afff --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/12_later_handle_written_view.jett @@ -0,0 +1,22 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function observe_retained(view net: Network, view token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 retained = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 505) handle error: + return nothing + accept_staged(token: view token, marker: fail_marker() handle error: + observe_retained(view net, view token) + return nothing + , net: view net) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/13_first_handle_failure.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/13_first_handle_failure.jett new file mode 100644 index 00000000..c7bd4031 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/13_first_handle_failure.jett @@ -0,0 +1,16 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function successful_marker() returns result[int64, string]: + return ok(7) +export function main(net: Network) returns nothing: + use resource_probe + accept_staged(token: resource_probe.create(view net, 506) handle error: + return nothing + , marker: successful_marker() handle error: + return nothing + , net: view net) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/14_first_handle_success.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/14_first_handle_success.jett new file mode 100644 index 00000000..fb4e94f8 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/14_first_handle_success.jett @@ -0,0 +1,16 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function successful_marker() returns result[int64, string]: + return ok(7) +export function main(net: Network) returns nothing: + use resource_probe + accept_staged(token: resource_probe.create(view net, 507) handle error: + return nothing + , marker: successful_marker() handle error: + return nothing + , net: view net) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/sum_views.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/sum_views.rs new file mode 100644 index 00000000..5f5117b2 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/sum_views.rs @@ -0,0 +1,101 @@ +//! Nonconsuming conditional Resource projections from the fresh current-CFG plan. +use super::*; + +impl Translator<'_, '_> { + pub(super) fn resource_borrowed_sum_statement( + &mut self, + statement: &Statement, + operations: &[&jett_mir::ResourceOperation], + ) -> Result { + match &statement.kind { + StatementKind::SumTag { target, .. } => { + let mut matching = operations.iter().copied().filter(|operation| + matches!(operation.role(), Role::ObserveSumView { target: exact, .. } if exact == target)); + let Some(operation) = matching.next() else { + return Ok(false); + }; + if matching.next().is_some() { + return Err(pending("sum view tag has ambiguous current authority")); + } + self.resource_sum_view_tag(operation, *target, statement.span)?; + Ok(true) + } + StatementKind::SumTake { + target, success, .. + } => { + let mut matching = operations.iter().copied().filter(|operation| { + if *success { matches!(operation.role(), Role::ProjectSumView { .. }) } + else { matches!(operation.role(), Role::ReadFailureCompanion { target: exact, .. } if exact.id == *target) } + }); + let Some(operation) = matching.next() else { + return Ok(false); + }; + if matching.next().is_some() { + return Err(pending( + "sum view projection has ambiguous current authority", + )); + } + self.resource_sum_view_projection(operation, *target, statement.span)?; + Ok(true) + } + _ => Ok(false), + } + } + fn resource_sum_view_tag( + &mut self, + operation: &jett_mir::ResourceOperation, + target: jett_mir::LocalId, + span: Span, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("sum view tag has no exact family"))?; + let Role::ObserveSumView { source, .. } = operation.role() else { + return Err(pending("sum view tag selected another role")); + }; + let value = self.resource_loan(*source)?; + let (context, frame, ordinal) = self.resource_operation(operation)?; + let tag = Leaf::SumViewTag.output( + self.module, + self.builder, + &[context, frame, ordinal, value], + resource.failure, + 4, + )?; + let tag = self.builder.ins().ireduce(ir::types::I8, tag); + self.define_local(target, LoweredValue::Scalar(tag), span) + } + fn resource_sum_view_projection( + &mut self, + operation: &jett_mir::ResourceOperation, + target: jett_mir::LocalId, + span: Span, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("sum view extraction has no exact family"))?; + let (source, leaf) = match operation.role() { + Role::ProjectSumView { source, .. } => (*source, Leaf::SumViewProject), + Role::ReadFailureCompanion { source, .. } => (*source, Leaf::SumFailureRead), + _ => return Err(pending("sum view extraction selected another role")), + }; + let value = self.resource_loan(source)?; + let (context, frame, ordinal) = self.resource_operation(operation)?; + let bits = leaf.output( + self.module, + self.builder, + &[context, frame, ordinal, value], + resource.failure, + 8, + )?; + let lowered = if let Role::ProjectSumView { destination, .. } = operation.role() { + self.builder + .ins() + .stack_store(bits, resource.loans[destination.index()], 0); + LoweredValue::Scalar(bits) + } else { + self.own(bits)? + }; + self.define_local(target, lowered, span) + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests.rs new file mode 100644 index 00000000..614af581 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests.rs @@ -0,0 +1,627 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; +use jett_parser::{ast::Item, parse}; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; +use jett_types::ResourceKernelRecipe; +use std::{collections::HashMap, sync::Arc}; +mod lexical_borrows; +mod named_indirect; +mod occupied_structs; +mod ordinary_entry; +mod program_entry; +mod returned_hooks; +mod sum_views; +const SUPPORT: &str = + include_str!("../../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); +fn checked(source: &str, release: bool) -> Arc { + let stdlib = FileId::new(10_000); + let project = FileId::new(0); + let mut parsed = parse(SUPPORT, stdlib); + let primary = parse(source, project); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(primary.errors.is_empty(), "{:?}", primary.errors); + let declaration = parsed + .module + .items + .iter() + .find_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .unwrap(); + parsed.module.items.extend(primary.module.items); + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: declaration, + member: member.into(), + recipe, + }) + .collect::>(); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (stdlib, SourceOrigin::Stdlib), + (project, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("{error:?}; {:?}", error.diagnostics())), + ) +} + +const SOURCE: &str = include_str!("../resource_layout/connected_entry.jett"); +fn lower_source(checked: &Arc) -> Program { + let hir = jett_hir::lower_checked_resource_program(checked).unwrap(); + jett_mir::lower(&hir, &checked.checked().interner).unwrap() +} +fn entry(program: &Program) -> FunctionId { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "main" + }) + .unwrap() + .id +} + +fn emitted(source: &str, release: bool) -> ObjectArtifact { + let checked = checked(source, release); + let program = lower_source(&checked); + super::super::emit_for_triple( + &program, + &checked.checked().interner, + HOST, + Some(entry(&program)), + CodegenOptions { optimize: release }, + ) + .unwrap() +} +fn symbols(artifact: &ObjectArtifact) -> (Vec, Vec) { + use object::{Object, ObjectSymbol}; + let file = object::File::parse(artifact.bytes.as_slice()).unwrap(); + let mut definitions = Vec::new(); + let mut imports = Vec::new(); + for symbol in file.symbols() { + if let Ok(name) = symbol.name() { + if symbol.is_definition() { + definitions.push(name.into()); + } else if symbol.is_undefined() { + imports.push(name.into()); + } + } + } + (definitions, imports) +} +#[test] +fn resource_native_body_emits_original_connected_source_lifecycle_and_selected_leaves() { + for release in [false, true] { + let artifact = emitted(SOURCE, release); + let (definitions, imports) = symbols(&artifact); + assert!( + definitions + .iter() + .any(|name| name == JETT_AOT_ENTRY_SYMBOL_V1) + ); + assert!( + definitions + .iter() + .any(|name| name == "jett_aot_resource_v1_manifest") + ); + for required in [ + "entry_scope", + "entry_network", + "entry_outcome", + "scope_validate", + "source_enter", + "source_parameter", + "source_status", + "factory_commit", + "borrow_commit", + "close", + "sum_take", + "failure_companion_take", + "scope_complete", + "return_publish", + ] { + assert!( + imports + .iter() + .any(|name| name == &format!("jett_rt_v1_resource_{required}")), + "{required}: {imports:?}" + ); + } + } +} +#[test] +fn resource_native_body_preserves_domain_abi_plus_exact_hidden_scope_without_public_scalar_admission() + { + for release in [false, true] { + let checked = checked(SOURCE, release); + let program = lower_source(&checked); + let types = &checked.checked().interner; + let layout = EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + let helper = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "pass_owner" + }) + .unwrap(); + let isa = isa::lookup(HOST) + .unwrap() + .finish(settings::Flags::new(settings::builder())) + .unwrap(); + let module = ObjectModule::new( + ObjectBuilder::new(isa, "resource_signature", default_libcall_names()).unwrap(), + ); + let selected = signature(&module, helper, types, &layout).unwrap(); + assert_eq!(selected.params.len(), 4); + assert!( + selected + .params + .iter() + .all(|parameter| parameter.value_type == ir::types::I64) + ); + assert_eq!(selected.returns.len(), 1); + assert_eq!(selected.returns[0].value_type, ir::types::I64); + assert!(scalar_kind(types, helper.return_type, "public Resource scalar gate").is_err()); + assert!(MoveValuePlan::analyze(&program, helper, types).is_err()); + } +} +#[test] +fn resource_native_body_stages_owned_actual_before_a_later_fallible_source_actual() { + let source = r#"namespace app +function consume(token: resource_probe.TestHandle, label: int64) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1) handle error: + return nothing + consume(token, resource_probe.terminal_label()) + return nothing +"#; + for release in [false, true] { + let artifact = emitted(source, release); + let (_, imports) = symbols(&artifact); + for required in [ + "transfer", + "operation_begin", + "operation_complete", + "scope_complete", + "source_actual", + "source_parameter", + ] { + assert!( + imports + .iter() + .any(|name| name == &format!("jett_rt_v1_resource_{required}")) + ); + } + } +} +#[test] +fn resource_native_body_authenticates_original_source_before_emission_and_keeps_absent_failure_shells_typed() + { + let source = r#"namespace app +function main(net: Network) returns nothing: + use resource_probe + optional[resource_probe.TestHandle] absent = resource_probe.empty() + result[resource_probe.TestHandle, string] rejected = fail("label") + return nothing +"#; + for release in [false, true] { + let artifact = emitted(source, release); + let (_, imports) = symbols(&artifact); + for required in [ + "absent_sum", + "failure_sum", + "scope_complete", + "return_publish", + ] { + assert!( + imports + .iter() + .any(|name| name == &format!("jett_rt_v1_resource_{required}")) + ); + } + let checked = checked(SOURCE, release); + let original = lower_source(&checked); + let types = &checked.checked().interner; + let selected = entry(&original); + for mutation in 0..3 { + let mut changed = original.clone(); + match mutation { + 0 => { + changed.functions[selected.index() as usize].params[0].mode = + jett_mir::ParamMode::View + } + 1 => changed.functions[selected.index() as usize].blocks[0] + .statements + .clear(), + 2 => changed.resource_manifest = jett_hir::ResourceManifest::empty(), + _ => unreachable!(), + } + assert!( + super::super::emit_for_triple( + &changed, + types, + HOST, + Some(selected), + CodegenOptions::default() + ) + .is_err() + ); + } + } +} + +#[test] +fn resource_native_body_refuses_ordinary_descriptor_abi_for_an_exact_hidden_scope_family_target() { + let source = r#"namespace app +function lifecycle(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1) handle error: + return nothing + resource_probe.close(token) + return nothing +function main(net: Network) returns nothing: + lifecycle(view net) + function(view Network) returns nothing callback = lifecycle + callback(view net) + return nothing +"#; + for release in [false, true] { + let checked = checked(source, release); + let program = lower_source(&checked); + let error = super::super::emit_for_triple( + &program, + &checked.checked().interner, + HOST, + Some(entry(&program)), + CodegenOptions { optimize: release }, + ) + .unwrap_err(); + assert!( + matches!(error, CodegenError::UnsupportedMir { construct, .. } if construct == "pending Resource family descriptor hidden Scope ABI") + ); + } +} + +#[test] +fn resource_native_replacement_original_source_emits_replace_reseat_and_vacant_routes() { + const SOURCE: &str = include_str!("replacement/mutable_assignment.jett"); + for release in [false, true] { + for (selected, replaces) in [ + ("replace_live", true), + ("rebind_self", false), + ("rebind_after_close", false), + ("failed_rhs_keeps_owner", true), + ] { + // Native verification deliberately emits every fresh plan function. + // Isolate this exact actual Source function so an unused live-replace + // helper cannot supply a misleading Replace import for a reseat. + let mut declarations = SOURCE.split("export function "); + let prefix = declarations.next().unwrap(); + let body = declarations + .find(|body| body.starts_with(&format!("{selected}("))) + .unwrap(); + let source = format!( + "{prefix}export function {body}export function main(net: Network) returns nothing: + {selected}(view net) + return nothing +" + ); + let artifact = emitted(&source, release); + let (_, imports) = symbols(&artifact); + assert_eq!( + imports + .iter() + .any(|name| name == "jett_rt_v1_resource_replace"), + replaces, + "{selected}: {imports:?}" + ); + for leaf in ["source_enter", "scope_complete", "transfer", "close"] { + assert!( + imports + .iter() + .any(|name| name == &format!("jett_rt_v1_resource_{leaf}")), + "{selected}/{leaf}: {imports:?}" + ); + } + } + } +} + +#[test] +fn resource_native_replacement_ordinary_rhs_failure_keeps_original_owner_until_cleanup() { + const SOURCE: &str = r#"namespace app +function replacement_label() returns int64: + use resource_probe + return resource_probe.terminal_label() +export function main(net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2441) handle error: + return nothing + token = resource_probe.create(view net, replacement_label()) handle error: + return nothing + resource_probe.close(token) + return nothing +"#; + for release in [false, true] { + let checked = checked(SOURCE, release); + let program = lower_source(&checked); + let layout = EmittedResourceLayout::from_program( + &program, + &checked.checked().interner, + entry(&program), + ) + .unwrap(); + let main = layout.plan().function(entry(&program)).unwrap(); + assert!( + main.operations() + .iter() + .any(|operation| matches!(operation.role(), Role::Replace { .. })) + ); + let artifact = emitted(SOURCE, release); + let (_, imports) = symbols(&artifact); + for leaf in ["replace", "operation_complete", "scope_complete"] { + assert!( + imports + .iter() + .any(|name| name == &format!("jett_rt_v1_resource_{leaf}")) + ); + } + assert!(imports.iter().any(|name| name.contains("failure"))); + } +} + +#[test] +fn resource_staged_real_source_emits_cross_block_prepare_stage_invoke_and_retirement_leaves() { + for release in [false, true] { + for source in [ + include_str!("staged_call/09_later_handle_minimal.jett"), + include_str!("staged_call/10_later_handle_original.jett"), + include_str!("staged_call/11_later_handle_success.jett"), + include_str!("staged_call/12_later_handle_written_view.jett"), + include_str!("staged_call/13_first_handle_failure.jett"), + include_str!("staged_call/14_first_handle_success.jett"), + ] { + let checked = checked(source, release); + let original = lower_source(&checked); + let selected = entry(&original); + assert!( + original + .functions + .iter() + .any(|function| function.blocks.iter().any(|block| block + .statements + .iter() + .any(|statement| matches!( + statement.kind, + StatementKind::ResourceCall(jett_mir::ResourceCallNode::End { + outcome: jett_mir::ResourceCompletion::Abort, + .. + }) + )))) + ); + for function in &original.functions { + for block in &function.blocks { + for statement in &block.statements { + if let StatementKind::ResourceCall(jett_mir::ResourceCallNode::Stage { + value, + .. + }) = &statement.kind + { + assert!(!matches!(value.kind, ExpressionKind::RuntimeFailure(_))); + } + } + } + } + let artifact = super::super::emit_for_triple( + &original, + &checked.checked().interner, + HOST, + Some(selected), + CodegenOptions { optimize: release }, + ) + .unwrap(); + let (_, imports) = symbols(&artifact); + for leaf in [ + "source_prepare", + "source_actual", + "source_enter", + "source_parameter", + "source_status", + "borrow_begin", + "borrow_end", + "operation_begin", + "operation_complete", + "scope_complete", + "entry_outcome", + ] { + assert!( + imports + .iter() + .any(|name| name == &format!("jett_rt_v1_resource_{leaf}")), + "{leaf}: {imports:?}" + ); + } + // This inspects an emitted object only. Provider events and all-before-destroy + // outcomes belong to Root's linked Source-deleted conformance gate. + } + } +} + +#[test] +fn resource_staged_object_refuses_altered_ordinary_endpoint_and_forged_current_node() { + let source = include_str!("staged_call/09_later_handle_minimal.jett"); + for release in [false, true] { + let checked = checked(source, release); + let original = lower_source(&checked); + let selected = entry(&original); + let (function, block, index, local) = original + .functions + .iter() + .find_map(|function| { + function.blocks.iter().find_map(|block| { + block + .statements + .iter() + .enumerate() + .find_map(|(index, statement)| match &statement.kind { + StatementKind::ResourceCall(jett_mir::ResourceCallNode::Stage { + ordinary: Some(local), + .. + }) => Some((function.id, block.id, index, *local)), + _ => None, + }) + }) + }) + .unwrap(); + for mutation in 0..3 { + let mut changed = original.clone(); + let current = &mut changed.functions[function.index() as usize]; + match mutation { + 0 => { + assert!(current.locals[local.index() as usize].mutable); + current.locals[local.index() as usize].mutable = false; + } + 1 => { + let StatementKind::ResourceCall(jett_mir::ResourceCallNode::Stage { + ordinary, + .. + }) = &mut current.blocks[block.index() as usize].statements[index].kind + else { + unreachable!() + }; + *ordinary = Some(current.params[0].local); + } + 2 => { + let copied = current.blocks[block.index() as usize].statements[index].clone(); + current.blocks[block.index() as usize] + .statements + .push(copied); + } + _ => unreachable!(), + } + assert!( + super::super::emit_for_triple( + &changed, + &checked.checked().interner, + HOST, + Some(selected), + CodegenOptions { optimize: release } + ) + .is_err(), + "mutation {mutation}" + ); + } + } +} + +#[test] +fn resource_staged_scope_sum_keeps_active_operation_rows_and_forgery_refusal() { + for release in [false, true] { + for source in [ + include_str!("staged_call/13_first_handle_failure.jett"), + include_str!("staged_call/14_first_handle_success.jett"), + ] { + let checked = checked(source, release); + let original = lower_source(&checked); + let selected = entry(&original); + let layout = EmittedResourceLayout::from_program( + &original, + &checked.checked().interner, + selected, + ) + .unwrap(); + let flow = layout.plan().function(selected).unwrap(); + let active = flow + .operations() + .iter() + .find(|operation| matches!(operation.role(), Role::BeginSourceFunction { .. })) + .unwrap() + .frame(); + let mut extractions = 0; + for operation in flow.operations() { + if matches!( + operation.role(), + Role::SumTake { .. } | Role::TakeFailureCompanion { .. } + ) { + extractions += 1; + assert_eq!(operation.frame(), active); + assert!(layout.operation(selected, operation.id()).is_some()); + } + } + assert_eq!(extractions, 2); + let main = &original.functions[selected.index() as usize]; + let (block, index) = main + .blocks + .iter() + .find_map(|block| { + block + .statements + .iter() + .enumerate() + .find_map(|(index, statement)| { + matches!(statement.kind, StatementKind::SumTag { .. }) + .then_some((block.id, index)) + }) + }) + .unwrap(); + assert_eq!( + flow.execution_frame(block, jett_mir::ResourcePosition::Statement(index)), + Some(active) + ); + super::super::emit_for_triple( + &original, + &checked.checked().interner, + HOST, + Some(selected), + CodegenOptions { optimize: release }, + ) + .unwrap(); + for copied in [false, true] { + let mut changed = original.clone(); + let current = &mut changed.functions[selected.index() as usize]; + if copied { + let statement = + current.blocks[block.index() as usize].statements[index].clone(); + current.blocks[block.index() as usize] + .statements + .insert(index, statement); + } else { + let StatementKind::SumTag { source, .. } = + &mut current.blocks[block.index() as usize].statements[index].kind + else { + unreachable!() + }; + *source = current.params[0].local; + } + assert!(matches!( + super::super::emit_for_triple( + &changed, + &checked.checked().interner, + HOST, + Some(selected), + CodegenOptions { optimize: release } + ), + Err(CodegenError::InvalidMir(_)) + )); + } + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/carrier_wrapper_sources.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/carrier_wrapper_sources.rs new file mode 100644 index 00000000..ff663db3 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/carrier_wrapper_sources.rs @@ -0,0 +1,226 @@ +// Exact additive suffixes from the frozen Source17 wrapper packet. +// The complete original fixture is included independently and never pruned. +const WRAPPERS: [(&str, &str); 14] = [ + ( + "01_empty_tokens", + r###" +export function main(net: Network) returns nothing: + list[resource_probe.TestHandle] values = empty_tokens() + if list.length[resource_probe.TestHandle](view values) != 0: + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "02_absent_tokens", + r###" +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false + +export function main(net: Network) returns nothing: + list[optional[resource_probe.TestHandle]] values = absent_tokens() + if list.length[optional[resource_probe.TestHandle]](view values) != 2: + int64 ignored = resource_probe.terminal_label() + mutable int64 absent_count = 0 + for candidate in values: + if source17_absent_optional(view candidate): + absent_count = absent_count + 1 + if absent_count != 2: + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "03_empty_map", + r###" +export function main(net: Network) returns nothing: + map[string, resource_probe.TestHandle] values = empty_map() + if map.length[string, resource_probe.TestHandle](view values) != 0: + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "04_absent_map", + r###" +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false + +export function main(net: Network) returns nothing: + map[string, optional[resource_probe.TestHandle]] values = absent_map() + if map.length[string, optional[resource_probe.TestHandle]](view values) != 1: + int64 ignored = resource_probe.terminal_label() + mutable int64 absent_count = 0 + for key, candidate in values: + if key != "empty": + int64 ignored = resource_probe.terminal_label() + if source17_absent_optional(view candidate): + absent_count = absent_count + 1 + if absent_count != 1: + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "05_empty_result_tokens", + r###" +export function main(net: Network) returns nothing: + list[resource_probe.TestHandle] values = empty_result_tokens() handle error: + int64 ignored = resource_probe.terminal_label() + return nothing + if list.length[resource_probe.TestHandle](view values) != 0: + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "06_failed_result_tokens", + r###" +export function main(net: Network) returns nothing: + list[resource_probe.TestHandle] values = failed_result_tokens() handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "07_absent_envelope", + r###" +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false + +export function main(net: Network) returns nothing: + TokenEnvelope value = absent_envelope() + if value.marker != 17: + int64 ignored = resource_probe.terminal_label() + if list.length[resource_probe.TestHandle](view value.tokens) != 0: + int64 ignored = resource_probe.terminal_label() + if !source17_absent_optional(view value.fallback): + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "08_absent_box", + r###" +export function main(net: Network) returns nothing: + TokenBox[resource_probe.TestHandle] value = absent_box() + if list.length[resource_probe.TestHandle](view value.items) != 0: + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "09_empty_choice", + r###" +export function main(net: Network) returns nothing: + TokenChoice value = empty_choice() + match value: + empty: + return nothing + other: + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "10_absent_choice", + r###" +export function main(net: Network) returns nothing: + TokenChoice value = absent_choice() + match value: + vacant(marker, tokens): + if marker != 17: + int64 ignored = resource_probe.terminal_label() + if list.length[resource_probe.TestHandle](view tokens) != 0: + int64 ignored = resource_probe.terminal_label() + return nothing + other: + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "11_absent_state", + r###" +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false + +export function main(net: Network) returns nothing: + TokenState value = absent_state() + if value at vacant: + if value.marker != "empty": + int64 ignored = resource_probe.terminal_label() + if !source17_absent_optional(view value.fallback): + int64 ignored = resource_probe.terminal_label() + return nothing + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "12_absent_exact_state", + r###" +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false + +export function main(net: Network) returns nothing: + TokenState at vacant value = absent_exact_state() + if value at vacant: + if value.marker != "empty": + int64 ignored = resource_probe.terminal_label() + if !source17_absent_optional(view value.fallback): + int64 ignored = resource_probe.terminal_label() + return nothing + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "13_wrong_empty_length_control", + r###" +export function main(net: Network) returns nothing: + list[resource_probe.TestHandle] values = empty_tokens() + if list.length[resource_probe.TestHandle](view values) != 1: + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "14_required_primitive_controls", + r###" +export function main(net: Network) returns nothing: + int64 answer = absent_required_controls() + if answer != 34: + int64 ignored = resource_probe.terminal_label() + if absent_namespace_value != 17: + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), +]; +const LIST_LENGTH: &str = r###"namespace list +export function length[T](view items: list[T]) returns int64: + return list.__length[T](view items) +"###; +const MAP_LENGTH: &str = r###"namespace map +export function length[K, V](view items: map[K, V]) returns int64: + return map.__length[K, V](view items) +"###; diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/lexical_borrows.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/lexical_borrows.rs new file mode 100644 index 00000000..cb91dfdb --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/lexical_borrows.rs @@ -0,0 +1,193 @@ +//! Public object admission must not infer lexical custody from editable MIR. +use super::*; + +const SOURCES: [(&str, &str); 3] = [ + ( + "Source58 if", + include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/58_if_optional_written_some.jett" + ), + ), + ( + "Source74 while", + include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/74_while_optional_written_some.jett" + ), + ), + ( + "Source89 sibling scopes", + include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/89_scope_sibling_result_bindings.jett" + ), + ), +]; + +#[derive(Debug, Clone, Copy)] +enum Mutation { + CopiedExit, + RetargetedExit, + MissingWitness, +} + +fn public_object( + program: &Program, + checked: &Arc, + release: bool, +) -> Result { + crate::emit_host_program_object_with_options( + program, + &checked.checked().interner, + entry(program), + CodegenOptions { optimize: release }, + ) +} + +/// Construct an ordinary function through public lowering, then copy only the +/// editable MIR fields. The private Resource witness and other private records +/// stay absent; no test-only authority constructor or representation cast is used. +fn without_resource_witness( + original: &jett_mir::Function, + checked: &Arc, +) -> jett_mir::Function { + let hir = jett_hir::lower_checked_resource_program(checked).unwrap(); + let mut seed = hir.functions[original.id.index() as usize].clone(); + seed.id = FunctionId::new(0); + seed.params.clear(); + seed.capture_count = 0; + seed.return_type = TypeInterner::NOTHING; + seed.locals.clear(); + seed.body.statements.clear(); + let ordinary = jett_hir::Program { + resource_manifest: jett_hir::ResourceManifest::empty(), + resource_source: jett_hir::ResourceSourceArchive::empty(), + functions: vec![seed], + equality_methods: HashMap::new(), + }; + let mut lowered = jett_mir::lower(&ordinary, &checked.checked().interner).unwrap(); + let mut unwitnessed = lowered.functions.remove(0); + unwitnessed.id = original.id; + unwitnessed.identity = original.identity.clone(); + unwitnessed.debug_kind = original.debug_kind.clone(); + unwitnessed.params = original.params.clone(); + unwitnessed.capture_count = original.capture_count; + unwitnessed.return_type = original.return_type; + unwitnessed.locals = original.locals.clone(); + unwitnessed.entry = original.entry; + unwitnessed.blocks = original.blocks.clone(); + unwitnessed.span = original.span; + unwitnessed +} + +#[test] +fn resource_lexical_borrows_public_object_refuses_copied_retargeted_and_unwitnessed_exits() { + for release in [false, true] { + for (label, source) in SOURCES { + let checked = checked(source, release); + let baseline = lower_source(&checked); + // Establish that this exact checked Source reaches public object + // emission before attributing any refusal to the mutation. + let artifact = public_object(&baseline, &checked, release) + .unwrap_or_else(|error| panic!("{label} release={release} baseline: {error:?}")); + let (definitions, imports) = symbols(&artifact); + assert!( + definitions + .iter() + .any(|name| name == JETT_AOT_ENTRY_SYMBOL_V1) + ); + assert!( + imports + .iter() + .any(|name| name == "jett_rt_v1_resource_borrow_end") + ); + + let (function_id, block_id, index) = baseline + .functions + .iter() + .find_map(|function| { + if function.identity.declaration.namespace != "app" { + return None; + } + function.blocks.iter().find_map(|block| { + block + .statements + .iter() + .enumerate() + .find_map(|(index, statement)| { + matches!(statement.kind, StatementKind::ResourceLexicalExit(_)) + .then_some((function.id, block.id, index)) + }) + }) + }) + .expect("genuine fixture has an app lexical-exit node"); + let position = jett_mir::ResourcePosition::Statement(index); + let helper = &baseline.functions[function_id.index() as usize]; + assert!( + helper + .resource_lexical_exit(block_id, position) + .unwrap() + .is_some() + ); + assert_ne!( + (block_id, index), + (helper.entry, 0), + "retarget must change the site" + ); + + for mutation in [ + Mutation::CopiedExit, + Mutation::RetargetedExit, + Mutation::MissingWitness, + ] { + let mut forged = baseline.clone(); + let helper = &mut forged.functions[function_id.index() as usize]; + match mutation { + Mutation::CopiedExit => { + let copied = + helper.blocks[block_id.index() as usize].statements[index].clone(); + helper.blocks[block_id.index() as usize] + .statements + .insert(index, copied); + } + Mutation::RetargetedExit => { + let moved = helper.blocks[block_id.index() as usize] + .statements + .remove(index); + helper.blocks[helper.entry.index() as usize] + .statements + .insert(0, moved); + } + Mutation::MissingWitness => { + *helper = without_resource_witness(helper, &checked); + assert!( + helper + .resource_lexical_exit(block_id, position) + .unwrap() + .is_none() + ); + } + } + jett_mir::validate(&forged).unwrap_or_else(|errors| + panic!("{label} release={release} {mutation:?} must retain structural MIR: {errors:?}")); + match public_object(&forged, &checked, release) { + Err(CodegenError::InvalidMir(errors)) => { + assert!(!errors.is_empty(), "{label} release={release} {mutation:?}"); + if matches!(mutation, Mutation::MissingWitness) { + assert!( + errors.iter().any(|error| error.message.contains( + "no initially authenticated Source constructor witness" + )), + "{label} release={release} {mutation:?}: {errors:?}" + ); + } + } + Err(error) => panic!( + "{label} release={release} {mutation:?}: wrong public refusal {error:?}" + ), + Ok(_) => panic!( + "{label} release={release} {mutation:?}: forged MIR produced an object" + ), + } + } + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/named_indirect.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/named_indirect.rs new file mode 100644 index 00000000..203c3cea --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/named_indirect.rs @@ -0,0 +1,421 @@ +use super::*; + +const NAMED: &str = include_str!("../named_indirect/18_indirect_named_close.jett"); +const WRONG: &str = include_str!("../named_indirect/19_indirect_named_terminal_failure.jett"); +const ALIAS: &str = include_str!("../named_indirect/20_indirect_named_alias.jett"); +const STDLIB: &str = r#"namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.close + resource_probe.TestHandle token = resource_probe.create(view net, 701) handle error: + return nothing + dispose(token) + return nothing +"#; + +fn named_clif(source: &str, release: bool) -> (ir::Function, HashMap, String) { + let checked = checked(source, release); + let types = &checked.checked().interner; + let mut program = lower_source(&checked); + jett_mir::prepare_native_sequences(&mut program, types); + jett_mir::prepare_native_uninhabited_sums(&mut program, types); + jett_mir::prepare_native_generated_functions(&mut program, types); + let selected = entry(&program); + let layout = EmittedResourceLayout::from_program(&program, types, selected).unwrap(); + let verified = crate::verify::verify_resource_program(layout.plan()).unwrap(); + let isa = isa::lookup(HOST) + .unwrap() + .finish(settings::Flags::new(settings::builder())) + .unwrap(); + let mut module = ObjectModule::new( + ObjectBuilder::new(isa, "named_source_clif", default_libcall_names()).unwrap(), + ); + let mut declarations = + DeclaredFunctions::new(program.functions.len(), verified.functions().len()); + for item in verified.functions() { + let function = program_function(&program, item.mir_id).unwrap(); + let signature = signature(&module, function, types, &layout).unwrap(); + let native_id = module + .declare_function(&item.symbol, Linkage::Local, &signature) + .unwrap(); + declarations + .insert(DeclaredFunction { + mir_id: function.id, + native_id, + symbol: item.symbol.clone(), + signature, + modes: function + .params + .iter() + .map(|parameter| parameter.mode) + .collect(), + parameter_types: function + .params + .iter() + .map(|parameter| parameter.ty) + .collect(), + debug_label: debug::function_label(function).unwrap(), + }) + .unwrap(); + } + let plan = layout.plan().function(selected).unwrap(); + let target = plan + .operations() + .iter() + .find_map(|operation| operation.named_indirect_target()) + .unwrap(); + let target_symbol = declarations.get(target.function()).unwrap().symbol.clone(); + let function = program_function(&program, selected).unwrap(); + let declaration = declarations.get(selected).unwrap(); + let mut context = module.make_context(); + context.func.signature = declaration.signature.clone(); + translate_function_inner( + &mut module, + &declarations, + &program, + function, + types, + &declaration.symbol, + &mut context, + Some(&layout), + ) + .unwrap(); + let names = context + .func + .dfg + .ext_funcs + .iter() + .map(|(reference, data)| { + let ir::ExternalName::User(user) = data.name else { + panic!("unexpected external name"); + }; + let name = &context.func.params.user_named_funcs()[user]; + ( + reference, + module + .declarations() + .get_function_decl(FuncId::from_u32(name.index)) + .name + .clone() + .expect("translated Source and runtime imports have declared symbols"), + ) + }) + .collect(); + (context.func, names, target_symbol) +} + +fn defining_instruction(function: &ir::Function, value: Value) -> ir::Inst { + let ir::ValueDef::Result(instruction, _) = + function.dfg.value_def(function.dfg.resolve_aliases(value)) + else { + panic!("expected an instruction result"); + }; + instruction +} + +fn call_name<'a>( + function: &ir::Function, + names: &'a HashMap, + instruction: ir::Inst, +) -> Option<&'a str> { + let ir::InstructionData::Call { func_ref, .. } = function.dfg.insts[instruction] else { + return None; + }; + names.get(&func_ref).map(String::as_str) +} + +fn constant(function: &ir::Function, value: Value) -> Option { + let instruction = defining_instruction(function, value); + match function.dfg.insts[instruction] { + ir::InstructionData::UnaryImm { + opcode: ir::Opcode::Iconst, + imm, + .. + } => Some(imm.bits()), + _ => None, + } +} + +#[test] +fn resource_named_indirect_original_source_emits_selected_bridge_and_retains_named_targets() { + for release in [false, true] { + for source in [NAMED, WRONG, ALIAS, STDLIB] { + let artifact = emitted(source, release); + let (_, imports) = symbols(&artifact); + for required in [ + "jett_rt_v1_function_callable_check", + "jett_rt_v1_struct_field", + "jett_rt_v1_resource_source_enter", + "jett_rt_v1_resource_source_status", + ] { + assert!( + imports.iter().any(|name| name == required), + "{required}: {imports:?}" + ); + } + let checked = checked(source, release); + let program = lower_source(&checked); + let plan = jett_mir::validate_resource_ownership(&program, &checked.checked().interner) + .unwrap(); + let selected_body = plan + .function(entry(&program)) + .unwrap() + .operations() + .iter() + .find_map(|operation| operation.named_indirect_target()) + .unwrap(); + let target = selected_body.function(); + assert!( + scalar_kind( + &checked.checked().interner, + selected_body.signature_type(), + "public Resource-bearing callable signature" + ) + .is_err() + ); + let reachable = crate::reachability::reachable_function_ids_with_types( + &program, + &checked.checked().interner, + ) + .unwrap(); + assert!(reachable.contains(&target)); + if source == STDLIB { + assert_eq!( + program.functions[target.index() as usize] + .identity + .declaration + .origin, + SourceOrigin::Stdlib + ); + } + } + } +} + +#[test] +fn resource_named_indirect_checks_original_actuals_then_code_and_environment_before_source_entry() { + use cranelift_codegen::{dominator_tree::DominatorTree, flowgraph::ControlFlowGraph}; + for release in [false, true] { + for source in [NAMED, WRONG, ALIAS, STDLIB] { + let (function, names, target) = named_clif(source, release); + let instructions = function + .layout + .blocks() + .flat_map(|block| function.layout.block_insts(block)) + .collect::>(); + assert!( + !instructions + .iter() + .any(|instruction| function.dfg.insts[*instruction].opcode() + == ir::Opcode::CallIndirect) + ); + let guard = instructions + .iter() + .copied() + .find(|instruction| { + let ir::InstructionData::Brif { arg, .. } = function.dfg.insts[*instruction] + else { + return false; + }; + function.dfg.insts[defining_instruction(&function, arg)].opcode() + == ir::Opcode::Band + }) + .unwrap(); + let ir::InstructionData::Brif { arg, blocks, .. } = function.dfg.insts[guard] else { + unreachable!(); + }; + let checks = function.dfg.inst_args(defining_instruction(&function, arg)); + let ir::InstructionData::IntCompare { + cond, args: code, .. + } = function.dfg.insts[defining_instruction(&function, checks[0])] + else { + panic!("missing code comparison"); + }; + assert_eq!(cond, IntCC::Equal); + let ir::InstructionData::FuncAddr { func_ref, .. } = + function.dfg.insts[defining_instruction(&function, code[1])] + else { + panic!("missing selected native address"); + }; + assert_eq!(names[&func_ref], target); + assert_eq!( + call_name(&function, &names, defining_instruction(&function, code[0])), + Some("jett_rt_v1_struct_field") + ); + let code_field = function + .dfg + .inst_args(defining_instruction(&function, code[0])); + assert_eq!( + constant(&function, code_field[2]), + Some(NATIVE_FUNCTION_CODE_FIELD as i64) + ); + let ir::InstructionData::IntCompareImm { + cond, + arg: environment, + imm, + .. + } = function.dfg.insts[defining_instruction(&function, checks[1])] + else { + panic!("missing environment comparison"); + }; + assert_eq!(cond, IntCC::Equal); + assert_eq!(imm.bits(), 0); + assert_eq!( + call_name( + &function, + &names, + defining_instruction(&function, environment) + ), + Some("jett_rt_v1_struct_field") + ); + let environment_field = function + .dfg + .inst_args(defining_instruction(&function, environment)); + assert_eq!( + constant(&function, environment_field[2]), + Some(NATIVE_FUNCTION_ENVIRONMENT_FIELD as i64) + ); + assert_eq!( + code_field[1], environment_field[1], + "code and environment must be read from the same evaluated descriptor" + ); + let refused = blocks[1] + .args(&function.dfg.value_lists) + .collect::>(); + let ir::BlockArg::Value(status) = refused[0] else { + panic!("missing refusal status"); + }; + assert_eq!(constant(&function, status), Some(1)); + let source_entry = instructions + .iter() + .copied() + .rfind(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_source_enter") + }) + .unwrap(); + let actual = instructions + .iter() + .copied() + .rfind(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_source_actual") + }) + .unwrap(); + let callable = instructions + .iter() + .copied() + .find(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_function_callable_check") + }) + .unwrap(); + let mut cfg = ControlFlowGraph::new(); + cfg.compute(&function); + let mut dominators = DominatorTree::new(); + dominators.compute(&function, &cfg); + assert!(dominators.dominates(actual, callable, &function.layout)); + assert!(dominators.dominates(callable, guard, &function.layout)); + assert!(dominators.dominates( + blocks[0].block(&function.dfg.value_lists), + source_entry, + &function.layout + )); + assert!(!dominators.dominates( + blocks[1].block(&function.dfg.value_lists), + source_entry, + &function.layout + )); + let mut suffix = vec![blocks[1].block(&function.dfg.value_lists)]; + let mut refused_blocks = std::collections::HashSet::new(); + while let Some(block) = suffix.pop() { + if refused_blocks.insert(block) { + assert_ne!(Some(block), function.layout.inst_block(source_entry)); + suffix.extend(cfg.succ_iter(block)); + } + } + assert!(refused_blocks.iter().any(|block| { + function.layout.block_insts(*block).any(|instruction| { + call_name(&function, &names, instruction) + == Some("jett_rt_v1_resource_scope_complete") + }) + })); + let selected_call = instructions + .iter() + .copied() + .find(|instruction| { + call_name(&function, &names, *instruction) == Some(target.as_str()) + }) + .unwrap(); + assert!(dominators.dominates(source_entry, selected_call, &function.layout)); + let actuals = function.dfg.inst_args(selected_call); + assert_eq!( + actuals.len(), + 4, + "context, empty environment, hidden Scope and owned actual" + ); + assert_eq!(constant(&function, actuals[1]), Some(0)); + let body_status = instructions + .iter() + .copied() + .rfind(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_source_status") + }) + .unwrap(); + assert!(dominators.dominates(selected_call, body_status, &function.layout)); + } + } +} + +#[test] +fn resource_named_indirect_refuses_same_signature_producer_swap_and_escaped_family_descriptor() { + for release in [false, true] { + let checked = checked(NAMED, release); + let original = lower_source(&checked); + let selected = entry(&original); + let wrong = original + .functions + .iter() + .find(|function| function.identity.declaration.name == "wrong_target") + .unwrap() + .id; + let mut changed = original.clone(); + let producer = changed.functions[selected.index() as usize] + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| { + let StatementKind::Let { value, .. } = &mut statement.kind else { + return None; + }; + matches!(value.kind, ExpressionKind::FunctionRef(_)).then_some(value) + }) + .unwrap(); + producer.kind = ExpressionKind::FunctionRef(wrong); + assert!(matches!( + super::super::super::emit_for_triple( + &changed, + &checked.checked().interner, + HOST, + Some(selected), + CodegenOptions { optimize: release } + ), + Err(CodegenError::InvalidMir(_)) + )); + let escaped = NAMED.replace("export function main", "function retain(view callback: function(resource_probe.TestHandle) returns nothing) returns nothing:\n return nothing\nexport function main") + .replace(" dispose(token)", " retain(view dispose)\n dispose(token)"); + let escaped_checked = super::checked(&escaped, release); + let escaped_program = lower_source(&escaped_checked); + assert!( + super::super::super::emit_for_triple( + &escaped_program, + &escaped_checked.checked().interner, + HOST, + Some(entry(&escaped_program)), + CodegenOptions { optimize: release } + ) + .is_err() + ); + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/occupied_structs.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/occupied_structs.rs new file mode 100644 index 00000000..f3e5c558 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/occupied_structs.rs @@ -0,0 +1,145 @@ +use super::*; +use jett_mir::{ResourceCarrierOperationRole as CarrierRole, ResourceCarrierValue, ResourceShape}; + +const SOURCES: [&str; 3] = [ + include_str!( + "../../../../../jett_comptime/src/resource_execution/fixtures/33_owned_struct_scope_drop_entry.jett" + ), + include_str!( + "../../../../../jett_comptime/src/resource_execution/fixtures/34_owned_struct_reverse_entry.jett" + ), + include_str!( + "../../../../../jett_comptime/src/resource_execution/fixtures/35_owned_struct_abandoned_prefix_entry.jett" + ), +]; + +#[test] +fn resource_occupied_struct_actual_sources_emit_carrier_and_plain_owner_adoption_in_both_profiles() +{ + for release in [false, true] { + for source in SOURCES { + let checked = checked(source, release); + let types = &checked.checked().interner; + let mut program = lower_source(&checked); + jett_mir::prepare_native_sequences(&mut program, types); + jett_mir::prepare_native_uninhabited_sums(&mut program, types); + jett_mir::prepare_native_generated_functions(&mut program, types); + jett_mir::validate_call_ownership(&program, types).unwrap(); + let selected = + jett_mir::validate_resource_ownership_for_entry(&program, types, entry(&program)) + .unwrap(); + let helper = program + .functions + .iter() + .find(|f| { + f.identity.declaration.namespace == "app" + && matches!( + f.identity.declaration.name.as_str(), + "retire_owned" | "retire_pair" + ) + }) + .unwrap(); + let plan = selected.function(helper.id).unwrap(); + let carrier = plan.carriers().unwrap(); + assert!(carrier.operations().iter().any(|op| matches!(op.role(),CarrierRole::ConstructorChild {child,..} if matches!(child.value,ResourceCarrierValue::LeafOwned {slot} if matches!(plan.owner_slots()[slot.index()].shape(),ResourceShape::Plain {..}))))); + let artifact = super::super::super::emit_for_triple( + &program, + types, + HOST, + Some(entry(&program)), + CodegenOptions { optimize: release }, + ) + .unwrap(); + let (definitions, imports) = symbols(&artifact); + assert!(definitions.iter().any(|n| n == JETT_AOT_ENTRY_SYMBOL_V1)); + for leaf in [ + "carrier_construct_begin", + "carrier_child", + "carrier_commit", + "carrier_transfer", + "operation_begin", + "operation_complete", + "scope_complete", + "source_status", + ] { + assert!( + imports + .iter() + .any(|n| n == &format!("jett_rt_v1_resource_{leaf}")), + "missing {leaf}" + ); + } + } + } +} + +#[test] +fn resource_occupied_struct_public_copied_field_or_header_cannot_reach_ordinary_emission() { + for release in [false, true] { + let checked = checked(SOURCES[1], release); + let types = &checked.checked().interner; + let original = lower_source(&checked); + for mutation in 0..3 { + let mut program = original.clone(); + let function = program + .functions + .iter_mut() + .find(|f| { + f.identity.declaration.namespace == "app" + && f.identity.declaration.name == "retire_pair" + }) + .unwrap(); + let (b, i) = function + .blocks + .iter() + .enumerate() + .find_map(|(b, block)| { + block.statements.iter().enumerate().find_map(|(i, s)| { + matches!( + s.kind, + StatementKind::ResourceConstructor( + jett_mir::ResourceConstructorNode::Stage { + source_index: 0, + .. + } + ) + ) + .then_some((b, i)) + }) + }) + .unwrap(); + match mutation { + 0 => { + let StatementKind::ResourceConstructor( + jett_mir::ResourceConstructorNode::Stage { field, .. }, + ) = &mut function.blocks[b].statements[i].kind + else { + unreachable!() + }; + *field = 0; + } + 1 => { + let copy = function.blocks[b].statements[i].clone(); + function.blocks[b].statements.insert(i + 1, copy); + } + 2 => { + function.locals[function.params[0].local.index() as usize].view_source = + Some(function.params[1].local); + } + _ => unreachable!(), + } + let selected = entry(&program); + let result = super::super::super::emit_for_triple( + &program, + types, + HOST, + Some(selected), + CodegenOptions { optimize: release }, + ); + assert!( + matches!(result, Err(CodegenError::InvalidMir(_))), + "forged constructor must refuse at fresh ownership validation" + ); + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/ordinary_entry.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/ordinary_entry.rs new file mode 100644 index 00000000..be82d62e --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/ordinary_entry.rs @@ -0,0 +1,554 @@ +use super::*; +use jett_comptime::checked_types::CheckedExpressionTypes; +use object::{Object, ObjectSection, ObjectSymbol, RelocationTarget, SymbolKind}; + +include!("carrier_wrapper_sources.rs"); + +const WHOLE_SOURCE: &str = include_str!( + "../../../../../jett_mir/src/resource_ownership/fixtures/17_absent_aggregate_shapes.jett" +); +const MINIMAL_SUPPORT: &str = "namespace resource_probe\nexport resource TestHandle\n"; +const PURE: &str = r#"namespace app +function helper() returns int64: + return 7 +export function main(net: Network) returns nothing: + int64 answer = helper() + if answer != 7: + return nothing + return nothing +"#; +const CALLEE: &str = r#"namespace app +function helper() returns int64: + return 7 +export function main() returns nothing: + list[int64] values = list(1, 2) + mutable int64 total = 0 + for number in values: + total = total + number + if total != 3: + int64 ignored = helper() + return nothing +function reenter() returns nothing: + main() + return nothing +"#; + +fn named<'a>(program: &'a Program, name: &str) -> &'a Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap_or_else(|| panic!("checked app.{name}")) +} + +fn checked_support(source: &str, support: &str, release: bool) -> Arc { + let stdlib = FileId::new(10_000); + let project = FileId::new(0); + let mut parsed = parse(support, stdlib); + let primary = parse(source, project); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(primary.errors.is_empty(), "{:?}", primary.errors); + let declaration = parsed + .module + .items + .iter() + .find_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .unwrap(); + parsed.module.items.extend(primary.module.items); + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: declaration, + member: member.into(), + recipe, + }) + .collect::>(); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (stdlib, SourceOrigin::Stdlib), + (project, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("{error:?}; {:?}", error.diagnostics())), + ) +} + +fn materialized(checked: &Arc) -> Program { + let expression_types = Arc::new(CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let required = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + expression_types, + Arc::new(HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "{:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + required.values.checked_required_values(checked).unwrap(); + assert!(!required.values.is_empty()); + let types = &checked.checked().interner; + let mut hir = jett_hir::lower_checked_resource_program(checked).unwrap(); + jett_hir::materialize_checked_required_values(&mut hir, &required.values, types).unwrap(); + jett_hir::complete_value_conversions(&mut hir, types).unwrap(); + jett_mir::lower(&hir, types).unwrap() +} + +fn prepare(program: &mut Program, types: &TypeInterner) { + jett_mir::prepare_native_sequences(program, types); + jett_mir::prepare_native_uninhabited_sums(program, types); + jett_mir::prepare_native_generated_functions(program, types); +} + +fn ordinary_clif( + program: &Program, + types: &TypeInterner, + selected: FunctionId, + name: &str, +) -> (ir::Function, HashMap) { + let layout = EmittedResourceLayout::from_program(program, types, selected).unwrap(); + let verified = crate::verify::verify_resource_program(layout.plan()).unwrap(); + let isa = isa::lookup(HOST) + .unwrap() + .finish(settings::Flags::new(settings::builder())) + .unwrap(); + let mut module = ObjectModule::new( + ObjectBuilder::new(isa, "ordinary_entry_clif", default_libcall_names()).unwrap(), + ); + let mut declarations = + DeclaredFunctions::new(program.functions.len(), verified.functions().len()); + for item in verified.functions() { + let function = program_function(program, item.mir_id).unwrap(); + let signature = signature(&module, function, types, &layout).unwrap(); + let native_id = module + .declare_function(&item.symbol, Linkage::Local, &signature) + .unwrap(); + declarations + .insert(DeclaredFunction { + mir_id: function.id, + native_id, + symbol: item.symbol.clone(), + signature, + modes: function + .params + .iter() + .map(|parameter| parameter.mode) + .collect(), + parameter_types: function + .params + .iter() + .map(|parameter| parameter.ty) + .collect(), + debug_label: debug::function_label(function).unwrap(), + }) + .unwrap(); + } + let function = named(program, name); + assert!(layout.plan().function(function.id).is_none()); + let declaration = declarations.get(function.id).unwrap(); + let mut context = module.make_context(); + context.func.signature = declaration.signature.clone(); + let selected_layout = layout + .plan() + .function(function.id) + .is_some() + .then_some(&layout); + assert!( + selected_layout.is_none(), + "ordinary body cannot select ResourceEmission" + ); + translate_function_inner( + &mut module, + &declarations, + program, + function, + types, + &declaration.symbol, + &mut context, + selected_layout, + ) + .unwrap(); + context.verify(module.isa()).unwrap(); + let names = context + .func + .dfg + .ext_funcs + .iter() + .map(|(reference, data)| { + let ir::ExternalName::User(user) = data.name else { + panic!("declared external function") + }; + let name = &context.func.params.user_named_funcs()[user]; + let symbol = module + .declarations() + .get_function_decl(FuncId::from_u32(name.index)) + .name + .clone() + .unwrap(); + (reference, symbol) + }) + .collect(); + (context.func, names) +} + +fn calls_to( + function: &ir::Function, + names: &HashMap, + symbol: &str, +) -> Vec { + function + .layout + .blocks() + .flat_map(|block| function.layout.block_insts(block)) + .filter(|instruction| { + matches!(function.dfg.insts[*instruction], ir::InstructionData::Call { func_ref, .. } + if names.get(&func_ref).is_some_and(|name| name == symbol)) + }) + .collect() +} + +fn assert_ordinary_function( + program: &Program, + types: &TypeInterner, + selected: FunctionId, + name: &str, +) -> (ir::Function, HashMap) { + let function = named(program, name); + let (clif, names) = ordinary_clif(program, types, selected, name); + assert_eq!(clif.signature.params.len(), function.params.len() + 2); + assert!( + clif.signature + .params + .iter() + .all(|parameter| parameter.value_type == ir::types::I64) + ); + assert!( + !names + .values() + .any(|symbol| symbol.starts_with("jett_rt_v1_resource_")), + "ordinary body imports Resource leaves: {names:?}" + ); + let storage = MoveValuePlan::analyze(program, function, types).unwrap(); + if !function + .locals + .iter() + .any(|local| jett_mir::move_values::is_linear(types, local.ty)) + { + let copy = jett_mir::copy_values::CopyValuePlan::analyze(function, types).unwrap(); + assert_eq!(copy.owned_locals, storage.owned_locals); + } + for local in function.locals.iter().filter(|local| { + matches!(types.resolve(local.ty), Type::List(_)) + && local.view_source.is_none() + && !function.is_view_local(local.id) + }) { + assert!( + storage.owned_locals.contains(&(local.id.index() as usize)), + "ordinary consuming-list storage must own its actual holder" + ); + } + jett_mir::validate_call_ownership(program, types).unwrap(); + (clif, names) +} + +fn import_callers(artifact: &ObjectArtifact, import: &str) -> Vec { + let file = object::File::parse(artifact.bytes.as_slice()).unwrap(); + let target = file + .symbols() + .find(|symbol| symbol.is_undefined() && symbol.name().ok() == Some(import)) + .expect("exact imported leaf") + .index(); + let mut callers = Vec::new(); + for section in file.sections() { + for (offset, relocation) in section.relocations() { + if relocation.target() != RelocationTarget::Symbol(target) { + continue; + } + let address = section.address().checked_add(offset).unwrap(); + let owner = file + .symbols() + .filter(|symbol| { + symbol.is_definition() + && symbol.kind() == SymbolKind::Text + && symbol.section_index() == Some(section.index()) + && symbol.address() <= address + && (symbol.size() == 0 + || address < symbol.address().checked_add(symbol.size()).unwrap()) + }) + .max_by_key(|symbol| symbol.address()) + .expect("relocation belongs to an exact defined text symbol"); + callers.push(owner.name().unwrap().to_owned()); + } + } + callers +} + +fn assert_wrapper_only_bridge(artifact: &ObjectArtifact) { + let (definitions, imports) = symbols(artifact); + assert!( + definitions + .iter() + .any(|symbol| symbol == JETT_AOT_ENTRY_SYMBOL_V1) + ); + assert!( + definitions + .iter() + .any(|symbol| symbol == "jett_aot_resource_v1_manifest") + ); + for leaf in [ + "entry_scope", + "scope_validate", + "scope_complete", + "entry_outcome", + ] { + let leaf = format!("jett_rt_v1_resource_{leaf}"); + assert_eq!( + imports + .iter() + .filter(|symbol| symbol.as_str() == leaf) + .count(), + 1 + ); + assert_eq!( + import_callers(artifact, &leaf), + [JETT_AOT_ENTRY_SYMBOL_V1.to_owned()] + ); + } + assert!( + imports + .iter() + .any(|symbol| symbol == NativeLeaf::Status.symbol()) + ); + for leaf in [ + "source_enter", + "source_parameter", + "source_status", + "source_actual", + "source_result", + "return_publish", + ] { + assert!( + !imports + .iter() + .any(|symbol| symbol == &format!("jett_rt_v1_resource_{leaf}")) + ); + } +} + +#[test] +fn ordinary_entry_genuine_pure_main_emits_only_wrapper_scope_with_two_hidden_body_parameters() { + for release in [false, true] { + let checked = checked_support(PURE, MINIMAL_SUPPORT, release); + let types = &checked.checked().interner; + let hir = jett_hir::lower_checked_resource_program(&checked).unwrap(); + assert!(hir.resource_source.required_materializations().is_empty()); + assert_eq!(hir.resource_manifest.hooks().count(), 3); + let baseline = jett_mir::lower(&hir, types).unwrap(); + let selected = entry(&baseline); + let mut prepared = baseline.clone(); + prepare(&mut prepared, types); + let layout = EmittedResourceLayout::from_program(&prepared, types, selected).unwrap(); + let bridge = layout.plan().entry_scope().unwrap(); + assert_eq!(bridge.function(), selected); + assert!(layout.entry_bridge_completion().is_some()); + assert!(layout.function_signature(selected).is_none()); + assert!(layout.frame(selected, bridge.root_scope().id()).is_none()); + assert!( + layout + .operation(selected, bridge.completion().id()) + .is_none() + ); + let (main, names) = assert_ordinary_function(&prepared, types, selected, "main"); + let helper_symbol = + crate::symbol_name(&named(&prepared, "helper").identity, types).unwrap(); + let calls = calls_to(&main, &names, &helper_symbol); + assert_eq!(calls.len(), 1); + assert_eq!(main.dfg.inst_args(calls[0]).len(), 2); + assert_ordinary_function(&prepared, types, selected, "helper"); + let artifact = super::super::super::emit_for_triple( + &baseline, + types, + HOST, + Some(selected), + CodegenOptions { optimize: release }, + ) + .unwrap(); + assert_wrapper_only_bridge(&artifact); + assert_eq!( + import_callers(&artifact, "jett_rt_v1_resource_entry_network"), + [JETT_AOT_ENTRY_SYMBOL_V1.to_owned()] + ); + } +} + +#[test] +fn ordinary_entry_main_as_callee_and_consuming_for_keep_the_ordinary_abi() { + for release in [false, true] { + let checked = checked_support(CALLEE, MINIMAL_SUPPORT, release); + let types = &checked.checked().interner; + let baseline = lower_source(&checked); + let selected = entry(&baseline); + assert!(named(&baseline, "main").blocks.iter().any(|block| matches!( + block.terminator.kind, + jett_mir::TerminatorKind::ForEach { .. } + ))); + let mut prepared = baseline.clone(); + prepare(&mut prepared, types); + assert!( + !named(&prepared, "main").blocks.iter().any(|block| matches!( + block.terminator.kind, + jett_mir::TerminatorKind::ForEach { .. } + )) + ); + assert_ordinary_function(&prepared, types, selected, "main"); + assert!( + named(&prepared, "main") + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| { + matches!( + statement.kind, + jett_mir::StatementKind::SequenceGet { consume: true, .. } + ) + }), + "the genuine For must retain its consuming element extraction" + ); + let (caller, names) = assert_ordinary_function(&prepared, types, selected, "reenter"); + let main_symbol = crate::symbol_name(&named(&prepared, "main").identity, types).unwrap(); + let calls = calls_to(&caller, &names, &main_symbol); + assert_eq!(calls.len(), 1); + assert_eq!(caller.dfg.inst_args(calls[0]).len(), 2); + let artifact = super::super::super::emit_for_triple( + &baseline, + types, + HOST, + Some(selected), + CodegenOptions { optimize: release }, + ) + .unwrap(); + assert_wrapper_only_bridge(&artifact); + let (_, imports) = symbols(&artifact); + assert!( + !imports + .iter() + .any(|symbol| symbol == "jett_rt_v1_resource_entry_network") + ); + } +} + +#[test] +fn ordinary_entry_complete_source17_wrapper14_emits_main_and_required_helper_with_ordinary_signatures() + { + let (name, suffix) = WRAPPERS[13]; + assert_eq!(name, "14_required_primitive_controls"); + let source = format!("{WHOLE_SOURCE}{suffix}"); + for release in [false, true] { + let checked = checked_support(&source, SUPPORT, release); + let types = &checked.checked().interner; + let baseline = materialized(&checked); + let selected = entry(&baseline); + let mut prepared = baseline.clone(); + prepare(&mut prepared, types); + let (main, names) = assert_ordinary_function(&prepared, types, selected, "main"); + let helper_symbol = crate::symbol_name( + &named(&prepared, "absent_required_controls").identity, + types, + ) + .unwrap(); + let calls = calls_to(&main, &names, &helper_symbol); + assert_eq!(calls.len(), 1); + assert_eq!(main.dfg.inst_args(calls[0]).len(), 2); + assert_ordinary_function(&prepared, types, selected, "absent_required_controls"); + let artifact = super::super::super::emit_for_triple( + &baseline, + types, + HOST, + Some(selected), + CodegenOptions { optimize: release }, + ) + .unwrap(); + let (definitions, imports) = symbols(&artifact); + assert!( + definitions + .iter() + .any(|symbol| symbol == JETT_AOT_ENTRY_SYMBOL_V1) + ); + assert_eq!( + imports + .iter() + .filter(|symbol| symbol.as_str() == "jett_rt_v1_resource_scope_complete") + .count(), + 1 + ); + let callers = import_callers(&artifact, "jett_rt_v1_resource_scope_complete"); + assert_eq!( + callers + .iter() + .filter(|symbol| symbol.as_str() == JETT_AOT_ENTRY_SYMBOL_V1) + .count(), + 1 + ); + for name in ["main", "absent_required_controls"] { + let symbol = crate::symbol_name(&named(&prepared, name).identity, types).unwrap(); + assert!(!callers.contains(&symbol)); + } + } +} + +#[test] +fn ordinary_entry_current_header_body_and_manifest_mutations_refuse_real_object_emission() { + for release in [false, true] { + let checked = checked_support(PURE, MINIMAL_SUPPORT, release); + let types = &checked.checked().interner; + let baseline = lower_source(&checked); + let selected = entry(&baseline); + for mutation in 0..4 { + let mut changed = baseline.clone(); + let index = usize::try_from(selected.index()).unwrap(); + match mutation { + 0 => changed.functions[index].params[0].mode = jett_mir::ParamMode::View, + 1 => changed.functions[index].blocks[0].statements.clear(), + 2 => changed.functions[index].capture_count = 1, + 3 => changed.resource_manifest = jett_hir::ResourceManifest::empty(), + _ => unreachable!(), + } + assert!( + super::super::super::emit_for_triple( + &changed, + types, + HOST, + Some(selected), + CodegenOptions { optimize: release }, + ) + .is_err(), + "mutation={mutation}; release={release}" + ); + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/program_entry.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/program_entry.rs new file mode 100644 index 00000000..44881b1b --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/program_entry.rs @@ -0,0 +1,354 @@ +//! Actual generated-entry IR/object controls; no linked native execution. +use super::*; +use object::{Object, ObjectSection, ObjectSymbol, RelocationTarget, SymbolKind}; +use std::collections::HashSet; + +const BEGIN: &str = "jett_rt_v1_resource_program_begin"; +const COMPLETE: &str = "jett_rt_v1_resource_program_complete"; +const MANIFEST: &str = "jett_aot_resource_v1_manifest"; + +fn actual_entry(release: bool) -> (ir::Function, HashMap) { + let checked = checked(SOURCE, release); + let program = lower_source(&checked); + let types = &checked.checked().interner; + let layout = EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + let verified = crate::verify::verify_resource_program(layout.plan()).unwrap(); + let target = isa::lookup(HOST) + .unwrap() + .finish(settings::Flags::new(settings::builder())) + .unwrap(); + let mut module = ObjectModule::new( + ObjectBuilder::new(target, "actual_resource_entry", default_libcall_names()).unwrap(), + ); + let mut declarations = + DeclaredFunctions::new(program.functions.len(), verified.functions().len()); + for item in verified.functions() { + let function = program_function(&program, item.mir_id).unwrap(); + let signature = signature(&module, function, types, &layout).unwrap(); + let native_id = module + .declare_function(&item.symbol, Linkage::Local, &signature) + .unwrap(); + declarations + .insert(DeclaredFunction { + mir_id: function.id, + native_id, + symbol: item.symbol.clone(), + signature, + modes: function + .params + .iter() + .map(|parameter| parameter.mode) + .collect(), + parameter_types: function + .params + .iter() + .map(|parameter| parameter.ty) + .collect(), + debug_label: debug::function_label(function).unwrap(), + }) + .unwrap(); + } + let (_, context) = build_entry(&mut module, &declarations, &layout).unwrap(); + context.verify(module.isa()).unwrap(); + let names = context + .func + .dfg + .ext_funcs + .iter() + .map(|(reference, data)| { + let ir::ExternalName::User(user) = data.name else { + panic!("declared external function") + }; + let name = &context.func.params.user_named_funcs()[user]; + let symbol = module + .declarations() + .get_function_decl(FuncId::from_u32(name.index)) + .name + .clone() + .unwrap(); + (reference, symbol) + }) + .collect(); + (context.func, names) +} + +fn call(function: &ir::Function, names: &HashMap, symbol: &str) -> ir::Inst { + let calls = function.layout.blocks().flat_map(|block| function.layout.block_insts(block)) + .filter(|instruction| matches!(function.dfg.insts[*instruction], + ir::InstructionData::Call { func_ref, .. } if names.get(&func_ref).is_some_and(|name| name == symbol))) + .collect::>(); + assert_eq!(calls.len(), 1, "one actual call to {symbol}"); + calls[0] +} + +fn destinations(function: &ir::Function, block: ir::Block) -> Vec { + let terminal = function.layout.last_inst(block).unwrap(); + function.dfg.insts[terminal] + .branch_destination(&function.dfg.jump_tables, &function.dfg.exception_tables) + .into_iter() + .map(|branch| branch.block(&function.dfg.value_lists)) + .collect() +} + +fn reachable_returns(function: &ir::Function, start: ir::Block) -> HashSet { + let mut pending = vec![start]; + let mut visited = HashSet::new(); + let mut returns = HashSet::new(); + while let Some(block) = pending.pop() { + if !visited.insert(block) { + continue; + } + let terminal = function.layout.last_inst(block).unwrap(); + if function.dfg.insts[terminal].opcode() == ir::Opcode::Return { + returns.insert(block); + } else { + pending.extend(destinations(function, block)); + } + } + returns +} + +fn returned(function: &ir::Function, block: ir::Block) -> &[Value] { + function + .dfg + .inst_args(function.layout.last_inst(block).unwrap()) +} + +#[test] +fn generated_entry_uses_own_manifest_exact_context_and_separate_startup_outputs() { + for release in [false, true] { + let (function, names) = actual_entry(release); + assert_eq!(function.signature.params, [AbiParam::new(ir::types::I64)]); + assert_eq!(function.signature.returns, [AbiParam::new(ir::types::I32)]); + let incoming = function + .dfg + .block_params(function.layout.entry_block().unwrap())[0]; + let manifest = call(&function, &names, MANIFEST); + let begin = call(&function, &names, BEGIN); + let complete = call(&function, &names, COMPLETE); + let scope = call(&function, &names, "jett_rt_v1_resource_entry_scope"); + let manifest_args = function.dfg.inst_args(manifest); + let begin_args = function.dfg.inst_args(begin); + assert_eq!(manifest_args, [begin_args[1]]); + assert_eq!(begin_args.len(), 4); + assert_eq!(begin_args[0], incoming); + assert_eq!(function.dfg.inst_args(scope)[0], incoming); + assert_eq!(function.dfg.inst_args(complete)[0], incoming); + assert_eq!( + begin_args[1..] + .iter() + .copied() + .collect::>() + .len(), + 3 + ); + for (instruction, expected) in [ + (manifest, vec![ir::types::I64]), + (begin, vec![ir::types::I64; 4]), + ( + complete, + vec![ir::types::I64, ir::types::I64, ir::types::I32], + ), + ] { + let ir::InstructionData::Call { func_ref, .. } = function.dfg.insts[instruction] else { + unreachable!() + }; + let signature = &function.dfg.signatures[function.dfg.ext_funcs[func_ref].signature]; + assert_eq!( + signature + .params + .iter() + .map(|parameter| parameter.value_type) + .collect::>(), + expected + ); + assert_eq!(signature.returns, [AbiParam::new(ir::types::I32)]); + } + let manifest_block = function.layout.inst_block(manifest).unwrap(); + let begin_block = function.layout.inst_block(begin).unwrap(); + let scope_block = function.layout.inst_block(scope).unwrap(); + assert_eq!(destinations(&function, manifest_block)[0], begin_block); + assert_eq!(destinations(&function, begin_block)[0], scope_block); + assert_eq!( + destinations(&function, manifest_block)[1], + destinations(&function, begin_block)[1] + ); + } +} + +#[test] +fn every_post_begin_return_uses_the_single_finish_gate_and_exact_terminal_status() { + for release in [false, true] { + let (function, names) = actual_entry(release); + let begin = call(&function, &names, BEGIN); + let complete = call(&function, &names, COMPLETE); + let begin_block = function.layout.inst_block(begin).unwrap(); + let generated = function.layout.inst_block(complete).unwrap(); + assert_eq!( + returned(&function, generated), + function.dfg.inst_results(complete) + ); + let gates = function + .layout + .blocks() + .filter(|block| destinations(&function, *block).contains(&generated)) + .collect::>(); + assert_eq!(gates.len(), 1); + let finish = gates[0]; + let successors = destinations(&function, finish); + assert_eq!(successors.len(), 2); + assert_eq!(successors[1], generated); + let borrowed = successors[0]; + assert_eq!( + returned(&function, borrowed), + &[function.dfg.block_params(finish)[0]] + ); + assert_eq!( + function.dfg.inst_args(complete)[2], + function.dfg.block_params(finish)[0] + ); + let terminal = function.layout.last_inst(finish).unwrap(); + let condition = function.dfg.inst_args(terminal)[0]; + let ir::ValueDef::Result(compare, _) = function.dfg.value_def(condition) else { + panic!("borrowed sentinel test") + }; + let ir::InstructionData::IntCompareImm { cond, arg, imm, .. } = function.dfg.insts[compare] + else { + panic!("exact zero sentinel comparison") + }; + assert_eq!(cond, IntCC::Equal); + assert_eq!(i64::from(imm), 0); + assert_eq!(arg, function.dfg.inst_args(complete)[1]); + let begin_success = destinations(&function, begin_block)[0]; + assert_eq!( + reachable_returns(&function, begin_success), + HashSet::from([borrowed, generated]) + ); + let startup_refusal = destinations(&function, begin_block)[1]; + assert_eq!( + reachable_returns(&function, startup_refusal), + HashSet::from([startup_refusal]) + ); + assert_eq!( + function + .layout + .blocks() + .filter( + |block| function.dfg.insts[function.layout.last_inst(*block).unwrap()].opcode() + == ir::Opcode::Return + ) + .count(), + 3 + ); + } +} + +fn relocation_callers(artifact: &ObjectArtifact, target_name: &str) -> Vec { + let file = object::File::parse(artifact.bytes.as_slice()).unwrap(); + let target = file + .symbols() + .find(|symbol| symbol.name().ok() == Some(target_name)) + .unwrap() + .index(); + let mut callers = Vec::new(); + for section in file.sections() { + for (offset, relocation) in section.relocations() { + if relocation.target() != RelocationTarget::Symbol(target) { + continue; + } + let address = section.address().checked_add(offset).unwrap(); + let owner = file + .symbols() + .filter(|symbol| { + symbol.is_definition() + && symbol.kind() == SymbolKind::Text + && symbol.section_index() == Some(section.index()) + && symbol.address() <= address + && (symbol.size() == 0 + || address < symbol.address().checked_add(symbol.size()).unwrap()) + }) + .max_by_key(|symbol| symbol.address()) + .unwrap(); + callers.push(owner.name().unwrap().to_owned()); + } + } + callers +} + +#[test] +fn real_resource_object_links_startup_only_from_its_generated_c_entry() { + for release in [false, true] { + let artifact = emitted(SOURCE, release); + let (definitions, imports) = symbols(&artifact); + assert_eq!( + definitions + .iter() + .filter(|name| name.as_str() == MANIFEST) + .count(), + 1 + ); + assert!(!imports.iter().any(|name| name == MANIFEST)); + for target in [BEGIN, COMPLETE, MANIFEST] { + assert_eq!( + relocation_callers(&artifact, target), + [JETT_AOT_ENTRY_SYMBOL_V1.to_owned()] + ); + } + for observer in ["unwind", "failure", "failure_copy"] { + assert!( + !imports + .iter() + .any(|name| name == &format!("jett_rt_v1_resource_program_{observer}")) + ); + } + } +} + +#[test] +fn ordinary_objects_keep_the_same_entry_without_resource_startup_or_manifest() { + const ORDINARY: &str = + "namespace app\nexport function main() returns nothing:\n return nothing\n"; + for release in [false, true] { + let file = FileId::new(0); + let parsed = parse(ORDINARY, file); + assert!(parsed.errors.is_empty()); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error) + ); + let hir = jett_hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .unwrap(); + let program = jett_mir::lower(&hir, &checked.interner).unwrap(); + let artifact = super::super::super::emit_for_triple( + &program, + &checked.interner, + HOST, + Some(entry(&program)), + CodegenOptions { optimize: release }, + ) + .unwrap(); + let (definitions, imports) = symbols(&artifact); + assert_eq!( + definitions + .iter() + .filter(|name| name.as_str() == JETT_AOT_ENTRY_SYMBOL_V1) + .count(), + 1 + ); + assert!( + !definitions + .iter() + .chain(&imports) + .any(|name| name == MANIFEST || name.starts_with("jett_rt_v1_resource_")) + ); + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/returned_hook_order.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/returned_hook_order.rs new file mode 100644 index 00000000..76a6918b --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/returned_hook_order.rs @@ -0,0 +1,108 @@ +use super::*; + +#[test] +fn resource_returned_hook_all_recipes_and_immediate_callees_prepare_exact_descriptor_before_commit() +{ + for release in [false, true] { + for (source, effect) in [ + (CLOSE, "jett_rt_v1_resource_descriptor_close"), + (IMMEDIATE, "jett_rt_v1_resource_descriptor_close"), + (FACTORY, "jett_rt_v1_resource_factory_prepare"), + (BORROW, "jett_rt_v1_resource_borrow_commit"), + (ORDER, "jett_rt_v1_resource_factory_prepare"), + (CALLEE_FAILURE, "jett_rt_v1_resource_factory_prepare"), + (ACTUAL_FAILURE, "jett_rt_v1_resource_factory_prepare"), + ] { + let (function, names, indirect) = hook_clif(source, release, "main"); + assert_eq!(indirect.len(), 1); + let all = instructions(&function); + let prepare = all + .iter() + .copied() + .find(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_hook_prepare") + && constant(&function, function.dfg.inst_args(*instruction)[2]) + == Some(i64::from(indirect[0])) + }) + .unwrap(); + assert_ne!( + constant(&function, function.dfg.inst_args(prepare)[3]), + Some(0) + ); + let effect = all + .iter() + .copied() + .find(|instruction| call_name(&function, &names, *instruction) == Some(effect)) + .unwrap(); + let (accepted, refused) = + checked_branch(&function, function.dfg.inst_results(prepare)[0]); + let mut cfg = ControlFlowGraph::new(); + cfg.compute(&function); + let mut dom = DominatorTree::new(); + dom.compute(&function, &cfg); + assert!(dom.dominates(accepted, effect, &function.layout)); + let mut pending = vec![refused]; + let mut seen = std::collections::HashSet::new(); + while let Some(block) = pending.pop() { + if seen.insert(block) { + assert_ne!(Some(block), function.layout.inst_block(effect)); + pending.extend(cfg.succ_iter(block)); + } + } + assert!(seen.iter().any(|block| { + function.layout.block_insts(*block).any(|instruction| { + call_name(&function, &names, instruction) + == Some("jett_rt_v1_resource_scope_complete") + }) + })); + if [ORDER, CALLEE_FAILURE, ACTUAL_FAILURE].contains(&source) { + let argument = all + .iter() + .copied() + .find(|instruction| { + call_name(&function, &names, *instruction) == Some("argument_label") + }) + .unwrap(); + let callee = all + .iter() + .copied() + .find(|instruction| { + call_name(&function, &names, *instruction) == Some("select_create") + }) + .unwrap(); + assert!(dom.dominates(argument, callee, &function.layout)); + assert!(dom.dominates(callee, prepare, &function.layout)); + let statuses = all + .iter() + .copied() + .filter(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_source_status") + }) + .collect::>(); + for (called, next) in [(argument, callee), (callee, prepare)] { + let status = statuses + .iter() + .copied() + .find(|status| { + dom.dominates(called, *status, &function.layout) + && dom.dominates(*status, next, &function.layout) + }) + .unwrap(); + let (accepted, refused) = + checked_branch(&function, function.dfg.inst_results(status)[0]); + assert!(dom.dominates(accepted, next, &function.layout)); + let mut pending = vec![refused]; + let mut seen = std::collections::HashSet::new(); + while let Some(block) = pending.pop() { + if seen.insert(block) { + assert_ne!(Some(block), function.layout.inst_block(next)); + pending.extend(cfg.succ_iter(block)); + } + } + } + } + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/returned_hooks.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/returned_hooks.rs new file mode 100644 index 00000000..8d7ebedb --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/returned_hooks.rs @@ -0,0 +1,464 @@ +use super::*; +use cranelift_codegen::{dominator_tree::DominatorTree, flowgraph::ControlFlowGraph}; +#[path = "returned_hook_order.rs"] +mod ordering; +const FACTORY: &str = include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/27_returned_hook_factory.jett" +); +const BORROW: &str = include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/28_returned_hook_borrow.jett" +); +const IMMEDIATE: &str = include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/29_immediate_returned_hook_close.jett" +); +const ORDER: &str = include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/30_immediate_returned_hook_factory.jett" +); +const CALLEE_FAILURE: &str = include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/31_immediate_returned_hook_callee_failure.jett" +); +const ACTUAL_FAILURE: &str = include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/32_immediate_returned_hook_actual_failure.jett" +); + +const CLOSE: &str = include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/23_returned_hook_close.jett" +); +const ALIAS: &str = include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/24_returned_hook_alias.jett" +); +const RELAY: &str = include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/25_returned_hook_relay.jett" +); +const UNUSED: &str = include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/26_returned_hook_unused.jett" +); + +fn hook_clif( + source: &str, + release: bool, + selected_name: &str, +) -> (ir::Function, HashMap, Vec) { + let checked = checked(source, release); + let types = &checked.checked().interner; + let mut program = lower_source(&checked); + jett_mir::prepare_native_sequences(&mut program, types); + jett_mir::prepare_native_uninhabited_sums(&mut program, types); + jett_mir::prepare_native_generated_functions(&mut program, types); + let layout = EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + let verified = crate::verify::verify_resource_program(layout.plan()).unwrap(); + let isa = isa::lookup(HOST) + .unwrap() + .finish(settings::Flags::new(settings::builder())) + .unwrap(); + let mut module = ObjectModule::new( + ObjectBuilder::new(isa, "returned_hook_clif", default_libcall_names()).unwrap(), + ); + let mut declarations = + DeclaredFunctions::new(program.functions.len(), verified.functions().len()); + for item in verified.functions() { + let function = program_function(&program, item.mir_id).unwrap(); + let signature = signature(&module, function, types, &layout).unwrap(); + let native_id = module + .declare_function(&item.symbol, Linkage::Local, &signature) + .unwrap(); + declarations + .insert(DeclaredFunction { + mir_id: function.id, + native_id, + symbol: item.symbol.clone(), + signature, + modes: function + .params + .iter() + .map(|parameter| parameter.mode) + .collect(), + parameter_types: function + .params + .iter() + .map(|parameter| parameter.ty) + .collect(), + debug_label: debug::function_label(function).unwrap(), + }) + .unwrap(); + } + let selected = program + .functions + .iter() + .find(|function| function.identity.declaration.name == selected_name) + .unwrap(); + let declaration = declarations.get(selected.id).unwrap(); + let mut context = module.make_context(); + context.func.signature = declaration.signature.clone(); + translate_function_inner( + &mut module, + &declarations, + &program, + selected, + types, + &declaration.symbol, + &mut context, + Some(&layout), + ) + .unwrap(); + context.verify(module.isa()).unwrap(); + let indirect = layout + .plan() + .function(selected.id) + .unwrap() + .operations() + .iter() + .filter(|operation| operation.indirect_hook_target().is_some()) + .map(|operation| layout.operation(selected.id, operation.id()).unwrap()) + .collect(); + let names = context + .func + .dfg + .ext_funcs + .iter() + .map(|(reference, data)| { + let ir::ExternalName::User(user) = data.name else { + panic!("unexpected external name"); + }; + let name = &context.func.params.user_named_funcs()[user]; + let symbol = module + .declarations() + .get_function_decl(FuncId::from_u32(name.index)) + .name + .clone() + .expect("Source/runtime import has its declared symbol"); + let name = declarations + .iter() + .find(|declaration| declaration.symbol == symbol) + .map(|declaration| { + program_function(&program, declaration.mir_id) + .unwrap() + .identity + .declaration + .name + .clone() + }) + .unwrap_or(symbol); + (reference, name) + }) + .collect(); + (context.func, names, indirect) +} +fn instructions(function: &ir::Function) -> Vec { + function + .layout + .blocks() + .flat_map(|block| function.layout.block_insts(block)) + .collect() +} +fn call_name<'a>( + function: &ir::Function, + names: &'a HashMap, + instruction: ir::Inst, +) -> Option<&'a str> { + let ir::InstructionData::Call { func_ref, .. } = function.dfg.insts[instruction] else { + return None; + }; + names.get(&func_ref).map(String::as_str) +} +fn defining_instruction(function: &ir::Function, value: Value) -> ir::Inst { + let ir::ValueDef::Result(instruction, _) = + function.dfg.value_def(function.dfg.resolve_aliases(value)) + else { + panic!("expected instruction result"); + }; + instruction +} +fn constant(function: &ir::Function, value: Value) -> Option { + match function.dfg.insts[defining_instruction(function, value)] { + ir::InstructionData::UnaryImm { + opcode: ir::Opcode::Iconst, + imm, + .. + } => Some(imm.bits()), + _ => None, + } +} +fn checked_branch(function: &ir::Function, status: Value) -> (ir::Block, ir::Block) { + for instruction in instructions(function) { + let ir::InstructionData::Brif { arg, blocks, .. } = function.dfg.insts[instruction] else { + continue; + }; + if let ir::InstructionData::IntCompareImm { + cond, + arg: checked, + imm, + .. + } = function.dfg.insts[defining_instruction(function, arg)] + { + if checked == status && cond == IntCC::Equal && imm.bits() == 0 { + return ( + blocks[0].block(&function.dfg.value_lists), + blocks[1].block(&function.dfg.value_lists), + ); + } + } + } + panic!("exact status-zero output guard is absent"); +} + +#[test] +fn resource_returned_hook_unused_selector_retains_its_transitive_ordinary_helper_declaration() { + for release in [false, true] { + let checked = checked(CLOSE, release); + let types = &checked.checked().interner; + let mut program = lower_source(&checked); + jett_mir::prepare_native_sequences(&mut program, types); + jett_mir::prepare_native_uninhabited_sums(&mut program, types); + jett_mir::prepare_native_generated_functions(&mut program, types); + let selector = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "resource_probe" + && function.identity.declaration.name == "select_create" + }) + .unwrap(); + let helper = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "resource_probe" + && function.identity.declaration.name == "terminal_label" + }) + .unwrap(); + let ordinary = + crate::reachability::reachable_function_ids_with_types(&program, types).unwrap(); + assert!(!ordinary.contains(&selector.id)); + assert!(!ordinary.contains(&helper.id)); + let layout = EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + assert!(layout.plan().function(selector.id).is_some()); + assert!(layout.plan().function(helper.id).is_none()); + let verified = crate::verify::verify_resource_program(layout.plan()).unwrap(); + let helper_symbol = &verified + .get(helper.id) + .expect("an executable Resource root retains its ordinary helper declaration") + .symbol; + let (definitions, _) = symbols(&emitted(CLOSE, release)); + assert!(definitions.iter().any(|symbol| symbol == helper_symbol)); + + let (function, names, indirect) = hook_clif(CLOSE, release, "select_create"); + assert!(indirect.is_empty()); + let calls = instructions(&function) + .into_iter() + .filter(|instruction| { + call_name(&function, &names, *instruction) == Some("terminal_label") + }) + .collect::>(); + assert_eq!(calls.len(), 1); + // This pure helper has no Resource operation or hidden Scope operand. + assert_eq!(function.dfg.inst_args(calls[0]).len(), 2); + } +} + +#[test] +fn resource_returned_hook_original_sources_use_opaque_metadata_without_ordinary_callable_ownership() +{ + for release in [false, true] { + for source in [ + CLOSE, + ALIAS, + RELAY, + UNUSED, + FACTORY, + BORROW, + IMMEDIATE, + ORDER, + CALLEE_FAILURE, + ACTUAL_FAILURE, + ] { + let artifact = emitted(source, release); + let (_, imports) = symbols(&artifact); + assert!( + imports + .iter() + .any(|name| name == "jett_rt_v1_resource_descriptor") + ); + assert!( + imports + .iter() + .any(|name| name == "jett_rt_v1_resource_source_status") + ); + for forbidden in [ + "jett_rt_v1_struct_clone", + "jett_rt_v1_struct_drop", + "jett_rt_v1_function_callable_check", + "jett_rt_v1_struct_field", + ] { + assert!( + !imports.iter().any(|name| name == forbidden), + "{forbidden}: {imports:?}" + ); + } + let checked = checked(source, release); + let program = lower_source(&checked); + let plan = jett_mir::validate_resource_ownership(&program, &checked.checked().interner) + .unwrap(); + for function in plan.functions() { + if let Some(hook) = function.descriptor_return() { + assert!( + scalar_kind( + &checked.checked().interner, + hook.function_type(), + "public hook signature" + ) + .is_err() + ); + let companion = + jett_mir::ResourceCompanionPlan::analyze(&plan, function.function()) + .unwrap(); + for local in &program.functions[function.function().index() as usize].locals { + if function.descriptor_local(local.id).is_some() { + assert!( + !companion + .storage() + .owned_locals + .contains(&(local.id.index() as usize)) + ); + } + } + } + } + } + } +} + +#[test] +fn resource_returned_hook_close_checks_descriptor_after_owner_stage_and_completed_source_before_effect() + { + for release in [false, true] { + for source in [CLOSE, ALIAS, RELAY] { + let (function, names, indirect) = hook_clif(source, release, "main"); + let all = instructions(&function); + assert!( + !all.iter() + .any(|instruction| function.dfg.insts[*instruction].opcode() + == ir::Opcode::CallIndirect) + ); + let close = all + .iter() + .copied() + .find(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_descriptor_close") + }) + .unwrap(); + let prepare = all + .iter() + .copied() + .find(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_hook_prepare") + && constant(&function, function.dfg.inst_args(*instruction)[3]) != Some(0) + }) + .unwrap(); + assert_eq!(indirect.len(), 1); + assert_eq!( + constant(&function, function.dfg.inst_args(prepare)[2]), + Some(i64::from(indirect[0])) + ); + let stage = all + .iter() + .copied() + .rfind(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_transfer") + }) + .unwrap(); + let complete = all + .iter() + .copied() + .find(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_source_status") + }) + .unwrap(); + let mut cfg = ControlFlowGraph::new(); + cfg.compute(&function); + let mut dom = DominatorTree::new(); + dom.compute(&function, &cfg); + assert!(dom.dominates(stage, prepare, &function.layout)); + assert!(dom.dominates(complete, prepare, &function.layout)); + let status = function.dfg.inst_results(prepare)[0]; + let (accepted, refused) = checked_branch(&function, status); + assert!(dom.dominates(accepted, close, &function.layout)); + let mut pending = vec![refused]; + let mut seen = std::collections::HashSet::new(); + while let Some(block) = pending.pop() { + if seen.insert(block) { + assert_ne!(Some(block), function.layout.inst_block(close)); + pending.extend(cfg.succ_iter(block)); + } + } + assert!(seen.iter().any(|block| { + function.layout.block_insts(*block).any(|instruction| { + call_name(&function, &names, instruction) + == Some("jett_rt_v1_resource_scope_complete") + }) + })); + } + } +} + +#[test] +fn resource_returned_hook_scope_completion_guards_real_return_and_failure_cannot_publish_metadata() +{ + for release in [false, true] { + for (source, selected) in [(CLOSE, "descriptor"), (RELAY, "relay")] { + let (function, names, _) = hook_clif(source, release, selected); + let all = instructions(&function); + let complete = all + .iter() + .copied() + .find(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_scope_complete") + }) + .unwrap(); + let body = function.dfg.inst_args(complete)[3]; + assert_eq!(constant(&function, body), Some(0)); + let (accepted, refused) = + checked_branch(&function, function.dfg.inst_results(complete)[0]); + let mut cfg = ControlFlowGraph::new(); + cfg.compute(&function); + let mut dom = DominatorTree::new(); + dom.compute(&function, &cfg); + let returned = all + .iter() + .copied() + .find(|instruction| { + function.dfg.insts[*instruction].opcode() == ir::Opcode::Return + && dom.dominates(accepted, *instruction, &function.layout) + }) + .unwrap(); + let value = function.dfg.inst_args(returned)[0]; + assert_ne!(constant(&function, value), Some(0)); + let mut pending = vec![refused]; + let mut seen = std::collections::HashSet::new(); + while let Some(block) = pending.pop() { + if seen.insert(block) { + assert_ne!(Some(block), function.layout.inst_block(returned)); + for instruction in function.layout.block_insts(block) { + if function.dfg.insts[instruction].opcode() == ir::Opcode::Return { + assert_eq!( + constant(&function, function.dfg.inst_args(instruction)[0]), + Some(0) + ); + } + } + pending.extend(cfg.succ_iter(block)); + } + } + assert!( + !all.iter() + .any(|instruction| call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_return_publish")) + ); + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/sum_views.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/sum_views.rs new file mode 100644 index 00000000..19985ab9 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/sum_views.rs @@ -0,0 +1,41 @@ +use super::*; +#[path = "../../../../../jett_driver/tests/native_conformance/resource_cases.rs"] +mod cases; + +#[test] +fn resource_borrowed_sum_objects_emit_checked_nonconsuming_leaves_in_both_profiles() { + let cases = cases::CASES + .iter() + .filter(|case| { + case.name.starts_with("view_optional_") || case.name.starts_with("view_result_") + }) + .collect::>(); + assert_eq!(cases.len(), 12); + for release in [false, true] { + for case in &cases { + let artifact = emitted(case.source, release); + let (_, imports) = symbols(&artifact); + for leaf in [ + "jett_rt_v1_resource_sum_borrow", + "jett_rt_v1_resource_sum_view_tag", + "jett_rt_v1_resource_sum_view_project", + "jett_rt_v1_resource_sum_borrow_end", + ] { + assert!( + imports.iter().any(|name| name == leaf), + "{} release={release} missing {leaf}", + case.name + ); + } + if case.name.starts_with("view_result_") { + assert!( + imports + .iter() + .any(|name| name == "jett_rt_v1_resource_sum_failure_read"), + "{} release={release}", + case.name + ); + } + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_layout.rs b/crates/jett_codegen_cranelift/src/emit/resource_layout.rs new file mode 100644 index 00000000..e1965d57 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_layout.rs @@ -0,0 +1,1690 @@ +//! Constructor-owned projection of a fresh Source custody proof into immutable wire rows. +//! Bytes and ordinals are consistency metadata; only the runtime issuer creates custody. +use super::*; +use jett_mir::{ + self as custody, ResourceCallOperand as Operand, ResourceCallResult as CallResult, + ResourceOperationRole as Role, +}; +use std::collections::BTreeMap; +#[path = "resource_layout/carrier_operations.rs"] +pub(super) mod carrier_operations; +#[path = "resource_layout/carriers.rs"] +mod carriers; + +const LIMIT: usize = 4096; +const BYTE_LIMIT: usize = 1024 * 1024; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct SelectedEntry { + pub(super) function: u32, + pub(super) signature: u32, + pub(super) scope: u32, +} +#[derive(Debug)] +pub(super) struct EmittedResourceLayout<'p> { + plan: custody::ResourceOwnershipPlan<'p>, + entry: SelectedEntry, + entry_bridge_completion: Option, + wire: Vec, + functions: BTreeMap, + frames: BTreeMap<(u32, usize), u32>, + slots: BTreeMap<(u32, usize), u32>, + operations: BTreeMap<(u32, usize), u32>, + carrier_slots: BTreeMap<(u32, usize), u32>, + carrier_operations: BTreeMap<(u32, usize), u32>, +} +impl<'p> EmittedResourceLayout<'p> { + /// The driver-selected identity is an input; no function name or block chooses entry. + pub(super) fn from_program( + program: &'p Program, + types: &'p TypeInterner, + entry: FunctionId, + ) -> Result { + let plan = custody::validate_resource_ownership_for_entry(program, types, entry) + .map_err(CodegenError::InvalidMir)?; + let selected = program + .functions + .get(entry.index() as usize) + .filter(|function| function.id == entry) + .ok_or_else(|| pending("driver entry has no exact current function"))?; + let original = plan + .original_source(entry) + .ok_or_else(|| pending("driver entry has no original Source execution closure"))?; + let headers_match = selected.params.len() == original.params.len() + && selected + .params + .iter() + .zip(&original.params) + .all(|(current, archived)| { + current.name == archived.name + && current.ty == archived.ty + && current.mode == archived.mode + && current.mutable == archived.mutable + && current.span == archived.span + }); + // Current local IDs are independently authenticated by the constructor witness; + // the immutable original IDs stay archival through exact canonical compaction. + if selected.identity != original.identity + || !headers_match + || selected.return_type != original.return_type + || selected.capture_count != 0 + || selected.return_type != TypeInterner::NOTHING + || selected + .params + .iter() + .any(|param| param.ty != TypeInterner::NETWORK) + { + return Err(pending( + "driver entry does not have the original closed Network/Nothing header", + )); + } + let mut rows = Rows::new(&plan)?; + rows.populate()?; + let (entry, entry_bridge_completion) = if let Some(bridge) = plan.entry_scope() { + let (selected, completion) = rows.entry_bridge(bridge)?; + (selected, Some(completion)) + } else { + let signature = rows.function_signature(entry)?; + let function = plan + .function(entry) + .ok_or_else(|| pending("entry has no fresh custody function plan"))?; + let scope = rows.frame(entry, function.root_scope().id())?; + let frame = &mut rows.frames[scope as usize]; + if function.provisional_return().is_some() || !frame.parents.is_empty() { + return Err(pending( + "entry Scope cannot also be activated as a Source callee", + )); + } + frame.parents.push(Parent::Root); + ( + SelectedEntry { + function: entry.index(), + signature, + scope, + }, + None, + ) + }; + let wire = rows.encode()?; + let functions = rows.functions.clone(); + let frames = rows.frame_ids.clone(); + let slots = rows.slot_ids.clone(); + let operations = rows.operation_ids.clone(); + let carrier_slots = rows.carrier_slot_ids.clone(); + let carrier_operations = rows.carrier_operation_ids.clone(); + drop(rows); + Ok(Self { + plan, + entry, + entry_bridge_completion, + wire, + functions, + frames, + slots, + operations, + carrier_slots, + carrier_operations, + }) + } + pub(super) fn entry(&self) -> SelectedEntry { + self.entry + } + pub(super) fn entry_bridge_completion(&self) -> Option { + self.entry_bridge_completion + } + pub(super) fn bytes(&self) -> &[u8] { + &self.wire + } + pub(super) fn plan(&self) -> &custody::ResourceOwnershipPlan<'p> { + &self.plan + } + pub(super) fn function_signature(&self, function: FunctionId) -> Option { + self.functions.get(&function.index()).copied() + } + pub(super) fn frame( + &self, + function: FunctionId, + frame: custody::ResourceFrameId, + ) -> Option { + self.frames.get(&(function.index(), frame.index())).copied() + } + pub(super) fn slot( + &self, + function: FunctionId, + slot: custody::ResourceOwnerSlotId, + ) -> Option { + self.slots.get(&(function.index(), slot.index())).copied() + } + pub(super) fn operation( + &self, + function: FunctionId, + operation: custody::ResourceOperationId, + ) -> Option { + self.operations + .get(&(function.index(), operation.index())) + .copied() + } + + pub(super) fn carrier_slot( + &self, + function: FunctionId, + slot: custody::ResourceCarrierSlotId, + ) -> Option { + self.carrier_slots + .get(&(function.index(), slot.index())) + .copied() + } + pub(super) fn carrier_operation( + &self, + function: FunctionId, + operation: custody::ResourceCarrierOperationId, + ) -> Option { + self.carrier_operations + .get(&(function.index(), operation.index())) + .copied() + } + + /// Emits only an immutable accessor; this does not emit or admit a Source body. + pub(super) fn define_accessor(&self, module: &mut ObjectModule) -> Result<(), CodegenError> { + const SYMBOL: &str = "jett_aot_resource_v1_manifest"; + if module.target_config().pointer_type() != ir::types::I64 { + return Err(pending( + "Resource manifest accessor requires the selected 64-bit layout", + )); + } + if module.get_name(SYMBOL).is_some() { + return Err(CodegenError::DuplicateSymbol(SYMBOL.into())); + } + let data_id = module + .declare_data("__jett_resource_layout_v2", Linkage::Local, false, false) + .map_err(|error| { + CodegenError::Backend(format!("Resource layout data declaration: {error}")) + })?; + let mut data = cranelift_module::DataDescription::new(); + data.define(self.wire.clone().into_boxed_slice()); + module.define_data(data_id, &data).map_err(|error| { + CodegenError::Backend(format!("Resource layout data definition: {error}")) + })?; + let mut signature = module.make_signature(); + signature.params.push(AbiParam::new(ir::types::I64)); + signature.returns.push(AbiParam::new(ir::types::I32)); + let id = module + .declare_function(SYMBOL, Linkage::Export, &signature) + .map_err(|error| { + CodegenError::Backend(format!("Resource manifest accessor declaration: {error}")) + })?; + let mut context = module.make_context(); + context.func.signature = signature; + let mut builder_context = FunctionBuilderContext::new(); + let mut builder = FunctionBuilder::new(&mut context.func, &mut builder_context); + let entry = builder.create_block(); + let valid = builder.create_block(); + let refused = builder.create_block(); + builder.append_block_params_for_function_params(entry); + builder.switch_to_block(entry); + let output = builder.block_params(entry)[0]; + let global = module.declare_data_in_func(data_id, builder.func); + let address = builder.ins().global_value(ir::types::I64, global); + let not_null = builder.ins().icmp_imm(IntCC::NotEqual, output, 0); + let low = builder.ins().band_imm(output, 7); + let aligned = builder.ins().icmp_imm(IntCC::Equal, low, 0); + let output_end = builder.ins().iadd_imm(output, 32); + let no_overflow = builder + .ins() + .icmp(IntCC::UnsignedGreaterThan, output_end, output); + let data_end = builder.ins().iadd_imm( + address, + i64::try_from(self.wire.len()) + .map_err(|_| pending("manifest data length exceeds selected address width"))?, + ); + let before = builder + .ins() + .icmp(IntCC::UnsignedLessThanOrEqual, output_end, address); + let after = builder + .ins() + .icmp(IntCC::UnsignedGreaterThanOrEqual, output, data_end); + let separate = builder.ins().bor(before, after); + let valid_alignment = builder.ins().band(not_null, aligned); + let valid_range = builder.ins().band(no_overflow, separate); + let accepted = builder.ins().band(valid_alignment, valid_range); + builder.ins().brif(accepted, valid, &[], refused, &[]); + builder.switch_to_block(valid); + let flags = ir::MemFlags::new(); + builder.ins().store(flags, address, output, 0); + let length = builder.ins().iconst( + ir::types::I64, + i64::try_from(self.wire.len()) + .map_err(|_| pending("manifest data length exceeds selected address width"))?, + ); + builder.ins().store(flags, length, output, 8); + for (offset, value) in [ + (16, self.entry.function), + (20, self.entry.signature), + (24, self.entry.scope), + (28, 0), + ] { + let value = builder.ins().iconst(ir::types::I32, i64::from(value)); + builder.ins().store(flags, value, output, offset); + } + let zero = builder.ins().iconst(ir::types::I32, 0); + builder.ins().return_(&[zero]); + builder.switch_to_block(refused); + let refusal = builder.ins().iconst(ir::types::I32, 1); + builder.ins().return_(&[refusal]); + builder.seal_all_blocks(); + builder.finalize(); + module.define_function(id, &mut context).map_err(|error| { + CodegenError::Backend(format!("Resource manifest accessor definition: {error}")) + })?; + Ok(()) + } +} +pub(super) fn pending(message: &str) -> CodegenError { + CodegenError::Backend(format!("pending native Resource execution: {message}")) +} +fn ordinal(value: usize) -> Result { + if value >= LIMIT { + return Err(pending( + "layout row capacity exceeds the bounded wire domain", + )); + } + u32::try_from(value).map_err(|_| pending("layout ordinal exceeds u32")) +} +fn count(value: usize) -> Result { + if value > LIMIT { + return Err(pending("layout vector exceeds the bounded wire domain")); + } + u32::try_from(value).map_err(|_| pending("layout vector exceeds u32")) +} +#[derive(Debug, Clone, PartialEq, Eq)] +struct Signature { + result: u32, + parameters: Vec<(u32, u32)>, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum Parent { + Root, + Frame(u32), +} +#[derive(Debug)] +struct Frame { + role: u32, + site: custody::ResourceSite, + signature: u32, + parents: Vec, +} +struct Rows<'a, 'p> { + plan: &'a custody::ResourceOwnershipPlan<'p>, + hooks: Vec<(u32, u32, u32)>, + hook_ids: Vec, + signatures: Vec, + shapes: Vec>, + shape_ids: BTreeMap, + functions: BTreeMap, + frames: Vec, + frame_ids: BTreeMap<(u32, usize), u32>, + slots: Vec>, + slot_ids: BTreeMap<(u32, usize), u32>, + operations: Vec<(custody::ResourceSite, Vec)>, + operation_ids: BTreeMap<(u32, usize), u32>, + borrow_ids: BTreeMap<(u32, usize), u32>, + descriptor_borrow_targets: BTreeMap<(u32, usize), u32>, + carrier_graph: carriers::CarrierGraphRows, + carrier_slots: Vec>, + carrier_slot_ids: BTreeMap<(u32, usize), u32>, + carrier_operations: Vec>, + carrier_operation_ids: BTreeMap<(u32, usize), u32>, + carrier_record_ids: BTreeMap<(u32, usize), u32>, + carrier_borrow_ids: BTreeMap<(u32, usize), u32>, +} +impl<'a, 'p> Rows<'a, 'p> { + fn new(plan: &'a custody::ResourceOwnershipPlan<'p>) -> Result { + count(plan.program().resource_manifest.kinds().len())?; + Ok(Self { + plan, + hooks: Vec::new(), + hook_ids: Vec::new(), + signatures: Vec::new(), + shapes: Vec::new(), + shape_ids: BTreeMap::new(), + functions: BTreeMap::new(), + frames: Vec::new(), + frame_ids: BTreeMap::new(), + slots: Vec::new(), + slot_ids: BTreeMap::new(), + operations: Vec::new(), + operation_ids: BTreeMap::new(), + borrow_ids: BTreeMap::new(), + descriptor_borrow_targets: BTreeMap::new(), + carrier_graph: carriers::CarrierGraphRows::default(), + carrier_slots: Vec::new(), + carrier_slot_ids: BTreeMap::new(), + carrier_operations: Vec::new(), + carrier_operation_ids: BTreeMap::new(), + carrier_record_ids: BTreeMap::new(), + carrier_borrow_ids: BTreeMap::new(), + }) + } + fn shape(&mut self, ty: TypeId) -> Result { + if let Some(id) = self.shape_ids.get(&ty.index()) { + return Ok(*id); + } + if carrier_operations::is_carrier_type(self.plan, ty) { + let node = self.carrier_node(ty)?; + let id = ordinal(self.shapes.len())?; + self.shapes.push(vec![11, node]); + self.shape_ids.insert(ty.index(), id); + return Ok(id); + } + let words = match self.plan.types().resolve(ty) { + Type::Int8 => vec![1, 8, 1], + Type::Int16 => vec![1, 16, 1], + Type::Int32 => vec![1, 32, 1], + Type::Int64 => vec![1, 64, 1], + Type::Uint8 => vec![1, 8, 0], + Type::Uint16 => vec![1, 16, 0], + Type::Uint32 => vec![1, 32, 0], + Type::Uint64 => vec![1, 64, 0], + Type::Float32 => vec![2, 32], + Type::Float64 => vec![2, 64], + Type::Bool => vec![3], + Type::String => vec![4], + Type::Nothing => vec![5], + Type::Capability(jett_types::CapabilityKind::Network) => vec![6], + Type::Resource(_) => vec![ + 7, + self.plan + .program() + .resource_manifest + .kind_for_type(ty) + .ok_or_else(|| pending("shape has no original manifest kind"))? + .id() + .index(), + ], + Type::Optional(child) => { + let child = *child; + vec![8, self.shape(child)?] + } + Type::Result(ok, fail) => { + let (ok, fail) = (*ok, *fail); + vec![9, self.shape(ok)?, self.shape(fail)?] + } + _ => { + return Err(pending( + "exact shape needs its dedicated native qualification, aggregate or callable transport", + )); + } + }; + let id = if let Some(index) = self.shapes.iter().position(|shape| shape == &words) { + ordinal(index)? + } else { + let id = ordinal(self.shapes.len())?; + self.shapes.push(words); + id + }; + self.shape_ids.insert(ty.index(), id); + Ok(id) + } + fn custody_shape(&mut self, shape: &custody::ResourceShape) -> Result { + let plain = self.shape(shape.kind().ty())?; + let words = match shape { + custody::ResourceShape::Plain { .. } => return Ok(plain), + custody::ResourceShape::Optional { .. } => vec![8, plain], + custody::ResourceShape::Result { failure, .. } => vec![9, plain, self.shape(*failure)?], + }; + if let Some(index) = self.shapes.iter().position(|shape| shape == &words) { + return ordinal(index); + } + let id = ordinal(self.shapes.len())?; + self.shapes.push(words); + Ok(id) + } + fn signature( + &mut self, + params: &[(TypeId, jett_mir::ParamMode)], + result: TypeId, + ) -> Result { + let result = self.shape(result)?; + self.signature_result(params, result) + } + fn signature_result( + &mut self, + params: &[(TypeId, jett_mir::ParamMode)], + result: u32, + ) -> Result { + let mut parameters = Vec::new(); + for (ty, mode) in params { + parameters.push(( + self.shape(*ty)?, + match mode { + jett_mir::ParamMode::Owned => 1, + jett_mir::ParamMode::View => 2, + }, + )); + } + let signature = Signature { result, parameters }; + if let Some(index) = self + .signatures + .iter() + .position(|existing| existing == &signature) + { + return ordinal(index); + } + let id = ordinal(self.signatures.len())?; + self.signatures.push(signature); + Ok(id) + } + fn function_signature(&self, function: FunctionId) -> Result { + self.functions + .get(&function.index()) + .copied() + .ok_or_else(|| pending("callee has no fresh execution-family signature")) + } + fn entry_bridge( + &mut self, + bridge: &custody::ResourceEntryScopePlan, + ) -> Result<(SelectedEntry, u32), CodegenError> { + let root = bridge.root_scope(); + let complete = bridge.completion(); + if self.plan.function(bridge.function()).is_some() + || root.role() != custody::ResourceFrameRole::Scope + || root.parent().is_some() + || root.site().function() != bridge.function() + || complete.frame() != root.id() + || complete.site() != root.site() + || complete.is_expression_operation() + || !matches!(complete.role(), Role::Complete { .. }) + { + return Err(pending( + "ordinary entry bridge differs from its exact wrapper Scope", + )); + } + let params = bridge + .parameters() + .iter() + .map(|param| (param.ty, param.mode)) + .collect::>(); + let signature = self.signature(¶ms, bridge.return_type())?; + let scope = ordinal(self.frames.len())?; + self.frames.push(Frame { + role: 1, + site: root.site(), + signature, + parents: vec![Parent::Root], + }); + let completion = ordinal(self.operations.len())?; + self.operations.push((complete.site(), vec![13, scope])); + // Entry-only rows do not populate Source-family signature/frame/operation maps. + Ok(( + SelectedEntry { + function: bridge.function().index(), + signature, + scope, + }, + completion, + )) + } + fn frame( + &self, + function: FunctionId, + frame: custody::ResourceFrameId, + ) -> Result { + self.frame_ids + .get(&(function.index(), frame.index())) + .copied() + .ok_or_else(|| pending("frame is outside its exact function plan")) + } + fn slot( + &self, + function: FunctionId, + slot: custody::ResourceOwnerSlotId, + ) -> Result { + self.slot_ids + .get(&(function.index(), slot.index())) + .copied() + .ok_or_else(|| pending("slot is outside its exact function plan")) + } + fn hook(&self, hook: &jett_hir::ResourceHookRef) -> Result { + self.hook_ids + .iter() + .position(|candidate| candidate == hook) + .map(ordinal) + .transpose()? + .ok_or_else(|| pending("operation has a foreign manifest hook")) + } + fn loan( + &self, + function: &custody::ResourceFunctionPlan, + loan: custody::ResourceLoanId, + ) -> Result, CodegenError> { + let source = function + .loans() + .get(loan.index()) + .filter(|record| record.id() == loan) + .ok_or_else(|| pending("formal has no exact plan loan"))?; + Ok(match source.source() { + custody::ResourceLoanSource::Owner(_) => vec![ + 1, + *self + .borrow_ids + .get(&(function.function().index(), loan.index())) + .ok_or_else(|| pending("loan has no unique actual Borrow operation"))?, + ], + custody::ResourceLoanSource::IncomingViewFormal { scope, parameter } => vec![ + 2, + self.frame(function.function(), scope)?, + count(parameter)?, + ], + custody::ResourceLoanSource::CarrierSumProjection { operation } => vec![ + 3, + *self + .carrier_operation_ids + .get(&(function.function().index(), operation.index())) + .ok_or_else(|| { + pending("carrier sum adapter has no exact installed operation") + })?, + ], + custody::ResourceLoanSource::ProjectedSumPayload { .. } => vec![ + 3, + *self + .borrow_ids + .get(&(function.function().index(), loan.index())) + .ok_or_else(|| { + pending("projected payload has no unique constructor projection operation") + })?, + ], + }) + } + fn populate(&mut self) -> Result<(), CodegenError> { + let plan = self.plan; + // Every original hook is retained, including unused descriptors. + for hook in plan.program().resource_manifest.hooks() { + let Type::Function { + params, + view_params, + return_type, + } = self.plan.types().resolve(hook.function_type()) + else { + return Err(pending("manifest hook lost its exact function signature")); + }; + if params.len() != view_params.len() { + return Err(pending("hook access vector is malformed")); + } + let parameters = params + .iter() + .zip(view_params) + .map(|(ty, view)| { + ( + *ty, + if *view { + jett_mir::ParamMode::View + } else { + jett_mir::ParamMode::Owned + }, + ) + }) + .collect::>(); + let result = *return_type; + let signature = self.signature(¶meters, result)?; + let recipe = match hook.recipe() { + jett_types::ResourceKernelRecipe::NetworkFactory => 1, + jett_types::ResourceKernelRecipe::NetworkBorrow => 2, + jett_types::ResourceKernelRecipe::Finalize => 3, + }; + ordinal(self.hooks.len())?; + self.hooks.push((hook.kind().index(), recipe, signature)); + self.hook_ids.push(hook); + } + for function in plan.functions() { + let params = function + .parameters() + .iter() + .map(|parameter| (parameter.ty, parameter.mode)) + .collect::>(); + let result = if matches!( + plan.types().resolve(function.return_type()), + Type::Function { .. } + ) { + let hook = function.descriptor_return().ok_or_else(|| { + pending("descriptor result has no exact original/current Return proof") + })?; + if hook.function_type() != function.return_type() { + return Err(pending( + "descriptor Return proof changes its function signature", + )); + } + let words = vec![10, self.hook(&hook)?]; + if let Some(index) = self.shapes.iter().position(|shape| shape == &words) { + ordinal(index)? + } else { + let id = ordinal(self.shapes.len())?; + self.shapes.push(words); + id + } + } else { + self.shape(function.return_type())? + }; + let signature = self.signature_result(¶ms, result)?; + self.functions + .insert(function.function().index(), signature); + for frame in function.frames() { + let id = ordinal(self.frames.len())?; + self.frame_ids + .insert((function.function().index(), frame.id().index()), id); + self.frames.push(Frame { + role: match frame.role() { + custody::ResourceFrameRole::Scope => 1, + custody::ResourceFrameRole::Operation => 2, + custody::ResourceFrameRole::Return => 3, + }, + site: frame.site(), + signature, + parents: Vec::new(), + }); + } + } + for function in plan.functions() { + for frame in function.frames() { + if let Some(parent) = frame.parent() { + let (id, parent) = ( + self.frame(function.function(), frame.id())?, + self.frame(function.function(), parent)?, + ); + self.frames[id as usize].parents.push(Parent::Frame(parent)); + } + } + if let Some(carriers) = function.carriers() { + for slot in carriers.slots() { + let id = ordinal(self.carrier_slots.len())?; + let frame = self.frame(function.function(), slot.frame())?; + let ty = carriers + .shape(slot.shape()) + .ok_or_else(|| pending("carrier slot lost its exact shape"))? + .ty(); + let node = self.carrier_node(ty)?; + self.carrier_slot_ids + .insert((function.function().index(), slot.id().index()), id); + self.carrier_slots.push(vec![frame, node]); + } + } + for slot in function.owner_slots() { + let id = ordinal(self.slots.len())?; + let frame = self.frame(function.function(), slot.frame())?; + let shape = self.custody_shape(slot.shape())?; + if matches!(slot.storage(), custody::ResourceSlotStorage::Return { .. }) + && shape + != self.signatures[self.function_signature(function.function())? as usize] + .result + { + return Err(pending( + "Return slot differs from its containing function signature result", + )); + } + let mut row = vec![frame, shape, count(slot.shape().active_path().len())?]; + row.extend(slot.shape().active_path().iter().map(|step| match step { + custody::ResourcePath::Some => 1, + custody::ResourcePath::Ok => 2, + })); + self.slot_ids + .insert((function.function().index(), slot.id().index()), id); + self.slots.push(row); + } + } + // Fix all operation ordinals before projecting references to Borrow rows. + for function in plan.functions() { + for operation in function.operations() { + if (!emitted(operation.role()) && operation.indirect_hook_target().is_none()) + || staged_alias(function, operation)?.is_some() + || carrier_operations::alias(function, operation)?.is_some() + { + continue; + } + let id = ordinal(self.operations.len())?; + self.operation_ids + .insert((function.function().index(), operation.id().index()), id); + if let Role::Borrow { loan } + | Role::PrepareSourceBorrow { loan, .. } + | Role::BorrowSum { loan } + | Role::PrepareSourceSumBorrow { loan, .. } + | Role::ProjectSumView { + destination: loan, .. + } = operation.role() + { + if self + .borrow_ids + .insert((function.function().index(), loan.index()), id) + .is_some() + { + return Err(pending("loan has duplicate constructor Borrow operations")); + } + } + if let Role::Carrier { operation: carrier } = operation.role() { + self.allocate_carrier_operation(function, *carrier, id)?; + } + self.operations.push((operation.site(), Vec::new())); + if operation.indirect_hook_target().is_some() + && matches!(operation.role(), Role::InvokeHook { hook, .. } + if hook.recipe() == jett_types::ResourceKernelRecipe::NetworkBorrow) + { + let target = ordinal(self.operations.len())?; + self.descriptor_borrow_targets.insert( + (function.function().index(), operation.id().index()), + target, + ); + self.operations.push((operation.site(), Vec::new())); + } + } + } + for function in plan.functions() { + for operation in function.operations() { + let Some(canonical) = carrier_operations::alias(function, operation)? else { + continue; + }; + let id = *self + .operation_ids + .get(&(function.function().index(), canonical.id().index())) + .ok_or_else(|| pending("carrier activation lost its constructor row"))?; + if self + .operation_ids + .insert((function.function().index(), operation.id().index()), id) + .is_some() + { + return Err(pending("carrier activation already owns another wire row")); + } + if let Role::Carrier { operation: carrier } = operation.role() { + self.carrier_operation_ids + .insert((function.function().index(), carrier.index()), id); + } + } + } + for function in plan.functions() { + for operation in function.operations() { + if carrier_operations::alias(function, operation)?.is_some() { + continue; + } + let Some(id) = self + .operation_ids + .get(&(function.function().index(), operation.id().index())) + .copied() + else { + continue; + }; + let row = self.operation_row(function, operation)?; + self.operations[id as usize].1 = row; + if let Some(target) = self + .descriptor_borrow_targets + .get(&(function.function().index(), operation.id().index())) + .copied() + { + self.operations[target as usize].1 = + self.borrow_hook_row(function, operation)?; + } + } + } + // Prepared metadata owns the single wire row. Reached activations use its + // exact ordinal; the private region witness owns their cross-block sites. + for function in plan.functions() { + for operation in function.operations() { + let Some(prepared) = staged_alias(function, operation)? else { + continue; + }; + let id = *self + .operation_ids + .get(&(function.function().index(), prepared.id().index())) + .ok_or_else(|| pending("staged activation lost its prepared row"))?; + if self + .operation_ids + .insert((function.function().index(), operation.id().index()), id) + .is_some() + { + return Err(pending("staged activation already owns another wire row")); + } + } + } + for frame in &mut self.frames { + frame.parents.sort(); + frame.parents.dedup(); + count(frame.parents.len())?; + } + Ok(()) + } + fn operation_row( + &mut self, + function: &custody::ResourceFunctionPlan, + operation: &custody::ResourceOperation, + ) -> Result, CodegenError> { + let f = function.function(); + let frame = self.frame(f, operation.frame())?; + if let Some(proof) = operation.indirect_hook_target() { + let Role::InvokeHook { hook, source, .. } = operation.role() else { + return Err(pending( + "descriptor target proof is not an exact hook invocation", + )); + }; + if hook != proof + || !matches!(source.target, jett_hir::CallTarget::Indirect { signature_type } + if signature_type == hook.function_type()) + || source.bridge != jett_hir::CallBridge::Direct + || !source.generated_operands.is_empty() + { + return Err(pending( + "descriptor wire projection changes its exact original hook tuple", + )); + } + let target = if hook.recipe() == jett_types::ResourceKernelRecipe::NetworkBorrow { + *self + .descriptor_borrow_targets + .get(&(f.index(), operation.id().index())) + .ok_or_else(|| pending("indirect Borrow has no separate physical target row"))? + } else { + let mut targets = function.operations().iter().filter(|physical| { + physical.frame() == operation.frame() + && match physical.role() { + Role::Acquire { hook: current, .. } => { + current == hook + && hook.recipe() + == jett_types::ResourceKernelRecipe::NetworkFactory + } + Role::Close { hook: current, .. } => { + current == hook + && hook.recipe() == jett_types::ResourceKernelRecipe::Finalize + } + _ => false, + } + }); + let target = targets + .next() + .ok_or_else(|| pending("indirect hook has no exact physical target"))?; + if targets.next().is_some() { + return Err(pending("indirect hook has multiple physical targets")); + } + *self + .operation_ids + .get(&(f.index(), target.id().index())) + .ok_or_else(|| pending("indirect hook physical target has no fixed row"))? + }; + let hook = self.hook(hook)?; + return Ok(vec![15, hook, self.hooks[hook as usize].2, target]); + } + Ok(match operation.role() { + Role::Acquire { hook, destination } => { + vec![1, frame, self.hook(hook)?, self.slot(f, *destination)?] + } + Role::Transfer { + source, + destination, + } => { + let slot = function + .owner_slots() + .get(destination.index()) + .ok_or_else(|| { + pending("Transfer destination is outside its fresh function plan") + })?; + let execution_frame = + if matches!(slot.storage(), custody::ResourceSlotStorage::Return { .. }) { + let returning = function.provisional_return().ok_or_else(|| { + pending("Return transfer has no exact provisional frame") + })?; + if operation.frame() != returning.id() || slot.frame() != returning.id() { + return Err(pending( + "Return transfer differs from its exact provisional storage", + )); + } + // Execution precedes Scope cleanup; storage remains parent-lived Return. + self.frame(f, function.root_scope().id())? + } else { + frame + }; + vec![ + 2, + execution_frame, + self.slot(f, *source)?, + self.slot(f, *destination)?, + ] + } + Role::Borrow { loan } | Role::PrepareSourceBorrow { loan, .. } => { + let record = function + .loans() + .get(loan.index()) + .ok_or_else(|| pending("Borrow loan is absent"))?; + let custody::ResourceLoanSource::Owner(source) = record.source() else { + return Err(pending("resident formals cannot mint child core loans")); + }; + vec![ + 3, + frame, + self.slot(f, source)?, + self.frame(f, record.frame())?, + ] + } + Role::EndBorrow { loan } => { + let source = self.loan(function, *loan)?; + if !matches!(source[0], 1 | 3) { + return Err(pending( + "resident formal cannot retire the caller's core loan", + )); + } + vec![5, frame, source[1]] + } + Role::BorrowSum { loan } | Role::PrepareSourceSumBorrow { loan, .. } => { + let record = function + .loans() + .get(loan.index()) + .ok_or_else(|| pending("sum borrow has no exact loan"))?; + let custody::ResourceLoanSource::Owner(source) = record.source() else { + return Err(pending( + "sum borrow cannot mint authority from an incoming formal", + )); + }; + if matches!(record.shape(), custody::ResourceShape::Plain { .. }) { + return Err(pending("sum borrow requires an exact conditional shape")); + } + vec![ + 21, + frame, + self.slot(f, source)?, + self.frame(f, record.frame())?, + ] + } + Role::ObserveSumView { source, .. } => { + let mut row = vec![22, frame]; + row.extend(self.loan(function, *source)?); + row + } + Role::ProjectSumView { + source, + destination, + path, + .. + } => { + let record = function + .loans() + .get(destination.index()) + .ok_or_else(|| pending("sum projection has no exact child loan"))?; + if !matches!(record.shape(), custody::ResourceShape::Plain { .. }) { + return Err(pending("sum projection child is not a plain Resource loan")); + } + let mut row = vec![23, frame]; + row.extend(self.loan(function, *source)?); + row.push(match path { + custody::ResourceSumPayloadPath::OptionalSome => 1, + custody::ResourceSumPayloadPath::ResultOk => 2, + }); + row.push(self.frame(f, record.frame())?); + row + } + Role::ReadFailureCompanion { + source, failure, .. + } => { + let mut row = vec![24, frame]; + row.extend(self.loan(function, *source)?); + row.push(self.shape(*failure)?); + row + } + Role::EndSumBorrow { loan } => { + let source = self.loan(function, *loan)?; + if !matches!(source[0], 1 | 3) { + return Err(pending( + "incoming sum formal cannot end its caller's shell lease", + )); + } + vec![25, frame, source[1]] + } + Role::InvokeHook { hook, .. } => { + if hook.recipe() != jett_types::ResourceKernelRecipe::NetworkBorrow { + return Err(pending("unexpected emitted hook metadata role")); + } + self.borrow_hook_row(function, operation)? + } + Role::Close { hook, source } => { + vec![7, frame, self.hook(hook)?, self.slot(f, *source)?] + } + Role::Drop { source, .. } => { + let slot = function + .owner_slots() + .get(source.index()) + .ok_or_else(|| pending("Drop slot is absent"))?; + vec![ + if matches!(slot.shape(), custody::ResourceShape::Plain { .. }) { + 8 + } else { + 11 + }, + frame, + self.slot(f, *source)?, + ] + } + Role::SumAdopt { + source, + destination, + } => vec![ + 9, + frame, + self.slot(f, *source)?, + self.slot(f, *destination)?, + ], + Role::SumTake { + source, + destination, + success, + .. + } => { + if !*success { + return Err(pending("failure extraction cannot adopt a Resource owner")); + } + vec![ + 10, + frame, + self.slot(f, *source)?, + self.slot(f, *destination)?, + ] + } + Role::Replace { + destination, + replacement, + old, + } => { + let old_slot = function + .owner_slots() + .get(destination.index()) + .ok_or_else(|| pending("Replace destination is outside its exact plan"))?; + let rhs_slot = function + .owner_slots() + .get(replacement.index()) + .ok_or_else(|| pending("Replace RHS is outside its exact plan"))?; + if destination == replacement + || *old != custody::ResourceOccupancy::Occupied + || !matches!(old_slot.shape(), custody::ResourceShape::Plain { .. }) + || old_slot.shape() != rhs_slot.shape() + || !matches!(old_slot.storage(), custody::ResourceSlotStorage::Local { header } if header.mutable) + || old_slot.frame() != operation.frame() + || rhs_slot.frame() != operation.frame() + || function.frames()[operation.frame().index()].role() + != custody::ResourceFrameRole::Scope + { + return Err(pending( + "mutable replacement requires distinct occupied plain owners in the exact Scope", + )); + } + vec![ + 12, + frame, + self.slot(f, *destination)?, + self.slot(f, *replacement)?, + ] + } + Role::SelfRebind { .. } => { + return Err(pending("SelfRebind has no runtime operation row")); + } + Role::Complete { .. } => vec![13, frame], + Role::Descriptor { hook } => vec![14, self.hook(hook)?], + Role::BeginSourceFunction { + function: callee, + formals, + evaluation_order, + operands, + result, + .. + } + | Role::InvokeSourceFunction { + function: callee, + formals, + evaluation_order, + operands, + result, + .. + } => self.source_row( + function, + frame, + *callee, + formals, + evaluation_order, + operands, + *result, + )?, + Role::TakeFailureCompanion { + source, failure, .. + } => vec![17, frame, self.slot(f, *source)?, self.shape(*failure)?], + Role::CompleteReturnAfterCleanup { source } => vec![ + 18, + self.frame(f, function.root_scope().id())?, + self.slot(f, *source)?, + ], + Role::CreateAbsentSum { destination } => vec![19, frame, self.slot(f, *destination)?], + Role::CreateFailureSum { + destination, + failure, + } => vec![ + 20, + frame, + self.slot(f, *destination)?, + self.shape(*failure)?, + ], + Role::Carrier { operation: carrier } => { + let record = self.encode_carrier_operation(function, *carrier)?; + vec![26, record] + } + Role::StageSourceActual { .. } | Role::BoundedBorrowUse { .. } => { + return Err(pending( + "bounded use is joined by the actual invocation row", + )); + } + }) + } + fn borrow_hook_row( + &mut self, + function: &custody::ResourceFunctionPlan, + operation: &custody::ResourceOperation, + ) -> Result, CodegenError> { + let Role::InvokeHook { hook, operands, .. } = operation.role() else { + return Err(pending( + "physical Borrow target is not an exact hook operation", + )); + }; + if hook.recipe() != jett_types::ResourceKernelRecipe::NetworkBorrow { + return Err(pending("physical Borrow target changes its hook recipe")); + } + let mut loans = operands.iter().filter_map(|operand| { + if let Operand::Borrowed { loan, .. } = operand { + Some(*loan) + } else { + None + } + }); + let loan = loans + .next() + .ok_or_else(|| pending("Borrow hook has no exact resource loan"))?; + if loans.next().is_some() { + return Err(pending("Borrow hook has multiple resource loan operands")); + } + let mut row = vec![ + 6, + self.frame(function.function(), operation.frame())?, + self.hook(hook)?, + ]; + row.extend(self.loan(function, loan)?); + Ok(row) + } + fn source_row( + &mut self, + caller: &custody::ResourceFunctionPlan, + frame: u32, + target: FunctionId, + formals: &[custody::ResourceCallFormal], + order: &[usize], + operands: &[Operand], + result: CallResult, + ) -> Result, CodegenError> { + let plan = self.plan; + let callee = plan + .function(target) + .ok_or_else(|| pending("Source target is outside the fresh execution closure"))?; + if formals.len() != callee.parameters().len() + || order.len() != formals.len() + || operands.len() != formals.len() + { + return Err(pending("Source formal tuple is incomplete")); + } + let scope = self.frame(target, callee.root_scope().id())?; + let returning = callee + .provisional_return() + .map(|record| self.frame(target, record.id())) + .transpose()?; + let parent = returning.unwrap_or(scope); + self.frames[parent as usize] + .parents + .push(Parent::Frame(frame)); + let mut row = vec![ + 16, + frame, + target.index(), + self.function_signature(target)?, + scope, + ]; + if let Some(returning) = returning { + row.extend([1, returning]); + } else { + row.push(0); + } + row.push(count(order.len())?); + for parameter in order { + row.push(count(*parameter)?); + } + row.push(count(formals.len())?); + for (parameter, param) in callee.parameters().iter().enumerate() { + let fact = formals + .iter() + .find(|fact| fact.parameter() == parameter) + .ok_or_else(|| pending("Source parameter has no original formal fact"))?; + if fact.parameter_type() != param.ty + || fact.access() != param.mode + || order.get(fact.source_index()) != Some(¶meter) + { + return Err(pending( + "Source target header/permutation differs from its sealed formal", + )); + } + let syntax = match fact.syntax() { + custody::ResourceArgumentSyntax::Bare => 1, + custody::ResourceArgumentSyntax::WrittenView => 2, + }; + let effect = match fact.effect() { + custody::ResourceArgumentEffect::Copy => 1, + custody::ResourceArgumentEffect::TransferOwned => 2, + custody::ResourceArgumentEffect::RelinquishOwned => 3, + custody::ResourceArgumentEffect::RetainBorrow => 4, + custody::ResourceArgumentEffect::ObserveData => 5, + }; + row.extend([ + count(parameter)?, + count(fact.source_index())?, + self.shape(fact.actual_type())?, + self.shape(param.ty)?, + syntax, + effect, + if param.mode == jett_mir::ParamMode::View { + 2 + } else { + 1 + }, + ]); + let operand = operands + .iter() + .find(|operand| match operand { + Operand::Ordinary { parameter: p, .. } + | Operand::Owned { parameter: p, .. } + | Operand::Borrowed { parameter: p, .. } + | Operand::CarrierOwned { parameter: p, .. } + | Operand::CarrierBorrowed { parameter: p, .. } => *p == parameter, + }) + .ok_or_else(|| pending("Source formal has no exact evaluated operand"))?; + match operand { + Operand::Ordinary { .. } => row.push(1), + Operand::CarrierOwned { slot, .. } => { + let carriers = callee + .carriers() + .ok_or_else(|| pending("carrier formal has no callee plan"))?; + let destinations = carriers.slots().iter().filter(|slot| matches!(slot.storage(), custody::ResourceSlotStorage::Local { header } if header.id == param.local)).collect::>(); + if destinations.len() != 1 + || destinations[0].frame() != callee.root_scope().id() + { + return Err(pending("carrier owned formal has no exact parameter slot")); + } + row.extend([ + 5, + self.carrier_slot(caller.function(), *slot)?, + self.carrier_slot(target, destinations[0].id())?, + ]); + } + Operand::CarrierBorrowed { loan, .. } => { + row.push(6); + row.extend(self.carrier_loan(caller, *loan)?); + } + Operand::Owned { slot, .. } => { + let destinations=callee.owner_slots().iter().filter(|slot| matches!(slot.storage(),custody::ResourceSlotStorage::Local {header} if header.id==param.local)).collect::>(); + if destinations.len() != 1 + || destinations[0].frame() != callee.root_scope().id() + { + return Err(pending("owned formal has no unique exact parameter slot")); + } + row.extend([ + 2, + self.slot(caller.function(), *slot)?, + self.slot(target, destinations[0].id())?, + ]); + } + Operand::Borrowed { loan, .. } => { + let record = caller + .loans() + .get(loan.index()) + .ok_or_else(|| pending("Source formal has no exact borrowed shape"))?; + row.push( + if matches!(record.shape(), custody::ResourceShape::Plain { .. }) { + 3 + } else { + 4 + }, + ); + row.extend(self.loan(caller, *loan)?); + } + } + } + match result { + CallResult::Ordinary { ty } => { + if ty != callee.return_type() || returning.is_some() { + return Err(pending( + "ordinary Source result changes the callee return contract", + )); + } + row.extend([ + 1, + self.signatures[self.function_signature(target)? as usize].result, + ]); + } + CallResult::Carrier { slot } => { + let returning = + returning.ok_or_else(|| pending("carrier result has no provisional Return"))?; + let carriers = caller + .carriers() + .ok_or_else(|| pending("carrier result has no caller plan"))?; + let destination = carriers + .slots() + .get(slot.index()) + .filter(|row| row.id() == slot) + .ok_or_else(|| pending("carrier caller result slot is absent"))?; + let ty = carriers + .shape(destination.shape()) + .ok_or_else(|| pending("carrier result shape is absent"))? + .ty(); + let callee_carriers = callee + .carriers() + .ok_or_else(|| pending("carrier result has no callee plan"))?; + let mut returns = callee_carriers + .slots() + .iter() + .filter(|slot| { + matches!(slot.storage(), custody::ResourceSlotStorage::Return { .. }) + }) + .map(|slot| self.carrier_slot(target, slot.id())) + .collect::, _>>()?; + returns.sort(); + returns.dedup(); + if returns.is_empty() { + return Err(pending("carrier callee has no exact Return publication")); + } + row.extend([ + 3, + self.shape(ty)?, + self.frame(caller.function(), destination.frame())?, + self.carrier_slot(caller.function(), slot)?, + returning, + count(returns.len())?, + ]); + row.extend(returns); + } + CallResult::Owned { slot } => { + let returning = returning.ok_or_else(|| { + pending("owned Source result has no callee provisional Return") + })?; + let destination = caller + .owner_slots() + .get(slot.index()) + .ok_or_else(|| pending("caller result slot is absent"))?; + let mut returns = callee + .owner_slots() + .iter() + .filter(|slot| { + matches!(slot.storage(), custody::ResourceSlotStorage::Return { .. }) + }) + .map(|slot| self.slot(target, slot.id())) + .collect::, _>>()?; + returns.sort(); + returns.dedup(); + if returns.is_empty() { + return Err(pending("callee has no actual Return publication site")); + } + row.extend([ + 2, + self.custody_shape(destination.shape())?, + self.frame(caller.function(), destination.frame())?, + self.slot(caller.function(), slot)?, + returning, + count(returns.len())?, + ]); + row.extend(returns); + } + } + Ok(row) + } + fn encode(&self) -> Result, CodegenError> { + let mut output = Vec::new(); + output.extend_from_slice(b"JTRSC001"); + word( + &mut output, + if self.carrier_graph.is_empty() { 2 } else { 3 }, + ); + word(&mut output, 0); + output.extend_from_slice(&0u64.to_le_bytes()); + for size in [ + self.plan.program().resource_manifest.kinds().len(), + self.hooks.len(), + self.signatures.len(), + self.shapes.len(), + self.frames.len(), + self.slots.len(), + self.operations.len(), + ] { + word(&mut output, count(size)?); + } + word(&mut output, 0); + for i in 0..self.plan.program().resource_manifest.kinds().len() { + word(&mut output, ordinal(i)?); + } + for (i, (kind, recipe, signature)) in self.hooks.iter().enumerate() { + words(&mut output, &[ordinal(i)?, *kind, *recipe, *signature]); + } + for (i, signature) in self.signatures.iter().enumerate() { + words( + &mut output, + &[ + ordinal(i)?, + count(signature.parameters.len())?, + signature.result, + ], + ); + for (shape, access) in &signature.parameters { + words(&mut output, &[*shape, *access]); + } + } + for (i, shape) in self.shapes.iter().enumerate() { + word(&mut output, ordinal(i)?); + words(&mut output, shape); + } + for (i, frame) in self.frames.iter().enumerate() { + words(&mut output, &[ordinal(i)?, frame.role]); + site(&mut output, frame.site)?; + words(&mut output, &[frame.signature, count(frame.parents.len())?]); + for parent in &frame.parents { + match parent { + Parent::Root => words(&mut output, &[0, 0]), + Parent::Frame(index) => words(&mut output, &[1, *index]), + } + } + } + for (i, slot) in self.slots.iter().enumerate() { + word(&mut output, ordinal(i)?); + words(&mut output, slot); + } + for (i, (at, operation)) in self.operations.iter().enumerate() { + word(&mut output, ordinal(i)?); + site(&mut output, *at)?; + words(&mut output, operation); + } + if !self.carrier_graph.is_empty() { + carriers::append_extension( + &mut output, + &self.carrier_graph, + &self.carrier_slots, + &self.carrier_operations, + )?; + } + if output.len() > BYTE_LIMIT { + return Err(pending( + "layout byte capacity exceeds the bounded wire domain", + )); + } + let length = + u64::try_from(output.len()).map_err(|_| pending("layout length exceeds u64"))?; + output[16..24].copy_from_slice(&length.to_le_bytes()); + Ok(output) + } +} +fn staged_alias<'a>( + function: &'a custody::ResourceFunctionPlan, + operation: &custody::ResourceOperation, +) -> Result, CodegenError> { + let mut prepared = function.operations().iter().filter(|candidate| { + match (operation.role(), candidate.role()) { + (Role::Borrow { loan }, Role::PrepareSourceBorrow { loan: expected, .. }) + | (Role::BorrowSum { loan }, Role::PrepareSourceSumBorrow { loan: expected, .. }) => { + loan == expected + } + (Role::InvokeSourceFunction { .. }, Role::BeginSourceFunction { .. }) => { + operation.frame() == candidate.frame() + } + _ => false, + } + }); + let Some(selected) = prepared.next() else { + return Ok(None); + }; + if prepared.next().is_some() || selected.frame() != operation.frame() { + return Err(pending( + "staged activation has ambiguous or foreign prepared metadata", + )); + } + match (operation.role(), selected.role()) { + (Role::Borrow { loan }, Role::PrepareSourceBorrow { parameter, .. }) + | (Role::BorrowSum { loan }, Role::PrepareSourceSumBorrow { parameter, .. }) => { + let record = function + .loans() + .get(loan.index()) + .ok_or_else(|| pending("staged activation loan is absent"))?; + if record.frame() != operation.frame() || record.parameter() != Some(*parameter) { + return Err(pending("staged activation changes its sealed loan/formal")); + } + } + ( + Role::InvokeSourceFunction { + function: current, + formals, + evaluation_order, + operands, + result, + .. + }, + Role::BeginSourceFunction { + function: expected, + formals: originals, + evaluation_order: order, + operands: planned, + result: output, + .. + }, + ) => { + if current != expected + || evaluation_order != order + || result != output + || formals.len() != originals.len() + || operands.len() != planned.len() + || !formals.iter().zip(originals).all(|(a, b)| { + a.parameter() == b.parameter() + && a.source_index() == b.source_index() + && a.actual_type() == b.actual_type() + && a.parameter_type() == b.parameter_type() + && a.syntax() == b.syntax() + && a.effect() == b.effect() + && a.access() == b.access() + }) + || !operands + .iter() + .zip(planned) + .all(|(a, b)| same_operand(a, b)) + { + return Err(pending( + "staged Invoke changes its complete prepared Source tuple", + )); + } + } + _ => return Err(pending("staged activation selected another prepared role")), + } + Ok(Some(selected)) +} + +fn same_operand(a: &Operand, b: &Operand) -> bool { + match (a, b) { + ( + Operand::Ordinary { + parameter: p, + ty: a, + }, + Operand::Ordinary { + parameter: q, + ty: b, + }, + ) => p == q && a == b, + ( + Operand::Owned { + parameter: p, + slot: a, + }, + Operand::Owned { + parameter: q, + slot: b, + }, + ) => p == q && a == b, + ( + Operand::Borrowed { + parameter: p, + loan: a, + }, + Operand::Borrowed { + parameter: q, + loan: b, + }, + ) => p == q && a == b, + ( + Operand::CarrierOwned { + parameter: p, + slot: a, + }, + Operand::CarrierOwned { + parameter: q, + slot: b, + }, + ) => p == q && a == b, + ( + Operand::CarrierBorrowed { + parameter: p, + loan: a, + }, + Operand::CarrierBorrowed { + parameter: q, + loan: b, + }, + ) => p == q && a == b, + _ => false, + } +} + +fn emitted(role: &Role) -> bool { + match role { + Role::StageSourceActual { .. } + | Role::BoundedBorrowUse { .. } + | Role::SelfRebind { .. } => false, + Role::InvokeHook { hook, .. } => { + hook.recipe() == jett_types::ResourceKernelRecipe::NetworkBorrow + } + _ => true, + } +} +fn word(output: &mut Vec, value: u32) { + output.extend_from_slice(&value.to_le_bytes()); +} +fn words(output: &mut Vec, values: &[u32]) { + for value in values { + word(output, *value); + } +} +fn site(output: &mut Vec, at: custody::ResourceSite) -> Result<(), CodegenError> { + words(output, &[at.function().index(), at.block().index()]); + match at.position() { + custody::ResourcePosition::Statement(index) => words(output, &[0, count(index)?]), + custody::ResourcePosition::Terminator => words(output, &[1, 0]), + }; + Ok(()) +} + +#[cfg(test)] +#[path = "resource_layout/tests.rs"] +mod tests; diff --git a/crates/jett_codegen_cranelift/src/emit/resource_layout/carrier_operations.rs b/crates/jett_codegen_cranelift/src/emit/resource_layout/carrier_operations.rs new file mode 100644 index 00000000..d9887d35 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_layout/carrier_operations.rs @@ -0,0 +1,711 @@ +//! Exact v3 operation projection. The fresh private plan, not a wire type/name, +//! selects every carrier slot, path, constructor child, and lifetime edge. +use super::*; +use custody::{ + ResourceCarrierConstructor as Constructor, ResourceCarrierLoanSource as LoanSource, + ResourceCarrierObservation as Observation, ResourceCarrierOperationRole as CarrierRole, + ResourceCarrierProjectionPath as Path, ResourceCarrierValue as CarrierValue, +}; + +pub(in crate::emit) fn is_carrier_type( + plan: &custody::ResourceOwnershipPlan<'_>, + ty: TypeId, +) -> bool { + if !plan + .carrier_shapes() + .iter() + .any(|shape| shape.ty() == ty && shape.contains_resource()) + { + return false; + } + match plan.types().resolve(ty) { + Type::Resource(_) | Type::Function { .. } => false, + Type::Optional(inner) if matches!(plan.types().resolve(*inner), Type::Resource(_)) => false, + Type::Result(ok, fail) + if matches!(plan.types().resolve(*ok), Type::Resource(_)) + && !plan.type_requires_custody(*fail) => + { + false + } + _ => true, + } +} + +/// Child/commit and the explicit borrowed-sum activation use the one row owned by +/// their constructor. A dead frame or matching signature cannot create an alias. +pub(super) fn alias<'a>( + function: &'a custody::ResourceFunctionPlan, + operation: &custody::ResourceOperation, +) -> Result, CodegenError> { + let Some(carriers) = function.carriers() else { + return Ok(None); + }; + let authority = match operation.role() { + Role::Carrier { operation: id } => { + let current = carriers + .operations() + .get(id.index()) + .filter(|row| row.id() == *id) + .ok_or_else(|| pending("carrier wrapper lost its exact private operation"))?; + let destination = match current.role() { + CarrierRole::ConstructorChild { destination, .. } + | CarrierRole::CommitConstructor { destination } => *destination, + _ => return Ok(None), + }; + let mut starts = carriers.operations().iter().filter(|row| + matches!(row.role(), CarrierRole::BeginConstructor { destination: slot, .. } if *slot == destination)); + let start = starts + .next() + .ok_or_else(|| pending("carrier activation has no constructor begin"))?; + if starts.next().is_some() + || (start.site() != current.site() + && (start.is_expression_operation() || current.is_expression_operation())) + || start.frame() != current.frame() + || start.id().index() >= current.id().index() + { + return Err(pending( + "carrier activation changes its exact constructor/site/order", + )); + } + start.id() + } + Role::BorrowSum { loan } => { + let record = function + .loans() + .get(loan.index()) + .filter(|row| row.id() == *loan) + .ok_or_else(|| pending("carrier sum activation lost its exact loan"))?; + let custody::ResourceLoanSource::CarrierSumProjection { operation: id } = + record.source() + else { + return Ok(None); + }; + let adapter = carriers + .operations() + .get(id.index()) + .filter(|row| row.id() == id) + .ok_or_else(|| pending("carrier sum loan lost its exact adapter"))?; + if adapter.frame() != operation.frame() + || !matches!(adapter.role(), CarrierRole::AdaptSum { loan: Some(expected), .. } if *expected == *loan) + { + return Err(pending( + "carrier sum activation changes its parent/lease/loan", + )); + } + id + } + _ => return Ok(None), + }; + let mut wrappers = function.operations().iter().filter(|candidate| + matches!(candidate.role(), Role::Carrier { operation } if *operation == authority)); + let selected = wrappers + .next() + .ok_or_else(|| pending("carrier authority has no exact wrapper"))?; + if wrappers.next().is_some() { + return Err(pending("carrier authority has duplicate wrappers")); + } + Ok(Some(selected)) +} + +impl Rows<'_, '_> { + pub(super) fn carrier_node(&mut self, ty: TypeId) -> Result { + if let Some(node) = self.carrier_graph.node_for_type(ty) { + return Ok(node); + } + let plan = self.plan; + let carriers = plan + .functions() + .iter() + .find_map(|function| function.carriers()) + .ok_or_else(|| pending("carrier shape has no constructor-owned graph"))?; + let shape = carriers + .shape_for_type(ty) + .ok_or_else(|| pending("carrier shape is outside its exact checked graph"))?; + let mut graph = std::mem::take(&mut self.carrier_graph); + let result = graph.project( + carriers, + plan.types(), + shape.id(), + &mut |ty| self.shape(ty), + &mut |ty| { + let predicate = carriers + .shape_for_type(ty) + .and_then(|shape| shape.refinement_predicate()) + .ok_or_else(|| { + pending("refinement graph lost its original checked predicate/body") + })?; + // The private getter authenticates the unique retained header and full + // body against the original checked archive/current witness. These + // bytes remain consistency metadata; runtime never grants a predicate + // or nominal proof because a type spelling happens to match. + use sha2::{Digest, Sha256}; + let mut bytes = predicate.id.index().to_le_bytes().to_vec(); + bytes.extend_from_slice(&Sha256::digest(format!("{:?}", predicate).as_bytes())); + Ok(bytes) + }, + ); + self.carrier_graph = graph; + result + } + + pub(super) fn carrier_slot( + &self, + function: FunctionId, + slot: custody::ResourceCarrierSlotId, + ) -> Result { + self.carrier_slot_ids + .get(&(function.index(), slot.index())) + .copied() + .ok_or_else(|| pending("carrier slot is outside its exact function plan")) + } + + pub(super) fn carrier_loan( + &self, + function: &custody::ResourceFunctionPlan, + loan: custody::ResourceCarrierLoanId, + ) -> Result, CodegenError> { + let carriers = function + .carriers() + .ok_or_else(|| pending("carrier loan has no current plan"))?; + let row = carriers + .loans() + .get(loan.index()) + .filter(|row| row.id() == loan) + .ok_or_else(|| pending("carrier loan is outside its exact plan"))?; + if let Some(operation) = self + .carrier_borrow_ids + .get(&(function.function().index(), loan.index())) + { + return Ok(vec![1, *operation]); + } + match row.source() { + LoanSource::IncomingViewFormal { scope, parameter } if row.path().is_empty() => { + Ok(vec![ + 2, + self.frame(function.function(), scope)?, + count(parameter)?, + ]) + } + _ => Err(pending( + "carrier loan has no unique Borrow or exact incoming formal", + )), + } + } + + fn carrier_source( + &self, + function: &custody::ResourceFunctionPlan, + source: LoanSource, + ) -> Result, CodegenError> { + let mut words = Vec::new(); + match source { + LoanSource::Slot(slot) => { + words.extend([1, self.carrier_slot(function.function(), slot)?]) + } + LoanSource::Loan(loan) => { + words.push(2); + words.extend(self.carrier_loan(function, loan)?); + } + LoanSource::IncomingViewFormal { scope, parameter } => words.extend([ + 2, + 2, + self.frame(function.function(), scope)?, + count(parameter)?, + ]), + } + Ok(words) + } + + fn borrowed_source( + &self, + function: &custody::ResourceFunctionPlan, + loan: custody::ResourceCarrierLoanId, + ) -> Result, CodegenError> { + let mut row = vec![2]; + row.extend(self.carrier_loan(function, loan)?); + Ok(row) + } + + /// Owning extraction must resolve to the exact root, rather than converting + /// an incoming or sibling loan into ownership. Paths stay in original order. + fn owned_root( + &self, + function: &custody::ResourceFunctionPlan, + loan: custody::ResourceCarrierLoanId, + ) -> Result<(custody::ResourceCarrierSlotId, Vec), CodegenError> { + let carriers = function + .carriers() + .ok_or_else(|| pending("carrier extraction has no plan"))?; + let mut visited = std::collections::BTreeSet::new(); + let mut cursor = loan; + let mut paths = Vec::new(); + loop { + if !visited.insert(cursor.index()) { + return Err(pending("carrier extraction has a cyclic loan origin")); + } + let row = carriers + .loans() + .get(cursor.index()) + .filter(|row| row.id() == cursor) + .ok_or_else(|| pending("carrier extraction lost its parent loan"))?; + paths.push(row.path().to_vec()); + match row.source() { + LoanSource::Slot(slot) => { + let mut path = Vec::new(); + for segment in paths.iter().rev() { + path.extend_from_slice(segment); + } + return Ok((slot, path)); + } + LoanSource::Loan(parent) => cursor = parent, + LoanSource::IncomingViewFormal { .. } => { + return Err(pending("owning extraction cannot consume an incoming view")); + } + } + } + } + + pub(super) fn allocate_carrier_operation( + &mut self, + function: &custody::ResourceFunctionPlan, + id: custody::ResourceCarrierOperationId, + wrapper: u32, + ) -> Result<(), CodegenError> { + let carriers = function + .carriers() + .ok_or_else(|| pending("carrier wrapper has no current plan"))?; + let row = carriers + .operations() + .get(id.index()) + .filter(|row| row.id() == id) + .ok_or_else(|| pending("carrier wrapper changed its exact operation"))?; + let record = ordinal(self.carrier_operations.len())?; + if self + .carrier_operation_ids + .insert((function.function().index(), id.index()), wrapper) + .is_some() + || self + .carrier_record_ids + .insert((function.function().index(), id.index()), record) + .is_some() + { + return Err(pending("carrier operation already has a wire row")); + } + match row.role() { + CarrierRole::Borrow { loan } + | CarrierRole::Project { + destination: loan, .. + } => { + if self + .carrier_borrow_ids + .insert((function.function().index(), loan.index()), wrapper) + .is_some() + { + return Err(pending("carrier loan has duplicate exact Borrow rows")); + } + } + CarrierRole::AdaptSum { + loan: Some(loan), .. + } => { + if self + .borrow_ids + .insert((function.function().index(), loan.index()), wrapper) + .is_some() + { + return Err(pending("carrier sum loan has duplicate adapter rows")); + } + } + _ => {} + } + self.carrier_operations.push(Vec::new()); + Ok(()) + } + + pub(super) fn encode_carrier_operation( + &mut self, + function: &custody::ResourceFunctionPlan, + id: custody::ResourceCarrierOperationId, + ) -> Result { + let carriers = function + .carriers() + .ok_or_else(|| pending("carrier wrapper lost its private plan"))?; + let operation = carriers + .operations() + .get(id.index()) + .filter(|row| row.id() == id) + .ok_or_else(|| pending("carrier wrapper lost its exact occurrence"))?; + let record = *self + .carrier_record_ids + .get(&(function.function().index(), id.index())) + .ok_or_else(|| pending("carrier occurrence has no allocated row"))?; + let frame = self.frame(function.function(), operation.frame())?; + let mut row = Vec::new(); + site(&mut row, operation.site())?; + let slot = |this: &Self, slot| this.carrier_slot(function.function(), slot); + match operation.role() { + CarrierRole::BeginConstructor { + destination, + constructor, + } => { + words( + &mut row, + &[ + 1, + frame, + slot(self, *destination)?, + constructor_selector(*constructor)?, + ], + ); + let mut children = Vec::new(); + let mut committed = false; + for candidate in carriers.operations().iter().skip(id.index() + 1) { + match candidate.role() { + CarrierRole::ConstructorChild { + destination: target, + child, + } if target == destination => { + if (candidate.site() != operation.site() + && (candidate.is_expression_operation() + || operation.is_expression_operation())) + || candidate.frame() != operation.frame() + || committed + { + return Err(pending( + "constructor child changes exact frame/site/commit order", + )); + } + children.push(child); + } + CarrierRole::CommitConstructor { + destination: target, + } if target == destination => { + if (candidate.site() != operation.site() + && (candidate.is_expression_operation() + || operation.is_expression_operation())) + || candidate.frame() != operation.frame() + || committed + { + return Err(pending( + "constructor commit is duplicated or disconnected", + )); + } + committed = true; + } + _ => {} + } + } + if !committed { + return Err(pending("carrier constructor has no exact commit")); + } + word(&mut row, count(children.len())?); + for child in children { + let ty = carriers + .shape(child.shape) + .ok_or_else(|| pending("constructor child lost its exact node"))? + .ty(); + words(&mut row, &[count(child.index)?, self.carrier_node(ty)?]); + match child.value { + CarrierValue::Ordinary { ty } => words(&mut row, &[1, self.shape(ty)?]), + CarrierValue::Owned { slot: source } => { + words(&mut row, &[2, slot(self, source)?]) + } + CarrierValue::LeafOwned { slot: source } => { + let tag = match function + .owner_slots() + .get(source.index()) + .ok_or_else(|| { + pending("constructor child lost its exact leaf slot") + })? + .shape() + { + custody::ResourceShape::Plain { .. } => 4, + custody::ResourceShape::Optional { .. } + | custody::ResourceShape::Result { .. } => 3, + }; + words(&mut row, &[tag, self.slot(function.function(), source)?]) + } + CarrierValue::Borrowed { .. } | CarrierValue::LeafBorrowed { .. } => { + return Err(pending( + "owning carrier constructor cannot adopt a borrowed child", + )); + } + } + } + } + CarrierRole::Transfer { + source, + destination, + } => words( + &mut row, + &[2, frame, slot(self, *source)?, slot(self, *destination)?], + ), + CarrierRole::QualifyMachine { + source, + destination, + machine, + state, + } => { + let ty = carriers + .shape(*machine) + .ok_or_else(|| pending("carrier qualification lost its exact machine"))? + .ty(); + words( + &mut row, + &[ + 11, + frame, + slot(self, *source)?, + slot(self, *destination)?, + self.carrier_node(ty)?, + count(*state)?, + ], + ); + } + CarrierRole::Borrow { loan } + | CarrierRole::Project { + destination: loan, .. + } => { + let loan = carriers + .loans() + .get(loan.index()) + .ok_or_else(|| pending("carrier Borrow lost its exact loan"))?; + words(&mut row, &[3, frame]); + row.extend(words_bytes(&self.carrier_source(function, loan.source())?)); + path(&mut row, loan.path())?; + let ty = carriers + .shape(loan.shape()) + .ok_or_else(|| pending("carrier Borrow lost its exact shape"))? + .ty(); + words( + &mut row, + &[ + self.carrier_node(ty)?, + self.frame(function.function(), loan.frame())?, + ], + ); + } + CarrierRole::EndBorrow { loan } => words( + &mut row, + &[ + 4, + frame, + *self + .carrier_borrow_ids + .get(&(function.function().index(), loan.index())) + .ok_or_else(|| pending("carrier EndBorrow has no exact activation row"))?, + ], + ), + CarrierRole::Observe { + source, + observation, + .. + } => { + words(&mut row, &[5, frame]); + row.extend(words_bytes(&self.borrowed_source(function, *source)?)); + match observation { + Observation::Length => { + path(&mut row, &[])?; + word(&mut row, 1); + } + Observation::Tag => { + path(&mut row, &[])?; + word(&mut row, 2); + } + Observation::State { .. } => { + path(&mut row, &[])?; + word(&mut row, 3); + } + Observation::Ordinary { path: selected, ty } => { + path(&mut row, selected)?; + let clone = match self.plan.types().resolve(*ty) { + Type::String => 1, + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::Bool + | Type::Nothing => 0, + _ => { + return Err(pending( + "carrier ordinary observation needs its exact owning clone transport", + )); + } + }; + words(&mut row, &[4, self.shape(*ty)?, clone]); + } + } + } + CarrierRole::Extract { + source, + path: selected, + destination, + } => { + let (root, mut full) = self.owned_root(function, *source)?; + full.extend_from_slice(selected); + words(&mut row, &[6, frame, slot(self, root)?]); + path(&mut row, &full)?; + words(&mut row, &[1, slot(self, *destination)?]); + } + CarrierRole::ExtractOrdinary { + source, + path: selected, + ty, + .. + } => { + let (root, mut full) = self.owned_root(function, *source)?; + full.extend_from_slice(selected); + words(&mut row, &[6, frame, slot(self, root)?]); + path(&mut row, &full)?; + words(&mut row, &[2, self.shape(*ty)?]); + } + CarrierRole::AdaptSum { + source, + path: selected, + destination, + loan, + lease_frame, + } => { + words(&mut row, &[7, frame]); + if loan.is_some() { + row.extend(words_bytes(&self.borrowed_source(function, *source)?)); + path(&mut row, selected)?; + } else { + let (root, mut full) = self.owned_root(function, *source)?; + full.extend_from_slice(selected); + words(&mut row, &[1, slot(self, root)?]); + path(&mut row, &full)?; + } + words( + &mut row, + &[ + self.slot(function.function(), *destination)?, + self.frame(function.function(), *lease_frame)?, + u32::from(loan.is_some()), + ], + ); + } + CarrierRole::BeginIteration { + source, + destination, + .. + } => { + let CarrierValue::Owned { slot: source } = source else { + return Err(pending( + "consuming carrier loop requires its exact owned source", + )); + }; + words( + &mut row, + &[2, frame, slot(self, *source)?, slot(self, *destination)?], + ); + } + CarrierRole::RetireIteration { source, binders } => { + let mut carrier_binders = Vec::new(); + let mut leaf_binders = Vec::new(); + for binder in binders { + let mut carrier = carriers.slots().iter().filter(|slot| matches!(slot.storage(), custody::ResourceSlotStorage::Local { header } if header.id == *binder)); + if let Some(selected) = carrier.next() { + if carrier.next().is_some() { + return Err(pending("iteration binder has ambiguous carrier storage")); + } + carrier_binders.push(slot(self, selected.id())?); + continue; + } + let mut leaf = function.owner_slots().iter().filter(|slot| matches!(slot.storage(), custody::ResourceSlotStorage::Local { header } if header.id == *binder)); + if let Some(selected) = leaf.next() { + if leaf.next().is_some() { + return Err(pending("iteration binder has ambiguous sum storage")); + } + leaf_binders.push(self.slot(function.function(), selected.id())?); + } + } + words( + &mut row, + &[ + 10, + frame, + slot(self, *source)?, + count(carrier_binders.len())?, + ], + ); + words(&mut row, &carrier_binders); + word(&mut row, count(leaf_binders.len())?); + words(&mut row, &leaf_binders); + } + CarrierRole::PublishReturn { source } => { + words(&mut row, &[9, frame, slot(self, *source)?]) + } + CarrierRole::Retire { source } => words(&mut row, &[8, frame, slot(self, *source)?]), + CarrierRole::ConstructorChild { .. } | CarrierRole::CommitConstructor { .. } => { + return Err(pending( + "constructor activation must alias its one Begin row", + )); + } + } + self.carrier_operations[record as usize] = row; + Ok(record) + } +} + +fn constructor_selector(constructor: Constructor) -> Result { + Ok(match constructor { + Constructor::List + | Constructor::Map + | Constructor::Struct + | Constructor::OptionalNone + | Constructor::ResultFail => 0, + Constructor::OptionalSome | Constructor::ResultOk => 1, + Constructor::Enum { variant } => variant.index(), + Constructor::Machine { state } => count(state)?, + }) +} +fn words_bytes(input: &[u32]) -> Vec { + let mut bytes = Vec::new(); + words(&mut bytes, input); + bytes +} +fn path(output: &mut Vec, input: &[Path]) -> Result<(), CodegenError> { + word(output, count(input.len())?); + for step in input { + match step { + Path::Field { field, .. } => words(output, &[1, field.index()]), + Path::Variant { variant, field, .. } => { + words(output, &[2, variant.index(), count(*field)?]) + } + Path::State { state, field, .. } => words(output, &[3, count(*state)?, count(*field)?]), + Path::Element { index } => { + word(output, 4); + selector(output, *index)?; + } + Path::MapKey { index } => { + word(output, 5); + selector(output, *index)?; + } + Path::MapValue { index } => { + word(output, 6); + selector(output, *index)?; + } + Path::OptionalSome => word(output, 7), + Path::ResultOk => word(output, 8), + Path::ResultFail => word(output, 9), + } + } + Ok(()) +} +fn selector( + output: &mut Vec, + index: custody::ResourceCarrierIndex, +) -> Result<(), CodegenError> { + match index { + custody::ResourceCarrierIndex::Constant(index) => words(output, &[0, count(index)?]), + custody::ResourceCarrierIndex::Local(_) + | custody::ResourceCarrierIndex::CurrentIteration { .. } => word(output, 1), + } + Ok(()) +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_layout/carrier_tests.rs b/crates/jett_codegen_cranelift/src/emit/resource_layout/carrier_tests.rs new file mode 100644 index 00000000..d3358783 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_layout/carrier_tests.rs @@ -0,0 +1,544 @@ +use super::*; +use jett_comptime::checked_types::CheckedExpressionTypes; +use jett_mir::{ResourceCarrierNode, ResourceCarrierOperationRole, ResourceCarrierProjectionPath}; +use object::{Object, ObjectSymbol}; + +include!("../resource_execution/tests/carrier_wrapper_sources.rs"); + +const WHOLE_SOURCE: &str = include_str!( + "../../../../jett_mir/src/resource_ownership/fixtures/17_absent_aggregate_shapes.jett" +); + +fn checked_wrapper(suffix: &str, release: bool) -> Arc { + checked_wrapper_source(WHOLE_SOURCE, suffix, release) +} + +fn checked_wrapper_source( + prefix: &str, + suffix: &str, + release: bool, +) -> Arc { + let mut parsed = parse(SUPPORT, FileId::new(10_000)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let declaration = parsed + .module + .items + .iter() + .find_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .unwrap(); + let source = format!("{prefix}{suffix}"); + assert!(source.starts_with(prefix)); + let mut origins = HashMap::from([(FileId::new(10_000), SourceOrigin::Stdlib)]); + for (text, file, origin) in [ + (LIST_LENGTH, FileId::new(10_001), SourceOrigin::Stdlib), + (MAP_LENGTH, FileId::new(10_002), SourceOrigin::Stdlib), + (source.as_str(), FileId::new(0), SourceOrigin::Project), + ] { + let next = parse(text, file); + assert!(next.errors.is_empty(), "{:?}", next.errors); + parsed.module.items.extend(next.module.items); + origins.insert(file, origin); + } + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: declaration, + member: member.into(), + recipe, + }) + .collect::>(); + Arc::new( + CheckedResourceProgram::prepare(parsed, origins, &catalog, CheckOptions { release }) + .unwrap_or_else(|error| panic!("{error:?}; {:?}", error.diagnostics())), + ) +} + +fn lowered_wrapper(checked: &Arc) -> Program { + let expression_types = Arc::new(CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let required = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + expression_types, + Arc::new(HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "{:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + required.values.checked_required_values(checked).unwrap(); + assert!(!required.values.is_empty()); + assert!( + required + .values + .values() + .all(|value| matches!(value, jett_comptime::Value::Int64(17))) + ); + let types = &checked.checked().interner; + let mut hir = jett_hir::lower_checked_resource_program(checked).unwrap(); + jett_hir::materialize_checked_required_values(&mut hir, &required.values, types).unwrap(); + jett_hir::complete_value_conversions(&mut hir, types).unwrap(); + jett_mir::lower(&hir, types).unwrap() +} + +fn named<'a>(program: &'a Program, name: &str) -> &'a Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap() +} + +fn named_mut<'a>(program: &'a mut Program, name: &str) -> &'a mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap() +} + +#[test] +fn resource_carrier_whole_source17_all_fourteen_wrappers_emit_real_objects_in_both_profiles() { + for release in [false, true] { + for (name, suffix) in WRAPPERS { + let checked = checked_wrapper(suffix, release); + let types = &checked.checked().interner; + let program = lowered_wrapper(&checked); + // The unused refined formal and latent occupied constructors remain in + // the original source and in its authenticated closure for every row. + let refined = named(&program, "refined_shape_contract"); + assert_eq!(refined.params.len(), 1); + assert_eq!(refined.return_type, TypeInterner::NOTHING); + for constructor in [ + "empty_tokens", + "absent_tokens", + "empty_map", + "absent_map", + "empty_result_tokens", + "failed_result_tokens", + "absent_envelope", + "absent_box", + "empty_choice", + "absent_choice", + "absent_state", + "absent_exact_state", + ] { + named(&program, constructor); + } + let artifact = crate::emit::emit_for_triple( + &program, + types, + HOST, + Some(entry(&program)), + CodegenOptions { optimize: release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: {error:?}")); + let object = object::File::parse(artifact.bytes.as_slice()).unwrap(); + let symbols = object + .symbols() + .filter_map(|symbol| { + symbol + .name() + .ok() + .map(|name| (name.to_owned(), symbol.is_definition())) + }) + .collect::>(); + assert!( + symbols + .iter() + .any(|(name, defined)| name == "jett_aot_resource_v1_manifest" && *defined) + ); + for leaf in [ + "carrier_construct_begin", + "carrier_child", + "carrier_commit", + "carrier_publish", + "source_enter", + "source_status", + "scope_complete", + ] { + assert!( + symbols.iter().any(|(name, defined)| name + == &format!("jett_rt_v1_resource_{leaf}") + && !*defined), + "{name}, release={release}, missing {leaf}: {symbols:?}" + ); + } + } + } +} + +#[test] +fn resource_carrier_v3_layout_retains_latent_shapes_refinement_and_exact_source_rows() { + for release in [false, true] { + let checked = checked_wrapper(WRAPPERS[6].1, release); + let program = lowered_wrapper(&checked); + let types = &checked.checked().interner; + let first = EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + let second = EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + assert_eq!(first.bytes(), second.bytes()); + assert_eq!( + u32::from_le_bytes(first.bytes()[8..12].try_into().unwrap()), + 3 + ); + assert_eq!( + u64::from_le_bytes(first.bytes()[16..24].try_into().unwrap()), + u64::try_from(first.bytes().len()).unwrap() + ); + assert!(first.bytes().windows(8).any(|bytes| bytes == b"JTCAR003")); + let mut rows = Rows::new(first.plan()).unwrap(); + rows.populate().unwrap(); + assert!(!rows.carrier_slots.is_empty()); + assert!(!rows.carrier_operations.is_empty()); + assert!(rows.shapes.iter().any(|row| row.first() == Some(&11))); + for tag in [2, 7, 8, 9, 10, 11] { + assert!( + rows.carrier_graph + .rows() + .iter() + .any(|row| u32::from_le_bytes(row[..4].try_into().unwrap()) == tag), + "retained node {tag}" + ); + } + let refined = first + .plan() + .function(named(&program, "refined_shape_contract").id) + .unwrap(); + let graph = refined.carriers().unwrap(); + assert!(graph.shapes().iter().any(|shape| matches!( + shape.node(), + ResourceCarrierNode::Refinement { .. } + ) && shape.refinement_predicate().is_some())); + // A carrier is never installed into the legacy leaf-owner table merely + // because its static graph contains a Resource in an inactive arm. + assert!( + rows.operations + .iter() + .any(|(_, row)| row.first() == Some(&26)) + ); + } +} + +#[test] +fn resource_carrier_prepared_map_keeps_key_before_value_reset_and_legacy_optional_adapter() { + for release in [false, true] { + let checked = checked_wrapper(WRAPPERS[3].1, release); + let types = &checked.checked().interner; + let mut program = lowered_wrapper(&checked); + jett_mir::prepare_native_sequences(&mut program, types); + let layout = EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + let function = layout.plan().function(entry(&program)).unwrap(); + let graph = function.carriers().unwrap(); + let [iteration] = graph.iterations() else { + panic!("one original map loop") + }; + assert!(iteration.cursor.is_some()); + let current = named(&program, "main"); + let gets = current.blocks[usize::try_from(iteration.body.index()).unwrap()] + .statements + .iter() + .filter_map(|statement| match &statement.kind { + StatementKind::SequenceGet { target, part, .. } => Some((*target, *part)), + _ => None, + }) + .collect::>(); + assert_eq!( + gets, + vec![ + (iteration.binders[0], jett_mir::SequencePart::Key), + (iteration.binders[1], jett_mir::SequencePart::Value) + ] + ); + assert!(graph.operations().iter().any(|operation| + matches!(operation.role(), ResourceCarrierOperationRole::RetireIteration { binders, .. } + if binders == &iteration.binders))); + assert!(graph.operations().iter().any(|operation| + matches!(operation.role(), ResourceCarrierOperationRole::AdaptSum { path, loan: None, .. } + if matches!(path.as_slice(), [ResourceCarrierProjectionPath::MapValue { .. }])))); + let mut rows = Rows::new(layout.plan()).unwrap(); + rows.populate().unwrap(); + assert!( + rows.carrier_operations + .iter() + .any(|row| u32::from_le_bytes(row[16..20].try_into().unwrap()) == 10) + ); + assert!( + rows.operations + .iter() + .any(|(_, row)| row.first() == Some(&16)), + "the ordinary Source body keeps its exact v2 formal tuple inside wire v3" + ); + } +} + +#[test] +fn resource_carrier_object_gate_rejects_public_return_header_and_unused_refined_formal_substitution() + { + let checked = checked_wrapper(WRAPPERS[0].1, false); + let types = &checked.checked().interner; + let program = lowered_wrapper(&checked); + EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + // Clone a publicly available Function from a separately authenticated Source. + // Its private witness remains opaque; the receiving program keeps its own + // original archive and manifest. Both complete originals are valid alone. + let foreign_source = WHOLE_SOURCE.replacen("marker: 17)", "marker: 19)", 1); + assert_ne!(foreign_source, WHOLE_SOURCE); + let foreign_checked = checked_wrapper_source(&foreign_source, WRAPPERS[0].1, false); + let foreign_program = lowered_wrapper(&foreign_checked); + EmittedResourceLayout::from_program( + &foreign_program, + &foreign_checked.checked().interner, + entry(&foreign_program), + ) + .unwrap(); + let foreign_function = named(&foreign_program, "absent_envelope"); + let original_function = named(&program, "absent_envelope"); + assert_eq!(foreign_function.id, original_function.id); + assert_eq!(foreign_function.identity, original_function.identity); + assert_eq!(foreign_function.return_type, original_function.return_type); + assert_ne!(foreign_function.blocks, original_function.blocks); + for mutation in 0..3 { + let mut changed = program.clone(); + let map = named(&changed, "empty_map").return_type; + let envelope = named(&changed, "absent_envelope").return_type; + match mutation { + 0 => named_mut(&mut changed, "empty_tokens").return_type = map, + 1 => { + let function = named_mut(&mut changed, "refined_shape_contract"); + let local = function.params[0].local; + function.locals[usize::try_from(local.index()).unwrap()].ty = envelope; + } + 2 => { + *named_mut(&mut changed, "absent_envelope") = foreign_function.clone(); + let errors = jett_mir::validate_resource_ownership(&changed, types).unwrap_err(); + assert!( + errors.iter().any(|error| error.message + == "Resource ownership mixes foreign original/materialized archives"), + "a public Function clone cannot transplant its opaque archive: {errors:?}" + ); + } + _ => unreachable!(), + } + assert!( + EmittedResourceLayout::from_program(&changed, types, entry(&changed)).is_err(), + "fresh archive/type/CFG authentication, mutation {mutation}" + ); + assert!( + crate::emit::emit_for_triple( + &changed, + types, + HOST, + Some(entry(&changed)), + CodegenOptions { optimize: false } + ) + .is_err(), + "no object, mutation {mutation}" + ); + } +} + +#[test] +fn resource_carrier_source13_and14_keep_original_required_evaluation_and_bounds_control() { + assert!(WRAPPERS[12].1.contains("view values) != 1")); + assert!(WRAPPERS[12].1.contains("resource_probe.terminal_label()")); + assert!(WRAPPERS[13].1.contains("answer != 34")); + assert!(WRAPPERS[13].1.contains("absent_namespace_value != 17")); + for release in [false, true] { + let checked = checked_wrapper(WRAPPERS[13].1, release); + let program = lowered_wrapper(&checked); + assert_eq!( + named(&program, "absent_required_controls").return_type, + TypeInterner::INT64 + ); + crate::emit::emit_for_triple( + &program, + &checked.checked().interner, + HOST, + Some(entry(&program)), + CodegenOptions { optimize: release }, + ) + .unwrap(); + } + // Runtime status/message goldens for these two exact Sources remain in the + // full driver gate: wrong empty length reaches list.__remove_at bounds error; + // required controls return 34. Object tests do not claim native execution. +} + +#[test] +fn resource_carrier_observation_rows_distinguish_scalars_from_owned_strings_and_exact_adapter_imports() + { + for release in [false, true] { + let mut observed_scalar = false; + let mut observed_string = false; + let mut imported_adapt = false; + let mut imported_end = false; + for suffix in [WRAPPERS[6].1, WRAPPERS[10].1] { + let checked = checked_wrapper(suffix, release); + let program = lowered_wrapper(&checked); + let types = &checked.checked().interner; + let layout = + EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + let mut rows = Rows::new(layout.plan()).unwrap(); + rows.populate().unwrap(); + for function in &program.functions { + let Some(carriers) = layout + .plan() + .function(function.id) + .and_then(|plan| plan.carriers()) + else { + continue; + }; + for operation in carriers.operations() { + let ResourceCarrierOperationRole::Observe { + observation: jett_mir::ResourceCarrierObservation::Ordinary { ty, .. }, + .. + } = operation.role() + else { + continue; + }; + let record = + rows.carrier_record_ids[&(function.id.index(), operation.id().index())]; + let row = &rows.carrier_operations[usize::try_from(record).unwrap()]; + let tail = &row[row.len() - 12..]; + assert_eq!(u32::from_le_bytes(tail[..4].try_into().unwrap()), 4); + let shape = usize::try_from(u32::from_le_bytes(tail[4..8].try_into().unwrap())) + .unwrap(); + let clone = u32::from_le_bytes(tail[8..12].try_into().unwrap()); + match types.resolve(*ty) { + Type::Int64 => { + assert_eq!(rows.shapes[shape][0], 1); + assert_eq!(clone, 0, "a scalar is never a NativeValues handle"); + observed_scalar = true; + } + Type::String => { + assert_eq!(rows.shapes[shape][0], 4); + assert_eq!( + clone, 1, + "a String observation needs independent ownership" + ); + observed_string = true; + } + _ => {} + } + } + } + let artifact = crate::emit::emit_for_triple( + &program, + types, + HOST, + Some(entry(&program)), + CodegenOptions { optimize: release }, + ) + .unwrap(); + let object = object::File::parse(artifact.bytes.as_slice()).unwrap(); + for symbol in object.symbols().filter(|symbol| !symbol.is_definition()) { + match symbol.name().unwrap() { + "jett_rt_v1_resource_carrier_adapt" => imported_adapt = true, + "jett_rt_v1_resource_carrier_end" => imported_end = true, + "jett_rt_v1_resource_carrier_adapt_sum" + | "jett_rt_v1_resource_carrier_end_borrow" => { + panic!("native import must name the exact existing exported leaf") + } + _ => {} + } + } + } + assert!(observed_scalar && observed_string); + assert!(imported_adapt && imported_end); + } +} + +#[test] +fn resource_carrier_lexical_constructor_order_and_local_machine_qualification_emit_from_original_source() + { + let original = "TokenEnvelope(tokens: list(), fallback: none, marker: 17)"; + assert_eq!(WHOLE_SOURCE.matches(original).count(), 1); + let reordered = WHOLE_SOURCE.replacen( + original, + "TokenEnvelope(marker: 17, tokens: list(), fallback: none)", + 1, + ); + for release in [false, true] { + let checked = checked_wrapper_source(&reordered, WRAPPERS[6].1, release); + let program = lowered_wrapper(&checked); + let types = &checked.checked().interner; + let layout = EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + let plan = layout + .plan() + .function(named(&program, "absent_envelope").id) + .unwrap() + .carriers() + .unwrap(); + let storage_indices = plan + .operations() + .iter() + .filter_map(|operation| match operation.role() { + ResourceCarrierOperationRole::ConstructorChild { child, .. } => Some(child.index), + _ => None, + }) + .collect::>(); + assert_eq!( + storage_indices, + vec![2, 0, 1], + "declared storage order differs from lexical arrival 0,1,2" + ); + crate::emit::emit_for_triple( + &program, + types, + HOST, + Some(entry(&program)), + CodegenOptions { optimize: release }, + ) + .unwrap(); + + let suffix = format!( + "\nexport function source17_qualified_local() returns TokenState:\n TokenState value = TokenState(vacant, none, \"empty\")\n return value\n{}", + WRAPPERS[10].1 + ); + let checked = checked_wrapper(&suffix, release); + let program = lowered_wrapper(&checked); + let types = &checked.checked().interner; + let layout = EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + let plan = layout + .plan() + .function(named(&program, "source17_qualified_local").id) + .unwrap() + .carriers() + .unwrap(); + assert!(plan.operations().iter().any(|operation| matches!( + operation.role(), + ResourceCarrierOperationRole::QualifyMachine { .. } + ))); + crate::emit::emit_for_triple( + &program, + types, + HOST, + Some(entry(&program)), + CodegenOptions { optimize: release }, + ) + .unwrap(); + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_layout/carriers.rs b/crates/jett_codegen_cranelift/src/emit/resource_layout/carriers.rs new file mode 100644 index 00000000..ae1a078a --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_layout/carriers.rs @@ -0,0 +1,287 @@ +//! V3 typed graph projection from a fresh constructor-owned carrier plan. +//! Serialized identities are consistency metadata, never Source/Scope authority. +use super::{BYTE_LIMIT, LIMIT, count, pending, word, words}; +use crate::CodegenError; +use jett_mir::{ResourceCarrierFunctionPlan, ResourceCarrierNode, ResourceCarrierShapeId}; +use jett_types::{TypeId, TypeInterner}; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Debug, Default)] +pub(super) struct CarrierGraphRows { + rows: Vec>, + /// Canonical TypeIds are only keys within the already authenticated session. + ids: BTreeMap, + building: BTreeSet, +} + +impl CarrierGraphRows { + pub(super) fn is_empty(&self) -> bool { + self.rows.is_empty() + } + pub(super) fn len(&self) -> usize { + self.rows.len() + } + pub(super) fn rows(&self) -> &[Vec] { + &self.rows + } + pub(super) fn node_for_type(&self, ty: TypeId) -> Option { + self.ids.get(&ty.index()).copied() + } + + /// Project only nodes reachable from original carrier slots/formals/results. + /// Unrelated ordinary signature/never metadata does not acquire a carrier ABI. + pub(super) fn project( + &mut self, + plan: &ResourceCarrierFunctionPlan, + types: &TypeInterner, + id: ResourceCarrierShapeId, + ordinary_shape: &mut impl FnMut(TypeId) -> Result, + predicate: &mut impl FnMut(TypeId) -> Result, CodegenError>, + ) -> Result { + let shape = plan + .shape(id) + .ok_or_else(|| pending("carrier graph lost its exact node"))?; + let ty = shape.ty(); + if let Some(row) = self.ids.get(&ty.index()).copied() { + return Ok(row); + } + if self.rows.len() >= LIMIT || !self.building.insert(ty.index()) { + return Err(pending( + "carrier graph needs its exact finite recursive transport", + )); + } + let node = shape.node().clone(); + let mut data = Vec::new(); + let result = (|| { + match node { + ResourceCarrierNode::Ordinary => words(&mut data, &[1, ordinary_shape(ty)?]), + ResourceCarrierNode::Resource { kind } => words(&mut data, &[2, kind.id().index()]), + ResourceCarrierNode::Optional { payload } => { + let child = self.project(plan, types, payload, ordinary_shape, predicate)?; + words(&mut data, &[3, child]); + } + ResourceCarrierNode::Result { success, failure } => { + let ok = self.project(plan, types, success, ordinary_shape, predicate)?; + let fail = self.project(plan, types, failure, ordinary_shape, predicate)?; + words(&mut data, &[4, ok, fail]); + } + ResourceCarrierNode::List { element } => { + let child = self.project(plan, types, element, ordinary_shape, predicate)?; + words(&mut data, &[5, child]); + } + ResourceCarrierNode::Map { key, value } => { + let key = self.project(plan, types, key, ordinary_shape, predicate)?; + let value = self.project(plan, types, value, ordinary_shape, predicate)?; + words(&mut data, &[6, key, value]); + } + ResourceCarrierNode::Struct { + arguments, fields, .. + } => { + word(&mut data, 7); + identity(&mut data, ty, types)?; + self.arguments( + &mut data, + plan, + types, + &arguments, + ordinary_shape, + predicate, + )?; + self.fields(&mut data, plan, types, &fields, ordinary_shape, predicate)?; + } + ResourceCarrierNode::Enum { + arguments, + variants, + .. + } => { + word(&mut data, 8); + identity(&mut data, ty, types)?; + self.arguments( + &mut data, + plan, + types, + &arguments, + ordinary_shape, + predicate, + )?; + word(&mut data, count(variants.len())?); + for (index, variant) in variants.iter().enumerate() { + if variant.ordinal != index { + return Err(pending( + "carrier enum changed its original ordered variant", + )); + } + blob(&mut data, variant.name.as_bytes())?; + data.extend_from_slice(&variant.discriminant.to_le_bytes()); + self.fields( + &mut data, + plan, + types, + &variant.fields, + ordinary_shape, + predicate, + )?; + } + } + ResourceCarrierNode::Machine { + states, + transitions, + .. + } => { + word(&mut data, 9); + identity(&mut data, ty, types)?; + word(&mut data, count(states.len())?); + for (index, state) in states.iter().enumerate() { + if state.ordinal != index { + return Err(pending( + "carrier machine changed its original ordered state", + )); + } + blob(&mut data, state.name.as_bytes())?; + data.extend_from_slice( + &i64::try_from(index) + .map_err(|_| { + pending("carrier state ordinal exceeds checked tag width") + })? + .to_le_bytes(), + ); + self.fields( + &mut data, + plan, + types, + &state.fields, + ordinary_shape, + predicate, + )?; + } + word(&mut data, count(transitions.len())?); + for (source, target) in transitions { + words(&mut data, &[count(source)?, count(target)?]); + } + } + ResourceCarrierNode::MachineState { machine, state } => { + let owner = self.project(plan, types, machine, ordinary_shape, predicate)?; + words(&mut data, &[10, owner, count(state)?]); + } + ResourceCarrierNode::Refinement { base, .. } => { + let base = self.project(plan, types, base, ordinary_shape, predicate)?; + word(&mut data, 11); + identity(&mut data, ty, types)?; + word(&mut data, base); + // This callback selects the exact authenticated predicate + // association. A type name or numerically valid base is not proof. + let original = predicate(ty)?; + if original.is_empty() { + return Err(pending( + "carrier refinement lost its original predicate association", + )); + } + blob(&mut data, &original)?; + } + } + if data.len() > BYTE_LIMIT { + return Err(pending("carrier graph row exceeds wire capacity")); + } + Ok(()) + })(); + self.building.remove(&ty.index()); + result?; + let ordinal = count(self.rows.len())?; + self.rows.push(data); + self.ids.insert(ty.index(), ordinal); + Ok(ordinal) + } + + fn arguments( + &mut self, + out: &mut Vec, + plan: &ResourceCarrierFunctionPlan, + types: &TypeInterner, + arguments: &[ResourceCarrierShapeId], + ordinary: &mut impl FnMut(TypeId) -> Result, + predicate: &mut impl FnMut(TypeId) -> Result, CodegenError>, + ) -> Result<(), CodegenError> { + word(out, count(arguments.len())?); + for argument in arguments { + word( + out, + self.project(plan, types, *argument, ordinary, predicate)?, + ); + } + Ok(()) + } + + fn fields( + &mut self, + out: &mut Vec, + plan: &ResourceCarrierFunctionPlan, + types: &TypeInterner, + fields: &[jett_mir::ResourceCarrierField], + ordinary: &mut impl FnMut(TypeId) -> Result, + predicate: &mut impl FnMut(TypeId) -> Result, CodegenError>, + ) -> Result<(), CodegenError> { + word(out, count(fields.len())?); + for (index, field) in fields.iter().enumerate() { + if field.ordinal != index { + return Err(pending("carrier graph changed its original ordered field")); + } + blob(out, field.name.as_bytes())?; + word( + out, + self.project(plan, types, field.shape, ordinary, predicate)?, + ); + } + Ok(()) + } +} + +fn blob(out: &mut Vec, value: &[u8]) -> Result<(), CodegenError> { + if value.len() > LIMIT { + return Err(pending("carrier identity exceeds bounded wire blob domain")); + } + word(out, count(value.len())?); + out.extend_from_slice(value); + Ok(()) +} + +fn identity(out: &mut Vec, ty: TypeId, types: &TypeInterner) -> Result<(), CodegenError> { + // This stamp is emitted only from the authenticated graph. The runtime binds + // it to its installed node and issues constructor identities independently. + let mut bytes = ty.index().to_le_bytes().to_vec(); + bytes.extend_from_slice(types.type_name(ty).as_bytes()); + blob(out, &bytes) +} + +pub(super) fn append_extension( + out: &mut Vec, + graph: &CarrierGraphRows, + slots: &[Vec], + operations: &[Vec], +) -> Result<(), CodegenError> { + out.extend_from_slice(b"JTCAR003"); + words( + out, + &[ + count(graph.len())?, + count(slots.len())?, + count(operations.len())?, + 0, + ], + ); + for (index, node) in graph.rows().iter().enumerate() { + word(out, count(index)?); + out.extend_from_slice(node); + } + for (index, slot) in slots.iter().enumerate() { + word(out, count(index)?); + words(out, slot); + } + for (index, operation) in operations.iter().enumerate() { + word(out, count(index)?); + out.extend_from_slice(operation); + } + if out.len() > BYTE_LIMIT { + return Err(pending("carrier extension exceeds bounded wire capacity")); + } + Ok(()) +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_layout/connected_entry.jett b/crates/jett_codegen_cranelift/src/emit/resource_layout/connected_entry.jett new file mode 100644 index 00000000..ecccd22d --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_layout/connected_entry.jett @@ -0,0 +1,14 @@ +namespace app +function pass_owner(token: resource_probe.TestHandle) returns resource_probe.TestHandle: + return token +function lifecycle(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1) handle error: + return nothing + resource_probe.TestHandle moved = pass_owner(token) + result[int64, string] observed = resource_probe.borrow(view net, view moved) + resource_probe.close(moved) + return nothing +function main(net: Network) returns nothing: + lifecycle(view net) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_layout/returned_hooks_tests.rs b/crates/jett_codegen_cranelift/src/emit/resource_layout/returned_hooks_tests.rs new file mode 100644 index 00000000..ff4ad9ad --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_layout/returned_hooks_tests.rs @@ -0,0 +1,217 @@ +use super::*; +const FACTORY: &str = include_str!( + "../../../../jett_driver/tests/native_conformance/resource/27_returned_hook_factory.jett" +); +const BORROW: &str = include_str!( + "../../../../jett_driver/tests/native_conformance/resource/28_returned_hook_borrow.jett" +); +const IMMEDIATE: &str = include_str!( + "../../../../jett_driver/tests/native_conformance/resource/29_immediate_returned_hook_close.jett" +); +const ORDER: &str = include_str!( + "../../../../jett_driver/tests/native_conformance/resource/30_immediate_returned_hook_factory.jett" +); +const CALLEE_FAILURE: &str = include_str!( + "../../../../jett_driver/tests/native_conformance/resource/31_immediate_returned_hook_callee_failure.jett" +); +const ACTUAL_FAILURE: &str = include_str!( + "../../../../jett_driver/tests/native_conformance/resource/32_immediate_returned_hook_actual_failure.jett" +); + +const CLOSE: &str = include_str!( + "../../../../jett_driver/tests/native_conformance/resource/23_returned_hook_close.jett" +); +const ALIAS: &str = include_str!( + "../../../../jett_driver/tests/native_conformance/resource/24_returned_hook_alias.jett" +); +const RELAY: &str = include_str!( + "../../../../jett_driver/tests/native_conformance/resource/25_returned_hook_relay.jett" +); +const UNUSED: &str = include_str!( + "../../../../jett_driver/tests/native_conformance/resource/26_returned_hook_unused.jett" +); + +#[test] +fn resource_returned_hook_layout_joins_return_shape_and_distinct_indirect_close_target() { + for release in [false, true] { + for source in [ + CLOSE, + ALIAS, + RELAY, + UNUSED, + FACTORY, + BORROW, + IMMEDIATE, + ORDER, + CALLEE_FAILURE, + ACTUAL_FAILURE, + ] { + let checked = checked(source, release); + let types = &checked.checked().interner; + let program = lower_source(&checked); + let layout = + EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + let mut rows = Rows::new(layout.plan()).unwrap(); + rows.populate().unwrap(); + let mut indirect = 0; + for function in layout.plan().functions() { + if let Some(hook) = function.descriptor_return() { + let signature = rows.function_signature(function.function()).unwrap(); + let result = rows.signatures[signature as usize].result; + let hook_id = rows.hook(hook).unwrap(); + assert_eq!(rows.shapes[result as usize], [10, hook_id]); + assert_eq!(function.return_type(), hook.function_type()); + assert!(function.provisional_return().is_none()); + } + for operation in function.operations() { + let Some(hook) = operation.indirect_hook_target() else { + continue; + }; + indirect += 1; + let ordinal = layout + .operation(function.function(), operation.id()) + .unwrap(); + let row = &rows.operations[ordinal as usize].1; + assert_eq!(row.len(), 4); + assert_eq!(row[0], 15); + assert_eq!(&rows.hook_ids[row[1] as usize], hook); + assert_eq!(row[2], rows.hooks[row[1] as usize].2); + assert_ne!( + row[3], ordinal, + "descriptor row cannot replace its physical target" + ); + let target = &rows.operations[row[3] as usize]; + assert_eq!(target.0, operation.site()); + assert_eq!( + target.1[0], + match hook.recipe() { + ResourceKernelRecipe::NetworkFactory => 1, + ResourceKernelRecipe::NetworkBorrow => 6, + ResourceKernelRecipe::Finalize => 7, + } + ); + assert_eq!( + target.1[1], + layout + .frame(function.function(), operation.frame()) + .unwrap() + ); + assert_eq!(target.1[2], row[1]); + let Role::InvokeHook { source, .. } = operation.role() else { + panic!("hook role"); + }; + assert_eq!( + source.target, + jett_hir::CallTarget::Indirect { + signature_type: hook.function_type() + } + ); + assert_eq!(source.bridge, jett_hir::CallBridge::Direct); + } + } + assert_eq!(indirect, usize::from(source != UNUSED)); + } + } +} + +#[test] +fn resource_returned_hook_layout_refuses_current_producer_signature_target_and_source_frame_substitution() + { + for release in [false, true] { + let checked = checked(CLOSE, release); + let types = &checked.checked().interner; + let program = lower_source(&checked); + let selected = entry(&program); + let descriptor = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "descriptor") + .unwrap() + .id; + for mutation in 0..6 { + let mut changed = program.clone(); + match mutation { + 0 => { + let function = &mut changed.functions[descriptor.index() as usize]; + let value = function + .blocks + .iter_mut() + .find_map(|block| { + if let TerminatorKind::Return(Some(value)) = &mut block.terminator.kind + { + Some(value) + } else { + None + } + }) + .unwrap(); + value.kind = ExpressionKind::ResourceHookValue { + hook: program + .resource_manifest + .hooks() + .find(|hook| hook.recipe() == ResourceKernelRecipe::NetworkBorrow) + .unwrap() + .clone(), + }; + } + 1 => { + changed.functions[descriptor.index() as usize].return_type = TypeInterner::INT64 + } + 2 => { + let function = &mut changed.functions[descriptor.index() as usize]; + function.identity.declaration.name.push_str("Foreign"); + } + 3 => { + let function = &mut changed.functions[selected.index() as usize]; + let mut found = false; + for block in &mut function.blocks { + for statement in &mut block.statements { + if let StatementKind::Evaluate(Expression { + kind: + ExpressionKind::IndirectCall { + ownership: jett_hir::CallOwnership::Source(source), + .. + }, + .. + }) = &mut statement.kind + { + source.target = jett_hir::CallTarget::Indirect { + signature_type: TypeInterner::INT64, + }; + found = true; + } + } + } + assert!(found, "authentic original Indirect site must be mutated"); + } + 4 => { + changed.functions[selected.index() as usize].params[0].ty = TypeInterner::INT64 + } + 5 => { + let function = &mut changed.functions[descriptor.index() as usize]; + let value = function + .blocks + .iter_mut() + .find_map(|block| { + if let TerminatorKind::Return(Some(value)) = &mut block.terminator.kind + { + Some(value) + } else { + None + } + }) + .unwrap(); + value.kind = ExpressionKind::Nothing; + } + _ => unreachable!(), + } + assert!( + matches!( + EmittedResourceLayout::from_program(&changed, types, selected), + Err(CodegenError::InvalidMir(_)) + ), + "mutation {mutation}" + ); + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_layout/sum_views_tests.rs b/crates/jett_codegen_cranelift/src/emit/resource_layout/sum_views_tests.rs new file mode 100644 index 00000000..5224522f --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_layout/sum_views_tests.rs @@ -0,0 +1,84 @@ +use super::*; +#[path = "../../../../jett_driver/tests/native_conformance/resource_cases.rs"] +mod cases; + +#[test] +fn resource_borrowed_sum_layout_retains_exact_incoming_shape_and_projection_issuer() { + let cases = cases::CASES + .iter() + .filter(|case| { + case.name.starts_with("view_optional_") || case.name.starts_with("view_result_") + }) + .collect::>(); + assert_eq!(cases.len(), 12); + for release in [false, true] { + for case in &cases { + let checked = checked(case.source, release); + let program = lower_source(&checked); + let layout = EmittedResourceLayout::from_program( + &program, + &checked.checked().interner, + entry(&program), + ) + .unwrap_or_else(|error| panic!("{} release={release}: {error}", case.name)); + let mut rows = Rows::new(layout.plan()).unwrap(); + rows.populate().unwrap(); + let helper = layout + .plan() + .functions() + .iter() + .find(|function| { + function.identity().declaration.namespace == "app" + && function.identity().declaration.name.starts_with("observe_") + }) + .unwrap(); + let (parameter, header) = helper + .parameters() + .iter() + .enumerate() + .find(|(_, header)| header.name == "outcome") + .unwrap(); + assert_eq!(header.mode, jett_mir::ParamMode::View); + assert!(helper.owner_slots().iter().all(|slot| + !matches!(slot.storage(),custody::ResourceSlotStorage::Local { header: stored } if stored.id == header.local))); + let incoming = helper.loans().iter().find(|loan| + matches!(loan.source(),custody::ResourceLoanSource::IncomingViewFormal { parameter: original, .. } if original == parameter)).unwrap(); + assert!(!matches!( + incoming.shape(), + custody::ResourceShape::Plain { .. } + )); + let projections = helper + .operations() + .iter() + .filter(|operation| matches!(operation.role(), Role::ProjectSumView { .. })) + .collect::>(); + assert_eq!(projections.len(), 1, "{} release={release}", case.name); + let projection = projections[0]; + let Role::ProjectSumView { + source, + destination, + path, + .. + } = projection.role() + else { + unreachable!() + }; + assert_eq!(*source, incoming.id()); + assert!(matches!(helper.loans()[destination.index()].source(), + custody::ResourceLoanSource::ProjectedSumPayload { parent, path: selected, .. } + if parent == *source && selected == *path)); + let row_id = layout + .operation(helper.function(), projection.id()) + .unwrap(); + assert_eq!(rows.loan(helper, *destination).unwrap(), [3, row_id]); + let row = &rows.operations[row_id as usize].1; + assert_eq!(row[0], 23); + assert_eq!(row[2], 2); + assert_eq!(row[4], parameter as u32); + assert!(helper.operations().iter().any(|operation| + matches!(operation.role(),Role::ObserveSumView { source: observed, .. } if observed == source))); + assert!(helper.operations().iter().all(|operation| + !matches!(operation.role(),Role::EndSumBorrow { loan } if *loan == incoming.id()))); + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_layout/tests.rs b/crates/jett_codegen_cranelift/src/emit/resource_layout/tests.rs new file mode 100644 index 00000000..9ea0e0b7 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_layout/tests.rs @@ -0,0 +1,631 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; +use jett_parser::{ast::Item, parse}; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; +use jett_types::ResourceKernelRecipe; +use std::{collections::HashMap, sync::Arc}; +#[path = "carrier_tests.rs"] +mod carrier_tests; +#[path = "returned_hooks_tests.rs"] +mod returned_hooks; +#[path = "sum_views_tests.rs"] +mod sum_views; +const SUPPORT: &str = + include_str!("../../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); +fn checked(source: &str, release: bool) -> Arc { + let stdlib = FileId::new(10_000); + let project = FileId::new(0); + let mut parsed = parse(SUPPORT, stdlib); + let primary = parse(source, project); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(primary.errors.is_empty(), "{:?}", primary.errors); + let declaration = parsed + .module + .items + .iter() + .find_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .unwrap(); + parsed.module.items.extend(primary.module.items); + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: declaration, + member: member.into(), + recipe, + }) + .collect::>(); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (stdlib, SourceOrigin::Stdlib), + (project, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("{error:?}; {:?}", error.diagnostics())), + ) +} + +const SOURCE: &str = include_str!("connected_entry.jett"); +fn lower_source(checked: &Arc) -> Program { + let hir = jett_hir::lower_checked_resource_program(checked).unwrap(); + jett_mir::lower(&hir, &checked.checked().interner).unwrap() +} +fn entry(program: &Program) -> FunctionId { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "main" + }) + .unwrap() + .id +} +#[test] +fn resource_emitted_layout_joins_actual_driver_identity_and_unused_hooks_deterministically() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let program = lower_source(&checked); + let types = &checked.checked().interner; + let selected = entry(&program); + let first = EmittedResourceLayout::from_program(&program, types, selected).unwrap(); + let second = EmittedResourceLayout::from_program(&program, types, selected).unwrap(); + assert_eq!(first.entry(), second.entry()); + assert_eq!(first.bytes(), second.bytes()); + assert_eq!(&first.bytes()[..8], b"JTRSC001"); + assert_eq!( + u32::from_le_bytes(first.bytes()[8..12].try_into().unwrap()), + 2 + ); + assert_eq!( + u64::from_le_bytes(first.bytes()[16..24].try_into().unwrap()), + first.bytes().len() as u64 + ); + assert_eq!(first.entry().function, selected.index()); + let mut rows = Rows::new(first.plan()).unwrap(); + rows.populate().unwrap(); + assert_eq!(rows.hooks.len(), 3); + assert_eq!( + rows.hook_ids.len(), + program.resource_manifest.hooks().count() + ); + for function in first.plan().functions() { + let signature = first.function_signature(function.function()).unwrap(); + for frame in function.frames() { + assert_eq!( + rows.frames[first.frame(function.function(), frame.id()).unwrap() as usize] + .signature, + signature + ); + } + } + let wrong = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "pass_owner" + }) + .unwrap(); + let error = EmittedResourceLayout::from_program(&program, types, wrong.id).unwrap_err(); + match error { + CodegenError::InvalidMir(errors) => assert!(errors.iter().any(|error| { + error.span == wrong.span + && error.message + == "native Resource entry changed its closed Network/Nothing Source header" + })), + other => panic!("wrong selected entry used an unexpected refusal boundary: {other}"), + } + } +} +#[test] +fn resource_emitted_layout_keeps_call_site_result_destinations_separate_from_callee_return() { + let source = SOURCE.replace("result[int64, string] observed = resource_probe.borrow(view net, view moved)", "resource_probe.TestHandle final = pass_owner(moved)\n result[int64, string] observed = resource_probe.borrow(view net, view final)").replace("resource_probe.close(moved)", "resource_probe.close(final)"); + for release in [false, true] { + let checked = checked(&source, release); + let program = lower_source(&checked); + let layout = EmittedResourceLayout::from_program( + &program, + &checked.checked().interner, + entry(&program), + ) + .unwrap(); + let helper = layout + .plan() + .functions() + .iter() + .find(|function| { + function.identity().declaration.namespace == "app" + && function.identity().declaration.name == "pass_owner" + }) + .unwrap(); + let mut calls = Vec::new(); + for function in layout.plan().functions() { + for operation in function.operations() { + if let Role::InvokeSourceFunction { + function: target, + result: CallResult::Owned { slot }, + .. + } = operation.role() + && *target == helper.function() + { + calls.push(( + layout + .operation(function.function(), operation.id()) + .unwrap(), + layout.slot(function.function(), *slot).unwrap(), + helper.provisional_return().unwrap().id(), + )); + assert_ne!( + function.owner_slots()[slot.index()].frame(), + operation.frame() + ); + } + } + } + assert_eq!(calls.len(), 2); + assert_ne!(calls[0].0, calls[1].0); + assert_ne!(calls[0].1, calls[1].1); + assert_eq!(calls[0].2, calls[1].2); + let mut rows = Rows::new(layout.plan()).unwrap(); + rows.populate().unwrap(); + let scope = layout + .frame(helper.function(), helper.root_scope().id()) + .unwrap(); + for operation in helper.operations() { + if let Role::Transfer { + source, + destination, + } = operation.role() + && matches!( + helper.owner_slots()[destination.index()].storage(), + custody::ResourceSlotStorage::Return { .. } + ) + { + let id = layout.operation(helper.function(), operation.id()).unwrap(); + assert_eq!( + rows.operations[id as usize].1, + [ + 2, + scope, + layout.slot(helper.function(), *source).unwrap(), + layout.slot(helper.function(), *destination).unwrap() + ] + ); + assert_eq!(operation.frame(), helper.provisional_return().unwrap().id()); + assert_eq!( + helper.owner_slots()[destination.index()].frame(), + operation.frame() + ); + } + if let Role::CompleteReturnAfterCleanup { source } = operation.role() { + let id = layout.operation(helper.function(), operation.id()).unwrap(); + assert_eq!( + rows.operations[id as usize].1, + [18, scope, layout.slot(helper.function(), *source).unwrap()] + ); + assert_ne!( + layout.frame(helper.function(), operation.frame()).unwrap(), + scope + ); + assert_eq!( + helper.owner_slots()[source.index()].frame(), + helper.provisional_return().unwrap().id() + ); + } + } + } +} +#[test] +fn resource_emitted_layout_refuses_changed_current_source_graph_before_projecting_rows() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let program = lower_source(&checked); + let selected = entry(&program); + for mutation in 0..3 { + let mut changed = program.clone(); + let function = &mut changed.functions[selected.index() as usize]; + match mutation { + 0 => function.params[0].mode = jett_mir::ParamMode::View, + 1 => function.identity.declaration.name.push_str("Foreign"), + 2 => function.blocks[function.entry.index() as usize] + .statements + .clear(), + _ => unreachable!(), + } + let error = EmittedResourceLayout::from_program(&changed, types, selected).unwrap_err(); + assert!(matches!(error, CodegenError::InvalidMir(_)), "{error:?}"); + } + } +} + +#[test] +fn resource_manifest_accessor_emits_only_the_immutable_compiler_owned_projection() { + use object::{Object, ObjectSymbol}; + let checked = checked(SOURCE, false); + let program = lower_source(&checked); + let layout = + EmittedResourceLayout::from_program(&program, &checked.checked().interner, entry(&program)) + .unwrap(); + let target = isa::lookup(HOST) + .unwrap() + .finish(settings::Flags::new(settings::builder())) + .unwrap(); + let object = ObjectBuilder::new( + target, + "resource_manifest_accessor", + default_libcall_names(), + ) + .unwrap(); + let mut module = ObjectModule::new(object); + layout.define_accessor(&mut module).unwrap(); + assert!(matches!( + layout.define_accessor(&mut module), + Err(CodegenError::DuplicateSymbol(_)) + )); + let bytes = module.finish().emit().unwrap(); + let object = object::File::parse(bytes.as_slice()).unwrap(); + assert!(object.symbols().any(|symbol| symbol.is_definition() + && symbol.name().ok() == Some("jett_aot_resource_v1_manifest"))); + assert!(!object.symbols().any(|symbol| symbol.is_definition() + && symbol.name().ok() == Some(super::super::JETT_AOT_ENTRY_SYMBOL_V1))); + // This control emits metadata/accessor only: no body, leaf or provider execution. +} + +#[test] +fn resource_layout_replacement_uses_exact_rhs_tag12_and_reseat_has_no_row() { + const SOURCE: &str = include_str!("../resource_execution/replacement/mutable_assignment.jett"); + for release in [false, true] { + let checked = checked(SOURCE, release); + let program = lower_source(&checked); + let layout = EmittedResourceLayout::from_program( + &program, + &checked.checked().interner, + entry(&program), + ) + .unwrap(); + let mut rows = Rows::new(layout.plan()).unwrap(); + rows.populate().unwrap(); + let mut replacements = 0; + let mut reseats = 0; + for function in layout.plan().functions() { + for operation in function.operations() { + match operation.role() { + Role::Replace { + destination, + replacement, + old, + } => { + replacements += 1; + assert_eq!(*old, custody::ResourceOccupancy::Occupied); + assert_ne!(destination, replacement); + let ordinal = layout + .operation(function.function(), operation.id()) + .unwrap(); + assert_eq!( + rows.operations[ordinal as usize].1, + [ + 12, + layout + .frame(function.function(), operation.frame()) + .unwrap(), + layout.slot(function.function(), *destination).unwrap(), + layout.slot(function.function(), *replacement).unwrap(), + ] + ); + } + Role::SelfRebind { .. } => { + reseats += 1; + assert!( + layout + .operation(function.function(), operation.id()) + .is_none() + ); + } + _ => {} + } + } + } + assert!(replacements >= 2); + assert_eq!(reseats, 1); + } +} + +#[test] +fn resource_layout_sum_replacement_stays_a_separate_transport_refusal() { + const SOURCE: &str = r#"namespace app +export function main(net: Network) returns nothing: + use resource_probe + mutable optional[resource_probe.TestHandle] token = none + token = resource_probe.empty() + return nothing +"#; + for release in [false, true] { + let checked = checked(SOURCE, release); + let program = lower_source(&checked); + let error = EmittedResourceLayout::from_program( + &program, + &checked.checked().interner, + entry(&program), + ) + .unwrap_err(); + assert!( + error.to_string().contains( + "mutable replacement requires distinct occupied plain owners in the exact Scope" + ), + "{error:?}" + ); + } +} + +const STAGED_SOURCES: &[&str] = &[ + include_str!("../resource_execution/staged_call/09_later_handle_minimal.jett"), + include_str!("../resource_execution/staged_call/10_later_handle_original.jett"), + include_str!("../resource_execution/staged_call/11_later_handle_success.jett"), + include_str!("../resource_execution/staged_call/12_later_handle_written_view.jett"), + include_str!("../resource_execution/staged_call/13_first_handle_failure.jett"), + include_str!("../resource_execution/staged_call/14_first_handle_success.jett"), +]; + +#[test] +fn resource_staged_layout_keeps_complete_source_tuple_and_exact_activation_aliases() { + for release in [false, true] { + for &source in STAGED_SOURCES { + let checked = checked(source, release); + let program = lower_source(&checked); + let types = &checked.checked().interner; + let selected = entry(&program); + let layout = EmittedResourceLayout::from_program(&program, types, selected).unwrap(); + let repeated = EmittedResourceLayout::from_program(&program, types, selected).unwrap(); + assert_eq!(layout.bytes(), repeated.bytes()); + assert_eq!( + u32::from_le_bytes(layout.bytes()[8..12].try_into().unwrap()), + 2 + ); + let mut rows = Rows::new(layout.plan()).unwrap(); + rows.populate().unwrap(); + let mut regions = 0; + let mut aliases = 0; + let mut abandoned = 0; + for function in layout.plan().functions() { + for operation in function.operations() { + if let Role::BeginSourceFunction { + function: target, + formals, + evaluation_order, + operands, + .. + } = operation.role() + { + regions += 1; + assert_eq!(evaluation_order, &[1, 0, 2]); + assert_eq!(formals.len(), 3); + assert_eq!(operands.len(), 3); + let id = layout + .operation(function.function(), operation.id()) + .unwrap(); + let row = &rows.operations[id as usize].1; + assert_eq!(row[0], 16); + assert_eq!(row[2], target.index()); + assert_eq!(row[3], layout.function_signature(*target).unwrap()); + assert_eq!( + rows.frames[row[1] as usize].signature, + layout.function_signature(function.function()).unwrap() + ); + // Full formal metadata exists on Begin before any reached Stage/Invoke. + for operand in operands { + if let Operand::Borrowed { parameter, loan } = operand { + let record = &function.loans()[loan.index()]; + if let custody::ResourceLoanSource::Owner(_) = record.source() { + assert_eq!(record.parameter(), Some(*parameter)); + let mut prepared = function.operations().iter().filter(|candidate| + matches!(candidate.role(), Role::PrepareSourceBorrow { loan: expected, parameter: p } if expected == loan && p == parameter)); + let prepared = prepared.next().unwrap(); + let borrow_id = layout + .operation(function.function(), prepared.id()) + .unwrap(); + assert_eq!(rows.operations[borrow_id as usize].1[0], 3); + assert_eq!(prepared.site(), operation.site()); + } + } + } + } + if let Some(prepared) = staged_alias(function, operation).unwrap() { + aliases += 1; + assert_eq!( + layout.operation(function.function(), operation.id()), + layout.operation(function.function(), prepared.id()) + ); + } + if matches!(operation.role(), Role::StageSourceActual { .. }) { + assert!( + layout + .operation(function.function(), operation.id()) + .is_none() + ); + } + if matches!( + operation.role(), + Role::Complete { + outcome: custody::ResourceCompletion::Abort + } + ) && function.operations().iter().any(|begin| { + begin.frame() == operation.frame() + && matches!(begin.role(), Role::BeginSourceFunction { .. }) + }) { + abandoned += 1; + let id = layout + .operation(function.function(), operation.id()) + .unwrap(); + assert_eq!( + rows.operations[id as usize].1, + [ + 13, + layout + .frame(function.function(), operation.frame()) + .unwrap() + ] + ); + } + } + } + assert_eq!(regions, 1); + assert!(aliases >= 2); + assert!(abandoned >= 1); + } + } +} + +#[test] +fn resource_staged_layout_refuses_edited_and_copied_public_nodes_before_rows() { + for release in [false, true] { + let checked = checked(STAGED_SOURCES[0], release); + let original = lower_source(&checked); + let selected = entry(&original); + let stages = original + .functions + .iter() + .flat_map(|function| { + function.blocks.iter().flat_map(move |block| { + block + .statements + .iter() + .enumerate() + .filter_map(move |(index, statement)| { + matches!( + statement.kind, + StatementKind::ResourceCall( + custody::ResourceCallNode::Stage { .. } + ) + ) + .then_some((function.id, block.id, index)) + }) + }) + }) + .collect::>(); + assert!(stages.len() >= 3); + for mutation in 0..5 { + let mut changed = original.clone(); + let (function, block, index) = stages[0]; + let current = &mut changed.functions[function.index() as usize]; + match mutation { + 0 => { + let StatementKind::ResourceCall(custody::ResourceCallNode::Stage { + source_index, + .. + }) = &mut current.blocks[block.index() as usize].statements[index].kind + else { + unreachable!() + }; + *source_index += 1; + } + 1 => { + let StatementKind::ResourceCall(custody::ResourceCallNode::Stage { + parameter, + .. + }) = &mut current.blocks[block.index() as usize].statements[index].kind + else { + unreachable!() + }; + *parameter = 0; + } + 2 => { + let StatementKind::ResourceCall(custody::ResourceCallNode::Stage { + value, .. + }) = &mut current.blocks[block.index() as usize].statements[index].kind + else { + unreachable!() + }; + value.ty = TypeInterner::INT64; + } + 3 => { + let copied = current.blocks[block.index() as usize].statements[index].clone(); + current.blocks[block.index() as usize] + .statements + .insert(index, copied); + } + 4 => { + let copied = current.blocks[block.index() as usize].statements[index].clone(); + current.blocks[current.entry.index() as usize] + .statements + .push(copied); + } + _ => unreachable!(), + } + assert!( + matches!( + EmittedResourceLayout::from_program( + &changed, + &checked.checked().interner, + selected + ), + Err(CodegenError::InvalidMir(_)) + ), + "mutation {mutation}" + ); + } + } +} + +#[test] +fn resource_staged_sum_rows_preserve_scope_storage_and_active_execution() { + for release in [false, true] { + for &source in &STAGED_SOURCES[4..] { + let checked = checked(source, release); + let program = lower_source(&checked); + let selected = entry(&program); + let layout = EmittedResourceLayout::from_program( + &program, + &checked.checked().interner, + selected, + ) + .unwrap(); + let flow = layout.plan().function(selected).unwrap(); + let active = flow + .operations() + .iter() + .find(|operation| matches!(operation.role(), Role::BeginSourceFunction { .. })) + .unwrap() + .frame(); + let scope = flow.root_scope().id(); + let mut rows = Rows::new(layout.plan()).unwrap(); + rows.populate().unwrap(); + let mut extractions = 0; + for operation in flow.operations() { + let source = match operation.role() { + Role::SumTake { + source, + destination, + .. + } => { + assert_eq!(flow.owner_slots()[destination.index()].frame(), scope); + *source + } + Role::TakeFailureCompanion { source, .. } => *source, + _ => continue, + }; + extractions += 1; + assert_eq!(flow.owner_slots()[source.index()].frame(), scope); + assert_eq!(operation.frame(), active); + let ordinal = layout.operation(selected, operation.id()).unwrap(); + assert_eq!( + rows.operations[ordinal as usize].1[1], + layout.frame(selected, active).unwrap() + ); + } + assert_eq!(extractions, 2); + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/values.rs b/crates/jett_codegen_cranelift/src/emit/values.rs index 7754d8a2..5df9b61b 100644 --- a/crates/jett_codegen_cranelift/src/emit/values.rs +++ b/crates/jett_codegen_cranelift/src/emit/values.rs @@ -1,5 +1,8 @@ use super::*; -use jett_hir::{InterfaceFunctionAdapter, IntrinsicId, MapEntry, StringSegment}; +use jett_hir::{ + InterfaceFunctionAdapter, IntrinsicId, MapEntry, ReflectedFieldAction, + ReflectedFieldValidation, StringSegment, +}; use jett_mir::move_values::is_erased_interface; use jett_runtime::native_abi::values::interface_conversion::NativeInterfaceConversion; use jett_types::BitfieldFieldKind; @@ -174,6 +177,46 @@ struct ReflectedFieldConversion { } impl Translator<'_, '_> { + pub(super) fn preflight_interface_dispatch_argument( + &mut self, + expression: &Expression, + ) -> Result<(), CodegenError> { + match &expression.kind { + ExpressionKind::View(inner) | ExpressionKind::Clone(inner) => { + self.preflight_interface_dispatch_argument(inner) + } + ExpressionKind::InterfaceCoerce { value, adapters } => { + let conversion = + interface_conversion(self.types, value.ty, expression.ty, adapters) + .ok_or_else(|| { + self.unsupported( + expression.span, + "interface dispatch conversion preflight", + ) + })?; + if !conversion.requires_owner_validation() { + return Ok(()); + } + // Generated dispatch operands are the already evaluated formal + // values. Observe them without taking custody or converting. + let input = self.argument(value, true)?; + let input = self.scalar(input, value.span)?; + let (mut bytes, functions) = conversion.encode_with_function_offsets(); + for (offset, _) in &functions { + bytes[*offset..*offset + 8].fill(0); + } + let (layout, length) = self.static_data_with_functions(&bytes, &functions)?; + self.leaf( + NativeLeaf::InterfaceValidate, + &[input, layout, length], + true, + )?; + Ok(()) + } + _ => Ok(()), + } + } + pub(super) fn interface_coerce( &mut self, expression: &Expression, @@ -281,9 +324,15 @@ impl Translator<'_, '_> { pub(super) fn check_failure(&mut self) -> Result<(), CodegenError> { let status = self.leaf(NativeLeaf::Status, &[], false)?; let success = self.builder.create_block(); - self.builder - .ins() - .brif(status, self.failure_block, &[], success, &[]); + if let Some(resource) = self.resource { + self.builder + .ins() + .brif(status, resource.failure, &[status.into()], success, &[]); + } else { + self.builder + .ins() + .brif(status, self.failure_block, &[], success, &[]); + } self.builder.switch_to_block(success); Ok(()) } @@ -2169,9 +2218,30 @@ impl Translator<'_, '_> { index: Value, result_type: TypeId, conversion: Option, + callable: Option, span: Span, ) -> Result { let bits = self.leaf(NativeLeaf::StructField, &[owner, index], true)?; + if let Some(address) = callable { + // The selector has completed its original metadata, schema and + // pending checks. Both leaves borrow the stored descriptor and + // create exactly one owned result; neither invokes its body. + let adapt = self.builder.create_block(); + let exact = self.builder.create_block(); + let joined = self.builder.create_block(); + self.builder.append_block_param(joined, ir::types::I64); + let changed = self.builder.ins().icmp_imm(IntCC::NotEqual, address, 0); + self.builder.ins().brif(changed, adapt, &[], exact, &[]); + self.builder.switch_to_block(adapt); + let adapted = self.leaf(NativeLeaf::FunctionAdapter, &[bits, address], true)?; + self.builder.ins().jump(joined, &[adapted.into()]); + self.builder.switch_to_block(exact); + let cloned = self.leaf(NativeLeaf::StructClone, &[bits], true)?; + self.builder.ins().jump(joined, &[cloned.into()]); + self.builder.switch_to_block(joined); + let owned = self.builder.block_params(joined)[0]; + return self.own(owned); + } if let Some(conversion) = conversion { // InterfaceConvert borrows and clones the stored owner. A nominal // interface-backed refinement needs its own outer identity box; @@ -2203,11 +2273,114 @@ impl Translator<'_, '_> { } } + fn select_reflected_callable( + &mut self, + selected: &mut Option, + matches: Value, + ordinal: usize, + actual: TypeId, + requested: TypeId, + validation: Option<&ReflectedFieldValidation>, + span: Span, + ) -> Result { + let Some(ReflectedFieldValidation::ReadCallable(plans)) = validation else { + return Ok(false); + }; + let row = plans.get(ordinal).ok_or_else(|| { + contract_error( + self.symbol, + span, + "reflected callable field ordinal is absent", + ) + })?; + if row.source_type != actual || row.requested_type != requested { + return Err(contract_error( + self.symbol, + span, + "reflected callable field tuple changed", + )); + } + let ReflectedFieldAction::CallableReturn(plan) = &row.action else { + return Ok(false); + }; + plan.validate_types(self.types) + .map_err(|message| contract_error(self.symbol, span, message))?; + if !plan.validate_original() || plan.is_materialized() { + return Err(contract_error( + self.symbol, + span, + "reflected getter has no exact callable constructor proof", + )); + } + let declared = self.declarations.get(plan.function()).ok_or_else(|| { + contract_error( + self.symbol, + span, + "reflected callable adapter is not reachable", + ) + })?; + let reference = self + .module + .declare_func_in_func(declared.native_id, self.builder.func); + let pointer_type = self.module.target_config().pointer_type(); + let address = self.builder.ins().func_addr(pointer_type, reference); + let address = if pointer_type == ir::types::I64 { + address + } else { + self.builder.ins().uextend(ir::types::I64, address) + }; + let previous = selected.unwrap_or_else(|| self.builder.ins().iconst(ir::types::I64, 0)); + *selected = Some(self.builder.ins().select(matches, address, previous)); + Ok(true) + } + + pub(super) fn reflected_value_ready( + &mut self, + value: LoweredValue, + ty: TypeId, + span: Span, + ) -> Result<(), CodegenError> { + let message = "reflected callback return: expected a ready value"; + if is_task_scalar(self.types, ty)? { + return self.reject_pending_scalars(&[value], message); + } + let kind = match scalar_kind(self.types, ty, "reflected callback return")? { + ScalarKind::Nothing => { + // Unit's I64 value is already its Pending depth, unlike the + // separate depth lane used by numeric and boolean scalars. + let depth = self.scalar(value, span)?; + let zero = self.builder.ins().iconst(ir::types::I64, 0); + let (pointer, length) = self.static_bytes(message)?; + self.leaf( + NativeLeaf::RejectPendingScalars, + &[depth, zero, zero, pointer, length], + true, + )?; + return Ok(()); + } + ScalarKind::String => 0, + ScalarKind::Bytes => 1, + ScalarKind::List => 2, + ScalarKind::Set => 5, + ScalarKind::Map => 6, + ScalarKind::Sum => 7, + ScalarKind::Struct + | ScalarKind::Enum + | ScalarKind::Machine + | ScalarKind::Bitfield + | ScalarKind::Function + | ScalarKind::Construction => 4, + _ => return Err(self.unsupported(span, "reflected callback return readiness carrier")), + }; + self.reject_pending_handle(value, kind, message, span) + } + pub(super) fn intrinsic( &mut self, id: IntrinsicId, type_arguments: &[TypeId], reflection_arguments: &[ReflectionTypeInfo], + field_validation: Option<&ReflectedFieldValidation>, args: &[Expression], order: &[usize], result_type: TypeId, @@ -2477,8 +2650,10 @@ impl Translator<'_, '_> { let mut expected = zero; let mut compatible = zero; let mut conversion = None; + let mut callable = None; for (index, field_ty) in field_types.iter().enumerate() { let candidate = self.scalar(evaluated[index + 2], span)?; + let ordinal = index; let index = i64::try_from(index) .map_err(|_| self.unsupported(span, "reflected field index"))?; let matches = self @@ -2486,7 +2661,16 @@ impl Translator<'_, '_> { .ins() .icmp_imm(IntCC::Equal, requested_index, index); expected = self.builder.ins().select(matches, candidate, expected); - if reflected_field_types_compatible(self.types, *field_ty, result_type) { + let adapted = self.select_reflected_callable( + &mut callable, + matches, + ordinal, + *field_ty, + result_type, + field_validation, + span, + )?; + if adapted || reflected_field_types_compatible(self.types, *field_ty, result_type) { compatible = self.builder.ins().select(matches, candidate, compatible); } self.select_reflected_field_conversion( @@ -2516,7 +2700,14 @@ impl Translator<'_, '_> { )?; let owner = self.scalar(evaluated[0], span)?; self.check_reflected_owner_pending(owner, args[0].ty, &owner_name, 2)?; - return self.reflected_field_value(owner, checked_index, result_type, conversion, span); + return self.reflected_field_value( + owner, + checked_index, + result_type, + conversion, + callable, + span, + ); } if matches!( id, @@ -2610,9 +2801,11 @@ impl Translator<'_, '_> { let mut expected = zero; let mut compatible = zero; let mut conversion = None; + let mut callable = None; let mut argument_index = 2; for (variant_index, fields) in field_groups.iter().enumerate() { for (field_index, field_ty) in fields.iter().enumerate() { + let ordinal = argument_index - 2; let candidate = self.scalar(evaluated[argument_index], span)?; argument_index += 1; let variant_index = i64::try_from(variant_index) @@ -2629,7 +2822,18 @@ impl Translator<'_, '_> { .icmp_imm(IntCC::Equal, requested_index, field_index); let matches = self.builder.ins().band(variant_matches, field_matches); expected = self.builder.ins().select(matches, candidate, expected); - if reflected_field_types_compatible(self.types, *field_ty, result_type) { + let adapted = self.select_reflected_callable( + &mut callable, + matches, + ordinal, + *field_ty, + result_type, + field_validation, + span, + )?; + if adapted + || reflected_field_types_compatible(self.types, *field_ty, result_type) + { compatible = self.builder.ins().select(matches, candidate, compatible); } self.select_reflected_field_conversion( @@ -2665,7 +2869,14 @@ impl Translator<'_, '_> { )?; let one = self.builder.ins().iconst(ir::types::I64, 1); let slot = self.builder.ins().iadd(checked_index, one); - return self.reflected_field_value(owner, slot, result_type, conversion, span); + return self.reflected_field_value( + owner, + slot, + result_type, + conversion, + callable, + span, + ); } if id == IntrinsicId::Range { self.check_index_count_arguments(id, &evaluated, span)?; diff --git a/crates/jett_codegen_cranelift/src/reachability.rs b/crates/jett_codegen_cranelift/src/reachability.rs index 212f24e2..58cde96d 100644 --- a/crates/jett_codegen_cranelift/src/reachability.rs +++ b/crates/jett_codegen_cranelift/src/reachability.rs @@ -9,14 +9,35 @@ use crate::CodegenError; /// declaration and object-symbol order stay deterministic. #[cfg(test)] fn reachable_function_ids(program: &Program) -> Result, CodegenError> { - reachable_functions(program, None) + reachable_functions(program, None, &[], &[]) } pub(crate) fn reachable_function_ids_with_types( program: &Program, types: &jett_types::TypeInterner, ) -> Result, CodegenError> { - reachable_functions(program, Some(types)) + reachable_functions(program, Some(types), &[], &[]) +} + +/// Constructor-selected families are executable roots even when ordinary +/// project reachability did not use their descriptor-producing bodies. +pub(crate) fn reachable_resource_function_ids( + plan: &jett_mir::ResourceOwnershipPlan<'_>, +) -> Result, CodegenError> { + let mut roots = plan + .functions() + .iter() + .map(|function| function.function()) + .collect::>(); + if let Some(entry) = plan.entry_scope() { + roots.push(entry.function()); + } + reachable_functions( + plan.program(), + Some(plan.types()), + &roots, + plan.required_only_function_ids(), + ) } struct References<'a> { @@ -33,6 +54,8 @@ impl References<'_> { fn reachable_functions( program: &Program, types: Option<&jett_types::TypeInterner>, + additional_roots: &[FunctionId], + required_only: &[FunctionId], ) -> Result, CodegenError> { let mut reachable = vec![false; program.functions.len()]; let mut pending = Vec::new(); @@ -42,6 +65,7 @@ fn reachable_functions( && function.identity.declaration.kind != hir::DeclarationKind::RefinementPredicate && function.debug_kind != hir::FunctionDebugKind::Inline && !uninhabited_specialization(function) + && !required_only.contains(&function.id) { mark_reachable( program, @@ -54,6 +78,27 @@ fn reachable_functions( } } + for &id in additional_roots { + if required_only.contains(&id) { + return Err(CodegenError::Backend( + "required-only helper cannot become an executable Resource root".into(), + )); + } + let function = resolve_function(program, id).ok_or_else(|| { + CodegenError::Backend( + "authenticated Resource root is absent from the current MIR function table".into(), + ) + })?; + mark_reachable( + program, + id, + function.span, + function, + &mut reachable, + &mut pending, + )?; + } + while let Some((function_id, function_span)) = pending.pop() { let function = resolve_function(program, function_id) .ok_or_else(|| invalid_target(program.functions.first(), function_span, function_id))?; @@ -68,6 +113,14 @@ fn reachable_functions( collect_function_references(function, &mut references); } for (target, span) in references.functions { + if required_only.contains(&target) { + return Err(CodegenError::InvalidMirContract { + function: function.identity.declaration.name.clone(), + span, + message: "runtime reference reaches a helper authenticated only for required evaluation" + .into(), + }); + } mark_reachable( program, target, @@ -156,6 +209,25 @@ fn collect_function_references(function: &Function, references: &mut References< for block in &function.blocks { for statement in &block.statements { match &statement.kind { + StatementKind::ResourceCall(jett_mir::ResourceCallNode::Stage { + value, .. + }) + | StatementKind::ResourceConstructor(jett_mir::ResourceConstructorNode::Stage { + value, + .. + }) => { + collect_expression_references(value, references); + } + StatementKind::ResourceCall( + jett_mir::ResourceCallNode::Begin { region } + | jett_mir::ResourceCallNode::Invoke { region, .. }, + ) => { + if let Some(region) = function.resource_call_region(*region) { + references.push((region.function(), statement.span)); + } + } + StatementKind::ResourceCall(jett_mir::ResourceCallNode::End { .. }) => {} + StatementKind::ResourceConstructor(_) => {} StatementKind::Let { value, .. } | StatementKind::BeginCallView { value, .. } | StatementKind::CheckRefinement { call: value, .. } @@ -184,7 +256,9 @@ fn collect_function_references(function: &Function, references: &mut References< | StatementKind::ReplaceCallOwnerGeneration { .. } | StatementKind::CloseCallOwnerGeneration { .. } | StatementKind::EndCallView { .. } + | StatementKind::ResourceLexicalExit(_) | StatementKind::ReflectedContainerReady { .. } + | StatementKind::ReflectedValueReady { .. } | StatementKind::IterationBorrow { .. } | StatementKind::SequenceLength { .. } | StatementKind::SequenceGet { .. } @@ -292,6 +366,12 @@ fn collect_hir_block_references(block: &hir::Block, references: &mut References< /// to traverse it. fn collect_expression_references(expression: &Expression, references: &mut References<'_>) { match &expression.kind { + ExpressionKind::ResourceHookValue { .. } => {} + ExpressionKind::ResourceInvoke { args, .. } => { + for argument in args { + collect_expression_references(argument, references); + } + } ExpressionKind::FunctionRef(function) => references.push((*function, expression.span)), ExpressionKind::FunctionAdapter { value, function } => { references.push((*function, expression.span)); @@ -353,11 +433,19 @@ fn collect_expression_references(expression: &Expression, references: &mut Refer type_arguments: _, reflection_arguments: _, refinement_predicates: _, - field_validation: _, + field_validation, args, evaluation_order: _, .. } => { + if let Some(jett_hir::ReflectedFieldValidation::ReadCallable(plans)) = field_validation + { + for plan in plans { + if let jett_hir::ReflectedFieldAction::CallableReturn(callable) = &plan.action { + references.push((callable.function(), expression.span)); + } + } + } for argument in args { collect_expression_references(argument, references); } diff --git a/crates/jett_codegen_cranelift/src/verify.rs b/crates/jett_codegen_cranelift/src/verify.rs index b68bd9fd..c5196bd7 100644 --- a/crates/jett_codegen_cranelift/src/verify.rs +++ b/crates/jett_codegen_cranelift/src/verify.rs @@ -1,4 +1,5 @@ use std::{cell::RefCell, collections::HashSet}; +mod carriers; use jett_common::{SourceOrigin, Span}; use jett_hir::{ @@ -194,8 +195,24 @@ pub(crate) fn descriptor_only_function(function: &Function) -> bool { pub(crate) fn verify_descriptor_bodies( program: &Program, types: &TypeInterner, +) -> Result<(), CodegenError> { + verify_descriptor_bodies_inner(program, types, None) +} + +pub(crate) fn verify_resource_descriptor_bodies( + plan: &jett_mir::ResourceOwnershipPlan<'_>, +) -> Result<(), CodegenError> { + verify_descriptor_bodies_inner(plan.program(), plan.types(), Some(plan)) +} + +fn verify_descriptor_bodies_inner<'a>( + program: &'a Program, + types: &'a TypeInterner, + resource: Option<&'a jett_mir::ResourceOwnershipPlan<'a>>, ) -> Result<(), CodegenError> { jett_mir::validate_call_ownership(program, types).map_err(CodegenError::InvalidMir)?; + jett_mir::validate_reflected_callable_plans(program, types) + .map_err(CodegenError::InvalidMir)?; // Shared inline tables and impossible sum arms contain valid Never metadata // that need not allocate a native frame slot. Validate original type shapes // without demanding a carrier for an absent function result; every surviving @@ -203,7 +220,9 @@ pub(crate) fn verify_descriptor_bodies( for function in &program.functions { for local in &function.locals { for (ty, role) in [(local.ty, "local"), (local.debug_ty, "debug local")] { - if !exact_uninhabited_metadata_type(types, ty) { + if !resource.is_some_and(|plan| plan.type_requires_custody(ty)) + && !exact_uninhabited_metadata_type(types, ty) + { scalar_kind_inner( types, ty, @@ -267,6 +286,7 @@ pub(crate) fn verify_descriptor_bodies( verified: &verified, phase: VerifierPhase::DescriptorMetadata, metadata_functions: RefCell::new(HashSet::new()), + resource, }; for function in &program.functions { if descriptor_only_function(function) { @@ -322,6 +342,7 @@ pub(crate) fn verify_callable_expression_for_test( verified: &verified, phase: VerifierPhase::CallableNative, metadata_functions: RefCell::new(HashSet::new()), + resource: None, }; verifier.expression(function, expression) } @@ -331,6 +352,8 @@ pub(crate) fn verify_program( types: &TypeInterner, ) -> Result { jett_mir::validate_call_ownership(program, types).map_err(CodegenError::InvalidMir)?; + jett_mir::validate_reflected_callable_plans(program, types) + .map_err(CodegenError::InvalidMir)?; // Reachability may omit lifted bodies, but malformed source metadata must // not disappear with an unused descriptor. @@ -398,12 +421,102 @@ pub(crate) fn verify_program( VerifierPhase::CallableNative }, metadata_functions: RefCell::new(HashSet::new()), + resource: None, }; verifier.function(function)?; } Ok(verified) } +pub(crate) fn verify_resource_program( + plan: &jett_mir::ResourceOwnershipPlan<'_>, +) -> Result { + let program = plan.program(); + let types = plan.types(); + // Reachability may omit lifted bodies, but malformed source metadata must + // not disappear with an unused descriptor. + for function in &program.functions { + super::emit::debug::function_label(function).map_err(|message| { + CodegenError::InvalidMirContract { + function: function.identity.declaration.name.clone(), + span: function.span, + message: message.into(), + } + })?; + } + + for (&owner, &method) in &program.equality_methods { + let (_, target) = function_by_id(program, method)?; + if owner.index() as usize >= types.len() + || !matches!(types.resolve(owner), Type::Struct(_)) + || target.capture_count != 0 + || target.params.len() != 2 + || target.return_type != TypeInterner::BOOL + || target.params.iter().any(|parameter| { + parameter.ty != owner || parameter.mode != jett_mir::ParamMode::View + }) + { + return Err(CodegenError::InvalidMirContract { + function: target.identity.declaration.name.clone(), + span: target.span, + message: "enum equality target must have two exact struct views and return bool" + .into(), + }); + } + } + let selected = crate::reachability::reachable_resource_function_ids(plan)?; + let mut functions = Vec::new(); + let mut by_mir_index = vec![None; program.functions.len()]; + let mut symbols = HashSet::new(); + for id in selected { + let (index, function) = function_by_id(program, id)?; + let symbol = symbol_name(&function.identity, types)?; + if !symbols.insert(symbol.clone()) { + return Err(CodegenError::DuplicateSymbol(symbol)); + } + by_mir_index[index] = Some(functions.len()); + functions.push(VerifiedFunction { mir_id: id, symbol }); + } + let verified = VerifiedProgram { + functions, + by_mir_index, + }; + + for function in &program.functions { + // The plan's archive authenticates all original Resource type graphs; + // unrelated ordinary metadata keeps the previous complete type visitor. + for local in &function.locals { + for ty in [local.ty, local.debug_ty] { + if !plan.type_requires_custody(ty) && !exact_uninhabited_metadata_type(types, ty) { + scalar_kind_inner( + types, + ty, + "original ordinary companion local".into(), + &mut HashSet::new(), + TypeValidationPhase::OriginalMetadata, + )?; + } + } + } + if verified.get(function.id).is_some() { + let verifier = Verifier { + program, + types, + verified: &verified, + phase: if descriptor_only_function(function) { + VerifierPhase::DescriptorMetadata + } else { + VerifierPhase::CallableNative + }, + metadata_functions: RefCell::new(HashSet::new()), + resource: Some(plan), + }; + verifier.function(function)?; + } + } + Ok(verified) +} + fn function_by_id(program: &Program, id: FunctionId) -> Result<(usize, &Function), CodegenError> { let index = usize::try_from(id.index()).map_err(|_| { CodegenError::Backend("reached MIR function index does not fit this host".to_string()) @@ -716,6 +829,7 @@ struct Verifier<'a> { phase: VerifierPhase, // This is an original-body integrity traversal, not callable reachability. metadata_functions: RefCell>, + resource: Option<&'a jett_mir::ResourceOwnershipPlan<'a>>, } impl Verifier<'_> { @@ -754,18 +868,32 @@ impl Verifier<'_> { let name = self.function_name(function); for param in &function.params { + if self.resource.is_some_and(|plan| { + plan.function(function.id).is_some() && plan.type_requires_custody(param.ty) + }) { + continue; + } self.value_kind( function, param.ty, format!("parameter `{}` of `{name}`", param.name), )?; } - self.value_kind( - function, - function.return_type, - format!("return type of `{name}`"), - )?; + if !(self.resource.is_some_and(|plan| { + plan.function(function.id).is_some() && plan.type_requires_custody(function.return_type) + })) { + self.value_kind( + function, + function.return_type, + format!("return type of `{name}`"), + )?; + } for local in &function.locals { + if self.resource.is_some_and(|plan| { + plan.function(function.id).is_some() && plan.type_requires_custody(local.ty) + }) { + continue; + } self.value_kind( function, local.ty, @@ -780,8 +908,8 @@ impl Verifier<'_> { } } for block in &function.blocks { - for statement in &block.statements { - self.statement(function, statement)?; + for (index, statement) in block.statements.iter().enumerate() { + self.statement(function, block.id, index, statement)?; } self.terminator(function, &block.terminator)?; } @@ -790,8 +918,16 @@ impl Verifier<'_> { // machine-code frame. Their caller still validates capture evaluation, // initialization, ownership and cleanup normally. if self.phase == VerifierPhase::CallableNative { - jett_mir::move_values::MoveValuePlan::analyze(self.program, function, self.types) - .map_err(|message| self.contract_error(function, function.span, message))?; + if let Some(plan) = self + .resource + .filter(|plan| plan.function(function.id).is_some()) + { + jett_mir::ResourceCompanionPlan::analyze(plan, function.id) + .map_err(|message| self.contract_error(function, function.span, message))?; + } else { + jett_mir::move_values::MoveValuePlan::analyze(self.program, function, self.types) + .map_err(|message| self.contract_error(function, function.span, message))?; + } } Ok(()) } @@ -854,8 +990,224 @@ impl Verifier<'_> { Ok(()) } - fn statement(&self, function: &Function, statement: &Statement) -> Result<(), CodegenError> { + fn statement( + &self, + function: &Function, + block: jett_mir::BlockId, + index: usize, + statement: &Statement, + ) -> Result<(), CodegenError> { + super::emit::ordinary_borrowed_sums::validate_statement( + function, self.types, block, index, statement, + ) + .map_err(|message| self.contract_error(function, statement.span, message))?; match &statement.kind { + StatementKind::ResourceConstructor(node) => { + use jett_mir::ResourceConstructorNode as Node; + let original = function + .resource_constructor(node.constructor()) + .map_err(|message| self.contract_error(function, statement.span, message))?; + let family = self + .resource + .and_then(|p| p.function(function.id)) + .ok_or_else(|| { + self.contract_error( + function, + statement.span, + "Resource constructor has no fresh carrier family", + ) + })?; + let plan = family.carriers().ok_or_else(|| { + self.contract_error( + function, + statement.span, + "Resource constructor has no fresh carrier family", + ) + })?; + let position = jett_mir::ResourcePosition::Statement(index); + let planned = match node { + Node::End { outcome, .. } => { + let begin = plan.operations().iter().find(|operation| { + operation.site() == original.begin() + && matches!( + operation.role(), + jett_mir::ResourceCarrierOperationRole::BeginConstructor { .. } + ) + }); + begin.is_some_and(|begin| { + family.operations().iter().filter(|operation| { + operation.site().block() == block + && operation.site().position() == position + && operation.frame() == begin.frame() + && matches!(operation.role(), jett_mir::ResourceOperationRole::Complete { outcome: found } if found == outcome) + && original.exits().contains(&(operation.site(), *outcome)) + }).count() == 1 + }) + } + _ => plan.operations().iter().any(|operation| { + operation.site().block() == block && operation.site().position() == position + }), + }; + if !planned { + return Err(self.contract_error( + function, + statement.span, + "Resource constructor has no exact planned occurrence", + )); + } + match node { + Node::Stage { + source_index, + field, + value, + .. + } => { + if original.fields().get(*source_index).is_none_or(|f| { + f.field() != *field + || f.stage().block() != block + || f.stage().position() != position + }) { + return Err(self.contract_error( + function, + statement.span, + "Resource constructor changed its exact field site", + )); + } + self.expression(function, value)?; + } + Node::Commit { output, .. } + if original.commit().is_none_or(|(site, header)| { + site.block() != block + || site.position() != position + || header.id != *output + }) => + { + return Err(self.contract_error( + function, + statement.span, + "Resource constructor changed its exact Commit site", + )); + } + _ => {} + } + Ok(()) + } + StatementKind::ResourceLexicalExit(_) => { + let position = jett_mir::ResourcePosition::Statement(index); + let ownership = self.resource.ok_or_else(|| { + self.contract_error( + function, + statement.span, + "lexical Resource node has no ownership proof", + ) + })?; + let plan = ownership + .function(function.id) + .and_then(|function| function.lexical_exit(block, position)) + .ok_or_else(|| { + self.contract_error( + function, + statement.span, + "lexical Resource node lost its exact planned site", + ) + })?; + let original = function + .resource_lexical_exit(block, position) + .map_err(|message| self.contract_error(function, statement.span, message))? + .ok_or_else(|| { + self.contract_error( + function, + statement.span, + "lexical Resource node lost its private constructor", + ) + })?; + if original.kind() != plan.kind() + || plan.kind() == jett_mir::ResourceLexicalExitKind::Return + { + return Err(self.contract_error( + function, + statement.span, + "lexical Resource node changed its exact exit kind", + )); + } + Ok(()) + } + StatementKind::ResourceCall(node) => { + use jett_mir::ResourceCallNode as Node; + let ownership = self.resource.ok_or_else(|| { + self.unsupported( + function, + statement.span, + "staged Resource node has no constructor-owned ownership proof", + ) + })?; + let region_id = match node { + Node::Begin { region } + | Node::Stage { region, .. } + | Node::Invoke { region, .. } + | Node::End { region, .. } => *region, + }; + let region = function.resource_call_region(region_id).ok_or_else(|| { + self.contract_error( + function, + statement.span, + "staged Resource node lost its private region", + ) + })?; + let plan = ownership.function(function.id).ok_or_else(|| { + self.contract_error( + function, + statement.span, + "staged region is outside the fresh execution family", + ) + })?; + if !plan.operations().iter().any(|operation| matches!(operation.role(), + jett_mir::ResourceOperationRole::BeginSourceFunction { region, .. } if *region == region_id)) { + return Err(self.contract_error(function, statement.span, + "staged region has no complete fresh Begin projection")); + } + match node { + Node::Stage { + value, ordinary, .. + } => { + self.expression(function, value)?; + if let Some(local) = ordinary { + let endpoint = function.local(*local).ok_or_else(|| { + self.contract_error( + function, + statement.span, + "staged ordinary endpoint is absent", + ) + })?; + self.require_same_type( + function, + statement.span, + endpoint.ty, + value.ty, + "staged ordinary endpoint changes its evaluated type", + )?; + } + } + Node::Invoke { output, .. } => { + let endpoint = function.local(*output).ok_or_else(|| { + self.contract_error( + function, + statement.span, + "staged invocation output is absent", + ) + })?; + self.require_same_type( + function, + statement.span, + endpoint.ty, + region.original().ty, + "staged invocation changes its original result type", + )?; + } + Node::Begin { .. } | Node::End { .. } => {} + } + Ok(()) + } StatementKind::IterationBorrow { source, token, .. } => { if !matches!( self.types.resolve(self.sequence_source_type( @@ -892,6 +1244,17 @@ impl Verifier<'_> { // Diagnostic text is canonical to the typed enum, never a source operand. Ok(()) } + StatementKind::ReflectedValueReady { source, .. } => { + let source = function.local(*source).ok_or_else(|| { + self.contract_error( + function, + statement.span, + "missing reflected callback result place", + ) + })?; + self.value_kind(function, source.ty, "reflected callback result")?; + Ok(()) + } StatementKind::SequenceLength { source, target } | StatementKind::SequenceGet { source, target, .. } => { if matches!( @@ -1545,12 +1908,247 @@ impl Verifier<'_> { } fn expression(&self, function: &Function, expression: &Expression) -> Result<(), CodegenError> { - let kind = self.value_kind( - function, - expression.ty, - format!("expression in `{}`", self.function_name(function)), - )?; + if self.carrier_expression(function, expression)?.is_some() { + return Ok(()); + } + let named_value = self + .resource + .and_then(|ownership| ownership.function(function.id)) + .and_then(|plan| plan.named_callable_value(expression)); + let descriptor_value = self + .resource + .and_then(|ownership| ownership.function(function.id)) + .and_then(|plan| plan.descriptor_value(expression)); + if let Some(hook) = descriptor_value { + if hook.function_type() != expression.ty + || !self.resource.is_some_and(|ownership| { + ownership.program().resource_manifest.contains_hook(hook) + }) + { + return Err(self.contract_error( + function, + expression.span, + "opaque descriptor occurrence changes its exact hook proof", + )); + } + if let ExpressionKind::Call { + function: target, .. + } = &expression.kind + { + if self + .resource + .and_then(|ownership| ownership.function(*target)) + .and_then(|callee| callee.descriptor_return()) + != Some(hook) + { + return Err(self.contract_error( + function, + expression.span, + "returned descriptor changes its exact Source callee Return proof", + )); + } + } + } + if let Some(ownership) = self.resource { + // Source signatures do not describe the selected family's hidden Scope. + // Descriptor and adapter emission requires its distinct authenticated ABI. + let family_descriptor = match &expression.kind { + ExpressionKind::FunctionRef(target) + | ExpressionKind::ClosureRef { + function: target, .. + } + | ExpressionKind::FunctionAdapter { + function: target, .. + } => ownership.function(*target).is_some(), + ExpressionKind::InterfaceCoerce { adapters, .. } => adapters + .iter() + .any(|adapter| ownership.function(adapter.function).is_some()), + _ => false, + }; + let named_producer = ownership + .function(function.id) + .and_then(|plan| plan.named_callable_producer(expression)); + let proved_named_descriptor = matches!(&expression.kind, + ExpressionKind::FunctionRef(target) if named_producer.is_some_and(|proof| + proof.function() == *target && proof.signature_type() == expression.ty)); + if family_descriptor && !proved_named_descriptor { + return Err(self.unsupported( + function, + expression.span, + "pending Resource family descriptor hidden Scope ABI", + )); + } + if let ExpressionKind::Call { + function: target, .. + } = &expression.kind + { + if ownership.function(*target).is_some() && !ownership.function(function.id).is_some_and(|plan| plan.operations_for_expression(expression).any(|operation| matches!(operation.role(), jett_mir::ResourceOperationRole::InvokeSourceFunction { function: callee, .. } if callee == target))) { + return Err(self.unsupported(function, expression.span, "Resource family direct edge has no exact Source scope activation")); + } + } + } + if let Some(plan) = self.resource.and_then(|plan| plan.function(function.id)) { + let invocation = plan.operations_for_expression(expression).any(|operation| { + matches!( + operation.role(), + jett_mir::ResourceOperationRole::InvokeHook { .. } + | jett_mir::ResourceOperationRole::InvokeSourceFunction { .. } + ) + }); + if invocation + || (named_value.is_none() + && self + .resource + .is_some_and(|proof| proof.type_requires_custody(expression.ty))) + { + match &expression.kind { + ExpressionKind::Local(local) => { + let local = function.local(*local).ok_or_else(|| { + self.contract_error( + function, + expression.span, + "custody local is outside its exact current header", + ) + })?; + return self.require_same_type( + function, + expression.span, + local.ty, + expression.ty, + "custody local type changed", + ); + } + ExpressionKind::View(inner) + | ExpressionKind::OptionalSome(inner) + | ExpressionKind::ResultOk(inner) + | ExpressionKind::ResultFail(inner) => return self.expression(function, inner), + ExpressionKind::Clone(inner) if descriptor_value.is_some() => { + if plan.descriptor_value(inner) != descriptor_value { + return Err(self.contract_error( + function, + expression.span, + "opaque descriptor alias changes its exact producer", + )); + } + return self.expression(function, inner); + } + ExpressionKind::ResourceHookValue { hook } => { + if descriptor_value != Some(hook) { + return Err(self.contract_error( + function, + expression.span, + "opaque hook value has no exact current producer proof", + )); + } + return Ok(()); + } + ExpressionKind::OptionalNone => { + return Ok(()); + } + ExpressionKind::Call { args, .. } + | ExpressionKind::ResourceInvoke { args, .. } => { + if !invocation { + return Err(self.contract_error( + function, + expression.span, + "Resource native call lost its exact fresh operation", + )); + } + for actual in args { + self.expression(function, actual)?; + } + if !self + .resource + .is_some_and(|proof| proof.type_requires_custody(expression.ty)) + { + self.value_kind( + function, + expression.ty, + "ordinary custody call result", + )?; + } + return Ok(()); + } + ExpressionKind::IndirectCall { callee, args, .. } => { + if !invocation { + return Err(self.contract_error( + function, + expression.span, + "Resource indirect hook lost its exact fresh operation", + )); + } + for operation in plan.operations_for_expression(expression) { + if let jett_mir::ResourceOperationRole::InvokeHook { + hook, + source, + .. + } = operation.role() + { + if operation.indirect_hook_target() != Some(hook) + || plan.descriptor_value(callee) != Some(hook) + || callee.ty != hook.function_type() + || !matches!(source.target, jett_hir::CallTarget::Indirect { signature_type } + if signature_type == callee.ty) + || source.bridge != jett_hir::CallBridge::Direct + || !source.generated_operands.is_empty() + { + return Err(self.contract_error(function, expression.span, + "Resource indirect hook changes its sealed descriptor target or Source tuple")); + } + } + if let jett_mir::ResourceOperationRole::InvokeSourceFunction { + function: target, + source, + .. + } = operation.role() + { + let proof = operation.named_indirect_target().ok_or_else(|| self.contract_error( + function, expression.span, "Resource indirect Source call lost its exact named target proof"))?; + if proof.function() != *target + || proof.signature_type() != callee.ty + || !matches!(source.target, jett_hir::CallTarget::Indirect { signature_type } if signature_type == callee.ty) + { + return Err(self.contract_error(function, expression.span, + "Resource indirect Source call changes its sealed selected target")); + } + } + } + self.expression(function, callee)?; + for actual in args { + self.expression(function, actual)?; + } + return Ok(()); + } + _ => { + return Err(self.unsupported( + function, + expression.span, + "dedicated Resource expression transport", + )); + } + } + } + } + let kind = if let Some(proof) = named_value { + if proof.signature_type() != expression.ty + || !matches!(self.types.resolve(expression.ty), Type::Function { .. }) + { + return Err(self.contract_error( + function, + expression.span, + "named descriptor occurrence changes its sealed signature", + )); + } + ScalarKind::Function + } else { + self.value_kind( + function, + expression.ty, + format!("expression in `{}`", self.function_name(function)), + )? + }; match &expression.kind { + ExpressionKind::ResourceHookValue { .. } | ExpressionKind::ResourceInvoke { .. } => Err(self.contract_error(function, expression.span, "pending ResourceOwnershipPlan: native Resource descriptor and invocation ABI are not admitted")), ExpressionKind::Int(value) => self.integer_literal(function, expression, *value, kind), ExpressionKind::Float(_) => { if matches!(kind, ScalarKind::Float(_)) { @@ -1886,7 +2484,8 @@ impl Verifier<'_> { if jett_hir::is_reflected_field_intrinsic(*intrinsic) { if !matches!( field_validation, - Some(jett_hir::ReflectedFieldValidation::Read) + Some(jett_hir::ReflectedFieldValidation::Read + | jett_hir::ReflectedFieldValidation::ReadCallable(_)) ) { return Err(self.contract_error( function, @@ -3473,7 +4072,6 @@ impl Verifier<'_> { | ScalarKind::Bitfield | ScalarKind::Machine | ScalarKind::Function - | ScalarKind::Actor ) && result == ScalarKind::Bool } BinaryOp::Less | BinaryOp::Greater | BinaryOp::LessEqual | BinaryOp::GreaterEqual => { diff --git a/crates/jett_codegen_cranelift/src/verify/carriers.rs b/crates/jett_codegen_cranelift/src/verify/carriers.rs new file mode 100644 index 00000000..c691784a --- /dev/null +++ b/crates/jett_codegen_cranelift/src/verify/carriers.rs @@ -0,0 +1,211 @@ +//! The generic native value visitor does not grant an aggregate Resource ABI. +//! Only exact occurrences in the freshly reconstructed carrier plan reach the +//! dedicated emitter; complete current-body/type seals are checked before here. +use super::*; +use jett_hir::IntrinsicId; +use jett_mir::ResourceCarrierOperationRole as Role; + +impl Verifier<'_> { + pub(super) fn carrier_expression( + &self, + function: &Function, + expression: &Expression, + ) -> Result, CodegenError> { + let Some(ownership) = self.resource else { + return Ok(None); + }; + let Some(plan) = ownership + .function(function.id) + .and_then(|plan| plan.carriers()) + else { + return Ok(None); + }; + let operations = plan + .operations_for_expression(expression) + .collect::>(); + if operations.is_empty() { + return Ok(None); + } + // Source invocation continues through the existing exact original tuple, + // callee/signature checks. A carrier row cannot replace that authority. + if ownership.function(function.id).is_some_and(|plan| { + plan.operations_for_expression(expression).any(|operation| { + matches!( + operation.role(), + jett_mir::ResourceOperationRole::InvokeSourceFunction { .. } + | jett_mir::ResourceOperationRole::InvokeHook { .. } + ) + }) + }) { + return Ok(None); + } + match &expression.kind { + ExpressionKind::ListConstruct { elements } => { + self.require_carrier_constructor(function, expression, plan, &operations)?; + for child in elements { + self.expression(function, child)?; + } + } + ExpressionKind::MapConstruct { entries } => { + self.require_carrier_constructor(function, expression, plan, &operations)?; + for child in entries { + self.expression(function, &child.key)?; + self.expression(function, &child.value)?; + } + } + ExpressionKind::StructConstruct { fields, .. } + | ExpressionKind::EnumConstruct { + payloads: fields, .. + } + | ExpressionKind::MachineConstruct { + payloads: fields, .. + } => { + self.require_carrier_constructor(function, expression, plan, &operations)?; + for child in fields { + self.expression(function, child)?; + } + } + ExpressionKind::OptionalNone => { + self.require_carrier_constructor(function, expression, plan, &operations)?; + } + ExpressionKind::OptionalSome(inner) + | ExpressionKind::ResultOk(inner) + | ExpressionKind::ResultFail(inner) => { + self.require_carrier_constructor(function, expression, plan, &operations)?; + self.expression(function, inner)?; + } + ExpressionKind::InterfaceCoerce { value, adapters } if adapters.is_empty() => { + let transfers = operations + .iter() + .filter_map(|operation| match operation.role() { + Role::Transfer { + source, + destination, + } + | Role::QualifyMachine { + source, + destination, + .. + } => Some((*source, *destination)), + _ => None, + }) + .collect::>(); + let [(source, destination)] = transfers.as_slice() else { + return Err(self.contract_error( + function, + expression.span, + "carrier qualification lacks its single exact private transfer", + )); + }; + let source_type = plan + .slots() + .get(source.index()) + .and_then(|slot| plan.shape(slot.shape())) + .map(|shape| shape.ty()); + let destination_type = plan + .slots() + .get(destination.index()) + .and_then(|slot| plan.shape(slot.shape())) + .map(|shape| shape.ty()); + if source_type != Some(value.ty) || destination_type != Some(expression.ty) { + return Err(self.contract_error( + function, + expression.span, + "carrier qualification changes its exact source/destination type", + )); + } + self.expression(function, value)?; + } + ExpressionKind::Field { base, .. } => { + if !operations.iter().any(|operation| { + matches!( + operation.role(), + Role::Extract { .. } + | Role::AdaptSum { .. } + | Role::Observe { .. } + | Role::Borrow { .. } + ) + }) { + return Err(self.contract_error( + function, + expression.span, + "carrier field lacks its exact projection/parent lease", + )); + } + self.expression(function, base)?; + } + ExpressionKind::StateIs { value, state } => { + if !operations.iter().any(|operation| { + matches!(operation.role(), Role::Observe { + observation: jett_mir::ResourceCarrierObservation::State { state: expected }, .. + } if u32::try_from(*expected).ok() == Some(state.index())) + }) { + return Err(self.contract_error( + function, + expression.span, + "carrier state observation changes its checked selected state", + )); + } + self.expression(function, value)?; + } + ExpressionKind::Intrinsic { + intrinsic, args, .. + } if matches!(intrinsic, IntrinsicId::ListLength | IntrinsicId::MapLength) + && args.len() == 1 => + { + if !operations.iter().any(|operation| { + matches!( + operation.role(), + Role::Observe { + observation: jett_mir::ResourceCarrierObservation::Length, + .. + } + ) + }) { + return Err(self.contract_error( + function, + expression.span, + "carrier length lacks its exact observation row", + )); + } + self.expression(function, &args[0])?; + } + _ => { + return Err(self.contract_error( + function, + expression.span, + "carrier occurrence does not have its dedicated exact native value transport", + )); + } + } + Ok(Some(())) + } + fn require_carrier_constructor( + &self, + function: &Function, + expression: &Expression, + plan: &jett_mir::ResourceCarrierFunctionPlan, + operations: &[&jett_mir::ResourceCarrierOperation], + ) -> Result<(), CodegenError> { + let mut begins = operations + .iter() + .filter_map(|operation| match operation.role() { + Role::BeginConstructor { destination, .. } => Some(*destination), + _ => None, + }); + let destination = begins.next().ok_or_else(|| { + self.contract_error( + function, + expression.span, + "carrier constructor lost its exact private Begin", + ) + })?; + if begins.next().is_some() + || plan.slots().get(destination.index()).and_then(|slot| plan.shape(slot.shape())).map(|shape| shape.ty()) != Some(expression.ty) + || operations.iter().filter(|operation| matches!(operation.role(), Role::CommitConstructor { destination: selected } if *selected == destination)).count() != 1 + { + return Err(self.contract_error(function, expression.span, "carrier constructor changes its exact node/destination/commit")); + } + Ok(()) + } +} diff --git a/crates/jett_codegen_cranelift/tests/object_emission.rs b/crates/jett_codegen_cranelift/tests/object_emission.rs index d4865d1d..8800e8de 100644 --- a/crates/jett_codegen_cranelift/tests/object_emission.rs +++ b/crates/jett_codegen_cranelift/tests/object_emission.rs @@ -991,7 +991,7 @@ fn container_callback_adapters_reject_missing_or_invalid_checked_metadata() { 0 => adapters.clear(), 1 => adapters[0].function = convert.id, 2 => adapters[0].target = adapters[0].source, - _ => adapters.push(adapters[0]), + _ => adapters.push(adapters[0].clone()), } let error = emit_host_object(&program, &types).expect_err("malformed callback conversion"); assert!( @@ -1686,6 +1686,7 @@ fn rejects_statically_zero_integer_divisors() { #[test] fn rejects_an_explicit_non_host_target_before_object_generation() { let program = Program { + resource_manifest: jett_hir::ResourceManifest::empty(), functions: Vec::new(), equality_methods: HashMap::new(), }; @@ -1704,6 +1705,7 @@ fn rejects_an_explicit_non_host_target_before_object_generation() { #[test] fn rejects_malformed_target_text() { let program = Program { + resource_manifest: jett_hir::ResourceManifest::empty(), functions: Vec::new(), equality_methods: HashMap::new(), }; @@ -2937,3 +2939,64 @@ function inspect(view source: Record, view field: TypeField) returns Independent .collect(); assert_eq!(checks, ["app.Independent"]); } + +#[test] +fn actor_identity_emits_a_checked_leaf_and_nested_layout_without_structural_state_comparison() { + for source in [ + include_str!("../../../tests/native/actor_enum_equality.jett"), + include_str!("../../../tests/native/actor_pending_alias_baseline.jett"), + include_str!("../../../tests/native/actor_pending_alias_identity.jett"), + include_str!("../../../tests/native/actor_direct_identity.jett"), + include_str!("../../../tests/native/actor_nested_identity.jett"), + ] { + let (program, types) = lower_source_with_equatable(source, true); + emit_host_object(&program, &types).expect("selected checked actor identity"); + } +} + +#[test] +fn actor_identity_rejects_a_different_nominal_mir_operand() { + let source = "namespace app\nactor First:\n receive read responds int64:\n respond 1\nactor Second:\n receive read responds int64:\n respond 1\nfunction compare(view first: First, view second: First) returns bool:\n return first == second\n"; + let (mut program, types) = lower_source(source); + emit_host_object(&program, &types).expect("same checked nominal actor type"); + let wrong_actor = types.type_ids() + .find(|id| matches!(types.resolve(*id), Type::Actor(actor) if types.resolve_actor(*actor).name == "app.Second")) + .expect("second nominal actor type"); + let function = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name.ends_with("compare")) + .unwrap(); + let entry = function.entry.index() as usize; + let TerminatorKind::Return(Some(value)) = &mut function.blocks[entry].terminator.kind else { + panic!("returned comparison"); + }; + let jett_hir::ExpressionKind::Binary { right, .. } = &mut value.kind else { + panic!("binary comparison"); + }; + right.ty = wrong_actor; + let error = + emit_host_object(&program, &types).expect_err("exact nominal actor handoff is required"); + assert!( + matches!(error, CodegenError::InvalidMirContract { .. }), + "{error:?}" + ); +} + +#[test] +fn actor_identity_frontend_rejects_different_nominal_owners() { + let parsed = jett_parser::parse( + "namespace app\nactor First:\n receive read responds int64:\n respond 1\nactor Second:\n receive read responds int64:\n respond 1\nfunction compare(view first: First, view second: Second) returns bool:\n return first == second\n", + FileId::new(0), + ); + assert!(parsed.errors.is_empty()); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked.diagnostics.iter().any(|diagnostic| { + diagnostic.severity == jett_diagnostics::Severity::Error + && diagnostic.code.code() == 301 + }), + "different actor declarations cannot share an equality operand type" + ); +} diff --git a/crates/jett_comptime/Cargo.toml b/crates/jett_comptime/Cargo.toml index 37e88a87..00fb5353 100644 --- a/crates/jett_comptime/Cargo.toml +++ b/crates/jett_comptime/Cargo.toml @@ -11,6 +11,8 @@ jett_diagnostics.workspace = true jett_intrinsics.workspace = true jett_parser.workspace = true jett_runtime.workspace = true +jett_resolve.workspace = true +jett_typecheck.workspace = true jett_types.workspace = true rand.workspace = true subtle.workspace = true diff --git a/crates/jett_comptime/src/checked_types.rs b/crates/jett_comptime/src/checked_types.rs index fd8d116d..a88336b2 100644 --- a/crates/jett_comptime/src/checked_types.rs +++ b/crates/jett_comptime/src/checked_types.rs @@ -9,6 +9,8 @@ use crate::value::{ClosureTypeArgument, Value}; #[derive(Debug, Clone, Default)] pub struct CheckedExpressionTypes { + /// Exact compiler session. Ordinary type-name projections confer no hook authority. + pub resource_program: Option>, pub bindings: CheckedScopedBindings, pub expressions: HashMap, pub functions: HashMap>>, diff --git a/crates/jett_comptime/src/explicit.rs b/crates/jett_comptime/src/explicit.rs index 00e756ef..f025c1ce 100644 --- a/crates/jett_comptime/src/explicit.rs +++ b/crates/jett_comptime/src/explicit.rs @@ -7,6 +7,10 @@ use jett_diagnostics::Diagnostic; use jett_parser::ast::{Block, Expr, Item, Module, Stmt, StringPart}; use jett_types::{ReflectionMetadata, ReflectionTypeInfo}; +use crate::resource_execution::{CheckedAttemptKey, PreparedRequiredExpression}; +pub use crate::resource_execution::{ + CheckedRequiredOwner, CheckedRequiredResourceHook, CheckedRequiredScope, CheckedRequiredValue, +}; use crate::value::ClosureScopedTypeBinding; use crate::{DebugEvent, Interpreter, Value}; @@ -42,6 +46,10 @@ impl ComptimeContext { pub struct ExplicitComptimeValues { values: HashMap<(Span, ComptimeContext), Value>, constants: HashMap, + // Private authority association. Each successful insertion below also + // mirrors the value into values or constants in that same branch, so the + // existing public collection views count reusable values once. + checked_values: HashMap, } impl ExplicitComptimeValues { @@ -65,6 +73,58 @@ impl ExplicitComptimeValues { self.values.insert((span, context), value); } + /// Authenticate successful private required entries for this exact checked + /// program. Public span/context mirrors are never used to mint proof. + pub fn checked_required_values( + &self, + program: &Arc, + ) -> Result, String> { + self.checked_values + .keys() + .map(|key| CheckedRequiredValue::from_cache(self, key, program)) + .collect() + } + + /// Readonly hook projections of the same private successful entries. + pub fn checked_resource_hook_values( + &self, + program: &Arc, + ) -> Result, String> { + Ok(self + .checked_required_values(program)? + .into_iter() + .filter_map(CheckedRequiredResourceHook::from_value) + .collect()) + } + pub(crate) fn checked_get(&self, key: &CheckedAttemptKey) -> Option<&Value> { + self.checked_values.get(key) + } + + fn checked_insert(&mut self, key: CheckedAttemptKey, value: Value) -> Result<(), String> { + if value.contains_live_resource_or_grant() { + return Err( + "required value cannot cache runtime authority or Resource custody".to_string(), + ); + } + self.checked_values.insert(key, value); + Ok(()) + } + + #[cfg(test)] + pub(crate) fn checked_values_are_mirrored(&self) -> bool { + self.checked_values.values().all(|checked| { + self.values() + .any(|value| match (value.payload(), checked.payload()) { + (Value::ResourceHook(left), Value::ResourceHook(right)) => { + crate::resource_execution::tests::same_resource_hook_descriptor_identity( + left, right, + ) + } + _ => value == checked, + }) + }) + } + /// A checked namespace constant, independent of any function instantiation. pub fn constant(&self, declaration: Span) -> Option<&Value> { self.constants.get(&declaration) @@ -91,6 +151,7 @@ struct CollectedExpression<'a> { #[derive(Clone, Default)] struct EvaluationContext { + required: Option, function: Option>, scope: Option>, bindings: Vec, @@ -189,7 +250,38 @@ fn evaluate_collected_expressions( checked_expression_types: Arc, breakpoint_exclusions: Arc>>, ) -> ExplicitComptimeEvaluation { + if let Some(program) = &checked_expression_types.resource_program { + if !std::ptr::eq(module, program.module()) { + return ExplicitComptimeEvaluation { + diagnostics: vec![Diagnostic::error( + 0, + "required worker received a foreign checked source module", + module.span, + )], + ..ExplicitComptimeEvaluation::default() + }; + } + } let mut interpreter = Interpreter::new(); + if let Some(program) = &checked_expression_types.resource_program { + if let Err(error) = interpreter + .install_checked_resource_program( + program.clone(), + crate::ExecutionPurpose::ExplicitComptime, + ) + .and_then(|()| { + interpreter.authorize_checked_resource_worker( + module, + crate::ExecutionPurpose::ExplicitComptime, + ) + }) + { + return ExplicitComptimeEvaluation { + diagnostics: vec![Diagnostic::error(0, error, module.span)], + ..ExplicitComptimeEvaluation::default() + }; + } + } interpreter.set_reflection_metadata(reflection_metadata); interpreter.set_checked_expression_types(checked_expression_types.clone()); interpreter.set_breakpoint_exclusions(breakpoint_exclusions); @@ -197,39 +289,90 @@ fn evaluate_collected_expressions( let mut values = ExplicitComptimeValues::default(); let mut diagnostics = Vec::new(); + let previous_purpose = + interpreter.checked_execution_purpose(crate::ExecutionPurpose::NamespaceConstant); + let mut attempted_checked = HashSet::new(); let mut attempted_expressions = evaluate_constants( module, &checked_expression_types, &mut interpreter, &mut values, &mut diagnostics, + &mut attempted_checked, ); + if let Some(purpose) = previous_purpose { + interpreter.checked_execution_purpose(purpose); + } for collected in expressions { - let contexts = evaluation_contexts(&collected, &checked_expression_types); + let contexts = match interpreter.checked_explicit_contexts( + collected.expression, + collected.span, + collected.owner.as_ref().map(|(span, _)| *span), + &collected.bindings, + ) { + Ok(Some(contexts)) => contexts + .into_iter() + .map(|required| EvaluationContext { + function: required.function_projection(), + scope: required.scope_projection(), + bindings: required.bindings(), + required: Some(required), + }) + .collect(), + Ok(None) => evaluation_contexts(&collected, &checked_expression_types), + Err(error) => { + diagnostics.push(Diagnostic::error(9001, error, collected.span)); + continue; + } + }; let parameters = collected .owner .as_ref() .map_or(&[][..], |(_, names)| names.as_slice()); for context in contexts { let key = context.key(); - if values.get(collected.span, &key).is_some() { - continue; - } - // Identical checked contexts also share a failed attempt, without - // inventing a baked value or suppressing independent contexts. - if !attempted_expressions.insert((collected.span, key.clone())) { - continue; + let checked_key = context.required.as_ref().map(|required| required.key()); + if let Some(identity) = &checked_key { + if values.checked_get(identity).is_some() + || !attempted_checked.insert(identity.clone()) + { + continue; + } + } else { + if values.get(collected.span, &key).is_some() { + continue; + } + if !attempted_expressions.insert((collected.span, key.clone())) { + continue; + } } - match interpreter.eval_closed_comptime_expression( - collected.namespace.as_deref(), - &collected.aliases, - collected.expression, - parameters, - context.function, - context.scope, - context.bindings, - ) { - Ok(value) => values.insert(collected.span, key, value), + let evaluated = if let Some(required) = &context.required { + interpreter.eval_checked_required_expression( + required, + collected.namespace.as_deref(), + &collected.aliases, + parameters, + ) + } else { + interpreter.eval_closed_comptime_expression( + collected.namespace.as_deref(), + &collected.aliases, + collected.expression, + parameters, + context.function, + context.scope, + context.bindings, + ) + }; + match evaluated { + Ok(value) => { + if let Some(identity) = checked_key { + if let Err(error) = values.checked_insert(identity, value.clone()) { + diagnostics.push(Diagnostic::error(9001, error, collected.span)); continue; + } + } + values.insert(collected.span, key, value); + } Err(error) => diagnostics.push(Diagnostic::error( 9001, format!("`comptime` expression must be closed and evaluable during compilation: {error}"), @@ -251,6 +394,7 @@ fn evaluate_constants( interpreter: &mut Interpreter, values: &mut ExplicitComptimeValues, diagnostics: &mut Vec, + attempted_checked: &mut HashSet, ) -> HashSet<(Span, ComptimeContext)> { let mut attempted = HashSet::new(); let mut current_file = None; @@ -303,15 +447,35 @@ fn evaluate_constants( if let Expr::Comptime(_, span) = &declaration.value { attempted.insert((*span, ComptimeContext::default())); } - match interpreter.eval_closed_comptime_expression( - namespace.as_deref(), - &HashMap::new(), - expression, - &[], - None, - None, - Vec::new(), - ) { + let prepared = match interpreter.checked_namespace_initializer(declaration) { + Ok(prepared) => prepared, + Err(error) => { + diagnostics.push(Diagnostic::error(9001, error, declaration.value.span())); + continue; + } + }; + if let Some(prepared) = &prepared { + attempted_checked.insert(prepared.key()); + } + let evaluated = if let Some(prepared) = &prepared { + interpreter.eval_checked_required_expression( + prepared, + namespace.as_deref(), + &HashMap::new(), + &[], + ) + } else { + interpreter.eval_closed_comptime_expression( + namespace.as_deref(), + &HashMap::new(), + expression, + &[], + None, + None, + Vec::new(), + ) + }; + match evaluated { Ok(value) => { if !constant_value_matches_type(&value, ty) { diagnostics.push(Diagnostic::error( @@ -321,6 +485,12 @@ fn evaluate_constants( )); continue; } + if let Some(prepared) = &prepared { + if let Err(error) = values.checked_insert(prepared.key(), value.clone()) { + diagnostics.push(Diagnostic::error(9001, error, declaration.value.span())); + continue; + } + } interpreter.register_constant_in_namespace( namespace.as_deref(), declaration, diff --git a/crates/jett_comptime/src/interpreter.rs b/crates/jett_comptime/src/interpreter.rs index 614307a7..00f4f28c 100644 --- a/crates/jett_comptime/src/interpreter.rs +++ b/crates/jett_comptime/src/interpreter.rs @@ -1,3 +1,11 @@ +#[path = "resource_execution/capture.rs"] +mod resource_capture; +#[path = "resource_execution/interpreter_transport.rs"] +mod resource_transport; +use crate::resource_execution::{ + EvaluatedValue, PreparedReflectedFieldIteration, ResourceTransport, +}; + use crate::checked_types::{ CheckedExpressionTypes, CheckedFunctionTypes, CheckedScopedTypes, select_scoped_types, }; @@ -45,7 +53,9 @@ use crate::value::{ClosureScopedTypeBinding, ClosureTypeArgument, ClosureTypeCon mod debug; mod generic_inference; mod graphics; +mod interface_arguments; mod nominal_types; +mod reflected_callables; use graphics::GraphicsProvider; pub use graphics::{GraphicsTestEvent, GraphicsTestObservation}; @@ -449,7 +459,9 @@ pub type Environment = HashMap; #[derive(Debug)] enum Signal { Return(Value, Option), + ResourceReturn(EvaluatedValue, Option), Default(Value), + ResourceDefault(EvaluatedValue), Respond(Value), Break, Continue, @@ -475,6 +487,7 @@ struct ProducedRefinementCheck { #[derive(Debug)] enum ExprFlow { Value(Value), + Resource(EvaluatedValue), Signal(Signal), } @@ -482,6 +495,12 @@ macro_rules! value_or_signal { ($self:expr, $expr:expr) => { match $self.eval_expr_flow($expr)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => { + return Err( + "Resource operand has no checked ownership transport for this operation" + .to_string(), + ) + } ExprFlow::Signal(signal) => return Ok(ExprFlow::Signal(signal)), } }; @@ -613,6 +632,11 @@ struct RegisteredFunction { } pub struct Interpreter { + resource_transport: Option, + retained_resource_program: Option<( + Arc, + crate::ExecutionPurpose, + )>, /// Stack of lexical scopes. The last element is the innermost scope. scopes: Vec, /// Runtime type annotations for variables declared in the matching scope. @@ -655,6 +679,8 @@ pub struct Interpreter { current_function_trusted_stdlib: bool, /// Trusted field metadata currently produced by direct `type.fields[T]()` loops. reflected_field_scopes: Vec>, + /// Private selectors for exact checked Source For iterations; never Value authority. + checked_reflected_fields: Vec, /// Trusted TypeInfo metadata currently produced by direct reflected `args` loops. reflected_type_info_scopes: Vec>, /// Trusted TypeVariant metadata currently produced by direct `type.variants[T]()` loops. @@ -668,6 +694,7 @@ pub struct Interpreter { checked_expression_types: Option>, active_checked_function: Option>, active_checked_scope: Option>, + active_reflected_capture_origin: Option, /// Checked source identities prove invariants only in checked invocation trees. /// Raw callers may supply aggregate payloads with stale refined children. allow_checked_refinement_proofs: bool, @@ -718,6 +745,8 @@ impl Interpreter { /// Create a new interpreter with an empty global scope. pub fn new() -> Self { Self { + resource_transport: None, + retained_resource_program: None, scopes: vec![HashMap::new()], variable_type_scopes: vec![HashMap::new()], namespace_alias_scopes: vec![HashMap::new()], @@ -739,6 +768,7 @@ impl Interpreter { lexical_scope_floor: 0, current_function_trusted_stdlib: false, reflected_field_scopes: Vec::new(), + checked_reflected_fields: Vec::new(), reflected_type_info_scopes: Vec::new(), reflected_variant_scopes: Vec::new(), reflected_machine_state_scopes: Vec::new(), @@ -746,6 +776,7 @@ impl Interpreter { checked_expression_types: None, active_checked_function: None, active_checked_scope: None, + active_reflected_capture_origin: None, allow_checked_refinement_proofs: true, breakpoint_exclusions: Arc::new(HashMap::new()), explicit_comptime_values: None, @@ -880,12 +911,18 @@ impl Interpreter { // -- Scope management --------------------------------------------------- fn push_scope(&mut self) { + if let Some(transport) = &mut self.resource_transport { + transport.push_scope(); + } self.scopes.push(HashMap::new()); self.variable_type_scopes.push(HashMap::new()); self.namespace_alias_scopes.push(HashMap::new()); } fn pop_scope(&mut self) { + if let Some(transport) = &mut self.resource_transport { + transport.pop_scope(); + } self.scopes.pop(); self.variable_type_scopes.pop(); self.namespace_alias_scopes.pop(); @@ -2124,7 +2161,13 @@ impl Interpreter { /// Evaluate an expression, returning its [`Value`]. pub fn eval_expr(&mut self, expr: &Expr) -> Result { match self.eval_expr_flow(expr)? { - ExprFlow::Value(value) => Ok(value), + ExprFlow::Value(value) if !value.contains_live_resource_or_grant() => Ok(value), + ExprFlow::Value(_) | ExprFlow::Resource(_) => { + Err("raw expression API cannot export live Resource custody".to_string()) + } + ExprFlow::Signal(Signal::ResourceReturn(..) | Signal::ResourceDefault(..)) => { + Err("Resource control flow cannot escape raw expression evaluation".to_string()) + } ExprFlow::Signal(Signal::Default(_)) => { Err("`default` can only be used inside a `handle` block".to_string()) } @@ -2231,6 +2274,24 @@ impl Interpreter { } fn eval_expr_flow(&mut self, expr: &Expr) -> Result { + if self.resource_transport.is_some() { + if let Some(flow) = self.eval_resource_transport(expr)? { + return Ok(flow); + } + } + // Only the original checked call route can return a custody envelope. + // Retain that caller fact before actuals or a callee switch body cursors. + let checked_resource_call = match (&self.resource_transport, expr) { + (Some(transport), Expr::Call(..) | Expr::GenericCall(..)) + if transport.checked_source_active => + { + transport + .checked + .has_invocation(expr.span()) + .map_err(|error| error.to_string())? + } + _ => false, + }; // Source calls can recurse through many small stdlib functions. Dispatch // them before entering the general expression match, whose aggregate // temporaries otherwise remain on the stack throughout each call. @@ -2245,6 +2306,10 @@ impl Interpreter { ExprFlow::Value(value) => Ok(ExprFlow::Value( self.normalize_value_for_checked_expr(expr, value)?, )), + ExprFlow::Resource(value) if checked_resource_call => Ok(ExprFlow::Resource(value)), + ExprFlow::Resource(_) => { + Err("Resource transport escaped its checked expression boundary".to_string()) + } ExprFlow::Signal(signal) => Ok(ExprFlow::Signal(signal)), } } @@ -2253,6 +2318,13 @@ impl Interpreter { &mut self, bind: &jett_parser::ast::ComptimeTypeBindStmt, ) -> Result, String> { + if self + .resource_transport + .as_ref() + .is_some_and(|transport| transport.checked_source_active) + { + return self.exec_checked_resource_type_bind(bind); + } let bound_type_expr = if let Some(bound_type_expr) = comptime_type_info_binding(&bind.value) { self.substitute_type_expr(bound_type_expr) @@ -2504,6 +2576,21 @@ impl Interpreter { .map(|value| self.retain_checked_identity(type_name, value)) } + fn actor_identity_equal(&self, left: u64, right: u64) -> Result { + let left_actor = self + .actor_instances + .get(&left) + .ok_or_else(|| format!("actor equality: unknown actor instance #{left}"))?; + let right_actor = self + .actor_instances + .get(&right) + .ok_or_else(|| format!("actor equality: unknown actor instance #{right}"))?; + if left_actor.type_name != right_actor.type_name { + return Err("actor equality requires matching nominal actor types".into()); + } + Ok(left == right) + } + fn enum_payload_equal(&mut self, left: &Value, right: &Value) -> Result { let mut work = vec![(left, right)]; while let Some((left, right)) = work.pop() { @@ -2595,6 +2682,11 @@ impl Interpreter { | (Value::OptionalSome(a), Value::OptionalSome(b)) | (Value::ResultOk(a), Value::ResultOk(b)) | (Value::ResultFail(a), Value::ResultFail(b)) => work.push((a, b)), + (Value::Actor(left), Value::Actor(right)) => { + if !self.actor_identity_equal(*left, *right)? { + return Ok(false); + } + } _ if left != right => return Ok(false), _ => {} } @@ -2665,6 +2757,16 @@ impl Interpreter { Expr::View(inner, _) => self.eval_expr_flow(inner), Expr::Comptime(inner, span) => { if let Some(values) = &self.explicit_comptime_values { + if let Some(transport) = &self.resource_transport { + if transport.checked_source_active { + let identity = transport + .checked + .original_comptime_key(expr) + .map_err(|error| error.to_string())?; + return values.checked_get(&identity).cloned().map(ExprFlow::Value) + .ok_or_else(|| "explicit comptime expression has no exact checked original value".to_string()); + } + } let mut context = crate::ComptimeContext::from_checked( self.active_checked_function.as_deref(), ); @@ -2698,6 +2800,17 @@ impl Interpreter { _ => {} } let right = value_or_signal!(self, rhs); + if matches!(op, BinOp::Eq | BinOp::NotEq) + && let (Value::Actor(left), Value::Actor(right)) = + (left.payload(), right.payload()) + { + let equal = self.actor_identity_equal(*left, *right)?; + return Ok(ExprFlow::Value(Value::Bool(if *op == BinOp::Eq { + equal + } else { + !equal + }))); + } if matches!(op, BinOp::Eq | BinOp::NotEq) && matches!( (left.payload(), right.payload()), @@ -2878,6 +2991,7 @@ impl Interpreter { }; value = match self.eval_pipeline_step(&step, value, args.get(1))? { ExprFlow::Value(next) => next, + ExprFlow::Resource(_) => return Err("Resource operand has no checked ownership transport for this operation".to_string()), ExprFlow::Signal(signal) => return Ok(ExprFlow::Signal(signal)), }; } @@ -2962,7 +3076,7 @@ impl Interpreter { } Expr::InlineFn(params, return_type, body, _) => Ok(ExprFlow::Value( - self.capture_closure(params, return_type.as_ref(), body), + self.capture_closure(params, return_type.as_ref(), body)?, )), // Unsupported expressions produce a clear error. @@ -3051,14 +3165,32 @@ impl Interpreter { params: &[Param], return_type: Option<&TypeExpr>, body: &Block, - ) -> Value { - // Capture the current environment (all visible variables) for closure semantics. + ) -> Result { + // Opaque carriers require custody before any capture copy. + // Ordinary lexical captures preserve their existing semantics. let mut captures = HashMap::new(); for (index, scope) in self.scopes.iter().enumerate() { if index != 0 && index < self.lexical_scope_floor { continue; } for (name, value) in scope { + if value.contains_live_resource_or_grant() { + let referenced = self + .resource_transport + .as_ref() + .and_then(|transport| { + transport.captured_resource_reference(name, body.span) + }) + .unwrap_or_else(|| { + resource_capture::raw_closure_uses_binding(params, body, name) + }); + if referenced { + return Err( + "opaque capture has no checked custody transport proof".to_string() + ); + } + continue; + } captures.insert(name.clone(), value.clone()); } } @@ -3071,7 +3203,7 @@ impl Interpreter { .collect(); let type_arguments = self.current_type_arguments.clone(); let captured_arguments = self.captured_type_arguments(&type_arguments); - Value::Function { + let mut value = Value::Function { type_context: Box::new(ClosureTypeContext { return_type: return_type.cloned(), checked_scope: self.active_checked_scope.clone(), @@ -3084,6 +3216,7 @@ impl Interpreter { .map(|(name, ty)| (name.clone(), self.resolve_installed_type_expr(ty))) .collect(), arguments: captured_arguments, + reflected_capture: None, }), params: params.to_vec(), body: body.clone(), @@ -3091,7 +3224,14 @@ impl Interpreter { capture_types, namespace_aliases: self.visible_namespace_aliases(), namespace: self.current_namespace.clone(), + }; + if let Some(origin) = &self.active_reflected_capture_origin { + let proof = crate::value::closure_capture::CheckedClosureCapture::seal(origin, &value); + if let Value::Function { type_context, .. } = &mut value { + type_context.reflected_capture = proof; + } } + Ok(value) } fn eval_negation_flow(&mut self, operand: &Expr) -> Result { @@ -3491,6 +3631,17 @@ impl Interpreter { args: &[CallArg], call_span: Span, ) -> Result { + if let Some(transport) = &self.resource_transport { + if transport.checked_source_active { + if transport + .checked + .source_call_dispatch(callee, args, call_span) + .map_err(|error| error.to_string())? + { + return self.eval_resource_call(callee, type_args, args, call_span); + } + } + } // Only a fact at this actual result can be reused after the call; an // unrelated installed map must not alter metadata-free API validation. let checked_producer = self.checked_refinement_source_type(call_span).is_some(); @@ -3778,6 +3929,12 @@ impl Interpreter { }; let value = match flow { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => { + return Err( + "Resource operand has no checked ownership transport for this operation" + .to_string(), + ); + } ExprFlow::Signal(signal) => return Ok(ExprFlow::Signal(signal)), }; self.eval_pipeline_step_handle(value, handle, error_type) @@ -3991,17 +4148,21 @@ impl Interpreter { self.set_inferred_debug_binding(name, value, bind_type); } - let mut signal = None; - for stmt in &body.stmts { - if let Some(next) = self.exec_stmt_inner(stmt)? { - signal = Some(next); - break; + let result = (|| { + let mut signal = None; + for stmt in &body.stmts { + if let Some(next) = self.exec_stmt_inner(stmt)? { + signal = Some(next); + break; + } } - } + Ok::<_, String>(signal) + })(); self.pop_scope(); - - match signal { + self.check_resource_cleanup()?; + match result? { Some(Signal::Default(value)) => Ok(ExprFlow::Value(value)), + Some(Signal::ResourceDefault(value)) => Ok(ExprFlow::Resource(value)), Some(other) => Ok(ExprFlow::Signal(other)), None => Err("handle block must end with return or default".to_string()), } @@ -4012,6 +4173,30 @@ impl Interpreter { /// Execute a single statement. Returns `Ok(None)` for normal flow, or /// a [`Signal`] if control flow must be altered. fn exec_stmt_inner(&mut self, stmt: &Stmt) -> Result, String> { + if let (Some(transport), Stmt::VarDecl(declaration)) = (&self.resource_transport, stmt) { + let fact = transport + .checked + .binding(declaration.name.span) + .map_err(|error| error.to_string())?; + if transport + .checked + .type_contains_resource(fact.ty) + .map_err(|error| error.to_string())? + { + // A sealed named descriptor has a Resource signature but no + // physical Resource payload or capture environment. + if declaration.mutable + || fact.mutable + || transport + .checked + .prepare_named_callable(&declaration.value) + .map_err(|error| error.to_string())? + .is_none() + { + return self.exec_resource_binding(declaration); + } + } + } match stmt { Stmt::VarDecl(decl) => { let declared_ty = self.substitute_type_expr(&decl.ty); @@ -4028,6 +4213,7 @@ impl Interpreter { let target_type = self.debug_expression_type(target); let target_value = match self.eval_expr_flow(target)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err("Resource operand has no checked ownership transport for this operation".to_string()), ExprFlow::Signal(signal) => return Ok(Some(signal)), }; let sum_payload = matches!( @@ -4091,6 +4277,7 @@ impl Interpreter { }; match flow { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err("Resource operand has no checked ownership transport for this operation".to_string()), ExprFlow::Signal(signal) => return Ok(Some(signal)), } } @@ -4100,6 +4287,7 @@ impl Interpreter { .cloned(); let val = match self.eval_expr_flow(&decl.value)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err("Resource operand has no checked ownership transport for this operation".to_string()), ExprFlow::Signal(signal) => return Ok(Some(signal)), }; self.check_refinement_from_resolved_source( @@ -4113,6 +4301,7 @@ impl Interpreter { } else { match self.eval_expr_flow(&decl.value)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err("Resource operand has no checked ownership transport for this operation".to_string()), ExprFlow::Signal(signal) => return Ok(Some(signal)), } }; @@ -4124,8 +4313,20 @@ impl Interpreter { Stmt::ComptimeTypeBind(bind) => self.exec_comptime_type_bind(bind), Stmt::Assign(assign) => { + if self + .resource_transport + .as_ref() + .is_some_and(|transport| transport.checked_source_active) + && self.resource_type_at(assign.target.span())? + { + return self.exec_resource_assignment(assign); + } let val = match self.eval_expr_flow(&assign.value)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err( + "Resource operand has no checked ownership transport for this operation" + .to_string(), + ), ExprFlow::Signal(signal) => return Ok(Some(signal)), }; match &assign.target { @@ -4148,6 +4349,15 @@ impl Interpreter { } Stmt::Return(ret) => { + if let Some(transport) = self + .resource_transport + .as_mut() + .filter(|transport| transport.checked_source_active) + { + transport + .retire_return_operations() + .map_err(|error| error.to_string())?; + } let source_type = ret.value.as_ref().and_then(|expression| { self.checked_refinement_source_type(expression.span()) .cloned() @@ -4155,6 +4365,14 @@ impl Interpreter { let val = match &ret.value { Some(expr) => match self.eval_expr_flow(expr)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(mut value) => { + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .preserve_return(&mut value) + .map_err(|error| error.to_string())?; + return Ok(Some(Signal::ResourceReturn(value, source_type))); + } ExprFlow::Signal(signal) => return Ok(Some(signal)), }, None => Value::Nothing, @@ -4165,6 +4383,10 @@ impl Interpreter { Stmt::If(if_stmt) => { let cond = match self.eval_expr_flow(&if_stmt.condition)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err( + "Resource operand has no checked ownership transport for this operation" + .to_string(), + ), ExprFlow::Signal(signal) => return Ok(Some(signal)), }; if is_truthy(&cond)? { @@ -4173,6 +4395,7 @@ impl Interpreter { for (else_if_cond, else_if_block) in &if_stmt.else_ifs { let val = match self.eval_expr_flow(else_if_cond)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err("Resource operand has no checked ownership transport for this operation".to_string()), ExprFlow::Signal(signal) => return Ok(Some(signal)), }; if is_truthy(&val)? { @@ -4186,8 +4409,25 @@ impl Interpreter { } Stmt::For(for_stmt) => { - let reflected_field_bindings = - self.reflected_field_loop_bindings(&for_stmt.iterable)?; + let checked_field_loop = self.prepare_checked_resource_field_loop(for_stmt)?; + let reflected_field_bindings = match &checked_field_loop { + Some(proof) => { + let owner_type = proof.owner_name(); + Some( + proof + .fields() + .map(|field| ReflectedFieldBinding { + index: field.index, + owner_type: owner_type.clone(), + owner_member: None, + name: field.name.clone(), + ty: Self::reflection_type_info_type_expr(&field.type_info), + }) + .collect::>(), + ) + } + None => self.reflected_field_loop_bindings(&for_stmt.iterable)?, + }; let reflected_variant_bindings = self.reflected_variant_loop_bindings(&for_stmt.iterable)?; let reflected_machine_state_bindings = @@ -4199,20 +4439,52 @@ impl Interpreter { let reflected_type_info_bindings = self.reflected_type_info_arg_loop_bindings(&for_stmt.iterable)?; let item_types = self.debug_expression_args(&for_stmt.iterable); - let iterable = match self.eval_expr_flow(&for_stmt.iterable)? { + let iterable = match self.eval_checked_absent_for_iterable(for_stmt)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err( + "Resource operand has no checked ownership transport for this operation" + .to_string(), + ), ExprFlow::Signal(signal) => return Ok(Some(signal)), }; + let absent_bindings = self.prepare_absent_for_bindings(for_stmt, &iterable)?; match iterable.into_payload() { Value::List(items) => { + if let Some(proof) = &checked_field_loop { + proof + .validate_count(items.len()) + .map_err(|error| error.to_string())?; + } for (index, item) in items.into_iter().enumerate() { + // Every fallible proof join precedes lexical or debug mutation. + let checked_field_item = checked_field_loop + .as_ref() + .map(|proof| proof.iteration(index)) + .transpose() + .map_err(|error| error.to_string())?; + if let Some(proof) = &checked_field_item { + proof + .validate_value(&item) + .map_err(|error| error.to_string())?; + } + self.validate_absent_generated_debug_binding( + absent_bindings.as_ref(), + &for_stmt.variable, + 0, + &item, + )?; self.push_scope(); let loop_item = item.clone(); - self.set_inferred_debug_binding( + if let Err(error) = self.set_absent_generated_debug_binding( + absent_bindings.as_ref(), &for_stmt.variable, + 0, item, item_types.first(), - ); + ) { + self.pop_scope(); + return Err(error); + } let pushed_field_scope = reflected_field_bindings .as_ref() @@ -4282,7 +4554,14 @@ impl Interpreter { }) .is_some(); + let pushed_checked_field = checked_field_item.is_some(); + if let Some(proof) = checked_field_item { + self.checked_reflected_fields.push(proof); + } let signal = self.exec_block_inner(&for_stmt.body); + if pushed_checked_field { + self.checked_reflected_fields.pop(); + } if pushed_type_info_scope { self.reflected_type_info_scopes.pop(); } @@ -4331,17 +4610,42 @@ impl Interpreter { } Value::Map(entries) => { for (key, val) in entries { - self.push_scope(); - self.set_inferred_debug_binding( + self.validate_absent_generated_debug_binding( + absent_bindings.as_ref(), &for_stmt.variable, - key, - item_types.first(), - ); - if let Some(ref val_var) = for_stmt.value_variable { - self.set_inferred_debug_binding(val_var, val, item_types.get(1)); + 0, + &key, + )?; + if let Some(value) = &for_stmt.value_variable { + self.validate_absent_generated_debug_binding( + absent_bindings.as_ref(), + value, + 1, + &val, + )?; } - let signal = self.exec_block_inner(&for_stmt.body)?; + self.push_scope(); + let signal = (|| { + self.set_absent_generated_debug_binding( + absent_bindings.as_ref(), + &for_stmt.variable, + 0, + key, + item_types.first(), + )?; + if let Some(ref val_var) = for_stmt.value_variable { + self.set_absent_generated_debug_binding( + absent_bindings.as_ref(), + val_var, + 1, + val, + item_types.get(1), + )?; + } + self.exec_block_inner(&for_stmt.body) + })(); self.pop_scope(); + let signal = signal?; match signal { Some(Signal::Break) => break, Some(Signal::Continue) => continue, @@ -4381,6 +4685,7 @@ impl Interpreter { loop { let cond = match self.eval_expr_flow(&while_stmt.condition)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err("Resource operand has no checked ownership transport for this operation".to_string()), ExprFlow::Signal(signal) => return Ok(Some(signal)), }; if !is_truthy(&cond)? { @@ -4403,6 +4708,10 @@ impl Interpreter { let owner_type = self.debug_expression_type(&match_stmt.expr); let val = match self.eval_expr_flow(&match_stmt.expr)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err( + "Resource operand has no checked ownership transport for this operation" + .to_string(), + ), ExprFlow::Signal(signal) => return Ok(Some(signal)), }; let (variant_name, fields) = match val.payload() { @@ -4412,15 +4721,35 @@ impl Interpreter { _ => return Err(format!("match requires an enum value, got {val}")), }; - for arm in &match_stmt.arms { + for (arm_index, arm) in match_stmt.arms.iter().enumerate() { match &arm.pattern { Pattern::Ident(ident) => { if ident.name == variant_name { + self.prepare_absent_match_bindings(match_stmt, arm_index, &val)?; return self.exec_block_inner(&arm.body); } } Pattern::Variant(name, bindings) => { if name.name == variant_name { + let absent_bindings = self + .prepare_absent_match_bindings(match_stmt, arm_index, &val)?; + if absent_bindings + .as_ref() + .is_some_and(|proof| proof.binding_count() != fields.len()) + { + return Err("resource value lost its checked payload custody" + .to_string()); + } + for (index, (binding, field)) in + bindings.iter().zip(&fields).enumerate() + { + self.validate_absent_generated_debug_binding( + absent_bindings.as_ref(), + binding, + index, + field, + )?; + } self.push_scope(); let field_types = owner_type .as_ref() @@ -4429,11 +4758,16 @@ impl Interpreter { for (index, (binding, field_val)) in bindings.iter().zip(fields.iter()).enumerate() { - self.set_inferred_debug_binding( + if let Err(error) = self.set_absent_generated_debug_binding( + absent_bindings.as_ref(), binding, + index, field_val.clone(), field_types.get(index).map(|(_, ty)| ty), - ); + ) { + self.pop_scope(); + return Err(error); + } } let result = self.exec_block_inner(&arm.body); self.pop_scope(); @@ -4441,6 +4775,7 @@ impl Interpreter { } } Pattern::Other(_) => { + self.prepare_absent_match_bindings(match_stmt, arm_index, &val)?; return self.exec_block_inner(&arm.body); } } @@ -4455,12 +4790,24 @@ impl Interpreter { } Stmt::Expr(expr_stmt) => match self.eval_expr_flow(&expr_stmt.expr)? { ExprFlow::Value(_) => Ok(None), + ExprFlow::Resource(value) => { + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .discard_value(value) + .map_err(|error| error.to_string())?; + Ok(None) + } ExprFlow::Signal(signal) => Ok(Some(signal)), }, Stmt::Assert(assert_stmt) => { let cond = match self.eval_expr_flow(&assert_stmt.condition)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err( + "Resource operand has no checked ownership transport for this operation" + .to_string(), + ), ExprFlow::Signal(signal) => return Ok(Some(signal)), }; match cond { @@ -4470,6 +4817,11 @@ impl Interpreter { match self.eval_expr_flow(msg_expr)? { ExprFlow::Value(Value::String(s)) => s, ExprFlow::Value(other) => other.to_string(), + ExprFlow::Resource(_) => { + return Err( + "Resource cannot become an assertion message".to_string() + ); + } ExprFlow::Signal(signal) => return Ok(Some(signal)), } } else { @@ -4493,6 +4845,9 @@ impl Interpreter { ExprFlow::Value(other) => { return Err(format!("breakpoint condition must be bool, got {other}")); } + ExprFlow::Resource(_) => { + return Err("Resource cannot become a breakpoint condition".to_string()); + } ExprFlow::Signal(signal) => return Ok(Some(signal)), } } else { @@ -4528,6 +4883,9 @@ impl Interpreter { None | Some(Signal::Break) | Some(Signal::Continue) | Some(Signal::Return(..)) => { Ok(()) } + Some(Signal::ResourceReturn(..) | Signal::ResourceDefault(..)) => { + Err("raw statement API cannot export live Resource custody".to_string()) + } Some(Signal::Default(_)) => { Err("`default` can only be used inside a `handle` block".to_string()) } @@ -4542,22 +4900,27 @@ impl Interpreter { /// Execute a block (list of statements), propagating control-flow signals. fn exec_block_inner(&mut self, block: &Block) -> Result, String> { self.push_scope(); - let mut result = None; - for stmt in &block.stmts { - if let Some(signal) = self.exec_stmt_inner(stmt)? { - result = Some(signal); - break; + let result = (|| { + for stmt in &block.stmts { + if let Some(signal) = self.exec_stmt_inner(stmt)? { + return Ok(Some(signal)); + } } - } + Ok(None) + })(); self.pop_scope(); - Ok(result) + self.check_resource_cleanup()?; + result } /// Execute a block, returning the value produced by a `return` statement /// (if any). pub fn exec_block(&mut self, block: &Block) -> Result, String> { match self.exec_block_inner(block)? { - Some(Signal::Return(v, _)) => Ok(Some(v)), + Some(Signal::Return(v, _)) if !v.contains_live_resource_or_grant() => Ok(Some(v)), + Some(Signal::Return(..) | Signal::ResourceReturn(..) | Signal::ResourceDefault(..)) => { + Err("raw block API cannot export live Resource custody".to_string()) + } Some(Signal::Default(_)) => { Err("`default` can only be used inside a `handle` block".to_string()) } @@ -4643,6 +5006,12 @@ impl Interpreter { // Evaluate actor handle. let actor_val = match self.eval_expr_flow(actor_expr)? { ExprFlow::Value(v) => v, + ExprFlow::Resource(_) => { + return Err( + "Resource operand has no checked ownership transport for this operation" + .to_string(), + ); + } ExprFlow::Signal(s) => { return Err(format!("send/ask: actor expression returned signal: {s:?}")); } @@ -4658,6 +5027,10 @@ impl Interpreter { for arg in args { let val = match self.eval_expr_flow(&arg.value)? { ExprFlow::Value(v) => v, + ExprFlow::Resource(_) => return Err( + "Resource operand has no checked ownership transport for this operation" + .to_string(), + ), ExprFlow::Signal(s) => { return Err(format!("send/ask: arg expression returned signal: {s:?}")); } @@ -4738,6 +5111,11 @@ impl Interpreter { respond_value = val; break; } + Some(Signal::ResourceReturn(..) | Signal::ResourceDefault(..)) => { + return Err( + "actor Resource custody has no checked transport proof".to_string() + ); + } Some(Signal::Return(..)) => break, Some(Signal::Break) | Some(Signal::Continue) => break, Some(Signal::Default(_)) => break, @@ -8161,7 +8539,11 @@ impl Interpreter { }; let expected_type_name = type_expr_display(expected_field_ty); - if !self.reflection_type_name_matches_expr(&actual_type_name, expected_field_ty) { + if !self.reflection_type_name_matches_expr(&actual_type_name, expected_field_ty) + && !(checked_producer + && self.allow_checked_refinement_proofs + && self.has_checked_reflected_callable_plan(&actual_type_name, expected_field_ty)) + { return Err(format!( "type.field_value: field '{}' has type '{}', requested '{}'", metadata.name, actual_type_name, expected_type_name @@ -8611,6 +8993,10 @@ impl Interpreter { if !self.producer_type_has_refinement(&requested) { return Ok(value); } + if matches!(requested, TypeExpr::Function(..)) { + // New callable result proof is lazy; retain its original descriptor. + return self.prepare_reflected_callable(value, &requested, actual_type_name); + } let actual = Self::simple_type_expr_from_name(actual_type_name, requested.span()) .map(|actual| self.concrete_resolved_type_expr(&actual, &mut HashSet::new())); if matches!(&requested, TypeExpr::Named(_)) { @@ -12122,6 +12508,12 @@ impl Interpreter { /// Built-in standard library functions are checked first; if the name /// does not match a built-in, user-defined functions are consulted. pub fn call_function(&mut self, name: &str, args: Vec) -> Result { + if args.iter().any(Value::contains_live_resource_or_grant) { + return Err( + "raw call API cannot adopt Resource custody or runtime authority".to_string(), + ); + } + self.call_function_with_type_args(name, &[], args) } @@ -12133,6 +12525,12 @@ impl Interpreter { name: &str, args: Vec, ) -> Result { + if args.iter().any(Value::contains_live_resource_or_grant) { + return Err( + "raw call API cannot adopt Resource custody or runtime authority".to_string(), + ); + } + let saved_namespace = self.current_namespace.clone(); self.current_namespace = namespace.map(str::to_string); let runtime_name = self @@ -12156,6 +12554,12 @@ impl Interpreter { name: &str, args: Vec, ) -> Result { + if args.iter().any(Value::contains_live_resource_or_grant) { + return Err( + "raw call API cannot adopt Resource custody or runtime authority".to_string(), + ); + } + let saved_namespace = self.current_namespace.clone(); self.current_namespace = namespace.map(str::to_string); let runtime_name = self @@ -12286,7 +12690,7 @@ impl Interpreter { if let Some(fn_val) = self.get_variable(name).cloned() { if matches!( fn_val.payload(), - Value::Function { .. } | Value::NamedFunction(_) + Value::Function { .. } | Value::NamedFunction(_) | Value::ReflectedCallable(_) ) { return self.call_fn_value_from_source(fn_val, args, source_types); } @@ -12341,6 +12745,26 @@ impl Interpreter { format!("undefined function '{name}'") })? .clone(); + let original_capture_origin = + if crate::value::body_may_mint_inline_function(®istered.definition.body) { + self.checked_expression_types + .as_ref() + .and_then(|types| { + types.resource_program.as_ref().filter(|program| { + crate::value::closure_capture::original_root_types(program, types) + }) + }) + .and_then(|program| { + crate::value::closure_capture::OriginalClosureOwner::registered( + program, + &resolved_name, + ®istered.definition, + ®istered.namespace, + ) + }) + } else { + None + }; let func = registered.definition; if args.len() != func.params.len() { @@ -12353,6 +12777,15 @@ impl Interpreter { } let type_scope = self.type_scope_for_function(&func, type_args)?; + if let Some(interface) = self.interface_dispatch_name(name) { + self.validate_interface_argument_owners( + &interface, + &func, + registered.namespace.as_deref(), + &type_scope, + &args, + )?; + } let arguments: Vec = func .type_params .iter() @@ -12370,6 +12803,10 @@ impl Interpreter { let saved_type_arguments = std::mem::replace(&mut self.current_type_arguments, arguments); let saved_scoped_types = std::mem::take(&mut self.scoped_type_bindings); let saved_checked_scope = self.active_checked_scope.take(); + let saved_capture_origin = std::mem::replace( + &mut self.active_reflected_capture_origin, + original_capture_origin, + ); // Resolve arguments above in the caller, then keep its type bindings // out of the callee's lexical scope, including non-generic callees. let saved_type_scopes = std::mem::replace(&mut self.type_arg_scopes, vec![type_scope]); @@ -12383,6 +12820,10 @@ impl Interpreter { let saved_scope_floor = self.lexical_scope_floor; self.lexical_scope_floor = scope_depth; self.push_scope(); + let ordinary_return_context = self + .resource_transport + .as_mut() + .is_some_and(|transport| transport.enter_ordinary_return()); let saved_proofs = self.allow_checked_refinement_proofs; self.allow_checked_refinement_proofs &= source_types.is_some(); let source_types = source_types.filter(|_| self.allow_checked_refinement_proofs); @@ -12400,6 +12841,9 @@ impl Interpreter { let result = self.exec_block_inner(&func.body)?; let (mut value, return_source_type) = match result { Some(Signal::Return(value, source_type)) => (value, source_type), + Some(Signal::ResourceReturn(..) | Signal::ResourceDefault(..)) => { + return Err("raw call API cannot return Resource custody".to_string()); + } Some(Signal::Default(_)) => { return Err("`default` can only be used inside a `handle` block".to_string()); } @@ -12420,6 +12864,11 @@ impl Interpreter { Ok(value) })(); + if ordinary_return_context { + if let Some(transport) = self.resource_transport.as_mut() { + transport.leave_return(); + } + } self.allow_checked_refinement_proofs = saved_proofs; while self.scopes.len() > scope_depth { self.pop_scope(); @@ -12432,6 +12881,7 @@ impl Interpreter { self.active_checked_function = saved_expression_types; self.current_namespace = saved_namespace; self.current_function_trusted_stdlib = saved_trusted_stdlib; + self.active_reflected_capture_origin = saved_capture_origin; call_result } @@ -12717,18 +13167,44 @@ impl Interpreter { source_types: Option<&[Option]>, ) -> Result { match fn_val.into_payload() { + Value::ReflectedCallable(adapter) => { + self.call_reflected_callable(*adapter, args, source_types) + } Value::NamedFunction(name) => { self.call_registered_function_from_source(&name, &[], args, source_types) } - Value::Function { - type_context, - params, - body, - captures, - capture_types, - namespace_aliases, - namespace, - } => { + source @ Value::Function { .. } => { + let original_capture_origin = match &source { + Value::Function { body, .. } + if crate::value::body_may_mint_inline_function(body) => + { + self.checked_expression_types + .as_ref() + .and_then(|types| { + types.resource_program.as_ref().filter(|program| { + crate::value::closure_capture::original_root_types( + program, types, + ) + }) + }) + .and_then(|program| { + crate::value::closure_capture::invocation_origin(&source, program) + }) + } + _ => None, + }; + let Value::Function { + type_context, + params, + body, + captures, + capture_types, + namespace_aliases, + namespace, + } = source + else { + unreachable!("inline call matched its function value above"); + }; if args.len() != params.len() { return Err(format!( "inline function expects {} argument(s), got {}", @@ -12736,6 +13212,10 @@ impl Interpreter { args.len() )); } + let saved_capture_origin = std::mem::replace( + &mut self.active_reflected_capture_origin, + original_capture_origin, + ); let saved_expression_types = std::mem::replace( &mut self.active_checked_function, type_context.checked_function, @@ -12774,6 +13254,10 @@ impl Interpreter { self.set_namespace_alias(name, target); } self.push_scope(); + let ordinary_return_context = self + .resource_transport + .as_mut() + .is_some_and(|transport| transport.enter_ordinary_return()); let saved_proofs = self.allow_checked_refinement_proofs; self.allow_checked_refinement_proofs &= source_types.is_some(); let source_types = source_types.filter(|_| self.allow_checked_refinement_proofs); @@ -12792,6 +13276,12 @@ impl Interpreter { } let (mut value, return_source_type) = match self.exec_block_inner(&body)? { Some(Signal::Return(value, source_type)) => (value, source_type), + Some(Signal::ResourceReturn(..) | Signal::ResourceDefault(..)) => { + return Err( + "captured Resource return has no checked custody transport proof" + .to_string(), + ); + } Some(Signal::Default(_)) => { return Err( "`default` can only be used inside a `handle` block".to_string() @@ -12810,6 +13300,11 @@ impl Interpreter { } Ok(value) })(); + if ordinary_return_context { + if let Some(transport) = self.resource_transport.as_mut() { + transport.leave_return(); + } + } self.allow_checked_refinement_proofs = saved_proofs; while self.scopes.len() > scope_depth { self.pop_scope(); @@ -12822,6 +13317,7 @@ impl Interpreter { self.type_arg_scopes = saved_type_scopes; self.current_type_arguments = saved_type_arguments; self.active_checked_function = saved_expression_types; + self.active_reflected_capture_origin = saved_capture_origin; result } other => Err(format!("expected function value, got {other}")), @@ -12830,7 +13326,11 @@ impl Interpreter { fn resolve_interface_dispatch(&self, name: &str, args: &[Value]) -> Option { if let Some(interface) = self.interface_dispatch_name(name) { - let receiver_type = runtime_type_name(args.first()?)?; + let receiver = args.first()?; + if matches!(receiver.payload(), Value::Pending(_)) { + return None; + } + let receiver_type = runtime_type_name(receiver)?; return self.interface_methods[&interface] .get(&receiver_type) .cloned(); @@ -13967,6 +14467,9 @@ fn runtime_type_name(value: &Value) -> Option { Value::OptionalSome(_) | Value::OptionalNone => Some("optional".to_string()), Value::Nothing => Some("nothing".to_string()), Value::Capability(name) => Some(name.clone()), + Value::Resource(_) => None, + Value::ResourceHook(_) => Some("function".to_string()), + Value::GrantedNetwork(_) => Some("Network".to_string()), Value::TypeConstruction { .. } => Some("TypeConstruction".to_string()), Value::Struct { type_name, @@ -13979,7 +14482,9 @@ fn runtime_type_name(value: &Value) -> Option { Value::Pending(_) => Some("pending".to_string()), Value::Map(_) => Some("map".to_string()), Value::Set(_) => Some("set".to_string()), - Value::Function { .. } | Value::NamedFunction(_) => Some("function".to_string()), + Value::Function { .. } | Value::NamedFunction(_) | Value::ReflectedCallable(_) => { + Some("function".to_string()) + } } } @@ -16890,11 +17395,13 @@ mod tests { }]; let returned = reuse_source("input", 720); reuse_facts(&mut interpreter, &[(returned.span(), "int64")]); - let closure = interpreter.capture_closure( - &func_def("unused", vec![("input", "int64")], block(vec![])).params, - Some(&type_named("Field")), - &block(vec![return_stmt(returned)]), - ); + let closure = interpreter + .capture_closure( + &func_def("unused", vec![("input", "int64")], block(vec![])).params, + Some(&type_named("Field")), + &block(vec![return_stmt(returned)]), + ) + .unwrap(); let Value::Function { ref type_context, ref namespace_aliases, @@ -17172,7 +17679,9 @@ function main() returns string: ); function.params[0].ty = parameter_type.clone(); interpreter.register_function(&function); - let closure = interpreter.capture_closure(&function.params, None, &function.body); + let closure = interpreter + .capture_closure(&function.params, None, &function.body) + .unwrap(); let wrapped_fact = type_expr_display(¶meter_type); for (number, checked) in [(-1, false), (7, false), (7, true)] { let input = Value::Typed { @@ -17244,16 +17753,20 @@ function main() returns string: interpreter.set_namespace_alias("selected".into(), "models".into()); interpreter.set_namespace_alias("models".into(), "shadow".into()); let original_aliases = interpreter.visible_namespace_aliases(); - let positive = interpreter.capture_closure( - &func_def("unused", vec![("input", "int64")], block(vec![])).params, - Some(&type_named("selected.Positive")), - &block(vec![return_stmt(var("input"))]), - ); - let small = interpreter.capture_closure( - &[], - Some(&type_named("selected.Small")), - &block(vec![return_stmt(int(42))]), - ); + let positive = interpreter + .capture_closure( + &func_def("unused", vec![("input", "int64")], block(vec![])).params, + Some(&type_named("selected.Positive")), + &block(vec![return_stmt(var("input"))]), + ) + .unwrap(); + let small = interpreter + .capture_closure( + &[], + Some(&type_named("selected.Small")), + &block(vec![return_stmt(int(42))]), + ) + .unwrap(); for input in [7, -1] { let result = interpreter.call_fn_value_from_source( positive.clone(), @@ -17281,11 +17794,13 @@ function main() returns string: Ok(Value::Int64(42)) ); assert_eq!(interpreter.visible_namespace_aliases(), original_aliases); - let overflow = interpreter.capture_closure( - &[], - Some(&type_named("selected.Small")), - &block(vec![return_stmt(int(128))]), - ); + let overflow = interpreter + .capture_closure( + &[], + Some(&type_named("selected.Small")), + &block(vec![return_stmt(int(128))]), + ) + .unwrap(); assert!( interpreter .call_fn_value_from_source(overflow, vec![], Some(&[])) @@ -17414,11 +17929,13 @@ function main() returns string: let returned = reuse_source("input", 620); let params = func_def("unused", vec![("input", "models.Positive")], block(vec![])).params; reuse_facts(&mut interpreter, &[(returned.span(), "models.Positive")]); - let closure = interpreter.capture_closure( - ¶ms, - Some(&type_named("models.Positive")), - &block(vec![return_stmt(returned)]), - ); + let closure = interpreter + .capture_closure( + ¶ms, + Some(&type_named("models.Positive")), + &block(vec![return_stmt(returned)]), + ) + .unwrap(); for (input, facts, expected_markers, succeeds) in [ (7, Some(vec![Some("models.Positive".into())]), vec![], true), ( @@ -17464,11 +17981,13 @@ function main() returns string: let returned = reuse_source("input", 630); let params = func_def("unused", vec![("input", "models.Positive")], block(vec![])).params; reuse_facts(&mut interpreter, &[(returned.span(), "models.Positive")]); - let closure = interpreter.capture_closure( - ¶ms, - Some(&type_named("models.Large")), - &block(vec![return_stmt(returned)]), - ); + let closure = interpreter + .capture_closure( + ¶ms, + Some(&type_named("models.Large")), + &block(vec![return_stmt(returned)]), + ) + .unwrap(); let facts = [Some("models.Positive".into())]; for input in [7, 2] { let result = interpreter.call_fn_value_from_source( @@ -17524,16 +18043,20 @@ function main() returns string: }); interpreter.active_checked_function = Some(captured_function); interpreter.active_checked_scope = Some(captured_scope); - let successful = interpreter.capture_closure( - &[], - Some(&type_named("Field")), - &block(vec![return_stmt(returned.clone())]), - ); - let failed = interpreter.capture_closure( - &[], - Some(&type_named("Field")), - &block(vec![return_stmt(Expr::IntLiteral(2, returned.span()))]), - ); + let successful = interpreter + .capture_closure( + &[], + Some(&type_named("Field")), + &block(vec![return_stmt(returned.clone())]), + ) + .unwrap(); + let failed = interpreter + .capture_closure( + &[], + Some(&type_named("Field")), + &block(vec![return_stmt(Expr::IntLiteral(2, returned.span()))]), + ) + .unwrap(); let caller_function = Arc::new(CheckedFunctionTypes::default()); let caller_scope = Arc::new(CheckedScopedTypes::default()); interpreter.active_checked_function = Some(caller_function.clone()); @@ -17636,14 +18159,16 @@ function main() returns string: let mut params = func_def("unused", vec![("input", &canonical)], block(vec![])).params; params[0].view = true; reuse_facts(&mut interpreter, &[(returned.span(), &canonical)]); - let closure = interpreter.capture_closure( - ¶ms, - Some(&type_named(&canonical)), - &block(vec![return_stmt(Expr::Clone( - Box::new(returned), - Span::new(FileId::new(0), 651, 652), - ))]), - ); + let closure = interpreter + .capture_closure( + ¶ms, + Some(&type_named(&canonical)), + &block(vec![return_stmt(Expr::Clone( + Box::new(returned), + Span::new(FileId::new(0), 651, 652), + ))]), + ) + .unwrap(); reuse_facts( &mut interpreter, &[(Span::new(FileId::new(0), 651, 652), &canonical)], @@ -17698,11 +18223,13 @@ function main() returns string: }, ); reuse_facts(&mut interpreter, &[(returned.span(), "models.Positive")]); - let closure = interpreter.capture_closure( - &[], - Some(&type_named("models.Positive")), - &block(vec![return_stmt(returned)]), - ); + let closure = interpreter + .capture_closure( + &[], + Some(&type_named("models.Positive")), + &block(vec![return_stmt(returned)]), + ) + .unwrap(); for previously_enabled in [true, false] { interpreter.allow_checked_refinement_proofs = previously_enabled; assert_eq!( @@ -23119,6 +23646,7 @@ function joined(candidate: result[int64, string]) returns int64: checked_function: None, bindings: HashMap::from([("T".into(), type_named("int8"))]), arguments: Vec::new(), + reflected_capture: None, }), params: vec![Param { view: false, @@ -27167,3 +27695,7 @@ function main() returns nothing: ); } } + +#[cfg(test)] +#[path = "interpreter/actor_identity_tests.rs"] +mod actor_identity_tests; diff --git a/crates/jett_comptime/src/interpreter/actor_identity_tests.rs b/crates/jett_comptime/src/interpreter/actor_identity_tests.rs new file mode 100644 index 00000000..249daa62 --- /dev/null +++ b/crates/jett_comptime/src/interpreter/actor_identity_tests.rs @@ -0,0 +1,66 @@ +use super::*; + +fn actor(type_name: &str, state: i64) -> ActorInstance { + ActorInstance { + type_name: type_name.into(), + state: HashMap::from([("count".into(), Value::Int64(state))]), + capabilities: HashMap::new(), + } +} + +#[test] +fn actor_identity_uses_live_nominal_instances_without_reading_state() { + let mut interpreter = Interpreter::new(); + interpreter + .actor_instances + .insert(0, actor("app.Counter", 7)); + interpreter + .actor_instances + .insert(1, actor("app.Counter", 7)); + interpreter.actor_instances.insert(2, actor("app.Other", 7)); + assert_eq!(interpreter.actor_identity_equal(0, 0), Ok(true)); + assert_eq!(interpreter.actor_identity_equal(0, 1), Ok(false)); + interpreter + .actor_instances + .get_mut(&0) + .unwrap() + .state + .insert("count".into(), Value::Int64(88)); + assert_eq!(interpreter.actor_identity_equal(0, 0), Ok(true)); + assert_eq!(interpreter.actor_identity_equal(0, 1), Ok(false)); + assert_eq!( + interpreter.actor_identity_equal(0, 2), + Err("actor equality requires matching nominal actor types".into()) + ); + assert_eq!( + interpreter.actor_identity_equal(99, 99), + Err("actor equality: unknown actor instance #99".into()) + ); + interpreter.actor_instances.remove(&0); + assert_eq!( + interpreter.actor_identity_equal(0, 0), + Err("actor equality: unknown actor instance #0".into()) + ); +} + +#[test] +fn actor_identity_enum_payloads_validate_liveness_and_preserve_pending_depth() { + let mut interpreter = Interpreter::new(); + interpreter + .actor_instances + .insert(0, actor("app.Counter", 7)); + interpreter + .actor_instances + .insert(1, actor("app.Counter", 7)); + let same = Value::Actor(0); + let distinct = Value::Actor(1); + assert_eq!(interpreter.enum_payload_equal(&same, &same), Ok(true)); + assert_eq!(interpreter.enum_payload_equal(&same, &distinct), Ok(false)); + let pending = Value::Pending(Box::new(same.clone())); + assert_eq!(interpreter.enum_payload_equal(&pending, &pending), Ok(true)); + assert_eq!(interpreter.enum_payload_equal(&same, &pending), Ok(false)); + assert_eq!( + interpreter.enum_payload_equal(&Value::Actor(99), &Value::Actor(99)), + Err("actor equality: unknown actor instance #99".into()) + ); +} diff --git a/crates/jett_comptime/src/interpreter/interface_arguments.rs b/crates/jett_comptime/src/interpreter/interface_arguments.rs new file mode 100644 index 00000000..9cb265a5 --- /dev/null +++ b/crates/jett_comptime/src/interpreter/interface_arguments.rs @@ -0,0 +1,195 @@ +//! Read-only owner preflight before erased dispatch changes argument identities. +use super::{FunctionDef, HashMap, HashSet, Interpreter, TypeExpr, Value, runtime_type_name}; + +const OWNER_MISMATCH: &str = "interface dispatch concrete argument owner mismatch"; + +impl Interpreter { + pub(super) fn validate_interface_argument_owners( + &self, + _interface: &str, + function: &FunctionDef, + namespace: Option<&str>, + type_scope: &HashMap, + arguments: &[Value], + ) -> Result<(), String> { + // Dispatch already established the ready receiver's concrete identity. + let owner = arguments + .first() + .and_then(runtime_type_name) + .ok_or(OWNER_MISMATCH)?; + for (parameter, argument) in function.params.iter().zip(arguments) { + let written = self.interface_parameter_type(¶meter.ty, type_scope, namespace); + let expected = self.concrete_resolved_type_expr(&written, &mut HashSet::new()); + self.validate_interface_owner_occurrence(&expected, &owner, argument)?; + } + Ok(()) + } + + fn interface_parameter_type( + &self, + written: &TypeExpr, + type_scope: &HashMap, + namespace: Option<&str>, + ) -> TypeExpr { + match written { + TypeExpr::Named(name) => type_scope.get(&name.name).cloned().unwrap_or_else(|| { + TypeExpr::Named(self.qualify_declared_type_ident(name, namespace)) + }), + TypeExpr::Generic(name, children, span) => TypeExpr::Generic( + self.qualify_declared_type_ident(name, namespace), + children + .iter() + .map(|child| self.interface_parameter_type(child, type_scope, namespace)) + .collect(), + *span, + ), + TypeExpr::View(inner, span) => TypeExpr::View( + Box::new(self.interface_parameter_type(inner, type_scope, namespace)), + *span, + ), + TypeExpr::StateQualified(inner, state, span) => TypeExpr::StateQualified( + Box::new(self.interface_parameter_type(inner, type_scope, namespace)), + state.clone(), + *span, + ), + TypeExpr::Function(parameters, output, span) => TypeExpr::Function( + parameters + .iter() + .map(|parameter| { + self.interface_parameter_type(parameter, type_scope, namespace) + }) + .collect(), + Box::new(self.interface_parameter_type(output, type_scope, namespace)), + *span, + ), + } + } + + fn interface_type_contains_owner(&self, expected: &TypeExpr, owner: &str) -> bool { + if self.resolved_concrete_type_display(expected) == owner { + return true; + } + match expected { + TypeExpr::Named(_) => false, + TypeExpr::Generic(_, children, _) => children + .iter() + .any(|child| self.interface_type_contains_owner(child, owner)), + TypeExpr::View(inner, _) | TypeExpr::StateQualified(inner, _, _) => { + self.interface_type_contains_owner(inner, owner) + } + // A checked callable adapter validates its arguments when invoked; + // the callable itself is not an occurrence of its argument values. + TypeExpr::Function(_, _, _) => false, + } + } + + fn validate_interface_owner_occurrence( + &self, + expected: &TypeExpr, + owner: &str, + argument: &Value, + ) -> Result<(), String> { + if let TypeExpr::View(inner, _) = expected { + return self.validate_interface_owner_occurrence(inner, owner, argument); + } + let expected_name = self.resolved_concrete_type_display(expected); + if expected_name == owner { + // Reading retained identity does not join a task or inspect fields. + return if retained_argument_owner(argument).as_deref() == Some(owner) { + Ok(()) + } else { + Err(OWNER_MISMATCH.into()) + }; + } + if !self.interface_type_contains_owner(expected, owner) { + return Ok(()); + } + if let Value::Pending(_) = argument { + // An unavailable container cannot supply new child-owner facts. + return if retained_argument_owner(argument).as_deref() == Some(expected_name.as_str()) { + Ok(()) + } else { + Err(OWNER_MISMATCH.into()) + }; + } + match (expected, argument.payload()) { + (TypeExpr::Generic(name, children, _), value) + if matches!( + name.name.as_str(), + "secret" | "list" | "set" | "optional" | "result" | "map" + ) => + { + self.validate_interface_owner_children(&name.name, children, owner, value) + } + (TypeExpr::StateQualified(inner, _, _), value) => { + self.validate_interface_owner_occurrence(inner, owner, value) + } + // Nominal generic records are invariant. Their retained identity + // proves all arguments without reinterpreting or inspecting fields. + _ if retained_argument_owner(argument).as_deref() == Some(expected_name.as_str()) => { + Ok(()) + } + _ => Err(OWNER_MISMATCH.into()), + } + } + + fn validate_interface_owner_children( + &self, + constructor: &str, + children: &[TypeExpr], + owner: &str, + value: &Value, + ) -> Result<(), String> { + match (constructor, children, value) { + ("secret", [inner], value) => { + self.validate_interface_owner_occurrence(inner, owner, value) + } + ("list", [element], Value::List(elements)) + | ("set", [element], Value::Set(elements)) => { + for value in elements { + self.validate_interface_owner_occurrence(element, owner, value)?; + } + Ok(()) + } + ("optional", [element], Value::OptionalSome(value)) + | ("result", [element, _], Value::ResultOk(value)) + | ("result", [_, element], Value::ResultFail(value)) => { + self.validate_interface_owner_occurrence(element, owner, value) + } + ("optional", [_], Value::OptionalNone) => Ok(()), + ("map", [key_type, value_type], Value::Map(entries)) => { + for (key, value) in entries { + self.validate_interface_owner_occurrence(key_type, owner, key)?; + self.validate_interface_owner_occurrence(value_type, owner, value)?; + } + Ok(()) + } + _ => { + let expected = format!( + "{}[{}]", + constructor, + children + .iter() + .map(|child| self.resolved_concrete_type_display(child)) + .collect::>() + .join(", ") + ); + if retained_argument_owner(value).as_deref() == Some(expected.as_str()) { + Ok(()) + } else { + Err(OWNER_MISMATCH.into()) + } + } + } + } +} + +fn retained_argument_owner(value: &Value) -> Option { + match value { + Value::Pending(inner) => retained_argument_owner(inner), + value => runtime_type_name(value), + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_comptime/src/interpreter/interface_arguments/tests.rs b/crates/jett_comptime/src/interpreter/interface_arguments/tests.rs new file mode 100644 index 00000000..4b7ae539 --- /dev/null +++ b/crates/jett_comptime/src/interpreter/interface_arguments/tests.rs @@ -0,0 +1,290 @@ +use super::*; +use crate::debug::render_debug_events; +use jett_common::FileId; + +const ORIGINAL_WRONG: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/native/erased_same_owner/05_erased_other_owner.jett" +)); +const ORIGINAL_MATCHING: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/native/erased_same_owner/12_erased_matching_owner.jett" +)); + +fn interpreter(source: &str) -> Interpreter { + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let mut interpreter = Interpreter::new(); + interpreter.register_module(&parsed.module); + interpreter +} + +fn named(name: &str) -> TypeExpr { + TypeExpr::Named(super::super::Ident { + name: name.into(), + span: super::super::Span::new(FileId::new(0), 0, 0), + }) +} + +fn generic(name: &str, arguments: Vec) -> TypeExpr { + TypeExpr::Generic( + super::super::Ident { + name: name.into(), + span: super::super::Span::new(FileId::new(0), 0, 0), + }, + arguments, + super::super::Span::new(FileId::new(0), 0, 0), + ) +} + +fn record(owner: &str) -> Value { + Value::Struct { + type_name: owner.into(), + concrete_type: None, + fields: vec![("name".into(), Value::String("label".into()))], + } +} + +#[test] +fn erased_owner_original_wrong_source_fails_before_result_print() { + let mut interpreter = interpreter(ORIGINAL_WRONG); + assert_eq!( + interpreter.call_function("app.main", vec![]), + Err(OWNER_MISMATCH.into()) + ); + assert!(interpreter.take_debug_events().is_empty()); + assert_eq!(interpreter.lexical_scope_floor, 0); + assert_eq!(interpreter.current_namespace, None); +} + +#[test] +fn erased_owner_original_matching_source_keeps_views_and_result_print() { + let mut interpreter = interpreter(ORIGINAL_MATCHING); + assert_eq!( + interpreter.call_function("app.main", vec![]), + Ok(Value::Nothing) + ); + assert_eq!( + render_debug_events(&interpreter.take_debug_events()), + "left:one:two\n" + ); +} + +#[test] +fn erased_owner_direct_and_named_callback_reject_before_body_entry() { + let source = ORIGINAL_WRONG.replace( + "return \"left:{self.name}:{other.name}\"", + "println(\"entered-left-body\")\n return \"left:{self.name}:{other.name}\"", + ); + for callback in [false, true] { + let mut interpreter = interpreter(&source); + let args = vec![record("app.Left"), record("app.Right")]; + let outcome = if callback { + interpreter.call_fn_value(Value::NamedFunction("app.Pairable.combine".into()), args) + } else { + interpreter.call_function("app.Pairable.combine", args) + }; + assert_eq!(outcome, Err(OWNER_MISMATCH.into())); + assert!(interpreter.take_debug_events().is_empty()); + assert_eq!(interpreter.current_namespace, None); + assert_eq!(interpreter.lexical_scope_floor, 0); + } +} + +#[test] +fn erased_owner_owned_formals_validate_before_installing_any_formal() { + let source = ORIGINAL_WRONG + .replace("view self:", "self:") + .replace("view other:", "other:"); + let mut interpreter = interpreter(&source); + let scopes = interpreter.scopes.len(); + assert_eq!( + interpreter.call_function( + "app.Pairable.combine", + vec![record("app.Left"), record("app.Right")] + ), + Err(OWNER_MISMATCH.into()) + ); + assert_eq!(interpreter.scopes.len(), scopes); + assert!(interpreter.take_debug_events().is_empty()); +} + +#[test] +fn erased_owner_primitive_widths_and_nominal_refinements_are_exact() { + let interpreter = interpreter(ORIGINAL_MATCHING); + for owner in ["int8", "app.Positive"] { + let wrong = Value::Int64(7); + let right = Value::Typed { + type_name: owner.into(), + value: Box::new(wrong.clone()), + }; + assert_eq!( + interpreter.validate_interface_owner_occurrence(&named(owner), owner, &right), + Ok(()) + ); + assert_eq!( + interpreter.validate_interface_owner_occurrence(&named(owner), owner, &wrong), + Err(OWNER_MISMATCH.into()) + ); + assert_eq!(runtime_type_name(&right).as_deref(), Some(owner)); + } +} + +#[test] +fn erased_owner_generic_record_arguments_keep_full_nominal_identity() { + let interpreter = interpreter(ORIGINAL_MATCHING); + let owner = "app.Box[int64]"; + let mut actual = record("app.Box"); + if let Value::Struct { concrete_type, .. } = &mut actual { + *concrete_type = Some(owner.into()); + } + let expected = generic("app.Box", vec![named("int64")]); + assert_eq!( + interpreter.validate_interface_owner_occurrence(&expected, owner, &actual), + Ok(()) + ); + if let Value::Struct { concrete_type, .. } = &mut actual { + *concrete_type = Some("app.Box[string]".into()); + } + assert_eq!( + interpreter.validate_interface_owner_occurrence(&expected, owner, &actual), + Err(OWNER_MISMATCH.into()) + ); +} + +#[test] +fn erased_owner_nested_occupied_payloads_require_matching_owners() { + let interpreter = interpreter(ORIGINAL_MATCHING); + let owner = "app.Left"; + let expected = generic("list", vec![generic("optional", vec![named(owner)])]); + for actual in [ + Value::List(vec![Value::OptionalSome(Box::new(record(owner)))]), + Value::List(vec![Value::OptionalNone]), + Value::List(vec![]), + ] { + assert_eq!( + interpreter.validate_interface_owner_occurrence(&expected, owner, &actual), + Ok(()) + ); + } + let wrong = Value::List(vec![Value::OptionalSome(Box::new(record("app.Right")))]); + assert_eq!( + interpreter.validate_interface_owner_occurrence(&expected, owner, &wrong), + Err(OWNER_MISMATCH.into()) + ); +} + +#[test] +fn erased_owner_map_values_and_active_result_arms_are_checked() { + let interpreter = interpreter(ORIGINAL_MATCHING); + let owner = "app.Left"; + let expected = generic( + "map", + vec![ + named("string"), + generic("result", vec![named(owner), named("string")]), + ], + ); + let unchanged_error = Value::Map(vec![( + Value::String("key".into()), + Value::ResultFail(Box::new(Value::String("earlier".into()))), + )]); + assert_eq!( + interpreter.validate_interface_owner_occurrence(&expected, owner, &unchanged_error), + Ok(()) + ); + let wrong = Value::Map(vec![( + Value::String("key".into()), + Value::ResultOk(Box::new(record("app.Right"))), + )]); + assert_eq!( + interpreter.validate_interface_owner_occurrence(&expected, owner, &wrong), + Err(OWNER_MISMATCH.into()) + ); +} + +#[test] +fn erased_owner_pending_receiver_keeps_canonical_dispatch_failure() { + let mut interpreter = interpreter(ORIGINAL_MATCHING); + let pending = Value::Pending(Box::new(record("app.Left"))); + let typed_pending = Value::Typed { + type_name: "app.Left".into(), + value: Box::new(pending.clone()), + }; + for receiver in [pending, typed_pending] { + for callback in [false, true] { + let args = vec![receiver.clone(), record("app.Right")]; + let actual = if callback { + interpreter.call_fn_value(Value::NamedFunction("app.Pairable.combine".into()), args) + } else { + interpreter.call_function("app.Pairable.combine", args) + }; + assert_eq!( + actual, + Err("undefined function 'app.Pairable.combine'".into()) + ); + } + } + assert!(interpreter.take_debug_events().is_empty()); +} + +#[test] +fn erased_owner_pending_argument_uses_identity_without_joining_payload() { + let interpreter = interpreter(ORIGINAL_MATCHING); + let owner = "app.Left"; + let matching = Value::Pending(Box::new(record(owner))); + let wrong = Value::Pending(Box::new(record("app.Right"))); + assert_eq!( + interpreter.validate_interface_owner_occurrence(&named(owner), owner, &matching), + Ok(()) + ); + assert_eq!( + interpreter.validate_interface_owner_occurrence(&named(owner), owner, &wrong), + Err(OWNER_MISMATCH.into()) + ); + assert!(matches!(matching, Value::Pending(_))); + assert!(matches!(wrong, Value::Pending(_))); +} + +#[test] +fn erased_owner_preflight_does_not_expand_caller_namespace_aliases() { + let mut interpreter = interpreter(ORIGINAL_MATCHING); + let parsed = jett_parser::parse( + "namespace fake\nstruct Left:\n name: string\n", + FileId::new(1), + ); + assert!(parsed.errors.is_empty()); + interpreter.register_module(&parsed.module); + interpreter + .namespace_alias_scopes + .last_mut() + .unwrap() + .insert("app".into(), "fake".into()); + let declared = named("app.Left"); + assert_eq!( + interpreter.resolved_concrete_type_display(&interpreter.interface_parameter_type( + &declared, + &HashMap::new(), + Some("app") + )), + "app.Left" + ); + let declared = named("Left"); + assert_eq!( + interpreter.resolved_concrete_type_display(&interpreter.interface_parameter_type( + &declared, + &HashMap::new(), + Some("app") + )), + "app.Left" + ); + assert_eq!( + interpreter + .namespace_alias_scopes + .last() + .unwrap() + .get("app") + .map(String::as_str), + Some("fake") + ); +} diff --git a/crates/jett_comptime/src/interpreter/reflected_callables.rs b/crates/jett_comptime/src/interpreter/reflected_callables.rs new file mode 100644 index 00000000..3e9db29b --- /dev/null +++ b/crates/jett_comptime/src/interpreter/reflected_callables.rs @@ -0,0 +1,230 @@ +//! A checked reflected read owns an adapter; its future result owns the check. +use super::*; +use crate::value::ReflectedCallableValue; + +#[cfg(test)] +mod tests; + +impl Interpreter { + fn authenticate_current_reflected_source( + &self, + value: &Value, + program: &Arc, + actual: jett_types::TypeId, + ) -> Result<(), String> { + if !self + .checked_expression_types + .as_ref() + .is_some_and(|installed| { + installed + .resource_program + .as_ref() + .is_some_and(|current| Arc::ptr_eq(current, program)) + && crate::value::closure_capture::original_root_types(program, installed) + }) + { + return Err( + "reflected callback checked expression context changed its original source".into(), + ); + } + ReflectedCallableValue::authenticate_source(value, program, actual)?; + match value.payload() { + Value::Pending(inner) => { + self.authenticate_current_reflected_source(inner, program, actual) + } + Value::NamedFunction(name) => { + let current = self + .functions + .get(name) + .ok_or("reflected callback original function is not registered")?; + if crate::value::closure_capture::OriginalClosureOwner::registered( + program, + name, + ¤t.definition, + ¤t.namespace, + ) + .is_none() + { + return Err( + "reflected callback current function changed its original source".into(), + ); + } + Ok(()) + } + Value::ReflectedCallable(admitted) => self.authenticate_current_reflected_source( + admitted.source(), + program, + admitted.source_type(), + ), + Value::Function { .. } => Ok(()), + _ => Err("reflected callback source is not a callable descriptor".into()), + } + } + + pub(super) fn has_checked_reflected_callable_plan( + &self, + actual_type_name: &str, + requested: &TypeExpr, + ) -> bool { + if !matches!(requested, TypeExpr::Function(..)) { + return false; + } + let Some(program) = self + .checked_expression_types + .as_ref() + .and_then(|types| types.resource_program.as_ref()) + else { + return false; + }; + let checked = program.checked(); + if !self + .reflection_metadata + .as_ref() + .is_some_and(|metadata| Arc::ptr_eq(metadata, &checked.reflection_metadata)) + { + return false; + } + let actual = checked + .reflection_metadata + .source_type_id_for_name(actual_type_name); + let target = checked + .reflection_metadata + .source_type_id_for_name(&type_expr_display(requested)); + matches!((actual, target), (Some(actual), Some(target)) + if checked.reflected_callable_plan(actual, target).is_some()) + } + + pub(super) fn prepare_reflected_callable( + &self, + value: Value, + requested: &TypeExpr, + actual_type_name: &str, + ) -> Result { + let program = self + .checked_expression_types + .as_ref() + .and_then(|types| types.resource_program.as_ref()) + .ok_or_else(|| { + "reflected field refinement: unsupported callable or nominal refinement conversion" + .to_string() + })?; + let checked = program.checked(); + if !self + .reflection_metadata + .as_ref() + .is_some_and(|metadata| Arc::ptr_eq(metadata, &checked.reflection_metadata)) + { + return Err("reflected callback metadata belongs to another checked program".into()); + } + let requested_name = type_expr_display(requested); + let actual = checked + .reflection_metadata + .source_type_id_for_name(actual_type_name) + .ok_or_else(|| "reflected callback has no declared source signature".to_string())?; + let target = checked + .reflection_metadata + .source_type_id_for_name(&requested_name) + .ok_or_else(|| "reflected callback has no checked requested signature".to_string())?; + self.authenticate_current_reflected_source(&value, program, actual)?; + if actual == target { + // Authenticate the retained descriptor, then transport its exact + // wrappers without invoking, joining, or establishing a new proof. + return Ok(value); + } + let plan = checked + .reflected_callable_plan(actual, target) + .ok_or_else(|| { + "reflected field refinement: unsupported callable or nominal refinement conversion" + .to_string() + })?; + let source_return = Self::simple_type_expr_from_name( + &checked.interner.type_name(plan.source_return()), + requested.span(), + ) + .ok_or_else(|| { + "reflected callback source return has no exact type expression".to_string() + })?; + let requested_return = Self::simple_type_expr_from_name( + &checked.interner.type_name(plan.requested_return()), + requested.span(), + ) + .ok_or_else(|| { + "reflected callback requested return has no exact type expression".to_string() + })?; + Ok(Value::ReflectedCallable(Box::new( + ReflectedCallableValue::new( + value, + program.clone(), + actual, + target, + source_return, + requested_return, + )?, + ))) + } + + pub(super) fn call_reflected_callable( + &mut self, + adapter: ReflectedCallableValue, + args: Vec, + source_types: Option<&[Option]>, + ) -> Result { + let current = self + .checked_expression_types + .as_ref() + .and_then(|types| types.resource_program.as_ref()) + .ok_or_else(|| { + "reflected callback invocation has no original checked program".to_string() + })?; + if !Arc::ptr_eq(current, adapter.checked_program()) { + return Err("reflected callback invocation belongs to another checked program".into()); + } + if !adapter.checked_plan().validate(current.checked()) { + return Err("reflected callback invocation changed its checked return plan".into()); + } + self.authenticate_current_reflected_source( + adapter.source(), + current, + adapter.source_type(), + )?; + // Retain the original Value, source signature and closure context. A read + // never calls it. Its own call boundary performs the source validations. + let mut result = + self.call_fn_value_from_source(adapter.source().clone(), args, source_types)?; + let actual_name = type_expr_display(adapter.source_return()); + let requested = adapter.requested_return(); + let mut checks = Vec::new(); + self.plan_produced_refinements( + requested, + Some(adapter.source_return()), + &result, + &mut Vec::new(), + &mut checks, + )?; + // Finish every occupied shape/pending preflight before the first new + // predicate. Do not inspect or implicitly join a pending result. + for check in &checks { + let slot = Self::produced_value_slot(&mut result, &check.path).ok_or_else(|| { + "reflected callback return shape changed during preflight".to_string() + })?; + if matches!(slot.payload(), Value::Pending(_)) { + return Err("reflected callback return: expected a ready value".into()); + } + } + let saved_proofs = self.allow_checked_refinement_proofs; + // The opaque adapter retains an authenticated program/type plan even + // when an ordinary higher-order helper has no source argument map. + self.allow_checked_refinement_proofs = true; + let output = self + .establish_reflected_field_refinement(result, requested, &actual_name, true) + .and_then(|value| { + self.normalize_and_validate_value_from_source( + requested, + value, + Some(&type_expr_display(requested)), + ) + }); + self.allow_checked_refinement_proofs = saved_proofs; + output + } +} diff --git a/crates/jett_comptime/src/interpreter/reflected_callables/tests.rs b/crates/jett_comptime/src/interpreter/reflected_callables/tests.rs new file mode 100644 index 00000000..8c88f23e --- /dev/null +++ b/crates/jett_comptime/src/interpreter/reflected_callables/tests.rs @@ -0,0 +1,704 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; + +const SOURCE: &str = r#"namespace app +function positive_check(value: int64) returns bool: + string marker = "predicate" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function requested(view callback: function() returns Positive) returns nothing: + return nothing +function five() returns int64: + string marker = "source" + trace marker + return 5 +function zero() returns int64: + string marker = "source" + trace marker + return 0 +function delayed() returns int64: + string marker = "source" + trace marker + return run 5 +function captured(offset: int64) returns function() returns int64: + function() returns int64 callback = function() returns int64: + string marker = "source" + trace marker + return offset + return callback +function requested_parameter(view callback: function(int64) returns Positive) returns nothing: + return nothing +function captured_parameter(offset: int64) returns function(int64) returns int64: + function(int64) returns int64 callback = function(value: int64) returns int64: + string marker = "source" + trace marker + return value + offset + return callback +function primitive_step(value: int64) returns int64: + string marker = "primitive" + trace marker + return value + 1 +function captured_after_primitive(offset: int64) returns function() returns int64: + int64 adjusted = primitive_step(offset) + function() returns int64 callback = function() returns int64: + string marker = "source" + trace marker + return adjusted + return callback +function captured_factory() returns function(int64) returns function() returns int64: + function(int64) returns function() returns int64 factory = function(offset: int64) returns function() returns int64: + int64 adjusted = primitive_step(offset) + function() returns int64 callback = function() returns int64: + string marker = "source" + trace marker + return adjusted + return callback + return factory +function forward_factory(view factory: function(int64) returns function() returns int64, offset: int64) returns function() returns int64: + return factory(offset) +"#; + +fn prepared() -> (Interpreter, Arc, TypeExpr) { + let file = FileId::new(0); + let program = Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(SOURCE, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions::default(), + ) + .unwrap(), + ); + let mut interpreter = Interpreter::new(); + interpreter.register_module(program.module()); + interpreter.set_reflection_metadata(program.checked().reflection_metadata.clone()); + interpreter.set_checked_expression_types(Arc::new(CheckedExpressionTypes { + resource_program: Some(program.clone()), + expressions: program + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, program.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + })); + let requested = Interpreter::simple_type_expr_from_name( + "function() returns app.Positive", + Span::new(file, 0, 0), + ) + .unwrap(); + (interpreter, program, requested) +} + +fn adapter(interpreter: &Interpreter, source: Value, requested: &TypeExpr) -> Value { + interpreter + .prepare_reflected_callable(source, requested, "function() returns int64") + .unwrap() +} + +fn traces(interpreter: &mut Interpreter) -> Vec { + interpreter + .take_debug_events() + .into_iter() + .map(|event| event.text) + .collect() +} + +#[test] +fn reflected_callable_field_relation_requires_same_checked_metadata_and_exact_signature_plan() { + let (mut interpreter, _, requested) = prepared(); + assert!( + interpreter.has_checked_reflected_callable_plan("function() returns int64", &requested) + ); + assert!( + !interpreter + .has_checked_reflected_callable_plan("function(int64) returns bool", &requested) + ); + let (_, foreign, _) = prepared(); + interpreter.set_reflection_metadata(foreign.checked().reflection_metadata.clone()); + assert!( + !interpreter.has_checked_reflected_callable_plan("function() returns int64", &requested) + ); + assert!(traces(&mut interpreter).is_empty()); +} + +#[test] +fn reflected_callable_read_is_lazy_and_each_call_owns_one_new_result_check() { + let (mut interpreter, _, requested) = prepared(); + let observed = adapter( + &interpreter, + Value::NamedFunction("app.five".into()), + &requested, + ); + assert!(traces(&mut interpreter).is_empty()); + for _ in 0..2 { + let result = interpreter + .call_fn_value(observed.clone(), Vec::new()) + .unwrap(); + assert_eq!(result.payload(), &Value::Int64(5)); + assert_eq!( + traces(&mut interpreter), + [ + "trace marker: string = source\n", + "trace marker: string = predicate\n", + ] + ); + } +} + +#[test] +fn reflected_callable_keeps_captured_environment_and_original_debug_identity() { + let (mut interpreter, _, requested) = prepared(); + let source = interpreter + .call_function("app.captured", vec![Value::Int64(7)]) + .unwrap(); + let label = source.to_string(); + let observed = adapter(&interpreter, source, &requested); + assert_eq!(observed.to_string(), label); + assert!(traces(&mut interpreter).is_empty()); + let result = interpreter.call_fn_value(observed, Vec::new()).unwrap(); + assert_eq!(result.payload(), &Value::Int64(7)); + assert_eq!(traces(&mut interpreter).len(), 2); +} + +#[test] +fn reflected_callable_failed_or_pending_result_never_publishes_target_proof() { + let (mut interpreter, _, requested) = prepared(); + let failed = adapter( + &interpreter, + Value::NamedFunction("app.zero".into()), + &requested, + ); + let error = interpreter.call_fn_value(failed, Vec::new()).unwrap_err(); + assert!( + error.contains("refinement type constraint failed for 'app.Positive'"), + "{error}" + ); + assert_eq!(traces(&mut interpreter).len(), 2); + let pending = adapter( + &interpreter, + Value::NamedFunction("app.delayed".into()), + &requested, + ); + assert_eq!( + interpreter.call_fn_value(pending, Vec::new()).unwrap_err(), + "reflected callback return: expected a ready value" + ); + assert_eq!( + traces(&mut interpreter), + ["trace marker: string = source\n"] + ); +} + +#[test] +fn reflected_callable_exact_pending_transport_and_foreign_program_refusal_stay_distinct() { + let (mut interpreter, program, requested) = prepared(); + let value = Value::Pending(Box::new(Value::NamedFunction("app.five".into()))); + let exact = + Interpreter::simple_type_expr_from_name("function() returns int64", requested.span()) + .unwrap(); + let unchanged = interpreter + .prepare_reflected_callable(value.clone(), &exact, "function() returns int64") + .unwrap(); + assert_eq!(unchanged.to_string(), value.to_string()); + assert!(!matches!(unchanged, Value::ReflectedCallable(_))); + assert!(traces(&mut interpreter).is_empty()); + assert!( + interpreter + .prepare_reflected_callable(value, &requested, "function() returns app.Positive") + .unwrap_err() + .contains("original source declaration") + ); + let observed = adapter( + &interpreter, + Value::NamedFunction("app.five".into()), + &requested, + ); + let (mut foreign, other, _) = prepared(); + assert!(!Arc::ptr_eq(&program, &other)); + assert_eq!( + foreign.call_fn_value(observed, Vec::new()).unwrap_err(), + "reflected callback invocation belongs to another checked program" + ); + assert!(traces(&mut foreign).is_empty()); + assert!( + interpreter + .prepare_reflected_callable(Value::Int64(5), &requested, "function() returns int64",) + .unwrap_err() + .contains("not a callable descriptor") + ); +} + +fn refuse_forged_capture(interpreter: &mut Interpreter, source: Value, requested: &TypeExpr) { + let error = interpreter + .prepare_reflected_callable(source, requested, "function() returns int64") + .unwrap_err(); + assert!( + error.contains("unchanged original closure capture"), + "{error}" + ); + assert!(traces(interpreter).is_empty()); +} + +#[test] +fn reflected_closure_same_span_body_and_header_forgery_refuse_before_effects() { + let (mut interpreter, program, requested) = prepared(); + let genuine = interpreter + .call_function("app.captured", vec![Value::Int64(7)]) + .unwrap(); + let mut forged = genuine.clone(); + let Value::Function { body, .. } = &mut forged else { + panic!("expected closure"); + }; + let Stmt::Return(statement) = body.stmts.last_mut().unwrap() else { + panic!("expected return"); + }; + let span = statement.value.as_ref().unwrap().span(); + statement.value = Some(Expr::IntLiteral(11, span)); + refuse_forged_capture(&mut interpreter, forged, &requested); + + let mut forged = genuine.clone(); + let Value::Function { params, .. } = &mut forged else { + panic!("expected closure"); + }; + let original_parameter = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "captured" => { + function.params.first().cloned() + } + _ => None, + }) + .unwrap(); + params.push(original_parameter); + refuse_forged_capture(&mut interpreter, forged, &requested); + + let mut forged = genuine.clone(); + let Value::Function { type_context, .. } = &mut forged else { + panic!("expected closure"); + }; + let TypeExpr::Named(name) = type_context.return_type.as_mut().unwrap() else { + panic!("expected named return"); + }; + name.name = "bool".into(); + refuse_forged_capture(&mut interpreter, forged, &requested); + let observed = adapter(&interpreter, genuine, &requested); + for _ in 0..2 { + assert_eq!( + interpreter + .call_fn_value(observed.clone(), Vec::new()) + .unwrap() + .payload(), + &Value::Int64(7) + ); + assert_eq!(traces(&mut interpreter).len(), 2); + } +} + +#[test] +fn reflected_closure_same_span_parameter_contract_mutation_refuses_before_effects() { + let (mut interpreter, _, requested) = prepared(); + let requested = Interpreter::simple_type_expr_from_name( + "function(int64) returns app.Positive", + requested.span(), + ) + .unwrap(); + let source = interpreter + .call_function("app.captured_parameter", vec![Value::Int64(7)]) + .unwrap(); + for mutation in 0..4 { + let mut forged = source.clone(); + let Value::Function { params, .. } = &mut forged else { + panic!("expected closure"); + }; + let param = &mut params[0]; + match mutation { + 0 => param.view = true, + 1 => param.mutable = true, + 2 => param.name.name = "other".into(), + 3 => { + let TypeExpr::Named(name) = &mut param.ty else { + panic!("expected named type"); + }; + name.name = "bool".into(); + } + _ => unreachable!(), + } + let error = interpreter + .prepare_reflected_callable(forged, &requested, "function(int64) returns int64") + .unwrap_err(); + assert!( + error.contains("unchanged original closure capture"), + "{error}" + ); + assert!(traces(&mut interpreter).is_empty()); + } + let observed = interpreter + .prepare_reflected_callable(source, &requested, "function(int64) returns int64") + .unwrap(); + assert!(traces(&mut interpreter).is_empty()); + assert_eq!( + interpreter + .call_fn_value(observed, vec![Value::Int64(2)]) + .unwrap() + .payload(), + &Value::Int64(9) + ); + assert_eq!(traces(&mut interpreter).len(), 2); +} + +#[test] +fn reflected_closure_capture_types_context_and_alias_forgery_refuse_before_effects() { + let (mut interpreter, _, requested) = prepared(); + let genuine = interpreter + .call_function("app.captured", vec![Value::Int64(7)]) + .unwrap(); + for mutation in 0..8 { + let mut forged = genuine.clone(); + let Value::Function { + type_context, + captures, + capture_types, + namespace_aliases, + namespace, + .. + } = &mut forged + else { + panic!("expected closure"); + }; + match mutation { + 0 => { + captures.insert("offset".into(), Value::Int64(0)); + } + 1 => { + captures.insert("extra".into(), Value::Nothing); + } + 2 => { + let TypeExpr::Named(name) = capture_types.get_mut("offset").unwrap() else { + panic!("expected capture type"); + }; + name.name = "bool".into(); + } + 3 => { + type_context.bindings.insert( + "Foreign".into(), + TypeExpr::Named(Ident { + name: "bool".into(), + span: requested.span(), + }), + ); + } + 4 => { + type_context.checked_scope = Some(Arc::new(CheckedScopedTypes::default())); + } + 5 => { + type_context.checked_function = Some(Arc::new(CheckedFunctionTypes::default())); + } + 6 => { + namespace_aliases.insert("changed".into(), "other".into()); + } + 7 => { + *namespace = Some("other".into()); + } + _ => unreachable!(), + } + refuse_forged_capture(&mut interpreter, forged, &requested); + } + let (_, foreign, _) = prepared(); + let Value::Function { type_context, .. } = &genuine else { + panic!("expected closure"); + }; + let actual = foreign + .checked() + .reflection_metadata + .source_type_id_for_name("function() returns int64") + .unwrap(); + assert!( + !type_context + .reflected_capture + .as_ref() + .unwrap() + .validate_source(&foreign, actual, &genuine) + ); + assert!(traces(&mut interpreter).is_empty()); +} + +#[test] +fn reflected_closure_changed_source_owner_cannot_mint_a_witness() { + let (mut interpreter, program, requested) = prepared(); + let original = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "captured" => Some(function.clone()), + _ => None, + }) + .unwrap(); + let mut changed = original.clone(); + // Keep the original inline node and all source spans while changing its + // enclosing owner. An exact subtree must not authenticate a changed owner. + changed.body.stmts.insert( + 0, + Stmt::Expr(jett_parser::ast::ExprStmt { + expr: Expr::Nothing(changed.body.span), + span: changed.body.span, + }), + ); + interpreter.register_function_in_namespace(Some("app"), &changed); + let raw = interpreter + .call_function("app.captured", vec![Value::Int64(7)]) + .unwrap(); + refuse_forged_capture(&mut interpreter, raw.clone(), &requested); + // Ordinary closure execution remains available without reflected authority. + assert_eq!( + interpreter.call_fn_value(raw, Vec::new()).unwrap(), + Value::Int64(7) + ); + assert_eq!(traces(&mut interpreter).len(), 1); + + let mut transplanted = original; + transplanted.name.name = "five".into(); + interpreter.register_function_in_namespace(Some("app"), &transplanted); + let raw = interpreter + .call_function("app.five", vec![Value::Int64(7)]) + .unwrap(); + refuse_forged_capture(&mut interpreter, raw, &requested); +} + +#[test] +fn reflected_closure_forged_installed_checked_context_cannot_mint_a_witness() { + let (mut interpreter, program, requested) = prepared(); + let declaration = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "captured" => { + Some(function.name.span) + } + _ => None, + }) + .unwrap(); + let mut installed = interpreter + .checked_expression_types + .as_ref() + .unwrap() + .as_ref() + .clone(); + installed + .functions + .insert(declaration, vec![Arc::new(CheckedFunctionTypes::default())]); + interpreter.set_checked_expression_types(Arc::new(installed)); + let raw = interpreter + .call_function("app.captured", vec![Value::Int64(7)]) + .unwrap(); + refuse_forged_capture(&mut interpreter, raw.clone(), &requested); + assert_eq!( + interpreter.call_fn_value(raw, Vec::new()).unwrap(), + Value::Int64(7) + ); + assert_eq!(traces(&mut interpreter).len(), 1); +} + +#[test] +fn reflected_closure_forged_root_type_context_refuses_mint_and_admission() { + for binding_map in [false, true] { + let (mut interpreter, _, requested) = prepared(); + let genuine = interpreter + .call_function("app.captured", vec![Value::Int64(7)]) + .unwrap(); + let mut installed = interpreter + .checked_expression_types + .as_ref() + .unwrap() + .as_ref() + .clone(); + let foreign_span = Span::new(requested.span().file, u32::MAX - 1, u32::MAX); + if binding_map { + installed + .bindings + .insert(foreign_span, vec![Arc::new(CheckedScopedTypes::default())]); + } else { + installed.expressions.insert(foreign_span, "bool".into()); + } + interpreter.set_checked_expression_types(Arc::new(installed)); + let error = interpreter + .prepare_reflected_callable(genuine, &requested, "function() returns int64") + .unwrap_err(); + assert!( + error.contains("checked expression context changed its original source"), + "{error}" + ); + assert!(traces(&mut interpreter).is_empty()); + let raw = interpreter + .call_function("app.captured", vec![Value::Int64(7)]) + .unwrap(); + let Value::Function { type_context, .. } = &raw else { + panic!("expected closure"); + }; + assert!(type_context.reflected_capture.is_none()); + assert_eq!( + interpreter.call_fn_value(raw, Vec::new()).unwrap(), + Value::Int64(7) + ); + assert_eq!(traces(&mut interpreter).len(), 1); + } +} + +#[test] +fn reflected_closure_root_arithmetic_fact_forgery_refuses_before_source_invocation() { + let (mut interpreter, program, requested) = prepared(); + let requested = Interpreter::simple_type_expr_from_name( + "function(int64) returns app.Positive", + requested.span(), + ) + .unwrap(); + let source = interpreter + .call_function("app.captured_parameter", vec![Value::Int64(7)]) + .unwrap(); + let observed = interpreter + .prepare_reflected_callable(source, &requested, "function(int64) returns int64") + .unwrap(); + let arithmetic = program + .module() + .items + .iter() + .find_map(|item| { + let Item::Function(function) = item else { + return None; + }; + if function.name.name != "captured_parameter" { + return None; + } + let Stmt::VarDecl(declaration) = function.body.stmts.first()? else { + return None; + }; + let Expr::InlineFn(_, _, body, _) = &declaration.value else { + return None; + }; + let Stmt::Return(statement) = body.stmts.last()? else { + return None; + }; + statement.value.as_ref().map(Expr::span) + }) + .unwrap(); + let mut installed = interpreter + .checked_expression_types + .as_ref() + .unwrap() + .as_ref() + .clone(); + installed.expressions.insert(arithmetic, "int8".into()); + interpreter.set_checked_expression_types(Arc::new(installed)); + let error = interpreter + .call_fn_value(observed, vec![Value::Int64(130)]) + .unwrap_err(); + assert!( + error.contains("checked expression context changed its original source"), + "{error}" + ); + assert!(traces(&mut interpreter).is_empty()); + let raw = interpreter + .call_function("app.captured_parameter", vec![Value::Int64(7)]) + .unwrap(); + let Value::Function { type_context, .. } = &raw else { + panic!("expected closure"); + }; + assert!(type_context.reflected_capture.is_none()); + assert_eq!( + interpreter + .call_fn_value(raw, vec![Value::Int64(130)]) + .unwrap(), + Value::Int64(-119) + ); + assert_eq!(traces(&mut interpreter).len(), 1); +} + +#[test] +fn reflected_closure_pending_nothing_snapshot_preserves_layers_without_joining() { + let (mut interpreter, _, requested) = prepared(); + interpreter.set_variable( + "unobserved", + Value::Pending(Box::new(Value::Pending(Box::new(Value::Nothing)))), + ); + let source = interpreter + .call_function("app.captured", vec![Value::Int64(7)]) + .unwrap(); + let Value::Function { captures, .. } = &source else { + panic!("expected closure"); + }; + assert!( + matches!(captures.get("unobserved"), Some(Value::Pending(outer)) if matches!(outer.as_ref(), Value::Pending(inner) if matches!(inner.as_ref(), Value::Nothing))) + ); + let exact = + Interpreter::simple_type_expr_from_name("function() returns int64", requested.span()) + .unwrap(); + let pending = Value::Pending(Box::new(Value::Pending(Box::new(source.clone())))); + let unchanged = interpreter + .prepare_reflected_callable(pending, &exact, "function() returns int64") + .unwrap(); + assert!( + matches!(unchanged, Value::Pending(outer) if matches!(outer.as_ref(), Value::Pending(inner) if matches!(inner.as_ref(), Value::Function { .. }))) + ); + assert!(traces(&mut interpreter).is_empty()); + let mut forged = source.clone(); + let Value::Function { captures, .. } = &mut forged else { + panic!("expected closure"); + }; + captures.insert( + "unobserved".into(), + Value::Pending(Box::new(Value::Nothing)), + ); + refuse_forged_capture(&mut interpreter, forged, &requested); + let observed = adapter(&interpreter, source, &requested); + assert_eq!( + interpreter + .call_fn_value(observed, Vec::new()) + .unwrap() + .payload(), + &Value::Int64(7) + ); + assert_eq!(traces(&mut interpreter).len(), 2); +} + +#[test] +fn reflected_closure_dynamic_factories_restore_mint_origin_after_primitive_calls() { + let (mut interpreter, _, requested) = prepared(); + let captured_factory = interpreter + .call_function("app.captured_factory", Vec::new()) + .unwrap(); + assert!(traces(&mut interpreter).is_empty()); + for factory in [ + Value::NamedFunction("app.captured_after_primitive".into()), + captured_factory, + ] { + let source = interpreter + .call_function("app.forward_factory", vec![factory, Value::Int64(7)]) + .unwrap(); + assert_eq!( + traces(&mut interpreter), + ["trace marker: string = primitive\n"] + ); + assert!(interpreter.active_reflected_capture_origin.is_none()); + assert!(interpreter.active_checked_function.is_none()); + assert!(interpreter.active_checked_scope.is_none()); + assert!(interpreter.allow_checked_refinement_proofs); + let observed = adapter(&interpreter, source, &requested); + assert!(traces(&mut interpreter).is_empty()); + let result = interpreter.call_fn_value(observed, Vec::new()).unwrap(); + assert_eq!(result.payload(), &Value::Int64(8)); + assert_eq!( + traces(&mut interpreter), + [ + "trace marker: string = source\n", + "trace marker: string = predicate\n", + ] + ); + assert!(interpreter.active_reflected_capture_origin.is_none()); + } +} diff --git a/crates/jett_comptime/src/lib.rs b/crates/jett_comptime/src/lib.rs index fd185985..c00e5ffd 100644 --- a/crates/jett_comptime/src/lib.rs +++ b/crates/jett_comptime/src/lib.rs @@ -2,6 +2,7 @@ pub mod checked_types; pub mod debug; pub mod explicit; pub mod interpreter; +mod resource_execution; pub mod value; pub mod verify; @@ -10,6 +11,7 @@ pub const INTERPRETER_STACK_SIZE: usize = 8 * 1024 * 1024; pub use debug::{DebugEvent, DebugEventKind, render_debug_events}; pub use explicit::{ + CheckedRequiredOwner, CheckedRequiredResourceHook, CheckedRequiredScope, CheckedRequiredValue, ComptimeContext, ExplicitComptimeEvaluation, ExplicitComptimeValues, evaluate_explicit_comptime_expressions, evaluate_explicit_comptime_expressions_capture, }; @@ -17,6 +19,7 @@ pub use interpreter::{ ClockTestSample, EnvironmentTestEntry, EnvironmentTestSnapshot, EnvironmentTestText, GraphicsTestEvent, GraphicsTestKey, GraphicsTestObservation, Interpreter, RandomTestSample, }; +pub use resource_execution::ExecutionPurpose; pub use value::Value; pub use verify::{ ComptimeError, eval_assert, eval_function, run_verify_blocks, diff --git a/crates/jett_comptime/src/resource_execution/assignment_tests.rs b/crates/jett_comptime/src/resource_execution/assignment_tests.rs new file mode 100644 index 00000000..bb6e8e19 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/assignment_tests.rs @@ -0,0 +1,139 @@ +use super::*; +use std::panic::{AssertUnwindSafe, catch_unwind}; + +const SOURCE: &str = include_str!("fixtures/24_mutable_assignment.jett"); + +fn run( + name: &str, + script: Vec, + expected: Vec, + panic_expected: bool, +) { + for release in [false, true] { + let program = program(SOURCE, release); + let definition = entry(&program, name); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(script.clone()) + .unwrap(); + let result = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(definition, vec![grant]) + })); + if panic_expected { + assert!( + result.is_err(), + "the old-owner finalizer panic must propagate" + ); + } else { + assert_eq!(result.unwrap().unwrap(), Value::Nothing); + } + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (expected.clone(), 0, 0) + ); + assert!(interpreter.take_debug_events().is_empty()); + } +} + +fn constructed(label: i64) -> ScriptOperation { + ScriptOperation::Construct { + label, + outcome: Ok(()), + } +} + +#[test] +fn mutable_resource_replacement_finalizes_old_before_using_new() { + run( + "replace_live", + vec![ + constructed(2401), + constructed(2402), + ScriptOperation::Borrow { + label: 2402, + outcome: Ok(7), + }, + ], + vec![ + ProviderEvent::Constructed(2401), + ProviderEvent::Constructed(2402), + ProviderEvent::Finalized(2401), + ProviderEvent::Borrowed(2402), + ProviderEvent::Finalized(2402), + ], + false, + ); +} + +#[test] +fn mutable_resource_can_rebind_its_original_consumed_slot() { + run( + "rebind_after_close", + vec![constructed(2411), constructed(2412)], + vec![ + ProviderEvent::Constructed(2411), + ProviderEvent::Finalized(2411), + ProviderEvent::Constructed(2412), + ProviderEvent::Finalized(2412), + ], + false, + ); +} + +#[test] +fn mutable_resource_self_move_retains_exactly_one_cleanup_owner() { + run( + "rebind_self", + vec![constructed(2421)], + vec![ + ProviderEvent::Constructed(2421), + ProviderEvent::Finalized(2421), + ], + false, + ); +} + +#[test] +fn mutable_resource_failed_rhs_handler_can_borrow_previous_owner() { + run( + "failed_rhs_keeps_owner", + vec![ + constructed(2431), + ScriptOperation::Construct { + label: 2432, + outcome: Err("rhs sentinel".to_string()), + }, + ScriptOperation::Borrow { + label: 2431, + outcome: Ok(7), + }, + ], + vec![ + ProviderEvent::Constructed(2431), + ProviderEvent::ConstructionFailed(2432), + ProviderEvent::Borrowed(2431), + ProviderEvent::Finalized(2431), + ], + false, + ); +} + +#[test] +fn mutable_resource_old_finalizer_panic_also_cleans_staged_replacement() { + run( + "replace_live", + vec![ + ScriptOperation::ConstructFinalizerPanic { label: 2401 }, + constructed(2402), + ], + vec![ + ProviderEvent::Constructed(2401), + ProviderEvent::Constructed(2402), + ProviderEvent::Finalized(2401), + ProviderEvent::Finalized(2402), + ], + true, + ); +} diff --git a/crates/jett_comptime/src/resource_execution/capture.rs b/crates/jett_comptime/src/resource_execution/capture.rs new file mode 100644 index 00000000..fff74064 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/capture.rs @@ -0,0 +1,257 @@ +//! Raw lexical references are only a refusal guard, never opaque copy authority. +use super::*; + +pub(super) fn raw_closure_uses_binding(params: &[Param], body: &Block, name: &str) -> bool { + block_uses_binding( + body, + name, + params.iter().any(|param| param.name.name == name), + ) +} + +fn block_uses_binding(body: &Block, name: &str, mut shadowed: bool) -> bool { + for statement in &body.stmts { + let used = match statement { + Stmt::VarDecl(value) => { + let used = expression_uses_binding(&value.value, name, shadowed); + shadowed |= value.name.name == name; + used + } + Stmt::Assign(value) => { + expression_uses_binding(&value.target, name, shadowed) + || expression_uses_binding(&value.value, name, shadowed) + } + Stmt::Return(value) => value + .value + .as_ref() + .is_some_and(|value| expression_uses_binding(value, name, shadowed)), + Stmt::Respond(value) => expression_uses_binding(&value.value, name, shadowed), + Stmt::ComptimeTypeBind(value) => { + expression_uses_binding(&value.value, name, shadowed) + || block_uses_binding(&value.body, name, shadowed || value.name.name == name) + } + Stmt::If(value) => { + expression_uses_binding(&value.condition, name, shadowed) + || block_uses_binding(&value.then_block, name, shadowed) + || value.else_ifs.iter().any(|(condition, body)| { + expression_uses_binding(condition, name, shadowed) + || block_uses_binding(body, name, shadowed) + }) + || value + .else_block + .as_ref() + .is_some_and(|body| block_uses_binding(body, name, shadowed)) + } + Stmt::For(value) => { + expression_uses_binding(&value.iterable, name, shadowed) + || block_uses_binding( + &value.body, + name, + shadowed + || value.variable.name == name + || value + .value_variable + .as_ref() + .is_some_and(|binding| binding.name == name), + ) + } + Stmt::While(value) => { + expression_uses_binding(&value.condition, name, shadowed) + || block_uses_binding(&value.body, name, shadowed) + } + Stmt::Match(value) => { + expression_uses_binding(&value.expr, name, shadowed) + || value.arms.iter().any(|arm| { + let bound = match &arm.pattern { + Pattern::Variant(_, bindings) => { + bindings.iter().any(|binding| binding.name == name) + } + Pattern::Ident(_) | Pattern::Other(_) => false, + }; + block_uses_binding(&arm.body, name, shadowed || bound) + }) + } + Stmt::Expr(value) => expression_uses_binding(&value.expr, name, shadowed), + Stmt::Use(value) => { + shadowed |= + Interpreter::use_bound_name(&value.path.name, value.alias.as_ref()) == name; + false + } + Stmt::Assert(value) => { + expression_uses_binding(&value.condition, name, shadowed) + || value + .message + .as_ref() + .is_some_and(|value| expression_uses_binding(value, name, shadowed)) + } + Stmt::Trace(value) => !shadowed && value.name.name == name, + Stmt::Breakpoint(value) => value + .condition + .as_ref() + .is_some_and(|value| expression_uses_binding(value, name, shadowed)), + Stmt::Break(_) | Stmt::Continue(_) => false, + }; + if used { + return true; + } + } + false +} + +fn expression_uses_binding(value: &Expr, name: &str, shadowed: bool) -> bool { + match value { + Expr::Ident(value) => !shadowed && value.name == name, + Expr::Binary(left, _, right, _) => { + expression_uses_binding(left, name, shadowed) + || expression_uses_binding(right, name, shadowed) + } + Expr::Unary(_, inner, _) + | Expr::FieldAccess(inner, _, _) + | Expr::Paren(inner, _) + | Expr::View(inner, _) + | Expr::Comptime(inner, _) + | Expr::Ok(inner, _) + | Expr::Fail(inner, _) + | Expr::Some(inner, _) + | Expr::Default(inner, _) + | Expr::Declassify(inner, _) + | Expr::Coarsen(inner, _) + | Expr::At(inner, _, _) + | Expr::Spawn(inner, _) + | Expr::Send(inner, _) + | Expr::Ask(inner, _) + | Expr::Clone(inner, _) + | Expr::Run(inner, _) + | Expr::Join(inner, _) + | Expr::Cancel(inner, _) => expression_uses_binding(inner, name, shadowed), + Expr::Call(callee, arguments, _) | Expr::GenericCall(callee, _, arguments, _) => { + expression_uses_binding(callee, name, shadowed) + || arguments + .iter() + .any(|argument| expression_uses_binding(&argument.value, name, shadowed)) + } + Expr::ListConstruct(values, _) => values + .iter() + .any(|value| expression_uses_binding(value, name, shadowed)), + Expr::MapConstruct(values, _) => values.iter().any(|(key, value)| { + expression_uses_binding(key, name, shadowed) + || expression_uses_binding(value, name, shadowed) + }), + Expr::Handle(target, error, body, _) => { + expression_uses_binding(target, name, shadowed) + || block_uses_binding( + body, + name, + shadowed || error.as_ref().is_some_and(|error| error.name == name), + ) + } + Expr::StringInterpolation(parts, _) => parts.iter().any(|part| match part { + StringPart::Expr(value) => expression_uses_binding(value, name, shadowed), + StringPart::Literal(_) => false, + }), + Expr::Pipeline(initial, steps, _) => { + expression_uses_binding(initial, name, shadowed) + || steps.iter().any(|step| { + expression_uses_binding(&step.function, name, shadowed) + || step.extra_args.iter().any(|argument| { + expression_uses_binding(&argument.value, name, shadowed) + }) + || step.handle.as_ref().is_some_and(|handle| { + block_uses_binding( + &handle.body, + name, + shadowed + || handle + .error_name + .as_ref() + .is_some_and(|error| error.name == name), + ) + }) + }) + } + Expr::InlineFn(params, _, body, _) => block_uses_binding( + body, + name, + shadowed || params.iter().any(|param| param.name.name == name), + ), + Expr::IntLiteral(..) + | Expr::FloatLiteral(..) + | Expr::StringLiteral(..) + | Expr::BoolLiteral(..) + | Expr::Nothing(_) + | Expr::None(_) + | Expr::EnumVariant(..) + | Expr::Error(_) => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::resource_execution::{ExecutionPurpose, tests::program}; + + fn function(source: &str) -> FunctionDef { + let parsed = jett_parser::parse(source, jett_common::FileId::new(0)); + assert!(parsed.errors.is_empty()); + parsed + .module + .items + .into_iter() + .find_map(|item| match item { + Item::Function(function) => Some(function), + _ => None, + }) + .unwrap() + } + + #[test] + fn raw_opaque_capture_refuses_referenced_binding_but_preserves_unrelated_and_shadowed_data() { + let checked = program( + include_str!("fixtures/04_bare_to_view_operation_cleanup.jett"), + false, + ); + let mut authority = Interpreter::from_checked_resource_program( + checked.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let grant = authority.install_resource_test_script(Vec::new()).unwrap(); + let mut raw = Interpreter::new(); + raw.set_variable("net", grant.clone()); + let source = function("namespace app\nfunction f() returns nothing:\n return net\n"); + assert!( + raw.capture_closure(&source.params, source.return_type.as_ref(), &source.body) + .unwrap_err() + .contains("opaque capture") + ); + let mut raw_checked = + Interpreter::from_checked_resource_program(checked, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + raw_checked.set_variable("net", grant); + assert!( + raw_checked + .capture_closure(&source.params, source.return_type.as_ref(), &source.body) + .unwrap_err() + .contains("opaque capture") + ); + for source in [ + "namespace app\nfunction f() returns int64:\n return 7\n", + "namespace app\nfunction f(net: int64) returns int64:\n return net\n", + "namespace app\nfunction f() returns int64:\n int64 net = 7\n return net\n", + ] { + let source = function(source); + let closure = raw + .capture_closure(&source.params, source.return_type.as_ref(), &source.body) + .unwrap(); + let Value::Function { captures, .. } = closure else { + panic!("ordinary closure"); + }; + assert!(!captures.contains_key("net")); + assert!( + captures + .values() + .all(|value| !value.contains_live_resource_or_grant()) + ); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked.rs b/crates/jett_comptime/src/resource_execution/checked.rs new file mode 100644 index 00000000..c1d02852 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked.rs @@ -0,0 +1,1171 @@ +#[path = "checked/assignment.rs"] +mod assignment; +pub(crate) use assignment::CheckedAssignment; + +#[path = "checked/body_reference.rs"] +mod body_reference; +pub(crate) use body_reference::{ + CheckedAttemptKey, CheckedBodyReference, PreparedAbsentBindings, PreparedDirectScope, + PreparedIntrinsicArguments, PreparedNamedCallable, PreparedOwnedStruct, PreparedPipelineStep, + PreparedReflectedFieldIteration, PreparedReflectedFieldLoop, PreparedRequiredExpression, +}; +pub use body_reference::{ + CheckedRequiredOwner, CheckedRequiredResourceHook, CheckedRequiredScope, CheckedRequiredValue, +}; +use std::collections::HashMap; +use std::sync::Arc; + +use jett_common::Span; +use jett_resolve::DefId; +use jett_runtime::ResourceTypeId; +use jett_typecheck::{ + CheckedArgumentOwnership, CheckedBindingFact, CheckedBodyFacts, CheckedCallOwnership, + CheckedComptimeTypeBinding, CheckedComptimeTypeSelection, CheckedGenericCall, + CheckedInvocationShape, CheckedInvocationTarget, CheckedResourceHook, CheckedResourceProgram, + validate_resource_hooks, +}; +use jett_types::{ReflectionTypeInfo, ResourceHookKind, ResourceKernelRecipe, Type, TypeId}; + +use super::{ResourceExecutionError, ResourceHookDescriptor, ResourceTypeBinding}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExecutionPurpose { + ReferenceRuntime, + NamespaceConstant, + ExplicitComptime, + Verify, + Property, +} + +#[derive(Debug, Clone)] +enum BodyRoot { + Ordinary, + Generic(usize), +} + +#[derive(Debug, Clone)] +struct ScopedSelection { + owner: Span, + index: usize, + bound_type: TypeId, + selection: CheckedComptimeTypeSelection, + reflection: ReflectionTypeInfo, +} + +#[derive(Debug, Clone)] +pub(crate) struct CheckedBodyCursor { + root: BodyRoot, + scopes: Vec, + executable: Option, +} + +impl CheckedBodyCursor { + fn ordinary() -> Self { + Self { + root: BodyRoot::Ordinary, + scopes: Vec::new(), + executable: None, + } + } +} + +pub(crate) struct CheckedInvocation { + packet: CheckedCallOwnership, + body: CheckedBodyCursor, + span: Span, +} + +impl CheckedInvocation { + pub(crate) fn packet(&self) -> &CheckedCallOwnership { + &self.packet + } + pub(crate) fn arguments(&self) -> &[CheckedArgumentOwnership] { + &self.packet.arguments + } + pub(crate) fn target(&self) -> &CheckedInvocationTarget { + &self.packet.target + } + pub(crate) fn span(&self) -> Span { + self.span + } +} + +pub(crate) struct FunctionParameter { + pub(crate) index: usize, + pub(crate) ty: TypeId, + pub(crate) access: jett_typecheck::CheckedCalleeAccess, +} + +/// External checked entry is not a fabricated source call packet. +pub(crate) enum FunctionInvocation<'a> { + Source(&'a CheckedInvocation), + NamedSource { + source: &'a CheckedInvocation, + target: &'a PreparedNamedCallable, + }, + Entry { + definition: DefId, + signature: TypeId, + }, +} + +impl FunctionInvocation<'_> { + pub(crate) fn source(&self) -> Option<&CheckedInvocation> { + match self { + Self::Source(source) | Self::NamedSource { source, .. } => Some(source), + Self::Entry { .. } => None, + } + } + + pub(crate) fn signature( + &self, + checked: &CheckedExecution, + ) -> Result { + match self { + Self::Source(invocation) + | Self::NamedSource { + source: invocation, .. + } => match &invocation.packet.shape { + CheckedInvocationShape::Function { signature_type } => Ok(*signature_type), + CheckedInvocationShape::Intrinsic { .. } => { + Err(ResourceExecutionError::InvalidInvocation) + } + }, + Self::Entry { + definition, + signature, + } if checked.program.checked().definition_types.get(definition) == Some(signature) => { + Ok(*signature) + } + Self::Entry { .. } => Err(ResourceExecutionError::InvalidInvocation), + } + } + pub(crate) fn parameters( + &self, + checked: &CheckedExecution, + ) -> Result, ResourceExecutionError> { + let signature = self.signature(checked)?; + let Type::Function { + params, + view_params, + .. + } = checked.program.checked().interner.resolve(signature) + else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + match self { + Self::Source(invocation) + | Self::NamedSource { + source: invocation, .. + } => Ok(invocation + .arguments() + .iter() + .map(|argument| FunctionParameter { + index: argument.parameter_index, + ty: argument.parameter_type, + access: argument.callee_access, + }) + .collect()), + Self::Entry { .. } => Ok(params + .iter() + .zip(view_params) + .enumerate() + .map(|(index, (ty, view))| FunctionParameter { + index, + ty: *ty, + access: if *view { + jett_typecheck::CheckedCalleeAccess::View + } else { + jett_typecheck::CheckedCalleeAccess::Owned + }, + }) + .collect()), + } + } +} + +pub(crate) struct CheckedExecution { + program: Arc, + purpose: ExecutionPurpose, + body: CheckedBodyCursor, + resources: HashMap, +} + +struct BodyFacts<'a> { + selections: Option<&'a HashMap>, + calls: &'a HashMap, + bindings: &'a HashMap, + types: &'a HashMap, + source_types: &'a HashMap, + pipeline_inputs: &'a HashMap, + pipeline_calls: &'a HashMap, + scopes: &'a HashMap>, + intrinsics: &'a HashMap, + intrinsic_types: &'a HashMap>, + intrinsic_reflections: &'a HashMap>, + constructions: &'a HashMap, +} + +impl<'a> BodyFacts<'a> { + fn scoped(body: &'a CheckedBodyFacts) -> Self { + Self { + selections: Some(&body.static_selections), + calls: &body.call_ownership, + bindings: &body.binding_facts, + types: &body.type_map, + source_types: &body.source_type_map, + pipeline_inputs: &body.pipeline_step_input_types, + pipeline_calls: &body.pipeline_step_call_types, + scopes: &body.comptime_type_bindings, + intrinsics: &body.intrinsic_ids, + intrinsic_types: &body.intrinsic_type_arguments, + intrinsic_reflections: &body.intrinsic_reflection_arguments, + constructions: &body.struct_constructions, + } + } +} + +// A present resolution is authoritative only after it rejoins this exact +// retained executable declaration. Ordinary declarations have no such key. +fn retained_declaration_definition( + module: &jett_parser::ast::Module, + definitions: &[jett_resolve::DefInfo], + resolutions: &HashMap, + declaration: Span, +) -> Result { + use jett_parser::ast::Item; + use jett_resolve::DefKind; + + let mut actuals = module + .items + .iter() + .enumerate() + .filter_map(|(index, item)| match item { + Item::Function(function) if function.name.span == declaration => { + Some((index, function)) + } + _ => None, + }); + let (index, function) = actuals + .next() + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if actuals.next().is_some() { + return Err(ResourceExecutionError::InvalidInvocation); + } + let namespace_at = |index: usize| { + module.items[..index] + .iter() + .filter_map(|item| match item { + Item::Namespace(namespace) if namespace.span.file == declaration.file => { + Some(namespace.name.name.as_str()) + } + _ => None, + }) + .last() + }; + let namespace = namespace_at(index); + let definition = match resolutions.get(&declaration) { + Some(definition) => *definition, + None => { + let mut candidates = definitions + .iter() + .filter(|info| info.kind == DefKind::Function && info.span == declaration); + let info = candidates + .next() + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if candidates.next().is_some() { + return Err(ResourceExecutionError::InvalidInvocation); + } + info.id + } + }; + let info = definitions + .get(definition.index() as usize) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + let canonical_name = namespace + .map(|namespace| format!("{namespace}.{}", function.name.name)) + .unwrap_or_else(|| function.name.name.clone()); + if info.id != definition + || info.kind != DefKind::Function + || info.namespace.as_deref() != namespace + || info.name != canonical_name + { + return Err(ResourceExecutionError::InvalidInvocation); + } + if info.span != declaration { + // Mutual definitions resolve to their original forward declaration. + // Rejoin that exact retained node, not another same-signature function. + let forwards = module + .items + .iter() + .enumerate() + .filter_map(|(index, item)| match item { + Item::Mutual(block) => Some((index, block)), + _ => None, + }) + .flat_map(|(index, block)| { + block + .declarations + .iter() + .map(move |forward| (index, forward)) + }) + .filter(|(index, forward)| { + forward.name.span == info.span + && forward.name.name == function.name.name + && namespace_at(*index) == namespace + }) + .count(); + if forwards != 1 { + return Err(ResourceExecutionError::InvalidInvocation); + } + } + Ok(definition) +} + +impl CheckedExecution { + pub(crate) fn new( + program: Arc, + purpose: ExecutionPurpose, + ) -> Result { + validate_resource_hooks(program.module(), program.resolved(), program.checked()) + .map_err(|_| ResourceExecutionError::InvalidCheckedProgram)?; + let mut resources: HashMap = HashMap::new(); + for hook in program.checked().resource_hooks.values() { + if let Some(existing) = resources.get(&hook.resource_definition) { + if existing.checked_type != hook.resource_type { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + continue; + } + // A per-program registry type. The registry itself binds a runtime + // context, so equal numeric IDs in independent sessions confer none. + let number = u32::try_from(resources.len()) + .map_err(|_| ResourceExecutionError::IdentityExhausted)?; + resources.insert( + hook.resource_definition, + ResourceTypeBinding { + definition: hook.resource_definition, + checked_type: hook.resource_type, + registry_type: ResourceTypeId::new(number), + }, + ); + } + Ok(Self { + program, + purpose, + body: CheckedBodyCursor::ordinary(), + resources, + }) + } + + pub(crate) fn program(&self) -> &Arc { + &self.program + } + pub(crate) fn purpose(&self) -> ExecutionPurpose { + self.purpose + } + pub(crate) fn replace_purpose(&mut self, purpose: ExecutionPurpose) -> ExecutionPurpose { + std::mem::replace(&mut self.purpose, purpose) + } + pub(crate) fn cursor(&self) -> CheckedBodyCursor { + self.body.clone() + } + + fn facts(&self, cursor: &CheckedBodyCursor) -> Result, ResourceExecutionError> { + let checked = self.program.checked(); + let mut facts = match cursor.root { + BodyRoot::Ordinary => BodyFacts { + selections: None, + calls: &checked.call_ownership, + bindings: &checked.binding_facts, + types: &checked.type_map, + source_types: &checked.source_type_map, + pipeline_inputs: &checked.pipeline_step_input_types, + pipeline_calls: &checked.pipeline_step_call_types, + scopes: &checked.comptime_type_bindings, + intrinsics: &checked.intrinsic_ids, + intrinsic_types: &checked.intrinsic_type_arguments, + intrinsic_reflections: &checked.intrinsic_reflection_arguments, + constructions: &checked.struct_constructions, + }, + BodyRoot::Generic(index) => { + let body = checked + .generic_function_instantiations + .get(index) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + BodyFacts { + selections: Some(&body.static_selections), + calls: &body.call_ownership, + bindings: &body.binding_facts, + types: &body.type_map, + source_types: &body.source_type_map, + pipeline_inputs: &body.pipeline_step_input_types, + pipeline_calls: &body.pipeline_step_call_types, + scopes: &body.comptime_type_bindings, + intrinsics: &body.intrinsic_ids, + intrinsic_types: &body.intrinsic_type_arguments, + intrinsic_reflections: &body.intrinsic_reflection_arguments, + constructions: &body.struct_constructions, + } + } + }; + for selection in &cursor.scopes { + let body = facts + .scopes + .get(&selection.owner) + .and_then(|scopes| scopes.get(selection.index)) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + if body.bound_type != selection.bound_type + || body.selection != selection.selection + || body.reflection != selection.reflection + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + facts = BodyFacts::scoped(&body.body); + } + Ok(facts) + } + + pub(crate) fn restore_cursor( + &mut self, + cursor: CheckedBodyCursor, + ) -> Result<(), ResourceExecutionError> { + self.facts(&cursor)?; + if cursor.executable.is_some() { + self.validate_executable_cursor(&cursor)?; + } + self.body = cursor; + Ok(()) + } + + pub(crate) fn enter_ordinary(&mut self) { + self.body = CheckedBodyCursor::ordinary(); + } + + pub(crate) fn enter_generic( + &mut self, + call: &CheckedGenericCall, + ) -> Result<(), ResourceExecutionError> { + let matches = self + .program + .checked() + .generic_function_instantiations + .iter() + .enumerate() + .filter(|(_, body)| { + body.definition == call.definition + && body.concrete_args == call.concrete_args + && body.specialization == call.specialization + }) + .map(|(index, _)| index) + .collect::>(); + let [index] = matches.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + self.body = CheckedBodyCursor { + root: BodyRoot::Generic(*index), + scopes: Vec::new(), + executable: None, + }; + Ok(()) + } + + pub(crate) fn invocation( + &self, + span: Span, + ) -> Result { + let facts = self.facts(&self.body)?; + let packet = facts + .calls + .get(&span) + .ok_or(ResourceExecutionError::MissingCheckedInvocation)?; + let mut sources = vec![false; packet.arguments.len()]; + let mut parameters = vec![false; packet.arguments.len()]; + for (index, argument) in packet.arguments.iter().enumerate() { + if argument.source_index != index + || argument.parameter_index >= parameters.len() + || parameters[argument.parameter_index] + { + return Err(ResourceExecutionError::InvalidInvocation); + } + sources[index] = true; + parameters[argument.parameter_index] = true; + if facts.source_types.get(&argument.source_span) != Some(&argument.actual_type) { + return Err(ResourceExecutionError::InvalidInvocation); + } + } + if sources.iter().any(|present| !present) || parameters.iter().any(|present| !present) { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(CheckedInvocation { + packet: packet.clone(), + body: self.body.clone(), + span, + }) + } + + pub(crate) fn enum_constructor( + &self, + callee: &jett_parser::ast::Expr, + invocation: &CheckedInvocation, + ) -> Result, ResourceExecutionError> { + use jett_parser::ast::Expr; + use jett_resolve::DefKind; + let CheckedInvocationTarget::Indirect(signature) = invocation.target() else { + return Ok(None); + }; + let facts = self.facts(&invocation.body)?; + if facts.calls.get(&invocation.span) != Some(&invocation.packet) { + return Err(ResourceExecutionError::InvalidInvocation); + } + let (owner, variant) = match callee { + Expr::EnumVariant(owner, variant, _) => (owner.span, variant), + Expr::FieldAccess(owner, variant, _) => (owner.span(), variant), + _ => return Ok(None), + }; + let Some(definition) = self.program.resolved().resolutions.get(&owner) else { + return Ok(None); + }; + let info = self + .program + .resolved() + .scope_table + .definitions + .get(definition.index() as usize) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if info.id != *definition { + return Err(ResourceExecutionError::InvalidInvocation); + } + if !matches!(info.kind, DefKind::Enum | DefKind::Type) { + return Ok(None); + } + if signature.index() as usize >= self.program.checked().interner.len() { + return Err(ResourceExecutionError::InvalidInvocation); + } + let Type::Function { + params, + view_params, + return_type, + } = self.program.checked().interner.resolve(*signature) + else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if invocation.packet.shape + != (CheckedInvocationShape::Function { + signature_type: *signature, + }) + || facts.source_types.get(&invocation.span) != Some(return_type) + || facts.types.get(&callee.span()) != Some(signature) + || self.program.checked().definition_types.get(definition) != Some(return_type) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let Type::Enum(enum_id) = self.program.checked().interner.resolve(*return_type) else { + return Ok(None); + }; + let nominal = self.program.checked().interner.resolve_enum(*enum_id); + let variants = nominal + .variants + .iter() + .filter(|candidate| candidate.name == variant.name) + .collect::>(); + let [selected] = variants.as_slice() else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if params.len() != selected.fields.len() + || view_params.len() != params.len() + || view_params.iter().any(|view| *view) + || params + .iter() + .zip(&selected.fields) + .any(|(parameter, (_, field))| parameter != field) + || invocation.arguments().len() != params.len() + || invocation.arguments().iter().any(|argument| { + params.get(argument.parameter_index) != Some(&argument.parameter_type) + || argument.callee_access != jett_typecheck::CheckedCalleeAccess::Owned + }) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(Some(( + nominal.name.clone(), + selected.name.clone(), + *return_type, + ))) + } + + pub(crate) fn entry( + &self, + definition: DefId, + ) -> Result, ResourceExecutionError> { + let definition_info = self + .program + .resolved() + .scope_table + .definitions + .get(definition.index() as usize) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if definition_info.id != definition + || definition_info.kind != jett_resolve::DefKind::Function + || self.has_hook(definition) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let signature = *self + .program + .checked() + .definition_types + .get(&definition) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if !matches!( + self.program.checked().interner.resolve(signature), + Type::Function { .. } + ) { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(FunctionInvocation::Entry { + definition, + signature, + }) + } + + pub(crate) fn referenced_in(&self, definition: DefId, body: Span) -> bool { + self.program + .resolved() + .resolutions + .iter() + .any(|(span, resolved)| { + *resolved == definition + && span.file == body.file + && span.start >= body.start + && span.end <= body.end + }) + } + + pub(crate) fn declaration_definition( + &self, + declaration: Span, + ) -> Result { + let definition = retained_declaration_definition( + self.program.module(), + &self.program.resolved().scope_table.definitions, + &self.program.resolved().resolutions, + declaration, + )?; + if self.has_hook(definition) { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(definition) + } + + /// Return only the executable node owned by this exact checked program. + /// Runtime registration clones never provide a replacement body. + pub(crate) fn retained_function( + &self, + definition: DefId, + ) -> Result<&jett_parser::ast::FunctionDef, ResourceExecutionError> { + if self.has_hook(definition) { + return Err(ResourceExecutionError::InvalidInvocation); + } + let mut matches = self + .program + .module() + .items + .iter() + .filter_map(|item| match item { + jett_parser::ast::Item::Function(function) + if self.declaration_definition(function.name.span) == Ok(definition) => + { + Some(function) + } + _ => None, + }); + let function = matches + .next() + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if matches.next().is_some() { + return Err(ResourceExecutionError::InvalidInvocation); + } + if function.type_params.is_empty() { + let signature = self.entry(definition)?.signature(self)?; + let Type::Function { + params, + view_params, + .. + } = self.program.checked().interner.resolve(signature) + else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if params.len() != function.params.len() + || view_params.len() != function.params.len() + || !view_params + .iter() + .copied() + .eq(function.params.iter().map(|parameter| parameter.view)) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + } else if !self + .program + .checked() + .generic_function_instantiations + .iter() + .any(|body| { + body.definition == definition + && body.concrete_args.len() == function.type_params.len() + && body.parameter_types.len() == function.params.len() + }) + { + // Generic templates have no fabricated definition_types entry. + // Exact concrete args/specialization/signature remain invocation + // and enter_generic obligations, independent of this body join. + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(function) + } + + pub(crate) fn source_function_context( + &self, + declaration: Span, + ) -> Result<(Option, bool), ResourceExecutionError> { + let definition = self.declaration_definition(declaration)?; + let info = &self.program.resolved().scope_table.definitions[definition.index() as usize]; + let trusted = self.program.source_origins().get(&declaration.file) + == Some(&jett_common::SourceOrigin::Stdlib); + Ok((info.namespace.clone(), trusted)) + } + + pub(crate) fn type_contains_resource( + &self, + ty: TypeId, + ) -> Result { + fn visit( + types: &jett_types::TypeInterner, + ty: TypeId, + seen: &mut std::collections::HashSet, + ) -> Result { + if ty.index() as usize >= types.len() { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + if !seen.insert(ty) { + return Ok(false); + } + let children: Vec = match types.resolve(ty) { + Type::Resource(_) => return Ok(true), + Type::List(inner) + | Type::Set(inner) + | Type::Optional(inner) + | Type::Secret(inner) + | Type::Refinement { base: inner, .. } => vec![*inner], + Type::Map(key, value) | Type::Result(key, value) => vec![*key, *value], + Type::Function { + params, + return_type, + .. + } => params + .iter() + .copied() + .chain(std::iter::once(*return_type)) + .collect(), + Type::Struct(owner) => types + .resolve_struct(*owner) + .fields + .iter() + .map(|field| field.1) + .collect(), + Type::Enum(owner) => types + .resolve_enum(*owner) + .variants + .iter() + .flat_map(|variant| variant.fields.iter().map(|field| field.1)) + .collect(), + Type::Machine(owner) => types + .resolve_machine(*owner) + .states + .iter() + .flat_map(|state| state.fields.iter().map(|field| field.1)) + .collect(), + Type::MachineState { machine, state } => types + .resolve_machine(*machine) + .state(*state) + .ok_or(ResourceExecutionError::InvalidCheckedProgram)? + .fields + .iter() + .map(|field| field.1) + .collect(), + Type::Bitfield(owner) => types + .resolve_bitfield(*owner) + .fields + .iter() + .map(|field| field.ty) + .collect(), + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::String + | Type::Bool + | Type::Bytes + | Type::Nothing + | Type::TypeConstruction + | Type::Never + | Type::Capability(_) + | Type::Interface(_) + | Type::Actor(_) => Vec::new(), + Type::Error => return Err(ResourceExecutionError::InvalidCheckedProgram), + }; + for child in children { + if visit(types, child, seen)? { + return Ok(true); + } + } + Ok(false) + } + visit( + &self.program.checked().interner, + ty, + &mut std::collections::HashSet::new(), + ) + } + + pub(crate) fn expression_type(&self, span: Span) -> Result { + self.facts(&self.body)? + .types + .get(&span) + .copied() + .ok_or(ResourceExecutionError::MissingCheckedBody) + } + + pub(crate) fn resolved_definition(&self, span: Span) -> Result { + self.program + .resolved() + .resolutions + .get(&span) + .copied() + .ok_or(ResourceExecutionError::InvalidInvocation) + } + + pub(crate) fn has_hook(&self, definition: DefId) -> bool { + self.program + .checked() + .resource_hooks + .contains_key(&definition) + } + + pub(crate) fn has_invocation(&self, span: Span) -> Result { + Ok(self.facts(&self.body)?.calls.contains_key(&span)) + } + + pub(crate) fn binding( + &self, + declaration: Span, + ) -> Result { + self.facts(&self.body)? + .bindings + .get(&declaration) + .copied() + .ok_or(ResourceExecutionError::MissingCheckedBody) + } + + pub(crate) fn descriptor( + &self, + definition: DefId, + ) -> Result { + self.program + .checked() + .resource_hooks + .get(&definition) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + Ok(ResourceHookDescriptor { + program: self.program.clone(), + definition, + }) + } + + pub(crate) fn reached_hook( + &self, + invocation: &CheckedInvocation, + descriptor: &ResourceHookDescriptor, + ) -> Result< + ( + CheckedResourceHook, + ResourceTypeBinding, + ResourceKernelRecipe, + ), + ResourceExecutionError, + > { + if !Arc::ptr_eq(&self.program, &descriptor.program) { + return Err(ResourceExecutionError::ForeignProgram); + } + // Retained lexical cursor is validated independently of the function + // currently running after nested actual evaluation. + let facts = self.facts(&invocation.body)?; + if facts.calls.get(&invocation.span) != Some(&invocation.packet) { + return Err(ResourceExecutionError::InvalidInvocation); + } + let hook = self + .program + .checked() + .resource_hooks + .get(&descriptor.definition) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + let target_matches = match &invocation.packet.target { + CheckedInvocationTarget::Resolved(definition) => *definition == hook.definition, + CheckedInvocationTarget::Indirect(function_type) => { + *function_type == hook.function_type + } + CheckedInvocationTarget::Generic(_) + | CheckedInvocationTarget::Method(_) + | CheckedInvocationTarget::Interface(_) + | CheckedInvocationTarget::Intrinsic(_) => false, + }; + if !target_matches + || invocation.packet.shape + != (CheckedInvocationShape::Function { + signature_type: hook.function_type, + }) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let resource = *self + .resources + .get(&hook.resource_definition) + .ok_or(ResourceExecutionError::InvalidCheckedProgram)?; + if resource.checked_type != hook.resource_type { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + let recipe = match hook.kind { + ResourceHookKind::Construct => ResourceKernelRecipe::NetworkFactory, + ResourceHookKind::BorrowOperation => ResourceKernelRecipe::NetworkBorrow, + ResourceHookKind::Close => ResourceKernelRecipe::Finalize, + }; + if !recipe.matches_signature( + &self.program.checked().interner, + hook.resource_type, + hook.function_type, + ) { + return Err(ResourceExecutionError::InvalidInvocation); + } + let Type::Function { + params, + view_params, + .. + } = self.program.checked().interner.resolve(hook.function_type) + else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if invocation.packet.arguments.len() != params.len() { + return Err(ResourceExecutionError::InvalidInvocation); + } + for argument in &invocation.packet.arguments { + let index = argument.parameter_index; + let view = argument.callee_access == jett_typecheck::CheckedCalleeAccess::View; + if params[index] != argument.parameter_type || view_params[index] != view { + return Err(ResourceExecutionError::InvalidInvocation); + } + } + if self.purpose != ExecutionPurpose::ReferenceRuntime { + return Err(ResourceExecutionError::WrongPurpose); + } + Ok((hook.clone(), resource, recipe)) + } +} + +#[cfg(test)] +mod declaration_tests { + use super::*; + use jett_parser::ast::Item; + use jett_resolve::DefKind; + + const ORDINARY: &str = r#"namespace other +export function answer() returns int64: + return 99 +namespace app +export function answer() returns int64: + return 7 +"#; + + const MUTUAL: &str = r#"namespace app +mutual: + function first() returns int64 + function second() returns int64 +function first() returns int64: + return second() +function second() returns int64: + return 11 +export function scenario() returns int64: + return first() +"#; + + fn original<'a>( + program: &'a CheckedResourceProgram, + namespace: &str, + name: &str, + ) -> &'a jett_parser::ast::FunctionDef { + let mut functions = program.module().items.iter().filter_map(|item| match item { + Item::Function(function) + if function.name.name == name + && program + .resolved() + .scope_table + .definitions + .iter() + .any(|info| { + info.kind == DefKind::Function + && info.span == function.name.span + && info.namespace.as_deref() == Some(namespace) + }) => + { + Some(function) + } + _ => None, + }); + let function = functions + .next() + .expect("unique original ordinary declaration"); + assert!(functions.next().is_none()); + function + } + + #[test] + fn retained_declaration_ordinary_has_no_resolution_and_no_name_fallback() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(ORDINARY, release); + let original = original(&program, "app", "answer"); + assert!( + !program + .resolved() + .resolutions + .contains_key(&original.name.span) + ); + let execution = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let definition = execution + .declaration_definition(original.name.span) + .unwrap(); + assert!(std::ptr::eq( + execution.retained_function(definition).unwrap(), + original + )); + let info = &program.resolved().scope_table.definitions[definition.index() as usize]; + assert_eq!(info.span, original.name.span); + assert_eq!(info.name, "app.answer"); + assert_eq!( + execution + .source_function_context(original.name.span) + .unwrap(), + (Some("app".to_string()), false) + ); + } + } + + #[test] + fn retained_declaration_present_malformed_resolution_never_uses_ordinary_fallback() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(ORDINARY, release); + let declaration = original(&program, "app", "answer").name.span; + let foreign = original(&program, "other", "answer").name.span; + let execution = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let foreign_definition = execution.declaration_definition(foreign).unwrap(); + for wrong in [foreign_definition, DefId::new(u32::MAX)] { + let mut resolutions = program.resolved().resolutions.clone(); + resolutions.insert(declaration, wrong); + assert_eq!( + retained_declaration_definition( + program.module(), + &program.resolved().scope_table.definitions, + &resolutions, + declaration + ), + Err(ResourceExecutionError::InvalidInvocation) + ); + } + let mut definitions = program.resolved().scope_table.definitions.clone(); + let definition = execution.declaration_definition(declaration).unwrap(); + let duplicate = definitions[definition.index() as usize].clone(); + definitions.push(duplicate); + assert_eq!( + retained_declaration_definition( + program.module(), + &definitions, + &program.resolved().resolutions, + declaration + ), + Err(ResourceExecutionError::InvalidInvocation) + ); + let mut module = program.module().clone(); + module + .items + .push(Item::Function(original(&program, "app", "answer").clone())); + assert_eq!( + retained_declaration_definition( + &module, + &program.resolved().scope_table.definitions, + &program.resolved().resolutions, + declaration + ), + Err(ResourceExecutionError::InvalidInvocation) + ); + } + } + + #[test] + fn retained_declaration_mutual_rejoins_exact_forward_and_actual_nodes() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(MUTUAL, release); + let actual = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "first" => Some(function), + _ => None, + }) + .unwrap(); + let execution = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let definition = execution.declaration_definition(actual.name.span).unwrap(); + assert_eq!( + program.resolved().resolutions.get(&actual.name.span), + Some(&definition) + ); + assert_ne!( + program.resolved().scope_table.definitions[definition.index() as usize].span, + actual.name.span + ); + assert!(std::ptr::eq( + execution.retained_function(definition).unwrap(), + actual + )); + let mut resolutions = program.resolved().resolutions.clone(); + resolutions.remove(&actual.name.span); + assert_eq!( + retained_declaration_definition( + program.module(), + &program.resolved().scope_table.definitions, + &resolutions, + actual.name.span + ), + Err(ResourceExecutionError::InvalidInvocation) + ); + let mut definitions = program.resolved().scope_table.definitions.clone(); + definitions[definition.index() as usize].span = actual.body.span; + assert_eq!( + retained_declaration_definition( + program.module(), + &definitions, + &program.resolved().resolutions, + actual.name.span + ), + Err(ResourceExecutionError::InvalidInvocation) + ); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/assignment.rs b/crates/jett_comptime/src/resource_execution/checked/assignment.rs new file mode 100644 index 00000000..493e5eb4 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/assignment.rs @@ -0,0 +1,162 @@ +//! A replacement joins the original statement and its complete checked region. +use super::*; +use jett_parser::ast::{AssignStmt, Expr}; +use jett_resolve::DefKind; +use jett_typecheck::CheckedBindingMode; + +pub(crate) struct CheckedAssignment<'source> { + source: &'source AssignStmt, + fact: CheckedBindingFact, + key: CheckedAttemptKey, +} + +impl CheckedAssignment<'_> { + pub(crate) fn fact(&self) -> CheckedBindingFact { + self.fact + } +} + +impl CheckedExecution { + pub(crate) fn prepare_assignment<'source>( + &self, + source: &'source AssignStmt, + ) -> Result, ResourceExecutionError> { + let key = self.original_assignment_key(source)?; + let Expr::Ident(target) = &source.target else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + let definition = self.resolved_definition(target.span)?; + let facts = self.facts(&self.body)?; + let mut candidates = facts + .bindings + .iter() + .filter(|(_, fact)| fact.definition == definition); + let (declaration, fact) = candidates + .next() + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let definition_info = self + .program + .resolved() + .scope_table + .definitions + .get(definition.index() as usize) + .ok_or(ResourceExecutionError::InvalidCheckedProgram)?; + if candidates.next().is_some() + || *declaration != fact.declaration_span + || definition_info.id != definition + || definition_info.span != fact.declaration_span + || definition_info.name != target.name + || !matches!(definition_info.kind, DefKind::Variable | DefKind::Param) + || !fact.mutable + || fact.mode != CheckedBindingMode::Owned + || self.expression_type(target.span)? != fact.ty + || self.expression_type(source.value.span())? != fact.ty + || !self.type_contains_resource(fact.ty)? + { + return Err(ResourceExecutionError::InvalidOwner); + } + Ok(CheckedAssignment { + source, + fact: *fact, + key, + }) + } + + pub(crate) fn revalidate_assignment( + &self, + assignment: &CheckedAssignment<'_>, + ) -> Result<(), ResourceExecutionError> { + let current = self.prepare_assignment(assignment.source)?; + if current.fact != assignment.fact || current.key != assignment.key { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use jett_common::FileId; + use jett_parser::ast::{FunctionDef, Item, Stmt}; + + const SOURCE: &str = include_str!("../fixtures/24_mutable_assignment.jett"); + + fn function<'a>(program: &'a CheckedResourceProgram, name: &str) -> &'a FunctionDef { + program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.name == name && function.name.span.file == FileId::new(0) => + { + Some(function) + } + _ => None, + }) + .unwrap() + } + + fn assignment(function: &FunctionDef) -> &AssignStmt { + function + .body + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::Assign(assignment) => Some(assignment), + _ => None, + }) + .unwrap() + } + + fn install(checked: &mut CheckedExecution, function: &FunctionDef) { + let definition = checked.declaration_definition(function.name.span).unwrap(); + let invocation = checked.entry(definition).unwrap(); + let reference = checked.prepare_function_body(&invocation).unwrap(); + checked.install_function_body(&reference).unwrap(); + } + + #[test] + fn checked_resource_assignment_refuses_clones_foreign_programs_and_other_bodies() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(SOURCE, release); + let original = function(&program, "replace_live"); + let original_assignment = assignment(original); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + install(&mut checked, original); + checked.prepare_assignment(original_assignment).unwrap(); + let clone = original_assignment.clone(); + assert!(checked.prepare_assignment(&clone).is_err()); + let foreign = crate::resource_execution::tests::program(SOURCE, release); + assert!( + checked + .prepare_assignment(assignment(function(&foreign, "replace_live"))) + .is_err() + ); + install(&mut checked, function(&program, "rebind_after_close")); + assert!(checked.prepare_assignment(original_assignment).is_err()); + } + } + + #[test] + fn checked_resource_assignment_rejoins_prepared_facts_and_body_after_rhs() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(SOURCE, release); + let original = function(&program, "replace_live"); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + install(&mut checked, original); + let mut prepared = checked.prepare_assignment(assignment(original)).unwrap(); + checked.revalidate_assignment(&prepared).unwrap(); + prepared.fact.mutable = false; + assert!(checked.revalidate_assignment(&prepared).is_err()); + prepared.fact.mutable = true; + install(&mut checked, function(&program, "rebind_after_close")); + assert!(checked.revalidate_assignment(&prepared).is_err()); + checked.enter_ordinary(); + assert!(checked.revalidate_assignment(&prepared).is_err()); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference.rs new file mode 100644 index 00000000..008febd4 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference.rs @@ -0,0 +1,935 @@ +//! Original executable identity for the first checked scoped-entry layer. +//! No runtime ownership or provider authority is carried by these references. + +use jett_intrinsics::IntrinsicId; +use jett_parser::ast::{ + AssignStmt, Block, CallArg, ComptimeTypeBindStmt, Expr, FunctionDef, Item, Stmt, StringPart, +}; + +use super::*; +use crate::checked_types::{CheckedScopedBindings, CheckedScopedTypes}; + +#[path = "body_reference/absent_generated.rs"] +mod absent_generated; +pub(crate) use absent_generated::PreparedAbsentBindings; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum ExecutableOrigin { + Function { definition: DefId, item: usize }, + NamespaceInitializer { item: usize }, + Verify { item: usize }, + Property { item: usize }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum RegionStep { + Scope(Span), + Expression(usize), +} + +#[derive(Debug, Clone)] +pub(super) struct ExecutableIdentity { + program: Arc, + origin: ExecutableOrigin, + path: Vec, +} + +#[derive(Clone, Copy)] +enum OriginalRegion<'a> { + Block(&'a Block), + Expression(&'a Expr), +} + +#[path = "body_reference/required.rs"] +mod required; +pub(crate) use required::{CheckedAttemptKey, PreparedRequiredExpression}; +pub use required::{ + CheckedRequiredOwner, CheckedRequiredResourceHook, CheckedRequiredScope, CheckedRequiredValue, +}; +#[path = "body_reference/pipeline.rs"] +mod pipeline; +pub(crate) use pipeline::PreparedPipelineStep; +#[path = "body_reference/intrinsic.rs"] +mod intrinsic; +pub(crate) use intrinsic::PreparedIntrinsicArguments; +#[path = "body_reference/named_callable.rs"] +mod named_callable; +pub(crate) use named_callable::PreparedNamedCallable; +#[path = "body_reference/owned_struct.rs"] +mod owned_struct; +pub(crate) use owned_struct::PreparedOwnedStruct; +#[path = "body_reference/reflected_fields.rs"] +mod reflected_fields; +pub(crate) use reflected_fields::{PreparedReflectedFieldIteration, PreparedReflectedFieldLoop}; + +/// Minted only from one retained program and an exact accepted body selection. +#[derive(Debug, Clone)] +pub(crate) struct CheckedBodyReference { + cursor: CheckedBodyCursor, +} + +pub(crate) struct PreparedDirectScope { + reference: CheckedBodyReference, + projection: Arc, + reflection: ReflectionTypeInfo, +} + +impl CheckedBodyReference { + pub(crate) fn function(&self) -> Result<&FunctionDef, ResourceExecutionError> { + let identity = self + .cursor + .executable + .as_ref() + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let ExecutableOrigin::Function { item, .. } = identity.origin else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + match identity.program.module().items.get(item) { + Some(Item::Function(function)) => Ok(function), + _ => Err(ResourceExecutionError::MissingCheckedBody), + } + } + + fn region(&self) -> Result, ResourceExecutionError> { + let identity = self + .cursor + .executable + .as_ref() + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let mut region = match identity.origin { + ExecutableOrigin::Function { .. } => OriginalRegion::Block(&self.function()?.body), + ExecutableOrigin::NamespaceInitializer { item } => { + match identity.program.module().items.get(item) { + Some(Item::VarDecl(value)) => OriginalRegion::Expression(&value.value), + _ => return Err(ResourceExecutionError::MissingCheckedBody), + } + } + ExecutableOrigin::Verify { item } => match identity.program.module().items.get(item) { + Some(Item::Verify(value)) => OriginalRegion::Block(&value.body), + _ => return Err(ResourceExecutionError::MissingCheckedBody), + }, + ExecutableOrigin::Property { item } => { + match identity.program.module().items.get(item) { + Some(Item::Property(value)) => OriginalRegion::Block(&value.body), + _ => return Err(ResourceExecutionError::MissingCheckedBody), + } + } + }; + for step in &identity.path { + region = match step { + RegionStep::Scope(owner) => { + OriginalRegion::Block(®ion_binding(region, *owner)?.body) + } + RegionStep::Expression(index) => { + OriginalRegion::Expression(region_expression(region, *index)?) + } + }; + } + Ok(region) + } + + fn block(&self) -> Result<&Block, ResourceExecutionError> { + match self.region()? { + OriginalRegion::Block(block) => Ok(block), + OriginalRegion::Expression(_) => Err(ResourceExecutionError::MissingCheckedBody), + } + } +} + +impl PreparedDirectScope { + pub(crate) fn projection(&self) -> Arc { + self.projection.clone() + } + pub(crate) fn reflection(&self) -> &ReflectionTypeInfo { + &self.reflection + } + pub(crate) fn bound_name(&self) -> &str { + &self.projection.bound_type + } + pub(crate) fn body(&self) -> Result<&Block, ResourceExecutionError> { + self.reference.block() + } +} + +impl CheckedExecution { + pub(crate) fn prepare_function_body( + &self, + invocation: &FunctionInvocation, + ) -> Result { + let (definition, root) = match invocation { + FunctionInvocation::Entry { definition, .. } => (*definition, BodyRoot::Ordinary), + FunctionInvocation::NamedSource { source, target } => { + self.validate_named_indirect(source, target)?; + (target.definition(), BodyRoot::Ordinary) + } + FunctionInvocation::Source(invocation) => { + self.validate_executable_cursor(&invocation.body)?; + if self.facts(&invocation.body)?.calls.get(&invocation.span) + != Some(&invocation.packet) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + match invocation.target() { + CheckedInvocationTarget::Resolved(definition) => { + (*definition, BodyRoot::Ordinary) + } + CheckedInvocationTarget::Generic(call) => { + let indices = self + .program + .checked() + .generic_function_instantiations + .iter() + .enumerate() + .filter(|(_, body)| { + body.definition == call.definition + && body.concrete_args == call.concrete_args + && body.specialization == call.specialization + }) + .map(|(index, _)| index) + .collect::>(); + let [index] = indices.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + (call.definition, BodyRoot::Generic(*index)) + } + _ => return Err(ResourceExecutionError::InvalidInvocation), + } + } + }; + let function = self.retained_function(definition)?; + let signature = invocation.signature(self)?; + if signature.index() as usize >= self.program.checked().interner.len() { + return Err(ResourceExecutionError::InvalidInvocation); + } + let Type::Function { + params, + view_params, + return_type, + } = self.program.checked().interner.resolve(signature) + else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if params.len() != function.params.len() + || view_params.len() != params.len() + || function + .params + .iter() + .zip(view_params) + .any(|(parameter, view)| parameter.view != *view) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + match root { + BodyRoot::Ordinary => { + if self.program.checked().definition_types.get(&definition) != Some(&signature) { + return Err(ResourceExecutionError::InvalidInvocation); + } + } + BodyRoot::Generic(index) => { + let concrete = self + .program + .checked() + .generic_function_instantiations + .get(index) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + if concrete.parameter_types != *params || concrete.return_type != *return_type { + return Err(ResourceExecutionError::InvalidInvocation); + } + } + } + if let Some(source) = invocation.source() { + if source.arguments().len() != params.len() + || source.arguments().iter().any(|argument| { + params.get(argument.parameter_index) != Some(&argument.parameter_type) + || view_params.get(argument.parameter_index).copied() + != Some( + argument.callee_access == jett_typecheck::CheckedCalleeAccess::View, + ) + }) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + } + let items = self + .program + .module() + .items + .iter() + .enumerate() + .filter_map(|(index, item)| { + matches!(item, Item::Function(original) if std::ptr::eq(original, function)) + .then_some(index) + }) + .collect::>(); + let [function_item] = items.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let reference = CheckedBodyReference { + cursor: CheckedBodyCursor { + root, + scopes: Vec::new(), + executable: Some(ExecutableIdentity { + program: self.program.clone(), + origin: ExecutableOrigin::Function { + definition, + item: *function_item, + }, + path: Vec::new(), + }), + }, + }; + self.validate_executable_cursor(&reference.cursor)?; + Ok(reference) + } + + pub(super) fn validate_executable_cursor( + &self, + cursor: &CheckedBodyCursor, + ) -> Result<(), ResourceExecutionError> { + let identity = cursor + .executable + .as_ref() + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + if !Arc::ptr_eq(&identity.program, &self.program) { + return Err(ResourceExecutionError::ForeignProgram); + } + let reference = CheckedBodyReference { + cursor: cursor.clone(), + }; + match identity.origin { + ExecutableOrigin::Function { definition, .. } => { + let function = reference.function()?; + if !std::ptr::eq(self.retained_function(definition)?, function) { + return Err(ResourceExecutionError::MissingCheckedBody); + } + match cursor.root { + BodyRoot::Ordinary if !function.type_params.is_empty() => { + return Err(ResourceExecutionError::MissingCheckedBody); + } + BodyRoot::Generic(index) => { + let instance = self + .program + .checked() + .generic_function_instantiations + .get(index) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + if instance.definition != definition + || instance.concrete_args.len() != function.type_params.len() + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + } + BodyRoot::Ordinary => {} + } + } + _ if !matches!(cursor.root, BodyRoot::Ordinary) => { + return Err(ResourceExecutionError::MissingCheckedBody); + } + _ => {} + } + let path_scopes = identity + .path + .iter() + .filter_map(|step| match step { + RegionStep::Scope(owner) => Some(*owner), + _ => None, + }) + .collect::>(); + if path_scopes + != cursor + .scopes + .iter() + .map(|scope| scope.owner) + .collect::>() + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + self.facts(cursor)?; + reference.region()?; + Ok(()) + } + + pub(crate) fn install_function_body( + &mut self, + reference: &CheckedBodyReference, + ) -> Result<(), ResourceExecutionError> { + self.validate_executable_cursor(&reference.cursor)?; + self.body = reference.cursor.clone(); + Ok(()) + } + + pub(crate) fn install_direct_scope( + &mut self, + prepared: &PreparedDirectScope, + ) -> Result<(), ResourceExecutionError> { + self.install_function_body(&prepared.reference) + } + + pub(crate) fn prepare_direct_scope( + &self, + bind: &ComptimeTypeBindStmt, + ) -> Result { + self.validate_executable_cursor(&self.body)?; + let parent = CheckedBodyReference { + cursor: self.body.clone(), + }; + let original = region_binding(parent.region()?, bind.span)?; + if !std::ptr::eq(original, bind) { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let Expr::GenericCall(_, source_types, arguments, occurrence) = &original.value else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + if source_types.len() != 1 { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let facts = self.facts(&self.body)?; + let intrinsic = facts + .intrinsics + .get(occurrence) + .ok_or(ResourceExecutionError::MissingCheckedInvocation)?; + let types = facts + .intrinsic_types + .get(occurrence) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let reflections = facts + .intrinsic_reflections + .get(occurrence) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let ([source_type], [source_reflection]) = (types.as_slice(), reflections.as_slice()) + else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + if source_type.index() as usize >= self.program.checked().interner.len() { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let selected = facts + .scopes + .get(&original.span) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let candidates = selected + .iter() + .enumerate() + .filter(|(_, candidate)| { + candidate.selection == CheckedComptimeTypeSelection::Unconditional + }) + .collect::>(); + let [(index, candidate)] = candidates.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let reflection_matches = match intrinsic { + IntrinsicId::TypeInfo if arguments.is_empty() => { + candidate.bound_type == *source_type && candidate.reflection == *source_reflection + } + IntrinsicId::TypeArg if arguments.len() == 1 && arguments[0].name.is_none() => { + let Expr::IntLiteral(index, _) = &arguments[0].value else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let index = usize::try_from(*index) + .map_err(|_| ResourceExecutionError::MissingCheckedBody)?; + source_reflection.args.get(index) == Some(&candidate.reflection) + } + _ => false, + }; + if !reflection_matches { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let mut cursor = self.body.clone(); + cursor.scopes.push(ScopedSelection { + owner: original.span, + index: *index, + bound_type: candidate.bound_type, + selection: candidate.selection, + reflection: candidate.reflection.clone(), + }); + cursor + .executable + .as_mut() + .ok_or(ResourceExecutionError::MissingCheckedBody)? + .path + .push(RegionStep::Scope(original.span)); + self.validate_executable_cursor(&cursor)?; + Ok(PreparedDirectScope { + reference: CheckedBodyReference { cursor }, + projection: scoped_projection(candidate, &self.program.checked().interner), + reflection: candidate.reflection.clone(), + }) + } + + /// True is mandatory checked dispatch, false is an exact checked constructor. + /// A missing body/packet never selects the raw source-name path. + #[cfg(test)] + pub(crate) fn original_body_scope_depth(&self) -> Option { + self.body + .executable + .as_ref() + .map(|_| self.body.scopes.len()) + } + + pub(crate) fn original_assignment( + &self, + assignment: &AssignStmt, + ) -> Result<(), ResourceExecutionError> { + self.validate_executable_cursor(&self.body)?; + let reference = CheckedBodyReference { + cursor: self.body.clone(), + }; + let mut matched = 0usize; + walk_region( + reference.region()?, + &mut |_| {}, + &mut |_| {}, + &mut |statement| { + if let Stmt::Assign(original) = statement { + if std::ptr::eq(original, assignment) { + matched += 1; + } + } + }, + ); + if matched == 1 { + Ok(()) + } else { + Err(ResourceExecutionError::MissingCheckedBody) + } + } + + pub(crate) fn source_call_dispatch( + &self, + callee: &Expr, + arguments: &[CallArg], + span: Span, + ) -> Result { + self.validate_executable_cursor(&self.body)?; + let reference = CheckedBodyReference { + cursor: self.body.clone(), + }; + let mut matched = 0usize; + walk_region( + reference.region()?, + &mut |_| {}, + &mut |expression| { + if let Expr::Call(original_callee, original_args, original_span) + | Expr::GenericCall(original_callee, _, original_args, original_span) = + expression + { + if *original_span == span + && std::ptr::eq(original_callee.as_ref(), callee) + && original_args.len() == arguments.len() + && (arguments.is_empty() + || std::ptr::eq(original_args.as_ptr(), arguments.as_ptr())) + { + matched += 1; + } + } + }, + &mut |_| {}, + ); + if matched != 1 { + return Err(ResourceExecutionError::MissingCheckedInvocation); + } + let facts = self.facts(&self.body)?; + if facts.calls.contains_key(&span) { + return Ok(true); + } + if facts.constructions.contains_key(&span) { + return Ok(false); + } + if self.typed_machine_constructor(callee, arguments, span)? { + return Ok(false); + } + Err(ResourceExecutionError::MissingCheckedInvocation) + } + + // The checker records machine construction as an exact nominal/state + // expression rather than a function invocation. Authenticate that existing + // typed data path separately; it grants no live Resource custody transport. + fn typed_machine_constructor( + &self, + callee: &Expr, + arguments: &[CallArg], + span: Span, + ) -> Result { + let definition = match self.resolved_definition(callee.span()) { + Ok(definition) => definition, + Err(_) => return Ok(false), + }; + let info = self + .program + .resolved() + .scope_table + .definitions + .get(definition.index() as usize) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if info.id != definition || info.kind != jett_resolve::DefKind::Machine { + return Ok(false); + } + let types = &self.program.checked().interner; + let declared = self + .program + .checked() + .definition_types + .get(&definition) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let Type::Machine(machine) = types.resolve(*declared) else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + let facts = self.facts(&self.body)?; + let actual = facts + .source_types + .get(&span) + .or_else(|| facts.types.get(&span)) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let Type::MachineState { + machine: actual_machine, + state, + } = types.resolve(*actual) + else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if machine != actual_machine { + return Err(ResourceExecutionError::InvalidInvocation); + } + let state = types + .resolve_machine(*machine) + .state(*state) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + let Some(first) = arguments.first() else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + let Expr::Ident(selected) = &first.value else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if first.name.is_some() + || selected.name != state.name + || arguments.len() != state.fields.len() + 1 + { + return Err(ResourceExecutionError::InvalidInvocation); + } + if arguments[1..] + .iter() + .zip(&state.fields) + .any(|(argument, (_, expected))| { + facts.types.get(&argument.value.span()) != Some(expected) + }) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(true) + } +} + +fn scoped_projection( + binding: &CheckedComptimeTypeBinding, + types: &jett_types::TypeInterner, +) -> Arc { + // Match the existing driver projection: descendants remain under their + // lexical parent, and their expression names are not reused outside it. + let descendants = binding + .body + .comptime_type_bindings + .values() + .flatten() + .flat_map(|nested| nested.body.comptime_type_bindings.keys().copied()) + .collect::>(); + let bindings: CheckedScopedBindings = binding + .body + .comptime_type_bindings + .iter() + .filter(|(owner, _)| !descendants.contains(owner)) + .map(|(owner, candidates)| { + ( + *owner, + candidates + .iter() + .map(|candidate| scoped_projection(candidate, types)) + .collect(), + ) + }) + .collect(); + let mut expressions = binding + .body + .type_map + .iter() + .map(|(span, ty)| (*span, types.type_name(*ty))) + .collect(); + remove_descendant_expression_names(&mut expressions, &binding.body.comptime_type_bindings); + Arc::new(CheckedScopedTypes { + bound_type: types.type_name(binding.bound_type), + reflection: Some(binding.reflection.clone()), + expressions, + bindings, + }) +} + +fn remove_descendant_expression_names( + expressions: &mut HashMap, + bindings: &HashMap>, +) { + for binding in bindings.values().flatten() { + for span in binding.body.type_map.keys() { + expressions.remove(span); + } + remove_descendant_expression_names(expressions, &binding.body.comptime_type_bindings); + } +} + +fn walk_region<'a>( + region: OriginalRegion<'a>, + binding: &mut impl FnMut(&'a ComptimeTypeBindStmt), + expression: &mut impl FnMut(&'a Expr), + statement: &mut impl FnMut(&'a Stmt), +) { + match region { + OriginalRegion::Block(block) => walk_block_nodes(block, binding, expression, statement), + OriginalRegion::Expression(value) => walk_expr_nodes(value, binding, expression, statement), + } +} + +fn region_binding( + region: OriginalRegion<'_>, + owner: Span, +) -> Result<&ComptimeTypeBindStmt, ResourceExecutionError> { + let mut found = Vec::new(); + walk_region( + region, + &mut |binding| { + if binding.span == owner { + found.push(binding); + } + }, + &mut |_| {}, + &mut |_| {}, + ); + let [binding] = found.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + Ok(*binding) +} + +fn region_expression( + region: OriginalRegion<'_>, + index: usize, +) -> Result<&Expr, ResourceExecutionError> { + let mut found = Vec::new(); + walk_region( + region, + &mut |_| {}, + &mut |expression| { + found.push(expression); + }, + &mut |_| {}, + ); + found + .get(index) + .copied() + .ok_or(ResourceExecutionError::MissingCheckedBody) +} + +fn expression_index( + region: OriginalRegion<'_>, + value: &Expr, +) -> Result { + let mut indices = Vec::new(); + let mut ordinal = 0usize; + walk_region( + region, + &mut |_| {}, + &mut |expression| { + if std::ptr::eq(value, expression) { + indices.push(ordinal); + } + ordinal += 1; + }, + &mut |_| {}, + ); + match indices.as_slice() { + [index] => Ok(*index), + _ => Err(ResourceExecutionError::MissingCheckedBody), + } +} + +fn binding_in(block: &Block, owner: Span) -> Result<&ComptimeTypeBindStmt, ResourceExecutionError> { + let mut bindings = Vec::new(); + walk_block( + block, + &mut |binding| { + if binding.span == owner { + bindings.push(binding); + } + }, + &mut |_| {}, + ); + let [binding] = bindings.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + Ok(*binding) +} + +/// Traverse executable control-flow only; scoped and callable bodies require +/// their own selected reference. Their initializer expressions remain here. +fn walk_block<'a>( + block: &'a Block, + binding: &mut impl FnMut(&'a ComptimeTypeBindStmt), + expression: &mut impl FnMut(&'a Expr), +) { + walk_block_nodes(block, binding, expression, &mut |_| {}); +} + +fn walk_statements<'a>(block: &'a Block, statement: &mut impl FnMut(&'a Stmt)) { + walk_block_nodes(block, &mut |_| {}, &mut |_| {}, statement); +} + +fn walk_block_nodes<'a>( + block: &'a Block, + binding: &mut impl FnMut(&'a ComptimeTypeBindStmt), + expression: &mut impl FnMut(&'a Expr), + statement: &mut impl FnMut(&'a Stmt), +) { + for node in &block.stmts { + statement(node); + match node { + Stmt::VarDecl(value) => walk_expr_nodes(&value.value, binding, expression, statement), + Stmt::Assign(value) => { + walk_expr_nodes(&value.target, binding, expression, statement); + walk_expr_nodes(&value.value, binding, expression, statement); + } + Stmt::Return(value) => { + if let Some(value) = &value.value { + walk_expr_nodes(value, binding, expression, statement); + } + } + Stmt::Respond(value) => walk_expr_nodes(&value.value, binding, expression, statement), + Stmt::ComptimeTypeBind(value) => { + binding(value); + walk_expr_nodes(&value.value, binding, expression, statement); + } + Stmt::If(value) => { + walk_expr_nodes(&value.condition, binding, expression, statement); + walk_block_nodes(&value.then_block, binding, expression, statement); + for (condition, block) in &value.else_ifs { + walk_expr_nodes(condition, binding, expression, statement); + walk_block_nodes(block, binding, expression, statement); + } + if let Some(block) = &value.else_block { + walk_block_nodes(block, binding, expression, statement); + } + } + Stmt::For(value) => { + walk_expr_nodes(&value.iterable, binding, expression, statement); + walk_block_nodes(&value.body, binding, expression, statement); + } + Stmt::While(value) => { + walk_expr_nodes(&value.condition, binding, expression, statement); + walk_block_nodes(&value.body, binding, expression, statement); + } + Stmt::Match(value) => { + walk_expr_nodes(&value.expr, binding, expression, statement); + for arm in &value.arms { + walk_block_nodes(&arm.body, binding, expression, statement); + } + } + Stmt::Expr(value) => walk_expr_nodes(&value.expr, binding, expression, statement), + Stmt::Assert(value) => { + walk_expr_nodes(&value.condition, binding, expression, statement); + if let Some(message) = &value.message { + walk_expr_nodes(message, binding, expression, statement); + } + } + Stmt::Breakpoint(value) => { + if let Some(condition) = &value.condition { + walk_expr_nodes(condition, binding, expression, statement); + } + } + Stmt::Use(_) | Stmt::Trace(_) | Stmt::Break(_) | Stmt::Continue(_) => {} + } + } +} + +fn walk_expr_nodes<'a>( + value: &'a Expr, + binding: &mut impl FnMut(&'a ComptimeTypeBindStmt), + expression: &mut impl FnMut(&'a Expr), + statement: &mut impl FnMut(&'a Stmt), +) { + expression(value); + match value { + Expr::Binary(left, _, right, _) => { + walk_expr_nodes(left, binding, expression, statement); + walk_expr_nodes(right, binding, expression, statement); + } + Expr::Unary(_, value, _) + | Expr::FieldAccess(value, _, _) + | Expr::Paren(value, _) + | Expr::View(value, _) + | Expr::Comptime(value, _) + | Expr::Ok(value, _) + | Expr::Fail(value, _) + | Expr::Some(value, _) + | Expr::Default(value, _) + | Expr::Declassify(value, _) + | Expr::Coarsen(value, _) + | Expr::At(value, _, _) + | Expr::Spawn(value, _) + | Expr::Send(value, _) + | Expr::Ask(value, _) + | Expr::Clone(value, _) + | Expr::Run(value, _) + | Expr::Join(value, _) + | Expr::Cancel(value, _) => walk_expr_nodes(value, binding, expression, statement), + Expr::Call(callee, arguments, _) | Expr::GenericCall(callee, _, arguments, _) => { + walk_expr_nodes(callee, binding, expression, statement); + for argument in arguments { + walk_expr_nodes(&argument.value, binding, expression, statement); + } + } + Expr::ListConstruct(values, _) => { + for value in values { + walk_expr_nodes(value, binding, expression, statement); + } + } + Expr::MapConstruct(values, _) => { + for (key, value) in values { + walk_expr_nodes(key, binding, expression, statement); + walk_expr_nodes(value, binding, expression, statement); + } + } + Expr::Handle(value, _, block, _) => { + walk_expr_nodes(value, binding, expression, statement); + walk_block_nodes(block, binding, expression, statement); + } + Expr::StringInterpolation(parts, _) => { + for part in parts { + if let StringPart::Expr(value) = part { + walk_expr_nodes(value, binding, expression, statement); + } + } + } + Expr::Pipeline(value, steps, _) => { + walk_expr_nodes(value, binding, expression, statement); + for step in steps { + walk_expr_nodes(&step.function, binding, expression, statement); + for argument in &step.extra_args { + walk_expr_nodes(&argument.value, binding, expression, statement); + } + if let Some(handle) = &step.handle { + walk_block_nodes(&handle.body, binding, expression, statement); + } + } + } + Expr::InlineFn(_, _, _, _) + | Expr::IntLiteral(_, _) + | Expr::FloatLiteral(_, _) + | Expr::StringLiteral(_, _) + | Expr::BoolLiteral(_, _) + | Expr::Nothing(_) + | Expr::Ident(_) + | Expr::None(_) + | Expr::EnumVariant(_, _, _) + | Expr::Error(_) => {} + } +} + +#[cfg(test)] +#[path = "body_reference/tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/absent_generated.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/absent_generated.rs new file mode 100644 index 00000000..1f69e4d7 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/absent_generated.rs @@ -0,0 +1,282 @@ +//! Exact generated control binders; the proof carries no runtime custody. +use super::*; +use crate::Value; +use jett_parser::ast::{ForStmt, Ident, MatchStmt, Pattern}; +use jett_typecheck::{CheckedBindingMode, CheckedViewSource}; + +enum Control<'source> { + For(&'source ForStmt), + Match(&'source MatchStmt, usize, String), +} + +pub(crate) struct PreparedAbsentBindings<'source> { + key: CheckedAttemptKey, + control: Control<'source>, + parent: TypeId, + payload_count: Option, + bindings: Vec<(&'source Ident, CheckedBindingFact)>, +} + +impl PreparedAbsentBindings<'_> { + pub(crate) fn parent_type(&self) -> TypeId { + self.parent + } + pub(crate) fn binding_count(&self) -> usize { + self.bindings.len() + } + + pub(crate) fn validate_binding( + &self, + original: &Ident, + ordinal: usize, + ) -> Result { + let (binder, fact) = self + .bindings + .get(ordinal) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if !std::ptr::eq(*binder, original) { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(*fact) + } + + pub(crate) fn validate_parent(&self, value: &Value) -> Result<(), ResourceExecutionError> { + match (&self.control, value.payload()) { + (Control::For(source), Value::List(_)) if source.value_variable.is_none() => Ok(()), + (Control::For(_), Value::Map(_)) => Ok(()), + ( + Control::Match(source, arm, variant), + Value::Enum { + variant: actual, + fields, + .. + }, + ) if (variant.is_empty() || variant == actual) + && self.payload_count.is_none_or(|count| count == fields.len()) + && source + .arms + .iter() + .position(|candidate| match &candidate.pattern { + Pattern::Ident(name) | Pattern::Variant(name, _) => name.name == *actual, + Pattern::Other(_) => true, + }) + == Some(*arm) => + { + Ok(()) + } + _ => Err(ResourceExecutionError::InvalidPayloadPath), + } + } +} + +impl CheckedExecution { + fn original_absent_control( + &self, + source: impl Fn(&Stmt) -> bool, + ) -> Result<(), ResourceExecutionError> { + self.validate_executable_cursor(&self.body)?; + let reference = CheckedBodyReference { + cursor: self.body.clone(), + }; + let mut count = 0; + walk_region( + reference.region()?, + &mut |_| {}, + &mut |_| {}, + &mut |statement| { + if source(statement) { + count += 1; + } + }, + ); + if count == 1 { + Ok(()) + } else { + Err(ResourceExecutionError::MissingCheckedBody) + } + } + + fn absent_binder( + &self, + original: &Ident, + ty: TypeId, + mode: CheckedBindingMode, + ) -> Result { + let fact = self.binding(original.span)?; + let info = self + .program + .resolved() + .scope_table + .definitions + .get(fact.definition.index() as usize) + .ok_or(ResourceExecutionError::InvalidCheckedProgram)?; + if info.id != fact.definition + || info.span != original.span + || info.name != original.name + || info.kind != jett_resolve::DefKind::Variable + || fact.declaration_span != original.span + || fact.ty != ty + || fact.mode != mode + || fact.mutable + || self.expression_type(original.span)? != ty + { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(fact) + } + + pub(crate) fn prepare_absent_for<'source>( + &self, + source: &'source ForStmt, + ) -> Result>, ResourceExecutionError> { + self.original_absent_control( + |statement| matches!(statement, Stmt::For(original) if std::ptr::eq(original, source)), + )?; + let parent = self.expression_type(source.iterable.span())?; + if !self.type_contains_resource(parent)? { + return Ok(None); + } + let types = &self.program.checked().interner; + let mode = |ty| { + if source.view && !jett_typecheck::ownership::is_implicitly_copyable(types, ty) { + CheckedBindingMode::View { + source: CheckedViewSource::Other, + } + } else { + CheckedBindingMode::Owned + } + }; + let mut bindings = Vec::new(); + match types.resolve(parent) { + Type::List(item) if source.value_variable.is_none() => { + bindings.push(( + &source.variable, + self.absent_binder(&source.variable, *item, mode(*item))?, + )); + } + Type::Map(key, item) => { + bindings.push(( + &source.variable, + self.absent_binder(&source.variable, *key, mode(*key))?, + )); + if let Some(value) = &source.value_variable { + bindings.push((value, self.absent_binder(value, *item, mode(*item))?)); + } + } + _ => return Err(ResourceExecutionError::InvalidPayloadPath), + } + Ok(Some(PreparedAbsentBindings { + key: self.attempt_key(&self.body)?, + control: Control::For(source), + parent, + payload_count: None, + bindings, + })) + } + + pub(crate) fn prepare_absent_match<'source>( + &self, + source: &'source MatchStmt, + arm: usize, + ) -> Result>, ResourceExecutionError> { + self.original_absent_control(|statement| matches!(statement, Stmt::Match(original) if std::ptr::eq(original, source)))?; + let parent = self.expression_type(source.expr.span())?; + if !self.type_contains_resource(parent)? { + return Ok(None); + } + let Some(selected) = source.arms.get(arm) else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if let Some(selections) = self.facts(&self.body)?.selections { + if let Some(selection) = selections.get(&source.span) { + if *selection != jett_typecheck::CheckedStaticSelection::MatchArm(arm) { + return Err(ResourceExecutionError::MissingCheckedBody); + } + } + } + let (variant, names) = match &selected.pattern { + Pattern::Variant(name, names) => (Some(name), names.as_slice()), + Pattern::Ident(name) => (Some(name), &[][..]), + Pattern::Other(_) => (None, &[][..]), + }; + let types = &self.program.checked().interner; + let Type::Enum(owner) = types.resolve(parent) else { + return Err(ResourceExecutionError::InvalidPayloadPath); + }; + let schema = types.resolve_enum(*owner); + let (bindings, payload_count) = if let Some(variant) = variant { + let mut matching = schema + .variants + .iter() + .filter(|candidate| candidate.name == variant.name); + let selected = matching + .next() + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if matching.next().is_some() + || (matches!(source.arms[arm].pattern, Pattern::Variant(..)) + && names.len() != selected.fields.len()) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let bindings = names + .iter() + .zip(&selected.fields) + .map(|(name, (_, ty))| { + self.absent_binder(name, *ty, CheckedBindingMode::Owned) + .map(|fact| (name, fact)) + }) + .collect::, _>>()?; + (bindings, Some(selected.fields.len())) + } else { + (Vec::new(), None) + }; + Ok(Some(PreparedAbsentBindings { + key: self.attempt_key(&self.body)?, + control: Control::Match( + source, + arm, + variant.map(|name| name.name.clone()).unwrap_or_default(), + ), + parent, + payload_count, + bindings, + })) + } + + pub(crate) fn revalidate_absent_bindings( + &self, + proof: &PreparedAbsentBindings<'_>, + ) -> Result<(), ResourceExecutionError> { + if self.attempt_key(&self.body)? != proof.key { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let current = match &proof.control { + Control::For(source) => self.prepare_absent_for(source)?, + Control::Match(source, arm, _) => self.prepare_absent_match(source, *arm)?, + } + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let same_control = match (¤t.control, &proof.control) { + (Control::For(left), Control::For(right)) => std::ptr::eq(*left, *right), + ( + Control::Match(left, arm, variant), + Control::Match(right, expected_arm, expected_variant), + ) => std::ptr::eq(*left, *right) && arm == expected_arm && variant == expected_variant, + _ => false, + }; + if !same_control + || current.parent != proof.parent + || current.payload_count != proof.payload_count + || current.bindings.len() != proof.bindings.len() + || !current.bindings.iter().zip(&proof.bindings).all( + |((left, fact), (right, expected))| std::ptr::eq(*left, *right) && fact == expected, + ) + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(()) + } +} + +#[cfg(test)] +#[path = "absent_generated/tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/absent_generated/tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/absent_generated/tests.rs new file mode 100644 index 00000000..49ad2941 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/absent_generated/tests.rs @@ -0,0 +1,315 @@ +//! Private rejection controls for exact generated binding proofs. +use super::*; +use jett_common::FileId; +use jett_parser::ast::{FunctionDef, Item}; +use jett_types::TypeInterner; + +#[path = "../../../../../../jett_driver/tests/native_conformance/resource_absent_aggregate_baseline_inputs.rs"] +mod inputs; + +fn checked_program(index: usize, release: bool) -> Arc { + inputs::prepare(&inputs::INPUTS[index], release, &mut Vec::new()) + .unwrap() + .0 +} + +fn function<'a>(program: &'a CheckedResourceProgram, name: &str) -> &'a FunctionDef { + program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) && function.name.name == name => + { + Some(function) + } + _ => None, + }) + .unwrap() +} + +fn install(checked: &mut CheckedExecution, program: &CheckedResourceProgram, name: &str) { + let function = function(program, name); + let definition = checked.declaration_definition(function.name.span).unwrap(); + let body = checked + .prepare_function_body(&checked.entry(definition).unwrap()) + .unwrap(); + checked.install_function_body(&body).unwrap(); +} + +fn for_statement(program: &CheckedResourceProgram) -> &ForStmt { + function(program, "main") + .body + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::For(statement) => Some(statement), + _ => None, + }) + .unwrap() +} + +fn match_statement(program: &CheckedResourceProgram) -> &MatchStmt { + function(program, "main") + .body + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::Match(statement) => Some(statement), + _ => None, + }) + .unwrap() +} + +fn current_main(checked: &CheckedExecution, program: &CheckedResourceProgram) { + let current = CheckedBodyReference { + cursor: checked.cursor(), + }; + assert!(std::ptr::eq( + current.function().unwrap(), + function(program, "main") + )); + assert!(checked.body.scopes.is_empty()); +} + +#[test] +fn absent_generated_proofs_select_original_list_map_and_variant_binders() { + for release in [false, true] { + for (index, count, parent_name, names, types) in [ + ( + 1, + 1, + "list[optional[resource_probe.TestHandle]]", + vec!["candidate"], + vec!["optional[resource_probe.TestHandle]"], + ), + ( + 3, + 2, + "map[string, optional[resource_probe.TestHandle]]", + vec!["key", "candidate"], + vec!["string", "optional[resource_probe.TestHandle]"], + ), + ( + 9, + 2, + "app.TokenChoice", + vec!["marker", "tokens"], + vec!["int64", "list[resource_probe.TestHandle]"], + ), + ] { + let program = checked_program(index, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + install(&mut checked, &program, "main"); + let proof = if index == 9 { + checked + .prepare_absent_match(match_statement(&program), 0) + .unwrap() + .unwrap() + } else { + checked + .prepare_absent_for(for_statement(&program)) + .unwrap() + .unwrap() + }; + assert_eq!(proof.binding_count(), count); + assert_eq!( + program.checked().interner.type_name(proof.parent_type()), + parent_name + ); + for (ordinal, (binder, fact)) in proof.bindings.iter().enumerate() { + assert_eq!(binder.name, names[ordinal]); + assert_eq!( + program.checked().interner.type_name(fact.ty), + types[ordinal] + ); + assert_eq!(fact.mode, CheckedBindingMode::Owned); + assert!(!fact.mutable); + assert_eq!(fact.declaration_span, binder.span); + assert_eq!(proof.validate_binding(binder, ordinal).unwrap(), *fact); + } + checked.revalidate_absent_bindings(&proof).unwrap(); + current_main(&checked, &program); + } + } +} + +#[test] +fn absent_generated_proofs_reject_copied_controls_binders_and_wrong_ordinals() { + for release in [false, true] { + for index in [1, 3] { + let program = checked_program(index, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + install(&mut checked, &program, "main"); + let statement = for_statement(&program); + let copied = statement.clone(); + assert!(checked.prepare_absent_for(&copied).is_err()); + let proof = checked.prepare_absent_for(statement).unwrap().unwrap(); + let binder = statement.variable.clone(); + assert!(proof.validate_binding(&binder, 0).is_err()); + assert!( + proof + .validate_binding(&statement.variable, proof.binding_count()) + .is_err() + ); + if let Some(value) = &statement.value_variable { + assert!(proof.validate_binding(value, 0).is_err()); + assert!(proof.validate_binding(&statement.variable, 1).is_err()); + } + current_main(&checked, &program); + } + let program = checked_program(9, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + install(&mut checked, &program, "main"); + let statement = match_statement(&program); + let copied = statement.clone(); + assert!(checked.prepare_absent_match(&copied, 0).is_err()); + assert!(checked.prepare_absent_match(statement, usize::MAX).is_err()); + let proof = checked.prepare_absent_match(statement, 0).unwrap().unwrap(); + let Pattern::Variant(_, binders) = &statement.arms[0].pattern else { + panic!("original vacant pattern") + }; + assert!(proof.validate_binding(&binders[0].clone(), 0).is_err()); + assert!(proof.validate_binding(&binders[1], 0).is_err()); + assert!(proof.validate_binding(&binders[0], 1).is_err()); + current_main(&checked, &program); + } +} + +#[test] +fn absent_generated_proofs_reject_foreign_program_changed_body_and_corrupt_facts() { + for release in [false, true] { + let program = checked_program(1, release); + let foreign_program = checked_program(1, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let mut foreign = + CheckedExecution::new(foreign_program.clone(), ExecutionPurpose::ReferenceRuntime) + .unwrap(); + install(&mut checked, &program, "main"); + install(&mut foreign, &foreign_program, "main"); + let statement = for_statement(&program); + let proof = checked.prepare_absent_for(statement).unwrap().unwrap(); + assert!(foreign.revalidate_absent_bindings(&proof).is_err()); + assert!(foreign.prepare_absent_for(statement).is_err()); + install(&mut checked, &program, "absent_tokens"); + assert!(checked.revalidate_absent_bindings(&proof).is_err()); + install(&mut checked, &program, "main"); + let copied = statement.clone(); + for corruption in 0..8 { + let mut bad = checked.prepare_absent_for(statement).unwrap().unwrap(); + match corruption { + 0 => bad.parent = TypeInterner::STRING, + 1 => bad.bindings[0].1.ty = TypeInterner::STRING, + 2 => bad.bindings[0].1.definition = DefId::new(u32::MAX), + 3 => bad.bindings[0].1.declaration_span = Span::new(FileId::new(0), 0, 0), + 4 => { + bad.bindings[0].1.mode = CheckedBindingMode::View { + source: CheckedViewSource::Other, + } + } + 5 => bad.bindings[0].1.mutable = true, + 6 => bad.control = Control::For(&copied), + 7 => bad.key = foreign.attempt_key(&foreign.body).unwrap(), + _ => unreachable!(), + } + assert!( + checked.revalidate_absent_bindings(&bad).is_err(), + "corruption {corruption}" + ); + current_main(&checked, &program); + } + checked.revalidate_absent_bindings(&proof).unwrap(); + } +} + +#[test] +fn absent_generated_match_proof_rejects_corrupt_arm_name_and_binding_order() { + for release in [false, true] { + let program = checked_program(9, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + install(&mut checked, &program, "main"); + let statement = match_statement(&program); + for corruption in 0..4 { + let mut bad = checked.prepare_absent_match(statement, 0).unwrap().unwrap(); + match corruption { + 0 => bad.control = Control::Match(statement, 0, "occupied".into()), + 1 => bad.control = Control::Match(statement, 1, "vacant".into()), + 2 => bad.bindings.swap(0, 1), + 3 => bad.payload_count = Some(usize::MAX), + _ => unreachable!(), + } + assert!( + checked.revalidate_absent_bindings(&bad).is_err(), + "corruption {corruption}" + ); + current_main(&checked, &program); + } + } +} + +#[test] +fn absent_generated_view_loops_keep_copyable_keys_owned_and_exact_occurrences_distinct() { + let input = inputs::Input { + name: "source17_view_continue_break_reuse", + source: include_str!("../../../runtime/absent_generated/view_control.jett"), + reference: inputs::ReferenceExpectation::Nothing, + }; + for release in [false, true] { + let program = inputs::prepare(&input, release, &mut Vec::new()).unwrap().0; + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + for name in ["source17_absent_view_list", "source17_absent_view_map"] { + install(&mut checked, &program, name); + let loops = function(&program, name) + .body + .stmts + .iter() + .filter_map(|statement| match statement { + Stmt::For(statement) => Some(statement), + _ => None, + }) + .collect::>(); + assert_eq!(loops.len(), 2); + assert!(loops.iter().all(|statement| statement.view)); + let proof = checked.prepare_absent_for(loops[0]).unwrap().unwrap(); + let second = checked.prepare_absent_for(loops[1]).unwrap().unwrap(); + assert!(proof.validate_binding(&loops[1].variable, 0).is_err()); + let first_mode = proof.validate_binding(&loops[0].variable, 0).unwrap().mode; + if name == "source17_absent_view_map" { + assert_eq!(first_mode, CheckedBindingMode::Owned); + let value = loops[0].value_variable.as_ref().unwrap(); + assert_eq!( + proof.validate_binding(value, 1).unwrap().mode, + CheckedBindingMode::View { + source: CheckedViewSource::Other + } + ); + assert!( + proof + .validate_binding(loops[1].value_variable.as_ref().unwrap(), 1) + .is_err() + ); + } else { + assert_eq!( + first_mode, + CheckedBindingMode::View { + source: CheckedViewSource::Other + } + ); + } + assert_eq!(proof.parent_type(), second.parent_type()); + let mut replaced = checked.prepare_absent_for(loops[0]).unwrap().unwrap(); + replaced.control = Control::For(loops[1]); + assert!(checked.revalidate_absent_bindings(&replaced).is_err()); + checked.revalidate_absent_bindings(&proof).unwrap(); + checked.revalidate_absent_bindings(&second).unwrap(); + assert!(checked.body.scopes.is_empty()); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/intrinsic.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/intrinsic.rs new file mode 100644 index 00000000..d7ccd612 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/intrinsic.rs @@ -0,0 +1,280 @@ +//! Concrete intrinsic operands from one original call and accepted body frame. +use super::*; +use jett_parser::ast::TypeExpr; + +pub(crate) struct PreparedIntrinsicArguments { + program: Arc, + key: CheckedAttemptKey, + packet: jett_typecheck::CheckedCallOwnership, + span: Span, + intrinsic: IntrinsicId, + types: Vec, + reflections: Vec, +} + +impl PreparedIntrinsicArguments { + pub(crate) fn intrinsic(&self) -> IntrinsicId { + self.intrinsic + } + pub(crate) fn types(&self) -> &[TypeId] { + &self.types + } + pub(crate) fn reflections(&self) -> &[ReflectionTypeInfo] { + &self.reflections + } + pub(crate) fn validate( + &self, + checked: &CheckedExecution, + invocation: &CheckedInvocation, + ) -> Result<(), ResourceExecutionError> { + if !Arc::ptr_eq(&self.program, checked.program()) { + return Err(ResourceExecutionError::ForeignProgram); + } + if self.key != checked.attempt_key(&invocation.body)? + || self.packet != invocation.packet + || self.span != invocation.span + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let facts = checked.facts(&invocation.body)?; + let (intrinsic, types, reflections) = intrinsic_records( + &facts, + invocation, + self.types.len(), + &checked.program.checked().interner, + )?; + if intrinsic != self.intrinsic || types != self.types || reflections != self.reflections { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(()) + } +} + +impl CheckedExecution { + pub(crate) fn prepare_intrinsic_arguments( + &self, + callee: &Expr, + source_types: &[TypeExpr], + source_arguments: &[CallArg], + invocation: &CheckedInvocation, + ) -> Result { + self.intrinsic_current_body(invocation)?; + let reference = CheckedBodyReference { + cursor: invocation.body.clone(), + }; + let mut matches = 0usize; + walk_region( + reference.region()?, + &mut |_| {}, + &mut |expression| { + let (original_callee, types, arguments, span) = match expression { + Expr::Call(callee, arguments, span) => { + (callee.as_ref(), &[][..], arguments.as_slice(), *span) + } + Expr::GenericCall(callee, types, arguments, span) => ( + callee.as_ref(), + types.as_slice(), + arguments.as_slice(), + *span, + ), + _ => return, + }; + if span == invocation.span + && std::ptr::eq(original_callee, callee) + && same_slice(arguments, source_arguments) + && same_slice(types, source_types) + { + matches += 1; + } + }, + &mut |_| {}, + ); + if matches != 1 { + return Err(ResourceExecutionError::MissingCheckedInvocation); + } + self.prepared_intrinsic(invocation, source_types.len()) + } + + pub(crate) fn prepare_pipeline_intrinsic_arguments( + &self, + prepared: &PreparedPipelineStep<'_>, + ) -> Result { + self.intrinsic_current_body(prepared.invocation())?; + let reference = CheckedBodyReference { + cursor: prepared.invocation().body.clone(), + }; + let mut found = None; + let mut count = 0usize; + walk_region( + reference.region()?, + &mut |_| {}, + &mut |expression| { + let Expr::Pipeline(_, steps, _) = expression else { + return; + }; + for (index, step) in steps.iter().enumerate() { + if std::ptr::eq(step, prepared.original()) { + found = Some((expression, index)); + count += 1; + } + } + }, + &mut |_| {}, + ); + if count != 1 { + return Err(ResourceExecutionError::MissingCheckedInvocation); + } + let (pipeline, index) = found.ok_or(ResourceExecutionError::MissingCheckedInvocation)?; + let original = self.prepare_pipeline_step(pipeline, index)?; + if original.invocation().packet != prepared.invocation().packet + || !std::ptr::eq(original.callee(), prepared.callee()) + || !same_slice(original.type_arguments(), prepared.type_arguments()) + || !same_slice(original.extra_arguments(), prepared.extra_arguments()) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + self.prepared_intrinsic(prepared.invocation(), prepared.type_arguments().len()) + } + + fn intrinsic_current_body( + &self, + invocation: &CheckedInvocation, + ) -> Result<(), ResourceExecutionError> { + if self.attempt_key(&self.body)? != self.attempt_key(&invocation.body)? { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(()) + } + + fn prepared_intrinsic( + &self, + invocation: &CheckedInvocation, + arity: usize, + ) -> Result { + let facts = self.facts(&invocation.body)?; + let (intrinsic, types, reflections) = + intrinsic_records(&facts, invocation, arity, &self.program.checked().interner)?; + Ok(PreparedIntrinsicArguments { + program: self.program.clone(), + key: self.attempt_key(&invocation.body)?, + packet: invocation.packet.clone(), + span: invocation.span, + intrinsic, + types: types.to_vec(), + reflections: reflections.to_vec(), + }) + } +} + +fn same_slice(left: &[T], right: &[T]) -> bool { + left.len() == right.len() && (left.is_empty() || std::ptr::eq(left.as_ptr(), right.as_ptr())) +} + +fn intrinsic_records<'a>( + facts: &BodyFacts<'a>, + invocation: &CheckedInvocation, + arity: usize, + types: &jett_types::TypeInterner, +) -> Result<(IntrinsicId, &'a [TypeId], &'a [ReflectionTypeInfo]), ResourceExecutionError> { + if facts.calls.get(&invocation.span) != Some(&invocation.packet) { + return Err(ResourceExecutionError::InvalidInvocation); + } + let CheckedInvocationTarget::Intrinsic(intrinsic) = invocation.target() else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + let CheckedInvocationShape::Intrinsic { + intrinsic: shape, + result_type, + operands, + } = &invocation.packet.shape + else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if shape != intrinsic + || facts.intrinsics.get(&invocation.span) != Some(intrinsic) + || operands.len() != invocation.arguments().len() + || result_type.index() as usize >= types.len() + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let concrete = facts + .intrinsic_types + .get(&invocation.span) + .map(Vec::as_slice) + .unwrap_or(&[]); + if concrete.len() != arity + || (arity != 0 && !facts.intrinsic_types.contains_key(&invocation.span)) + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + for &ty in concrete { + concrete_type(types, ty)?; + } + let reflected = facts + .intrinsic_reflections + .get(&invocation.span) + .map(Vec::as_slice) + .unwrap_or(&[]); + if (metadata_leaf(*intrinsic) && (arity != 1 || reflected.len() != 1)) + || (!reflected.is_empty() && reflected.len() != arity) + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok((*intrinsic, concrete, reflected)) +} + +fn metadata_leaf(intrinsic: IntrinsicId) -> bool { + matches!( + intrinsic, + IntrinsicId::TypeName + | IntrinsicId::TypeKind + | IntrinsicId::TypeKindTag + | IntrinsicId::TypePrimitiveTag + | IntrinsicId::TypeHasSecret + | IntrinsicId::TypeInfo + | IntrinsicId::TypeArg + ) +} + +fn concrete_type( + types: &jett_types::TypeInterner, + ty: TypeId, +) -> Result<(), ResourceExecutionError> { + let mut pending = vec![ty]; + let mut seen = std::collections::HashSet::new(); + while let Some(ty) = pending.pop() { + if ty.index() as usize >= types.len() { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + if !seen.insert(ty) { + continue; + } + pending.extend_from_slice(types.nominal_type_arguments(ty)); + match types.resolve(ty) { + Type::Error => return Err(ResourceExecutionError::MissingCheckedBody), + Type::List(inner) + | Type::Set(inner) + | Type::Optional(inner) + | Type::Secret(inner) + | Type::Refinement { base: inner, .. } => pending.push(*inner), + Type::Map(key, value) | Type::Result(key, value) => { + pending.push(*key); + pending.push(*value); + } + Type::Function { + params, + return_type, + .. + } => { + pending.extend_from_slice(params); + pending.push(*return_type); + } + _ => {} + } + } + Ok(()) +} + +#[cfg(test)] +#[path = "intrinsic/tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/intrinsic/tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/intrinsic/tests.rs new file mode 100644 index 00000000..0de3844b --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/intrinsic/tests.rs @@ -0,0 +1,325 @@ +use super::*; +use crate::resource_execution::tests::program as checked_program; +use crate::{Interpreter, Value}; +use jett_types::{ReflectionMetadata, TypeInterner}; + +const SOURCE: &str = r#"namespace app +type Label = int64 +function label[T]() returns string: + return type.name[T]() +export function integer_label() returns string: + return label[int64]() +export function string_label() returns string: + return label[string]() +export function alias_label() returns string: + return label[Label]() +export function alias_kind() returns string: + return type.kind[Label]() +export function alias_info() returns string: + return type.info[Label]().type_name +export function scoped_label() returns string: + comptime type Outer = type.info[list[int64]](): + comptime type Element = type.arg[Outer](0): + return type.name[Element]() + return "unreached" +export function pipeline_label() returns string: + return (0 into type.arg[list[int64]]()).type_name +export function secret_flag() returns bool: + return type.has_secret[secret[int64]]() +export function list_label() returns string: + return type.name[list[int64]]() +"#; + +fn function<'a>(program: &'a Arc, name: &str) -> &'a FunctionDef { + program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.name == name + && function.name.span.file == jett_common::FileId::new(0) => + { + Some(function) + } + _ => None, + }) + .expect("one retained fixture function") +} + +fn entry(checked: &mut CheckedExecution, program: &Arc, name: &str) { + let function = function(program, name); + let definition = checked.declaration_definition(function.name.span).unwrap(); + let signature = program.checked().definition_types[&definition]; + let reference = checked + .prepare_function_body(&FunctionInvocation::Entry { + definition, + signature, + }) + .unwrap(); + checked.install_function_body(&reference).unwrap(); +} + +fn first_call(function: &FunctionDef) -> &Expr { + let mut found = None; + walk_block(&function.body, &mut |_| {}, &mut |expression| { + if found.is_none() && matches!(expression, Expr::Call(..) | Expr::GenericCall(..)) { + found = Some(expression); + } + }); + found.expect("actual original call") +} + +fn parts(call: &Expr) -> (&Expr, &[TypeExpr], &[CallArg], Span) { + match call { + Expr::Call(callee, arguments, span) => (callee.as_ref(), &[], arguments, *span), + Expr::GenericCall(callee, types, arguments, span) => { + (callee.as_ref(), types, arguments, *span) + } + _ => panic!("original source call"), + } +} + +fn generic_invocation( + checked: &mut CheckedExecution, + program: &Arc, + wrapper: &str, +) -> CheckedInvocation { + entry(checked, program, wrapper); + let (_, _, _, span) = parts(first_call(function(program, wrapper))); + let caller = checked.invocation(span).unwrap(); + let body = checked + .prepare_function_body(&FunctionInvocation::Source(&caller)) + .unwrap(); + checked.install_function_body(&body).unwrap(); + checked + .invocation(first_call(function(program, "label")).span()) + .unwrap() +} + +#[test] +fn resource_intrinsic_source_calls_preserve_concrete_alias_and_nested_metadata_without_ambient_maps() + { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + // Empty legacy reflection metadata cannot stand in for the exact body records. + interpreter.set_reflection_metadata(Arc::new(ReflectionMetadata::new())); + for (name, expected) in [ + ("integer_label", Value::String("int64".to_string())), + ("string_label", Value::String("string".to_string())), + ("alias_label", Value::String("app.Label".to_string())), + ("alias_kind", Value::String("alias".to_string())), + ("alias_info", Value::String("app.Label".to_string())), + ("scoped_label", Value::String("int64".to_string())), + ("pipeline_label", Value::String("int64".to_string())), + ("secret_flag", Value::Bool(true)), + ("list_label", Value::String("list[int64]".to_string())), + ("integer_label", Value::String("int64".to_string())), + ] { + let definition = checked + .declaration_definition(function(&program, name).name.span) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(definition, Vec::new()) + .unwrap(), + expected, + "{name} release={release}" + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); + } + } +} + +#[test] +fn resource_intrinsic_packet_rejects_cloned_original_foreign_and_other_concrete_contexts() { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let integer = generic_invocation(&mut checked, &program, "integer_label"); + let (callee, types, arguments, _) = parts(first_call(function(&program, "label"))); + let prepared = checked + .prepare_intrinsic_arguments(callee, types, arguments, &integer) + .unwrap(); + assert_eq!(prepared.types(), &[TypeInterner::INT64]); + assert_eq!(prepared.reflections()[0].type_name, "int64"); + prepared.validate(&checked, &integer).unwrap(); + assert!( + checked + .prepare_intrinsic_arguments(callee, &types.to_vec(), arguments, &integer) + .is_err() + ); + let clone = first_call(function(&program, "label")).clone(); + let (callee_clone, types_clone, arguments_clone, _) = parts(&clone); + assert!( + checked + .prepare_intrinsic_arguments(callee_clone, types_clone, arguments_clone, &integer) + .is_err() + ); + let text = generic_invocation(&mut checked, &program, "string_label"); + assert_eq!(integer.span(), text.span()); + assert_eq!(integer.packet(), text.packet()); // Only the saved concrete body distinguishes this call. + assert!( + checked + .prepare_intrinsic_arguments(callee, types, arguments, &integer) + .is_err() + ); + assert!(prepared.validate(&checked, &text).is_err()); + let foreign_program = checked_program(SOURCE, release); + let mut foreign = + CheckedExecution::new(foreign_program.clone(), ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let foreign_call = generic_invocation(&mut foreign, &foreign_program, "integer_label"); + assert_eq!( + prepared.validate(&foreign, &foreign_call), + Err(ResourceExecutionError::ForeignProgram) + ); + assert!( + foreign + .prepare_intrinsic_arguments(callee, types, arguments, &integer) + .is_err() + ); + } +} + +#[test] +fn resource_intrinsic_packet_rejects_missing_records_counts_ids_shapes_and_unresolved_types() { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let invocation = generic_invocation(&mut checked, &program, "integer_label"); + for mutation in 0..7 { + let original = checked.facts(&invocation.body).unwrap(); + let mut concrete = original.intrinsic_types.clone(); + let mut reflections = original.intrinsic_reflections.clone(); + let mut ids = original.intrinsics.clone(); + let mut calls = original.calls.clone(); + let mut copied = CheckedInvocation { + packet: invocation.packet.clone(), + body: invocation.body.clone(), + span: invocation.span, + }; + match mutation { + 0 => { + concrete.remove(&invocation.span); + } + 1 => { + reflections.remove(&invocation.span); + } + 2 => { + ids.insert(invocation.span, IntrinsicId::TypeKind); + } + 3 => { + concrete.insert( + invocation.span, + vec![TypeInterner::INT64, TypeInterner::STRING], + ); + } + 4 => { + let extra = reflections[&invocation.span][0].clone(); + reflections.get_mut(&invocation.span).unwrap().push(extra); + } + 5 => { + concrete.insert(invocation.span, vec![TypeInterner::ERROR]); + } + _ => { + let CheckedInvocationShape::Intrinsic { intrinsic, .. } = + &mut copied.packet.shape + else { + unreachable!() + }; + *intrinsic = IntrinsicId::TypeKind; + calls.insert(invocation.span, copied.packet.clone()); + } + } + let facts = BodyFacts { + calls: &calls, + bindings: original.bindings, + types: original.types, + source_types: original.source_types, + pipeline_inputs: original.pipeline_inputs, + pipeline_calls: original.pipeline_calls, + scopes: original.scopes, + intrinsics: &ids, + intrinsic_types: &concrete, + intrinsic_reflections: &reflections, + constructions: original.constructions, + selections: original.selections, + }; + assert!( + intrinsic_records(&facts, &copied, 1, &program.checked().interner).is_err(), + "mutation {mutation}" + ); + } + let (callee, types, arguments, _) = parts(first_call(function(&program, "label"))); + for mutation in 0..3 { + let mut prepared = checked + .prepare_intrinsic_arguments(callee, types, arguments, &invocation) + .unwrap(); + match mutation { + 0 => prepared.types[0] = TypeInterner::STRING, + 1 => prepared.reflections[0].type_name = "forged".to_string(), + _ => prepared.intrinsic = IntrinsicId::TypeKind, + } + assert!(prepared.validate(&checked, &invocation).is_err()); + } + entry(&mut checked, &program, "list_label"); + let (callee, types, arguments, span) = parts(first_call(function(&program, "list_label"))); + let invocation = checked.invocation(span).unwrap(); + let prepared = checked + .prepare_intrinsic_arguments(callee, types, arguments, &invocation) + .unwrap(); + let empty = TypeInterner::new(); + assert!(prepared.types()[0].index() as usize >= empty.len()); + assert_eq!( + concrete_type(&empty, prepared.types()[0]), + Err(ResourceExecutionError::InvalidCheckedProgram) + ); + } +} + +#[test] +fn resource_intrinsic_pipeline_packet_uses_original_step_and_cannot_accept_a_cloned_step() { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + entry(&mut checked, &program, "pipeline_label"); + let mut pipeline = None; + walk_block( + &function(&program, "pipeline_label").body, + &mut |_| {}, + &mut |expression| { + if matches!(expression, Expr::Pipeline(..)) { + pipeline = Some(expression); + } + }, + ); + let pipeline = pipeline.unwrap(); + let step = checked.prepare_pipeline_step(pipeline, 0).unwrap(); + let prepared = checked.prepare_pipeline_intrinsic_arguments(&step).unwrap(); + assert_eq!(prepared.intrinsic(), IntrinsicId::TypeArg); + assert_eq!(prepared.reflections()[0].args[0].type_name, "int64"); + prepared.validate(&checked, step.invocation()).unwrap(); + let clone = pipeline.clone(); + assert!(checked.prepare_pipeline_step(&clone, 0).is_err()); + let foreign_program = checked_program(SOURCE, release); + let mut foreign = + CheckedExecution::new(foreign_program.clone(), ExecutionPurpose::ReferenceRuntime) + .unwrap(); + entry(&mut foreign, &foreign_program, "pipeline_label"); + assert!(foreign.prepare_pipeline_intrinsic_arguments(&step).is_err()); + entry(&mut checked, &program, "integer_label"); + assert!(checked.prepare_pipeline_intrinsic_arguments(&step).is_err()); + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/named_callable.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/named_callable.rs new file mode 100644 index 00000000..c05410e6 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/named_callable.rs @@ -0,0 +1,218 @@ +//! Exact named Source descriptors contain no physical Resource or capture environment. +//! Their function signature remains Resource-bearing; only original occurrence +//! provenance permits ordinary storage and the dedicated checked indirect bridge. + +use super::*; + +pub(crate) struct PreparedNamedCallable { + key: CheckedAttemptKey, + expression: usize, + definition: DefId, + signature: TypeId, + aliases: Vec<(Span, DefId)>, +} + +impl PreparedNamedCallable { + pub(crate) fn definition(&self) -> DefId { + self.definition + } +} + +impl CheckedExecution { + pub(crate) fn prepare_named_callable( + &self, + expression: &Expr, + ) -> Result, ResourceExecutionError> { + self.validate_executable_cursor(&self.body)?; + // Scoped/generic callable adaptation needs its own complete body proof. + if !matches!(self.body.root, BodyRoot::Ordinary) || !self.body.scopes.is_empty() { + return Ok(None); + } + let reference = CheckedBodyReference { + cursor: self.body.clone(), + }; + let region = reference.region()?; + let index = expression_index(region, expression)?; + let mut aliases = Vec::new(); + let Some((definition, signature)) = + self.named_callable_in(region, expression, &mut aliases)? + else { + return Ok(None); + }; + Ok(Some(PreparedNamedCallable { + key: self.attempt_key(&self.body)?, + expression: index, + definition, + signature, + aliases, + })) + } + + fn named_callable_in( + &self, + region: OriginalRegion<'_>, + expression: &Expr, + aliases: &mut Vec<(Span, DefId)>, + ) -> Result, ResourceExecutionError> { + if let Expr::Paren(inner, _) = expression { + return self.named_callable_in(region, inner, aliases); + } + let (definition, identifier, member) = match expression { + Expr::Ident(identifier) => ( + self.resolved_definition(identifier.span)?, + Some(identifier), + None, + ), + Expr::FieldAccess(_, member, span) => { + // Namespace resolution authenticates the complete producer, + // not its base/member spelling or a runtime dotted name. + if self.program.checked().method_values.contains_key(span) { + return Ok(None); + } + let Some(definition) = self.program.resolved().resolutions.get(span).copied() + else { + return Ok(None); + }; + (definition, None, Some(member)) + } + _ => return Ok(None), + }; + let info = self + .program + .resolved() + .scope_table + .definitions + .get(definition.index() as usize) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if info.id != definition { + return Ok(None); + } + match info.kind { + jett_resolve::DefKind::Function if !self.has_hook(definition) => { + let function = self.retained_function(definition)?; + if !function.type_params.is_empty() + || member.is_some_and(|member| { + info.namespace.is_none() || member.name != function.name.name + }) + { + return Ok(None); + } + let signature = self.expression_type(expression.span())?; + if !matches!( + self.program.checked().interner.resolve(signature), + Type::Function { .. } + ) || self.program.checked().definition_types.get(&definition) != Some(&signature) + { + return Ok(None); + } + Ok(Some((definition, signature))) + } + jett_resolve::DefKind::Variable => { + let Some(identifier) = identifier else { + return Ok(None); + }; + let signature = self.expression_type(expression.span())?; + if !matches!( + self.program.checked().interner.resolve(signature), + Type::Function { .. } + ) { + return Ok(None); + } + if aliases.iter().any(|(_, seen)| *seen == definition) { + return Err(ResourceExecutionError::InvalidInvocation); + } + let mut declarations = Vec::new(); + walk_region(region, &mut |_| {}, &mut |_| {}, &mut |statement| { + if let Stmt::VarDecl(declaration) = statement { + if self + .facts(&self.body) + .ok() + .and_then(|facts| facts.bindings.get(&declaration.name.span)) + .is_some_and(|fact| fact.definition == definition) + { + declarations.push(declaration); + } + } + }); + let [declaration] = declarations.as_slice() else { + return Ok(None); + }; + let fact = self.binding(declaration.name.span)?; + if declaration.mutable + || fact.mutable + || fact.ty != signature + || declaration.span.file != identifier.span.file + || declaration.span.end > identifier.span.start + { + return Ok(None); + } + aliases.push((declaration.name.span, definition)); + let target = self.named_callable_in(region, &declaration.value, aliases)?; + Ok(target.filter(|(_, target_signature)| *target_signature == signature)) + } + _ => Ok(None), + } + } + + pub(crate) fn prepare_named_indirect( + &self, + source: &CheckedInvocation, + callee: &Expr, + ) -> Result { + let target = self + .prepare_named_callable(callee)? + .ok_or(ResourceExecutionError::InvalidInvocation)?; + self.validate_named_indirect(source, &target)?; + Ok(target) + } + + pub(super) fn validate_named_indirect( + &self, + source: &CheckedInvocation, + target: &PreparedNamedCallable, + ) -> Result<(), ResourceExecutionError> { + self.validate_executable_cursor(&source.body)?; + if target.key != self.attempt_key(&source.body)? + || self.facts(&source.body)?.calls.get(&source.span) != Some(&source.packet) + || !matches!(source.target(), CheckedInvocationTarget::Indirect(signature) if *signature == target.signature) + || !matches!(&source.packet.shape, CheckedInvocationShape::Function { signature_type } if *signature_type == target.signature) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let reference = CheckedBodyReference { + cursor: source.body.clone(), + }; + let region = reference.region()?; + let expression = region_expression(region, target.expression)?; + let mut calls = 0; + walk_region( + region, + &mut |_| {}, + &mut |original| { + if let Expr::Call(callee, _, span) = original { + let mut bare = callee.as_ref(); + while let Expr::Paren(inner, _) = bare { + bare = inner; + } + if *span == source.span && std::ptr::eq(bare, expression) { + calls += 1; + } + } + }, + &mut |_| {}, + ); + let mut aliases = Vec::new(); + if calls != 1 + || self.named_callable_in(region, expression, &mut aliases)? + != Some((target.definition, target.signature)) + || aliases != target.aliases + { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(()) + } +} + +#[cfg(test)] +#[path = "named_callable_tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/named_callable_tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/named_callable_tests.rs new file mode 100644 index 00000000..56a45241 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/named_callable_tests.rs @@ -0,0 +1,516 @@ +use super::*; +use crate::resource_execution::{ProviderEvent, ScriptOperation}; +use crate::{Interpreter, Value}; +use jett_common::FileId; + +const SOURCE: &str = "namespace app +function close_selected(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +function wrong_target(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.terminal_failure() + return nothing +export function scenario(view net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = close_selected + function(resource_probe.TestHandle) returns nothing alias = dispose + resource_probe.TestHandle token = resource_probe.create(view net, 701) handle error: + return nothing + alias(token) + return nothing +"; + +fn execution(release: bool) -> (CheckedExecution, CheckedBodyReference, DefId) { + execution_of(SOURCE, "scenario", release) +} + +fn execution_of( + source: &str, + name: &str, + release: bool, +) -> (CheckedExecution, CheckedBodyReference, DefId) { + let program = crate::resource_execution::tests::program(source, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let scenario = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) && function.name.name == name => + { + Some(function) + } + _ => None, + }) + .unwrap(); + let entry = checked.declaration_definition(scenario.name.span).unwrap(); + let body = checked + .prepare_function_body(&checked.entry(entry).unwrap()) + .unwrap(); + checked.install_function_body(&body).unwrap(); + (checked, body, entry) +} + +fn indirect(body: &CheckedBodyReference) -> (&Expr, Span) { + let mut found = Vec::new(); + walk_block( + &body.function().unwrap().body, + &mut |_| {}, + &mut |expression| { + if let Expr::Call(callee, _, span) = expression { + if matches!(callee.as_ref(), Expr::Ident(identifier) if identifier.name == "alias") + { + found.push((callee.as_ref(), *span)); + } + } + }, + ); + let [call] = found.as_slice() else { + panic!("exact original indirect call"); + }; + *call +} + +#[test] +fn checked_named_indirect_keeps_original_packet_and_exact_same_signature_body() { + for release in [false, true] { + let (checked, body, _) = execution(release); + let (callee, span) = indirect(&body); + let source = checked.invocation(span).unwrap(); + let target = checked.prepare_named_indirect(&source, callee).unwrap(); + assert!( + matches!(source.target(), CheckedInvocationTarget::Indirect(signature) if *signature == target.signature) + ); + assert_eq!(target.aliases.len(), 2); + let selected = checked + .prepare_function_body(&FunctionInvocation::NamedSource { + source: &source, + target: &target, + }) + .unwrap(); + assert_eq!(selected.function().unwrap().name.name, "close_selected"); + let wrong = checked + .program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "wrong_target" => Some(function), + _ => None, + }) + .unwrap(); + let wrong_definition = checked.declaration_definition(wrong.name.span).unwrap(); + assert_eq!( + checked + .program + .checked() + .definition_types + .get(&wrong_definition), + Some(&target.signature) + ); + assert_ne!(target.definition(), wrong_definition); + } +} + +#[test] +fn checked_named_indirect_refuses_cloned_foreign_and_same_signature_substituted_authority() { + for release in [false, true] { + let (checked, body, _) = execution(release); + let (callee, span) = indirect(&body); + let source = checked.invocation(span).unwrap(); + assert!(matches!( + checked.prepare_named_indirect(&source, &callee.clone()), + Err(ResourceExecutionError::MissingCheckedBody) + )); + let mut target = checked.prepare_named_indirect(&source, callee).unwrap(); + let wrong = checked + .program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "wrong_target" => Some(function), + _ => None, + }) + .unwrap(); + target.definition = checked.declaration_definition(wrong.name.span).unwrap(); + assert!(matches!( + checked.prepare_function_body(&FunctionInvocation::NamedSource { + source: &source, + target: &target + }), + Err(ResourceExecutionError::InvalidInvocation) + )); + let (foreign, foreign_body, _) = execution(release); + let (foreign_callee, foreign_span) = indirect(&foreign_body); + let foreign_source = foreign.invocation(foreign_span).unwrap(); + let foreign_target = foreign + .prepare_named_indirect(&foreign_source, foreign_callee) + .unwrap(); + assert!(matches!( + checked.prepare_function_body(&FunctionInvocation::NamedSource { + source: &source, + target: &foreign_target + }), + Err(ResourceExecutionError::InvalidInvocation) + )); + let mut target = checked.prepare_named_indirect(&source, callee).unwrap(); + target.aliases.pop(); + assert!(matches!( + checked.prepare_function_body(&FunctionInvocation::NamedSource { + source: &source, + target: &target + }), + Err(ResourceExecutionError::InvalidInvocation) + )); + let target = checked.prepare_named_indirect(&source, callee).unwrap(); + let mut corrupt = checked.invocation(span).unwrap(); + corrupt.packet.arguments[0].parameter_type = jett_types::TypeInterner::INT64; + assert!(matches!( + checked.prepare_function_body(&FunctionInvocation::NamedSource { + source: &corrupt, + target: &target + }), + Err(ResourceExecutionError::InvalidInvocation) + )); + } +} + +#[test] +fn checked_source_named_indirect_immutable_alias_closes_once() { + for release in [false, true] { + let (checked, _, entry) = execution(release); + let mut interpreter = Interpreter::from_checked_resource_program( + checked.program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ScriptOperation::Construct { + label: 701, + outcome: Ok(()), + }]) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(entry, vec![grant]) + .unwrap(), + Value::Nothing + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(701), + ProviderEvent::Finalized(701) + ], + 0, + 0 + ) + ); + } +} + +const NAMESPACE_SOURCE: &str = "namespace controls +export function wrong_target(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.terminal_failure() + return nothing +export function generic_close[T](token: resource_probe.TestHandle, unused: T) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +namespace app +struct CallbackHolder: + callback: function(resource_probe.TestHandle) returns nothing +struct Observer: + value: int64 + function close(view self: Observer, token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +function field_control(view holder: CallbackHolder) returns nothing: + function(resource_probe.TestHandle) returns nothing candidate = clone holder.callback + return nothing +function method_control() returns nothing: + function(view Observer, resource_probe.TestHandle) returns nothing candidate = Observer.close + return nothing +function generic_control(token: resource_probe.TestHandle) returns nothing: + use controls + controls.generic_close[int64](token, 7) + return nothing +export function scenario(view net: Network) returns nothing: + use resource_probe as probe + function(resource_probe.TestHandle) returns nothing dispose = probe.close + function(resource_probe.TestHandle) returns nothing alias = dispose + resource_probe.TestHandle token = probe.create(view net, 711) handle error: + return nothing + alias(token) + return nothing +"; + +fn initializer<'a>(body: &'a CheckedBodyReference, name: &str) -> &'a Expr { + let declarations = body + .function() + .unwrap() + .body + .stmts + .iter() + .filter_map(|statement| match statement { + Stmt::VarDecl(declaration) if declaration.name.name == name => Some(&declaration.value), + _ => None, + }) + .collect::>(); + let [expression] = declarations.as_slice() else { + panic!("exact original initializer `{name}`"); + }; + expression +} + +#[test] +fn checked_namespace_callable_joins_full_producer_span_and_retained_body() { + for release in [false, true] { + let (checked, body, _) = execution_of(NAMESPACE_SOURCE, "scenario", release); + let producer = initializer(&body, "dispose"); + let Expr::FieldAccess(base, member, span) = producer else { + panic!("original namespace producer"); + }; + let target = checked.prepare_named_callable(producer).unwrap().unwrap(); + assert_eq!(checked.resolved_definition(*span), Ok(target.definition)); + assert!( + !checked + .program + .resolved() + .resolutions + .contains_key(&base.span()) + ); + assert!( + !checked + .program + .resolved() + .resolutions + .contains_key(&member.span) + ); + assert_eq!(member.name, "close"); + let (callee, span) = indirect(&body); + let source = checked.invocation(span).unwrap(); + let target = checked.prepare_named_indirect(&source, callee).unwrap(); + assert_eq!(target.aliases.len(), 2); + assert!( + matches!(source.target(), CheckedInvocationTarget::Indirect(signature) if *signature == target.signature) + ); + let selected = checked + .prepare_function_body(&FunctionInvocation::NamedSource { + source: &source, + target: &target, + }) + .unwrap(); + assert_eq!(selected.function().unwrap().name.name, "close"); + assert_eq!( + selected.function().unwrap().name.span.file, + FileId::new(10_000) + ); + let wrong = checked + .program + .resolved() + .scope_table + .definitions + .iter() + .find(|definition| definition.name == "controls.wrong_target") + .unwrap(); + assert_eq!( + checked.program.checked().definition_types.get(&wrong.id), + Some(&target.signature) + ); + assert_ne!(wrong.id, target.definition); + } +} + +#[test] +fn checked_namespace_callable_refuses_cloned_foreign_target_and_signature_substitution() { + for release in [false, true] { + let (checked, body, _) = execution_of(NAMESPACE_SOURCE, "scenario", release); + let producer = initializer(&body, "dispose"); + assert!(matches!( + checked.prepare_named_callable(&producer.clone()), + Err(ResourceExecutionError::MissingCheckedBody) + )); + let (callee, span) = indirect(&body); + let source = checked.invocation(span).unwrap(); + let wrong = checked + .program + .resolved() + .scope_table + .definitions + .iter() + .find(|definition| definition.name == "controls.wrong_target") + .unwrap(); + let mut target = checked.prepare_named_indirect(&source, callee).unwrap(); + target.definition = wrong.id; + assert!(matches!( + checked.validate_named_indirect(&source, &target), + Err(ResourceExecutionError::InvalidInvocation) + )); + let mut target = checked.prepare_named_indirect(&source, callee).unwrap(); + target.signature = jett_types::TypeInterner::INT64; + assert!(matches!( + checked.validate_named_indirect(&source, &target), + Err(ResourceExecutionError::InvalidInvocation) + )); + let target = checked.prepare_named_callable(producer).unwrap().unwrap(); + assert!(matches!( + checked.validate_named_indirect(&source, &target), + Err(ResourceExecutionError::InvalidInvocation) + )); + let (foreign, foreign_body, _) = execution_of(NAMESPACE_SOURCE, "scenario", release); + let (foreign_callee, foreign_span) = indirect(&foreign_body); + let foreign_source = foreign.invocation(foreign_span).unwrap(); + let foreign_target = foreign + .prepare_named_indirect(&foreign_source, foreign_callee) + .unwrap(); + assert!(matches!( + checked.validate_named_indirect(&source, &foreign_target), + Err(ResourceExecutionError::InvalidInvocation) + )); + } +} + +#[test] +fn checked_namespace_callable_refuses_original_instance_method_generic_and_hook_producers() { + for release in [false, true] { + let (checked, body, _) = execution_of(NAMESPACE_SOURCE, "scenario", release); + let signature = checked + .expression_type(initializer(&body, "dispose").span()) + .unwrap(); + let (field_checked, field_body, _) = + execution_of(NAMESPACE_SOURCE, "field_control", release); + let Expr::Clone(field, _) = initializer(&field_body, "candidate") else { + panic!("original function field read"); + }; + assert!( + matches!(field.as_ref(), Expr::FieldAccess(_, member, _) if member.name == "callback") + ); + assert_eq!( + field_checked.expression_type(field.span()).unwrap(), + signature + ); + assert!( + !field_checked + .program + .resolved() + .resolutions + .contains_key(&field.span()) + ); + assert!( + field_checked + .prepare_named_callable(field) + .unwrap() + .is_none() + ); + let (method_checked, method_body, _) = + execution_of(NAMESPACE_SOURCE, "method_control", release); + let method = initializer(&method_body, "candidate"); + assert!( + method_checked + .program + .checked() + .method_values + .contains_key(&method.span()) + ); + assert!( + method_checked + .prepare_named_callable(method) + .unwrap() + .is_none() + ); + let (generic_checked, generic_body, _) = + execution_of(NAMESPACE_SOURCE, "generic_control", release); + let Stmt::Expr(statement) = &generic_body.function().unwrap().body.stmts[1] else { + panic!("original generic call"); + }; + let Expr::GenericCall(callee, _, _, _) = &statement.expr else { + panic!("original generic callee"); + }; + let definition = generic_checked.resolved_definition(callee.span()).unwrap(); + assert!( + !generic_checked + .retained_function(definition) + .unwrap() + .type_params + .is_empty() + ); + assert!( + generic_checked + .prepare_named_callable(callee) + .unwrap() + .is_none() + ); + let mut hook_checked = + CheckedExecution::new(checked.program.clone(), ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let hook_function = checked + .program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "close_descriptor" => { + Some(function) + } + _ => None, + }) + .unwrap(); + let entry = hook_checked + .declaration_definition(hook_function.name.span) + .unwrap(); + let hook_body = hook_checked + .prepare_function_body(&hook_checked.entry(entry).unwrap()) + .unwrap(); + hook_checked.install_function_body(&hook_body).unwrap(); + let Stmt::Return(statement) = &hook_body.function().unwrap().body.stmts[0] else { + panic!("original hook descriptor return"); + }; + let hook = statement.value.as_ref().unwrap(); + assert!(hook_checked.has_hook(hook_checked.resolved_definition(hook.span()).unwrap())); + assert!(hook_checked.prepare_named_callable(hook).unwrap().is_none()); + } +} + +#[test] +fn checked_source_namespace_callable_and_immutable_alias_close_once() { + for release in [false, true] { + let (checked, _, entry) = execution_of(NAMESPACE_SOURCE, "scenario", release); + let mut interpreter = Interpreter::from_checked_resource_program( + checked.program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ScriptOperation::Construct { + label: 711, + outcome: Ok(()), + }]) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(entry, vec![grant]) + .unwrap(), + Value::Nothing + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(711), + ProviderEvent::Finalized(711) + ], + 0, + 0 + ) + ); + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/owned_struct.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/owned_struct.rs new file mode 100644 index 00000000..82147dba --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/owned_struct.rs @@ -0,0 +1,185 @@ +//! Constructor identity only; runtime tickets are supplied by reached actuals. +use super::*; + +pub(crate) struct PreparedOwnedStruct { + key: CheckedAttemptKey, + expression: usize, + definition: DefId, + ty: TypeId, + name: String, + fields: Vec<(String, TypeId)>, + source_fields: Vec, +} + +impl PreparedOwnedStruct { + pub(crate) fn ty(&self) -> TypeId { + self.ty + } + + pub(crate) fn name(&self) -> &str { + &self.name + } + + pub(crate) fn fields(&self) -> &[(String, TypeId)] { + &self.fields + } + + pub(crate) fn source_fields(&self) -> &[usize] { + &self.source_fields + } +} + +impl CheckedExecution { + pub(crate) fn prepare_owned_struct( + &self, + expression: &Expr, + ) -> Result, ResourceExecutionError> { + self.validate_executable_cursor(&self.body)?; + let Expr::Call(callee, arguments, span) = expression else { + return Ok(None); + }; + let facts = self.facts(&self.body)?; + let Some(construction) = facts.constructions.get(span) else { + return Ok(None); + }; + let ty = construction.struct_type; + if !self.type_contains_resource(ty)? { + return Ok(None); + } + // Refinement evaluation and generic declaration association need their + // own connected transport. They remain Source requirements. + if construction.validates_refinements + || !matches!(self.body.root, BodyRoot::Ordinary) + || !self.body.scopes.is_empty() + { + return Ok(None); + } + let reference = CheckedBodyReference { + cursor: self.body.clone(), + }; + let index = expression_index(reference.region()?, expression)?; + if self.source_call_dispatch(callee, arguments, *span)? + || self.expression_type(*span)? != ty + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let definition = self.resolved_definition(callee.span())?; + let info = self + .program + .resolved() + .scope_table + .definitions + .get(definition.index() as usize) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + let Type::Struct(owner) = self.program.checked().interner.resolve(ty) else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + let structure = self.program.checked().interner.resolve_struct(*owner); + if info.id != definition + || info.kind != jett_resolve::DefKind::Struct + || info.name != structure.name + || self.program.checked().definition_types.get(&definition) != Some(&ty) + || !self + .program + .checked() + .interner + .nominal_type_arguments(ty) + .is_empty() + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let declarations = self + .program + .module() + .items + .iter() + .filter_map(|item| match item { + Item::Struct(declaration) if declaration.name.span == info.span => { + Some(declaration) + } + _ => None, + }) + .collect::>(); + let [declaration] = declarations.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + if !declaration.type_params.is_empty() { + return Ok(None); + } + if declaration.fields.len() != structure.fields.len() + || declaration + .fields + .iter() + .zip(&structure.fields) + .any(|(original, field)| original.name.name != field.0) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + // This vertical has direct nominal leaves and scalar companions only. + // Do not accidentally grant a nested/capability/refined carrier path. + if structure.fields.iter().any(|(_, field)| { + !matches!( + self.program.checked().interner.resolve(*field), + Type::Resource(_) | Type::Int64 | Type::String | Type::Bool | Type::Nothing + ) + }) { + return Ok(None); + } + if arguments.len() != structure.fields.len() { + return Err(ResourceExecutionError::InvalidInvocation); + } + let mut assigned = vec![false; structure.fields.len()]; + let mut source_fields = Vec::with_capacity(arguments.len()); + for argument in arguments { + let ordinal = match &argument.name { + Some(name) => structure + .fields + .iter() + .position(|(field, _)| field == &name.name), + None => assigned.iter().position(|assigned| !assigned), + } + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if assigned[ordinal] + || self.expression_type(argument.value.span())? != structure.fields[ordinal].1 + { + return Err(ResourceExecutionError::InvalidInvocation); + } + assigned[ordinal] = true; + source_fields.push(ordinal); + } + Ok(Some(PreparedOwnedStruct { + key: self.attempt_key(&self.body)?, + expression: index, + definition, + ty, + name: structure.name.clone(), + fields: structure.fields.clone(), + source_fields, + })) + } + + pub(crate) fn revalidate_owned_struct( + &self, + proof: &PreparedOwnedStruct, + expression: &Expr, + ) -> Result<(), ResourceExecutionError> { + let current = self + .prepare_owned_struct(expression)? + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if current.key != proof.key + || current.expression != proof.expression + || current.definition != proof.definition + || current.ty != proof.ty + || current.name != proof.name + || current.fields != proof.fields + || current.source_fields != proof.source_fields + { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(()) + } +} + +#[cfg(test)] +#[path = "owned_struct_tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/owned_struct_tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/owned_struct_tests.rs new file mode 100644 index 00000000..5763e314 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/owned_struct_tests.rs @@ -0,0 +1,119 @@ +use super::*; +use jett_common::FileId; + +const SOURCE: &str = include_str!("../../fixtures/33_owned_struct_scope_drop_entry.jett"); + +fn execution(source: &str, name: &str) -> (CheckedExecution, CheckedBodyReference) { + let program = crate::resource_execution::tests::program(source, false); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let function = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) && function.name.name == name => + { + Some(function) + } + _ => None, + }) + .unwrap(); + let definition = checked.declaration_definition(function.name.span).unwrap(); + let body = checked + .prepare_function_body(&checked.entry(definition).unwrap()) + .unwrap(); + checked.install_function_body(&body).unwrap(); + (checked, body) +} + +fn constructor(body: &CheckedBodyReference) -> &Expr { + let mut found = Vec::new(); + walk_region( + body.region().unwrap(), + &mut |_| {}, + &mut |expression| { + if let Expr::Call(callee, _, _) = expression { + if matches!(callee.as_ref(), Expr::Ident(name) if name.name == "OwnedEnvelope") { + found.push(expression); + } + } + }, + &mut |_| {}, + ); + assert_eq!(found.len(), 1); + found[0] +} + +#[test] +fn owned_struct_original_constructor_has_exact_type_and_source_field_mapping() { + let (checked, body) = execution(SOURCE, "retire_owned"); + let expression = constructor(&body); + let proof = checked.prepare_owned_struct(expression).unwrap().unwrap(); + assert_eq!(proof.source_fields(), &[0, 1]); + assert_eq!(proof.fields()[0].0, "token"); + assert_eq!( + proof.fields()[1], + ("marker".into(), jett_types::TypeInterner::INT64) + ); + checked.revalidate_owned_struct(&proof, expression).unwrap(); +} + +#[test] +fn owned_struct_constructor_copied_foreign_wrong_body_and_redirected_proofs_are_refused() { + let (checked, body) = execution(SOURCE, "retire_owned"); + let expression = constructor(&body); + let proof = checked.prepare_owned_struct(expression).unwrap().unwrap(); + assert!(checked.prepare_owned_struct(&expression.clone()).is_err()); + let (foreign, foreign_body) = execution(SOURCE, "retire_owned"); + assert!( + checked + .revalidate_owned_struct(&proof, constructor(&foreign_body)) + .is_err() + ); + assert!( + foreign + .revalidate_owned_struct(&proof, constructor(&foreign_body)) + .is_err() + ); + let (other, _) = execution(SOURCE, "main"); + assert!(other.revalidate_owned_struct(&proof, expression).is_err()); + for mutation in 0..4 { + let mut forged = checked.prepare_owned_struct(expression).unwrap().unwrap(); + match mutation { + 0 => forged.source_fields.swap(0, 1), + 1 => forged.ty = jett_types::TypeInterner::INT64, + 2 => forged.fields[0].1 = jett_types::TypeInterner::INT64, + 3 => forged.expression += 1, + _ => unreachable!(), + } + assert!( + checked + .revalidate_owned_struct(&forged, expression) + .is_err() + ); + } +} + +#[test] +fn occupied_struct_same_layout_declaration_cannot_substitute_for_selected_constructor() { + let source = SOURCE.replace( + "function retire_owned(", + "struct OtherEnvelope:\n token: resource_probe.TestHandle\n marker: int64\nfunction retire_owned(", + ); + let (checked, body) = execution(&source, "retire_owned"); + let expression = constructor(&body); + let mut proof = checked.prepare_owned_struct(expression).unwrap().unwrap(); + let other = checked + .program + .resolved() + .scope_table + .definitions + .iter() + .find(|info| info.kind == jett_resolve::DefKind::Struct && info.name == "app.OtherEnvelope") + .unwrap(); + assert_ne!(other.id, proof.definition); + proof.definition = other.id; + assert!(checked.revalidate_owned_struct(&proof, expression).is_err()); +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/pipeline.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/pipeline.rs new file mode 100644 index 00000000..a534270a --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/pipeline.rs @@ -0,0 +1,212 @@ +//! One original checked PipelineStep, distinct from an Expr::Call packet. +use jett_parser::ast::{PipelineStep, TypeExpr}; +use jett_typecheck::{CheckedCallerEffect, CheckedCallerOrigin, CheckedCallerSyntax}; + +use super::*; + +pub(crate) struct PreparedPipelineStep<'source> { + original: &'source PipelineStep, + initial: Option<&'source Expr>, + callee: &'source Expr, + type_arguments: &'source [TypeExpr], + extra_arguments: &'source [CallArg], + invocation: CheckedInvocation, + input_type: TypeId, + raw_call_type: TypeId, + output_type: TypeId, + initial_borrow: Option, + constructor: Option<(String, String, TypeId)>, +} + +impl<'source> PreparedPipelineStep<'source> { + pub(crate) fn original(&self) -> &'source PipelineStep { + self.original + } + pub(crate) fn initial(&self) -> Option<&'source Expr> { + self.initial + } + pub(crate) fn callee(&self) -> &'source Expr { + self.callee + } + pub(crate) fn type_arguments(&self) -> &'source [TypeExpr] { + self.type_arguments + } + pub(crate) fn extra_arguments(&self) -> &'source [CallArg] { + self.extra_arguments + } + pub(crate) fn invocation(&self) -> &CheckedInvocation { + &self.invocation + } + pub(crate) fn input_type(&self) -> TypeId { + self.input_type + } + pub(crate) fn raw_call_type(&self) -> TypeId { + self.raw_call_type + } + pub(crate) fn output_type(&self) -> TypeId { + self.output_type + } + pub(crate) fn initial_borrow(&self) -> Option { + self.initial_borrow + } + pub(crate) fn constructor(&self) -> Option<(String, String, TypeId)> { + self.constructor.clone() + } +} + +impl CheckedExecution { + pub(crate) fn prepare_pipeline_step<'source>( + &self, + pipeline: &'source Expr, + index: usize, + ) -> Result, ResourceExecutionError> { + self.validate_executable_cursor(&self.body)?; + let reference = CheckedBodyReference { + cursor: self.body.clone(), + }; + let mut matches = 0usize; + walk_region( + reference.region()?, + &mut |_| {}, + &mut |original| { + if matches!(original, Expr::Pipeline(..)) && std::ptr::eq(original, pipeline) { + matches += 1; + } + }, + &mut |_| {}, + ); + if matches != 1 { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let Expr::Pipeline(initial, steps, _) = pipeline else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let step = steps + .get(index) + .ok_or(ResourceExecutionError::MissingCheckedInvocation)?; + let input_span = if index == 0 { + initial.span() + } else { + steps + .get(index - 1) + .ok_or(ResourceExecutionError::MissingCheckedInvocation)? + .span + }; + let (callee, type_arguments, extra_arguments, step_view) = pipeline_parts(step); + let invocation = self.invocation(step.span)?; + if invocation.arguments().len() != extra_arguments.len() + 1 { + return Err(ResourceExecutionError::InvalidInvocation); + } + let source0 = &invocation.arguments()[0]; + let source_view = step_view || (index == 0 && written_view(initial)); + if source0.source_span != input_span + || source0.source_index != 0 + || source0.parameter_index != 0 + || (source0.syntax == CheckedCallerSyntax::WrittenView) != source_view + || (index != 0 && source0.origin != CheckedCallerOrigin::OwnedExpression) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + for (argument, fact) in extra_arguments.iter().zip(&invocation.arguments()[1..]) { + if argument.value.span() != fact.source_span + || (fact.syntax == CheckedCallerSyntax::WrittenView) + != written_view(&argument.value) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + } + let facts = self.facts(&self.body)?; + let input_type = *facts + .pipeline_inputs + .get(&step.span) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let raw_call_type = *facts + .pipeline_calls + .get(&step.span) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let output_type = *facts + .source_types + .get(&step.span) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + for ty in [input_type, raw_call_type, output_type] { + if ty.index() as usize >= self.program.checked().interner.len() { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + } + // A Function signature is its declaration result, independently of + // checked secret propagation at this actual invocation occurrence. + if let CheckedInvocationShape::Intrinsic { result_type, .. } = &invocation.packet().shape { + if raw_call_type != *result_type { + return Err(ResourceExecutionError::InvalidInvocation); + } + } + let initial_borrow = + if index == 0 && source_view && source0.effect == CheckedCallerEffect::RetainBorrow { + let mut bare = initial.as_ref(); + while let Expr::Paren(inner, _) | Expr::View(inner, _) = bare { + bare = inner; + } + if let (Expr::Ident(identifier), CheckedCallerOrigin::Binding(binding)) = + (bare, &source0.origin) + { + if self.resolved_definition(identifier.span)? != binding.definition + || facts.bindings.get(&binding.declaration_span) != Some(binding) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + Some(binding.definition) + } else { + None + } + } else { + None + }; + // Keep root's closed enum preparation before evaluating even source0. + let constructor = self.enum_constructor(callee, &invocation)?; + Ok(PreparedPipelineStep { + original: step, + initial: (index == 0).then_some(initial.as_ref()), + callee, + type_arguments, + extra_arguments, + invocation, + input_type, + raw_call_type, + output_type, + initial_borrow, + constructor, + }) + } +} + +fn written_view(mut expression: &Expr) -> bool { + while let Expr::Paren(inner, _) | Expr::Coarsen(inner, _) | Expr::Declassify(inner, _) = + expression + { + expression = inner; + } + matches!(expression, Expr::View(..)) +} + +fn pipeline_parts(step: &PipelineStep) -> (&Expr, &[TypeExpr], &[CallArg], bool) { + let (function, view) = match &step.function { + Expr::View(inner, _) => (inner.as_ref(), true), + other => (other, false), + }; + let (callee, types, arguments) = match function { + Expr::Call(callee, arguments, _) => (callee.as_ref(), &[][..], arguments.as_slice()), + Expr::GenericCall(callee, types, arguments, _) => { + (callee.as_ref(), types.as_slice(), arguments.as_slice()) + } + other => (other, &[][..], step.extra_args.as_slice()), + }; + let mut callee = callee; + while let Expr::Paren(inner, _) = callee { + callee = inner; + } + (callee, types, arguments, view) +} + +#[cfg(test)] +#[path = "pipeline/tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/pipeline/tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/pipeline/tests.rs new file mode 100644 index 00000000..595f275a --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/pipeline/tests.rs @@ -0,0 +1,345 @@ +use super::*; +use crate::resource_execution::tests::program; + +const CONSTRUCT: &str = include_str!("../../../fixtures/18_pipeline_construct_move_close.jett"); +const RETAIN: &str = include_str!("../../../fixtures/19_pipeline_written_view_retains_owner.jett"); +const ABORT: &str = include_str!("../../../fixtures/21_pipeline_named_actual_abort.jett"); + +fn scenario(program: &CheckedResourceProgram) -> DefId { + let functions = program + .module() + .items + .iter() + .filter_map(|item| match item { + Item::Function(function) + if function.name.span.file == jett_common::FileId::new(0) + && function.name.name == "scenario" => + { + Some(function) + } + _ => None, + }) + .collect::>(); + let [function] = functions.as_slice() else { + panic!("one original source scenario"); + }; + let definitions = program + .resolved() + .scope_table + .definitions + .iter() + .filter(|info| { + info.kind == jett_resolve::DefKind::Function + && info.span == function.name.span + && info.namespace.as_deref() == Some("app") + }) + .map(|info| info.id) + .collect::>(); + let [definition] = definitions.as_slice() else { + panic!("one exact primary declaration"); + }; + *definition +} + +fn pipelines(reference: &CheckedBodyReference) -> Vec<&Expr> { + let mut found = Vec::new(); + walk_block(reference.block().unwrap(), &mut |_| {}, &mut |expression| { + if matches!(expression, Expr::Pipeline(..)) { + found.push(expression); + } + }); + found +} + +#[test] +fn resource_pipeline_reference_joins_original_step_inputs_outputs_and_source_permutation() { + for release in [false, true] { + for source in [CONSTRUCT, ABORT] { + let program = program(source, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let entry = checked.entry(scenario(&program)).unwrap(); + let reference = checked.prepare_function_body(&entry).unwrap(); + checked.install_function_body(&reference).unwrap(); + for pipeline in pipelines(&reference) { + let Expr::Pipeline(initial, steps, _) = pipeline else { + unreachable!(); + }; + for index in 0..steps.len() { + let prepared = checked.prepare_pipeline_step(pipeline, index).unwrap(); + let input_span = if index == 0 { + initial.span() + } else { + steps[index - 1].span + }; + assert_eq!(prepared.invocation().arguments()[0].source_span, input_span); + let facts = checked.facts(&checked.body).unwrap(); + assert_eq!( + facts.pipeline_inputs[&steps[index].span], + prepared.input_type() + ); + assert_eq!( + facts.pipeline_calls[&steps[index].span], + prepared.raw_call_type() + ); + assert_eq!( + facts.source_types[&steps[index].span], + prepared.output_type() + ); + if index > 0 { + assert_eq!( + prepared.invocation().arguments()[0].origin, + CheckedCallerOrigin::OwnedExpression + ); + } + if source == ABORT { + assert_eq!( + prepared + .invocation() + .arguments() + .iter() + .map(|argument| (argument.source_index, argument.parameter_index)) + .collect::>(), + vec![(0, 0), (1, 2), (2, 1)] + ); + } + } + } + } + } +} + +#[test] +fn resource_pipeline_reference_refuses_foreign_clones_missing_body_and_invalid_step_index() { + for release in [false, true] { + let program = program(RETAIN, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let entry = checked.entry(scenario(&program)).unwrap(); + let reference = checked.prepare_function_body(&entry).unwrap(); + checked.install_function_body(&reference).unwrap(); + let found = pipelines(&reference); + let [original] = found.as_slice() else { + panic!("one exact pipeline source"); + }; + let prepared = checked.prepare_pipeline_step(original, 0).unwrap(); + assert_eq!( + prepared.invocation().arguments()[0].syntax, + CheckedCallerSyntax::WrittenView + ); + assert_eq!( + prepared.invocation().arguments()[0].effect, + CheckedCallerEffect::RetainBorrow + ); + let CheckedCallerOrigin::Binding(binding) = &prepared.invocation().arguments()[0].origin + else { + panic!("exact original root binding"); + }; + assert_eq!(prepared.initial_borrow(), Some(binding.definition)); + let other_program = crate::resource_execution::tests::program(RETAIN, release); + let mut foreign = + CheckedExecution::new(other_program, ExecutionPurpose::ReferenceRuntime).unwrap(); + assert!(matches!( + foreign.install_function_body(&reference), + Err(ResourceExecutionError::ForeignProgram) + )); + let cloned = (**original).clone(); + assert!(matches!( + checked.prepare_pipeline_step(&cloned, 0), + Err(ResourceExecutionError::MissingCheckedBody) + )); + assert!(matches!( + checked.prepare_pipeline_step(original, usize::MAX), + Err(ResourceExecutionError::MissingCheckedInvocation) + )); + let fabricated = Expr::Call( + Box::new(prepared.callee().clone()), + prepared.extra_arguments().to_vec(), + prepared.original().span, + ); + assert!(matches!( + checked.prepare_pipeline_step(&fabricated, 0), + Err(ResourceExecutionError::MissingCheckedBody) + )); + checked.enter_ordinary(); + assert!(matches!( + checked.prepare_pipeline_step(original, 0), + Err(ResourceExecutionError::MissingCheckedBody) + )); + } +} + +const REQUIRED_PIPELINES: &str = include_str!("../../../fixtures/28_pipeline_absence_regions.jett"); + +fn region_pipelines(reference: &CheckedBodyReference) -> Vec<&Expr> { + let mut found = Vec::new(); + walk_region( + reference.region().unwrap(), + &mut |_| {}, + &mut |expression| { + if matches!(expression, Expr::Pipeline(..)) { + found.push(expression); + } + }, + &mut |_| {}, + ); + found +} + +fn verify_region_steps(checked: &CheckedExecution, reference: &CheckedBodyReference) { + let found = region_pipelines(reference); + assert_eq!(found.len(), 1); + let original = found[0]; + let Expr::Pipeline(initial, steps, _) = original else { + unreachable!(); + }; + assert_eq!(steps.len(), 2); + for index in 0..steps.len() { + let prepared = checked.prepare_pipeline_step(original, index).unwrap(); + let facts = checked.facts(&checked.body).unwrap(); + assert_eq!( + prepared.invocation().arguments()[0].source_span, + if index == 0 { + initial.span() + } else { + steps[index - 1].span + } + ); + assert_eq!( + prepared.input_type(), + facts.pipeline_inputs[&steps[index].span] + ); + assert_eq!( + prepared.raw_call_type(), + facts.pipeline_calls[&steps[index].span] + ); + assert_eq!( + prepared.output_type(), + facts.source_types[&steps[index].span] + ); + } + let cloned = (*original).clone(); + assert!(matches!( + checked.prepare_pipeline_step(&cloned, 0), + Err(ResourceExecutionError::MissingCheckedBody) + )); +} + +#[test] +fn resource_pipeline_reference_authenticates_namespace_and_explicit_expression_regions() { + for release in [false, true] { + let program = program(REQUIRED_PIPELINES, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::NamespaceConstant).unwrap(); + let declaration = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::VarDecl(value) + if value.name.name == "namespace_marker" + && value.name.span.file == jett_common::FileId::new(0) => + { + Some(value) + } + _ => None, + }) + .unwrap(); + let initializer = checked.prepare_namespace_initializer(declaration).unwrap(); + let reference = initializer.reference(); + assert!(matches!( + reference.region().unwrap(), + OriginalRegion::Expression(_) + )); + checked.install_function_body(reference).unwrap(); + verify_region_steps(&checked, reference); + let namespace_pipeline = region_pipelines(reference)[0]; + + checked.replace_purpose(ExecutionPurpose::ExplicitComptime); + let function = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(value) if value.name.name == "explicit_marker" => Some(value), + _ => None, + }) + .unwrap(); + let returned = function + .body + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::Return(value) => value.value.as_ref(), + _ => None, + }) + .unwrap(); + let Expr::Comptime(expression, span) = returned else { + panic!("original required expression"); + }; + let contexts = checked + .prepare_explicit_contexts(expression, *span, Some(function.name.span), &[]) + .unwrap(); + let [context] = contexts.as_slice() else { + panic!("one exact ordinary required context"); + }; + let explicit = context.reference(); + assert!(matches!( + explicit.region().unwrap(), + OriginalRegion::Expression(_) + )); + checked.install_function_body(explicit).unwrap(); + verify_region_steps(&checked, explicit); + assert!(matches!( + checked.prepare_pipeline_step(namespace_pipeline, 0), + Err(ResourceExecutionError::MissingCheckedBody) + )); + let mut foreign = CheckedExecution::new( + crate::resource_execution::tests::program(REQUIRED_PIPELINES, release), + ExecutionPurpose::ExplicitComptime, + ) + .unwrap(); + assert!(matches!( + foreign.install_function_body(explicit), + Err(ResourceExecutionError::ForeignProgram) + )); + } +} + +#[test] +fn resource_pipeline_reference_authenticates_verify_and_property_original_regions() { + for release in [false, true] { + let program = program(REQUIRED_PIPELINES, release); + let mut checked = CheckedExecution::new(program.clone(), ExecutionPurpose::Verify).unwrap(); + let verify = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Verify(value) => Some(value), + _ => None, + }) + .unwrap(); + let reference = checked.prepare_verify_body(verify).unwrap(); + checked.install_function_body(&reference).unwrap(); + verify_region_steps(&checked, &reference); + let verify_pipeline = region_pipelines(&reference)[0]; + checked.replace_purpose(ExecutionPurpose::Property); + let property = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Property(value) => Some(value), + _ => None, + }) + .unwrap(); + let property_reference = checked.prepare_property_body(property).unwrap(); + checked.install_function_body(&property_reference).unwrap(); + verify_region_steps(&checked, &property_reference); + assert!(matches!( + checked.prepare_pipeline_step(verify_pipeline, 0), + Err(ResourceExecutionError::MissingCheckedBody) + )); + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields.rs new file mode 100644 index 00000000..131f572b --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields.rs @@ -0,0 +1,358 @@ +//! Original reflected field loops select ordinal-specific checked bodies. +//! Runtime metadata values remain data, never executable or provider authority. +use super::*; +use jett_parser::ast::ForStmt; +use jett_resolve::DefKind; +use jett_types::ReflectionFieldInfo; + +pub(crate) struct PreparedReflectedFieldLoop { + parent: CheckedBodyReference, + key: CheckedAttemptKey, + loop_span: Span, + variable: DefId, + owner: TypeId, + fields: Vec<(TypeId, ReflectionFieldInfo)>, +} + +#[derive(Clone)] +pub(crate) struct PreparedReflectedFieldIteration { + owner: Arc, + index: usize, +} + +impl PreparedReflectedFieldLoop { + pub(crate) fn owner_name(&self) -> String { + self.parent + .cursor + .executable + .as_ref() + .expect("prepared original field loop") + .program + .checked() + .interner + .type_name(self.owner) + } + + pub(crate) fn fields(&self) -> impl Iterator { + self.fields.iter().map(|(_, field)| field) + } + + pub(crate) fn validate_count(&self, count: usize) -> Result<(), ResourceExecutionError> { + if count != self.fields.len() { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(()) + } + + pub(crate) fn iteration( + self: &Arc, + index: usize, + ) -> Result { + if index >= self.fields.len() { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(PreparedReflectedFieldIteration { + owner: self.clone(), + index, + }) + } + + fn original(&self) -> Result<&ForStmt, ResourceExecutionError> { + original_for(self.parent.region()?, self.loop_span) + } +} + +impl PreparedReflectedFieldIteration { + pub(crate) fn variable(&self) -> DefId { + self.owner.variable + } + + pub(crate) fn validate_value( + &self, + value: &crate::Value, + ) -> Result<(), ResourceExecutionError> { + let (_, field) = self + .owner + .fields + .get(self.index) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let expected = + crate::Interpreter::reflection_field_info_value(&self.owner.owner_name(), None, field); + if value != &expected { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(()) + } +} + +impl CheckedExecution { + pub(crate) fn checked_reflection_fields( + &self, + owner: TypeId, + ) -> Result, ResourceExecutionError> { + Ok(checked_owner_fields(self.program(), owner)? + .into_iter() + .map(|(_, field)| field) + .collect()) + } + + pub(crate) fn prepare_reflected_field_loop( + &self, + source: &ForStmt, + ) -> Result>, ResourceExecutionError> { + self.validate_executable_cursor(&self.body)?; + let parent = CheckedBodyReference { + cursor: self.body.clone(), + }; + let original = original_for(parent.region()?, source.span)?; + if !std::ptr::eq(original, source) { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let Expr::GenericCall(callee, source_types, arguments, occurrence) = &original.iterable + else { + return Ok(None); + }; + if self.facts(&self.body)?.intrinsics.get(occurrence) != Some(&IntrinsicId::TypeFields) { + return Ok(None); + } + if source_types.len() != 1 || !arguments.is_empty() || original.value_variable.is_some() { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let invocation = self.invocation(*occurrence)?; + let prepared = + self.prepare_intrinsic_arguments(callee, source_types, arguments, &invocation)?; + if prepared.intrinsic() != IntrinsicId::TypeFields { + return Err(ResourceExecutionError::InvalidInvocation); + } + let [owner] = prepared.types() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let fields = checked_owner_fields(self.program(), *owner)?; + let variables = self + .program + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == DefKind::Variable + && definition.span == original.variable.span + && definition.name == original.variable.name + }) + .map(|definition| definition.id) + .collect::>(); + let [variable] = variables.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let loop_span = original.span; + Ok(Some(Arc::new(PreparedReflectedFieldLoop { + parent, + key: self.attempt_key(&self.body)?, + loop_span, + variable: *variable, + owner: *owner, + fields, + }))) + } + + pub(crate) fn prepare_reflected_field_scope( + &self, + bind: &ComptimeTypeBindStmt, + iteration: &PreparedReflectedFieldIteration, + ) -> Result { + let proof = &iteration.owner; + self.validate_executable_cursor(&proof.parent.cursor)?; + if self.attempt_key(&proof.parent.cursor)? != proof.key { + return Err(ResourceExecutionError::MissingCheckedBody); + } + if !self + .attempt_key(&self.body)? + .is_lexical_descendant_of(&proof.key) + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let source_loop = proof.original()?; + let Expr::GenericCall(_, source_types, arguments, occurrence) = &source_loop.iterable + else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let loop_facts = self.facts(&proof.parent.cursor)?; + let variable = self + .program + .resolved() + .scope_table + .definitions + .get(proof.variable.index() as usize) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + if source_types.len() != 1 + || !arguments.is_empty() + || loop_facts.intrinsics.get(occurrence) != Some(&IntrinsicId::TypeFields) + || loop_facts + .intrinsic_types + .get(occurrence) + .map(Vec::as_slice) + != Some(&[proof.owner][..]) + || variable.id != proof.variable + || variable.kind != DefKind::Variable + || variable.span != source_loop.variable.span + || variable.name != source_loop.variable.name + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let parent = CheckedBodyReference { + cursor: self.body.clone(), + }; + let original = region_binding(parent.region()?, bind.span)?; + if !std::ptr::eq(original, bind) || !binding_under_loop(&source_loop.body, original) { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let Expr::FieldAccess(base, member, _) = &original.value else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let Expr::Ident(variable) = base.as_ref() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + if member.name != "type_info" + || self.program.resolved().resolutions.get(&variable.span) != Some(&proof.variable) + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + // Rejoin the immutable checked owner table, not a cloned runtime TypeInfo. + let fields = checked_owner_fields(self.program(), proof.owner)?; + if fields != proof.fields { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let (bound_type, field) = fields + .get(iteration.index) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let facts = self.facts(&self.body)?; + let candidates = facts + .scopes + .get(&original.span) + .ok_or(ResourceExecutionError::MissingCheckedBody)? + .iter() + .enumerate() + .filter(|(_, candidate)| { + candidate.selection + == CheckedComptimeTypeSelection::ReflectedIteration(iteration.index) + }) + .collect::>(); + let [(index, candidate)] = candidates.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + if candidate.bound_type != *bound_type || candidate.reflection != field.type_info { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let mut cursor = self.body.clone(); + cursor.scopes.push(ScopedSelection { + owner: original.span, + index: *index, + bound_type: candidate.bound_type, + selection: candidate.selection, + reflection: candidate.reflection.clone(), + }); + cursor + .executable + .as_mut() + .ok_or(ResourceExecutionError::MissingCheckedBody)? + .path + .push(RegionStep::Scope(original.span)); + self.validate_executable_cursor(&cursor)?; + Ok(PreparedDirectScope { + reference: CheckedBodyReference { cursor }, + projection: scoped_projection(candidate, &self.program.checked().interner), + reflection: candidate.reflection.clone(), + }) + } +} + +fn checked_owner_fields( + program: &CheckedResourceProgram, + owner: TypeId, +) -> Result, ResourceExecutionError> { + let types = &program.checked().interner; + if owner.index() as usize >= types.len() { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + let field_types: Vec = match types.resolve(owner) { + Type::Struct(id) => types + .resolve_struct(*id) + .fields + .iter() + .map(|(_, ty)| *ty) + .collect(), + Type::Bitfield(id) => types + .resolve_bitfield(*id) + .fields + .iter() + .map(|field| field.ty) + .collect(), + _ => return Ok(Vec::new()), + }; + let fields = program + .checked() + .reflection_metadata + .get_type_fields_for_id(owner) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + if fields.len() != field_types.len() + || fields + .iter() + .enumerate() + .any(|(index, field)| field.index != index) + || field_types + .iter() + .any(|ty| ty.index() as usize >= types.len()) + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(field_types + .into_iter() + .zip(fields.iter().cloned()) + .collect()) +} + +fn original_for( + region: OriginalRegion<'_>, + span: Span, +) -> Result<&ForStmt, ResourceExecutionError> { + let mut loops = Vec::new(); + walk_region(region, &mut |_| {}, &mut |_| {}, &mut |statement| { + if let Stmt::For(value) = statement { + if value.span == span { + loops.push(value); + } + } + }); + let [original] = loops.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + Ok(*original) +} + +fn binding_under_loop(block: &Block, selected: &ComptimeTypeBindStmt) -> bool { + let mut matches = 0; + walk_block( + block, + &mut |binding| { + if std::ptr::eq(binding, selected) { + matches += 1; + } else if binding_under_loop(&binding.body, selected) { + matches += 1; + } + }, + &mut |_| {}, + ); + matches == 1 +} + +#[cfg(test)] +#[path = "reflected_fields/tests.rs"] +mod tests; + +#[cfg(test)] +#[path = "reflected_fields/recursion_tests.rs"] +mod recursion_tests; + +#[cfg(test)] +#[path = "reflected_fields/metadata_shapes_tests.rs"] +mod metadata_shapes_tests; diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/01_reflected_return.jett b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/01_reflected_return.jett new file mode 100644 index 00000000..15f0aa22 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/01_reflected_return.jett @@ -0,0 +1,14 @@ +namespace app +struct FlowShape: + first: int64 + second: int64 + third: int64 +export function scenario() returns int64: + mutable int64 path = 0 + for field in type.fields[FlowShape](): + comptime type Element = field.type_info: + Element digit = field.index + 1 + path = path * 10 + digit + if field.index == 1: + return path + return 999 diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/02_reflected_break.jett b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/02_reflected_break.jett new file mode 100644 index 00000000..6a794822 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/02_reflected_break.jett @@ -0,0 +1,16 @@ +namespace app +struct FlowShape: + first: int64 + second: int64 + third: int64 +export function scenario() returns int64: + mutable int64 path = 0 + for field in type.fields[FlowShape](): + comptime type Element = field.type_info: + Element digit = field.index + 1 + path = path * 10 + digit + if field.index == 1: + break + path = path * 10 + 8 + path = path * 10 + 9 + return path * 10 + 7 diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/03_reflected_continue.jett b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/03_reflected_continue.jett new file mode 100644 index 00000000..714af610 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/03_reflected_continue.jett @@ -0,0 +1,16 @@ +namespace app +struct FlowShape: + first: int64 + second: int64 + third: int64 +export function scenario() returns int64: + mutable int64 path = 0 + for field in type.fields[FlowShape](): + comptime type Element = field.type_info: + Element digit = field.index + 1 + path = path * 10 + digit + if field.index == 1: + continue + path = path * 10 + 8 + path = path * 10 + 9 + return path * 10 + 7 diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/04_reflected_default.jett b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/04_reflected_default.jett new file mode 100644 index 00000000..01d23feb --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/04_reflected_default.jett @@ -0,0 +1,21 @@ +namespace app +type Count = int64 +struct FlowShape: + first: int64 + second: int64 + named: Count +function selected_or_failure(index: int64) returns result[int64, string]: + if index == 1: + return fail("selected middle field") + return ok(index + 1) +export function scenario() returns int64: + mutable int64 path = 0 + for field in type.fields[FlowShape](): + comptime type Element = field.type_info: + Element observed = selected_or_failure(field.index) handle error: + default 9 + Element resumed = observed + path = path * 10 + resumed + if type.name[Element]() == "app.Count": + path = path + 1000 + return path diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/metadata_shapes.jett b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/metadata_shapes.jett new file mode 100644 index 00000000..582051d9 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/metadata_shapes.jett @@ -0,0 +1,31 @@ +namespace app +struct Heterogeneous: + label: string + count: int64 + enabled: bool +bitfield WideCounter: + sequence: 64 bits +struct Pair[T, U]: + first: T + second: U +export function heterogeneous_fields() returns string: + mutable string labels = "" + for field in type.fields[Heterogeneous](): + comptime type Element = field.type_info: + string label = type.name[Element]() + labels = "{labels}{field.name}:{label}|" + return labels +export function bitfield_fields() returns string: + mutable string labels = "" + for field in type.fields[WideCounter](): + comptime type Element = field.type_info: + string label = type.name[Element]() + labels = "{labels}{field.name}:{label}|" + return labels +export function generic_fields() returns string: + mutable string labels = "" + for field in type.fields[Pair[int64, string]](): + comptime type Element = field.type_info: + string label = type.name[Element]() + labels = "{labels}{field.name}:{label}|" + return labels diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/metadata_shapes_tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/metadata_shapes_tests.rs new file mode 100644 index 00000000..291a4ef5 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/metadata_shapes_tests.rs @@ -0,0 +1,173 @@ +//! Checked field types for heterogeneous, bitfield and generic owners. + +use super::*; +use crate::{Interpreter, Value}; +use jett_common::FileId; +use jett_parser::ast::{ComptimeTypeBindStmt, ForStmt, FunctionDef, Item, Stmt}; +use jett_types::{ReflectionMetadata, Type, TypeInterner}; +use std::sync::Arc; + +const SHAPES_SOURCE: &str = include_str!("metadata_shapes.jett"); + +fn original_function<'a>(program: &'a CheckedResourceProgram, name: &str) -> &'a FunctionDef { + program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) && function.name.name == name => + { + Some(function) + } + _ => None, + }) + .expect("one original project function") +} + +fn original_loop(function: &FunctionDef) -> &ForStmt { + function + .body + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::For(statement) => Some(statement), + _ => None, + }) + .expect("one original direct field loop") +} + +fn original_binding(statement: &ForStmt) -> &ComptimeTypeBindStmt { + statement + .body + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::ComptimeTypeBind(binding) => Some(binding), + _ => None, + }) + .expect("original field.type_info binding") +} + +#[test] +fn heterogeneous_bitfield_and_concrete_generic_owners_select_original_ordinal_types() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(SHAPES_SOURCE, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + for (name, is_bitfield, expected) in [ + ( + "heterogeneous_fields", + false, + vec![ + TypeInterner::STRING, + TypeInterner::INT64, + TypeInterner::BOOL, + ], + ), + ("bitfield_fields", true, vec![TypeInterner::UINT64]), + ( + "generic_fields", + false, + vec![TypeInterner::INT64, TypeInterner::STRING], + ), + ] { + let function = original_function(&program, name); + let definition = checked.declaration_definition(function.name.span).unwrap(); + let parent = checked + .prepare_function_body(&checked.entry(definition).unwrap()) + .unwrap(); + checked.install_function_body(&parent).unwrap(); + let before = checked.attempt_key(&checked.cursor()).unwrap(); + let source_loop = original_loop(function); + let binding = original_binding(source_loop); + let prepared = checked + .prepare_reflected_field_loop(source_loop) + .unwrap() + .unwrap(); + assert_eq!(checked.attempt_key(&checked.cursor()).unwrap(), before); + assert_eq!( + prepared + .fields + .iter() + .map(|field| field.0) + .collect::>(), + expected + ); + assert_eq!( + matches!( + program.checked().interner.resolve(prepared.owner), + Type::Bitfield(_) + ), + is_bitfield + ); + if !is_bitfield { + assert!(matches!( + program.checked().interner.resolve(prepared.owner), + Type::Struct(_) + )); + } + for index in 0..prepared.fields.len() { + let scope = checked + .prepare_reflected_field_scope(binding, &prepared.iteration(index).unwrap()) + .unwrap(); + assert_eq!(checked.attempt_key(&checked.cursor()).unwrap(), before); + assert!(std::ptr::eq(scope.body().unwrap(), &binding.body)); + assert_eq!(scope.reflection(), &prepared.fields[index].1.type_info); + let selected = scope.reference.cursor.scopes.last().unwrap(); + assert_eq!( + selected.selection, + CheckedComptimeTypeSelection::ReflectedIteration(index) + ); + assert_eq!(selected.bound_type, expected[index]); + checked.install_direct_scope(&scope).unwrap(); + checked.install_function_body(&parent).unwrap(); + assert_eq!(checked.attempt_key(&checked.cursor()).unwrap(), before); + } + } + } +} + +#[test] +fn selected_metadata_shapes_execute_in_source_order_without_runtime_authority() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(SHAPES_SOURCE, release); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + interpreter.set_reflection_metadata(Arc::new(ReflectionMetadata::new())); + let baseline = interpreter.resource_test_entry_context().unwrap(); + for _ in 0..2 { + for (name, expected) in [ + ( + "heterogeneous_fields", + "label:string|count:int64|enabled:bool|", + ), + ("bitfield_fields", "sequence:uint64|"), + ("generic_fields", "first:int64|second:string|"), + ] { + let definition = checked + .declaration_definition(original_function(&program, name).name.span) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(definition, Vec::new()) + .unwrap(), + Value::String(expected.to_string()), + "{name}, release={release}" + ); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), baseline); + assert_eq!( + interpreter.resource_test_observations(), + Err(ResourceExecutionError::ProviderDisabled.to_string()) + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); + } + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/recursion_tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/recursion_tests.rs new file mode 100644 index 00000000..dad18186 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/recursion_tests.rs @@ -0,0 +1,161 @@ +//! Recursive reflected calls restore each caller's field selector and alias. + +use super::*; +use crate::{Interpreter, Value}; +use jett_common::FileId; +use jett_parser::ast::{FunctionDef, Item, Stmt}; +use jett_types::ReflectionMetadata; +use std::sync::Arc; + +const RECURSIVE_SOURCE: &str = include_str!("recursive_labels.jett"); +const BASE: &str = concat!( + "first:int64<>first:int64|", + "second:int64<>second:int64|", + "named:app.Count<>named:app.Count|", +); +const DEPTH_ONE: &str = concat!( + "first:int64<", + "first:int64<>first:int64|second:int64<>second:int64|named:app.Count<>named:app.Count|", + ">first:int64|", + "second:int64<", + "first:int64<>first:int64|second:int64<>second:int64|named:app.Count<>named:app.Count|", + ">second:int64|", + "named:app.Count<", + "first:int64<>first:int64|second:int64<>second:int64|named:app.Count<>named:app.Count|", + ">named:app.Count|", +); + +fn depth_two_expected() -> String { + // A fixed golden composition, not a second recursive evaluator. Every + // outer field must observe its own ordinal and alias after the same Source + // function executes all three inner field scopes twice more in depth. + format!( + "first:int64<{DEPTH_ONE}>first:int64|second:int64<{DEPTH_ONE}>second:int64|named:app.Count<{DEPTH_ONE}>named:app.Count|" + ) +} + +fn original_function<'a>(program: &'a CheckedResourceProgram, name: &str) -> &'a FunctionDef { + program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) && function.name.name == name => + { + Some(function) + } + _ => None, + }) + .expect("one original project function") +} + +fn assert_no_resource_effects(interpreter: &mut Interpreter) { + assert_eq!( + interpreter.resource_test_observations(), + Err(ResourceExecutionError::ProviderDisabled.to_string()) + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); +} + +#[test] +fn same_source_body_recursion_restores_each_callers_field_ordinal_and_alias() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(RECURSIVE_SOURCE, release); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let original = original_function(&program, "recursive_labels"); + let definition = checked.declaration_definition(original.name.span).unwrap(); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + interpreter.set_reflection_metadata(Arc::new(ReflectionMetadata::new())); + let baseline = interpreter.resource_test_entry_context().unwrap(); + assert_no_resource_effects(&mut interpreter); + for (depth, expected) in [ + (0, BASE.to_string()), + (1, DEPTH_ONE.to_string()), + (2, depth_two_expected()), + (1, DEPTH_ONE.to_string()), + (0, BASE.to_string()), + ] { + assert_eq!( + interpreter + .call_checked_program_entry(definition, vec![Value::Int64(depth)]) + .unwrap(), + Value::String(expected), + "depth={depth}, release={release}" + ); + // Frozen observation includes checked reflected selector depth, + // checked body/source state, metadata stacks and lexical scopes. + assert_eq!(interpreter.resource_test_entry_context().unwrap(), baseline); + assert_no_resource_effects(&mut interpreter); + } + } +} + +#[test] +fn closed_pure_same_body_recursion_is_selected_by_required_evaluation() { + let source = format!( + "{RECURSIVE_SOURCE}{}", + r#" +export function required_recursive_labels() returns string: + return comptime recursive_labels(2) +"# + ); + for release in [false, true] { + let program = crate::resource_execution::tests::program(&source, release); + let types = Arc::new(crate::checked_types::CheckedExpressionTypes { + resource_program: Some(program.clone()), + expressions: program + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, program.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let required = crate::evaluate_explicit_comptime_expressions_capture( + program.module(), + Arc::new(ReflectionMetadata::new()), + types, + Arc::new(std::collections::HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "release={release}: {:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + let expected = Value::String(depth_two_expected()); + assert_eq!(required.values.len(), 1); + assert_eq!( + required.values.values().cloned().collect::>(), + vec![expected.clone()] + ); + assert!(required.values.checked_values_are_mirrored()); + assert!( + required + .values + .checked_resource_hook_values(&program) + .unwrap() + .is_empty() + ); + let proofs = required.values.checked_required_values(&program).unwrap(); + let [proof] = proofs.as_slice() else { + panic!("one authenticated required recursion value"); + }; + let original = original_function(&program, "required_recursive_labels"); + let [Stmt::Return(statement)] = original.body.stmts.as_slice() else { + panic!("one original explicit comptime return"); + }; + assert!(proof.matches_original_comptime(statement.value.as_ref().unwrap())); + assert!(proof.belongs_to(&program)); + assert!(proof.is_explicit_comptime()); + assert_eq!(proof.value(), &expected); + assert!(proof.checked_hook(&program).unwrap().is_none()); + // Required capture creates its own worker without a provider. Its API + // does not expose provider observations or the worker's final snapshot. + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/recursive_labels.jett b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/recursive_labels.jett new file mode 100644 index 00000000..4a26488a --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/recursive_labels.jett @@ -0,0 +1,19 @@ +namespace app +type Count = int64 +struct IterationShape: + first: int64 + second: int64 + named: Count +export function recursive_labels(depth: int64) returns string: + mutable string labels = "" + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + string field_before = field.name + string before = type.name[Element]() + mutable string inside = "" + if depth > 0: + inside = recursive_labels(depth - 1) + string field_after = field.name + string after = type.name[Element]() + labels = "{labels}{field_before}:{before}<{inside}>{field_after}:{after}|" + return labels diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/tests.rs new file mode 100644 index 00000000..03db648c --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/tests.rs @@ -0,0 +1,759 @@ +//! Exact original-loop and ordinal-specific checked-body proofs. +//! Deliberate proof corruption remains confined to this private test module. + +use super::*; +use crate::{Interpreter, Value}; +use jett_common::FileId; +use jett_parser::ast::{Block, ComptimeTypeBindStmt, ForStmt, FunctionDef, Item, Stmt}; +use jett_types::{ReflectionMetadata, TypeInterner}; +use std::sync::Arc; + +// The first two fields intentionally share both TypeId and reflection. The +// third has the same canonical TypeId but retains an explicit source alias. +// Both loops in `two_loops` use the same owner and variable spelling, so only +// original Source occurrence identity can separate their prepared proofs. +const SOURCE: &str = r#"namespace app +type Count = int64 +struct IterationShape: + first: int64 + second: int64 + named: Count +enum NoFields: + missing +export function reflected() returns nothing: + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + string label = type.name[Element]() + return nothing +export function other() returns nothing: + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + string label = type.name[Element]() + return nothing +export function two_loops() returns nothing: + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + string label = type.name[Element]() + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + string label = type.name[Element]() + return nothing +export function ordinary_loop() returns nothing: + for value in list(1): + int64 marker = value + return nothing +export function nested_scope() returns nothing: + for field in type.fields[IterationShape](): + comptime type Outer = type.info[int64](): + comptime type Element = field.type_info: + string label = type.name[Element]() + return nothing +export function reflected_labels() returns string: + mutable string labels = "" + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + string label = type.name[Element]() + labels = "{labels}{label}|" + return labels +export function two_loop_labels() returns string: + mutable string labels = "" + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + string label = type.name[Element]() + labels = "{labels}{label}|" + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + string label = type.name[Element]() + labels = "{labels}{label}|" + return labels +function helper_labels() returns string: + mutable string labels = "" + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + string label = type.name[Element]() + labels = "{labels}{label}|" + return labels +export function helper_inside_loop_labels() returns string: + mutable string labels = "" + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + string before = type.name[Element]() + string inside = helper_labels() + string after = type.name[Element]() + labels = "{labels}{before}>{inside}>{after}|" + return labels +export function empty_fields() returns int64: + mutable int64 total = 0 + for field in type.fields[int64](): + total = total + 1 + for field in type.fields[NoFields](): + total = total + 1 + for field in type.fields[resource_probe.TestHandle](): + total = total + 1 + return total +"#; + +fn function<'a>(program: &'a CheckedResourceProgram, name: &str) -> &'a FunctionDef { + program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.name == name && function.name.span.file == FileId::new(0) => + { + Some(function) + } + _ => None, + }) + .expect("one original project function") +} + +fn loop_at(function: &FunctionDef, index: usize) -> &ForStmt { + function + .body + .stmts + .iter() + .filter_map(|statement| match statement { + Stmt::For(statement) => Some(statement), + _ => None, + }) + .nth(index) + .expect("one original top-level loop at the requested position") +} + +fn field_scope(block: &Block) -> &ComptimeTypeBindStmt { + block + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::ComptimeTypeBind(binding) => Some(binding), + _ => None, + }) + .expect("original field.type_info binding") +} + +fn install_entry( + checked: &mut CheckedExecution, + program: &CheckedResourceProgram, + name: &str, +) -> CheckedBodyReference { + let source = function(program, name); + let definition = checked.declaration_definition(source.name.span).unwrap(); + let reference = checked + .prepare_function_body(&checked.entry(definition).unwrap()) + .unwrap(); + checked.install_function_body(&reference).unwrap(); + reference +} + +fn execution( + release: bool, + name: &str, +) -> ( + Arc, + CheckedExecution, + CheckedBodyReference, +) { + let program = crate::resource_execution::tests::program(SOURCE, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let reference = install_entry(&mut checked, &program, name); + (program, checked, reference) +} + +fn current_key(checked: &CheckedExecution) -> CheckedAttemptKey { + checked.attempt_key(&checked.cursor()).unwrap() +} + +// Deliberate proof forgery is confined to the child test module. No public +// constructor or production Clone requirement is introduced by this helper. +fn copy_loop(source: &PreparedReflectedFieldLoop) -> PreparedReflectedFieldLoop { + PreparedReflectedFieldLoop { + parent: source.parent.clone(), + key: source.key.clone(), + loop_span: source.loop_span, + variable: source.variable, + owner: source.owner, + fields: source.fields.clone(), + } +} + +fn assert_scope_refused_without_mutation( + checked: &CheckedExecution, + binding: &ComptimeTypeBindStmt, + iteration: &PreparedReflectedFieldIteration, + reason: &str, +) { + let before = current_key(checked); + assert!( + checked + .prepare_reflected_field_scope(binding, iteration) + .is_err(), + "{reason}" + ); + assert_eq!(current_key(checked), before, "{reason}: cursor changed"); +} + +#[test] +fn reflected_field_scope_keeps_equal_types_and_source_aliases_in_distinct_iterations() { + for release in [false, true] { + let (program, mut checked, parent) = execution(release, "reflected"); + let original_loop = loop_at(function(&program, "reflected"), 0); + let binding = field_scope(&original_loop.body); + let before = current_key(&checked); + let prepared = checked + .prepare_reflected_field_loop(original_loop) + .unwrap() + .expect("trusted direct type.fields loop"); + assert_eq!(current_key(&checked), before); + assert_eq!(prepared.fields.len(), 3); + assert_eq!(prepared.fields[0].0, TypeInterner::INT64); + assert_eq!(prepared.fields[0].0, prepared.fields[1].0); + assert_eq!(prepared.fields[0].0, prepared.fields[2].0); + assert_eq!( + prepared.fields[0].1.type_info, + prepared.fields[1].1.type_info + ); + assert_eq!(prepared.fields[2].1.type_info.kind, "alias"); + assert_eq!(prepared.fields[2].1.type_info.type_name, "app.Count"); + assert_ne!( + prepared.fields[0].1.type_info, + prepared.fields[2].1.type_info + ); + + for index in 0..prepared.fields.len() { + let iteration = prepared.iteration(index).unwrap(); + let scope = checked + .prepare_reflected_field_scope(binding, &iteration) + .unwrap(); + assert_eq!(current_key(&checked), before); + assert!(std::ptr::eq(scope.body().unwrap(), &binding.body)); + assert_eq!(scope.bound_name(), "int64"); + assert_eq!(scope.reflection(), &prepared.fields[index].1.type_info); + let selected = scope.reference.cursor.scopes.last().unwrap(); + assert_eq!( + selected.selection, + CheckedComptimeTypeSelection::ReflectedIteration(index) + ); + assert_eq!(selected.bound_type, prepared.fields[index].0); + + checked.install_direct_scope(&scope).unwrap(); + assert_ne!(current_key(&checked), before); + assert_scope_refused_without_mutation( + &checked, + binding, + &iteration, + "parent-loop proof cannot enter from its own selected child body", + ); + checked.install_function_body(&parent).unwrap(); + assert_eq!(current_key(&checked), before); + } + assert!(prepared.iteration(prepared.fields.len()).is_err()); + assert_eq!(current_key(&checked), before); + } +} + +#[test] +fn reflected_field_loop_and_scope_require_original_nodes_within_the_current_body() { + for release in [false, true] { + let (program, checked, _) = execution(release, "reflected"); + let original_loop = loop_at(function(&program, "reflected"), 0); + let binding = field_scope(&original_loop.body); + let before = current_key(&checked); + let cloned_loop = original_loop.clone(); + assert!(checked.prepare_reflected_field_loop(&cloned_loop).is_err()); + assert_eq!(current_key(&checked), before); + + let prepared = checked + .prepare_reflected_field_loop(original_loop) + .unwrap() + .unwrap(); + let iteration = prepared.iteration(0).unwrap(); + let cloned_binding = binding.clone(); + assert_scope_refused_without_mutation( + &checked, + &cloned_binding, + &iteration, + "a clone preserves spans but has no original binding identity", + ); + + let other_loop = loop_at(function(&program, "other"), 0); + assert!(checked.prepare_reflected_field_loop(other_loop).is_err()); + assert_scope_refused_without_mutation( + &checked, + field_scope(&other_loop.body), + &iteration, + "a different function's original binding is not the selected loop body", + ); + assert_eq!(current_key(&checked), before); + } +} + +#[test] +fn reflected_field_iteration_cannot_cross_programs_or_changed_parent_frames() { + for release in [false, true] { + let (program, mut checked, _) = execution(release, "reflected"); + let original_loop = loop_at(function(&program, "reflected"), 0); + let binding = field_scope(&original_loop.body); + let prepared = checked + .prepare_reflected_field_loop(original_loop) + .unwrap() + .unwrap(); + let iteration = prepared.iteration(0).unwrap(); + + let (foreign_program, foreign, _) = execution(release, "reflected"); + let foreign_loop = loop_at(function(&foreign_program, "reflected"), 0); + assert_scope_refused_without_mutation( + &foreign, + field_scope(&foreign_loop.body), + &iteration, + "identical Source in another compiler session has no shared authority", + ); + + install_entry(&mut checked, &program, "other"); + assert_scope_refused_without_mutation( + &checked, + binding, + &iteration, + "a valid proof cannot be replayed after the active function changes", + ); + } +} + +#[test] +fn reflected_field_iteration_cannot_cross_equal_looking_original_loops() { + for release in [false, true] { + let (program, checked, _) = execution(release, "two_loops"); + let function = function(&program, "two_loops"); + let first = loop_at(function, 0); + let second = loop_at(function, 1); + let first_proof = checked + .prepare_reflected_field_loop(first) + .unwrap() + .unwrap(); + let second_proof = checked + .prepare_reflected_field_loop(second) + .unwrap() + .unwrap(); + assert_eq!(first_proof.owner, second_proof.owner); + assert_eq!(first_proof.fields, second_proof.fields); + assert_ne!(first_proof.loop_span, second_proof.loop_span); + assert_ne!(first_proof.variable, second_proof.variable); + assert_scope_refused_without_mutation( + &checked, + field_scope(&second.body), + &first_proof.iteration(0).unwrap(), + "same owner, variable spelling, index and type do not identify a Source loop", + ); + assert_scope_refused_without_mutation( + &checked, + field_scope(&first.body), + &second_proof.iteration(0).unwrap(), + "the converse loop replay must also fail", + ); + } +} + +#[test] +fn reflected_field_scope_revalidates_private_iteration_and_loop_proof_contents() { + for release in [false, true] { + let (program, checked, _) = execution(release, "reflected"); + let original_loop = loop_at(function(&program, "reflected"), 0); + let binding = field_scope(&original_loop.body); + let prepared = checked + .prepare_reflected_field_loop(original_loop) + .unwrap() + .unwrap(); + let (_, foreign, foreign_parent) = execution(release, "reflected"); + let bad_index = PreparedReflectedFieldIteration { + owner: prepared.clone(), + index: prepared.fields.len(), + }; + assert_scope_refused_without_mutation( + &checked, + binding, + &bad_index, + "out-of-range index must fail even if iteration() was bypassed", + ); + + for corruption in 0..8 { + let mut forged = copy_loop(&prepared); + match corruption { + 0 => forged.variable = DefId::new(u32::MAX), + 1 => forged.owner = TypeInterner::STRING, + 2 => forged.fields[0].0 = TypeInterner::STRING, + 3 => forged.fields[0].1.type_info.kind = "struct".to_string(), + 4 => forged.fields[0].1.name = "different_field".to_string(), + 5 => forged.loop_span = loop_at(function(&program, "other"), 0).span, + 6 => forged.key = current_key(&foreign), + 7 => forged.parent = foreign_parent.clone(), + _ => unreachable!(), + } + let iteration = PreparedReflectedFieldIteration { + owner: Arc::new(forged), + index: 0, + }; + assert_scope_refused_without_mutation( + &checked, + binding, + &iteration, + &format!("private proof corruption {corruption}"), + ); + } + } +} + +#[test] +fn ordinary_source_loop_returns_none_without_minting_a_reflected_proof() { + for release in [false, true] { + let (program, checked, _) = execution(release, "ordinary_loop"); + let original_loop = loop_at(function(&program, "ordinary_loop"), 0); + let before = current_key(&checked); + assert!( + checked + .prepare_reflected_field_loop(original_loop) + .unwrap() + .is_none() + ); + assert_eq!(current_key(&checked), before); + } +} + +#[test] +fn reflected_field_scope_accepts_a_direct_lexical_descendant_of_its_original_loop() { + for release in [false, true] { + let (program, mut checked, parent) = execution(release, "nested_scope"); + let original_loop = loop_at(function(&program, "nested_scope"), 0); + let outer_binding = field_scope(&original_loop.body); + let element_binding = field_scope(&outer_binding.body); + let prepared_loop = checked + .prepare_reflected_field_loop(original_loop) + .unwrap() + .unwrap(); + let parent_key = current_key(&checked); + for index in 0..prepared_loop.fields.len() { + let iteration = prepared_loop.iteration(index).unwrap(); + let outer = checked.prepare_direct_scope(outer_binding).unwrap(); + checked.install_direct_scope(&outer).unwrap(); + let outer_key = current_key(&checked); + assert_ne!(outer_key, parent_key); + let element = checked + .prepare_reflected_field_scope(element_binding, &iteration) + .unwrap(); + assert_eq!(current_key(&checked), outer_key); + assert!(std::ptr::eq(element.body().unwrap(), &element_binding.body)); + assert_eq!( + element.reflection(), + &prepared_loop.fields[index].1.type_info + ); + assert_eq!(element.reference.cursor.scopes.len(), 2); + assert_eq!( + element.reference.cursor.scopes[1].selection, + CheckedComptimeTypeSelection::ReflectedIteration(index) + ); + assert_eq!( + element.reference.cursor.scopes[1].bound_type, + prepared_loop.fields[index].0 + ); + checked.install_direct_scope(&element).unwrap(); + assert_ne!(current_key(&checked), outer_key); + checked.install_function_body(&parent).unwrap(); + assert_eq!(current_key(&checked), parent_key); + } + } +} + +fn assert_runtime_has_no_resource_effects(interpreter: &mut Interpreter) { + // No script is installed by these pure Source tests. Type-level metadata + // does not create a Network grant or permit a runtime Resource operation. + assert_eq!( + interpreter.resource_test_observations(), + Err(ResourceExecutionError::ProviderDisabled.to_string()) + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); +} + +#[test] +fn reflected_field_runtime_uses_retained_metadata_and_retires_each_ordinal_entry_scope() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(SOURCE, release); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + // An ambient table cannot supply the retained owner's field metadata. + interpreter.set_reflection_metadata(Arc::new(ReflectionMetadata::new())); + let baseline = interpreter.resource_test_entry_context().unwrap(); + assert_runtime_has_no_resource_effects(&mut interpreter); + for (name, expected) in [ + ("reflected", Value::Nothing), + ("two_loops", Value::Nothing), + ( + "reflected_labels", + Value::String("int64|int64|app.Count|".to_string()), + ), + ( + "two_loop_labels", + Value::String("int64|int64|app.Count|int64|int64|app.Count|".to_string()), + ), + ( + "reflected_labels", + Value::String("int64|int64|app.Count|".to_string()), + ), + ] { + let definition = checked + .declaration_definition(function(&program, name).name.span) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(definition, Vec::new()) + .unwrap(), + expected, + "{name} release={release}" + ); + // The entry observation includes checked reflected selector depth. + // Equality to the fresh-entry baseline proves it returns to zero. + assert_eq!(interpreter.resource_test_entry_context().unwrap(), baseline); + assert_runtime_has_no_resource_effects(&mut interpreter); + } + } +} + +#[test] +fn reflected_field_callable_frames_select_their_own_loops_and_restore_caller_aliases() { + const EXPECTED: &str = concat!( + "int64>int64|int64|app.Count|>int64|", + "int64>int64|int64|app.Count|>int64|", + "app.Count>int64|int64|app.Count|>app.Count|", + ); + for release in [false, true] { + let program = crate::resource_execution::tests::program(SOURCE, release); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + interpreter.set_reflection_metadata(Arc::new(ReflectionMetadata::new())); + let baseline = interpreter.resource_test_entry_context().unwrap(); + for (name, expected) in [ + ("helper_inside_loop_labels", EXPECTED), + ("helper_inside_loop_labels", EXPECTED), + ("reflected_labels", "int64|int64|app.Count|"), + ] { + let definition = checked + .declaration_definition(function(&program, name).name.span) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(definition, Vec::new()) + .unwrap(), + Value::String(expected.to_string()), + "{name} release={release}" + ); + // The helper uses the same field/Element spellings as its caller. + // Its three labels prove its own ordinals were selected; the outer + // labels before and after the call prove caller alias restoration. + assert_eq!(interpreter.resource_test_entry_context().unwrap(), baseline); + assert_runtime_has_no_resource_effects(&mut interpreter); + } + } +} + +#[test] +fn primitive_enum_and_opaque_resource_field_metadata_are_empty_without_a_provider() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(SOURCE, release); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let definition = checked + .declaration_definition(function(&program, "empty_fields").name.span) + .unwrap(); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + interpreter.set_reflection_metadata(Arc::new(ReflectionMetadata::new())); + let baseline = interpreter.resource_test_entry_context().unwrap(); + for _ in 0..2 { + assert_eq!( + interpreter + .call_checked_program_entry(definition, Vec::new()) + .unwrap(), + Value::Int64(0), + "release={release}" + ); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), baseline); + assert_runtime_has_no_resource_effects(&mut interpreter); + } + } +} + +#[test] +fn closed_pure_reflected_labels_are_captured_by_actual_required_evaluation() { + // Preserve every byte of SOURCE and append one required occurrence after + // its pure callee. This is the real required worker, not a runtime entry. + let source = format!( + "{SOURCE}{}", + r#" +export function required_labels() returns string: + return comptime reflected_labels() +"# + ); + for release in [false, true] { + let program = crate::resource_execution::tests::program(&source, release); + let types = Arc::new(crate::checked_types::CheckedExpressionTypes { + resource_program: Some(program.clone()), + expressions: program + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, program.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let required = crate::evaluate_explicit_comptime_expressions_capture( + program.module(), + Arc::new(ReflectionMetadata::new()), + types, + Arc::new(std::collections::HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "release={release}: {:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + let expected = Value::String("int64|int64|app.Count|".to_string()); + assert_eq!(required.values.len(), 1); + assert_eq!( + required.values.values().cloned().collect::>(), + vec![expected.clone()] + ); + assert!(required.values.checked_values_are_mirrored()); + + // The worker receives no runtime provider, capability argument or + // live Resource value. Its capture API exposes values and debug events, + // not provider observations; do not invent an internal state assertion. + assert!( + required + .values + .checked_resource_hook_values(&program) + .unwrap() + .is_empty() + ); + let proofs = required.values.checked_required_values(&program).unwrap(); + let [proof] = proofs.as_slice() else { + panic!("exactly one authenticated required occurrence"); + }; + let original = function(&program, "required_labels"); + let [Stmt::Return(statement)] = original.body.stmts.as_slice() else { + panic!("one original return containing the explicit comptime call"); + }; + let expression = statement.value.as_ref().unwrap(); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ExplicitComptime).unwrap(); + assert_eq!(proof.value(), &expected); + assert!(proof.belongs_to(&program)); + assert!(proof.is_explicit_comptime()); + assert!(proof.matches_original_comptime(expression)); + assert_eq!( + proof.owner(), + crate::CheckedRequiredOwner::Function { + definition: checked.declaration_definition(original.name.span).unwrap(), + declaration: original.name.span, + } + ); + assert!(proof.checked_hook(&program).unwrap().is_none()); + } +} + +// Original checked Sources exercise signals from selected field bodies. + +mod selected_field_control_flow { + use super::*; + + const CASES: &[(&str, &str, i64)] = &[ + ( + "return_from_second_selected_field", + include_str!("control_flow/01_reflected_return.jett"), + 12, + ), + ( + "break_from_second_selected_field", + include_str!("control_flow/02_reflected_break.jett"), + 18_927, + ), + ( + "continue_from_second_selected_field", + include_str!("control_flow/03_reflected_continue.jett"), + 18_923_897, + ), + ( + "default_then_resume_selected_alias_body", + include_str!("control_flow/04_reflected_default.jett"), + 1_193, + ), + ]; + + fn run_original_source(name: &str, source: &str, expected: i64, release: bool) { + // This helper retains actual checked SourceOrigin and Resource kernel + // declarations through the existing synthetic Support/program path. + let program = crate::resource_execution::tests::program(source, release); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let definition = checked + .declaration_definition(function(&program, "scenario").name.span) + .unwrap(); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + // The checked intrinsic must use retained metadata, not an ambient map. + interpreter.set_reflection_metadata(Arc::new(ReflectionMetadata::new())); + let baseline = interpreter.resource_test_entry_context().unwrap(); + for attempt in 0..2 { + assert_eq!( + interpreter + .call_checked_program_entry(definition, Vec::new()) + .unwrap(), + Value::Int64(expected), + "{name} release={release} attempt={attempt}" + ); + // This observation includes private reflected selector depth. + assert_eq!( + interpreter.resource_test_entry_context().unwrap(), + baseline, + "{name} release={release} attempt={attempt}: entry state changed" + ); + // No provider script or grant is installed for these pure controls. + assert_eq!( + interpreter.resource_test_observations(), + Err(ResourceExecutionError::ProviderDisabled.to_string()), + "{name} release={release} attempt={attempt}: provider became enabled" + ); + assert_eq!( + interpreter.resource_test_custody_counts().unwrap(), + (0, 0), + "{name} release={release} attempt={attempt}: custody remained" + ); + assert!( + interpreter.take_debug_events().is_empty(), + "{name} release={release} attempt={attempt}: unexpected debug events" + ); + } + } + + #[test] + fn reflected_field_return_break_continue_and_default_execute_original_sources() { + for release in [false, true] { + for &(name, source, expected) in CASES { + run_original_source(name, source, expected, release); + } + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/required.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/required.rs new file mode 100644 index 00000000..59c80fc3 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/required.rs @@ -0,0 +1,552 @@ +//! Required workers select original retained nodes and complete typed contexts. +//! Legacy type-name projections are data only; they cannot mint this identity. + +use super::*; +use crate::checked_types::CheckedFunctionTypes; +use crate::value::ClosureScopedTypeBinding; +use std::hash::{Hash, Hasher}; + +pub use value_proof::{ + CheckedRequiredOwner, CheckedRequiredResourceHook, CheckedRequiredScope, CheckedRequiredValue, +}; +#[path = "required/value_proof.rs"] +mod value_proof; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct AttemptNode { + origin: ExecutableOrigin, + path: Vec, + generic: Option, + scopes: Vec<(Span, usize, TypeId, Option, ReflectionTypeInfo)>, +} + +/// Private program-bound key; its program Arc keeps numeric/path identity live. +#[derive(Debug, Clone)] +pub(crate) struct CheckedAttemptKey { + program: Arc, + node: AttemptNode, +} + +impl CheckedAttemptKey { + /// A selected lexical scope retains every concrete ancestor selection. + /// Another program, callable, generic instance or sibling is never an ancestor. + pub(super) fn is_lexical_descendant_of(&self, parent: &Self) -> bool { + Arc::ptr_eq(&self.program, &parent.program) + && self.node.origin == parent.node.origin + && self.node.generic == parent.node.generic + && self.node.path.starts_with(&parent.node.path) + && self.node.scopes.starts_with(&parent.node.scopes) + } +} + +impl PartialEq for CheckedAttemptKey { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.program, &other.program) && self.node == other.node + } +} +impl Eq for CheckedAttemptKey {} +impl Hash for CheckedAttemptKey { + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.program).hash(state); + self.node.hash(state); + } +} + +#[derive(Clone)] +pub(crate) struct PreparedRequiredExpression { + reference: CheckedBodyReference, + purpose: ExecutionPurpose, + key: CheckedAttemptKey, + function: Option>, + scope: Option>, + bindings: Vec, +} + +impl PreparedRequiredExpression { + pub(crate) fn purpose(&self) -> ExecutionPurpose { + self.purpose + } + pub(crate) fn key(&self) -> CheckedAttemptKey { + self.key.clone() + } + pub(crate) fn function_projection(&self) -> Option> { + self.function.clone() + } + pub(crate) fn scope_projection(&self) -> Option> { + self.scope.clone() + } + pub(crate) fn bindings(&self) -> Vec { + self.bindings.clone() + } + pub(crate) fn reference(&self) -> &CheckedBodyReference { + &self.reference + } + pub(crate) fn expression(&self) -> Result<&Expr, ResourceExecutionError> { + match self.reference.region()? { + OriginalRegion::Expression(expression) => Ok(expression), + _ => Err(ResourceExecutionError::MissingCheckedBody), + } + } +} + +impl CheckedExecution { + pub(super) fn attempt_key( + &self, + cursor: &CheckedBodyCursor, + ) -> Result { + self.validate_executable_cursor(cursor)?; + let executable = cursor + .executable + .as_ref() + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + Ok(CheckedAttemptKey { + program: self.program.clone(), + node: AttemptNode { + origin: executable.origin.clone(), + path: executable.path.clone(), + generic: match cursor.root { + BodyRoot::Ordinary => None, + BodyRoot::Generic(index) => Some(index), + }, + scopes: cursor + .scopes + .iter() + .map(|selection| { + ( + selection.owner, + selection.index, + selection.bound_type, + match selection.selection { + CheckedComptimeTypeSelection::Unconditional => None, + CheckedComptimeTypeSelection::ReflectedIteration(index) => { + Some(index) + } + }, + selection.reflection.clone(), + ) + }) + .collect(), + }, + }) + } + + fn required_expression( + &self, + mut cursor: CheckedBodyCursor, + expression: &Expr, + bindings: Vec, + ) -> Result { + self.validate_executable_cursor(&cursor)?; + let reference = CheckedBodyReference { + cursor: cursor.clone(), + }; + let index = expression_index(reference.region()?, expression)?; + cursor + .executable + .as_mut() + .ok_or(ResourceExecutionError::MissingCheckedBody)? + .path + .push(RegionStep::Expression(index)); + let key = self.attempt_key(&cursor)?; + let function = match cursor.root { + BodyRoot::Ordinary => None, + BodyRoot::Generic(index) => Some(function_projection(self, index)?), + }; + let scope = if cursor.scopes.is_empty() { + None + } else { + let mut scopes = &self.program.checked().comptime_type_bindings; + if let BodyRoot::Generic(index) = cursor.root { + scopes = &self.program.checked().generic_function_instantiations[index] + .comptime_type_bindings; + } + let mut selected = None; + for selection in &cursor.scopes { + let candidate = scopes + .get(&selection.owner) + .and_then(|values| values.get(selection.index)) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + selected = Some(scoped_projection( + candidate, + &self.program.checked().interner, + )); + scopes = &candidate.body.comptime_type_bindings; + } + selected + }; + Ok(PreparedRequiredExpression { + reference: CheckedBodyReference { cursor }, + purpose: self.purpose, + key, + function, + scope, + bindings, + }) + } + + pub(crate) fn prepare_namespace_initializer( + &self, + declaration: &jett_parser::ast::VarDecl, + ) -> Result { + if self.purpose != ExecutionPurpose::NamespaceConstant { + return Err(ResourceExecutionError::WrongPurpose); + } + let items = self + .program + .module() + .items + .iter() + .enumerate() + .filter_map(|(index, item)| match item { + Item::VarDecl(original) if std::ptr::eq(original, declaration) => Some(index), + _ => None, + }) + .collect::>(); + let [item] = items.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + if !self + .program + .checked() + .type_map + .contains_key(&declaration.name.span) + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let cursor = CheckedBodyCursor { + root: BodyRoot::Ordinary, + scopes: Vec::new(), + executable: Some(ExecutableIdentity { + program: self.program.clone(), + origin: ExecutableOrigin::NamespaceInitializer { item: *item }, + path: Vec::new(), + }), + }; + let expression = match &declaration.value { + Expr::Comptime(value, _) => value.as_ref(), + value => value, + }; + self.required_expression(cursor, expression, Vec::new()) + } + + pub(crate) fn prepare_verify_body( + &self, + original: &jett_parser::ast::VerifyBlock, + ) -> Result { + if self.purpose != ExecutionPurpose::Verify { + return Err(ResourceExecutionError::WrongPurpose); + } + let items = self + .program + .module() + .items + .iter() + .enumerate() + .filter_map(|(index, item)| match item { + Item::Verify(value) if std::ptr::eq(value, original) => Some(index), + _ => None, + }) + .collect::>(); + let [item] = items.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + self.required_body(ExecutableOrigin::Verify { item: *item }) + } + + pub(crate) fn prepare_property_body( + &self, + original: &jett_parser::ast::PropertyBlock, + ) -> Result { + if self.purpose != ExecutionPurpose::Property { + return Err(ResourceExecutionError::WrongPurpose); + } + let items = self + .program + .module() + .items + .iter() + .enumerate() + .filter_map(|(index, item)| match item { + Item::Property(value) if std::ptr::eq(value, original) => Some(index), + _ => None, + }) + .collect::>(); + let [item] = items.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + self.required_body(ExecutableOrigin::Property { item: *item }) + } + + fn required_body( + &self, + origin: ExecutableOrigin, + ) -> Result { + let reference = CheckedBodyReference { + cursor: CheckedBodyCursor { + root: BodyRoot::Ordinary, + scopes: Vec::new(), + executable: Some(ExecutableIdentity { + program: self.program.clone(), + origin, + path: Vec::new(), + }), + }, + }; + self.validate_executable_cursor(&reference.cursor)?; + Ok(reference) + } + + pub(crate) fn prepare_explicit_contexts( + &self, + expression: &Expr, + occurrence: Span, + owner: Option, + bindings: &[(Span, String)], + ) -> Result, ResourceExecutionError> { + if self.purpose != ExecutionPurpose::ExplicitComptime { + return Err(ResourceExecutionError::WrongPurpose); + } + let mut bases = Vec::new(); + let mut source_is_original = false; + for (item, original) in self.program.module().items.iter().enumerate() { + let origin = match original { + Item::Function(function) if owner == Some(function.name.span) => { + let definition = self.declaration_definition(function.name.span)?; + ExecutableOrigin::Function { definition, item } + } + Item::VarDecl(_) if owner.is_none() => { + ExecutableOrigin::NamespaceInitializer { item } + } + Item::Verify(_) if owner.is_none() => ExecutableOrigin::Verify { item }, + Item::Property(_) if owner.is_none() => ExecutableOrigin::Property { item }, + _ => continue, + }; + // Authenticate syntax independently of concrete execution eligibility. + // This structural cursor is never installed or used to obtain facts. + let structural = CheckedBodyReference { + cursor: CheckedBodyCursor { + root: BodyRoot::Ordinary, + scopes: Vec::new(), + executable: Some(ExecutableIdentity { + program: self.program.clone(), + origin, + path: Vec::new(), + }), + }, + }; + let mut region = structural.region()?; + let mut owns_path = true; + for (binding_owner, name) in bindings { + let Ok(binding) = region_binding(region, *binding_owner) else { + owns_path = false; + break; + }; + if &binding.name.name != name { + return Err(ResourceExecutionError::MissingCheckedBody); + } + region = OriginalRegion::Block(&binding.body); + } + if !owns_path { + continue; + } + let mut owns_expression = false; + walk_region( + region, + &mut |_| {}, + &mut |node| { + if let Expr::Comptime(inner, span) = node { + if *span == occurrence && std::ptr::eq(inner.as_ref(), expression) { + owns_expression = true; + } + } + }, + &mut |_| {}, + ); + if !owns_expression { + continue; + } + source_is_original = true; + let roots = match (origin, original) { + (ExecutableOrigin::Function { definition, .. }, Item::Function(function)) + if !function.type_params.is_empty() => + { + self.program + .checked() + .generic_function_instantiations + .iter() + .enumerate() + .filter(|(_, body)| body.definition == definition) + .map(|(index, _)| BodyRoot::Generic(index)) + .collect::>() + } + _ => vec![BodyRoot::Ordinary], + }; + for root in roots { + bases.push(( + CheckedBodyCursor { + root, + scopes: Vec::new(), + executable: Some(ExecutableIdentity { + program: self.program.clone(), + origin: origin.clone(), + path: Vec::new(), + }), + }, + Vec::new(), + )); + } + } + for (owner, name) in bindings { + let mut expanded = Vec::new(); + for (cursor, prior_bindings) in bases { + let reference = CheckedBodyReference { + cursor: cursor.clone(), + }; + // A foreign owner simply is not this retained lexical region. + let Ok(binding) = region_binding(reference.region()?, *owner) else { + continue; + }; + if name != &binding.name.name { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let Some(candidates) = self.facts(&cursor)?.scopes.get(owner) else { + continue; + }; + for (index, candidate) in candidates.iter().enumerate() { + let mut next = cursor.clone(); + next.scopes.push(ScopedSelection { + owner: *owner, + index, + bound_type: candidate.bound_type, + selection: candidate.selection, + reflection: candidate.reflection.clone(), + }); + next.executable + .as_mut() + .ok_or(ResourceExecutionError::MissingCheckedBody)? + .path + .push(RegionStep::Scope(binding.span)); + self.validate_executable_cursor(&next)?; + let mut projected = prior_bindings.clone(); + projected.push(ClosureScopedTypeBinding { + name: name.clone(), + canonical_name: self + .program + .checked() + .interner + .type_name(candidate.bound_type), + reflection: Some(candidate.reflection.clone()), + }); + expanded.push((next, projected)); + } + } + bases = expanded; + } + let mut prepared = Vec::new(); + for (cursor, bindings) in bases { + let reference = CheckedBodyReference { + cursor: cursor.clone(), + }; + let mut original = false; + walk_region( + reference.region()?, + &mut |_| {}, + &mut |node| { + if let Expr::Comptime(inner, span) = node { + if *span == occurrence && std::ptr::eq(inner.as_ref(), expression) { + original = true; + } + } + }, + &mut |_| {}, + ); + if original && self.facts(&cursor)?.types.contains_key(&occurrence) { + prepared.push(self.required_expression(cursor, expression, bindings)?); + } + } + if !source_is_original { + return Err(ResourceExecutionError::MissingCheckedBody); + } + // A retained original expression with no accepted concrete/scoped type + // occurrence has no evaluation attempt, as on the ordinary route. + // Eligible contexts still require exact packets when they execute. + Ok(prepared) + } + + pub(crate) fn original_assignment_key( + &self, + assignment: &AssignStmt, + ) -> Result { + self.original_assignment(assignment)?; + self.attempt_key(&self.body) + } + + pub(crate) fn original_comptime_key( + &self, + expression: &Expr, + ) -> Result { + let Expr::Comptime(inner, span) = expression else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + self.validate_executable_cursor(&self.body)?; + let reference = CheckedBodyReference { + cursor: self.body.clone(), + }; + expression_index(reference.region()?, expression)?; + if !self.facts(&self.body)?.types.contains_key(span) { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(self + .required_expression(self.body.clone(), inner, Vec::new())? + .key) + } +} + +fn function_projection( + checked: &CheckedExecution, + index: usize, +) -> Result, ResourceExecutionError> { + let body = checked + .program + .checked() + .generic_function_instantiations + .get(index) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let types = &checked.program.checked().interner; + let mut expressions = body + .type_map + .iter() + .map(|(span, ty)| (*span, types.type_name(*ty))) + .collect(); + remove_descendant_expression_names(&mut expressions, &body.comptime_type_bindings); + Ok(Arc::new(CheckedFunctionTypes { + bindings: body + .comptime_type_bindings + .iter() + .map(|(span, candidates)| { + ( + *span, + candidates + .iter() + .map(|candidate| scoped_projection(candidate, types)) + .collect(), + ) + }) + .collect(), + type_arguments: body + .concrete_args + .iter() + .map(|ty| types.type_name(*ty)) + .collect(), + type_argument_reflections: body.specialization.type_argument_reflections.clone(), + type_info_kinds: body.specialization.type_info_kinds.clone(), + type_info_primitives: body.specialization.type_info_primitives.clone(), + type_kind_values: body.specialization.type_kind_values.clone(), + type_primitive_values: body.specialization.type_primitive_values.clone(), + expressions: Arc::new(expressions), + })) +} + +#[cfg(test)] +#[path = "required/tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/required/tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/required/tests.rs new file mode 100644 index 00000000..0a331feb --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/required/tests.rs @@ -0,0 +1,238 @@ +use super::*; +use crate::checked_types::CheckedExpressionTypes; +use crate::resource_execution::tests::program as checked_program; +use crate::{Interpreter, Value}; + +const SOURCE: &str = include_str!("../../../fixtures/26_original_required_regions.jett"); + +fn types(program: &Arc) -> Arc { + Arc::new(CheckedExpressionTypes { + resource_program: Some(program.clone()), + expressions: program + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, program.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }) +} + +#[test] +fn resource_original_required_regions_preserve_existing_descriptor_absence_and_primitive_workers() { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let types = types(&program); + let required = crate::evaluate_explicit_comptime_expressions_capture( + program.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + types.clone(), + Arc::new(HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "{:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + assert!(required.values.checked_values_are_mirrored()); + assert_eq!(required.values.len(), required.values.values().count()); + let blocks = crate::verify::run_verify_blocks_detailed_with_checked_values( + program.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + types.clone(), + Arc::new(HashMap::new()), + Arc::new(required.values.clone()), + ); + assert_eq!(blocks.len(), 2); + assert!( + blocks + .iter() + .all(|block| block.passed && block.debug_events.is_empty()), + "{blocks:?}" + ); + let original = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "scenario" => Some(function), + _ => None, + }) + .unwrap(); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let target = checked.declaration_definition(original.name.span).unwrap(); + // Match the driver: baked constants are installed before module registration. + let mut interpreter = Interpreter::new(); + interpreter.set_checked_expression_types(types); + interpreter.set_explicit_comptime_values(Arc::new(required.values)); + interpreter.register_module(program.module()); + interpreter + .install_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(target, Vec::new()) + .unwrap(), + Value::Int64(7) + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); + } +} + +#[test] +fn resource_required_reference_refuses_cloned_suite_initializer_and_wrong_purpose_without_installing() + { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::NamespaceConstant).unwrap(); + let declaration = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::VarDecl(value) => Some(value), + _ => None, + }) + .unwrap(); + let prepared = checked.prepare_namespace_initializer(declaration).unwrap(); + assert_eq!(prepared.purpose(), ExecutionPurpose::NamespaceConstant); + assert!(matches!( + checked.prepare_namespace_initializer(&declaration.clone()), + Err(ResourceExecutionError::MissingCheckedBody) + )); + checked.replace_purpose(ExecutionPurpose::Verify); + assert!(matches!( + checked.prepare_namespace_initializer(declaration), + Err(ResourceExecutionError::WrongPurpose) + )); + let verify = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Verify(value) => Some(value), + _ => None, + }) + .unwrap(); + let reference = checked.prepare_verify_body(verify).unwrap(); + assert!(std::ptr::eq(reference.block().unwrap(), &verify.body)); + assert!(matches!( + checked.prepare_verify_body(&verify.clone()), + Err(ResourceExecutionError::MissingCheckedBody) + )); + let foreign = + CheckedExecution::new(checked_program(SOURCE, release), ExecutionPurpose::Verify) + .unwrap(); + assert_eq!( + foreign.validate_executable_cursor(&reference.cursor), + Err(ResourceExecutionError::ForeignProgram) + ); + assert!(matches!( + foreign.attempt_key(&reference.cursor), + Err(ResourceExecutionError::ForeignProgram) + )); + } +} + +#[test] +fn resource_required_cache_key_keeps_exact_program_original_node_and_scope_identity() { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::NamespaceConstant).unwrap(); + let declaration = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::VarDecl(value) => Some(value), + _ => None, + }) + .unwrap(); + let original = checked.prepare_namespace_initializer(declaration).unwrap(); + let foreign_program = checked_program(SOURCE, release); + let foreign = + CheckedExecution::new(foreign_program.clone(), ExecutionPurpose::NamespaceConstant) + .unwrap(); + let foreign_declaration = foreign_program + .module() + .items + .iter() + .find_map(|item| match item { + Item::VarDecl(value) => Some(value), + _ => None, + }) + .unwrap(); + let other = foreign + .prepare_namespace_initializer(foreign_declaration) + .unwrap(); + assert_ne!(original.key(), other.key()); + assert_eq!( + original.key(), + checked + .prepare_namespace_initializer(declaration) + .unwrap() + .key() + ); + let clone = original.expression().unwrap().clone(); + assert!(matches!( + checked.required_expression(original.reference.cursor.clone(), &clone, Vec::new()), + Err(ResourceExecutionError::MissingCheckedBody) + )); + } +} + +#[test] +fn resource_required_generic_and_nested_scopes_use_exact_selected_cache_contexts() { + for release in [false, true] { + let program = checked_program( + include_str!("../../../fixtures/27_original_generic_scoped_required.jett"), + release, + ); + let required = crate::evaluate_explicit_comptime_expressions_capture( + program.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + types(&program), + Arc::new(HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "{:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + assert!(required.values.checked_values_are_mirrored()); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + interpreter.set_explicit_comptime_values(Arc::new(required.values)); + for name in ["generic_label", "scoped_label"] { + let function = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == name => Some(function), + _ => None, + }) + .unwrap(); + let target = checked.declaration_definition(function.name.span).unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(target, Vec::new()) + .unwrap(), + Value::String("int64".to_string()) + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/required/value_proof.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/required/value_proof.rs new file mode 100644 index 00000000..1cd53879 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/required/value_proof.rs @@ -0,0 +1,433 @@ +//! Readonly proof of one successful required value in the private checked cache. +//! Source names, spans and mirrored values cannot construct this association. + +use super::*; +use crate::Value; +use crate::explicit::ExplicitComptimeValues; +use jett_typecheck::CheckedGenericSpecialization; + +/// The retained declaration that owns a successful required occurrence. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CheckedRequiredOwner { + Function { + definition: DefId, + declaration: Span, + }, + Verify { + declaration: Span, + }, + Property { + declaration: Span, + }, + NamespaceConstant { + declaration: Span, + }, +} + +/// One exact selected scoped type context, in outer-to-inner source order. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CheckedRequiredScope { + name: String, + owner: Span, + index: usize, + bound_type: TypeId, + selection: CheckedComptimeTypeSelection, + reflection: ReflectionTypeInfo, +} + +impl CheckedRequiredScope { + pub fn name(&self) -> &str { + &self.name + } + pub fn owner(&self) -> Span { + self.owner + } + pub fn index(&self) -> usize { + self.index + } + pub fn bound_type(&self) -> TypeId { + self.bound_type + } + pub fn selection(&self) -> CheckedComptimeTypeSelection { + self.selection + } + pub fn reflection(&self) -> &ReflectionTypeInfo { + &self.reflection + } +} + +#[derive(Debug, Clone)] +struct RequiredValueSite { + source_span: Span, + owner: CheckedRequiredOwner, + type_arguments: Vec, + specialization: CheckedGenericSpecialization, + scopes: Vec, + explicit: bool, +} + +/// Opaque association between a retained required occurrence, complete checked +/// context, and its successful provider-free value. There is no public mint. +#[derive(Debug, Clone)] +pub struct CheckedRequiredValue { + key: CheckedAttemptKey, + value: Value, + site: RequiredValueSite, +} + +impl CheckedRequiredValue { + pub(crate) fn from_cache( + cache: &ExplicitComptimeValues, + key: &CheckedAttemptKey, + program: &Arc, + ) -> Result { + if !Arc::ptr_eq(&key.program, program) { + return Err(ResourceExecutionError::ForeignProgram.to_string()); + } + let value = cache + .checked_get(key) + .ok_or_else(|| "required value has no successful checked cache entry".to_string())?; + if value.contains_live_resource_or_grant() { + return Err( + "required value contains runtime authority or Resource custody".to_string(), + ); + } + let site = key.value_site().map_err(|error| error.to_string())?; + let proof = Self { + key: key.clone(), + value: value.clone(), + site, + }; + // Hook metadata must belong to the same retained program as the worker + // occurrence. Its nominal identity and exact signature stay checked. + proof.checked_hook(program)?; + Ok(proof) + } + + pub fn value(&self) -> &Value { + &self.value + } + pub fn belongs_to(&self, program: &Arc) -> bool { + Arc::ptr_eq(&self.key.program, program) + } + pub fn source_span(&self) -> Span { + self.site.source_span + } + pub fn owner(&self) -> CheckedRequiredOwner { + self.site.owner + } + pub fn generic_index(&self) -> Option { + self.key.node.generic + } + pub fn type_arguments(&self) -> &[TypeId] { + &self.site.type_arguments + } + pub fn specialization(&self) -> &CheckedGenericSpecialization { + &self.site.specialization + } + pub fn scoped_bindings(&self) -> &[CheckedRequiredScope] { + &self.site.scopes + } + pub fn is_explicit_comptime(&self) -> bool { + self.site.explicit + } + + /// Return the original outer `comptime` node retained by this proof. A raw + /// namespace initializer has a required value but no such outer node. + pub fn original_comptime(&self) -> Result<&Expr, String> { + self.key + .original_comptime() + .map_err(|error| error.to_string())? + .ok_or_else(|| { + "required namespace initializer has no explicit comptime node".to_string() + }) + } + + pub fn matches_original_comptime(&self, expression: &Expr) -> bool { + self.original_comptime() + .is_ok_and(|original| std::ptr::eq(original, expression)) + } + + /// Project only an opaque hook descriptor bound to this exact checked + /// program. Reading the metadata does not invoke a runtime hook. + pub fn checked_hook<'a>( + &'a self, + program: &Arc, + ) -> Result, String> { + if !self.belongs_to(program) { + return Err(ResourceExecutionError::ForeignProgram.to_string()); + } + let Value::ResourceHook(descriptor) = self.value.payload() else { + return Ok(None); + }; + if !Arc::ptr_eq(&descriptor.program, &self.key.program) { + return Err(ResourceExecutionError::ForeignProgram.to_string()); + } + descriptor + .program + .checked() + .resource_hooks + .get(&descriptor.definition) + .map(Some) + .ok_or_else(|| ResourceExecutionError::InvalidInvocation.to_string()) + } +} + +/// Hook projection of a successful checked required value. It remains metadata, +/// separate from a physical Resource owner or a runtime capability. +#[derive(Debug, Clone)] +pub struct CheckedRequiredResourceHook { + required: CheckedRequiredValue, +} + +impl CheckedRequiredResourceHook { + pub(crate) fn from_value(required: CheckedRequiredValue) -> Option { + matches!(required.value.payload(), Value::ResourceHook(_)).then_some(Self { required }) + } + pub fn value(&self) -> &Value { + self.required.value() + } + pub fn belongs_to(&self, program: &Arc) -> bool { + self.required.belongs_to(program) + } + pub fn source_span(&self) -> Span { + self.required.source_span() + } + pub fn owner(&self) -> CheckedRequiredOwner { + self.required.owner() + } + pub fn generic_index(&self) -> Option { + self.required.generic_index() + } + pub fn type_arguments(&self) -> &[TypeId] { + self.required.type_arguments() + } + pub fn specialization(&self) -> &CheckedGenericSpecialization { + self.required.specialization() + } + pub fn scoped_bindings(&self) -> &[CheckedRequiredScope] { + self.required.scoped_bindings() + } + pub fn is_explicit_comptime(&self) -> bool { + self.required.is_explicit_comptime() + } + pub fn original_comptime(&self) -> Result<&Expr, String> { + self.required.original_comptime() + } + pub fn matches_original_comptime(&self, expression: &Expr) -> bool { + self.required.matches_original_comptime(expression) + } + pub fn checked_hook<'a>( + &'a self, + program: &Arc, + ) -> Result<&'a CheckedResourceHook, String> { + self.required + .checked_hook(program)? + .ok_or_else(|| "required value is not a checked Resource hook descriptor".to_string()) + } +} + +impl CheckedAttemptKey { + fn cursor(&self) -> CheckedBodyCursor { + CheckedBodyCursor { + root: match self.node.generic { + Some(index) => BodyRoot::Generic(index), + None => BodyRoot::Ordinary, + }, + scopes: self + .node + .scopes + .iter() + .map( + |(owner, index, bound_type, selection, reflection)| ScopedSelection { + owner: *owner, + index: *index, + bound_type: *bound_type, + selection: match selection { + Some(index) => CheckedComptimeTypeSelection::ReflectedIteration(*index), + None => CheckedComptimeTypeSelection::Unconditional, + }, + reflection: reflection.clone(), + }, + ) + .collect(), + executable: Some(ExecutableIdentity { + program: self.program.clone(), + origin: self.node.origin, + path: self.node.path.clone(), + }), + } + } + + fn region(&self, path: &[RegionStep]) -> Result, ResourceExecutionError> { + let mut region = match self.node.origin { + ExecutableOrigin::Function { item, .. } => { + match self.program.module().items.get(item) { + Some(Item::Function(value)) => OriginalRegion::Block(&value.body), + _ => return Err(ResourceExecutionError::MissingCheckedBody), + } + } + ExecutableOrigin::NamespaceInitializer { item } => { + match self.program.module().items.get(item) { + Some(Item::VarDecl(value)) => OriginalRegion::Expression(&value.value), + _ => return Err(ResourceExecutionError::MissingCheckedBody), + } + } + ExecutableOrigin::Verify { item } => match self.program.module().items.get(item) { + Some(Item::Verify(value)) => OriginalRegion::Block(&value.body), + _ => return Err(ResourceExecutionError::MissingCheckedBody), + }, + ExecutableOrigin::Property { item } => match self.program.module().items.get(item) { + Some(Item::Property(value)) => OriginalRegion::Block(&value.body), + _ => return Err(ResourceExecutionError::MissingCheckedBody), + }, + }; + for step in path { + region = match step { + RegionStep::Scope(owner) => { + OriginalRegion::Block(®ion_binding(region, *owner)?.body) + } + RegionStep::Expression(index) => { + OriginalRegion::Expression(region_expression(region, *index)?) + } + }; + } + Ok(region) + } + + fn original_comptime(&self) -> Result, ResourceExecutionError> { + let Some((RegionStep::Expression(index), parent)) = self.node.path.split_last() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let region = self.region(parent)?; + let selected = region_expression(region, *index)?; + let mut original = Vec::new(); + walk_region( + region, + &mut |_| {}, + &mut |expression| { + if let Expr::Comptime(inner, _) = expression { + if std::ptr::eq(inner.as_ref(), selected) { + original.push(expression); + } + } + }, + &mut |_| {}, + ); + match original.as_slice() { + [original] => Ok(Some(*original)), + [] if matches!( + self.node.origin, + ExecutableOrigin::NamespaceInitializer { .. } + ) => + { + Ok(None) + } + _ => Err(ResourceExecutionError::MissingCheckedBody), + } + } + + fn value_site(&self) -> Result { + let checked = + CheckedExecution::new(self.program.clone(), ExecutionPurpose::ExplicitComptime)?; + checked.validate_executable_cursor(&self.cursor())?; + let owner = match self.node.origin { + ExecutableOrigin::Function { definition, item } => { + match self.program.module().items.get(item) { + Some(Item::Function(value)) => CheckedRequiredOwner::Function { + definition, + declaration: value.name.span, + }, + _ => return Err(ResourceExecutionError::MissingCheckedBody), + } + } + ExecutableOrigin::NamespaceInitializer { item } => { + match self.program.module().items.get(item) { + Some(Item::VarDecl(value)) => CheckedRequiredOwner::NamespaceConstant { + declaration: value.name.span, + }, + _ => return Err(ResourceExecutionError::MissingCheckedBody), + } + } + ExecutableOrigin::Verify { item } => match self.program.module().items.get(item) { + Some(Item::Verify(value)) => CheckedRequiredOwner::Verify { + declaration: value.name.span, + }, + _ => return Err(ResourceExecutionError::MissingCheckedBody), + }, + ExecutableOrigin::Property { item } => match self.program.module().items.get(item) { + Some(Item::Property(value)) => CheckedRequiredOwner::Property { + declaration: value.name.span, + }, + _ => return Err(ResourceExecutionError::MissingCheckedBody), + }, + }; + let (type_arguments, specialization) = match self.node.generic { + Some(index) => { + let instance = self + .program + .checked() + .generic_function_instantiations + .get(index) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + ( + instance.concrete_args.clone(), + instance.specialization.clone(), + ) + } + None => (Vec::new(), CheckedGenericSpecialization::default()), + }; + let mut region = self.region(&[])?; + let mut selections = self.node.scopes.iter(); + let mut scopes = Vec::new(); + for step in &self.node.path { + region = match step { + RegionStep::Scope(owner) => { + let binding = region_binding(region, *owner)?; + let Some((selected_owner, index, bound_type, selection, reflection)) = + selections.next() + else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + if owner != selected_owner { + return Err(ResourceExecutionError::MissingCheckedBody); + } + scopes.push(CheckedRequiredScope { + name: binding.name.name.clone(), + owner: *owner, + index: *index, + bound_type: *bound_type, + selection: match selection { + Some(index) => CheckedComptimeTypeSelection::ReflectedIteration(*index), + None => CheckedComptimeTypeSelection::Unconditional, + }, + reflection: reflection.clone(), + }); + OriginalRegion::Block(&binding.body) + } + RegionStep::Expression(index) => { + OriginalRegion::Expression(region_expression(region, *index)?) + } + }; + } + if selections.next().is_some() { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let OriginalRegion::Expression(expression) = region else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let original = self.original_comptime()?; + Ok(RequiredValueSite { + source_span: original.map_or_else(|| expression.span(), Expr::span), + owner, + type_arguments, + specialization, + scopes, + explicit: original.is_some(), + }) + } +} + +#[cfg(test)] +#[path = "value_proof/tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/required/value_proof/tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/required/value_proof/tests.rs new file mode 100644 index 00000000..048285b0 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/required/value_proof/tests.rs @@ -0,0 +1,575 @@ +use super::*; +use crate::checked_types::CheckedExpressionTypes; +use crate::explicit::ComptimeContext; +use crate::resource_execution::tests::program as checked_program; +use jett_common::{FileId, SourceOrigin}; +use jett_diagnostics::Severity; +use jett_parser::parse; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::CheckOptions; +use jett_types::{ResourceHookKind, ResourceKernelRecipe}; + +const SOURCE: &str = include_str!("../../../../fixtures/26_original_required_regions.jett"); +const GENERIC_SCOPED: &str = + include_str!("../../../../fixtures/27_original_generic_scoped_required.jett"); +const SHARED_SUPPORT: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/resource_probe.jett" +)); + +fn required(program: &Arc) -> crate::ExplicitComptimeEvaluation { + crate::evaluate_explicit_comptime_expressions_capture( + program.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + Arc::new(CheckedExpressionTypes { + resource_program: Some(program.clone()), + expressions: program + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, program.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }), + Arc::new(HashMap::new()), + ) +} + +fn successful(program: &Arc) -> ExplicitComptimeValues { + let evaluated = required(program); + assert!( + evaluated.diagnostics.is_empty(), + "{:?}", + evaluated.diagnostics + ); + assert!(evaluated.debug_events.is_empty()); + evaluated.values +} + +fn shared_program(source: &str, release: bool) -> Arc { + let stdlib = FileId::new(10_000); + let project = FileId::new(0); + let mut parsed = parse(SHARED_SUPPORT, stdlib); + let application = parse(source, project); + assert!( + !parsed + .errors + .iter() + .chain(&application.errors) + .any(|diagnostic| diagnostic.severity == Severity::Error) + ); + let resource = parsed + .module + .items + .iter() + .find_map(|item| match item { + Item::Resource(value) => Some(value.name.span), + _ => None, + }) + .unwrap(); + parsed.module.items.extend(application.module.items); + parsed.errors.extend(application.errors); + let kernels = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: resource, + member: member.to_string(), + recipe, + }) + .collect::>(); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (stdlib, SourceOrigin::Stdlib), + (project, SourceOrigin::Project), + ]), + &kernels, + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("{error:?}; {:?}", error.diagnostics())), + ) +} + +#[test] +fn resource_required_value_proof_keeps_outer_node_owner_primitive_absence_and_hook() { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let cache = successful(&program); + let values = cache.checked_required_values(&program).unwrap(); + assert_eq!(values.len(), 5); + for value in &values { + assert!(value.belongs_to(&program)); + assert!(value.is_explicit_comptime()); + let original = value.original_comptime().unwrap(); + assert!(matches!(original, Expr::Comptime(_, _))); + assert_eq!(value.source_span(), original.span()); + assert!(value.matches_original_comptime(original)); + assert!(!value.matches_original_comptime(&original.clone())); + assert_eq!(value.generic_index(), None); + assert!(value.type_arguments().is_empty()); + assert_eq!( + value.specialization(), + &CheckedGenericSpecialization::default() + ); + assert!(value.scoped_bindings().is_empty()); + } + assert_eq!( + values + .iter() + .filter(|value| value.value() == &Value::Int64(7)) + .count(), + 3 + ); + assert_eq!( + values + .iter() + .filter(|value| value.value() == &Value::OptionalNone) + .count(), + 1 + ); + assert_eq!( + values + .iter() + .filter(|value| matches!( + value.owner(), + CheckedRequiredOwner::NamespaceConstant { .. } + )) + .count(), + 1 + ); + assert_eq!( + values + .iter() + .filter(|value| matches!(value.owner(), CheckedRequiredOwner::Verify { .. })) + .count(), + 1 + ); + assert_eq!( + values + .iter() + .filter(|value| matches!(value.owner(), CheckedRequiredOwner::Property { .. })) + .count(), + 1 + ); + let hooks = cache.checked_resource_hook_values(&program).unwrap(); + assert_eq!(hooks.len(), 1); + let hook = &hooks[0]; + assert_eq!( + hook.checked_hook(&program).unwrap().kind, + ResourceHookKind::Close + ); + assert!(matches!(hook.value().payload(), Value::ResourceHook(_))); + let function = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(value) if value.name.name == "scenario" => Some(value), + _ => None, + }) + .unwrap(); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ExplicitComptime).unwrap(); + assert_eq!( + hook.owner(), + CheckedRequiredOwner::Function { + definition: checked.declaration_definition(function.name.span).unwrap(), + declaration: function.name.span, + } + ); + assert!(hook.matches_original_comptime(hook.original_comptime().unwrap())); + assert!(!hook.matches_original_comptime(&hook.original_comptime().unwrap().clone())); + } +} + +#[test] +fn resource_required_value_proof_distinguishes_raw_namespace_initializer() { + for release in [false, true] { + let source = SOURCE.replace("int64 baked = comptime seven()", "int64 baked = 7"); + let program = checked_program(&source, release); + let cache = successful(&program); + let values = cache.checked_required_values(&program).unwrap(); + let constant = values + .iter() + .find(|value| { + matches!( + value.owner(), + CheckedRequiredOwner::NamespaceConstant { .. } + ) + }) + .unwrap(); + let declaration = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::VarDecl(value) => Some(value), + _ => None, + }) + .unwrap(); + assert_eq!(constant.value(), &Value::Int64(7)); + assert_eq!( + constant.owner(), + CheckedRequiredOwner::NamespaceConstant { + declaration: declaration.name.span + } + ); + assert_eq!(constant.source_span(), declaration.value.span()); + assert!(!constant.is_explicit_comptime()); + assert!(constant.original_comptime().is_err()); + assert!(!constant.matches_original_comptime(&declaration.value)); + assert!(constant.checked_hook(&program).unwrap().is_none()); + assert!( + values + .iter() + .filter(|value| value.is_explicit_comptime()) + .all(|value| value.original_comptime().is_ok()) + ); + } +} + +#[test] +fn resource_required_value_proof_refuses_foreign_program_and_public_mirror_replacement() { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let foreign = checked_program(SOURCE, release); + let mut cache = successful(&program); + let hooks = cache.checked_resource_hook_values(&program).unwrap(); + let hook = &hooks[0]; + let checked_hook = hook.checked_hook(&program).unwrap(); + // Same source produces the same numeric definition and signature, but + // those observations cannot authenticate an independent program Arc. + assert_eq!( + foreign + .checked() + .resource_hooks + .get(&checked_hook.definition), + Some(checked_hook) + ); + assert!(!hook.belongs_to(&foreign)); + assert!(hook.checked_hook(&foreign).is_err()); + assert!(cache.checked_required_values(&foreign).is_err()); + assert!(cache.checked_resource_hook_values(&foreign).is_err()); + let foreign_cache = successful(&foreign); + let foreign_hook = foreign_cache + .checked_resource_hook_values(&foreign) + .unwrap() + .remove(0); + assert_eq!(foreign_hook.source_span(), hook.source_span()); + assert!(!hook.matches_original_comptime(foreign_hook.original_comptime().unwrap())); + assert!(foreign_hook.checked_hook(&program).is_err()); + cache.insert( + hook.source_span(), + ComptimeContext::default(), + Value::Int64(99), + ); + assert_eq!( + cache.get(hook.source_span(), &ComptimeContext::default()), + Some(&Value::Int64(99)) + ); + let retained = cache + .checked_resource_hook_values(&program) + .unwrap() + .remove(0); + assert_eq!(retained.checked_hook(&program).unwrap(), checked_hook); + assert!(matches!(retained.value().payload(), Value::ResourceHook(_))); + let mut mirror = ExplicitComptimeValues::default(); + mirror.insert( + hook.source_span(), + ComptimeContext::default(), + hook.value().clone(), + ); + assert_eq!(mirror.len(), 1); + assert!(mirror.checked_required_values(&program).unwrap().is_empty()); + assert!( + mirror + .checked_resource_hook_values(&program) + .unwrap() + .is_empty() + ); + } +} + +#[test] +fn resource_required_value_proof_preserves_complete_generic_and_nested_scoped_context() { + for release in [false, true] { + let program = checked_program(GENERIC_SCOPED, release); + let cache = successful(&program); + let values = cache.checked_required_values(&program).unwrap(); + assert_eq!(values.len(), 2); + let generic = values + .iter() + .find(|value| value.generic_index().is_some()) + .unwrap(); + let instance = + &program.checked().generic_function_instantiations[generic.generic_index().unwrap()]; + assert_eq!(generic.type_arguments(), instance.concrete_args); + assert_eq!(generic.specialization(), &instance.specialization); + assert!( + matches!(generic.owner(), CheckedRequiredOwner::Function { definition, .. } if definition == instance.definition) + ); + assert_eq!(generic.value(), &Value::String("int64".to_string())); + let mut changed_generic = generic.key.clone(); + changed_generic.node.generic = None; + assert!(CheckedRequiredValue::from_cache(&cache, &changed_generic, &program).is_err()); + assert!(changed_generic.value_site().is_err()); + let scoped = values + .iter() + .find(|value| value.scoped_bindings().len() == 2) + .unwrap(); + assert_eq!( + scoped + .scoped_bindings() + .iter() + .map(CheckedRequiredScope::name) + .collect::>(), + ["Outer", "Element"] + ); + let mut bindings = &program.checked().comptime_type_bindings; + for scope in scoped.scoped_bindings() { + let selected = &bindings[&scope.owner()][scope.index()]; + assert_eq!(scope.bound_type(), selected.bound_type); + assert_eq!(scope.selection(), selected.selection); + assert_eq!(scope.reflection(), &selected.reflection); + bindings = &selected.body.comptime_type_bindings; + } + assert_eq!(scoped.value(), &Value::String("int64".to_string())); + let mut changed_scope = scoped.key.clone(); + changed_scope.node.scopes[1].3 = Some(0); + assert!(CheckedRequiredValue::from_cache(&cache, &changed_scope, &program).is_err()); + assert!(changed_scope.value_site().is_err()); + let mut changed_owner = scoped.key.clone(); + changed_owner.node.origin = generic.key.node.origin; + assert!(CheckedRequiredValue::from_cache(&cache, &changed_owner, &program).is_err()); + assert!(changed_owner.value_site().is_err()); + let mut changed_occurrence = scoped.key.clone(); + let Some(RegionStep::Expression(index)) = changed_occurrence.node.path.last_mut() else { + panic!("required expression path"); + }; + *index += 1; + assert!(CheckedRequiredValue::from_cache(&cache, &changed_occurrence, &program).is_err()); + } +} + +#[test] +fn resource_required_value_proof_covers_actual_all_recipe_alias_relay_unused_and_pure_sources() { + let sources = [ + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/33_comptime_hook_close.jett" + )), + Some(ResourceHookKind::Close), + 2, + Some(761), + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/34_comptime_hook_factory.jett" + )), + Some(ResourceHookKind::Construct), + 1, + None, + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/35_comptime_hook_borrow.jett" + )), + Some(ResourceHookKind::BorrowOperation), + 1, + None, + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/36_comptime_hook_alias.jett" + )), + Some(ResourceHookKind::Close), + 1, + None, + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/37_comptime_hook_relay.jett" + )), + Some(ResourceHookKind::Close), + 1, + None, + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/38_comptime_hook_unused.jett" + )), + Some(ResourceHookKind::Close), + 1, + None, + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/39_immediate_comptime_hook_close.jett" + )), + Some(ResourceHookKind::Close), + 1, + None, + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/40_comptime_hook_absence.jett" + )), + None, + 1, + None, + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/41_comptime_hook_pure_choice.jett" + )), + Some(ResourceHookKind::Close), + 1, + None, + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/42_comptime_hook_generic.jett" + )), + Some(ResourceHookKind::Close), + 1, + None, + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/43_comptime_hook_scoped.jett" + )), + Some(ResourceHookKind::Close), + 1, + None, + ), + ]; + for release in [false, true] { + for (source, expected, expected_values, expected_scalar) in sources { + let program = shared_program(source, release); + let cache = successful(&program); + let values = cache.checked_required_values(&program).unwrap(); + assert_eq!(values.len(), expected_values); + let main = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "main" => Some(function), + _ => None, + }) + .unwrap(); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ExplicitComptime).unwrap(); + let owner = CheckedRequiredOwner::Function { + definition: checked.declaration_definition(main.name.span).unwrap(), + declaration: main.name.span, + }; + for value in &values { + assert!(value.belongs_to(&program)); + assert!(value.is_explicit_comptime()); + assert_eq!(value.owner(), owner); + let original = value.original_comptime().unwrap(); + assert!(matches!(original, Expr::Comptime(_, _))); + assert_eq!(value.source_span(), original.span()); + assert!(value.matches_original_comptime(original)); + assert!(!value.matches_original_comptime(&original.clone())); + } + let hooks = cache.checked_resource_hook_values(&program).unwrap(); + match expected { + Some(kind) => { + assert_eq!(hooks.len(), 1); + let hook = hooks[0].checked_hook(&program).unwrap(); + assert_eq!(hook.kind, kind); + let selected = values + .iter() + .filter(|value| value.checked_hook(&program).unwrap() == Some(hook)) + .collect::>(); + assert_eq!(selected.len(), 1); + assert!( + selected[0] + .matches_original_comptime(hooks[0].original_comptime().unwrap()) + ); + if let Some(expected_scalar) = expected_scalar { + let label = main + .body + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::VarDecl(declaration) if declaration.name.name == "label" => { + Some(&declaration.value) + } + _ => None, + }) + .unwrap(); + let scalar = values + .iter() + .filter(|value| value.matches_original_comptime(label)) + .collect::>(); + assert_eq!(scalar.len(), 1); + assert_eq!(scalar[0].value(), &Value::Int64(expected_scalar)); + assert!(scalar[0].checked_hook(&program).unwrap().is_none()); + assert!(matches!( + label, + Expr::Comptime(inner, _) if matches!( + inner.as_ref(), Expr::IntLiteral(value, _) + if *value == i128::from(expected_scalar) + ) + )); + assert_ne!(scalar[0].source_span(), selected[0].source_span()); + assert!(!selected[0].matches_original_comptime(label)); + assert!( + !scalar[0].matches_original_comptime( + selected[0].original_comptime().unwrap() + ) + ); + } + } + None => { + assert_eq!(hooks.len(), 0); + assert_eq!(values[0].value(), &Value::OptionalNone); + assert!(values[0].checked_hook(&program).unwrap().is_none()); + } + } + } + } +} + +#[test] +fn resource_required_value_proof_cannot_mint_without_successful_checked_evaluation() { + const FAILED: &str = "namespace app\nexport function scenario() returns nothing:\n use resource_probe\n nothing failed = comptime resource_probe.terminal_failure()\n return nothing\n"; + for release in [false, true] { + let program = checked_program(FAILED, release); + let evaluated = required(&program); + assert!(!evaluated.diagnostics.is_empty()); + assert!( + evaluated + .values + .checked_required_values(&program) + .unwrap() + .is_empty() + ); + assert!( + evaluated + .values + .checked_resource_hook_values(&program) + .unwrap() + .is_empty() + ); + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/tests.rs new file mode 100644 index 00000000..14531f85 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/tests.rs @@ -0,0 +1,247 @@ +use super::*; +use jett_common::FileId; + +const SOURCE: &str = include_str!("../../fixtures/13_direct_nested_scoped_lifecycle.jett"); + +fn execution( + release: bool, +) -> ( + Arc, + CheckedExecution, + CheckedBodyReference, +) { + let program = crate::resource_execution::tests::program(SOURCE, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let function = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.name == "scenario" + && function.name.span.file == FileId::new(0) => + { + Some(function) + } + _ => None, + }) + .unwrap(); + let definition = checked.declaration_definition(function.name.span).unwrap(); + let reference = checked + .prepare_function_body(&checked.entry(definition).unwrap()) + .unwrap(); + assert!(std::ptr::eq(reference.function().unwrap(), function)); + checked.install_function_body(&reference).unwrap(); + (program, checked, reference) +} + +fn first_binding(block: &Block) -> &ComptimeTypeBindStmt { + block + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::ComptimeTypeBind(binding) => Some(binding), + _ => None, + }) + .unwrap() +} + +fn first_call(block: &Block) -> (&Expr, &[CallArg], Span) { + let mut calls = Vec::new(); + walk_block(block, &mut |_| {}, &mut |expression| { + if let Expr::Call(callee, arguments, span) | Expr::GenericCall(callee, _, arguments, span) = + expression + { + calls.push((callee.as_ref(), arguments.as_slice(), *span)); + } + }); + *calls.first().unwrap() +} + +#[test] +fn checked_direct_scope_rejoins_original_nested_type_and_exact_parent_without_mutation() { + for release in [false, true] { + let (_, mut checked, original) = execution(release); + let outer = first_binding(&original.function().unwrap().body); + let outer_entry = checked.prepare_direct_scope(outer).unwrap(); + assert!( + checked.body.scopes.is_empty(), + "preparation must not install its selection" + ); + assert!(std::ptr::eq(outer_entry.body().unwrap(), &outer.body)); + checked.install_direct_scope(&outer_entry).unwrap(); + assert_eq!(checked.body.scopes.len(), 1); + let outer_cursor = checked.cursor(); + let nested = first_binding(outer_entry.body().unwrap()); + let nested_entry = checked.prepare_direct_scope(nested).unwrap(); + assert_eq!(checked.body.scopes.len(), 1); + assert!(std::ptr::eq(nested_entry.body().unwrap(), &nested.body)); + assert_eq!(nested_entry.bound_name(), "int64"); + checked.install_direct_scope(&nested_entry).unwrap(); + assert_eq!(checked.body.scopes.len(), 2); + assert!(matches!( + checked.prepare_direct_scope(outer), + Err(ResourceExecutionError::MissingCheckedBody) + )); + assert_eq!( + checked.body.scopes.len(), + 2, + "rejected parent must leave the active cursor unchanged" + ); + checked.restore_cursor(outer_cursor).unwrap(); + assert_eq!(checked.body.scopes.len(), 1); + checked.install_function_body(&original).unwrap(); + assert!(checked.body.scopes.is_empty()); + } +} + +#[test] +fn checked_body_reference_refuses_foreign_program_wrong_root_and_scope_identity() { + for release in [false, true] { + let (_, mut checked, original) = execution(release); + let (_, foreign, _) = execution(release); + assert_eq!( + foreign.validate_executable_cursor(&original.cursor), + Err(ResourceExecutionError::ForeignProgram) + ); + for corruption in 0..4 { + let mut bad = original.clone(); + match corruption { + 0 => { + bad.cursor.executable.as_mut().unwrap().origin = ExecutableOrigin::Function { + definition: checked + .declaration_definition(original.function().unwrap().name.span) + .unwrap(), + item: usize::MAX, + } + } + 1 => { + bad.cursor.executable.as_mut().unwrap().origin = ExecutableOrigin::Function { + definition: DefId::new(u32::MAX), + item: 0, + } + } + 2 => bad.cursor.root = BodyRoot::Generic(usize::MAX), + 3 => { + let outer = first_binding(&original.function().unwrap().body); + bad = checked.prepare_direct_scope(outer).unwrap().reference; + bad.cursor.scopes[0].bound_type = jett_types::TypeInterner::INT64; + } + _ => unreachable!(), + } + assert!( + checked.install_function_body(&bad).is_err(), + "corruption {corruption}" + ); + assert!(checked.body.scopes.is_empty()); + assert!(std::ptr::eq( + CheckedBodyReference { + cursor: checked.cursor() + } + .function() + .unwrap(), + original.function().unwrap() + )); + } + } +} + +#[test] +fn checked_source_calls_require_original_occurrence_and_exact_scoped_packet() { + for release in [false, true] { + let (_, mut checked, original) = execution(release); + let outer = first_binding(&original.function().unwrap().body); + let cloned_binding = outer.clone(); + assert!(matches!( + checked.prepare_direct_scope(&cloned_binding), + Err(ResourceExecutionError::MissingCheckedBody) + )); + let scoped_call = first_call(&outer.body); + assert_eq!( + checked.source_call_dispatch(scoped_call.0, scoped_call.1, scoped_call.2), + Err(ResourceExecutionError::MissingCheckedInvocation) + ); + let prepared = checked.prepare_direct_scope(outer).unwrap(); + checked.install_direct_scope(&prepared).unwrap(); + assert_eq!( + checked.source_call_dispatch(scoped_call.0, scoped_call.1, scoped_call.2), + Ok(true) + ); + let cloned_callee = scoped_call.0.clone(); + assert_eq!( + checked.source_call_dispatch(&cloned_callee, scoped_call.1, scoped_call.2), + Err(ResourceExecutionError::MissingCheckedInvocation) + ); + let cloned_arguments = scoped_call.1.to_vec(); + assert_eq!( + checked.source_call_dispatch(scoped_call.0, &cloned_arguments, scoped_call.2), + Err(ResourceExecutionError::MissingCheckedInvocation) + ); + let unselected = CheckedExecution::new( + checked.program().clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + assert_eq!( + unselected.source_call_dispatch(scoped_call.0, scoped_call.1, scoped_call.2), + Err(ResourceExecutionError::MissingCheckedBody) + ); + } +} + +#[test] +fn resource_original_typed_machine_constructor_refuses_copied_source_and_wrong_body() { + let source = include_str!("../../fixtures/17_absent_aggregate_shapes.jett"); + for release in [false, true] { + let program = crate::resource_execution::tests::program(source, release); + let function = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "absent_state" => Some(function), + _ => None, + }) + .unwrap(); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let definition = checked.declaration_definition(function.name.span).unwrap(); + let reference = checked + .prepare_function_body(&checked.entry(definition).unwrap()) + .unwrap(); + checked.install_function_body(&reference).unwrap(); + let (callee, arguments, span) = first_call(&function.body); + assert!(!checked.has_invocation(span).unwrap()); + assert_eq!( + checked.source_call_dispatch(callee, arguments, span), + Ok(false) + ); + assert_eq!( + checked.source_call_dispatch(&callee.clone(), arguments, span), + Err(ResourceExecutionError::MissingCheckedInvocation) + ); + assert_eq!( + checked.source_call_dispatch(callee, &arguments.to_vec(), span), + Err(ResourceExecutionError::MissingCheckedInvocation) + ); + let wrong = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "empty_tokens" => Some(function), + _ => None, + }) + .unwrap(); + let wrong_definition = checked.declaration_definition(wrong.name.span).unwrap(); + let wrong_reference = checked + .prepare_function_body(&checked.entry(wrong_definition).unwrap()) + .unwrap(); + checked.install_function_body(&wrong_reference).unwrap(); + assert_eq!( + checked.source_call_dispatch(callee, arguments, span), + Err(ResourceExecutionError::MissingCheckedInvocation) + ); + } +} diff --git a/crates/jett_comptime/src/resource_execution/custody.rs b/crates/jett_comptime/src/resource_execution/custody.rs new file mode 100644 index 00000000..a23126f6 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/custody.rs @@ -0,0 +1,697 @@ +use std::collections::HashSet; +use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind}; + +use jett_resolve::DefId; +use jett_runtime::ResourceRegistry; + +use super::{ResourceCarrier, ResourceExecutionError}; +use crate::value::Value; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct FrameId(usize); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct OwnerId(usize); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum OwnerHolder { + Temporary { frame: FrameId, slot: usize }, + Binding { frame: FrameId, definition: DefId }, +} + +impl OwnerHolder { + fn frame(self) -> FrameId { + match self { + Self::Temporary { frame, .. } | Self::Binding { frame, .. } => frame, + } + } +} + +/// One transferable obligation. No Clone, Copy, or finalizing Rust Drop. +#[derive(Debug)] +pub(crate) struct OwnerTicket { + id: OwnerId, + holder_generation: u64, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum OwnerState { + Live, + Retired, +} + +struct OwnerRecord { + carrier: ResourceCarrier, + current_holder: OwnerHolder, + current_holder_generation: u64, + state: OwnerState, +} + +#[derive(Debug, Clone, Copy)] +struct AcquisitionEntry { + owner: OwnerId, + holder: OwnerHolder, + holder_generation: u64, +} + +struct FrameLedger { + live: bool, + acquisitions: Vec, + borrows: HashSet, +} + +/// Structural location, never a schema-only request to manufacture an owner. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum PayloadStep { + Typed, + Some, + Ok, + Fail, + Field(usize), + Element(usize), + MapKey(usize), + MapValue(usize), +} + +#[derive(Debug)] +pub(crate) struct PathOwner { + pub(crate) path: Vec, + pub(crate) ticket: OwnerTicket, +} + +#[derive(Debug, Clone)] +pub(crate) struct PathBorrow { + pub(crate) path: Vec, + backing: OwnerId, + carrier: ResourceCarrier, +} + +/// Physical Value copies cannot clone this ownership bundle. +#[derive(Debug, Default)] +pub(crate) struct ValueCustody { + owned: Vec, + borrowed: Vec, +} + +#[derive(Debug)] +pub(crate) struct EvaluatedValue { + pub(crate) value: Value, + pub(crate) custody: ValueCustody, +} + +impl ValueCustody { + /// Consume tickets in reached lexical order; no physical copy can supply one. + pub(crate) fn append_owned(&mut self, mut child: Self) -> Result<(), ResourceExecutionError> { + if !self.borrowed.is_empty() || !child.borrowed.is_empty() { + return Err(ResourceExecutionError::InvalidOwner); + } + self.owned.append(&mut child.owned); + Ok(()) + } + + pub(crate) fn is_empty(&self) -> bool { + self.owned.is_empty() && self.borrowed.is_empty() + } + pub(crate) fn has_owners(&self) -> bool { + !self.owned.is_empty() + } + pub(crate) fn has_borrows(&self) -> bool { + !self.borrowed.is_empty() + } +} + +impl EvaluatedValue { + pub(crate) fn ordinary(value: Value) -> Self { + Self { + value, + custody: ValueCustody::default(), + } + } + + pub(crate) fn prefix(mut self, step: PayloadStep) -> Self { + for owner in &mut self.custody.owned { + owner.path.insert(0, step.clone()); + } + for borrow in &mut self.custody.borrowed { + borrow.path.insert(0, step.clone()); + } + self + } + + pub(crate) fn remove_prefix( + &mut self, + step: PayloadStep, + ) -> Result<(), ResourceExecutionError> { + if self + .custody + .owned + .iter() + .any(|owner| owner.path.first() != Some(&step)) + || self + .custody + .borrowed + .iter() + .any(|borrow| borrow.path.first() != Some(&step)) + { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + for owner in &mut self.custody.owned { + owner.path.remove(0); + } + for borrow in &mut self.custody.borrowed { + borrow.path.remove(0); + } + Ok(()) + } +} + +#[derive(Default)] +pub(crate) struct OwnerLedger { + records: Vec, + frames: Vec, +} + +impl OwnerLedger { + pub(crate) fn frame(&mut self) -> FrameId { + let id = FrameId(self.frames.len()); + self.frames.push(FrameLedger { + live: true, + acquisitions: Vec::new(), + borrows: HashSet::new(), + }); + id + } + + fn live_frame(&self, frame: FrameId) -> Result<(), ResourceExecutionError> { + if self.frames.get(frame.0).is_some_and(|frame| frame.live) { + return Ok(()); + } + Err(ResourceExecutionError::InvalidFrame) + } + + pub(crate) fn frame_is_live(&self, frame: FrameId) -> bool { + self.frames.get(frame.0).is_some_and(|frame| frame.live) + } + + pub(crate) fn acquire( + &mut self, + carrier: ResourceCarrier, + holder: OwnerHolder, + ) -> Result { + self.live_frame(holder.frame())?; + if self + .records + .iter() + .any(|record| record.carrier.key == carrier.key && record.state == OwnerState::Live) + { + return Err(ResourceExecutionError::InvalidOwner); + } + let id = OwnerId(self.records.len()); + let generation = 1; + self.records.push(OwnerRecord { + carrier, + current_holder: holder, + current_holder_generation: generation, + state: OwnerState::Live, + }); + self.frames[holder.frame().0] + .acquisitions + .push(AcquisitionEntry { + owner: id, + holder, + holder_generation: generation, + }); + Ok(ValueCustody { + owned: vec![PathOwner { + path: Vec::new(), + ticket: OwnerTicket { + id, + holder_generation: generation, + }, + }], + borrowed: Vec::new(), + }) + } + + fn record(&self, ticket: &OwnerTicket) -> Result<&OwnerRecord, ResourceExecutionError> { + let record = self + .records + .get(ticket.id.0) + .ok_or(ResourceExecutionError::InvalidOwner)?; + if record.state != OwnerState::Live + || record.current_holder_generation != ticket.holder_generation + { + return Err(ResourceExecutionError::InvalidOwner); + } + self.live_frame(record.current_holder.frame())?; + Ok(record) + } + + fn has_active_borrow(&self, owner: OwnerId) -> bool { + self.frames + .iter() + .any(|frame| frame.live && frame.borrows.contains(&owner)) + } + + pub(crate) fn hold_borrows( + &mut self, + custody: &ValueCustody, + frame: FrameId, + ) -> Result<(), ResourceExecutionError> { + self.live_frame(frame)?; + for borrow in &custody.borrowed { + self.validate_borrow(borrow)?; + } + for borrow in &custody.borrowed { + self.frames[frame.0].borrows.insert(borrow.backing); + } + Ok(()) + } + + pub(crate) fn transfer( + &mut self, + custody: &mut ValueCustody, + destination: OwnerHolder, + ) -> Result<(), ResourceExecutionError> { + self.live_frame(destination.frame())?; + let mut owners = HashSet::new(); + // Validate every member before changing any holder. The vector's order + // is lexical acquisition order, never formal/field/hash ordering. + for owner in &custody.owned { + if !owners.insert(owner.ticket.id) { + return Err(ResourceExecutionError::InvalidOwner); + } + if self.has_active_borrow(owner.ticket.id) { + return Err(ResourceExecutionError::InvalidBorrow); + } + self.record(&owner.ticket)? + .current_holder_generation + .checked_add(1) + .ok_or(ResourceExecutionError::IdentityExhausted)?; + } + for owner in &mut custody.owned { + let record = &mut self.records[owner.ticket.id.0]; + record.current_holder_generation += 1; + record.current_holder = destination; + owner.ticket.holder_generation = record.current_holder_generation; + self.frames[destination.frame().0] + .acquisitions + .push(AcquisitionEntry { + owner: owner.ticket.id, + holder: destination, + holder_generation: record.current_holder_generation, + }); + } + Ok(()) + } + + pub(crate) fn borrow( + &self, + value: &EvaluatedValue, + ) -> Result { + let mut borrowed = value.custody.borrowed.clone(); + for borrow in &borrowed { + self.validate_borrow(borrow)?; + } + for owner in &value.custody.owned { + let record = self.record(&owner.ticket)?; + borrowed.push(PathBorrow { + path: owner.path.clone(), + backing: owner.ticket.id, + carrier: record.carrier.clone(), + }); + } + Ok(EvaluatedValue { + value: value.value.clone(), + custody: ValueCustody { + owned: Vec::new(), + borrowed, + }, + }) + } + + fn validate_borrow(&self, borrow: &PathBorrow) -> Result<(), ResourceExecutionError> { + let record = self + .records + .get(borrow.backing.0) + .ok_or(ResourceExecutionError::InvalidBorrow)?; + if record.state != OwnerState::Live || record.carrier.key != borrow.carrier.key { + return Err(ResourceExecutionError::InvalidBorrow); + } + self.live_frame(record.current_holder.frame())?; + Ok(()) + } + + pub(crate) fn borrowed_carrier( + &self, + custody: &ValueCustody, + ) -> Result<&ResourceCarrier, ResourceExecutionError> { + if !custody.owned.is_empty() { + return Err(ResourceExecutionError::InvalidBorrow); + } + let [borrow] = custody.borrowed.as_slice() else { + return Err(ResourceExecutionError::InvalidBorrow); + }; + if !borrow.path.is_empty() { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + self.validate_borrow(borrow)?; + Ok(&self.records[borrow.backing.0].carrier) + } + + pub(crate) fn validate_positions( + &self, + custody: &ValueCustody, + positions: &[(Vec, &ResourceCarrier)], + ) -> Result<(), ResourceExecutionError> { + if positions.len() != custody.owned.len() + custody.borrowed.len() { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + let mut claimed = HashSet::new(); + for owner in &custody.owned { + let record = self.record(&owner.ticket)?; + let candidates = positions + .iter() + .enumerate() + .filter(|(_, (path, carrier))| { + path.as_slice() == owner.path.as_slice() + && carrier.key == record.carrier.key + && carrier.resource == record.carrier.resource + && std::sync::Arc::ptr_eq(&carrier.program, &record.carrier.program) + }) + .map(|(index, _)| index) + .collect::>(); + let [index] = candidates.as_slice() else { + return Err(ResourceExecutionError::InvalidPayloadPath); + }; + if !claimed.insert(*index) { + return Err(ResourceExecutionError::InvalidOwner); + } + } + for borrow in &custody.borrowed { + self.validate_borrow(borrow)?; + let candidates = positions + .iter() + .enumerate() + .filter(|(_, (path, carrier))| { + path.as_slice() == borrow.path.as_slice() + && carrier.key == borrow.carrier.key + && carrier.resource == borrow.carrier.resource + && std::sync::Arc::ptr_eq(&carrier.program, &borrow.carrier.program) + }) + .map(|(index, _)| index) + .collect::>(); + let [index] = candidates.as_slice() else { + return Err(ResourceExecutionError::InvalidPayloadPath); + }; + if !claimed.insert(*index) { + return Err(ResourceExecutionError::InvalidBorrow); + } + } + Ok(()) + } + + pub(crate) fn close( + &mut self, + custody: ValueCustody, + registry: &mut ResourceRegistry, + ) -> Result<(), ResourceExecutionError> { + if !custody.borrowed.is_empty() { + return Err(ResourceExecutionError::InvalidOwner); + } + let [owner] = custody.owned.as_slice() else { + return Err(ResourceExecutionError::MissingOwner); + }; + if !owner.path.is_empty() { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + if self.has_active_borrow(owner.ticket.id) { + return Err(ResourceExecutionError::InvalidBorrow); + } + let record = self.record(&owner.ticket)?; + let key = record.carrier.key; + let resource_type = record.carrier.resource.registry_type; + // Retire before the finalizer, including if it raises a host panic. + self.records[owner.ticket.id.0].state = OwnerState::Retired; + registry.close(key, resource_type)?; + Ok(()) + } + + pub(crate) fn unwind( + &mut self, + frame: FrameId, + registry: &mut ResourceRegistry, + ) -> Result<(), ResourceExecutionError> { + self.live_frame(frame)?; + self.frames[frame.0].borrows.clear(); + let protected = self.frames[frame.0].acquisitions.iter().any(|entry| { + let record = &self.records[entry.owner.0]; + record.state == OwnerState::Live + && record.current_holder == entry.holder + && record.current_holder_generation == entry.holder_generation + && self.has_active_borrow(entry.owner) + }); + if protected { + return Err(ResourceExecutionError::InvalidBorrow); + } + let entries = std::mem::take(&mut self.frames[frame.0].acquisitions); + let mut failure = None; + let mut panic = None; + for entry in entries.into_iter().rev() { + let record = &mut self.records[entry.owner.0]; + if record.state != OwnerState::Live + || record.current_holder != entry.holder + || record.current_holder_generation != entry.holder_generation + { + continue; + } + record.state = OwnerState::Retired; + let finalized = catch_unwind(AssertUnwindSafe(|| { + registry.close(record.carrier.key, record.carrier.resource.registry_type) + })); + match finalized { + Ok(Ok(())) => {} + Ok(Err(error)) => { + failure.get_or_insert(ResourceExecutionError::Registry(error)); + } + Err(payload) if panic.is_none() => panic = Some(payload), + Err(payload) => std::mem::forget(payload), + } + } + self.frames[frame.0].live = false; + if let Some(payload) = panic { + resume_unwind(payload); + } + failure.map_or(Ok(()), Err) + } + + pub(crate) fn has_live_frames(&self) -> bool { + self.frames.iter().any(|frame| frame.live) + } + pub(crate) fn live_frame_count(&self) -> usize { + self.frames.iter().filter(|frame| frame.live).count() + } + pub(crate) fn has_live_borrows(&self) -> bool { + self.frames.iter().any(|frame| !frame.borrows.is_empty()) + } + + pub(crate) fn live_owners(&self) -> usize { + self.records + .iter() + .filter(|record| record.state == OwnerState::Live) + .count() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::resource_execution::{ResourceTypeBinding, tests::program}; + use jett_runtime::{AuthorityProvenance, ResourceTypeId}; + use std::sync::{Arc, Mutex}; + + fn carrier( + registry: &mut ResourceRegistry, + label: i64, + events: Arc>>, + panic: bool, + ) -> ResourceCarrier { + let program = program(include_str!("fixtures/02_reverse_scope_drop.jett"), false); + let hook = program.checked().resource_hooks.values().next().unwrap(); + let resource = ResourceTypeBinding { + definition: hook.resource_definition, + checked_type: hook.resource_type, + registry_type: ResourceTypeId::new(0), + }; + let key = registry + .insert( + resource.registry_type, + label, + AuthorityProvenance::new(1, 1), + move |label| { + events.lock().unwrap().push(label); + if panic { + panic!("selected test finalizer"); + } + }, + ) + .unwrap(); + ResourceCarrier { + program, + resource, + key, + } + } + + #[test] + fn holder_generation_prevents_old_entries_from_finalizing_returned_storage() { + let mut registry = ResourceRegistry::new(); + let mut ledger = OwnerLedger::default(); + let events = Arc::new(Mutex::new(Vec::new())); + let outer = ledger.frame(); + let inner = ledger.frame(); + let original = OwnerHolder::Temporary { + frame: outer, + slot: 0, + }; + let mut first = ledger + .acquire(carrier(&mut registry, 1, events.clone(), false), original) + .unwrap(); + let second = ledger + .acquire( + carrier(&mut registry, 2, events.clone(), false), + OwnerHolder::Temporary { + frame: outer, + slot: 1, + }, + ) + .unwrap(); + ledger + .transfer( + &mut first, + OwnerHolder::Temporary { + frame: inner, + slot: 0, + }, + ) + .unwrap(); + ledger.transfer(&mut first, original).unwrap(); + ledger.unwind(inner, &mut registry).unwrap(); + assert!(events.lock().unwrap().is_empty()); + ledger.unwind(outer, &mut registry).unwrap(); + assert_eq!(*events.lock().unwrap(), [1, 2]); // Latest holder acquisitions, not original registry order. + assert_eq!(ledger.live_owners(), 0); + assert_eq!(registry.live_count(), 0); + drop((first, second)); + assert_eq!(*events.lock().unwrap(), [1, 2]); + } + + #[test] + fn borrowed_operation_protects_owner_before_transfer_or_close_effect() { + let mut registry = ResourceRegistry::new(); + let mut ledger = OwnerLedger::default(); + let events = Arc::new(Mutex::new(Vec::new())); + let owner = ledger.frame(); + let operation = ledger.frame(); + let carrier = carrier(&mut registry, 3, events.clone(), false); + let mut value = EvaluatedValue { + value: Value::Resource(carrier.clone()), + custody: ledger + .acquire( + carrier, + OwnerHolder::Temporary { + frame: owner, + slot: 0, + }, + ) + .unwrap(), + }; + let borrowed = ledger.borrow(&value).unwrap(); + ledger.hold_borrows(&borrowed.custody, operation).unwrap(); + assert_eq!( + ledger.transfer( + &mut value.custody, + OwnerHolder::Temporary { + frame: operation, + slot: 0 + } + ), + Err(ResourceExecutionError::InvalidBorrow) + ); + assert!(events.lock().unwrap().is_empty()); + assert_eq!(registry.live_count(), 1); + ledger.unwind(operation, &mut registry).unwrap(); + ledger.close(value.custody, &mut registry).unwrap(); + assert_eq!(*events.lock().unwrap(), [3]); + assert_eq!(ledger.live_owners(), 0); + assert_eq!( + ledger.borrowed_carrier(&borrowed.custody).unwrap_err(), + ResourceExecutionError::InvalidBorrow + ); + ledger.unwind(owner, &mut registry).unwrap(); + assert_eq!(*events.lock().unwrap(), [3]); + } + + #[test] + fn copied_carrier_or_foreign_payload_cannot_create_custody() { + let mut registry = ResourceRegistry::new(); + let mut ledger = OwnerLedger::default(); + let events = Arc::new(Mutex::new(Vec::new())); + let frame = ledger.frame(); + let carrier = carrier(&mut registry, 4, events.clone(), false); + let custody = ledger + .acquire(carrier.clone(), OwnerHolder::Temporary { frame, slot: 0 }) + .unwrap(); + assert_eq!( + ledger.validate_positions(&ValueCustody::default(), &[(Vec::new(), &carrier)]), + Err(ResourceExecutionError::InvalidPayloadPath) + ); + assert_eq!( + ledger + .acquire(carrier.clone(), OwnerHolder::Temporary { frame, slot: 1 }) + .unwrap_err(), + ResourceExecutionError::InvalidOwner + ); + let foreign = ResourceCarrier { + program: program(include_str!("fixtures/02_reverse_scope_drop.jett"), false), + ..carrier.clone() + }; + assert_eq!( + ledger.validate_positions(&custody, &[(Vec::new(), &foreign)]), + Err(ResourceExecutionError::InvalidPayloadPath) + ); + ledger.unwind(frame, &mut registry).unwrap(); + assert_eq!(*events.lock().unwrap(), [4]); + } + + #[test] + fn finalizer_panic_retires_before_panic_and_continues_every_remaining_owner() { + let mut registry = ResourceRegistry::new(); + let mut ledger = OwnerLedger::default(); + let events = Arc::new(Mutex::new(Vec::new())); + let frame = ledger.frame(); + let first = ledger + .acquire( + carrier(&mut registry, 5, events.clone(), false), + OwnerHolder::Temporary { frame, slot: 0 }, + ) + .unwrap(); + let second = ledger + .acquire( + carrier(&mut registry, 6, events.clone(), true), + OwnerHolder::Temporary { frame, slot: 1 }, + ) + .unwrap(); + assert!(catch_unwind(AssertUnwindSafe(|| ledger.unwind(frame, &mut registry))).is_err()); + assert_eq!(*events.lock().unwrap(), [6, 5]); + assert_eq!(ledger.live_owners(), 0); + assert_eq!(registry.live_count(), 0); + assert_eq!( + ledger.unwind(frame, &mut registry), + Err(ResourceExecutionError::InvalidFrame) + ); + drop((first, second)); + drop(registry); + assert_eq!(*events.lock().unwrap(), [6, 5]); + } +} diff --git a/crates/jett_comptime/src/resource_execution/fixtures/01_construct_borrow_move_close.jett b/crates/jett_comptime/src/resource_execution/fixtures/01_construct_borrow_move_close.jett new file mode 100644 index 00000000..7cbfefbf --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/01_construct_borrow_move_close.jett @@ -0,0 +1,14 @@ +namespace app +function move_owned(token: resource_probe.TestHandle) returns resource_probe.TestHandle: + return token +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle first = resource_probe.create(view net, 101) handle error: + return nothing + int64 before = resource_probe.borrow(view net, view first) handle error: + return nothing + int64 again = resource_probe.borrow(view net, view first) handle error: + return nothing + resource_probe.TestHandle moved = move_owned(first) + resource_probe.close(moved) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/02_reverse_scope_drop.jett b/crates/jett_comptime/src/resource_execution/fixtures/02_reverse_scope_drop.jett new file mode 100644 index 00000000..15e93563 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/02_reverse_scope_drop.jett @@ -0,0 +1,12 @@ +namespace app +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle first = resource_probe.create(view net, 201) handle error: + return nothing + resource_probe.TestHandle second = resource_probe.create(view net, 202) handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view first) handle error: + return nothing + int64 later = resource_probe.borrow(view net, view second) handle error: + return nothing + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/03_domain_failures.jett b/crates/jett_comptime/src/resource_execution/fixtures/03_domain_failures.jett new file mode 100644 index 00000000..28e1e6cb --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/03_domain_failures.jett @@ -0,0 +1,9 @@ +namespace app +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 301) handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view token) handle error: + default -1 + resource_probe.close(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/04_bare_to_view_operation_cleanup.jett b/crates/jett_comptime/src/resource_execution/fixtures/04_bare_to_view_operation_cleanup.jett new file mode 100644 index 00000000..cede9b07 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/04_bare_to_view_operation_cleanup.jett @@ -0,0 +1,12 @@ +namespace app +function consume_at_view_boundary(view net: Network, view token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 401) handle error: + return nothing + consume_at_view_boundary(view net, token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/05_later_named_argument_failure.jett b/crates/jett_comptime/src/resource_execution/fixtures/05_later_named_argument_failure.jett new file mode 100644 index 00000000..329e8a8a --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/05_later_named_argument_failure.jett @@ -0,0 +1,13 @@ +namespace app +function receive(marker: int64, view token: resource_probe.TestHandle) returns nothing: + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 501) handle error: + return nothing + receive(token: token, marker: fail_marker() handle error: + return nothing + ) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/05_later_named_argument_failure_v2.jett b/crates/jett_comptime/src/resource_execution/fixtures/05_later_named_argument_failure_v2.jett new file mode 100644 index 00000000..7a258cbd --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/05_later_named_argument_failure_v2.jett @@ -0,0 +1,21 @@ +namespace app +function stage_owned(view net: Network, token: resource_probe.TestHandle) returns resource_probe.TestHandle: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return token + return token +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 501) handle error: + return nothing + accept_staged(token: stage_owned(view net, token), marker: fail_marker() handle error: + return nothing + , net: view net) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/06_return_occupied_result.jett b/crates/jett_comptime/src/resource_execution/fixtures/06_return_occupied_result.jett new file mode 100644 index 00000000..990f8c0b --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/06_return_occupied_result.jett @@ -0,0 +1,15 @@ +namespace app +function carry_result(outcome: result[resource_probe.TestHandle, string]) returns result[resource_probe.TestHandle, string]: + return outcome +function acquire(view net: Network) returns result[resource_probe.TestHandle, string]: + use resource_probe + return carry_result(resource_probe.create(view net, 601)) +export function scenario(view net: Network) returns nothing: + use resource_probe + result[resource_probe.TestHandle, string] held = acquire(view net) + resource_probe.TestHandle token = held handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/07_indirect_close.jett b/crates/jett_comptime/src/resource_execution/fixtures/07_indirect_close.jett new file mode 100644 index 00000000..90692c68 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/07_indirect_close.jett @@ -0,0 +1,8 @@ +namespace app +export function scenario(view net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.close_descriptor() + resource_probe.TestHandle token = resource_probe.create(view net, 701) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/08_required_descriptor_and_absence.jett b/crates/jett_comptime/src/resource_execution/fixtures/08_required_descriptor_and_absence.jett new file mode 100644 index 00000000..aab76330 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/08_required_descriptor_and_absence.jett @@ -0,0 +1,21 @@ +namespace app +export function descriptor_storage() returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = comptime resource_probe.close_descriptor() + return nothing +export function absent_close_path() returns nothing: + use resource_probe + optional[resource_probe.TestHandle] empty = comptime resource_probe.empty() + resource_probe.TestHandle token = empty handle: + return nothing + resource_probe.close(token) + return nothing +verify descriptor_verify_control: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.close_descriptor() + assert true +property descriptor_property_control: + given marker: int64 + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.close_descriptor() + assert marker == marker diff --git a/crates/jett_comptime/src/resource_execution/fixtures/09_capability_free_close_target.jett b/crates/jett_comptime/src/resource_execution/fixtures/09_capability_free_close_target.jett new file mode 100644 index 00000000..43916baf --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/09_capability_free_close_target.jett @@ -0,0 +1,11 @@ +namespace app +export function close_entry(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 901) handle error: + return nothing + close_entry(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/10_ordinary_closure_unreferenced_resource.jett b/crates/jett_comptime/src/resource_execution/fixtures/10_ordinary_closure_unreferenced_resource.jett new file mode 100644 index 00000000..ec75a3af --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/10_ordinary_closure_unreferenced_resource.jett @@ -0,0 +1,9 @@ +namespace app +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1001) handle error: + return -1 + function() returns int64 ordinary = function() returns int64: return 7 + int64 observed = ordinary() + resource_probe.close(token) + return observed diff --git a/crates/jett_comptime/src/resource_execution/fixtures/11_terminal_assertion_cleanup.jett b/crates/jett_comptime/src/resource_execution/fixtures/11_terminal_assertion_cleanup.jett new file mode 100644 index 00000000..f11b39e7 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/11_terminal_assertion_cleanup.jett @@ -0,0 +1,9 @@ +namespace app +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1101) handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + resource_probe.terminal_failure() + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/12_mixed_failure_cleanup_continues.jett b/crates/jett_comptime/src/resource_execution/fixtures/12_mixed_failure_cleanup_continues.jett new file mode 100644 index 00000000..f0eb7004 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/12_mixed_failure_cleanup_continues.jett @@ -0,0 +1,16 @@ +namespace app +function consume_at_view_boundary(view net: Network, view token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle first = resource_probe.create(view net, 1201) handle error: + return nothing + resource_probe.TestHandle second = resource_probe.create(view net, 1202) handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view first) handle error: + return nothing + consume_at_view_boundary(view net, second) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/13_direct_nested_scoped_lifecycle.jett b/crates/jett_comptime/src/resource_execution/fixtures/13_direct_nested_scoped_lifecycle.jett new file mode 100644 index 00000000..fc646d9a --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/13_direct_nested_scoped_lifecycle.jett @@ -0,0 +1,26 @@ +namespace app +function move_scoped_owner(token: resource_probe.TestHandle) returns resource_probe.TestHandle: + return token +export function scenario(view net: Network) returns nothing: + use resource_probe + comptime type Token = type.info[resource_probe.TestHandle](): + Token first = resource_probe.create(view net, 1301) handle error: + return nothing + int64 before = resource_probe.borrow(view net, view first) handle error: + return nothing + Token moved = move_scoped_owner(first) + resource_probe.close(moved) + comptime type Element = type.arg[list[int64]](0): + Element label = 1302 + resource_probe.TestHandle second = resource_probe.create(view net, label) handle error: + return nothing + Element observed = resource_probe.borrow(view net, view second) handle error: + return nothing + resource_probe.close(second) + Token restored = resource_probe.create(view net, 1304) handle error: + return nothing + resource_probe.close(restored) + resource_probe.TestHandle after = resource_probe.create(view net, 1303) handle error: + return nothing + resource_probe.close(after) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/14_direct_scoped_descriptor_absence.jett b/crates/jett_comptime/src/resource_execution/fixtures/14_direct_scoped_descriptor_absence.jett new file mode 100644 index 00000000..546e7bfb --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/14_direct_scoped_descriptor_absence.jett @@ -0,0 +1,15 @@ +namespace app +export function descriptor_storage() returns nothing: + use resource_probe + comptime type Token = type.info[resource_probe.TestHandle](): + function(Token) returns nothing finalizer = resource_probe.close_descriptor() + comptime type Nested = type.arg[optional[Token]](0): + optional[Nested] absent = resource_probe.empty() + return nothing +export function absent_close_path() returns nothing: + use resource_probe + comptime type Token = type.info[resource_probe.TestHandle](): + Token missing = resource_probe.empty() handle: + return nothing + resource_probe.close(missing) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/15_direct_scoped_ordinary_error.jett b/crates/jett_comptime/src/resource_execution/fixtures/15_direct_scoped_ordinary_error.jett new file mode 100644 index 00000000..172f5404 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/15_direct_scoped_ordinary_error.jett @@ -0,0 +1,10 @@ +namespace app +export function scenario(view net: Network) returns nothing: + use resource_probe + comptime type Token = type.info[resource_probe.TestHandle](): + Token owner = resource_probe.create(view net, 1501) handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view owner) handle error: + return nothing + resource_probe.terminal_failure() + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/16_direct_scoped_return_default.jett b/crates/jett_comptime/src/resource_execution/fixtures/16_direct_scoped_return_default.jett new file mode 100644 index 00000000..4e7b47e7 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/16_direct_scoped_return_default.jett @@ -0,0 +1,12 @@ +namespace app +function observed_or_default(view net: Network, label: int64) returns int64: + use resource_probe + comptime type Element = type.info[int64](): + resource_probe.TestHandle owner = resource_probe.create(view net, label) handle error: + return -1 + Element observed = resource_probe.borrow(view net, view owner) handle error: + default 17 + return observed + return -2 +export function scenario(view net: Network) returns int64: + return observed_or_default(view net, 1601) diff --git a/crates/jett_comptime/src/resource_execution/fixtures/17_absent_aggregate_shapes.jett b/crates/jett_comptime/src/resource_execution/fixtures/17_absent_aggregate_shapes.jett new file mode 100644 index 00000000..3a1d96cf --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/17_absent_aggregate_shapes.jett @@ -0,0 +1,78 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker diff --git a/crates/jett_comptime/src/resource_execution/fixtures/18_pipeline_construct_move_close.jett b/crates/jett_comptime/src/resource_execution/fixtures/18_pipeline_construct_move_close.jett new file mode 100644 index 00000000..18865303 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/18_pipeline_construct_move_close.jett @@ -0,0 +1,9 @@ +namespace app +function move_owned(token: resource_probe.TestHandle) returns resource_probe.TestHandle: + return token +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = net into view resource_probe.create(1801) handle error: + return nothing + token into move_owned() into resource_probe.close() + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/19_pipeline_written_view_retains_owner.jett b/crates/jett_comptime/src/resource_execution/fixtures/19_pipeline_written_view_retains_owner.jett new file mode 100644 index 00000000..4e02320b --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/19_pipeline_written_view_retains_owner.jett @@ -0,0 +1,12 @@ +namespace app +function read_first(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1901) handle error: + return -1 + int64 observed = token into view read_first(view net) handle error: + return -2 + resource_probe.close(token) + return observed diff --git a/crates/jett_comptime/src/resource_execution/fixtures/20_pipeline_bare_owner_view_operation.jett b/crates/jett_comptime/src/resource_execution/fixtures/20_pipeline_bare_owner_view_operation.jett new file mode 100644 index 00000000..f7d76a77 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/20_pipeline_bare_owner_view_operation.jett @@ -0,0 +1,11 @@ +namespace app +function read_first(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 2001) handle error: + return -1 + int64 observed = token into read_first(view net) handle error: + return -2 + return observed diff --git a/crates/jett_comptime/src/resource_execution/fixtures/21_pipeline_named_actual_abort.jett b/crates/jett_comptime/src/resource_execution/fixtures/21_pipeline_named_actual_abort.jett new file mode 100644 index 00000000..afb926ea --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/21_pipeline_named_actual_abort.jett @@ -0,0 +1,22 @@ +namespace app +function stage_owned(view net: Network, token: resource_probe.TestHandle) returns resource_probe.TestHandle: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return token + return token +function accept_staged(token: resource_probe.TestHandle, marker: int64, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + resource_probe.close(token) + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 2101) handle error: + return nothing + stage_owned(view net, token) into accept_staged(net: view net, marker: fail_marker() handle error: + return nothing + ) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/22_pipeline_domain_failure_stops_next_step.jett b/crates/jett_comptime/src/resource_execution/fixtures/22_pipeline_domain_failure_stops_next_step.jett new file mode 100644 index 00000000..92cd42f4 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/22_pipeline_domain_failure_stops_next_step.jett @@ -0,0 +1,7 @@ +namespace app +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = net into view resource_probe.create(2201) handle error: + return -1 + token into resource_probe.close() + return 9 diff --git a/crates/jett_comptime/src/resource_execution/fixtures/23_pipeline_indirect_close_descriptor.jett b/crates/jett_comptime/src/resource_execution/fixtures/23_pipeline_indirect_close_descriptor.jett new file mode 100644 index 00000000..2076d3fe --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/23_pipeline_indirect_close_descriptor.jett @@ -0,0 +1,8 @@ +namespace app +export function scenario(view net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing close_token = resource_probe.close_descriptor() + resource_probe.TestHandle token = resource_probe.create(view net, 2301) handle error: + return nothing + token into close_token + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/24_mutable_assignment.jett b/crates/jett_comptime/src/resource_execution/fixtures/24_mutable_assignment.jett new file mode 100644 index 00000000..acc73b8c --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/24_mutable_assignment.jett @@ -0,0 +1,41 @@ +namespace app +function observe_previous(view net: Network, view token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +export function replace_live(view net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2401) handle error: + return nothing + token = resource_probe.create(view net, 2402) handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + resource_probe.close(token) + return nothing +export function rebind_after_close(view net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2411) handle error: + return nothing + resource_probe.close(token) + token = resource_probe.create(view net, 2412) handle error: + return nothing + resource_probe.close(token) + return nothing +export function rebind_self(view net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2421) handle error: + return nothing + token = token + resource_probe.close(token) + return nothing +export function failed_rhs_keeps_owner(view net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2431) handle error: + return nothing + token = resource_probe.create(view net, 2432) handle error: + observe_previous(view net, view token) + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/25_repeated_scoped_entry.jett b/crates/jett_comptime/src/resource_execution/fixtures/25_repeated_scoped_entry.jett new file mode 100644 index 00000000..22420b4a --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/25_repeated_scoped_entry.jett @@ -0,0 +1,20 @@ +namespace app +export function clean(view net: Network) returns int64: + use resource_probe + comptime type Token = type.info[resource_probe.TestHandle](): + Token token = resource_probe.create(view net, 1801) handle error: + return -1 + int64 observed = resource_probe.borrow(view net, view token) handle error: + return -2 + resource_probe.close(token) + return observed + return -3 +export function terminal_error(view net: Network) returns nothing: + use resource_probe + comptime type Token = type.info[resource_probe.TestHandle](): + Token token = resource_probe.create(view net, 1802) handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + resource_probe.terminal_failure() + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/26_original_required_regions.jett b/crates/jett_comptime/src/resource_execution/fixtures/26_original_required_regions.jett new file mode 100644 index 00000000..f9d18505 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/26_original_required_regions.jett @@ -0,0 +1,21 @@ +namespace app +function seven() returns int64: + return 7 +int64 baked = comptime seven() +export function scenario() returns int64: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = comptime resource_probe.close_descriptor() + optional[resource_probe.TestHandle] absent = comptime resource_probe.empty() + return baked +verify original_required_verify: + use resource_probe + int64 observed = comptime seven() + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.close_descriptor() + assert observed == 7 +property original_required_property: + given marker: int64 + use resource_probe + int64 observed = comptime seven() + optional[resource_probe.TestHandle] absent = resource_probe.empty() + assert observed == 7 + assert marker == marker diff --git a/crates/jett_comptime/src/resource_execution/fixtures/27_original_generic_scoped_required.jett b/crates/jett_comptime/src/resource_execution/fixtures/27_original_generic_scoped_required.jett new file mode 100644 index 00000000..3a22c964 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/27_original_generic_scoped_required.jett @@ -0,0 +1,13 @@ +namespace app +function unused[T]() returns string: + return comptime type.name[T]() +function label[T]() returns string: + string label_value = comptime type.name[T]() + return label_value +export function generic_label() returns string: + return label[int64]() +export function scoped_label() returns string: + comptime type Outer = type.info[list[int64]](): + comptime type Element = type.arg[Outer](0): + return comptime type.name[Element]() + return "unreached" diff --git a/crates/jett_comptime/src/resource_execution/fixtures/28_pipeline_absence_regions.jett b/crates/jett_comptime/src/resource_execution/fixtures/28_pipeline_absence_regions.jett new file mode 100644 index 00000000..6da904dc --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/28_pipeline_absence_regions.jett @@ -0,0 +1,16 @@ +namespace app +function absent_token() returns optional[resource_probe.TestHandle]: + return none +function forward_absent(token: optional[resource_probe.TestHandle]) returns optional[resource_probe.TestHandle]: + return token +function marker(token: optional[resource_probe.TestHandle]) returns int64: + return 17 +int64 namespace_marker = comptime(absent_token() into forward_absent() into marker()) +export function explicit_marker() returns int64: + return comptime(absent_token() into forward_absent() into marker()) +verify absent_pipeline_verify: + assert(absent_token() into forward_absent() into marker()) == 17 +property absent_pipeline_property: + given count: int64 + assert(absent_token() into forward_absent() into marker()) == 17 + assert count == count diff --git a/crates/jett_comptime/src/resource_execution/fixtures/30_owned_struct_scope_drop.jett b/crates/jett_comptime/src/resource_execution/fixtures/30_owned_struct_scope_drop.jett new file mode 100644 index 00000000..2bf7e86d --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/30_owned_struct_scope_drop.jett @@ -0,0 +1,13 @@ +namespace app +struct OwnedEnvelope: + token: resource_probe.TestHandle + marker: int64 +function retire_owned(token: resource_probe.TestHandle) returns nothing: + OwnedEnvelope held = OwnedEnvelope(token: token, marker: 17) + return nothing +export function main(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1001) handle error: + return nothing + retire_owned(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/31_owned_struct_reverse.jett b/crates/jett_comptime/src/resource_execution/fixtures/31_owned_struct_reverse.jett new file mode 100644 index 00000000..03016231 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/31_owned_struct_reverse.jett @@ -0,0 +1,16 @@ +namespace app +struct OwnedPair: + first: resource_probe.TestHandle + second: resource_probe.TestHandle + marker: int64 +function retire_pair(first: resource_probe.TestHandle, second: resource_probe.TestHandle) returns nothing: + OwnedPair held = OwnedPair(second: second, first: first, marker: 17) + return nothing +export function main(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle first = resource_probe.create(view net, 1002) handle error: + return nothing + resource_probe.TestHandle second = resource_probe.create(view net, 1003) handle error: + return nothing + retire_pair(first, second) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/32_owned_struct_abandoned_prefix.jett b/crates/jett_comptime/src/resource_execution/fixtures/32_owned_struct_abandoned_prefix.jett new file mode 100644 index 00000000..3ccedf99 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/32_owned_struct_abandoned_prefix.jett @@ -0,0 +1,15 @@ +namespace app +struct OwnedEnvelope: + token: resource_probe.TestHandle + marker: int64 +function fail_marker() returns result[int64, string]: + return fail("selected constructor marker failure") +function retire_owned(token: resource_probe.TestHandle) returns nothing: + OwnedEnvelope held = OwnedEnvelope(token: token, marker: fail_marker() handle error: return nothing) + return nothing +export function main(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1004) handle error: + return nothing + retire_owned(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/33_owned_struct_scope_drop_entry.jett b/crates/jett_comptime/src/resource_execution/fixtures/33_owned_struct_scope_drop_entry.jett new file mode 100644 index 00000000..4f825e21 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/33_owned_struct_scope_drop_entry.jett @@ -0,0 +1,13 @@ +namespace app +struct OwnedEnvelope: + token: resource_probe.TestHandle + marker: int64 +function retire_owned(token: resource_probe.TestHandle) returns nothing: + OwnedEnvelope held = OwnedEnvelope(token: token, marker: 17) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1001) handle error: + return nothing + retire_owned(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/34_owned_struct_reverse_entry.jett b/crates/jett_comptime/src/resource_execution/fixtures/34_owned_struct_reverse_entry.jett new file mode 100644 index 00000000..128b8398 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/34_owned_struct_reverse_entry.jett @@ -0,0 +1,16 @@ +namespace app +struct OwnedPair: + first: resource_probe.TestHandle + second: resource_probe.TestHandle + marker: int64 +function retire_pair(first: resource_probe.TestHandle, second: resource_probe.TestHandle) returns nothing: + OwnedPair held = OwnedPair(second: second, first: first, marker: 17) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle first = resource_probe.create(view net, 1002) handle error: + return nothing + resource_probe.TestHandle second = resource_probe.create(view net, 1003) handle error: + return nothing + retire_pair(first, second) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/35_owned_struct_abandoned_prefix_entry.jett b/crates/jett_comptime/src/resource_execution/fixtures/35_owned_struct_abandoned_prefix_entry.jett new file mode 100644 index 00000000..36252ec3 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/35_owned_struct_abandoned_prefix_entry.jett @@ -0,0 +1,15 @@ +namespace app +struct OwnedEnvelope: + token: resource_probe.TestHandle + marker: int64 +function fail_marker() returns result[int64, string]: + return fail("selected constructor marker failure") +function retire_owned(token: resource_probe.TestHandle) returns nothing: + OwnedEnvelope held = OwnedEnvelope(token: token, marker: fail_marker() handle error: return nothing) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1004) handle error: + return nothing + retire_owned(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/resource_probe.jett b/crates/jett_comptime/src/resource_execution/fixtures/resource_probe.jett new file mode 100644 index 00000000..abcd67ae --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/resource_probe.jett @@ -0,0 +1,17 @@ +namespace resource_probe +export resource TestHandle +export function create(view net: Network, label: int64) returns result[TestHandle, string]: + return kernel_create(view net, label) +export function borrow(view net: Network, view token: TestHandle) returns result[int64, string]: + return kernel_borrow(view net, view token) +export function close(token: TestHandle) returns nothing: + kernel_close(token) + return nothing +export function close_descriptor() returns function(TestHandle) returns nothing: + return kernel_close +export function empty() returns optional[TestHandle]: + return none +export function terminal_failure() returns nothing: + list[int64] numbers = list(1) + list[int64] removed = list.__remove_at[int64](numbers, -1) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/interpreter_transport.rs b/crates/jett_comptime/src/resource_execution/interpreter_transport.rs new file mode 100644 index 00000000..f63e5cc2 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/interpreter_transport.rs @@ -0,0 +1,1812 @@ +//! Envelope transport in the existing checked source evaluator. +#[path = "interpreter_transport/assignment.rs"] +mod assignment; + +#[path = "interpreter_transport/absent_generated.rs"] +mod absent_generated; + +#[path = "interpreter_transport/owned_struct.rs"] +mod owned_struct; + +use super::*; +use crate::resource_execution::FunctionInvocation; +use crate::resource_execution::{ + CheckedInvocation, EvaluatedValue, ExecutionPurpose, FrameId, OperationFrame, PayloadStep, + PreparedIntrinsicArguments, ResourceHookDescriptor, ResourceTransport, +}; +use jett_parser::ast::VarDecl; +use jett_resolve::DefId; +use jett_typecheck::{CheckedCalleeAccess, CheckedInvocationTarget}; +use jett_types::Type; + +#[path = "interpreter_transport/entry_context.rs"] +mod entry_context; +use entry_context::SavedResourceEntryContext; + +#[path = "interpreter_transport/intrinsic.rs"] +mod intrinsic; +#[path = "interpreter_transport/pipeline.rs"] +mod pipeline; +#[path = "interpreter_transport/required.rs"] +mod required; + +// Runtime registration identifies a retained declaration; its cloned body is +// deliberately excluded because only the retained checked body is executed. +fn same_registered_function_header(left: &FunctionDef, right: &FunctionDef) -> bool { + fn ident(left: &Ident, right: &Ident) -> bool { + left.name == right.name && left.span == right.span + } + fn types(left: &[TypeExpr], right: &[TypeExpr]) -> bool { + left.len() == right.len() && left.iter().zip(right).all(|(left, right)| ty(left, right)) + } + fn ty(left: &TypeExpr, right: &TypeExpr) -> bool { + match (left, right) { + (TypeExpr::Named(left), TypeExpr::Named(right)) => ident(left, right), + ( + TypeExpr::Generic(left, left_args, left_span), + TypeExpr::Generic(right, right_args, right_span), + ) => left_span == right_span && ident(left, right) && types(left_args, right_args), + (TypeExpr::View(left, left_span), TypeExpr::View(right, right_span)) => { + left_span == right_span && ty(left, right) + } + ( + TypeExpr::Function(left, left_result, left_span), + TypeExpr::Function(right, right_result, right_span), + ) => left_span == right_span && types(left, right) && ty(left_result, right_result), + ( + TypeExpr::StateQualified(left, left_state, left_span), + TypeExpr::StateQualified(right, right_state, right_span), + ) => left_span == right_span && ty(left, right) && ident(left_state, right_state), + _ => false, + } + } + left.span == right.span + && ident(&left.name, &right.name) + && left.exported == right.exported + && left.body.span == right.body.span + && left.type_params.len() == right.type_params.len() + && left + .type_params + .iter() + .zip(&right.type_params) + .all(|(left, right)| ident(left, right)) + && left.params.len() == right.params.len() + && left.params.iter().zip(&right.params).all(|(left, right)| { + left.span == right.span + && left.view == right.view + && left.mutable == right.mutable + && ident(&left.name, &right.name) + && ty(&left.ty, &right.ty) + }) + && match (&left.return_type, &right.return_type) { + (None, None) => true, + (Some(left), Some(right)) => ty(left, right), + _ => false, + } +} + +impl Interpreter { + pub(super) fn check_resource_cleanup(&mut self) -> Result<(), String> { + match &mut self.resource_transport { + Some(transport) => transport.check_cleanup().map_err(|error| error.to_string()), + None => Ok(()), + } + } + + pub(super) fn prepare_checked_resource_field_loop( + &self, + source: &jett_parser::ast::ForStmt, + ) -> Result>, String> { + let Some(transport) = self + .resource_transport + .as_ref() + .filter(|transport| transport.checked_source_active) + else { + return Ok(None); + }; + transport + .checked + .prepare_reflected_field_loop(source) + .map_err(|error| error.to_string()) + } + + pub(super) fn exec_checked_resource_type_bind( + &mut self, + bind: &jett_parser::ast::ComptimeTypeBindStmt, + ) -> Result, String> { + // Every fallible source/type/body join precedes interpreter mutation. + let checked = &self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked; + let prepared = match &bind.value { + Expr::FieldAccess(base, member, _) if member.name == "type_info" => { + let Expr::Ident(variable) = base.as_ref() else { + return Err("resource execution has no exact checked body".to_string()); + }; + let definition = checked + .program() + .resolved() + .resolutions + .get(&variable.span) + .ok_or("resource execution has no exact checked body")?; + let iteration = self + .checked_reflected_fields + .iter() + .rev() + .find(|proof| proof.variable() == *definition) + .ok_or("resource execution has no exact checked body")?; + let current = self + .get_variable(&variable.name) + .ok_or("resource execution has no exact checked body")?; + iteration + .validate_value(current) + .map_err(|error| error.to_string())?; + checked.prepare_reflected_field_scope(bind, iteration) + } + _ => checked.prepare_direct_scope(bind), + } + .map_err(|error| error.to_string())?; + let body = prepared.body().map_err(|error| error.to_string())?; + let bound_type_expr = + Self::simple_type_expr_from_name(&prepared.reflection().type_name, bind.value.span()) + .ok_or("checked scoped type has no canonical interpreter syntax")?; + let projection = prepared.projection(); + let binding = ClosureScopedTypeBinding { + name: bind.name.name.clone(), + canonical_name: prepared.bound_name().to_string(), + reflection: Some(prepared.reflection().clone()), + }; + let saved_cursor = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .cursor(); + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .checked + .install_direct_scope(&prepared) + .map_err(|error| error.to_string())?; + let saved_checked_scope = self.active_checked_scope.replace(projection); + self.scoped_type_bindings.push(binding); + self.type_arg_scopes + .push(HashMap::from([(bind.name.name.clone(), bound_type_expr)])); + let result = self.exec_block_inner(body); + self.type_arg_scopes.pop(); + self.scoped_type_bindings.pop(); + self.active_checked_scope = saved_checked_scope; + let cleanup = self.check_resource_cleanup(); + let restore = match &mut self.resource_transport { + Some(transport) => transport + .checked + .restore_cursor(saved_cursor) + .map_err(|error| error.to_string()), + None => Err("missing checked Resource transport".to_string()), + }; + match (result, cleanup, restore) { + (_, Err(error), _) => Err(error), + (Err(error), Ok(()), _) | (_, Ok(()), Err(error)) => Err(error), + (Ok(value), Ok(()), Ok(())) => Ok(value), + } + } + + fn resource_flow(value: EvaluatedValue) -> ExprFlow { + if value.custody.is_empty() && !value.value.contains_live_resource_or_grant() { + ExprFlow::Value(value.value) + } else { + ExprFlow::Resource(value) + } + } + + fn resource_envelope(flow: ExprFlow) -> Result, String> { + match flow { + ExprFlow::Value(value) if !value.contains_live_resource_or_grant() => { + Ok(Ok(EvaluatedValue::ordinary(value))) + } + ExprFlow::Value(_) => { + Err("physical Resource carrier has no owning or borrowed envelope".to_string()) + } + ExprFlow::Resource(value) => Ok(Ok(value)), + ExprFlow::Signal(signal) => Ok(Err(signal)), + } + } + + pub(super) fn resource_type_at(&self, span: Span) -> Result { + let transport = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")?; + let ty = transport + .checked + .expression_type(span) + .map_err(|error| error.to_string())?; + transport + .checked + .type_contains_resource(ty) + .map_err(|error| error.to_string()) + } + + pub(super) fn eval_resource_transport( + &mut self, + expression: &Expr, + ) -> Result, String> { + let transport = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")?; + if !transport.checked_source_active { + return Ok(None); + } + if matches!(expression, Expr::Call(..)) { + if let Some(flow) = self.eval_resource_owned_struct(expression)? { + return Ok(Some(flow)); + } + } + let transport = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")?; + match expression { + Expr::Ident(identifier) => { + let definition = transport + .checked + .resolved_definition(identifier.span) + .map_err(|error| error.to_string())?; + if transport.checked.has_hook(definition) { + let descriptor = transport + .checked + .descriptor(definition) + .map_err(|error| error.to_string())?; + return Ok(Some(ExprFlow::Value(Value::ResourceHook(descriptor)))); + } + if transport.has_binding(definition) { + let value = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .binding(definition, false) + .map_err(|error| error.to_string())?; + return Ok(Some(Self::resource_flow(value))); + } + if self.resource_type_at(identifier.span)? + && transport + .checked + .prepare_named_callable(expression) + .map_err(|error| error.to_string())? + .is_none() + { + return Err("Resource source binding has no live cleanup custody".to_string()); + } + Ok(None) + } + Expr::View(inner, _) => { + let bare = Self::unparenthesized(inner); + if let Expr::Ident(identifier) = bare { + let definition = transport + .checked + .resolved_definition(identifier.span) + .map_err(|error| error.to_string())?; + if transport.has_binding(definition) { + let value = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .binding(definition, true) + .map_err(|error| error.to_string())?; + return Ok(Some(Self::resource_flow(value))); + } + } + // A written view of an owned producer retains one operation + // backing ticket; formal binding creates only the borrow. + let flow = self.eval_expr_flow(inner)?; + Ok(Some(flow)) + } + Expr::Paren(inner, _) => self.eval_expr_flow(inner).map(Some), + Expr::Ok(inner, _) | Expr::Fail(inner, _) | Expr::Some(inner, _) + if self.resource_type_at(expression.span())? => + { + let mut value = match Self::resource_envelope(self.eval_expr_flow(inner)?)? { + Ok(value) => value, + Err(signal) => return Ok(Some(ExprFlow::Signal(signal))), + }; + let step = match expression { + Expr::Ok(..) => PayloadStep::Ok, + Expr::Fail(..) => PayloadStep::Fail, + Expr::Some(..) => PayloadStep::Some, + _ => return Err("invalid checked sum transport".to_string()), + }; + value = value.prefix(step.clone()); + value.value = match step { + PayloadStep::Ok => Value::ResultOk(Box::new(value.value)), + PayloadStep::Fail => Value::ResultFail(Box::new(value.value)), + PayloadStep::Some => Value::OptionalSome(Box::new(value.value)), + _ => return Err("invalid checked sum transport".to_string()), + }; + Ok(Some(Self::resource_flow(value))) + } + Expr::Handle(target, binding, body, _) if self.resource_type_at(target.span())? => { + let mut target = match Self::resource_envelope(self.eval_expr_flow(target)?)? { + Ok(value) => value, + Err(signal) => return Ok(Some(ExprFlow::Signal(signal))), + }; + // Typed storage is a separate custody path; this operation does + // not erase nominal/secret policy. Checker-selected sum typing + // already establishes the exact occupied arm. + while let Value::Typed { value, .. } = target.value { + target.value = *value; + target + .remove_prefix(PayloadStep::Typed) + .map_err(|error| error.to_string())?; + } + match target.value { + Value::ResultOk(value) => { + target.value = *value; + target + .remove_prefix(PayloadStep::Ok) + .map_err(|error| error.to_string())?; + Ok(Some(Self::resource_flow(target))) + } + Value::OptionalSome(value) => { + target.value = *value; + target + .remove_prefix(PayloadStep::Some) + .map_err(|error| error.to_string())?; + Ok(Some(Self::resource_flow(target))) + } + Value::ResultFail(error) => { + if !target.custody.is_empty() { + return Err( + "Resource-bearing error custody has no selected handler transport" + .to_string(), + ); + } + self.exec_resource_handle(binding.as_ref(), Some(*error), body) + .map(Some) + } + Value::OptionalNone => { + if !target.custody.is_empty() { + return Err("absent Resource sum retained a live owner".to_string()); + } + self.exec_resource_handle(None, None, body).map(Some) + } + _ => Err("checked Resource handle has no exact sum carrier".to_string()), + } + } + Expr::Default(inner, _) if self.resource_type_at(inner.span())? => { + let mut value = match Self::resource_envelope(self.eval_expr_flow(inner)?)? { + Ok(value) => value, + Err(signal) => return Ok(Some(ExprFlow::Signal(signal))), + }; + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .preserve_default(&mut value) + .map_err(|error| error.to_string())?; + Ok(Some(ExprFlow::Signal(Signal::ResourceDefault(value)))) + } + Expr::Comptime(inner, _) => { + let previous = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .checked + .replace_purpose(ExecutionPurpose::ExplicitComptime); + let result = self.eval_expr_flow(inner); + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .checked + .replace_purpose(previous); + let flow = result?; + if matches!(&flow, ExprFlow::Resource(_)) { + return Err( + "required evaluation cannot produce live Resource custody".to_string() + ); + } + Ok(Some(flow)) + } + Expr::Pipeline(..) => self.eval_resource_pipeline(expression).map(Some), + // All other ordinary expressions remain the established evaluator. + // Its operand extraction rejects a transported Resource envelope. + _ => Ok(None), + } + } + + pub(super) fn exec_resource_binding( + &mut self, + declaration: &VarDecl, + ) -> Result, String> { + let value = match Self::resource_envelope(self.eval_expr_flow(&declaration.value)?)? { + Ok(value) => value, + Err(signal) => return Ok(Some(signal)), + }; + // The checked source type and occupied payload paths remain unchanged; + // opaque carriers do not enter ordinary clone/retag normalization. + let physical = value.value.clone(); + let ty = self.substitute_type_expr(&declaration.ty); + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .install_binding(declaration.name.span, value) + .map_err(|error| error.to_string())?; + self.set_variable_with_type(&declaration.name.name, physical, ty); + Ok(None) + } + + fn exec_resource_handle( + &mut self, + binding: Option<&Ident>, + value: Option, + body: &Block, + ) -> Result { + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .enter_default() + .map_err(|error| error.to_string())?; + let result = self.exec_handle_block(binding, value, body, None); + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .leave_default(); + result + } + + fn finish_resource_operation( + &mut self, + operation: OperationFrame, + result: Result, + ) -> Result { + // A handler Return already retired its exact acquired suffix before + // evaluating its operand. Its suspended envelopes acknowledge once; + // ordinary success and output adoption never consume that authority. + if matches!( + &result, + Err(_) + | Ok(ExprFlow::Signal( + Signal::Return(..) | Signal::ResourceReturn(..) + )) + ) && self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .acknowledge_return_operation(&operation) + .map_err(|error| error.to_string())? + { + return result; + } + let mut result = result; + let value = match &mut result { + Ok(ExprFlow::Resource(value)) => Some(value), + _ => None, + }; + let cleanup = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .end_operation(operation, value) + .map_err(|error| error.to_string()); + match (result, cleanup) { + (_, Err(error)) => Err(error), + (Err(error), Ok(())) => Err(error), + (Ok(flow), Ok(())) => Ok(flow), + } + } + + pub(super) fn eval_resource_call( + &mut self, + callee: &Expr, + type_arguments: &[TypeExpr], + arguments: &[CallArg], + span: Span, + ) -> Result { + let invocation = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .invocation(span) + .map_err(|error| error.to_string())?; + if invocation.arguments().len() != arguments.len() + || invocation + .arguments() + .iter() + .zip(arguments) + .any(|(fact, argument)| fact.source_span != argument.value.span()) + { + return Err("checked call lost its original source argument occurrence".to_string()); + } + let intrinsic = if matches!(invocation.target(), CheckedInvocationTarget::Intrinsic(_)) { + Some( + self.resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .prepare_intrinsic_arguments(callee, type_arguments, arguments, &invocation) + .map_err(|error| error.to_string())?, + ) + } else { + None + }; + // Variant construction has a checked function-shaped packet, but its + // Source endpoint is not a runtime callable value. Prepare its exact + // nominal schema before evaluating any actuals. + let constructor = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .enum_constructor(Self::unparenthesized(callee), &invocation) + .map_err(|error| error.to_string())?; + let operation = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .begin_operation() + .map_err(|error| error.to_string())?; + let result = (|| { + let mut actuals = Vec::with_capacity(arguments.len()); + for (argument, fact) in arguments.iter().zip(invocation.arguments()) { + let mut value = + match Self::resource_envelope(self.eval_expr_flow(&argument.value)?)? { + Ok(value) => value, + Err(signal) => return Ok(ExprFlow::Signal(signal)), + }; + let transport = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")?; + transport + .validate_actual(&value, fact.effect) + .map_err(|error| error.to_string())?; + transport + .hold_actual(&mut value) + .map_err(|error| error.to_string())?; + actuals.push(value); + } + self.dispatch_resource_actuals( + &invocation, + callee, + type_arguments, + actuals, + constructor, + operation.frame, + intrinsic.as_ref(), + ) + })(); + self.finish_resource_operation(operation, result) + } + + // Both routes provide original checked identity and already-evaluated + // source-order envelopes. This dispatcher never re-evaluates an actual. + fn dispatch_resource_actuals( + &mut self, + invocation: &CheckedInvocation, + callee: &Expr, + type_arguments: &[TypeExpr], + actuals: Vec, + constructor: Option<(String, String, jett_types::TypeId)>, + return_destination: FrameId, + intrinsic: Option<&PreparedIntrinsicArguments>, + ) -> Result { + if matches!(invocation.target(), CheckedInvocationTarget::Intrinsic(_)) + != intrinsic.is_some() + { + return Err("checked intrinsic lost its prepared concrete argument packet".to_string()); + } + if let Some(prepared) = intrinsic { + prepared + .validate( + &self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked, + invocation, + ) + .map_err(|error| error.to_string())?; + } + if actuals.len() != invocation.arguments().len() { + return Err("checked call lost its evaluated source argument count".to_string()); + } + // Source lexical order is retained above. Ownership adoption below + // follows that same order even when destination parameters permute. + let mut formal: Vec> = (0..actuals.len()).map(|_| None).collect(); + for (actual, fact) in actuals.into_iter().zip(invocation.arguments()) { + let borrowed = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .formal_value(&actual, fact.callee_access) + .map_err(|error| error.to_string())?; + formal[fact.parameter_index] = Some(borrowed.unwrap_or(actual)); + } + let formal = formal + .into_iter() + .map(|value| value.ok_or("checked call has no exact parameter value".to_string())) + .collect::, _>>()?; + if let Some((type_name, variant, ty)) = constructor { + if formal.iter().any(|value| { + !value.custody.is_empty() || value.value.contains_live_resource_or_grant() + }) { + return Err( + "occupied Resource enum construction has no selected custody transport" + .to_string(), + ); + } + let value = EvaluatedValue::ordinary(Value::Enum { + type_name, + variant, + fields: formal.into_iter().map(|value| value.value).collect(), + }); + self.resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .validate_value_type(&value, ty) + .map_err(|error| error.to_string())?; + return Ok(Self::resource_flow(value)); + } + let bare_callee = Self::unparenthesized(callee); + match invocation.target() { + CheckedInvocationTarget::Resolved(definition) + if self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .has_hook(*definition) => + { + let resolved = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .resolved_definition(bare_callee.span()) + .map_err(|error| error.to_string())?; + if resolved != *definition { + return Err("checked Resource hook lost its exact declaration".to_string()); + } + let descriptor = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .descriptor(*definition) + .map_err(|error| error.to_string())?; + let value = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .invoke_hook(invocation, &descriptor, formal) + .map_err(|error| error.to_string())?; + Ok(Self::resource_flow(value)) + } + CheckedInvocationTarget::Resolved(definition) + | CheckedInvocationTarget::Generic(jett_typecheck::CheckedGenericCall { + definition, + .. + }) => { + let name = self.exact_registered_resource_function(*definition)?; + let resolved_arguments = match invocation.target() { + CheckedInvocationTarget::Generic(call) => call + .concrete_args + .iter() + .map(|ty| { + let types = &self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .program() + .checked() + .interner; + Self::debug_type(&types.type_name(*ty)).ok_or( + "checked generic argument has no canonical interpreter type" + .to_string(), + ) + }) + .collect::, String>>()?, + _ => type_arguments + .iter() + .map(|ty| self.substitute_type_expr(ty)) + .collect(), + }; + let value = self.call_registered_function_envelopes( + &name, + &resolved_arguments, + formal, + &FunctionInvocation::Source(invocation), + return_destination, + )?; + Ok(Self::resource_flow(value)) + } + CheckedInvocationTarget::Indirect(_) => { + let function = match Self::resource_envelope(self.eval_expr_flow(bare_callee)?)? { + Ok(value) => value, + Err(signal) => return Ok(ExprFlow::Signal(signal)), + }; + match function.value { + Value::ResourceHook(descriptor) if function.custody.is_empty() => { + let value = self.resource_transport.as_mut().ok_or("missing checked Resource transport")? + .invoke_hook(invocation, &descriptor, formal).map_err(|error| error.to_string())?; + Ok(Self::resource_flow(value)) + } + Value::NamedFunction(name) if function.custody.is_empty() + && (formal.iter().any(|argument| !argument.custody.is_empty() || argument.value.contains_live_resource_or_grant()) + || self.resource_type_at(bare_callee.span())?) => { + let target = self.resource_transport.as_ref().ok_or("missing checked Resource transport")? + .checked.prepare_named_indirect(invocation, bare_callee).map_err(|error| error.to_string())?; + let expected = self.exact_registered_resource_function(target.definition())?; + if name != expected { + return Err("named Resource callable differs from its exact original target".to_string()); + } + let value = self.call_registered_function_envelopes( + &expected, &[], formal, + &FunctionInvocation::NamedSource { source: invocation, target: &target }, + return_destination, + )?; + Ok(Self::resource_flow(value)) + } + value if function.custody.is_empty() && !value.contains_live_resource_or_grant() + && formal.iter().all(|argument| argument.custody.is_empty() && !argument.value.contains_live_resource_or_grant()) => { + Ok(ExprFlow::Value(self.call_fn_value_from_source(value, formal.into_iter().map(|value| value.value).collect(), None)?)) + } + _ => Err("Resource-bearing captured invocation has no checked custody transfer proof".to_string()), + } + } + CheckedInvocationTarget::Intrinsic(_) => { + if formal.iter().any(|value| { + !value.custody.is_empty() || value.value.contains_live_resource_or_grant() + }) { + return Err( + "Resource-bearing intrinsic transport has no selected acquisition proof" + .to_string(), + ); + } + let prepared = + intrinsic.ok_or("checked intrinsic has no prepared concrete arguments")?; + let values = formal.into_iter().map(|value| value.value).collect(); + Ok(ExprFlow::Value( + self.call_resource_intrinsic(invocation, prepared, values)?, + )) + } + CheckedInvocationTarget::Method(_) | CheckedInvocationTarget::Interface(_) => { + if formal.iter().any(|value| { + !value.custody.is_empty() || value.value.contains_live_resource_or_grant() + }) { + return Err("Resource-bearing intrinsic/method transport has no selected acquisition proof".to_string()); + } + // Evaluate the selected ordinary operation with the already + // evaluated actual values. No source actual is evaluated twice. + let values = formal + .into_iter() + .map(|value| value.value) + .collect::>(); + let name = Self::dotted_expr_name(bare_callee) + .ok_or("ordinary checked intrinsic has no source operation")?; + Ok(ExprFlow::Value(self.call_function_from_resolved_source( + &name, + type_arguments, + values, + None, + )?)) + } + } + } + + fn exact_registered_resource_function(&self, definition: DefId) -> Result { + let transport = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")?; + let original = transport + .checked + .retained_function(definition) + .map_err(|error| error.to_string())?; + let (namespace, _) = transport + .checked + .source_function_context(original.name.span) + .map_err(|error| error.to_string())?; + let mut matches = Vec::new(); + for (name, registered) in &self.functions { + if transport + .checked + .declaration_definition(registered.definition.name.span) + .ok() + != Some(definition) + { + continue; + } + if registered.namespace != namespace + || !same_registered_function_header(®istered.definition, original) + { + return Err( + "checked registration differs from its retained source declaration".to_string(), + ); + } + matches.push(name.clone()); + } + let [name] = matches.as_slice() else { + return Err("checked invocation has no exact retained source function".to_string()); + }; + Ok(name.clone()) + } +} + +impl Interpreter { + fn call_registered_function_envelopes( + &mut self, + name: &str, + type_args: &[TypeExpr], + args: Vec, + invocation: &FunctionInvocation, + return_destination: FrameId, + ) -> Result { + let resolved_name = name.to_string(); + let physical_arguments = args + .iter() + .map(|argument| argument.value.clone()) + .collect::>(); + + // Look up the function definition. + let registered = self + .functions + .get(&resolved_name) + .ok_or_else(|| { + let name = self + .registry_name(&self.interface_methods, name) + .unwrap_or_else(|| name.to_string()); + format!("undefined function '{name}'") + })? + .clone(); + let transport = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")?; + let definition = match invocation { + FunctionInvocation::Entry { definition, .. } => *definition, + FunctionInvocation::NamedSource { target, .. } => target.definition(), + FunctionInvocation::Source(invocation) => match invocation.target() { + CheckedInvocationTarget::Resolved(definition) => *definition, + CheckedInvocationTarget::Generic(call) => call.definition, + _ => { + return Err("checked source call has no exact retained declaration".to_string()); + } + }, + }; + let original = transport + .checked + .retained_function(definition) + .map_err(|error| error.to_string())?; + let (namespace, trusted_stdlib) = transport + .checked + .source_function_context(original.name.span) + .map_err(|error| error.to_string())?; + if transport + .checked + .declaration_definition(registered.definition.name.span) + .map_err(|error| error.to_string())? + != definition + || registered.namespace != namespace + || !same_registered_function_header(®istered.definition, original) + { + return Err( + "checked registration differs from its retained source declaration".to_string(), + ); + } + // The original checked AST supplies both header and executable body. + // A host-edited registration clone is never executable authority. + let body_reference = transport + .checked + .prepare_function_body(invocation) + .map_err(|error| error.to_string())?; + let func = body_reference + .function() + .map_err(|error| error.to_string())?; + + if args.len() != func.params.len() { + return Err(format!( + "function '{}' expects {} argument(s), got {}", + resolved_name, + func.params.len(), + args.len() + )); + } + + let type_scope = self.type_scope_for_function(&func, type_args)?; + let arguments: Vec = func + .type_params + .iter() + .map(|param| type_scope[¶m.name].clone()) + .collect(); + let captured_arguments = self.captured_type_arguments(&arguments); + let expression_types = match &self.checked_expression_types { + Some(types) => types + .select(func.name.span, &captured_arguments, &physical_arguments) + .map_err(|error| format!("{resolved_name}: {error}"))?, + None => None, + }; + let saved_resource_cursor = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .cursor(); + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .checked + .install_function_body(&body_reference) + .map_err(|error| error.to_string())?; + let saved_expression_types = + std::mem::replace(&mut self.active_checked_function, expression_types); + let saved_type_arguments = std::mem::replace(&mut self.current_type_arguments, arguments); + let saved_scoped_types = std::mem::take(&mut self.scoped_type_bindings); + let saved_checked_scope = self.active_checked_scope.take(); + let saved_reflected_fields = std::mem::take(&mut self.checked_reflected_fields); + // Resolve arguments above in the caller, then keep its type bindings + // out of the callee's lexical scope, including non-generic callees. + let saved_type_scopes = std::mem::replace(&mut self.type_arg_scopes, vec![type_scope]); + + let saved_namespace = self.current_namespace.clone(); + let saved_trusted_stdlib = self.current_function_trusted_stdlib; + self.current_namespace = namespace; + self.current_function_trusted_stdlib = trusted_stdlib; + + let scope_depth = self.scopes.len(); + let saved_scope_floor = self.lexical_scope_floor; + self.lexical_scope_floor = scope_depth; + self.push_scope(); + let saved_proofs = self.allow_checked_refinement_proofs; + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .enter_return(return_destination); + let call_result = (|| { + let mut arguments = args.into_iter().map(Some).collect::>(); + // Install each formal holder in SOURCE acquisition order. Formal + // permutation affects lookup only, never reverse cleanup ordering. + let contracts = invocation + .parameters( + &self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked, + ) + .map_err(|error| error.to_string())?; + for fact in &contracts { + let index = fact.index; + let param = func + .params + .get(index) + .ok_or("checked call has no exact source parameter")?; + if param.view != (fact.access == CheckedCalleeAccess::View) { + return Err( + "checked source parameter access differs from retained declaration" + .to_string(), + ); + } + let mut arg = arguments + .get_mut(index) + .and_then(Option::take) + .ok_or("checked call repeated a formal value")?; + let param_ty = self.substitute_type_expr(¶m.ty); + if arg.value.contains_live_resource_or_grant() + || !arg.custody.is_empty() + || self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .type_contains_resource(fact.ty) + .map_err(|error| error.to_string())? + { + self.resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .validate_value_type(&arg, fact.ty) + .map_err(|error| error.to_string())?; + } else { + let source_type = match invocation { + FunctionInvocation::Source(call) + | FunctionInvocation::NamedSource { source: call, .. } => call + .arguments() + .iter() + .find(|argument| argument.parameter_index == index) + .map(|argument| { + self.resource_transport + .as_ref() + .ok_or("missing checked Resource transport") + .map(|transport| { + transport + .checked + .program() + .checked() + .interner + .type_name(argument.actual_type) + }) + }) + .transpose()?, + FunctionInvocation::Entry { .. } => None, + }; + arg.value = self.normalize_and_validate_value_from_source( + ¶m_ty, + arg.value, + source_type.as_deref(), + )?; + } + let physical = arg.value.clone(); + if arg.value.contains_live_resource_or_grant() + || !arg.custody.is_empty() + || self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .type_contains_resource(fact.ty) + .map_err(|error| error.to_string())? + { + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .install_binding(param.name.span, arg) + .map_err(|error| error.to_string())?; + } + self.set_variable_with_type(¶m.name.name, physical, param_ty); + } + + let result = self.exec_block_inner(&func.body)?; + let (mut value, return_source_type) = match result { + Some(Signal::ResourceReturn(value, source_type)) => (value, source_type), + Some(Signal::Return(value, source_type)) => { + (EvaluatedValue::ordinary(value), source_type) + } + Some(Signal::Default(_) | Signal::ResourceDefault(_)) => { + return Err("`default` can only be used inside a `handle` block".to_string()); + } + _ => (EvaluatedValue::ordinary(Value::Nothing), None), + }; + if let Some(return_type) = &func.return_type { + let return_type = self.substitute_type_expr(return_type); + let signature_type = invocation + .signature( + &self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked, + ) + .map_err(|error| error.to_string())?; + let Type::Function { + return_type: checked_return, + .. + } = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .program() + .checked() + .interner + .resolve(signature_type) + else { + return Err("source function has no exact checked result type".to_string()); + }; + let checked_return = *checked_return; + if value.value.contains_live_resource_or_grant() + || !value.custody.is_empty() + || self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .type_contains_resource(checked_return) + .map_err(|error| error.to_string())? + { + self.resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .validate_value_type(&value, checked_return) + .map_err(|error| error.to_string())?; + } else { + value.value = self.normalize_and_validate_value_from_source( + &return_type, + value.value, + return_source_type.as_deref(), + )?; + } + } + + Ok(value) + })(); + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .leave_return(); + self.allow_checked_refinement_proofs = saved_proofs; + while self.scopes.len() > scope_depth { + self.pop_scope(); + } + self.lexical_scope_floor = saved_scope_floor; + self.active_checked_scope = saved_checked_scope; + self.checked_reflected_fields = saved_reflected_fields; + self.scoped_type_bindings = saved_scoped_types; + self.type_arg_scopes = saved_type_scopes; + self.current_type_arguments = saved_type_arguments; + self.active_checked_function = saved_expression_types; + self.current_namespace = saved_namespace; + self.current_function_trusted_stdlib = saved_trusted_stdlib; + let cleanup = self.check_resource_cleanup(); + let restore = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .checked + .restore_cursor(saved_resource_cursor) + .map_err(|error| error.to_string()); + match (call_result, cleanup, restore) { + (_, Err(error), _) => Err(error), + (Err(error), Ok(()), _) | (_, Ok(()), Err(error)) => Err(error), + (Ok(value), Ok(()), Ok(())) => Ok(value), + } + } +} + +impl Interpreter { + /// Install one compiler-derived snapshot. Production providers remain disabled. + /// Debug emission is configured independently and grants no runtime purpose. + pub fn from_checked_resource_program( + program: Arc, + purpose: ExecutionPurpose, + ) -> Result { + let mut interpreter = Self::new(); + interpreter.register_module(program.module()); + interpreter.install_checked_resource_program(program, purpose)?; + Ok(interpreter) + } + + pub fn install_checked_resource_program( + &mut self, + program: Arc, + purpose: ExecutionPurpose, + ) -> Result<(), String> { + if self.retained_resource_program.is_some() { + return Err("checked Resource program is already installed".to_string()); + } + if !program.checked().resource_hooks.is_empty() { + self.resource_transport = Some( + ResourceTransport::checked_only(program.clone(), purpose, self.scopes.len()) + .map_err(|error| error.to_string())?, + ); + } + self.retained_resource_program = Some((program, purpose)); + Ok(()) + } + + pub(crate) fn authorize_checked_resource_worker( + &mut self, + module: &Module, + purpose: ExecutionPurpose, + ) -> Result<(), String> { + let (program, selected) = self + .retained_resource_program + .as_ref() + .ok_or("worker has no retained checked program")?; + if purpose == ExecutionPurpose::ReferenceRuntime + || *selected != purpose + || !std::ptr::eq(module, program.module()) + { + return Err( + "required worker has no exact retained source identity and purpose".to_string(), + ); + } + if let Some(transport) = &mut self.resource_transport { + transport.checked_source_active = true; + } + Ok(()) + } + + pub(crate) fn checked_execution_purpose( + &mut self, + purpose: ExecutionPurpose, + ) -> Option { + let (_, current) = self.retained_resource_program.as_mut()?; + let previous = std::mem::replace(current, purpose); + if let Some(transport) = &mut self.resource_transport { + transport.checked.replace_purpose(purpose); + } + Some(previous) + } + + /// Checked entry is an exact declaration, not a source-name hook dispatcher. + /// Incoming raw live Resource carriers are never accepted as owning inputs. + pub fn call_checked_program_entry( + &mut self, + definition: DefId, + arguments: Vec, + ) -> Result { + if arguments.iter().any(|argument| { + matches!(argument, Value::Resource(_)) + || (argument.contains_live_resource_or_grant() + && !matches!(argument, Value::GrantedNetwork(_))) + }) { + return Err( + "checked entry cannot adopt custody from a copied Resource carrier".to_string(), + ); + } + let transport = self + .resource_transport + .as_ref() + .ok_or("checked entry has no retained program")?; + if transport.checked.purpose() != ExecutionPurpose::ReferenceRuntime + && arguments.iter().any(Value::contains_live_resource_or_grant) + { + return Err( + "required worker cannot import live runtime authority or Resource custody" + .to_string(), + ); + } + transport + .validate_entry_scopes(self.scopes.len()) + .map_err(|error| error.to_string())?; + let invocation = transport + .checked + .entry(definition) + .map_err(|error| error.to_string())?; + let name = self.exact_registered_resource_function(definition)?; + let saved_context = SavedResourceEntryContext::capture(self)?; + let scope_depth = saved_context.scope_depth(); + let operation = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .begin_operation() + .map_err(|error| error.to_string())?; + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .checked_source_active = true; + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let arguments = arguments + .into_iter() + .map(EvaluatedValue::ordinary) + .collect(); + let value = self.call_registered_function_envelopes( + &name, + &[], + arguments, + &invocation, + operation.frame, + )?; + let flow = self.finish_resource_operation(operation, Ok(Self::resource_flow(value)))?; + match flow { + ExprFlow::Value(value) if !value.contains_live_resource_or_grant() => Ok(value), + _ => Err( + "checked entry cannot export live custody through a raw Value result" + .to_string(), + ), + } + })); + let cleanup = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .unwind_all() + .map_err(|error| error.to_string()) + })); + // A panic may skip ordinary callee/scope restoration. Restore the exact + // entry metadata before choosing/resuming any result or cleanup outcome. + let restore = saved_context.restore(self); + let completion = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .restore_entry_scopes(scope_depth) + .map_err(|error| error.to_string()) + })); + let cleanup = Self::complete_resource_cleanup(cleanup, completion); + let result = match (result, restore) { + (Ok(Ok(_)), Err(error)) => Ok(Err(error)), + (result, _) => result, + }; + Self::complete_resource_entry(result, cleanup) + } + + fn complete_resource_cleanup( + cleanup: std::thread::Result>, + completion: std::thread::Result>, + ) -> std::thread::Result> { + match (cleanup, completion) { + (Err(panic), completion) => { + if let Err(other) = completion { + std::mem::forget(other); + } + Err(panic) + } + (Ok(Err(error)), completion) => { + if let Err(panic) = completion { + std::mem::forget(panic); + } + Ok(Err(error)) + } + (Ok(Ok(())), completion) => completion, + } + } + + fn complete_resource_entry( + result: std::thread::Result>, + cleanup: std::thread::Result>, + ) -> Result { + match (result, cleanup) { + (Err(entry_panic), Err(cleanup_panic)) => { + std::mem::forget(entry_panic); + std::panic::resume_unwind(cleanup_panic) + } + (Ok(_), Err(cleanup_panic)) => std::panic::resume_unwind(cleanup_panic), + (Err(entry_panic), Ok(Err(cleanup_error))) => { + std::mem::forget(entry_panic); + Err(cleanup_error) + } + (Ok(_), Ok(Err(cleanup_error))) => Err(cleanup_error), + (Err(entry_panic), Ok(Ok(()))) => std::panic::resume_unwind(entry_panic), + (Ok(result), Ok(Ok(()))) => result, + } + } +} + +#[cfg(test)] +impl Interpreter { + pub(crate) fn install_resource_test_script( + &mut self, + operations: Vec, + ) -> Result { + let grant = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .install_script(operations) + .map_err(|error| error.to_string())?; + Ok(Value::GrantedNetwork(grant)) + } + pub(crate) fn resource_test_scoped_context( + &self, + ) -> Result<(bool, usize, usize, usize, Option), String> { + let transport = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")?; + Ok(( + self.active_checked_scope.is_some(), + self.scoped_type_bindings.len(), + self.current_type_arguments.len(), + self.type_arg_scopes.len(), + transport.checked.original_body_scope_depth(), + )) + } + pub(crate) fn resource_test_custody_counts(&self) -> Result<(usize, usize), String> { + let transport = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")?; + Ok((transport.live_owners(), transport.registry_live_count())) + } + pub(crate) fn resource_test_observations( + &self, + ) -> Result<(Vec, usize, usize), String> { + let transport = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")?; + Ok(( + transport + .provider_events() + .map_err(|error| error.to_string())?, + transport.live_owners(), + transport.registry_live_count(), + )) + } +} + +#[cfg(test)] +mod completion_tests { + use super::*; + use std::panic::{AssertUnwindSafe, catch_unwind}; + + #[derive(Clone, Copy)] + enum Outcome { + Success, + Error, + Panic, + } + + #[test] + fn checked_entry_completion_selects_cleanup_before_every_entry_outcome() { + for entry in [Outcome::Success, Outcome::Error, Outcome::Panic] { + for cleanup in [Outcome::Success, Outcome::Error, Outcome::Panic] { + let entry_result: std::thread::Result> = match entry { + Outcome::Success => Ok(Ok(Value::Nothing)), + Outcome::Error => Ok(Err("entry sentinel".to_string())), + Outcome::Panic => Err(Box::new("entry panic")), + }; + let cleanup_result: std::thread::Result> = match cleanup { + Outcome::Success => Ok(Ok(())), + Outcome::Error => Ok(Err("cleanup sentinel".to_string())), + Outcome::Panic => Err(Box::new("cleanup panic")), + }; + let completed = catch_unwind(AssertUnwindSafe(|| { + Interpreter::complete_resource_entry(entry_result, cleanup_result) + })); + match (entry, cleanup) { + (_, Outcome::Panic) => { + let panic = completed.expect_err("cleanup panic must win"); + assert_eq!(panic.downcast_ref::<&str>(), Some(&"cleanup panic")); + } + (_, Outcome::Error) => assert_eq!( + completed.expect("cleanup error is ordinary"), + Err("cleanup sentinel".to_string()) + ), + (Outcome::Panic, Outcome::Success) => { + let panic = completed.expect_err("clean cleanup preserves the entry panic"); + assert_eq!(panic.downcast_ref::<&str>(), Some(&"entry panic")); + } + (Outcome::Error, Outcome::Success) => assert_eq!( + completed.expect("entry error is ordinary"), + Err("entry sentinel".to_string()) + ), + (Outcome::Success, Outcome::Success) => { + assert_eq!(completed.unwrap().unwrap(), Value::Nothing) + } + } + } + } + } +} + +#[cfg(test)] +mod declaration_registration_tests { + use super::*; + use crate::resource_execution::CheckedExecution; + use jett_common::FileId; + + const QUALIFIED: &str = r#"namespace helper +export function answer() returns int64: + return 7 +namespace other +export function scenario() returns int64: + return 99 +namespace app +export function scenario() returns int64: + use helper + return helper.answer() +"#; + + const MUTUAL: &str = r#"namespace app +mutual: + function first() returns int64 + function second() returns int64 +function first() returns int64: + return second() +function second() returns int64: + return 11 +export function scenario() returns int64: + return first() +"#; + + fn target( + program: &Arc, + namespace: &str, + name: &str, + ) -> DefId { + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let definitions = program + .module() + .items + .iter() + .filter_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) && function.name.name == name => + { + let definition = checked.declaration_definition(function.name.span).unwrap(); + (program.resolved().scope_table.definitions[definition.index() as usize] + .namespace + .as_deref() + == Some(namespace)) + .then_some(definition) + } + _ => None, + }) + .collect::>(); + let [definition] = definitions.as_slice() else { + panic!("unique exact retained test entry"); + }; + *definition + } + + fn runtime(source: &str, release: bool) -> (Interpreter, DefId) { + let program = crate::resource_execution::tests::program(source, release); + let definition = target(&program, "app", "scenario"); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + interpreter + .install_resource_test_script(Vec::new()) + .unwrap(); + (interpreter, definition) + } + + fn empty_before_teardown(interpreter: &mut Interpreter) { + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (Vec::new(), 0, 0) + ); + assert!(interpreter.take_debug_events().is_empty()); + } + + #[test] + fn checked_registration_calls_original_qualified_and_mutual_bodies() { + for release in [false, true] { + for (source, result) in [(QUALIFIED, 7), (MUTUAL, 11)] { + let (mut interpreter, definition) = runtime(source, release); + assert_eq!( + interpreter + .call_checked_program_entry(definition, Vec::new()) + .unwrap(), + Value::Int64(result) + ); + empty_before_teardown(&mut interpreter); + } + } + } + + #[test] + fn checked_registration_refuses_namespace_header_missing_wrong_and_duplicate_identity() { + for release in [false, true] { + for corruption in 0..5 { + let (mut interpreter, definition) = runtime(QUALIFIED, release); + match corruption { + 0 => { + interpreter + .functions + .get_mut("app.scenario") + .unwrap() + .namespace = Some("other".to_string()) + } + 1 => { + let registered = interpreter.functions.get_mut("app.scenario").unwrap(); + let function = Arc::make_mut(&mut registered.definition); + let Some(TypeExpr::Named(result)) = &mut function.return_type else { + panic!("named source result"); + }; + result.name = "string".to_string(); + } + 2 => { + interpreter.functions.remove("app.scenario"); + } + 3 => { + let wrong = interpreter.functions.get("other.scenario").unwrap().clone(); + interpreter + .functions + .insert("app.scenario".to_string(), wrong); + } + 4 => { + let duplicate = interpreter.functions.get("app.scenario").unwrap().clone(); + interpreter + .functions + .insert("host.duplicate".to_string(), duplicate); + } + _ => unreachable!(), + } + let error = interpreter + .call_checked_program_entry(definition, Vec::new()) + .expect_err("malformed registration must refuse before provider activity"); + assert_eq!( + error, + if corruption < 2 { + "checked registration differs from its retained source declaration" + } else { + "checked invocation has no exact retained source function" + } + ); + empty_before_teardown(&mut interpreter); + } + } + } + + #[test] + fn checked_registration_generic_body_uses_accepted_concrete_selection() { + const SOURCE: &str = r#"namespace app +function echo[T](value: T) returns T: + return value +export function scenario() returns int64: + return echo[int64](7) +"#; + for release in [false, true] { + let (mut interpreter, definition) = runtime(SOURCE, release); + assert_eq!( + interpreter + .call_checked_program_entry(definition, Vec::new()) + .unwrap(), + Value::Int64(7) + ); + empty_before_teardown(&mut interpreter); + } + } + + #[test] + fn checked_registration_cloned_body_cannot_replace_retained_executable_source() { + for release in [false, true] { + let (mut interpreter, definition) = runtime(QUALIFIED, release); + let registered = interpreter.functions.get_mut("helper.answer").unwrap(); + let function = Arc::make_mut(&mut registered.definition); + let Stmt::Return(result) = &mut function.body.stmts[0] else { + panic!("original return"); + }; + let original_span = result.value.as_ref().unwrap().span(); + result.value = Some(Expr::IntLiteral(99, original_span)); + // Header/body spans are unchanged; executable authority remains the + // exact retained program rather than this host-edited clone. + assert_eq!( + interpreter + .call_checked_program_entry(definition, Vec::new()) + .unwrap(), + Value::Int64(7) + ); + empty_before_teardown(&mut interpreter); + } + } +} + +#[cfg(test)] +mod entry_readiness_tests { + use super::*; + use crate::resource_execution::{ + ProviderEvent, ResourceExecutionError, ScriptOperation, tests::program, + }; + use jett_types::ResourceHookKind; + + #[test] + fn checked_host_entry_refusal_preserves_an_outer_operation_and_real_owner() { + for release in [false, true] { + let program = program( + include_str!("fixtures/25_repeated_scoped_entry.jett"), + release, + ); + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let original = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.name == "clean" + && function.name.span.file == jett_common::FileId::new(0) => + { + Some(function) + } + _ => None, + }) + .unwrap(); + let target = interpreter + .resource_transport + .as_ref() + .unwrap() + .checked + .declaration_definition(original.name.span) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ + ScriptOperation::Construct { + label: 1801, + outcome: Ok(()), + }, + ScriptOperation::Construct { + label: 1801, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1801, + outcome: Ok(21), + }, + ]) + .unwrap(); + let Value::GrantedNetwork(network) = &grant else { + panic!("actual private grant"); + }; + let runtime = interpreter.resource_transport.as_mut().unwrap(); + runtime.checked_source_active = true; + let operation = runtime.begin_operation().unwrap(); + let hook = runtime + .checked + .program() + .checked() + .resource_hooks + .values() + .find(|hook| hook.kind == ResourceHookKind::Construct) + .unwrap() + .definition; + let occurrence = *runtime + .checked + .program() + .checked() + .call_ownership + .iter() + .find(|(_, packet)| packet.target == CheckedInvocationTarget::Resolved(hook)) + .unwrap() + .0; + let invocation = runtime.checked.invocation(occurrence).unwrap(); + let descriptor = runtime.checked.descriptor(hook).unwrap(); + let owner = runtime + .invoke_hook( + &invocation, + &descriptor, + vec![ + EvaluatedValue::ordinary(Value::GrantedNetwork(network.clone())), + EvaluatedValue::ordinary(Value::Int64(1801)), + ], + ) + .unwrap(); + assert!(!owner.custody.is_empty()); + let context = interpreter.resource_test_entry_context().unwrap(); + assert_eq!( + interpreter.call_checked_program_entry(target, vec![grant.clone()]), + Err(ResourceExecutionError::InvalidFrame.to_string()) + ); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (vec![ProviderEvent::Constructed(1801)], 1, 1) + ); + interpreter + .resource_transport + .as_mut() + .unwrap() + .end_operation(operation, None) + .unwrap(); + drop(owner); + interpreter + .resource_transport + .as_mut() + .unwrap() + .checked_source_active = false; + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(1801), + ProviderEvent::Finalized(1801) + ], + 0, + 0 + ) + ); + assert_eq!( + interpreter + .call_checked_program_entry(target, vec![grant]) + .unwrap(), + Value::Int64(21) + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(1801), + ProviderEvent::Finalized(1801), + ProviderEvent::Constructed(1801), + ProviderEvent::Borrowed(1801), + ProviderEvent::Finalized(1801) + ], + 0, + 0 + ) + ); + assert!(interpreter.take_debug_events().is_empty()); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/interpreter_transport/absent_generated.rs b/crates/jett_comptime/src/resource_execution/interpreter_transport/absent_generated.rs new file mode 100644 index 00000000..7ef81da4 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/interpreter_transport/absent_generated.rs @@ -0,0 +1,104 @@ +//! The ordinary generated variable/debug copy carries no cleanup authority. +use super::*; +use crate::resource_execution::PreparedAbsentBindings; +use jett_parser::ast::{ForStmt, MatchStmt}; + +impl Interpreter { + pub(in crate::interpreter) fn eval_checked_absent_for_iterable( + &mut self, + source: &ForStmt, + ) -> Result { + if source.view { + if let Some(transport) = self + .resource_transport + .as_mut() + .filter(|transport| transport.checked_source_active) + { + // Authenticate the original For before using its stripped view spelling. + if transport + .checked + .prepare_absent_for(source) + .map_err(|error| error.to_string())? + .is_some() + { + if let Expr::Ident(name) = Self::unparenthesized(&source.iterable) { + let definition = transport + .checked + .resolved_definition(name.span) + .map_err(|error| error.to_string())?; + if transport.has_binding(definition) { + let borrowed = transport + .binding(definition, true) + .map_err(|error| error.to_string())?; + return Ok(Self::resource_flow(borrowed)); + } + } + } + } + } + self.eval_expr_flow(&source.iterable) + } + + pub(in crate::interpreter) fn prepare_absent_for_bindings<'source>( + &self, + source: &'source ForStmt, + value: &Value, + ) -> Result>, String> { + match &self.resource_transport { + Some(transport) => transport + .prepare_absent_for(source, value) + .map_err(|error| error.to_string()), + None => Ok(None), + } + } + + pub(in crate::interpreter) fn prepare_absent_match_bindings<'source>( + &self, + source: &'source MatchStmt, + arm: usize, + value: &Value, + ) -> Result>, String> { + match &self.resource_transport { + Some(transport) => transport + .prepare_absent_match(source, arm, value) + .map_err(|error| error.to_string()), + None => Ok(None), + } + } + + pub(in crate::interpreter) fn set_absent_generated_debug_binding( + &mut self, + proof: Option<&PreparedAbsentBindings<'_>>, + name: &Ident, + ordinal: usize, + value: Value, + fallback: Option<&TypeExpr>, + ) -> Result<(), String> { + if let Some(proof) = proof { + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .publish_absent_binding(proof, name, ordinal, value.clone()) + .map_err(|error| error.to_string())?; + } + self.set_inferred_debug_binding(name, value, fallback); + Ok(()) + } + + pub(in crate::interpreter) fn validate_absent_generated_debug_binding( + &self, + proof: Option<&PreparedAbsentBindings<'_>>, + name: &Ident, + ordinal: usize, + value: &Value, + ) -> Result<(), String> { + if let Some(proof) = proof { + self.resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .validate_absent_binding(proof, name, ordinal, value) + .map_err(|error| error.to_string())?; + } + Ok(()) + } +} diff --git a/crates/jett_comptime/src/resource_execution/interpreter_transport/assignment.rs b/crates/jett_comptime/src/resource_execution/interpreter_transport/assignment.rs new file mode 100644 index 00000000..8559f4d6 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/interpreter_transport/assignment.rs @@ -0,0 +1,60 @@ +//! Evaluate an owning replacement once, after preparing its exact lexical slot. +use super::*; +use jett_parser::ast::AssignStmt; + +impl Interpreter { + pub(in crate::interpreter) fn exec_resource_assignment( + &mut self, + source: &AssignStmt, + ) -> Result, String> { + let destination = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .prepare_resource_assignment(source) + .map_err(|error| error.to_string())?; + let scope_index = destination.scope_index(); + let Expr::Ident(target) = &source.target else { + return Err("Resource assignment has no original binding target".to_string()); + }; + if scope_index < self.lexical_scope_floor + || !self + .scopes + .get(scope_index) + .is_some_and(|scope| scope.contains_key(&target.name)) + { + return Err("Resource assignment has no exact lexical storage slot".to_string()); + } + let operation = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .begin_operation() + .map_err(|error| error.to_string())?; + let result = (|| { + let value = match Self::resource_envelope(self.eval_expr_flow(&source.value)?)? { + Ok(value) => value, + Err(signal) => return Ok(ExprFlow::Signal(signal)), + }; + // Physical mirroring carries no owning custody and deliberately + // bypasses ordinary clone/retag/refinement normalization. + let physical = value.value.clone(); + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .replace_resource_assignment(destination, value) + .map_err(|error| error.to_string())?; + let scope = self + .scopes + .get_mut(scope_index) + .ok_or("Resource assignment lost its exact lexical storage slot")?; + scope.insert(target.name.clone(), physical); + Ok(ExprFlow::Value(Value::Nothing)) + })(); + match self.finish_resource_operation(operation, result)? { + ExprFlow::Value(Value::Nothing) => Ok(None), + ExprFlow::Signal(signal) => Ok(Some(signal)), + _ => Err("Resource assignment returned an unexpected expression flow".to_string()), + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/interpreter_transport/entry_context.rs b/crates/jett_comptime/src/resource_execution/interpreter_transport/entry_context.rs new file mode 100644 index 00000000..e727ce12 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/interpreter_transport/entry_context.rs @@ -0,0 +1,174 @@ +//! Metadata restoration at the checked top-level entry boundary. +//! This snapshot contains no Value, Environment or cleanup ticket. +use super::*; +use crate::resource_execution::CheckedBodyCursor; + +pub(super) struct SavedResourceEntryContext { + program: Arc, + retained_purpose: ExecutionPurpose, + checked_purpose: ExecutionPurpose, + source_active: bool, + cursor: CheckedBodyCursor, + scope_depth: usize, + variable_depth: usize, + alias_depth: usize, + namespace: Option, + trusted: bool, + floor: usize, + proofs: bool, + checked_function: Option>, + checked_scope: Option>, + arguments: Vec, + type_scopes: Vec>, + scoped_bindings: Vec, + fields: Vec>, + checked_fields: Vec, + type_infos: Vec>, + variants: Vec>, + states: Vec>, +} + +impl SavedResourceEntryContext { + pub(super) fn capture(interpreter: &Interpreter) -> Result { + let transport = interpreter + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")?; + let (program, retained_purpose) = interpreter + .retained_resource_program + .as_ref() + .ok_or("checked entry has no retained program")?; + if !Arc::ptr_eq(program, transport.checked.program()) { + return Err("checked entry context belongs to another retained program".to_string()); + } + Ok(Self { + program: program.clone(), + retained_purpose: *retained_purpose, + checked_purpose: transport.checked.purpose(), + source_active: transport.checked_source_active, + cursor: transport.cursor(), + scope_depth: interpreter.scopes.len(), + variable_depth: interpreter.variable_type_scopes.len(), + alias_depth: interpreter.namespace_alias_scopes.len(), + namespace: interpreter.current_namespace.clone(), + trusted: interpreter.current_function_trusted_stdlib, + floor: interpreter.lexical_scope_floor, + proofs: interpreter.allow_checked_refinement_proofs, + checked_function: interpreter.active_checked_function.clone(), + checked_scope: interpreter.active_checked_scope.clone(), + arguments: interpreter.current_type_arguments.clone(), + type_scopes: interpreter.type_arg_scopes.clone(), + scoped_bindings: interpreter.scoped_type_bindings.clone(), + fields: interpreter.reflected_field_scopes.clone(), + checked_fields: interpreter.checked_reflected_fields.clone(), + type_infos: interpreter.reflected_type_info_scopes.clone(), + variants: interpreter.reflected_variant_scopes.clone(), + states: interpreter.reflected_machine_state_scopes.clone(), + }) + } + + pub(super) fn scope_depth(&self) -> usize { + self.scope_depth + } + + pub(super) fn restore(self, interpreter: &mut Interpreter) -> Result<(), String> { + let underflow = interpreter.scopes.len() < self.scope_depth + || interpreter.variable_type_scopes.len() < self.variable_depth + || interpreter.namespace_alias_scopes.len() < self.alias_depth; + interpreter.scopes.truncate(self.scope_depth); + interpreter + .variable_type_scopes + .truncate(self.variable_depth); + interpreter + .namespace_alias_scopes + .truncate(self.alias_depth); + interpreter.current_namespace = self.namespace; + interpreter.current_function_trusted_stdlib = self.trusted; + interpreter.lexical_scope_floor = self.floor; + interpreter.allow_checked_refinement_proofs = self.proofs; + interpreter.active_checked_function = self.checked_function; + interpreter.active_checked_scope = self.checked_scope; + interpreter.current_type_arguments = self.arguments; + interpreter.type_arg_scopes = self.type_scopes; + interpreter.scoped_type_bindings = self.scoped_bindings; + interpreter.reflected_field_scopes = self.fields; + interpreter.checked_reflected_fields = self.checked_fields; + interpreter.reflected_type_info_scopes = self.type_infos; + interpreter.reflected_variant_scopes = self.variants; + interpreter.reflected_machine_state_scopes = self.states; + let retained = match interpreter.retained_resource_program.as_mut() { + Some((program, purpose)) if Arc::ptr_eq(program, &self.program) => { + *purpose = self.retained_purpose; + Ok(()) + } + _ => Err("checked entry lost its exact retained program".to_string()), + }; + let cursor = match interpreter.resource_transport.as_mut() { + Some(transport) if Arc::ptr_eq(transport.checked.program(), &self.program) => { + transport.checked_source_active = self.source_active; + transport.checked.replace_purpose(self.checked_purpose); + transport + .checked + .restore_cursor(self.cursor) + .map_err(|error| error.to_string()) + } + _ => Err("checked entry lost its exact Resource transport".to_string()), + }; + if underflow { + return Err("checked entry removed a preexisting interpreter scope".to_string()); + } + retained.and(cursor) + } +} + +#[cfg(test)] +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct EntryContextObservation { + namespace: Option, + trusted: bool, + floor: usize, + proofs: bool, + scope_depths: [usize; 3], + metadata_depths: [usize; 8], + checked_function: bool, + checked_scope: bool, + source_active: bool, + purpose: ExecutionPurpose, + original_scope_depth: Option, +} + +#[cfg(test)] +impl Interpreter { + pub(crate) fn resource_test_entry_context(&self) -> Result { + let transport = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")?; + Ok(EntryContextObservation { + namespace: self.current_namespace.clone(), + trusted: self.current_function_trusted_stdlib, + floor: self.lexical_scope_floor, + proofs: self.allow_checked_refinement_proofs, + scope_depths: [ + self.scopes.len(), + self.variable_type_scopes.len(), + self.namespace_alias_scopes.len(), + ], + metadata_depths: [ + self.current_type_arguments.len(), + self.type_arg_scopes.len(), + self.scoped_type_bindings.len(), + self.reflected_field_scopes.len(), + self.checked_reflected_fields.len(), + self.reflected_type_info_scopes.len(), + self.reflected_variant_scopes.len(), + self.reflected_machine_state_scopes.len(), + ], + checked_function: self.active_checked_function.is_some(), + checked_scope: self.active_checked_scope.is_some(), + source_active: transport.checked_source_active, + purpose: transport.checked.purpose(), + original_scope_depth: transport.checked.original_body_scope_depth(), + }) + } +} diff --git a/crates/jett_comptime/src/resource_execution/interpreter_transport/intrinsic.rs b/crates/jett_comptime/src/resource_execution/interpreter_transport/intrinsic.rs new file mode 100644 index 00000000..097c2155 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/interpreter_transport/intrinsic.rs @@ -0,0 +1,104 @@ +//! Exact checked intrinsic metadata, without a mutable ambient reflection map. +use super::*; +use crate::resource_execution::PreparedIntrinsicArguments; + +impl Interpreter { + pub(super) fn call_resource_intrinsic( + &mut self, + invocation: &CheckedInvocation, + prepared: &PreparedIntrinsicArguments, + args: Vec, + ) -> Result { + let checked = &self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked; + prepared + .validate(checked, invocation) + .map_err(|error| error.to_string())?; + let intrinsic = prepared.intrinsic(); + let name = intrinsic.canonical_name(); + let info = prepared.reflections().first(); + match intrinsic { + IntrinsicId::TypeName + | IntrinsicId::TypeKind + | IntrinsicId::TypeKindTag + | IntrinsicId::TypePrimitiveTag + | IntrinsicId::TypeHasSecret + | IntrinsicId::TypeInfo => { + if let Some(result) = check_args(name, 0, &args) { + return result; + } + let info = info.ok_or("checked intrinsic has no exact reflection argument")?; + Ok(match intrinsic { + IntrinsicId::TypeName => Value::String(info.type_name.clone()), + IntrinsicId::TypeKind => Value::String(info.kind.clone()), + IntrinsicId::TypeKindTag => Self::type_kind_tag_value(&info.kind), + IntrinsicId::TypePrimitiveTag => { + Self::primitive_tag_value(info.primitive_tag.as_deref()) + } + IntrinsicId::TypeHasSecret => Value::Bool(info.has_secret), + IntrinsicId::TypeInfo => Self::reflection_type_info_value(info), + _ => return Err("invalid checked metadata operation".to_string()), + }) + } + IntrinsicId::TypeArg => { + if let Some(result) = check_args(name, 1, &args) { + return result; + } + let info = info.ok_or("checked intrinsic has no exact reflection argument")?; + let index = match args.first() { + Some(Value::Int64(index)) if *index >= 0 => *index as usize, + Some(other) => { + return Err(format!( + "type.arg expects a non-negative int64 index, got {other}" + )); + } + None => return Err("checked type.arg has no index".to_string()), + }; + info.args + .get(index) + .map(Self::reflection_type_info_value) + .ok_or_else(|| { + format!( + "type.arg index {index} is out of range for type '{}'", + info.type_name + ) + }) + } + IntrinsicId::TypeFields => { + if let Some(result) = check_args(name, 0, &args) { + return result; + } + let [owner] = prepared.types() else { + return Err("checked type.fields has no exact owner type".to_string()); + }; + let owner_name = checked.program().checked().interner.type_name(*owner); + let fields = checked + .checked_reflection_fields(*owner) + .map_err(|error| error.to_string())?; + Ok(Value::List( + fields + .iter() + .map(|field| Self::reflection_field_info_value(&owner_name, None, field)) + .collect(), + )) + } + _ => { + let resolved = prepared + .types() + .iter() + .map(|&ty| { + Self::debug_type(&checked.program().checked().interner.type_name(ty)) + .ok_or_else(|| { + "checked intrinsic argument has no canonical interpreter type" + .to_string() + }) + }) + .collect::, String>>()?; + self.call_function_from_resolved_source(name, &resolved, args, None) + } + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/interpreter_transport/owned_struct.rs b/crates/jett_comptime/src/resource_execution/interpreter_transport/owned_struct.rs new file mode 100644 index 00000000..c643ef1d --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/interpreter_transport/owned_struct.rs @@ -0,0 +1,65 @@ +//! Reached owned actual envelopes stay inside the constructor operation. +use super::*; + +impl Interpreter { + pub(super) fn eval_resource_owned_struct( + &mut self, + expression: &Expr, + ) -> Result, String> { + let proof = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .prepare_owned_struct(expression) + .map_err(|error| error.to_string())?; + let Some(proof) = proof else { + return Ok(None); + }; + let Expr::Call(_, arguments, _) = expression else { + return Err("owned struct lost its exact original call".to_string()); + }; + let operation = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .begin_operation() + .map_err(|error| error.to_string())?; + let result = (|| { + let mut actuals = Vec::with_capacity(arguments.len()); + for (argument, ordinal) in arguments.iter().zip(proof.source_fields()) { + let mut value = + match Self::resource_envelope(self.eval_expr_flow(&argument.value)?)? { + Ok(value) => value, + Err(signal) => return Ok(ExprFlow::Signal(signal)), + }; + let transport = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")?; + transport + .validate_value_type(&value, proof.fields()[*ordinal].1) + .map_err(|error| error.to_string())?; + if value.custody.has_borrows() { + return Err("owned struct cannot acquire a borrowed payload".to_string()); + } + transport + .hold_actual(&mut value) + .map_err(|error| error.to_string())?; + actuals.push(value); + } + let value = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .assemble_owned_struct(&proof, expression, actuals) + .map_err(|error| error.to_string())?; + Ok(Self::resource_flow(value)) + })(); + self.finish_resource_operation(operation, result).map(Some) + } +} + +#[cfg(test)] +#[path = "owned_struct_tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/interpreter_transport/owned_struct_tests.rs b/crates/jett_comptime/src/resource_execution/interpreter_transport/owned_struct_tests.rs new file mode 100644 index 00000000..8746c0eb --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/interpreter_transport/owned_struct_tests.rs @@ -0,0 +1,261 @@ +use super::*; +use crate::resource_execution::{ProviderEvent, ScriptOperation}; +use jett_common::FileId; + +const SINGLE: &str = include_str!("../fixtures/33_owned_struct_scope_drop_entry.jett"); +const REVERSE: &str = include_str!("../fixtures/34_owned_struct_reverse_entry.jett"); +const PREFIX: &str = include_str!("../fixtures/35_owned_struct_abandoned_prefix_entry.jett"); + +fn entry(program: &jett_typecheck::CheckedResourceProgram) -> DefId { + program + .resolved() + .scope_table + .definitions + .iter() + .find(|info| { + info.kind == jett_resolve::DefKind::Function + && info.namespace.as_deref() == Some("app") + && info.name == "app.main" + && info.span.file == FileId::new(0) + }) + .unwrap() + .id +} + +fn run(source: &str, script: Vec, events: Vec) { + for release in [false, true] { + let program = crate::resource_execution::tests::program(source, release); + let target = entry(&program); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(script.clone()) + .unwrap(); + for invocation in 0..2 { + assert_eq!( + interpreter + .call_checked_program_entry(target, vec![grant.clone()]) + .unwrap(), + Value::Nothing, + "release={release}, invocation={invocation}" + ); + let expected = events + .iter() + .cloned() + .cycle() + .take(events.len() * (invocation + 1)) + .collect::>(); + // The tuple's third field is registry_live_count, never remaining + // provider script. Both custody counts are zero at this entry boundary. + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (expected, 0, 0) + ); + } + } +} + +#[test] +fn occupied_struct_owned_formal_moves_into_checked_root_and_reenters_without_teardown() { + run( + SINGLE, + vec![ + ScriptOperation::Construct { + label: 1001, + outcome: Ok(()) + }; + 2 + ], + vec![ + ProviderEvent::Constructed(1001), + ProviderEvent::Finalized(1001), + ], + ); + run( + SINGLE, + vec![ + ScriptOperation::Construct { + label: 1001, + outcome: Err("denied".into()) + }; + 2 + ], + vec![ProviderEvent::ConstructionFailed(1001)], + ); +} + +#[test] +fn occupied_struct_reverse_cleanup_follows_lexical_actuals_not_field_positions() { + run( + REVERSE, + vec![ + ScriptOperation::Construct { + label: 1002, + outcome: Ok(()), + }, + ScriptOperation::Construct { + label: 1003, + outcome: Ok(()), + }, + ScriptOperation::Construct { + label: 1002, + outcome: Ok(()), + }, + ScriptOperation::Construct { + label: 1003, + outcome: Ok(()), + }, + ], + vec![ + ProviderEvent::Constructed(1002), + ProviderEvent::Constructed(1003), + ProviderEvent::Finalized(1002), + ProviderEvent::Finalized(1003), + ], + ); +} + +#[test] +fn occupied_struct_later_handler_return_retires_acquired_prefix_and_reenters() { + run( + PREFIX, + vec![ + ScriptOperation::Construct { + label: 1004, + outcome: Ok(()) + }; + 2 + ], + vec![ + ProviderEvent::Constructed(1004), + ProviderEvent::Finalized(1004), + ], + ); +} + +#[test] +fn occupied_struct_finalizer_panic_retires_owner_before_error_and_keeps_ready_reentry() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(SINGLE, release); + let target = entry(&program); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ + ScriptOperation::ConstructFinalizerPanic { label: 1001 }, + ScriptOperation::Construct { + label: 1001, + outcome: Ok(()), + }, + ]) + .unwrap(); + let first = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(target, vec![grant.clone()]) + })); + assert!(first.is_err()); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(1001), + ProviderEvent::Finalized(1001) + ], + 0, + 0 + ) + ); + assert_eq!( + interpreter + .call_checked_program_entry(target, vec![grant]) + .unwrap(), + Value::Nothing + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(1001), + ProviderEvent::Finalized(1001), + ProviderEvent::Constructed(1001), + ProviderEvent::Finalized(1001) + ], + 0, + 0 + ) + ); + } +} + +/// Historical drafts retain their actual E0503 boundary. Owning field custody +/// must never be inferred by weakening the checked external entry policy. +#[test] +fn occupied_struct_original_view_main_is_still_rejected_before_entry() { + use jett_common::SourceOrigin; + use jett_parser::{ast::Item, parse}; + use jett_resolve::ResourceKernelSpec; + use jett_typecheck::{CheckOptions, CheckedResourceProgram}; + use jett_types::ResourceKernelRecipe; + use std::collections::HashMap; + const SUPPORT: &str = include_str!("../fixtures/resource_probe.jett"); + const INVALID: [&str; 3] = [ + include_str!("../fixtures/30_owned_struct_scope_drop.jett"), + include_str!("../fixtures/31_owned_struct_reverse.jett"), + include_str!("../fixtures/32_owned_struct_abandoned_prefix.jett"), + ]; + for release in [false, true] { + for source in INVALID { + let stdlib = FileId::new(10_000); + let project = FileId::new(0); + let mut parsed = parse(SUPPORT, stdlib); + let original = parse(source, project); + assert!(parsed.errors.is_empty() && original.errors.is_empty()); + let resource = parsed + .module + .items + .iter() + .find_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .unwrap(); + parsed.module.items.extend(original.module.items); + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: resource, + member: member.into(), + recipe, + }) + .collect::>(); + let error = CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (stdlib, SourceOrigin::Stdlib), + (project, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .err() + .expect("original viewed main remains rejected"); + let diagnostics = error + .diagnostics() + .expect("original E0503 checker diagnostics"); + assert!( + diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 503 + && diagnostic.span.file == project + && diagnostic.message + == "`main` must own runtime capability parameter `net`; remove `view`"), + "release={release}: {diagnostics:?}" + ); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/interpreter_transport/pipeline.rs b/crates/jett_comptime/src/resource_execution/interpreter_transport/pipeline.rs new file mode 100644 index 00000000..29adc43e --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/interpreter_transport/pipeline.rs @@ -0,0 +1,234 @@ +//! Checked pipeline envelopes; source expressions are evaluated once. +use crate::resource_execution::PreparedPipelineStep; + +use super::*; + +impl Interpreter { + pub(super) fn eval_resource_pipeline(&mut self, pipeline: &Expr) -> Result { + let Expr::Pipeline(_, steps, _) = pipeline else { + return Err("checked pipeline lost its original node".to_string()); + }; + let first = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .prepare_pipeline_step(pipeline, 0) + .map_err(|error| error.to_string())?; + let first_intrinsic = self.prepare_resource_pipeline_intrinsic(&first)?; + let operation = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .begin_operation() + .map_err(|error| error.to_string())?; + let result = (|| { + let mut current = + match Self::resource_envelope(self.eval_resource_pipeline_initial(&first)?)? { + Ok(value) => value, + Err(signal) => return Ok(ExprFlow::Signal(signal)), + }; + let mut first = Some((first, first_intrinsic)); + for index in 0..steps.len() { + let (prepared, intrinsic) = if index == 0 { + first.take().ok_or("checked pipeline lost its first step")? + } else { + let prepared = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .prepare_pipeline_step(pipeline, index) + .map_err(|error| error.to_string())?; + let intrinsic = self.prepare_resource_pipeline_intrinsic(&prepared)?; + (prepared, intrinsic) + }; + let flow = + self.eval_resource_pipeline_step(&prepared, current, intrinsic.as_ref())?; + current = match Self::resource_envelope(flow)? { + Ok(value) => value, + Err(signal) => return Ok(ExprFlow::Signal(signal)), + }; + } + Ok(Self::resource_flow(current)) + })(); + self.finish_resource_operation(operation, result) + } + + fn prepare_resource_pipeline_intrinsic( + &self, + prepared: &PreparedPipelineStep<'_>, + ) -> Result, String> { + if matches!( + prepared.invocation().target(), + CheckedInvocationTarget::Intrinsic(_) + ) { + self.resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .prepare_pipeline_intrinsic_arguments(prepared) + .map(Some) + .map_err(|error| error.to_string()) + } else { + Ok(None) + } + } + + fn eval_resource_pipeline_initial( + &mut self, + prepared: &PreparedPipelineStep<'_>, + ) -> Result { + let original = prepared + .initial() + .ok_or("checked pipeline has no original first input")?; + if let Some(definition) = prepared.initial_borrow() { + let transport = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")?; + if transport.has_binding(definition) { + let value = transport + .binding(definition, true) + .map_err(|error| error.to_string())?; + return Ok(Self::resource_flow(value)); + } + } + self.eval_expr_flow(original) + } + + fn eval_resource_pipeline_step( + &mut self, + prepared: &PreparedPipelineStep<'_>, + mut input: EvaluatedValue, + intrinsic: Option<&PreparedIntrinsicArguments>, + ) -> Result { + let operation = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .begin_operation() + .map_err(|error| error.to_string())?; + let result = (|| { + let invocation = prepared.invocation(); + let source0 = invocation + .arguments() + .first() + .ok_or("checked pipeline has no original input occurrence")?; + let transport = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")?; + transport + .validate_value_type(&input, prepared.input_type()) + .map_err(|error| error.to_string())?; + transport + .validate_actual(&input, source0.effect) + .map_err(|error| error.to_string())?; + transport + .hold_actual(&mut input) + .map_err(|error| error.to_string())?; + let mut actuals = Vec::with_capacity(invocation.arguments().len()); + actuals.push(input); + for (argument, fact) in prepared + .extra_arguments() + .iter() + .zip(&invocation.arguments()[1..]) + { + let mut value = + match Self::resource_envelope(self.eval_expr_flow(&argument.value)?)? { + Ok(value) => value, + Err(signal) => return Ok(ExprFlow::Signal(signal)), + }; + let transport = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")?; + transport + .validate_actual(&value, fact.effect) + .map_err(|error| error.to_string())?; + transport + .hold_actual(&mut value) + .map_err(|error| error.to_string())?; + actuals.push(value); + } + let flow = self.dispatch_resource_actuals( + invocation, + prepared.callee(), + prepared.type_arguments(), + actuals, + prepared.constructor(), + operation.frame, + intrinsic, + )?; + let value = match Self::resource_envelope(flow)? { + Ok(value) => value, + Err(signal) => return Ok(ExprFlow::Signal(signal)), + }; + self.resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .validate_value_type(&value, prepared.raw_call_type()) + .map_err(|error| error.to_string())?; + Ok(Self::resource_flow(value)) + })(); + let flow = self.finish_resource_operation(operation, result)?; + let value = match Self::resource_envelope(flow)? { + Ok(value) => value, + Err(signal) => return Ok(ExprFlow::Signal(signal)), + }; + let flow = if let Some(handle) = &prepared.original().handle { + self.eval_resource_pipeline_handle(value, handle)? + } else { + Self::resource_flow(value) + }; + if let ExprFlow::Resource(value) = &flow { + self.resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .validate_value_type(value, prepared.output_type()) + .map_err(|error| error.to_string())?; + } + Ok(flow) + } + + fn eval_resource_pipeline_handle( + &mut self, + mut target: EvaluatedValue, + handle: &jett_parser::ast::PipelineStepHandle, + ) -> Result { + while let Value::Typed { value, .. } = target.value { + target.value = *value; + target + .remove_prefix(PayloadStep::Typed) + .map_err(|error| error.to_string())?; + } + match target.value { + Value::ResultOk(value) => { + target.value = *value; + target + .remove_prefix(PayloadStep::Ok) + .map_err(|error| error.to_string())?; + Ok(Self::resource_flow(target)) + } + Value::OptionalSome(value) => { + target.value = *value; + target + .remove_prefix(PayloadStep::Some) + .map_err(|error| error.to_string())?; + Ok(Self::resource_flow(target)) + } + Value::ResultFail(error) if target.custody.is_empty() => { + self.exec_resource_handle(handle.error_name.as_ref(), Some(*error), &handle.body) + } + Value::OptionalNone if target.custody.is_empty() => { + self.exec_resource_handle(None, None, &handle.body) + } + _ => Err("checked pipeline handle has no selected sum custody transport".to_string()), + } + } +} + +#[cfg(test)] +#[path = "pipeline/tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/interpreter_transport/pipeline/tests.rs b/crates/jett_comptime/src/resource_execution/interpreter_transport/pipeline/tests.rs new file mode 100644 index 00000000..cd94f311 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/interpreter_transport/pipeline/tests.rs @@ -0,0 +1,348 @@ +use jett_common::FileId; +use jett_parser::ast::Item; +use jett_resolve::{DefId, DefKind}; + +use super::*; +use crate::resource_execution::tests::program; +use crate::resource_execution::{ProviderEvent, ScriptOperation}; + +const CONSTRUCT: &str = include_str!("../../fixtures/18_pipeline_construct_move_close.jett"); +const RETAIN: &str = include_str!("../../fixtures/19_pipeline_written_view_retains_owner.jett"); +const RELINQUISH: &str = include_str!("../../fixtures/20_pipeline_bare_owner_view_operation.jett"); +const ABORT: &str = include_str!("../../fixtures/21_pipeline_named_actual_abort.jett"); +const DOMAIN: &str = include_str!("../../fixtures/22_pipeline_domain_failure_stops_next_step.jett"); +const INDIRECT: &str = include_str!("../../fixtures/23_pipeline_indirect_close_descriptor.jett"); + +fn entry(checked: &jett_typecheck::CheckedResourceProgram) -> DefId { + let functions = checked + .module() + .items + .iter() + .filter_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) + && function.name.name == "scenario" => + { + Some(function) + } + _ => None, + }) + .collect::>(); + let [function] = functions.as_slice() else { + panic!("one original primary scenario"); + }; + let definitions = checked + .resolved() + .scope_table + .definitions + .iter() + .filter(|info| { + info.kind == DefKind::Function + && info.span == function.name.span + && info.namespace.as_deref() == Some("app") + }) + .map(|info| info.id) + .collect::>(); + let [definition] = definitions.as_slice() else { + panic!("one exact original declaration"); + }; + *definition +} + +fn execute( + source: &str, + script: Vec, + returned: Value, + events: Vec, +) { + for release in [false, true] { + let checked = program(source, release); + let target = entry(&checked); + let mut interpreter = + Interpreter::from_checked_resource_program(checked, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(script.clone()) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(target, vec![grant]) + .unwrap(), + returned, + "release={release}" + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (events.clone(), 0, 0) + ); + assert!(interpreter.take_debug_events().is_empty()); + } +} + +#[test] +fn resource_pipeline_constructs_moves_and_closes_one_source_owner() { + execute( + CONSTRUCT, + vec![ScriptOperation::Construct { + label: 1801, + outcome: Ok(()), + }], + Value::Nothing, + vec![ + ProviderEvent::Constructed(1801), + ProviderEvent::Finalized(1801), + ], + ); +} + +#[test] +fn resource_pipeline_written_view_keeps_root_for_later_explicit_close() { + execute( + RETAIN, + vec![ + ScriptOperation::Construct { + label: 1901, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1901, + outcome: Ok(5), + }, + ], + Value::Int64(5), + vec![ + ProviderEvent::Constructed(1901), + ProviderEvent::Borrowed(1901), + ProviderEvent::Finalized(1901), + ], + ); +} + +#[test] +fn resource_pipeline_bare_owner_backs_view_only_until_operation_cleanup() { + execute( + RELINQUISH, + vec![ + ScriptOperation::Construct { + label: 2001, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 2001, + outcome: Ok(5), + }, + ], + Value::Int64(5), + vec![ + ProviderEvent::Constructed(2001), + ProviderEvent::Borrowed(2001), + ProviderEvent::Finalized(2001), + ], + ); +} + +#[test] +fn resource_pipeline_input_precedes_named_actual_abort_and_callee_never_enters() { + execute( + ABORT, + vec![ + ScriptOperation::Construct { + label: 2101, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 2101, + outcome: Ok(1), + }, + ], + Value::Nothing, + vec![ + ProviderEvent::Constructed(2101), + ProviderEvent::Borrowed(2101), + ProviderEvent::Finalized(2101), + ], + ); +} + +#[test] +fn resource_pipeline_domain_failure_and_borrow_failure_keep_exact_source_handlers() { + execute( + DOMAIN, + vec![ScriptOperation::Construct { + label: 2201, + outcome: Err("denied".into()), + }], + Value::Int64(-1), + vec![ProviderEvent::ConstructionFailed(2201)], + ); + execute( + RETAIN, + vec![ + ScriptOperation::Construct { + label: 1901, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1901, + outcome: Err("unavailable".into()), + }, + ], + Value::Int64(-2), + vec![ + ProviderEvent::Constructed(1901), + ProviderEvent::BorrowFailed(1901), + ProviderEvent::Finalized(1901), + ], + ); +} + +#[test] +fn resource_pipeline_indirect_hook_descriptor_executes_only_at_original_step() { + execute( + INDIRECT, + vec![ScriptOperation::Construct { + label: 2301, + outcome: Ok(()), + }], + Value::Nothing, + vec![ + ProviderEvent::Constructed(2301), + ProviderEvent::Finalized(2301), + ], + ); +} + +#[test] +fn resource_pipeline_provider_panic_unwinds_real_call_backing_before_teardown() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + for release in [false, true] { + let checked = program(RELINQUISH, release); + let target = entry(&checked); + let mut interpreter = + Interpreter::from_checked_resource_program(checked, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ + ScriptOperation::Construct { + label: 2001, + outcome: Ok(()), + }, + ScriptOperation::BorrowPanic { label: 2001 }, + ]) + .unwrap(); + assert!( + catch_unwind(AssertUnwindSafe( + || interpreter.call_checked_program_entry(target, vec![grant]) + )) + .is_err() + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(2001), + ProviderEvent::Borrowed(2001), + ProviderEvent::Finalized(2001) + ], + 0, + 0 + ) + ); + assert!(interpreter.take_debug_events().is_empty()); + } +} +#[test] +fn resource_pipeline_required_regions_execute_absence_without_runtime_authority() { + use crate::checked_types::CheckedExpressionTypes; + use crate::resource_execution::CheckedExecution; + use jett_parser::ast::Item; + + const REQUIRED: &str = include_str!("../../fixtures/28_pipeline_absence_regions.jett"); + for release in [false, true] { + let program = program(REQUIRED, release); + let expression_types = std::sync::Arc::new(CheckedExpressionTypes { + resource_program: Some(program.clone()), + expressions: program + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, program.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let required = crate::evaluate_explicit_comptime_expressions_capture( + program.module(), + std::sync::Arc::new(jett_types::ReflectionMetadata::new()), + expression_types.clone(), + std::sync::Arc::new(std::collections::HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "{:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + assert!(required.values.checked_values_are_mirrored()); + let declaration = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::VarDecl(value) + if value.name.name == "namespace_marker" + && value.name.span.file == jett_common::FileId::new(0) => + { + Some(value) + } + _ => None, + }) + .unwrap(); + assert_eq!( + required.values.constant(declaration.name.span), + Some(&Value::Int64(17)) + ); + let suites = crate::verify::run_verify_blocks_detailed_with_checked_values( + program.module(), + std::sync::Arc::new(jett_types::ReflectionMetadata::new()), + expression_types, + std::sync::Arc::new(std::collections::HashMap::new()), + std::sync::Arc::new(required.values.clone()), + ); + assert_eq!(suites.len(), 2); + assert!( + suites + .iter() + .all(|suite| suite.passed && suite.debug_events.is_empty()), + "{suites:?}" + ); + let original = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.name == "explicit_marker" + && function.name.span.file == jett_common::FileId::new(0) => + { + Some(function) + } + _ => None, + }) + .unwrap(); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let target = checked.declaration_definition(original.name.span).unwrap(); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + interpreter.set_explicit_comptime_values(std::sync::Arc::new(required.values)); + assert_eq!( + interpreter + .call_checked_program_entry(target, Vec::new()) + .unwrap(), + Value::Int64(17) + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); + } +} diff --git a/crates/jett_comptime/src/resource_execution/interpreter_transport/required.rs b/crates/jett_comptime/src/resource_execution/interpreter_transport/required.rs new file mode 100644 index 00000000..71411446 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/interpreter_transport/required.rs @@ -0,0 +1,146 @@ +//! Original required-region transactions. No provider or ownership permission. + +use super::*; +use crate::resource_execution::{ + CheckedBodyReference, PreparedRequiredExpression, ResourceExecutionError, +}; + +impl Interpreter { + pub(crate) fn checked_explicit_contexts( + &self, + expression: &Expr, + occurrence: Span, + owner: Option, + bindings: &[(Span, String)], + ) -> Result>, String> { + self.resource_transport + .as_ref() + .map(|transport| { + transport + .checked + .prepare_explicit_contexts(expression, occurrence, owner, bindings) + .map_err(|error| error.to_string()) + }) + .transpose() + } + + pub(crate) fn checked_namespace_initializer( + &self, + declaration: &VarDecl, + ) -> Result, String> { + self.resource_transport + .as_ref() + .map(|transport| { + transport + .checked + .prepare_namespace_initializer(declaration) + .map_err(|error| error.to_string()) + }) + .transpose() + } + + pub(crate) fn eval_checked_required_expression( + &mut self, + prepared: &PreparedRequiredExpression, + namespace: Option<&str>, + aliases: &HashMap, + parameters: &[String], + ) -> Result { + if self + .resource_transport + .as_ref() + .ok_or("required region has no checked program")? + .checked + .purpose() + != prepared.purpose() + { + return Err(ResourceExecutionError::WrongPurpose.to_string()); + } + self.with_checked_required_region(prepared.reference(), |interpreter| { + interpreter.eval_closed_comptime_expression( + namespace, + aliases, + prepared.expression().map_err(|error| error.to_string())?, + parameters, + prepared.function_projection(), + prepared.scope_projection(), + prepared.bindings(), + ) + }) + } + + pub(crate) fn exec_checked_verify_region( + &mut self, + namespace: Option<&str>, + original: &jett_parser::ast::VerifyBlock, + ) -> Result, String> { + let Some(transport) = &self.resource_transport else { + return self.exec_block_in_namespace(namespace, &original.body); + }; + let reference = transport + .checked + .prepare_verify_body(original) + .map_err(|error| error.to_string())?; + self.with_checked_required_region(&reference, |interpreter| { + interpreter.exec_block_in_namespace(namespace, &original.body) + }) + } + + pub(crate) fn with_checked_property_region( + &mut self, + original: &jett_parser::ast::PropertyBlock, + body: impl FnOnce(&mut Self) -> Result, + ) -> Result { + let Some(transport) = &self.resource_transport else { + return body(self); + }; + let reference = transport + .checked + .prepare_property_body(original) + .map_err(|error| error.to_string())?; + self.with_checked_required_region(&reference, body) + } + + fn with_checked_required_region( + &mut self, + reference: &CheckedBodyReference, + body: impl FnOnce(&mut Self) -> Result, + ) -> Result { + let transport = self + .resource_transport + .as_ref() + .ok_or("required region has no checked program")?; + transport + .validate_entry_scopes(self.scopes.len()) + .map_err(|error| error.to_string())?; + let saved = SavedResourceEntryContext::capture(self)?; + let depth = saved.scope_depth(); + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .checked + .install_function_body(reference) + .map_err(|error| error.to_string())?; + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| body(self))); + let cleanup = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .unwind_all() + .map_err(|error| error.to_string()) + })); + let restore = saved.restore(self); + let completion = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .restore_entry_scopes(depth) + .map_err(|error| error.to_string()) + })); + let result = match (result, restore) { + (Ok(Ok(_)), Err(error)) => Ok(Err(error)), + (result, _) => result, + }; + Self::complete_resource_entry(result, Self::complete_resource_cleanup(cleanup, completion)) + } +} diff --git a/crates/jett_comptime/src/resource_execution/mod.rs b/crates/jett_comptime/src/resource_execution/mod.rs new file mode 100644 index 00000000..4a8ba386 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/mod.rs @@ -0,0 +1,125 @@ +//! Checked source Resource execution. A carrier is never a cleanup owner. + +mod checked; +mod custody; +mod provider; +mod runtime; + +use std::fmt; +use std::sync::Arc; + +use jett_resolve::DefId; +use jett_runtime::{RegistryError, ResourceKey, ResourceTypeId}; +use jett_typecheck::CheckedResourceProgram; +use jett_types::TypeId; + +pub use checked::ExecutionPurpose; +pub(crate) use checked::{ + CheckedAttemptKey, CheckedBodyCursor, CheckedBodyReference, CheckedExecution, + CheckedInvocation, FunctionInvocation, PreparedAbsentBindings, PreparedIntrinsicArguments, + PreparedPipelineStep, PreparedReflectedFieldIteration, PreparedReflectedFieldLoop, + PreparedRequiredExpression, +}; +pub use checked::{ + CheckedRequiredOwner, CheckedRequiredResourceHook, CheckedRequiredScope, CheckedRequiredValue, +}; +pub(crate) use custody::{ + EvaluatedValue, FrameId, OwnerHolder, OwnerLedger, PayloadStep, ValueCustody, +}; +pub use provider::GrantedNetwork; +pub(crate) use runtime::{OperationFrame, ResourceTransport}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ResourceTypeBinding { + definition: DefId, + checked_type: TypeId, + registry_type: ResourceTypeId, +} + +/// Opaque physical identity. Cloning this value cannot create a cleanup ticket. +#[derive(Clone)] +pub struct ResourceCarrier { + program: Arc, + resource: ResourceTypeBinding, + key: ResourceKey, +} + +impl fmt::Debug for ResourceCarrier { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("ResourceCarrier()") + } +} + +/// A closed callable descriptor is data; construction does not execute a hook. +#[derive(Clone)] +pub struct ResourceHookDescriptor { + program: Arc, + definition: DefId, +} + +impl fmt::Debug for ResourceHookDescriptor { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("ResourceHookDescriptor()") + } +} + +impl ResourceHookDescriptor { + pub(crate) fn same_descriptor(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.program, &other.program) && self.definition == other.definition + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ResourceExecutionError { + InvalidCheckedProgram, + MissingCheckedBody, + MissingCheckedInvocation, + InvalidInvocation, + ForeignProgram, + WrongPurpose, + ProviderDisabled, + InvalidGrant, + MissingOwner, + InvalidOwner, + InvalidBorrow, + InvalidPayloadPath, + InvalidFrame, + IdentityExhausted, + Registry(RegistryError), +} + +impl fmt::Display for ResourceExecutionError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = match self { + Self::InvalidCheckedProgram => "resource execution requires a valid checked program", + Self::MissingCheckedBody => "resource execution has no exact checked body", + Self::MissingCheckedInvocation => "resource execution has no checked call occurrence", + Self::InvalidInvocation => "resource execution has invalid checked call metadata", + Self::ForeignProgram => "resource execution belongs to another checked program", + Self::WrongPurpose => "resource operation requires runtime execution", + Self::ProviderDisabled => "resource runtime provider is not installed", + Self::InvalidGrant => "resource operation has no matching runtime authority", + Self::MissingOwner => "resource value has no owning cleanup obligation", + Self::InvalidOwner => "resource cleanup obligation is not available", + Self::InvalidBorrow => "resource view has no live checked backing owner", + Self::InvalidPayloadPath => "resource value lost its checked payload custody", + Self::InvalidFrame => "resource execution has no live owning frame", + Self::IdentityExhausted => "resource execution identity space is exhausted", + Self::Registry(_) => "resource carrier is not valid for this runtime operation", + }; + formatter.write_str(message) + } +} + +impl std::error::Error for ResourceExecutionError {} + +impl From for ResourceExecutionError { + fn from(error: RegistryError) -> Self { + Self::Registry(error) + } +} + +#[cfg(test)] +pub(crate) use provider::{ProviderEvent, ScriptOperation}; +#[cfg(test)] +pub(crate) mod tests; diff --git a/crates/jett_comptime/src/resource_execution/prerequisite_tests.rs b/crates/jett_comptime/src/resource_execution/prerequisite_tests.rs new file mode 100644 index 00000000..a970f585 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/prerequisite_tests.rs @@ -0,0 +1,44 @@ +use super::*; + +#[test] +fn checked_source_mutable_assignment_frontend_prerequisites() { + for release in [false, true] { + let checked = program(include_str!("fixtures/24_mutable_assignment.jett"), release); + for name in [ + "replace_live", + "rebind_after_close", + "rebind_self", + "failed_rhs_keeps_owner", + ] { + let _ = entry(&checked, name); + } + } +} + +#[test] +fn checked_source_pipeline_frontend_prerequisites() { + for release in [false, true] { + for source in [ + include_str!("fixtures/18_pipeline_construct_move_close.jett"), + include_str!("fixtures/19_pipeline_written_view_retains_owner.jett"), + include_str!("fixtures/20_pipeline_bare_owner_view_operation.jett"), + include_str!("fixtures/21_pipeline_named_actual_abort.jett"), + include_str!("fixtures/22_pipeline_domain_failure_stops_next_step.jett"), + include_str!("fixtures/23_pipeline_indirect_close_descriptor.jett"), + ] { + let checked = program(source, release); + let _ = entry(&checked, "scenario"); + } + } +} + +#[test] +fn checked_source_pipeline_required_regions_frontend_prerequisites() { + for release in [false, true] { + let checked = program( + include_str!("fixtures/28_pipeline_absence_regions.jett"), + release, + ); + let _ = entry(&checked, "explicit_marker"); + } +} diff --git a/crates/jett_comptime/src/resource_execution/provider.rs b/crates/jett_comptime/src/resource_execution/provider.rs new file mode 100644 index 00000000..ef426247 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/provider.rs @@ -0,0 +1,255 @@ +use std::fmt; + +use jett_runtime::{AuthorityProvenance, ResourceRegistry}; + +use super::{ResourceExecutionError, ResourceTypeBinding}; + +/// This carrier can be copied as capability authority, never as a Resource owner. +#[derive(Clone)] +pub struct GrantedNetwork { + context: u64, + authority: AuthorityProvenance, +} + +impl fmt::Debug for GrantedNetwork { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("GrantedNetwork()") + } +} + +pub(crate) enum InstalledResourceProvider { + Disabled, + #[cfg(test)] + Scripted(ScriptedResourceProvider), +} + +impl InstalledResourceProvider { + pub(crate) fn validate_grant( + &self, + grant: &GrantedNetwork, + ) -> Result { + match self { + Self::Disabled => Err(ResourceExecutionError::ProviderDisabled), + #[cfg(test)] + Self::Scripted(provider) + if provider.grant.context == grant.context + && provider.grant.authority == grant.authority => + { + Ok(grant.authority) + } + #[cfg(test)] + Self::Scripted(_) => Err(ResourceExecutionError::InvalidGrant), + } + } + + pub(crate) fn construct( + &mut self, + registry: &mut ResourceRegistry, + resource: ResourceTypeBinding, + grant: &GrantedNetwork, + label: i64, + ) -> Result, ResourceExecutionError> { + let authority = self.validate_grant(grant)?; + match self { + Self::Disabled => Err(ResourceExecutionError::ProviderDisabled), + #[cfg(test)] + Self::Scripted(provider) => provider.construct(registry, resource, authority, label), + } + } + + pub(crate) fn borrow( + &mut self, + registry: &mut ResourceRegistry, + resource: ResourceTypeBinding, + grant: &GrantedNetwork, + key: jett_runtime::ResourceKey, + ) -> Result, ResourceExecutionError> { + let authority = self.validate_grant(grant)?; + match self { + Self::Disabled => Err(ResourceExecutionError::ProviderDisabled), + #[cfg(test)] + Self::Scripted(provider) => provider.borrow(registry, resource, authority, key), + } + } + + pub(crate) fn ensure_installed(&self) -> Result<(), ResourceExecutionError> { + match self { + Self::Disabled => Err(ResourceExecutionError::ProviderDisabled), + #[cfg(test)] + Self::Scripted(_) => Ok(()), + } + } +} + +// No production provider, public provider spelling or host-hook callback map. +// Compiler unit tests alone can supply the selected exact three recipe script. +#[cfg(test)] +use std::collections::VecDeque; +#[cfg(test)] +use std::sync::atomic::{AtomicU64, Ordering}; +#[cfg(test)] +use std::sync::{Arc, Mutex}; + +#[cfg(test)] +static NEXT_TEST_GRANT: AtomicU64 = AtomicU64::new(1); + +#[cfg(test)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum ProviderEvent { + Constructed(i64), + ConstructionFailed(i64), + Borrowed(i64), + BorrowFailed(i64), + Finalized(i64), +} + +#[cfg(test)] +#[derive(Clone)] +pub(crate) enum ScriptOperation { + Construct { + label: i64, + outcome: Result<(), String>, + }, + ConstructFinalizerPanic { + label: i64, + }, + Borrow { + label: i64, + outcome: Result, + }, + BorrowPanic { + label: i64, + }, +} + +#[cfg(test)] +struct FakePayload { + label: i64, +} + +#[cfg(test)] +pub(crate) struct ScriptedResourceProvider { + grant: GrantedNetwork, + operations: VecDeque, + events: Arc>>, +} + +#[cfg(test)] +impl ScriptedResourceProvider { + pub(crate) fn new(operations: Vec) -> Result { + let context = NEXT_TEST_GRANT + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| { + value.checked_add(1) + }) + .map_err(|_| ResourceExecutionError::IdentityExhausted)?; + Ok(Self { + grant: GrantedNetwork { + context, + authority: AuthorityProvenance::new(context, 1), + }, + operations: operations.into(), + events: Arc::new(Mutex::new(Vec::new())), + }) + } + + pub(crate) fn grant(&self) -> GrantedNetwork { + self.grant.clone() + } + + pub(crate) fn events(&self) -> Result, ResourceExecutionError> { + self.events + .lock() + .map(|events| events.clone()) + .map_err(|_| ResourceExecutionError::InvalidGrant) + } + + pub(crate) fn remaining(&self) -> usize { + self.operations.len() + } + + fn record(&self, event: ProviderEvent) -> Result<(), ResourceExecutionError> { + self.events + .lock() + .map_err(|_| ResourceExecutionError::InvalidGrant)? + .push(event); + Ok(()) + } + + fn construct( + &mut self, + registry: &mut ResourceRegistry, + resource: ResourceTypeBinding, + authority: AuthorityProvenance, + label: i64, + ) -> Result, ResourceExecutionError> { + let (expected, outcome, finalizer_panic) = match self.operations.front() { + Some(ScriptOperation::Construct { label, outcome }) => (*label, outcome.clone(), false), + Some(ScriptOperation::ConstructFinalizerPanic { label }) => (*label, Ok(()), true), + _ => return Err(ResourceExecutionError::InvalidInvocation), + }; + if expected != label { + return Err(ResourceExecutionError::InvalidInvocation); + } + if let Err(message) = outcome { + self.record(ProviderEvent::ConstructionFailed(label))?; + self.operations.pop_front(); + return Ok(Err(message)); + } + // Record bookkeeping before publication; a poisoned test log cannot + // publish an owner and then report construction failure. + self.record(ProviderEvent::Constructed(label))?; + let finalized_events = Arc::clone(&self.events); + let key = registry.insert( + resource.registry_type, + FakePayload { label }, + authority, + move |payload| { + // Test telemetry is separate from source stdout/debug surfaces. + if let Ok(mut events) = finalized_events.lock() { + events.push(ProviderEvent::Finalized(payload.label)); + } + if finalizer_panic { + panic!("selected test resource cleanup panic"); + } + }, + )?; + self.operations.pop_front(); + Ok(Ok(key)) + } + + fn borrow( + &mut self, + registry: &mut ResourceRegistry, + resource: ResourceTypeBinding, + authority: AuthorityProvenance, + key: jett_runtime::ResourceKey, + ) -> Result, ResourceExecutionError> { + // Carrier/provenance validation occurs before consuming script or event. + let label = registry.access( + key, + resource.registry_type, + &authority, + |payload: &mut FakePayload| payload.label, + )?; + let (expected, outcome) = match self.operations.front() { + Some(ScriptOperation::Borrow { label, outcome }) => (*label, Some(outcome.clone())), + Some(ScriptOperation::BorrowPanic { label }) => (*label, None), + _ => return Err(ResourceExecutionError::InvalidInvocation), + }; + if expected != label { + return Err(ResourceExecutionError::InvalidInvocation); + } + let Some(outcome) = outcome else { + self.record(ProviderEvent::Borrowed(label))?; + self.operations.pop_front(); + panic!("selected test borrow provider panic"); + }; + self.record(if outcome.is_ok() { + ProviderEvent::Borrowed(label) + } else { + ProviderEvent::BorrowFailed(label) + })?; + self.operations.pop_front(); + Ok(outcome) + } +} diff --git a/crates/jett_comptime/src/resource_execution/runtime.rs b/crates/jett_comptime/src/resource_execution/runtime.rs new file mode 100644 index 00000000..1ca72e55 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/runtime.rs @@ -0,0 +1,1324 @@ +#[path = "runtime/assignment.rs"] +mod assignment; + +#[path = "runtime/absent_generated.rs"] +mod absent_generated; + +#[path = "runtime/owned_struct.rs"] +mod owned_struct; + +use std::collections::HashMap; +use std::sync::Arc; + +use jett_common::Span; +use jett_resolve::DefId; +use jett_runtime::ResourceRegistry; +use jett_typecheck::{ + CheckedBindingFact, CheckedCalleeAccess, CheckedCallerEffect, CheckedInvocationTarget, + CheckedResourceProgram, +}; +use jett_types::ResourceKernelRecipe; + +use super::checked::{CheckedBodyCursor, CheckedInvocation, FunctionInvocation}; +use super::custody::PayloadStep; +use super::provider::InstalledResourceProvider; +use super::{ + CheckedExecution, EvaluatedValue, ExecutionPurpose, FrameId, OwnerHolder, OwnerLedger, + ResourceCarrier, ResourceExecutionError, ResourceHookDescriptor, +}; +use crate::value::Value; + +pub(crate) struct OperationFrame { + pub(crate) frame: FrameId, + parent: FrameId, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ActiveFrame { + Scope(FrameId), + Operation(FrameId), +} + +impl ActiveFrame { + fn id(self) -> FrameId { + match self { + Self::Scope(frame) | Self::Operation(frame) => frame, + } + } +} + +#[derive(Debug, Clone, Copy)] +struct ReturnContext { + destination: Option, + operation_floor: usize, +} + +/// The lexical declaration survives consuming its value. No copied physical +/// carrier can repopulate this slot or restore an owning cleanup ticket. +struct ResourceBindingSlot { + fact: CheckedBindingFact, + value: Option, +} + +/// Internal transport only. Neither ordinary Value copies nor a raw interpreter +/// constructor can install a provider or mint an owning ticket. +pub(crate) struct ResourceTransport { + pub(crate) checked: CheckedExecution, + pub(crate) checked_source_active: bool, + ledger: OwnerLedger, + registry: ResourceRegistry, + provider: InstalledResourceProvider, + scopes: Vec<(FrameId, HashMap)>, + operations: Vec, + operation_parents: HashMap, + abandoned_operations: HashMap, + active_frames: Vec, + returns: Vec, + defaults: Vec, + next_temporary: usize, + cleanup_error: Option, +} + +impl ResourceTransport { + pub(crate) fn checked_only( + program: Arc, + purpose: ExecutionPurpose, + scope_depth: usize, + ) -> Result { + let checked = CheckedExecution::new(program, purpose)?; + let mut transport = Self { + checked, + checked_source_active: false, + ledger: OwnerLedger::default(), + registry: ResourceRegistry::new(), + provider: InstalledResourceProvider::Disabled, + scopes: Vec::new(), + operations: Vec::new(), + operation_parents: HashMap::new(), + abandoned_operations: HashMap::new(), + active_frames: Vec::new(), + returns: Vec::new(), + defaults: Vec::new(), + next_temporary: 0, + cleanup_error: None, + }; + for _ in 0..scope_depth { + transport.push_scope(); + } + Ok(transport) + } + + pub(crate) fn push_scope(&mut self) { + let frame = self.ledger.frame(); + self.scopes.push((frame, HashMap::new())); + self.active_frames.push(ActiveFrame::Scope(frame)); + } + + pub(crate) fn pop_scope(&mut self) { + let Some((frame, _)) = self.scopes.last() else { + self.cleanup_error + .get_or_insert(ResourceExecutionError::InvalidFrame); + return; + }; + let frame = *frame; + if self.active_frames.last() != Some(&ActiveFrame::Scope(frame)) { + self.cleanup_error + .get_or_insert(ResourceExecutionError::InvalidFrame); + return; + } + let result = self.ledger.unwind(frame, &mut self.registry); + if !self.ledger.frame_is_live(frame) { + self.scopes.pop(); + self.active_frames.pop(); + } + if let Err(error) = result { + self.cleanup_error.get_or_insert(error); + } + } + + pub(crate) fn check_cleanup(&mut self) -> Result<(), ResourceExecutionError> { + self.cleanup_error.take().map_or(Ok(()), Err) + } + + fn scope_frame(&self) -> Result { + self.scopes + .last() + .map(|(frame, _)| *frame) + .ok_or(ResourceExecutionError::InvalidFrame) + } + + fn current_frame(&self) -> Result { + self.operations + .last() + .copied() + .map_or_else(|| self.scope_frame(), Ok) + } + + fn temporary(&mut self, frame: FrameId) -> Result { + let slot = self.next_temporary; + self.next_temporary = slot + .checked_add(1) + .ok_or(ResourceExecutionError::IdentityExhausted)?; + Ok(OwnerHolder::Temporary { frame, slot }) + } + + pub(crate) fn begin_operation(&mut self) -> Result { + let parent = self.current_frame()?; + let frame = self.ledger.frame(); + self.operations.push(frame); + self.operation_parents.insert(frame, parent); + self.active_frames.push(ActiveFrame::Operation(frame)); + Ok(OperationFrame { frame, parent }) + } + + pub(crate) fn end_operation( + &mut self, + operation: OperationFrame, + value: Option<&mut EvaluatedValue>, + ) -> Result<(), ResourceExecutionError> { + if self.operations.last() != Some(&operation.frame) + || self.operation_parents.get(&operation.frame) != Some(&operation.parent) + || self.active_frames.last() != Some(&ActiveFrame::Operation(operation.frame)) + { + return Err(ResourceExecutionError::InvalidFrame); + } + let preserved = match value { + Some(value) => match self.temporary(operation.parent) { + Ok(holder) => self.ledger.transfer(&mut value.custody, holder), + Err(error) => Err(error), + }, + None => Ok(()), + }; + // A failed output adoption still unwinds the original frame. Never + // remove its only cleanup path before a fallible holder transfer. + let cleanup = self.ledger.unwind(operation.frame, &mut self.registry); + if !self.ledger.frame_is_live(operation.frame) { + self.operations.pop(); + self.operation_parents.remove(&operation.frame); + self.active_frames.pop(); + } + match (preserved, cleanup, self.check_cleanup()) { + (_, Err(error), _) | (_, Ok(()), Err(error)) => Err(error), + (Err(error), Ok(()), Ok(())) => Err(error), + (Ok(()), Ok(()), Ok(())) => Ok(()), + } + } + + pub(crate) fn discard_value( + &mut self, + mut value: EvaluatedValue, + ) -> Result<(), ResourceExecutionError> { + let operation = self.begin_operation()?; + let adopted = self.hold_actual(&mut value); + let completed = self.end_operation(operation, None); + match (adopted, completed) { + (_, Err(error)) => Err(error), + (Err(error), Ok(())) => Err(error), + (Ok(()), Ok(())) => Ok(()), + } + } + + pub(crate) fn hold_actual( + &mut self, + value: &mut EvaluatedValue, + ) -> Result<(), ResourceExecutionError> { + let holder = self.temporary(self.current_frame()?)?; + self.ledger.transfer(&mut value.custody, holder)?; + self.ledger + .hold_borrows(&value.custody, self.current_frame()?) + } + + pub(crate) fn borrow( + &self, + value: &EvaluatedValue, + ) -> Result { + self.ledger.borrow(value) + } + + pub(crate) fn captured_resource_reference(&self, name: &str, body: Span) -> Option { + let mut found = false; + for (_, bindings) in self.scopes.iter().rev() { + for definition in bindings.keys() { + if self + .checked + .program() + .resolved() + .scope_table + .definitions + .get(definition.index() as usize) + .is_some_and(|info| info.name == name) + { + found = true; + if self.checked.referenced_in(*definition, body) { + return Some(true); + } + } + } + } + found.then_some(false) + } + + pub(crate) fn has_binding(&self, definition: DefId) -> bool { + self.scopes + .iter() + .rev() + .any(|(_, bindings)| bindings.contains_key(&definition)) + } + + pub(crate) fn binding( + &mut self, + definition: DefId, + borrow: bool, + ) -> Result { + let index = self + .scopes + .iter() + .rposition(|(_, bindings)| bindings.contains_key(&definition)) + .ok_or(ResourceExecutionError::InvalidOwner)?; + if borrow { + let value = self.scopes[index] + .1 + .get(&definition) + .and_then(|slot| slot.value.as_ref()) + .ok_or(ResourceExecutionError::InvalidOwner)?; + self.ledger.borrow(value) + } else { + let mut value = self.scopes[index] + .1 + .get_mut(&definition) + .and_then(|slot| slot.value.take()) + .ok_or(ResourceExecutionError::InvalidOwner)?; + // A borrowed alias is a physical carrier only; it cannot mint a + // ticket merely by being used with owned syntax. + if !value.custody.is_empty() && !value.custody.has_owners() { + self.scopes[index] + .1 + .get_mut(&definition) + .ok_or(ResourceExecutionError::InvalidOwner)? + .value = Some(value); + return Err(ResourceExecutionError::InvalidOwner); + } + self.hold_actual(&mut value)?; + Ok(value) + } + } + + pub(crate) fn install_binding( + &mut self, + declaration: Span, + mut value: EvaluatedValue, + ) -> Result { + let fact = self.checked.binding(declaration)?; + self.validate_value_type(&value, fact.ty)?; + let frame = self.scope_frame()?; + if self + .scopes + .last() + .is_some_and(|(_, bindings)| bindings.contains_key(&fact.definition)) + { + return Err(ResourceExecutionError::InvalidOwner); + } + self.ledger.transfer( + &mut value.custody, + OwnerHolder::Binding { + frame, + definition: fact.definition, + }, + )?; + self.ledger.hold_borrows(&value.custody, frame)?; + self.scopes + .last_mut() + .ok_or(ResourceExecutionError::InvalidFrame)? + .1 + .insert( + fact.definition, + ResourceBindingSlot { + fact, + value: Some(value), + }, + ); + Ok(fact.definition) + } + + pub(crate) fn enter_return(&mut self, destination: FrameId) { + self.returns.push(ReturnContext { + destination: Some(destination), + operation_floor: self.operations.len(), + }); + } + + /// Ordinary callable bodies cannot inherit a caller's custody destination. + pub(crate) fn enter_ordinary_return(&mut self) -> bool { + if !self.checked_source_active { + return false; + } + self.returns.push(ReturnContext { + destination: None, + operation_floor: self.operations.len(), + }); + true + } + + /// Cancel only calls opened in this checked function, before Return effects. + pub(crate) fn retire_return_operations(&mut self) -> Result<(), ResourceExecutionError> { + use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind}; + + let context = self + .returns + .last() + .copied() + .ok_or(ResourceExecutionError::InvalidFrame)?; + if !self.checked_source_active || context.operation_floor > self.operations.len() { + return Err(ResourceExecutionError::InvalidFrame); + } + let suffix = self.operations[context.operation_floor..] + .iter() + .rev() + .copied() + .collect::>(); + let mut first_error = None; + let mut first_panic = None; + for frame in suffix { + match catch_unwind(AssertUnwindSafe(|| self.retire_return_operation(frame))) { + Ok(Ok(())) => {} + Ok(Err(error)) => { + first_error.get_or_insert(error); + } + Err(payload) if first_panic.is_none() => first_panic = Some(payload), + Err(payload) => std::mem::forget(payload), + } + } + if let Some(payload) = first_panic { + resume_unwind(payload); + } + first_error.map_or_else(|| self.check_cleanup(), Err) + } + + fn retire_return_operation(&mut self, frame: FrameId) -> Result<(), ResourceExecutionError> { + use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind}; + + let parent = self + .operation_parents + .get(&frame) + .copied() + .ok_or(ResourceExecutionError::InvalidFrame)?; + if !self.ledger.frame_is_live(frame) + || self.abandoned_operations.contains_key(&frame) + || self.operations.iter().filter(|&&id| id == frame).count() != 1 + || self + .active_frames + .iter() + .filter(|&&active| active == ActiveFrame::Operation(frame)) + .count() + != 1 + { + return Err(ResourceExecutionError::InvalidFrame); + } + let cleanup = catch_unwind(AssertUnwindSafe(|| { + self.ledger.unwind(frame, &mut self.registry) + })); + // Handler/block Scopes may be interleaved. Retain their exact owners + // and loans; only the physically retired Operation leaves this stack. + if !self.ledger.frame_is_live(frame) { + self.operations.retain(|&id| id != frame); + self.operation_parents.remove(&frame); + self.active_frames + .retain(|&active| active != ActiveFrame::Operation(frame)); + self.abandoned_operations.insert(frame, parent); + } + match cleanup { + Ok(result) => result, + Err(payload) => resume_unwind(payload), + } + } + + /// Exact one-shot completion; called only for a propagated Return or error. + pub(crate) fn acknowledge_return_operation( + &mut self, + operation: &OperationFrame, + ) -> Result { + let Some(parent) = self.abandoned_operations.get(&operation.frame) else { + return Ok(false); + }; + if *parent != operation.parent + || self.ledger.frame_is_live(operation.frame) + || self.operation_parents.contains_key(&operation.frame) + || self.operations.contains(&operation.frame) + || self + .active_frames + .contains(&ActiveFrame::Operation(operation.frame)) + { + return Err(ResourceExecutionError::InvalidFrame); + } + self.abandoned_operations.remove(&operation.frame); + Ok(true) + } + pub(crate) fn leave_return(&mut self) { + self.returns.pop(); + } + pub(crate) fn preserve_return( + &mut self, + value: &mut EvaluatedValue, + ) -> Result<(), ResourceExecutionError> { + if value.custody.has_borrows() { + return Err(ResourceExecutionError::InvalidOwner); + } + let destination = self + .returns + .last() + .and_then(|context| context.destination) + .ok_or(ResourceExecutionError::InvalidFrame)?; + let holder = self.temporary(destination)?; + self.ledger.transfer(&mut value.custody, holder) + } + + pub(crate) fn enter_default(&mut self) -> Result<(), ResourceExecutionError> { + self.defaults.push(self.current_frame()?); + Ok(()) + } + pub(crate) fn leave_default(&mut self) { + self.defaults.pop(); + } + pub(crate) fn preserve_default( + &mut self, + value: &mut EvaluatedValue, + ) -> Result<(), ResourceExecutionError> { + if value.custody.has_borrows() { + return Err(ResourceExecutionError::InvalidOwner); + } + let destination = self + .defaults + .last() + .copied() + .ok_or(ResourceExecutionError::InvalidFrame)?; + let holder = self.temporary(destination)?; + self.ledger.transfer(&mut value.custody, holder) + } + + pub(crate) fn cursor(&self) -> CheckedBodyCursor { + self.checked.cursor() + } + pub(crate) fn enter_callee( + &mut self, + invocation: &FunctionInvocation, + ) -> Result<(), ResourceExecutionError> { + let FunctionInvocation::Source(invocation) = invocation else { + self.checked.enter_ordinary(); + return Ok(()); + }; + match invocation.target() { + CheckedInvocationTarget::Resolved(_) => { + self.checked.enter_ordinary(); + Ok(()) + } + CheckedInvocationTarget::Generic(call) => self.checked.enter_generic(call), + CheckedInvocationTarget::Method(_) + | CheckedInvocationTarget::Interface(_) + | CheckedInvocationTarget::Indirect(_) + | CheckedInvocationTarget::Intrinsic(_) => { + Err(ResourceExecutionError::InvalidInvocation) + } + } + } + + pub(crate) fn formal_value( + &self, + value: &EvaluatedValue, + access: CheckedCalleeAccess, + ) -> Result, ResourceExecutionError> { + match access { + CheckedCalleeAccess::View => self.ledger.borrow(value).map(Some), + CheckedCalleeAccess::Owned => Ok(None), + } + } + + pub(crate) fn validate_actual( + &self, + value: &EvaluatedValue, + effect: CheckedCallerEffect, + ) -> Result<(), ResourceExecutionError> { + if value.custody.is_empty() { + return Ok(()); + } + match effect { + CheckedCallerEffect::Copy | CheckedCallerEffect::ObserveData => { + Err(ResourceExecutionError::InvalidOwner) + } + CheckedCallerEffect::TransferOwned | CheckedCallerEffect::RelinquishOwned => { + if value.custody.has_owners() { + Ok(()) + } else { + Err(ResourceExecutionError::InvalidOwner) + } + } + CheckedCallerEffect::RetainBorrow => { + // Written view of a producer may still have an operation-owned + // backing; the callee receives only the derived borrow. + self.ledger.borrow(value).map(|_| ()) + } + } + } + + pub(crate) fn invoke_hook( + &mut self, + invocation: &CheckedInvocation, + descriptor: &ResourceHookDescriptor, + arguments: Vec, + ) -> Result { + if !self.checked_source_active { + return Err(ResourceExecutionError::InvalidInvocation); + } + let (hook, resource, recipe) = self.checked.reached_hook(invocation, descriptor)?; + let jett_types::Type::Function { params, .. } = self + .checked + .program() + .checked() + .interner + .resolve(hook.function_type) + else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if params.len() != arguments.len() { + return Err(ResourceExecutionError::InvalidInvocation); + } + for (argument, ty) in arguments.iter().zip(params) { + self.validate_value_type(argument, *ty)?; + } + self.provider.ensure_installed()?; + if recipe == ResourceKernelRecipe::Finalize { + let [token] = <[EvaluatedValue; 1]>::try_from(arguments) + .map_err(|_| ResourceExecutionError::InvalidInvocation)?; + self.ledger.close(token.custody, &mut self.registry)?; + return Ok(EvaluatedValue::ordinary(Value::Nothing)); + } + match (recipe, arguments.as_slice()) { + (ResourceKernelRecipe::NetworkFactory, [network, label]) => { + let (Value::GrantedNetwork(grant), Value::Int64(label)) = + (network.value.payload(), label.value.payload()) + else { + return Err(ResourceExecutionError::InvalidGrant); + }; + // All fallible holder allocation precedes provider publication. + // A successful key is adopted immediately or explicitly closed. + let holder = self.temporary(self.current_frame()?)?; + match self + .provider + .construct(&mut self.registry, resource, grant, *label)? + { + Ok(key) => { + let carrier = ResourceCarrier { + program: self.checked.program().clone(), + resource, + key, + }; + let custody = match self.ledger.acquire(carrier.clone(), holder) { + Ok(custody) => custody, + Err(error) => { + self.registry.close(key, resource.registry_type)?; + return Err(error); + } + }; + Ok(EvaluatedValue { + value: Value::ResultOk(Box::new(Value::Resource(carrier))), + custody, + } + .prefix(PayloadStep::Ok)) + } + Err(error) => Ok(EvaluatedValue::ordinary(Value::ResultFail(Box::new( + Value::String(error), + )))), + } + } + (ResourceKernelRecipe::NetworkBorrow, [network, token]) => { + let Value::GrantedNetwork(grant) = network.value.payload() else { + return Err(ResourceExecutionError::InvalidGrant); + }; + let carrier = self.ledger.borrowed_carrier(&token.custody)?; + if !Arc::ptr_eq(&carrier.program, self.checked.program()) + || carrier.resource != resource + { + return Err(ResourceExecutionError::ForeignProgram); + } + let outcome = + self.provider + .borrow(&mut self.registry, resource, grant, carrier.key)?; + Ok(EvaluatedValue::ordinary(match outcome { + Ok(value) => Value::ResultOk(Box::new(Value::Int64(value))), + Err(error) => Value::ResultFail(Box::new(Value::String(error))), + })) + } + _ => Err(ResourceExecutionError::InvalidInvocation), + } + } + + pub(crate) fn validate_value_type( + &self, + value: &EvaluatedValue, + ty: jett_types::TypeId, + ) -> Result<(), ResourceExecutionError> { + fn positions<'a>( + value: &'a Value, + path: &mut Vec, + out: &mut Vec<(Vec, &'a ResourceCarrier)>, + ) -> Result<(), ResourceExecutionError> { + let (step, child) = match value { + Value::Resource(carrier) => { + out.push((path.clone(), carrier)); + return Ok(()); + } + Value::Struct { fields, .. } => { + for (ordinal, (_, child)) in fields.iter().enumerate() { + path.push(PayloadStep::Field(ordinal)); + positions(child, path, out)?; + path.pop(); + } + return Ok(()); + } + Value::Typed { value, .. } => (PayloadStep::Typed, value.as_ref()), + Value::ResultOk(value) => (PayloadStep::Ok, value.as_ref()), + Value::ResultFail(value) => (PayloadStep::Fail, value.as_ref()), + Value::OptionalSome(value) => (PayloadStep::Some, value.as_ref()), + value + if !value.contains_live_resource_or_grant() + || matches!(value, Value::GrantedNetwork(_)) => + { + return Ok(()); + } + _ => return Err(ResourceExecutionError::InvalidPayloadPath), + }; + path.push(step); + positions(child, path, out)?; + path.pop(); + Ok(()) + } + fn shape( + value: &Value, + ty: jett_types::TypeId, + checked: &CheckedExecution, + ) -> Result<(), ResourceExecutionError> { + use jett_types::{CapabilityKind, Type, TypeInterner}; + let types = &checked.program().checked().interner; + if ty.index() as usize >= types.len() { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + if !value.contains_live_resource_or_grant() && checked.type_contains_resource(ty)? { + return absent_generated::absent_shape(value, ty, checked); + } + match (value, types.resolve(ty)) { + (Value::Resource(carrier), Type::Resource(_)) + if Arc::ptr_eq(&carrier.program, checked.program()) + && carrier.resource.checked_type == ty => + { + Ok(()) + } + ( + Value::Struct { + type_name, + concrete_type, + fields, + }, + Type::Struct(owner), + ) if checked.type_contains_resource(ty)? => { + let definition = types.resolve_struct(*owner); + if type_name != &definition.name + || concrete_type + .as_ref() + .is_some_and(|name| name != &definition.name) + || !types.nominal_type_arguments(ty).is_empty() + || fields.len() != definition.fields.len() + { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + for ((name, value), (expected_name, field_type)) in + fields.iter().zip(&definition.fields) + { + if name != expected_name { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + match types.resolve(*field_type) { + Type::Resource(_) => shape(value, *field_type, checked)?, + Type::Int64 | Type::String | Type::Bool | Type::Nothing => { + absent_generated::absent_shape(value, *field_type, checked)?; + } + _ => return Err(ResourceExecutionError::InvalidPayloadPath), + } + } + Ok(()) + } + (Value::GrantedNetwork(_), Type::Capability(CapabilityKind::Network)) => Ok(()), + (Value::ResultOk(value), Type::Result(success, _)) => { + shape(value, *success, checked) + } + (Value::ResultFail(value), Type::Result(_, error)) => shape(value, *error, checked), + (Value::OptionalSome(value), Type::Optional(success)) => { + shape(value, *success, checked) + } + (Value::OptionalNone, Type::Optional(_)) => Ok(()), + (Value::ResourceHook(descriptor), Type::Function { .. }) + if Arc::ptr_eq(&descriptor.program, checked.program()) + && checked + .program() + .checked() + .resource_hooks + .get(&descriptor.definition) + .is_some_and(|hook| hook.function_type == ty) => + { + Ok(()) + } + (Value::Int64(_), _) if ty == TypeInterner::INT64 => Ok(()), + (Value::String(_), _) if ty == TypeInterner::STRING => Ok(()), + (Value::Nothing, _) if ty == TypeInterner::NOTHING => Ok(()), + (value, _) + if !value.contains_live_resource_or_grant() + && !checked.type_contains_resource(ty)? => + { + Ok(()) + } + _ => Err(ResourceExecutionError::InvalidPayloadPath), + } + } + shape(&value.value, ty, &self.checked)?; + let mut carriers = Vec::new(); + positions(&value.value, &mut Vec::new(), &mut carriers)?; + self.ledger.validate_positions(&value.custody, &carriers) + } + + pub(crate) fn unwind_all(&mut self) -> Result<(), ResourceExecutionError> { + use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind}; + let mut first_panic = None; + let mut first_error = None; + let mut retry = Vec::new(); + // One chronological stack preserves the scope/operation interleaving. + // A callee's borrowed formal is released before its caller owner. + while let Some(active) = self.active_frames.pop() { + let frame = active.id(); + let role_matches = match active { + ActiveFrame::Scope(_) => { + if self.scopes.last().map(|(id, _)| *id) == Some(frame) { + self.scopes.pop(); + true + } else { + false + } + } + ActiveFrame::Operation(_) => { + if self.operations.last() == Some(&frame) { + self.operations.pop(); + self.operation_parents.remove(&frame); + true + } else { + false + } + } + }; + if !role_matches { + first_error.get_or_insert(ResourceExecutionError::InvalidFrame); + } + // A finalizer panic can leave a physically retired frame on the + // activation stack; do not finalize any member a second time. + if !self.ledger.frame_is_live(frame) { + continue; + } + match catch_unwind(AssertUnwindSafe(|| { + self.ledger.unwind(frame, &mut self.registry) + })) { + Ok(Ok(())) => {} + Ok(Err(error)) => { + first_error.get_or_insert(error); + } + Err(payload) if first_panic.is_none() => first_panic = Some(payload), + Err(payload) => std::mem::forget(payload), + } + if self.ledger.frame_is_live(frame) { + retry.push(frame); + } + } + // Retain failed cleanup obligations until older exact frames have + // released their leases. This does not make the invalid relation valid. + for frame in retry { + match catch_unwind(AssertUnwindSafe(|| { + self.ledger.unwind(frame, &mut self.registry) + })) { + Ok(Ok(())) => {} + Ok(Err(error)) => { + first_error.get_or_insert(error); + } + Err(payload) if first_panic.is_none() => first_panic = Some(payload), + Err(payload) => std::mem::forget(payload), + } + } + if !self.operations.is_empty() + || !self.operation_parents.is_empty() + || !self.scopes.is_empty() + { + first_error.get_or_insert(ResourceExecutionError::InvalidFrame); + } + if let Some(payload) = first_panic { + resume_unwind(payload); + } + first_error.map_or_else(|| self.check_cleanup(), Err) + } + + pub(crate) fn validate_entry_scopes( + &self, + scope_depth: usize, + ) -> Result<(), ResourceExecutionError> { + if !self.operations.is_empty() + || !self.operation_parents.is_empty() + || !self.abandoned_operations.is_empty() + || !self.returns.is_empty() + || !self.defaults.is_empty() + || self.ledger.live_owners() != 0 + || self.ledger.has_live_borrows() + || self.registry.live_count() != 0 + || self.scopes.len() != scope_depth + || self.active_frames.len() != scope_depth + || self.ledger.live_frame_count() != scope_depth + { + return Err(ResourceExecutionError::InvalidFrame); + } + for ((frame, bindings), active) in self.scopes.iter().zip(&self.active_frames) { + if !bindings.is_empty() + || *active != ActiveFrame::Scope(*frame) + || !self.ledger.frame_is_live(*frame) + { + return Err(ResourceExecutionError::InvalidFrame); + } + } + Ok(()) + } + pub(crate) fn restore_entry_scopes( + &mut self, + scope_depth: usize, + ) -> Result<(), ResourceExecutionError> { + if !self.active_frames.is_empty() + || !self.operations.is_empty() + || !self.operation_parents.is_empty() + || !self.scopes.is_empty() + || self.ledger.has_live_frames() + || self.ledger.live_owners() != 0 + || self.registry.live_count() != 0 + { + return Err(ResourceExecutionError::InvalidFrame); + } + self.returns.clear(); + self.defaults.clear(); + // Panics skip suspended envelopes. Receipts expire only after the + // complete entry has no remaining frame, owner or registry obligation. + self.abandoned_operations.clear(); + let prior_cleanup = self.check_cleanup(); + for _ in 0..scope_depth { + self.push_scope(); + } + prior_cleanup + } + + pub(crate) fn live_owners(&self) -> usize { + self.ledger.live_owners() + } + pub(crate) fn registry_live_count(&self) -> usize { + self.registry.live_count() + } + + #[cfg(test)] + pub(crate) fn install_script( + &mut self, + operations: Vec, + ) -> Result { + if self.checked.purpose() != ExecutionPurpose::ReferenceRuntime { + return Err(ResourceExecutionError::WrongPurpose); + } + if !matches!(self.provider, InstalledResourceProvider::Disabled) + || self.ledger.live_owners() != 0 + { + return Err(ResourceExecutionError::InvalidGrant); + } + let provider = super::provider::ScriptedResourceProvider::new(operations)?; + let grant = provider.grant(); + self.provider = InstalledResourceProvider::Scripted(provider); + Ok(grant) + } + + #[cfg(test)] + pub(crate) fn provider_events( + &self, + ) -> Result, ResourceExecutionError> { + match &self.provider { + InstalledResourceProvider::Disabled => Err(ResourceExecutionError::ProviderDisabled), + InstalledResourceProvider::Scripted(provider) => provider.events(), + } + } +} + +#[cfg(test)] +mod absence_tests; +#[cfg(test)] +mod return_prefix_tests; + +#[cfg(test)] +mod tests { + use super::*; + use crate::resource_execution::{ProviderEvent, ScriptOperation, tests::program}; + use jett_types::ResourceHookKind; + + fn constructed(runtime: &mut ResourceTransport, label: i64) -> EvaluatedValue { + let definition = runtime + .checked + .program() + .checked() + .resource_hooks + .values() + .find(|hook| hook.kind == ResourceHookKind::Construct) + .unwrap() + .definition; + let span = *runtime + .checked + .program() + .checked() + .call_ownership + .iter() + .find(|(_, packet)| packet.target == CheckedInvocationTarget::Resolved(definition)) + .unwrap() + .0; + let invocation = runtime.checked.invocation(span).unwrap(); + let descriptor = runtime.checked.descriptor(definition).unwrap(); + let grant = match &runtime.provider { + InstalledResourceProvider::Scripted(provider) => provider.grant(), + _ => panic!("selected private provider"), + }; + let mut value = runtime + .invoke_hook( + &invocation, + &descriptor, + vec![ + EvaluatedValue::ordinary(Value::GrantedNetwork(grant)), + EvaluatedValue::ordinary(Value::Int64(label)), + ], + ) + .unwrap(); + let Value::ResultOk(payload) = value.value else { + panic!("occupied real construction"); + }; + value.value = *payload; + value.remove_prefix(PayloadStep::Ok).unwrap(); + value + } + + #[test] + fn failed_discard_adoption_completes_its_frame_without_retiring_protected_owner() { + let checked = program( + include_str!("fixtures/04_bare_to_view_operation_cleanup.jett"), + false, + ); + let mut runtime = + ResourceTransport::checked_only(checked, ExecutionPurpose::ReferenceRuntime, 1) + .unwrap(); + runtime.checked_source_active = true; + runtime + .install_script(vec![ScriptOperation::Construct { + label: 401, + outcome: Ok(()), + }]) + .unwrap(); + let value = constructed(&mut runtime, 401); + let outer = runtime.begin_operation().unwrap(); + let mut borrow = runtime.borrow(&value).unwrap(); + runtime.hold_actual(&mut borrow).unwrap(); + assert_eq!( + runtime.discard_value(value), + Err(ResourceExecutionError::InvalidBorrow) + ); + assert_eq!(runtime.operations, [outer.frame]); + assert_eq!( + runtime.active_frames.last(), + Some(&ActiveFrame::Operation(outer.frame)) + ); + assert_eq!( + runtime.provider_events().unwrap(), + [ProviderEvent::Constructed(401)] + ); + assert_eq!(runtime.live_owners(), 1); + runtime.end_operation(outer, None).unwrap(); + runtime.unwind_all().unwrap(); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(401), + ProviderEvent::Finalized(401) + ] + ); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); + } + + #[test] + fn interleaved_scope_and_operation_cleanup_preserves_reverse_holder_order() { + let checked = program(include_str!("fixtures/02_reverse_scope_drop.jett"), false); + let mut runtime = + ResourceTransport::checked_only(checked, ExecutionPurpose::ReferenceRuntime, 1) + .unwrap(); + runtime.checked_source_active = true; + runtime + .install_script(vec![ + ScriptOperation::Construct { + label: 201, + outcome: Ok(()), + }, + ScriptOperation::Construct { + label: 202, + outcome: Ok(()), + }, + ]) + .unwrap(); + let operation = runtime.begin_operation().unwrap(); + let first = constructed(&mut runtime, 201); + runtime.push_scope(); + let mut second = constructed(&mut runtime, 202); + let holder = runtime.temporary(runtime.scope_frame().unwrap()).unwrap(); + runtime + .ledger + .transfer(&mut second.custody, holder) + .unwrap(); + assert!(matches!( + runtime.active_frames.last(), + Some(ActiveFrame::Scope(_)) + )); + runtime.unwind_all().unwrap(); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(201), + ProviderEvent::Constructed(202), + ProviderEvent::Finalized(202), + ProviderEvent::Finalized(201) + ] + ); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); + assert!( + runtime.operations.is_empty() + && runtime.scopes.is_empty() + && runtime.active_frames.is_empty() + ); + drop((operation, first, second)); + } + + #[test] + fn reached_capability_free_close_refuses_worker_purpose_before_owner_retirement() { + for purpose in [ + ExecutionPurpose::NamespaceConstant, + ExecutionPurpose::ExplicitComptime, + ExecutionPurpose::Verify, + ExecutionPurpose::Property, + ] { + let program = program( + include_str!("fixtures/09_capability_free_close_target.jett"), + false, + ); + let mut runtime = ResourceTransport::checked_only( + program.clone(), + ExecutionPurpose::ReferenceRuntime, + 1, + ) + .unwrap(); + runtime.checked_source_active = true; + let grant = runtime + .install_script(vec![ScriptOperation::Construct { + label: 901, + outcome: Ok(()), + }]) + .unwrap(); + let definition = |kind| { + program + .checked() + .resource_hooks + .values() + .find(|hook| hook.kind == kind) + .unwrap() + .definition + }; + let occurrence = |definition| { + *program + .checked() + .call_ownership + .iter() + .find(|(_, packet)| { + packet.target == CheckedInvocationTarget::Resolved(definition) + }) + .unwrap() + .0 + }; + // Exact source kernel packets and a real registry publication create + // custody; no Resource carrier or owning ticket is fabricated. + let create = runtime + .checked + .invocation(occurrence(definition(ResourceHookKind::Construct))) + .unwrap(); + let create_descriptor = runtime + .checked + .descriptor(definition(ResourceHookKind::Construct)) + .unwrap(); + let mut token = runtime + .invoke_hook( + &create, + &create_descriptor, + vec![ + EvaluatedValue::ordinary(Value::GrantedNetwork(grant)), + EvaluatedValue::ordinary(Value::Int64(901)), + ], + ) + .unwrap(); + let Value::ResultOk(value) = token.value else { + panic!("occupied factory result"); + }; + token.value = *value; + token.remove_prefix(PayloadStep::Ok).unwrap(); + let close = runtime + .checked + .invocation(occurrence(definition(ResourceHookKind::Close))) + .unwrap(); + let close_descriptor = runtime + .checked + .descriptor(definition(ResourceHookKind::Close)) + .unwrap(); + runtime.checked.replace_purpose(purpose); + assert_eq!( + runtime + .invoke_hook(&close, &close_descriptor, vec![token]) + .unwrap_err(), + ResourceExecutionError::WrongPurpose + ); + assert_eq!( + runtime.provider_events().unwrap(), + [ProviderEvent::Constructed(901)] + ); + assert_eq!(runtime.live_owners(), 1); + assert_eq!(runtime.registry_live_count(), 1); + runtime.unwind_all().unwrap(); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(901), + ProviderEvent::Finalized(901) + ] + ); + assert_eq!(runtime.live_owners(), 0); + assert_eq!(runtime.registry_live_count(), 0); + } + } + + #[test] + fn operation_cleanup_error_overrides_failed_output_preservation_and_retires_once() { + let checked = program( + include_str!("fixtures/04_bare_to_view_operation_cleanup.jett"), + false, + ); + let mut runtime = + ResourceTransport::checked_only(checked, ExecutionPurpose::ReferenceRuntime, 1) + .unwrap(); + runtime.checked_source_active = true; + runtime + .install_script(vec![ScriptOperation::Construct { + label: 401, + outcome: Ok(()), + }]) + .unwrap(); + let operation = runtime.begin_operation().unwrap(); + let mut value = constructed(&mut runtime, 401); + let Value::Resource(carrier) = &value.value else { + panic!("actual occupied resource construction"); + }; + // Private corruption control: retire the actual registry entry without + // retiring the ledger obligation. No source program can perform this. + runtime + .registry + .close(carrier.key, carrier.resource.registry_type) + .unwrap(); + runtime.next_temporary = usize::MAX; + assert_eq!( + runtime.end_operation(operation, Some(&mut value)), + Err(ResourceExecutionError::Registry( + jett_runtime::RegistryError::StaleGeneration + )) + ); + assert!(runtime.operations.is_empty()); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(401), + ProviderEvent::Finalized(401) + ] + ); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); + runtime.unwind_all().unwrap(); + assert!( + runtime.active_frames.is_empty() + && runtime.operations.is_empty() + && runtime.scopes.is_empty() + ); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(401), + ProviderEvent::Finalized(401) + ] + ); + } + + #[test] + fn discard_cleanup_error_overrides_adoption_error_without_losing_parent_custody() { + let checked = program( + include_str!("fixtures/04_bare_to_view_operation_cleanup.jett"), + false, + ); + let mut runtime = + ResourceTransport::checked_only(checked, ExecutionPurpose::ReferenceRuntime, 1) + .unwrap(); + runtime.checked_source_active = true; + runtime + .install_script(vec![ScriptOperation::Construct { + label: 401, + outcome: Ok(()), + }]) + .unwrap(); + let value = constructed(&mut runtime, 401); + // Private completion fault: holder allocation fails while a distinct + // earlier cleanup error remains queued. Neither is provider authority. + runtime.next_temporary = usize::MAX; + runtime.cleanup_error = Some(ResourceExecutionError::InvalidFrame); + assert_eq!( + runtime.discard_value(value), + Err(ResourceExecutionError::InvalidFrame) + ); + assert!(runtime.operations.is_empty()); + assert_eq!(runtime.active_frames.len(), 1); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (1, 1) + ); + assert_eq!( + runtime.provider_events().unwrap(), + [ProviderEvent::Constructed(401)] + ); + runtime.unwind_all().unwrap(); + assert!(runtime.active_frames.is_empty() && runtime.scopes.is_empty()); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(401), + ProviderEvent::Finalized(401) + ] + ); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); + } +} diff --git a/crates/jett_comptime/src/resource_execution/runtime/absence_tests.rs b/crates/jett_comptime/src/resource_execution/runtime/absence_tests.rs new file mode 100644 index 00000000..564dacc7 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/runtime/absence_tests.rs @@ -0,0 +1,579 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use jett_common::FileId; +use jett_parser::ast::Item; +use jett_resolve::{DefId, DefKind}; +use jett_types::{Type, TypeId}; + +use super::*; +use crate::resource_execution::tests::program; +use crate::{Interpreter, Value}; + +const SOURCE: &str = include_str!("../fixtures/17_absent_aggregate_shapes.jett"); + +fn source_entry(program: &CheckedResourceProgram, name: &str) -> DefId { + let functions = program + .module() + .items + .iter() + .filter_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) && function.name.name == name => + { + Some(function) + } + _ => None, + }) + .collect::>(); + let [function] = functions.as_slice() else { + panic!("one primary ordinary source entry {name}"); + }; + let definitions = program + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == DefKind::Function + && definition.span == function.name.span + && definition.namespace.as_deref() == Some("app") + }) + .map(|definition| definition.id) + .collect::>(); + let [definition] = definitions.as_slice() else { + panic!("one exact retained ordinary declaration {name}"); + }; + *definition +} + +fn returned_type(program: &CheckedResourceProgram, name: &str) -> TypeId { + let definition = source_entry(program, name); + let signature = program.checked().definition_types[&definition]; + let Type::Function { return_type, .. } = program.checked().interner.resolve(signature) else { + panic!("exact checked signature"); + }; + *return_type +} + +fn absent_values() -> Vec<(&'static str, Value)> { + vec![ + ("empty_tokens", Value::List(Vec::new())), + ( + "absent_tokens", + Value::List(vec![Value::OptionalNone, Value::OptionalNone]), + ), + ("empty_map", Value::Map(Vec::new())), + ( + "absent_map", + Value::Map(vec![(Value::String("empty".into()), Value::OptionalNone)]), + ), + ( + "empty_result_tokens", + Value::ResultOk(Box::new(Value::List(Vec::new()))), + ), + ( + "failed_result_tokens", + Value::ResultFail(Box::new(Value::String("empty".into()))), + ), + ( + "absent_envelope", + Value::Struct { + type_name: "app.TokenEnvelope".into(), + concrete_type: None, + fields: vec![ + ("tokens".into(), Value::List(Vec::new())), + ("fallback".into(), Value::OptionalNone), + ("marker".into(), Value::Int64(17)), + ], + }, + ), + ( + "absent_box", + Value::Struct { + type_name: "app.TokenBox".into(), + concrete_type: Some("app.TokenBox[resource_probe.TestHandle]".into()), + fields: vec![("items".into(), Value::List(Vec::new()))], + }, + ), + ( + "empty_choice", + Value::Enum { + type_name: "app.TokenChoice".into(), + variant: "empty".into(), + fields: Vec::new(), + }, + ), + ( + "absent_choice", + Value::Enum { + type_name: "app.TokenChoice".into(), + variant: "vacant".into(), + fields: vec![Value::Int64(17), Value::List(Vec::new())], + }, + ), + ( + "absent_state", + Value::Machine { + type_name: "app.TokenState".into(), + state: "vacant".into(), + fields: vec![Value::OptionalNone, Value::String("empty".into())], + }, + ), + ( + "absent_exact_state", + Value::Machine { + type_name: "app.TokenState".into(), + state: "vacant".into(), + fields: vec![Value::OptionalNone, Value::String("empty".into())], + }, + ), + ] +} + +#[test] +fn resource_absent_aggregates_execute_exact_checked_source_without_provider_events() { + for release in [false, true] { + let checked = program(SOURCE, release); + for (name, expected) in absent_values() { + let target = source_entry(&checked, name); + let mut interpreter = Interpreter::from_checked_resource_program( + checked.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + interpreter + .install_resource_test_script(Vec::new()) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(target, Vec::new()) + .unwrap(), + expected, + "{name}, release={release}" + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (Vec::new(), 0, 0) + ); + assert!(interpreter.take_debug_events().is_empty()); + } + } +} + +#[test] +fn resource_absent_aggregates_remain_pure_in_all_required_purposes_and_workers() { + for release in [false, true] { + let checked = program(SOURCE, release); + for purpose in [ + ExecutionPurpose::NamespaceConstant, + ExecutionPurpose::ExplicitComptime, + ExecutionPurpose::Verify, + ExecutionPurpose::Property, + ] { + for (name, expected) in absent_values() { + let target = source_entry(&checked, name); + let mut interpreter = + Interpreter::from_checked_resource_program(checked.clone(), purpose).unwrap(); + assert!( + interpreter + .install_resource_test_script(Vec::new()) + .is_err() + ); + assert_eq!( + interpreter + .call_checked_program_entry(target, Vec::new()) + .unwrap(), + expected + ); + assert!(interpreter.take_debug_events().is_empty()); + } + } + let types = Arc::new(crate::checked_types::CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let explicit = crate::evaluate_explicit_comptime_expressions_capture( + checked.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + types.clone(), + Arc::new(HashMap::new()), + ); + assert!( + explicit.diagnostics.is_empty(), + "{:?}", + explicit.diagnostics + ); + assert!(explicit.debug_events.is_empty()); + let declaration = checked + .module() + .items + .iter() + .find_map(|item| match item { + Item::VarDecl(declaration) if declaration.name.name == "absent_namespace_value" => { + Some(declaration.name.span) + } + _ => None, + }) + .expect("one actual source namespace constant"); + assert_eq!( + explicit.values.constant(declaration), + Some(&Value::Int64(17)) + ); + let blocks = crate::run_verify_blocks_detailed_with_metadata_and_expression_types( + checked.module(), + None, + Some(types), + None, + ); + assert_eq!(blocks.len(), 2); + assert!( + blocks + .iter() + .all(|block| block.passed && block.debug_events.is_empty()), + "{blocks:?}" + ); + assert_eq!(blocks.iter().filter(|block| block.is_property).count(), 1); + assert_eq!( + blocks + .iter() + .find(|block| block.is_property) + .unwrap() + .iterations, + Some(100) + ); + } +} + +#[test] +fn resource_absent_aggregate_shape_checks_reject_wrong_elements_keys_and_sum_payloads() { + for release in [false, true] { + let checked = program(SOURCE, release); + let runtime = + ResourceTransport::checked_only(checked.clone(), ExecutionPurpose::ReferenceRuntime, 1) + .unwrap(); + let bad = vec![ + ("empty_tokens", Value::String("not a list".into())), + ("empty_tokens", Value::List(vec![Value::Nothing])), + ("absent_tokens", Value::List(vec![Value::Int64(1)])), + ( + "absent_tokens", + Value::List(vec![Value::OptionalSome(Box::new(Value::Nothing))]), + ), + ("empty_map", Value::List(Vec::new())), + ( + "absent_map", + Value::Map(vec![(Value::Int64(1), Value::OptionalNone)]), + ), + ( + "absent_map", + Value::Map(vec![(Value::String("empty".into()), Value::Nothing)]), + ), + ( + "empty_result_tokens", + Value::ResultOk(Box::new(Value::String("not a list".into()))), + ), + ( + "failed_result_tokens", + Value::ResultFail(Box::new(Value::Int64(1))), + ), + ( + "absent_tokens", + Value::Typed { + type_name: "list[optional[string]]".into(), + value: Box::new(Value::List(Vec::new())), + }, + ), + ]; + for (name, value) in bad { + assert_eq!( + runtime.validate_value_type( + &EvaluatedValue::ordinary(value), + returned_type(&checked, name) + ), + Err(ResourceExecutionError::InvalidPayloadPath), + "{name}" + ); + } + let signature = + checked.checked().definition_types[&source_entry(&checked, "refined_shape_contract")]; + let Type::Function { params, .. } = checked.checked().interner.resolve(signature) else { + panic!("exact refined shape contract"); + }; + let refined = params[0]; + for marker in [0, 17] { + let unproved = Value::Struct { + type_name: "app.RefinedEnvelope".into(), + concrete_type: None, + fields: vec![ + ( + "marker".into(), + Value::Typed { + type_name: "app.PositiveMarker".into(), + value: Box::new(Value::Int64(marker)), + }, + ), + ("tokens".into(), Value::List(Vec::new())), + ], + }; + assert_eq!( + runtime.validate_value_type(&EvaluatedValue::ordinary(unproved), refined), + Err(ResourceExecutionError::InvalidPayloadPath) + ); + } + assert_eq!(runtime.live_owners(), 0); + assert_eq!(runtime.registry_live_count(), 0); + } +} + +#[test] +fn resource_absent_nominal_shape_checks_reject_foreign_names_fields_variants_states_and_generics() { + for release in [false, true] { + let checked = program(SOURCE, release); + let runtime = + ResourceTransport::checked_only(checked.clone(), ExecutionPurpose::ReferenceRuntime, 1) + .unwrap(); + for (name, value) in absent_values() + .into_iter() + .filter(|(name, _)| name.starts_with("absent_") || *name == "empty_choice") + { + let ty = returned_type(&checked, name); + assert_eq!( + runtime.validate_value_type(&EvaluatedValue::ordinary(value.clone()), ty), + Ok(()) + ); + let mut corruptions = Vec::new(); + match &value { + Value::Struct { .. } => { + let mut wrong = value.clone(); + if let Value::Struct { type_name, .. } = &mut wrong { + *type_name = "foreign.SameShape".into(); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Struct { fields, .. } = &mut wrong { + fields[0].0 = "foreign".into(); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Struct { fields, .. } = &mut wrong { + fields.pop(); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Struct { fields, .. } = &mut wrong { + fields.push(fields[0].clone()); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Struct { concrete_type, .. } = &mut wrong { + *concrete_type = Some("app.TokenBox[string]".into()); + } + corruptions.push(wrong); + if name == "absent_envelope" { + let mut wrong = value.clone(); + if let Value::Struct { fields, .. } = &mut wrong { + fields.swap(0, 1); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Struct { fields, .. } = &mut wrong { + fields[2].1 = Value::String("17".into()); + } + corruptions.push(wrong); + } + } + Value::Enum { .. } => { + let mut wrong = value.clone(); + if let Value::Enum { type_name, .. } = &mut wrong { + *type_name = "foreign.TokenChoice".into(); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Enum { variant, .. } = &mut wrong { + *variant = "missing".into(); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Enum { fields, .. } = &mut wrong { + fields.push(Value::Nothing); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Enum { + variant, fields, .. + } = &mut wrong + { + *variant = "occupied".into(); + *fields = vec![Value::Nothing]; + } + corruptions.push(wrong); + } + Value::Machine { .. } => { + let mut wrong = value.clone(); + if let Value::Machine { type_name, .. } = &mut wrong { + *type_name = "foreign.TokenState".into(); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Machine { state, .. } = &mut wrong { + *state = "missing".into(); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Machine { fields, .. } = &mut wrong { + fields.pop(); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Machine { state, fields, .. } = &mut wrong { + *state = "occupied".into(); + *fields = vec![Value::Nothing]; + } + corruptions.push(wrong); + if name == "absent_exact_state" { + let mut wrong = value.clone(); + if let Value::Machine { state, .. } = &mut wrong { + *state = "waiting".into(); + } + corruptions.push(wrong); + } + } + _ => continue, + } + for wrong in corruptions { + assert_eq!( + runtime.validate_value_type(&EvaluatedValue::ordinary(wrong), ty), + Err(ResourceExecutionError::InvalidPayloadPath), + "{name}" + ); + } + } + assert_eq!(runtime.live_owners(), 0); + assert_eq!(runtime.registry_live_count(), 0); + } +} + +#[test] +fn resource_absent_shape_validation_keeps_occupied_aggregate_custody_refused() { + use crate::resource_execution::{ProviderEvent, ScriptOperation}; + use jett_types::ResourceHookKind; + + for release in [false, true] { + let checked = program(SOURCE, release); + let mut runtime = + ResourceTransport::checked_only(checked.clone(), ExecutionPurpose::ReferenceRuntime, 1) + .unwrap(); + runtime.checked_source_active = true; + let grant = runtime + .install_script(vec![ScriptOperation::Construct { + label: 1701, + outcome: Ok(()), + }]) + .unwrap(); + let definition = checked + .checked() + .resource_hooks + .values() + .find(|hook| hook.kind == ResourceHookKind::Construct) + .unwrap() + .definition; + let span = *checked + .checked() + .call_ownership + .iter() + .find(|(_, packet)| packet.target == CheckedInvocationTarget::Resolved(definition)) + .unwrap() + .0; + let invocation = runtime.checked.invocation(span).unwrap(); + let descriptor = runtime.checked.descriptor(definition).unwrap(); + let produced = runtime + .invoke_hook( + &invocation, + &descriptor, + vec![ + EvaluatedValue::ordinary(Value::GrantedNetwork(grant.clone())), + EvaluatedValue::ordinary(Value::Int64(1701)), + ], + ) + .unwrap(); + let Value::ResultOk(token) = &produced.value else { + panic!("occupied factory result"); + }; + // Physical test copies have no custody; no aggregate owner path is + // authorized by the new absent-layout proof. + let malformed = vec![ + ("empty_tokens", Value::List(vec![token.as_ref().clone()])), + ( + "absent_map", + Value::Map(vec![( + Value::String("occupied".into()), + Value::OptionalSome(token.clone()), + )]), + ), + ( + "absent_envelope", + Value::Struct { + type_name: "app.TokenEnvelope".into(), + concrete_type: None, + fields: vec![ + ("tokens".into(), Value::List(vec![token.as_ref().clone()])), + ("fallback".into(), Value::OptionalNone), + ("marker".into(), Value::Int64(17)), + ], + }, + ), + ( + "empty_choice", + Value::Enum { + type_name: "app.TokenChoice".into(), + variant: "occupied".into(), + fields: vec![token.as_ref().clone()], + }, + ), + ( + "absent_state", + Value::Machine { + type_name: "app.TokenState".into(), + state: "occupied".into(), + fields: vec![token.as_ref().clone()], + }, + ), + ( + "empty_tokens", + Value::List(vec![Value::GrantedNetwork(grant)]), + ), + ]; + for (name, value) in malformed { + assert_eq!( + runtime.validate_value_type( + &EvaluatedValue::ordinary(value), + returned_type(&checked, name) + ), + Err(ResourceExecutionError::InvalidPayloadPath), + "{name}" + ); + } + assert_eq!(runtime.live_owners(), 1); + runtime.pop_scope(); + runtime.check_cleanup().unwrap(); + assert_eq!( + runtime.provider_events().unwrap(), + vec![ + ProviderEvent::Constructed(1701), + ProviderEvent::Finalized(1701) + ] + ); + assert_eq!(runtime.live_owners(), 0); + assert_eq!(runtime.registry_live_count(), 0); + } +} + +#[path = "absence_tests/augmented_baseline.rs"] +mod augmented_baseline; diff --git a/crates/jett_comptime/src/resource_execution/runtime/absence_tests/augmented_baseline.rs b/crates/jett_comptime/src/resource_execution/runtime/absence_tests/augmented_baseline.rs new file mode 100644 index 00000000..c4342c49 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/runtime/absence_tests/augmented_baseline.rs @@ -0,0 +1,258 @@ +//! Distinct original-constructor/worker gate under the existing absence tests. +//! Reuse its twelve exact typed Value goldens, rather than the new main guards. +use super::*; +use std::panic::{AssertUnwindSafe, catch_unwind}; +#[path = "../../../../../jett_driver/tests/native_conformance/resource_absent_aggregate_baseline_inputs.rs"] +mod inputs; + +fn original_calls( + checked: &Arc, + purpose: ExecutionPurpose, + release: bool, + entered: &mut Vec, +) -> Result<(), inputs::Failure> { + use inputs::{Phase, enter, failure}; + let mut failures = Vec::new(); + let mut calls = 0; + for (name, expected) in absent_values() { + enter(entered, Phase::WorkerEligibility); + let setup = inputs::attempt(|stages| { + enter(stages, Phase::WorkerEligibility); + let mut interpreter = + Interpreter::from_checked_resource_program(checked.clone(), purpose) + .map_err(|error| failure(Phase::WorkerEligibility, error))?; + let installation = interpreter.install_resource_test_script(Vec::new()); + if (purpose == ExecutionPurpose::ReferenceRuntime) != installation.is_ok() { + return Err(failure(Phase::WorkerEligibility, installation)); + } + let baseline = interpreter + .resource_test_entry_context() + .map_err(|error| failure(Phase::WorkerEligibility, error))?; + enter(stages, Phase::OriginalConstructorCall); + let target = source_entry(checked, name); + let actual = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(target, Vec::new()) + })); + let actual_text = match &actual { + Ok(result) => format!("{result:?}"), + Err(payload) => format!( + "UnexpectedPanic({:?})", + inputs::panic_message(payload.as_ref()) + ), + }; + let call_outcome = match actual { + Ok(Ok(actual)) if actual == expected => Ok(()), + Ok(actual) => Err(failure(Phase::OriginalConstructorCall, (actual, expected))), + Err(payload) => Err(inputs::Failure { + phase: Phase::OriginalConstructorCall, + kind: inputs::FailureKind::UnexpectedPanic, + detail: inputs::panic_message(payload.as_ref()), + }), + }; + enter(stages, Phase::ReferenceObservations); + let effects = interpreter.resource_test_observations(); + let custody = interpreter.resource_test_custody_counts(); + let context = interpreter.resource_test_entry_context(); + let debug = interpreter.take_debug_events(); + eprintln!( + "ABSENT_ORIGINAL name={name:?} purpose={purpose:?} release={release} observed={actual_text:?} effects={effects:?} custody={custody:?} context={context:?} debug={debug:?}" + ); + let effects_match = if purpose == ExecutionPurpose::ReferenceRuntime { + effects == Ok((Vec::new(), 0, 0)) + } else { + effects == Err(ResourceExecutionError::ProviderDisabled.to_string()) + }; + if !effects_match + || custody != Ok((0, 0)) + || context.as_ref() != Ok(&baseline) + || !debug.is_empty() + { + if let Err(mut first) = call_outcome { + first.detail.push_str("; constructor effects/custody/context/debug invariant also failed; see actual log"); + return Err(first); + } + return Err(failure( + Phase::ReferenceObservations, + "constructor effects/custody/context/debug changed; see log", + )); + } + call_outcome + }); + calls += 1; + if let Err(error) = setup.outcome { + eprintln!( + "ABSENT_ORIGINAL_FAILURE name={name:?} purpose={purpose:?} release={release} entered={:?} failure={error:?}", + setup.entered + ); + failures.push((name, error)); + } + } + eprintln!( + "ABSENT_ORIGINAL_TOTAL purpose={purpose:?} release={release} calls={calls} failed={}", + failures.len() + ); + if calls != 12 || !failures.is_empty() { + if let Some((_, first)) = failures.first() { + return Err(inputs::Failure { + phase: first.phase, + kind: first.kind, + detail: format!("{calls} original constructor attempts; failures={failures:#?}"), + }); + } + return Err(failure(Phase::OriginalConstructorCall, calls)); + } + Ok(()) +} + +#[test] +fn source17_augmented_original_constructors_keep_exact_values_in_all_five_purposes() { + use inputs::{Phase, failure}; + let input = inputs::INPUTS.last().unwrap(); // Same entire prefix, simple required main wrapper. + let mut attempted = 0; + let mut failures = Vec::new(); + for release in [false, true] { + for purpose in [ + ExecutionPurpose::ReferenceRuntime, + ExecutionPurpose::NamespaceConstant, + ExecutionPurpose::ExplicitComptime, + ExecutionPurpose::Verify, + ExecutionPurpose::Property, + ] { + attempted += 1; + let attempt = inputs::attempt(|entered| { + let (checked, _) = inputs::prepare(input, release, entered)?; + original_calls(&checked, purpose, release, entered) + }); + if let Err(error) = attempt.outcome { + eprintln!( + "ABSENT_PURPOSE_FAILURE purpose={purpose:?} release={release} entered={:?} failure={error:?}", + attempt.entered + ); + failures.push((purpose, release, error)); + } + } + } + assert_eq!(attempted, 10); + assert!( + failures.is_empty(), + "{}", + failure(Phase::OriginalConstructorCall, failures).detail + ); + // Ten accepted preparations imply 120 separately logged exact typed calls. +} + +#[test] +fn source17_augmented_required_capture_and_original_verify_property100_are_separate() { + use inputs::{Phase, enter, failure}; + let input = inputs::INPUTS.last().unwrap(); + let mut failures = Vec::new(); + let mut attempts = 0; + for release in [false, true] { + attempts += 1; + let attempt = inputs::attempt(|entered| { + let (checked, _) = inputs::prepare(input, release, entered)?; + let types = Arc::new(crate::checked_types::CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + enter(entered, Phase::RequiredCapture); + let required = crate::evaluate_explicit_comptime_expressions_capture( + checked.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + types.clone(), + Arc::new(HashMap::new()), + ); + eprintln!( + "ABSENT_REQUIRED release={release} diagnostics={:?} debug={:?} values={:?}", + required.diagnostics, + required.debug_events, + required.values.values().collect::>() + ); + if !required.diagnostics.is_empty() || !required.debug_events.is_empty() { + return Err(failure( + Phase::RequiredCapture, + (&required.diagnostics, &required.debug_events), + )); + } + enter(entered, Phase::RequiredCache); + required + .values + .checked_required_values(&checked) + .map_err(|error| failure(Phase::RequiredCache, error))?; + let declarations = checked + .module() + .items + .iter() + .filter_map(|item| match item { + Item::VarDecl(declaration) + if declaration.name.span.file == FileId::new(0) + && declaration.name.name == "absent_namespace_value" => + { + Some(declaration.name.span) + } + _ => None, + }) + .collect::>(); + let [declaration] = declarations.as_slice() else { + return Err(failure( + Phase::RequiredCache, + "missing original namespace constant", + )); + }; + if required.values.constant(*declaration) != Some(&Value::Int64(17)) + || !required.values.checked_values_are_mirrored() + || required + .values + .values() + .any(Value::contains_live_resource_or_grant) + { + return Err(failure( + Phase::RequiredCache, + "constant/mirror/authority contract failed", + )); + } + enter(entered, Phase::VerifyPropertyWorkers); + let blocks = crate::run_verify_blocks_detailed_with_metadata_and_expression_types( + checked.module(), + None, + Some(types), + None, + ); + eprintln!("ABSENT_VERIFY_PROPERTY release={release} blocks={blocks:#?}"); + if blocks.len() != 2 + || !blocks + .iter() + .all(|block| block.passed && block.debug_events.is_empty()) + || blocks.iter().filter(|block| block.is_property).count() != 1 + || blocks + .iter() + .find(|block| block.is_property) + .unwrap() + .iterations + != Some(100) + { + return Err(failure(Phase::VerifyPropertyWorkers, blocks)); + } + Ok(()) + }); + if let Err(error) = attempt.outcome { + eprintln!( + "ABSENT_WORKERS_FAILURE release={release} entered={:?} failure={error:?}", + attempt.entered + ); + failures.push((release, error)); + } + } + assert_eq!(attempts, 2); + assert!( + failures.is_empty(), + "separate required/verify/property failure: {failures:#?}" + ); + // This reference-worker result is not native suite execution evidence. +} diff --git a/crates/jett_comptime/src/resource_execution/runtime/absent_generated.rs b/crates/jett_comptime/src/resource_execution/runtime/absent_generated.rs new file mode 100644 index 00000000..427b4ff0 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/runtime/absent_generated.rs @@ -0,0 +1,291 @@ +//! Publication admits checked inactive data, never occupied aggregate custody. +use super::super::checked::PreparedAbsentBindings; +use super::*; +use jett_parser::ast::{ForStmt, Ident, MatchStmt}; + +impl ResourceTransport { + fn validate_absent_parent( + &self, + proof: &PreparedAbsentBindings<'_>, + value: &Value, + ) -> Result<(), ResourceExecutionError> { + if !self.checked_source_active || value.contains_live_resource_or_grant() { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + self.checked.revalidate_absent_bindings(proof)?; + self.validate_value_type( + &EvaluatedValue::ordinary(value.clone()), + proof.parent_type(), + )?; + proof.validate_parent(value) + } + + pub(crate) fn prepare_absent_for<'source>( + &self, + source: &'source ForStmt, + value: &Value, + ) -> Result>, ResourceExecutionError> { + if !self.checked_source_active { + return Ok(None); + } + let proof = self.checked.prepare_absent_for(source)?; + if let Some(proof) = &proof { + self.validate_absent_parent(proof, value)?; + } + Ok(proof) + } + + pub(crate) fn prepare_absent_match<'source>( + &self, + source: &'source MatchStmt, + arm: usize, + value: &Value, + ) -> Result>, ResourceExecutionError> { + if !self.checked_source_active { + return Ok(None); + } + let proof = self.checked.prepare_absent_match(source, arm)?; + if let Some(proof) = &proof { + self.validate_absent_parent(proof, value)?; + } + Ok(proof) + } + + pub(crate) fn validate_absent_binding( + &self, + proof: &PreparedAbsentBindings<'_>, + binder: &Ident, + ordinal: usize, + value: &Value, + ) -> Result { + if !self.checked_source_active || value.contains_live_resource_or_grant() { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + self.checked.revalidate_absent_bindings(proof)?; + let fact = proof.validate_binding(binder, ordinal)?; + absent_shape(value, fact.ty, &self.checked)?; + let ordinary = EvaluatedValue::ordinary(value.clone()); + self.validate_value_type(&ordinary, fact.ty)?; + Ok(fact) + } + + pub(crate) fn publish_absent_binding( + &mut self, + proof: &PreparedAbsentBindings<'_>, + binder: &Ident, + ordinal: usize, + value: Value, + ) -> Result<(), ResourceExecutionError> { + let fact = self.validate_absent_binding(proof, binder, ordinal, &value)?; + if self.checked.type_contains_resource(fact.ty)? { + self.install_binding(fact.declaration_span, EvaluatedValue::ordinary(value))?; + } + Ok(()) + } +} + +// Absence is a value/layout proof, not an exemption based on the lack +// of a carrier. Recurse through every occupied slot of the exact type. +pub(super) fn absent_shape( + value: &Value, + ty: jett_types::TypeId, + checked: &CheckedExecution, +) -> Result<(), ResourceExecutionError> { + use jett_types::Type; + let types = &checked.program().checked().interner; + if ty.index() as usize >= types.len() { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + if value.contains_live_resource_or_grant() { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + let kind = types.resolve(ty); + // A layout match cannot replace the runtime predicate boundary. + // Resource-bearing return normalization does not prove it here. + if matches!(kind, Type::Refinement { .. }) { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + if let Value::Typed { type_name, value } = value { + if type_name != &types.type_name(ty) { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + let inner = match kind { + Type::Secret(inner) => *inner, + _ => ty, + }; + return absent_shape(value, inner, checked); + } + let fields = |values: &[(String, Value)], expected: &[(String, jett_types::TypeId)]| { + if values.len() != expected.len() { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + for ((name, value), (expected_name, field_type)) in values.iter().zip(expected) { + if name != expected_name { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + absent_shape(value, *field_type, checked)?; + } + Ok(()) + }; + let payloads = |values: &[Value], expected: &[(String, jett_types::TypeId)]| { + if values.len() != expected.len() { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + for (value, (_, field_type)) in values.iter().zip(expected) { + absent_shape(value, *field_type, checked)?; + } + Ok(()) + }; + match (value, kind) { + (value, Type::Secret(inner)) => absent_shape(value, *inner, checked), + (Value::Int64(number), Type::Int8) if i8::try_from(*number).is_ok() => Ok(()), + (Value::Int64(number), Type::Int16) if i16::try_from(*number).is_ok() => Ok(()), + (Value::Int64(number), Type::Int32) if i32::try_from(*number).is_ok() => Ok(()), + (Value::Int64(_), Type::Int64) => Ok(()), + (Value::Int64(number), Type::Uint8) if u8::try_from(*number).is_ok() => Ok(()), + (Value::Int64(number), Type::Uint16) if u16::try_from(*number).is_ok() => Ok(()), + (Value::Int64(number), Type::Uint32) if u32::try_from(*number).is_ok() => Ok(()), + (Value::Uint64(_), Type::Uint64) + | (Value::Float64(_), Type::Float32 | Type::Float64) + | (Value::String(_), Type::String) + | (Value::Bool(_), Type::Bool) + | (Value::Bytes(_), Type::Bytes) + | (Value::Nothing, Type::Nothing) => Ok(()), + (Value::List(values), Type::List(inner)) | (Value::Set(values), Type::Set(inner)) => { + for value in values { + absent_shape(value, *inner, checked)?; + } + Ok(()) + } + (Value::Map(values), Type::Map(key, item)) => { + for (key_value, item_value) in values { + absent_shape(key_value, *key, checked)?; + absent_shape(item_value, *item, checked)?; + } + Ok(()) + } + (Value::OptionalNone, Type::Optional(_)) => Ok(()), + (Value::OptionalSome(value), Type::Optional(inner)) => absent_shape(value, *inner, checked), + (Value::ResultOk(value), Type::Result(success, _)) => { + absent_shape(value, *success, checked) + } + (Value::ResultFail(value), Type::Result(_, error)) => absent_shape(value, *error, checked), + ( + Value::Struct { + type_name, + concrete_type, + fields: values, + }, + Type::Struct(owner), + ) => { + let definition = types.resolve_struct(*owner); + let arguments = types.nominal_type_arguments(ty); + let header_matches = if arguments.is_empty() { + type_name == &definition.name + && concrete_type + .as_ref() + .is_none_or(|name| name == &definition.name) + } else { + let physical_instance = format!( + "{}[{}]", + type_name, + arguments + .iter() + .map(|argument| types.type_name(*argument)) + .collect::>() + .join(", ") + ); + physical_instance == definition.name + && concrete_type.as_deref() == Some(definition.name.as_str()) + }; + if !header_matches { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + fields(values, &definition.fields) + } + ( + Value::Enum { + type_name, + variant, + fields: values, + }, + Type::Enum(owner), + ) => { + let definition = types.resolve_enum(*owner); + if type_name != &definition.name { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + let mut variants = definition + .variants + .iter() + .filter(|candidate| candidate.name == *variant); + let selected = variants + .next() + .ok_or(ResourceExecutionError::InvalidPayloadPath)?; + if variants.next().is_some() { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + payloads(values, &selected.fields) + } + ( + Value::Machine { + type_name, + state, + fields: values, + }, + Type::Machine(owner), + ) => { + let definition = types.resolve_machine(*owner); + if type_name != &definition.name { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + let mut states = definition + .states + .iter() + .filter(|candidate| candidate.name == *state); + let selected = states + .next() + .ok_or(ResourceExecutionError::InvalidPayloadPath)?; + if states.next().is_some() { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + payloads(values, &selected.fields) + } + ( + Value::Machine { + type_name, + state, + fields: values, + }, + Type::MachineState { + machine, + state: expected, + }, + ) => { + let definition = types.resolve_machine(*machine); + let selected = definition + .state(*expected) + .ok_or(ResourceExecutionError::InvalidCheckedProgram)?; + if type_name != &definition.name || state != &selected.name { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + payloads(values, &selected.fields) + } + (Value::ResourceHook(descriptor), Type::Function { .. }) + if Arc::ptr_eq(&descriptor.program, checked.program()) + && checked + .program() + .checked() + .resource_hooks + .get(&descriptor.definition) + .is_some_and(|hook| hook.function_type == ty) => + { + Ok(()) + } + _ => Err(ResourceExecutionError::InvalidPayloadPath), + } +} + +#[cfg(test)] +#[path = "absent_generated/tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/runtime/absent_generated/tests.rs b/crates/jett_comptime/src/resource_execution/runtime/absent_generated/tests.rs new file mode 100644 index 00000000..c7c2e23c --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/runtime/absent_generated/tests.rs @@ -0,0 +1,549 @@ +//! Source17 executions remain distinct from the private malformed-value controls. +use super::*; +use crate::resource_execution::{ExecutionPurpose, ProviderEvent, ScriptOperation}; +use crate::{Interpreter, Value}; +use jett_common::FileId; +use jett_parser::ast::{ForStmt, FunctionDef, Item, MatchStmt, Pattern, Stmt}; +use jett_types::ResourceHookKind; + +#[path = "../../../../../jett_driver/tests/native_conformance/resource_absent_aggregate_baseline_inputs.rs"] +mod inputs; + +fn checked_program(index: usize, release: bool) -> Arc { + inputs::prepare(&inputs::INPUTS[index], release, &mut Vec::new()) + .unwrap() + .0 +} + +fn function<'a>(program: &'a CheckedResourceProgram, name: &str) -> &'a FunctionDef { + program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) && function.name.name == name => + { + Some(function) + } + _ => None, + }) + .unwrap() +} + +fn install_main(runtime: &mut ResourceTransport, program: &CheckedResourceProgram) { + let definition = runtime + .checked + .declaration_definition(function(program, "main").name.span) + .unwrap(); + let body = runtime + .checked + .prepare_function_body(&runtime.checked.entry(definition).unwrap()) + .unwrap(); + runtime.checked.install_function_body(&body).unwrap(); +} + +fn for_statement(program: &CheckedResourceProgram) -> &ForStmt { + function(program, "main") + .body + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::For(statement) => Some(statement), + _ => None, + }) + .unwrap() +} + +fn match_statement(program: &CheckedResourceProgram) -> &MatchStmt { + function(program, "main") + .body + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::Match(statement) => Some(statement), + _ => None, + }) + .unwrap() +} + +fn parent(index: usize) -> Value { + match index { + 1 => Value::List(vec![Value::OptionalNone, Value::OptionalNone]), + 3 => Value::Map(vec![(Value::String("empty".into()), Value::OptionalNone)]), + 9 => Value::Enum { + type_name: "app.TokenChoice".into(), + variant: "vacant".into(), + fields: vec![Value::Int64(17), Value::List(Vec::new())], + }, + _ => unreachable!(), + } +} + +fn absent_runtime(index: usize, release: bool) -> (Arc, ResourceTransport) { + let program = checked_program(index, release); + let mut runtime = + ResourceTransport::checked_only(program.clone(), ExecutionPurpose::ReferenceRuntime, 1) + .unwrap(); + runtime.checked_source_active = true; + install_main(&mut runtime, &program); + (program, runtime) +} + +fn clean(runtime: &ResourceTransport) { + assert_eq!(runtime.live_owners(), 0); + assert_eq!(runtime.registry_live_count(), 0); + assert_eq!( + runtime.provider_events(), + Err(ResourceExecutionError::ProviderDisabled) + ); +} + +fn execute_twice(input: &inputs::Input, release: bool) -> usize { + let (checked, _) = inputs::prepare(input, release, &mut Vec::new()).unwrap(); + let types = Arc::new(crate::checked_types::CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let metadata = Arc::new(jett_types::ReflectionMetadata::new()); + let exclusions = Arc::new(HashMap::new()); + let required = crate::evaluate_explicit_comptime_expressions_capture( + checked.module(), + metadata.clone(), + types.clone(), + exclusions.clone(), + ); + assert!( + required.diagnostics.is_empty(), + "{} release={release}: {:?}", + input.name, + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + required.values.checked_required_values(&checked).unwrap(); + assert!(required.values.checked_values_are_mirrored()); + assert!( + required + .values + .values() + .all(|value| !value.contains_live_resource_or_grant()) + ); + let mut interpreter = Interpreter::new(); + interpreter.set_reflection_metadata(metadata); + interpreter.set_checked_expression_types(types); + interpreter.set_breakpoint_exclusions(exclusions); + interpreter.set_explicit_comptime_values(Arc::new(required.values)); + interpreter.register_module(checked.module()); + interpreter + .install_checked_resource_program(checked.clone(), ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let main_span = function(&checked, "main").name.span; + let definitions = checked + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == jett_resolve::DefKind::Function + && definition.span == main_span + && definition.namespace.as_deref() == Some("app") + }) + .map(|definition| definition.id) + .collect::>(); + let [definition] = definitions.as_slice() else { + panic!("unique original main identity") + }; + let grant = interpreter + .install_resource_test_script(Vec::new()) + .unwrap(); + let baseline = interpreter.resource_test_entry_context().unwrap(); + for repeat in 0..2 { + let actual = interpreter.call_checked_program_entry(*definition, vec![grant.clone()]); + match (input.reference, actual) { + (inputs::ReferenceExpectation::Nothing, Ok(Value::Nothing)) => {} + (inputs::ReferenceExpectation::Error(expected), Err(actual)) if actual == expected => {} + (_, actual) => panic!( + "{} release={release} repeat={repeat}: {actual:?}", + input.name + ), + } + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (Vec::new(), 0, 0) + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), baseline); + assert!(interpreter.take_debug_events().is_empty()); + } + 2 +} + +#[test] +fn absent_generated_unchanged_source17_wrappers_repeat_exact_outcomes_and_restore_entry() { + assert_eq!(inputs::INPUTS.len(), 14); + let mut entries = 0; + for release in [false, true] { + for input in inputs::INPUTS { + entries += execute_twice(input, release); + } + } + assert_eq!(entries, 56); +} + +#[test] +fn absent_generated_view_continue_break_and_post_loop_reuse_restore_every_entry() { + let input = inputs::Input { + name: "source17_view_continue_break_reuse", + source: include_str!("view_control.jett"), + reference: inputs::ReferenceExpectation::Nothing, + }; + let mut entries = 0; + for release in [false, true] { + entries += execute_twice(&input, release); + } + assert_eq!(entries, 4); +} + +#[test] +fn absent_generated_parent_child_shapes_and_duplicate_publication_remain_checked() { + for release in [false, true] { + for index in [1, 3, 9] { + let (program, mut runtime) = absent_runtime(index, release); + let original = parent(index); + if index == 1 { + let types = &program.checked().interner; + let refined = types + .type_ids() + .find(|ty| types.type_name(*ty) == "app.RefinedEnvelope") + .expect("original retained RefinedEnvelope"); + let jett_types::Type::Struct(owner) = types.resolve(refined) else { + panic!("original RefinedEnvelope struct"); + }; + let marker = types + .resolve_struct(*owner) + .fields + .iter() + .find(|(name, _)| name == "marker") + .unwrap() + .1; + assert_eq!(types.type_name(marker), "app.PositiveMarker"); + let claimed = Value::Typed { + type_name: "app.PositiveMarker".into(), + value: Box::new(Value::Int64(17)), + }; + for value in [Value::Int64(17), claimed.clone()] { + assert_eq!( + absent_shape(&value, marker, &runtime.checked), + Err(ResourceExecutionError::InvalidPayloadPath) + ); + } + let unproved = Value::Struct { + type_name: "app.RefinedEnvelope".into(), + concrete_type: None, + fields: vec![ + ("marker".into(), claimed), + ("tokens".into(), Value::List(Vec::new())), + ], + }; + assert_eq!( + absent_shape(&unproved, refined, &runtime.checked), + Err(ResourceExecutionError::InvalidPayloadPath) + ); + clean(&runtime); + } + let bad_parents = match index { + 1 => vec![ + Value::List(vec![Value::Nothing]), + Value::List(vec![Value::Int64(1)]), + Value::List(vec![Value::OptionalSome(Box::new(Value::Nothing))]), + Value::Typed { + type_name: "list[optional[string]]".into(), + value: Box::new(original.clone()), + }, + ], + 3 => vec![ + Value::Map(vec![(Value::Int64(1), Value::OptionalNone)]), + Value::Map(vec![(Value::String("empty".into()), Value::Nothing)]), + Value::List(Vec::new()), + ], + 9 => vec![ + Value::Enum { + type_name: "app.TokenChoice".into(), + variant: "empty".into(), + fields: Vec::new(), + }, + Value::Enum { + type_name: "foreign.TokenChoice".into(), + variant: "vacant".into(), + fields: vec![Value::Int64(17), Value::List(Vec::new())], + }, + Value::Enum { + type_name: "app.TokenChoice".into(), + variant: "missing".into(), + fields: Vec::new(), + }, + Value::Enum { + type_name: "app.TokenChoice".into(), + variant: "vacant".into(), + fields: vec![Value::List(Vec::new()), Value::Int64(17)], + }, + Value::Enum { + type_name: "app.TokenChoice".into(), + variant: "vacant".into(), + fields: vec![Value::Int64(17)], + }, + ], + _ => unreachable!(), + }; + for wrong in bad_parents { + let result = if index == 9 { + runtime.prepare_absent_match(match_statement(&program), 0, &wrong) + } else { + runtime.prepare_absent_for(for_statement(&program), &wrong) + }; + assert!(result.is_err(), "index={index}, malformed parent={wrong:?}"); + clean(&runtime); + } + if index == 9 { + assert!( + runtime + .prepare_absent_match(match_statement(&program), 1, &original) + .is_err(), + "vacant belongs to the earlier original variant arm, not Other" + ); + clean(&runtime); + } + let proof = if index == 9 { + runtime + .prepare_absent_match(match_statement(&program), 0, &original) + .unwrap() + .unwrap() + } else { + runtime + .prepare_absent_for(for_statement(&program), &original) + .unwrap() + .unwrap() + }; + let (binder, ordinal, valid) = match index { + 1 => (&for_statement(&program).variable, 0, Value::OptionalNone), + 3 => ( + for_statement(&program).value_variable.as_ref().unwrap(), + 1, + Value::OptionalNone, + ), + 9 => { + let Pattern::Variant(_, names) = &match_statement(&program).arms[0].pattern + else { + unreachable!() + }; + (&names[1], 1, Value::List(Vec::new())) + } + _ => unreachable!(), + }; + let fact = proof.validate_binding(binder, ordinal).unwrap(); + runtime.push_scope(); + if index != 1 { + let (scalar_binder, scalar, wrong_scalar) = if index == 3 { + ( + &for_statement(&program).variable, + Value::String("empty".into()), + Value::Int64(17), + ) + } else { + let Pattern::Variant(_, names) = &match_statement(&program).arms[0].pattern + else { + unreachable!() + }; + (&names[0], Value::Int64(17), Value::String("17".into())) + }; + let scalar_fact = proof.validate_binding(scalar_binder, 0).unwrap(); + assert!( + runtime + .validate_absent_binding(&proof, scalar_binder, 0, &wrong_scalar) + .is_err() + ); + assert!( + runtime + .publish_absent_binding(&proof, scalar_binder, 0, wrong_scalar) + .is_err() + ); + runtime + .publish_absent_binding(&proof, scalar_binder, 0, scalar) + .unwrap(); + assert!( + !runtime.has_binding(scalar_fact.definition), + "ordinary scalar child must not acquire a Resource slot" + ); + } + assert!( + runtime + .publish_absent_binding(&proof, &binder.clone(), ordinal, valid.clone()) + .is_err() + ); + assert!( + runtime + .publish_absent_binding(&proof, binder, usize::MAX, valid.clone()) + .is_err() + ); + assert!( + runtime + .publish_absent_binding(&proof, binder, ordinal, Value::Nothing) + .is_err() + ); + assert!(!runtime.has_binding(fact.definition)); + runtime + .publish_absent_binding(&proof, binder, ordinal, valid.clone()) + .unwrap(); + assert!(runtime.has_binding(fact.definition)); + assert!( + runtime + .publish_absent_binding(&proof, binder, ordinal, valid.clone()) + .is_err() + ); + let borrowed = runtime.binding(fact.definition, true).unwrap(); + assert_eq!(borrowed.value, valid); + assert!(borrowed.custody.is_empty()); + assert_eq!( + runtime.binding(fact.definition, false).unwrap().value, + valid + ); + assert!(runtime.binding(fact.definition, false).is_err()); + runtime.pop_scope(); + runtime.check_cleanup().unwrap(); + assert!(!runtime.has_binding(fact.definition)); + runtime.checked_source_active = false; + let inactive = if index == 9 { + runtime.prepare_absent_match(match_statement(&program), 0, &original) + } else { + runtime.prepare_absent_for(for_statement(&program), &original) + }; + assert!(matches!(inactive, Ok(None))); + assert!( + runtime + .publish_absent_binding(&proof, binder, ordinal, valid) + .is_err() + ); + clean(&runtime); + } + } +} + +#[test] +fn absent_generated_binding_does_not_require_unrelated_live_owners_to_be_zero() { + for release in [false, true] { + let program = checked_program(1, release); + let mut runtime = + ResourceTransport::checked_only(program.clone(), ExecutionPurpose::ReferenceRuntime, 1) + .unwrap(); + runtime.checked_source_active = true; + let grant = runtime + .install_script(vec![ScriptOperation::Construct { + label: 1717, + outcome: Ok(()), + }]) + .unwrap(); + let definition = program + .checked() + .resource_hooks + .values() + .find(|hook| hook.kind == ResourceHookKind::Construct) + .unwrap() + .definition; + let span = *program + .checked() + .call_ownership + .iter() + .find(|(_, packet)| packet.target == CheckedInvocationTarget::Resolved(definition)) + .unwrap() + .0; + let invocation = runtime.checked.invocation(span).unwrap(); + let descriptor = runtime.checked.descriptor(definition).unwrap(); + let owner = runtime + .invoke_hook( + &invocation, + &descriptor, + vec![ + EvaluatedValue::ordinary(Value::GrantedNetwork(grant.clone())), + EvaluatedValue::ordinary(Value::Int64(1717)), + ], + ) + .unwrap(); + let Value::ResultOk(token) = &owner.value else { + panic!("original factory success") + }; + assert!(!owner.custody.is_empty()); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (1, 1) + ); + install_main(&mut runtime, &program); + let statement = for_statement(&program); + let original = parent(1); + let proof = runtime + .prepare_absent_for(statement, &original) + .unwrap() + .unwrap(); + let fact = proof.validate_binding(&statement.variable, 0).unwrap(); + runtime.push_scope(); + for live in [ + Value::OptionalSome(token.clone()), + Value::GrantedNetwork(grant), + ] { + assert!( + runtime + .publish_absent_binding(&proof, &statement.variable, 0, live.clone()) + .is_err() + ); + assert!( + runtime + .prepare_absent_for(statement, &Value::List(vec![live])) + .is_err() + ); + assert!(!runtime.has_binding(fact.definition)); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (1, 1) + ); + } + runtime + .publish_absent_binding(&proof, &statement.variable, 0, Value::OptionalNone) + .unwrap(); + assert_eq!( + runtime.binding(fact.definition, true).unwrap().value, + Value::OptionalNone + ); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (1, 1) + ); + runtime.pop_scope(); + runtime.check_cleanup().unwrap(); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (1, 1) + ); + assert_eq!( + runtime.provider_events().unwrap(), + vec![ProviderEvent::Constructed(1717)] + ); + runtime.pop_scope(); + runtime.check_cleanup().unwrap(); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); + assert_eq!( + runtime.provider_events().unwrap(), + vec![ + ProviderEvent::Constructed(1717), + ProviderEvent::Finalized(1717) + ] + ); + } +} diff --git a/crates/jett_comptime/src/resource_execution/runtime/absent_generated/view_control.jett b/crates/jett_comptime/src/resource_execution/runtime/absent_generated/view_control.jett new file mode 100644 index 00000000..2dd17ff5 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/runtime/absent_generated/view_control.jett @@ -0,0 +1,129 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false +function source17_absent_view_list() returns bool: + list[optional[resource_probe.TestHandle]] values = absent_tokens() + mutable int64 continued = 0 + for candidate in view values: + if source17_absent_optional(view candidate): + continued = continued + 1 + continue + return false + if continued != 2 || list.length[optional[resource_probe.TestHandle]](view values) != 2: + return false + mutable int64 broken = 0 + for candidate in view values: + if !source17_absent_optional(view candidate): + return false + broken = broken + 1 + break + if broken != 1 || list.length[optional[resource_probe.TestHandle]](view values) != 2: + return false + return true +function source17_absent_map_item(key: string, view candidate: optional[resource_probe.TestHandle]) returns bool: + return key == "empty" && source17_absent_optional(view candidate) +function source17_absent_view_map() returns bool: + map[string, optional[resource_probe.TestHandle]] entries = absent_map() + mutable int64 continued = 0 + for key, candidate in view entries: + if source17_absent_map_item(key, view candidate): + continued = continued + 1 + continue + return false + if continued != 1 || map.length[string, optional[resource_probe.TestHandle]](view entries) != 1: + return false + mutable int64 broken = 0 + for key, candidate in view entries: + if !source17_absent_map_item(key, view candidate): + return false + broken = broken + 1 + break + if broken != 1 || map.length[string, optional[resource_probe.TestHandle]](view entries) != 1: + return false + return true +export function main(net: Network) returns nothing: + if !source17_absent_view_list(): + int64 ignored = resource_probe.terminal_label() + if !source17_absent_view_map(): + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_comptime/src/resource_execution/runtime/assignment.rs b/crates/jett_comptime/src/resource_execution/runtime/assignment.rs new file mode 100644 index 00000000..988eca79 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/runtime/assignment.rs @@ -0,0 +1,94 @@ +//! A declared slot can outlive its current owner; replacement never copies one. +use super::super::checked::CheckedAssignment; +use super::*; +use jett_parser::ast::AssignStmt; + +pub(crate) struct PreparedResourceAssignment<'source> { + checked: CheckedAssignment<'source>, + scope_index: usize, + frame: FrameId, +} + +impl PreparedResourceAssignment<'_> { + pub(crate) fn scope_index(&self) -> usize { + self.scope_index + } +} + +impl ResourceTransport { + pub(crate) fn prepare_resource_assignment<'source>( + &self, + source: &'source AssignStmt, + ) -> Result, ResourceExecutionError> { + let checked = self.checked.prepare_assignment(source)?; + let fact = checked.fact(); + let scope_index = self + .scopes + .iter() + .rposition(|(_, bindings)| bindings.contains_key(&fact.definition)) + .ok_or(ResourceExecutionError::InvalidOwner)?; + let (frame, bindings) = &self.scopes[scope_index]; + let slot = bindings + .get(&fact.definition) + .ok_or(ResourceExecutionError::InvalidOwner)?; + if slot.fact != fact || !self.ledger.frame_is_live(*frame) { + return Err(ResourceExecutionError::InvalidOwner); + } + Ok(PreparedResourceAssignment { + checked, + scope_index, + frame: *frame, + }) + } + + fn assignment_slot_mut( + &mut self, + destination: &PreparedResourceAssignment<'_>, + ) -> Result<&mut ResourceBindingSlot, ResourceExecutionError> { + let fact = destination.checked.fact(); + let (frame, bindings) = self + .scopes + .get_mut(destination.scope_index) + .ok_or(ResourceExecutionError::InvalidFrame)?; + if *frame != destination.frame || !self.ledger.frame_is_live(*frame) { + return Err(ResourceExecutionError::InvalidFrame); + } + let slot = bindings + .get_mut(&fact.definition) + .ok_or(ResourceExecutionError::InvalidOwner)?; + if slot.fact != fact { + return Err(ResourceExecutionError::InvalidOwner); + } + Ok(slot) + } + + pub(crate) fn replace_resource_assignment( + &mut self, + destination: PreparedResourceAssignment<'_>, + mut value: EvaluatedValue, + ) -> Result<(), ResourceExecutionError> { + self.checked.revalidate_assignment(&destination.checked)?; + self.assignment_slot_mut(&destination)?; + let fact = destination.checked.fact(); + self.validate_value_type(&value, fact.ty)?; + if value.custody.has_borrows() { + return Err(ResourceExecutionError::InvalidOwner); + } + // Keep the new owner in the active RHS operation until old-value + // cleanup completes. A finalizer panic leaves both cleanup paths live. + self.hold_actual(&mut value)?; + let previous = self.assignment_slot_mut(&destination)?.value.take(); + if let Some(previous) = previous { + self.discard_value(previous)?; + } + self.ledger.transfer( + &mut value.custody, + OwnerHolder::Binding { + frame: destination.frame, + definition: fact.definition, + }, + )?; + self.assignment_slot_mut(&destination)?.value = Some(value); + Ok(()) + } +} diff --git a/crates/jett_comptime/src/resource_execution/runtime/owned_struct.rs b/crates/jett_comptime/src/resource_execution/runtime/owned_struct.rs new file mode 100644 index 00000000..52c5c079 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/runtime/owned_struct.rs @@ -0,0 +1,63 @@ +//! An aggregate is a new holder root, never a collection of copied raw tokens. +use super::super::checked::PreparedOwnedStruct; +use super::*; +use jett_parser::ast::Expr; + +impl ResourceTransport { + pub(crate) fn assemble_owned_struct( + &mut self, + proof: &PreparedOwnedStruct, + expression: &Expr, + actuals: Vec, + ) -> Result { + self.checked.revalidate_owned_struct(proof, expression)?; + if actuals.len() != proof.source_fields().len() { + return Err(ResourceExecutionError::InvalidInvocation); + } + // Validate every child before combining or migrating any holder. The + // operation retains each reached child's cleanup path on any refusal. + for (actual, ordinal) in actuals.iter().zip(proof.source_fields()) { + if actual.custody.has_borrows() { + return Err(ResourceExecutionError::InvalidOwner); + } + self.validate_value_type(actual, proof.fields()[*ordinal].1)?; + } + let mut fields: Vec> = (0..actuals.len()).map(|_| None).collect(); + let mut custody = super::super::ValueCustody::default(); + // Preserve lexical acquisition order even when field destinations are + // permuted. Struct storage order never dictates finalizer ordering. + for (actual, ordinal) in actuals.into_iter().zip(proof.source_fields()) { + let actual = actual.prefix(PayloadStep::Field(*ordinal)); + if fields[*ordinal].replace(actual.value).is_some() { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + custody.append_owned(actual.custody)?; + } + let fields = proof + .fields() + .iter() + .zip(fields) + .map(|((name, _), value)| { + value + .map(|value| (name.clone(), value)) + .ok_or(ResourceExecutionError::InvalidPayloadPath) + }) + .collect::, _>>()?; + let mut value = EvaluatedValue { + value: Value::Struct { + type_name: proof.name().to_string(), + concrete_type: None, + fields, + }, + custody, + }; + self.validate_value_type(&value, proof.ty())?; + let holder = self.temporary(self.current_frame()?)?; + self.ledger.transfer(&mut value.custody, holder)?; + Ok(value) + } +} + +#[cfg(test)] +#[path = "owned_struct_tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/runtime/owned_struct_tests.rs b/crates/jett_comptime/src/resource_execution/runtime/owned_struct_tests.rs new file mode 100644 index 00000000..6690048e --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/runtime/owned_struct_tests.rs @@ -0,0 +1,250 @@ +use super::*; +use crate::resource_execution::{ProviderEvent, ScriptOperation, tests::program}; +use jett_common::FileId; +use jett_parser::ast::{Item, Stmt}; +use jett_types::ResourceHookKind; + +const SOURCE: &str = include_str!("../fixtures/33_owned_struct_scope_drop_entry.jett"); + +fn setup() -> ( + ResourceTransport, + crate::resource_execution::CheckedBodyReference, +) { + let program = program(SOURCE, false); + let function = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) + && function.name.name == "retire_owned" => + { + Some(function) + } + _ => None, + }) + .unwrap(); + let mut runtime = + ResourceTransport::checked_only(program.clone(), ExecutionPurpose::ReferenceRuntime, 1) + .unwrap(); + runtime.checked_source_active = true; + let target = runtime + .checked + .declaration_definition(function.name.span) + .unwrap(); + let body = runtime + .checked + .prepare_function_body(&runtime.checked.entry(target).unwrap()) + .unwrap(); + runtime.checked.install_function_body(&body).unwrap(); + runtime + .install_script(vec![ScriptOperation::Construct { + label: 1001, + outcome: Ok(()), + }]) + .unwrap(); + (runtime, body) +} + +fn constructed(runtime: &mut ResourceTransport) -> EvaluatedValue { + let definition = runtime + .checked + .program() + .checked() + .resource_hooks + .values() + .find(|hook| hook.kind == ResourceHookKind::Construct) + .unwrap() + .definition; + let span = *runtime + .checked + .program() + .checked() + .call_ownership + .iter() + .find(|(_, packet)| packet.target == CheckedInvocationTarget::Resolved(definition)) + .unwrap() + .0; + let invocation = runtime.checked.invocation(span).unwrap(); + let descriptor = runtime.checked.descriptor(definition).unwrap(); + let grant = match &runtime.provider { + InstalledResourceProvider::Scripted(provider) => provider.grant(), + _ => panic!("private scripted provider"), + }; + let mut value = runtime + .invoke_hook( + &invocation, + &descriptor, + vec![ + EvaluatedValue::ordinary(Value::GrantedNetwork(grant)), + EvaluatedValue::ordinary(Value::Int64(1001)), + ], + ) + .unwrap(); + let Value::ResultOk(payload) = value.value else { + panic!("actual constructed owner") + }; + value.value = *payload; + value.remove_prefix(PayloadStep::Ok).unwrap(); + value +} + +fn declaration( + body: &crate::resource_execution::CheckedBodyReference, +) -> &jett_parser::ast::VarDecl { + let Stmt::VarDecl(declaration) = &body.function().unwrap().body.stmts[0] else { + panic!("exact occupied struct binding") + }; + declaration +} + +#[test] +fn occupied_struct_root_paths_migrate_through_operation_and_binding_without_old_finalization() { + let (mut runtime, body) = setup(); + let declaration = declaration(&body); + let expression = &declaration.value; + let proof = runtime + .checked + .prepare_owned_struct(expression) + .unwrap() + .unwrap(); + let operation = runtime.begin_operation().unwrap(); + let token = constructed(&mut runtime); + let mut aggregate = runtime + .assemble_owned_struct( + &proof, + expression, + vec![token, EvaluatedValue::ordinary(Value::Int64(17))], + ) + .unwrap(); + assert_eq!(runtime.live_owners(), 1); + let physical_copy = EvaluatedValue::ordinary(aggregate.value.clone()); + assert!(matches!( + runtime.validate_value_type(&physical_copy, proof.ty()), + Err(ResourceExecutionError::InvalidPayloadPath) + )); + runtime + .end_operation(operation, Some(&mut aggregate)) + .unwrap(); + assert_eq!( + runtime.provider_events().unwrap(), + [ProviderEvent::Constructed(1001)] + ); + runtime + .install_binding(declaration.name.span, aggregate) + .unwrap(); + let binding = runtime.checked.binding(declaration.name.span).unwrap(); + let viewed = runtime.binding(binding.definition, true).unwrap(); + runtime.validate_value_type(&viewed, proof.ty()).unwrap(); + assert!(viewed.custody.has_borrows()); + assert!(!viewed.custody.has_owners()); + runtime.unwind_all().unwrap(); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(1001), + ProviderEvent::Finalized(1001) + ] + ); +} + +#[test] +fn occupied_struct_raw_or_borrowed_members_cannot_mint_owner_paths() { + for raw_copy in [false, true] { + let (mut runtime, body) = setup(); + let expression = &declaration(&body).value; + let proof = runtime + .checked + .prepare_owned_struct(expression) + .unwrap() + .unwrap(); + let operation = runtime.begin_operation().unwrap(); + let owner = constructed(&mut runtime); + let bad = if raw_copy { + EvaluatedValue::ordinary(owner.value.clone()) + } else { + runtime.borrow(&owner).unwrap() + }; + assert!( + runtime + .assemble_owned_struct( + &proof, + expression, + vec![bad, EvaluatedValue::ordinary(Value::Int64(17)),] + ) + .is_err() + ); + assert_eq!(runtime.live_owners(), 1); + assert_eq!( + runtime.provider_events().unwrap(), + [ProviderEvent::Constructed(1001)] + ); + runtime.end_operation(operation, None).unwrap(); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); + runtime.unwind_all().unwrap(); + } +} + +#[test] +fn occupied_struct_wrong_scalar_and_redirected_physical_field_fail_before_publication() { + let (mut runtime, body) = setup(); + let expression = &declaration(&body).value; + let proof = runtime + .checked + .prepare_owned_struct(expression) + .unwrap() + .unwrap(); + let operation = runtime.begin_operation().unwrap(); + let owner = constructed(&mut runtime); + assert!( + runtime + .assemble_owned_struct( + &proof, + expression, + vec![ + owner, + EvaluatedValue::ordinary(Value::String("wrong scalar".into())), + ] + ) + .is_err() + ); + assert_eq!(runtime.live_owners(), 1); + runtime.end_operation(operation, None).unwrap(); + runtime.unwind_all().unwrap(); + + let (mut runtime, body) = setup(); + let expression = &declaration(&body).value; + let proof = runtime + .checked + .prepare_owned_struct(expression) + .unwrap() + .unwrap(); + let operation = runtime.begin_operation().unwrap(); + let owner = constructed(&mut runtime); + let mut aggregate = runtime + .assemble_owned_struct( + &proof, + expression, + vec![owner, EvaluatedValue::ordinary(Value::Int64(17))], + ) + .unwrap(); + let Value::Struct { fields, .. } = &mut aggregate.value else { + panic!("occupied struct") + }; + fields.swap(0, 1); + assert!(runtime.validate_value_type(&aggregate, proof.ty()).is_err()); + runtime.end_operation(operation, None).unwrap(); + runtime.unwind_all().unwrap(); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); +} diff --git a/crates/jett_comptime/src/resource_execution/runtime/return_prefix_tests.rs b/crates/jett_comptime/src/resource_execution/runtime/return_prefix_tests.rs new file mode 100644 index 00000000..c12e728a --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/runtime/return_prefix_tests.rs @@ -0,0 +1,371 @@ +use super::*; +use crate::resource_execution::{ProviderEvent, ScriptOperation, tests::program}; +use jett_types::ResourceHookKind; + +fn runtime(script: Vec) -> ResourceTransport { + let checked = program( + include_str!("../fixtures/04_bare_to_view_operation_cleanup.jett"), + false, + ); + let mut runtime = + ResourceTransport::checked_only(checked, ExecutionPurpose::ReferenceRuntime, 1).unwrap(); + runtime.checked_source_active = true; + runtime.install_script(script).unwrap(); + runtime +} + +fn construct(label: i64) -> ScriptOperation { + ScriptOperation::Construct { + label, + outcome: Ok(()), + } +} + +fn constructed(runtime: &mut ResourceTransport, label: i64) -> EvaluatedValue { + let definition = runtime + .checked + .program() + .checked() + .resource_hooks + .values() + .find(|hook| hook.kind == ResourceHookKind::Construct) + .unwrap() + .definition; + let span = *runtime + .checked + .program() + .checked() + .call_ownership + .iter() + .find(|(_, packet)| packet.target == CheckedInvocationTarget::Resolved(definition)) + .unwrap() + .0; + let invocation = runtime.checked.invocation(span).unwrap(); + let descriptor = runtime.checked.descriptor(definition).unwrap(); + let grant = match &runtime.provider { + InstalledResourceProvider::Scripted(provider) => provider.grant(), + _ => panic!("selected private provider"), + }; + let mut value = runtime + .invoke_hook( + &invocation, + &descriptor, + vec![ + EvaluatedValue::ordinary(Value::GrantedNetwork(grant)), + EvaluatedValue::ordinary(Value::Int64(label)), + ], + ) + .unwrap(); + let Value::ResultOk(payload) = value.value else { + panic!("occupied real construction"); + }; + value.value = *payload; + value.remove_prefix(PayloadStep::Ok).unwrap(); + value +} + +#[test] +fn return_prefix_retires_interleaved_operations_and_retains_scope_owner_and_loan() { + let mut runtime = runtime(vec![construct(903), construct(901), construct(902)]); + let retained = constructed(&mut runtime, 903); + let retained_borrow = runtime.borrow(&retained).unwrap(); + let scope = runtime.scope_frame().unwrap(); + runtime + .ledger + .hold_borrows(&retained_borrow.custody, scope) + .unwrap(); + let caller = runtime.begin_operation().unwrap(); + runtime.push_scope(); + runtime.enter_return(caller.frame); + let first = runtime.begin_operation().unwrap(); + let _first_owner = constructed(&mut runtime, 901); + runtime.push_scope(); + let second = runtime.begin_operation().unwrap(); + let _second_owner = constructed(&mut runtime, 902); + runtime.push_scope(); + + runtime.retire_return_operations().unwrap(); + assert_eq!(runtime.operations, [caller.frame]); + assert_eq!(runtime.scopes.len(), 4); + assert!(runtime.ledger.frame_is_live(scope)); + assert!(runtime.ledger.has_live_borrows()); + assert!( + runtime + .ledger + .borrowed_carrier(&retained_borrow.custody) + .is_ok() + ); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (1, 1) + ); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(903), + ProviderEvent::Constructed(901), + ProviderEvent::Constructed(902), + ProviderEvent::Finalized(902), + ProviderEvent::Finalized(901), + ] + ); + assert!(runtime.acknowledge_return_operation(&second).unwrap()); + assert!(runtime.acknowledge_return_operation(&first).unwrap()); + runtime.leave_return(); + runtime.unwind_all().unwrap(); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); + assert_eq!( + runtime.provider_events().unwrap().last(), + Some(&ProviderEvent::Finalized(903)) + ); +} + +#[test] +fn return_prefix_nested_callee_boundary_preserves_callers_pending_actuals() { + let mut runtime = runtime(vec![construct(901), construct(902)]); + let caller = runtime.begin_operation().unwrap(); + let _caller_owner = constructed(&mut runtime, 901); + runtime.enter_return(caller.frame); + let pending = runtime.begin_operation().unwrap(); + let _pending_owner = constructed(&mut runtime, 902); + runtime.push_scope(); + runtime.enter_return(pending.frame); + + runtime.retire_return_operations().unwrap(); + assert_eq!(runtime.operations, [caller.frame, pending.frame]); + assert_eq!(runtime.live_owners(), 2); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(901), + ProviderEvent::Constructed(902), + ] + ); + runtime.leave_return(); + runtime.pop_scope(); + runtime.end_operation(pending, None).unwrap(); + runtime.retire_return_operations().unwrap(); + assert_eq!(runtime.operations, [caller.frame]); + assert_eq!(runtime.live_owners(), 1); + runtime.leave_return(); + runtime.end_operation(caller, None).unwrap(); + runtime.unwind_all().unwrap(); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); +} + +#[test] +fn return_prefix_receipt_rejects_wrong_parent_output_adoption_and_reuse() { + let mut runtime = runtime(vec![construct(901)]); + let scope = runtime.scope_frame().unwrap(); + let caller = runtime.begin_operation().unwrap(); + runtime.enter_return(caller.frame); + let pending = runtime.begin_operation().unwrap(); + let _owner = constructed(&mut runtime, 901); + runtime.retire_return_operations().unwrap(); + + let wrong = OperationFrame { + frame: pending.frame, + parent: scope, + }; + assert_eq!( + runtime.acknowledge_return_operation(&wrong), + Err(ResourceExecutionError::InvalidFrame) + ); + let copied = OperationFrame { + frame: pending.frame, + parent: pending.parent, + }; + let mut output = EvaluatedValue::ordinary(Value::Nothing); + assert_eq!( + runtime.end_operation(copied, Some(&mut output)), + Err(ResourceExecutionError::InvalidFrame) + ); + assert!(runtime.acknowledge_return_operation(&pending).unwrap()); + assert!(!runtime.acknowledge_return_operation(&pending).unwrap()); + assert_eq!( + runtime.end_operation(pending, None), + Err(ResourceExecutionError::InvalidFrame) + ); + runtime.leave_return(); + runtime.end_operation(caller, None).unwrap(); + runtime.unwind_all().unwrap(); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); +} + +#[test] +fn return_prefix_cleanup_panic_continues_suffix_and_expires_receipts_after_teardown() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + + let mut runtime = runtime(vec![ + construct(903), + construct(901), + ScriptOperation::ConstructFinalizerPanic { label: 902 }, + ]); + let _retained = constructed(&mut runtime, 903); + let caller = runtime.begin_operation().unwrap(); + runtime.enter_return(caller.frame); + let _first = runtime.begin_operation().unwrap(); + let _first_owner = constructed(&mut runtime, 901); + runtime.push_scope(); + let _second = runtime.begin_operation().unwrap(); + let _second_owner = constructed(&mut runtime, 902); + runtime.push_scope(); + + let panic = catch_unwind(AssertUnwindSafe(|| runtime.retire_return_operations())) + .expect_err("actual prefix cleanup panic"); + assert_eq!( + panic + .downcast_ref::() + .map(String::as_str) + .or_else(|| panic.downcast_ref::<&str>().copied()), + Some("selected test resource cleanup panic") + ); + assert_eq!(runtime.operations, [caller.frame]); + assert_eq!(runtime.abandoned_operations.len(), 2); + assert_eq!(runtime.live_owners(), 1); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(903), + ProviderEvent::Constructed(901), + ProviderEvent::Constructed(902), + ProviderEvent::Finalized(902), + ProviderEvent::Finalized(901), + ] + ); + runtime.unwind_all().unwrap(); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); + assert_eq!(runtime.abandoned_operations.len(), 2); + runtime.restore_entry_scopes(1).unwrap(); + runtime.validate_entry_scopes(1).unwrap(); + assert!(runtime.abandoned_operations.is_empty()); + assert_eq!( + runtime.provider_events().unwrap().last(), + Some(&ProviderEvent::Finalized(903)) + ); +} + +#[test] +fn return_prefix_ordinary_callable_has_own_floor_and_no_resource_destination() { + let mut runtime = runtime(vec![construct(901), construct(902)]); + let mut retained = constructed(&mut runtime, 901); + let caller = runtime.begin_operation().unwrap(); + runtime.enter_return(caller.frame); + let invocation = runtime.begin_operation().unwrap(); + let _invocation_owner = constructed(&mut runtime, 902); + runtime.push_scope(); + assert!(runtime.enter_ordinary_return()); + + runtime.retire_return_operations().unwrap(); + assert_eq!(runtime.operations, [caller.frame, invocation.frame]); + assert_eq!(runtime.live_owners(), 2); + assert_eq!(runtime.returns.last().unwrap().destination, None); + assert_eq!(runtime.returns.last().unwrap().operation_floor, 2); + assert_eq!( + runtime.preserve_return(&mut retained), + Err(ResourceExecutionError::InvalidFrame) + ); + assert!(runtime.borrow(&retained).is_ok()); + assert!(runtime.abandoned_operations.is_empty()); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(901), + ProviderEvent::Constructed(902) + ] + ); + + runtime.leave_return(); + runtime.pop_scope(); + runtime.end_operation(invocation, None).unwrap(); + runtime.retire_return_operations().unwrap(); + assert_eq!(runtime.operations, [caller.frame]); + runtime.leave_return(); + runtime.end_operation(caller, None).unwrap(); + runtime.unwind_all().unwrap(); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(901), + ProviderEvent::Constructed(902), + ProviderEvent::Finalized(902), + ProviderEvent::Finalized(901) + ] + ); +} + +#[test] +fn return_prefix_checked_source_named_ordinary_callable_retains_independent_owner() { + const SOURCE: &str = "namespace app\nfunction read_value() returns int64:\n return 7\nexport function scenario(view net: Network) returns int64:\n use resource_probe\n resource_probe.TestHandle token = resource_probe.create(view net, 1002) handle error:\n return -1\n function() returns int64 callback = read_value\n int64 observed = callback()\n resource_probe.close(token)\n return observed\n"; + for release in [false, true] { + let checked = program(SOURCE, release); + let declaration = checked + .module() + .items + .iter() + .find_map(|item| match item { + jett_parser::ast::Item::Function(function) if function.name.name == "scenario" => { + Some(function.name.span) + } + _ => None, + }) + .unwrap(); + let definitions = checked + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == jett_resolve::DefKind::Function + && definition.span == declaration + && definition.namespace.as_deref() == Some("app") + }) + .map(|definition| definition.id) + .collect::>(); + let [target] = definitions.as_slice() else { + panic!("unique checked scenario") + }; + let mut interpreter = crate::Interpreter::from_checked_resource_program( + checked, + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![construct(1002)]) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(*target, vec![grant]) + .unwrap(), + Value::Int64(7) + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(1002), + ProviderEvent::Finalized(1002) + ], + 0, + 0 + ) + ); + assert!(interpreter.take_debug_events().is_empty()); + } +} diff --git a/crates/jett_comptime/src/resource_execution/tests.rs b/crates/jett_comptime/src/resource_execution/tests.rs new file mode 100644 index 00000000..a7c9d32f --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests.rs @@ -0,0 +1,1160 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use jett_common::{FileId, SourceOrigin}; +use jett_diagnostics::Severity; +use jett_parser::{ast::Item, parse}; +use jett_resolve::{DefId, DefKind, ResourceKernelSpec}; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; +use jett_types::ResourceKernelRecipe; + +use super::{ExecutionPurpose, ProviderEvent, ScriptOperation}; +use crate::{Interpreter, Value}; + +const SUPPORT: &str = include_str!("fixtures/resource_probe.jett"); + +pub(crate) fn program(source: &str, release: bool) -> Arc { + let stdlib_file = FileId::new(10_000); + let project_file = FileId::new(0); + let mut parsed = parse(SUPPORT, stdlib_file); + let project = parse(source, project_file); + assert!( + !parsed + .errors + .iter() + .chain(&project.errors) + .any(|diagnostic| diagnostic.severity == Severity::Error), + "Resource source parse prerequisite failed (release={release}): support diagnostics={:?}; project diagnostics={:?}", + parsed.errors, + project.errors, + ); + let resource = parsed + .module + .items + .iter() + .find_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .expect("actual source Resource declaration"); + parsed.module.items.extend(project.module.items); + parsed.errors.extend(project.errors); + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: resource, + member: member.to_string(), + recipe, + }) + .collect::>(); + Arc::new(CheckedResourceProgram::prepare(parsed, HashMap::from([(stdlib_file, SourceOrigin::Stdlib), + (project_file, SourceOrigin::Project)]), &catalog, CheckOptions { release }).unwrap_or_else(|error| { + panic!("Resource source check prerequisite failed (release={release}): {error:?}; retained diagnostics={:?}", + error.diagnostics()); + })) +} + +fn entry(program: &CheckedResourceProgram, name: &str) -> DefId { + let functions = program + .module() + .items + .iter() + .filter_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) && function.name.name == name => + { + Some(function) + } + _ => None, + }) + .collect::>(); + let [function] = functions.as_slice() else { + panic!( + "primary source entry `{name}` must be unique; found {}", + functions.len() + ); + }; + let definitions = program + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == DefKind::Function + && definition.span == function.name.span + && definition.namespace.as_deref() == Some("app") + }) + .map(|definition| definition.id) + .collect::>(); + let [definition] = definitions.as_slice() else { + panic!( + "primary source entry `{name}` has no unique exact ordinary declaration: {:?}", + definitions + ); + }; + *definition +} + +fn lifecycle(source: &str, script: Vec, expected: Vec) { + for release in [false, true] { + let program = program(source, release); + let target = entry(&program, "scenario"); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(script.clone()) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(target, vec![grant]) + .unwrap(), + Value::Nothing + ); + // Inspect before teardown. Registry::Drop is not the proof of source cleanup. + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (expected.clone(), 0, 0) + ); + assert!(interpreter.take_debug_events().is_empty()); + } +} + +#[test] +fn checked_source_constructs_borrows_moves_returns_and_closes_one_owner() { + lifecycle( + include_str!("fixtures/01_construct_borrow_move_close.jett"), + vec![ + ScriptOperation::Construct { + label: 101, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 101, + outcome: Ok(7), + }, + ScriptOperation::Borrow { + label: 101, + outcome: Ok(8), + }, + ], + vec![ + ProviderEvent::Constructed(101), + ProviderEvent::Borrowed(101), + ProviderEvent::Borrowed(101), + ProviderEvent::Finalized(101), + ], + ); +} + +#[test] +fn checked_source_scope_cleanup_is_reverse_holder_acquisition_before_teardown() { + lifecycle( + include_str!("fixtures/02_reverse_scope_drop.jett"), + vec![ + ScriptOperation::Construct { + label: 201, + outcome: Ok(()), + }, + ScriptOperation::Construct { + label: 202, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 201, + outcome: Ok(1), + }, + ScriptOperation::Borrow { + label: 202, + outcome: Ok(2), + }, + ], + vec![ + ProviderEvent::Constructed(201), + ProviderEvent::Constructed(202), + ProviderEvent::Borrowed(201), + ProviderEvent::Borrowed(202), + ProviderEvent::Finalized(202), + ProviderEvent::Finalized(201), + ], + ); +} + +#[test] +fn checked_source_domain_failures_publish_no_extra_owner() { + let source = include_str!("fixtures/03_domain_failures.jett"); + lifecycle( + source, + vec![ScriptOperation::Construct { + label: 301, + outcome: Err("denied".to_string()), + }], + vec![ProviderEvent::ConstructionFailed(301)], + ); + lifecycle( + source, + vec![ + ScriptOperation::Construct { + label: 301, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 301, + outcome: Err("unavailable".to_string()), + }, + ], + vec![ + ProviderEvent::Constructed(301), + ProviderEvent::BorrowFailed(301), + ProviderEvent::Finalized(301), + ], + ); +} + +#[test] +fn checked_source_bare_owned_actual_has_one_view_operation_backing() { + lifecycle( + include_str!("fixtures/04_bare_to_view_operation_cleanup.jett"), + vec![ + ScriptOperation::Construct { + label: 401, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 401, + outcome: Ok(4), + }, + ], + vec![ + ProviderEvent::Constructed(401), + ProviderEvent::Borrowed(401), + ProviderEvent::Finalized(401), + ], + ); +} + +#[test] +fn checked_source_occupied_result_moves_across_owned_calls_and_returns() { + lifecycle( + include_str!("fixtures/06_return_occupied_result.jett"), + vec![ + ScriptOperation::Construct { + label: 601, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 601, + outcome: Ok(6), + }, + ], + vec![ + ProviderEvent::Constructed(601), + ProviderEvent::Borrowed(601), + ProviderEvent::Finalized(601), + ], + ); +} + +#[test] +fn checked_source_indirect_hook_descriptor_does_not_finalize_until_invoked() { + lifecycle( + include_str!("fixtures/07_indirect_close.jett"), + vec![ScriptOperation::Construct { + label: 701, + outcome: Ok(()), + }], + vec![ + ProviderEvent::Constructed(701), + ProviderEvent::Finalized(701), + ], + ); +} + +#[test] +fn checked_source_capability_free_close_wrapper_still_requires_runtime_purpose() { + lifecycle( + include_str!("fixtures/09_capability_free_close_target.jett"), + vec![ScriptOperation::Construct { + label: 901, + outcome: Ok(()), + }], + vec![ + ProviderEvent::Constructed(901), + ProviderEvent::Finalized(901), + ], + ); +} + +#[test] +fn checked_required_purposes_allow_closed_descriptors_and_absence_without_provider() { + let source = include_str!("fixtures/08_required_descriptor_and_absence.jett"); + for release in [false, true] { + for purpose in [ + ExecutionPurpose::NamespaceConstant, + ExecutionPurpose::ExplicitComptime, + ExecutionPurpose::Verify, + ExecutionPurpose::Property, + ] { + for name in ["descriptor_storage", "absent_close_path"] { + let program = program(source, release); + let target = entry(&program, name); + let mut interpreter = + Interpreter::from_checked_resource_program(program, purpose).unwrap(); + assert!( + interpreter + .install_resource_test_script(Vec::new()) + .is_err() + ); + assert_eq!( + interpreter + .call_checked_program_entry(target, Vec::new()) + .unwrap(), + Value::Nothing + ); + } + } + } +} + +#[test] +fn checked_source_later_actual_failure_preserves_lexical_borrow_and_skips_callee() { + lifecycle( + include_str!("fixtures/05_later_named_argument_failure_v2.jett"), + vec![ + ScriptOperation::Construct { + label: 501, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 501, + outcome: Ok(5), + }, + ], + vec![ + ProviderEvent::Constructed(501), + ProviderEvent::Borrowed(501), + ProviderEvent::Finalized(501), + ], + ); +} + +#[test] +fn checked_worker_apis_evaluate_closed_descriptors_and_property_trials_without_a_provider() { + let source = include_str!("fixtures/08_required_descriptor_and_absence.jett"); + for release in [false, true] { + let program = program(source, release); + let types = Arc::new(crate::checked_types::CheckedExpressionTypes { + resource_program: Some(program.clone()), + ..Default::default() + }); + let required = crate::evaluate_explicit_comptime_expressions_capture( + program.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + types.clone(), + Arc::new(HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "{:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + let blocks = crate::run_verify_blocks_detailed_with_metadata_and_expression_types( + program.module(), + None, + Some(types.clone()), + None, + ); + assert_eq!(blocks.len(), 2); + assert!( + blocks + .iter() + .all(|block| block.passed && block.debug_events.is_empty()), + "{blocks:?}" + ); + assert_eq!(blocks.iter().filter(|block| block.is_property).count(), 1); + assert_eq!( + blocks + .iter() + .find(|block| block.is_property) + .unwrap() + .iterations, + Some(100) + ); + let foreign = program.module().clone(); + let rejected = crate::run_verify_blocks_detailed_with_metadata_and_expression_types( + &foreign, + None, + Some(types), + None, + ); + assert_eq!(rejected.len(), 1); + assert!(!rejected[0].passed); + assert!( + rejected[0] + .error + .as_deref() + .unwrap() + .contains("foreign checked source module") + ); + } +} + +#[test] +fn raw_runtime_names_and_debug_configuration_do_not_grant_resource_dispatch() { + let program = program( + include_str!("fixtures/09_capability_free_close_target.jett"), + false, + ); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(Vec::new()) + .unwrap(); + assert!( + interpreter + .call_function("app.scenario", vec![grant]) + .is_err() + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (Vec::new(), 0, 0) + ); + let mut raw = Interpreter::new_runtime(); + assert!( + raw.call_function( + "resource_probe.kernel_create", + vec![Value::Capability("Network".to_string()), Value::Int64(1)] + ) + .is_err() + ); + assert!(raw.take_debug_events().is_empty()); +} + +#[test] +fn checked_source_bare_owned_view_provider_panic_unwinds_callee_before_parent_owner() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + for release in [false, true] { + let checked = program( + include_str!("fixtures/04_bare_to_view_operation_cleanup.jett"), + release, + ); + let target = entry(&checked, "scenario"); + let mut interpreter = + Interpreter::from_checked_resource_program(checked, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ + ScriptOperation::Construct { + label: 401, + outcome: Ok(()), + }, + ScriptOperation::BorrowPanic { label: 401 }, + ]) + .unwrap(); + assert!( + catch_unwind(AssertUnwindSafe( + || interpreter.call_checked_program_entry(target, vec![grant]) + )) + .is_err() + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(401), + ProviderEvent::Borrowed(401), + ProviderEvent::Finalized(401) + ], + 0, + 0 + ) + ); + assert!(interpreter.take_debug_events().is_empty()); + } +} + +#[test] +fn checked_source_ordinary_closure_ignores_unreferenced_live_owner_and_grant() { + for release in [false, true] { + let checked = program( + include_str!("fixtures/10_ordinary_closure_unreferenced_resource.jett"), + release, + ); + let target = entry(&checked, "scenario"); + let mut interpreter = + Interpreter::from_checked_resource_program(checked, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ScriptOperation::Construct { + label: 1001, + outcome: Ok(()), + }]) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(target, vec![grant]) + .unwrap(), + Value::Int64(7) + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(1001), + ProviderEvent::Finalized(1001) + ], + 0, + 0 + ) + ); + assert!(interpreter.take_debug_events().is_empty()); + } +} + +#[test] +fn checked_source_terminal_runtime_error_cleans_owner_before_teardown() { + for release in [false, true] { + let checked = program( + include_str!("fixtures/11_terminal_assertion_cleanup.jett"), + release, + ); + let target = entry(&checked, "scenario"); + let mut interpreter = + Interpreter::from_checked_resource_program(checked, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ + ScriptOperation::Construct { + label: 1101, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1101, + outcome: Ok(7), + }, + ]) + .unwrap(); + let error = interpreter + .call_checked_program_entry(target, vec![grant]) + .expect_err("the checked source kernel must return an ordinary terminal error"); + assert_eq!(error, "list.__remove_at: index -1 out of bounds"); + // This same interpreter is still alive: teardown cannot satisfy these counts. + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(1101), + ProviderEvent::Borrowed(1101), + ProviderEvent::Finalized(1101) + ], + 0, + 0, + ) + ); + assert!(interpreter.take_debug_events().is_empty()); + } +} + +fn mixed_failure_panic_message(payload: &(dyn std::any::Any + Send)) -> &str { + if let Some(message) = payload.downcast_ref::<&str>() { + return message; + } + payload + .downcast_ref::() + .map(String::as_str) + .expect("selected private fault has a textual panic payload") +} + +#[test] +fn checked_source_cleanup_panic_overrides_provider_panic_after_real_view_cleanup() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + for release in [false, true] { + for cleanup_panics in [false, true] { + let checked = program( + include_str!("fixtures/04_bare_to_view_operation_cleanup.jett"), + release, + ); + let target = entry(&checked, "scenario"); + let mut interpreter = Interpreter::from_checked_resource_program( + checked, + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let construction = if cleanup_panics { + ScriptOperation::ConstructFinalizerPanic { label: 401 } + } else { + ScriptOperation::Construct { + label: 401, + outcome: Ok(()), + } + }; + let grant = interpreter + .install_resource_test_script(vec![ + construction, + ScriptOperation::BorrowPanic { label: 401 }, + ]) + .unwrap(); + let panic = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(target, vec![grant]) + })) + .expect_err("private provider fault must unwind"); + let expected = if cleanup_panics { + "selected test resource cleanup panic" + } else { + "selected test borrow provider panic" + }; + assert_eq!(mixed_failure_panic_message(panic.as_ref()), expected); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(401), + ProviderEvent::Borrowed(401), + ProviderEvent::Finalized(401) + ], + 0, + 0 + ) + ); + assert!(interpreter.take_debug_events().is_empty()); + } + } +} + +#[test] +fn checked_source_mixed_failure_continues_older_owner_cleanup_before_teardown() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + for release in [false, true] { + let checked = program( + include_str!("fixtures/12_mixed_failure_cleanup_continues.jett"), + release, + ); + let target = entry(&checked, "scenario"); + let mut interpreter = + Interpreter::from_checked_resource_program(checked, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ + ScriptOperation::Construct { + label: 1201, + outcome: Ok(()), + }, + ScriptOperation::ConstructFinalizerPanic { label: 1202 }, + ScriptOperation::Borrow { + label: 1201, + outcome: Ok(1), + }, + ScriptOperation::BorrowPanic { label: 1202 }, + ]) + .unwrap(); + let panic = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(target, vec![grant]) + })) + .expect_err("both selected private faults unwind"); + assert_eq!( + mixed_failure_panic_message(panic.as_ref()), + "selected test resource cleanup panic" + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(1201), + ProviderEvent::Constructed(1202), + ProviderEvent::Borrowed(1201), + ProviderEvent::Borrowed(1202), + ProviderEvent::Finalized(1202), + ProviderEvent::Finalized(1201) + ], + 0, + 0 + ) + ); + assert!(interpreter.take_debug_events().is_empty()); + } +} + +#[test] +fn checked_source_clean_cleanup_preserves_ordinary_protocol_error() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + for release in [false, true] { + for cleanup_panics in [false, true] { + let checked = program( + include_str!("fixtures/04_bare_to_view_operation_cleanup.jett"), + release, + ); + let target = entry(&checked, "scenario"); + let mut interpreter = Interpreter::from_checked_resource_program( + checked, + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let construction = if cleanup_panics { + ScriptOperation::ConstructFinalizerPanic { label: 401 } + } else { + ScriptOperation::Construct { + label: 401, + outcome: Ok(()), + } + }; + // Deliberately missing Borrow is a private harness protocol fault, + // not a source result.fail or a newly permitted provider behavior. + let grant = interpreter + .install_resource_test_script(vec![construction]) + .unwrap(); + let outcome = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(target, vec![grant]) + })); + if cleanup_panics { + let panic = + outcome.expect_err("cleanup panic overrides an ordinary operation error"); + assert_eq!( + mixed_failure_panic_message(panic.as_ref()), + "selected test resource cleanup panic" + ); + } else { + assert_eq!( + outcome.expect("protocol error remains an ordinary Err"), + Err("resource execution has invalid checked call metadata".to_string()) + ); + } + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(401), + ProviderEvent::Finalized(401) + ], + 0, + 0 + ) + ); + assert!(interpreter.take_debug_events().is_empty()); + } + } +} + +#[path = "prerequisite_tests.rs"] +mod frontend_prerequisites; + +#[test] +fn checked_direct_and_nested_type_scopes_execute_original_lifecycle_and_restore_caller() { + lifecycle( + include_str!("fixtures/13_direct_nested_scoped_lifecycle.jett"), + vec![ + ScriptOperation::Construct { + label: 1301, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1301, + outcome: Ok(1), + }, + ScriptOperation::Construct { + label: 1302, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1302, + outcome: Ok(2), + }, + ScriptOperation::Construct { + label: 1304, + outcome: Ok(()), + }, + ScriptOperation::Construct { + label: 1303, + outcome: Ok(()), + }, + ], + vec![ + ProviderEvent::Constructed(1301), + ProviderEvent::Borrowed(1301), + ProviderEvent::Finalized(1301), + ProviderEvent::Constructed(1302), + ProviderEvent::Borrowed(1302), + ProviderEvent::Finalized(1302), + ProviderEvent::Constructed(1304), + ProviderEvent::Finalized(1304), + ProviderEvent::Constructed(1303), + ProviderEvent::Finalized(1303), + ], + ); +} + +#[test] +fn checked_direct_scopes_keep_named_worker_descriptor_and_absence_entries_provider_free() { + let source = include_str!("fixtures/14_direct_scoped_descriptor_absence.jett"); + for release in [false, true] { + for purpose in [ + ExecutionPurpose::NamespaceConstant, + ExecutionPurpose::ExplicitComptime, + ExecutionPurpose::Verify, + ExecutionPurpose::Property, + ] { + for name in ["descriptor_storage", "absent_close_path"] { + let program = program(source, release); + let target = entry(&program, name); + let mut interpreter = + Interpreter::from_checked_resource_program(program.clone(), purpose).unwrap(); + interpreter + .authorize_checked_resource_worker(program.module(), purpose) + .unwrap(); + let context = interpreter.resource_test_entry_context().unwrap(); + assert!( + interpreter + .install_resource_test_script(Vec::new()) + .is_err() + ); + assert_eq!( + interpreter + .call_checked_program_entry(target, Vec::new()) + .unwrap(), + Value::Nothing + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + assert!(interpreter.take_debug_events().is_empty()); + } + } + } + // This is a checked named-entry test. Collected required expressions and + // callable/cache body references remain a separate mandatory later layer. +} + +#[test] +fn checked_direct_scope_error_restores_typed_and_legacy_context_before_reentry() { + let source = include_str!("fixtures/15_direct_scoped_ordinary_error.jett"); + for release in [false, true] { + let program = program(source, release); + let target = entry(&program, "scenario"); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ + ScriptOperation::Construct { + label: 1501, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1501, + outcome: Ok(1), + }, + ScriptOperation::Construct { + label: 1501, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1501, + outcome: Ok(2), + }, + ]) + .unwrap(); + for attempt in 0..2 { + let error = interpreter + .call_checked_program_entry(target, vec![grant.clone()]) + .unwrap_err(); + assert_eq!(error, "list.__remove_at: index -1 out of bounds"); + let mut expected = vec![ + ProviderEvent::Constructed(1501), + ProviderEvent::Borrowed(1501), + ProviderEvent::Finalized(1501), + ]; + if attempt == 1 { + expected.extend(expected.clone()); + } + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (expected, 0, 0) + ); + assert_eq!( + interpreter.resource_test_scoped_context().unwrap(), + (false, 0, 0, 0, None) + ); + assert!(interpreter.take_debug_events().is_empty()); + } + } +} + +#[test] +fn checked_direct_scopes_restore_after_return_and_default_with_live_owner() { + let source = include_str!("fixtures/16_direct_scoped_return_default.jett"); + for release in [false, true] { + for outcome in [Ok(19), Err("unavailable".to_string())] { + let program = program(source, release); + let target = entry(&program, "scenario"); + let mut interpreter = Interpreter::from_checked_resource_program( + program, + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ + ScriptOperation::Construct { + label: 1601, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1601, + outcome: outcome.clone(), + }, + ]) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(target, vec![grant]) + .unwrap(), + Value::Int64(if outcome.is_ok() { 19 } else { 17 }) + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(1601), + if outcome.is_ok() { + ProviderEvent::Borrowed(1601) + } else { + ProviderEvent::BorrowFailed(1601) + }, + ProviderEvent::Finalized(1601) + ], + 0, + 0 + ) + ); + assert_eq!( + interpreter.resource_test_scoped_context().unwrap(), + (false, 0, 0, 0, None) + ); + assert!(interpreter.take_debug_events().is_empty()); + } + } +} + +#[test] +fn checked_entries_reuse_the_same_provider_after_clean_and_ordinary_error_completion() { + let source = include_str!("fixtures/25_repeated_scoped_entry.jett"); + for release in [false, true] { + for first_errors in [false, true] { + let program = program(source, release); + let clean = entry(&program, "clean"); + let first = if first_errors { + entry(&program, "terminal_error") + } else { + clean + }; + let mut interpreter = Interpreter::from_checked_resource_program( + program, + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let first_label = if first_errors { 1802 } else { 1801 }; + let grant = interpreter + .install_resource_test_script(vec![ + ScriptOperation::Construct { + label: first_label, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: first_label, + outcome: Ok(11), + }, + ScriptOperation::Construct { + label: 1801, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1801, + outcome: Ok(12), + }, + ]) + .unwrap(); + let context = interpreter.resource_test_entry_context().unwrap(); + let first_result = interpreter.call_checked_program_entry(first, vec![grant.clone()]); + if first_errors { + assert_eq!( + first_result, + Err("list.__remove_at: index -1 out of bounds".to_string()) + ); + } else { + assert_eq!(first_result.unwrap(), Value::Int64(11)); + } + let mut events = vec![ + ProviderEvent::Constructed(first_label), + ProviderEvent::Borrowed(first_label), + ProviderEvent::Finalized(first_label), + ]; + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (events.clone(), 0, 0) + ); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + assert_eq!( + interpreter + .call_checked_program_entry(clean, vec![grant]) + .unwrap(), + Value::Int64(12) + ); + events.extend([ + ProviderEvent::Constructed(1801), + ProviderEvent::Borrowed(1801), + ProviderEvent::Finalized(1801), + ]); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (events, 0, 0) + ); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + assert!(interpreter.take_debug_events().is_empty()); + } + } +} + +#[test] +fn checked_entries_restore_after_provider_or_finalizer_panic_before_same_grant_reentry() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + let source = include_str!("fixtures/25_repeated_scoped_entry.jett"); + for release in [false, true] { + for cleanup_panics in [false, true] { + let program = program(source, release); + let target = entry(&program, "clean"); + let mut interpreter = Interpreter::from_checked_resource_program( + program, + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ + if cleanup_panics { + ScriptOperation::ConstructFinalizerPanic { label: 1801 } + } else { + ScriptOperation::Construct { + label: 1801, + outcome: Ok(()), + } + }, + if cleanup_panics { + ScriptOperation::Borrow { + label: 1801, + outcome: Ok(7), + } + } else { + ScriptOperation::BorrowPanic { label: 1801 } + }, + ScriptOperation::Construct { + label: 1801, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1801, + outcome: Ok(19), + }, + ]) + .unwrap(); + let context = interpreter.resource_test_entry_context().unwrap(); + let panic = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(target, vec![grant.clone()]) + })) + .expect_err("private scripted host panic remains observable"); + assert_eq!( + mixed_failure_panic_message(panic.as_ref()), + if cleanup_panics { + "selected test resource cleanup panic" + } else { + "selected test borrow provider panic" + } + ); + let mut events = vec![ + ProviderEvent::Constructed(1801), + ProviderEvent::Borrowed(1801), + ProviderEvent::Finalized(1801), + ]; + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (events.clone(), 0, 0) + ); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + assert_eq!( + interpreter + .call_checked_program_entry(target, vec![grant]) + .unwrap(), + Value::Int64(19) + ); + events.extend([ + ProviderEvent::Constructed(1801), + ProviderEvent::Borrowed(1801), + ProviderEvent::Finalized(1801), + ]); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (events, 0, 0) + ); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + assert!(interpreter.take_debug_events().is_empty()); + } + } +} + +pub(crate) fn same_resource_hook_descriptor_identity( + left: &super::ResourceHookDescriptor, + right: &super::ResourceHookDescriptor, +) -> bool { + Arc::ptr_eq(&left.program, &right.program) && left.definition == right.definition +} + +#[test] +fn closed_descriptor_mirror_identity_preserves_function_inequality_and_program_boundary() { + use super::CheckedExecution; + use jett_types::ResourceHookKind; + const SOURCE: &str = include_str!("fixtures/27_original_generic_scoped_required.jett"); + for release in [false, true] { + let checked_program = program(SOURCE, release); + let checked = + CheckedExecution::new(checked_program.clone(), ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let hook = |kind| { + checked_program + .checked() + .resource_hooks + .values() + .find(|hook| hook.kind == kind) + .unwrap() + .definition + }; + let closed = checked.descriptor(hook(ResourceHookKind::Close)).unwrap(); + assert!(same_resource_hook_descriptor_identity( + &closed, + &closed.clone() + )); + assert!(!same_resource_hook_descriptor_identity( + &closed, + &checked + .descriptor(hook(ResourceHookKind::BorrowOperation)) + .unwrap() + )); + let foreign = + CheckedExecution::new(program(SOURCE, release), ExecutionPurpose::ReferenceRuntime) + .unwrap(); + assert!(!same_resource_hook_descriptor_identity( + &closed, + &foreign.descriptor(hook(ResourceHookKind::Close)).unwrap() + )); + assert_ne!( + Value::ResourceHook(closed.clone()), + Value::ResourceHook(closed) + ); + } +} + +#[path = "assignment_tests.rs"] +mod mutable_assignment; + +#[path = "tests/native_source_oracles.rs"] +mod native_source_oracles; diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles.rs b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles.rs new file mode 100644 index 00000000..6cb79487 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles.rs @@ -0,0 +1,284 @@ +//! Real original checked-source counterpart to the linked private archive cases. +#[path = "../../../../jett_driver/tests/native_conformance/resource_cases.rs"] +mod cases; +use super::*; +use std::panic::{AssertUnwindSafe, catch_unwind}; + +#[path = "../../../../jett_driver/tests/native_conformance/resource_occupied_struct_cases.rs"] +mod occupied_struct_cases; +#[path = "native_source_oracles/occupied_struct_reentry.rs"] +mod occupied_struct_reentry; + +#[test] +fn native_resource_occupied_struct_shared_scripts_match_real_reference() { + assert_eq!(occupied_struct_cases::CASES.len(), 11); + run_reference_cases(occupied_struct_cases::CASES); +} +#[test] +fn native_resource_occupied_struct_shared_reentry_inputs_match_real_reference() { + for release in [false, true] { + for first in occupied_struct_cases::CASES { + occupied_struct_reentry::original_then_ready( + first, + occupied_struct_cases::ready_for(first), + release, + ); + } + } +} + +fn checked_case(case: &cases::Case, release: bool) -> Arc { + let stdlib = FileId::new(10_000); + let primary = FileId::new(0); + let mut parsed = parse(cases::SUPPORT, stdlib); + let project = parse(case.source, primary); + assert!(parsed.errors.is_empty(), "support: {:?}", parsed.errors); + assert!( + project.errors.is_empty(), + "{}: {:?}", + case.name, + project.errors + ); + let resources = parsed + .module + .items + .iter() + .filter_map(|item| match item { + Item::Resource(item) => Some(item.name.span), + _ => None, + }) + .collect::>(); + let [declaration] = resources.as_slice() else { + panic!("exact synthetic Resource declaration is not unique"); + }; + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: *declaration, + member: member.into(), + recipe, + }) + .collect::>(); + parsed.module.items.extend(project.module.items); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (stdlib, SourceOrigin::Stdlib), + (primary, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap_or_else(|error| { + panic!( + "{} reference prerequisite: {error:?}; {:?}", + case.name, + error.diagnostics() + ) + }), + ) +} +fn script(input: cases::Script) -> ScriptOperation { + match input { + cases::Script::Construct(label) => ScriptOperation::Construct { + label, + outcome: Ok(()), + }, + cases::Script::ConstructFail(label, error) => ScriptOperation::Construct { + label, + outcome: Err(error.into()), + }, + cases::Script::FinalizerPanic(label) => ScriptOperation::ConstructFinalizerPanic { label }, + cases::Script::BorrowPanic(label) => ScriptOperation::BorrowPanic { label }, + cases::Script::Borrow(label, value) => ScriptOperation::Borrow { + label, + outcome: Ok(value), + }, + cases::Script::BorrowFail(label, error) => ScriptOperation::Borrow { + label, + outcome: Err(error.into()), + }, + } +} +fn event(input: &cases::Event) -> ProviderEvent { + match *input { + cases::Event::Constructed(label) => ProviderEvent::Constructed(label), + cases::Event::ConstructionFailed(label) => ProviderEvent::ConstructionFailed(label), + cases::Event::Borrowed(label) => ProviderEvent::Borrowed(label), + cases::Event::BorrowFailed(label) => ProviderEvent::BorrowFailed(label), + cases::Event::Finalized(label) => ProviderEvent::Finalized(label), + } +} + +fn reference_with_required_values( + checked: &Arc, + case: &cases::Case, + release: bool, +) -> Interpreter { + // Check the worker's existing purpose boundary without executing Source or + // installing a provider. The real required worker below receives no grant. + let mut worker_guard = Interpreter::new(); + worker_guard + .install_checked_resource_program(checked.clone(), ExecutionPurpose::ExplicitComptime) + .unwrap(); + worker_guard + .authorize_checked_resource_worker(checked.module(), ExecutionPurpose::ExplicitComptime) + .unwrap(); + assert!( + worker_guard + .install_resource_test_script(Vec::new()) + .is_err(), + "{} release={release}: required purpose must refuse a runtime provider", + case.name + ); + assert_eq!(worker_guard.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(worker_guard.resource_test_observations().is_err()); + + let types = Arc::new(crate::checked_types::CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let metadata = Arc::new(jett_types::ReflectionMetadata::new()); + let exclusions = Arc::new(HashMap::new()); + let required = crate::evaluate_explicit_comptime_expressions_capture( + checked.module(), + metadata.clone(), + types.clone(), + exclusions.clone(), + ); + assert!( + required.diagnostics.is_empty(), + "{} release={release}: required evaluation failed: {:?}", + case.name, + required.diagnostics + ); + assert!( + required.debug_events.is_empty(), + "{} release={release}: unexpected required-phase debug observations: {:?}", + case.name, + required.debug_events + ); + assert!(required.values.checked_values_are_mirrored()); + assert_eq!(required.values.len(), required.values.values().count()); + assert!( + required + .values + .values() + .all(|value| !value.contains_live_resource_or_grant()), + "{} release={release}: required values cannot import runtime authority", + case.name + ); + + // Install even an empty checked cache. Expr::Comptime then refuses a missing + // exact checked key instead of falling back to evaluating its runtime body. + // Match the driver's order so namespace constants cannot replay at register. + let mut interpreter = Interpreter::new(); + interpreter.set_reflection_metadata(metadata); + interpreter.set_checked_expression_types(types); + interpreter.set_breakpoint_exclusions(exclusions); + interpreter.set_explicit_comptime_values(Arc::new(required.values)); + interpreter.register_module(checked.module()); + interpreter + .install_checked_resource_program(checked.clone(), ExecutionPurpose::ReferenceRuntime) + .unwrap(); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.resource_test_observations().is_err()); + interpreter +} + +fn run_reference_cases(selected: &[cases::Case]) { + for release in [false, true] { + for case in selected { + let checked = checked_case(case, release); + let definition = entry(&checked, "main"); + let mut interpreter = reference_with_required_values(&checked, case, release); + let grant = interpreter + .install_resource_test_script(case.script.iter().copied().map(script).collect()) + .unwrap(); + let outcome = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(definition, vec![grant]) + })); + match case.reference { + cases::ReferenceOutcome::Clean => assert_eq!( + outcome + .unwrap() + .unwrap_or_else(|error| panic!("{} release={release}: {error}", case.name)), + Value::Nothing, + "{} release={release}", + case.name + ), + cases::ReferenceOutcome::Error(expected) => assert_eq!( + outcome.unwrap().unwrap_err(), + expected, + "{} release={release}", + case.name + ), + cases::ReferenceOutcome::ProviderPanic(expected) => { + let panic = outcome.expect_err("actual Source borrow provider panic"); + let text = panic + .downcast_ref::() + .map(String::as_str) + .or_else(|| panic.downcast_ref::<&str>().copied()) + .unwrap(); + assert_eq!(text, expected, "{} release={release}", case.name); + } + cases::ReferenceOutcome::CleanupPanic(expected) => { + let panic = outcome.expect_err("actual Source finalizer panic"); + let text = panic + .downcast_ref::() + .map(String::as_str) + .or_else(|| panic.downcast_ref::<&str>().copied()) + .unwrap(); + assert_eq!(text, expected, "{} release={release}", case.name); + } + } + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (case.events.iter().map(event).collect(), 0, 0), + "{} release={release}", + case.name + ); + assert!( + interpreter.take_debug_events().is_empty(), + "{} release={release}", + case.name + ); + } + } +} + +#[test] +fn native_resource_source_cases_match_real_reference_events_and_cleanup_before_teardown() { + run_reference_cases(cases::CASES); +} + +#[test] +fn native_resource_pipeline_source_cases_match_real_reference_events_and_cleanup_before_teardown() { + run_reference_cases(cases::pipeline_cases()); +} + +#[test] +fn native_resource_control_flow_source_cases_match_real_reference_events_and_cleanup_before_teardown() + { + run_reference_cases(cases::control_flow_cases()); +} + +#[path = "native_source_oracles/source06_exceptional.rs"] +mod source06_exceptional; + +#[path = "native_source_oracles/reflected_field_positive.rs"] +mod reflected_field_positive; + +#[path = "native_source_oracles/absent_aggregate_baseline.rs"] +mod absent_aggregate_baseline; diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/absent_aggregate_baseline.rs b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/absent_aggregate_baseline.rs new file mode 100644 index 00000000..7ad54ea6 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/absent_aggregate_baseline.rs @@ -0,0 +1,235 @@ +//! Strict new-wrapper reference baseline; existing twelve typed returns are separate. +use super::*; +#[path = "../../../../../jett_driver/tests/native_conformance/resource_absent_aggregate_baseline_inputs.rs"] +mod inputs; + +fn runtime( + checked: &Arc, + entered: &mut Vec, +) -> Result { + use inputs::{Phase, enter, failure}; + let types = Arc::new(crate::checked_types::CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let metadata = Arc::new(jett_types::ReflectionMetadata::new()); + let exclusions = Arc::new(HashMap::new()); + enter(entered, Phase::RequiredCapture); + let required = crate::evaluate_explicit_comptime_expressions_capture( + checked.module(), + metadata.clone(), + types.clone(), + exclusions.clone(), + ); + if !required.diagnostics.is_empty() || !required.debug_events.is_empty() { + return Err(failure( + Phase::RequiredCapture, + (&required.diagnostics, &required.debug_events), + )); + } + enter(entered, Phase::RequiredCache); + required + .values + .checked_required_values(checked) + .map_err(|error| failure(Phase::RequiredCache, error))?; + if !required.values.checked_values_are_mirrored() + || required + .values + .values() + .any(Value::contains_live_resource_or_grant) + { + return Err(failure( + Phase::RequiredCache, + "unmirrored or runtime-authority required value", + )); + } + let constants = checked + .module() + .items + .iter() + .filter_map(|item| match item { + Item::VarDecl(declaration) + if declaration.name.span.file == FileId::new(0) + && declaration.name.name == "absent_namespace_value" => + { + Some(declaration.name.span) + } + _ => None, + }) + .collect::>(); + let [constant] = constants.as_slice() else { + return Err(failure( + Phase::RequiredCache, + "original namespace constant is not unique", + )); + }; + if required.values.constant(*constant) != Some(&Value::Int64(17)) { + return Err(failure( + Phase::RequiredCache, + required.values.constant(*constant), + )); + } + enter(entered, Phase::ReferenceSetup); + let mut interpreter = Interpreter::new(); + interpreter.set_reflection_metadata(metadata); + interpreter.set_checked_expression_types(types); + interpreter.set_breakpoint_exclusions(exclusions); + interpreter.set_explicit_comptime_values(Arc::new(required.values)); + // Baked values precede registration; no namespace initializer fallback. + interpreter.register_module(checked.module()); + interpreter + .install_checked_resource_program(checked.clone(), ExecutionPurpose::ReferenceRuntime) + .map_err(|error| failure(Phase::ReferenceSetup, error))?; + Ok(interpreter) +} + +fn execute( + input: &inputs::Input, + release: bool, + entered: &mut Vec, +) -> Result<(), inputs::Failure> { + use inputs::{Phase, enter, failure}; + let (checked, span) = inputs::prepare(input, release, entered)?; + let mut interpreter = runtime(&checked, entered)?; + enter(entered, Phase::ReferenceEntryIdentity); + let functions = checked + .module() + .items + .iter() + .filter_map(|item| match item { + Item::Function(function) if function.span == span => Some(function), + _ => None, + }) + .collect::>(); + let [function] = functions.as_slice() else { + return Err(failure( + Phase::ReferenceEntryIdentity, + "original primary main body missing", + )); + }; + let definitions = checked + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == jett_resolve::DefKind::Function + && definition.span == function.name.span + && definition.namespace.as_deref() == Some("app") + }) + .map(|definition| definition.id) + .collect::>(); + let [definition] = definitions.as_slice() else { + return Err(failure( + Phase::ReferenceEntryIdentity, + "original main has no unique checked definition", + )); + }; + let grant = interpreter + .install_resource_test_script(Vec::new()) + .map_err(|error| failure(Phase::ReferenceSetup, error))?; + let baseline = interpreter + .resource_test_entry_context() + .map_err(|error| failure(Phase::ReferenceSetup, error))?; + enter(entered, Phase::ReferenceCall); + // Retain the interpreter for observations even if the Source panics. + let actual = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(*definition, vec![grant]) + })); + let actual_text = match &actual { + Ok(result) => format!("{result:?}"), + Err(payload) => format!( + "UnexpectedPanic({:?})", + inputs::panic_message(payload.as_ref()) + ), + }; + let call_outcome = match actual { + Ok(Ok(Value::Nothing)) + if matches!(input.reference, inputs::ReferenceExpectation::Nothing) => + { + Ok(()) + } + Ok(Err(actual)) => match input.reference { + inputs::ReferenceExpectation::Error(expected) if actual == expected => Ok(()), + _ => Err(failure(Phase::ReferenceCall, actual)), + }, + Ok(other) => Err(failure(Phase::ReferenceCall, other)), + Err(payload) => Err(inputs::Failure { + phase: Phase::ReferenceCall, + kind: inputs::FailureKind::UnexpectedPanic, + detail: inputs::panic_message(payload.as_ref()), + }), + }; + enter(entered, Phase::ReferenceObservations); + let observations = interpreter.resource_test_observations(); + let custody = interpreter.resource_test_custody_counts(); + let context = interpreter.resource_test_entry_context(); + let debug = interpreter.take_debug_events(); + eprintln!( + "ABSENT_REFERENCE name={:?} release={release} observed={actual_text:?} observations={observations:?} custody={custody:?} context={context:?} debug={debug:?}", + input.name + ); + if observations != Ok((Vec::new(), 0, 0)) + || custody != Ok((0, 0)) + || context.as_ref() != Ok(&baseline) + || !debug.is_empty() + { + if let Err(mut first) = call_outcome { + first.detail.push_str( + "; exact empty effects/custody/context/debug invariant also failed; see actual log", + ); + return Err(first); + } + return Err(failure( + Phase::ReferenceObservations, + "exact empty effects/custody/context/debug invariant failed; see actual log", + )); + } + call_outcome +} + +#[test] +fn native_resource_absent_aggregate_new_wrappers_record_all_reference_outcomes() { + assert_eq!(inputs::INPUTS.len(), 14); + let mut attempts = 0; + let mut matched = 0; + let mut failures = Vec::new(); + for release in [false, true] { + for input in inputs::INPUTS { + attempts += 1; + let attempt = inputs::attempt(|entered| execute(input, release, entered)); + match attempt.outcome { + Ok(()) => { + matched += 1; + eprintln!( + "ABSENT_REFERENCE_MATCH name={:?} release={release} entered={:?}", + input.name, attempt.entered + ); + } + Err(failure) => { + eprintln!( + "ABSENT_REFERENCE_FAILURE name={:?} release={release} entered={:?} failure={failure:?}", + input.name, attempt.entered + ); + failures.push((input.name, release, failure)); + } + } + } + } + eprintln!( + "ABSENT_REFERENCE_TOTAL attempts={attempts} matched={matched} failed={}", + failures.len() + ); + assert_eq!(attempts, 28); + assert!( + failures.is_empty(), + "strict new-wrapper reference failures: {failures:#?}" + ); + assert_eq!(matched, 28); +} diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/occupied_struct_reentry.rs b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/occupied_struct_reentry.rs new file mode 100644 index 00000000..4fadada4 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/occupied_struct_reentry.rs @@ -0,0 +1,80 @@ +//! Genuine reference counterpart to the shared native original-then-READY matrix. +use super::*; + +fn first_outcome( + actual: std::thread::Result>, + case: &cases::Case, + release: bool, +) { + match case.reference { + cases::ReferenceOutcome::Clean => { + assert_eq!( + actual.unwrap().unwrap(), + Value::Nothing, + "{} release={release}", + case.name + ); + } + cases::ReferenceOutcome::Error(expected) => { + assert_eq!( + actual.unwrap().unwrap_err(), + expected, + "{} release={release}", + case.name + ); + } + cases::ReferenceOutcome::ProviderPanic(expected) + | cases::ReferenceOutcome::CleanupPanic(expected) => { + let payload = actual.expect_err("the selected Source panic stays observable"); + let message = payload + .downcast_ref::() + .map(String::as_str) + .or_else(|| payload.downcast_ref::<&str>().copied()) + .expect("the scripted provider/finalizer panic has a string payload"); + assert_eq!(message, expected, "{} release={release}", case.name); + } + } +} + +fn empty_before_teardown(interpreter: &mut Interpreter, expected: &[ProviderEvent]) { + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (expected.to_vec(), 0, 0) + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); +} + +pub(super) fn original_then_ready(first: &cases::Case, ready: &cases::Case, release: bool) { + assert_eq!(first.source.as_bytes(), ready.source.as_bytes()); + let checked = checked_case(first, release); + let definition = entry(&checked, "main"); + let mut interpreter = reference_with_required_values(&checked, first, release); + let combined = first + .script + .iter() + .chain(ready.script.iter()) + .copied() + .map(script) + .collect(); + let grant = interpreter.install_resource_test_script(combined).unwrap(); + let context = interpreter.resource_test_entry_context().unwrap(); + let actual = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(definition, vec![grant.clone()]) + })); + first_outcome(actual, first, release); + let mut expected = first.events.iter().map(event).collect::>(); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + empty_before_teardown(&mut interpreter, &expected); + assert_eq!( + interpreter + .call_checked_program_entry(definition, vec![grant]) + .unwrap(), + Value::Nothing, + "{} release={release}: same-grant READY", + first.name + ); + expected.extend(ready.events.iter().map(event)); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + empty_before_teardown(&mut interpreter, &expected); +} diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive.rs b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive.rs new file mode 100644 index 00000000..229727dd --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive.rs @@ -0,0 +1,73 @@ +//! Reference counterpart of the standalone four-Source clean native rows. +use super::*; + +#[path = "../../../../../jett_driver/tests/native_conformance/resource_reflected_field_cases.rs"] +mod reflected_field_cases; + +#[test] +fn reflected_field_four_positive_sources_match_real_reference() { + assert_eq!(reflected_field_cases::CASES.len(), 4); + run_reference_cases(reflected_field_cases::CASES); +} + +#[test] +fn reflected_field_four_positive_sources_clean_same_grant_reentry() { + for release in [false, true] { + for case in reflected_field_cases::CASES { + let checked = checked_case(case, release); + let definition = entry(&checked, "main"); + let mut interpreter = reference_with_required_values(&checked, case, release); + let grant = interpreter + .install_resource_test_script( + case.script + .iter() + .chain(case.script.iter()) + .copied() + .map(script) + .collect(), + ) + .unwrap(); + let context = interpreter.resource_test_entry_context().unwrap(); + let mut expected = Vec::new(); + for _ in 0..2 { + assert_eq!( + interpreter + .call_checked_program_entry(definition, vec![grant.clone()]) + .unwrap(), + Value::Nothing, + "{} release={release}", + case.name + ); + expected.extend(case.events.iter().map(event)); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (expected.clone(), 0, 0) + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); + } + } + } +} + +#[test] +fn reflected_field_source06_exceptional_native_inputs_match_real_reference() { + assert_eq!(reflected_field_cases::SOURCE06_EXCEPTIONAL_CASES.len(), 8); + run_reference_cases(reflected_field_cases::SOURCE06_EXCEPTIONAL_CASES); +} + +#[path = "reflected_field_positive/reentry.rs"] +mod source06_native_reentry; + +#[path = "../../../../../jett_driver/tests/native_conformance/resource_reflected_derived_cases.rs"] +mod reflected_derived_cases; + +#[path = "reflected_field_positive/derived_controls.rs"] +mod derived_controls; + +#[path = "../../../../../jett_driver/tests/native_conformance/resource_reflected_return_cases.rs"] +mod reflected_return_cases; + +#[path = "reflected_field_positive/reflected_returns.rs"] +mod reflected_returns; diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive/derived_controls.rs b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive/derived_controls.rs new file mode 100644 index 00000000..ecddfe61 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive/derived_controls.rs @@ -0,0 +1,20 @@ +//! Genuine checked reference counterpart to every shared derived native input. +use super::*; + +#[test] +fn reflected_derived_native_single_inputs_match_real_reference() { + assert_eq!(reflected_derived_cases::CASES.len(), 11); + assert_eq!(reflected_derived_cases::original_cases().len(), 10); + run_reference_cases(reflected_derived_cases::CASES); +} + +#[test] +fn reflected_derived_native_reentry_inputs_match_real_reference() { + assert_eq!(reflected_derived_cases::original_cases().len(), 10); + for release in [false, true] { + for first in reflected_derived_cases::original_cases() { + let ready = reflected_derived_cases::ready_for(first); + source06_native_reentry::original_then_ready(first, ready, release); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive/reentry.rs b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive/reentry.rs new file mode 100644 index 00000000..0e0ae34b --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive/reentry.rs @@ -0,0 +1,96 @@ +//! Genuine reference counterpart to the shared native original-then-READY matrix. +use super::*; + +fn first_outcome( + actual: std::thread::Result>, + case: &cases::Case, + release: bool, +) { + match case.reference { + cases::ReferenceOutcome::Clean => { + assert_eq!( + actual.unwrap().unwrap(), + Value::Nothing, + "{} release={release}", + case.name + ); + } + cases::ReferenceOutcome::Error(expected) => { + assert_eq!( + actual.unwrap().unwrap_err(), + expected, + "{} release={release}", + case.name + ); + } + cases::ReferenceOutcome::ProviderPanic(expected) + | cases::ReferenceOutcome::CleanupPanic(expected) => { + let payload = actual.expect_err("the selected Source panic stays observable"); + let message = payload + .downcast_ref::() + .map(String::as_str) + .or_else(|| payload.downcast_ref::<&str>().copied()) + .expect("the scripted provider/finalizer panic has a string payload"); + assert_eq!(message, expected, "{} release={release}", case.name); + } + } +} + +fn empty_before_teardown(interpreter: &mut Interpreter, expected: &[ProviderEvent]) { + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (expected.to_vec(), 0, 0) + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); +} + +pub(super) fn original_then_ready(first: &cases::Case, ready: &cases::Case, release: bool) { + assert_eq!(first.source.as_bytes(), ready.source.as_bytes()); + let checked = checked_case(first, release); + let definition = entry(&checked, "main"); + let mut interpreter = reference_with_required_values(&checked, first, release); + let combined = first + .script + .iter() + .chain(ready.script.iter()) + .copied() + .map(script) + .collect(); + let grant = interpreter.install_resource_test_script(combined).unwrap(); + let context = interpreter.resource_test_entry_context().unwrap(); + let actual = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(definition, vec![grant.clone()]) + })); + first_outcome(actual, first, release); + let mut expected = first.events.iter().map(event).collect::>(); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + empty_before_teardown(&mut interpreter, &expected); + assert_eq!( + interpreter + .call_checked_program_entry(definition, vec![grant]) + .unwrap(), + Value::Nothing, + "{} release={release}: same-grant READY", + first.name + ); + expected.extend(ready.events.iter().map(event)); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + empty_before_teardown(&mut interpreter, &expected); +} + +#[test] +fn reflected_field_source06_native_reentry_inputs_match_real_reference() { + let ready = &reflected_field_cases::CASES[0]; + assert_eq!(ready.name, "reflected_field_original"); + assert_eq!(reflected_field_cases::SOURCE06_EXCEPTIONAL_CASES.len(), 8); + for release in [false, true] { + let first_scripts = std::iter::once(ready) + .chain(reflected_field_cases::SOURCE06_EXCEPTIONAL_CASES.iter()) + .collect::>(); + assert_eq!(first_scripts.len(), 9); + for first in first_scripts { + original_then_ready(first, ready, release); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive/reflected_returns.rs b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive/reflected_returns.rs new file mode 100644 index 00000000..573fce3a --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive/reflected_returns.rs @@ -0,0 +1,19 @@ +//! Genuine checked reference counterpart of all shared dedicated Return inputs. +use super::*; + +#[test] +fn reflected_body_and_owned_return_single_inputs_match_real_reference() { + assert_eq!(reflected_return_cases::CASES.len(), 12); + run_reference_cases(reflected_return_cases::CASES); +} + +#[test] +fn reflected_body_and_owned_return_reentry_inputs_match_real_reference() { + assert_eq!(reflected_return_cases::CASES.len(), 12); + for release in [false, true] { + for first in reflected_return_cases::CASES { + let ready = reflected_return_cases::ready_for(first); + source06_native_reentry::original_then_ready(first, ready, release); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional.rs b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional.rs new file mode 100644 index 00000000..7914214f --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional.rs @@ -0,0 +1,489 @@ +//! Reflected field Resource execution, failure cleanup and same-grant reentry. +//! These assertions describe reference execution; native admission is separate. +use super::*; + +const SOURCE: &str = + include_str!("source06_exceptional/06_reflected_field_owned_optional_reuse.jett"); +const BREAK_SOURCE: &str = + include_str!("source06_exceptional/07_reflected_field_optional_break.jett"); +const CONTINUE_SOURCE: &str = + include_str!("source06_exceptional/08_reflected_field_optional_continue.jett"); +const DEFAULT_SOURCE: &str = + include_str!("source06_exceptional/09_reflected_field_optional_scalar_default.jett"); + +struct Input { + name: &'static str, + script: &'static [cases::Script], + events: &'static [cases::Event], + outcome: cases::ReferenceOutcome, +} + +const READY_SCRIPT: &[cases::Script] = &[ + cases::Script::Construct(871), + cases::Script::Borrow(871, 17), + cases::Script::Borrow(871, 17), +]; +const READY_EVENTS: &[cases::Event] = &[ + cases::Event::Constructed(871), + cases::Event::Borrowed(871), + cases::Event::Borrowed(871), + cases::Event::Finalized(871), +]; + +const INPUTS: &[Input] = &[ + Input { + name: "reflected_field_ready", + script: READY_SCRIPT, + events: READY_EVENTS, + outcome: cases::ReferenceOutcome::Clean, + }, + Input { + name: "reflected_field_construct_fail", + script: &[cases::Script::ConstructFail(871, "nested factory sentinel")], + events: &[cases::Event::ConstructionFailed(871)], + outcome: cases::ReferenceOutcome::Clean, + }, + Input { + name: "reflected_field_first_borrow_fail", + script: &[ + cases::Script::Construct(871), + cases::Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::BorrowFailed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::Clean, + }, + Input { + name: "reflected_field_later_borrow_fail", + script: &[ + cases::Script::Construct(871), + cases::Script::Borrow(871, 17), + cases::Script::BorrowFail(871, "later borrow sentinel"), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::Borrowed(871), + cases::Event::BorrowFailed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::Clean, + }, + Input { + name: "reflected_field_ready_cleanup_panic", + script: &[ + cases::Script::FinalizerPanic(871), + cases::Script::Borrow(871, 17), + cases::Script::Borrow(871, 17), + ], + events: READY_EVENTS, + outcome: cases::ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + }, + Input { + name: "reflected_field_first_borrow_provider_panic", + script: &[ + cases::Script::Construct(871), + cases::Script::BorrowPanic(871), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::Borrowed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::ProviderPanic("selected test borrow provider panic"), + }, + Input { + name: "reflected_field_later_borrow_provider_panic", + script: &[ + cases::Script::Construct(871), + cases::Script::Borrow(871, 17), + cases::Script::BorrowPanic(871), + ], + events: READY_EVENTS, + outcome: cases::ReferenceOutcome::ProviderPanic("selected test borrow provider panic"), + }, + Input { + name: "reflected_field_cleanup_wins_provider_panic", + script: &[ + cases::Script::FinalizerPanic(871), + cases::Script::BorrowPanic(871), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::Borrowed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + }, + Input { + name: "reflected_field_cleanup_wins_domain_failure_return", + script: &[ + cases::Script::FinalizerPanic(871), + cases::Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::BorrowFailed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + }, +]; + +struct DerivedInput { + source: &'static str, + input: Input, +} + +// These are explicit adaptations of Source06, never claims of unchanged Source. +const DERIVED_INPUTS: &[DerivedInput] = &[ + DerivedInput { + source: BREAK_SOURCE, + input: Input { + name: "reflected_resource_break_ready", + script: &[ + cases::Script::Construct(871), + cases::Script::Borrow(871, 17), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::Borrowed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::Clean, + }, + }, + DerivedInput { + source: BREAK_SOURCE, + input: Input { + name: "reflected_resource_break_first_borrow_fail", + script: &[ + cases::Script::Construct(871), + cases::Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::BorrowFailed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::Clean, + }, + }, + DerivedInput { + source: BREAK_SOURCE, + input: Input { + name: "reflected_resource_break_cleanup_panic", + script: &[ + cases::Script::FinalizerPanic(871), + cases::Script::Borrow(871, 17), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::Borrowed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + }, + }, + DerivedInput { + source: CONTINUE_SOURCE, + input: Input { + name: "reflected_resource_continue_ready", + script: READY_SCRIPT, + events: READY_EVENTS, + outcome: cases::ReferenceOutcome::Clean, + }, + }, + DerivedInput { + source: CONTINUE_SOURCE, + input: Input { + name: "reflected_resource_continue_first_borrow_fail", + script: &[ + cases::Script::Construct(871), + cases::Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::BorrowFailed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::Clean, + }, + }, + DerivedInput { + source: CONTINUE_SOURCE, + input: Input { + name: "reflected_resource_continue_later_borrow_fail", + script: &[ + cases::Script::Construct(871), + cases::Script::Borrow(871, 17), + cases::Script::BorrowFail(871, "later borrow sentinel"), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::Borrowed(871), + cases::Event::BorrowFailed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::Clean, + }, + }, + DerivedInput { + source: CONTINUE_SOURCE, + input: Input { + name: "reflected_resource_continue_cleanup_panic", + script: &[ + cases::Script::FinalizerPanic(871), + cases::Script::Borrow(871, 17), + cases::Script::Borrow(871, 17), + ], + events: READY_EVENTS, + outcome: cases::ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + }, + }, + DerivedInput { + source: DEFAULT_SOURCE, + input: Input { + name: "reflected_resource_scalar_default_first_borrow_fail", + script: &[ + cases::Script::Construct(871), + cases::Script::BorrowFail(871, "borrow sentinel"), + cases::Script::Borrow(871, 17), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::BorrowFailed(871), + cases::Event::Borrowed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::Clean, + }, + }, + DerivedInput { + source: DEFAULT_SOURCE, + input: Input { + name: "reflected_resource_scalar_default_later_borrow_fail", + script: &[ + cases::Script::Construct(871), + cases::Script::Borrow(871, 17), + cases::Script::BorrowFail(871, "later borrow sentinel"), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::Borrowed(871), + cases::Event::BorrowFailed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::Clean, + }, + }, + DerivedInput { + source: DEFAULT_SOURCE, + input: Input { + name: "reflected_resource_scalar_default_cleanup_panic", + script: &[ + cases::Script::FinalizerPanic(871), + cases::Script::BorrowFail(871, "borrow sentinel"), + cases::Script::Borrow(871, 17), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::BorrowFailed(871), + cases::Event::Borrowed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + }, + }, +]; + +fn reference_case(source: &'static str, input: &Input) -> cases::Case { + // checked_case/reference_with_required_values read only Source/script/name. + // This reference-only module never supplies these placeholders to a native + // Case gate. Native assertions must come from an independent linked run. + cases::Case { + name: input.name, + source, + script: input.script, + events: input.events, + reference: input.outcome, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + } +} + +fn panic_text(payload: &(dyn std::any::Any + Send)) -> &str { + payload + .downcast_ref::() + .map(String::as_str) + .or_else(|| payload.downcast_ref::<&str>().copied()) + .expect("selected provider/finalizer panic has a string payload") +} + +fn assert_outcome( + actual: std::thread::Result>, + expected: cases::ReferenceOutcome, + name: &str, + release: bool, +) { + match expected { + cases::ReferenceOutcome::Clean => assert_eq!( + actual + .unwrap() + .unwrap_or_else(|error| panic!("{name} release={release}: {error}")), + Value::Nothing, + "{name} release={release}", + ), + cases::ReferenceOutcome::Error(expected) => assert_eq!( + actual.unwrap().unwrap_err(), + expected, + "{name} release={release}", + ), + cases::ReferenceOutcome::ProviderPanic(expected) + | cases::ReferenceOutcome::CleanupPanic(expected) => { + let payload = actual.expect_err("the actual selected Source panic remains observable"); + assert_eq!( + panic_text(payload.as_ref()), + expected, + "{name} release={release}" + ); + } + } +} + +fn assert_empty_before_teardown( + interpreter: &mut Interpreter, + expected: &[ProviderEvent], + name: &str, + release: bool, +) { + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (expected.to_vec(), 0, 0), + "{name} release={release}" + ); + assert_eq!( + interpreter.resource_test_custody_counts().unwrap(), + (0, 0), + "{name} release={release}" + ); + assert!( + interpreter.take_debug_events().is_empty(), + "{name} release={release}" + ); +} + +#[test] +fn reflected_field_source06_exceptional_paths_restore_before_teardown() { + assert_eq!(INPUTS.len(), 9); + for release in [false, true] { + for input in INPUTS { + let case = reference_case(SOURCE, input); + let checked = checked_case(&case, release); + let target = entry(&checked, "main"); + let mut interpreter = reference_with_required_values(&checked, &case, release); + let grant = interpreter + .install_resource_test_script(input.script.iter().copied().map(script).collect()) + .unwrap(); + let context = interpreter.resource_test_entry_context().unwrap(); + let actual = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(target, vec![grant]) + })); + assert_outcome(actual, input.outcome, input.name, release); + let expected = input.events.iter().map(event).collect::>(); + assert_eq!( + interpreter.resource_test_entry_context().unwrap(), + context, + "{} release={release}: exact entry metadata/cursor restored", + input.name + ); + assert_empty_before_teardown(&mut interpreter, &expected, input.name, release); + } + } +} + +#[test] +fn reflected_field_source06_reenters_after_every_exception_with_the_same_provider_grant() { + for release in [false, true] { + for input in INPUTS { + let case = reference_case(SOURCE, input); + let checked = checked_case(&case, release); + let target = entry(&checked, "main"); + let mut interpreter = reference_with_required_values(&checked, &case, release); + let combined = input + .script + .iter() + .chain(READY_SCRIPT.iter()) + .copied() + .map(script) + .collect(); + let grant = interpreter.install_resource_test_script(combined).unwrap(); + let context = interpreter.resource_test_entry_context().unwrap(); + let first = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(target, vec![grant.clone()]) + })); + assert_outcome(first, input.outcome, input.name, release); + let mut expected = input.events.iter().map(event).collect::>(); + assert_eq!( + interpreter.resource_test_entry_context().unwrap(), + context, + "{} release={release}: first exceptional entry restored", + input.name + ); + assert_empty_before_teardown(&mut interpreter, &expected, input.name, release); + + assert_eq!( + interpreter + .call_checked_program_entry(target, vec![grant]) + .unwrap(), + Value::Nothing, + "{} release={release}: same-grant ready reentry", + input.name + ); + expected.extend(READY_EVENTS.iter().map(event)); + assert_eq!( + interpreter.resource_test_entry_context().unwrap(), + context, + "{} release={release}: same-grant ready entry restored", + input.name + ); + assert_empty_before_teardown(&mut interpreter, &expected, input.name, release); + } + } +} + +#[test] +fn reflected_field_resource_aliases_retire_at_break_continue_and_scalar_default() { + assert_eq!(DERIVED_INPUTS.len(), 10); + for release in [false, true] { + for control in DERIVED_INPUTS { + let input = &control.input; + let case = reference_case(control.source, input); + let checked = checked_case(&case, release); + let target = entry(&checked, "main"); + let mut interpreter = reference_with_required_values(&checked, &case, release); + let grant = interpreter + .install_resource_test_script(input.script.iter().copied().map(script).collect()) + .unwrap(); + let context = interpreter.resource_test_entry_context().unwrap(); + let actual = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(target, vec![grant]) + })); + assert_outcome(actual, input.outcome, input.name, release); + assert_eq!( + interpreter.resource_test_entry_context().unwrap(), + context, + "{} release={release}: selected field control restored", + input.name + ); + let expected = input.events.iter().map(event).collect::>(); + assert_empty_before_teardown(&mut interpreter, &expected, input.name, release); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/06_reflected_field_owned_optional_reuse.jett b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/06_reflected_field_owned_optional_reuse.jett new file mode 100644 index 00000000..44c19d0e --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/06_reflected_field_owned_optional_reuse.jett @@ -0,0 +1,24 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/07_reflected_field_optional_break.jett b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/07_reflected_field_optional_break.jett new file mode 100644 index 00000000..b772f6f3 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/07_reflected_field_optional_break.jett @@ -0,0 +1,25 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + break + if total != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/08_reflected_field_optional_continue.jett b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/08_reflected_field_optional_continue.jett new file mode 100644 index 00000000..c747f992 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/08_reflected_field_optional_continue.jett @@ -0,0 +1,26 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + continue + total = total + 1000 + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/09_reflected_field_optional_scalar_default.jett b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/09_reflected_field_optional_scalar_default.jett new file mode 100644 index 00000000..f4880684 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/09_reflected_field_optional_scalar_default.jett @@ -0,0 +1,24 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + default 17 + total = total + observed + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/10_reflected_field_successful_return.jett b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/10_reflected_field_successful_return.jett new file mode 100644 index 00000000..ff7db23c --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/10_reflected_field_successful_return.jett @@ -0,0 +1,25 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 + third: int64 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 881) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + return nothing + if observed == 17: + return nothing + if observed == 17: + int64 skipped_after_return = resource_probe.terminal_label() + if field.index != 0: + int64 skipped_after_body = resource_probe.terminal_label() + int64 skipped_after_loop = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/11_reflected_field_owned_return.jett b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/11_reflected_field_owned_return.jett new file mode 100644 index 00000000..1a6a5662 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/11_reflected_field_owned_return.jett @@ -0,0 +1,36 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 + third: int64 +function return_selected(view net: Network, survivor: resource_probe.TestHandle, outer: resource_probe.TestHandle) returns resource_probe.TestHandle: + use resource_probe + optional[resource_probe.TestHandle] held = some(outer) + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return survivor + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + return survivor + if observed == 17: + return survivor + if observed == 17: + int64 skipped_after_return = resource_probe.terminal_label() + if field.index != 0: + int64 skipped_after_body = resource_probe.terminal_label() + int64 skipped_after_loop = resource_probe.terminal_label() + return survivor +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle outer = resource_probe.create(view net, 891) handle error: + return nothing + resource_probe.TestHandle survivor = resource_probe.create(view net, 892) handle error: + return nothing + resource_probe.TestHandle returned = return_selected(view net, survivor, outer) + int64 observed = resource_probe.borrow(view net, view returned) handle error: + return nothing + if observed != 29: + int64 wrong_return = resource_probe.terminal_label() + resource_probe.close(returned) + return nothing diff --git a/crates/jett_comptime/src/value.rs b/crates/jett_comptime/src/value.rs index 2a6fd0b1..22e8cc56 100644 --- a/crates/jett_comptime/src/value.rs +++ b/crates/jett_comptime/src/value.rs @@ -16,6 +16,8 @@ pub struct ClosureTypeContext { pub checked_function: Option>, pub bindings: HashMap, pub arguments: Vec, + /// Constructor-owned source witness; ordinary closure values need none. + pub(crate) reflected_capture: Option>, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -32,6 +34,14 @@ pub struct ClosureTypeArgument { pub reflection: Option, } +#[path = "value/reflected_callable.rs"] +mod reflected_callable; +pub use reflected_callable::ReflectedCallableValue; +pub(crate) use reflected_callable::original_source::body_may_mint_inline_function; + +#[path = "value/closure_capture.rs"] +pub(crate) mod closure_capture; + /// Runtime value for the compile-time interpreter. #[derive(Debug, Clone)] pub enum Value { @@ -67,6 +77,12 @@ pub enum Value { Nothing, /// An opaque runtime-provided capability handle. Capability(String), + /// Physical carrier only; cleanup custody is never part of Value::Clone. + Resource(crate::resource_execution::ResourceCarrier), + /// Program-bound closed hook descriptor; storing it does not invoke a hook. + ResourceHook(crate::resource_execution::ResourceHookDescriptor), + /// Compiler-installed provenance-bearing authority, never a capability string. + GrantedNetwork(crate::resource_execution::GrantedNetwork), /// An opaque reflected construction builder. TypeConstruction { type_name: String, @@ -106,6 +122,8 @@ pub enum Value { Set(Vec), /// A named function reference retaining its canonical declaration identity. NamedFunction(String), + /// Checked lazy adapter retaining the original evaluated callback. + ReflectedCallable(Box), /// A captured inline function expression (closure). Function { /// Keep generic metadata off the stack: inline storage enlarges every @@ -121,6 +139,49 @@ pub enum Value { } impl Value { + pub(crate) fn contains_live_resource_or_grant(&self) -> bool { + match self { + Self::Resource(_) | Self::GrantedNetwork(_) => true, + Self::Typed { value, .. } + | Self::ResultOk(value) + | Self::ResultFail(value) + | Self::OptionalSome(value) + | Self::Pending(value) => value.contains_live_resource_or_grant(), + Self::List(values) + | Self::Set(values) + | Self::Enum { fields: values, .. } + | Self::Machine { fields: values, .. } => { + values.iter().any(Self::contains_live_resource_or_grant) + } + Self::Struct { fields, .. } => fields + .iter() + .any(|(_, value)| value.contains_live_resource_or_grant()), + Self::Map(values) => values.iter().any(|(key, value)| { + key.contains_live_resource_or_grant() || value.contains_live_resource_or_grant() + }), + Self::TypeConstruction { fields, .. } => fields + .iter() + .any(|(_, _, _, value)| value.contains_live_resource_or_grant()), + Self::ReflectedCallable(value) => value.source().contains_live_resource_or_grant(), + Self::Function { captures, .. } => { + captures.values().any(Self::contains_live_resource_or_grant) + } + Self::Int64(_) + | Self::Uint64(_) + | Self::Float64(_) + | Self::String(_) + | Self::Bool(_) + | Self::Bytes(_) + | Self::OptionalNone + | Self::Nothing + | Self::Capability(_) + | Self::Actor(_) + | Self::Error(_) + | Self::NamedFunction(_) + | Self::ResourceHook(_) => false, + } + } + pub(crate) fn primitive_carrier_name(&self) -> Option<&'static str> { match self { Self::Int64(_) => Some("int64"), @@ -265,6 +326,9 @@ impl fmt::Display for Value { Value::OptionalNone => write!(f, "none"), Value::Nothing => write!(f, "nothing"), Value::Capability(name) => write!(f, "<{name} capability>"), + Value::Resource(_) => f.write_str(""), + Value::ResourceHook(_) => f.write_str(""), + Value::GrantedNetwork(_) => f.write_str(""), Value::TypeConstruction { type_name, variant, @@ -338,6 +402,7 @@ impl fmt::Display for Value { Ok(()) } Value::NamedFunction(name) => write!(f, "function({name})"), + Value::ReflectedCallable(value) => fmt::Display::fmt(value.source(), f), Value::Function { params, .. } => { write!(f, "function(")?; for (i, p) in params.iter().enumerate() { diff --git a/crates/jett_comptime/src/value/closure_capture.rs b/crates/jett_comptime/src/value/closure_capture.rs new file mode 100644 index 00000000..e90d9824 --- /dev/null +++ b/crates/jett_comptime/src/value/closure_capture.rs @@ -0,0 +1,202 @@ +//! Private capture-time proof. Admission cannot mint or reseal this witness. +use std::sync::Arc; + +use jett_common::Span; +use jett_parser::ast::{Block, Expr, FunctionDef}; +use jett_resolve::{DefId, DefKind}; +use jett_typecheck::CheckedResourceProgram; +use jett_types::{Type, TypeId}; + +use super::{Value, reflected_callable::original_source}; + +#[path = "closure_capture/ast_equal.rs"] +mod ast_equal; +#[path = "closure_capture/checked_context.rs"] +mod checked_context; +#[path = "closure_capture/value_equal.rs"] +mod value_equal; + +#[derive(Debug, Clone, Copy)] +enum Owner { + Named(DefId), + Closure(Span), +} + +#[derive(Debug, Clone)] +pub(crate) struct OriginalClosureOwner { + checked: Arc, + owner: Owner, + namespace: Option, + definition: DefId, +} + +impl OriginalClosureOwner { + pub(crate) fn registered( + checked: &Arc, + name: &str, + current: &FunctionDef, + namespace: &Option, + ) -> Option { + let mut candidates = checked + .resolved() + .scope_table + .definitions + .iter() + .filter(|info| { + info.kind == DefKind::Function + && info.name == name + && info.span == current.name.span + && &info.namespace == namespace + && original_source::named_function(checked, info.id) + .is_some_and(|original| ast_equal::function(original, current)) + }); + let selected = candidates.next()?; + if candidates.next().is_some() { + return None; + } + Some(Self { + checked: checked.clone(), + owner: Owner::Named(selected.id), + namespace: namespace.clone(), + definition: selected.id, + }) + } + + fn body(&self) -> Option<&Block> { + match self.owner { + Owner::Named(definition) => original_source::named_function(&self.checked, definition) + .map(|function| &function.body), + Owner::Closure(span) => match original_source::closure_node(&self.checked, span)? { + Expr::InlineFn(_, _, body, _) => Some(body), + _ => None, + }, + } + } +} + +#[derive(Debug)] +pub(crate) struct CheckedClosureCapture { + origin: OriginalClosureOwner, + node_span: Span, + body_span: Span, + source_type: TypeId, + snapshot: Box, +} + +impl CheckedClosureCapture { + pub(crate) fn seal(origin: &OriginalClosureOwner, value: &Value) -> Option> { + let Value::Function { + type_context, + params, + body, + namespace, + .. + } = value + else { + return None; + }; + if namespace != &origin.namespace || type_context.reflected_capture.is_some() { + return None; + } + let original = original_source::closure_node_in_body(origin.body()?, body.span)?; + let global = original_source::closure_node(&origin.checked, body.span)?; + if !std::ptr::eq(original, global) { + return None; + } + let Expr::InlineFn(original_params, original_return, original_body, node_span) = original + else { + return None; + }; + if !ast_equal::params(params, original_params) + || !ast_equal::optional( + &type_context.return_type, + original_return, + ast_equal::type_expr, + ) + || !ast_equal::block(body, original_body) + { + return None; + } + let source_type = checked_context::callable_type(origin, type_context, *node_span)?; + if !matches!( + origin.checked.checked().interner.resolve(source_type), + Type::Function { .. } + ) { + return None; + } + if value.contains_live_resource_or_grant() { + return None; + } + Some(Arc::new(Self { + origin: origin.clone(), + node_span: *node_span, + body_span: body.span, + source_type, + snapshot: Box::new(value.clone()), + })) + } + + pub(crate) fn validate_source( + &self, + checked: &Arc, + actual: TypeId, + value: &Value, + ) -> bool { + Arc::ptr_eq(&self.origin.checked, checked) + && actual == self.source_type + && matches!(value, Value::Function { type_context, .. } if checked_context::callable_type(&self.origin, type_context, self.node_span) == Some(actual)) + && original_source::closure_node(checked, self.body_span).is_some_and(|node| { + let Expr::InlineFn(params, result, body, span) = node else { + return false; + }; + let Value::Function { + type_context, + params: current, + body: current_body, + .. + } = value + else { + return false; + }; + *span == self.node_span + && ast_equal::params(params, current) + && ast_equal::optional(result, &type_context.return_type, ast_equal::type_expr) + && ast_equal::block(body, current_body) + }) + && value_equal::closure(&self.snapshot, value, false) + } +} + +pub(crate) fn invocation_origin( + value: &Value, + checked: &Arc, +) -> Option { + let Value::Function { type_context, .. } = value.payload() else { + return None; + }; + let proof = type_context.reflected_capture.as_ref()?; + proof + .validate_source(checked, proof.source_type, value.payload()) + .then(|| OriginalClosureOwner { + checked: checked.clone(), + owner: Owner::Closure(proof.body_span), + namespace: proof.origin.namespace.clone(), + definition: proof.origin.definition, + }) +} + +pub(crate) fn same_snapshot(a: &Value, b: &Value) -> bool { + value_equal::value(a, b) +} +pub(crate) fn same_type_expr( + a: &jett_parser::ast::TypeExpr, + b: &jett_parser::ast::TypeExpr, +) -> bool { + ast_equal::type_expr(a, b) +} +pub(crate) fn original_root_types( + checked: &CheckedResourceProgram, + installed: &crate::checked_types::CheckedExpressionTypes, +) -> bool { + checked_context::root_matches(checked, installed) +} diff --git a/crates/jett_comptime/src/value/closure_capture/ast_equal.rs b/crates/jett_comptime/src/value/closure_capture/ast_equal.rs new file mode 100644 index 00000000..9d2d1fc5 --- /dev/null +++ b/crates/jett_comptime/src/value/closure_capture/ast_equal.rs @@ -0,0 +1,212 @@ +//! Typed source comparison for producer custody; no display/hash authority. +use jett_parser::ast::*; + +pub(super) fn list(a: &[T], b: &[T], mut same: impl FnMut(&T, &T) -> bool) -> bool { + a.len() == b.len() && a.iter().zip(b).all(|(a, b)| same(a, b)) +} +pub(super) fn optional(a: &Option, b: &Option, same: impl FnOnce(&T, &T) -> bool) -> bool { + match (a, b) { + (Some(a), Some(b)) => same(a, b), + (None, None) => true, + _ => false, + } +} +fn ident(a: &Ident, b: &Ident) -> bool { + a.name == b.name && a.span == b.span +} +pub(super) fn type_expr(a: &TypeExpr, b: &TypeExpr) -> bool { + if a.span() != b.span() { + return false; + } + match (a, b) { + (TypeExpr::Named(a), TypeExpr::Named(b)) => ident(a, b), + (TypeExpr::Generic(an, aa, _), TypeExpr::Generic(bn, ba, _)) => { + ident(an, bn) && list(aa, ba, type_expr) + } + (TypeExpr::View(a, _), TypeExpr::View(b, _)) => type_expr(a, b), + (TypeExpr::Function(ap, ar, _), TypeExpr::Function(bp, br, _)) => { + list(ap, bp, type_expr) && type_expr(ar, br) + } + (TypeExpr::StateQualified(a, an, _), TypeExpr::StateQualified(b, bn, _)) => { + type_expr(a, b) && ident(an, bn) + } + _ => false, + } +} +pub(super) fn params(a: &[Param], b: &[Param]) -> bool { + list(a, b, |a, b| { + a.view == b.view + && a.mutable == b.mutable + && a.span == b.span + && ident(&a.name, &b.name) + && type_expr(&a.ty, &b.ty) + }) +} +pub(super) fn function(a: &FunctionDef, b: &FunctionDef) -> bool { + a.span == b.span + && a.exported == b.exported + && ident(&a.name, &b.name) + && list(&a.type_params, &b.type_params, ident) + && params(&a.params, &b.params) + && optional(&a.return_type, &b.return_type, type_expr) + && block(&a.body, &b.body) +} +pub(super) fn block(a: &Block, b: &Block) -> bool { + a.span == b.span && list(&a.stmts, &b.stmts, stmt) +} +fn pattern(a: &Pattern, b: &Pattern) -> bool { + match (a, b) { + (Pattern::Ident(a), Pattern::Ident(b)) => ident(a, b), + (Pattern::Variant(an, aa), Pattern::Variant(bn, ba)) => { + ident(an, bn) && list(aa, ba, ident) + } + (Pattern::Other(a), Pattern::Other(b)) => a == b, + _ => false, + } +} +fn stmt(a: &Stmt, b: &Stmt) -> bool { + match (a, b) { + (Stmt::VarDecl(a), Stmt::VarDecl(b)) => { + a.span == b.span + && a.mutable == b.mutable + && type_expr(&a.ty, &b.ty) + && ident(&a.name, &b.name) + && expr(&a.value, &b.value) + } + (Stmt::Assign(a), Stmt::Assign(b)) => { + a.span == b.span && expr(&a.target, &b.target) && expr(&a.value, &b.value) + } + (Stmt::Return(a), Stmt::Return(b)) => { + a.span == b.span && optional(&a.value, &b.value, expr) + } + (Stmt::Respond(a), Stmt::Respond(b)) => a.span == b.span && expr(&a.value, &b.value), + (Stmt::ComptimeTypeBind(a), Stmt::ComptimeTypeBind(b)) => { + a.span == b.span + && ident(&a.name, &b.name) + && expr(&a.value, &b.value) + && block(&a.body, &b.body) + } + (Stmt::If(a), Stmt::If(b)) => { + a.span == b.span + && expr(&a.condition, &b.condition) + && block(&a.then_block, &b.then_block) + && list(&a.else_ifs, &b.else_ifs, |(ac, ab), (bc, bb)| { + expr(ac, bc) && block(ab, bb) + }) + && optional(&a.else_block, &b.else_block, block) + } + (Stmt::For(a), Stmt::For(b)) => { + a.span == b.span + && a.view == b.view + && ident(&a.variable, &b.variable) + && optional(&a.value_variable, &b.value_variable, ident) + && expr(&a.iterable, &b.iterable) + && block(&a.body, &b.body) + } + (Stmt::While(a), Stmt::While(b)) => { + a.span == b.span && expr(&a.condition, &b.condition) && block(&a.body, &b.body) + } + (Stmt::Match(a), Stmt::Match(b)) => { + a.span == b.span + && expr(&a.expr, &b.expr) + && list(&a.arms, &b.arms, |a, b| { + a.span == b.span && pattern(&a.pattern, &b.pattern) && block(&a.body, &b.body) + }) + } + (Stmt::Expr(a), Stmt::Expr(b)) => a.span == b.span && expr(&a.expr, &b.expr), + (Stmt::Use(a), Stmt::Use(b)) => { + a.span == b.span && ident(&a.path, &b.path) && optional(&a.alias, &b.alias, ident) + } + (Stmt::Assert(a), Stmt::Assert(b)) => { + a.span == b.span + && expr(&a.condition, &b.condition) + && optional(&a.message, &b.message, expr) + } + (Stmt::Trace(a), Stmt::Trace(b)) => a.span == b.span && ident(&a.name, &b.name), + (Stmt::Breakpoint(a), Stmt::Breakpoint(b)) => { + a.span == b.span && optional(&a.condition, &b.condition, expr) + } + (Stmt::Break(a), Stmt::Break(b)) | (Stmt::Continue(a), Stmt::Continue(b)) => a == b, + _ => false, + } +} +fn args(a: &[CallArg], b: &[CallArg]) -> bool { + list(a, b, |a, b| { + a.span == b.span && optional(&a.name, &b.name, ident) && expr(&a.value, &b.value) + }) +} +pub(super) fn expr(a: &Expr, b: &Expr) -> bool { + if a.span() != b.span() { + return false; + } + match (a, b) { + (Expr::IntLiteral(a, _), Expr::IntLiteral(b, _)) => a == b, + (Expr::FloatLiteral(a, _), Expr::FloatLiteral(b, _)) => a.to_bits() == b.to_bits(), + (Expr::StringLiteral(a, _), Expr::StringLiteral(b, _)) => a == b, + (Expr::BoolLiteral(a, _), Expr::BoolLiteral(b, _)) => a == b, + (Expr::Nothing(_), Expr::Nothing(_)) + | (Expr::None(_), Expr::None(_)) + | (Expr::Error(_), Expr::Error(_)) => true, + (Expr::Ident(a), Expr::Ident(b)) => ident(a, b), + (Expr::Binary(al, ao, ar, _), Expr::Binary(bl, bo, br, _)) => { + ao == bo && expr(al, bl) && expr(ar, br) + } + (Expr::Unary(ao, a, _), Expr::Unary(bo, b, _)) => ao == bo && expr(a, b), + (Expr::FieldAccess(a, an, _), Expr::FieldAccess(b, bn, _)) + | (Expr::At(a, an, _), Expr::At(b, bn, _)) => expr(a, b) && ident(an, bn), + (Expr::Call(a, aa, _), Expr::Call(b, ba, _)) => expr(a, b) && args(aa, ba), + (Expr::GenericCall(a, at, aa, _), Expr::GenericCall(b, bt, ba, _)) => { + expr(a, b) && list(at, bt, type_expr) && args(aa, ba) + } + (Expr::Paren(a, _), Expr::Paren(b, _)) + | (Expr::View(a, _), Expr::View(b, _)) + | (Expr::Comptime(a, _), Expr::Comptime(b, _)) + | (Expr::Ok(a, _), Expr::Ok(b, _)) + | (Expr::Fail(a, _), Expr::Fail(b, _)) + | (Expr::Some(a, _), Expr::Some(b, _)) + | (Expr::Default(a, _), Expr::Default(b, _)) + | (Expr::Declassify(a, _), Expr::Declassify(b, _)) + | (Expr::Coarsen(a, _), Expr::Coarsen(b, _)) + | (Expr::Spawn(a, _), Expr::Spawn(b, _)) + | (Expr::Send(a, _), Expr::Send(b, _)) + | (Expr::Ask(a, _), Expr::Ask(b, _)) + | (Expr::Clone(a, _), Expr::Clone(b, _)) + | (Expr::Run(a, _), Expr::Run(b, _)) + | (Expr::Join(a, _), Expr::Join(b, _)) + | (Expr::Cancel(a, _), Expr::Cancel(b, _)) => expr(a, b), + (Expr::ListConstruct(a, _), Expr::ListConstruct(b, _)) => list(a, b, expr), + (Expr::MapConstruct(a, _), Expr::MapConstruct(b, _)) => { + list(a, b, |(ak, av), (bk, bv)| expr(ak, bk) && expr(av, bv)) + } + (Expr::Handle(a, an, ab, _), Expr::Handle(b, bn, bb, _)) => { + expr(a, b) && optional(an, bn, ident) && block(ab, bb) + } + (Expr::EnumVariant(at, av, _), Expr::EnumVariant(bt, bv, _)) => { + ident(at, bt) && ident(av, bv) + } + (Expr::StringInterpolation(a, _), Expr::StringInterpolation(b, _)) => { + list(a, b, |a, b| match (a, b) { + (StringPart::Literal(a), StringPart::Literal(b)) => a == b, + (StringPart::Expr(a), StringPart::Expr(b)) => expr(a, b), + _ => false, + }) + } + (Expr::Pipeline(a, aa, _), Expr::Pipeline(b, ba, _)) => { + expr(a, b) + && list(aa, ba, |a, b| { + a.span == b.span + && expr(&a.function, &b.function) + && args(&a.extra_args, &b.extra_args) + && optional(&a.handle, &b.handle, |a, b| { + a.span == b.span + && optional(&a.error_name, &b.error_name, ident) + && block(&a.body, &b.body) + }) + }) + } + (Expr::InlineFn(ap, ar, ab, _), Expr::InlineFn(bp, br, bb, _)) => { + params(ap, bp) && optional(ar, br, type_expr) && block(ab, bb) + } + _ => false, + } +} diff --git a/crates/jett_comptime/src/value/closure_capture/checked_context.rs b/crates/jett_comptime/src/value/closure_capture/checked_context.rs new file mode 100644 index 00000000..ea6c54f9 --- /dev/null +++ b/crates/jett_comptime/src/value/closure_capture/checked_context.rs @@ -0,0 +1,198 @@ +//! Join interpreter projections to complete immutable checker facts. +use super::OriginalClosureOwner; +use crate::checked_types::{ + CheckedExpressionTypes, CheckedFunctionTypes, CheckedScopedBindings, CheckedScopedTypes, +}; +use crate::value::ClosureTypeContext; +use jett_common::Span; +use jett_typecheck::{CheckedComptimeTypeBinding, CheckedGenericFunctionInstantiation}; +use jett_types::{TypeId, TypeInterner}; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; + +fn remove_scoped( + names: &mut HashMap, + bindings: &HashMap>, +) { + for binding in bindings.values().flatten() { + for span in binding.body.type_map.keys() { + names.remove(span); + } + remove_scoped(names, &binding.body.comptime_type_bindings); + } +} +fn projection( + types: &HashMap, + bindings: &HashMap>, + interner: &TypeInterner, +) -> (HashMap, CheckedScopedBindings) { + let mut names = types + .iter() + .map(|(span, ty)| (*span, interner.type_name(*ty))) + .collect(); + remove_scoped(&mut names, bindings); + let nested: HashSet<_> = bindings + .values() + .flatten() + .flat_map(|binding| binding.body.comptime_type_bindings.keys().copied()) + .collect(); + let scopes = bindings + .iter() + .filter(|(span, _)| !nested.contains(span)) + .map(|(span, candidates)| { + ( + *span, + candidates + .iter() + .map(|binding| { + let (expressions, bindings) = projection( + &binding.body.type_map, + &binding.body.comptime_type_bindings, + interner, + ); + Arc::new(CheckedScopedTypes { + bound_type: interner.type_name(binding.bound_type), + reflection: Some(binding.reflection.clone()), + expressions, + bindings, + }) + }) + .collect(), + ) + }) + .collect(); + (names, scopes) +} +pub(super) fn root_matches( + checked: &jett_typecheck::CheckedResourceProgram, + installed: &CheckedExpressionTypes, +) -> bool { + let checked = checked.checked(); + let (mut expressions, bindings) = projection( + &checked.type_map, + &checked.comptime_type_bindings, + &checked.interner, + ); + for instance in &checked.generic_function_instantiations { + for span in instance.type_map.keys() { + expressions.remove(span); + } + remove_scoped(&mut expressions, &instance.comptime_type_bindings); + } + installed.expressions == expressions && installed.bindings == bindings +} +fn function_matches( + current: &CheckedFunctionTypes, + context: &ClosureTypeContext, + original: &CheckedGenericFunctionInstantiation, + interner: &TypeInterner, +) -> bool { + let (names, bindings) = projection( + &original.type_map, + &original.comptime_type_bindings, + interner, + ); + let args: Vec<_> = original + .concrete_args + .iter() + .map(|ty| interner.type_name(*ty)) + .collect(); + let facts = &original.specialization; + current.expressions.as_ref() == &names + && current.bindings == bindings + && current.type_arguments == args + && current.type_argument_reflections == facts.type_argument_reflections + && current.type_info_kinds == facts.type_info_kinds + && current.type_info_primitives == facts.type_info_primitives + && current.type_kind_values == facts.type_kind_values + && current.type_primitive_values == facts.type_primitive_values + && context.arguments.len() == args.len() + && context + .arguments + .iter() + .zip(&args) + .all(|(actual, expected)| &actual.canonical_name == expected) + && (facts.type_argument_reflections.is_empty() + || context + .arguments + .iter() + .zip(&facts.type_argument_reflections) + .all(|(actual, expected)| actual.reflection.as_ref() == Some(expected))) +} +fn scoped_types( + current: &CheckedScopedTypes, + span: Span, + bindings: &HashMap>, + interner: &TypeInterner, + found: &mut Vec, +) { + for original in bindings.values().flatten() { + let (expressions, bindings) = projection( + &original.body.type_map, + &original.body.comptime_type_bindings, + interner, + ); + let expected = CheckedScopedTypes { + bound_type: interner.type_name(original.bound_type), + reflection: Some(original.reflection.clone()), + expressions, + bindings, + }; + if current == &expected { + if let Some(ty) = original.body.type_map.get(&span) { + found.push(*ty); + } + } + scoped_types( + current, + span, + &original.body.comptime_type_bindings, + interner, + found, + ); + } +} +pub(super) fn callable_type( + origin: &OriginalClosureOwner, + context: &ClosureTypeContext, + span: Span, +) -> Option { + let checked = origin.checked.checked(); + let mut found = Vec::new(); + if let Some(current) = &context.checked_function { + for original in &checked.generic_function_instantiations { + if original.definition != origin.definition + || !function_matches(current, context, original, &checked.interner) + { + continue; + } + if let Some(scope) = &context.checked_scope { + scoped_types( + scope, + span, + &original.comptime_type_bindings, + &checked.interner, + &mut found, + ); + } else if let Some(ty) = original.type_map.get(&span) { + found.push(*ty); + } + } + } else if context.arguments.is_empty() { + if let Some(scope) = &context.checked_scope { + scoped_types( + scope, + span, + &checked.comptime_type_bindings, + &checked.interner, + &mut found, + ); + } else if let Some(ty) = checked.type_map.get(&span) { + found.push(*ty); + } + } + let selected = *found.first()?; + found.iter().all(|ty| *ty == selected).then_some(selected) +} diff --git a/crates/jett_comptime/src/value/closure_capture/value_equal.rs b/crates/jett_comptime/src/value/closure_capture/value_equal.rs new file mode 100644 index 00000000..aff26813 --- /dev/null +++ b/crates/jett_comptime/src/value/closure_capture/value_equal.rs @@ -0,0 +1,163 @@ +//! Snapshot comparison retains tags, pending layers, and callable contexts. +use super::{Value, ast_equal}; +use crate::value::ClosureTypeContext; +use jett_parser::ast::TypeExpr; +use std::{collections::HashMap, sync::Arc}; + +fn map(a: &HashMap, b: &HashMap, same: impl Fn(&T, &T) -> bool) -> bool { + a.len() == b.len() + && a.iter() + .all(|(key, a)| b.get(key).is_some_and(|b| same(a, b))) +} +fn shared(a: &Option>, b: &Option>) -> bool { + ast_equal::optional(a, b, Arc::ptr_eq) +} +fn context(a: &ClosureTypeContext, b: &ClosureTypeContext, proof: bool) -> bool { + (!proof || shared(&a.reflected_capture, &b.reflected_capture)) + && ast_equal::optional(&a.return_type, &b.return_type, ast_equal::type_expr) + && shared(&a.checked_scope, &b.checked_scope) + && shared(&a.checked_function, &b.checked_function) + && a.scoped_type_bindings == b.scoped_type_bindings + && map(&a.bindings, &b.bindings, ast_equal::type_expr) + && ast_equal::list(&a.arguments, &b.arguments, |a, b| { + a.canonical_name == b.canonical_name + && a.reflection == b.reflection + && ast_equal::type_expr(&a.ty, &b.ty) + }) +} +pub(super) fn closure(a: &Value, b: &Value, proof: bool) -> bool { + let ( + Value::Function { + type_context: ac, + params: ap, + body: ab, + captures: av, + capture_types: at, + namespace_aliases: aa, + namespace: an, + }, + Value::Function { + type_context: bc, + params: bp, + body: bb, + captures: bv, + capture_types: bt, + namespace_aliases: ba, + namespace: bn, + }, + ) = (a, b) + else { + return false; + }; + context(ac, bc, proof) + && ast_equal::params(ap, bp) + && ast_equal::block(ab, bb) + && map(av, bv, value) + && map::(at, bt, ast_equal::type_expr) + && aa == ba + && an == bn +} +pub(super) fn value(a: &Value, b: &Value) -> bool { + match (a, b) { + ( + Value::Typed { + type_name: an, + value: a, + }, + Value::Typed { + type_name: bn, + value: b, + }, + ) => an == bn && value(a, b), + (Value::Int64(a), Value::Int64(b)) => a == b, + (Value::Uint64(a), Value::Uint64(b)) => a == b, + (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(), + (Value::String(a), Value::String(b)) + | (Value::Capability(a), Value::Capability(b)) + | (Value::Error(a), Value::Error(b)) + | (Value::NamedFunction(a), Value::NamedFunction(b)) => a == b, + (Value::Bool(a), Value::Bool(b)) => a == b, + (Value::Bytes(a), Value::Bytes(b)) => a == b, + (Value::Actor(a), Value::Actor(b)) => a == b, + (Value::OptionalNone, Value::OptionalNone) | (Value::Nothing, Value::Nothing) => true, + (Value::ResultOk(a), Value::ResultOk(b)) + | (Value::ResultFail(a), Value::ResultFail(b)) + | (Value::OptionalSome(a), Value::OptionalSome(b)) + | (Value::Pending(a), Value::Pending(b)) => value(a, b), + (Value::List(a), Value::List(b)) | (Value::Set(a), Value::Set(b)) => { + ast_equal::list(a, b, value) + } + (Value::Map(a), Value::Map(b)) => { + ast_equal::list(a, b, |(ak, av), (bk, bv)| value(ak, bk) && value(av, bv)) + } + ( + Value::TypeConstruction { + type_name: an, + variant: ar, + state: as_, + fields: a, + }, + Value::TypeConstruction { + type_name: bn, + variant: br, + state: bs, + fields: b, + }, + ) => { + an == bn + && ar == br + && as_ == bs + && ast_equal::list(a, b, |(ai, an, at, av), (bi, bn, bt, bv)| { + ai == bi && an == bn && at == bt && value(av, bv) + }) + } + ( + Value::Struct { + type_name: an, + concrete_type: ac, + fields: a, + }, + Value::Struct { + type_name: bn, + concrete_type: bc, + fields: b, + }, + ) => { + an == bn + && ac == bc + && ast_equal::list(a, b, |(an, av), (bn, bv)| an == bn && value(av, bv)) + } + ( + Value::Enum { + type_name: an, + variant: ar, + fields: a, + }, + Value::Enum { + type_name: bn, + variant: br, + fields: b, + }, + ) + | ( + Value::Machine { + type_name: an, + state: ar, + fields: a, + }, + Value::Machine { + type_name: bn, + state: br, + fields: b, + }, + ) => an == bn && ar == br && ast_equal::list(a, b, value), + (Value::ResourceHook(a), Value::ResourceHook(b)) => a.same_descriptor(b), + (Value::ReflectedCallable(a), Value::ReflectedCallable(b)) => a.same_snapshot(b), + (Value::Function { .. }, Value::Function { .. }) => closure(a, b, true), + // Capture construction already refuses live custody; never copy or + // inspect it to manufacture equality for this unrelated capability. + (Value::Resource(_), Value::Resource(_)) + | (Value::GrantedNetwork(_), Value::GrantedNetwork(_)) => false, + _ => false, + } +} diff --git a/crates/jett_comptime/src/value/reflected_callable.rs b/crates/jett_comptime/src/value/reflected_callable.rs new file mode 100644 index 00000000..ba352580 --- /dev/null +++ b/crates/jett_comptime/src/value/reflected_callable.rs @@ -0,0 +1,201 @@ +//! Owned lazy callback conversion. No public constructor can relabel a callable. +use std::sync::Arc; + +use jett_common::Span; +use jett_parser::ast::{Block, TypeExpr}; +use jett_resolve::{DefId, DefKind}; +use jett_typecheck::{CheckedReflectedCallablePlan, CheckedResourceProgram}; +use jett_types::TypeId; + +use super::Value; + +#[path = "reflected_callable_source.rs"] +pub(crate) mod original_source; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SourceIdentity { + Named(DefId), + Closure(Span), +} + +#[derive(Clone)] +pub struct ReflectedCallableValue { + source: Box, + checked: Arc, + plan: CheckedReflectedCallablePlan, + identity: SourceIdentity, + source_return: TypeExpr, + requested_return: TypeExpr, +} + +impl std::fmt::Debug for ReflectedCallableValue { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("ReflectedCallableValue") + .field("source_label", &self.source.to_string()) + .field("source_type", &self.source_type()) + .field("requested_type", &self.requested_type()) + .finish_non_exhaustive() + } +} + +impl ReflectedCallableValue { + pub(crate) fn new( + source: Value, + checked: Arc, + actual: TypeId, + requested: TypeId, + source_return: TypeExpr, + requested_return: TypeExpr, + ) -> Result { + let plan = checked + .checked() + .reflected_callable_plan(actual, requested) + .ok_or_else(|| { + "reflected callback has no checked return conversion plan".to_string() + })?; + let identity = Self::source_identity(&source, &checked, actual)?; + Ok(Self { + source: Box::new(source), + checked, + plan, + identity, + source_return, + requested_return, + }) + } + + pub(crate) fn authenticate_source( + source: &Value, + checked: &Arc, + actual: TypeId, + ) -> Result<(), String> { + Self::source_identity(source, checked, actual).map(|_| ()) + } + + fn source_identity( + source: &Value, + checked: &Arc, + actual: TypeId, + ) -> Result { + if source.contains_live_resource_or_grant() { + return Err("reflected callback cannot copy live Resource custody".into()); + } + if !matches!( + source.payload(), + Value::NamedFunction(_) + | Value::Function { .. } + | Value::ReflectedCallable(_) + | Value::Pending(_) + ) { + return Err("reflected callback source is not a callable descriptor".into()); + } + fn descriptor(value: &Value) -> &Value { + // Retain descriptor identity without joining or invoking it. This + // never observes a pending callback result or establishes a proof. + match value.payload() { + Value::Pending(inner) => descriptor(inner), + value => value, + } + } + let identity = match descriptor(source) { + Value::NamedFunction(name) => { + let mut definitions = + checked + .resolved() + .scope_table + .definitions + .iter() + .filter(|info| { + info.kind == DefKind::Function + && info.name == *name + && checked.checked().definition_types.get(&info.id) == Some(&actual) + && original_source::named_body(&checked, info.id).is_some() + }); + let selected = definitions + .next() + .ok_or("reflected callback has no exact original source declaration")?; + if definitions.next().is_some() { + return Err( + "reflected callback original source declaration is ambiguous".into(), + ); + } + SourceIdentity::Named(selected.id) + } + Value::Function { + type_context, body, .. + } => { + if !type_context + .reflected_capture + .as_ref() + .is_some_and(|proof| proof.validate_source(checked, actual, descriptor(source))) + { + return Err( + "reflected callback has no unchanged original closure capture".into(), + ); + } + SourceIdentity::Closure(body.span) + } + Value::ReflectedCallable(admitted) + if Arc::ptr_eq(&checked, admitted.checked_program()) + && admitted.requested_type() == actual => + { + admitted.identity + } + _ => { + return Err( + "reflected callback source belongs to another checked callable context".into(), + ); + } + }; + Ok(identity) + } + + pub fn source(&self) -> &Value { + &self.source + } + pub fn source_type(&self) -> TypeId { + self.plan.source_type() + } + pub fn requested_type(&self) -> TypeId { + self.plan.requested_type() + } + pub fn checked_program(&self) -> &Arc { + &self.checked + } + pub fn checked_plan(&self) -> &CheckedReflectedCallablePlan { + &self.plan + } + pub fn source_definition(&self) -> Option { + match self.identity { + SourceIdentity::Named(definition) => Some(definition), + _ => None, + } + } + pub fn source_body(&self) -> Option<&Block> { + match self.identity { + SourceIdentity::Named(definition) => { + original_source::named_body(&self.checked, definition) + } + SourceIdentity::Closure(span) => original_source::closure_body(&self.checked, span), + } + } + pub(crate) fn source_return(&self) -> &TypeExpr { + &self.source_return + } + pub(crate) fn requested_return(&self) -> &TypeExpr { + &self.requested_return + } + + pub(crate) fn same_snapshot(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.checked, &other.checked) + && self.plan == other.plan + && self.identity == other.identity + && super::closure_capture::same_type_expr(&self.source_return, &other.source_return) + && super::closure_capture::same_type_expr( + &self.requested_return, + &other.requested_return, + ) + && super::closure_capture::same_snapshot(&self.source, &other.source) + } +} diff --git a/crates/jett_comptime/src/value/reflected_callable_source.rs b/crates/jett_comptime/src/value/reflected_callable_source.rs new file mode 100644 index 00000000..6ae0d72b --- /dev/null +++ b/crates/jett_comptime/src/value/reflected_callable_source.rs @@ -0,0 +1,300 @@ +//! Exact original Source body lookup; names choose a resolved declaration only. +use jett_parser::ast::{Block, ComptimeTypeBindStmt, Expr, FunctionDef, Item, Stmt, StringPart}; +use jett_resolve::{DefId, DefKind}; +use jett_typecheck::CheckedResourceProgram; + +pub(super) fn named_body(program: &CheckedResourceProgram, definition: DefId) -> Option<&Block> { + named_function(program, definition).map(|function| &function.body) +} + +pub(crate) fn named_function( + program: &CheckedResourceProgram, + definition: DefId, +) -> Option<&FunctionDef> { + let info = program + .resolved() + .scope_table + .definitions + .get(definition.index() as usize)?; + if info.id != definition || info.kind != DefKind::Function { + return None; + } + let mut matches = Vec::new(); + for item in &program.module().items { + match item { + Item::Function(function) if function.name.span == info.span => matches.push(function), + Item::Implement(block) => { + for method in &block.methods { + if method.name.span == info.span { + matches.push(method); + } + } + } + _ => {} + } + } + match matches.as_slice() { + [function] => Some(*function), + _ => None, + } +} + +pub(super) fn closure_body<'a>( + program: &'a CheckedResourceProgram, + span: jett_common::Span, +) -> Option<&'a Block> { + match closure_node(program, span)? { + Expr::InlineFn(_, _, body, _) => Some(body), + _ => None, + } +} + +pub(crate) fn closure_node<'a>( + program: &'a CheckedResourceProgram, + span: jett_common::Span, +) -> Option<&'a Expr> { + let mut matches = Vec::new(); + let mut expression = |value: &'a Expr| { + if let Expr::InlineFn(_, _, body, _) = value { + if body.span == span { + matches.push(value); + } + } + }; + for item in &program.module().items { + match item { + Item::Function(function) => { + walk_block_nodes(&function.body, &mut |_| {}, &mut expression, &mut |_| {}) + } + Item::Implement(block) => { + for method in &block.methods { + walk_block_nodes(&method.body, &mut |_| {}, &mut expression, &mut |_| {}); + } + } + Item::Verify(block) => { + walk_block_nodes(&block.body, &mut |_| {}, &mut expression, &mut |_| {}) + } + Item::Property(block) => { + walk_block_nodes(&block.body, &mut |_| {}, &mut expression, &mut |_| {}) + } + Item::VarDecl(declaration) => walk_expr_nodes( + &declaration.value, + &mut |_| {}, + &mut expression, + &mut |_| {}, + ), + Item::Actor(actor) => { + for state in &actor.state_fields { + walk_expr_nodes(&state.value, &mut |_| {}, &mut expression, &mut |_| {}); + } + for handler in &actor.handlers { + walk_block_nodes(&handler.body, &mut |_| {}, &mut expression, &mut |_| {}); + } + } + _ => {} + } + } + match matches.as_slice() { + [node] => Some(*node), + _ => None, + } +} + +pub(crate) fn closure_node_in_body<'a>( + body: &'a Block, + span: jett_common::Span, +) -> Option<&'a Expr> { + let mut matches = Vec::new(); + walk_block_nodes( + body, + &mut |_| {}, + &mut |value| { + if let Expr::InlineFn(_, _, body, _) = value { + if body.span == span { + matches.push(value); + } + } + }, + &mut |_| {}, + ); + match matches.as_slice() { + [node] => Some(*node), + _ => None, + } +} + +/// A negative result proves this body cannot directly construct an inline value. +/// Required materialization and scoped type binding remain conservative positives; +/// a dynamic factory call installs its own origin at its separate call boundary. +pub(crate) fn body_may_mint_inline_function(body: &Block) -> bool { + let mut found_expression = false; + let mut found_binding = false; + walk_block_nodes( + body, + &mut |_| {}, + &mut |expression| { + found_expression |= matches!( + expression, + Expr::InlineFn(..) | Expr::Comptime(..) | Expr::Error(..) + ); + }, + &mut |statement| { + found_binding |= matches!(statement, Stmt::ComptimeTypeBind(_)); + }, + ); + found_expression || found_binding +} + +fn walk_block_nodes<'a>( + block: &'a Block, + binding: &mut impl FnMut(&'a ComptimeTypeBindStmt), + expression: &mut impl FnMut(&'a Expr), + statement: &mut impl FnMut(&'a Stmt), +) { + for node in &block.stmts { + statement(node); + match node { + Stmt::VarDecl(value) => walk_expr_nodes(&value.value, binding, expression, statement), + Stmt::Assign(value) => { + walk_expr_nodes(&value.target, binding, expression, statement); + walk_expr_nodes(&value.value, binding, expression, statement); + } + Stmt::Return(value) => { + if let Some(value) = &value.value { + walk_expr_nodes(value, binding, expression, statement); + } + } + Stmt::Respond(value) => walk_expr_nodes(&value.value, binding, expression, statement), + Stmt::ComptimeTypeBind(value) => { + binding(value); + walk_expr_nodes(&value.value, binding, expression, statement); + walk_block_nodes(&value.body, binding, expression, statement); + } + Stmt::If(value) => { + walk_expr_nodes(&value.condition, binding, expression, statement); + walk_block_nodes(&value.then_block, binding, expression, statement); + for (condition, block) in &value.else_ifs { + walk_expr_nodes(condition, binding, expression, statement); + walk_block_nodes(block, binding, expression, statement); + } + if let Some(block) = &value.else_block { + walk_block_nodes(block, binding, expression, statement); + } + } + Stmt::For(value) => { + walk_expr_nodes(&value.iterable, binding, expression, statement); + walk_block_nodes(&value.body, binding, expression, statement); + } + Stmt::While(value) => { + walk_expr_nodes(&value.condition, binding, expression, statement); + walk_block_nodes(&value.body, binding, expression, statement); + } + Stmt::Match(value) => { + walk_expr_nodes(&value.expr, binding, expression, statement); + for arm in &value.arms { + walk_block_nodes(&arm.body, binding, expression, statement); + } + } + Stmt::Expr(value) => walk_expr_nodes(&value.expr, binding, expression, statement), + Stmt::Assert(value) => { + walk_expr_nodes(&value.condition, binding, expression, statement); + if let Some(message) = &value.message { + walk_expr_nodes(message, binding, expression, statement); + } + } + Stmt::Breakpoint(value) => { + if let Some(condition) = &value.condition { + walk_expr_nodes(condition, binding, expression, statement); + } + } + Stmt::Use(_) | Stmt::Trace(_) | Stmt::Break(_) | Stmt::Continue(_) => {} + } + } +} + +fn walk_expr_nodes<'a>( + value: &'a Expr, + binding: &mut impl FnMut(&'a ComptimeTypeBindStmt), + expression: &mut impl FnMut(&'a Expr), + statement: &mut impl FnMut(&'a Stmt), +) { + expression(value); + match value { + Expr::Binary(left, _, right, _) => { + walk_expr_nodes(left, binding, expression, statement); + walk_expr_nodes(right, binding, expression, statement); + } + Expr::Unary(_, value, _) + | Expr::FieldAccess(value, _, _) + | Expr::Paren(value, _) + | Expr::View(value, _) + | Expr::Comptime(value, _) + | Expr::Ok(value, _) + | Expr::Fail(value, _) + | Expr::Some(value, _) + | Expr::Default(value, _) + | Expr::Declassify(value, _) + | Expr::Coarsen(value, _) + | Expr::At(value, _, _) + | Expr::Spawn(value, _) + | Expr::Send(value, _) + | Expr::Ask(value, _) + | Expr::Clone(value, _) + | Expr::Run(value, _) + | Expr::Join(value, _) + | Expr::Cancel(value, _) => walk_expr_nodes(value, binding, expression, statement), + Expr::Call(callee, arguments, _) | Expr::GenericCall(callee, _, arguments, _) => { + walk_expr_nodes(callee, binding, expression, statement); + for argument in arguments { + walk_expr_nodes(&argument.value, binding, expression, statement); + } + } + Expr::ListConstruct(values, _) => { + for value in values { + walk_expr_nodes(value, binding, expression, statement); + } + } + Expr::MapConstruct(values, _) => { + for (key, value) in values { + walk_expr_nodes(key, binding, expression, statement); + walk_expr_nodes(value, binding, expression, statement); + } + } + Expr::Handle(value, _, block, _) => { + walk_expr_nodes(value, binding, expression, statement); + walk_block_nodes(block, binding, expression, statement); + } + Expr::StringInterpolation(parts, _) => { + for part in parts { + match part { + StringPart::Expr(value) => { + walk_expr_nodes(value, binding, expression, statement); + } + StringPart::Literal(_) => {} + } + } + } + Expr::Pipeline(value, steps, _) => { + walk_expr_nodes(value, binding, expression, statement); + for step in steps { + walk_expr_nodes(&step.function, binding, expression, statement); + for argument in &step.extra_args { + walk_expr_nodes(&argument.value, binding, expression, statement); + } + if let Some(handle) = &step.handle { + walk_block_nodes(&handle.body, binding, expression, statement); + } + } + } + Expr::InlineFn(_, _, body, _) => walk_block_nodes(body, binding, expression, statement), + Expr::IntLiteral(_, _) + | Expr::FloatLiteral(_, _) + | Expr::StringLiteral(_, _) + | Expr::BoolLiteral(_, _) + | Expr::Nothing(_) + | Expr::Ident(_) + | Expr::None(_) + | Expr::EnumVariant(_, _, _) + | Expr::Error(_) => {} + } +} diff --git a/crates/jett_comptime/src/verify.rs b/crates/jett_comptime/src/verify.rs index 033d79f6..1fced1f8 100644 --- a/crates/jett_comptime/src/verify.rs +++ b/crates/jett_comptime/src/verify.rs @@ -389,6 +389,22 @@ fn run_verification( values: Option>, collect_cases: bool, ) -> VerificationRun { + // A checked worker retains the same immutable AST rather than attaching + // DefId facts to a freshly cloned same-span module. + if expression_types + .as_ref() + .and_then(|types| types.resource_program.as_ref()) + .is_some_and(|program| !std::ptr::eq(module, program.module())) + { + return run_verify_blocks_detailed_inner( + module, + metadata, + expression_types, + breakpoint_exclusions, + values, + collect_cases, + ); + } let module_for_thread = module.clone(); let thread_metadata = metadata.clone(); let thread_expression_types = expression_types.clone(); @@ -398,8 +414,14 @@ fn run_verification( .name("jett-verify".to_string()) .stack_size(VERIFY_STACK_SIZE) .spawn(move || { + let retained = thread_expression_types + .as_ref() + .and_then(|types| types.resource_program.clone()); + let worker_module = retained + .as_ref() + .map_or(&module_for_thread, |program| program.module()); run_verify_blocks_detailed_inner( - &module_for_thread, + worker_module, thread_metadata, thread_expression_types, thread_exclusions, @@ -430,7 +452,40 @@ fn run_verify_blocks_detailed_inner( values: Option>, collect_cases: bool, ) -> VerificationRun { + let resource_program = expression_types + .as_ref() + .and_then(|types| types.resource_program.clone()); + let setup_failure = |message: String| VerificationRun { + results: vec![VerifyResult { + name: "checked resource worker".to_string(), + span: module.span, + passed: false, + error: Some(message), + iterations: None, + is_property: false, + debug_events: Vec::new(), + }], + property_cases: Vec::new(), + }; + if resource_program + .as_ref() + .is_some_and(|program| !std::ptr::eq(module, program.module())) + { + return setup_failure( + "required worker received a foreign checked source module".to_string(), + ); + } let mut interp = Interpreter::new(); + if let Some(program) = resource_program { + if let Err(error) = interp + .install_checked_resource_program(program, crate::ExecutionPurpose::Verify) + .and_then(|()| { + interp.authorize_checked_resource_worker(module, crate::ExecutionPurpose::Verify) + }) + { + return setup_failure(error); + } + } if let Some(values) = values { interp.set_explicit_comptime_values(values); } @@ -449,9 +504,9 @@ fn run_verify_blocks_detailed_inner( // First pass: register all functions and type aliases so verify blocks // can call them and use refinement types. interp.register_module(module); - let mut legacy_verify_functions: Vec<(Option, FunctionDef)> = Vec::new(); - let mut verify_blocks: Vec<(Option, VerifyBlock)> = Vec::new(); - let mut property_blocks: Vec<(Option, PropertyBlock)> = Vec::new(); + let mut legacy_verify_functions: Vec<(Option, &FunctionDef)> = Vec::new(); + let mut verify_blocks: Vec<(Option, &VerifyBlock)> = Vec::new(); + let mut property_blocks: Vec<(Option, &PropertyBlock)> = Vec::new(); let mut property_enums: Vec = Vec::new(); let mut property_structs: Vec = Vec::new(); let mut property_bitfields: Vec = Vec::new(); @@ -507,14 +562,14 @@ fn run_verify_blocks_detailed_inner( } Item::Function(func) => { if has_assert_stmts(func) && func.params.is_empty() && func.name.name != "main" { - legacy_verify_functions.push((current_namespace.clone(), func.clone())); + legacy_verify_functions.push((current_namespace.clone(), func)); } } Item::Verify(vb) => { - verify_blocks.push((current_namespace.clone(), vb.clone())); + verify_blocks.push((current_namespace.clone(), vb)); } Item::Property(pb) => { - property_blocks.push((current_namespace.clone(), pb.clone())); + property_blocks.push((current_namespace.clone(), pb)); } _ => {} } @@ -522,7 +577,7 @@ fn run_verify_blocks_detailed_inner( // Execute proper verify blocks. for (namespace, vb) in &verify_blocks { - match interp.exec_block_in_namespace(namespace.as_deref(), &vb.body) { + match interp.exec_checked_verify_region(namespace.as_deref(), vb) { Ok(_) => { results.push(VerifyResult { name: vb.name.name.clone(), @@ -958,6 +1013,40 @@ struct PropertyDefinitions<'a> { } fn run_property_block( + interp: &mut Interpreter, + namespace: Option<&str>, + pb: &PropertyBlock, + definitions: PropertyDefinitions<'_>, + collected_cases: Option<&mut Vec>, +) -> VerifyResult { + let previous_purpose = interp.checked_execution_purpose(crate::ExecutionPurpose::Property); + let result = match interp.with_checked_property_region(pb, |interp| { + Ok(run_property_block_inner( + interp, + namespace, + pb, + definitions, + collected_cases, + )) + }) { + Ok(result) => result, + Err(error) => VerifyResult { + name: pb.name.name.clone(), + span: pb.name.span, + passed: false, + error: Some(error), + iterations: Some(0), + is_property: true, + debug_events: interp.take_debug_events(), + }, + }; + if let Some(purpose) = previous_purpose { + interp.checked_execution_purpose(purpose); + } + result +} + +fn run_property_block_inner( interp: &mut Interpreter, namespace: Option<&str>, pb: &PropertyBlock, @@ -5024,4 +5113,136 @@ property order: ); assert_eq!(results[1].iterations, Some(100)); } + + #[test] + fn checked_property_purpose_restores_previous_purpose_after_pass_and_failed_trial() { + use crate::ExecutionPurpose; + for release in [false, true] { + for purpose in [ + ExecutionPurpose::Verify, + ExecutionPurpose::NamespaceConstant, + ] { + for passes in [true, false] { + let source = format!( + "namespace app\nproperty restored_purpose:\n given sample: int64\n assert {passes}\n" + ); + let checked = crate::resource_execution::tests::program(&source, release); + let property = checked + .module() + .items + .iter() + .find_map(|item| match item { + Item::Property(property) => Some(property), + _ => None, + }) + .expect("exact checked property"); + let mut interpreter = + Interpreter::from_checked_resource_program(checked.clone(), purpose) + .unwrap(); + let result = run_property_block( + &mut interpreter, + Some("app"), + property, + PropertyDefinitions { + enums: &[], + structs: &[], + bitfields: &[], + aliases: &[], + }, + None, + ); + assert_eq!(result.passed, passes, "{:?}", result.error); + assert_eq!( + result.iterations, + Some(if passes { + PROPERTY_DEFAULT_ITERATIONS + } else { + 1 + }) + ); + if !passes { + assert!(result.error.as_deref().unwrap().contains("counterexample:")); + } + assert!(result.debug_events.is_empty()); + assert_eq!( + interpreter.checked_execution_purpose(purpose), + Some(purpose) + ); + + // Metadata-free execution still has no retained purpose. + let mut raw = Interpreter::new(); + let raw_result = run_property_block( + &mut raw, + Some("app"), + property, + PropertyDefinitions { + enums: &[], + structs: &[], + bitfields: &[], + aliases: &[], + }, + None, + ); + assert_eq!(raw_result.passed, result.passed); + assert_eq!(raw_result.error, result.error); + assert_eq!(raw_result.iterations, result.iterations); + assert!(raw_result.debug_events.is_empty()); + assert_eq!(raw.checked_execution_purpose(purpose), None); + } + } + } + } + + #[test] + fn checked_property_purpose_restores_previous_purpose_after_unsupported_pool() { + use crate::ExecutionPurpose; + let source = "namespace app\nstruct UnsupportedPurposeProbe:\n marker: int64\nproperty restored_pool:\n given sample: UnsupportedPurposeProbe\n assert true\n"; + for release in [false, true] { + for purpose in [ + ExecutionPurpose::Verify, + ExecutionPurpose::NamespaceConstant, + ] { + let checked = crate::resource_execution::tests::program(source, release); + let property = checked + .module() + .items + .iter() + .find_map(|item| match item { + Item::Property(property) => Some(property), + _ => None, + }) + .expect("checked generator control"); + // Retain the original checked property and omit its generator + // definition below to exercise unsupported-pool recovery. + let mut interpreter = + Interpreter::from_checked_resource_program(checked.clone(), purpose).unwrap(); + let result = run_property_block( + &mut interpreter, + Some("app"), + property, + PropertyDefinitions { + enums: &[], + structs: &[], + bitfields: &[], + aliases: &[], + }, + None, + ); + assert!(!result.passed); + assert_eq!(result.iterations, Some(0)); + assert!( + result + .error + .as_deref() + .unwrap() + .contains("cannot generate values") + ); + assert!(result.debug_events.is_empty()); + assert_eq!( + interpreter.checked_execution_purpose(purpose), + Some(purpose) + ); + } + } + } } diff --git a/crates/jett_driver/src/lib.rs b/crates/jett_driver/src/lib.rs index d173cf31..a47987a6 100644 --- a/crates/jett_driver/src/lib.rs +++ b/crates/jett_driver/src/lib.rs @@ -4,6 +4,7 @@ use jett_comptime::checked_types::{ mod native_builder_constants; mod native_constants; mod native_property_cases; +mod native_resource_constants; use jett_common::{FileId, STDLIB_FILE_ID_START, Span}; use jett_comptime::evaluate_explicit_comptime_expressions_capture; use jett_comptime::value::Value; @@ -23,10 +24,12 @@ use jett_query::{QueryDatabase, SourceOrigin}; use jett_resolve::resolve; use jett_typecheck::{ CheckOptions, CheckResult, CheckedResourceProgram, ResourceProgramError, check, - check_with_options, }; use jett_types::ReflectionMetadata; pub use native_property_cases::NativePropertyPlan; +pub use native_resource_constants::{ + bake_checked_resource_values, evaluate_checked_resource_required_values, +}; use std::borrow::Cow; use std::collections::{BTreeSet, HashMap, HashSet}; use std::fs; @@ -2931,28 +2934,46 @@ fn lower_file_for_backend_inner( source_origins.extend(support_modules.origins); prepend_support_modules(&mut parse_result.module, support_modules.modules); - let resolve_result = resolve(&parse_result.module); - diagnostics.extend(resolve_result.diagnostics.clone()); - if has_error_diagnostics(&diagnostics) { - return Err(build_failure(diagnostics, None, None, None, Vec::new())); - } - - let check_result = check_with_options( - &parse_result.module, - &resolve_result, + let primary_span = parse_result.module.span; + parse_result.errors = diagnostics; + let checked_program = match CheckedResourceProgram::prepare( + parse_result, + source_origins, + &[], CheckOptions { release: options.release, }, - ); - diagnostics.extend(check_result.diagnostics.clone()); - if has_error_diagnostics(&diagnostics) { - return Err(build_failure(diagnostics, None, None, None, Vec::new())); - } + ) { + Ok(program) => Arc::new(program), + Err(error) => { + let mut diagnostics = error.diagnostics().unwrap_or(&[]).to_vec(); + if matches!( + &error, + ResourceProgramError::ResolveMetadata { .. } + | ResourceProgramError::CheckMetadata { .. } + ) { + diagnostics.push(Diagnostic::error(0, error.to_string(), primary_span)); + } + return Err(build_failure(diagnostics, None, None, None, Vec::new())); + } + }; + let module = checked_program.module(); + let resolve_result = checked_program.resolved(); + let check_result = checked_program.checked(); + let mut diagnostics = checked_program + .parse_diagnostics() + .iter() + .chain(&resolve_result.diagnostics) + .chain(&check_result.diagnostics) + .cloned() + .collect::>(); let reflection_metadata = check_result.reflection_metadata.clone(); - let checked_expression_types = Arc::new(expression_type_names(&check_result, &resolve_result)); + let mut checked_type_names = expression_type_names(check_result, resolve_result); + checked_type_names.resource_program = Some(checked_program.clone()); + let checked_expression_types = Arc::new(checked_type_names); let evaluation = evaluate_explicit_comptime_expressions_capture( - &parse_result.module, + module, reflection_metadata.clone(), checked_expression_types.clone(), Arc::new(check_result.breakpoint_exclusions.clone()), @@ -2967,7 +2988,7 @@ fn lower_file_for_backend_inner( let explicit_comptime_values = Arc::new(evaluation.values); if !has_error_diagnostics(&diagnostics) { let results = run_verify_blocks_detailed_with_checked_values( - &parse_result.module, + module, reflection_metadata.clone(), checked_expression_types.clone(), Arc::new(check_result.breakpoint_exclusions.clone()), @@ -2993,19 +3014,9 @@ fn lower_file_for_backend_inner( } let mut hir = if mode != BackendLoweringMode::Program { - jett_hir::lower_with_test_bodies( - &parse_result.module, - &resolve_result, - &check_result, - &source_origins, - ) + jett_hir::lower_checked_ordinary_program_with_test_bodies(&checked_program) } else { - jett_hir::lower( - &parse_result.module, - &resolve_result, - &check_result, - &source_origins, - ) + jett_hir::lower_checked_ordinary_program(&checked_program) } .map_err(|errors| { BackendLoweringError::Hir(errors).with_debug_observations(&debug_observations) @@ -3021,19 +3032,14 @@ fn lower_file_for_backend_inner( BackendLoweringError::Hir(errors).with_debug_observations(&debug_observations) })?; let native_verify_entry = if mode == BackendLoweringMode::VerifySuite { - append_native_verify_suite( - &mut hir, - &parse_result.module, - entry_file, - &check_result.interner, - ) - .map_err(|error| error.with_debug_observations(&debug_observations))? + append_native_verify_suite(&mut hir, module, entry_file, &check_result.interner) + .map_err(|error| error.with_debug_observations(&debug_observations))? } else { None }; let native_property_plan = if mode == BackendLoweringMode::PropertySuite { let cases = collect_property_cases_with_checked_values( - &parse_result.module, + module, reflection_metadata.clone(), checked_expression_types.clone(), Arc::new(check_result.breakpoint_exclusions.clone()), @@ -3041,7 +3047,7 @@ fn lower_file_for_backend_inner( ); native_property_cases::append_property_suite( &mut hir, - &parse_result.module, + module, entry_file, &cases, &check_result.interner, @@ -3076,8 +3082,8 @@ fn lower_file_for_backend_inner( native_verify_entry, native_property_entry, native_property_plan, - interner: check_result.interner, - source_origins, + interner: check_result.interner.snapshot(), + source_origins: checked_program.source_origins().clone(), reflection_metadata, checked_expression_types, explicit_comptime_values, @@ -3215,7 +3221,9 @@ fn prepare_reference_file( // Phase 5: Execute verify blocks at compile time let reflection_metadata = check_result.reflection_metadata.clone(); - let checked_expression_types = Arc::new(expression_type_names(check_result, resolve_result)); + let mut checked_type_names = expression_type_names(check_result, resolve_result); + checked_type_names.resource_program = Some(program.clone()); + let checked_expression_types = Arc::new(checked_type_names); let evaluation = evaluate_explicit_comptime_expressions_capture( program.module(), reflection_metadata.clone(), @@ -3443,6 +3451,7 @@ fn expression_type_names( })); } CheckedExpressionTypes { + resource_program: None, bindings, expressions: names, functions, @@ -4245,6 +4254,12 @@ fn run_prepared_reference( } else { Interpreter::new() }; + interp + .install_checked_resource_program( + program.clone(), + jett_comptime::ExecutionPurpose::ReferenceRuntime, + ) + .map_err(&frontend_failure)?; if main_func .params .iter() @@ -5314,6 +5329,32 @@ mod tests { std::env::temp_dir().join(format!("{name}_{nanos}")) } + #[test] + fn backend_checked_ordinary_constants_and_refinements_keep_their_unsealed_path() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write( + &source, + "namespace app\ntype Positive = int64 where value > 0\nfunction positive() returns Positive:\n return 5\nint64 cached = comptime 5\nfunction main(stdout: Stdout) returns nothing:\n Positive refined = comptime positive()\n int64 ordinary = comptime 7\n int64 total = cached + coarsen refined + ordinary\n Stdout.write(view stdout, \"{total}\\n\")\n return nothing\n", + ) + .unwrap(); + for release in [false, true] { + let lowered = lower_file_for_backend_with_options(&source, BuildOptions { release }) + .expect("ordinary constants and raw refinement lowering"); + assert!(!jett_hir::has_reflected_callable_plans(&lowered.hir)); + assert!(lowered.hir.resource_source.manifest().is_none()); + assert!(lowered.hir.resource_source.functions().is_empty()); + assert!(lowered.mir.resource_manifest.kinds().is_empty()); + jett_codegen_cranelift::emit_host_program_object_with_options( + &lowered.mir, + &lowered.interner, + lowered.program_entry.expect("ordinary main"), + jett_codegen_cranelift::CodegenOptions { optimize: release }, + ) + .expect("ordinary constants and raw refinements still emit an object"); + } + } + const DEBUG_PHASE_SOURCE: &str = r#"namespace app function observe(label: string) returns int64: print(view label) diff --git a/crates/jett_driver/src/native.rs b/crates/jett_driver/src/native.rs index 8e14d2f0..10c29f22 100644 --- a/crates/jett_driver/src/native.rs +++ b/crates/jett_driver/src/native.rs @@ -1354,27 +1354,13 @@ fn run_command_with_timeout( program: &Path, timeout: Duration, ) -> Result { - // Files do not require EOF from every descendant that inherited a handle. - // Reopen gives writers independent offsets; capture only the exit snapshot. - let mut stdout = capture_file("stdout")?; - let mut stderr = capture_file("stderr")?; + // Retained files do not require EOF from inherited descendant handles. + // Duplicate append-only handles; positioned reads keep a bounded snapshot. + let stdout = capture_file("stdout")?; + let stderr = capture_file("stderr")?; command - .stdout( - stdout - .reopen() - .map_err(|source| NativeBuildError::CaptureLinkerOutput { - stream: "stdout", - source, - })?, - ) - .stderr( - stderr - .reopen() - .map_err(|source| NativeBuildError::CaptureLinkerOutput { - stream: "stderr", - source, - })?, - ); + .stdout(capture_writer(stdout.as_file(), "stdout")?) + .stderr(capture_writer(stderr.as_file(), "stderr")?); let child = command .spawn() .map_err(|source| NativeBuildError::SpawnLinker { @@ -1405,8 +1391,8 @@ fn run_command_with_timeout( } }; - let stdout = read_output_snapshot(stdout.as_file_mut(), "stdout")?; - let stderr = read_output_snapshot(stderr.as_file_mut(), "stderr")?; + let stdout = read_output_snapshot(stdout.as_file(), "stdout")?; + let stderr = read_output_snapshot(stderr.as_file(), "stderr")?; if timed_out { return Err(NativeBuildError::LinkTimedOut { linker: program.to_path_buf(), @@ -1471,22 +1457,109 @@ impl Drop for ManagedChild { } } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum CaptureStage { + Create, + Duplicate, + Snapshot, +} + +#[derive(Debug)] +struct CaptureIoError { + stage: CaptureStage, + source: io::Error, +} + +impl fmt::Display for CaptureIoError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let stage = match self.stage { + CaptureStage::Create => "create", + CaptureStage::Duplicate => "duplicate", + CaptureStage::Snapshot => "snapshot", + }; + write!(formatter, "{stage} capture: {}", self.source) + } +} + +impl std::error::Error for CaptureIoError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.source) + } +} + +fn capture_error(stream: &'static str, stage: CaptureStage, source: io::Error) -> NativeBuildError { + NativeBuildError::CaptureLinkerOutput { + stream, + source: io::Error::new(source.kind(), CaptureIoError { stage, source }), + } +} + fn capture_file(stream: &'static str) -> Result { - tempfile::NamedTempFile::new() - .map_err(|source| NativeBuildError::CaptureLinkerOutput { stream, source }) + // Windows positioned reads update the shared cursor. Append-only writes + // still target EOF when a surviving descendant writes during a snapshot. + tempfile::Builder::new() + .append(true) + .tempfile() + .map_err(|source| capture_error(stream, CaptureStage::Create, source)) +} + +fn capture_writer(file: &fs::File, stream: &'static str) -> Result { + file.try_clone() + .map_err(|source| capture_error(stream, CaptureStage::Duplicate, source)) +} + +struct CaptureSnapshot<'a> { + file: &'a fs::File, + offset: u64, +} + +impl Read for CaptureSnapshot<'_> { + fn read(&mut self, buffer: &mut [u8]) -> io::Result { + let count = read_capture_at(self.file, buffer, self.offset)?; + let advance = u64::try_from(count).map_err(io::Error::other)?; + self.offset = self.offset.checked_add(advance).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + "capture snapshot offset overflow", + ) + })?; + Ok(count) + } +} + +#[cfg(unix)] +fn read_capture_at(file: &fs::File, buffer: &mut [u8], offset: u64) -> io::Result { + use std::os::unix::fs::FileExt; + file.read_at(buffer, offset) +} + +#[cfg(windows)] +fn read_capture_at(file: &fs::File, buffer: &mut [u8], offset: u64) -> io::Result { + use std::os::windows::fs::FileExt; + file.seek_read(buffer, offset) +} + +#[cfg(not(any(unix, windows)))] +fn read_capture_at(_file: &fs::File, _buffer: &mut [u8], _offset: u64) -> io::Result { + Err(io::Error::new( + io::ErrorKind::Unsupported, + "positioned native capture is unavailable on this host", + )) } fn read_output_snapshot( - file: &mut fs::File, + file: &fs::File, stream: &'static str, ) -> Result, NativeBuildError> { - let mut read = || -> io::Result> { + let read = || -> io::Result> { let length = file.metadata()?.len(); let mut bytes = Vec::new(); - file.take(length).read_to_end(&mut bytes)?; + CaptureSnapshot { file, offset: 0 } + .take(length) + .read_to_end(&mut bytes)?; Ok(bytes) }; - read().map_err(|source| NativeBuildError::CaptureLinkerOutput { stream, source }) + read().map_err(|source| capture_error(stream, CaptureStage::Snapshot, source)) } #[cfg(windows)] @@ -1850,6 +1923,154 @@ verify checking: )); } + #[cfg(any(unix, windows))] + #[test] + fn capture_snapshots_retain_handle_identity_and_append_after_cursor_changes() { + use std::io::{Seek, SeekFrom, Write}; + + let capture = capture_file("stdout").unwrap(); + let mut writer = capture_writer(capture.as_file(), "stdout").unwrap(); + assert!( + read_output_snapshot(capture.as_file(), "stdout") + .unwrap() + .is_empty() + ); + writer.write_all(b"prefix").unwrap(); + assert_eq!( + read_output_snapshot(capture.as_file(), "stdout").unwrap(), + b"prefix" + ); + // A duplicate writer must remain append-only after the parent moves + // their shared cursor. No reopen is used to restore an independent one. + capture.as_file().seek(SeekFrom::Start(1)).unwrap(); + writer.write_all(b"-suffix").unwrap(); + for _ in 0..2 { + assert_eq!( + read_output_snapshot(capture.as_file(), "stdout").unwrap(), + b"prefix-suffix" + ); + } + } + + #[cfg(any(unix, windows))] + #[test] + fn capture_snapshot_bounds_multichunk_reads_despite_interleaved_appends() { + use std::io::Write; + + let capture = capture_file("stderr").unwrap(); + let mut writer = capture_writer(capture.as_file(), "stderr").unwrap(); + let prefix = b"0123456789abcdef".repeat(8 * 1024); + let suffix = b"later-output".repeat(1024); + writer.write_all(&prefix).unwrap(); + let sampled_length = capture.as_file().metadata().unwrap().len(); + let mut snapshot = CaptureSnapshot { + file: capture.as_file(), + offset: 0, + } + .take(sampled_length); + let mut first = [0; 17]; + snapshot.read_exact(&mut first).unwrap(); + // On Windows this append moves the shared cursor to EOF. The next + // snapshot read must still use its own offset and the original bound. + writer.write_all(&suffix).unwrap(); + let mut captured = first.to_vec(); + snapshot.read_to_end(&mut captured).unwrap(); + assert_eq!(captured, prefix); + let mut all_output = prefix; + all_output.extend_from_slice(&suffix); + assert_eq!( + read_output_snapshot(capture.as_file(), "stderr").unwrap(), + all_output + ); + + // Short EOF remains valid if the file is shorter than a sampled bound. + let mut short = Vec::new(); + CaptureSnapshot { + file: capture.as_file(), + offset: 0, + } + .take(u64::try_from(all_output.len()).unwrap() + 1) + .read_to_end(&mut short) + .unwrap(); + assert_eq!(short, all_output); + } + + #[test] + fn capture_error_context_preserves_stream_kind_and_original_os_error() { + for stage in [ + CaptureStage::Create, + CaptureStage::Duplicate, + CaptureStage::Snapshot, + ] { + let original = io::Error::from_raw_os_error(5); + let kind = original.kind(); + let NativeBuildError::CaptureLinkerOutput { stream, source } = + capture_error("stderr", stage, original) + else { + panic!("capture context changed the public error variant"); + }; + assert_eq!(stream, "stderr"); + assert_eq!(source.kind(), kind); + let detail = source + .get_ref() + .unwrap() + .downcast_ref::() + .unwrap(); + assert_eq!(detail.stage, stage); + assert_eq!(detail.source.raw_os_error(), Some(5)); + let nested = std::error::Error::source(detail).unwrap(); + assert_eq!( + nested.downcast_ref::().unwrap().raw_os_error(), + Some(5) + ); + } + } + + #[cfg(windows)] + #[test] + fn command_runner_captures_windows_streams_and_nonzero_status() { + use std::os::windows::process::CommandExt; + + let mut command = Command::new("cmd.exe"); + command + .args(["/d", "/s", "/c"]) + .raw_arg(r#"&2&exit /b 7"#) + .stdin(Stdio::null()); + let output = + run_command_with_timeout(command, Path::new("cmd.exe"), Duration::from_secs(5)) + .unwrap(); + assert_eq!(output.status.code(), Some(7)); + assert_eq!(output.stdout, b"captured"); + assert_eq!(output.stderr, b"diagnostic"); + } + + #[cfg(windows)] + #[test] + fn command_windows_deadline_does_not_wait_for_inherited_capture_handles() { + use std::os::windows::process::CommandExt; + + let mut command = Command::new("cmd.exe"); + command + .args(["/d", "/s", "/c"]) + .raw_arg(r#"&2&start "" /b cmd.exe /d /c "ping.exe -n 6 127.0.0.1 >nul"&ping.exe -n 6 127.0.0.1 >nul"#) + .stdin(Stdio::null()); + let start = Instant::now(); + let error = + run_command_with_timeout(command, Path::new("cmd.exe"), Duration::from_millis(500)) + .expect_err("parent must time out while a descendant retains the captures"); + assert!( + start.elapsed() < Duration::from_secs(3), + "inherited handles defeated the deadline" + ); + match error { + NativeBuildError::LinkTimedOut { stdout, stderr, .. } => { + assert_eq!(stdout, "captured"); + assert_eq!(stderr, "diagnostic"); + } + error => panic!("unexpected error: {error}"), + } + } + #[cfg(unix)] #[test] fn command_runner_captures_large_streams_and_failure_status() { diff --git a/crates/jett_driver/src/native_constants.rs b/crates/jett_driver/src/native_constants.rs index 0b43805d..20650362 100644 --- a/crates/jett_driver/src/native_constants.rs +++ b/crates/jett_driver/src/native_constants.rs @@ -23,7 +23,32 @@ pub(crate) fn bake_values( reflection: &ReflectionMetadata, method_value_definitions: &HashSet, ) -> Result<(), Vec> { + if !program.resource_manifest.kinds().is_empty() { + return jett_hir::materialize_checked_required_values(program, values, types); + } let function_values = function_value_candidates(&program.functions, method_value_definitions); + if jett_hir::has_reflected_callable_plans(program) { + return jett_hir::materialize_checked_reflected_values( + program, + values, + types, + |value, ty, span, locals, bindings| { + value_expression( + value, + ty, + span, + &mut ValueContext { + types, + reflection, + functions: &function_values, + refinement_functions: None, + locals, + bindings, + }, + ) + }, + ); + } let mut errors = Vec::new(); for function in &mut program.functions { let specialization = &function.identity.specialization; @@ -219,6 +244,10 @@ impl Baker<'_> { return; } match &mut expr.kind { + E::ResourceInvoke { .. } | E::ResourceHookValue { .. } => self.errors.push(LowerError { + span: expr.span, + message: "pending ResourceOwnershipPlan: native constant materialization cannot represent a Resource operation or descriptor".into(), + }), E::Binary { left, right, .. } => { self.expression(left); self.expression(right); diff --git a/crates/jett_driver/src/native_property_cases.rs b/crates/jett_driver/src/native_property_cases.rs index 62f57908..48cc6253 100644 --- a/crates/jett_driver/src/native_property_cases.rs +++ b/crates/jett_driver/src/native_property_cases.rs @@ -644,6 +644,20 @@ pub(super) fn value_expression( (Type::String, Value::String(text)) => ExpressionKind::String(text.clone()), (Type::Bool, Value::Bool(flag)) => ExpressionKind::Bool(*flag), (Type::Nothing, Value::Nothing) => ExpressionKind::Nothing, + (Type::Function { .. }, Value::ReflectedCallable(adapter)) => { + if ty != adapter.requested_type() + || !std::ptr::eq(types, &adapter.checked_program().checked().interner) + || !adapter + .checked_plan() + .validate(adapter.checked_program().checked()) + { + return Err( + "generated reflected callback belongs to another checked type context".into(), + ); + } + let source = value_expression(adapter.source(), adapter.source_type(), span, context)?; + return jett_hir::lower_reflected_callable_value(source, adapter, span); + } (Type::Function { .. }, Value::NamedFunction(_) | Value::Function { .. }) => { return function_value_expression(value, ty, span, context); } diff --git a/crates/jett_driver/src/native_resource_constants.rs b/crates/jett_driver/src/native_resource_constants.rs new file mode 100644 index 00000000..be471a6f --- /dev/null +++ b/crates/jett_driver/src/native_resource_constants.rs @@ -0,0 +1,129 @@ +//! Required evaluation and native materialization share one checked Resource session. +use std::sync::Arc; + +use jett_comptime::{ExplicitComptimeEvaluation, ExplicitComptimeValues}; +use jett_hir::{LowerError, Program}; +use jett_typecheck::CheckedResourceProgram; + +/// Evaluate required values with the exact checked module and no runtime provider. +/// Observations belong to this phase; reading the cache never replays them. +pub fn evaluate_checked_resource_required_values( + checked: &Arc, +) -> ExplicitComptimeEvaluation { + let mut types = crate::expression_type_names(checked.checked(), checked.resolved()); + types.resource_program = Some(checked.clone()); + jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + checked.checked().reflection_metadata.clone(), + Arc::new(types), + Arc::new(checked.checked().breakpoint_exclusions.clone()), + ) +} + +/// Bake required values without replacing the retained original Source archive. +/// Resource authority comes from the private checked cache, never its public mirror. +pub fn bake_checked_resource_values( + program: &mut Program, + checked: &Arc, + values: &ExplicitComptimeValues, +) -> Result<(), Vec> { + if !program.resource_source.belongs_to(checked) { + return Err(vec![LowerError { + span: checked.module().span, + message: "native required-value materialization received a foreign checked program" + .into(), + }]); + } + crate::native_constants::bake_values( + program, + values, + &checked.checked().interner, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use jett_common::{FileId, SourceOrigin}; + use jett_parser::{ast::Item, parse}; + use jett_resolve::ResourceKernelSpec; + use jett_typecheck::CheckOptions; + use jett_types::ResourceKernelRecipe; + use std::collections::HashMap; + + fn checked(release: bool) -> Arc { + let support = FileId::new(10_000); + let primary = FileId::new(0); + let mut parsed = parse( + include_str!("../tests/native_conformance/resource/resource_probe.jett"), + support, + ); + let project = parse( + include_str!("../tests/native_conformance/resource/38_comptime_hook_unused.jett"), + primary, + ); + assert!(parsed.errors.is_empty()); + assert!(project.errors.is_empty()); + let declarations = parsed + .module + .items + .iter() + .filter_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .collect::>(); + let [declaration] = declarations.as_slice() else { + panic!("one original synthetic Resource declaration"); + }; + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: *declaration, + member: member.into(), + recipe, + }) + .collect::>(); + parsed.module.items.extend(project.module.items); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (support, SourceOrigin::Stdlib), + (primary, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap(), + ) + } + + #[test] + fn native_required_materialization_rejects_foreign_checked_context() { + for release in [false, true] { + let original = checked(release); + let same_source_foreign = checked(release); + let required = evaluate_checked_resource_required_values(&original); + assert!(required.diagnostics.is_empty()); + let mut program = jett_hir::lower_checked_resource_program(&original).unwrap(); + let before = program.clone(); + let errors = + bake_checked_resource_values(&mut program, &same_source_foreign, &required.values) + .unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("foreign checked program")) + ); + assert_eq!(program, before); + bake_checked_resource_values(&mut program, &original, &required.values).unwrap(); + } + } +} diff --git a/crates/jett_driver/tests/native_conformance.rs b/crates/jett_driver/tests/native_conformance.rs index f454f2fc..2dda01d9 100644 --- a/crates/jett_driver/tests/native_conformance.rs +++ b/crates/jett_driver/tests/native_conformance.rs @@ -27,12 +27,24 @@ mod inventory_execution; #[path = "native_conformance/call_ownership.rs"] mod call_ownership; +#[path = "native_conformance/resource_report.rs"] +mod resource_report; + +#[path = "native_conformance/resource_cases.rs"] +mod resource_cases; + +#[path = "native_conformance/resource_execution.rs"] +mod resource_execution; + #[path = "native_conformance/scoped_call_views.rs"] mod scoped_call_views; #[path = "native_conformance/local_view_aliases.rs"] mod local_view_aliases; +#[path = "native_conformance/erased_same_owner_arguments.rs"] +mod erased_same_owner_arguments; + #[path = "native_conformance/pending_graphics.rs"] mod pending_graphics; @@ -45,6 +57,9 @@ mod display_results; #[path = "native_conformance/equality_results.rs"] mod equality_results; +#[path = "native_conformance/actor_identity.rs"] +mod actor_identity; + #[path = "native_conformance/secret_struct_constructors.rs"] mod secret_struct_constructors; @@ -78,6 +93,12 @@ mod refinement_sum; #[path = "native_conformance/reflected_producers.rs"] mod reflected_producers; +#[path = "native_conformance/reflected_callables.rs"] +mod reflected_callables; + +#[path = "native_conformance/measured_launcher.rs"] +mod measured_launcher; + #[path = "native_conformance/suite_options.rs"] mod suite_options; @@ -108,6 +129,12 @@ mod uninhabited_callbacks; #[path = "native_conformance/latent_callbacks.rs"] mod latent_callbacks; +#[path = "native_conformance/ordinary_borrowed_sums.rs"] +mod ordinary_borrowed_sums; + +#[path = "native_conformance/ordinary_borrowed_sums_additional.rs"] +mod ordinary_borrowed_sums_additional; + struct Launcher { bundle: NativeLauncherBundle, _directory: tempfile::TempDir, @@ -148,6 +175,9 @@ fn debug_trace_lines(events: &[jett_driver::DebugEvent]) -> Vec { } fn launcher() -> &'static NativeLauncherBundle { + if let Some(bundle) = measured_launcher::supplied(!cfg!(debug_assertions)) { + return bundle; + } static LAUNCHER: OnceLock = OnceLock::new(); &LAUNCHER .get_or_init(|| { @@ -250,11 +280,77 @@ fn run_bounded_with_env( .unwrap(); bytes }; - std::process::Output { + let actual = std::process::Output { status, stdout: snapshot(stdout.as_file_mut()), stderr: snapshot(stderr.as_file_mut()), + }; + retain_resource_native_evidence(executable, directory, scripted_provider, &actual); + actual +} + +fn retain_resource_native_evidence( + executable: &Path, + directory: &Path, + scripted_provider: Option<(&str, &str)>, + actual: &std::process::Output, +) { + let Some(root) = std::env::var_os("JETT_RESOURCE_NATIVE_EVIDENCE_DIR_V1") else { + return; + }; + let root = std::path::PathBuf::from(root); + assert!(root.is_absolute()); + fs::create_dir_all(&root).unwrap(); + let destination = root.join(directory.file_name().unwrap()); + fs::create_dir(&destination).unwrap(); + let mut files = Vec::new(); + for path in [ + executable.to_path_buf(), + directory.join("resource.obj"), + directory.join("resource-native-report-v1.bin"), + ] { + if path.is_file() { + let metadata = path.metadata().unwrap(); + assert!(metadata.len() <= 256 * 1024 * 1024); + let saved = destination.join(path.file_name().unwrap()); + fs::copy(&path, &saved).unwrap(); + let hash = jett_runtime::crypto::sha256_digest(&fs::read(&saved).unwrap()); + let hash = hash + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + files.push(serde_json::json!({"path": saved, "sha256": hash, "bytes": metadata.len()})); + } + } + for (name, bytes) in [ + ("stdout.bin", &actual.stdout), + ("stderr.bin", &actual.stderr), + ] { + let saved = destination.join(name); + fs::write(&saved, bytes).unwrap(); + let hash = jett_runtime::crypto::sha256_digest(bytes); + let hash = hash + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + files.push(serde_json::json!({"path": saved, "sha256": hash, "bytes": bytes.len()})); + } + if let Some((name, script)) = scripted_provider { + let saved = destination.join("provider-script.txt"); + fs::write(&saved, script).unwrap(); + let hash = jett_runtime::crypto::sha256_digest(script.as_bytes()); + let hash = hash + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + files.push(serde_json::json!({"path": saved, "sha256": hash, "bytes": script.len(), "environment_key": name})); } + let receipt = serde_json::json!({"exit_code": actual.status.code(), "files": files, "captured_before_assertions_and_teardown": true}); + fs::write( + destination.join("receipt.json"), + serde_json::to_vec_pretty(&receipt).unwrap(), + ) + .unwrap(); } #[test] @@ -8835,3 +8931,6 @@ fn native_whole_machine_rebinding_preserves_owned_payloads_pending_depth_and_sou assert!(actual.stderr.is_empty(), "{actual:?}"); } } + +#[path = "native_conformance/reflected_iteration_controls.rs"] +mod reflected_iteration_controls; diff --git a/crates/jett_driver/tests/native_conformance/actor_identity.rs b/crates/jett_driver/tests/native_conformance/actor_identity.rs new file mode 100644 index 00000000..fe82edc6 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/actor_identity.rs @@ -0,0 +1,144 @@ +use super::*; + +fn fixture(name: &str) -> std::path::PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native") + .join(name) +} + +#[test] +fn native_actor_identity_original_and_nested_sources_match_current_runtime_profile_without_source() +{ + for (name, debug_text) in [ + ("actor_enum_equality.jett", "true false\n"), + ( + "actor_pending_alias_baseline.jett", + "actor#0 pending(actor#0)\nactor#0 actor#0\n", + ), + ( + "actor_pending_alias_identity.jett", + "true false\ntrue false false true false true\ntrue false 10 10\n", + ), + ( + "actor_direct_identity.jett", + "true false false true\ntrue false\n", + ), + ( + "actor_nested_identity.jett", + "true false false true\ntrue true\ntrue false\n", + ), + ] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::copy(fixture(name), &source).unwrap(); + let expected = jett_driver::run_file_capture_output(&source) + .expect("selected actor identity reference"); + assert!(expected.stdout.is_empty(), "{name}"); + assert_eq!( + debug_print_text(&expected.debug_events), + debug_text, + "{name}" + ); + assert!(debug_trace_lines(&expected.debug_events).is_empty()); + let binary = directory.path().join("actor.exe"); + // Original08 uses println, whose checked Source policy is debug. The + // Cargo test profile selects the matching normal runtime archive. + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release: false }, + ) + .unwrap_or_else(|error| panic!("{name}: {error}")); + fs::remove_file(&source).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{name}: {actual:?}"); + assert!(actual.stdout.is_empty(), "{name}: {actual:?}"); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_actor_identity_pending_direct_operations_keep_reference_errors_in_current_runtime_profile() + { + let template = fs::read_to_string(fixture("actor_pending_identity.jett")).unwrap(); + for (operator, label) in [("==", "Eq"), ("!=", "NotEq")] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write( + &source, + template.replace("pending == pending", &format!("pending {operator} pending")), + ) + .unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source) + .expect_err("direct pending actor equality remains unsupported"); + let message = format!( + "runtime error: unsupported binary operation: pending(actor#0) {label} pending(actor#0)" + ); + assert_eq!(expected.message, message); + assert!(expected.output.stdout.is_empty()); + assert!(debug_trace_lines(&expected.output.debug_events).is_empty()); + let binary = directory.path().join("actor_pending.exe"); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release: false }, + ) + .expect("native pending actor comparison"); + fs::remove_file(&source).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert_eq!( + actual.stderr, + format!("{message}\n").as_bytes(), + "{actual:?}" + ); + } +} + +#[test] +fn native_actor_identity_production_output_matches_with_optimized_objects_without_source() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::copy(fixture("actor_optimized_identity.jett"), &source).unwrap(); + let expected = + jett_driver::run_file_capture_output(&source).expect("production actor identity reference"); + assert_eq!( + expected.stdout, + concat!( + "true false false true\n", + "true false false true\n", + "true false false true false true\n", + "true false 10 10\n", + "true false\n", + ) + ); + assert!(expected.debug_events.is_empty()); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("actor_identity_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native production actor identity"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} diff --git a/crates/jett_driver/tests/native_conformance/erased_same_owner_arguments.rs b/crates/jett_driver/tests/native_conformance/erased_same_owner_arguments.rs new file mode 100644 index 00000000..85298cbc --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/erased_same_owner_arguments.rs @@ -0,0 +1,317 @@ +use super::*; + +const ORIGINAL_WRONG: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/native/erased_same_owner/05_erased_other_owner.jett" +)); +const ORIGINAL_MATCHING: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/native/erased_same_owner/12_erased_matching_owner.jett" +)); +const OWNER_FAILURE: &str = "runtime error: interface dispatch concrete argument owner mismatch"; + +fn matching_case(name: &str, source_text: &[u8], debug: &str) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, source_text).unwrap(); + let expected = jett_driver::run_file_capture_output(&source).unwrap(); + assert!(expected.stdout.is_empty()); + assert_eq!( + jett_driver::render_debug_events(&expected.debug_events), + debug + ); + let executable = directory.path().join(format!("{name}.exe")); + build_host_executable(&source, launcher(), &executable).unwrap(); + fs::remove_file(&source).unwrap(); + let actual = run_bounded(&executable, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{actual:?}"); + assert!(actual.stdout.is_empty()); + assert_eq!(actual.stderr, debug.as_bytes()); +} + +fn mismatch_case(name: &str, source_text: &[u8], prefix: &str) { + failure_case(name, source_text, prefix, OWNER_FAILURE); +} + +fn failure_case(name: &str, source_text: &[u8], prefix: &str, message: &str) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, source_text).unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source).unwrap_err(); + assert_eq!(expected.message, message); + assert!(expected.output.stdout.is_empty()); + assert_eq!( + jett_driver::render_debug_events(&expected.output.debug_events), + prefix + ); + let executable = directory.path().join(format!("{name}.exe")); + build_host_executable(&source, launcher(), &executable).unwrap(); + fs::remove_file(&source).unwrap(); + let actual = run_bounded(&executable, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert!(actual.stdout.is_empty()); + assert_eq!(actual.stderr, format!("{prefix}{message}\n").as_bytes()); +} + +#[test] +fn native_erased_owner_original_wrong_source_rejects_before_implementation() { + mismatch_case("original_wrong_owner", ORIGINAL_WRONG, ""); +} + +#[test] +fn native_erased_owner_original_matching_source_preserves_view_dispatch() { + matching_case( + "original_matching_owner", + ORIGINAL_MATCHING, + "left:one:two\n", + ); +} + +#[test] +fn native_erased_owner_direct_and_callback_preserve_argument_order_before_failure() { + let original = std::str::from_utf8(ORIGINAL_WRONG) + .unwrap() + .replace("\r\n", "\n"); + let source = original.replace( + "return \"left:{self.name}:{other.name}\"", + "println(\"entered-left-body\")\n return \"left:{self.name}:{other.name}\"", + ).replace( + "function main() returns nothing:", + "function receiver() returns Pairable:\n println(\"receiver\")\n return Left(name: \"one\")\nfunction other_value() returns Pairable:\n println(\"other\")\n return Right(name: \"two\")\nfunction main() returns nothing:", + ).replace( + "Pairable first = Left(name: \"one\")\n Pairable second = Right(name: \"two\")", + "Pairable first = receiver()\n Pairable second = other_value()", + ); + for callback in [false, true] { + let source = if callback { + source.replace( + "println(Pairable.combine(view first, view second))", + "function(view Pairable, view Pairable) returns string callback = Pairable.combine\n println(callback(view first, view second))", + ) + } else { + source.clone() + }; + mismatch_case( + &format!("ordered_callback_{callback}"), + source.as_bytes(), + "receiver\nother\n", + ); + } +} + +#[test] +fn native_erased_owner_owned_arguments_reject_before_body() { + let source = std::str::from_utf8(ORIGINAL_WRONG) + .unwrap() + .replace("view self:", "self:") + .replace("view other:", "other:") + .replace( + "Pairable.combine(view first, view second)", + "Pairable.combine(first, second)", + ) + .replace( + "return \"left:{self.name}:{other.name}\"", + "println(\"entered-left-body\")\n return \"left:{self.name}:{other.name}\"", + ); + mismatch_case("owned_wrong_owner", source.as_bytes(), ""); +} + +const COLLECTION_OWNER: &str = r#"namespace app +interface Pairable: + function combine(view self: Pairable, view other: Pairable) returns string +struct Left: + name: string +struct Right: + name: string +implement Pairable for list[optional[Left]]: + function combine(view self: list[optional[Left]], view other: list[optional[Left]]) returns string: + println("entered-left-body") + return "left" +implement Pairable for list[optional[Right]]: + function combine(view self: list[optional[Right]], view other: list[optional[Right]]) returns string: + return "right" +function main() returns nothing: + Pairable first = list(some(Left(name: "one"))) + Pairable second = list(some(OTHER_OWNER(name: "two"))) + println(Pairable.combine(view first, view second)) + return nothing +"#; + +#[test] +fn native_erased_owner_nested_collection_identity_requires_exact_owner() { + mismatch_case( + "nested_wrong_owner", + COLLECTION_OWNER.replace("OTHER_OWNER", "Right").as_bytes(), + "", + ); + matching_case( + "nested_matching_owner", + COLLECTION_OWNER.replace("OTHER_OWNER", "Left").as_bytes(), + "entered-left-body\nleft\n", + ); +} + +#[test] +fn native_erased_owner_interface_children_keep_their_declared_erased_contract() { + let source = r#"namespace app +interface Pairable: + function combine(view self: Pairable, view others: list[Pairable]) returns string +struct Left: + name: string +struct Right: + name: string +implement Pairable for Left: + function combine(view self: Left, view others: list[Pairable]) returns string: + return self.name +implement Pairable for Right: + function combine(view self: Right, view others: list[Pairable]) returns string: + return self.name +function main() returns nothing: + Pairable first = Left(name: "one") + Pairable second = Right(name: "two") + list[Pairable] others = list(second) + println(Pairable.combine(view first, view others)) + return nothing +"#; + matching_case("retained_erased_children", source.as_bytes(), "one\n"); +} + +#[test] +fn native_erased_owner_prior_argument_failure_keeps_its_message_and_order() { + let original = std::str::from_utf8(ORIGINAL_WRONG) + .unwrap() + .replace("\r\n", "\n"); + let source = original.replace( + "function main() returns nothing:", + "function other_value() returns Pairable:\n println(\"other\")\n string rejected = string.repeat(\"ab\", 9223372036854775807)\n return Right(name: rejected)\nfunction main() returns nothing:", + ).replace("Pairable second = Right(name: \"two\")", "Pairable second = other_value()"); + failure_case( + "prior_argument_failure", + source.as_bytes(), + "other\n", + "runtime error: string.repeat: requested output is too large", + ); +} + +const PRIMITIVE_OWNER: &str = r#"namespace app +interface Pairable: + function combine(view self: Pairable, view other: Pairable) returns string +implement Pairable for int8: + function combine(view self: int8, view other: int8) returns string: + println("entered-int8-body") + return "int8" +implement Pairable for int64: + function combine(view self: int64, view other: int64) returns string: + return "int64" +function main() returns nothing: + int8 narrow = 7 + OTHER_TYPE second_value = 9 + Pairable first = narrow + Pairable second = second_value + println(Pairable.combine(view first, view second)) + return nothing +"#; + +#[test] +fn native_erased_owner_equal_primitive_carriers_keep_checked_widths() { + mismatch_case( + "wrong_primitive_width", + PRIMITIVE_OWNER.replace("OTHER_TYPE", "int64").as_bytes(), + "", + ); + matching_case( + "matching_primitive_width", + PRIMITIVE_OWNER.replace("OTHER_TYPE", "int8").as_bytes(), + "entered-int8-body\nint8\n", + ); +} + +const GENERIC_OWNER: &str = r#"namespace app +interface Pairable: + function combine(view self: Pairable, view other: Pairable) returns string +struct Box[T]: + item: T +implement Pairable for Box[int64]: + function combine(view self: Box[int64], view other: Box[int64]) returns string: + println("entered-int-box-body") + return "integer" +implement Pairable for Box[string]: + function combine(view self: Box[string], view other: Box[string]) returns string: + return "text" +function main() returns nothing: + Pairable first = Box[int64](item: 7) + Pairable second = OTHER_BOX + println(Pairable.combine(view first, view second)) + return nothing +"#; + +#[test] +fn native_erased_owner_nominal_generic_arguments_are_not_layout_casts() { + mismatch_case( + "wrong_generic_owner", + GENERIC_OWNER + .replace("OTHER_BOX", "Box[string](item: \"nine\")") + .as_bytes(), + "", + ); + matching_case( + "matching_generic_owner", + GENERIC_OWNER + .replace("OTHER_BOX", "Box[int64](item: 9)") + .as_bytes(), + "entered-int-box-body\ninteger\n", + ); +} + +#[test] +fn native_erased_owner_production_dispatch_matches_with_optimized_objects_without_source() { + for (name, original, status, output) in [ + ("matching", ORIGINAL_MATCHING, 0, b"left:one:two".as_slice()), + ("mismatched", ORIGINAL_WRONG, 71, b"".as_slice()), + ] { + let original = std::str::from_utf8(original).unwrap().replace("\r\n", "\n"); + assert!(original.contains("function main() returns nothing:")); + assert!(original.contains("println(Pairable.combine(view first, view second))")); + let production = original + .replace( + "function main() returns nothing:", + "function main(stdout: Stdout) returns nothing:", + ) + .replace( + "println(Pairable.combine(view first, view second))", + "Stdout.write(view stdout, Pairable.combine(view first, view second))", + ); + let directory = tempfile::tempdir().unwrap(); + let source = directory + .path() + .join(format!("production_owner_{name}.jett")); + fs::write(&source, production).unwrap(); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("production_owner_{name}_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap(); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(status), "{actual:?}"); + assert_eq!(actual.stdout, output, "{actual:?}"); + let error = if status == 0 { + String::new() + } else { + format!("{OWNER_FAILURE}\n") + }; + assert_eq!(actual.stderr, error.as_bytes(), "{actual:?}"); + } + } +} diff --git a/crates/jett_driver/tests/native_conformance/measured_launcher.rs b/crates/jett_driver/tests/native_conformance/measured_launcher.rs new file mode 100644 index 00000000..8fad1ab8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/measured_launcher.rs @@ -0,0 +1,117 @@ +//! Reuse explicitly supplied normal bundles in native integration tests. +use jett_driver::native::NativeLauncherBundle; +use std::ffi::OsString; +use std::path::PathBuf; +use std::sync::OnceLock; + +const DEBUG_MANIFEST: &str = "JETT_NATIVE_TEST_DEBUG_BUNDLE_MANIFEST"; +const RELEASE_MANIFEST: &str = "JETT_NATIVE_TEST_RELEASE_BUNDLE_MANIFEST"; + +fn selected_manifest( + debug: Option, + release: Option, + use_release: bool, +) -> Result, String> { + match (debug, release) { + (None, None) => Ok(None), + (Some(debug), Some(release)) => { + if debug.is_empty() || release.is_empty() { + return Err("supplied native test bundle manifests must be nonempty".into()); + } + let debug = PathBuf::from(debug); + let release = PathBuf::from(release); + if !debug.is_absolute() || !release.is_absolute() { + return Err("supplied native test bundle manifests must be absolute paths".into()); + } + Ok(Some(if use_release { release } else { debug })) + } + _ => Err(format!( + "{DEBUG_MANIFEST} and {RELEASE_MANIFEST} must be supplied together" + )), + } +} + +pub(super) fn supplied(use_release: bool) -> Option<&'static NativeLauncherBundle> { + static DEBUG: OnceLock<(PathBuf, NativeLauncherBundle)> = OnceLock::new(); + static RELEASE: OnceLock<(PathBuf, NativeLauncherBundle)> = OnceLock::new(); + let manifest = selected_manifest( + std::env::var_os(DEBUG_MANIFEST), + std::env::var_os(RELEASE_MANIFEST), + use_release, + ) + .expect("explicit native test bundle selection")?; + let slot = if use_release { &RELEASE } else { &DEBUG }; + let selected = slot.get_or_init(|| { + let bundle = NativeLauncherBundle::from_manifest(&manifest, use_release) + .expect("explicit native test bundle must pass normal manifest validation"); + (manifest.clone(), bundle) + }); + assert_eq!( + selected.0, manifest, + "explicit native test bundle changed after selection" + ); + Some(&selected.1) +} + +#[test] +fn explicit_manifest_selection_requires_a_complete_nonempty_pair() { + assert_eq!(selected_manifest(None, None, false).unwrap(), None); + assert!(selected_manifest(Some("debug.json".into()), None, false).is_err()); + assert!(selected_manifest(None, Some("release.json".into()), true).is_err()); + assert!(selected_manifest(Some(OsString::new()), Some("release.json".into()), false).is_err()); + assert!(selected_manifest(Some("debug.json".into()), Some(OsString::new()), true).is_err()); + assert!( + selected_manifest( + Some("debug.json".into()), + Some("release.json".into()), + false + ) + .is_err() + ); +} + +#[test] +fn explicit_manifest_selection_uses_the_requested_runtime_profile() { + let directory = tempfile::tempdir().unwrap(); + let debug = directory.path().join("debug.json"); + let release = directory.path().join("release.json"); + for use_release in [false, true] { + assert_eq!( + selected_manifest( + Some(debug.as_os_str().to_owned()), + Some(release.as_os_str().to_owned()), + use_release, + ) + .unwrap(), + Some(if use_release { + release.clone() + } else { + debug.clone() + }), + ); + } +} + +#[test] +fn explicit_manifest_validation_rejects_missing_invalid_and_wrong_profile_bundles() { + let directory = tempfile::tempdir().unwrap(); + let manifest = directory.path().join("runtime.json"); + assert!(NativeLauncherBundle::from_manifest(&manifest, false).is_err()); + std::fs::write(&manifest, b"invalid JSON").unwrap(); + assert!(NativeLauncherBundle::from_manifest(&manifest, false).is_err()); + std::fs::write( + &manifest, + serde_json::to_vec(&serde_json::json!({ + "manifest_version": 1, + "compiler_version": env!("CARGO_PKG_VERSION"), + "profile": "debug", + })) + .unwrap(), + ) + .unwrap(); + assert!( + NativeLauncherBundle::from_manifest(&manifest, true) + .unwrap_err() + .contains("profile mismatch") + ); +} diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums.rs b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums.rs new file mode 100644 index 00000000..c471c507 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums.rs @@ -0,0 +1,262 @@ +use super::run_bounded; +use super::suite_options::launcher_for_options; +use jett_driver::BuildOptions; +use jett_driver::native::{ + build_host_executable_with_options, emit_host_program_object_for_file_with_options, +}; +use std::fs; +use std::path::Path; + +#[derive(Clone, Copy)] +enum ExpectedOutcome { + Success(&'static str), + RuntimeFailure { + stdout: &'static str, + message: &'static str, + }, +} + +struct SourceCase { + name: &'static str, + source: &'static str, + expected: ExpectedOutcome, +} + +const CASES: &[SourceCase] = &[ + SourceCase { + name: "01_optional_branches", + source: include_str!("ordinary_borrowed_sums/01_optional_branches.jett"), + expected: ExpectedOutcome::Success(concat!( + "before:written-some\nsome:written-some:2:5:5\n", + "before:written-none\nnone:written-none:7\n", + "before:bare-some\nsome:bare-some:2:12:12\n", + "before:bare-none\nnone:bare-none:7\n", + )), + }, + SourceCase { + name: "02_result_branches", + source: include_str!("ordinary_borrowed_sums/02_result_branches.jett"), + expected: ExpectedOutcome::Success(concat!( + "before:written-ok\nok:written-ok:2:5:5\n", + "before:written-fail\nfail:written-fail:written error\n", + "before:bare-ok\nok:bare-ok:2:12:12\n", + "before:bare-fail\nfail:bare-fail:bare error\n", + )), + }, + SourceCase { + name: "03_owned_scoped_forwarded", + source: include_str!("ordinary_borrowed_sums/03_owned_scoped_forwarded.jett"), + expected: ExpectedOutcome::Success(concat!( + "optional:nested:5\noptional:5:18:15:5\n", + "result:nested:12\nresult:12:29:36:12\n", + "generic:1:1\n", + )), + }, + SourceCase { + name: "04_named_prefix_return", + source: include_str!("ordinary_borrowed_sums/04_named_prefix_return.jett"), + expected: ExpectedOutcome::Success(concat!( + "prefix:optional:5\nlater:optional\nreturn:optional\n", + "prefix:result:12\nlater:result\nreturn:result\n", + "answers:7:11\n", + "prefix:optional:5\nlater:optional\nreturn:optional\n", + "prefix:result:12\nlater:result\nreturn:result\n", + "retained:7:11\n", + )), + }, + SourceCase { + name: "05_optional_terminal_prefix", + source: include_str!("ordinary_borrowed_sums/05_optional_terminal_prefix.jett"), + expected: ExpectedOutcome::RuntimeFailure { + stdout: "before:optional:8\nprefix:optional:5\nfailure:optional\n", + message: "runtime error: list.__remove_at: index -1 out of bounds", + }, + }, + SourceCase { + name: "06_result_terminal_prefix", + source: include_str!("ordinary_borrowed_sums/06_result_terminal_prefix.jett"), + expected: ExpectedOutcome::RuntimeFailure { + stdout: "before:result:8\nprefix:result:12\nfailure:result\n", + message: "runtime error: list.__remove_at: index -1 out of bounds", + }, + }, + SourceCase { + name: "07_outer_pending_some", + source: include_str!("ordinary_borrowed_sums/07_outer_pending_some.jett"), + expected: ExpectedOutcome::RuntimeFailure { + stdout: "before\n", + message: "runtime error: handle block requires a result or optional value, got pending(pending(some(list(2, 3))))", + }, + }, + SourceCase { + name: "08_outer_pending_none", + source: include_str!("ordinary_borrowed_sums/08_outer_pending_none.jett"), + expected: ExpectedOutcome::RuntimeFailure { + stdout: "before\n", + message: "runtime error: handle block requires a result or optional value, got pending(pending(none))", + }, + }, + SourceCase { + name: "09_outer_pending_ok", + source: include_str!("ordinary_borrowed_sums/09_outer_pending_ok.jett"), + expected: ExpectedOutcome::RuntimeFailure { + stdout: "before\n", + message: "runtime error: handle block requires a result or optional value, got pending(pending(ok(list(2, 3))))", + }, + }, + SourceCase { + name: "10_outer_pending_fail", + source: include_str!("ordinary_borrowed_sums/10_outer_pending_fail.jett"), + expected: ExpectedOutcome::RuntimeFailure { + stdout: "before\n", + message: "runtime error: handle block requires a result or optional value, got pending(pending(fail(empty)))", + }, + }, + SourceCase { + name: "11_pending_optional_payload", + source: include_str!("ordinary_borrowed_sums/11_pending_optional_payload.jett"), + expected: ExpectedOutcome::RuntimeFailure { + stdout: "afteralias\n", + message: "runtime error: list.__sum: argument must be a list", + }, + }, + SourceCase { + name: "12_pending_result_payload", + source: include_str!("ordinary_borrowed_sums/12_pending_result_payload.jett"), + expected: ExpectedOutcome::RuntimeFailure { + stdout: "afteralias\n", + message: "runtime error: list.__sum: argument must be a list", + }, + }, +]; + +struct CheckedOutcome { + output: jett_driver::RunOutput, + status: i32, + stderr: String, +} + +fn reference(case: &SourceCase, source: &Path) -> CheckedOutcome { + let checked = jett_driver::build_file_with_options(source, BuildOptions::default()); + assert!( + !checked.has_errors, + "{}: Source must pass checking: {:?}", + case.name, checked.diagnostics + ); + let outcome = match case.expected { + ExpectedOutcome::Success(stdout) => { + let output = jett_driver::run_file_capture_outcome(source) + .unwrap_or_else(|failure| panic!("{}: reference: {failure:?}", case.name)); + assert_eq!(output.stdout, stdout, "{}", case.name); + CheckedOutcome { + output, + status: 0, + stderr: String::new(), + } + } + ExpectedOutcome::RuntimeFailure { stdout, message } => { + let failure = jett_driver::run_file_capture_outcome(source) + .expect_err("Source must reach its specified reference runtime failure"); + assert_eq!(failure.output.stdout, stdout, "{}", case.name); + assert_eq!(failure.message, message, "{}", case.name); + CheckedOutcome { + output: failure.output, + status: 71, + stderr: format!("{message}\n"), + } + } + }; + assert!(outcome.output.debug_events.is_empty(), "{}", case.name); + assert!( + outcome.output.frontend_debug_observations.is_empty(), + "{}", + case.name + ); + eprintln!( + "reference {}: status={} stdout={:?} stderr={:?}", + case.name, outcome.status, outcome.output.stdout, outcome.stderr + ); + outcome +} + +#[test] +fn ordinary_borrowed_sums_reference_packet_matches_exact_source_outcomes() { + assert_eq!(CASES.len(), 12); + for case in CASES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{}.jett", case.name)); + fs::write(&source, case.source).unwrap(); + reference(case, &source); + } +} + +#[test] +fn ordinary_borrowed_sums_native_packet_matches_reference_without_source() { + let mut refusals = Vec::new(); + for case in CASES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{}.jett", case.name)); + fs::write(&source, case.source).unwrap(); + let expected = reference(case, &source); + let mut binaries = Vec::new(); + for release in [false, true] { + let options = BuildOptions { release }; + // Reach native verification before building a runtime archive. A + // refused object is a real parity failure, never an expected pass. + if let Err(error) = emit_host_program_object_for_file_with_options(&source, options) { + let refusal = format!("{}, release={release}: {error:?}", case.name); + eprintln!("native refusal {refusal}"); + refusals.push(refusal); + continue; + } + let binary = directory + .path() + .join(format!("{}_{release}.exe", case.name)); + match build_host_executable_with_options( + &source, + launcher_for_options(release), + &binary, + options, + ) { + Ok(artifact) => { + assert!(artifact.debug_observations.is_empty(), "{}", case.name); + binaries.push((binary, release)); + } + Err(error) => { + let refusal = format!("{}, release={release}: {error:?}", case.name); + eprintln!("native refusal {refusal}"); + refusals.push(refusal); + } + } + } + fs::remove_file(&source).unwrap(); + assert!(!source.exists(), "{}: Source remains", case.name); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(expected.status), + "{}, release={release}: {actual:?}", + case.name + ); + assert_eq!( + actual.stdout, + expected.output.stdout.as_bytes(), + "{}, release={release}", + case.name + ); + assert_eq!( + actual.stderr, + expected.stderr.as_bytes(), + "{}, release={release}: {actual:?}", + case.name + ); + } + } + assert!( + refusals.is_empty(), + "ordinary borrowed-sum parity has {} native refusals:\n{}", + refusals.len(), + refusals.join("\n") + ); +} diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/01_optional_branches.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/01_optional_branches.jett new file mode 100644 index 00000000..2bfd12a7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/01_optional_branches.jett @@ -0,0 +1,26 @@ +namespace app +function inspect(view source: optional[list[int64]], label: string, view stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before:{label}\n") + list[int64] alias = view((view source) handle: + optional[int64] missing = none + int64 marker = missing handle: + default 7 + Stdout.write(view stdout, "none:{label}:{marker}\n") + return nothing + ) + list[int64] repeated = view((view source) handle: + Stdout.write(view stdout, "unexpected repeat:{label}\n") + return nothing + ) + Stdout.write(view stdout, "some:{label}:{list.length[int64](view alias)}:{list.sum[int64](view alias)}:{list.sum[int64](view repeated)}\n") + return nothing +function main(stdout: Stdout) returns nothing: + optional[list[int64]] written_some = some(list(2, 3)) + optional[list[int64]] written_none = none + optional[list[int64]] bare_some = some(list(5, 7)) + optional[list[int64]] bare_none = none + inspect(view written_some, "written-some", view stdout) + inspect(view written_none, "written-none", view stdout) + inspect(bare_some, "bare-some", view stdout) + inspect(bare_none, "bare-none", view stdout) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/02_result_branches.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/02_result_branches.jett new file mode 100644 index 00000000..b9b64458 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/02_result_branches.jett @@ -0,0 +1,23 @@ +namespace app +function inspect(view source: result[list[int64], string], label: string, view stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before:{label}\n") + list[int64] alias = view((view source) handle error: + Stdout.write(view stdout, "fail:{label}:{error}\n") + return nothing + ) + list[int64] repeated = view((view source) handle error: + Stdout.write(view stdout, "unexpected repeat:{label}:{error}\n") + return nothing + ) + Stdout.write(view stdout, "ok:{label}:{list.length[int64](view alias)}:{list.sum[int64](view alias)}:{list.sum[int64](view repeated)}\n") + return nothing +function main(stdout: Stdout) returns nothing: + result[list[int64], string] written_ok = ok(list(2, 3)) + result[list[int64], string] written_fail = fail("written error") + result[list[int64], string] bare_ok = ok(list(5, 7)) + result[list[int64], string] bare_fail = fail("bare error") + inspect(view written_ok, "written-ok", view stdout) + inspect(view written_fail, "written-fail", view stdout) + inspect(bare_ok, "bare-ok", view stdout) + inspect(bare_fail, "bare-fail", view stdout) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/03_owned_scoped_forwarded.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/03_owned_scoped_forwarded.jett new file mode 100644 index 00000000..0077160f --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/03_owned_scoped_forwarded.jett @@ -0,0 +1,61 @@ +namespace app +function count[T](view source: optional[T]) returns int64: + T alias = view((view source) handle: + return 0 + ) + return 1 +function inspect_optional(view stdout: Stdout) returns nothing: + optional[list[int64]] source = some(list(2, 3)) + list[int64] alias = view((view source) handle: + return nothing + ) + list[int64] forwarded = alias + mutable list[int64] copied = clone forwarded + copied = list.append[int64](copied, 13) + if true: + list[int64] nested = view((view source) handle: + return nothing + ) + Stdout.write(view stdout, "optional:nested:{list.sum[int64](view nested)}\n") + mutable int64 total = 0 + for number in view forwarded: + list[int64] loop_alias = view((view source) handle: + return nothing + ) + total = total + number + list.sum[int64](view loop_alias) + list[int64] retained = view((view source) handle: + return nothing + ) + Stdout.write(view stdout, "optional:{list.sum[int64](view forwarded)}:{list.sum[int64](view copied)}:{total}:{list.sum[int64](view retained)}\n") + return nothing +function inspect_result(view stdout: Stdout) returns nothing: + result[list[int64], string] source = ok(list(5, 7)) + list[int64] alias = view((view source) handle error: + return nothing + ) + list[int64] forwarded = alias + mutable list[int64] copied = clone forwarded + copied = list.append[int64](copied, 17) + if true: + list[int64] nested = view((view source) handle error: + return nothing + ) + Stdout.write(view stdout, "result:nested:{list.sum[int64](view nested)}\n") + mutable int64 total = 0 + for number in view forwarded: + list[int64] loop_alias = view((view source) handle error: + return nothing + ) + total = total + number + list.sum[int64](view loop_alias) + list[int64] retained = view((view source) handle error: + return nothing + ) + Stdout.write(view stdout, "result:{list.sum[int64](view forwarded)}:{list.sum[int64](view copied)}:{total}:{list.sum[int64](view retained)}\n") + return nothing +function main(stdout: Stdout) returns nothing: + inspect_optional(view stdout) + inspect_result(view stdout) + optional[int64] scalar = some(7) + optional[list[int64]] values = some(list(11)) + Stdout.write(view stdout, "generic:{count[int64](view scalar)}:{count[list[int64]](view values)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/04_named_prefix_return.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/04_named_prefix_return.jett new file mode 100644 index 00000000..788e8a61 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/04_named_prefix_return.jett @@ -0,0 +1,36 @@ +namespace app +function consume(extra: int64, amount: int64, view values: list[int64], view stdout: Stdout) returns int64: + Stdout.write(view stdout, "unexpected consume\n") + return extra + amount + list.sum[int64](view values) +function observe(view values: list[int64], label: string, view stdout: Stdout) returns int64: + Stdout.write(view stdout, "prefix:{label}:{list.sum[int64](view values)}\n") + return list.length[int64](view values) +function later(label: string, view stdout: Stdout) returns optional[int64]: + Stdout.write(view stdout, "later:{label}\n") + return none +function inspect_optional(view source: optional[list[int64]], view stdout: Stdout) returns int64: + list[int64] alias = view((view source) handle: + return -1 + ) + return consume(amount: observe(view alias, "optional", view stdout), values: view alias, stdout: view stdout, extra: later("optional", view stdout) handle: + Stdout.write(view stdout, "return:optional\n") + return 7 + ) +function inspect_result(view source: result[list[int64], string], view stdout: Stdout) returns int64: + list[int64] alias = view((view source) handle error: + return -1 + ) + return consume(amount: observe(view alias, "result", view stdout), values: view alias, stdout: view stdout, extra: later("result", view stdout) handle: + Stdout.write(view stdout, "return:result\n") + return 11 + ) +function main(stdout: Stdout) returns nothing: + optional[list[int64]] optional_source = some(list(2, 3)) + result[list[int64], string] result_source = ok(list(5, 7)) + int64 optional_answer = inspect_optional(view optional_source, view stdout) + int64 result_answer = inspect_result(view result_source, view stdout) + Stdout.write(view stdout, "answers:{optional_answer}:{result_answer}\n") + int64 optional_retained = inspect_optional(view optional_source, view stdout) + int64 result_retained = inspect_result(view result_source, view stdout) + Stdout.write(view stdout, "retained:{optional_retained}:{result_retained}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/05_optional_terminal_prefix.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/05_optional_terminal_prefix.jett new file mode 100644 index 00000000..8c40459c --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/05_optional_terminal_prefix.jett @@ -0,0 +1,22 @@ +namespace app +function observe(view values: list[int64], view stdout: Stdout) returns int64: + Stdout.write(view stdout, "prefix:optional:{list.sum[int64](view values)}\n") + return list.length[int64](view values) +function failing(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "failure:optional\n") + list[string] broken = list.remove_at[string](list("owned", "other"), -1) + return list.length[string](broken) +function consume(amount: int64, extra: int64, view values: list[int64], view stdout: Stdout) returns nothing: + Stdout.write(view stdout, "unexpected consume\n") + return nothing +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("retained") + optional[list[int64]] source = some(list(2, 3)) + list[int64] alias = view((view source) handle: + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "before:optional:{bytes.length(view earlier)}\n") + consume(amount: observe(view forwarded, view stdout), values: view forwarded, stdout: view stdout, extra: failing(view stdout)) + Stdout.write(view stdout, "unexpected after\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/06_result_terminal_prefix.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/06_result_terminal_prefix.jett new file mode 100644 index 00000000..8512bda6 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/06_result_terminal_prefix.jett @@ -0,0 +1,22 @@ +namespace app +function observe(view values: list[int64], view stdout: Stdout) returns int64: + Stdout.write(view stdout, "prefix:result:{list.sum[int64](view values)}\n") + return list.length[int64](view values) +function failing(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "failure:result\n") + list[string] broken = list.remove_at[string](list("owned", "other"), -1) + return list.length[string](broken) +function consume(amount: int64, extra: int64, view values: list[int64], view stdout: Stdout) returns nothing: + Stdout.write(view stdout, "unexpected consume\n") + return nothing +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("retained") + result[list[int64], string] source = ok(list(5, 7)) + list[int64] alias = view((view source) handle error: + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "before:result:{bytes.length(view earlier)}\n") + consume(amount: observe(view forwarded, view stdout), values: view forwarded, stdout: view stdout, extra: failing(view stdout)) + Stdout.write(view stdout, "unexpected after\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/07_outer_pending_some.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/07_outer_pending_some.jett new file mode 100644 index 00000000..9f2c25a8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/07_outer_pending_some.jett @@ -0,0 +1,16 @@ +namespace app +function inspect(view stdout: Stdout, view source: optional[list[int64]]) returns nothing: + Stdout.write(view stdout, "before\n") + list[int64] alias = view((view source) handle: + Stdout.write(view stdout, "unexpected handler\n") + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "afteralias\n") + int64 total = list.sum[int64](view forwarded) + Stdout.write(view stdout, "unexpected sum:{total}\n") + return nothing +function main(stdout: Stdout) returns nothing: + optional[list[int64]] source = run run some(list(2, 3)) + inspect(view stdout, view source) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/08_outer_pending_none.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/08_outer_pending_none.jett new file mode 100644 index 00000000..72059abe --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/08_outer_pending_none.jett @@ -0,0 +1,16 @@ +namespace app +function inspect(view stdout: Stdout, view source: optional[list[int64]]) returns nothing: + Stdout.write(view stdout, "before\n") + list[int64] alias = view((view source) handle: + Stdout.write(view stdout, "unexpected handler\n") + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "afteralias\n") + int64 total = list.sum[int64](view forwarded) + Stdout.write(view stdout, "unexpected sum:{total}\n") + return nothing +function main(stdout: Stdout) returns nothing: + optional[list[int64]] source = run run none + inspect(view stdout, view source) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/09_outer_pending_ok.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/09_outer_pending_ok.jett new file mode 100644 index 00000000..f0b989e7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/09_outer_pending_ok.jett @@ -0,0 +1,16 @@ +namespace app +function inspect(view stdout: Stdout, view source: result[list[int64], string]) returns nothing: + Stdout.write(view stdout, "before\n") + list[int64] alias = view((view source) handle error: + Stdout.write(view stdout, "unexpected handler\n") + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "afteralias\n") + int64 total = list.sum[int64](view forwarded) + Stdout.write(view stdout, "unexpected sum:{total}\n") + return nothing +function main(stdout: Stdout) returns nothing: + result[list[int64], string] source = run run ok(list(2, 3)) + inspect(view stdout, view source) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/10_outer_pending_fail.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/10_outer_pending_fail.jett new file mode 100644 index 00000000..282e4554 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/10_outer_pending_fail.jett @@ -0,0 +1,16 @@ +namespace app +function inspect(view stdout: Stdout, view source: result[list[int64], string]) returns nothing: + Stdout.write(view stdout, "before\n") + list[int64] alias = view((view source) handle error: + Stdout.write(view stdout, "unexpected handler\n") + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "afteralias\n") + int64 total = list.sum[int64](view forwarded) + Stdout.write(view stdout, "unexpected sum:{total}\n") + return nothing +function main(stdout: Stdout) returns nothing: + result[list[int64], string] source = run run fail("empty") + inspect(view stdout, view source) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/11_pending_optional_payload.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/11_pending_optional_payload.jett new file mode 100644 index 00000000..1625ca6e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/11_pending_optional_payload.jett @@ -0,0 +1,15 @@ +namespace app +function inspect(view stdout: Stdout, view source: optional[list[int64]]) returns nothing: + list[int64] alias = view((view source) handle: + Stdout.write(view stdout, "unexpected handler\n") + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "afteralias\n") + int64 total = list.sum[int64](view forwarded) + Stdout.write(view stdout, "unexpected sum:{total}\n") + return nothing +function main(stdout: Stdout) returns nothing: + optional[list[int64]] source = some(run run list(2, 3)) + inspect(view stdout, view source) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/12_pending_result_payload.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/12_pending_result_payload.jett new file mode 100644 index 00000000..64ee128e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/12_pending_result_payload.jett @@ -0,0 +1,15 @@ +namespace app +function inspect(view stdout: Stdout, view source: result[list[int64], string]) returns nothing: + list[int64] alias = view((view source) handle error: + Stdout.write(view stdout, "unexpected handler\n") + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "afteralias\n") + int64 total = list.sum[int64](view forwarded) + Stdout.write(view stdout, "unexpected sum:{total}\n") + return nothing +function main(stdout: Stdout) returns nothing: + result[list[int64], string] source = ok(run run list(2, 3)) + inspect(view stdout, view source) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional.rs b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional.rs new file mode 100644 index 00000000..d5566756 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional.rs @@ -0,0 +1,198 @@ +use super::run_bounded; +use super::suite_options::launcher_for_options; +use jett_driver::BuildOptions; +use jett_driver::native::{ + build_host_executable_with_options, emit_host_program_object_for_file_with_options, +}; +use std::fs; +use std::path::Path; + +#[derive(Clone, Copy)] +enum ExpectedOutcome { + Success(&'static str), + RuntimeFailure { + stdout: &'static str, + message: &'static str, + }, +} + +struct SourceCase { + name: &'static str, + source: &'static str, + expected: ExpectedOutcome, +} + +const CASES: &[SourceCase] = &[ + SourceCase { + name: "13_optional_named_handle_failure", + source: include_str!( + "ordinary_borrowed_sums_additional/13_optional_named_handle_failure.jett" + ), + expected: ExpectedOutcome::RuntimeFailure { + stdout: concat!( + "before:optional:8\nprefix:optional:5\nlater:optional\n", + "default:optional\nfailure:optional\n", + ), + message: "runtime error: list.__remove_at: index -1 out of bounds", + }, + }, + SourceCase { + name: "14_result_named_handle_failure", + source: include_str!( + "ordinary_borrowed_sums_additional/14_result_named_handle_failure.jett" + ), + expected: ExpectedOutcome::RuntimeFailure { + stdout: concat!( + "before:result:8\nprefix:result:12\nlater:result\n", + "default:result\nfailure:result\n", + ), + message: "runtime error: list.__remove_at: index -1 out of bounds", + }, + }, + SourceCase { + name: "15_mutable_copied_payloads", + source: include_str!("ordinary_borrowed_sums_additional/15_mutable_copied_payloads.jett"), + expected: ExpectedOutcome::Success("integer:4:3\nstring:changed:kept\n"), + }, +]; + +struct CheckedOutcome { + output: jett_driver::RunOutput, + status: i32, + stderr: String, +} + +fn reference(case: &SourceCase, source: &Path) -> CheckedOutcome { + let checked = jett_driver::build_file_with_options(source, BuildOptions::default()); + assert!( + !checked.has_errors, + "{}: Source must pass checking: {:?}", + case.name, checked.diagnostics + ); + let outcome = match case.expected { + ExpectedOutcome::Success(stdout) => { + let output = jett_driver::run_file_capture_outcome(source) + .unwrap_or_else(|failure| panic!("{}: reference: {failure:?}", case.name)); + assert_eq!(output.stdout, stdout, "{}", case.name); + CheckedOutcome { + output, + status: 0, + stderr: String::new(), + } + } + ExpectedOutcome::RuntimeFailure { stdout, message } => { + let failure = jett_driver::run_file_capture_outcome(source) + .expect_err("Source must reach its specified reference runtime failure"); + assert_eq!(failure.output.stdout, stdout, "{}", case.name); + assert_eq!(failure.message, message, "{}", case.name); + CheckedOutcome { + output: failure.output, + status: 71, + stderr: format!("{message}\n"), + } + } + }; + assert!(outcome.output.debug_events.is_empty(), "{}", case.name); + assert!( + outcome.output.frontend_debug_observations.is_empty(), + "{}", + case.name + ); + eprintln!( + "supplemental reference {}: status={} stdout={:?} stderr={:?}", + case.name, outcome.status, outcome.output.stdout, outcome.stderr + ); + outcome +} + +#[test] +fn ordinary_borrowed_sums_additional_reference_matches_three_source_outcomes() { + assert_eq!(CASES.len(), 3); + for case in CASES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{}.jett", case.name)); + fs::write(&source, case.source).unwrap(); + reference(case, &source); + } + eprintln!("supplemental reference: 3 Source cases matched exact outcomes"); +} + +#[test] +fn ordinary_borrowed_sums_additional_native_matches_six_executions_without_source() { + assert_eq!(CASES.len(), 3); + let mut refusals = Vec::new(); + let mut executions = 0; + for case in CASES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{}.jett", case.name)); + fs::write(&source, case.source).unwrap(); + let expected = reference(case, &source); + let mut binaries = Vec::new(); + for release in [false, true] { + let options = BuildOptions { release }; + // Refusals are parity failures. Attempt both profiles and remove + // Source before executing any available linked artifacts. + if let Err(error) = emit_host_program_object_for_file_with_options(&source, options) { + let refusal = format!("{}, release={release}: {error:?}", case.name); + eprintln!("supplemental native refusal {refusal}"); + refusals.push(refusal); + continue; + } + let binary = directory + .path() + .join(format!("{}_{release}.exe", case.name)); + match build_host_executable_with_options( + &source, + launcher_for_options(release), + &binary, + options, + ) { + Ok(artifact) => { + assert!(artifact.debug_observations.is_empty(), "{}", case.name); + binaries.push((binary, release)); + } + Err(error) => { + let refusal = format!("{}, release={release}: {error:?}", case.name); + eprintln!("supplemental native refusal {refusal}"); + refusals.push(refusal); + } + } + } + fs::remove_file(&source).unwrap(); + assert!(!source.exists(), "{}: Source remains", case.name); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(expected.status), + "{}, release={release}: {actual:?}", + case.name + ); + assert_eq!( + actual.stdout, + expected.output.stdout.as_bytes(), + "{}, release={release}", + case.name + ); + assert_eq!( + actual.stderr, + expected.stderr.as_bytes(), + "{}, release={release}: {actual:?}", + case.name + ); + executions += 1; + eprintln!( + "supplemental native {}: release={release} status={} Source absent", + case.name, expected.status + ); + } + } + assert!( + refusals.is_empty(), + "supplemental ordinary borrowed-sum parity has {} native refusals:\n{}", + refusals.len(), + refusals.join("\n") + ); + assert_eq!(executions, 6, "all 3 Sources require both native profiles"); + eprintln!("supplemental native: 3 Source cases / 6 executions matched exact outcomes"); +} diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional/13_optional_named_handle_failure.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional/13_optional_named_handle_failure.jett new file mode 100644 index 00000000..d77d9ae5 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional/13_optional_named_handle_failure.jett @@ -0,0 +1,28 @@ +namespace app +function consume(extra: int64, amount: int64, view values: list[int64], view stdout: Stdout) returns nothing: + Stdout.write(view stdout, "unexpected consume\n") + return nothing +function observe(view values: list[int64], view stdout: Stdout) returns int64: + Stdout.write(view stdout, "prefix:optional:{list.sum[int64](view values)}\n") + return list.length[int64](view values) +function later(view stdout: Stdout) returns optional[int64]: + Stdout.write(view stdout, "later:optional\n") + return none +function failing(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "failure:optional\n") + list[string] broken = list.remove_at[string](list("owned", "other"), -1) + return list.length[string](broken) +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("retained") + optional[list[int64]] source = some(list(2, 3)) + list[int64] alias = view((view source) handle: + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "before:optional:{bytes.length(view earlier)}\n") + consume(values: view forwarded, amount: observe(view forwarded, view stdout), stdout: view stdout, extra: later(view stdout) handle: + Stdout.write(view stdout, "default:optional\n") + default failing(view stdout) + ) + Stdout.write(view stdout, "unexpected after\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional/14_result_named_handle_failure.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional/14_result_named_handle_failure.jett new file mode 100644 index 00000000..d0d6ee65 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional/14_result_named_handle_failure.jett @@ -0,0 +1,28 @@ +namespace app +function consume(extra: int64, amount: int64, view values: list[int64], view stdout: Stdout) returns nothing: + Stdout.write(view stdout, "unexpected consume\n") + return nothing +function observe(view values: list[int64], view stdout: Stdout) returns int64: + Stdout.write(view stdout, "prefix:result:{list.sum[int64](view values)}\n") + return list.length[int64](view values) +function later(view stdout: Stdout) returns optional[int64]: + Stdout.write(view stdout, "later:result\n") + return none +function failing(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "failure:result\n") + list[string] broken = list.remove_at[string](list("owned", "other"), -1) + return list.length[string](broken) +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("retained") + result[list[int64], string] source = ok(list(5, 7)) + list[int64] alias = view((view source) handle error: + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "before:result:{bytes.length(view earlier)}\n") + consume(values: view forwarded, amount: observe(view forwarded, view stdout), stdout: view stdout, extra: later(view stdout) handle: + Stdout.write(view stdout, "default:result\n") + default failing(view stdout) + ) + Stdout.write(view stdout, "unexpected after\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional/15_mutable_copied_payloads.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional/15_mutable_copied_payloads.jett new file mode 100644 index 00000000..30b22192 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional/15_mutable_copied_payloads.jett @@ -0,0 +1,25 @@ +namespace app +function copied_int(view stdout: Stdout) returns nothing: + optional[int64] source = some(3) + mutable int64 copied = view((view source) handle: + return nothing + ) + copied = copied + 1 + int64 original = source handle: + return nothing + Stdout.write(view stdout, "integer:{copied}:{original}\n") + return nothing +function copied_string(view stdout: Stdout) returns nothing: + result[string, string] source = ok("kept") + mutable string copied = view((view source) handle error: + return nothing + ) + copied = "changed" + string original = source handle error: + return nothing + Stdout.write(view stdout, "string:{copied}:{original}\n") + return nothing +function main(stdout: Stdout) returns nothing: + copied_int(view stdout) + copied_string(view stdout) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/01_original_wider.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/01_original_wider.jett new file mode 100644 index 00000000..139d0dfd --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/01_original_wider.jett @@ -0,0 +1,13 @@ +namespace app +type Positive = int64 where value > 0 +struct CallbackHolder: + callback: function() returns int64 +function five() returns int64: + return 5 +function main() returns nothing: + CallbackHolder holder = CallbackHolder(callback: five) + for field in type.fields[CallbackHolder](): + function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field) + Positive value = observed() + println(coarsen value) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/02_original_exact.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/02_original_exact.jett new file mode 100644 index 00000000..71d19b65 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/02_original_exact.jett @@ -0,0 +1,12 @@ +namespace app +struct CallbackHolder: + callback: function() returns int64 +function five() returns int64: + return 5 +function main() returns nothing: + CallbackHolder holder = CallbackHolder(callback: five) + for field in type.fields[CallbackHolder](): + function() returns int64 observed = type.field_value[CallbackHolder, function() returns int64](view holder, view field) + int64 value = observed() + println(value) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/03_read_without_invocation.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/03_read_without_invocation.jett new file mode 100644 index 00000000..42ff28dd --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/03_read_without_invocation.jett @@ -0,0 +1,29 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "predicate" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function() returns int64 +function source_five() returns int64: + string marker = "source" + trace marker + return 5 +function source_zero() returns int64: + string marker = "source" + trace marker + return 0 +function source_pending() returns int64: + string marker = "source" + trace marker + return run 5 +function selected_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return selected_field() +function main(stdout: Stdout) returns nothing: + Holder holder = Holder(callback: source_five) + TypeField field = selected_field() + function() returns Positive observed = type.field_value[Holder, function() returns Positive](view holder, view field) + Stdout.write(view stdout, "stored\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/04_named_twice_source_unchanged.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/04_named_twice_source_unchanged.jett new file mode 100644 index 00000000..a6750ba2 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/04_named_twice_source_unchanged.jett @@ -0,0 +1,32 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "predicate" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function() returns int64 +function source_five() returns int64: + string marker = "source" + trace marker + return 5 +function source_zero() returns int64: + string marker = "source" + trace marker + return 0 +function source_pending() returns int64: + string marker = "source" + trace marker + return run 5 +function selected_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return selected_field() +function main(stdout: Stdout) returns nothing: + Holder holder = Holder(callback: source_five) + TypeField field = selected_field() + function() returns Positive observed = type.field_value[Holder, function() returns Positive](view holder, view field) + Positive first = observed() + Positive second = observed() + int64 original = holder.callback() + Stdout.write(view stdout, "{coarsen first}:{coarsen second}:{original}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/05_captured_twice.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/05_captured_twice.jett new file mode 100644 index 00000000..bba444e2 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/05_captured_twice.jett @@ -0,0 +1,36 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "predicate" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function() returns int64 +function source_five() returns int64: + string marker = "source" + trace marker + return 5 +function source_zero() returns int64: + string marker = "source" + trace marker + return 0 +function source_pending() returns int64: + string marker = "source" + trace marker + return run 5 +function selected_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return selected_field() +function main(stdout: Stdout) returns nothing: + int64 offset = 7 + function() returns int64 captured = function() returns int64: + string marker = "source" + trace marker + return offset + Holder holder = Holder(callback: captured) + TypeField field = selected_field() + function() returns Positive observed = type.field_value[Holder, function() returns Positive](view holder, view field) + Positive first = observed() + Positive second = observed() + Stdout.write(view stdout, "{coarsen first}:{coarsen second}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/06_invalid_return.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/06_invalid_return.jett new file mode 100644 index 00000000..479d6a9d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/06_invalid_return.jett @@ -0,0 +1,30 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "predicate" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function() returns int64 +function source_five() returns int64: + string marker = "source" + trace marker + return 5 +function source_zero() returns int64: + string marker = "source" + trace marker + return 0 +function source_pending() returns int64: + string marker = "source" + trace marker + return run 5 +function selected_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return selected_field() +function main(stdout: Stdout) returns nothing: + Holder holder = Holder(callback: source_zero) + TypeField field = selected_field() + function() returns Positive observed = type.field_value[Holder, function() returns Positive](view holder, view field) + Positive returned = observed() + Stdout.write(view stdout, "consumer:{coarsen returned}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/07_pending_result.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/07_pending_result.jett new file mode 100644 index 00000000..93f40684 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/07_pending_result.jett @@ -0,0 +1,30 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "predicate" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function() returns int64 +function source_five() returns int64: + string marker = "source" + trace marker + return 5 +function source_zero() returns int64: + string marker = "source" + trace marker + return 0 +function source_pending() returns int64: + string marker = "source" + trace marker + return run 5 +function selected_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return selected_field() +function main(stdout: Stdout) returns nothing: + Holder holder = Holder(callback: source_pending) + TypeField field = selected_field() + function() returns Positive observed = type.field_value[Holder, function() returns Positive](view holder, view field) + Positive returned = observed() + Stdout.write(view stdout, "consumer:{coarsen returned}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/08_pending_callable.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/08_pending_callable.jett new file mode 100644 index 00000000..3fcaa27d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/08_pending_callable.jett @@ -0,0 +1,30 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "predicate" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function() returns int64 +function source_five() returns int64: + string marker = "source" + trace marker + return 5 +function source_zero() returns int64: + string marker = "source" + trace marker + return 0 +function source_pending() returns int64: + string marker = "source" + trace marker + return run 5 +function selected_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return selected_field() +function main(stdout: Stdout) returns nothing: + Holder holder = Holder(callback: run source_five) + TypeField field = selected_field() + function() returns Positive observed = type.field_value[Holder, function() returns Positive](view holder, view field) + Positive returned = observed() + Stdout.write(view stdout, "consumer:{coarsen returned}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/09_wrong_selector_owner.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/09_wrong_selector_owner.jett new file mode 100644 index 00000000..d5269a56 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/09_wrong_selector_owner.jett @@ -0,0 +1,35 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "predicate" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function() returns int64 +function source_five() returns int64: + string marker = "source" + trace marker + return 5 +function source_zero() returns int64: + string marker = "source" + trace marker + return 0 +function source_pending() returns int64: + string marker = "source" + trace marker + return run 5 +function selected_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return selected_field() +struct Other: + callback: function() returns int64 +function other_field() returns TypeField: + return list.get[TypeField](type.fields[Other](), 0) handle: + return other_field() +function main(stdout: Stdout) returns nothing: + Holder holder = Holder(callback: source_five) + TypeField field = other_field() + function() returns Positive observed = type.field_value[Holder, function() returns Positive](view holder, view field) + Positive returned = observed() + Stdout.write(view stdout, "consumer:{coarsen returned}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/10_optional_return.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/10_optional_return.jett new file mode 100644 index 00000000..132d0fb5 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/10_optional_return.jett @@ -0,0 +1,40 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "predicate" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function() returns int64 +function source_five() returns int64: + string marker = "source" + trace marker + return 5 +function source_zero() returns int64: + string marker = "source" + trace marker + return 0 +function source_pending() returns int64: + string marker = "source" + trace marker + return run 5 +function selected_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return selected_field() +struct OptionalHolder: + callback: function() returns optional[int64] +function some_five() returns optional[int64]: + string marker = "source" + trace marker + return some(5) +function optional_field() returns TypeField: + return list.get[TypeField](type.fields[OptionalHolder](), 0) handle: + return optional_field() +function main(stdout: Stdout) returns nothing: + OptionalHolder holder = OptionalHolder(callback: some_five) + TypeField field = optional_field() + function() returns optional[Positive] observed = type.field_value[OptionalHolder, function() returns optional[Positive]](view holder, view field) + Positive returned = observed() handle: + return nothing + Stdout.write(view stdout, "{coarsen returned}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/11_ready_refined_unit.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/11_ready_refined_unit.jett new file mode 100644 index 00000000..4e638fee --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/11_ready_refined_unit.jett @@ -0,0 +1,36 @@ +namespace app +function unit_check(value: nothing) returns bool: + string marker = "predicate" + trace marker + return true +type ReadyUnit = nothing where unit_check(value) +function higher_unit_check(value: nothing) returns bool: + string marker = "higher" + trace marker + return true +type HigherUnit = ReadyUnit where higher_unit_check(value) +function rejected_unit_check(value: nothing) returns bool: + string marker = "rejected" + trace marker + return false +type RejectedUnit = nothing where rejected_unit_check(value) +struct UnitHolder: + callback: function() returns nothing +function unit_source() returns nothing: + string marker = "source" + trace marker + return nothing +function pending_unit_source() returns nothing: + string marker = "source" + trace marker + return run run nothing +function unit_field() returns TypeField: + return list.get[TypeField](type.fields[UnitHolder](), 0) handle: + return unit_field() +function main(stdout: Stdout) returns nothing: + UnitHolder holder = UnitHolder(callback: unit_source) + TypeField field = unit_field() + function() returns ReadyUnit observed = type.field_value[UnitHolder, function() returns ReadyUnit](view holder, view field) + ReadyUnit returned = observed() + Stdout.write(view stdout, "ready\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/12_pending_refined_unit.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/12_pending_refined_unit.jett new file mode 100644 index 00000000..b9106665 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/12_pending_refined_unit.jett @@ -0,0 +1,36 @@ +namespace app +function unit_check(value: nothing) returns bool: + string marker = "predicate" + trace marker + return true +type ReadyUnit = nothing where unit_check(value) +function higher_unit_check(value: nothing) returns bool: + string marker = "higher" + trace marker + return true +type HigherUnit = ReadyUnit where higher_unit_check(value) +function rejected_unit_check(value: nothing) returns bool: + string marker = "rejected" + trace marker + return false +type RejectedUnit = nothing where rejected_unit_check(value) +struct UnitHolder: + callback: function() returns nothing +function unit_source() returns nothing: + string marker = "source" + trace marker + return nothing +function pending_unit_source() returns nothing: + string marker = "source" + trace marker + return run run nothing +function unit_field() returns TypeField: + return list.get[TypeField](type.fields[UnitHolder](), 0) handle: + return unit_field() +function main(stdout: Stdout) returns nothing: + UnitHolder holder = UnitHolder(callback: pending_unit_source) + TypeField field = unit_field() + function() returns ReadyUnit observed = type.field_value[UnitHolder, function() returns ReadyUnit](view holder, view field) + ReadyUnit returned = observed() + Stdout.write(view stdout, "consumer\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/13_exact_pending_unit.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/13_exact_pending_unit.jett new file mode 100644 index 00000000..150696c4 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/13_exact_pending_unit.jett @@ -0,0 +1,36 @@ +namespace app +function unit_check(value: nothing) returns bool: + string marker = "predicate" + trace marker + return true +type ReadyUnit = nothing where unit_check(value) +function higher_unit_check(value: nothing) returns bool: + string marker = "higher" + trace marker + return true +type HigherUnit = ReadyUnit where higher_unit_check(value) +function rejected_unit_check(value: nothing) returns bool: + string marker = "rejected" + trace marker + return false +type RejectedUnit = nothing where rejected_unit_check(value) +struct UnitHolder: + callback: function() returns nothing +function unit_source() returns nothing: + string marker = "source" + trace marker + return nothing +function pending_unit_source() returns nothing: + string marker = "source" + trace marker + return run run nothing +function unit_field() returns TypeField: + return list.get[TypeField](type.fields[UnitHolder](), 0) handle: + return unit_field() +function main(stdout: Stdout) returns nothing: + UnitHolder holder = UnitHolder(callback: pending_unit_source) + TypeField field = unit_field() + function() returns nothing observed = type.field_value[UnitHolder, function() returns nothing](view holder, view field) + nothing returned = observed() + println(returned) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/14_exact_proved_pending_unit.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/14_exact_proved_pending_unit.jett new file mode 100644 index 00000000..a997dfef --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/14_exact_proved_pending_unit.jett @@ -0,0 +1,47 @@ +namespace app +function unit_check(value: nothing) returns bool: + string marker = "predicate" + trace marker + return true +type ReadyUnit = nothing where unit_check(value) +function higher_unit_check(value: nothing) returns bool: + string marker = "higher" + trace marker + return true +type HigherUnit = ReadyUnit where higher_unit_check(value) +function rejected_unit_check(value: nothing) returns bool: + string marker = "rejected" + trace marker + return false +type RejectedUnit = nothing where rejected_unit_check(value) +struct UnitHolder: + callback: function() returns nothing +function unit_source() returns nothing: + string marker = "source" + trace marker + return nothing +function pending_unit_source() returns nothing: + string marker = "source" + trace marker + return run run nothing +function unit_field() returns TypeField: + return list.get[TypeField](type.fields[UnitHolder](), 0) handle: + return unit_field() +struct ProvenHolder: + callback: function() returns ReadyUnit +function proved_unit_source() returns ReadyUnit: + string marker = "source" + trace marker + ReadyUnit proved = nothing handle error: + return proved_unit_source() + return run run proved +function proved_field() returns TypeField: + return list.get[TypeField](type.fields[ProvenHolder](), 0) handle: + return proved_field() +function main(stdout: Stdout) returns nothing: + ProvenHolder holder = ProvenHolder(callback: proved_unit_source) + TypeField field = proved_field() + function() returns ReadyUnit observed = type.field_value[ProvenHolder, function() returns ReadyUnit](view holder, view field) + ReadyUnit returned = observed() + println(coarsen returned) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/15_ancestor_proved_pending_unit.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/15_ancestor_proved_pending_unit.jett new file mode 100644 index 00000000..faadfe74 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/15_ancestor_proved_pending_unit.jett @@ -0,0 +1,47 @@ +namespace app +function unit_check(value: nothing) returns bool: + string marker = "predicate" + trace marker + return true +type ReadyUnit = nothing where unit_check(value) +function higher_unit_check(value: nothing) returns bool: + string marker = "higher" + trace marker + return true +type HigherUnit = ReadyUnit where higher_unit_check(value) +function rejected_unit_check(value: nothing) returns bool: + string marker = "rejected" + trace marker + return false +type RejectedUnit = nothing where rejected_unit_check(value) +struct UnitHolder: + callback: function() returns nothing +function unit_source() returns nothing: + string marker = "source" + trace marker + return nothing +function pending_unit_source() returns nothing: + string marker = "source" + trace marker + return run run nothing +function unit_field() returns TypeField: + return list.get[TypeField](type.fields[UnitHolder](), 0) handle: + return unit_field() +struct HigherHolder: + callback: function() returns HigherUnit +function higher_unit_source() returns HigherUnit: + string marker = "source" + trace marker + HigherUnit proved = nothing handle error: + return higher_unit_source() + return run run proved +function higher_field() returns TypeField: + return list.get[TypeField](type.fields[HigherHolder](), 0) handle: + return higher_field() +function main(stdout: Stdout) returns nothing: + HigherHolder holder = HigherHolder(callback: higher_unit_source) + TypeField field = higher_field() + function() returns ReadyUnit observed = type.field_value[HigherHolder, function() returns ReadyUnit](view holder, view field) + ReadyUnit returned = observed() + println(coarsen returned) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/16_captured_pending_unit.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/16_captured_pending_unit.jett new file mode 100644 index 00000000..6188975d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/16_captured_pending_unit.jett @@ -0,0 +1,41 @@ +namespace app +function unit_check(value: nothing) returns bool: + string marker = "predicate" + trace marker + return true +type ReadyUnit = nothing where unit_check(value) +function higher_unit_check(value: nothing) returns bool: + string marker = "higher" + trace marker + return true +type HigherUnit = ReadyUnit where higher_unit_check(value) +function rejected_unit_check(value: nothing) returns bool: + string marker = "rejected" + trace marker + return false +type RejectedUnit = nothing where rejected_unit_check(value) +struct UnitHolder: + callback: function() returns nothing +function unit_source() returns nothing: + string marker = "source" + trace marker + return nothing +function pending_unit_source() returns nothing: + string marker = "source" + trace marker + return run run nothing +function unit_field() returns TypeField: + return list.get[TypeField](type.fields[UnitHolder](), 0) handle: + return unit_field() +function main(stdout: Stdout) returns nothing: + nothing delayed = run run nothing + function() returns nothing captured = function() returns nothing: + string marker = "source" + trace marker + return delayed + UnitHolder holder = UnitHolder(callback: captured) + TypeField field = unit_field() + function() returns ReadyUnit observed = type.field_value[UnitHolder, function() returns ReadyUnit](view holder, view field) + ReadyUnit returned = observed() + Stdout.write(view stdout, "consumer\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/17_optional_pending_unit.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/17_optional_pending_unit.jett new file mode 100644 index 00000000..e0e46ae3 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/17_optional_pending_unit.jett @@ -0,0 +1,46 @@ +namespace app +function unit_check(value: nothing) returns bool: + string marker = "predicate" + trace marker + return true +type ReadyUnit = nothing where unit_check(value) +function higher_unit_check(value: nothing) returns bool: + string marker = "higher" + trace marker + return true +type HigherUnit = ReadyUnit where higher_unit_check(value) +function rejected_unit_check(value: nothing) returns bool: + string marker = "rejected" + trace marker + return false +type RejectedUnit = nothing where rejected_unit_check(value) +struct UnitHolder: + callback: function() returns nothing +function unit_source() returns nothing: + string marker = "source" + trace marker + return nothing +function pending_unit_source() returns nothing: + string marker = "source" + trace marker + return run run nothing +function unit_field() returns TypeField: + return list.get[TypeField](type.fields[UnitHolder](), 0) handle: + return unit_field() +struct OptionalUnitHolder: + callback: function() returns optional[nothing] +function optional_unit_source() returns optional[nothing]: + string marker = "source" + trace marker + return some(run run nothing) +function optional_unit_field() returns TypeField: + return list.get[TypeField](type.fields[OptionalUnitHolder](), 0) handle: + return optional_unit_field() +function main(stdout: Stdout) returns nothing: + OptionalUnitHolder holder = OptionalUnitHolder(callback: optional_unit_source) + TypeField field = optional_unit_field() + function() returns optional[ReadyUnit] observed = type.field_value[OptionalUnitHolder, function() returns optional[ReadyUnit]](view holder, view field) + ReadyUnit returned = observed() handle: + return nothing + Stdout.write(view stdout, "consumer\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/18_invalid_refined_unit.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/18_invalid_refined_unit.jett new file mode 100644 index 00000000..014339a7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/18_invalid_refined_unit.jett @@ -0,0 +1,36 @@ +namespace app +function unit_check(value: nothing) returns bool: + string marker = "predicate" + trace marker + return true +type ReadyUnit = nothing where unit_check(value) +function higher_unit_check(value: nothing) returns bool: + string marker = "higher" + trace marker + return true +type HigherUnit = ReadyUnit where higher_unit_check(value) +function rejected_unit_check(value: nothing) returns bool: + string marker = "rejected" + trace marker + return false +type RejectedUnit = nothing where rejected_unit_check(value) +struct UnitHolder: + callback: function() returns nothing +function unit_source() returns nothing: + string marker = "source" + trace marker + return nothing +function pending_unit_source() returns nothing: + string marker = "source" + trace marker + return run run nothing +function unit_field() returns TypeField: + return list.get[TypeField](type.fields[UnitHolder](), 0) handle: + return unit_field() +function main(stdout: Stdout) returns nothing: + UnitHolder holder = UnitHolder(callback: unit_source) + TypeField field = unit_field() + function() returns RejectedUnit observed = type.field_value[UnitHolder, function() returns RejectedUnit](view holder, view field) + RejectedUnit returned = observed() + Stdout.write(view stdout, "consumer\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/production_invalid_return.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/production_invalid_return.jett new file mode 100644 index 00000000..4fa43642 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/production_invalid_return.jett @@ -0,0 +1,20 @@ +namespace app +function positive_check(value: int64) returns bool: + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function(view Stdout) returns int64 +function source_zero(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "source\n") + return 0 +function selected_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return selected_field() +function main(stdout: Stdout) returns nothing: + Holder holder = Holder(callback: source_zero) + TypeField field = selected_field() + function(view Stdout) returns Positive observed = type.field_value[Holder, function(view Stdout) returns Positive](view holder, view field) + Stdout.write(view stdout, "stored\n") + Positive returned = observed(view stdout) + Stdout.write(view stdout, "consumer:{coarsen returned}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/production_named_lazy.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/production_named_lazy.jett new file mode 100644 index 00000000..9779cbbf --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/production_named_lazy.jett @@ -0,0 +1,24 @@ +namespace app +function positive_check(value: int64) returns bool: + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function(view Stdout) returns int64 +function source_five(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "source\n") + return 5 +function selected_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return selected_field() +function main(stdout: Stdout) returns nothing: + Holder holder = Holder(callback: source_five) + TypeField field = selected_field() + function(view Stdout) returns Positive observed = type.field_value[Holder, function(view Stdout) returns Positive](view holder, view field) + Stdout.write(view stdout, "stored\n") + Positive first = observed(view stdout) + Stdout.write(view stdout, "first:{coarsen first}\n") + Positive second = observed(view stdout) + Stdout.write(view stdout, "second:{coarsen second}\n") + int64 original = holder.callback(view stdout) + Stdout.write(view stdout, "original:{original}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/production_wrong_selector_owner.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/production_wrong_selector_owner.jett new file mode 100644 index 00000000..a219c37e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/production_wrong_selector_owner.jett @@ -0,0 +1,22 @@ +namespace app +function positive_check(value: int64) returns bool: + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function(view Stdout) returns int64 +function source_five(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "source\n") + return 5 +struct Other: + callback: function(view Stdout) returns int64 +function other_field() returns TypeField: + return list.get[TypeField](type.fields[Other](), 0) handle: + return other_field() +function main(stdout: Stdout) returns nothing: + Holder holder = Holder(callback: source_five) + TypeField field = other_field() + Stdout.write(view stdout, "before\n") + function(view Stdout) returns Positive observed = type.field_value[Holder, function(view Stdout) returns Positive](view holder, view field) + Positive returned = observed(view stdout) + Stdout.write(view stdout, "consumer:{coarsen returned}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callables.rs b/crates/jett_driver/tests/native_conformance/reflected_callables.rs new file mode 100644 index 00000000..91b3a811 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callables.rs @@ -0,0 +1,487 @@ +//! Original Sources and additive callable-return controls. No Resource launcher. +use super::*; + +struct Case { + name: &'static str, + source: &'static str, + stdout: &'static str, + message: Option<&'static str>, + markers: &'static [&'static str], +} + +const CASES: &[Case] = &[ + Case { + name: "original_wider", + source: include_str!("reflected_callable_sources/01_original_wider.jett"), + stdout: "", + message: None, + markers: &[], + }, + Case { + name: "original_exact", + source: include_str!("reflected_callable_sources/02_original_exact.jett"), + stdout: "", + message: None, + markers: &[], + }, + Case { + name: "read_without_invocation", + source: include_str!("reflected_callable_sources/03_read_without_invocation.jett"), + stdout: "stored\n", + message: None, + markers: &[], + }, + Case { + name: "named_twice_source_unchanged", + source: include_str!("reflected_callable_sources/04_named_twice_source_unchanged.jett"), + stdout: "5:5:5\n", + message: None, + markers: &["source", "predicate", "source", "predicate", "source"], + }, + Case { + name: "captured_twice", + source: include_str!("reflected_callable_sources/05_captured_twice.jett"), + stdout: "7:7\n", + message: None, + markers: &["source", "predicate", "source", "predicate"], + }, + Case { + name: "invalid_return", + source: include_str!("reflected_callable_sources/06_invalid_return.jett"), + stdout: "", + message: Some("runtime error: refinement type constraint failed for 'app.Positive'"), + markers: &["source", "predicate"], + }, + Case { + name: "pending_result", + source: include_str!("reflected_callable_sources/07_pending_result.jett"), + stdout: "", + message: Some("runtime error: reflected callback return: expected a ready value"), + markers: &["source"], + }, + Case { + name: "pending_callable", + source: include_str!("reflected_callable_sources/08_pending_callable.jett"), + stdout: "", + message: Some( + "runtime error: expected function value, got pending(function(app.source_five))", + ), + markers: &[], + }, + Case { + name: "wrong_selector_owner", + source: include_str!("reflected_callable_sources/09_wrong_selector_owner.jett"), + stdout: "", + message: Some( + "runtime error: type.field_value: field metadata belongs to 'app.Other', expected 'app.Holder'", + ), + markers: &[], + }, + Case { + name: "optional_return", + source: include_str!("reflected_callable_sources/10_optional_return.jett"), + stdout: "5\n", + message: None, + markers: &["source", "predicate"], + }, + Case { + name: "ready_refined_unit", + source: include_str!("reflected_callable_sources/11_ready_refined_unit.jett"), + stdout: "ready\n", + message: None, + markers: &["source", "predicate"], + }, + Case { + name: "pending_refined_unit", + source: include_str!("reflected_callable_sources/12_pending_refined_unit.jett"), + stdout: "", + message: Some("runtime error: reflected callback return: expected a ready value"), + markers: &["source"], + }, + Case { + name: "exact_pending_unit", + source: include_str!("reflected_callable_sources/13_exact_pending_unit.jett"), + stdout: "", + message: None, + markers: &["source"], + }, + Case { + name: "exact_proved_pending_unit", + source: include_str!("reflected_callable_sources/14_exact_proved_pending_unit.jett"), + stdout: "", + message: None, + markers: &["source", "predicate"], + }, + Case { + name: "ancestor_proved_pending_unit", + source: include_str!("reflected_callable_sources/15_ancestor_proved_pending_unit.jett"), + stdout: "", + message: None, + markers: &["source", "predicate", "higher"], + }, + Case { + name: "captured_pending_unit", + source: include_str!("reflected_callable_sources/16_captured_pending_unit.jett"), + stdout: "", + message: Some("runtime error: reflected callback return: expected a ready value"), + markers: &["source"], + }, + Case { + name: "optional_pending_unit", + source: include_str!("reflected_callable_sources/17_optional_pending_unit.jett"), + stdout: "", + message: Some("runtime error: reflected callback return: expected a ready value"), + markers: &["source"], + }, + Case { + name: "invalid_refined_unit", + source: include_str!("reflected_callable_sources/18_invalid_refined_unit.jett"), + stdout: "", + message: Some("runtime error: refinement type constraint failed for 'app.RejectedUnit'"), + markers: &["source", "rejected"], + }, +]; + +fn reference(case: &Case, source: &Path) -> Vec { + let events = match (jett_driver::run_file_capture_outcome(source), case.message) { + (Ok(output), None) => { + assert_eq!(output.stdout, case.stdout, "{}", case.name); + output.debug_events + } + (Err(error), Some(message)) => { + assert_eq!(error.message, message, "{}", case.name); + assert_eq!(error.output.stdout, case.stdout, "{}", case.name); + error.output.debug_events + } + (actual, message) => panic!("{}: {actual:?}, expected {message:?}", case.name), + }; + let expected = case + .markers + .iter() + .map(|marker| format!("trace marker: string = {marker}")) + .collect::>(); + assert_eq!(debug_trace_lines(&events), expected, "{}", case.name); + if matches!( + case.name, + "exact_pending_unit" | "exact_proved_pending_unit" | "ancestor_proved_pending_unit" + ) { + assert_eq!( + debug_print_text(&events), + "pending(pending(nothing))\n", + "{} retains both existing pending layers without new validation", + case.name + ); + } + if matches!(case.name, "original_wider" | "original_exact") { + assert_eq!( + jett_driver::render_debug_events(&events), + "5\n", + "{}", + case.name + ); + } + events +} + +#[test] +fn reflected_callable_sources_reference_lazy_results_and_selector_precedence() { + for case in CASES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, case.source).unwrap(); + reference(case, &source); + } +} + +#[test] +fn reflected_callable_sources_emit_objects_and_reject_changed_source_and_predicate_bodies() { + for case in CASES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, case.source).unwrap(); + let lowered = jett_driver::lower_file_for_backend_with_options( + &source, + jett_driver::BuildOptions { release: false }, + ) + .unwrap_or_else(|error| panic!("{}: {error}", case.name)); + let entry = lowered.program_entry.expect("checked ordinary main"); + let object = jett_codegen_cranelift::emit_host_program_object_with_options( + &lowered.mir, + &lowered.interner, + entry, + jett_codegen_cranelift::CodegenOptions { optimize: false }, + ) + .unwrap_or_else(|error| panic!("{}: {error}", case.name)); + assert!(!object.bytes.is_empty(), "{}", case.name); + if case.name == "original_wider" { + for predicate in [false, true] { + let mut forged = lowered.mir.clone(); + let function = forged + .functions + .iter_mut() + .find(|function| { + if predicate { + function.identity.declaration.kind + == jett_hir::DeclarationKind::RefinementPredicate + && function.identity.declaration.namespace == "app" + } else { + function.identity.declaration.name == "five" + && function.identity.declaration.namespace == "app" + } + }) + .expect("original source or exact predicate body"); + let returned = function + .blocks + .iter_mut() + .find_map(|block| match &mut block.terminator.kind { + jett_mir::TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("original typed return"); + returned.kind = if predicate { + jett_hir::ExpressionKind::Bool(false) + } else { + jett_hir::ExpressionKind::Int(6) + }; + let outcome = jett_codegen_cranelift::emit_host_program_object_with_options( + &forged, + &lowered.interner, + entry, + jett_codegen_cranelift::CodegenOptions { optimize: false }, + ); + let error = match outcome { + Err(error) => error, + Ok(object) => panic!( + "public MIR changed {} body but emitted {} object bytes", + if predicate { "predicate" } else { "source" }, + object.bytes.len() + ), + }; + assert!( + matches!(&error, jett_codegen_cranelift::CodegenError::InvalidMir(errors) + if errors.iter().any(|error| error.message.contains("reflected callable"))), + "{error:?}" + ); + } + } + } +} + +#[test] +fn native_reflected_callable_sources_delete_source_in_both_normal_runtime_profiles() { + let measured = + std::env::var_os("JETT_REFLECTED_CALLABLE_NORMAL_ARCHIVES").map(std::path::PathBuf::from); + let evidence = + std::env::var_os("JETT_REFLECTED_CALLABLE_NATIVE_EVIDENCE").map(std::path::PathBuf::from); + if let Some(directory) = &evidence { + fs::create_dir(directory).expect("new native evidence directory"); + } + for case in CASES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, case.source).unwrap(); + let events = reference(case, &source); + let object = jett_driver::native::emit_host_program_object_for_file_with_options( + &source, + // Source/debug policy is independent of the runtime archive profile. + jett_driver::BuildOptions { release: false }, + ) + .unwrap_or_else(|error| panic!("{}: {error}", case.name)); + let case_evidence = evidence.as_ref().map(|root| root.join(case.name)); + if let Some(path) = &case_evidence { + fs::create_dir(path).unwrap(); + fs::write(path.join("source.snapshot"), case.source).unwrap(); + fs::write(path.join("object.obj"), object.bytes()).unwrap(); + } + let mut executables = Vec::new(); + for runtime_release in [false, true] { + let profile = if runtime_release { "release" } else { "debug" }; + let executable = directory + .path() + .join(format!("callback_{runtime_release}.exe")); + let supplied_launcher = measured_launcher::supplied(runtime_release); + let launcher = measured + .as_ref() + .filter(|_| supplied_launcher.is_none()) + .map(|root| { + jett_driver::native::NativeLauncherBundle::from_manifest( + &root.join(format!("{profile}-bundle/launcher.json")), + runtime_release, + ) + .expect("exact measured normal runtime profile manifest") + }); + let launcher = supplied_launcher + .or(launcher.as_ref()) + .unwrap_or_else(|| suite_options::launcher_for_options(runtime_release)); + jett_driver::native::link_host_object(&object, launcher, &executable) + .unwrap_or_else(|error| panic!("{}: {error}", case.name)); + if let Some(path) = &case_evidence { + fs::copy(&executable, path.join(format!("{profile}.exe"))).unwrap(); + } + executables.push((executable, profile)); + } + fs::remove_file(&source).unwrap(); + assert!( + !source.exists(), + "Source must be absent before either launch" + ); + for (executable, profile) in executables { + let output = run_bounded(&executable, directory.path()); + if let Some(path) = &case_evidence { + fs::write( + path.join(format!("{profile}-observation.json")), + serde_json::to_vec_pretty(&serde_json::json!({ + "case": case.name, + "runtime_profile": profile, + "source_build_release": false, + "source_deleted_before_launch": !source.exists(), + "object_target": object.target(), + "object_entry": object.program_entry().index(), + "status": output.status.code(), + "stdout": output.stdout, + "stderr": output.stderr, + "public_ordinary_context_retirement_observation": null, + "retirement_observation_available": false, + })) + .unwrap(), + ) + .unwrap(); + } + assert_eq!( + output.status.code(), + Some(if case.message.is_some() { 71 } else { 0 }), + "{}: {output:?}", + case.name + ); + assert_eq!(output.stdout, case.stdout.as_bytes(), "{}", case.name); + let mut stderr = jett_driver::render_debug_events(&events); + if let Some(message) = case.message { + stderr.push_str(message); + stderr.push('\n'); + } + assert_eq!( + output.stderr, + stderr.as_bytes(), + "{}: {output:?}", + case.name + ); + } + } +} + +const PRODUCTION_CASES: &[Case] = &[ + Case { + name: "production_named_lazy", + source: include_str!("reflected_callable_sources/production_named_lazy.jett"), + stdout: "stored\nsource\nfirst:5\nsource\nsecond:5\nsource\noriginal:5\n", + message: None, + markers: &[], + }, + Case { + name: "production_invalid_return", + source: include_str!("reflected_callable_sources/production_invalid_return.jett"), + stdout: "stored\nsource\n", + message: Some("runtime error: refinement type constraint failed for 'app.Positive'"), + markers: &[], + }, + Case { + name: "production_wrong_selector_owner", + source: include_str!("reflected_callable_sources/production_wrong_selector_owner.jett"), + stdout: "before\n", + message: Some( + "runtime error: type.field_value: field metadata belongs to 'app.Other', expected 'app.Holder'", + ), + markers: &[], + }, +]; + +#[test] +fn native_reflected_callable_production_output_matches_with_optimized_objects_without_source() { + let measured = + std::env::var_os("JETT_REFLECTED_CALLABLE_NORMAL_ARCHIVES").map(std::path::PathBuf::from); + let mut object_count = 0; + let mut execution_count = 0; + for case in PRODUCTION_CASES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, case.source).unwrap(); + let events = reference(case, &source); + assert!(events.is_empty(), "{} uses canonical Stdout", case.name); + let mut executables = Vec::new(); + for optimize in [false, true] { + let object = jett_driver::native::emit_host_program_object_for_file_with_options( + &source, + // Production Source has no debug operations. This option reaches + // CodegenOptions.optimize independently of the runtime profile. + jett_driver::BuildOptions { release: optimize }, + ) + .unwrap_or_else(|error| panic!("{} optimize={optimize}: {error}", case.name)); + object_count += 1; + for runtime_release in [false, true] { + let profile = if runtime_release { "release" } else { "debug" }; + let executable = directory.path().join(format!( + "callback_production_{optimize}_{runtime_release}.exe" + )); + let supplied_launcher = measured_launcher::supplied(runtime_release); + let measured_launcher = measured + .as_ref() + .filter(|_| supplied_launcher.is_none()) + .map(|root| { + jett_driver::native::NativeLauncherBundle::from_manifest( + &root.join(format!("{profile}-bundle/launcher.json")), + runtime_release, + ) + .expect("exact measured normal runtime profile manifest") + }); + let launcher = supplied_launcher + .or(measured_launcher.as_ref()) + .unwrap_or_else(|| suite_options::launcher_for_options(runtime_release)); + jett_driver::native::link_host_object(&object, launcher, &executable) + .unwrap_or_else(|error| { + panic!( + "{} optimize={optimize} runtime={profile}: {error}", + case.name + ) + }); + executables.push((executable, optimize, profile)); + } + } + fs::remove_file(&source).unwrap(); + assert!( + !source.exists(), + "Source must be absent before every launch" + ); + for (executable, optimize, profile) in executables { + let output = run_bounded(&executable, directory.path()); + assert_eq!( + output.status.code(), + Some(if case.message.is_some() { 71 } else { 0 }), + "{} optimize={optimize} runtime={profile}: {output:?}", + case.name + ); + assert_eq!( + output.stdout, + case.stdout.as_bytes(), + "{} optimize={optimize} runtime={profile}: {output:?}", + case.name + ); + let stderr = case + .message + .map(|message| format!("{message}\n")) + .unwrap_or_default(); + assert_eq!( + output.stderr, + stderr.as_bytes(), + "{} optimize={optimize} runtime={profile}: {output:?}", + case.name + ); + execution_count += 1; + } + } + assert_eq!(object_count, PRODUCTION_CASES.len() * 2); + assert_eq!(execution_count, PRODUCTION_CASES.len() * 4); + println!( + "reflected callable production controls: {} Sources, {object_count} objects, {execution_count} Source-deleted executions", + PRODUCTION_CASES.len() + ); +} diff --git a/crates/jett_driver/tests/native_conformance/reflected_iteration_controls.rs b/crates/jett_driver/tests/native_conformance/reflected_iteration_controls.rs new file mode 100644 index 00000000..12262767 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_iteration_controls.rs @@ -0,0 +1,119 @@ +//! Ordinary reflected metadata controls for runtime, comptime, and native suites. +use super::run_bounded; +use super::suite_options::launcher_for_options; +use jett_driver::BuildOptions; +use jett_driver::native::{ + build_host_executable_with_options, build_host_property_suite_executable_with_options, + build_host_verify_suite_executable_with_options, +}; +use std::fs; + +struct Case { + name: &'static str, + source: &'static str, + stdout: &'static str, +} + +macro_rules! case { + ($name:literal) => { + Case { + name: $name, + source: include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/native/reflected_iteration_controls/", + $name, + ".jett" + )), + stdout: include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/native/reflected_iteration_controls/", + $name, + ".stdout" + )), + } + }; +} + +fn run_form(name: &str, source_text: &str, expected_stdout: &str) { + let expected_stdout = expected_stdout.replace("\r\n", "\n"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, source_text).unwrap(); + let reference = jett_driver::run_file_capture_outcome(&source) + .unwrap_or_else(|failure| panic!("{name}: {failure:?}")); + assert_eq!(reference.stdout, expected_stdout, "{name}"); + assert!(reference.debug_events.is_empty(), "{name}: {reference:?}"); + let mut programs = Vec::new(); + let mut suites = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("iteration_{release}.exe")); + build_host_executable_with_options( + &source, + launcher_for_options(release), + &binary, + BuildOptions { release }, + ) + .unwrap_or_else(|failure| panic!("{name}: {failure}")); + programs.push(binary); + let verify = directory + .path() + .join(format!("iteration_verify_{release}.exe")); + let property = directory + .path() + .join(format!("iteration_property_{release}.exe")); + build_host_verify_suite_executable_with_options( + &source, + launcher_for_options(release), + &verify, + BuildOptions { release }, + ) + .unwrap_or_else(|failure| panic!("{name}: {failure}")); + build_host_property_suite_executable_with_options( + &source, + launcher_for_options(release), + &property, + BuildOptions { release }, + ) + .unwrap_or_else(|failure| panic!("{name}: {failure}")); + suites.extend([verify, property]); + } + fs::remove_file(&source).unwrap(); + for binary in programs { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{name}: {actual:?}"); + assert_eq!( + actual.stdout, + expected_stdout.as_bytes(), + "{name}: {actual:?}" + ); + assert!(actual.stderr.is_empty(), "{name}: {actual:?}"); + } + for binary in suites { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{name}: {actual:?}"); + assert!(actual.stdout.is_empty(), "{name}: {actual:?}"); + assert!(actual.stderr.is_empty(), "{name}: {actual:?}"); + } +} + +#[test] +fn native_ordinary_equal_type_metadata_branches_and_pure_helpers_match_reference() { + for case in [ + case!("nominal"), + case!("members"), + case!("arguments_nested"), + ] { + run_form(case.name, case.source, case.stdout); + if matches!(case.name, "nominal" | "members") { + let mut piped = case.source.to_owned(); + for owner in ["Record", "T", "Header"] { + piped = piped.replace( + &format!("type.field_value[{owner}, Field](view source, view field)"), + &format!("source into view type.field_value[{owner}, Field](view field)"), + ); + } + assert_ne!(piped, case.source, "{}: getter pipeline control", case.name); + run_form(&format!("{}_pipeline", case.name), &piped, case.stdout); + } + } +} diff --git a/crates/jett_driver/tests/native_conformance/resource/01_connected_entry.jett b/crates/jett_driver/tests/native_conformance/resource/01_connected_entry.jett new file mode 100644 index 00000000..ecccd22d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/01_connected_entry.jett @@ -0,0 +1,14 @@ +namespace app +function pass_owner(token: resource_probe.TestHandle) returns resource_probe.TestHandle: + return token +function lifecycle(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1) handle error: + return nothing + resource_probe.TestHandle moved = pass_owner(token) + result[int64, string] observed = resource_probe.borrow(view net, view moved) + resource_probe.close(moved) + return nothing +function main(net: Network) returns nothing: + lifecycle(view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/02_partial_actual_failure.jett b/crates/jett_driver/tests/native_conformance/resource/02_partial_actual_failure.jett new file mode 100644 index 00000000..d4318743 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/02_partial_actual_failure.jett @@ -0,0 +1,11 @@ +namespace app +function consume(token: resource_probe.TestHandle, label: int64) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1) handle error: + return nothing + consume(token, resource_probe.terminal_label()) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/03_implicit_drop.jett b/crates/jett_driver/tests/native_conformance/resource/03_implicit_drop.jett new file mode 100644 index 00000000..c4f7dffc --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/03_implicit_drop.jett @@ -0,0 +1,6 @@ +namespace app +function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1) handle error: + return nothing + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/04_live_replace.jett b/crates/jett_driver/tests/native_conformance/resource/04_live_replace.jett new file mode 100644 index 00000000..b0c6ffdd --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/04_live_replace.jett @@ -0,0 +1,11 @@ +namespace app +function main(net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2401) handle error: + return nothing + token = resource_probe.create(view net, 2402) handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/05_self_rebind.jett b/crates/jett_driver/tests/native_conformance/resource/05_self_rebind.jett new file mode 100644 index 00000000..90c458f7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/05_self_rebind.jett @@ -0,0 +1,8 @@ +namespace app +function main(net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2421) handle error: + return nothing + token = token + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/06_replace_closed.jett b/crates/jett_driver/tests/native_conformance/resource/06_replace_closed.jett new file mode 100644 index 00000000..3a169c38 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/06_replace_closed.jett @@ -0,0 +1,10 @@ +namespace app +function main(net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2411) handle error: + return nothing + resource_probe.close(token) + token = resource_probe.create(view net, 2412) handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/07_rhs_domain_failure.jett b/crates/jett_driver/tests/native_conformance/resource/07_rhs_domain_failure.jett new file mode 100644 index 00000000..701fd3c1 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/07_rhs_domain_failure.jett @@ -0,0 +1,15 @@ +namespace app +function observe_previous(view net: Network, view token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function main(net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2431) handle error: + return nothing + token = resource_probe.create(view net, 2432) handle error: + observe_previous(view net, view token) + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/08_rhs_ordinary_failure.jett b/crates/jett_driver/tests/native_conformance/resource/08_rhs_ordinary_failure.jett new file mode 100644 index 00000000..49051902 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/08_rhs_ordinary_failure.jett @@ -0,0 +1,9 @@ +namespace app +function main(net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2441) handle error: + return nothing + token = resource_probe.create(view net, resource_probe.terminal_label()) handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/09_later_handle_minimal.jett b/crates/jett_driver/tests/native_conformance/resource/09_later_handle_minimal.jett new file mode 100644 index 00000000..97660978 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/09_later_handle_minimal.jett @@ -0,0 +1,16 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 502) handle error: + return nothing + accept_staged(token: token, marker: fail_marker() handle error: + return nothing + , net: view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/10_later_handle_original.jett b/crates/jett_driver/tests/native_conformance/resource/10_later_handle_original.jett new file mode 100644 index 00000000..a56e1c27 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/10_later_handle_original.jett @@ -0,0 +1,24 @@ +namespace app +function stage_owned(view net: Network, token: resource_probe.TestHandle) returns resource_probe.TestHandle: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return token + return token +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 501) handle error: + return nothing + accept_staged(token: stage_owned(view net, token), marker: fail_marker() handle error: + return nothing + , net: view net) + return nothing +export function main(net: Network) returns nothing: + scenario(view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/11_later_handle_success.jett b/crates/jett_driver/tests/native_conformance/resource/11_later_handle_success.jett new file mode 100644 index 00000000..0e951718 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/11_later_handle_success.jett @@ -0,0 +1,16 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function successful_marker() returns result[int64, string]: + return ok(7) +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 503) handle error: + return nothing + accept_staged(token: token, marker: successful_marker() handle error: + return nothing + , net: view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/12_later_handle_written_view.jett b/crates/jett_driver/tests/native_conformance/resource/12_later_handle_written_view.jett new file mode 100644 index 00000000..b0e4afff --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/12_later_handle_written_view.jett @@ -0,0 +1,22 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function observe_retained(view net: Network, view token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 retained = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 505) handle error: + return nothing + accept_staged(token: view token, marker: fail_marker() handle error: + observe_retained(view net, view token) + return nothing + , net: view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/13_first_handle_failure.jett b/crates/jett_driver/tests/native_conformance/resource/13_first_handle_failure.jett new file mode 100644 index 00000000..c7bd4031 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/13_first_handle_failure.jett @@ -0,0 +1,16 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function successful_marker() returns result[int64, string]: + return ok(7) +export function main(net: Network) returns nothing: + use resource_probe + accept_staged(token: resource_probe.create(view net, 506) handle error: + return nothing + , marker: successful_marker() handle error: + return nothing + , net: view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/14_first_handle_success.jett b/crates/jett_driver/tests/native_conformance/resource/14_first_handle_success.jett new file mode 100644 index 00000000..fb4e94f8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/14_first_handle_success.jett @@ -0,0 +1,16 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function successful_marker() returns result[int64, string]: + return ok(7) +export function main(net: Network) returns nothing: + use resource_probe + accept_staged(token: resource_probe.create(view net, 507) handle error: + return nothing + , marker: successful_marker() handle error: + return nothing + , net: view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/15_later_handle_two_owned_views.jett b/crates/jett_driver/tests/native_conformance/resource/15_later_handle_two_owned_views.jett new file mode 100644 index 00000000..3e8f5712 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/15_later_handle_two_owned_views.jett @@ -0,0 +1,20 @@ +namespace app +function accept_staged(marker: int64, view first: resource_probe.TestHandle, view second: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed_first = resource_probe.borrow(view net, view first) handle error: + return nothing + int64 observed_second = resource_probe.borrow(view net, view second) handle error: + return nothing + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle first = resource_probe.create(view net, 511) handle error: + return nothing + resource_probe.TestHandle second = resource_probe.create(view net, 512) handle error: + return nothing + accept_staged(first: first, second: second, marker: fail_marker() handle error: + return nothing + , net: view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/16_later_handle_return_other_owner.jett b/crates/jett_driver/tests/native_conformance/resource/16_later_handle_return_other_owner.jett new file mode 100644 index 00000000..e6e4de00 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/16_later_handle_return_other_owner.jett @@ -0,0 +1,27 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +function observe_return(view net: Network, token: resource_probe.TestHandle) returns resource_probe.TestHandle: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return token + return token +function return_retained(view net: Network, token: resource_probe.TestHandle, retained: resource_probe.TestHandle) returns resource_probe.TestHandle: + accept_staged(token: token, marker: fail_marker() handle error: + return observe_return(view net, retained) + , net: view net) + return retained +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 521) handle error: + return nothing + resource_probe.TestHandle retained = resource_probe.create(view net, 522) handle error: + return nothing + resource_probe.TestHandle returned = return_retained(view net, token, retained) + resource_probe.close(returned) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/17_later_handle_default.jett b/crates/jett_driver/tests/native_conformance/resource/17_later_handle_default.jett new file mode 100644 index 00000000..bf16a7ea --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/17_later_handle_default.jett @@ -0,0 +1,16 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + if marker == 7: + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 531) handle error: + return nothing + accept_staged(token: token, marker:(fail_marker() handle error: default 7), net: view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/18_indirect_named_close.jett b/crates/jett_driver/tests/native_conformance/resource/18_indirect_named_close.jett new file mode 100644 index 00000000..b75d5af7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/18_indirect_named_close.jett @@ -0,0 +1,16 @@ +namespace app +function wrong_target(token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 marker = resource_probe.terminal_label() + return nothing +function close_selected(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = close_selected + resource_probe.TestHandle token = resource_probe.create(view net, 701) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/19_indirect_named_terminal_failure.jett b/crates/jett_driver/tests/native_conformance/resource/19_indirect_named_terminal_failure.jett new file mode 100644 index 00000000..eaa7e5a8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/19_indirect_named_terminal_failure.jett @@ -0,0 +1,16 @@ +namespace app +function wrong_target(token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 marker = resource_probe.terminal_label() + return nothing +function close_selected(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = wrong_target + resource_probe.TestHandle token = resource_probe.create(view net, 701) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/20_indirect_named_alias.jett b/crates/jett_driver/tests/native_conformance/resource/20_indirect_named_alias.jett new file mode 100644 index 00000000..c0aa00e0 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/20_indirect_named_alias.jett @@ -0,0 +1,17 @@ +namespace app +function wrong_target(token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 marker = resource_probe.terminal_label() + return nothing +function close_selected(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing selected = close_selected + function(resource_probe.TestHandle) returns nothing dispose = selected + resource_probe.TestHandle token = resource_probe.create(view net, 701) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/21_indirect_namespace_close.jett b/crates/jett_driver/tests/native_conformance/resource/21_indirect_namespace_close.jett new file mode 100644 index 00000000..969f6727 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/21_indirect_namespace_close.jett @@ -0,0 +1,8 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.close + resource_probe.TestHandle token = resource_probe.create(view net, 711) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/22_indirect_namespace_alias.jett b/crates/jett_driver/tests/native_conformance/resource/22_indirect_namespace_alias.jett new file mode 100644 index 00000000..3745559a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/22_indirect_namespace_alias.jett @@ -0,0 +1,9 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.close + function(resource_probe.TestHandle) returns nothing alias = dispose + resource_probe.TestHandle token = resource_probe.create(view net, 711) handle error: + return nothing + alias(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/23_returned_hook_close.jett b/crates/jett_driver/tests/native_conformance/resource/23_returned_hook_close.jett new file mode 100644 index 00000000..0b02ae6f --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/23_returned_hook_close.jett @@ -0,0 +1,8 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.descriptor() + resource_probe.TestHandle token = resource_probe.create(view net, 721) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/24_returned_hook_alias.jett b/crates/jett_driver/tests/native_conformance/resource/24_returned_hook_alias.jett new file mode 100644 index 00000000..bc1f3ae8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/24_returned_hook_alias.jett @@ -0,0 +1,9 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.descriptor() + function(resource_probe.TestHandle) returns nothing alias = dispose + resource_probe.TestHandle token = resource_probe.create(view net, 721) handle error: + return nothing + alias(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/25_returned_hook_relay.jett b/crates/jett_driver/tests/native_conformance/resource/25_returned_hook_relay.jett new file mode 100644 index 00000000..41ac9d49 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/25_returned_hook_relay.jett @@ -0,0 +1,11 @@ +namespace app +function relay() returns function(resource_probe.TestHandle) returns nothing: + use resource_probe + return resource_probe.descriptor() +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = relay() + resource_probe.TestHandle token = resource_probe.create(view net, 721) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/26_returned_hook_unused.jett b/crates/jett_driver/tests/native_conformance/resource/26_returned_hook_unused.jett new file mode 100644 index 00000000..a3f62a81 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/26_returned_hook_unused.jett @@ -0,0 +1,5 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.descriptor() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/27_returned_hook_factory.jett b/crates/jett_driver/tests/native_conformance/resource/27_returned_hook_factory.jett new file mode 100644 index 00000000..60fe237d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/27_returned_hook_factory.jett @@ -0,0 +1,8 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(view Network, int64) returns result[resource_probe.TestHandle, string] create_selected = resource_probe.create_descriptor() + resource_probe.TestHandle token = create_selected(view net, 731) handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/28_returned_hook_borrow.jett b/crates/jett_driver/tests/native_conformance/resource/28_returned_hook_borrow.jett new file mode 100644 index 00000000..bb06d7a0 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/28_returned_hook_borrow.jett @@ -0,0 +1,12 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(view Network, view resource_probe.TestHandle) returns result[int64, string] borrow_selected = resource_probe.borrow_descriptor() + resource_probe.TestHandle token = resource_probe.create(view net, 741) handle error: + return nothing + int64 observed = borrow_selected(view net, view token) handle error: + default 0 + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/29_immediate_returned_hook_close.jett b/crates/jett_driver/tests/native_conformance/resource/29_immediate_returned_hook_close.jett new file mode 100644 index 00000000..2a15c925 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/29_immediate_returned_hook_close.jett @@ -0,0 +1,7 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 751) handle error: + return nothing + resource_probe.descriptor()(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/30_immediate_returned_hook_factory.jett b/crates/jett_driver/tests/native_conformance/resource/30_immediate_returned_hook_factory.jett new file mode 100644 index 00000000..4f9ab427 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/30_immediate_returned_hook_factory.jett @@ -0,0 +1,16 @@ +namespace app +function argument_label(view net: Network, view marker: resource_probe.TestHandle) returns int64: + use resource_probe + int64 label = resource_probe.borrow(view net, view marker) handle error: + default 0 + return label +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle anchor = resource_probe.create(view net, 742) handle error: + return nothing + resource_probe.TestHandle marker = resource_probe.create(view net, 744) handle error: + return nothing + resource_probe.TestHandle token = resource_probe.select_create(view net, view anchor, false)(view net, argument_label(view net, view marker)) handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/31_immediate_returned_hook_callee_failure.jett b/crates/jett_driver/tests/native_conformance/resource/31_immediate_returned_hook_callee_failure.jett new file mode 100644 index 00000000..489613a5 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/31_immediate_returned_hook_callee_failure.jett @@ -0,0 +1,16 @@ +namespace app +function argument_label(view net: Network, view marker: resource_probe.TestHandle) returns int64: + use resource_probe + int64 label = resource_probe.borrow(view net, view marker) handle error: + default 0 + return label +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle anchor = resource_probe.create(view net, 742) handle error: + return nothing + resource_probe.TestHandle marker = resource_probe.create(view net, 744) handle error: + return nothing + resource_probe.TestHandle token = resource_probe.select_create(view net, view anchor, true)(view net, argument_label(view net, view marker)) handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/32_immediate_returned_hook_actual_failure.jett b/crates/jett_driver/tests/native_conformance/resource/32_immediate_returned_hook_actual_failure.jett new file mode 100644 index 00000000..5d8a51a0 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/32_immediate_returned_hook_actual_failure.jett @@ -0,0 +1,17 @@ +namespace app +function argument_label(view net: Network, view marker: resource_probe.TestHandle) returns int64: + use resource_probe + int64 label = resource_probe.borrow(view net, view marker) handle error: + default 0 + int64 ignored = resource_probe.terminal_label() + return label +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle anchor = resource_probe.create(view net, 742) handle error: + return nothing + resource_probe.TestHandle marker = resource_probe.create(view net, 744) handle error: + return nothing + resource_probe.TestHandle token = resource_probe.select_create(view net, view anchor, false)(view net, argument_label(view net, view marker)) handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/33_comptime_hook_close.jett b/crates/jett_driver/tests/native_conformance/resource/33_comptime_hook_close.jett new file mode 100644 index 00000000..c16c8295 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/33_comptime_hook_close.jett @@ -0,0 +1,9 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = comptime resource_probe.descriptor() + int64 label = comptime 761 + resource_probe.TestHandle token = resource_probe.create(view net, label) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/34_comptime_hook_factory.jett b/crates/jett_driver/tests/native_conformance/resource/34_comptime_hook_factory.jett new file mode 100644 index 00000000..e8fe4b75 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/34_comptime_hook_factory.jett @@ -0,0 +1,8 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(view Network, int64) returns result[resource_probe.TestHandle, string] acquire = comptime resource_probe.create_descriptor() + resource_probe.TestHandle token = acquire(view net, 771) handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/35_comptime_hook_borrow.jett b/crates/jett_driver/tests/native_conformance/resource/35_comptime_hook_borrow.jett new file mode 100644 index 00000000..598eb712 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/35_comptime_hook_borrow.jett @@ -0,0 +1,12 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(view Network, view resource_probe.TestHandle) returns result[int64, string] inspect = comptime resource_probe.borrow_descriptor() + resource_probe.TestHandle token = resource_probe.create(view net, 781) handle error: + return nothing + int64 observed = inspect(view net, view token) handle error: + default 0 + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/36_comptime_hook_alias.jett b/crates/jett_driver/tests/native_conformance/resource/36_comptime_hook_alias.jett new file mode 100644 index 00000000..820e6f86 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/36_comptime_hook_alias.jett @@ -0,0 +1,9 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = comptime resource_probe.descriptor() + function(resource_probe.TestHandle) returns nothing alias = dispose + resource_probe.TestHandle token = resource_probe.create(view net, 761) handle error: + return nothing + alias(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/37_comptime_hook_relay.jett b/crates/jett_driver/tests/native_conformance/resource/37_comptime_hook_relay.jett new file mode 100644 index 00000000..d78724a0 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/37_comptime_hook_relay.jett @@ -0,0 +1,11 @@ +namespace app +function relay() returns function(resource_probe.TestHandle) returns nothing: + use resource_probe + return resource_probe.descriptor() +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = comptime relay() + resource_probe.TestHandle token = resource_probe.create(view net, 761) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/38_comptime_hook_unused.jett b/crates/jett_driver/tests/native_conformance/resource/38_comptime_hook_unused.jett new file mode 100644 index 00000000..7a9016c7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/38_comptime_hook_unused.jett @@ -0,0 +1,5 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = comptime resource_probe.descriptor() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/39_immediate_comptime_hook_close.jett b/crates/jett_driver/tests/native_conformance/resource/39_immediate_comptime_hook_close.jett new file mode 100644 index 00000000..e5e15a6c --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/39_immediate_comptime_hook_close.jett @@ -0,0 +1,6 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 761) handle error: + return nothing + return(comptime resource_probe.descriptor())(token) diff --git a/crates/jett_driver/tests/native_conformance/resource/40_comptime_hook_absence.jett b/crates/jett_driver/tests/native_conformance/resource/40_comptime_hook_absence.jett new file mode 100644 index 00000000..bf7c520e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/40_comptime_hook_absence.jett @@ -0,0 +1,8 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + optional[resource_probe.TestHandle] empty = comptime resource_probe.empty() + resource_probe.TestHandle token = empty handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/41_comptime_hook_pure_choice.jett b/crates/jett_driver/tests/native_conformance/resource/41_comptime_hook_pure_choice.jett new file mode 100644 index 00000000..8ad0264c --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/41_comptime_hook_pure_choice.jett @@ -0,0 +1,13 @@ +namespace app +function choose(flag: bool) returns function(resource_probe.TestHandle) returns nothing: + use resource_probe + if flag: + return resource_probe.descriptor() + return resource_probe.descriptor() +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = comptime choose(true) + resource_probe.TestHandle token = resource_probe.create(view net, 761) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/42_comptime_hook_generic.jett b/crates/jett_driver/tests/native_conformance/resource/42_comptime_hook_generic.jett new file mode 100644 index 00000000..42f9ee20 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/42_comptime_hook_generic.jett @@ -0,0 +1,14 @@ +namespace app +function choose[T](flag: bool) returns function(resource_probe.TestHandle) returns nothing: + use resource_probe + string type_label = type.name[T]() + if flag and type_label == "int64": + return resource_probe.descriptor() + return resource_probe.descriptor() +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = comptime choose[int64](true) + resource_probe.TestHandle token = resource_probe.create(view net, 761) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/43_comptime_hook_scoped.jett b/crates/jett_driver/tests/native_conformance/resource/43_comptime_hook_scoped.jett new file mode 100644 index 00000000..5435534b --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/43_comptime_hook_scoped.jett @@ -0,0 +1,13 @@ +namespace app +function choose() returns function(resource_probe.TestHandle) returns nothing: + use resource_probe + comptime type Inner = type.info[int64](): + return resource_probe.descriptor() + return resource_probe.descriptor() +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = comptime choose() + resource_probe.TestHandle token = resource_probe.create(view net, 761) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/44_comptime_scalar_helper.jett b/crates/jett_driver/tests/native_conformance/resource/44_comptime_scalar_helper.jett new file mode 100644 index 00000000..7f2b97b7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/44_comptime_scalar_helper.jett @@ -0,0 +1,12 @@ +namespace app +function selector() returns bool: + return comptime false +export function main(net: Network) returns nothing: + use resource_probe + bool stop = selector() + if stop: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = resource_probe.create(view net, 761) handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/45_view_optional_none_written.jett b/crates/jett_driver/tests/native_conformance/resource/45_view_optional_none_written.jett new file mode 100644 index 00000000..8a7bb79a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/45_view_optional_none_written.jett @@ -0,0 +1,18 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 +export function main(net: Network) returns nothing: + use resource_probe + optional[resource_probe.TestHandle] held = none + int64 observed = observe_optional(view net, view held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/46_view_optional_none_bare.jett b/crates/jett_driver/tests/native_conformance/resource/46_view_optional_none_bare.jett new file mode 100644 index 00000000..91588893 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/46_view_optional_none_bare.jett @@ -0,0 +1,15 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 +export function main(net: Network) returns nothing: + use resource_probe + optional[resource_probe.TestHandle] held = none + int64 observed = observe_optional(view net, held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/47_view_optional_some_written.jett b/crates/jett_driver/tests/native_conformance/resource/47_view_optional_some_written.jett new file mode 100644 index 00000000..fcdb9eab --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/47_view_optional_some_written.jett @@ -0,0 +1,20 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 831) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, view held) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/48_view_optional_some_bare.jett b/crates/jett_driver/tests/native_conformance/resource/48_view_optional_some_bare.jett new file mode 100644 index 00000000..5eae8bd4 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/48_view_optional_some_bare.jett @@ -0,0 +1,17 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 831) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, held) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/49_view_result_fail_written.jett b/crates/jett_driver/tests/native_conformance/resource/49_view_result_fail_written.jett new file mode 100644 index 00000000..21dac3ee --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/49_view_result_fail_written.jett @@ -0,0 +1,22 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 +export function main(net: Network) returns nothing: + use resource_probe + result[resource_probe.TestHandle, string] held = fail("empty") + int64 observed = observe_result(view net, view held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/50_view_result_fail_bare.jett b/crates/jett_driver/tests/native_conformance/resource/50_view_result_fail_bare.jett new file mode 100644 index 00000000..d155b91b --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/50_view_result_fail_bare.jett @@ -0,0 +1,17 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 +export function main(net: Network) returns nothing: + use resource_probe + result[resource_probe.TestHandle, string] held = fail("empty") + int64 observed = observe_result(view net, held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/51_view_result_ok_written.jett b/crates/jett_driver/tests/native_conformance/resource/51_view_result_ok_written.jett new file mode 100644 index 00000000..cc4e815e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/51_view_result_ok_written.jett @@ -0,0 +1,20 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle error: + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 831) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, view held) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/52_view_result_ok_bare.jett b/crates/jett_driver/tests/native_conformance/resource/52_view_result_ok_bare.jett new file mode 100644 index 00000000..cabc7ca1 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/52_view_result_ok_bare.jett @@ -0,0 +1,17 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle error: + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 831) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, held) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/53_view_optional_written_argument_abort.jett b/crates/jett_driver/tests/native_conformance/resource/53_view_optional_written_argument_abort.jett new file mode 100644 index 00000000..b8015fb8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/53_view_optional_written_argument_abort.jett @@ -0,0 +1,22 @@ +namespace app +function observe_optional(view net: Network, filler: resource_probe.TestHandle, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle: + resource_probe.close(filler) + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + default 0 + resource_probe.close(filler) + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 851) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, outcome: view held, filler: resource_probe.create(view net, 852) handle error: + return nothing + ) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/54_view_result_bare_argument_abort.jett b/crates/jett_driver/tests/native_conformance/resource/54_view_result_bare_argument_abort.jett new file mode 100644 index 00000000..3ae29ba7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/54_view_result_bare_argument_abort.jett @@ -0,0 +1,22 @@ +namespace app +function observe_result(view net: Network, filler: resource_probe.TestHandle, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle error: + resource_probe.close(filler) + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + default 0 + resource_probe.close(filler) + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 851) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, outcome: held, filler: resource_probe.create(view net, 852) handle error: + return nothing + ) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/55_view_optional_written_borrow_fail.jett b/crates/jett_driver/tests/native_conformance/resource/55_view_optional_written_borrow_fail.jett new file mode 100644 index 00000000..9509c21a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/55_view_optional_written_borrow_fail.jett @@ -0,0 +1,20 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 861) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, view held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/56_view_result_bare_borrow_fail.jett b/crates/jett_driver/tests/native_conformance/resource/56_view_result_bare_borrow_fail.jett new file mode 100644 index 00000000..b37bfe80 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/56_view_result_bare_borrow_fail.jett @@ -0,0 +1,17 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle error: + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 861) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/57_nested_view_optional_some_written.jett b/crates/jett_driver/tests/native_conformance/resource/57_nested_view_optional_some_written.jett new file mode 100644 index 00000000..7ce1d945 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/57_nested_view_optional_some_written.jett @@ -0,0 +1,22 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle], enabled: bool) returns int64: + use resource_probe + if enabled: + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 + return 0 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, view held, true) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/58_if_optional_written_some.jett b/crates/jett_driver/tests/native_conformance/resource/58_if_optional_written_some.jett new file mode 100644 index 00000000..8c2ce807 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/58_if_optional_written_some.jett @@ -0,0 +1,26 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle], enabled: bool) returns int64: + use resource_probe + mutable int64 observed = 0 + if enabled: + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 disabled = observe_optional(view net, view held, false) + if disabled != 0: + int64 ignored = resource_probe.terminal_label() + int64 observed = observe_optional(view net, view held, true) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/59_if_optional_written_none.jett b/crates/jett_driver/tests/native_conformance/resource/59_if_optional_written_none.jett new file mode 100644 index 00000000..b22832f0 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/59_if_optional_written_none.jett @@ -0,0 +1,24 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle], enabled: bool) returns int64: + use resource_probe + mutable int64 observed = 0 + if enabled: + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + optional[resource_probe.TestHandle] held = none + int64 disabled = observe_optional(view net, view held, false) + if disabled != 0: + int64 ignored = resource_probe.terminal_label() + int64 observed = observe_optional(view net, view held, true) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/60_if_optional_bare_some.jett b/crates/jett_driver/tests/native_conformance/resource/60_if_optional_bare_some.jett new file mode 100644 index 00000000..71713278 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/60_if_optional_bare_some.jett @@ -0,0 +1,26 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle], enabled: bool) returns int64: + use resource_probe + mutable int64 observed = 0 + if enabled: + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle disabled_owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] disabled_held = some(disabled_owner) + int64 disabled = observe_optional(view net, disabled_held, false) + if disabled != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle owner = resource_probe.create(view net, 872) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, held, true) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/61_if_optional_bare_none.jett b/crates/jett_driver/tests/native_conformance/resource/61_if_optional_bare_none.jett new file mode 100644 index 00000000..4a36e386 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/61_if_optional_bare_none.jett @@ -0,0 +1,22 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle], enabled: bool) returns int64: + use resource_probe + mutable int64 observed = 0 + if enabled: + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + optional[resource_probe.TestHandle] disabled_held = none + int64 disabled = observe_optional(view net, disabled_held, false) + if disabled != 0: + int64 ignored = resource_probe.terminal_label() + optional[resource_probe.TestHandle] held = none + int64 observed = observe_optional(view net, held, true) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/62_if_result_written_ok.jett b/crates/jett_driver/tests/native_conformance/resource/62_if_result_written_ok.jett new file mode 100644 index 00000000..1e4dfcc5 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/62_if_result_written_ok.jett @@ -0,0 +1,30 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string], enabled: bool) returns int64: + use resource_probe + mutable int64 observed = 0 + if enabled: + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 disabled = observe_result(view net, view held, false) + if disabled != 0: + int64 ignored = resource_probe.terminal_label() + int64 observed = observe_result(view net, view held, true) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/63_if_result_written_fail.jett b/crates/jett_driver/tests/native_conformance/resource/63_if_result_written_fail.jett new file mode 100644 index 00000000..0eb5ebd5 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/63_if_result_written_fail.jett @@ -0,0 +1,28 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string], enabled: bool) returns int64: + use resource_probe + mutable int64 observed = 0 + if enabled: + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + result[resource_probe.TestHandle, string] held = fail("empty") + int64 disabled = observe_result(view net, view held, false) + if disabled != 0: + int64 ignored = resource_probe.terminal_label() + int64 observed = observe_result(view net, view held, true) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/64_if_result_bare_ok.jett b/crates/jett_driver/tests/native_conformance/resource/64_if_result_bare_ok.jett new file mode 100644 index 00000000..34a46a77 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/64_if_result_bare_ok.jett @@ -0,0 +1,28 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string], enabled: bool) returns int64: + use resource_probe + mutable int64 observed = 0 + if enabled: + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle disabled_owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] disabled_held = ok(disabled_owner) + int64 disabled = observe_result(view net, disabled_held, false) + if disabled != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle owner = resource_probe.create(view net, 872) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, held, true) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/65_if_result_bare_fail.jett b/crates/jett_driver/tests/native_conformance/resource/65_if_result_bare_fail.jett new file mode 100644 index 00000000..73d50356 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/65_if_result_bare_fail.jett @@ -0,0 +1,24 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string], enabled: bool) returns int64: + use resource_probe + mutable int64 observed = 0 + if enabled: + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + result[resource_probe.TestHandle, string] disabled_held = fail("empty") + int64 disabled = observe_result(view net, disabled_held, false) + if disabled != 0: + int64 ignored = resource_probe.terminal_label() + result[resource_probe.TestHandle, string] held = fail("empty") + int64 observed = observe_result(view net, held, true) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/66_scope_optional_written_some.jett b/crates/jett_driver/tests/native_conformance/resource/66_scope_optional_written_some.jett new file mode 100644 index 00000000..49086a77 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/66_scope_optional_written_some.jett @@ -0,0 +1,23 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, view held) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/67_scope_optional_written_none.jett b/crates/jett_driver/tests/native_conformance/resource/67_scope_optional_written_none.jett new file mode 100644 index 00000000..11b412c3 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/67_scope_optional_written_none.jett @@ -0,0 +1,21 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + optional[resource_probe.TestHandle] held = none + int64 observed = observe_optional(view net, view held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/68_scope_optional_bare_some.jett b/crates/jett_driver/tests/native_conformance/resource/68_scope_optional_bare_some.jett new file mode 100644 index 00000000..d867e34b --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/68_scope_optional_bare_some.jett @@ -0,0 +1,20 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, held) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/69_scope_optional_bare_none.jett b/crates/jett_driver/tests/native_conformance/resource/69_scope_optional_bare_none.jett new file mode 100644 index 00000000..519c73f2 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/69_scope_optional_bare_none.jett @@ -0,0 +1,18 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + optional[resource_probe.TestHandle] held = none + int64 observed = observe_optional(view net, held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/70_scope_result_written_ok.jett b/crates/jett_driver/tests/native_conformance/resource/70_scope_result_written_ok.jett new file mode 100644 index 00000000..3c2b76ba --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/70_scope_result_written_ok.jett @@ -0,0 +1,27 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, view held) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/71_scope_result_written_fail.jett b/crates/jett_driver/tests/native_conformance/resource/71_scope_result_written_fail.jett new file mode 100644 index 00000000..4be5b3df --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/71_scope_result_written_fail.jett @@ -0,0 +1,25 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + result[resource_probe.TestHandle, string] held = fail("empty") + int64 observed = observe_result(view net, view held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/72_scope_result_bare_ok.jett b/crates/jett_driver/tests/native_conformance/resource/72_scope_result_bare_ok.jett new file mode 100644 index 00000000..0f5126af --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/72_scope_result_bare_ok.jett @@ -0,0 +1,22 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, held) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/73_scope_result_bare_fail.jett b/crates/jett_driver/tests/native_conformance/resource/73_scope_result_bare_fail.jett new file mode 100644 index 00000000..e4894503 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/73_scope_result_bare_fail.jett @@ -0,0 +1,20 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + result[resource_probe.TestHandle, string] held = fail("empty") + int64 observed = observe_result(view net, held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/74_while_optional_written_some.jett b/crates/jett_driver/tests/native_conformance/resource/74_while_optional_written_some.jett new file mode 100644 index 00000000..dda12c17 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/74_while_optional_written_some.jett @@ -0,0 +1,26 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + mutable int64 remaining = 2 + mutable int64 total = 0 + while remaining > 0: + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return 0 + total = total + observed + remaining = remaining - 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, view held) + if observed != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/75_while_optional_written_none.jett b/crates/jett_driver/tests/native_conformance/resource/75_while_optional_written_none.jett new file mode 100644 index 00000000..6693b253 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/75_while_optional_written_none.jett @@ -0,0 +1,24 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + mutable int64 remaining = 2 + mutable int64 total = 0 + while remaining > 0: + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return 0 + total = total + observed + remaining = remaining - 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + optional[resource_probe.TestHandle] held = none + int64 observed = observe_optional(view net, view held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/76_while_optional_bare_some.jett b/crates/jett_driver/tests/native_conformance/resource/76_while_optional_bare_some.jett new file mode 100644 index 00000000..82128d9e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/76_while_optional_bare_some.jett @@ -0,0 +1,23 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + mutable int64 remaining = 2 + mutable int64 total = 0 + while remaining > 0: + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return 0 + total = total + observed + remaining = remaining - 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, held) + if observed != 34: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/77_while_optional_bare_none.jett b/crates/jett_driver/tests/native_conformance/resource/77_while_optional_bare_none.jett new file mode 100644 index 00000000..edf4f740 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/77_while_optional_bare_none.jett @@ -0,0 +1,21 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + mutable int64 remaining = 2 + mutable int64 total = 0 + while remaining > 0: + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return 0 + total = total + observed + remaining = remaining - 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + optional[resource_probe.TestHandle] held = none + int64 observed = observe_optional(view net, held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/78_while_result_written_ok.jett b/crates/jett_driver/tests/native_conformance/resource/78_while_result_written_ok.jett new file mode 100644 index 00000000..5af3cd4f --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/78_while_result_written_ok.jett @@ -0,0 +1,30 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 remaining = 2 + mutable int64 total = 0 + while remaining > 0: + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return 0 + total = total + observed + remaining = remaining - 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, view held) + if observed != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/79_while_result_written_fail.jett b/crates/jett_driver/tests/native_conformance/resource/79_while_result_written_fail.jett new file mode 100644 index 00000000..fb1634ca --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/79_while_result_written_fail.jett @@ -0,0 +1,28 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 remaining = 2 + mutable int64 total = 0 + while remaining > 0: + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return 0 + total = total + observed + remaining = remaining - 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + result[resource_probe.TestHandle, string] held = fail("empty") + int64 observed = observe_result(view net, view held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/80_while_result_bare_ok.jett b/crates/jett_driver/tests/native_conformance/resource/80_while_result_bare_ok.jett new file mode 100644 index 00000000..3f019392 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/80_while_result_bare_ok.jett @@ -0,0 +1,25 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 remaining = 2 + mutable int64 total = 0 + while remaining > 0: + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return 0 + total = total + observed + remaining = remaining - 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, held) + if observed != 34: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/81_while_result_bare_fail.jett b/crates/jett_driver/tests/native_conformance/resource/81_while_result_bare_fail.jett new file mode 100644 index 00000000..fcadaaf2 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/81_while_result_bare_fail.jett @@ -0,0 +1,23 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 remaining = 2 + mutable int64 total = 0 + while remaining > 0: + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return 0 + total = total + observed + remaining = remaining - 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + result[resource_probe.TestHandle, string] held = fail("empty") + int64 observed = observe_result(view net, held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/82_if_owned_optional_reuse.jett b/crates/jett_driver/tests/native_conformance/resource/82_if_owned_optional_reuse.jett new file mode 100644 index 00000000..2ad34f70 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/82_if_owned_optional_reuse.jett @@ -0,0 +1,27 @@ +namespace app +function exercise_optional(view net: Network, label: int64, enabled: bool) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, label) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 observed = 0 + if enabled: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + observed = resource_probe.borrow(view net, view token) handle error: + return nothing + if enabled: + if observed != 17: + int64 ignored = resource_probe.terminal_label() + else: + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing +export function main(net: Network) returns nothing: + exercise_optional(view net, 871, false) + exercise_optional(view net, 872, true) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/83_if_owned_result_reuse.jett b/crates/jett_driver/tests/native_conformance/resource/83_if_owned_result_reuse.jett new file mode 100644 index 00000000..26280998 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/83_if_owned_result_reuse.jett @@ -0,0 +1,29 @@ +namespace app +function exercise_result(view net: Network, label: int64, enabled: bool) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, label) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + mutable int64 observed = 0 + if enabled: + resource_probe.TestHandle token = view((view held) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + ) + observed = resource_probe.borrow(view net, view token) handle error: + return nothing + if enabled: + if observed != 17: + int64 ignored = resource_probe.terminal_label() + else: + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle error: + return nothing + resource_probe.close(final_token) + return nothing +export function main(net: Network) returns nothing: + exercise_result(view net, 871, false) + exercise_result(view net, 872, true) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/84_scope_owned_optional_reuse.jett b/crates/jett_driver/tests/native_conformance/resource/84_scope_owned_optional_reuse.jett new file mode 100644 index 00000000..58a532b6 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/84_scope_owned_optional_reuse.jett @@ -0,0 +1,19 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + observed = resource_probe.borrow(view net, view token) handle error: + return nothing + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/85_scope_owned_result_reuse.jett b/crates/jett_driver/tests/native_conformance/resource/85_scope_owned_result_reuse.jett new file mode 100644 index 00000000..c2500a8b --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/85_scope_owned_result_reuse.jett @@ -0,0 +1,21 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view held) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + ) + observed = resource_probe.borrow(view net, view token) handle error: + return nothing + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle error: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/86_while_owned_optional_reuse.jett b/crates/jett_driver/tests/native_conformance/resource/86_while_owned_optional_reuse.jett new file mode 100644 index 00000000..14c24282 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/86_while_owned_optional_reuse.jett @@ -0,0 +1,22 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + mutable int64 remaining = 2 + while remaining > 0: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + remaining = remaining - 1 + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/87_while_owned_result_reuse.jett b/crates/jett_driver/tests/native_conformance/resource/87_while_owned_result_reuse.jett new file mode 100644 index 00000000..4ea52874 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/87_while_owned_result_reuse.jett @@ -0,0 +1,24 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + mutable int64 total = 0 + mutable int64 remaining = 2 + while remaining > 0: + resource_probe.TestHandle token = view((view held) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + remaining = remaining - 1 + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle error: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/88_if_sibling_optional_bindings.jett b/crates/jett_driver/tests/native_conformance/resource/88_if_sibling_optional_bindings.jett new file mode 100644 index 00000000..255c37b7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/88_if_sibling_optional_bindings.jett @@ -0,0 +1,38 @@ +namespace app +function observe_optional(view net: Network, view first_outcome: optional[resource_probe.TestHandle], view second_outcome: optional[resource_probe.TestHandle], choose_first: bool) returns int64: + use resource_probe + mutable int64 observed = 0 + if choose_first: + resource_probe.TestHandle token = view((view first_outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + else: + resource_probe.TestHandle token = view((view second_outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle first_owner = resource_probe.create(view net, 891) handle error: + return nothing + optional[resource_probe.TestHandle] first_held = some(first_owner) + resource_probe.TestHandle second_owner = resource_probe.create(view net, 892) handle error: + return nothing + optional[resource_probe.TestHandle] second_held = some(second_owner) + int64 first_observed = observe_optional(view net, view first_held, view second_held, true) + if first_observed != 17: + int64 ignored = resource_probe.terminal_label() + int64 second_observed = observe_optional(view net, view first_held, view second_held, false) + if second_observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle first_token = first_held handle: + return nothing + resource_probe.close(first_token) + resource_probe.TestHandle second_token = second_held handle: + return nothing + resource_probe.close(second_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/89_scope_sibling_result_bindings.jett b/crates/jett_driver/tests/native_conformance/resource/89_scope_sibling_result_bindings.jett new file mode 100644 index 00000000..92a58600 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/89_scope_sibling_result_bindings.jett @@ -0,0 +1,40 @@ +namespace app +function observe_result(view net: Network, view first_outcome: result[resource_probe.TestHandle, string], view second_outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Left = type.info[int64](): + resource_probe.TestHandle token = view((view first_outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + comptime type Right = type.info[int64](): + resource_probe.TestHandle token = view((view second_outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + int64 current = resource_probe.borrow(view net, view token) handle error: + return 0 + observed = observed + current + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle first_owner = resource_probe.create(view net, 891) handle error: + return nothing + result[resource_probe.TestHandle, string] first_held = ok(first_owner) + resource_probe.TestHandle second_owner = resource_probe.create(view net, 892) handle error: + return nothing + result[resource_probe.TestHandle, string] second_held = ok(second_owner) + int64 observed = observe_result(view net, view first_held, view second_held) + if observed != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle first_token = first_held handle error: + return nothing + resource_probe.close(first_token) + resource_probe.TestHandle second_token = second_held handle error: + return nothing + resource_probe.close(second_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/90_scope_optional_written_borrow_fail.jett b/crates/jett_driver/tests/native_conformance/resource/90_scope_optional_written_borrow_fail.jett new file mode 100644 index 00000000..dbb2e832 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/90_scope_optional_written_borrow_fail.jett @@ -0,0 +1,25 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + if error != "borrow sentinel": + int64 ignored = resource_probe.terminal_label() + default 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, view held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/91_scope_result_bare_borrow_fail.jett b/crates/jett_driver/tests/native_conformance/resource/91_scope_result_bare_borrow_fail.jett new file mode 100644 index 00000000..68e6a392 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/91_scope_result_bare_borrow_fail.jett @@ -0,0 +1,24 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + if error != "borrow sentinel": + int64 ignored = resource_probe.terminal_label() + default 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/92_while_optional_bare_borrow_fail.jett b/crates/jett_driver/tests/native_conformance/resource/92_while_optional_bare_borrow_fail.jett new file mode 100644 index 00000000..38337fcb --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/92_while_optional_bare_borrow_fail.jett @@ -0,0 +1,25 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + mutable int64 total = 0 + mutable int64 remaining = 2 + while remaining > 0: + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + if error != "borrow sentinel": + int64 ignored = resource_probe.terminal_label() + default 0 + total = total + observed + remaining = remaining - 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/93_while_result_written_borrow_fail.jett b/crates/jett_driver/tests/native_conformance/resource/93_while_result_written_borrow_fail.jett new file mode 100644 index 00000000..aaa66729 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/93_while_result_written_borrow_fail.jett @@ -0,0 +1,30 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 total = 0 + mutable int64 remaining = 2 + while remaining > 0: + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + if error != "borrow sentinel": + int64 ignored = resource_probe.terminal_label() + default 0 + total = total + observed + remaining = remaining - 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, view held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle error: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/94_while_owned_optional_break.jett b/crates/jett_driver/tests/native_conformance/resource/94_while_owned_optional_break.jett new file mode 100644 index 00000000..f80726df --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/94_while_owned_optional_break.jett @@ -0,0 +1,22 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 observed = 0 + mutable int64 remaining = 2 + while remaining > 0: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + observed = resource_probe.borrow(view net, view token) handle error: + return nothing + remaining = remaining - 1 + break + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/95_while_owned_result_continue.jett b/crates/jett_driver/tests/native_conformance/resource/95_while_owned_result_continue.jett new file mode 100644 index 00000000..6cf316dc --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/95_while_owned_result_continue.jett @@ -0,0 +1,25 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + mutable int64 total = 0 + mutable int64 remaining = 2 + while remaining > 0: + resource_probe.TestHandle token = view((view held) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + remaining = remaining - 1 + continue + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle error: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/control_flow/01_for_owned_optional_reuse.jett b/crates/jett_driver/tests/native_conformance/resource/control_flow/01_for_owned_optional_reuse.jett new file mode 100644 index 00000000..779604c5 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/control_flow/01_for_owned_optional_reuse.jett @@ -0,0 +1,21 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + list[int64] phases = list(1, 2) + for phase in phases: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/control_flow/02_for_owned_optional_break.jett b/crates/jett_driver/tests/native_conformance/resource/control_flow/02_for_owned_optional_break.jett new file mode 100644 index 00000000..8cf6020a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/control_flow/02_for_owned_optional_break.jett @@ -0,0 +1,21 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 observed = 0 + list[int64] phases = list(1, 2) + for phase in phases: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + observed = resource_probe.borrow(view net, view token) handle error: + return nothing + break + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/control_flow/03_for_owned_result_continue.jett b/crates/jett_driver/tests/native_conformance/resource/control_flow/03_for_owned_result_continue.jett new file mode 100644 index 00000000..853e8316 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/control_flow/03_for_owned_result_continue.jett @@ -0,0 +1,24 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + mutable int64 total = 0 + list[int64] phases = list(1, 2) + for phase in phases: + resource_probe.TestHandle token = view((view held) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + continue + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle error: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/control_flow/04_match_owned_optional_first.jett b/crates/jett_driver/tests/native_conformance/resource/control_flow/04_match_owned_optional_first.jett new file mode 100644 index 00000000..8092050a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/control_flow/04_match_owned_optional_first.jett @@ -0,0 +1,30 @@ +namespace app +enum Route: + first + second +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 observed = 0 + Route route = Route.first + match route: + first: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + observed = resource_probe.borrow(view net, view token) handle error: + return nothing + second: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + observed = resource_probe.borrow(view net, view token) handle error: + return nothing + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/control_flow/05_match_owned_optional_second.jett b/crates/jett_driver/tests/native_conformance/resource/control_flow/05_match_owned_optional_second.jett new file mode 100644 index 00000000..23246401 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/control_flow/05_match_owned_optional_second.jett @@ -0,0 +1,30 @@ +namespace app +enum Route: + first + second +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 observed = 0 + Route route = Route.second + match route: + first: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + observed = resource_probe.borrow(view net, view token) handle error: + return nothing + second: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + observed = resource_probe.borrow(view net, view token) handle error: + return nothing + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/control_flow/07_consecutive_for_owned_optional_reuse.jett b/crates/jett_driver/tests/native_conformance/resource/control_flow/07_consecutive_for_owned_optional_reuse.jett new file mode 100644 index 00000000..95b3591e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/control_flow/07_consecutive_for_owned_optional_reuse.jett @@ -0,0 +1,29 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + list[int64] first_phases = list(1, 2) + for phase in first_phases: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + list[int64] second_phases = list(3, 4) + for phase in second_phases: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + if total != 68: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/control_flow/08_nested_for_owned_optional_reuse.jett b/crates/jett_driver/tests/native_conformance/resource/control_flow/08_nested_for_owned_optional_reuse.jett new file mode 100644 index 00000000..bf2d8684 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/control_flow/08_nested_for_owned_optional_reuse.jett @@ -0,0 +1,29 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + list[int64] outer_phases = list(1, 2) + for outer_phase in outer_phases: + resource_probe.TestHandle outer_token = view((view held) handle: + return nothing + ) + int64 outer_observed = resource_probe.borrow(view net, view outer_token) handle error: + return nothing + total = total + outer_observed + list[int64] inner_phases = list(3, 4) + for inner_phase in inner_phases: + resource_probe.TestHandle inner_token = view((view held) handle: + return nothing + ) + int64 inner_observed = resource_probe.borrow(view net, view inner_token) handle error: + return nothing + total = total + inner_observed + if total != 102: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/18_pipeline_construct_move_close.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/18_pipeline_construct_move_close.jett new file mode 100644 index 00000000..51e154b7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/18_pipeline_construct_move_close.jett @@ -0,0 +1,12 @@ +namespace app +function move_owned(token: resource_probe.TestHandle) returns resource_probe.TestHandle: + return token +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = net into view resource_probe.create(1801) handle error: + return nothing + token into move_owned() into resource_probe.close() + return nothing +export function main(net: Network) returns nothing: + scenario(view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/19_pipeline_written_view_borrow_fail.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/19_pipeline_written_view_borrow_fail.jett new file mode 100644 index 00000000..ad31ad94 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/19_pipeline_written_view_borrow_fail.jett @@ -0,0 +1,18 @@ +namespace app +function read_first(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1901) handle error: + return -1 + int64 observed = token into view read_first(view net) handle error: + return -2 + resource_probe.close(token) + return observed +export function main(net: Network) returns nothing: + use resource_probe + int64 observed = scenario(view net) + if observed != -2: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/19_pipeline_written_view_construct_fail.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/19_pipeline_written_view_construct_fail.jett new file mode 100644 index 00000000..049c2b67 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/19_pipeline_written_view_construct_fail.jett @@ -0,0 +1,18 @@ +namespace app +function read_first(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1901) handle error: + return -1 + int64 observed = token into view read_first(view net) handle error: + return -2 + resource_probe.close(token) + return observed +export function main(net: Network) returns nothing: + use resource_probe + int64 observed = scenario(view net) + if observed != -1: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/19_pipeline_written_view_retains_owner.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/19_pipeline_written_view_retains_owner.jett new file mode 100644 index 00000000..ddd72270 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/19_pipeline_written_view_retains_owner.jett @@ -0,0 +1,18 @@ +namespace app +function read_first(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1901) handle error: + return -1 + int64 observed = token into view read_first(view net) handle error: + return -2 + resource_probe.close(token) + return observed +export function main(net: Network) returns nothing: + use resource_probe + int64 observed = scenario(view net) + if observed != 5: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/20_pipeline_bare_owner_borrow_fail.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/20_pipeline_bare_owner_borrow_fail.jett new file mode 100644 index 00000000..8e5ec656 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/20_pipeline_bare_owner_borrow_fail.jett @@ -0,0 +1,17 @@ +namespace app +function read_first(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 2001) handle error: + return -1 + int64 observed = token into read_first(view net) handle error: + return -2 + return observed +export function main(net: Network) returns nothing: + use resource_probe + int64 observed = scenario(view net) + if observed != -2: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/20_pipeline_bare_owner_construct_fail.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/20_pipeline_bare_owner_construct_fail.jett new file mode 100644 index 00000000..fb437fff --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/20_pipeline_bare_owner_construct_fail.jett @@ -0,0 +1,17 @@ +namespace app +function read_first(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 2001) handle error: + return -1 + int64 observed = token into read_first(view net) handle error: + return -2 + return observed +export function main(net: Network) returns nothing: + use resource_probe + int64 observed = scenario(view net) + if observed != -1: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/20_pipeline_bare_owner_view_operation.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/20_pipeline_bare_owner_view_operation.jett new file mode 100644 index 00000000..ca1d1167 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/20_pipeline_bare_owner_view_operation.jett @@ -0,0 +1,17 @@ +namespace app +function read_first(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 2001) handle error: + return -1 + int64 observed = token into read_first(view net) handle error: + return -2 + return observed +export function main(net: Network) returns nothing: + use resource_probe + int64 observed = scenario(view net) + if observed != 5: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/21_pipeline_named_actual_abort.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/21_pipeline_named_actual_abort.jett new file mode 100644 index 00000000..658660bd --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/21_pipeline_named_actual_abort.jett @@ -0,0 +1,25 @@ +namespace app +function stage_owned(view net: Network, token: resource_probe.TestHandle) returns resource_probe.TestHandle: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return token + return token +function accept_staged(token: resource_probe.TestHandle, marker: int64, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + resource_probe.close(token) + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 2101) handle error: + return nothing + stage_owned(view net, token) into accept_staged(net: view net, marker: fail_marker() handle error: + return nothing + ) + return nothing +export function main(net: Network) returns nothing: + scenario(view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/22_pipeline_domain_failure_stops_next_step.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/22_pipeline_domain_failure_stops_next_step.jett new file mode 100644 index 00000000..081c061d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/22_pipeline_domain_failure_stops_next_step.jett @@ -0,0 +1,13 @@ +namespace app +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = net into view resource_probe.create(2201) handle error: + return -1 + token into resource_probe.close() + return 9 +export function main(net: Network) returns nothing: + use resource_probe + int64 observed = scenario(view net) + if observed != -1: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/22_pipeline_domain_success_close.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/22_pipeline_domain_success_close.jett new file mode 100644 index 00000000..3bed80e8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/22_pipeline_domain_success_close.jett @@ -0,0 +1,13 @@ +namespace app +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = net into view resource_probe.create(2201) handle error: + return -1 + token into resource_probe.close() + return 9 +export function main(net: Network) returns nothing: + use resource_probe + int64 observed = scenario(view net) + if observed != 9: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/23_pipeline_indirect_close_descriptor.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/23_pipeline_indirect_close_descriptor.jett new file mode 100644 index 00000000..da5ad406 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/23_pipeline_indirect_close_descriptor.jett @@ -0,0 +1,11 @@ +namespace app +export function scenario(view net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing close_token = resource_probe.descriptor() + resource_probe.TestHandle token = resource_probe.create(view net, 2301) handle error: + return nothing + token into close_token + return nothing +export function main(net: Network) returns nothing: + scenario(view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/01_original.jett b/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/01_original.jett new file mode 100644 index 00000000..44c19d0e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/01_original.jett @@ -0,0 +1,24 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/02_index_distinct.jett b/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/02_index_distinct.jett new file mode 100644 index 00000000..8a0c2fc3 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/02_index_distinct.jett @@ -0,0 +1,27 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + return nothing + if field.index == 0: + total = total + observed + else: + total = total + observed + 1 + if total != 35: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/03_name_distinct.jett b/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/03_name_distinct.jett new file mode 100644 index 00000000..68ab8f1f --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/03_name_distinct.jett @@ -0,0 +1,27 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + return nothing + if field.name == "first": + total = total + observed + else: + total = total + observed + 1 + if total != 35: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/04_ordinary_helper.jett b/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/04_ordinary_helper.jett new file mode 100644 index 00000000..8e72a798 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/04_ordinary_helper.jett @@ -0,0 +1,36 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 +function field_ordinal_total() returns int64: + mutable int64 total = 0 + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + Element observed = 17 + if field.index == 0: + total = total + observed + else: + total = total + observed + 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + if field_ordinal_total() != 35: + int64 ignored = resource_probe.terminal_label() + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/resource_probe.jett b/crates/jett_driver/tests/native_conformance/resource/resource_probe.jett new file mode 100644 index 00000000..77588e58 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/resource_probe.jett @@ -0,0 +1,27 @@ +namespace resource_probe +export resource TestHandle +export function create(view net: Network, label: int64) returns result[TestHandle, string]: + return kernel_create(label: label, net: view net) +export function borrow(view net: Network, view token: TestHandle) returns result[int64, string]: + return kernel_borrow(view net, view token) +export function close(token: TestHandle) returns nothing: + kernel_close(token) + return nothing +export function empty() returns optional[TestHandle]: + return none +export function descriptor() returns function(TestHandle) returns nothing: + return kernel_close +export function terminal_label() returns int64: + list[int64] numbers = list(1) + list[int64] removed = list.__remove_at[int64](numbers, -1) + return 1 +export function create_descriptor() returns function(view Network, int64) returns result[TestHandle, string]: + return kernel_create +export function borrow_descriptor() returns function(view Network, view TestHandle) returns result[int64, string]: + return kernel_borrow +export function select_create(view net: Network, view anchor: TestHandle, fail_now: bool) returns function(view Network, int64) returns result[TestHandle, string]: + int64 observed = borrow(view net, view anchor) handle error: + default 0 + if fail_now: + int64 ignored = terminal_label() + return kernel_create diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/01_empty_tokens.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/01_empty_tokens.jett new file mode 100644 index 00000000..106c9e23 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/01_empty_tokens.jett @@ -0,0 +1,84 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +export function main(net: Network) returns nothing: + list[resource_probe.TestHandle] values = empty_tokens() + if list.length[resource_probe.TestHandle](view values) != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/02_absent_tokens.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/02_absent_tokens.jett new file mode 100644 index 00000000..fece130d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/02_absent_tokens.jett @@ -0,0 +1,96 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false + +export function main(net: Network) returns nothing: + list[optional[resource_probe.TestHandle]] values = absent_tokens() + if list.length[optional[resource_probe.TestHandle]](view values) != 2: + int64 ignored = resource_probe.terminal_label() + mutable int64 absent_count = 0 + for candidate in values: + if source17_absent_optional(view candidate): + absent_count = absent_count + 1 + if absent_count != 2: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/03_empty_map.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/03_empty_map.jett new file mode 100644 index 00000000..01bca721 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/03_empty_map.jett @@ -0,0 +1,84 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +export function main(net: Network) returns nothing: + map[string, resource_probe.TestHandle] values = empty_map() + if map.length[string, resource_probe.TestHandle](view values) != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/04_absent_map.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/04_absent_map.jett new file mode 100644 index 00000000..805fd1be --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/04_absent_map.jett @@ -0,0 +1,98 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false + +export function main(net: Network) returns nothing: + map[string, optional[resource_probe.TestHandle]] values = absent_map() + if map.length[string, optional[resource_probe.TestHandle]](view values) != 1: + int64 ignored = resource_probe.terminal_label() + mutable int64 absent_count = 0 + for key, candidate in values: + if key != "empty": + int64 ignored = resource_probe.terminal_label() + if source17_absent_optional(view candidate): + absent_count = absent_count + 1 + if absent_count != 1: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/05_empty_result_tokens.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/05_empty_result_tokens.jett new file mode 100644 index 00000000..83606a00 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/05_empty_result_tokens.jett @@ -0,0 +1,86 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +export function main(net: Network) returns nothing: + list[resource_probe.TestHandle] values = empty_result_tokens() handle error: + int64 ignored = resource_probe.terminal_label() + return nothing + if list.length[resource_probe.TestHandle](view values) != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/06_failed_result_tokens.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/06_failed_result_tokens.jett new file mode 100644 index 00000000..caf33d0a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/06_failed_result_tokens.jett @@ -0,0 +1,86 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +export function main(net: Network) returns nothing: + list[resource_probe.TestHandle] values = failed_result_tokens() handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/07_absent_envelope.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/07_absent_envelope.jett new file mode 100644 index 00000000..de8f7bf4 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/07_absent_envelope.jett @@ -0,0 +1,94 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false + +export function main(net: Network) returns nothing: + TokenEnvelope value = absent_envelope() + if value.marker != 17: + int64 ignored = resource_probe.terminal_label() + if list.length[resource_probe.TestHandle](view value.tokens) != 0: + int64 ignored = resource_probe.terminal_label() + if !source17_absent_optional(view value.fallback): + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/08_absent_box.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/08_absent_box.jett new file mode 100644 index 00000000..acb9f717 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/08_absent_box.jett @@ -0,0 +1,84 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +export function main(net: Network) returns nothing: + TokenBox[resource_probe.TestHandle] value = absent_box() + if list.length[resource_probe.TestHandle](view value.items) != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/09_empty_choice.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/09_empty_choice.jett new file mode 100644 index 00000000..fcd6e511 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/09_empty_choice.jett @@ -0,0 +1,87 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +export function main(net: Network) returns nothing: + TokenChoice value = empty_choice() + match value: + empty: + return nothing + other: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/10_absent_choice.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/10_absent_choice.jett new file mode 100644 index 00000000..c829b3b7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/10_absent_choice.jett @@ -0,0 +1,91 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +export function main(net: Network) returns nothing: + TokenChoice value = absent_choice() + match value: + vacant(marker, tokens): + if marker != 17: + int64 ignored = resource_probe.terminal_label() + if list.length[resource_probe.TestHandle](view tokens) != 0: + int64 ignored = resource_probe.terminal_label() + return nothing + other: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/11_absent_state.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/11_absent_state.jett new file mode 100644 index 00000000..f2ae060f --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/11_absent_state.jett @@ -0,0 +1,95 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false + +export function main(net: Network) returns nothing: + TokenState value = absent_state() + if value at vacant: + if value.marker != "empty": + int64 ignored = resource_probe.terminal_label() + if !source17_absent_optional(view value.fallback): + int64 ignored = resource_probe.terminal_label() + return nothing + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/12_absent_exact_state.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/12_absent_exact_state.jett new file mode 100644 index 00000000..bc73159a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/12_absent_exact_state.jett @@ -0,0 +1,95 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false + +export function main(net: Network) returns nothing: + TokenState at vacant value = absent_exact_state() + if value at vacant: + if value.marker != "empty": + int64 ignored = resource_probe.terminal_label() + if !source17_absent_optional(view value.fallback): + int64 ignored = resource_probe.terminal_label() + return nothing + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/13_wrong_empty_length_control.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/13_wrong_empty_length_control.jett new file mode 100644 index 00000000..09cabeb8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/13_wrong_empty_length_control.jett @@ -0,0 +1,84 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +export function main(net: Network) returns nothing: + list[resource_probe.TestHandle] values = empty_tokens() + if list.length[resource_probe.TestHandle](view values) != 1: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/14_required_primitive_controls.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/14_required_primitive_controls.jett new file mode 100644 index 00000000..359dc142 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/14_required_primitive_controls.jett @@ -0,0 +1,86 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +export function main(net: Network) returns nothing: + int64 answer = absent_required_controls() + if answer != 34: + int64 ignored = resource_probe.terminal_label() + if absent_namespace_value != 17: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/list_length.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/list_length.jett new file mode 100644 index 00000000..7dac2f29 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/list_length.jett @@ -0,0 +1,3 @@ +namespace list +export function length[T](view items: list[T]) returns int64: + return list.__length[T](view items) diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/map_length.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/map_length.jett new file mode 100644 index 00000000..d540cfab --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/map_length.jett @@ -0,0 +1,3 @@ +namespace map +export function length[K, V](view items: map[K, V]) returns int64: + return map.__length[K, V](view items) diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/original.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/original.jett new file mode 100644 index 00000000..3a1d96cf --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/original.jett @@ -0,0 +1,78 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate_baseline_inputs.rs b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate_baseline_inputs.rs new file mode 100644 index 00000000..c9a3cdd3 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate_baseline_inputs.rs @@ -0,0 +1,287 @@ +//! Finite Source-only baseline data shared by reference and object probes. +//! No native status, channel, message or exit assertion is admitted here. +#![allow(dead_code)] // Each independent harness consumes its own phases. + +use jett_common::{FileId, SourceOrigin, Span}; +use jett_parser::ast::Item; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::{CheckOptions, CheckedResourceProgram, ResourceProgramError}; +use jett_types::ResourceKernelRecipe; +use std::collections::HashMap; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::sync::Arc; + +pub const SUPPORT: &str = include_str!("resource/resource_probe.jett"); +pub const ORIGINAL: &str = include_str!("resource_absent_aggregate/original.jett"); +pub const LIST_LENGTH: &str = include_str!("resource_absent_aggregate/list_length.jett"); +pub const MAP_LENGTH: &str = include_str!("resource_absent_aggregate/map_length.jett"); + +#[derive(Clone, Copy, Debug)] +pub enum ReferenceExpectation { + Nothing, + Error(&'static str), +} + +pub struct Input { + pub name: &'static str, + pub source: &'static str, + pub reference: ReferenceExpectation, +} + +macro_rules! clean { + ($name:literal, $path:literal) => { + Input { + name: $name, + source: include_str!($path), + reference: ReferenceExpectation::Nothing, + } + }; +} + +pub const INPUTS: &[Input] = &[ + clean!( + "01_empty_tokens", + "resource_absent_aggregate/01_empty_tokens.jett" + ), + clean!( + "02_absent_tokens", + "resource_absent_aggregate/02_absent_tokens.jett" + ), + clean!( + "03_empty_map", + "resource_absent_aggregate/03_empty_map.jett" + ), + clean!( + "04_absent_map", + "resource_absent_aggregate/04_absent_map.jett" + ), + clean!( + "05_empty_result_tokens", + "resource_absent_aggregate/05_empty_result_tokens.jett" + ), + clean!( + "06_failed_result_tokens", + "resource_absent_aggregate/06_failed_result_tokens.jett" + ), + clean!( + "07_absent_envelope", + "resource_absent_aggregate/07_absent_envelope.jett" + ), + clean!( + "08_absent_box", + "resource_absent_aggregate/08_absent_box.jett" + ), + clean!( + "09_empty_choice", + "resource_absent_aggregate/09_empty_choice.jett" + ), + clean!( + "10_absent_choice", + "resource_absent_aggregate/10_absent_choice.jett" + ), + clean!( + "11_absent_state", + "resource_absent_aggregate/11_absent_state.jett" + ), + clean!( + "12_absent_exact_state", + "resource_absent_aggregate/12_absent_exact_state.jett" + ), + Input { + name: "13_wrong_empty_length_control", + source: include_str!("resource_absent_aggregate/13_wrong_empty_length_control.jett"), + reference: ReferenceExpectation::Error("list.__remove_at: index -1 out of bounds"), + }, + clean!( + "14_required_primitive_controls", + "resource_absent_aggregate/14_required_primitive_controls.jett" + ), +]; + +#[derive(Clone, Copy, Debug)] +pub enum Phase { + Unstarted, + SourceIdentity, + Parsing, + Catalog, + CheckedPrepare, + Resolution, + Checking, + ResolveMetadata, + CheckMetadata, + RequiredCapture, + RequiredCache, + ReferenceSetup, + ReferenceEntryIdentity, + ReferenceCall, + ReferenceObservations, + WorkerEligibility, + OriginalConstructorCall, + VerifyPropertyWorkers, + HirLowering, + ValueBaking, + HirEntryIdentity, + ValueConversions, + MirLowering, + MirValidation, + ObjectEmission, + ObjectObservations, +} + +#[derive(Clone, Copy, Debug)] +pub enum FailureKind { + Returned, + UnexpectedPanic, +} + +#[derive(Debug)] +pub struct Failure { + pub phase: Phase, + pub kind: FailureKind, + pub detail: String, +} + +pub struct Attempt { + pub entered: Vec, + pub outcome: Result, +} + +pub fn enter(entered: &mut Vec, phase: Phase) { + entered.push(phase); +} + +pub fn failure(phase: Phase, detail: impl std::fmt::Debug) -> Failure { + Failure { + phase, + kind: FailureKind::Returned, + detail: format!("{detail:?}"), + } +} + +pub fn panic_message(payload: &(dyn std::any::Any + Send)) -> String { + payload + .downcast_ref::() + .cloned() + .or_else(|| { + payload + .downcast_ref::<&str>() + .map(|text| (*text).to_string()) + }) + .unwrap_or_else(|| "non-string panic payload".to_string()) +} + +pub fn attempt(run: impl FnOnce(&mut Vec) -> Result) -> Attempt { + let mut entered = vec![Phase::Unstarted]; + let outcome = match catch_unwind(AssertUnwindSafe(|| run(&mut entered))) { + Ok(outcome) => outcome, + Err(payload) => Err(Failure { + phase: *entered.last().unwrap(), + kind: FailureKind::UnexpectedPanic, + detail: panic_message(payload.as_ref()), + }), + }; + Attempt { entered, outcome } +} + +fn checked_failure(error: ResourceProgramError) -> Failure { + let phase = match &error { + ResourceProgramError::ParseDiagnostics(_) => Phase::Parsing, + ResourceProgramError::ResolveDiagnostics(_) => Phase::Resolution, + ResourceProgramError::CheckDiagnostics(_) => Phase::Checking, + ResourceProgramError::ResolveMetadata { .. } => Phase::ResolveMetadata, + ResourceProgramError::CheckMetadata { .. } => Phase::CheckMetadata, + }; + // Display alone omits ordinary diagnostic messages; retain full spans/codes. + failure(phase, (&error, error.diagnostics())) +} + +pub fn prepare( + input: &Input, + release: bool, + entered: &mut Vec, +) -> Result<(Arc, Span), Failure> { + enter(entered, Phase::SourceIdentity); + if !input.source.starts_with(ORIGINAL) { + return Err(failure( + Phase::SourceIdentity, + "whole original Source17 prefix changed", + )); + } + let primary = FileId::new(0); + let support = FileId::new(10_000); + let list = FileId::new(10_001); + let map = FileId::new(10_002); + enter(entered, Phase::Parsing); + let mut parsed = jett_parser::parse(SUPPORT, support); + let resource_spans = parsed + .module + .items + .iter() + .filter_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .collect::>(); + let mut entries = Vec::new(); + for (source, file) in [ + (LIST_LENGTH, list), + (MAP_LENGTH, map), + (input.source, primary), + ] { + let fragment = jett_parser::parse(source, file); + if file == primary { + entries.extend(fragment.module.items.iter().filter_map(|item| match item { + Item::Function(function) + if function.name.name == "main" && function.name.span.file == primary => + { + Some(function.span) + } + _ => None, + })); + } + parsed.module.items.extend(fragment.module.items); + parsed.errors.extend(fragment.errors); + } + if !parsed.errors.is_empty() { + return Err(failure(Phase::Parsing, &parsed.errors)); + } + enter(entered, Phase::Catalog); + let [resource] = resource_spans.as_slice() else { + return Err(failure( + Phase::Catalog, + "sole original Support Resource declaration missing", + )); + }; + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: *resource, + member: member.into(), + recipe, + }) + .collect::>(); + let [entry] = entries.as_slice() else { + return Err(failure( + Phase::Catalog, + "primary original main declaration is not unique", + )); + }; + enter(entered, Phase::CheckedPrepare); + let checked = CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (primary, SourceOrigin::Project), + (support, SourceOrigin::Stdlib), + (list, SourceOrigin::Stdlib), + (map, SourceOrigin::Stdlib), + ]), + &catalog, + CheckOptions { release }, + ) + .map_err(checked_failure)?; + Ok((Arc::new(checked), *entry)) +} diff --git a/crates/jett_driver/tests/native_conformance/resource_cases.rs b/crates/jett_driver/tests/native_conformance/resource_cases.rs new file mode 100644 index 00000000..1793b66f --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_cases.rs @@ -0,0 +1,3268 @@ +//! Identical original Source and finite script/oracle inputs for both test harnesses. +//! No record contains a runtime token, Resource key or installer authority. +#![allow(dead_code)] // Each harness consumes its own side of the shared observation oracle. +pub(crate) const SUPPORT: &str = include_str!("resource/resource_probe.jett"); +#[derive(Clone, Copy, Debug)] +pub(crate) enum Script { + Construct(i64), + ConstructFail(i64, &'static str), + FinalizerPanic(i64), + Borrow(i64, i64), + BorrowFail(i64, &'static str), + BorrowPanic(i64), +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Event { + Constructed(i64), + ConstructionFailed(i64), + Borrowed(i64), + BorrowFailed(i64), + Finalized(i64), +} +#[derive(Clone, Copy, Debug)] +pub(crate) enum ReferenceOutcome { + Clean, + Error(&'static str), + ProviderPanic(&'static str), + CleanupPanic(&'static str), +} +pub(crate) struct Case { + pub(crate) name: &'static str, + pub(crate) source: &'static str, + pub(crate) script: &'static [Script], + pub(crate) events: &'static [Event], + pub(crate) reference: ReferenceOutcome, + // Native status/channel literals are separate from the reference error representation. + pub(crate) body: u32, + pub(crate) cleanup: u32, + pub(crate) channel: u32, + pub(crate) ordinary_message: &'static [u8], + pub(crate) resource_message: &'static [u8], + pub(crate) exit: i32, +} +pub(crate) const CASES: &[Case] = &[ + Case { + name: "connected", + source: include_str!("resource/01_connected_entry.jett"), + script: &[Script::Construct(1), Script::Borrow(1, 7)], + events: &[ + Event::Constructed(1), + Event::Borrowed(1), + Event::Finalized(1), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "factory_fail", + source: include_str!("resource/01_connected_entry.jett"), + script: &[Script::ConstructFail(1, "declined")], + events: &[Event::ConstructionFailed(1)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "later_actual", + source: include_str!("resource/02_partial_actual_failure.jett"), + script: &[Script::Construct(1)], + events: &[Event::Constructed(1), Event::Finalized(1)], + reference: ReferenceOutcome::Error("list.__remove_at: index -1 out of bounds"), + body: 1, + cleanup: 0, + channel: 1, + ordinary_message: b"list.__remove_at: index -1 out of bounds", + resource_message: b"native Resource protocol refused", + exit: 71, + }, + Case { + name: "implicit_drop", + source: include_str!("resource/03_implicit_drop.jett"), + script: &[Script::Construct(1)], + events: &[Event::Constructed(1), Event::Finalized(1)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "finalizer_panic", + source: include_str!("resource/03_implicit_drop.jett"), + script: &[Script::FinalizerPanic(1)], + events: &[Event::Constructed(1), Event::Finalized(1)], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 0, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "replace_live", + source: include_str!("resource/04_live_replace.jett"), + script: &[ + Script::Construct(2401), + Script::Construct(2402), + Script::Borrow(2402, 7), + ], + events: &[ + Event::Constructed(2401), + Event::Constructed(2402), + Event::Finalized(2401), + Event::Borrowed(2402), + Event::Finalized(2402), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "rebind_self", + source: include_str!("resource/05_self_rebind.jett"), + script: &[Script::Construct(2421)], + events: &[Event::Constructed(2421), Event::Finalized(2421)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "replace_closed", + source: include_str!("resource/06_replace_closed.jett"), + script: &[Script::Construct(2411), Script::Construct(2412)], + events: &[ + Event::Constructed(2411), + Event::Finalized(2411), + Event::Constructed(2412), + Event::Finalized(2412), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "rhs_domain_failure", + source: include_str!("resource/07_rhs_domain_failure.jett"), + script: &[ + Script::Construct(2431), + Script::ConstructFail(2432, "rhs sentinel"), + Script::Borrow(2431, 7), + ], + events: &[ + Event::Constructed(2431), + Event::ConstructionFailed(2432), + Event::Borrowed(2431), + Event::Finalized(2431), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "rhs_ordinary_failure", + source: include_str!("resource/08_rhs_ordinary_failure.jett"), + script: &[Script::Construct(2441)], + events: &[Event::Constructed(2441), Event::Finalized(2441)], + reference: ReferenceOutcome::Error("list.__remove_at: index -1 out of bounds"), + body: 1, + cleanup: 0, + channel: 1, + ordinary_message: b"list.__remove_at: index -1 out of bounds", + resource_message: b"native Resource protocol refused", + exit: 71, + }, + Case { + name: "replace_old_finalizer_panic", + source: include_str!("resource/04_live_replace.jett"), + script: &[Script::FinalizerPanic(2401), Script::Construct(2402)], + events: &[ + Event::Constructed(2401), + Event::Constructed(2402), + Event::Finalized(2401), + Event::Finalized(2402), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "later_handle_minimal", + source: include_str!("resource/09_later_handle_minimal.jett"), + script: &[Script::Construct(502)], + events: &[Event::Constructed(502), Event::Finalized(502)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "later_handle_original", + source: include_str!("resource/10_later_handle_original.jett"), + script: &[Script::Construct(501), Script::Borrow(501, 7)], + events: &[ + Event::Constructed(501), + Event::Borrowed(501), + Event::Finalized(501), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "later_handle_success", + source: include_str!("resource/11_later_handle_success.jett"), + script: &[Script::Construct(503), Script::Borrow(503, 7)], + events: &[ + Event::Constructed(503), + Event::Borrowed(503), + Event::Finalized(503), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "later_handle_written_view", + source: include_str!("resource/12_later_handle_written_view.jett"), + script: &[Script::Construct(505), Script::Borrow(505, 7)], + events: &[ + Event::Constructed(505), + Event::Borrowed(505), + Event::Finalized(505), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "first_handle_failure", + source: include_str!("resource/13_first_handle_failure.jett"), + script: &[Script::ConstructFail(506, "first sentinel")], + events: &[Event::ConstructionFailed(506)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "later_handle_cleanup_panic", + source: include_str!("resource/09_later_handle_minimal.jett"), + script: &[Script::FinalizerPanic(502)], + events: &[Event::Constructed(502), Event::Finalized(502)], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "first_handle_success", + source: include_str!("resource/14_first_handle_success.jett"), + script: &[Script::Construct(507), Script::Borrow(507, 7)], + events: &[ + Event::Constructed(507), + Event::Borrowed(507), + Event::Finalized(507), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "later_handle_two_owned_views", + source: include_str!("resource/15_later_handle_two_owned_views.jett"), + script: &[Script::Construct(511), Script::Construct(512)], + events: &[ + Event::Constructed(511), + Event::Constructed(512), + Event::Finalized(512), + Event::Finalized(511), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "later_handle_return_other_owner", + source: include_str!("resource/16_later_handle_return_other_owner.jett"), + script: &[ + Script::Construct(521), + Script::Construct(522), + Script::Borrow(522, 7), + ], + events: &[ + Event::Constructed(521), + Event::Constructed(522), + Event::Finalized(521), + Event::Borrowed(522), + Event::Finalized(522), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "later_handle_default", + source: include_str!("resource/17_later_handle_default.jett"), + script: &[Script::Construct(531), Script::Borrow(531, 7)], + events: &[ + Event::Constructed(531), + Event::Borrowed(531), + Event::Finalized(531), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "later_handle_return_cleanup_panic", + source: include_str!("resource/16_later_handle_return_other_owner.jett"), + script: &[Script::FinalizerPanic(521), Script::Construct(522)], + events: &[ + Event::Constructed(521), + Event::Constructed(522), + Event::Finalized(521), + Event::Finalized(522), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "indirect_named_close", + source: include_str!("resource/18_indirect_named_close.jett"), + script: &[Script::Construct(701)], + events: &[Event::Constructed(701), Event::Finalized(701)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "indirect_named_close_factory_fail", + source: include_str!("resource/18_indirect_named_close.jett"), + script: &[Script::ConstructFail(701, "indirect factory sentinel")], + events: &[Event::ConstructionFailed(701)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "indirect_named_terminal_failure", + source: include_str!("resource/19_indirect_named_terminal_failure.jett"), + script: &[Script::Construct(701)], + events: &[Event::Constructed(701), Event::Finalized(701)], + reference: ReferenceOutcome::Error("list.__remove_at: index -1 out of bounds"), + body: 1, + cleanup: 0, + channel: 1, + ordinary_message: b"list.__remove_at: index -1 out of bounds", + resource_message: b"native Resource protocol refused", + exit: 71, + }, + Case { + name: "indirect_named_close_cleanup_panic", + source: include_str!("resource/18_indirect_named_close.jett"), + script: &[Script::FinalizerPanic(701)], + events: &[Event::Constructed(701), Event::Finalized(701)], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "indirect_named_terminal_cleanup_panic", + source: include_str!("resource/19_indirect_named_terminal_failure.jett"), + script: &[Script::FinalizerPanic(701)], + events: &[Event::Constructed(701), Event::Finalized(701)], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 1, + cleanup: 255, + channel: 3, + ordinary_message: b"list.__remove_at: index -1 out of bounds", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "indirect_named_alias", + source: include_str!("resource/20_indirect_named_alias.jett"), + script: &[Script::Construct(701)], + events: &[Event::Constructed(701), Event::Finalized(701)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "indirect_namespace_close", + source: include_str!("resource/21_indirect_namespace_close.jett"), + script: &[Script::Construct(711)], + events: &[Event::Constructed(711), Event::Finalized(711)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "indirect_namespace_close_factory_fail", + source: include_str!("resource/21_indirect_namespace_close.jett"), + script: &[Script::ConstructFail(711, "namespace factory sentinel")], + events: &[Event::ConstructionFailed(711)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "indirect_namespace_close_cleanup_panic", + source: include_str!("resource/21_indirect_namespace_close.jett"), + script: &[Script::FinalizerPanic(711)], + events: &[Event::Constructed(711), Event::Finalized(711)], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "indirect_namespace_alias", + source: include_str!("resource/22_indirect_namespace_alias.jett"), + script: &[Script::Construct(711)], + events: &[Event::Constructed(711), Event::Finalized(711)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "returned_hook_close", + source: include_str!("resource/23_returned_hook_close.jett"), + script: &[Script::Construct(721)], + events: &[Event::Constructed(721), Event::Finalized(721)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "returned_hook_factory_fail", + source: include_str!("resource/23_returned_hook_close.jett"), + script: &[Script::ConstructFail(721, "returned hook factory sentinel")], + events: &[Event::ConstructionFailed(721)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "returned_hook_cleanup_panic", + source: include_str!("resource/23_returned_hook_close.jett"), + script: &[Script::FinalizerPanic(721)], + events: &[Event::Constructed(721), Event::Finalized(721)], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "returned_hook_alias", + source: include_str!("resource/24_returned_hook_alias.jett"), + script: &[Script::Construct(721)], + events: &[Event::Constructed(721), Event::Finalized(721)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "returned_hook_relay", + source: include_str!("resource/25_returned_hook_relay.jett"), + script: &[Script::Construct(721)], + events: &[Event::Constructed(721), Event::Finalized(721)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "returned_hook_unused", + source: include_str!("resource/26_returned_hook_unused.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "returned_hook_factory", + source: include_str!("resource/27_returned_hook_factory.jett"), + script: &[Script::Construct(731)], + events: &[Event::Constructed(731), Event::Finalized(731)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "returned_hook_factory_domain_fail", + source: include_str!("resource/27_returned_hook_factory.jett"), + script: &[Script::ConstructFail(731, "returned factory sentinel")], + events: &[Event::ConstructionFailed(731)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "returned_hook_borrow", + source: include_str!("resource/28_returned_hook_borrow.jett"), + script: &[Script::Construct(741), Script::Borrow(741, 17)], + events: &[ + Event::Constructed(741), + Event::Borrowed(741), + Event::Finalized(741), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "immediate_returned_hook_close", + source: include_str!("resource/29_immediate_returned_hook_close.jett"), + script: &[Script::Construct(751)], + events: &[Event::Constructed(751), Event::Finalized(751)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "immediate_returned_hook_factory", + source: include_str!("resource/30_immediate_returned_hook_factory.jett"), + script: &[ + Script::Construct(742), + Script::Construct(744), + Script::Borrow(744, 743), + Script::Borrow(742, 1), + Script::Construct(743), + ], + events: &[ + Event::Constructed(742), + Event::Constructed(744), + Event::Borrowed(744), + Event::Borrowed(742), + Event::Constructed(743), + Event::Finalized(743), + Event::Finalized(744), + Event::Finalized(742), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "immediate_returned_hook_factory_domain_fail", + source: include_str!("resource/30_immediate_returned_hook_factory.jett"), + script: &[ + Script::Construct(742), + Script::Construct(744), + Script::Borrow(744, 743), + Script::Borrow(742, 1), + Script::ConstructFail(743, "immediate returned factory sentinel"), + ], + events: &[ + Event::Constructed(742), + Event::Constructed(744), + Event::Borrowed(744), + Event::Borrowed(742), + Event::ConstructionFailed(743), + Event::Finalized(744), + Event::Finalized(742), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "immediate_returned_hook_callee_failure", + source: include_str!("resource/31_immediate_returned_hook_callee_failure.jett"), + script: &[ + Script::Construct(742), + Script::Construct(744), + Script::Borrow(744, 743), + Script::Borrow(742, 1), + ], + events: &[ + Event::Constructed(742), + Event::Constructed(744), + Event::Borrowed(744), + Event::Borrowed(742), + Event::Finalized(744), + Event::Finalized(742), + ], + reference: ReferenceOutcome::Error("list.__remove_at: index -1 out of bounds"), + body: 1, + cleanup: 0, + channel: 1, + ordinary_message: b"list.__remove_at: index -1 out of bounds", + resource_message: b"native Resource protocol refused", + exit: 71, + }, + Case { + name: "immediate_returned_hook_actual_failure", + source: include_str!("resource/32_immediate_returned_hook_actual_failure.jett"), + script: &[ + Script::Construct(742), + Script::Construct(744), + Script::Borrow(744, 743), + ], + events: &[ + Event::Constructed(742), + Event::Constructed(744), + Event::Borrowed(744), + Event::Finalized(744), + Event::Finalized(742), + ], + reference: ReferenceOutcome::Error("list.__remove_at: index -1 out of bounds"), + body: 1, + cleanup: 0, + channel: 1, + ordinary_message: b"list.__remove_at: index -1 out of bounds", + resource_message: b"native Resource protocol refused", + exit: 71, + }, + Case { + name: "comptime_hook_close", + source: include_str!("resource/33_comptime_hook_close.jett"), + script: &[Script::Construct(761)], + events: &[Event::Constructed(761), Event::Finalized(761)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_close_factory_fail", + source: include_str!("resource/33_comptime_hook_close.jett"), + script: &[Script::ConstructFail( + 761, + "comptime close factory sentinel", + )], + events: &[Event::ConstructionFailed(761)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_close_cleanup_panic", + source: include_str!("resource/33_comptime_hook_close.jett"), + script: &[Script::FinalizerPanic(761)], + events: &[Event::Constructed(761), Event::Finalized(761)], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "comptime_hook_factory", + source: include_str!("resource/34_comptime_hook_factory.jett"), + script: &[Script::Construct(771)], + events: &[Event::Constructed(771), Event::Finalized(771)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_factory_domain_fail", + source: include_str!("resource/34_comptime_hook_factory.jett"), + script: &[Script::ConstructFail(771, "comptime factory sentinel")], + events: &[Event::ConstructionFailed(771)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_borrow", + source: include_str!("resource/35_comptime_hook_borrow.jett"), + script: &[Script::Construct(781), Script::Borrow(781, 17)], + events: &[ + Event::Constructed(781), + Event::Borrowed(781), + Event::Finalized(781), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_alias", + source: include_str!("resource/36_comptime_hook_alias.jett"), + script: &[Script::Construct(761)], + events: &[Event::Constructed(761), Event::Finalized(761)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_relay", + source: include_str!("resource/37_comptime_hook_relay.jett"), + script: &[Script::Construct(761)], + events: &[Event::Constructed(761), Event::Finalized(761)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_unused", + source: include_str!("resource/38_comptime_hook_unused.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "immediate_comptime_hook_close", + source: include_str!("resource/39_immediate_comptime_hook_close.jett"), + script: &[Script::Construct(761)], + events: &[Event::Constructed(761), Event::Finalized(761)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_absence", + source: include_str!("resource/40_comptime_hook_absence.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_pure_choice", + source: include_str!("resource/41_comptime_hook_pure_choice.jett"), + script: &[Script::Construct(761)], + events: &[Event::Constructed(761), Event::Finalized(761)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_generic", + source: include_str!("resource/42_comptime_hook_generic.jett"), + script: &[Script::Construct(761)], + events: &[Event::Constructed(761), Event::Finalized(761)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_scoped", + source: include_str!("resource/43_comptime_hook_scoped.jett"), + script: &[Script::Construct(761)], + events: &[Event::Constructed(761), Event::Finalized(761)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_scalar_helper", + source: include_str!("resource/44_comptime_scalar_helper.jett"), + script: &[Script::Construct(761)], + events: &[Event::Constructed(761), Event::Finalized(761)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_optional_none_written", + source: include_str!("resource/45_view_optional_none_written.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_optional_none_bare", + source: include_str!("resource/46_view_optional_none_bare.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_optional_some_written", + source: include_str!("resource/47_view_optional_some_written.jett"), + script: &[Script::Construct(831), Script::Borrow(831, 17)], + events: &[ + Event::Constructed(831), + Event::Borrowed(831), + Event::Finalized(831), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_optional_some_bare", + source: include_str!("resource/48_view_optional_some_bare.jett"), + script: &[Script::Construct(831), Script::Borrow(831, 17)], + events: &[ + Event::Constructed(831), + Event::Borrowed(831), + Event::Finalized(831), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_result_fail_written", + source: include_str!("resource/49_view_result_fail_written.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_result_fail_bare", + source: include_str!("resource/50_view_result_fail_bare.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_result_ok_written", + source: include_str!("resource/51_view_result_ok_written.jett"), + script: &[Script::Construct(831), Script::Borrow(831, 17)], + events: &[ + Event::Constructed(831), + Event::Borrowed(831), + Event::Finalized(831), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_result_ok_bare", + source: include_str!("resource/52_view_result_ok_bare.jett"), + script: &[Script::Construct(831), Script::Borrow(831, 17)], + events: &[ + Event::Constructed(831), + Event::Borrowed(831), + Event::Finalized(831), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_optional_written_argument_abort", + source: include_str!("resource/53_view_optional_written_argument_abort.jett"), + script: &[ + Script::Construct(851), + Script::ConstructFail(852, "late sentinel"), + ], + events: &[ + Event::Constructed(851), + Event::ConstructionFailed(852), + Event::Finalized(851), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_result_bare_argument_abort", + source: include_str!("resource/54_view_result_bare_argument_abort.jett"), + script: &[ + Script::Construct(851), + Script::ConstructFail(852, "late sentinel"), + ], + events: &[ + Event::Constructed(851), + Event::ConstructionFailed(852), + Event::Finalized(851), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_optional_written_borrow_fail", + source: include_str!("resource/55_view_optional_written_borrow_fail.jett"), + script: &[ + Script::Construct(861), + Script::BorrowFail(861, "borrow sentinel"), + ], + events: &[ + Event::Constructed(861), + Event::BorrowFailed(861), + Event::Finalized(861), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_result_bare_borrow_fail", + source: include_str!("resource/56_view_result_bare_borrow_fail.jett"), + script: &[ + Script::Construct(861), + Script::BorrowFail(861, "borrow sentinel"), + ], + events: &[ + Event::Constructed(861), + Event::BorrowFailed(861), + Event::Finalized(861), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "nested_view_optional_some_written", + source: include_str!("resource/57_nested_view_optional_some_written.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_optional_written_some", + source: include_str!("resource/58_if_optional_written_some.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_optional_written_some_construct_fail", + source: include_str!("resource/58_if_optional_written_some.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_optional_written_none", + source: include_str!("resource/59_if_optional_written_none.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_optional_bare_some", + source: include_str!("resource/60_if_optional_bare_some.jett"), + script: &[ + Script::Construct(871), + Script::Construct(872), + Script::Borrow(872, 17), + ], + events: &[ + Event::Constructed(871), + Event::Finalized(871), + Event::Constructed(872), + Event::Borrowed(872), + Event::Finalized(872), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_optional_bare_some_construct_fail", + source: include_str!("resource/60_if_optional_bare_some.jett"), + script: &[ + Script::Construct(871), + Script::ConstructFail(872, "nested factory sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Finalized(871), + Event::ConstructionFailed(872), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_optional_bare_none", + source: include_str!("resource/61_if_optional_bare_none.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_result_written_ok", + source: include_str!("resource/62_if_result_written_ok.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_result_written_ok_construct_fail", + source: include_str!("resource/62_if_result_written_ok.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_result_written_fail", + source: include_str!("resource/63_if_result_written_fail.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_result_bare_ok", + source: include_str!("resource/64_if_result_bare_ok.jett"), + script: &[ + Script::Construct(871), + Script::Construct(872), + Script::Borrow(872, 17), + ], + events: &[ + Event::Constructed(871), + Event::Finalized(871), + Event::Constructed(872), + Event::Borrowed(872), + Event::Finalized(872), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_result_bare_ok_construct_fail", + source: include_str!("resource/64_if_result_bare_ok.jett"), + script: &[ + Script::Construct(871), + Script::ConstructFail(872, "nested factory sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Finalized(871), + Event::ConstructionFailed(872), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_result_bare_fail", + source: include_str!("resource/65_if_result_bare_fail.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_optional_written_some", + source: include_str!("resource/66_scope_optional_written_some.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_optional_written_some_construct_fail", + source: include_str!("resource/66_scope_optional_written_some.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_optional_written_none", + source: include_str!("resource/67_scope_optional_written_none.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_optional_bare_some", + source: include_str!("resource/68_scope_optional_bare_some.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_optional_bare_some_construct_fail", + source: include_str!("resource/68_scope_optional_bare_some.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_optional_bare_none", + source: include_str!("resource/69_scope_optional_bare_none.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_result_written_ok", + source: include_str!("resource/70_scope_result_written_ok.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_result_written_ok_construct_fail", + source: include_str!("resource/70_scope_result_written_ok.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_result_written_fail", + source: include_str!("resource/71_scope_result_written_fail.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_result_bare_ok", + source: include_str!("resource/72_scope_result_bare_ok.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_result_bare_ok_construct_fail", + source: include_str!("resource/72_scope_result_bare_ok.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_result_bare_fail", + source: include_str!("resource/73_scope_result_bare_fail.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_optional_written_some", + source: include_str!("resource/74_while_optional_written_some.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_optional_written_some_construct_fail", + source: include_str!("resource/74_while_optional_written_some.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_optional_written_none", + source: include_str!("resource/75_while_optional_written_none.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_optional_bare_some", + source: include_str!("resource/76_while_optional_bare_some.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_optional_bare_some_construct_fail", + source: include_str!("resource/76_while_optional_bare_some.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_optional_bare_none", + source: include_str!("resource/77_while_optional_bare_none.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_result_written_ok", + source: include_str!("resource/78_while_result_written_ok.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_result_written_ok_construct_fail", + source: include_str!("resource/78_while_result_written_ok.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_result_written_fail", + source: include_str!("resource/79_while_result_written_fail.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_result_bare_ok", + source: include_str!("resource/80_while_result_bare_ok.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_result_bare_ok_construct_fail", + source: include_str!("resource/80_while_result_bare_ok.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_result_bare_fail", + source: include_str!("resource/81_while_result_bare_fail.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_owned_optional_reuse", + source: include_str!("resource/82_if_owned_optional_reuse.jett"), + script: &[ + Script::Construct(871), + Script::Construct(872), + Script::Borrow(872, 17), + ], + events: &[ + Event::Constructed(871), + Event::Finalized(871), + Event::Constructed(872), + Event::Borrowed(872), + Event::Finalized(872), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_owned_optional_reuse_construct_fail", + source: include_str!("resource/82_if_owned_optional_reuse.jett"), + script: &[ + Script::Construct(871), + Script::ConstructFail(872, "nested factory sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Finalized(871), + Event::ConstructionFailed(872), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_owned_result_reuse", + source: include_str!("resource/83_if_owned_result_reuse.jett"), + script: &[ + Script::Construct(871), + Script::Construct(872), + Script::Borrow(872, 17), + ], + events: &[ + Event::Constructed(871), + Event::Finalized(871), + Event::Constructed(872), + Event::Borrowed(872), + Event::Finalized(872), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_owned_result_reuse_construct_fail", + source: include_str!("resource/83_if_owned_result_reuse.jett"), + script: &[ + Script::Construct(871), + Script::ConstructFail(872, "nested factory sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Finalized(871), + Event::ConstructionFailed(872), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_owned_optional_reuse", + source: include_str!("resource/84_scope_owned_optional_reuse.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_owned_optional_reuse_construct_fail", + source: include_str!("resource/84_scope_owned_optional_reuse.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_owned_result_reuse", + source: include_str!("resource/85_scope_owned_result_reuse.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_owned_result_reuse_construct_fail", + source: include_str!("resource/85_scope_owned_result_reuse.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_owned_optional_reuse", + source: include_str!("resource/86_while_owned_optional_reuse.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_owned_optional_reuse_construct_fail", + source: include_str!("resource/86_while_owned_optional_reuse.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_owned_result_reuse", + source: include_str!("resource/87_while_owned_result_reuse.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_owned_result_reuse_construct_fail", + source: include_str!("resource/87_while_owned_result_reuse.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_sibling_optional_bindings", + source: include_str!("resource/88_if_sibling_optional_bindings.jett"), + script: &[ + Script::Construct(891), + Script::Construct(892), + Script::Borrow(891, 17), + Script::Borrow(892, 17), + ], + events: &[ + Event::Constructed(891), + Event::Constructed(892), + Event::Borrowed(891), + Event::Borrowed(892), + Event::Finalized(891), + Event::Finalized(892), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_sibling_optional_bindings_construct_fail", + source: include_str!("resource/88_if_sibling_optional_bindings.jett"), + script: &[ + Script::Construct(891), + Script::ConstructFail(892, "nested factory sentinel"), + ], + events: &[ + Event::Constructed(891), + Event::ConstructionFailed(892), + Event::Finalized(891), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_sibling_result_bindings", + source: include_str!("resource/89_scope_sibling_result_bindings.jett"), + script: &[ + Script::Construct(891), + Script::Construct(892), + Script::Borrow(891, 17), + Script::Borrow(892, 17), + ], + events: &[ + Event::Constructed(891), + Event::Constructed(892), + Event::Borrowed(891), + Event::Borrowed(892), + Event::Finalized(891), + Event::Finalized(892), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_sibling_result_bindings_construct_fail", + source: include_str!("resource/89_scope_sibling_result_bindings.jett"), + script: &[ + Script::Construct(891), + Script::ConstructFail(892, "nested factory sentinel"), + ], + events: &[ + Event::Constructed(891), + Event::ConstructionFailed(892), + Event::Finalized(891), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_optional_written_borrow_fail", + source: include_str!("resource/90_scope_optional_written_borrow_fail.jett"), + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_optional_written_borrow_fail_construct_fail", + source: include_str!("resource/90_scope_optional_written_borrow_fail.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_result_bare_borrow_fail", + source: include_str!("resource/91_scope_result_bare_borrow_fail.jett"), + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_result_bare_borrow_fail_construct_fail", + source: include_str!("resource/91_scope_result_bare_borrow_fail.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_optional_bare_borrow_fail", + source: include_str!("resource/92_while_optional_bare_borrow_fail.jett"), + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_optional_bare_borrow_fail_construct_fail", + source: include_str!("resource/92_while_optional_bare_borrow_fail.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_result_written_borrow_fail", + source: include_str!("resource/93_while_result_written_borrow_fail.jett"), + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_result_written_borrow_fail_construct_fail", + source: include_str!("resource/93_while_result_written_borrow_fail.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_owned_optional_break", + source: include_str!("resource/94_while_owned_optional_break.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_owned_optional_break_construct_fail", + source: include_str!("resource/94_while_owned_optional_break.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_owned_result_continue", + source: include_str!("resource/95_while_owned_result_continue.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_owned_result_continue_construct_fail", + source: include_str!("resource/95_while_owned_result_continue.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Pipeline Source scenarios preserve operation outcomes and retirement order. + // Exact panic body/cleanup/channel reports are enforced by the native gate. + Case { + name: "pipeline_construct_move_close", + source: include_str!("resource/pipeline/18_pipeline_construct_move_close.jett"), + script: &[Script::Construct(1801)], + events: &[Event::Constructed(1801), Event::Finalized(1801)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "pipeline_written_view_retains_owner", + source: include_str!("resource/pipeline/19_pipeline_written_view_retains_owner.jett"), + script: &[Script::Construct(1901), Script::Borrow(1901, 5)], + events: &[ + Event::Constructed(1901), + Event::Borrowed(1901), + Event::Finalized(1901), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "pipeline_bare_owner_view_operation", + source: include_str!("resource/pipeline/20_pipeline_bare_owner_view_operation.jett"), + script: &[Script::Construct(2001), Script::Borrow(2001, 5)], + events: &[ + Event::Constructed(2001), + Event::Borrowed(2001), + Event::Finalized(2001), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Provider-panic body and cleanup status must be checked through real Source. + // Existing Reference fixture20 provider panic remains inside full native parity scope. + Case { + name: "pipeline_bare_owner_provider_panic", + source: include_str!("resource/pipeline/20_pipeline_bare_owner_view_operation.jett"), + script: &[Script::Construct(2001), Script::BorrowPanic(2001)], + events: &[ + Event::Constructed(2001), + Event::Borrowed(2001), + Event::Finalized(2001), + ], + reference: ReferenceOutcome::ProviderPanic("selected test borrow provider panic"), + body: 1, + cleanup: 0, + channel: 2, + ordinary_message: b"", + resource_message: b"runtime operation panicked", + exit: 73, + }, + // Provider-panic body and cleanup status must be checked through real Source. + // Cleanup precedence must be established through the same genuine fixture20 Source. + Case { + name: "pipeline_bare_owner_provider_panic_cleanup_wins", + source: include_str!("resource/pipeline/20_pipeline_bare_owner_view_operation.jett"), + script: &[Script::FinalizerPanic(2001), Script::BorrowPanic(2001)], + events: &[ + Event::Constructed(2001), + Event::Borrowed(2001), + Event::Finalized(2001), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 1, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "pipeline_named_actual_abort", + source: include_str!("resource/pipeline/21_pipeline_named_actual_abort.jett"), + script: &[Script::Construct(2101), Script::Borrow(2101, 1)], + events: &[ + Event::Constructed(2101), + Event::Borrowed(2101), + Event::Finalized(2101), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "pipeline_domain_failure_stops_next_step", + source: include_str!("resource/pipeline/22_pipeline_domain_failure_stops_next_step.jett"), + script: &[Script::ConstructFail(2201, "denied")], + events: &[Event::ConstructionFailed(2201)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "pipeline_written_view_borrow_fail", + source: include_str!("resource/pipeline/19_pipeline_written_view_borrow_fail.jett"), + script: &[ + Script::Construct(1901), + Script::BorrowFail(1901, "unavailable"), + ], + events: &[ + Event::Constructed(1901), + Event::BorrowFailed(1901), + Event::Finalized(1901), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "pipeline_indirect_close_descriptor", + source: include_str!("resource/pipeline/23_pipeline_indirect_close_descriptor.jett"), + script: &[Script::Construct(2301)], + events: &[Event::Constructed(2301), Event::Finalized(2301)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "pipeline_construct_factory_fail", + source: include_str!("resource/pipeline/18_pipeline_construct_move_close.jett"), + script: &[Script::ConstructFail(1801, "declined")], + events: &[Event::ConstructionFailed(1801)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Cleanup panic must retain its exact native status and channel. + Case { + name: "pipeline_construct_close_cleanup_panic", + source: include_str!("resource/pipeline/18_pipeline_construct_move_close.jett"), + script: &[Script::FinalizerPanic(1801)], + events: &[Event::Constructed(1801), Event::Finalized(1801)], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "pipeline_written_view_construct_fail", + source: include_str!("resource/pipeline/19_pipeline_written_view_construct_fail.jett"), + script: &[Script::ConstructFail(1901, "declined")], + events: &[Event::ConstructionFailed(1901)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Cleanup panic must retain its exact native status and channel. + Case { + name: "pipeline_written_view_close_cleanup_panic", + source: include_str!("resource/pipeline/19_pipeline_written_view_retains_owner.jett"), + script: &[Script::FinalizerPanic(1901), Script::Borrow(1901, 5)], + events: &[ + Event::Constructed(1901), + Event::Borrowed(1901), + Event::Finalized(1901), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "pipeline_bare_owner_construct_fail", + source: include_str!("resource/pipeline/20_pipeline_bare_owner_construct_fail.jett"), + script: &[Script::ConstructFail(2001, "declined")], + events: &[Event::ConstructionFailed(2001)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "pipeline_bare_owner_borrow_fail", + source: include_str!("resource/pipeline/20_pipeline_bare_owner_borrow_fail.jett"), + script: &[ + Script::Construct(2001), + Script::BorrowFail(2001, "unavailable"), + ], + events: &[ + Event::Constructed(2001), + Event::BorrowFailed(2001), + Event::Finalized(2001), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Cleanup panic must retain its exact native status and channel. + Case { + name: "pipeline_bare_owner_operation_cleanup_panic", + source: include_str!("resource/pipeline/20_pipeline_bare_owner_view_operation.jett"), + script: &[Script::FinalizerPanic(2001), Script::Borrow(2001, 5)], + events: &[ + Event::Constructed(2001), + Event::Borrowed(2001), + Event::Finalized(2001), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "pipeline_named_actual_construct_fail", + source: include_str!("resource/pipeline/21_pipeline_named_actual_abort.jett"), + script: &[Script::ConstructFail(2101, "declined")], + events: &[Event::ConstructionFailed(2101)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "pipeline_named_actual_stage_borrow_fail", + source: include_str!("resource/pipeline/21_pipeline_named_actual_abort.jett"), + script: &[ + Script::Construct(2101), + Script::BorrowFail(2101, "stage unavailable"), + ], + events: &[ + Event::Constructed(2101), + Event::BorrowFailed(2101), + Event::Finalized(2101), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Cleanup panic must retain its exact native status and channel. + Case { + name: "pipeline_named_actual_abort_cleanup_panic", + source: include_str!("resource/pipeline/21_pipeline_named_actual_abort.jett"), + script: &[Script::FinalizerPanic(2101), Script::Borrow(2101, 1)], + events: &[ + Event::Constructed(2101), + Event::Borrowed(2101), + Event::Finalized(2101), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "pipeline_domain_success_close", + source: include_str!("resource/pipeline/22_pipeline_domain_success_close.jett"), + script: &[Script::Construct(2201)], + events: &[Event::Constructed(2201), Event::Finalized(2201)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Cleanup panic must retain its exact native status and channel. + Case { + name: "pipeline_domain_success_close_cleanup_panic", + source: include_str!("resource/pipeline/22_pipeline_domain_success_close.jett"), + script: &[Script::FinalizerPanic(2201)], + events: &[Event::Constructed(2201), Event::Finalized(2201)], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "pipeline_indirect_construct_fail", + source: include_str!("resource/pipeline/23_pipeline_indirect_close_descriptor.jett"), + script: &[Script::ConstructFail(2301, "declined")], + events: &[Event::ConstructionFailed(2301)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Cleanup panic must retain its exact native status and channel. + Case { + name: "pipeline_indirect_close_cleanup_panic", + source: include_str!("resource/pipeline/23_pipeline_indirect_close_descriptor.jett"), + script: &[Script::FinalizerPanic(2301)], + events: &[Event::Constructed(2301), Event::Finalized(2301)], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "control_flow_for_owned_optional_reuse", + source: include_str!("resource/control_flow/01_for_owned_optional_reuse.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_for_owned_optional_reuse_construct_fail", + source: include_str!("resource/control_flow/01_for_owned_optional_reuse.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_for_owned_optional_reuse_first_borrow_fail", + source: include_str!("resource/control_flow/01_for_owned_optional_reuse.jett"), + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_for_owned_optional_reuse_second_borrow_fail", + source: include_str!("resource/control_flow/01_for_owned_optional_reuse.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // INFERRED for this new enclosing Source: characterize native cleanup status before acceptance. + Case { + name: "control_flow_for_owned_optional_reuse_cleanup_panic", + source: include_str!("resource/control_flow/01_for_owned_optional_reuse.jett"), + script: &[ + Script::FinalizerPanic(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "control_flow_for_owned_optional_break", + source: include_str!("resource/control_flow/02_for_owned_optional_break.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_for_owned_optional_break_construct_fail", + source: include_str!("resource/control_flow/02_for_owned_optional_break.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_for_owned_optional_break_first_borrow_fail", + source: include_str!("resource/control_flow/02_for_owned_optional_break.jett"), + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // INFERRED for this new enclosing Source: characterize native cleanup status before acceptance. + Case { + name: "control_flow_for_owned_optional_break_cleanup_panic", + source: include_str!("resource/control_flow/02_for_owned_optional_break.jett"), + script: &[Script::FinalizerPanic(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "control_flow_for_owned_result_continue", + source: include_str!("resource/control_flow/03_for_owned_result_continue.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_for_owned_result_continue_construct_fail", + source: include_str!("resource/control_flow/03_for_owned_result_continue.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_for_owned_result_continue_first_borrow_fail", + source: include_str!("resource/control_flow/03_for_owned_result_continue.jett"), + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_for_owned_result_continue_second_borrow_fail", + source: include_str!("resource/control_flow/03_for_owned_result_continue.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // INFERRED for this new enclosing Source: characterize native cleanup status before acceptance. + Case { + name: "control_flow_for_owned_result_continue_cleanup_panic", + source: include_str!("resource/control_flow/03_for_owned_result_continue.jett"), + script: &[ + Script::FinalizerPanic(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "control_flow_match_owned_optional_first", + source: include_str!("resource/control_flow/04_match_owned_optional_first.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_match_owned_optional_first_construct_fail", + source: include_str!("resource/control_flow/04_match_owned_optional_first.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_match_owned_optional_first_first_borrow_fail", + source: include_str!("resource/control_flow/04_match_owned_optional_first.jett"), + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // INFERRED for this new enclosing Source: characterize native cleanup status before acceptance. + Case { + name: "control_flow_match_owned_optional_first_cleanup_panic", + source: include_str!("resource/control_flow/04_match_owned_optional_first.jett"), + script: &[Script::FinalizerPanic(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "control_flow_match_owned_optional_second", + source: include_str!("resource/control_flow/05_match_owned_optional_second.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_match_owned_optional_second_construct_fail", + source: include_str!("resource/control_flow/05_match_owned_optional_second.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_match_owned_optional_second_first_borrow_fail", + source: include_str!("resource/control_flow/05_match_owned_optional_second.jett"), + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // INFERRED for this new enclosing Source: characterize native cleanup status before acceptance. + Case { + name: "control_flow_match_owned_optional_second_cleanup_panic", + source: include_str!("resource/control_flow/05_match_owned_optional_second.jett"), + script: &[Script::FinalizerPanic(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + // Native outcomes are checked by the linked Source-deleted gate. + Case { + name: "control_flow_consecutive_for_owned_optional_reuse", + source: include_str!("resource/control_flow/07_consecutive_for_owned_optional_reuse.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Native outcomes are checked by the linked Source-deleted gate. + Case { + name: "control_flow_consecutive_for_owned_optional_reuse_construct_fail", + source: include_str!("resource/control_flow/07_consecutive_for_owned_optional_reuse.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Native outcomes are checked by the linked Source-deleted gate. + Case { + name: "control_flow_consecutive_for_owned_optional_reuse_third_borrow_fail", + source: include_str!("resource/control_flow/07_consecutive_for_owned_optional_reuse.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Native outcomes are checked by the linked Source-deleted gate. + Case { + name: "control_flow_consecutive_for_owned_optional_reuse_cleanup_panic", + source: include_str!("resource/control_flow/07_consecutive_for_owned_optional_reuse.jett"), + script: &[ + Script::FinalizerPanic(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + // Native outcomes are checked by the linked Source-deleted gate. + Case { + name: "control_flow_nested_for_owned_optional_reuse", + source: include_str!("resource/control_flow/08_nested_for_owned_optional_reuse.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Native outcomes are checked by the linked Source-deleted gate. + Case { + name: "control_flow_nested_for_owned_optional_reuse_construct_fail", + source: include_str!("resource/control_flow/08_nested_for_owned_optional_reuse.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Native outcomes are checked by the linked Source-deleted gate. + Case { + name: "control_flow_nested_for_owned_optional_reuse_first_inner_borrow_fail", + source: include_str!("resource/control_flow/08_nested_for_owned_optional_reuse.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Native outcomes are checked by the linked Source-deleted gate. + Case { + name: "control_flow_nested_for_owned_optional_reuse_cleanup_panic", + source: include_str!("resource/control_flow/08_nested_for_owned_optional_reuse.jett"), + script: &[ + Script::FinalizerPanic(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, +]; + +pub(crate) const PIPELINE_CASE_START: usize = 138; +pub(crate) const PIPELINE_CASE_COUNT: usize = 23; +pub(crate) fn pipeline_cases() -> &'static [Case] { + let end = PIPELINE_CASE_START + PIPELINE_CASE_COUNT; + assert_eq!( + CASES.len(), + CONTROL_FLOW_CASE_START + CONTROL_FLOW_CASE_COUNT, + "full Source-only additive control-flow candidate corpus" + ); + assert!( + CASES[..PIPELINE_CASE_START] + .iter() + .all(|case| !case.name.starts_with("pipeline_")), + "original corpus must not contain the new pipeline family" + ); + let selected = &CASES[PIPELINE_CASE_START..end]; + assert!( + selected + .iter() + .all(|case| case.name.starts_with("pipeline_")), + "focused gate must select only the appended pipeline family" + ); + selected +} + +pub(crate) const CONTROL_FLOW_CASE_START: usize = 161; +pub(crate) const CONTROL_FLOW_CASE_COUNT: usize = 30; +pub(crate) fn control_flow_cases() -> &'static [Case] { + let end = CONTROL_FLOW_CASE_START + CONTROL_FLOW_CASE_COUNT; + assert_eq!( + CASES.len(), + end, + "full Source-only control-flow candidate corpus" + ); + assert!( + CASES[..CONTROL_FLOW_CASE_START] + .iter() + .all(|case| !case.name.starts_with("control_flow_")), + "original corpus must not contain the new control-flow family" + ); + let selected = &CASES[CONTROL_FLOW_CASE_START..end]; + assert!( + selected + .iter() + .all(|case| case.name.starts_with("control_flow_")), + "focused gate must select only the appended control-flow family" + ); + selected +} diff --git a/crates/jett_driver/tests/native_conformance/resource_execution.rs b/crates/jett_driver/tests/native_conformance/resource_execution.rs new file mode 100644 index 00000000..03239795 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_execution.rs @@ -0,0 +1,777 @@ +//! Explicit matched single-runtime tests. Production launcher metadata is never used. +use super::{resource_cases as cases, resource_report as report}; +use jett_common::{FileId, SourceOrigin}; +use jett_parser::{ast::Item, parse}; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; +use jett_types::ResourceKernelRecipe; +use std::{ + collections::HashMap, + ffi::OsString, + fs, + io::Read, + path::{Path, PathBuf}, + process::{Command, Stdio}, + sync::Arc, + time::{Duration, Instant}, +}; + +const RECEIPT_ENV: &str = "JETT_RESOURCE_NATIVE_TEST_ARCHIVE_RECEIPT_V1"; +// CI supplies an independently measured digest while compiling this test binary. +// Runtime environment selects only the receipt path and cannot replace this pin. +const RECEIPT_SHA256: &str = + match option_env!("JETT_RESOURCE_NATIVE_TEST_ARCHIVE_EXPECTED_SHA256_V1") { + Some(measured) => measured, + None => "984f1f05f3ff09b66358cf5e213482c0e4d4b9ebfab8b0988f9a555246177753", + }; +struct Archive { + path: PathBuf, + archive_sha256: String, + target: String, + profile: String, + linker: PathBuf, + linker_sha256: Option, + libraries: Vec, + sdk_lib: Option, +} +fn digest(bytes: &[u8]) -> String { + jett_runtime::crypto::sha256_digest(bytes) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} +fn field<'a>(value: &'a serde_json::Value, name: &str) -> &'a str { + value + .get(name) + .and_then(serde_json::Value::as_str) + .unwrap_or_else(|| panic!("archive receipt lacks string {name}")) +} +fn measured_file(value: &serde_json::Value, path: &str, hash: &str) -> PathBuf { + let path = PathBuf::from(field(value, path)); + assert!(path.is_absolute(), "receipt paths must be absolute"); + let metadata = fs::metadata(&path).unwrap(); + assert!( + metadata.is_file() && metadata.len() <= 256 * 1024 * 1024, + "{}", + path.display() + ); + let bytes = fs::read(&path).unwrap(); + assert_eq!( + digest(&bytes), + field(value, hash).to_ascii_lowercase(), + "{}", + path.display() + ); + path +} +fn archive(release: bool) -> Archive { + let path = PathBuf::from( + std::env::var_os(RECEIPT_ENV) + .expect("explicit Root-measured matched archive receipt is required"), + ); + let bytes = fs::read(path).unwrap(); + assert!(bytes.len() <= 1024 * 1024); + assert_eq!( + digest(&bytes), + RECEIPT_SHA256, + "exact independently measured archive receipt" + ); + let receipt: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!( + receipt.get("version").and_then(serde_json::Value::as_u64), + Some(1) + ); + let profile = if release { "release" } else { "debug" }; + let rows = receipt + .get("archives") + .and_then(serde_json::Value::as_array) + .unwrap(); + let matching = rows + .iter() + .filter(|row| field(row, "profile") == profile) + .collect::>(); + let [row] = matching.as_slice() else { + panic!("exact {profile} archive receipt must be unique"); + }; + let target = field(row, "target"); + assert_eq!(target, jett_driver::native::host_target()); + assert_eq!( + row.get("runtime_abi").and_then(serde_json::Value::as_u64), + Some(1) + ); + assert_eq!( + row.get("layout_wire").and_then(serde_json::Value::as_u64), + Some(3) + ); + assert_eq!( + row.get("private_cfgs"), + Some(&serde_json::json!([ + "test", + "jett_resource_native_test_archive" + ])) + ); + assert!(matches!(field(row, "crt_mode"), "static" | "dynamic")); + let path = measured_file(row, "archive_path", "archive_sha256"); + let linker = PathBuf::from(field(row, "linker_path")); + assert!(linker.is_absolute() && linker.is_file()); + for name in ["build_receipt", "native_static_libs_receipt", "crt_receipt"] { + let provenance = row.get(name).unwrap(); + let _ = measured_file(provenance, "path", "sha256"); + } + let libraries = row + .get("native_library_args") + .and_then(serde_json::Value::as_array) + .unwrap() + .iter() + .map(|item| { + let value = item.as_str().expect("exact native library argument"); + let lower = value.to_ascii_lowercase(); + assert!( + !lower.contains("jett_runtime") && !lower.contains("jett_native_launcher"), + "a second Rust runtime archive is forbidden" + ); + OsString::from(value) + }) + .collect::>(); + assert!( + !libraries.is_empty(), + "actual native-static-libs vector must be recorded" + ); + let library_receipt = measured_file( + row.get("native_static_libs_receipt").unwrap(), + "path", + "sha256", + ); + let observed: serde_json::Value = + serde_json::from_slice(&fs::read(library_receipt).unwrap()).unwrap(); + assert_eq!( + row.get("native_library_args"), + observed.get("arguments"), + "exact measured library order and duplicates" + ); + let crt_receipt = measured_file(row.get("crt_receipt").unwrap(), "path", "sha256"); + let crt: serde_json::Value = serde_json::from_slice(&fs::read(crt_receipt).unwrap()).unwrap(); + assert_eq!(field(row, "crt_mode"), field(&crt, "mode")); + assert_eq!(field(row, "linker_path"), field(&crt, "linker_path")); + let linker_sha256 = crt.get("linker_executable").map(|measurement| { + let measured = measured_file(measurement, "path", "sha256"); + assert_eq!( + fs::canonicalize(&measured).unwrap(), + fs::canonicalize(&linker).unwrap(), + "measured linker executable" + ); + field(measurement, "sha256").to_ascii_lowercase() + }); + if target == jett_driver::native::LINUX_GNU_NATIVE_TARGET { + assert_eq!(field(row, "crt_mode"), "dynamic"); + assert!(linker_sha256.is_some(), "GNU linker bytes must be measured"); + } + let sdk_lib = crt + .get("sdk_LIB") + .and_then(serde_json::Value::as_str) + .map(OsString::from); + Archive { + path, + archive_sha256: field(row, "archive_sha256").to_ascii_lowercase(), + target: target.into(), + profile: profile.into(), + linker, + linker_sha256, + libraries, + sdk_lib, + } +} +fn checked( + project: &Path, + stdlib: &Path, + release: bool, +) -> (Arc, jett_common::Span) { + let project_text = fs::read_to_string(project).unwrap(); + let support_text = fs::read_to_string(stdlib).unwrap(); + let primary = FileId::new(0); + let support = FileId::new(10_000); + let mut parsed = parse(&support_text, support); + let project = parse(&project_text, primary); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(project.errors.is_empty(), "{:?}", project.errors); + let entries = project + .module + .items + .iter() + .filter_map(|item| match item { + Item::Function(function) + if function.name.name == "main" && function.name.span.file == primary => + { + Some(function.span) + } + _ => None, + }) + .collect::>(); + let [entry] = entries.as_slice() else { + panic!("actual primary main declaration must be unique"); + }; + let resources = parsed + .module + .items + .iter() + .filter_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .collect::>(); + let [resource] = resources.as_slice() else { + panic!("actual synthetic Stdlib Resource must be unique"); + }; + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: *resource, + member: member.into(), + recipe, + }) + .collect::>(); + parsed.module.items.extend(project.module.items); + let checked = CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (primary, SourceOrigin::Project), + (support, SourceOrigin::Stdlib), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap_or_else(|error| { + panic!( + "original checked Source: {error:?}; {:?}", + error.diagnostics() + ) + }); + (Arc::new(checked), *entry) +} +fn try_object( + project: &Path, + stdlib: &Path, + release: bool, +) -> Result { + let (checked, span) = checked(project, stdlib, release); + let required = jett_driver::evaluate_checked_resource_required_values(&checked); + assert!( + required.diagnostics.is_empty(), + "required Source evaluation: {:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + required + .values + .checked_required_values(&checked) + .expect("required checked cache must not contain runtime Resource authority"); + let mut hir = jett_hir::lower_checked_resource_program(&checked).unwrap(); + jett_driver::bake_checked_resource_values(&mut hir, &checked, &required.values).unwrap(); + // Same original declaration span -> FunctionId join as the actual driver. + let entries = hir + .functions + .iter() + .filter(|function| { + function.span == span + && function.identity.declaration.origin == SourceOrigin::Project + && function.identity.declaration.kind == jett_hir::DeclarationKind::Function + }) + .map(|function| function.id) + .collect::>(); + let [entry] = entries.as_slice() else { + panic!("original driver entry has no unique HIR identity"); + }; + jett_hir::complete_value_conversions(&mut hir, &checked.checked().interner).unwrap(); + let mir = jett_mir::lower(&hir, &checked.checked().interner).unwrap(); + jett_mir::validate(&mir).unwrap(); + jett_codegen_cranelift::emit_host_program_object_with_options( + &mir, + &checked.checked().interner, + *entry, + jett_codegen_cranelift::CodegenOptions { optimize: release }, + ) +} +fn object(project: &Path, stdlib: &Path, release: bool) -> jett_codegen_cranelift::ObjectArtifact { + try_object(project, stdlib, release).unwrap() +} + +fn prefix(prefix: &str, path: &Path) -> OsString { + let mut argument = OsString::from(prefix); + argument.push(path.as_os_str()); + argument +} +fn command(archive: &Archive, object: &Path, binary: &Path) -> Command { + assert_eq!( + digest(&fs::read(&archive.path).unwrap()), + archive.archive_sha256, + "measured runtime archive before each link" + ); + if let Some(expected) = &archive.linker_sha256 { + assert_eq!( + &digest(&fs::read(&archive.linker).unwrap()), + expected, + "measured linker bytes before each link" + ); + } + #[cfg(windows)] + let mut command = { + let tool = find_msvc_tools::find_tool(&archive.target, "link.exe") + .expect("same MSVC linker discovery used by driver"); + assert_eq!( + fs::canonicalize(tool.path()).unwrap(), + fs::canonicalize(&archive.linker).unwrap(), + "measured linker identity" + ); + let mut command = tool.to_command(); + let actual_lib = command + .get_envs() + .find_map(|(name, value)| { + name.to_str() + .filter(|name| name.eq_ignore_ascii_case("LIB")) + .and(value) + }) + .map(|value| value.to_os_string()) + .or_else(|| std::env::var_os("LIB")); + assert_eq!( + actual_lib, archive.sdk_lib, + "preserved measured MSVC SDK LIB environment" + ); + command.args([ + "/NOLOGO", + "/MACHINE:X64", + "/SUBSYSTEM:CONSOLE", + "/INCREMENTAL:NO", + "/MANIFEST:EMBED", + ]); + command + .arg(prefix("/OUT:", binary)) + .arg(prefix("/PDB:", &binary.with_extension("pdb"))); + command + }; + #[cfg(not(windows))] + let mut command = { + let mut command = Command::new(&archive.linker); + command.arg("-no-pie").arg("-o").arg(binary); + command + }; + command + .arg(object) + .arg(&archive.path) + .args(&archive.libraries); + command +} +fn link(archive: &Archive, object: &Path, binary: &Path, directory: &Path) { + // Match the driver native link timeout, file-backed capture and kill+reap contract. + let mut stdout = tempfile::NamedTempFile::new().unwrap(); + let mut stderr = tempfile::NamedTempFile::new().unwrap(); + let mut command = command(archive, object, binary); + command + .current_dir(directory) + .stdin(Stdio::null()) + .stdout(stdout.reopen().unwrap()) + .stderr(stderr.reopen().unwrap()); + let mut child = + super::ExecutionChild(command.spawn().expect("link sole matched private runtime")); + let start = Instant::now(); + let status = loop { + if let Some(status) = child.0.try_wait().unwrap() { + break status; + } + assert!( + start.elapsed() < jett_driver::native::DEFAULT_NATIVE_LINK_TIMEOUT, + "Resource native linker exceeded 60 seconds" + ); + std::thread::sleep(Duration::from_millis(10)); + }; + let read = |file: &mut fs::File| { + let mut bytes = Vec::new(); + file.take(file.metadata().unwrap().len()) + .read_to_end(&mut bytes) + .unwrap(); + bytes + }; + let out = read(stdout.as_file_mut()); + let err = read(stderr.as_file_mut()); + assert!( + status.success(), + "{status}; stdout={} stderr={}", + String::from_utf8_lossy(&out), + String::from_utf8_lossy(&err) + ); + assert!(binary.is_file()); +} +fn script(case: &cases::Case) -> String { + let mut output = b"JTRST001".to_vec(); + output.extend(1u32.to_le_bytes()); + output.extend(0u32.to_le_bytes()); + output.extend(0u64.to_le_bytes()); + output.extend(1u32.to_le_bytes()); + output.extend((case.script.len() as u32).to_le_bytes()); + for row in case.script { + let (tag, label) = match *row { + cases::Script::Construct(label) => (1u32, label), + cases::Script::ConstructFail(label, _) => (2, label), + cases::Script::FinalizerPanic(label) => (3, label), + cases::Script::Borrow(label, _) => (4, label), + cases::Script::BorrowFail(label, _) => (5, label), + cases::Script::BorrowPanic(label) => (6, label), + }; + output.extend(tag.to_le_bytes()); + output.extend(label.to_le_bytes()); + match *row { + cases::Script::ConstructFail(_, error) | cases::Script::BorrowFail(_, error) => { + output.extend((error.len() as u32).to_le_bytes()); + output.extend(error.as_bytes()); + } + cases::Script::Borrow(_, value) => output.extend(value.to_le_bytes()), + _ => {} + } + } + let length = output.len() as u64; + output[16..24].copy_from_slice(&length.to_le_bytes()); + output.iter().map(|byte| format!("{byte:02x}")).collect() +} +fn event(expected: cases::Event) -> (i64, report::EventKind) { + match expected { + cases::Event::Constructed(label) => (label, report::EventKind::Constructed), + cases::Event::ConstructionFailed(label) => (label, report::EventKind::ConstructionFailed), + cases::Event::Borrowed(label) => (label, report::EventKind::Borrowed), + cases::Event::BorrowFailed(label) => (label, report::EventKind::BorrowFailed), + cases::Event::Finalized(label) => (label, report::EventKind::Finalized), + } +} +fn assert_report(case: &cases::Case, report: &report::Report) { + assert_eq!(report.attempts.len(), 1, "{}", case.name); + let attempt = &report.attempts[0]; + assert_eq!( + attempt.completion.body_status, case.body, + "{} original body; full report: {report:?}", + case.name + ); + assert_eq!( + attempt.completion.cleanup_status, case.cleanup, + "{} cleanup", + case.name + ); + let channel = match case.channel { + 0 => report::FailureChannel::Clean, + 1 => report::FailureChannel::Body, + 2 => report::FailureChannel::HostPanic, + 3 => report::FailureChannel::Cleanup, + _ => panic!("invalid oracle"), + }; + assert_eq!( + attempt.completion.selected_channel, channel, + "{} channel", + case.name + ); + assert_eq!( + attempt.ordinary_message, case.ordinary_message, + "{} ordinary message", + case.name + ); + assert_eq!( + attempt.resource_message, case.resource_message, + "{} resource message", + case.name + ); + assert!( + report.observations_ready_for_acceptance(), + "{} zero BEFORE-teardown obligations: {report:?}", + case.name + ); + assert_eq!(report.events.len(), case.events.len()); + for (index, (actual, expected)) in report.events.iter().zip(case.events).enumerate() { + let (label, kind) = event(*expected); + assert_eq!( + (actual.sequence, actual.label, actual.kind), + ((index + 1) as u64, label, kind), + "{} event {index}", + case.name + ); + } + assert_eq!(attempt.event_sequence_end, case.events.len() as u64); + assert_eq!(report.script_remaining, 0); + assert_eq!( + ( + report.created, + report.destroy_attempts, + report.destroy_status, + report.write_status + ), + (true, 1, 0, 0) + ); + assert_eq!( + report.all_attempts_succeeded(), + case.body == 0 && case.cleanup == 0 && case.channel == 0 + ); +} +#[test] +fn native_resource_receipt_pin_runtime_env_cannot_replace_compiled_pin() { + let directory = tempfile::tempdir().unwrap(); + let receipt = directory.path().join("unmeasured-receipt.json"); + let bytes = br#"{"version":1,"archives":[]}"#; + fs::write(&receipt, bytes).unwrap(); + let forged = digest(bytes); + assert_ne!(forged, RECEIPT_SHA256); + let output = Command::new(std::env::current_exe().unwrap()) + .args([ + "resource_execution::native_resource_original_source_lifecycle_matches_reference_and_retires_before_teardown", + "--ignored", + "--exact", + "--test-threads=1", + ]) + .env(RECEIPT_ENV, &receipt) + .env("JETT_RESOURCE_NATIVE_TEST_ARCHIVE_EXPECTED_SHA256_V1", forged) + .current_dir(directory.path()) + .output() + .unwrap(); + assert!(!output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stdout.contains("exact independently measured archive receipt") + || stderr.contains("exact independently measured archive receipt"), + "{output:?}" + ); +} + +fn run_native_cases(selected: &[cases::Case]) { + struct LinkedProgram { + source: &'static str, + directory: tempfile::TempDir, + binary: PathBuf, + binary_sha256: String, + } + + for release in [false, true] { + let archive = archive(release); + // This cache cannot outlive the exact profile and measured archive. Support + // bytes, FileIds/origins, declaration order and kernel catalog are fixed by + // checked(); only the exact project bytes vary between these script rows. + // Cache completed artifacts, never compiler-session proofs or runtime state. + let mut previous: Option = None; + let mut builds = 0usize; + for case in selected { + let reuse = previous + .as_ref() + .is_some_and(|program| program.source.as_bytes() == case.source.as_bytes()); + if !reuse { + // Retire the previous files before allocating the next build directory. + // At most one completed program is retained for the current profile. + drop(previous.take()); + let directory = tempfile::tempdir().unwrap(); + let root = directory.path(); + let project = root.join("project"); + let stdlib = root.join("synthetic-stdlib"); + fs::create_dir(&project).unwrap(); + fs::create_dir(&stdlib).unwrap(); + let entry = project.join("main.jett"); + let support = stdlib.join("resource_probe.jett"); + fs::write(&entry, case.source).unwrap(); + fs::write(&support, cases::SUPPORT).unwrap(); + let artifact = object(&entry, &support, release); + assert_eq!(artifact.target, archive.target); + assert!( + artifact + .symbols + .iter() + .any(|symbol| symbol == jett_codegen_cranelift::JETT_AOT_ENTRY_SYMBOL_V1) + ); + assert!( + artifact + .symbols + .iter() + .any(|symbol| symbol == "jett_aot_resource_v1_manifest") + ); + let object_path = root.join("resource.obj"); + let binary = root.join(if cfg!(windows) { + "resource.exe" + } else { + "resource" + }); + fs::write(&object_path, artifact.bytes).unwrap(); + // Every actual link retains the measured receipt/archive/linker guards. + link(&archive, &object_path, &binary, root); + fs::remove_dir_all(&project).unwrap(); + fs::remove_dir_all(&stdlib).unwrap(); + assert!( + !entry.exists() && !support.exists() && !project.exists() && !stdlib.exists() + ); + let binary_sha256 = digest(&fs::read(&binary).unwrap()); + previous = Some(LinkedProgram { + source: case.source, + directory, + binary, + binary_sha256, + }); + builds += 1; + } + let program = previous.as_ref().unwrap(); + let compiled = program.directory.path(); + assert!( + !compiled.join("project/main.jett").exists() + && !compiled + .join("synthetic-stdlib/resource_probe.jett") + .exists() + && !compiled.join("project").exists() + && !compiled.join("synthetic-stdlib").exists() + ); + assert_eq!( + digest(&fs::read(&program.binary).unwrap()), + program.binary_sha256, + "completed native program bytes before every execution" + ); + // A cache hit still gets a cold directory, fresh child and its own report. + let directory = tempfile::tempdir().unwrap(); + let root = directory.path(); + assert!(!root.join("resource-native-report-v1.bin").exists()); + assert!(!root.join("project").exists() && !root.join("synthetic-stdlib").exists()); + if std::env::var_os("JETT_RESOURCE_NATIVE_EVIDENCE_DIR_V1").is_some() { + // Preserve the existing per-case object evidence without copying a + // linked executable into every transient execution directory. + fs::copy(compiled.join("resource.obj"), root.join("resource.obj")).unwrap(); + } + let encoded = script(case); + let actual = super::run_bounded_with_env( + &program.binary, + root, + Some(("JETT_RESOURCE_NATIVE_TEST_SCRIPT_V1", &encoded)), + ); + let path = root.join("resource-native-report-v1.bin"); + assert!(path.metadata().unwrap().len() <= 16 * 1024 * 1024); + let bytes = fs::read(path).unwrap(); + let report = report::decode(&bytes).unwrap(); + assert_eq!( + actual.status.code(), + Some(case.exit), + "{} {}: {actual:?}; {report:?}", + case.name, + archive.profile + ); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + assert_report(case, &report); + } + eprintln!( + "Resource native build reuse: profile={}; {} Source-deleted executions; {builds} emitted/linked programs", + archive.profile, + selected.len() + ); + } +} + +#[test] +#[ignore = "requires exact Root-measured double-cfg Debug/Release single-runtime archive receipt"] +fn native_resource_original_source_lifecycle_matches_reference_and_retires_before_teardown() { + run_native_cases(cases::CASES); + eprintln!( + "Resource native acceptance: {} cases; {} Source-deleted executions; profiles=debug,release", + cases::CASES.len(), + 2 * cases::CASES.len() + ); +} + +#[test] +#[ignore = "requires exact Root-measured double-cfg Debug/Release single-runtime archive receipt"] +fn native_resource_pipeline_original_source_lifecycle_matches_reference_and_retires_before_teardown() + { + let selected = cases::pipeline_cases(); + run_native_cases(selected); + eprintln!( + "Resource pipeline native acceptance: {} cases; {} Source-deleted executions; profiles=debug,release", + selected.len(), + 2 * selected.len() + ); +} + +fn run_object_preflight(selected: &[cases::Case], family: &str) { + let expected = 2 * selected.len(); + let mut emitted = 0usize; + let mut refusals = Vec::new(); + for release in [false, true] { + for case in selected { + let directory = tempfile::tempdir().unwrap(); + let entry = directory.path().join("main.jett"); + let support = directory.path().join("resource_probe.jett"); + fs::write(&entry, case.source).unwrap(); + fs::write(&support, cases::SUPPORT).unwrap(); + // Source/HIR/MIR assertions remain fatal; only final CodegenError is collected. + let outcome = try_object(&entry, &support, release); + fs::remove_file(&entry).unwrap(); + fs::remove_file(&support).unwrap(); + assert!(!entry.exists() && !support.exists()); + match outcome { + Ok(artifact) => { + assert_eq!(artifact.target, jett_driver::native::host_target()); + assert!( + artifact.symbols.iter().any( + |symbol| symbol == jett_codegen_cranelift::JETT_AOT_ENTRY_SYMBOL_V1 + ) + ); + assert!( + artifact + .symbols + .iter() + .any(|symbol| symbol == "jett_aot_resource_v1_manifest") + ); + emitted += 1; + } + Err(error) => { + let refusal = format!("case={} release={release}: {error:?}", case.name); + eprintln!("Resource {family} native object refusal: {refusal}"); + refusals.push(refusal); + } + } + } + } + assert_eq!(emitted + refusals.len(), expected); + assert!( + refusals.is_empty(), + "Resource {family} native object preflight: {}/{} attempts refused\n{}", + refusals.len(), + expected, + refusals.join("\n") + ); + assert_eq!(emitted, expected); + eprintln!( + "Resource {family} native object preflight: {} cases; {emitted} emitted objects; profiles=debug,release; no linked execution", + selected.len() + ); +} + +#[test] +fn native_resource_pipeline_object_preflight_checks_all_sources_in_both_profiles() { + run_object_preflight(cases::pipeline_cases(), "pipeline"); +} + +#[test] +fn native_resource_control_flow_object_preflight_checks_all_sources_in_both_profiles() { + run_object_preflight(cases::control_flow_cases(), "control-flow"); +} + +#[test] +#[ignore = "requires exact Root-measured double-cfg Debug/Release single-runtime archive receipt"] +fn native_resource_control_flow_original_source_lifecycle_matches_reference_and_retires_before_teardown() + { + let selected = cases::control_flow_cases(); + run_native_cases(selected); + eprintln!( + "Resource control-flow native acceptance: {} cases; {} Source-deleted executions; profiles=debug,release", + selected.len(), + 2 * selected.len() + ); +} + +#[path = "resource_reflected_field_harness.rs"] +mod reflected_field_harness; + +#[path = "resource_execution/absent_aggregate_baseline.rs"] +mod absent_aggregate_baseline; + +#[path = "resource_occupied_struct_harness.rs"] +mod occupied_struct_harness; diff --git a/crates/jett_driver/tests/native_conformance/resource_execution/absent_aggregate_baseline.rs b/crates/jett_driver/tests/native_conformance/resource_execution/absent_aggregate_baseline.rs new file mode 100644 index 00000000..a4a01a52 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_execution/absent_aggregate_baseline.rs @@ -0,0 +1,448 @@ +//! Whole Source17 through strict object emission and Source-deleted native execution. +use super::*; +#[path = "../resource_absent_aggregate_baseline_inputs.rs"] +mod inputs; + +fn emit( + input: &inputs::Input, + release: bool, + entered: &mut Vec, +) -> Result { + use inputs::{Phase, enter, failure}; + let (checked, span) = inputs::prepare(input, release, entered)?; + enter(entered, Phase::RequiredCapture); + let required = jett_driver::evaluate_checked_resource_required_values(&checked); + if !required.diagnostics.is_empty() || !required.debug_events.is_empty() { + return Err(failure( + Phase::RequiredCapture, + (&required.diagnostics, &required.debug_events), + )); + } + enter(entered, Phase::RequiredCache); + required + .values + .checked_required_values(&checked) + .map_err(|error| failure(Phase::RequiredCache, error))?; + if required.values.is_empty() + || !required + .values + .values() + .all(|value| matches!(value, jett_comptime::Value::Int64(17))) + { + return Err(failure( + Phase::RequiredCache, + "all original required values must be primitive 17", + )); + } + let constants = checked + .module() + .items + .iter() + .filter_map(|item| match item { + Item::VarDecl(declaration) + if declaration.name.span.file == FileId::new(0) + && declaration.name.name == "absent_namespace_value" => + { + Some(declaration.name.span) + } + _ => None, + }) + .collect::>(); + let [constant] = constants.as_slice() else { + return Err(failure( + Phase::RequiredCache, + "unique original namespace constant required", + )); + }; + if required.values.constant(*constant) != Some(&jett_comptime::Value::Int64(17)) { + return Err(failure( + Phase::RequiredCache, + "original namespace value must be baked as 17", + )); + } + enter(entered, Phase::HirLowering); + let mut hir = jett_hir::lower_checked_resource_program(&checked) + .map_err(|error| failure(Phase::HirLowering, error))?; + enter(entered, Phase::ValueBaking); + jett_driver::bake_checked_resource_values(&mut hir, &checked, &required.values) + .map_err(|error| failure(Phase::ValueBaking, error))?; + for name in [ + "empty_tokens", + "absent_tokens", + "empty_map", + "absent_map", + "empty_result_tokens", + "failed_result_tokens", + "absent_envelope", + "absent_box", + "empty_choice", + "absent_choice", + "absent_state", + "absent_exact_state", + "refined_shape_contract", + "absent_required_controls", + ] { + if !hir.functions.iter().any(|function| { + function.identity.declaration.origin == SourceOrigin::Project + && function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) { + return Err(failure( + Phase::ValueBaking, + ("original declaration must remain", name), + )); + } + } + enter(entered, Phase::HirEntryIdentity); + let entries = hir + .functions + .iter() + .filter(|function| { + function.span == span + && function.identity.declaration.origin == SourceOrigin::Project + && function.identity.declaration.kind == jett_hir::DeclarationKind::Function + }) + .map(|function| function.id) + .collect::>(); + let [entry] = entries.as_slice() else { + return Err(failure( + Phase::HirEntryIdentity, + "unique original main -> HIR identity required", + )); + }; + enter(entered, Phase::ValueConversions); + jett_hir::complete_value_conversions(&mut hir, &checked.checked().interner) + .map_err(|error| failure(Phase::ValueConversions, error))?; + enter(entered, Phase::MirLowering); + let mir = jett_mir::lower(&hir, &checked.checked().interner) + .map_err(|error| failure(Phase::MirLowering, error))?; + enter(entered, Phase::MirValidation); + jett_mir::validate(&mir).map_err(|error| failure(Phase::MirValidation, error))?; + enter(entered, Phase::ObjectEmission); + let object = jett_codegen_cranelift::emit_host_program_object_with_options( + &mir, + &checked.checked().interner, + *entry, + jett_codegen_cranelift::CodegenOptions { optimize: release }, + ) + .map_err(|error| failure(Phase::ObjectEmission, error))?; + enter(entered, Phase::ObjectObservations); + if object.bytes.is_empty() + || object.target != jett_driver::native::host_target() + || !object + .symbols + .iter() + .any(|symbol| symbol == jett_codegen_cranelift::JETT_AOT_ENTRY_SYMBOL_V1) + || !object + .symbols + .iter() + .any(|symbol| symbol == "jett_aot_resource_v1_manifest") + { + return Err(failure( + Phase::ObjectObservations, + ( + &object.target, + &object.symbols, + "nonempty host object with entry and Resource manifest required", + ), + )); + } + Ok(object) +} + +#[test] +fn native_resource_absent_aggregate_object_baseline_records_all_first_rejecting_phases() { + assert_eq!(inputs::INPUTS.len(), 14); + let mut emitted = 0; + let mut failures = Vec::new(); + for release in [false, true] { + for input in inputs::INPUTS { + let attempt = inputs::attempt(|entered| emit(input, release, entered)); + match attempt.outcome { + Ok(object) => { + emitted += 1; + eprintln!( + "ABSENT_OBJECT name={:?} release={release} entered={:?} bytes={} target={:?}", + input.name, + attempt.entered, + object.bytes.len(), + object.target + ); + } + Err(failure) => { + eprintln!( + "ABSENT_OBJECT name={:?} release={release} entered={:?} failure={failure:?}", + input.name, attempt.entered + ); + failures.push((input.name, release, failure)); + } + } + } + } + assert_eq!(emitted + failures.len(), 28); + eprintln!( + "ABSENT_OBJECT_TOTAL attempts=28 emitted={emitted} failed={}", + failures.len() + ); + assert!( + failures.is_empty(), + "strict object gate failures: {failures:#?}" + ); + assert_eq!(emitted, 28); +} + +struct NativeExpectation { + body: u32, + channel: report::FailureChannel, + ordinary_message: &'static [u8], + resource_message: &'static [u8], + exit: i32, +} +fn expected(input: &inputs::Input) -> NativeExpectation { + match input.reference { + inputs::ReferenceExpectation::Nothing => NativeExpectation { + body: 0, + channel: report::FailureChannel::Clean, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + inputs::ReferenceExpectation::Error(error) => { + assert_eq!(input.name, "13_wrong_empty_length_control"); + assert_eq!(error, "list.__remove_at: index -1 out of bounds"); + NativeExpectation { + body: 1, + channel: report::FailureChannel::Body, + ordinary_message: error.as_bytes(), + resource_message: b"native Resource protocol refused", + exit: 71, + } + } + } +} + +fn empty_script(attempts: u32) -> String { + assert!(matches!(attempts, 1 | 2)); + let mut bytes = b"JTRST001".to_vec(); + bytes.extend(1u32.to_le_bytes()); + bytes.extend(0u32.to_le_bytes()); + bytes.extend(32u64.to_le_bytes()); + bytes.extend(attempts.to_le_bytes()); + bytes.extend(0u32.to_le_bytes()); + assert_eq!(bytes.len(), 32); + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + +fn assert_observed(input: &inputs::Input, observed: &report::Report, attempts: usize) { + let expected = expected(input); + assert_eq!( + observed.attempts.len(), + attempts, + "{}: {observed:?}", + input.name + ); + let mut previous = 0; + for (index, attempt) in observed.attempts.iter().enumerate() { + assert!( + attempt.completion.attempt > previous, + "{} attempt identity", + input.name + ); + previous = attempt.completion.attempt; + assert_eq!( + attempt.completion.body_status, expected.body, + "{} entry{index} original body: {observed:?}", + input.name + ); + assert_eq!( + attempt.completion.cleanup_status, 0, + "{} cleanup", + input.name + ); + assert_eq!( + attempt.completion.selected_channel, expected.channel, + "{} channel", + input.name + ); + assert_eq!( + attempt.ordinary_message, expected.ordinary_message, + "{} ordinary message", + input.name + ); + assert_eq!( + attempt.resource_message, expected.resource_message, + "{} resource message", + input.name + ); + assert!( + attempt.counts.resources_retired() && attempt.counts.ordinary_empty, + "{} entry{index} obligations BEFORE destruction: {observed:?}", + input.name + ); + assert_eq!( + attempt.event_sequence_end, 0, + "{} event boundary", + input.name + ); + } + assert!(observed.events.is_empty(), "{}: {observed:?}", input.name); + assert_eq!(observed.script_remaining, 0); + assert_eq!( + ( + observed.created, + observed.destroy_attempts, + observed.destroy_status, + observed.write_status + ), + (true, 1, 0, 0) + ); + assert!( + observed.observations_ready_for_acceptance(), + "{}: {observed:?}", + input.name + ); + assert_eq!(observed.all_attempts_succeeded(), expected.body == 0); +} + +fn execute(input: &inputs::Input, release: bool, archive: &Archive, attempts: u32) { + assert!(input.source.starts_with(inputs::ORIGINAL)); + let directory = tempfile::tempdir().unwrap(); + let root = directory.path(); + let project = root.join("project"); + let stdlib = root.join("synthetic-stdlib"); + fs::create_dir(&project).unwrap(); + fs::create_dir(&stdlib).unwrap(); + let sources = [ + (project.join("main.jett"), input.source), + (stdlib.join("resource_probe.jett"), inputs::SUPPORT), + (stdlib.join("list_length.jett"), inputs::LIST_LENGTH), + (stdlib.join("map_length.jett"), inputs::MAP_LENGTH), + ]; + for (path, source) in &sources { + fs::write(path, source).unwrap(); + assert_eq!(fs::read(path).unwrap(), source.as_bytes()); + } + // The shared preparation authenticates these exact finite Source bytes, + // trusted fragments and original FileId/span/DefId joins. + let attempt = inputs::attempt(|entered| emit(input, release, entered)); + let artifact = attempt.outcome.unwrap_or_else(|failure| { + panic!( + "{} release={release} entered={:?}: {failure:?}", + input.name, attempt.entered + ) + }); + assert_eq!(artifact.target, archive.target); + let object_path = root.join("resource.obj"); + let binary = root.join(if cfg!(windows) { + "resource.exe" + } else { + "resource" + }); + fs::write(&object_path, artifact.bytes).unwrap(); + link(archive, &object_path, &binary, root); + fs::remove_dir_all(&project).unwrap(); + fs::remove_dir_all(&stdlib).unwrap(); + assert!(!project.exists() && !stdlib.exists()); + assert!(sources.iter().all(|(path, _)| !path.exists())); + let encoded = empty_script(attempts); + let actual = super::super::run_bounded_with_env( + &binary, + root, + Some(("JETT_RESOURCE_NATIVE_TEST_SCRIPT_V1", &encoded)), + ); + let report_path = root.join("resource-native-report-v1.bin"); + assert!(report_path.metadata().unwrap().len() <= 16 * 1024 * 1024); + let observed = report::decode(&fs::read(report_path).unwrap()).unwrap(); + assert_eq!( + actual.status.code(), + Some(expected(input).exit), + "{} {}: {actual:?}; {observed:?}", + input.name, + archive.profile + ); + assert!( + actual.stdout.is_empty() && actual.stderr.is_empty(), + "{actual:?}" + ); + assert_observed(input, &observed, usize::try_from(attempts).unwrap()); + eprintln!( + "ABSENT_NATIVE name={:?} release={release} attempts={attempts} source_deleted=true report={observed:?}", + input.name + ); +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum NativeAttemptMode { + Single, + CleanReentry, + AllReentry, +} + +fn run_native(mode: NativeAttemptMode) { + let clean_reentry = mode == NativeAttemptMode::CleanReentry; + assert_eq!(inputs::INPUTS.len(), 14); + let mut passed = 0; + let mut failures = Vec::new(); + for release in [false, true] { + let archive = archive(release); + for input in inputs::INPUTS { + if clean_reentry && matches!(input.reference, inputs::ReferenceExpectation::Error(_)) { + continue; + } + let attempts = if mode == NativeAttemptMode::Single { + 1 + } else { + 2 + }; + let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + execute(input, release, &archive, attempts) + })); + match outcome { + Ok(()) => passed += 1, + Err(payload) => { + failures.push((input.name, release, inputs::panic_message(payload.as_ref()))) + } + } + } + } + let required = if clean_reentry { 26 } else { 28 }; + assert_eq!(passed + failures.len(), required); + assert!( + failures.is_empty(), + "strict Source-deleted native gate failures: {failures:#?}" + ); + assert_eq!(passed, required); + let entries = required + * if mode == NativeAttemptMode::Single { + 1 + } else { + 2 + }; + eprintln!( + "ABSENT_NATIVE_TOTAL executables={passed} entries={entries} clean_reentry={clean_reentry}" + ); +} + +#[test] +#[ignore = "requires exact Root-measured double-cfg Debug/Release single-runtime archive receipt"] +fn native_resource_absent_aggregate_all_original_sources_retire_before_teardown() { + run_native(NativeAttemptMode::Single); +} + +#[test] +#[ignore = "requires exact Root-measured double-cfg Debug/Release single-runtime archive receipt"] +fn native_resource_absent_aggregate_thirteen_clean_sources_same_grant_reentry() { + // Source13 remains in the full single-entry gate above. Its original two-error + // reference behavior is a separate unresolved native failure-lifetime obligation. + run_native(NativeAttemptMode::CleanReentry); +} + +#[test] +#[ignore = "requires exact Root-measured ordinary-error-reentry Debug/Release archive receipt"] +fn native_resource_absent_aggregate_all_original_sources_reenter_with_same_provider_grant() { + // Every original row is selected, including Source13's signed bounds error + // twice on one retained context/provider/grant. assert_observed checks both + // original completions, exact diagnostics, zero pre-teardown obligations, + // continuous empty events and first process failure; execute deletes Source. + run_native(NativeAttemptMode::AllReentry); +} diff --git a/crates/jett_driver/tests/native_conformance/resource_occupied_struct_cases.rs b/crates/jett_driver/tests/native_conformance/resource_occupied_struct_cases.rs new file mode 100644 index 00000000..173e193b --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_occupied_struct_cases.rs @@ -0,0 +1,149 @@ +//! Shared original Source observations. These rows grant no runtime authority. +use super::cases::{Case, Event, ReferenceOutcome, Script}; +const SINGLE: &str = include_str!( + "../../../jett_comptime/src/resource_execution/fixtures/33_owned_struct_scope_drop_entry.jett" +); +const PAIR: &str = include_str!( + "../../../jett_comptime/src/resource_execution/fixtures/34_owned_struct_reverse_entry.jett" +); +const PREFIX: &str = include_str!( + "../../../jett_comptime/src/resource_execution/fixtures/35_owned_struct_abandoned_prefix_entry.jett" +); +const fn clean( + name: &'static str, + source: &'static str, + script: &'static [Script], + events: &'static [Event], +) -> Case { + Case { + name, + source, + script, + events, + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + } +} +const fn panic( + name: &'static str, + source: &'static str, + script: &'static [Script], + events: &'static [Event], +) -> Case { + Case { + name, + source, + script, + events, + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + } +} +pub(super) const CASES: &[Case] = &[ + clean( + "occupied_struct_single_ready", + SINGLE, + &[Script::Construct(1001)], + &[Event::Constructed(1001), Event::Finalized(1001)], + ), + clean( + "occupied_struct_single_factory_fail", + SINGLE, + &[Script::ConstructFail(1001, "declined")], + &[Event::ConstructionFailed(1001)], + ), + panic( + "occupied_struct_single_cleanup_panic", + SINGLE, + &[Script::FinalizerPanic(1001)], + &[Event::Constructed(1001), Event::Finalized(1001)], + ), + clean( + "occupied_struct_pair_ready", + PAIR, + &[Script::Construct(1002), Script::Construct(1003)], + &[ + Event::Constructed(1002), + Event::Constructed(1003), + Event::Finalized(1002), + Event::Finalized(1003), + ], + ), + clean( + "occupied_struct_pair_first_factory_fail", + PAIR, + &[Script::ConstructFail(1002, "declined")], + &[Event::ConstructionFailed(1002)], + ), + clean( + "occupied_struct_pair_second_factory_fail", + PAIR, + &[ + Script::Construct(1002), + Script::ConstructFail(1003, "declined"), + ], + &[ + Event::Constructed(1002), + Event::ConstructionFailed(1003), + Event::Finalized(1002), + ], + ), + panic( + "occupied_struct_pair_first_cleanup_panic", + PAIR, + &[Script::FinalizerPanic(1002), Script::Construct(1003)], + &[ + Event::Constructed(1002), + Event::Constructed(1003), + Event::Finalized(1002), + Event::Finalized(1003), + ], + ), + panic( + "occupied_struct_pair_second_cleanup_panic", + PAIR, + &[Script::Construct(1002), Script::FinalizerPanic(1003)], + &[ + Event::Constructed(1002), + Event::Constructed(1003), + Event::Finalized(1002), + Event::Finalized(1003), + ], + ), + clean( + "occupied_struct_prefix_ready", + PREFIX, + &[Script::Construct(1004)], + &[Event::Constructed(1004), Event::Finalized(1004)], + ), + clean( + "occupied_struct_prefix_factory_fail", + PREFIX, + &[Script::ConstructFail(1004, "declined")], + &[Event::ConstructionFailed(1004)], + ), + panic( + "occupied_struct_prefix_cleanup_panic", + PREFIX, + &[Script::FinalizerPanic(1004)], + &[Event::Constructed(1004), Event::Finalized(1004)], + ), +]; +pub(super) fn ready_for(first: &Case) -> &'static Case { + CASES + .iter() + .find(|case| { + case.source.as_bytes() == first.source.as_bytes() && case.name.ends_with("_ready") + }) + .unwrap() +} diff --git a/crates/jett_driver/tests/native_conformance/resource_occupied_struct_harness.rs b/crates/jett_driver/tests/native_conformance/resource_occupied_struct_harness.rs new file mode 100644 index 00000000..e9b30041 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_occupied_struct_harness.rs @@ -0,0 +1,33 @@ +//! Exact original occupied Sources; receipt guards and retirement oracle are shared. +use super::{ + archive, cases, fs, link, object, report, run_native_cases, run_object_preflight, script, +}; +#[path = "resource_occupied_struct_cases.rs"] +mod occupied_struct_cases; +#[path = "resource_occupied_struct_reentry.rs"] +mod reentry; + +#[test] +fn native_resource_occupied_struct_three_sources_emit_in_both_profiles() { + assert_eq!(occupied_struct_cases::CASES.len(), 11); + run_object_preflight(occupied_struct_cases::CASES, "occupied-struct"); +} +#[test] +#[ignore = "requires fresh independently measured occupied-capable Debug/Release single-runtime archives"] +fn native_resource_occupied_struct_scripts_retire_before_teardown() { + assert_eq!(occupied_struct_cases::CASES.len(), 11); + run_native_cases(occupied_struct_cases::CASES); +} +#[test] +#[ignore = "requires fresh independently measured occupied-capable Debug/Release single-runtime archives"] +fn native_resource_occupied_struct_scripts_reenter_with_same_provider_grant() { + for release in [false, true] { + for first in occupied_struct_cases::CASES { + reentry::run_original_then_ready( + first, + occupied_struct_cases::ready_for(first), + release, + ); + } + } +} diff --git a/crates/jett_driver/tests/native_conformance/resource_occupied_struct_reentry.rs b/crates/jett_driver/tests/native_conformance/resource_occupied_struct_reentry.rs new file mode 100644 index 00000000..3a991f54 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_occupied_struct_reentry.rs @@ -0,0 +1,178 @@ +//! Original Source06 first script followed by READY on one retained Session. +//! Every completion remains observable; exceptional reentry is never exit zero. +use super::{archive, cases, fs, link, object, report, script}; + +fn script_bytes(case: &cases::Case) -> Vec { + let encoded = script(case); + let bytes = encoded + .as_bytes() + .chunks_exact(2) + .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap()) + .collect::>(); + assert_eq!(&bytes[..8], b"JTRST001"); + assert_eq!(u32::from_le_bytes(bytes[24..28].try_into().unwrap()), 1); + assert_eq!( + u32::from_le_bytes(bytes[28..32].try_into().unwrap()) as usize, + case.script.len() + ); + assert_eq!( + u64::from_le_bytes(bytes[16..24].try_into().unwrap()) as usize, + bytes.len() + ); + bytes +} + +fn original_then_ready_script(first: &cases::Case, ready: &cases::Case) -> String { + assert_eq!(first.source.as_bytes(), ready.source.as_bytes()); + assert_eq!( + (ready.body, ready.cleanup, ready.channel, ready.exit), + (0, 0, 0, 0) + ); + let mut bytes = script_bytes(first); + let ready_bytes = script_bytes(ready); + assert_eq!(&bytes[..16], &ready_bytes[..16]); + bytes.extend_from_slice(&ready_bytes[32..]); + bytes[24..28].copy_from_slice(&2u32.to_le_bytes()); + let operations = u32::try_from(first.script.len() + ready.script.len()).unwrap(); + bytes[28..32].copy_from_slice(&operations.to_le_bytes()); + let length = u64::try_from(bytes.len()).unwrap(); + bytes[16..24].copy_from_slice(&length.to_le_bytes()); + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + +fn channel(expected: u32) -> report::FailureChannel { + match expected { + 0 => report::FailureChannel::Clean, + 1 => report::FailureChannel::Body, + 2 => report::FailureChannel::HostPanic, + 3 => report::FailureChannel::Cleanup, + _ => panic!("invalid native completion oracle"), + } +} + +fn assert_completion(case: &cases::Case, attempt: &report::Attempt, end: usize) { + assert_eq!( + ( + attempt.completion.body_status, + attempt.completion.cleanup_status, + attempt.completion.selected_channel, + ), + (case.body, case.cleanup, channel(case.channel)), + "{} retained completion: {attempt:?}", + case.name + ); + assert_eq!(attempt.ordinary_message, case.ordinary_message); + assert_eq!(attempt.resource_message, case.resource_message); + assert!( + attempt.counts.resources_retired() && attempt.counts.ordinary_empty, + "{} BEFORE teardown: {attempt:?}", + case.name + ); + assert_eq!(attempt.event_sequence_end, u64::try_from(end).unwrap()); +} + +fn assert_original_then_ready(first: &cases::Case, ready: &cases::Case, observed: &report::Report) { + assert_eq!(observed.attempts.len(), 2, "{}: {observed:?}", first.name); + let first_id = observed.attempts[0].completion.attempt; + let second_id = observed.attempts[1].completion.attempt; + assert!(first_id != 0 && second_id > first_id); + assert_completion(first, &observed.attempts[0], first.events.len()); + assert_completion( + ready, + &observed.attempts[1], + first.events.len() + ready.events.len(), + ); + assert_eq!( + observed.events.len(), + first.events.len() + ready.events.len() + ); + for (index, (actual, expected)) in observed + .events + .iter() + .zip(first.events.iter().chain(ready.events.iter())) + .enumerate() + { + let (label, kind) = super::super::event(*expected); + assert_eq!( + (actual.sequence, actual.label, actual.kind), + ((index + 1) as u64, label, kind), + "{} event {index}", + first.name + ); + } + assert_eq!(observed.script_remaining, 0); + assert_eq!( + ( + observed.created, + observed.destroy_attempts, + observed.destroy_status, + observed.write_status, + ), + (true, 1, 0, 0) + ); + assert!(observed.observations_ready_for_acceptance()); + assert_eq!( + observed.all_attempts_succeeded(), + first.body == 0 && first.cleanup == 0 && first.channel == 0 + ); +} + +pub(super) fn run_original_then_ready(first: &cases::Case, ready: &cases::Case, release: bool) { + let archive = archive(release); + let directory = tempfile::tempdir().unwrap(); + let root = directory.path(); + let project = root.join("project"); + let stdlib = root.join("synthetic-stdlib"); + fs::create_dir(&project).unwrap(); + fs::create_dir(&stdlib).unwrap(); + let entry = project.join("main.jett"); + let support = stdlib.join("resource_probe.jett"); + fs::write(&entry, first.source).unwrap(); + fs::write(&support, cases::SUPPORT).unwrap(); + let artifact = object(&entry, &support, release); + assert_eq!(artifact.target, archive.target); + assert!( + artifact + .symbols + .iter() + .any(|name| name == jett_codegen_cranelift::JETT_AOT_ENTRY_SYMBOL_V1) + ); + assert!( + artifact + .symbols + .iter() + .any(|name| name == "jett_aot_resource_v1_manifest") + ); + let object_path = root.join("resource.obj"); + let binary = root.join(if cfg!(windows) { + "resource.exe" + } else { + "resource" + }); + fs::write(&object_path, artifact.bytes).unwrap(); + link(&archive, &object_path, &binary, root); + fs::remove_dir_all(&project).unwrap(); + fs::remove_dir_all(&stdlib).unwrap(); + assert!(!entry.exists() && !support.exists() && !project.exists() && !stdlib.exists()); + let encoded = original_then_ready_script(first, ready); + let actual = super::super::super::run_bounded_with_env( + &binary, + root, + Some(("JETT_RESOURCE_NATIVE_TEST_SCRIPT_V1", &encoded)), + ); + let path = root.join("resource-native-report-v1.bin"); + assert!(path.metadata().unwrap().len() <= 16 * 1024 * 1024); + let observed = report::decode(&fs::read(path).unwrap()).unwrap(); + assert_eq!( + actual.status.code(), + Some(first.exit), + "{} {} retained first exit: {actual:?}; {observed:?}", + first.name, + archive.profile + ); + assert!( + actual.stdout.is_empty() && actual.stderr.is_empty(), + "{actual:?}" + ); + assert_original_then_ready(first, ready, &observed); +} diff --git a/crates/jett_driver/tests/native_conformance/resource_reflected_derived_cases.rs b/crates/jett_driver/tests/native_conformance/resource_reflected_derived_cases.rs new file mode 100644 index 00000000..6e85efa7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_reflected_derived_cases.rs @@ -0,0 +1,249 @@ +//! Shared exact derived Source/script observations; no execution or custody authority. +use super::cases::{Case, Event, ReferenceOutcome, Script}; + +const BREAK_SOURCE: &str = include_str!( + "../../../jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/07_reflected_field_optional_break.jett" +); +const CONTINUE_SOURCE: &str = include_str!( + "../../../jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/08_reflected_field_optional_continue.jett" +); +const DEFAULT_SOURCE: &str = include_str!( + "../../../jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/09_reflected_field_optional_scalar_default.jett" +); + +const READY_SCRIPT: &[Script] = &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), +]; +const READY_EVENTS: &[Event] = &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), +]; + +// First ten rows preserve the existing reference DERIVED_INPUTS exactly. +pub(super) const CASES: &[Case] = &[ + Case { + name: "reflected_resource_break_ready", + source: BREAK_SOURCE, + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_resource_break_first_borrow_fail", + source: BREAK_SOURCE, + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_resource_break_cleanup_panic", + source: BREAK_SOURCE, + script: &[Script::FinalizerPanic(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "reflected_resource_continue_ready", + source: CONTINUE_SOURCE, + script: READY_SCRIPT, + events: READY_EVENTS, + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_resource_continue_first_borrow_fail", + source: CONTINUE_SOURCE, + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_resource_continue_later_borrow_fail", + source: CONTINUE_SOURCE, + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::BorrowFail(871, "later borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_resource_continue_cleanup_panic", + source: CONTINUE_SOURCE, + script: &[ + Script::FinalizerPanic(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: READY_EVENTS, + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "reflected_resource_scalar_default_first_borrow_fail", + source: DEFAULT_SOURCE, + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_resource_scalar_default_later_borrow_fail", + source: DEFAULT_SOURCE, + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::BorrowFail(871, "later borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_resource_scalar_default_cleanup_panic", + source: DEFAULT_SOURCE, + script: &[ + Script::FinalizerPanic(871), + Script::BorrowFail(871, "borrow sentinel"), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + // Additional same-Source successful tail for scalar Default reentry. + Case { + name: "reflected_resource_scalar_default_ready", + source: DEFAULT_SOURCE, + script: READY_SCRIPT, + events: READY_EVENTS, + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, +]; + +pub(super) fn original_cases() -> &'static [Case] { + &CASES[..10] +} + +pub(super) fn ready_for(case: &Case) -> &'static Case { + if case.source == BREAK_SOURCE { + &CASES[0] + } else if case.source == CONTINUE_SOURCE { + &CASES[3] + } else { + assert_eq!(case.source, DEFAULT_SOURCE); + &CASES[10] + } +} diff --git a/crates/jett_driver/tests/native_conformance/resource_reflected_derived_harness.rs b/crates/jett_driver/tests/native_conformance/resource_reflected_derived_harness.rs new file mode 100644 index 00000000..aa7b4304 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_reflected_derived_harness.rs @@ -0,0 +1,34 @@ +//! Exact derived reflected Sources through object, single-entry and reentry gates. +use super::{reflected_derived_cases, run_native_cases, run_object_preflight}; + +#[test] +fn native_resource_reflected_derived_controls_emit_in_both_profiles() { + assert_eq!(reflected_derived_cases::CASES.len(), 11); + assert_eq!(reflected_derived_cases::original_cases().len(), 10); + run_object_preflight(reflected_derived_cases::CASES, "reflected-derived-controls"); +} + +#[test] +#[ignore = "requires exact independently measured reentry-capable Debug/Release single-runtime archive receipt"] +fn native_resource_reflected_derived_controls_retire_before_teardown() { + assert_eq!(reflected_derived_cases::CASES.len(), 11); + run_native_cases(reflected_derived_cases::CASES); + eprintln!( + "Resource reflected derived single-entry acceptance: 10 original scripts plus scalar-default READY baseline; 22 Source-deleted executions; profiles=debug,release" + ); +} + +#[test] +#[ignore = "requires exact independently measured reentry-capable Debug/Release single-runtime archive receipt"] +fn native_resource_reflected_derived_controls_reenter_with_same_provider_grant() { + assert_eq!(reflected_derived_cases::original_cases().len(), 10); + for release in [false, true] { + for first in reflected_derived_cases::original_cases() { + let ready = reflected_derived_cases::ready_for(first); + super::source06_reentry::run_original_then_ready(first, ready, release); + } + } + eprintln!( + "Resource reflected derived same-grant reentry acceptance: 10 original scripts then their same-Source READY; 20 Source-deleted executions; 40 entries; profiles=debug,release; retained completions and first nonzero exits" + ); +} diff --git a/crates/jett_driver/tests/native_conformance/resource_reflected_field_cases.rs b/crates/jett_driver/tests/native_conformance/resource_reflected_field_cases.rs new file mode 100644 index 00000000..8076234d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_reflected_field_cases.rs @@ -0,0 +1,219 @@ +//! Standalone reflected-field Sources; existing191case catalog remains unchanged. +//! Exact tuples are checked by single-entry and same-grant native execution. +use super::cases::{Case, Event, ReferenceOutcome, Script}; + +const READY_SCRIPT: &[Script] = &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), +]; +const READY_EVENTS: &[Event] = &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), +]; +pub(crate) const CASES: &[Case] = &[ + Case { + name: "reflected_field_original", + source: include_str!("resource/reflected_field_native/01_original.jett"), + script: READY_SCRIPT, + events: READY_EVENTS, + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_field_index_distinct", + source: include_str!("resource/reflected_field_native/02_index_distinct.jett"), + script: READY_SCRIPT, + events: READY_EVENTS, + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_field_name_distinct", + source: include_str!("resource/reflected_field_native/03_name_distinct.jett"), + script: READY_SCRIPT, + events: READY_EVENTS, + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_field_ordinary_helper", + source: include_str!("resource/reflected_field_native/04_ordinary_helper.jett"), + script: READY_SCRIPT, + events: READY_EVENTS, + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, +]; + +// Exact original Source06 scripts from its checked exceptional reference gate. +// Native statuses/messages use the existing matched-launcher completion contract; +// these new enclosing cases require linked execution before acceptance. +const SOURCE06: &str = include_str!("resource/reflected_field_native/01_original.jett"); +pub(crate) const SOURCE06_EXCEPTIONAL_CASES: &[Case] = &[ + Case { + name: "reflected_field_construct_fail", + source: SOURCE06, + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_field_first_borrow_fail", + source: SOURCE06, + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_field_later_borrow_fail", + source: SOURCE06, + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::BorrowFail(871, "later borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_field_ready_cleanup_panic", + source: SOURCE06, + script: &[ + Script::FinalizerPanic(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: READY_EVENTS, + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "reflected_field_first_borrow_provider_panic", + source: SOURCE06, + script: &[Script::Construct(871), Script::BorrowPanic(871)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::ProviderPanic("selected test borrow provider panic"), + body: 1, + cleanup: 0, + channel: 2, + ordinary_message: b"", + resource_message: b"runtime operation panicked", + exit: 73, + }, + Case { + name: "reflected_field_later_borrow_provider_panic", + source: SOURCE06, + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::BorrowPanic(871), + ], + events: READY_EVENTS, + reference: ReferenceOutcome::ProviderPanic("selected test borrow provider panic"), + body: 1, + cleanup: 0, + channel: 2, + ordinary_message: b"", + resource_message: b"runtime operation panicked", + exit: 73, + }, + Case { + name: "reflected_field_cleanup_wins_provider_panic", + source: SOURCE06, + script: &[Script::FinalizerPanic(871), Script::BorrowPanic(871)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 1, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "reflected_field_cleanup_wins_domain_failure_return", + source: SOURCE06, + script: &[ + Script::FinalizerPanic(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 0, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, +]; diff --git a/crates/jett_driver/tests/native_conformance/resource_reflected_field_harness.rs b/crates/jett_driver/tests/native_conformance/resource_reflected_field_harness.rs new file mode 100644 index 00000000..39b2730d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_reflected_field_harness.rs @@ -0,0 +1,211 @@ +//! Four original checked Sources through strict final emit and matched native execution. +use super::{ + archive, cases, fs, link, object, report, run_native_cases, run_object_preflight, script, +}; + +#[path = "resource_reflected_field_cases.rs"] +mod reflected_field_cases; + +#[test] +fn native_resource_reflected_field_four_sources_emit_in_both_profiles() { + assert_eq!(reflected_field_cases::CASES.len(), 4); + run_object_preflight(reflected_field_cases::CASES, "reflected-field"); +} + +#[test] +#[ignore = "requires exact Root-measured double-cfg Debug/Release single-runtime archive receipt"] +fn native_resource_reflected_field_four_sources_retire_before_teardown() { + assert_eq!(reflected_field_cases::CASES.len(), 4); + run_native_cases(reflected_field_cases::CASES); + eprintln!( + "Resource reflected-field native acceptance: 4 cases; 8 Source-deleted executions; profiles=debug,release" + ); +} + +fn two_clean_entries(case: &cases::Case) -> String { + assert_eq!( + (case.body, case.cleanup, case.channel, case.exit), + (0, 0, 0, 0) + ); + let encoded = script(case); + let mut bytes = encoded + .as_bytes() + .chunks_exact(2) + .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap()) + .collect::>(); + assert_eq!(&bytes[..8], b"JTRST001"); + assert_eq!(u32::from_le_bytes(bytes[24..28].try_into().unwrap()), 1); + assert_eq!(case.script.len(), 3); + assert_eq!(bytes.len(), 84); + let operations = bytes[32..].to_vec(); + bytes.extend(operations); + bytes[24..28].copy_from_slice(&2u32.to_le_bytes()); + bytes[28..32].copy_from_slice(&6u32.to_le_bytes()); + let length = bytes.len() as u64; + bytes[16..24].copy_from_slice(&length.to_le_bytes()); + assert_eq!(bytes.len(), 136); + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + +fn assert_clean_reentry(case: &cases::Case, observed: &report::Report) { + assert_eq!(observed.attempts.len(), 2, "{}: {observed:?}", case.name); + let mut previous = 0; + for (index, attempt) in observed.attempts.iter().enumerate() { + assert!( + attempt.completion.attempt > previous, + "{} attempt identity", + case.name + ); + previous = attempt.completion.attempt; + assert_eq!( + ( + attempt.completion.body_status, + attempt.completion.cleanup_status, + attempt.completion.selected_channel + ), + (0, 0, report::FailureChannel::Clean), + "{} entry {index}: {observed:?}", + case.name + ); + assert!(attempt.ordinary_message.is_empty() && attempt.resource_message.is_empty()); + assert!( + attempt.counts.resources_retired() && attempt.counts.ordinary_empty, + "{} entry {index} BEFORE teardown: {observed:?}", + case.name + ); + assert_eq!( + attempt.event_sequence_end, + ((index + 1) * case.events.len()) as u64 + ); + } + assert_eq!(observed.events.len(), 2 * case.events.len()); + for (index, (actual, expected)) in observed + .events + .iter() + .zip(case.events.iter().cycle().take(2 * case.events.len())) + .enumerate() + { + let (label, kind) = super::event(*expected); + assert_eq!( + (actual.sequence, actual.label, actual.kind), + ((index + 1) as u64, label, kind), + "{} event {index}", + case.name + ); + } + assert_eq!(observed.script_remaining, 0); + assert_eq!( + ( + observed.created, + observed.destroy_attempts, + observed.destroy_status, + observed.write_status + ), + (true, 1, 0, 0) + ); + assert!(observed.observations_ready_for_acceptance()); + assert!(observed.all_attempts_succeeded()); +} + +#[test] +#[ignore = "requires exact Root-measured double-cfg Debug/Release single-runtime archive receipt"] +fn native_resource_reflected_field_four_sources_clean_same_grant_reentry() { + for release in [false, true] { + let archive = archive(release); + for case in reflected_field_cases::CASES { + let directory = tempfile::tempdir().unwrap(); + let root = directory.path(); + let project = root.join("project"); + let stdlib = root.join("synthetic-stdlib"); + fs::create_dir(&project).unwrap(); + fs::create_dir(&stdlib).unwrap(); + let entry = project.join("main.jett"); + let support = stdlib.join("resource_probe.jett"); + fs::write(&entry, case.source).unwrap(); + fs::write(&support, cases::SUPPORT).unwrap(); + let artifact = object(&entry, &support, release); + assert_eq!(artifact.target, archive.target); + assert!( + artifact + .symbols + .iter() + .any(|name| name == jett_codegen_cranelift::JETT_AOT_ENTRY_SYMBOL_V1) + ); + assert!( + artifact + .symbols + .iter() + .any(|name| name == "jett_aot_resource_v1_manifest") + ); + let object_path = root.join("resource.obj"); + let binary = root.join(if cfg!(windows) { + "resource.exe" + } else { + "resource" + }); + fs::write(&object_path, artifact.bytes).unwrap(); + link(&archive, &object_path, &binary, root); + fs::remove_dir_all(&project).unwrap(); + fs::remove_dir_all(&stdlib).unwrap(); + assert!(!entry.exists() && !support.exists() && !project.exists() && !stdlib.exists()); + let encoded = two_clean_entries(case); + let actual = super::super::run_bounded_with_env( + &binary, + root, + Some(("JETT_RESOURCE_NATIVE_TEST_SCRIPT_V1", &encoded)), + ); + let path = root.join("resource-native-report-v1.bin"); + assert!(path.metadata().unwrap().len() <= 16 * 1024 * 1024); + let observed = report::decode(&fs::read(path).unwrap()).unwrap(); + assert_eq!( + actual.status.code(), + Some(0), + "{} {}: {actual:?}; {observed:?}", + case.name, + archive.profile + ); + assert!( + actual.stdout.is_empty() && actual.stderr.is_empty(), + "{actual:?}" + ); + assert_clean_reentry(case, &observed); + } + } + eprintln!( + "Resource reflected-field clean reentry acceptance: 4 cases; 8 Source-deleted executions; 16 entries on the same per-executable provider/grant; profiles=debug,release" + ); +} + +#[test] +fn native_resource_reflected_field_source06_exceptional_scripts_emit_in_both_profiles() { + assert_eq!(reflected_field_cases::SOURCE06_EXCEPTIONAL_CASES.len(), 8); + run_object_preflight( + reflected_field_cases::SOURCE06_EXCEPTIONAL_CASES, + "reflected-field-source06-exceptional", + ); +} + +#[test] +#[ignore = "requires exact Root-measured double-cfg Debug/Release single-runtime archive receipt"] +fn native_resource_reflected_field_source06_exceptional_scripts_retire_before_teardown() { + assert_eq!(reflected_field_cases::SOURCE06_EXCEPTIONAL_CASES.len(), 8); + run_native_cases(reflected_field_cases::SOURCE06_EXCEPTIONAL_CASES); + eprintln!( + "Resource reflected-field exceptional single-entry acceptance: 8 cases; 16 Source-deleted executions; profiles=debug,release; exceptional same-grant reentry remains separate" + ); +} + +#[path = "resource_reflected_field_reentry.rs"] +mod source06_reentry; + +#[path = "resource_reflected_derived_cases.rs"] +mod reflected_derived_cases; + +#[path = "resource_reflected_derived_harness.rs"] +mod derived_controls; + +#[path = "resource_reflected_return_cases.rs"] +mod reflected_return_cases; + +#[path = "resource_reflected_return_harness.rs"] +mod reflected_returns; diff --git a/crates/jett_driver/tests/native_conformance/resource_reflected_field_reentry.rs b/crates/jett_driver/tests/native_conformance/resource_reflected_field_reentry.rs new file mode 100644 index 00000000..73b87b49 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_reflected_field_reentry.rs @@ -0,0 +1,200 @@ +//! Original Source06 first script followed by READY on one retained Session. +//! Every completion remains observable; exceptional reentry is never exit zero. +use super::{archive, cases, fs, link, object, reflected_field_cases, report, script}; + +fn original_cases() -> impl Iterator { + std::iter::once(&reflected_field_cases::CASES[0]) + .chain(reflected_field_cases::SOURCE06_EXCEPTIONAL_CASES.iter()) +} + +fn script_bytes(case: &cases::Case) -> Vec { + let encoded = script(case); + let bytes = encoded + .as_bytes() + .chunks_exact(2) + .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap()) + .collect::>(); + assert_eq!(&bytes[..8], b"JTRST001"); + assert_eq!(u32::from_le_bytes(bytes[24..28].try_into().unwrap()), 1); + assert_eq!( + u32::from_le_bytes(bytes[28..32].try_into().unwrap()) as usize, + case.script.len() + ); + assert_eq!( + u64::from_le_bytes(bytes[16..24].try_into().unwrap()) as usize, + bytes.len() + ); + bytes +} + +fn original_then_ready_script(first: &cases::Case, ready: &cases::Case) -> String { + assert_eq!(first.source.as_bytes(), ready.source.as_bytes()); + assert_eq!( + (ready.body, ready.cleanup, ready.channel, ready.exit), + (0, 0, 0, 0) + ); + let mut bytes = script_bytes(first); + let ready_bytes = script_bytes(ready); + assert_eq!(&bytes[..16], &ready_bytes[..16]); + bytes.extend_from_slice(&ready_bytes[32..]); + bytes[24..28].copy_from_slice(&2u32.to_le_bytes()); + let operations = u32::try_from(first.script.len() + ready.script.len()).unwrap(); + bytes[28..32].copy_from_slice(&operations.to_le_bytes()); + let length = u64::try_from(bytes.len()).unwrap(); + bytes[16..24].copy_from_slice(&length.to_le_bytes()); + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + +fn channel(expected: u32) -> report::FailureChannel { + match expected { + 0 => report::FailureChannel::Clean, + 1 => report::FailureChannel::Body, + 2 => report::FailureChannel::HostPanic, + 3 => report::FailureChannel::Cleanup, + _ => panic!("invalid native completion oracle"), + } +} + +fn assert_completion(case: &cases::Case, attempt: &report::Attempt, end: usize) { + assert_eq!( + ( + attempt.completion.body_status, + attempt.completion.cleanup_status, + attempt.completion.selected_channel, + ), + (case.body, case.cleanup, channel(case.channel)), + "{} retained completion: {attempt:?}", + case.name + ); + assert_eq!(attempt.ordinary_message, case.ordinary_message); + assert_eq!(attempt.resource_message, case.resource_message); + assert!( + attempt.counts.resources_retired() && attempt.counts.ordinary_empty, + "{} BEFORE teardown: {attempt:?}", + case.name + ); + assert_eq!(attempt.event_sequence_end, u64::try_from(end).unwrap()); +} + +fn assert_original_then_ready(first: &cases::Case, ready: &cases::Case, observed: &report::Report) { + assert_eq!(observed.attempts.len(), 2, "{}: {observed:?}", first.name); + let first_id = observed.attempts[0].completion.attempt; + let second_id = observed.attempts[1].completion.attempt; + assert!(first_id != 0 && second_id > first_id); + assert_completion(first, &observed.attempts[0], first.events.len()); + assert_completion( + ready, + &observed.attempts[1], + first.events.len() + ready.events.len(), + ); + assert_eq!( + observed.events.len(), + first.events.len() + ready.events.len() + ); + for (index, (actual, expected)) in observed + .events + .iter() + .zip(first.events.iter().chain(ready.events.iter())) + .enumerate() + { + let (label, kind) = super::super::event(*expected); + assert_eq!( + (actual.sequence, actual.label, actual.kind), + ((index + 1) as u64, label, kind), + "{} event {index}", + first.name + ); + } + assert_eq!(observed.script_remaining, 0); + assert_eq!( + ( + observed.created, + observed.destroy_attempts, + observed.destroy_status, + observed.write_status, + ), + (true, 1, 0, 0) + ); + assert!(observed.observations_ready_for_acceptance()); + assert_eq!( + observed.all_attempts_succeeded(), + first.body == 0 && first.cleanup == 0 && first.channel == 0 + ); +} + +pub(super) fn run_original_then_ready(first: &cases::Case, ready: &cases::Case, release: bool) { + let archive = archive(release); + let directory = tempfile::tempdir().unwrap(); + let root = directory.path(); + let project = root.join("project"); + let stdlib = root.join("synthetic-stdlib"); + fs::create_dir(&project).unwrap(); + fs::create_dir(&stdlib).unwrap(); + let entry = project.join("main.jett"); + let support = stdlib.join("resource_probe.jett"); + fs::write(&entry, first.source).unwrap(); + fs::write(&support, cases::SUPPORT).unwrap(); + let artifact = object(&entry, &support, release); + assert_eq!(artifact.target, archive.target); + assert!( + artifact + .symbols + .iter() + .any(|name| name == jett_codegen_cranelift::JETT_AOT_ENTRY_SYMBOL_V1) + ); + assert!( + artifact + .symbols + .iter() + .any(|name| name == "jett_aot_resource_v1_manifest") + ); + let object_path = root.join("resource.obj"); + let binary = root.join(if cfg!(windows) { + "resource.exe" + } else { + "resource" + }); + fs::write(&object_path, artifact.bytes).unwrap(); + link(&archive, &object_path, &binary, root); + fs::remove_dir_all(&project).unwrap(); + fs::remove_dir_all(&stdlib).unwrap(); + assert!(!entry.exists() && !support.exists() && !project.exists() && !stdlib.exists()); + let encoded = original_then_ready_script(first, ready); + let actual = super::super::super::run_bounded_with_env( + &binary, + root, + Some(("JETT_RESOURCE_NATIVE_TEST_SCRIPT_V1", &encoded)), + ); + let path = root.join("resource-native-report-v1.bin"); + assert!(path.metadata().unwrap().len() <= 16 * 1024 * 1024); + let observed = report::decode(&fs::read(path).unwrap()).unwrap(); + assert_eq!( + actual.status.code(), + Some(first.exit), + "{} {} retained first exit: {actual:?}; {observed:?}", + first.name, + archive.profile + ); + assert!( + actual.stdout.is_empty() && actual.stderr.is_empty(), + "{actual:?}" + ); + assert_original_then_ready(first, ready, &observed); +} + +#[test] +#[ignore = "requires fresh independently measured reentry-capable Debug/Release single-runtime archives"] +fn native_resource_reflected_field_source06_all_scripts_reenter_with_same_provider_grant() { + let ready = &reflected_field_cases::CASES[0]; + assert_eq!(ready.name, "reflected_field_original"); + assert_eq!(reflected_field_cases::SOURCE06_EXCEPTIONAL_CASES.len(), 8); + assert_eq!(original_cases().count(), 9); + for release in [false, true] { + for first in original_cases() { + run_original_then_ready(first, ready, release); + } + } + eprintln!( + "Resource reflected Source06 same-grant reentry acceptance: 9 original scripts then READY; 18 Source-deleted executions; 36 entries; profiles=debug,release; retained original completions and first nonzero exits" + ); +} diff --git a/crates/jett_driver/tests/native_conformance/resource_reflected_return_cases.rs b/crates/jett_driver/tests/native_conformance/resource_reflected_return_cases.rs new file mode 100644 index 00000000..1f48e9dc --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_reflected_return_cases.rs @@ -0,0 +1,297 @@ +//! Shared proposed reflected body/owned Return observations; no custody authority. +//! All new Source/completion oracles require actual reference/native gates. +use super::cases::{Case, Event, ReferenceOutcome, Script}; + +const BODY_SOURCE: &str = include_str!( + "../../../jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/10_reflected_field_successful_return.jett" +); +const OWNED_SOURCE: &str = include_str!( + "../../../jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/11_reflected_field_owned_return.jett" +); + +pub(super) const CASES: &[Case] = &[ + Case { + name: "reflected_body_return_first_ready", + source: BODY_SOURCE, + script: &[Script::Construct(881), Script::Borrow(881, 17)], + events: &[ + Event::Constructed(881), + Event::Borrowed(881), + Event::Finalized(881), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_body_return_second_ready", + source: BODY_SOURCE, + script: &[ + Script::Construct(881), + Script::Borrow(881, 11), + Script::Borrow(881, 17), + ], + events: &[ + Event::Constructed(881), + Event::Borrowed(881), + Event::Borrowed(881), + Event::Finalized(881), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_body_return_first_borrow_fail", + source: BODY_SOURCE, + script: &[ + Script::Construct(881), + Script::BorrowFail(881, "borrow sentinel"), + ], + events: &[ + Event::Constructed(881), + Event::BorrowFailed(881), + Event::Finalized(881), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_body_return_second_borrow_fail", + source: BODY_SOURCE, + script: &[ + Script::Construct(881), + Script::Borrow(881, 11), + Script::BorrowFail(881, "later borrow sentinel"), + ], + events: &[ + Event::Constructed(881), + Event::Borrowed(881), + Event::BorrowFailed(881), + Event::Finalized(881), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_body_return_second_cleanup_panic", + source: BODY_SOURCE, + script: &[ + Script::FinalizerPanic(881), + Script::Borrow(881, 11), + Script::Borrow(881, 17), + ], + events: &[ + Event::Constructed(881), + Event::Borrowed(881), + Event::Borrowed(881), + Event::Finalized(881), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 0, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "reflected_body_return_handled_failure_cleanup_panic", + source: BODY_SOURCE, + script: &[ + Script::FinalizerPanic(881), + Script::BorrowFail(881, "borrow sentinel"), + ], + events: &[ + Event::Constructed(881), + Event::BorrowFailed(881), + Event::Finalized(881), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 0, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "reflected_owned_return_first_ready", + source: OWNED_SOURCE, + script: &[ + Script::Construct(891), + Script::Construct(892), + Script::Borrow(891, 17), + Script::Borrow(892, 29), + ], + events: &[ + Event::Constructed(891), + Event::Constructed(892), + Event::Borrowed(891), + Event::Finalized(891), + Event::Borrowed(892), + Event::Finalized(892), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_owned_return_second_ready", + source: OWNED_SOURCE, + script: &[ + Script::Construct(891), + Script::Construct(892), + Script::Borrow(891, 11), + Script::Borrow(891, 17), + Script::Borrow(892, 29), + ], + events: &[ + Event::Constructed(891), + Event::Constructed(892), + Event::Borrowed(891), + Event::Borrowed(891), + Event::Finalized(891), + Event::Borrowed(892), + Event::Finalized(892), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_owned_return_first_borrow_fail", + source: OWNED_SOURCE, + script: &[ + Script::Construct(891), + Script::Construct(892), + Script::BorrowFail(891, "borrow sentinel"), + Script::Borrow(892, 29), + ], + events: &[ + Event::Constructed(891), + Event::Constructed(892), + Event::BorrowFailed(891), + Event::Finalized(891), + Event::Borrowed(892), + Event::Finalized(892), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_owned_return_second_borrow_fail", + source: OWNED_SOURCE, + script: &[ + Script::Construct(891), + Script::Construct(892), + Script::Borrow(891, 11), + Script::BorrowFail(891, "later borrow sentinel"), + Script::Borrow(892, 29), + ], + events: &[ + Event::Constructed(891), + Event::Constructed(892), + Event::Borrowed(891), + Event::BorrowFailed(891), + Event::Finalized(891), + Event::Borrowed(892), + Event::Finalized(892), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_owned_return_cleanup_suppresses_publication", + source: OWNED_SOURCE, + script: &[ + Script::FinalizerPanic(891), + Script::Construct(892), + Script::Borrow(891, 17), + ], + events: &[ + Event::Constructed(891), + Event::Constructed(892), + Event::Borrowed(891), + Event::Finalized(891), + Event::Finalized(892), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "reflected_owned_return_handled_failure_cleanup_suppresses_publication", + source: OWNED_SOURCE, + script: &[ + Script::FinalizerPanic(891), + Script::Construct(892), + Script::Borrow(891, 11), + Script::BorrowFail(891, "later borrow sentinel"), + ], + events: &[ + Event::Constructed(891), + Event::Constructed(892), + Event::Borrowed(891), + Event::BorrowFailed(891), + Event::Finalized(891), + Event::Finalized(892), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, +]; + +pub(super) fn ready_for(case: &Case) -> &'static Case { + if case.source == BODY_SOURCE { + &CASES[0] + } else { + assert_eq!(case.source, OWNED_SOURCE); + &CASES[6] + } +} diff --git a/crates/jett_driver/tests/native_conformance/resource_reflected_return_harness.rs b/crates/jett_driver/tests/native_conformance/resource_reflected_return_harness.rs new file mode 100644 index 00000000..fa650365 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_reflected_return_harness.rs @@ -0,0 +1,36 @@ +//! Dedicated reflected Return objects, Source-deleted singles and immutable reentry. +use super::{reflected_return_cases, run_native_cases, run_object_preflight}; + +#[test] +fn native_resource_reflected_body_and_owned_returns_emit_in_both_profiles() { + assert_eq!(reflected_return_cases::CASES.len(), 12); + run_object_preflight( + reflected_return_cases::CASES, + "reflected-body-and-owned-returns", + ); +} + +#[test] +#[ignore = "requires exact independently measured reentry-capable Debug/Release single-runtime archive receipt"] +fn native_resource_reflected_body_and_owned_returns_retire_before_teardown() { + assert_eq!(reflected_return_cases::CASES.len(), 12); + run_native_cases(reflected_return_cases::CASES); + eprintln!( + "Resource dedicated reflected Return single-entry acceptance: 12 cases; 24 Source-deleted executions; profiles=debug,release" + ); +} + +#[test] +#[ignore = "requires exact independently measured reentry-capable Debug/Release single-runtime archive receipt"] +fn native_resource_reflected_body_and_owned_returns_reenter_with_same_provider_grant() { + assert_eq!(reflected_return_cases::CASES.len(), 12); + for release in [false, true] { + for first in reflected_return_cases::CASES { + let ready = reflected_return_cases::ready_for(first); + super::source06_reentry::run_original_then_ready(first, ready, release); + } + } + eprintln!( + "Resource dedicated reflected Return same-grant reentry acceptance: 12 first scripts then same-Source READY; 24 Source-deleted executions; 48 entries; profiles=debug,release; retained original completions and first exits" + ); +} diff --git a/crates/jett_driver/tests/native_conformance/resource_report.rs b/crates/jett_driver/tests/native_conformance/resource_report.rs new file mode 100644 index 00000000..b947bd73 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_report.rs @@ -0,0 +1,695 @@ +//! Strict owned observations from the matched private Resource launcher. +//! This module grants no Source, provider, registry or custody authority. + +const MAX_REPORT: usize = 16 * 1024 * 1024; +const MAX_ATTEMPTS: u32 = 64; +const MAX_EVENTS: u32 = 8192; +const MAX_MESSAGE: u32 = 65_536; +const MAX_SCRIPT_REMAINING: u64 = 4096; + +#[derive(Debug, PartialEq, Eq)] +pub(super) enum DecodeError { + TooLarge, + Truncated { + offset: usize, + needed: usize, + }, + Magic, + Version(u32), + Length { + declared: u64, + actual: usize, + }, + Reserved { + field: &'static str, + value: u32, + }, + Limit { + field: &'static str, + value: u64, + maximum: u64, + }, + Flag { + field: &'static str, + value: u32, + }, + Channel(u32), + EventKind(u32), + AttemptOrder(usize), + EventOrder(usize), + EventEnd { + attempt: usize, + end: u64, + }, + Completion(usize), + Teardown, + Trailing(usize), + Allocation, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum FailureChannel { + Clean, + Body, + HostPanic, + Cleanup, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum EventKind { + Constructed, + ConstructionFailed, + Borrowed, + BorrowFailed, + Finalized, +} + +#[derive(Debug, PartialEq, Eq)] +pub(super) struct Completion { + pub(super) attempt: u64, + pub(super) body_status: u32, + pub(super) cleanup_status: u32, + pub(super) selected_channel: FailureChannel, +} +#[derive(Debug, PartialEq, Eq)] +pub(super) struct Counts { + pub(super) owners: u64, + pub(super) loans: u64, + pub(super) frames: u64, + pub(super) registry_entries: u64, + pub(super) owner_handles: u64, + pub(super) loan_handles: u64, + pub(super) frame_handles: u64, + pub(super) provisional_returns: u64, + pub(super) ordinary_empty: bool, +} +impl Counts { + pub(super) fn resources_retired(&self) -> bool { + [ + self.owners, + self.loans, + self.frames, + self.registry_entries, + self.owner_handles, + self.loan_handles, + self.frame_handles, + self.provisional_returns, + ] + .into_iter() + .all(|count| count == 0) + } +} +#[derive(Debug, PartialEq, Eq)] +pub(super) struct Attempt { + pub(super) completion: Completion, + pub(super) counts: Counts, + pub(super) ordinary_message: Vec, + pub(super) resource_message: Vec, + pub(super) event_sequence_end: u64, +} +#[derive(Debug, PartialEq, Eq)] +pub(super) struct Event { + pub(super) sequence: u64, + pub(super) label: i64, + pub(super) kind: EventKind, +} +#[derive(Debug, PartialEq, Eq)] +pub(super) struct Report { + pub(super) attempts: Vec, + pub(super) events: Vec, + pub(super) script_remaining: u64, + pub(super) destroy_status: u32, + pub(super) created: bool, + pub(super) destroy_attempts: u32, + pub(super) write_status: u32, +} +impl Report { + /// Counts are the real observations copied BEFORE destruction. A cleanup + /// panic can still retire every owner, and remains visible in its channel. + pub(super) fn resources_retired(&self) -> bool { + self.attempts + .iter() + .all(|attempt| attempt.counts.resources_retired()) + } + pub(super) fn ordinary_storage_empty(&self) -> bool { + self.attempts + .iter() + .all(|attempt| attempt.counts.ordinary_empty) + } + pub(super) fn cleanup_succeeded(&self) -> bool { + self.attempts + .iter() + .all(|attempt| attempt.completion.cleanup_status == 0) + } + pub(super) fn script_consumed(&self) -> bool { + self.script_remaining == 0 + } + pub(super) fn teardown_succeeded(&self) -> bool { + self.created + && self.destroy_attempts == 1 + && self.destroy_status == 0 + && self.write_status == 0 + } + pub(super) fn events_all_assigned(&self) -> bool { + self.attempts + .last() + .map_or(0, |attempt| attempt.event_sequence_end) + == self.events.last().map_or(0, |event| event.sequence) + } + /// Expected Source body/cleanup outcomes, process exit, stdout and stderr + /// remain separate exact harness oracles. This never treats a panic as clean. + pub(super) fn observations_ready_for_acceptance(&self) -> bool { + !self.attempts.is_empty() + && self.resources_retired() + && self.ordinary_storage_empty() + && self.script_consumed() + && self.teardown_succeeded() + && self.events_all_assigned() + } + pub(super) fn all_attempts_succeeded(&self) -> bool { + !self.attempts.is_empty() + && self.attempts.iter().all(|attempt| { + attempt.completion.selected_channel == FailureChannel::Clean + && attempt.completion.body_status == 0 + && attempt.completion.cleanup_status == 0 + }) + } +} + +struct Cursor<'a> { + bytes: &'a [u8], + offset: usize, +} +impl<'a> Cursor<'a> { + fn take(&mut self, length: usize) -> Result<&'a [u8], DecodeError> { + let end = self + .offset + .checked_add(length) + .ok_or(DecodeError::TooLarge)?; + let output = self + .bytes + .get(self.offset..end) + .ok_or(DecodeError::Truncated { + offset: self.offset, + needed: length, + })?; + self.offset = end; + Ok(output) + } + fn u32(&mut self) -> Result { + let data = self.take(4)?; + Ok(u32::from_le_bytes([data[0], data[1], data[2], data[3]])) + } + fn u64(&mut self) -> Result { + let data = self.take(8)?; + Ok(u64::from_le_bytes([ + data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], + ])) + } + fn zero(&mut self, field: &'static str) -> Result<(), DecodeError> { + let value = self.u32()?; + if value != 0 { + return Err(DecodeError::Reserved { field, value }); + } + Ok(()) + } + fn flag(&mut self, field: &'static str) -> Result { + match self.u32()? { + 0 => Ok(false), + 1 => Ok(true), + value => Err(DecodeError::Flag { field, value }), + } + } + fn bounded(&mut self, field: &'static str, maximum: u32) -> Result { + let value = self.u32()?; + if value > maximum { + return Err(DecodeError::Limit { + field, + value: u64::from(value), + maximum: u64::from(maximum), + }); + } + Ok(value) + } + fn counts(&mut self) -> Result { + let result = Counts { + owners: self.u64()?, + loans: self.u64()?, + frames: self.u64()?, + registry_entries: self.u64()?, + owner_handles: self.u64()?, + loan_handles: self.u64()?, + frame_handles: self.u64()?, + provisional_returns: self.u64()?, + ordinary_empty: self.flag("ordinary_empty")?, + }; + self.zero("counts")?; + Ok(result) + } + fn message(&mut self, length: u32) -> Result, DecodeError> { + let bytes = self.take(length as usize)?; + let mut result = Vec::new(); + result + .try_reserve_exact(bytes.len()) + .map_err(|_| DecodeError::Allocation)?; + result.extend_from_slice(bytes); + Ok(result) + } + fn attempt(&mut self, index: usize) -> Result { + let attempt = self.u64()?; + let body_status = self.u32()?; + let cleanup_status = self.u32()?; + let selected_channel = match self.u32()? { + 0 => FailureChannel::Clean, + 1 => FailureChannel::Body, + 2 => FailureChannel::HostPanic, + 3 => FailureChannel::Cleanup, + value => return Err(DecodeError::Channel(value)), + }; + self.zero("completion")?; + if attempt == 0 + || match selected_channel { + FailureChannel::Clean => body_status != 0 || cleanup_status != 0, + FailureChannel::Body | FailureChannel::HostPanic => cleanup_status != 0, + FailureChannel::Cleanup => cleanup_status == 0, + } + { + return Err(DecodeError::Completion(index)); + } + let counts = self.counts()?; + let ordinary_length = self.bounded("ordinary_message", MAX_MESSAGE)?; + let resource_length = self.bounded("resource_message", MAX_MESSAGE)?; + let event_sequence_end = self.u64()?; + let ordinary_message = self.message(ordinary_length)?; + let resource_message = self.message(resource_length)?; + Ok(Attempt { + completion: Completion { + attempt, + body_status, + cleanup_status, + selected_channel, + }, + counts, + ordinary_message, + resource_message, + event_sequence_end, + }) + } + fn event(&mut self) -> Result { + let sequence = self.u64()?; + let label = i64::from_le_bytes( + self.take(8)? + .try_into() + .map_err(|_| DecodeError::TooLarge)?, + ); + let kind = match self.u32()? { + 1 => EventKind::Constructed, + 2 => EventKind::ConstructionFailed, + 3 => EventKind::Borrowed, + 4 => EventKind::BorrowFailed, + 5 => EventKind::Finalized, + value => return Err(DecodeError::EventKind(value)), + }; + self.zero("event")?; + Ok(Event { + sequence, + label, + kind, + }) + } +} + +pub(super) fn decode(bytes: &[u8]) -> Result { + if bytes.len() > MAX_REPORT { + return Err(DecodeError::TooLarge); + } + let mut cursor = Cursor { bytes, offset: 0 }; + if cursor.take(8)? != b"JTRRP001" { + return Err(DecodeError::Magic); + } + let version = cursor.u32()?; + if version != 1 { + return Err(DecodeError::Version(version)); + } + cursor.zero("header")?; + let declared = cursor.u64()?; + if declared != bytes.len() as u64 { + return Err(DecodeError::Length { + declared, + actual: bytes.len(), + }); + } + let attempt_count = cursor.bounded("attempts", MAX_ATTEMPTS)?; + let event_count = cursor.bounded("events", MAX_EVENTS)?; + let script_remaining = cursor.u64()?; + if script_remaining > MAX_SCRIPT_REMAINING { + return Err(DecodeError::Limit { + field: "script_remaining", + value: script_remaining, + maximum: MAX_SCRIPT_REMAINING, + }); + } + let mut attempts = Vec::new(); + let mut events = Vec::new(); + attempts + .try_reserve_exact(attempt_count as usize) + .map_err(|_| DecodeError::Allocation)?; + events + .try_reserve_exact(event_count as usize) + .map_err(|_| DecodeError::Allocation)?; + let mut previous_attempt = 0; + let mut previous_end = 0; + for index in 0..attempt_count as usize { + let attempt = cursor.attempt(index)?; + if attempt.completion.attempt <= previous_attempt { + return Err(DecodeError::AttemptOrder(index)); + } + if attempt.event_sequence_end < previous_end { + return Err(DecodeError::EventEnd { + attempt: index, + end: attempt.event_sequence_end, + }); + } + previous_attempt = attempt.completion.attempt; + previous_end = attempt.event_sequence_end; + attempts.push(attempt); + } + let mut previous_sequence = 0; + for index in 0..event_count as usize { + let event = cursor.event()?; + if event.sequence <= previous_sequence { + return Err(DecodeError::EventOrder(index)); + } + previous_sequence = event.sequence; + events.push(event); + } + for (index, attempt) in attempts.iter().enumerate() { + let end = attempt.event_sequence_end; + if end != 0 + && events + .binary_search_by_key(&end, |event| event.sequence) + .is_err() + { + return Err(DecodeError::EventEnd { + attempt: index, + end, + }); + } + } + let destroy_status = cursor.u32()?; + let created = cursor.flag("created")?; + let destroy_attempts = cursor.u32()?; + let write_status = cursor.u32()?; + if destroy_attempts != u32::from(created) + || (!created && (destroy_status != 0 || !attempts.is_empty() || !events.is_empty())) + { + return Err(DecodeError::Teardown); + } + if cursor.offset != bytes.len() { + return Err(DecodeError::Trailing(bytes.len() - cursor.offset)); + } + Ok(Report { + attempts, + events, + script_remaining, + destroy_status, + created, + destroy_attempts, + write_status, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + fn word(bytes: &mut Vec, value: u32) { + bytes.extend_from_slice(&value.to_le_bytes()); + } + fn wide(bytes: &mut Vec, value: u64) { + bytes.extend_from_slice(&value.to_le_bytes()); + } + fn fixture() -> Vec { + let mut bytes = b"JTRRP001".to_vec(); + word(&mut bytes, 1); + word(&mut bytes, 0); + wide(&mut bytes, 0); + word(&mut bytes, 2); + word(&mut bytes, 2); + wide(&mut bytes, 3); + for (attempt, body, channel, end, ordinary, resource) in [ + (41, 71, 1, 1, &b"ordinary"[..], &[0xff, b'x'][..]), + (44, 0, 0, 4, &[][..], &[][..]), + ] { + wide(&mut bytes, attempt); + for value in [body, 0, channel, 0] { + word(&mut bytes, value); + } + for _ in 0..8 { + wide(&mut bytes, 0); + } + word(&mut bytes, 1); + word(&mut bytes, 0); + word(&mut bytes, ordinary.len() as u32); + word(&mut bytes, resource.len() as u32); + wide(&mut bytes, end); + bytes.extend_from_slice(ordinary); + bytes.extend_from_slice(resource); + } + for (sequence, label, kind) in [(1, -501i64, 3), (4, i64::MIN, 5)] { + wide(&mut bytes, sequence); + wide(&mut bytes, label as u64); + word(&mut bytes, kind); + word(&mut bytes, 0); + } + for value in [0, 1, 1, 0] { + word(&mut bytes, value); + } + let length = bytes.len() as u64; + bytes[16..24].copy_from_slice(&length.to_le_bytes()); + bytes + } + fn set_word(bytes: &mut [u8], offset: usize, value: u32) { + bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + } + fn set_wide(bytes: &mut [u8], offset: usize, value: u64) { + bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); + } + #[test] + fn resource_report_preserves_original_fields_and_keeps_acceptance_predicates_separate() { + let report = decode(&fixture()).unwrap(); + assert_eq!(report.attempts.len(), 2); + assert_eq!(report.attempts[0].completion.body_status, 71); + assert_eq!( + report.attempts[0].completion.selected_channel, + FailureChannel::Body + ); + assert_eq!(report.attempts[0].ordinary_message, b"ordinary"); + assert_eq!(report.attempts[0].resource_message, [0xff, b'x']); + assert_eq!(report.events[0].label, -501); + assert_eq!(report.events[1].label, i64::MIN); + assert_eq!(report.events[0].kind, EventKind::Borrowed); + assert!( + report.resources_retired() + && report.ordinary_storage_empty() + && report.cleanup_succeeded() + ); + assert!(report.teardown_succeeded() && report.events_all_assigned()); + assert!( + !report.script_consumed() + && !report.all_attempts_succeeded() + && !report.observations_ready_for_acceptance() + ); + } + #[test] + fn resource_report_rejects_every_truncation_trailing_bytes_and_declared_length_lies() { + let bytes = fixture(); + for end in 0..bytes.len() { + let mut prefix = bytes[..end].to_vec(); + if end >= 24 { + set_wide(&mut prefix, 16, end as u64); + } + assert!(decode(&prefix).is_err(), "truncated prefix {end}"); + } + let mut trailing = bytes.clone(); + trailing.push(0); + let length = trailing.len() as u64; + set_wide(&mut trailing, 16, length); + assert_eq!(decode(&trailing), Err(DecodeError::Trailing(1))); + let mut wrong = bytes.clone(); + set_wide(&mut wrong, 16, u64::MAX); + assert_eq!( + decode(&wrong), + Err(DecodeError::Length { + declared: u64::MAX, + actual: bytes.len() + }) + ); + } + #[test] + fn resource_report_rejects_oversized_storage_before_copying_or_allocating_messages() { + assert_eq!(decode(&vec![0; MAX_REPORT + 1]), Err(DecodeError::TooLarge)); + for (offset, value, field, maximum) in [ + (24, 65, "attempts", 64), + (28, 8193, "events", 8192), + (136, 65537, "ordinary_message", 65536), + (140, 65537, "resource_message", 65536), + ] { + let mut bytes = fixture(); + set_word(&mut bytes, offset, value); + assert_eq!( + decode(&bytes), + Err(DecodeError::Limit { + field, + value: u64::from(value), + maximum + }) + ); + } + let mut bytes = fixture(); + set_wide(&mut bytes, 32, 4097); + assert_eq!( + decode(&bytes), + Err(DecodeError::Limit { + field: "script_remaining", + value: 4097, + maximum: 4096 + }) + ); + } + #[test] + fn resource_report_rejects_reserved_unknown_kinds_and_nonboolean_flags() { + let bytes = fixture(); + let first_event = bytes.len() - 16 - 48; + let mut wrong = bytes.clone(); + wrong[0] = b'X'; + assert_eq!(decode(&wrong), Err(DecodeError::Magic)); + let mut wrong = bytes.clone(); + set_word(&mut wrong, 8, 2); + assert_eq!(decode(&wrong), Err(DecodeError::Version(2))); + for (offset, field) in [ + (12, "header"), + (60, "completion"), + (132, "counts"), + (first_event + 20, "event"), + ] { + let mut wrong = bytes.clone(); + set_word(&mut wrong, offset, 9); + assert_eq!( + decode(&wrong), + Err(DecodeError::Reserved { field, value: 9 }) + ); + } + let mut wrong = bytes.clone(); + set_word(&mut wrong, 56, 4); + assert_eq!(decode(&wrong), Err(DecodeError::Channel(4))); + let mut wrong = bytes.clone(); + set_word(&mut wrong, first_event + 16, 6); + assert_eq!(decode(&wrong), Err(DecodeError::EventKind(6))); + let mut wrong = bytes.clone(); + set_word(&mut wrong, 128, 2); + assert_eq!( + decode(&wrong), + Err(DecodeError::Flag { + field: "ordinary_empty", + value: 2 + }) + ); + let mut wrong = bytes.clone(); + let created = wrong.len() - 12; + set_word(&mut wrong, created, 2); + assert_eq!( + decode(&wrong), + Err(DecodeError::Flag { + field: "created", + value: 2 + }) + ); + } + #[test] + fn resource_report_rejects_nonmonotonic_identities_and_impossible_event_prefixes() { + let bytes = fixture(); + let second_attempt = 162; + let first_event = bytes.len() - 16 - 48; + let mut wrong = bytes.clone(); + set_wide(&mut wrong, second_attempt, 41); + assert_eq!(decode(&wrong), Err(DecodeError::AttemptOrder(1))); + let mut wrong = bytes.clone(); + set_wide(&mut wrong, first_event, 0); + assert_eq!(decode(&wrong), Err(DecodeError::EventOrder(0))); + let mut wrong = bytes.clone(); + set_wide(&mut wrong, first_event + 24, 1); + assert_eq!(decode(&wrong), Err(DecodeError::EventOrder(1))); + for end in [2, u64::MAX] { + let mut wrong = bytes.clone(); + set_wide(&mut wrong, 144, end); + assert!(matches!(decode(&wrong), Err(DecodeError::EventEnd { .. }))); + } + let mut wrong = bytes.clone(); + set_wide(&mut wrong, second_attempt + 104, 0); + assert_eq!( + decode(&wrong), + Err(DecodeError::EventEnd { attempt: 1, end: 0 }) + ); + } + #[test] + fn resource_report_never_masks_live_custody_cleanup_panic_or_teardown_failure() { + let mut bytes = fixture(); + set_wide(&mut bytes, 32, 0); + let mut report = decode(&bytes).unwrap(); + assert!(report.observations_ready_for_acceptance()); + assert!(!report.all_attempts_succeeded()); + report.attempts[0].counts.owners = 1; + assert!(!report.resources_retired() && !report.observations_ready_for_acceptance()); + for index in 0..8 { + let mut live = fixture(); + set_wide(&mut live, 64 + index * 8, 17); + let report = decode(&live).unwrap(); + assert!(!report.resources_retired(), "live count {index}"); + } + let mut unknown_end = fixture(); + set_wide(&mut unknown_end, 162 + 104, 1); + let report = decode(&unknown_end).unwrap(); + assert!(!report.events_all_assigned()); + let mut panic = fixture(); + set_word(&mut panic, 48, 0); + set_word(&mut panic, 56, 2); + let report = decode(&panic).unwrap(); + assert_eq!(report.attempts[0].completion.body_status, 0); + assert_eq!( + report.attempts[0].completion.selected_channel, + FailureChannel::HostPanic + ); + assert!(!report.all_attempts_succeeded()); + let mut bytes = fixture(); + set_word(&mut bytes, 52, 37); + set_word(&mut bytes, 56, 3); + let report = decode(&bytes).unwrap(); + assert_eq!(report.attempts[0].completion.cleanup_status, 37); + assert_eq!(report.attempts[0].completion.body_status, 71); + assert_eq!( + report.attempts[0].completion.selected_channel, + FailureChannel::Cleanup + ); + assert!(!report.cleanup_succeeded()); + assert!(report.resources_retired()); + let mut wrong = fixture(); + set_word(&mut wrong, 52, 37); + assert_eq!(decode(&wrong), Err(DecodeError::Completion(0))); + let mut wrong = fixture(); + set_word(&mut wrong, 56, 3); + assert_eq!(decode(&wrong), Err(DecodeError::Completion(0))); + let mut wrong = fixture(); + set_word(&mut wrong, 56, 0); + assert_eq!(decode(&wrong), Err(DecodeError::Completion(0))); + let mut wrong = fixture(); + let footer = wrong.len() - 16; + set_word(&mut wrong, footer + 8, 2); + assert_eq!(decode(&wrong), Err(DecodeError::Teardown)); + let mut failed = fixture(); + let footer = failed.len() - 16; + set_word(&mut failed, footer, 13); + set_word(&mut failed, footer + 12, 14); + let report = decode(&failed).unwrap(); + assert_eq!((report.destroy_status, report.write_status), (13, 14)); + assert!(!report.teardown_succeeded()); + } +} diff --git a/crates/jett_driver/tests/native_conformance/suite_options.rs b/crates/jett_driver/tests/native_conformance/suite_options.rs index 8484d94d..ae1d073f 100644 --- a/crates/jett_driver/tests/native_conformance/suite_options.rs +++ b/crates/jett_driver/tests/native_conformance/suite_options.rs @@ -56,6 +56,9 @@ struct ProfileLauncher { /// Suite options select Jett source/optimization policy. The caller separately /// supplies an archive compiled for that runtime profile, as the public API requires. pub(super) fn launcher_for_options(release: bool) -> &'static NativeLauncherBundle { + if let Some(bundle) = super::measured_launcher::supplied(release) { + return bundle; + } static DEBUG: OnceLock = OnceLock::new(); static RELEASE: OnceLock = OnceLock::new(); let slot = if release { &RELEASE } else { &DEBUG }; diff --git a/crates/jett_hir/Cargo.toml b/crates/jett_hir/Cargo.toml index b289227d..e947f069 100644 --- a/crates/jett_hir/Cargo.toml +++ b/crates/jett_hir/Cargo.toml @@ -7,6 +7,7 @@ license.workspace = true [dependencies] jett_common.workspace = true +jett_comptime.workspace = true jett_intrinsics.workspace = true jett_parser.workspace = true jett_resolve.workspace = true diff --git a/crates/jett_hir/src/borrowed_sum_view_tests.rs b/crates/jett_hir/src/borrowed_sum_view_tests.rs new file mode 100644 index 00000000..b4bdd454 --- /dev/null +++ b/crates/jett_hir/src/borrowed_sum_view_tests.rs @@ -0,0 +1,380 @@ +use super::*; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; +use jett_types::ResourceKernelRecipe; + +const SUPPORT: &str = + include_str!("../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); +const OCCUPIED: &str = include_str!( + "../../jett_driver/tests/native_conformance/resource/47_view_optional_some_written.jett" +); + +fn checked_resource_source(source: &str, release: bool) -> Arc { + let support = FileId::new(10_000); + let project = FileId::new(0); + let mut parsed = jett_parser::parse(SUPPORT, support); + let primary = jett_parser::parse(source, project); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(primary.errors.is_empty(), "{:?}", primary.errors); + let declarations = parsed + .module + .items + .iter() + .filter_map(|item| match item { + Item::Resource(value) => Some(value.name.span), + _ => None, + }) + .collect::>(); + let [declaration] = declarations.as_slice() else { + panic!("one Resource declaration"); + }; + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: *declaration, + member: member.into(), + recipe, + }) + .collect::>(); + parsed.module.items.extend(primary.module.items); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (support, SourceOrigin::Stdlib), + (project, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("{error:?}: {:?}", error.diagnostics())), + ) +} + +fn observer(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + ["observe_optional", "observe_result"] + .contains(&function.identity.declaration.name.as_str()) + }) + .expect("genuine observer function") +} + +fn alias_initializer(function: &Function) -> (&Local, &Expression) { + let alias = function + .locals + .iter() + .find(|local| local.name == "token") + .expect("borrowed payload local"); + let value = function + .body + .statements + .iter() + .find_map(|statement| match &statement.kind { + StatementKind::Let { local, value } if *local == alias.id => Some(value), + _ => None, + }) + .expect("original payload initializer"); + (alias, value) +} + +#[test] +fn borrowed_sum_views_lower_shared_checked_sources() { + let sources = [ + include_str!( + "../../jett_driver/tests/native_conformance/resource/45_view_optional_none_written.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/46_view_optional_none_bare.jett" + ), + OCCUPIED, + include_str!( + "../../jett_driver/tests/native_conformance/resource/48_view_optional_some_bare.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/49_view_result_fail_written.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/50_view_result_fail_bare.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/51_view_result_ok_written.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/52_view_result_ok_bare.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/53_view_optional_written_argument_abort.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/54_view_result_bare_argument_abort.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/55_view_optional_written_borrow_fail.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/56_view_result_bare_borrow_fail.jett" + ), + ]; + for release in [false, true] { + for source in sources { + let checked = checked_resource_source(source, release); + let program = lower_checked_resource_program(&checked).unwrap(); + validate_backend_types(&program, &checked.checked().interner).unwrap(); + assert!(program.resource_source.belongs_to(&checked)); + assert_eq!(program.resource_source.functions(), program.functions); + let function = observer(&program); + let (alias, initializer) = alias_initializer(function); + let backing = function + .locals + .iter() + .find(|local| local.name == "outcome") + .unwrap(); + assert_eq!(alias.view_source, Some(backing.id)); + assert_ne!(alias.ty, backing.ty); + assert!(function.params.iter().any(|parameter| { + parameter.local == backing.id && parameter.mode == ParamMode::View + })); + let handle = borrowed_sum_view_initializer( + initializer, + backing.id, + backing.ty, + alias.ty, + &checked.checked().interner, + ) + .unwrap() + .unwrap(); + assert!(matches!(handle.kind, ExpressionKind::Handle { .. })); + } + } +} + +#[test] +fn borrowed_sum_views_reject_changed_hir_projection() { + let checked = checked_resource_source(OCCUPIED, false); + let program = lower_checked_resource_program(&checked).unwrap(); + let function = observer(&program); + let (alias, _) = alias_initializer(function); + let alias_id = alias.id; + let backing_id = alias.view_source.unwrap(); + let foreign_id = function.params[0].local; + for change in [ + "kind", + "source", + "source type", + "endpoint", + "inner view", + "outer view", + "error binding", + "default", + "conditional default", + "orphan", + ] { + let mut changed = program.clone(); + let function = changed + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "observe_optional") + .unwrap(); + if change == "orphan" { + function.body.statements.retain(|statement| { + !matches!(statement.kind, StatementKind::Let { local, .. } if local == alias_id) + }); + } else { + let source_type = function.locals[backing_id.index() as usize].ty; + if change == "endpoint" { + function.locals[alias_id.index() as usize].ty = source_type; + } + let value = function + .body + .statements + .iter_mut() + .find_map(|statement| match &mut statement.kind { + StatementKind::Let { local, value } if *local == alias_id => Some(value), + _ => None, + }) + .unwrap(); + if change == "outer view" { + let ExpressionKind::View(inner) = &value.kind else { + panic!("original outer view"); + }; + *value = inner.as_ref().clone(); + } else { + if change == "endpoint" { + value.ty = source_type; + } + let ExpressionKind::View(handle) = &mut value.kind else { + panic!("original outer view"); + }; + if change == "endpoint" { + handle.ty = source_type; + } + let ExpressionKind::Handle { + target, + kind, + error_local, + failure, + } = &mut handle.kind + else { + panic!("original sum Handle"); + }; + match change { + "kind" => *kind = HandleKind::Result, + "error binding" => *error_local = Some(foreign_id), + "inner view" => { + let ExpressionKind::View(inner) = &target.kind else { + panic!("original inner view"); + }; + *target = inner.clone(); + } + "source" | "source type" => { + let ExpressionKind::View(backing) = &mut target.kind else { + panic!("original inner view"); + }; + if change == "source" { + backing.kind = ExpressionKind::Local(foreign_id); + } else { + backing.ty = alias.ty; + } + } + "default" | "conditional default" => { + let alternate = Statement { + kind: StatementKind::HandleDefault(Expression { + kind: ExpressionKind::Local(foreign_id), + ty: alias.ty, + span: handle.span, + }), + span: handle.span, + }; + let alternate = if change == "conditional default" { + Statement { + kind: StatementKind::If { + condition: Expression { + kind: ExpressionKind::Bool(true), + ty: TypeInterner::BOOL, + span: handle.span, + }, + then_block: Block { + statements: vec![alternate], + span: handle.span, + }, + else_block: None, + }, + span: handle.span, + } + } else { + alternate + }; + failure.statements.insert(0, alternate); + } + "endpoint" => {} + _ => panic!("bounded mutation"), + } + } + } + let errors = validate_backend_types(&changed, &checked.checked().interner).unwrap_err(); + assert!( + errors.iter().any(|error| { + error.message.contains("borrowed Handle") + || error.message.contains("borrowed alias") + || error.message.contains("no validated initializer") + }), + "{change}: {errors:?}" + ); + } +} + +#[test] +fn borrowed_sum_views_refuse_temporaries_and_outer_handler_defaults() { + for initializer in [ + "view((view temporary()) handle:\n return nothing\n )", + "view((view source) handle:\n default list(9)\n )", + "view((view source) handle:\n if flag:\n default list(9)\n return nothing\n )", + "view(source handle:\n return nothing\n )", + ] { + let source = format!( + "namespace app\nfunction temporary() returns optional[list[int64]]:\n return some(list(1))\nfunction inspect(view source: optional[list[int64]], flag: bool) returns nothing:\n list[int64] alias = {initializer}\n return nothing\n" + ); + let errors = crate::local_view_tests::checked_source(&source, false).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("native borrowed alias")), + "{initializer}: {errors:?}" + ); + } +} + +#[test] +fn borrowed_sum_views_keep_nested_handle_defaults_and_generic_payload_identity() { + let source = r#"namespace app +function inspect[T](view source: optional[T], view result_source: result[T, string]) returns nothing: + T alias = view((view source) handle: + optional[int64] missing = none + int64 marker = missing handle: + default 7 + return nothing + ) + T result_alias = view((view result_source) handle error: + return nothing + ) + return nothing +function main() returns nothing: + inspect[list[int64]](view some(list(1)), view ok(list(2))) + inspect[int64](view some(3), view ok(4)) + return nothing +"#; + let (program, checked) = crate::local_view_tests::checked_source(source, false).unwrap(); + validate_backend_types(&program, &checked.interner).unwrap(); + let instances = program + .functions + .iter() + .filter(|function| function.identity.declaration.name == "inspect") + .collect::>(); + assert_eq!(instances.len(), 2); + for function in instances { + for (name, source_name) in [("alias", "source"), ("result_alias", "result_source")] { + let alias = function + .locals + .iter() + .find(|local| local.name == name) + .unwrap(); + let backing = function + .locals + .iter() + .find(|local| local.name == source_name) + .unwrap(); + if alias.ty == TypeInterner::INT64 { + assert_eq!(alias.view_source, None, "implicitly copyable payload"); + } else { + assert_eq!(alias.view_source, Some(backing.id)); + let value = function + .body + .statements + .iter() + .find_map(|statement| match &statement.kind { + StatementKind::Let { local, value } if *local == alias.id => Some(value), + _ => None, + }) + .unwrap(); + assert!( + borrowed_sum_view_initializer( + value, + backing.id, + backing.ty, + alias.ty, + &checked.interner + ) + .unwrap() + .is_some() + ); + } + } + } +} diff --git a/crates/jett_hir/src/call_ownership.rs b/crates/jett_hir/src/call_ownership.rs index 8cadc4a4..a8a90853 100644 --- a/crates/jett_hir/src/call_ownership.rs +++ b/crates/jett_hir/src/call_ownership.rs @@ -168,6 +168,7 @@ pub enum ArgumentStaging { #[derive(Debug, Clone, PartialEq, Eq)] pub enum CallTarget { + ResourceHook(crate::ResourceHookRef), Function(FunctionId), /// Proven source facade identity; its implementation body need not survive pruning. Declaration { @@ -188,6 +189,9 @@ pub enum CallTarget { /// An implementation helper does not change the original caller disposition. #[derive(Debug, Clone, PartialEq, Eq)] pub enum CallBridge { + ResourceHook { + hook: crate::ResourceHookRef, + }, Direct, TrustedIntrinsic { intrinsic: IntrinsicId, @@ -272,6 +276,12 @@ struct GeneratedProducerShape { #[derive(Debug, Clone, PartialEq, Eq)] enum GeneratedProducerIdentity { + ResourceHook(crate::ResourceHookRef), + ResourceInvoke { + hook: crate::ResourceHookRef, + operands: Vec<(TypeId, Span)>, + order: Vec, + }, Data, Transparent(GeneratedTransparentShape), Local(LocalId), @@ -557,6 +567,8 @@ fn generated_backing_values(value: &Expression) -> Vec<&Expression> { | E::Local(_) | E::Constant { .. } | E::FunctionRef(_) + | E::ResourceHookValue { .. } + | E::ResourceInvoke { .. } | E::ClosureRef { .. } | E::Call { .. } | E::Intrinsic { .. } @@ -578,6 +590,17 @@ impl GeneratedProducerShape { E::Local(local) => GeneratedProducerIdentity::Local(*local), E::Constant { declaration } => GeneratedProducerIdentity::Constant(*declaration), E::FunctionRef(function) => GeneratedProducerIdentity::Function(*function), + E::ResourceHookValue { hook } => GeneratedProducerIdentity::ResourceHook(hook.clone()), + E::ResourceInvoke { + hook, + args, + evaluation_order, + .. + } => GeneratedProducerIdentity::ResourceInvoke { + hook: hook.clone(), + operands: occurrence(args), + order: evaluation_order.clone(), + }, E::ClosureRef { function, captures } => GeneratedProducerIdentity::Closure { function: *function, captures: captures.clone(), @@ -950,7 +973,8 @@ impl GeneratedProducerShape { match &self.identity { GeneratedProducerIdentity::Call { operands, .. } | GeneratedProducerIdentity::Indirect { operands, .. } - | GeneratedProducerIdentity::Intrinsic { operands, .. } => { + | GeneratedProducerIdentity::Intrinsic { operands, .. } + | GeneratedProducerIdentity::ResourceInvoke { operands, .. } => { for &(ty, _) in operands { visit(ty); } @@ -958,6 +982,10 @@ impl GeneratedProducerShape { _ => {} } match &self.identity { + GeneratedProducerIdentity::ResourceHook(hook) + | GeneratedProducerIdentity::ResourceInvoke { hook, .. } => { + hook.metadata_types(&mut *visit) + } GeneratedProducerIdentity::Intrinsic { type_arguments, .. } => { for &ty in type_arguments { visit(ty); @@ -1286,7 +1314,9 @@ fn remap_generated_operation_locals( operation: &mut GeneratedOperation, map: &mut impl FnMut(LocalId) -> Result, ) -> Result<(), E> { - if let GeneratedOperation::FunctionAdapter { callee, .. } = operation { + if let GeneratedOperation::FunctionAdapter { callee, .. } + | GeneratedOperation::ReflectedCallableAdapter { callee, .. } = operation + { *callee = map(*callee)?; } Ok(()) @@ -1400,6 +1430,10 @@ pub enum GeneratedOperation { source_type: TypeId, target_type: TypeId, }, + ReflectedCallableAdapter { + callee: LocalId, + plan: jett_typecheck::CheckedReflectedCallablePlan, + }, JsonBridge { intrinsic: IntrinsicId, }, @@ -1463,6 +1497,7 @@ impl CallOwnership { visit(binding.ty); } } + CallTarget::ResourceHook(hook) => hook.metadata_types(&mut visit), CallTarget::Interface { interface_type, .. } => visit(*interface_type), CallTarget::Indirect { signature_type } => visit(*signature_type), _ => {} @@ -1512,6 +1547,10 @@ impl CallOwnership { visit(source_type); visit(target_type); } + GeneratedOperation::ReflectedCallableAdapter { ref plan, .. } => { + visit(plan.source_type()); + visit(plan.requested_type()); + } _ => {} } for argument in &generated.arguments { @@ -1783,7 +1822,10 @@ impl CallOwnership { } } Self::Generated(generated) => { - if let GeneratedOperation::FunctionAdapter { callee, .. } = generated.operation { + if let GeneratedOperation::FunctionAdapter { callee, .. } + | GeneratedOperation::ReflectedCallableAdapter { callee, .. } = + generated.operation + { visit(callee); } for operand in &generated.arguments { @@ -2575,6 +2617,18 @@ pub fn validate_operand_ownership( return Err("generated callable adapter capture differs from its sealed source signature".into()); } } + if let GeneratedOperation::ReflectedCallableAdapter { callee, ref plan } = + generated.operation + { + if !plan.validate_types(types) { + return Err("reflected callable checked signature meaning changed".into()); + } + if require_local(callee, &local)?.ty != plan.source_type() { + return Err( + "reflected callable capture differs from its checked signature".into(), + ); + } + } } } Ok(()) @@ -2855,6 +2909,44 @@ impl crate::BodyLowerer<'_, '_> { Some(ownership) } + pub(crate) fn finish_resource_call_ownership( + &mut self, + mut ownership: CallOwnership, + hook: &crate::ResourceHookRef, + args: &[Expression], + span: Span, + ) -> Option { + let proof = (|| -> Result<(), String> { + hook.validate(&self.parent.check.interner)?; + if !self.parent.resource_manifest.contains_hook(hook) { + return Err("Resource hook is from another checked program".into()); + } + let CallOwnership::Source(source) = &mut ownership else { + return Err("Resource invocation requires an original Source certificate".into()); + }; + if source.target != CallTarget::ResourceHook(hook.clone()) + || source.shape + != (CheckedInvocationShape::Function { + signature_type: hook.function_type(), + }) + || source.arguments.len() != args.len() + || !source.generated_operands.is_empty() + { + return Err( + "Resource invocation differs from its exact checked hook target".into(), + ); + } + source.bridge = CallBridge::ResourceHook { hook: hook.clone() }; + source.certificate.bridge = source.bridge.clone(); + Ok(()) + })(); + if let Err(message) = proof { + self.parent.error(span, message); + return None; + } + Some(ownership) + } + fn join_physical_ownership( &self, ownership: &mut CallOwnership, @@ -3328,6 +3420,27 @@ impl crate::BodyLowerer<'_, '_> { shape: &CheckedInvocationShape, ) -> Result { use jett_typecheck::CheckedInvocationTarget as Target; + if let Target::Resolved(definition) = target + && let Some(hook) = self + .parent + .resource_manifest + .hook_for_definition(*definition) + { + return Ok(CallTarget::ResourceHook(hook)); + } + // A resolver kernel without the original envelope cannot become an ordinary function. + if let Target::Resolved(definition) = target + && self + .parent + .resolve + .resource_kernels + .contains_definition(*definition) + { + return Err( + "Resource hook requires lower_checked_resource_program and its original manifest" + .into(), + ); + } let key = match target { Target::Resolved(definition) => crate::FunctionKey::Definition { definition: *definition, @@ -3789,6 +3902,12 @@ pub fn validate_hir_invocation( args, evaluation_order, .. + } + | ExpressionKind::ResourceInvoke { + ownership, + args, + evaluation_order, + .. } => (ownership, args, evaluation_order), _ => return Ok(()), }; @@ -3862,6 +3981,12 @@ pub fn validate_invocation_target( args, evaluation_order, .. + } + | ExpressionKind::ResourceInvoke { + ownership, + args, + evaluation_order, + .. } => (ownership, args, evaluation_order), _ => return Ok(()), }; @@ -3903,6 +4028,52 @@ pub fn validate_invocation_target( return Err("call ownership source context is not its lexical owner".into()); } match (&source.target, &source.bridge, &expression.kind) { + ( + CallTarget::ResourceHook(target_hook), + CallBridge::ResourceHook { hook: bridge_hook }, + ExpressionKind::ResourceInvoke { hook, .. }, + ) if target_hook == hook && bridge_hook == hook => { + hook.validate(types)?; + if source.shape + != (CheckedInvocationShape::Function { + signature_type: hook.function_type(), + }) + || !source.generated_operands.is_empty() + { + return Err("Resource invocation changed its original hook signature or added Generated operands".into()); + } + let Type::Function { + params, + view_params, + .. + } = types.resolve(hook.function_type()) + else { + return Err("Resource hook has no exact function signature".into()); + }; + if params.len() != args.len() || view_params.len() != args.len() { + return Err("Resource invocation hook arity changed".into()); + } + for (index, argument) in source.arguments.iter().enumerate() { + let expected_access = if view_params[index] { + CheckedCalleeAccess::View + } else { + CheckedCalleeAccess::Owned + }; + if argument.physical_access != expected_access + || argument.callee_access != expected_access + || argument.parameter_type != params[index] + || !source_parameter_matches( + argument, + args[index].ty, + params[index], + types, + ) + { + return Err("Resource invocation differs from its exact source formal type or mode".into()); + } + } + } + ( CallTarget::Function(source_target), CallBridge::Direct, @@ -4151,6 +4322,23 @@ pub fn validate_invocation_target( return Err("generated callable adapter ownership types are invalid".into()); } } + ( + GeneratedOperation::ReflectedCallableAdapter { + callee: expected, + plan, + }, + ExpressionKind::IndirectCall { callee, .. }, + ) => { + if !plan.validate_types(types) { + return Err("reflected callable checked signature meaning changed".into()); + } + if !matches!(callee.kind, ExpressionKind::Local(local) if local == *expected) + || callee.ty != plan.source_type() + || expression.ty != plan.source_return() + { + return Err("reflected callable invocation differs from its actual checked signature".into()); + } + } ( GeneratedOperation::RefinementPredicate { function: predicate, @@ -4283,7 +4471,12 @@ pub fn validate_invocation_target( } } } - let declared_result = if let Some(target) = target { + let declared_result = if let ExpressionKind::ResourceInvoke { hook, .. } = &expression.kind { + match types.resolve(hook.function_type()) { + Type::Function { return_type, .. } => Some(*return_type), + _ => None, + } + } else if let Some(target) = target { Some(target.return_type) } else if let ExpressionKind::IndirectCall { callee, .. } = &expression.kind { match types.resolve(callee.ty) { diff --git a/crates/jett_hir/src/inline_functions.rs b/crates/jett_hir/src/inline_functions.rs index ec7cfc5d..61bfa1fb 100644 --- a/crates/jett_hir/src/inline_functions.rs +++ b/crates/jett_hir/src/inline_functions.rs @@ -289,6 +289,9 @@ impl Extractor<'_> { } | ExpressionKind::Intrinsic { args, ownership, .. + } + | ExpressionKind::ResourceInvoke { + args, ownership, .. } => { for argument in args { self.expression(argument, parent); @@ -371,6 +374,7 @@ impl Extractor<'_> { | ExpressionKind::PropertyCaseContext(_) | ExpressionKind::RuntimeFailure(_) | ExpressionKind::Local(_) + | ExpressionKind::ResourceHookValue { .. } | ExpressionKind::FunctionRef(_) | ExpressionKind::ClosureRef { .. } | ExpressionKind::OptionalNone @@ -466,7 +470,8 @@ fn expression_mentions_local(expression: &Expression, target: u32, include_bindi if include_bindings { if let ExpressionKind::Call { ownership, .. } | ExpressionKind::IndirectCall { ownership, .. } - | ExpressionKind::Intrinsic { ownership, .. } = &expression.kind + | ExpressionKind::Intrinsic { ownership, .. } + | ExpressionKind::ResourceInvoke { ownership, .. } = &expression.kind { let mut mentioned = false; ownership.metadata_local_ids(|local| mentioned |= local.index() == target); @@ -505,6 +510,7 @@ fn expression_mentions_local(expression: &Expression, target: u32, include_bindi expression_mentions_local(value, target, include_bindings) } ExpressionKind::Call { args, .. } + | ExpressionKind::ResourceInvoke { args, .. } | ExpressionKind::Intrinsic { args, .. } | ExpressionKind::ActorSpawn { args, .. } => args .iter() @@ -572,6 +578,7 @@ fn expression_mentions_local(expression: &Expression, target: u32, include_bindi | ExpressionKind::Nothing | ExpressionKind::PropertyCaseContext(_) | ExpressionKind::RuntimeFailure(_) + | ExpressionKind::ResourceHookValue { .. } | ExpressionKind::FunctionRef(_) | ExpressionKind::OptionalNone => false, } diff --git a/crates/jett_hir/src/interface_values.rs b/crates/jett_hir/src/interface_values.rs index 234ac54e..84d295b9 100644 --- a/crates/jett_hir/src/interface_values.rs +++ b/crates/jett_hir/src/interface_values.rs @@ -311,7 +311,7 @@ fn never_slots_compatible(types: &TypeInterner, source: TypeId, target: TypeId) } } -fn coerce(value: &mut Expression, expected: TypeId, types: &TypeInterner) { +pub(crate) fn coerce(value: &mut Expression, expected: TypeId, types: &TypeInterner) { if value.ty == expected || value.ty == TypeInterner::NEVER // Pure calls retain secret arguments so their result stays tainted. @@ -373,6 +373,7 @@ pub(super) fn coerce_program(program: &mut Program, types: &TypeInterner) { }); let mut index = 0; while index < program.functions.len() { + let original_functions = program.functions.clone(); let function = &mut program.functions[index]; let locals = function .locals @@ -386,6 +387,7 @@ pub(super) fn coerce_program(program: &mut Program, types: &TypeInterner) { return_type: function.return_type, identity: &function.identity, adapters: &adapters, + functions: &original_functions, }; pass.block(&mut function.body, None); let pending = std::mem::take(&mut adapters.borrow_mut().pending); @@ -408,7 +410,11 @@ pub fn complete_value_conversions( program: &mut Program, types: &TypeInterner, ) -> Result<(), Vec> { + let before = program.clone(); + super::reflected_callable::baked::validate_conversion_input(&before)?; coerce_program(program, types); + super::reflected_callable::baked::complete_conversion(program, &before)?; + super::reflected_callable::seal_program(program)?; validate(program) .and_then(|()| validate_backend_types(program, types)) .map_err(|errors| { @@ -573,6 +579,7 @@ struct Coercions<'a> { return_type: TypeId, identity: &'a FunctionIdentity, adapters: &'a std::cell::RefCell, + functions: &'a [Function], } impl Coercions<'_> { @@ -582,6 +589,46 @@ impl Coercions<'_> { self.adapt(value); } fn adapt(&self, expression: &mut Expression) { + if let ExpressionKind::InterfaceCoerce { value, adapters } = &mut expression.kind { + if let Some(callable) = adapters + .iter_mut() + .find_map(|entry| entry.reflected.as_mut()) + { + if callable.resolve_return(self.functions).is_err() { + return; + } + let next = FunctionId(self.adapters.borrow().next); + if let Some(mut function) = + super::reflected_callable::adapter(callable, next, self.identity, self.types) + { + let id = function.id; + callable.bind(id, self.identity); + if let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::Handle { + kind: + HandleKind::ReflectedReturn { + callable: stored, .. + }, + .. + }, + .. + })) = &mut function.body.statements[0].kind + { + *stored = Some(callable.clone()); + } + let mut pending = self.adapters.borrow_mut(); + pending.next += 1; + pending.pending.push(function); + expression.kind = ExpressionKind::FunctionAdapter { + value: value.clone(), + function: id, + }; + return; + } + return; + } + } let ExpressionKind::InterfaceCoerce { value, .. } = &expression.kind else { return; }; @@ -632,6 +679,7 @@ impl Coercions<'_> { source, target, function, + reflected: None, }); } } @@ -733,6 +781,13 @@ impl Coercions<'_> { fn expression(&self, expression: &mut Expression, handled: Option) { let ty = expression.ty; match &mut expression.kind { + ExpressionKind::ResourceInvoke { hook, args, .. } => { + if let Type::Function { params, .. } = self.types.resolve(hook.function_type()) { + for (argument, expected) in args.iter_mut().zip(params) { + self.expected(argument, *expected, handled); + } + } + } ExpressionKind::Call { function, args, .. } => { for (argument, expected) in args .iter_mut() @@ -941,6 +996,30 @@ impl Coercions<'_> { _ => {} } self.adapt(expression); + if let ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(plans)), + .. + } = &mut expression.kind + { + for plan in plans { + if let ReflectedFieldAction::CallableReturn(callable) = &mut plan.action { + if callable.canonical_adapter().is_none() { + let mut adapters = self.adapters.borrow_mut(); + let id = FunctionId(adapters.next); + if let Some(function) = super::reflected_callable::adapter( + callable, + id, + self.identity, + self.types, + ) { + adapters.next += 1; + adapters.pending.push(function); + callable.bind(id, self.identity); + } + } + } + } + } } } diff --git a/crates/jett_hir/src/lib.rs b/crates/jett_hir/src/lib.rs index bfcbe594..562e30b7 100644 --- a/crates/jett_hir/src/lib.rs +++ b/crates/jett_hir/src/lib.rs @@ -5,6 +5,7 @@ //! they never survive as embedded AST nodes. use std::collections::{HashMap, HashSet}; +use std::sync::Arc; use jett_common::{FileId, SourceOrigin, Span}; pub use jett_intrinsics::IntrinsicId; @@ -25,16 +26,33 @@ use jett_types::{ }; mod call_ownership; +mod reflected_field_sources; +mod resource_manifest; +mod resource_materialization; +mod resource_source; +pub use reflected_field_sources::ReflectedFieldDispatchProof; +pub use resource_manifest::{ + ResourceHookRef, ResourceKind, ResourceKindId, ResourceKindRef, ResourceManifest, +}; +pub use resource_materialization::materialize_checked_required_values; +pub use resource_source::{RequiredMaterialization, ResourceSourceArchive}; mod iteration_bindings; pub use iteration_bindings::{IterationPart, ViewIterationBinding, checked_view_iteration_binding}; #[cfg(test)] +mod borrowed_sum_view_tests; +#[cfg(test)] mod call_ownership_tests; mod inline_functions; mod interface_values; #[cfg(test)] mod local_view_tests; mod local_views; +mod reflected_callable; mod reflected_fields; +pub use reflected_callable::{ + ReflectedCallablePlan, has_reflected_callable_plans, lower_reflected_callable_value, + materialize_checked_reflected_values, reflected_callable_plans, +}; mod type_validation; pub use call_ownership::{ @@ -50,7 +68,8 @@ pub use call_ownership::{ }; pub use interface_values::complete_value_conversions; pub use local_views::{ - is_borrowed_local, local_view_root, validate_local_view_initializer, validate_local_views, + borrowed_sum_view_initializer, is_borrowed_local, local_view_root, + validate_local_view_initializer, validate_local_views, }; pub use reflected_fields::{ ReflectedFieldAction, ReflectedFieldPlan, ReflectedFieldRequirement, @@ -173,8 +192,18 @@ pub struct ScopedTypeBinding { pub reflection: jett_types::ReflectionTypeInfo, } +/// Exact checker-selected lexical scope of a required expression. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RequiredScopeIdentity { + pub owner: Span, + pub index: usize, + pub selection: CheckedComptimeTypeSelection, +} + #[derive(Debug, Clone, PartialEq)] pub struct Program { + pub resource_manifest: ResourceManifest, + pub resource_source: ResourceSourceArchive, pub functions: Vec, pub equality_methods: HashMap, } @@ -355,6 +384,17 @@ pub enum ExpressionKind { }, /// A checked, concrete source function used as a first-class value. FunctionRef(FunctionId), + /// Compiler-owned descriptor; no opaque runtime representation is implied. + ResourceHookValue { + hook: ResourceHookRef, + }, + /// Original Source invocation; ordinary owner/drop plans cannot execute it. + ResourceInvoke { + hook: ResourceHookRef, + args: Vec, + evaluation_order: Vec, + ownership: CallOwnership, + }, /// A checked inline function with an environment copied from caller locals. ClosureRef { function: FunctionId, @@ -461,6 +501,7 @@ pub enum ExpressionKind { Comptime { value: Box, bindings: Vec, + scopes: Vec, /// Stable lookup identity, even when parentheses widen the expression span. source_span: Span, }, @@ -530,11 +571,13 @@ pub enum ExpressionKind { } /// Generated callback conversion required inside a container conversion. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct InterfaceFunctionAdapter { pub source: TypeId, pub target: TypeId, pub function: FunctionId, + /// Present only while completing a checked reflected callable value. + pub reflected: Option, } impl ExpressionKind { @@ -554,6 +597,11 @@ pub enum HandleKind { refined_type: TypeId, predicates: Vec, }, + /// Private callable adapter boundary; never admitted as a source handler. + ReflectedReturn { + plan: Box, + callable: Option, + }, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -622,6 +670,7 @@ pub struct ValidationError { /// Validate backend-facing HIR invariants after lowering and before MIR. pub fn validate(program: &Program) -> Result<(), Vec> { + reflected_callable::validate_program(program)?; let mut validator = Validator { program, errors: Vec::new(), @@ -894,6 +943,31 @@ impl Validator<'_> { } self.expression(value); } + ExpressionKind::ResourceHookValue { hook } => { + if !self.program.resource_manifest.contains_hook(hook) { + self.error( + expression.span, + "Resource descriptor has no original program manifest", + ); + } + } + ExpressionKind::ResourceInvoke { + hook, + args, + evaluation_order, + .. + } => { + if !self.program.resource_manifest.contains_hook(hook) { + self.error( + expression.span, + "Resource invocation has no original program manifest", + ); + } + self.check_evaluation_order(evaluation_order, args.len(), expression.span); + for argument in args { + self.expression(argument); + } + } ExpressionKind::Call { function, args, @@ -1193,6 +1267,77 @@ pub fn lower_with_test_bodies( Lowerer::new(module, resolve, check, origins, true).lower() } +/// Preserve ordinary lowering policy while retaining one immutable checked +/// session for genuine reflected callable constructors and required values. +pub fn lower_checked_ordinary_program( + original: &Arc, +) -> Result> { + lower_ordinary_program(original, false) +} + +pub fn lower_checked_ordinary_program_with_test_bodies( + original: &Arc, +) -> Result> { + lower_ordinary_program(original, true) +} + +fn lower_ordinary_program( + original: &Arc, + tests: bool, +) -> Result> { + let mut lowerer = Lowerer::new( + original.module(), + original.resolved(), + original.checked(), + original.source_origins(), + tests, + ); + lowerer.reflected_checked_program = Some(original.clone()); + let mut program = lowerer.lower()?; + if has_reflected_callable_plans(&program) { + program.resource_source = + ResourceSourceArchive::checked(&program, original, &original.checked().interner); + } + Ok(program) +} + +/// Lower the original immutable checked Resource envelope, including unused hooks. +pub fn lower_checked_resource_program( + original: &Arc, +) -> Result> { + lower_resource_program(original, false) +} +pub fn lower_checked_resource_program_with_test_bodies( + original: &Arc, +) -> Result> { + lower_resource_program(original, true) +} +fn lower_resource_program( + original: &Arc, + tests: bool, +) -> Result> { + let manifest = ResourceManifest::checked(original).map_err(|message| { + vec![LowerError { + span: Span::new(FileId::new(0), 0, 0), + message, + }] + })?; + let mut lowerer = Lowerer::new( + original.module(), + original.resolved(), + original.checked(), + original.source_origins(), + tests, + ); + lowerer.resource_manifest = manifest; + lowerer.authenticated_resource_lowering = true; + lowerer.reflected_checked_program = Some(original.clone()); + let mut program = lowerer.lower()?; + program.resource_source = + ResourceSourceArchive::checked(&program, original, &original.checked().interner); + Ok(program) +} + struct FunctionSource<'a> { id: FunctionId, definition: Option, @@ -1240,6 +1385,9 @@ enum FunctionKey { } struct Lowerer<'a> { + resource_manifest: ResourceManifest, + authenticated_resource_lowering: bool, + reflected_checked_program: Option>, module: &'a Module, resolve: &'a ResolveResult, check: &'a CheckResult, @@ -1284,6 +1432,9 @@ impl<'a> Lowerer<'a> { }) .collect(); Self { + resource_manifest: ResourceManifest::empty(), + authenticated_resource_lowering: false, + reflected_checked_program: None, module, resolve, check, @@ -1395,10 +1546,13 @@ impl<'a> Lowerer<'a> { }) .collect::, _>>()?; let mut program = Program { + resource_manifest: self.resource_manifest.clone(), + resource_source: ResourceSourceArchive::empty(), functions, equality_methods, }; interface_values::coerce_program(&mut program, &self.check.interner); + reflected_callable::seal_program(&mut program)?; validate(&program).map_err(|errors| { errors .into_iter() @@ -1894,6 +2048,21 @@ impl<'a> Lowerer<'a> { static_selections, comptime_type_bindings, ); + if body_lowerer.parent.authenticated_resource_lowering + && source.instantiation.is_none() + && source.method.is_none() + && source.function.type_params.is_empty() + && source.definition.is_some_and(|definition| { + reflected_field_sources::original_root_function( + body_lowerer.parent.module, + body_lowerer.parent.resolve, + source.function, + definition, + ) + }) + { + body_lowerer.reflected_field_root = Some(&source.function.body); + } body_lowerer.return_type = Some(return_type); let mut params = Vec::with_capacity(source.function.params.len()); for (param, ty) in source.function.params.iter().zip(parameter_types) { @@ -2601,6 +2770,9 @@ struct BodyLowerer<'lowerer, 'program> { locals: Vec, visible_bindings: Vec>, scoped_type_bindings: Vec, + required_scopes: Vec, + reflected_field_root: Option<&'program ast::Block>, + retained_reflected_bindings: HashSet, return_type: Option, ownership_context: jett_typecheck::CheckedOwnershipContext, view_parameter_locals: HashSet, @@ -2660,6 +2832,9 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { locals: Vec::new(), visible_bindings: vec![HashMap::new()], scoped_type_bindings: Vec::new(), + required_scopes: Vec::new(), + reflected_field_root: None, + retained_reflected_bindings: HashSet::new(), return_type: None, ownership_context: jett_typecheck::CheckedOwnershipContext::Ordinary, view_parameter_locals: HashSet::new(), @@ -2816,52 +2991,7 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { fn lower_statement(&mut self, statement: &Stmt) -> Option { let (kind, span) = match statement { - Stmt::VarDecl(decl) => { - let Some(definition) = self.parent.definition_at(decl.name.span, DefKind::Variable) - else { - self.parent - .error(decl.name.span, "local has no resolved definition"); - return None; - }; - let Some(ty) = - self.expression_types - .get(&decl.name.span) - .copied() - .filter(|ty| { - // Bare machine annotations erase precise state in - // the binding while its producer keeps that state. - // Secret qualification also preserves both sides. - matches!( - self.parent.check.interner.resolve(*ty), - Type::Machine(_) | Type::Secret(_) - ) || interface_values::contains_erased_boundary( - &self.parent.check.interner, - *ty, - ) || self.expression_types.get(&decl.value.span()).is_some_and( - |actual| { - interface_values::contains_erased_boundary( - &self.parent.check.interner, - *actual, - ) - }, - ) - }) - .or_else(|| self.expression_types.get(&decl.value.span()).copied()) - .or_else(|| self.parent.check.definition_types.get(&definition).copied()) - else { - self.parent - .error(decl.name.span, "local has no checked type"); - return None; - }; - // The new binding becomes visible only after its initializer, - // including any handled failure and nested breakpoint. - let value = self.lower_expression(&decl.value)?; - let view_source = self.local_view_source(decl, &value, ty)?; - let local = - self.allocate_local(definition, &decl.name.name, ty, decl.mutable, decl.span); - self.locals[local.index() as usize].view_source = view_source; - (StatementKind::Let { local, value }, decl.span) - } + Stmt::VarDecl(decl) => self.lower_local_declaration(decl)?, Stmt::Assign(assign) => ( StatementKind::Assign { target: self.lower_expression(&assign.target)?, @@ -2887,72 +3017,7 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { }; (kind, expr.span) } - Stmt::If(branch) => { - if let Some(selection) = self.static_selection(branch.span) { - let selected = match selection { - CheckedStaticSelection::IfThen => Some(&branch.then_block), - CheckedStaticSelection::IfElseIf(index) => { - match branch.else_ifs.get(index) { - Some((_, block)) => Some(block), - None => { - self.parent.error( - branch.span, - "checked static else-if selection is out of range", - ); - return None; - } - } - } - CheckedStaticSelection::IfElse => match branch.else_block.as_ref() { - Some(block) => Some(block), - None => { - self.parent.error( - branch.span, - "checked static else selection has no source branch", - ); - return None; - } - }, - CheckedStaticSelection::IfNoBranch => { - if branch.else_block.is_some() { - self.parent.error( - branch.span, - "checked static no-branch selection disagrees with source else", - ); - return None; - } - None - } - CheckedStaticSelection::MatchArm(_) => { - self.parent.error( - branch.span, - "checked static match selection attached to an if statement", - ); - return None; - } - }; - let Some(selected) = selected else { - return None; - }; - ( - StatementKind::Scope(self.lower_block(selected)), - branch.span, - ) - } else { - let condition = self.lower_expression(&branch.condition)?; - let then_block = self.lower_block(&branch.then_block); - let else_block = - self.lower_else_chain(&branch.else_ifs, branch.else_block.as_ref()); - ( - StatementKind::If { - condition, - then_block, - else_block, - }, - branch.span, - ) - } - } + Stmt::If(branch) => self.lower_if_statement(branch)?, Stmt::While(loop_stmt) => ( StatementKind::While { condition: self.lower_expression(&loop_stmt.condition)?, @@ -2960,127 +3025,8 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { }, loop_stmt.span, ), - Stmt::For(loop_stmt) => { - let iterable = self.lower_expression(&loop_stmt.iterable)?; - self.visible_bindings.push(HashMap::new()); - let key = self.allocate_declared_local(&loop_stmt.variable, false)?; - let value = match &loop_stmt.value_variable { - Some(binding) => Some(self.allocate_declared_local(binding, false)?), - None => None, - }; - let saved_iteration_bindings = self.view_iteration_bindings.clone(); - if loop_stmt.view { - let part = if matches!( - self.parent.check.interner.resolve(iterable.ty), - Type::Map(..) - ) { - IterationPart::Key - } else { - IterationPart::Element - }; - let bindings = [(key, part)] - .into_iter() - .chain(value.map(|value| (value, IterationPart::Value))); - for (local, part) in bindings { - let metadata = &self.locals[local.index() as usize]; - if metadata.mutable - || metadata.view_source.is_some() - || self.view_parameter_locals.contains(&local) - || self.view_iteration_bindings.contains_key(&local) - { - self.parent.error( - loop_stmt.span, - "viewed iteration binder is not a fresh immutable local", - ); - return None; - } - match checked_view_iteration_binding( - &self.parent.check.interner, - loop_stmt.span, - iterable.ty, - metadata.ty, - part, - ) { - Ok(proof) - if !jett_typecheck::ownership::is_implicitly_copyable( - &self.parent.check.interner, - proof.binder_type(), - ) => - { - self.view_iteration_bindings.insert(local, proof); - } - Ok(_) => {} - Err(message) => { - self.parent.error(loop_stmt.span, message); - return None; - } - } - } - } - let body = if matches!( - self.parent.check.interner.resolve(iterable.ty), - Type::List(element) if *element == TypeInterner::NEVER - ) || matches!( - self.parent.check.interner.resolve(iterable.ty), - Type::Map(key, value) - if *key == TypeInterner::NEVER || *value == TypeInterner::NEVER - ) { - // The frontend still checks this body. An uninhabited - // element type proves that no iteration can enter it; - // retain the iterable so MIR validates pending values. - self.consume_omitted_body_facts(loop_stmt.body.span); - Block { - statements: Vec::new(), - span: loop_stmt.body.span, - } - } else { - self.lower_block(&loop_stmt.body) - }; - self.view_iteration_bindings = saved_iteration_bindings; - self.visible_bindings.pop(); - ( - StatementKind::For { - key, - value, - by_view: loop_stmt.view, - iterable, - body, - }, - loop_stmt.span, - ) - } - Stmt::Match(match_stmt) => { - if let Some(selection) = self.static_selection(match_stmt.span) { - let CheckedStaticSelection::MatchArm(index) = selection else { - self.parent.error( - match_stmt.span, - "checked static if selection attached to a match statement", - ); - return None; - }; - let Some(arm) = match_stmt.arms.get(index) else { - self.parent.error( - match_stmt.span, - "checked static match-arm selection is out of range", - ); - return None; - }; - if matches!(&arm.pattern, ast::Pattern::Variant(_, bindings) if !bindings.is_empty()) - { - self.parent.error( - arm.span, - "checked static match arm unexpectedly binds runtime payloads", - ); - return None; - } - ( - StatementKind::Scope(self.lower_block(&arm.body)), - match_stmt.span, - ) - } else { - (self.lower_match(match_stmt)?, match_stmt.span) - } - } + Stmt::For(loop_stmt) => self.lower_for_statement(loop_stmt)?, + Stmt::Match(match_stmt) => self.lower_match_statement(match_stmt)?, Stmt::Break(span) => (StatementKind::Break, *span), Stmt::Continue(span) => (StatementKind::Continue, *span), Stmt::Assert(assertion) => ( @@ -3159,6 +3105,272 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { Some(Statement { kind, span }) } + #[inline(never)] + fn lower_local_declaration(&mut self, decl: &ast::VarDecl) -> Option<(StatementKind, Span)> { + Some({ + let Some(definition) = self.parent.definition_at(decl.name.span, DefKind::Variable) + else { + self.parent + .error(decl.name.span, "local has no resolved definition"); + return None; + }; + let Some(ty) = self + .expression_types + .get(&decl.name.span) + .copied() + .filter(|ty| { + // Bare machine annotations erase precise state in + // the binding while its producer keeps that state. + // Secret qualification also preserves both sides. + matches!( + self.parent.check.interner.resolve(*ty), + Type::Machine(_) | Type::Secret(_) + ) || interface_values::contains_erased_boundary( + &self.parent.check.interner, + *ty, + ) || self + .expression_types + .get(&decl.value.span()) + .is_some_and(|actual| { + interface_values::contains_erased_boundary( + &self.parent.check.interner, + *actual, + ) + }) + }) + .or_else(|| self.expression_types.get(&decl.value.span()).copied()) + .or_else(|| self.parent.check.definition_types.get(&definition).copied()) + else { + self.parent + .error(decl.name.span, "local has no checked type"); + return None; + }; + // The new binding becomes visible only after its initializer, + // including any handled failure and nested breakpoint. + let value = self.lower_expression(&decl.value)?; + let view_source = self.local_view_source(decl, &value, ty)?; + let local = + self.allocate_local(definition, &decl.name.name, ty, decl.mutable, decl.span); + self.locals[local.index() as usize].view_source = view_source; + (StatementKind::Let { local, value }, decl.span) + }) + } + + #[inline(never)] + fn lower_if_statement(&mut self, branch: &ast::IfStmt) -> Option<(StatementKind, Span)> { + Some({ + if let Some(selection) = self.static_selection(branch.span) { + let selected = match selection { + CheckedStaticSelection::IfThen => Some(&branch.then_block), + CheckedStaticSelection::IfElseIf(index) => match branch.else_ifs.get(index) { + Some((_, block)) => Some(block), + None => { + self.parent.error( + branch.span, + "checked static else-if selection is out of range", + ); + return None; + } + }, + CheckedStaticSelection::IfElse => match branch.else_block.as_ref() { + Some(block) => Some(block), + None => { + self.parent.error( + branch.span, + "checked static else selection has no source branch", + ); + return None; + } + }, + CheckedStaticSelection::IfNoBranch => { + if branch.else_block.is_some() { + self.parent.error( + branch.span, + "checked static no-branch selection disagrees with source else", + ); + return None; + } + None + } + CheckedStaticSelection::MatchArm(_) => { + self.parent.error( + branch.span, + "checked static match selection attached to an if statement", + ); + return None; + } + }; + let Some(selected) = selected else { + return None; + }; + ( + StatementKind::Scope(self.lower_block(selected)), + branch.span, + ) + } else { + let condition = self.lower_expression(&branch.condition)?; + let then_block = self.lower_block(&branch.then_block); + let else_block = + self.lower_else_chain(&branch.else_ifs, branch.else_block.as_ref()); + ( + StatementKind::If { + condition, + then_block, + else_block, + }, + branch.span, + ) + } + }) + } + + #[inline(never)] + fn lower_for_statement(&mut self, loop_stmt: &ast::ForStmt) -> Option<(StatementKind, Span)> { + Some({ + let iterable = self.lower_expression(&loop_stmt.iterable)?; + self.visible_bindings.push(HashMap::new()); + let key = self.allocate_declared_local(&loop_stmt.variable, false)?; + let value = match &loop_stmt.value_variable { + Some(binding) => Some(self.allocate_declared_local(binding, false)?), + None => None, + }; + let saved_iteration_bindings = self.view_iteration_bindings.clone(); + if loop_stmt.view { + let part = if matches!( + self.parent.check.interner.resolve(iterable.ty), + Type::Map(..) + ) { + IterationPart::Key + } else { + IterationPart::Element + }; + let bindings = [(key, part)] + .into_iter() + .chain(value.map(|value| (value, IterationPart::Value))); + for (local, part) in bindings { + let metadata = &self.locals[local.index() as usize]; + if metadata.mutable + || metadata.view_source.is_some() + || self.view_parameter_locals.contains(&local) + || self.view_iteration_bindings.contains_key(&local) + { + self.parent.error( + loop_stmt.span, + "viewed iteration binder is not a fresh immutable local", + ); + return None; + } + match checked_view_iteration_binding( + &self.parent.check.interner, + loop_stmt.span, + iterable.ty, + metadata.ty, + part, + ) { + Ok(proof) + if !jett_typecheck::ownership::is_implicitly_copyable( + &self.parent.check.interner, + proof.binder_type(), + ) => + { + self.view_iteration_bindings.insert(local, proof); + } + Ok(_) => {} + Err(message) => { + self.parent.error(loop_stmt.span, message); + return None; + } + } + } + } + let saved_reflected_bindings = std::mem::take(&mut self.retained_reflected_bindings); + if self.required_scopes.is_empty() + && self.reflected_field_root.is_some_and(|root| { + root.stmts.iter().any(|statement| { + matches!(statement, Stmt::For(original) if std::ptr::eq(original, loop_stmt)) + }) + }) + { + self.retained_reflected_bindings = reflected_field_sources::original_loop_bindings( + self.parent.resolve, + self.parent.check, + loop_stmt, + ).into_iter().collect(); + } + let body = if matches!( + self.parent.check.interner.resolve(iterable.ty), + Type::List(element) if *element == TypeInterner::NEVER + ) || matches!( + self.parent.check.interner.resolve(iterable.ty), + Type::Map(key, value) + if *key == TypeInterner::NEVER || *value == TypeInterner::NEVER + ) { + // The frontend still checks this body. An uninhabited + // element type proves that no iteration can enter it; + // retain the iterable so MIR validates pending values. + self.consume_omitted_body_facts(loop_stmt.body.span); + Block { + statements: Vec::new(), + span: loop_stmt.body.span, + } + } else { + self.lower_block(&loop_stmt.body) + }; + self.retained_reflected_bindings = saved_reflected_bindings; + self.view_iteration_bindings = saved_iteration_bindings; + self.visible_bindings.pop(); + ( + StatementKind::For { + key, + value, + by_view: loop_stmt.view, + iterable, + body, + }, + loop_stmt.span, + ) + }) + } + + #[inline(never)] + fn lower_match_statement( + &mut self, + match_stmt: &ast::MatchStmt, + ) -> Option<(StatementKind, Span)> { + Some({ + if let Some(selection) = self.static_selection(match_stmt.span) { + let CheckedStaticSelection::MatchArm(index) = selection else { + self.parent.error( + match_stmt.span, + "checked static if selection attached to a match statement", + ); + return None; + }; + let Some(arm) = match_stmt.arms.get(index) else { + self.parent.error( + match_stmt.span, + "checked static match-arm selection is out of range", + ); + return None; + }; + if matches!(&arm.pattern, ast::Pattern::Variant(_, bindings) if !bindings.is_empty()) + { + self.parent.error( + arm.span, + "checked static match arm unexpectedly binds runtime payloads", + ); + return None; + } + ( + StatementKind::Scope(self.lower_block(&arm.body)), + match_stmt.span, + ) + } else { + (self.lower_match(match_stmt)?, match_stmt.span) + } + }) + } + fn lower_constant_trace( &mut self, trace: &ast::TraceStmt, @@ -3260,7 +3472,7 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { .next() .expect("single checked binding exists"); return Some(StatementKind::Scope( - self.lower_bound_type_body(binding, checked), + self.lower_bound_type_body(binding, 0, checked), )); } @@ -3293,9 +3505,10 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { ); return None; } + let retain_ordinals = self.retained_reflected_bindings.contains(&binding.span); let mut arms = Vec::with_capacity(bindings.len()); let mut bound_types = HashSet::new(); - for checked in bindings { + for (index, checked) in bindings.into_iter().enumerate() { let CheckedComptimeTypeSelection::ReflectedIteration(iteration_index) = checked.selection else { @@ -3305,14 +3518,31 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { // Share an arm only when both canonical type and source-visible // reflection agree; aliases can specialize the body differently. let reflection_identity = checked.reflection.reflection_identity(); - if !bound_types.insert((checked.bound_type, reflection_identity.clone())) { + if !retain_ordinals + && !bound_types.insert((checked.bound_type, reflection_identity.clone())) + { continue; } + let bound_type = checked.bound_type; + let body = self.lower_bound_type_body(binding, index, checked); + let body = if retain_ordinals { + // An ordinal owns an exact lexical Source body even when another + // field has the same canonical type and reflection identity. + Block { + statements: vec![Statement { + kind: StatementKind::Scope(body), + span: binding.span, + }], + span: binding.body.span, + } + } else { + body + }; arms.push(ReflectedTypeArm { iteration_index, - bound_type: checked.bound_type, + bound_type, reflection_identity, - body: self.lower_bound_type_body(binding, checked), + body, }); } Some(StatementKind::ReflectedTypeDispatch { type_info, arms }) @@ -3321,6 +3551,7 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { fn lower_bound_type_body( &mut self, binding: &ast::ComptimeTypeBindStmt, + index: usize, checked: CheckedComptimeTypeBinding, ) -> Block { self.scoped_type_bindings.push(ScopedTypeBinding { @@ -3328,7 +3559,13 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { ty: checked.bound_type, reflection: checked.reflection.clone(), }); + self.required_scopes.push(RequiredScopeIdentity { + owner: binding.span, + index, + selection: checked.selection, + }); let body = self.lower_block_with_checked_facts(&binding.body, checked.body); + self.required_scopes.pop(); self.scoped_type_bindings.pop(); body } @@ -3578,6 +3815,12 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { ExpressionKind::Constant { declaration: *declaration, } + } else if let Some(hook) = self + .parent + .resource_manifest + .hook_for_definition(definition) + { + ExpressionKind::ResourceHookValue { hook } } else if self.parent.resolve.scope_table.def(definition).kind == DefKind::Function { ExpressionKind::FunctionRef(self.resolve_function_value_target(expression)?) @@ -3592,43 +3835,7 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { Expr::Binary(left, op @ (ast::BinOp::Eq | ast::BinOp::NotEq), right, _) if self.method_calls.contains_key(&span) => { - let function = self.resolve_user_call_target(left, span)?; - let args = vec![self.lower_expression(left)?, self.lower_expression(right)?]; - let (parameter_types, _) = self - .ownership_function_parameters(function) - .map_err(|message| self.parent.error(span, message)) - .ok()?; - let ownership = self.generated_ownership( - GeneratedOperation::Equality { method: function }, - &args, - ¶meter_types, - &[CheckedCalleeAccess::View, CheckedCalleeAccess::View], - ty, - &[0, 1], - span, - )?; - let call = ExpressionKind::EquatableResult(Box::new(Expression { - kind: ExpressionKind::Call { - ownership, - function, - args, - evaluation_order: vec![0, 1], - }, - ty, - span, - })); - if *op == ast::BinOp::NotEq { - ExpressionKind::Unary { - op: UnaryOp::Not, - value: Box::new(Expression { - kind: call, - ty, - span, - }), - } - } else { - call - } + self.lower_equality_expression(left, *op, right, ty, span)? } Expr::Binary(left, op, right, _) => ExpressionKind::Binary { left: Box::new(self.lower_expression(left)?), @@ -3655,6 +3862,19 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { value: Box::new(self.lower_expression(value)?), }, Expr::FieldAccess(base, field, _) => { + if let Some(definition) = self.parent.resolve.resolutions.get(&span) + && let Some(hook) = self + .parent + .resource_manifest + .hook_for_definition(*definition) + { + return Some(Expression { + kind: ExpressionKind::ResourceHookValue { hook }, + ty, + span, + }); + } + if self.method_values.contains_key(&span) || self.resolved_expression_kind(expression) == Some(DefKind::Function) { @@ -3726,6 +3946,7 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { Expr::Comptime(value, source_span) => ExpressionKind::Comptime { value: Box::new(self.lower_expression(value)?), bindings: self.scoped_type_bindings.clone(), + scopes: self.required_scopes.clone(), source_span: *source_span, }, Expr::Declassify(value, _) => { @@ -3766,81 +3987,7 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { ExpressionKind::Cancel(Box::new(self.lower_expression(value)?)) } Expr::InlineFn(params, _, body, _) => { - let Type::Function { - params: parameter_types, - view_params: parameter_views, - return_type, - .. - } = self.parent.check.interner.resolve(ty).clone() - else { - self.parent - .error(span, "closure has no checked function signature"); - return None; - }; - if params.len() != parameter_types.len() - || params.len() != parameter_views.len() - || params - .iter() - .zip(¶meter_views) - .any(|(param, view)| param.view != *view) - { - self.parent.error( - span, - "closure parameters disagree with its checked signature", - ); - return None; - } - let local_floor = self.locals.len() as u32; - self.visible_bindings.push(HashMap::new()); - let mut lowered_params = Vec::with_capacity(params.len()); - let mut view_params = Vec::new(); - for (param, param_type) in params.iter().zip(parameter_types) { - let Some(definition) = - self.parent.definition_at(param.name.span, DefKind::Param) - else { - self.parent - .error(param.name.span, "closure parameter is unresolved"); - return None; - }; - // A generic instantiation's expression signature contains - // its concrete parameter types; global definition metadata - // shares source DefIds across specializations. - let local = self.allocate_local( - definition, - ¶m.name.name, - param_type, - param.mutable, - param.span, - ); - lowered_params.push(local); - if param.view { - view_params.push(local); - } - } - let enclosing_return_type = self.return_type.replace(return_type); - let enclosing_context = std::mem::replace( - &mut self.ownership_context, - jett_typecheck::CheckedOwnershipContext::Ordinary, - ); - let enclosing_view_parameters = std::mem::replace( - &mut self.view_parameter_locals, - view_params.iter().copied().collect(), - ); - let enclosing_iteration_bindings = - std::mem::take(&mut self.view_iteration_bindings); - let body = self.lower_block(body); - self.view_parameter_locals = enclosing_view_parameters; - self.view_iteration_bindings = enclosing_iteration_bindings; - self.ownership_context = enclosing_context; - self.return_type = enclosing_return_type; - self.visible_bindings.pop(); - ExpressionKind::InlineFunction { - scoped_type_bindings: self.scoped_type_bindings.clone(), - params: lowered_params, - view_params, - local_floor, - body, - } + self.lower_inline_function(params, body, ty, span)? } Expr::Spawn(inner, _) => self.lower_actor_spawn(inner, ty)?, Expr::Send(inner, _) => self.lower_actor_message(inner, ActorMessageKind::Send)?, @@ -3883,6 +4030,141 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { ) } + #[inline(never)] + fn lower_equality_expression( + &mut self, + left: &Expr, + op: ast::BinOp, + right: &Expr, + ty: TypeId, + span: Span, + ) -> Option { + Some({ + let function = self.resolve_user_call_target(left, span)?; + let args = vec![self.lower_expression(left)?, self.lower_expression(right)?]; + let (parameter_types, _) = self + .ownership_function_parameters(function) + .map_err(|message| self.parent.error(span, message)) + .ok()?; + let ownership = self.generated_ownership( + GeneratedOperation::Equality { method: function }, + &args, + ¶meter_types, + &[CheckedCalleeAccess::View, CheckedCalleeAccess::View], + ty, + &[0, 1], + span, + )?; + let call = ExpressionKind::EquatableResult(Box::new(Expression { + kind: ExpressionKind::Call { + ownership, + function, + args, + evaluation_order: vec![0, 1], + }, + ty, + span, + })); + if op == ast::BinOp::NotEq { + ExpressionKind::Unary { + op: UnaryOp::Not, + value: Box::new(Expression { + kind: call, + ty, + span, + }), + } + } else { + call + } + }) + } + + #[inline(never)] + fn lower_inline_function( + &mut self, + params: &[ast::Param], + body: &ast::Block, + ty: TypeId, + span: Span, + ) -> Option { + Some({ + let Type::Function { + params: parameter_types, + view_params: parameter_views, + return_type, + .. + } = self.parent.check.interner.resolve(ty).clone() + else { + self.parent + .error(span, "closure has no checked function signature"); + return None; + }; + if params.len() != parameter_types.len() + || params.len() != parameter_views.len() + || params + .iter() + .zip(¶meter_views) + .any(|(param, view)| param.view != *view) + { + self.parent.error( + span, + "closure parameters disagree with its checked signature", + ); + return None; + } + let local_floor = self.locals.len() as u32; + self.visible_bindings.push(HashMap::new()); + let mut lowered_params = Vec::with_capacity(params.len()); + let mut view_params = Vec::new(); + for (param, param_type) in params.iter().zip(parameter_types) { + let Some(definition) = self.parent.definition_at(param.name.span, DefKind::Param) + else { + self.parent + .error(param.name.span, "closure parameter is unresolved"); + return None; + }; + // A generic instantiation's expression signature contains + // its concrete parameter types; global definition metadata + // shares source DefIds across specializations. + let local = self.allocate_local( + definition, + ¶m.name.name, + param_type, + param.mutable, + param.span, + ); + lowered_params.push(local); + if param.view { + view_params.push(local); + } + } + let enclosing_return_type = self.return_type.replace(return_type); + let enclosing_context = std::mem::replace( + &mut self.ownership_context, + jett_typecheck::CheckedOwnershipContext::Ordinary, + ); + let enclosing_view_parameters = std::mem::replace( + &mut self.view_parameter_locals, + view_params.iter().copied().collect(), + ); + let enclosing_iteration_bindings = std::mem::take(&mut self.view_iteration_bindings); + let body = self.lower_block(body); + self.view_parameter_locals = enclosing_view_parameters; + self.view_iteration_bindings = enclosing_iteration_bindings; + self.ownership_context = enclosing_context; + self.return_type = enclosing_return_type; + self.visible_bindings.pop(); + ExpressionKind::InlineFunction { + scoped_type_bindings: self.scoped_type_bindings.clone(), + params: lowered_params, + view_params, + local_floor, + body, + } + }) + } + fn lower_actor_spawn(&mut self, inner: &Expr, ty: TypeId) -> Option { let (callee, args, call_span) = match inner { Expr::Call(callee, args, span) => (callee.as_ref(), args.as_slice(), *span), @@ -4311,6 +4593,19 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { self.lower_arguments_in_parameter_order(args, call_span)?; self.check_source_actuals(call_span, args)?; let ownership = self.source_ownership(call_span, &lowered_args, &evaluation_order)?; + if let CallOwnership::Source(source) = &ownership + && let CallTarget::ResourceHook(hook) = &source.target + { + let hook = hook.clone(); + let ownership = + self.finish_resource_call_ownership(ownership, &hook, &lowered_args, call_span)?; + return Some(ExpressionKind::ResourceInvoke { + hook, + ownership, + args: lowered_args, + evaluation_order, + }); + } let source_call = self.is_source_call(callee, call_span); // A callee can be any checked function value, including a projected // field or another call's result. Declaration and intrinsic identities @@ -6528,6 +6823,14 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { payloads: args, evaluation_order, } + } else if self.call_ownership.get(&step.span).is_some_and(|packet| { + matches!(&packet.target, jett_typecheck::CheckedInvocationTarget::Resolved(definition) if self.parent.resource_manifest.hook_for_definition(*definition).is_some()) + }) { + let ownership = self.source_ownership(step.span, &args, &evaluation_order)?; + let CallOwnership::Source(source) = &ownership else { return None; }; + let CallTarget::ResourceHook(hook) = &source.target else { return None; }; + let hook = hook.clone(); + ExpressionKind::ResourceInvoke { ownership: self.finish_resource_call_ownership(ownership, &hook, &args, step.span)?, hook, args, evaluation_order } } else if !source_call && !self.intrinsic_ids.contains_key(&step.span) && self.is_checked_function_value(callee) diff --git a/crates/jett_hir/src/local_view_tests.rs b/crates/jett_hir/src/local_view_tests.rs index f9ba7639..5a922049 100644 --- a/crates/jett_hir/src/local_view_tests.rs +++ b/crates/jett_hir/src/local_view_tests.rs @@ -1,7 +1,10 @@ use super::*; use jett_diagnostics::Severity; -fn checked_source(source: &str, tests: bool) -> Result<(Program, CheckResult), Vec> { +pub(super) fn checked_source( + source: &str, + tests: bool, +) -> Result<(Program, CheckResult), Vec> { let file = FileId::new(0); let parsed = jett_parser::parse(source, file); assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); diff --git a/crates/jett_hir/src/local_views.rs b/crates/jett_hir/src/local_views.rs index 239ae1da..00b9ec47 100644 --- a/crates/jett_hir/src/local_views.rs +++ b/crates/jett_hir/src/local_views.rs @@ -4,8 +4,8 @@ use jett_types::{Type, TypeId, TypeInterner}; use crate::{ - Expression, ExpressionKind, Function, Local, LocalId, Param, ParamMode, Program, - ValidationError, + Block, Expression, ExpressionKind, Function, HandleKind, Local, LocalId, Param, ParamMode, + Program, StatementKind, ValidationError, }; /// Resolve an alias to its backing local. Invalid IDs and cycles have no root. @@ -267,6 +267,9 @@ pub fn validate_local_view_initializer( target: TypeId, types: &TypeInterner, ) -> Result<(), &'static str> { + if borrowed_sum_view_initializer(value, source, source_type, target, types)?.is_some() { + return Ok(()); + } if !same_backing_type(types, value.ty, target) { return Err("native borrowed alias cannot change its backing representation"); } @@ -341,3 +344,98 @@ pub fn validate_local_view_initializer( } } } + +/// Read the exact typed borrowed Handle projection. This structural check does +/// not grant custody authority: Resource consumers must additionally join the +/// privately archived original occurrence and their constructor-owned CFG. +pub fn borrowed_sum_view_initializer<'a>( + value: &'a Expression, + source: LocalId, + source_type: TypeId, + endpoint: TypeId, + types: &TypeInterner, +) -> Result, &'static str> { + let ExpressionKind::View(handle) = &value.kind else { + return Ok(None); + }; + let ExpressionKind::Handle { + target, + kind, + error_local, + failure, + } = &handle.kind + else { + return Ok(None); + }; + if source_type.index() as usize >= types.len() + || endpoint.index() as usize >= types.len() + || value.ty != endpoint + || handle.ty != endpoint + { + return Err("native borrowed Handle changed its exact payload endpoint"); + } + let payload = match (types.resolve(source_type), kind, error_local) { + (Type::Optional(payload), HandleKind::Optional, None) + | (Type::Result(payload, _), HandleKind::Result, Some(_)) => *payload, + _ => return Err("native borrowed Handle kind differs from its backing sum"), + }; + if payload != endpoint { + return Err("native borrowed Handle changed its exact payload endpoint"); + } + let ExpressionKind::View(backing) = &target.kind else { + return Err("native borrowed Handle requires its original written target view"); + }; + if target.ty != source_type + || backing.ty != source_type + || !matches!(backing.kind, ExpressionKind::Local(id) if id == source) + { + return Err("native borrowed Handle lost its exact stable backing local"); + } + if handle_body_has_default(failure) { + return Err("native borrowed Handle cannot yield an alternate default payload"); + } + if !matches!( + failure.statements.last().map(|statement| &statement.kind), + Some(StatementKind::Return(_)) + ) { + return Err("native borrowed Handle failure must exit without yielding a payload"); + } + Ok(Some(handle)) +} + +/// Only lexical blocks share this Handle's continuation. Expression-level +/// nested Handles and inline function bodies have their own nearest handler. +fn handle_body_has_default(body: &Block) -> bool { + body.statements + .iter() + .any(|statement| match &statement.kind { + StatementKind::HandleDefault(_) => true, + StatementKind::If { + then_block, + else_block, + .. + } => { + handle_body_has_default(then_block) + || else_block.as_ref().is_some_and(handle_body_has_default) + } + StatementKind::While { body, .. } + | StatementKind::For { body, .. } + | StatementKind::Scope(body) => handle_body_has_default(body), + StatementKind::Match { arms, .. } => { + arms.iter().any(|arm| handle_body_has_default(&arm.body)) + } + StatementKind::ReflectedTypeDispatch { arms, .. } => { + arms.iter().any(|arm| handle_body_has_default(&arm.body)) + } + StatementKind::Let { .. } + | StatementKind::Assign { .. } + | StatementKind::Return(_) + | StatementKind::Expression(_) + | StatementKind::Respond(_) + | StatementKind::Break + | StatementKind::Continue + | StatementKind::Assert { .. } + | StatementKind::Trace(_) + | StatementKind::Breakpoint { .. } => false, + }) +} diff --git a/crates/jett_hir/src/reflected_callable.rs b/crates/jett_hir/src/reflected_callable.rs new file mode 100644 index 00000000..5ee22f87 --- /dev/null +++ b/crates/jett_hir/src/reflected_callable.rs @@ -0,0 +1,936 @@ +//! Constructor-only reflected callable evidence. Labels are diagnostic data; +//! custody comes from the immutable checked session and original typed tuple. + +use super::*; + +#[path = "reflected_callable_baked.rs"] +pub(crate) mod baked; +#[path = "reflected_callable_materialized.rs"] +mod materialized_source; +pub use baked::{has_reflected_callable_plans, materialize_checked_reflected_values}; + +#[derive(Debug, Clone)] +pub struct ReflectedCallablePlan { + data: Arc, +} + +#[derive(Debug, Clone)] +struct Plan { + checked: Arc, + admission: jett_typecheck::CheckedReflectedCallablePlan, + returned: ReflectedFieldPlan, + span: Span, + function: Option, + context: Option, + seal: Option>, + materialized: bool, + ordinal: Option, + reference: Option>, +} + +#[derive(Debug)] +struct Seal { + owner: FunctionId, + getter: Expression, + adapter: Function, + predicates: Vec, + sources: Arc>, +} + +impl PartialEq for ReflectedCallablePlan { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.data, &other.data) + } +} +impl Eq for ReflectedCallablePlan {} + +impl ReflectedCallablePlan { + pub(crate) fn new( + checked: Arc, + admission: jett_typecheck::CheckedReflectedCallablePlan, + returned: ReflectedFieldPlan, + span: Span, + ordinal: Option, + ) -> Self { + Self { + data: Arc::new(Plan { + checked, + admission, + returned, + span, + function: None, + context: None, + seal: None, + materialized: false, + ordinal, + reference: None, + }), + } + } + + pub fn source_type(&self) -> TypeId { + self.data.admission.source_type() + } + pub fn requested_type(&self) -> TypeId { + self.data.admission.requested_type() + } + pub fn checked_plan(&self) -> &jett_typecheck::CheckedReflectedCallablePlan { + &self.data.admission + } + pub fn return_plan(&self) -> &ReflectedFieldPlan { + &self.data.returned + } + pub fn function(&self) -> FunctionId { + self.data.function.unwrap_or(FunctionId(u32::MAX)) + } + pub fn canonical_adapter(&self) -> Option<&Function> { + self.data.seal.as_ref().map(|seal| &seal.adapter) + } + pub fn original_owner(&self) -> Option { + self.data.seal.as_ref().map(|seal| seal.owner) + } + pub fn original_getter(&self) -> Option<&Expression> { + self.data.seal.as_ref().map(|seal| &seal.getter) + } + pub fn original_predicates(&self) -> &[Function] { + self.data + .seal + .as_ref() + .map_or(&[], |seal| seal.predicates.as_slice()) + } + pub fn is_materialized(&self) -> bool { + self.data.materialized + } + pub fn is_sealed(&self) -> bool { + self.data.seal.is_some() + } + + pub(crate) fn resolve_return(&mut self, functions: &[Function]) -> Result<(), String> { + if self.data.materialized && self.data.function.is_none() { + let returned = returned_plan( + &self.data.checked, + functions, + ReflectedFieldRequirementPlan { + source_type: self.data.admission.source_return(), + requested_type: self.data.admission.requested_return(), + requirement: self.data.admission.return_requirement().clone(), + }, + )?; + Arc::make_mut(&mut self.data).returned = returned; + } + Ok(()) + } + + pub(crate) fn bind(&mut self, function: FunctionId, context: &FunctionIdentity) { + let data = Arc::make_mut(&mut self.data); + data.function = Some(function); + data.context = Some(context.clone()); + } + + pub fn validate_types(&self, types: &TypeInterner) -> Result<(), &'static str> { + let original = self.data.checked.checked(); + if types.len() < original.interner.len() { + return Err("reflected callable lost its original checked type table"); + } + for ty in original.interner.type_ids() { + let expected = original.interner.resolve(ty); + if types.resolve(ty) != expected + || types.nominal_type_arguments(ty) != original.interner.nominal_type_arguments(ty) + || !types.contains_nominal_definition(expected) + { + return Err("reflected callable original type meaning changed"); + } + let same = match expected { + Type::Struct(id) => { + types.resolve_struct(*id) == original.interner.resolve_struct(*id) + } + Type::Enum(id) => types.resolve_enum(*id) == original.interner.resolve_enum(*id), + Type::Bitfield(id) => { + types.resolve_bitfield(*id) == original.interner.resolve_bitfield(*id) + } + Type::Interface(id) => { + types.resolve_interface(*id) == original.interner.resolve_interface(*id) + } + Type::Actor(id) => types.resolve_actor(*id) == original.interner.resolve_actor(*id), + Type::Machine(id) | Type::MachineState { machine: id, .. } => { + types.resolve_machine(*id) == original.interner.resolve_machine(*id) + } + _ => true, + }; + if !same { + return Err("reflected callable original nominal definition changed"); + } + } + if !self.data.admission.validate(original) + || self.source_type().index() as usize >= types.len() + || self.requested_type().index() as usize >= types.len() + || types.resolve(self.source_type()) != original.interner.resolve(self.source_type()) + || types.resolve(self.requested_type()) + != original.interner.resolve(self.requested_type()) + || self.data.returned.source_type != self.data.admission.source_return() + || self.data.returned.requested_type != self.data.admission.requested_return() + { + return Err("reflected callable admission differs from its checked session"); + } + let expected = ReflectedFieldRequirementPlan { + source_type: self.data.returned.source_type, + requested_type: self.data.returned.requested_type, + requirement: self.data.admission.return_requirement().clone(), + }; + super::reflected_fields::validate_requirement_node(types, &expected, &self.data.returned) + } + + pub fn validate_original(&self) -> bool { + let Some(seal) = &self.data.seal else { + return false; + }; + self.validate_types(&self.data.checked.checked().interner) + .is_ok() + && self.data.function == Some(seal.adapter.id) + && self.data.context.as_ref().is_some_and(|context| { + seal.adapter.identity.scoped_type_bindings == context.scoped_type_bindings + && seal.adapter.identity.type_arguments == context.type_arguments + && seal.adapter.identity.specialization == context.specialization + && seal.adapter.identity.declaration.origin == context.declaration.origin + && seal.adapter.identity.declaration.namespace == context.declaration.namespace + }) + } + + pub(crate) fn validate_current( + &self, + program: &Program, + owner: &Function, + getter: &Expression, + ordinal: Option, + ) -> Result<(), &'static str> { + let seal = self + .data + .seal + .as_ref() + .ok_or("reflected callable has no original constructor seal")?; + if !self.validate_original() + || seal.owner != owner.id + || self.data.ordinal != ordinal + || self.data.context.as_ref() != Some(&owner.identity) + || getter.span != self.data.span + || !super::resource_materialization::expression_equal( + &erase_callable(getter), + &seal.getter, + ) + || !program + .functions + .get(self.function().index() as usize) + .is_some_and(|function| { + super::resource_materialization::functions_equal( + std::slice::from_ref(&erase_adapter(function)), + std::slice::from_ref(&seal.adapter), + ) + }) + || seal.predicates.iter().any(|expected| { + !program + .functions + .get(expected.id.index() as usize) + .is_some_and(|current| { + let Ok(current) = original_function( + current, + &program.resource_source, + &self.data.checked, + ) else { + return false; + }; + let Ok(expected) = original_function( + expected, + &program.resource_source, + &self.data.checked, + ) else { + return false; + }; + super::resource_materialization::functions_equal( + std::slice::from_ref(¤t), + std::slice::from_ref(&expected), + ) + }) + }) + || !sources_match(&seal.sources, program, &self.data.checked) + { + return Err("reflected callable getter, adapter, predicate or context changed"); + } + if program.resource_source.checked_program().is_some() + && !program.resource_source.belongs_to(&self.data.checked) + { + return Err("reflected callable belongs to another original checked session"); + } + let context = self + .data + .context + .as_ref() + .ok_or("reflected callable context is missing")?; + let expected = adapter( + self, + self.function(), + context, + &self.data.checked.checked().interner, + ) + .ok_or("reflected callable adapter cannot be reconstructed")?; + let mut reconstruction = program.clone(); + reconstruction.functions[self.function().index() as usize] = expected; + super::interface_values::coerce_program( + &mut reconstruction, + &self.data.checked.checked().interner, + ); + if !super::resource_materialization::functions_equal( + std::slice::from_ref(&erase_adapter( + &reconstruction.functions[self.function().index() as usize], + )), + std::slice::from_ref(&seal.adapter), + ) { + return Err("reflected callable canonical body, tuple or return conversion changed"); + } + Ok(()) + } +} + +fn erase_callable(expression: &Expression) -> Expression { + let mut erased = expression.clone(); + if let ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(plans)), + .. + } = &mut erased.kind + { + for plan in plans { + if matches!(plan.action, ReflectedFieldAction::CallableReturn(_)) { + plan.action = ReflectedFieldAction::Exact; + } + } + } + erased +} + +pub(crate) fn adapter( + plan: &ReflectedCallablePlan, + id: FunctionId, + context: &FunctionIdentity, + types: &TypeInterner, +) -> Option { + plan.validate_types(types).ok()?; + let Type::Function { + params: source_params, + view_params: source_views, + return_type: source_return, + } = types.resolve(plan.source_type()) + else { + return None; + }; + let Type::Function { + params: target_params, + view_params: target_views, + return_type: target_return, + } = types.resolve(plan.requested_type()) + else { + return None; + }; + let span = plan.data.span; + let mut params = vec![Param { + local: LocalId(0), + name: "$source".into(), + ty: plan.source_type(), + mode: ParamMode::Owned, + mutable: false, + span, + }]; + params.extend( + target_params + .iter() + .zip(target_views) + .enumerate() + .map(|(index, (ty, view))| Param { + local: LocalId(index as u32 + 1), + name: format!("$argument{index}"), + ty: *ty, + mode: if *view { + ParamMode::View + } else { + ParamMode::Owned + }, + mutable: false, + span, + }), + ); + let locals = params + .iter() + .map(|param| Local { + id: param.local, + name: param.name.clone(), + ty: param.ty, + debug_ty: param.ty, + debug_type_name: None, + mutable: false, + view_source: None, + span, + }) + .collect(); + let mut args = params[1..] + .iter() + .map(|param| Expression { + kind: ExpressionKind::Local(param.local), + ty: param.ty, + span, + }) + .collect::>(); + for (argument, expected) in args.iter_mut().zip(source_params) { + super::interface_values::coerce(argument, *expected, types); + if argument.ty != *expected { + return None; + } + } + let access = source_views + .iter() + .map(|view| { + if *view { + CheckedCalleeAccess::View + } else { + CheckedCalleeAccess::Owned + } + }) + .collect::>(); + let order = (0..args.len()).collect::>(); + let ownership = CallOwnership::generated( + GeneratedOperation::ReflectedCallableAdapter { + callee: LocalId(0), + plan: plan.checked_plan().clone(), + }, + &args, + source_params, + &access, + *source_return, + &order, + types, + ) + .ok()?; + let invoked = Expression { + kind: ExpressionKind::IndirectCall { + ownership, + callee: Box::new(Expression { + kind: ExpressionKind::Local(LocalId(0)), + ty: plan.source_type(), + span, + }), + args, + evaluation_order: order, + }, + ty: *source_return, + span, + }; + let returned = Expression { + kind: ExpressionKind::Handle { + target: Box::new(invoked), + kind: HandleKind::ReflectedReturn { + plan: Box::new(plan.return_plan().clone()), + callable: None, + }, + error_local: None, + failure: Block { + statements: Vec::new(), + span, + }, + }, + ty: *target_return, + span, + }; + let mut identity = context.clone(); + identity.declaration.name = format!("$reflected.callable.{}", id.index()); + identity.declaration.kind = DeclarationKind::Function; + Some(Function { + id, + identity, + debug_kind: FunctionDebugKind::Inline, + source_definition: None, + params, + capture_count: 1, + return_type: *target_return, + locals, + body: Block { + statements: vec![Statement { + kind: StatementKind::Return(Some(returned)), + span, + }], + span, + }, + span, + }) +} + +pub(crate) fn seal_program(program: &mut Program) -> Result<(), Vec> { + let originals = program.functions.clone(); + let sources = Arc::new(originals.iter().map(erase_function).collect::>()); + let mut attached = Vec::new(); + for owner in &mut program.functions { + let identity = owner.identity.clone(); + let owner_id = owner.id; + super::resource_materialization::walk_block(&mut owner.body, &mut |expression| { + let getter = erase_callable(expression); + if let ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(plans)), + .. + } = &mut expression.kind + { + for (ordinal, plan) in plans.iter_mut().enumerate() { + if let ReflectedFieldAction::CallableReturn(callable) = &mut plan.action { + if callable.is_sealed() { + continue; + } + if callable.data.ordinal != Some(ordinal) { + return Err("reflected callable selected field ordinal changed".into()); + } + let adapter = originals + .get(callable.function().index() as usize) + .ok_or("reflected callable adapter is missing")? + .clone(); + let predicates = callable + .return_plan() + .predicates() + .into_iter() + .map(|predicate| { + originals + .get(predicate.function.index() as usize) + .cloned() + .ok_or("reflected callable predicate is missing") + }) + .collect::, _>>()?; + let data = Arc::make_mut(&mut callable.data); + if data.context.as_ref() != Some(&identity) { + return Err("reflected callable constructor context changed".into()); + } + data.seal = Some(Arc::new(Seal { + owner: owner_id, + getter: getter.clone(), + adapter, + predicates, + sources: sources.clone(), + })); + attached.push((callable.function(), callable.clone())); + } + } + } + Ok(()) + }) + .map_err(|message| { + vec![LowerError { + span: owner.span, + message, + }] + })?; + } + for function in &originals { + let mut body = function.body.clone(); + super::resource_materialization::walk_block(&mut body, &mut |expression| { + if let ExpressionKind::Handle { kind: HandleKind::ReflectedReturn { callable: Some(callable), .. }, .. } = &expression.kind { + if !callable.is_materialized() || callable.is_sealed() { return Ok(()); } + let mut callable = callable.clone(); + let context = callable.data.context.clone().ok_or("materialized reflected callable lacks context")?; + let mut sites = Vec::new(); + for owner in &originals { + let mut owner_body = owner.body.clone(); + super::resource_materialization::walk_block(&mut owner_body, &mut |value| { + if matches!(value.kind, ExpressionKind::FunctionAdapter { function: id, .. } if id == callable.function()) { + sites.push((owner.id, owner.identity.clone(), value.clone())); + } + Ok(()) + })?; + } + if sites.len() != 1 || sites[0].1 != context { return Err("materialized reflected callable has no exact construction site".into()); } + let predicates = callable.return_plan().predicates().into_iter().map(|predicate| originals.get(predicate.function.index() as usize).cloned().ok_or("materialized reflected predicate is missing")).collect::, _>>()?; + let reference = callable.data.reference.as_ref().ok_or("materialized reflected callable lost read provenance")?; + if !program.resource_source.belongs_to(reference.checked_program()) { return Err("materialized reflected callable lacks its original checked HIR archive".into()); } + if !baked::owns_materialized_owner(&program.resource_source, &originals[sites[0].0.index() as usize]) { + return Err("materialized reflected callable has no protected checked required-value construction site".into()); + } + for expected in &predicates { + let original = program.resource_source.functions().get(expected.id.index() as usize).ok_or("materialized predicate is absent from original archive")?; + let expected = original_function(expected,&program.resource_source,reference.checked_program())?; + if !super::resource_materialization::functions_equal(std::slice::from_ref(original), std::slice::from_ref(&expected)) { + return Err("materialized reflected predicate body differs from original checked Source".into()); + } + } + materialized_source::validate(reference, &sites[0].2, &originals[sites[0].0.index() as usize], &originals, &program.resource_source)?; + Arc::make_mut(&mut callable.data).seal = Some(Arc::new(Seal { owner: sites[0].0, getter: sites[0].2.clone(), adapter: erase_adapter(function), predicates, sources: sources.clone() })); + attached.push((function.id, callable)); + } + Ok(()) + }).map_err(|message| vec![LowerError { span: function.span, message }])?; + } + for (id, callable) in attached { + let function = &mut program.functions[id.index() as usize]; + if let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::Handle { + kind: + HandleKind::ReflectedReturn { + callable: stored, .. + }, + .. + }, + .. + })) = &mut function.body.statements[0].kind + { + *stored = Some(callable); + } + } + Ok(()) +} + +fn erase_adapter(function: &Function) -> Function { + let mut function = function.clone(); + let _ = super::resource_materialization::walk_block(&mut function.body, &mut |expression| { + if let ExpressionKind::Handle { + kind: HandleKind::ReflectedReturn { callable, .. }, + .. + } = &mut expression.kind + { + *callable = None; + } + Ok(()) + }); + function +} + +fn erase_function(function: &Function) -> Function { + let mut function = erase_adapter(function); + let _ = super::resource_materialization::walk_block(&mut function.body, &mut |expression| { + *expression = erase_callable(expression); + Ok(()) + }); + function +} + +pub(crate) fn validate_program(program: &Program) -> Result<(), Vec> { + let mut errors = Vec::new(); + let mut authorized = HashMap::new(); + for owner in &program.functions { + let mut body = owner.body.clone(); + let _ = super::resource_materialization::walk_block(&mut body, &mut |getter| { + if let ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(plans)), + .. + } = &getter.kind + { + for (ordinal, plan) in plans.iter().enumerate() { + if let ReflectedFieldAction::CallableReturn(callable) = &plan.action { + if let Err(message) = + callable.validate_current(program, owner, getter, Some(ordinal)) + { + errors.push(ValidationError { + span: getter.span, + message: message.into(), + }); + } else { + authorized.insert(callable.function(), callable.clone()); + } + } + } + } + if let ExpressionKind::FunctionAdapter { function, .. } = &getter.kind { + if let Some(adapter) = program.functions.get(function.index() as usize) { + if let Some(Statement { + kind: + StatementKind::Return(Some(Expression { + kind: + ExpressionKind::Handle { + kind: + HandleKind::ReflectedReturn { + callable: Some(callable), + .. + }, + .. + }, + .. + })), + .. + }) = adapter.body.statements.first() + { + if callable.is_materialized() { + if let Err(message) = + callable.validate_current(program, owner, getter, None) + { + errors.push(ValidationError { + span: getter.span, + message: message.into(), + }); + } else { + authorized.insert(*function, callable.clone()); + } + } + } + } + } + Ok(()) + }); + } + for function in &program.functions { + let mut body = function.body.clone(); + let _ = super::resource_materialization::walk_block(&mut body, &mut |expression| { + if let ExpressionKind::Handle { + kind: HandleKind::ReflectedReturn { plan, callable }, + .. + } = &expression.kind + { + if !callable.as_ref().is_some_and(|callable| { + authorized.get(&function.id) == Some(callable) + && callable.function() == function.id + && callable.return_plan() == plan.as_ref() + }) { + errors.push(ValidationError { + span: expression.span, + message: "reflected callable return has no authenticated original getter" + .into(), + }); + } + } + if let ExpressionKind::IndirectCall { + ownership: CallOwnership::Generated(generated), + .. + } = &expression.kind + { + if let GeneratedOperation::ReflectedCallableAdapter { plan, .. } = + &generated.operation + { + if !authorized + .get(&function.id) + .is_some_and(|callable| callable.checked_plan() == plan) + { + errors.push(ValidationError { + span: expression.span, + message: + "reflected callable source invocation has no authenticated adapter" + .into(), + }); + } + } + } + Ok(()) + }); + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } +} + +/// Stage an already admitted, owned reflected callable for ordinary conversion +/// completion. This constructs no callback result and runs no predicate. +pub fn lower_reflected_callable_value( + value: Expression, + admitted: &jett_comptime::value::ReflectedCallableValue, + span: Span, +) -> Result { + let checked = admitted.checked_program().clone(); + let admission = admitted.checked_plan().clone(); + if value.ty != admission.source_type() || !admission.validate(checked.checked()) { + return Err("materialized reflected callable has no exact checked admission".into()); + } + let source = admission.source_type(); + let target = admission.requested_type(); + let placeholder = ReflectedFieldPlan { + source_type: admission.source_return(), + requested_type: admission.requested_return(), + action: ReflectedFieldAction::Exact, + }; + let mut callable = ReflectedCallablePlan::new(checked, admission, placeholder, span, None); + Arc::make_mut(&mut callable.data).materialized = true; + Arc::make_mut(&mut callable.data).reference = Some(Arc::new(admitted.clone())); + Ok(Expression { + kind: ExpressionKind::InterfaceCoerce { + value: Box::new(value), + adapters: vec![InterfaceFunctionAdapter { + source, + target, + function: FunctionId(u32::MAX), + reflected: Some(callable), + }], + }, + ty: target, + span, + }) +} + +fn sources_match( + expected: &[Function], + program: &Program, + checked: &Arc, +) -> bool { + expected.iter().all(|expected| { + program + .functions + .get(expected.id.index() as usize) + .is_some_and(|current| { + let Ok(current) = original_function(current, &program.resource_source, checked) + else { + return false; + }; + let Ok(expected) = original_function(expected, &program.resource_source, checked) + else { + return false; + }; + super::resource_materialization::functions_equal( + std::slice::from_ref(&erase_function(¤t)), + std::slice::from_ref(&erase_function(&expected)), + ) + }) + }) +} + +/// Undo only protected checked replacements. Public edited HIR cannot create a +/// row or renew the seal, and each row must still occupy its exact owner/site. +fn original_function( + function: &Function, + archive: &ResourceSourceArchive, + checked: &Arc, +) -> Result { + if let Some(original) = baked::original_function(function, archive, checked)? { + return Ok(original); + } + let mut original = function.clone(); + for row in archive + .required_materializations() + .iter() + .filter(|row| row.function() == function.id) + { + if !archive.belongs_to(checked) + || !row.proof.belongs_to(checked) + || row.identity() != &function.identity + { + return Err( + "reflected callable required replacement has no original checked owner".into(), + ); + } + let mut matches = 0usize; + super::resource_materialization::walk_block(&mut original.body, &mut |expression| { + if super::resource_materialization::expression_equal(expression, row.current()) { + matches += 1; + *expression = row.original().clone(); + } else if super::resource_materialization::expression_equal(expression, row.original()) + { + matches += 1; + } + Ok(()) + })?; + if matches != 1 { + return Err("reflected callable required replacement moved or was duplicated".into()); + } + } + Ok(original) +} + +fn returned_plan( + checked: &Arc, + functions: &[Function], + node: ReflectedFieldRequirementPlan, +) -> Result { + use ReflectedFieldAction as A; + use ReflectedFieldRequirement as R; + let predicates = |ids: Vec| -> Result, String> { + ids.into_iter() + .map(|refined_type| { + let types = &checked.checked().interner; + let Type::Refinement { name, .. } = types.resolve(refined_type) else { + return Err("materialized return predicate is not a refinement".into()); + }; + let (base_type, _) = super::reflected_fields::refinement_chain(types, refined_type) + .ok_or("invalid materialized return chain")?; + let input_type = match types.resolve(base_type) { + Type::Secret(inner) => *inner, + _ => base_type, + }; + let mut matches = functions.iter().filter(|function| { + function.source_definition.is_some_and(|definition| { + checked.checked().definition_types.get(&definition) == Some(&refined_type) + }) && refinement_predicate_declaration_matches( + &function.identity.declaration, + name, + ) && function.params.len() == 1 + && function.params[0].ty == input_type + && function.params[0].mode == ParamMode::Owned + && function.capture_count == 0 + && function.return_type == TypeInterner::BOOL + }); + let function = matches + .next() + .ok_or("materialized return has no checked predicate declaration")?; + if matches.next().is_some() { + return Err("materialized return predicate context is ambiguous".into()); + } + Ok(RefinementPredicate { + refined_type, + type_name: name.clone(), + function: function.id, + base_type, + input_type, + }) + }) + .collect() + }; + let action = match node.requirement { + R::Exact => A::Exact, + R::Predicates(ids) => A::Predicates(predicates(ids)?), + R::Unsupported(reason) => A::Unsupported(reason), + R::Refine { + base, + predicates: ids, + } => A::Refine { + base: Box::new(returned_plan(checked, functions, *base)?), + predicates: predicates(ids)?, + }, + R::List(child) => A::List(Box::new(returned_plan(checked, functions, *child)?)), + R::Set(child) => A::Set(Box::new(returned_plan(checked, functions, *child)?)), + R::Optional(child) => A::Optional(Box::new(returned_plan(checked, functions, *child)?)), + R::Map { key, value } => A::Map { + key: Box::new(returned_plan(checked, functions, *key)?), + value: Box::new(returned_plan(checked, functions, *value)?), + }, + R::Result { ok, error } => A::Result { + ok: Box::new(returned_plan(checked, functions, *ok)?), + error: Box::new(returned_plan(checked, functions, *error)?), + }, + }; + Ok(ReflectedFieldPlan { + source_type: node.source_type, + requested_type: node.requested_type, + action, + }) +} + +pub fn reflected_callable_plans(function: &Function) -> Vec { + let mut plans = Vec::new(); + let mut body = function.body.clone(); + let _ = super::resource_materialization::walk_block(&mut body, &mut |expression| { + if let ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(fields)), + .. + } = &expression.kind + { + for field in fields { + if let ReflectedFieldAction::CallableReturn(plan) = &field.action { + plans.push(plan.clone()); + } + } + } + if let ExpressionKind::Handle { + kind: + HandleKind::ReflectedReturn { + callable: Some(plan), + .. + }, + .. + } = &expression.kind + { + plans.push(plan.clone()); + } + Ok(()) + }); + plans +} + +#[cfg(test)] +#[path = "reflected_callable_tests.rs"] +mod tests; diff --git a/crates/jett_hir/src/reflected_callable_baked.rs b/crates/jett_hir/src/reflected_callable_baked.rs new file mode 100644 index 00000000..cf7ef3e0 --- /dev/null +++ b/crates/jett_hir/src/reflected_callable_baked.rs @@ -0,0 +1,232 @@ +//! Private checked transition for ordinary required values in an adapter owner. +use super::*; +use jett_comptime::{ExplicitComptimeValues, Value}; + +#[derive(Debug)] +pub(crate) struct Transition { + checked: Arc, + original: Vec, + current: Vec, +} + +impl Transition { + pub(crate) fn current_functions(&self) -> &[Function] { + &self.current + } +} + +fn equal(left: &[Function], right: &[Function]) -> bool { + let left = left.iter().map(erase_function).collect::>(); + let right = right.iter().map(erase_function).collect::>(); + super::super::resource_materialization::functions_equal(&left, &right) +} + +pub fn has_reflected_callable_plans(program: &Program) -> bool { + program + .functions + .iter() + .any(|function| !reflected_callable_plans(function).is_empty()) +} + +/// The callback constructs typed HIR only. Every result, added local and binding +/// is checked against the private required cache before becoming a transition. +pub fn materialize_checked_reflected_values( + program: &mut Program, + values: &ExplicitComptimeValues, + types: &TypeInterner, + mut materialize: impl FnMut( + &Value, + TypeId, + Span, + &mut Vec, + &mut Vec, + ) -> Result, +) -> Result<(), Vec> { + let span = program + .functions + .first() + .map_or(Span::new(FileId::new(0), 0, 0), |function| function.span); + let result = (|| -> Result { + if !has_reflected_callable_plans(program) || !program.resource_manifest.kinds().is_empty() { + return Err( + "reflected required transition has no ordinary checked adapter owner".into(), + ); + } + super::validate_program(program) + .map_err(|errors| format!("invalid original reflected owner: {errors:?}"))?; + program.resource_source.validate_types(types)?; + let checked = program + .resource_source + .checked_program() + .ok_or("reflected required transition lacks original checked Source")? + .clone(); + let original = program + .functions + .iter() + .map(|function| super::original_function(function, &program.resource_source, &checked)) + .collect::, _>>()?; + if !equal(&original, program.resource_source.functions()) { + return Err( + "reflected required transition changed original Source before construction".into(), + ); + } + let proofs = values.checked_required_values(&checked)?; + let mut candidate = program.clone(); + let functions = program.functions.clone(); + for function in &mut candidate.functions { + let owner = function.clone(); + let mut bindings = Vec::new(); + let original_local_count = function.locals.len(); + let mut replacements = Vec::new(); + super::super::resource_materialization::walk_block( + &mut function.body, + &mut |expression| { + if !matches!( + expression.kind, + ExpressionKind::Constant { .. } | ExpressionKind::Comptime { .. } + ) { + return Ok(()); + } + let selected = proofs + .iter() + .filter(|proof| { + super::super::resource_materialization::proof_matches( + proof, &owner, expression, &checked, + ) + }) + .collect::>(); + let [proof] = selected.as_slice() else { + return Err( + "required value lacks a unique original checked owner/context".into(), + ); + }; + let replacement = materialize( + proof.value(), + expression.ty, + expression.span, + &mut function.locals, + &mut bindings, + )?; + if replacement.ty != expression.ty || replacement.span != expression.span { + return Err("required replacement changed its original typed site".into()); + } + replacements.push(((*proof).clone(), replacement.clone())); + *expression = replacement; + Ok(()) + }, + )?; + if function.locals.get(..original_local_count) != Some(owner.locals.as_slice()) { + return Err("required materializer changed an original local".into()); + } + function.body.statements.splice(0..0, bindings.clone()); + let used = super::materialized_source::validate_required_values( + &checked, + &candidate.resource_source, + function, + &functions, + &replacements, + )?; + let added = &function.locals[original_local_count..]; + if added.len() != bindings.len() || used.len() != added.len() + || added.iter().enumerate().any(|(offset,local)| { + local.id.index() as usize != original_local_count + offset + || local.mutable || local.view_source.is_some() || local.debug_ty != local.ty + || local.debug_type_name.is_some() || !used.contains(&local.id) + }) + || bindings.iter().any(|statement| !matches!(statement.kind, StatementKind::Let { local, .. } if used.contains(&local))) + { return Err("required materializer added an unauthenticated local or statement".into()); } + } + candidate.resource_source.reflected_values = Some(Arc::new(Transition { + checked, + original, + current: candidate.functions.clone(), + })); + Ok(candidate) + })(); + match result { + Ok(candidate) => { + *program = candidate; + Ok(()) + } + Err(message) => Err(vec![LowerError { span, message }]), + } +} + +pub(super) fn original_function( + function: &Function, + archive: &ResourceSourceArchive, + checked: &Arc, +) -> Result, String> { + let Some(transition) = &archive.reflected_values else { + return Ok(None); + }; + if !archive.belongs_to(checked) || !Arc::ptr_eq(&transition.checked, checked) { + return Err("ordinary reflected materialization belongs to another checked Source".into()); + } + let Some(current) = transition.current.get(function.id.index() as usize) else { + return Ok(None); + }; + if !equal( + std::slice::from_ref(function), + std::slice::from_ref(current), + ) { + return Ok(None); + } + Ok(transition + .original + .get(function.id.index() as usize) + .cloned()) +} + +pub(crate) fn validate_conversion_input(program: &Program) -> Result<(), Vec> { + let Some(transition) = &program.resource_source.reflected_values else { + return Ok(()); + }; + if !program.resource_source.belongs_to(&transition.checked) + || !equal(&program.functions, &transition.current) + { + return Err(vec![LowerError { + span: program + .functions + .first() + .map_or(Span::new(FileId::new(0), 0, 0), |function| function.span), + message: "reflected conversion changed its protected required-value input".into(), + }]); + } + Ok(()) +} + +/// Called only around the ordinary canonical conversion pass after its gate. +pub(crate) fn complete_conversion( + program: &mut Program, + before: &Program, +) -> Result<(), Vec> { + validate_conversion_input(before)?; + let Some(transition) = &before.resource_source.reflected_values else { + return Ok(()); + }; + if program.resource_source != before.resource_source { + return Err(vec![LowerError { + span: before.functions[0].span, + message: "reflected conversion replaced original archive".into(), + }]); + } + program.resource_source.reflected_values = Some(Arc::new(Transition { + checked: transition.checked.clone(), + original: transition.original.clone(), + current: program.functions.clone(), + })); + Ok(()) +} + +pub(super) fn owns_materialized_owner(archive: &ResourceSourceArchive, owner: &Function) -> bool { + archive.reflected_values.as_ref().is_some_and(|transition| { + archive.belongs_to(&transition.checked) + && transition + .current + .get(owner.id.index() as usize) + .is_some_and(|current| { + equal(std::slice::from_ref(current), std::slice::from_ref(owner)) + }) + }) +} diff --git a/crates/jett_hir/src/reflected_callable_materialized.rs b/crates/jett_hir/src/reflected_callable_materialized.rs new file mode 100644 index 00000000..23cb0a70 --- /dev/null +++ b/crates/jett_hir/src/reflected_callable_materialized.rs @@ -0,0 +1,485 @@ +//! Match materialized descriptors and capture values without invoking Source. +use super::*; +use jett_comptime::Value; +use std::cell::RefCell; + +pub(super) fn validate( + reference: &jett_comptime::value::ReflectedCallableValue, + site: &Expression, + owner: &Function, + functions: &[Function], + archive: &ResourceSourceArchive, +) -> Result<(), String> { + let ExpressionKind::FunctionAdapter { value, .. } = &site.kind else { + return Err("materialized reflected callable has no source descriptor".into()); + }; + if !archive.belongs_to(reference.checked_program()) || value.ty != reference.source_type() { + return Err( + "materialized reflected callable source belongs to another checked context".into(), + ); + } + let context = Context { + owner, + functions, + archive, + checked: reference.checked_program(), + used: RefCell::new(HashSet::new()), + }; + if !context.matches( + reference.source(), + value, + context.checked.checked().interner.len(), + ) { + return Err("materialized source callback, captures or pending layers differ from the admitted read".into()); + } + let mut descriptor = value.as_ref(); + while let ExpressionKind::Run(inner) = &descriptor.kind { + descriptor = inner; + } + let id = match &descriptor.kind { + ExpressionKind::FunctionRef(id) | ExpressionKind::ClosureRef { function: id, .. } => { + Some(*id) + } + ExpressionKind::FunctionAdapter { .. } => None, + _ => return Err("materialized callback lost its original descriptor".into()), + }; + if let Some(id) = id { + let original = archive + .functions() + .get(id.index() as usize) + .ok_or("materialized callback target is absent")?; + if (reference.source_definition().is_some() + && original.source_definition != reference.source_definition()) + || reference + .source_body() + .is_some_and(|body| original.body.span != body.span) + { + return Err("materialized callback selects another original source declaration".into()); + } + } + Ok(()) +} + +struct Context<'a> { + owner: &'a Function, + functions: &'a [Function], + archive: &'a ResourceSourceArchive, + checked: &'a Arc, + used: RefCell>, +} + +pub(super) fn validate_required_values( + checked: &Arc, + archive: &ResourceSourceArchive, + owner: &Function, + functions: &[Function], + replacements: &[(jett_comptime::CheckedRequiredValue, Expression)], +) -> Result, String> { + let context = Context { + owner, + functions, + archive, + checked, + used: RefCell::new(HashSet::new()), + }; + for (proof, current) in replacements { + if !proof.belongs_to(checked) + || !context.matches(proof.value(), current, checked.checked().interner.len()) + { + return Err( + "ordinary required replacement differs from its opaque checked value".into(), + ); + } + } + Ok(context.used.into_inner()) +} + +impl Context<'_> { + fn target(&self, id: FunctionId, ty: TypeId) -> Option<&Function> { + let current = self.functions.get(id.index() as usize)?; + let original = self.archive.functions().get(id.index() as usize)?; + let current = original_function(current, self.archive, self.checked).ok()?; + if !super::resource_materialization::functions_equal( + std::slice::from_ref(¤t), + std::slice::from_ref(original), + ) { + return None; + } + let signature = Type::Function { + params: original.params[original.capture_count..] + .iter() + .map(|param| param.ty) + .collect(), + view_params: original.params[original.capture_count..] + .iter() + .map(|param| param.mode == ParamMode::View) + .collect(), + return_type: original.return_type, + }; + (self.checked.checked().interner.resolve(ty) == &signature).then_some(original) + } + + fn binding(&self, local: LocalId) -> Option { + let metadata = self.owner.locals.get(local.index() as usize)?; + if metadata.id != local || metadata.mutable || metadata.view_source.is_some() { + return None; + } + let mut values = Vec::new(); + bindings(&self.owner.body, local, &mut values); + let [value] = values.as_slice() else { + return None; + }; + self.used.borrow_mut().insert(local); + (value.ty == metadata.ty).then(|| value.clone()) + } + + fn matches(&self, value: &Value, expression: &Expression, budget: usize) -> bool { + if budget == 0 || expression.ty.index() as usize >= self.checked.checked().interner.len() { + return false; + } + let child = + |value: &Value, expression: &Expression| self.matches(value, expression, budget - 1); + let types = &self.checked.checked().interner; + use ExpressionKind as E; + if let E::Clone(inner) = &expression.kind { + return child(value, inner); + } + if let E::Local(local) = &expression.kind { + return self + .binding(*local) + .is_some_and(|binding| child(value, &binding)); + } + if let Value::Typed { type_name, value } = value { + if let E::RefinementValidated(inner) = &expression.kind { + return types.type_name(expression.ty) == *type_name && child(value, inner); + } + return child(value, expression); + } + if let Value::Pending(value) = value { + return matches!(&expression.kind, E::Run(inner) if inner.ty == expression.ty && child(value, inner)); + } + if let E::Run(_) = &expression.kind { + return false; + } + if let E::RefinementValidated(inner) = &expression.kind { + return matches!(types.resolve(expression.ty), Type::Refinement { base, .. } if *base == inner.ty) + && child(value, inner); + } + if let E::InterfaceCoerce { + value: inner, + adapters, + } = &expression.kind + { + if let Value::ReflectedCallable(admitted) = value { + return adapters.len() == 1 + && adapters[0].reflected.as_ref().is_some_and(|plan| { + plan.is_materialized() + && plan.checked_plan() == admitted.checked_plan() + && Arc::ptr_eq(&plan.data.checked, admitted.checked_program()) + && plan.data.reference.is_some() + && expression.ty == admitted.requested_type() + && inner.ty == admitted.source_type() + && child(admitted.source(), inner) + }); + } + return adapters.is_empty() + && child(value, inner) + && self + .checked + .checked() + .ordinary_types_compatible(expression.ty, inner.ty); + } + match (value, &expression.kind) { + (Value::Int64(a), E::Int(b)) => i128::from(*a) == *b, + (Value::Uint64(a), E::Int(b)) => i128::from(*a) == *b, + (Value::Float64(a), E::Float(b)) => a.to_bits() == b.to_bits(), + (Value::String(a), E::String(b)) => a == b, + (Value::Bool(a), E::Bool(b)) => a == b, + (Value::Nothing, E::Nothing) | (Value::OptionalNone, E::OptionalNone) => true, + (Value::OptionalSome(a), E::OptionalSome(b)) + | (Value::ResultOk(a), E::ResultOk(b)) + | (Value::ResultFail(a), E::ResultFail(b)) => child(a, b), + (Value::List(a), E::ListConstruct { elements: b }) => { + a.len() == b.len() && a.iter().zip(b).all(|(a, b)| child(a, b)) + } + (Value::Map(a), E::MapConstruct { entries: b }) => { + a.len() == b.len() + && a.iter() + .zip(b) + .all(|((ak, av), b)| child(ak, &b.key) && child(av, &b.value)) + } + ( + Value::Struct { + type_name, + concrete_type, + fields, + }, + E::StructConstruct { + struct_type, + fields: current, + evaluation_order, + validates_refinements, + refinement_predicates, + }, + ) => { + let Type::Struct(id) = types.resolve(*struct_type) else { + return false; + }; + let definition = types.resolve_struct(*id); + *struct_type == expression.ty + && !validates_refinements + && refinement_predicates.is_empty() + && concrete_type.as_ref().unwrap_or(type_name) == &types.type_name(*struct_type) + && *evaluation_order == (0..current.len()).collect::>() + && fields.len() == current.len() + && definition.fields.len() == current.len() + && definition + .fields + .iter() + .zip(current) + .all(|((name, ty), current)| { + current.ty == *ty + && fields + .iter() + .find(|(field, _)| field == name) + .is_some_and(|(_, value)| child(value, current)) + }) + } + ( + Value::Enum { + type_name, + variant, + fields, + }, + E::EnumConstruct { + enum_type, + variant: current, + payloads, + evaluation_order, + }, + ) => { + let Type::Enum(id) = types.resolve(*enum_type) else { + return false; + }; + let Some(definition) = types + .resolve_enum(*id) + .variants + .get(current.index() as usize) + else { + return false; + }; + *enum_type == expression.ty + && *type_name == types.type_name(*enum_type) + && definition.name == *variant + && fields.len() == payloads.len() + && definition.fields.len() == payloads.len() + && *evaluation_order == (0..payloads.len()).collect::>() + && fields + .iter() + .zip(payloads) + .zip(&definition.fields) + .all(|((a, b), (_, ty))| b.ty == *ty && child(a, b)) + } + (Value::NamedFunction(name), E::FunctionRef(id)) => { + let Some(original) = self.target(*id, expression.ty) else { + return false; + }; + let Some(definition) = original.source_definition else { + return false; + }; + let info = self.checked.resolved().scope_table.def(definition); + original.capture_count == 0 + && info.kind == jett_resolve::DefKind::Function + && info.name == *name + } + (Value::Function { body, captures, .. }, E::FunctionRef(id)) + | (Value::Function { body, captures, .. }, E::ClosureRef { function: id, .. }) => { + let Some(original) = self.target(*id, expression.ty) else { + return false; + }; + let locals: &[LocalId] = match &expression.kind { + E::ClosureRef { captures, .. } => captures, + _ => &[], + }; + original.source_definition.is_none() + && original.body.span == body.span + && original.capture_count == locals.len() + && original.params[..original.capture_count] + .iter() + .zip(locals) + .all(|(param, local)| { + self.binding(*local).is_some_and(|current| { + current.ty == param.ty + && captures + .get(¶m.name) + .is_some_and(|value| child(value, ¤t)) + }) + }) + } + (Value::ReflectedCallable(admitted), E::FunctionAdapter { function, .. }) => { + let Some(adapter) = self.functions.get(function.index() as usize) else { + return false; + }; + let Some(Statement { + kind: + StatementKind::Return(Some(Expression { + kind: + E::Handle { + kind: + HandleKind::ReflectedReturn { + callable: Some(plan), + .. + }, + .. + }, + .. + })), + .. + }) = adapter.body.statements.first() + else { + return false; + }; + plan.is_materialized() + && plan.checked_plan() == admitted.checked_plan() + && Arc::ptr_eq(&plan.data.checked, admitted.checked_program()) + && expression.ty == admitted.requested_type() + && validate( + admitted, + expression, + self.owner, + self.functions, + self.archive, + ) + .is_ok() + } + _ => false, + } + } +} + +fn bindings(block: &Block, local: LocalId, values: &mut Vec) { + for statement in &block.statements { + match &statement.kind { + StatementKind::Let { + local: current, + value, + } if *current == local => values.push(value.clone()), + StatementKind::Assign { + target: + Expression { + kind: ExpressionKind::Local(current), + .. + }, + value, + } if *current == local => values.push(value.clone()), + StatementKind::If { + then_block, + else_block, + .. + } => { + bindings(then_block, local, values); + if let Some(block) = else_block { + bindings(block, local, values); + } + } + StatementKind::For { body, .. } + | StatementKind::While { body, .. } + | StatementKind::Scope(body) => bindings(body, local, values), + StatementKind::Match { arms, .. } => { + for arm in arms { + bindings(&arm.body, local, values); + } + } + StatementKind::ReflectedTypeDispatch { arms, .. } => { + for arm in arms { + bindings(&arm.body, local, values); + } + } + _ => {} + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn materialized_values_preserve_nominal_identity_and_every_pending_layer() { + let file = FileId::new(0); + let parsed = jett_parser::parse( + "namespace app\nstruct First:\n raw: int64\nstruct Second:\n raw: int64\nfunction context() returns nothing:\n return nothing\n", + file, + ); + assert!(parsed.errors.is_empty()); + let checked = Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + parsed, + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions { release: false }, + ) + .unwrap(), + ); + let program = lower_checked_resource_program(&checked).unwrap(); + let owner = &program.functions[0]; + let context = Context { + owner, + functions: &program.functions, + archive: &program.resource_source, + checked: &checked, + used: RefCell::new(HashSet::new()), + }; + let types = &checked.checked().interner; + let first = types + .type_ids() + .find(|ty| types.type_name(*ty) == "app.First") + .unwrap(); + let second = types + .type_ids() + .find(|ty| types.type_name(*ty) == "app.Second") + .unwrap(); + let span = owner.span; + let primitive = Expression { + kind: ExpressionKind::Int(5), + ty: TypeInterner::INT64, + span, + }; + let construct = |ty| Expression { + kind: ExpressionKind::StructConstruct { + struct_type: ty, + fields: vec![primitive.clone()], + evaluation_order: vec![0], + validates_refinements: false, + refinement_predicates: Vec::new(), + }, + ty, + span, + }; + let retained = Value::Struct { + type_name: "app.First".into(), + concrete_type: None, + fields: vec![("raw".into(), Value::Int64(5))], + }; + assert!(context.matches(&retained, &construct(first), types.len())); + assert!( + !context.matches(&retained, &construct(second), types.len()), + "same-layout nominal substitution" + ); + let run = |value| Expression { + kind: ExpressionKind::Run(Box::new(value)), + ty: TypeInterner::INT64, + span, + }; + let pending = Value::Pending(Box::new(Value::Pending(Box::new(Value::Int64(5))))); + assert!(context.matches(&pending, &run(run(primitive.clone())), types.len())); + assert!( + !context.matches(&pending, &run(primitive.clone()), types.len()), + "pending layer removed" + ); + assert!( + !context.matches(&pending, &run(run(run(primitive))), types.len()), + "pending layer added" + ); + } +} diff --git a/crates/jett_hir/src/reflected_callable_tests.rs b/crates/jett_hir/src/reflected_callable_tests.rs new file mode 100644 index 00000000..53b3778f --- /dev/null +++ b/crates/jett_hir/src/reflected_callable_tests.rs @@ -0,0 +1,300 @@ +use super::*; + +const SOURCE: &str = "namespace app\ntype Positive = int64 where value > 0\nfunction five() returns int64:\n return 5\nfunction zero() returns int64:\n return 0\nstruct Holder:\n first: function() returns int64\n second: function() returns int64\nfunction inspect(view holder: Holder, view field: TypeField) returns function() returns Positive:\n return type.field_value[Holder, function() returns Positive](view holder, view field)\n"; + +fn checked(source: &str) -> Arc { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + parsed, + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions { release: false }, + ) + .unwrap(), + ) +} + +fn fixture() -> (Program, Arc) { + let checked = checked(SOURCE); + let hir = + lower_checked_ordinary_program(&checked).expect("ordinary checked reflected lowering"); + (hir, checked) +} + +#[test] +fn ordinary_checked_constants_and_raw_refinements_have_no_reflected_source_archive() { + let checked = checked( + "namespace app\ntype Positive = int64 where value > 0\nfunction positive() returns Positive:\n return 5\nPositive cached = comptime positive()\nfunction main() returns nothing:\n int64 ordinary = comptime 7\n println(coarsen cached + ordinary)\n return nothing\n", + ); + let program = lower_checked_ordinary_program(&checked).expect("ordinary checked constants"); + assert!(!has_reflected_callable_plans(&program)); + assert!(program.resource_source.manifest().is_none()); + assert!(program.resource_source.functions().is_empty()); + assert!(program.resource_manifest.kinds().is_empty()); + assert!(program.resource_manifest.hooks().next().is_none()); + let unsealed = lower( + checked.module(), + checked.resolved(), + checked.checked(), + checked.source_origins(), + ) + .expect("existing ordinary lowering"); + assert!(super::super::resource_materialization::functions_equal( + &program.functions, + &unsealed.functions, + )); +} + +#[test] +fn checked_read_is_lazy_and_each_ordinal_owns_an_original_adapter() { + let (program, checked) = fixture(); + assert!(program.resource_source.belongs_to(&checked)); + assert!(program.resource_manifest.kinds().is_empty()); + assert!(program.resource_manifest.hooks().next().is_none()); + validate(&program).unwrap(); + validate_backend_types(&program, &checked.checked().interner).unwrap(); + let owner = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let plans = reflected_callable_plans(owner); + assert_eq!(plans.len(), 2); + assert_ne!(plans[0].function(), plans[1].function()); + for plan in plans { + assert!(plan.validate_original()); + assert_eq!(plan.original_owner(), Some(owner.id)); + let adapter = &program.functions[plan.function().index() as usize]; + assert_eq!(adapter.capture_count, 1); + assert_eq!(adapter.params[0].ty, plan.source_type()); + let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::Handle { + target, + kind: + HandleKind::ReflectedReturn { + callable: Some(root), + .. + }, + .. + }, + .. + })) = &adapter.body.statements[0].kind + else { + panic!("canonical return root") + }; + assert_eq!(root, &plan); + let ExpressionKind::IndirectCall { + callee, + args, + evaluation_order, + ownership: CallOwnership::Generated(generated), + } = &target.kind + else { + panic!("one original source invocation") + }; + assert_eq!(target.ty, plan.checked_plan().source_return()); + assert!(matches!(callee.kind, ExpressionKind::Local(LocalId(0)))); + assert!(args.is_empty() && evaluation_order.is_empty()); + assert!( + matches!(&generated.operation,GeneratedOperation::ReflectedCallableAdapter {plan:admission,..} if admission == plan.checked_plan()) + ); + } + let mut owner = owner.clone(); + let mut read_calls = 0; + super::super::resource_materialization::walk_block(&mut owner.body, &mut |value| { + if matches!( + value.kind, + ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(_)), + .. + } + ) { + read_calls += 1; + } + assert!(!matches!( + value.kind, + ExpressionKind::Handle { + kind: HandleKind::ReflectedReturn { .. }, + .. + } + )); + Ok(()) + }) + .unwrap(); + assert_eq!( + read_calls, 1, + "read constructs descriptors; invocation owns validation" + ); +} + +#[test] +fn same_signature_source_body_and_ordinal_and_root_substitution_are_rejected() { + let (original, checked) = fixture(); + for mutation in 0..5 { + let mut program = original.clone(); + let owner_index = program + .functions + .iter() + .position(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let plans = reflected_callable_plans(&program.functions[owner_index]); + match mutation { + 0 => { + let function = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "five") + .unwrap(); + let StatementKind::Return(Some(value)) = &mut function.body.statements[0].kind + else { + panic!("source return") + }; + value.kind = ExpressionKind::Int(0); + } + 1 => { + super::super::resource_materialization::walk_block( + &mut program.functions[owner_index].body, + &mut |value| { + if let ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(plans)), + .. + } = &mut value.kind + { + plans.swap(0, 1); + } + Ok(()) + }, + ) + .unwrap(); + } + 2 => { + let adapter = &mut program.functions[plans[0].function().index() as usize]; + let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::Handle { + kind: HandleKind::ReflectedReturn { callable, .. }, + .. + }, + .. + })) = &mut adapter.body.statements[0].kind + else { + panic!("return root") + }; + *callable = Some(plans[1].clone()); + } + 3 => { + let predicate = plans[0].original_predicates()[0].id; + let StatementKind::Return(Some(value)) = &mut program.functions + [predicate.index() as usize] + .body + .statements[0] + .kind + else { + panic!("predicate return") + }; + value.kind = ExpressionKind::Bool(true); + } + _ => { + program.functions[owner_index] + .identity + .scoped_type_bindings + .clear(); + program.functions[owner_index] + .identity + .declaration + .namespace = "other".into(); + } + } + assert!(validate(&program).is_err(), "forgery {mutation}"); + assert!( + complete_value_conversions(&mut program, &checked.checked().interner).is_err(), + "forgery cannot reseal {mutation}" + ); + } +} + +#[test] +fn checked_plan_rejects_view_parameter_and_carrier_strengthening() { + let checked = checked( + "namespace app\ntype Positive = int64 where value > 0\ntype Text = string where value != \"\"\nstruct Holder:\n callback: function(int64) returns int64\n validated: function(int64) returns Positive\n refined_parameter: function(Positive) returns Positive\n view_parameter: function(view int64) returns Positive\n foreign_return: function(int64) returns Text\nfunction inspect(view holder: Holder, view field: TypeField) returns function(int64) returns Positive:\n return type.field_value[Holder, function(int64) returns Positive](view holder, view field)\nfunction unrelated(value: Text) returns nothing:\n return nothing\n", + ); + let types = &checked.checked().interner; + let positive = types + .type_ids() + .find( + |ty| matches!(types.resolve(*ty), Type::Refinement {name,..} if name == "app.Positive"), + ) + .unwrap(); + let text = types + .type_ids() + .find(|ty| matches!(types.resolve(*ty), Type::Refinement {name,..} if name == "app.Text")) + .unwrap(); + let function_type = |parameter, view, returned| { + types + .type_ids() + .find(|ty| { + matches!(types.resolve(*ty), Type::Function {params,view_params,return_type} + if params == &[parameter] && view_params == &[view] && *return_type == returned) + }) + .unwrap() + }; + let actual = function_type(TypeInterner::INT64, false, TypeInterner::INT64); + let requested = function_type(TypeInterner::INT64, false, positive); + let refined_parameter = function_type(positive, false, positive); + let view_parameter = function_type(TypeInterner::INT64, true, positive); + let foreign_return = function_type(TypeInterner::INT64, false, text); + assert!( + checked + .checked() + .reflected_callable_plan(actual, requested) + .is_some() + ); + assert!( + checked + .checked() + .reflected_callable_plan(requested, requested) + .is_some(), + "the exact callable contract remains admitted" + ); + assert!( + checked + .checked() + .reflected_callable_plan(actual, refined_parameter) + .is_none(), + "a refined caller parameter is not implicitly coarsened to int64" + ); + assert!( + checked + .checked() + .reflected_callable_plan(refined_parameter, actual) + .is_none(), + "an int64 caller parameter cannot implicitly establish Positive" + ); + assert!( + checked + .checked() + .reflected_callable_plan(actual, view_parameter) + .is_none(), + "owned and view parameter contracts must match" + ); + assert!( + checked + .checked() + .reflected_callable_plan(view_parameter, requested) + .is_none(), + "view mismatch is refused in both directions" + ); + assert!( + checked + .checked() + .reflected_callable_plan(actual, foreign_return) + .is_none(), + "an int64 result cannot establish a string-backed refinement" + ); + let (program, _) = fixture(); + assert!(has_reflected_callable_plans(&program)); +} diff --git a/crates/jett_hir/src/reflected_field_sources.rs b/crates/jett_hir/src/reflected_field_sources.rs new file mode 100644 index 00000000..973c64af --- /dev/null +++ b/crates/jett_hir/src/reflected_field_sources.rs @@ -0,0 +1,910 @@ +//! Constructor-only correspondence for direct original nominal struct field loops. +//! Metadata is runtime data; ordinal-specific checked bodies remain Source authority. +use crate::{ + Block, DeclarationKind, Expression, ExpressionKind as E, FieldId, Function, FunctionId, + LocalId, Program, ReflectedTypeArm, Statement, StatementKind as S, +}; +use jett_common::Span; +use jett_intrinsics::IntrinsicId; +use jett_parser::ast::{self, Expr, Item, Stmt, TypeExpr}; +use jett_resolve::{DefId, DefKind, ResolveResult}; +use jett_typecheck::{ + CheckResult, CheckedBodyFacts, CheckedComptimeTypeSelection, CheckedGenericSpecialization, + CheckedResourceProgram, +}; +use jett_types::{ReflectionFieldInfo, ReflectionTypeInfo, Type, TypeId, TypeInterner}; +use std::sync::Arc; + +#[derive(Debug)] +struct SelectedBody { + checked_index: usize, + address: usize, + bound_type: TypeId, + reflection: ReflectionTypeInfo, + // The constructor retains the full original checked expansion. No public + // constructor, setter, or caller-supplied fact map can substitute this body. + _facts: CheckedBodyFacts, +} + +#[derive(Debug)] +struct Dispatch { + checked: Arc, + function: FunctionId, + declaration: DefId, + function_span: Span, + loop_address: usize, + binding_address: usize, + loop_span: Span, + binding_span: Span, + binder: DefId, + owner_definition: DefId, + owner: TypeId, + owner_reflection: ReflectionTypeInfo, + fields: Vec<(TypeId, ReflectionFieldInfo)>, + selected: Vec, + key: LocalId, + field_index: FieldId, + type_info_field: FieldId, + loop_path: Vec, + dispatch_path: Vec, + containers: Vec>, + scopes: Vec>, + type_info: Expression, + arms: Vec, +} + +/// Immutable original Source authority; only successful checked HIR lowering +/// can create it. Clones retain the same checked session and exact body record. +#[derive(Debug, Clone)] +pub struct ReflectedFieldDispatchProof { + data: Arc, +} +impl PartialEq for ReflectedFieldDispatchProof { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.data, &other.data) + } +} +impl Eq for ReflectedFieldDispatchProof {} + +impl ReflectedFieldDispatchProof { + pub fn function(&self) -> FunctionId { + self.data.function + } + pub fn key(&self) -> LocalId { + self.data.key + } + pub fn owner(&self) -> TypeId { + self.data.owner + } + pub fn owner_reflection(&self) -> &ReflectionTypeInfo { + &self.data.owner_reflection + } + pub fn fields(&self) -> &[(TypeId, ReflectionFieldInfo)] { + &self.data.fields + } + pub fn type_info(&self) -> &Expression { + &self.data.type_info + } + pub fn arms(&self) -> &[ReflectedTypeArm] { + &self.data.arms + } + pub fn field_index(&self) -> FieldId { + self.data.field_index + } + pub fn type_info_field(&self) -> FieldId { + self.data.type_info_field + } + pub fn loop_path(&self) -> &[usize] { + &self.data.loop_path + } + pub fn dispatch_path(&self) -> &[usize] { + &self.data.dispatch_path + } + pub fn arm_container_path(&self, index: usize) -> Option<&[usize]> { + self.data.containers.get(index).map(Vec::as_slice) + } + /// The explicit Scope body path in the original lexical inventory. + pub fn arm_path(&self, index: usize) -> Option<&[usize]> { + self.data.scopes.get(index).map(Vec::as_slice) + } + pub fn iteration_index(&self, index: usize) -> Option { + self.data.arms.get(index).map(|arm| arm.iteration_index) + } + pub(crate) fn belongs_to(&self, checked: &Arc) -> bool { + Arc::ptr_eq(&self.data.checked, checked) + } + pub(crate) fn matches( + &self, + functions: &[Function], + type_info: &Expression, + arms: &[ReflectedTypeArm], + ) -> bool { + if !self.validate_original() { + return false; + } + let Some(original) = functions + .iter() + .find(|function| function.id == self.function()) + else { + return false; + }; + let mut candidate = original.clone(); + let Some(statement) = root_dispatch_mut(&mut candidate.body, &self.data.dispatch_path) + else { + return false; + }; + statement.kind = S::ReflectedTypeDispatch { + type_info: type_info.clone(), + arms: arms.to_vec(), + }; + // The existing archive comparison preserves float bits, including NaN. + crate::resource_materialization::functions_equal( + std::slice::from_ref(original), + std::slice::from_ref(&candidate), + ) + } + pub(crate) fn validate_original(&self) -> bool { + let data = &self.data; + let Some(source) = original_function(&data.checked, data.declaration, data.function_span) + else { + return false; + }; + let loops = source + .body + .stmts + .iter() + .filter_map(|statement| match statement { + Stmt::For(source) if source.span == data.loop_span => Some(source), + _ => None, + }) + .collect::>(); + let [source_loop] = loops.as_slice() else { + return false; + }; + if std::ptr::from_ref(*source_loop).addr() != data.loop_address { + return false; + } + let bindings = source_loop + .body + .stmts + .iter() + .filter_map(|statement| match statement { + Stmt::ComptimeTypeBind(source) if source.span == data.binding_span => Some(source), + _ => None, + }) + .collect::>(); + let [binding] = bindings.as_slice() else { + return false; + }; + if std::ptr::from_ref(*binding).addr() != data.binding_address + || direct_owner(&data.checked, source_loop) + != Some(( + data.owner_definition, + data.owner, + data.owner_reflection.clone(), + )) + || binder(&data.checked, source_loop) != Some(data.binder) + || checked_fields(&data.checked, data.owner).as_ref() != Some(&data.fields) + || !binding_selects_binder(&data.checked, binding, data.binder) + { + return false; + } + let Some(rows) = data + .checked + .checked() + .comptime_type_bindings + .get(&binding.span) + else { + return false; + }; + if rows.len() != data.selected.len() { + return false; + } + data.selected.iter().enumerate().all(|(ordinal, selected)| { + let Some(row) = rows.get(selected.checked_index) else { + return false; + }; + std::ptr::from_ref(&row.body).addr() == selected.address + && row.selection == CheckedComptimeTypeSelection::ReflectedIteration(ordinal) + && row.bound_type == selected.bound_type + && row.reflection == selected.reflection + }) + } +} + +pub(crate) fn capture( + program: &Program, + checked: &Arc, +) -> Vec { + let mut proofs = Vec::new(); + for function in &program.functions { + if function.identity.declaration.kind != DeclarationKind::Function + || !function.identity.type_arguments.is_empty() + || !function.identity.scoped_type_bindings.is_empty() + || function.identity.specialization != CheckedGenericSpecialization::default() + || function.capture_count != 0 + { + continue; + } + let Some(declaration) = function.source_definition else { + continue; + }; + let Some(source) = original_function(checked, declaration, function.span) else { + continue; + }; + let Some(definition) = checked + .resolved() + .scope_table + .definitions + .get(declaration.index() as usize) + else { + continue; + }; + let namespace = definition.namespace.as_deref().unwrap_or(""); + let canonical_name = if namespace.is_empty() { + source.name.name.clone() + } else { + format!("{namespace}.{}", source.name.name) + }; + if function.identity.declaration.name != source.name.name + || definition.name != canonical_name + || function.identity.declaration.namespace + != definition.namespace.as_deref().unwrap_or("") + || checked.source_origins().get(&source.span.file) + != Some(&function.identity.declaration.origin) + { + continue; + } + // Root body facts are the only admitted producer context. Nested or + // generic reflected expansions need their own checked scope cursor. + for statement in &source.body.stmts { + let Stmt::For(source_loop) = statement else { + continue; + }; + let Some((owner_definition, owner, owner_reflection)) = + direct_owner(checked, source_loop) + else { + continue; + }; + let Some(fields) = checked_fields(checked, owner) else { + continue; + }; + let Some(binder) = binder(checked, source_loop) else { + continue; + }; + let loops = function + .body + .statements + .iter() + .enumerate() + .filter(|(_, statement)| { + statement.span == source_loop.span && matches!(statement.kind, S::For { .. }) + }) + .collect::>(); + let [(loop_index, loop_statement)] = loops.as_slice() else { + continue; + }; + let S::For { + key, + value: None, + by_view: false, + iterable, + body, + } = &loop_statement.kind + else { + continue; + }; + let Some(local) = function.locals.get(key.index() as usize) else { + continue; + }; + if local.id != *key + || local.span != source_loop.variable.span + || local.mutable + || local.name != source_loop.variable.name + || local.view_source.is_some() + || checked.checked().definition_types.get(&binder) != Some(&local.ty) + || !metadata_list_matches( + iterable, + local.ty, + owner_reflection.type_name.as_str(), + &fields, + checked, + ) + { + continue; + } + for statement in &source_loop.body.stmts { + let Stmt::ComptimeTypeBind(binding) = statement else { + continue; + }; + if !binding_selects_binder(checked, binding, binder) { + continue; + } + if let Some(proof) = capture_dispatch( + checked, + function, + declaration, + source_loop, + binding, + *loop_index, + body, + *key, + binder, + owner_definition, + owner, + &owner_reflection, + &fields, + ) { + proofs.push(proof); + } + } + } + } + proofs +} + +#[allow(clippy::too_many_arguments)] +fn capture_dispatch( + checked: &Arc, + function: &Function, + declaration: DefId, + source_loop: &ast::ForStmt, + binding: &ast::ComptimeTypeBindStmt, + loop_index: usize, + body: &Block, + key: LocalId, + binder: DefId, + owner_definition: DefId, + owner: TypeId, + owner_reflection: &ReflectionTypeInfo, + fields: &[(TypeId, ReflectionFieldInfo)], +) -> Option { + let dispatches = body + .statements + .iter() + .enumerate() + .filter(|(_, statement)| { + statement.span == binding.span + && matches!(statement.kind, S::ReflectedTypeDispatch { .. }) + }) + .collect::>(); + let [(dispatch_index, statement)] = dispatches.as_slice() else { + return None; + }; + let S::ReflectedTypeDispatch { type_info, arms } = &statement.kind else { + return None; + }; + let local = function.locals.get(key.index() as usize)?; + let E::Field { + base, + owner_type, + field, + } = &type_info.kind + else { + return None; + }; + let Type::Struct(field_struct) = checked.checked().interner.resolve(local.ty) else { + return None; + }; + let field_header = checked.checked().interner.resolve_struct(*field_struct); + if *field != FieldId::new(9) + || *owner_type != local.ty + || field_header.fields.get(9).map(|(_, ty)| *ty) != Some(type_info.ty) + || !matches!(base.kind, E::Local(found) if found == key) + || base.ty != local.ty + || type_info.span != binding.value.span() + { + return None; + } + let rows = checked + .checked() + .comptime_type_bindings + .get(&binding.span)?; + if arms.len() != fields.len() || rows.len() != fields.len() || fields.is_empty() { + return None; + } + let mut selected = Vec::with_capacity(fields.len()); + let mut containers = Vec::with_capacity(fields.len()); + let mut scopes = Vec::with_capacity(fields.len()); + let loop_path = vec![0, loop_index]; + let dispatch_path = vec![0, loop_index, 2, 0, 0, *dispatch_index]; + for (ordinal, ((bound_type, metadata), arm)) in fields.iter().zip(arms).enumerate() { + let candidates = rows + .iter() + .enumerate() + .filter(|(_, row)| { + row.selection == CheckedComptimeTypeSelection::ReflectedIteration(ordinal) + }) + .collect::>(); + let [(checked_index, row)] = candidates.as_slice() else { + return None; + }; + let [wrapper] = arm.body.statements.as_slice() else { + return None; + }; + let S::Scope(original_body) = &wrapper.kind else { + return None; + }; + if row.bound_type != *bound_type + || row.reflection != metadata.type_info + || arm.iteration_index != ordinal + || arm.bound_type != *bound_type + || arm.reflection_identity != metadata.type_info.reflection_identity() + || wrapper.span != binding.span + || original_body.span != binding.body.span + || arm.body.span != binding.body.span + { + return None; + } + selected.push(SelectedBody { + checked_index: *checked_index, + address: std::ptr::from_ref(&row.body).addr(), + bound_type: row.bound_type, + reflection: row.reflection.clone(), + _facts: row.body.clone(), + }); + let mut container = dispatch_path.clone(); + container.extend([2, ordinal]); + let mut scope = container.clone(); + scope.extend([0, 0, 2, 0]); + containers.push(container); + scopes.push(scope); + } + Some(ReflectedFieldDispatchProof { + data: Arc::new(Dispatch { + checked: checked.clone(), + function: function.id, + declaration, + function_span: function.span, + loop_address: std::ptr::from_ref(source_loop).addr(), + binding_address: std::ptr::from_ref(binding).addr(), + loop_span: source_loop.span, + binding_span: binding.span, + binder, + owner_definition, + owner, + owner_reflection: owner_reflection.clone(), + fields: fields.to_vec(), + selected, + key, + field_index: FieldId::new(0), + type_info_field: FieldId::new(9), + loop_path, + dispatch_path, + containers, + scopes, + type_info: type_info.clone(), + arms: arms.clone(), + }), + }) +} + +fn original_function( + checked: &CheckedResourceProgram, + declaration: DefId, + span: Span, +) -> Option<&ast::FunctionDef> { + original_function_parts(checked.module(), checked.resolved(), declaration, span) +} + +fn original_function_parts<'a>( + module: &'a ast::Module, + resolve: &ResolveResult, + declaration: DefId, + span: Span, +) -> Option<&'a ast::FunctionDef> { + let definition = resolve + .scope_table + .definitions + .get(declaration.index() as usize)?; + if definition.id != declaration || definition.kind != DefKind::Function { + return None; + } + let candidates = module + .items + .iter() + .filter_map(|item| match item { + Item::Function(source) + if source.name.span == definition.span + && source.span == span + && source.type_params.is_empty() => + { + Some(source) + } + _ => None, + }) + .collect::>(); + let [source] = candidates.as_slice() else { + return None; + }; + Some(*source) +} + +pub(crate) fn original_root_function( + module: &ast::Module, + resolve: &ResolveResult, + source: &ast::FunctionDef, + declaration: DefId, +) -> bool { + original_function_parts(module, resolve, declaration, source.span) + .is_some_and(|original| std::ptr::eq(original, source)) +} + +fn direct_owner( + checked: &CheckedResourceProgram, + source: &ast::ForStmt, +) -> Option<(DefId, TypeId, ReflectionTypeInfo)> { + direct_owner_parts(checked.resolved(), checked.checked(), source) +} + +fn direct_owner_parts( + resolve: &ResolveResult, + check: &CheckResult, + source: &ast::ForStmt, +) -> Option<(DefId, TypeId, ReflectionTypeInfo)> { + let Expr::GenericCall(_, operands, arguments, occurrence) = &source.iterable else { + return None; + }; + if source.view + || source.value_variable.is_some() + || !arguments.is_empty() + || check.intrinsic_ids.get(occurrence) != Some(&IntrinsicId::TypeFields) + { + return None; + } + let [TypeExpr::Named(operand)] = operands.as_slice() else { + return None; + }; + let definition = *resolve.resolutions.get(&operand.span)?; + let header = resolve + .scope_table + .definitions + .get(definition.index() as usize)?; + if header.id != definition || header.kind != DefKind::Struct { + return None; + } + let [owner] = check.intrinsic_type_arguments.get(occurrence)?.as_slice() else { + return None; + }; + let [reflection] = check + .intrinsic_reflection_arguments + .get(occurrence)? + .as_slice() + else { + return None; + }; + let types = &check.interner; + if check.definition_types.get(&definition) != Some(owner) + || !matches!(types.resolve(*owner), Type::Struct(_)) + || !types.nominal_type_arguments(*owner).is_empty() + || reflection.kind != "struct" + || !reflection.args.is_empty() + || check.reflection_metadata.get_type_info_for_id(*owner) != Some(reflection) + { + return None; + } + Some((definition, *owner, reflection.clone())) +} + +fn binder(checked: &CheckedResourceProgram, source: &ast::ForStmt) -> Option { + binder_parts(checked.resolved(), source) +} + +fn binder_parts(resolve: &ResolveResult, source: &ast::ForStmt) -> Option { + let candidates = resolve + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == DefKind::Variable + && definition.span == source.variable.span + && definition.name == source.variable.name + }) + .collect::>(); + let [definition] = candidates.as_slice() else { + return None; + }; + Some(definition.id) +} + +fn binding_selects_binder( + checked: &CheckedResourceProgram, + binding: &ast::ComptimeTypeBindStmt, + binder: DefId, +) -> bool { + binding_selects_binder_parts(checked.resolved(), binding, binder) +} + +fn binding_selects_binder_parts( + resolve: &ResolveResult, + binding: &ast::ComptimeTypeBindStmt, + binder: DefId, +) -> bool { + let Expr::FieldAccess(base, member, _) = &binding.value else { + return false; + }; + let Expr::Ident(variable) = base.as_ref() else { + return false; + }; + member.name == "type_info" && resolve.resolutions.get(&variable.span) == Some(&binder) +} + +fn checked_fields( + checked: &CheckedResourceProgram, + owner: TypeId, +) -> Option> { + checked_fields_parts(checked.checked(), owner) +} + +fn checked_fields_parts( + check: &CheckResult, + owner: TypeId, +) -> Option> { + let types = &check.interner; + let Type::Struct(id) = types.resolve(owner) else { + return None; + }; + let fields = &types.resolve_struct(*id).fields; + let metadata = check.reflection_metadata.get_type_fields_for_id(owner)?; + if metadata.len() != fields.len() + || metadata + .iter() + .zip(fields) + .enumerate() + .any(|(index, (metadata, (name, ty)))| { + metadata.index != index + || metadata.name != *name + || ty.index() as usize >= types.len() + }) + { + return None; + } + Some( + fields + .iter() + .map(|(_, ty)| *ty) + .zip(metadata.iter().cloned()) + .collect(), + ) +} + +/// Called while lowering the exact original root For AST occurrence. This is +/// only a lowering choice; Source authority is minted later in archive capture. +pub(crate) fn original_loop_bindings( + resolve: &ResolveResult, + check: &CheckResult, + source: &ast::ForStmt, +) -> Vec { + if source.view || source.value_variable.is_some() { + return Vec::new(); + } + let Some((_, owner, _)) = direct_owner_parts(resolve, check, source) else { + return Vec::new(); + }; + let Some(fields) = checked_fields_parts(check, owner) else { + return Vec::new(); + }; + let Some(binder) = binder_parts(resolve, source) else { + return Vec::new(); + }; + source + .body + .stmts + .iter() + .filter_map(|statement| { + let Stmt::ComptimeTypeBind(binding) = statement else { + return None; + }; + if !binding_selects_binder_parts(resolve, binding, binder) { + return None; + } + let rows = check.comptime_type_bindings.get(&binding.span)?; + if rows.len() != fields.len() || fields.is_empty() { + return None; + } + fields + .iter() + .enumerate() + .all(|(ordinal, (ty, field))| { + let candidates = rows + .iter() + .filter(|row| { + row.selection + == CheckedComptimeTypeSelection::ReflectedIteration(ordinal) + }) + .collect::>(); + let [row] = candidates.as_slice() else { + return false; + }; + row.bound_type == *ty && row.reflection == field.type_info + }) + .then_some(binding.span) + }) + .collect() +} + +fn metadata_list_matches( + iterable: &Expression, + element_type: TypeId, + owner_name: &str, + fields: &[(TypeId, ReflectionFieldInfo)], + checked: &CheckedResourceProgram, +) -> bool { + let types = &checked.checked().interner; + let Type::Struct(id) = types.resolve(element_type) else { + return false; + }; + let definition = types.resolve_struct(*id); + let expected = [ + "index", + "owner_type", + "owner_member", + "name", + "type_name", + "kind", + "kind_tag", + "serialize_name", + "has_secret", + "type_info", + ]; + if definition.name != "TypeField" + || !definition + .fields + .iter() + .map(|(name, _)| name.as_str()) + .eq(expected) + || definition.fields[0].1 != TypeInterner::INT64 + || !matches!(types.resolve(iterable.ty), Type::List(element) if *element == element_type) + { + return false; + } + let E::ListConstruct { elements } = &iterable.kind else { + return false; + }; + elements.len() == fields.len() + && elements.iter().zip(fields).all(|(element, (_, metadata))| { + let E::StructConstruct { + struct_type, + fields, + evaluation_order, + validates_refinements: false, + refinement_predicates, + } = &element.kind + else { + return false; + }; + *struct_type == element_type + && element.ty == element_type + && fields.len() == expected.len() + && evaluation_order.iter().copied().eq(0..expected.len()) + && refinement_predicates.is_empty() + && fields + .iter() + .zip(&definition.fields) + .all(|(field, (_, ty))| field.ty == *ty) + && i128::try_from(metadata.index) + .ok() + .is_some_and(|index| matches!(fields[0].kind, E::Int(value) if value == index)) + && matches!(&fields[1].kind, E::String(value) if value == owner_name) + && matches!(fields[2].kind, E::OptionalNone) + && matches!(&fields[3].kind, E::String(value) if value == &metadata.name) + && matches!(&fields[4].kind, E::String(value) if value == &metadata.type_name) + && matches!(&fields[5].kind, E::String(value) if value == &metadata.kind) + && metadata_tag_matches( + &fields[6], + ReflectionTypeInfo::kind_tag_variant(&metadata.kind), + types, + ) + && matches!(&fields[7].kind, E::String(value) if value == &metadata.serialize_name) + && matches!(fields[8].kind, E::Bool(value) if value == metadata.has_secret) + && metadata_type_info_matches(&fields[9], &metadata.type_info, types) + }) +} + +fn metadata_tag_matches(value: &Expression, expected: &str, types: &TypeInterner) -> bool { + let E::EnumConstruct { + enum_type, + variant, + payloads, + evaluation_order, + } = &value.kind + else { + return false; + }; + let Type::Enum(id) = types.resolve(*enum_type) else { + return false; + }; + *enum_type == value.ty + && payloads.is_empty() + && evaluation_order.is_empty() + && types + .resolve_enum(*id) + .variants + .get(variant.index() as usize) + .is_some_and(|variant| variant.name == expected && variant.fields.is_empty()) +} + +fn metadata_type_info_matches( + value: &Expression, + expected: &ReflectionTypeInfo, + types: &TypeInterner, +) -> bool { + let E::StructConstruct { + struct_type, + fields, + evaluation_order, + validates_refinements: false, + refinement_predicates, + } = &value.kind + else { + return false; + }; + let Type::Struct(id) = types.resolve(*struct_type) else { + return false; + }; + let header = types.resolve_struct(*id); + let members = [ + "type_name", + "kind", + "kind_tag", + "primitive_tag", + "has_secret", + "args", + ]; + if *struct_type != value.ty + || header.name != "TypeInfo" + || !header + .fields + .iter() + .map(|(name, _)| name.as_str()) + .eq(members) + || fields.len() != members.len() + || !evaluation_order.iter().copied().eq(0..members.len()) + || !refinement_predicates.is_empty() + || !fields + .iter() + .zip(&header.fields) + .all(|(field, (_, ty))| field.ty == *ty) + || !matches!(&fields[0].kind, E::String(name) if name == &expected.type_name) + || !matches!(&fields[1].kind, E::String(kind) if kind == &expected.kind) + || !metadata_tag_matches( + &fields[2], + ReflectionTypeInfo::kind_tag_variant(&expected.kind), + types, + ) + || !matches!(fields[4].kind, E::Bool(secret) if secret == expected.has_secret) + { + return false; + } + let primitive_matches = match (&fields[3].kind, expected.primitive_tag.as_deref()) { + (E::OptionalNone, None) => true, + (E::OptionalSome(tag), Some(expected)) => { + matches!(types.resolve(fields[3].ty), Type::Optional(inner) if *inner == tag.ty) + && metadata_tag_matches(tag, expected, types) + } + _ => false, + }; + let E::ListConstruct { elements } = &fields[5].kind else { + return false; + }; + primitive_matches + && matches!(types.resolve(fields[5].ty), Type::List(inner) if *inner == value.ty) + && elements.len() == expected.args.len() + && elements + .iter() + .zip(&expected.args) + .all(|(argument, expected)| { + argument.ty == value.ty && metadata_type_info_matches(argument, expected, types) + }) +} + +fn root_dispatch_mut<'a>(body: &'a mut Block, path: &[usize]) -> Option<&'a mut Statement> { + let [0, loop_index, 2, 0, 0, dispatch_index] = path else { + return None; + }; + let S::For { body, .. } = &mut body.statements.get_mut(*loop_index)?.kind else { + return None; + }; + let statement = body.statements.get_mut(*dispatch_index)?; + matches!(statement.kind, S::ReflectedTypeDispatch { .. }).then_some(statement) +} diff --git a/crates/jett_hir/src/reflected_fields.rs b/crates/jett_hir/src/reflected_fields.rs index e60d39ef..e4afb77f 100644 --- a/crates/jett_hir/src/reflected_fields.rs +++ b/crates/jett_hir/src/reflected_fields.rs @@ -7,6 +7,8 @@ pub enum ReflectedFieldValidation { Validate(Vec), /// Internal MIR stage: execute only the original selector checks and read. Read, + /// Internal MIR stage: strict original read followed by a lazy adapter. + ReadCallable(Vec), } #[derive(Debug, Clone, PartialEq, Eq)] @@ -21,6 +23,7 @@ pub enum ReflectedFieldAction { Exact, Predicates(Vec), Unsupported(ReflectedFieldUnsupported), + CallableReturn(ReflectedCallablePlan), Refine { base: Box, predicates: Vec, @@ -50,6 +53,9 @@ impl ReflectedFieldPlan { match &self.action { ReflectedFieldAction::Exact | ReflectedFieldAction::Unsupported(_) => {} ReflectedFieldAction::Predicates(predicates) => result.extend(predicates), + ReflectedFieldAction::CallableReturn(plan) => { + plan.return_plan().collect_predicates(result) + } ReflectedFieldAction::Refine { base, predicates } => { base.collect_predicates(result); result.extend(predicates); @@ -69,57 +75,11 @@ impl ReflectedFieldPlan { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ReflectedFieldUnsupported { - Nested, - Secret, - CallableOrNominal, -} - -impl ReflectedFieldUnsupported { - pub fn message(self) -> &'static str { - match self { - Self::Nested => { - "reflected field refinement: native nested refinement validation is not implemented" - } - Self::Secret => { - "reflected field refinement: cannot establish new invariants beneath secret" - } - Self::CallableOrNominal => { - "reflected field refinement: unsupported callable or nominal refinement conversion" - } - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ReflectedFieldRequirement { - Exact, - Predicates(Vec), - Unsupported(ReflectedFieldUnsupported), - Refine { - base: Box, - predicates: Vec, - }, - List(Box), - Set(Box), - Map { - key: Box, - value: Box, - }, - Optional(Box), - Result { - ok: Box, - error: Box, - }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ReflectedFieldRequirementPlan { - pub source_type: TypeId, - pub requested_type: TypeId, - pub requirement: ReflectedFieldRequirement, -} +pub use jett_typecheck::{ + ProducedRefinementRequirement as ReflectedFieldRequirement, + ProducedRefinementRequirementPlan as ReflectedFieldRequirementPlan, + ProducedRefinementUnsupported as ReflectedFieldUnsupported, +}; pub fn is_reflected_field_intrinsic(id: IntrinsicId) -> bool { matches!( @@ -268,7 +228,10 @@ fn contains_refinement(types: &TypeInterner, ty: TypeId, seen: &mut HashSet Option<(TypeId, Vec)> { +pub(crate) fn refinement_chain( + types: &TypeInterner, + mut ty: TypeId, +) -> Option<(TypeId, Vec)> { let mut chain = Vec::new(); for _ in 0..types.len() { if ty.index() as usize >= types.len() { @@ -293,7 +256,7 @@ pub fn reflected_field_requirement( actual: TypeId, requested: TypeId, ) -> Option { - requirement_node(types, actual, requested, types.len()).map(|node| node.requirement) + jett_typecheck::produced_refinement_requirement(types, actual, requested) } fn requirement_node( @@ -401,6 +364,15 @@ pub fn validate_reflected_field_plans( if plan.source_type != *actual || plan.requested_type != requested { return Err("reflected validation types disagree with declared field and request"); } + if let ReflectedFieldAction::CallableReturn(callable) = &plan.action { + if callable.source_type() != plan.source_type + || callable.requested_type() != plan.requested_type + { + return Err("reflected callable action differs from its selected field types"); + } + callable.validate_types(types)?; + continue; + } let requirement = requirement_node(types, *actual, requested, types.len()) .ok_or("invalid reflected validation type")?; validate_requirement_node(types, &requirement, plan)?; @@ -408,7 +380,7 @@ pub fn validate_reflected_field_plans( Ok(()) } -fn validate_requirement_node( +pub(crate) fn validate_requirement_node( types: &TypeInterner, expected: &ReflectedFieldRequirementPlan, plan: &ReflectedFieldPlan, @@ -691,7 +663,36 @@ impl BodyLowerer<'_, '_> { return None; }; let mut plans = Vec::with_capacity(layout.len()); - for (_, _, source_type) in layout { + for (ordinal, (_, _, source_type)) in layout.into_iter().enumerate() { + if source_type != *requested { + if let Some(checked) = self.parent.reflected_checked_program.clone() { + if let Some(callable) = checked + .checked() + .reflected_callable_plan(source_type, *requested) + { + let child = ReflectedFieldRequirementPlan { + source_type: callable.source_return(), + requested_type: callable.requested_return(), + requirement: callable.return_requirement().clone(), + }; + let return_plan = self.lower_reflected_requirement(child, span)?; + plans.push(ReflectedFieldPlan { + source_type, + requested_type: *requested, + action: ReflectedFieldAction::CallableReturn( + ReflectedCallablePlan::new( + checked, + callable, + return_plan, + span, + Some(ordinal), + ), + ), + }); + continue; + } + } + } let Some(requirement) = requirement_node( &self.parent.check.interner, source_type, diff --git a/crates/jett_hir/src/resource_manifest.rs b/crates/jett_hir/src/resource_manifest.rs new file mode 100644 index 00000000..93af0465 --- /dev/null +++ b/crates/jett_hir/src/resource_manifest.rs @@ -0,0 +1,431 @@ +//! Original checked Resource declarations and hooks, including unused entries. +//! Public references cannot be constructed from a type, name or raw lowering input. +use std::sync::Arc; + +use jett_common::{SourceOrigin, Span}; +use jett_parser::ast::Item; +use jett_resolve::{DefId, DefKind}; +use jett_typecheck::{CheckedResourceHook, CheckedResourceProgram, validate_resource_hooks}; +use jett_types::{ResourceKernelRecipe, Type, TypeId, TypeInterner}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ResourceKindId(u32); +impl ResourceKindId { + pub fn index(self) -> u32 { + self.0 + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResourceKind { + definition: DefId, + ty: TypeId, + declaration: Span, + name: String, + namespace: Option, +} +impl ResourceKind { + pub fn definition(&self) -> DefId { + self.definition + } + pub fn ty(&self) -> TypeId { + self.ty + } + pub fn declaration(&self) -> Span { + self.declaration + } + pub fn name(&self) -> &str { + &self.name + } + pub fn namespace(&self) -> Option<&str> { + self.namespace.as_deref() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct Hook { + checked: CheckedResourceHook, + kind: ResourceKindId, + recipe: ResourceKernelRecipe, +} + +#[derive(Debug)] +struct ManifestData { + original: Arc, + kinds: Vec, + hooks: Vec, +} + +#[derive(Debug, Clone, Default)] +pub struct ResourceManifest { + data: Option>, +} +impl PartialEq for ResourceManifest { + fn eq(&self, other: &Self) -> bool { + match (&self.data, &other.data) { + (None, None) => true, + (Some(left), Some(right)) => Arc::ptr_eq(left, right), + _ => false, + } + } +} +impl Eq for ResourceManifest {} + +/// Exact nominal provenance, separate from the dense index used for table lookup. +#[derive(Debug, Clone)] +pub struct ResourceKindRef { + data: Arc, + index: usize, +} +impl PartialEq for ResourceKindRef { + fn eq(&self, other: &Self) -> bool { + self.index == other.index && Arc::ptr_eq(&self.data, &other.data) + } +} +impl Eq for ResourceKindRef {} +impl ResourceKindRef { + pub fn id(&self) -> ResourceKindId { + ResourceKindId(self.index as u32) + } + pub fn ty(&self) -> TypeId { + self.data.kinds[self.index].ty + } + pub fn declaration(&self) -> Span { + self.data.kinds[self.index].declaration + } + pub fn definition(&self) -> DefId { + self.data.kinds[self.index].definition + } + pub fn validate(&self, types: &TypeInterner) -> Result<(), String> { + ResourceManifest { + data: Some(self.data.clone()), + } + .validate(types)?; + self.data + .kinds + .get(self.index) + .ok_or("Resource kind reference is outside its original manifest")?; + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub struct ResourceHookRef { + data: Arc, + index: usize, +} +impl PartialEq for ResourceHookRef { + fn eq(&self, other: &Self) -> bool { + self.index == other.index && Arc::ptr_eq(&self.data, &other.data) + } +} +impl Eq for ResourceHookRef {} +impl ResourceHookRef { + pub fn definition(&self) -> DefId { + self.data.hooks[self.index].checked.definition + } + pub fn resource_type(&self) -> TypeId { + self.data.hooks[self.index].checked.resource_type + } + pub fn function_type(&self) -> TypeId { + self.data.hooks[self.index].checked.function_type + } + pub fn kind(&self) -> ResourceKindId { + self.data.hooks[self.index].kind + } + pub fn resource_kind(&self) -> ResourceKindRef { + ResourceKindRef { + data: self.data.clone(), + index: self.kind().index() as usize, + } + } + + pub fn recipe(&self) -> ResourceKernelRecipe { + self.data.hooks[self.index].recipe + } + pub fn validate(&self, types: &TypeInterner) -> Result<(), String> { + ResourceManifest { + data: Some(self.data.clone()), + } + .validate(types)?; + self.data + .hooks + .get(self.index) + .ok_or("Resource hook reference is outside its original manifest")?; + Ok(()) + } + pub fn metadata_types(&self, mut visit: impl FnMut(TypeId)) { + visit(self.resource_type()); + visit(self.function_type()); + if let Type::Function { + params, + return_type, + .. + } = self + .data + .original + .checked() + .interner + .resolve(self.function_type()) + { + for &ty in params { + visit(ty); + } + visit(*return_type); + } + } +} + +impl ResourceManifest { + pub fn empty() -> Self { + Self::default() + } + pub fn kinds(&self) -> &[ResourceKind] { + self.data.as_ref().map_or(&[], |data| data.kinds.as_slice()) + } + pub fn hooks(&self) -> impl Iterator + '_ { + self.data.iter().flat_map(|data| { + (0..data.hooks.len()).map(move |index| ResourceHookRef { + data: data.clone(), + index, + }) + }) + } + pub fn contains_type(&self, ty: TypeId) -> bool { + self.kinds().iter().any(|kind| kind.ty == ty) + } + pub fn kind_for_type(&self, ty: TypeId) -> Option { + let data = self.data.as_ref()?; + let index = data.kinds.iter().position(|kind| kind.ty == ty)?; + Some(ResourceKindRef { + data: data.clone(), + index, + }) + } + pub fn contains_kind(&self, kind: &ResourceKindRef) -> bool { + self.data + .as_ref() + .is_some_and(|data| Arc::ptr_eq(data, &kind.data) && kind.index < data.kinds.len()) + } + + pub fn contains_hook(&self, hook: &ResourceHookRef) -> bool { + self.data + .as_ref() + .is_some_and(|data| Arc::ptr_eq(data, &hook.data) && hook.index < data.hooks.len()) + } + pub(crate) fn hook_for_definition(&self, definition: DefId) -> Option { + self.hooks().find(|hook| hook.definition() == definition) + } + pub(crate) fn checked(original: &Arc) -> Result { + let (kinds, hooks) = records(original)?; + let manifest = Self { + data: Some(Arc::new(ManifestData { + original: original.clone(), + kinds, + hooks, + })), + }; + manifest.validate(&original.checked().interner)?; + Ok(manifest) + } + /// Reject unknown Resource kinds anywhere in a checked layout or signature. + /// This validates provenance only; it does not grant aggregate custody. + pub fn validate_type(&self, types: &TypeInterner, ty: TypeId) -> Result<(), String> { + let mut pending = vec![ty]; + let mut seen = std::collections::HashSet::new(); + while let Some(ty) = pending.pop() { + if ty.index() as usize >= types.len() { + return Err("Resource manifest type visitor is outside its interner".into()); + } + if !seen.insert(ty) { + continue; + } + pending.extend_from_slice(types.nominal_type_arguments(ty)); + match types.resolve(ty) { + Type::Resource(_) if !self.contains_type(ty) => { + return Err( + "Resource type has no original checked nominal manifest entry".into(), + ); + } + Type::List(inner) + | Type::Set(inner) + | Type::Optional(inner) + | Type::Secret(inner) + | Type::Refinement { base: inner, .. } => pending.push(*inner), + Type::Map(key, value) | Type::Result(key, value) => { + pending.push(*key); + pending.push(*value); + } + Type::Function { + params, + return_type, + .. + } => { + pending.extend_from_slice(params); + pending.push(*return_type); + } + Type::Struct(id) => { + let definition = types.resolve_struct(*id); + pending.extend(definition.fields.iter().map(|(_, ty)| *ty)); + for method in &definition.methods { + pending.extend(method.params.iter().map(|(_, ty, _)| *ty)); + pending.push(method.return_type); + } + } + Type::Enum(id) => { + for variant in &types.resolve_enum(*id).variants { + pending.extend(variant.fields.iter().map(|(_, ty)| *ty)); + } + } + Type::Machine(id) | Type::MachineState { machine: id, .. } => { + for state in &types.resolve_machine(*id).states { + pending.extend(state.fields.iter().map(|(_, ty)| *ty)); + } + } + Type::Interface(id) => { + for method in &types.resolve_interface(*id).methods { + pending.extend(method.params.iter().map(|(_, ty, _)| *ty)); + pending.push(method.return_type); + } + } + Type::Actor(id) => { + let actor = types.resolve_actor(*id); + pending.extend( + actor + .capability_params + .iter() + .chain(&actor.state_fields) + .map(|(_, ty)| *ty), + ); + for message in &actor.messages { + pending.extend(message.params.iter().map(|(_, ty)| *ty)); + pending.push(message.responds); + } + } + _ => {} + } + } + Ok(()) + } + pub fn validate(&self, types: &TypeInterner) -> Result<(), String> { + let Some(data) = &self.data else { + return Ok(()); + }; + let (kinds, hooks) = records(&data.original)?; + if kinds != data.kinds || hooks != data.hooks { + return Err( + "Resource manifest differs from its original checked declarations or unused hooks" + .into(), + ); + } + for kind in &data.kinds { + if kind.ty.index() as usize >= types.len() + || types.resolve(kind.ty) != data.original.checked().interner.resolve(kind.ty) + || !matches!(types.resolve(kind.ty), Type::Resource(name) if name == &kind.name) + { + return Err( + "Resource manifest nominal type differs from its original declaration".into(), + ); + } + } + for hook in &data.hooks { + if !hook.recipe.matches_signature( + types, + hook.checked.resource_type, + hook.checked.function_type, + ) || types.resolve(hook.checked.function_type) + != data + .original + .checked() + .interner + .resolve(hook.checked.function_type) + { + return Err( + "Resource manifest hook signature differs from its exact checked recipe".into(), + ); + } + } + Ok(()) + } +} + +fn records( + original: &Arc, +) -> Result<(Vec, Vec), String> { + validate_resource_hooks(original.module(), original.resolved(), original.checked()) + .map_err(|error| error.to_string())?; + let mut kinds = Vec::new(); + for item in &original.module().items { + let Item::Resource(resource) = item else { + continue; + }; + let declarations = original + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == DefKind::Resource && definition.span == resource.name.span + }) + .collect::>(); + let [declaration] = declarations.as_slice() else { + return Err( + "Resource manifest requires one exact original Resource declaration".into(), + ); + }; + if original.source_origins().get(&resource.name.span.file) != Some(&SourceOrigin::Stdlib) { + return Err( + "Resource manifest declaration has no compiler-shipped source origin".into(), + ); + } + let ty = *original + .checked() + .definition_types + .get(&declaration.id) + .ok_or("Resource manifest declaration has no checked nominal type")?; + if ty.index() as usize >= original.checked().interner.len() + || !matches!(original.checked().interner.resolve(ty), Type::Resource(name) if name == &declaration.name) + { + return Err("Resource manifest declaration has another nominal type".into()); + } + if kinds + .iter() + .any(|kind: &ResourceKind| kind.definition == declaration.id || kind.ty == ty) + { + return Err("Resource manifest duplicates a nominal declaration".into()); + } + kinds.push(ResourceKind { + definition: declaration.id, + ty, + declaration: resource.name.span, + name: declaration.name.clone(), + namespace: declaration.namespace.clone(), + }); + } + let mut hooks = Vec::new(); + for kernel in original.resolved().resource_kernels.iter() { + let checked = original + .checked() + .resource_hooks + .get(&kernel.definition) + .ok_or("Resource manifest kernel has no checked hook")? + .clone(); + let kind = kinds + .iter() + .position(|kind| { + kind.definition == kernel.resource_definition && kind.ty == checked.resource_type + }) + .ok_or("Resource manifest hook has no original nominal declaration")?; + hooks.push(Hook { + checked, + kind: ResourceKindId(kind as u32), + recipe: kernel.recipe, + }); + } + hooks.sort_by_key(|hook| hook.checked.definition.index()); + Ok((kinds, hooks)) +} + +#[cfg(test)] +#[path = "resource_manifest_tests.rs"] +mod tests; diff --git a/crates/jett_hir/src/resource_manifest_tests.rs b/crates/jett_hir/src/resource_manifest_tests.rs new file mode 100644 index 00000000..13910786 --- /dev/null +++ b/crates/jett_hir/src/resource_manifest_tests.rs @@ -0,0 +1,258 @@ +use super::*; +use crate::{ + CallBridge, CallOwnership, CallTarget, Expression, ExpressionKind as E, StatementKind as S, +}; +use jett_common::{FileId, SourceOrigin}; +use jett_parser::{ast::Item, parse}; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::{CheckOptions, CheckedCallerEffect, CheckedCallerSyntax}; +use std::collections::HashMap; + +const SUPPORT: &str = r#"namespace resource_probe +export resource TestHandle +export function create(view net: Network, label: int64) returns result[TestHandle, string]: + return kernel_create(label: label, net: view net) +export function close(token: TestHandle) returns nothing: + kernel_close(token) + return nothing +export function descriptor() returns function(TestHandle) returns nothing: + return kernel_close +"#; +const SOURCE: &str = r#"namespace app +function factory(view net: Network, label: int64) returns result[resource_probe.TestHandle, string]: + use resource_probe + return resource_probe.create(view net, label) +function descriptor() returns function(resource_probe.TestHandle) returns nothing: + use resource_probe + return resource_probe.descriptor() +"#; + +fn checked(release: bool) -> Arc { + let stdlib = FileId::new(10_000); + let project = FileId::new(0); + let mut parsed = parse(SUPPORT, stdlib); + let source = parse(SOURCE, project); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(source.errors.is_empty(), "{:?}", source.errors); + let declaration = parsed + .module + .items + .iter() + .find_map(|item| { + if let Item::Resource(resource) = item { + Some(resource.name.span) + } else { + None + } + }) + .unwrap(); + parsed.module.items.extend(source.module.items); + parsed.errors.extend(source.errors); + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: declaration, + member: member.into(), + recipe, + }) + .collect::>(); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (stdlib, SourceOrigin::Stdlib), + (project, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("{error:?}; {:?}", error.diagnostics())), + ) +} +fn invoke(program: &crate::Program) -> &Expression { + program + .functions + .iter() + .flat_map(|function| &function.body.statements) + .find_map(|statement| match &statement.kind { + S::Return(Some(value)) | S::Expression(value) + if matches!(value.kind, E::ResourceInvoke { .. }) => + { + Some(value) + } + _ => None, + }) + .expect("exact Source hook invocation") +} +fn invoke_mut(program: &mut crate::Program) -> &mut Expression { + program + .functions + .iter_mut() + .flat_map(|function| &mut function.body.statements) + .find_map(|statement| match &mut statement.kind { + S::Return(Some(value)) | S::Expression(value) + if matches!(value.kind, E::ResourceInvoke { .. }) => + { + Some(value) + } + _ => None, + }) + .unwrap() +} + +#[test] +fn resource_manifest_keeps_source_hooks_descriptors_and_lexical_argument_order() { + for release in [false, true] { + let original = checked(release); + let program = crate::lower_checked_resource_program(&original).unwrap(); + let types = &original.checked().interner; + crate::validate_backend_types(&program, types).unwrap(); + assert_eq!(program.resource_manifest.kinds().len(), 1); + assert_eq!(program.resource_manifest.hooks().count(), 3); // Borrow remains unused in Source. + let E::ResourceInvoke { + hook, + args, + evaluation_order, + ownership: CallOwnership::Source(source), + } = &invoke(&program).kind + else { + panic!("Source hook node"); + }; + assert_eq!(hook.recipe(), ResourceKernelRecipe::NetworkFactory); + let kind = hook.resource_kind(); + kind.validate(types).unwrap(); + assert!(program.resource_manifest.contains_kind(&kind)); + assert_eq!(kind.ty(), hook.resource_type()); + assert_eq!( + program.resource_manifest.kind_for_type(kind.ty()), + Some(kind) + ); + assert_eq!(evaluation_order, &[1, 0]); + assert_eq!(args.len(), 2); + assert_eq!(source.target, CallTarget::ResourceHook(hook.clone())); + assert_eq!( + source.bridge, + CallBridge::ResourceHook { hook: hook.clone() } + ); + assert_eq!(source.arguments[0].source_index, 1); + assert_eq!(source.arguments[0].syntax, CheckedCallerSyntax::WrittenView); + assert_eq!( + source.arguments[0].effect, + CheckedCallerEffect::RetainBorrow + ); + assert_eq!(source.arguments[1].effect, CheckedCallerEffect::Copy); + assert!(program.functions.iter().flat_map(|function| &function.body.statements).any(|statement| matches!(&statement.kind, S::Return(Some(Expression { kind: E::ResourceHookValue { hook }, ty, .. })) if hook.recipe() == ResourceKernelRecipe::Finalize && *ty == hook.function_type()))); + assert!( + crate::lower( + original.module(), + original.resolved(), + original.checked(), + original.source_origins() + ) + .is_err() + ); + crate::lower_checked_resource_program_with_test_bodies(&original).unwrap(); + } +} + +#[test] +fn resource_manifest_validates_unused_hooks_and_nominal_kind_records() { + for release in [false, true] { + let original = checked(release); + let program = crate::lower_checked_resource_program(&original).unwrap(); + let data = program.resource_manifest.data.as_ref().unwrap(); + for mutation in 0..4 { + let mut kinds = data.kinds.clone(); + let mut hooks = data.hooks.clone(); + match mutation { + 0 => { + hooks.retain(|hook| hook.recipe != ResourceKernelRecipe::NetworkBorrow); + } + 1 => { + hooks + .iter_mut() + .find(|hook| hook.recipe == ResourceKernelRecipe::NetworkBorrow) + .unwrap() + .recipe = ResourceKernelRecipe::Finalize; + } + 2 => { + kinds[0].ty = TypeInterner::STRING; + } + _ => { + kinds[0].declaration.end += 1; + } + } + let mut changed = program.clone(); + changed.resource_manifest = ResourceManifest { + data: Some(Arc::new(ManifestData { + original: original.clone(), + kinds, + hooks, + })), + }; + let errors = + crate::validate_backend_types(&changed, &original.checked().interner).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("Resource manifest differs")), + "{errors:?}" + ); + } + } +} + +#[test] +fn resource_manifest_refuses_foreign_refs_empty_manifest_and_source_packet_changes() { + for release in [false, true] { + let original = checked(release); + let program = crate::lower_checked_resource_program(&original).unwrap(); + let foreign = crate::lower_checked_resource_program(&checked(release)).unwrap(); + for mutation in 0..6 { + let mut changed = program.clone(); + if mutation == 0 { + changed.resource_manifest = ResourceManifest::empty(); + } else { + let call = invoke_mut(&mut changed); + let E::ResourceInvoke { + hook, + evaluation_order, + args, + ownership: CallOwnership::Source(source), + } = &mut call.kind + else { + unreachable!() + }; + match mutation { + 1 => { + *hook = foreign + .resource_manifest + .hooks() + .find(|hook| hook.recipe() == ResourceKernelRecipe::NetworkFactory) + .unwrap(); + } + 2 => { + *evaluation_order = vec![0, 1]; + } + 3 => { + source.bridge = CallBridge::Direct; + } + 4 => { + args[1].ty = TypeInterner::STRING; + } + _ => { + call.ty = TypeInterner::STRING; + } + } + } + assert!( + crate::validate_backend_types(&changed, &original.checked().interner).is_err(), + "mutation {mutation}" + ); + } + } +} diff --git a/crates/jett_hir/src/resource_materialization.rs b/crates/jett_hir/src/resource_materialization.rs new file mode 100644 index 00000000..b69eb3ff --- /dev/null +++ b/crates/jett_hir/src/resource_materialization.rs @@ -0,0 +1,598 @@ +//! Required values become execution data only through the original checked cache. +use crate::resource_source::{Materialized, RequiredMaterialization}; +use crate::{ + Block, DeclarationKind, Expression, ExpressionKind as E, Function, FunctionId, LowerError, + Program, ResourceHookRef, StatementKind as S, StringSegment, +}; +use jett_common::{FileId, Span}; +use jett_comptime::{CheckedRequiredOwner, CheckedRequiredValue, ExplicitComptimeValues, Value}; +use jett_parser::ast::{Expr, Item}; +use jett_typecheck::CheckedResourceProgram; +use jett_types::{Type, TypeId, TypeInterner}; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; + +/// Derive required replacements from successful private checked entries. This +/// does not evaluate Source, install a provider, or accept a caller-baked view. +pub fn materialize_checked_required_values( + program: &mut Program, + values: &ExplicitComptimeValues, + types: &TypeInterner, +) -> Result<(), Vec> { + let archive = program.resource_source.clone(); + let span = program + .functions + .first() + .map_or(Span::new(FileId::new(0), 0, 0), |f| f.span); + let result = (|| -> Result { + archive.validate_types(types)?; + if archive.manifest() != Some(&program.resource_manifest) + || archive.equality_methods() != Some(&program.equality_methods) + || !functions_equal(archive.execution_functions(), &program.functions) + { + return Err( + "required materialization changed original checked HIR before authentication" + .into(), + ); + } + let checked = archive + .checked_program() + .ok_or("required materialization has no original checked program")?; + let proofs = values.checked_required_values(checked)?; + let mut functions = program.functions.clone(); + let mut rows = archive.required_materializations().to_vec(); + let mut candidates = HashSet::new(); + for function in &mut functions { + let owner = function.clone(); + walk_block(&mut function.body, &mut |expression| { + if !matches!(expression.kind, E::Comptime { .. } | E::Constant { .. }) { + return Ok(()); + } + let original = expression.clone(); + let selected = proofs + .iter() + .filter(|proof| proof_matches(proof, &owner, expression, checked)) + .collect::>(); + let [proof] = selected.as_slice() else { + return Err("required expression has no unique original checked value and complete context".into()); + }; + if let E::Comptime { value, .. } = &original.kind { + references(value, &mut candidates); + } + let (kind, hook) = derive_value( + proof, + expression.ty, + &program.resource_manifest, + checked, + types, + )?; + expression.kind = kind; + rows.push(RequiredMaterialization { + function: owner.id, + identity: owner.identity.clone(), + original, + current: expression.clone(), + hook, + proof: (*proof).clone(), + }); + Ok(()) + })?; + } + // Include transitive helpers of the removed required operands. They are + // excluded only if no preserved runtime root can still reach them. + for row in &rows { + if let E::Comptime { value, .. } = &row.original.kind { + references(value, &mut candidates); + } + if !row.proof.belongs_to(checked) { + return Err("required execution view has foreign proof".into()); + } + } + let original_edges = call_graph(archive.functions()); + closure(&mut candidates, &original_edges); + let exports = archive + .exported_function_ids() + .iter() + .copied() + .collect::>(); + let mut live = functions + .iter() + .filter(|f| { + !candidates.contains(&f.id) + || exports.contains(&f.id) + || f.identity.declaration.name == "main" + || f.identity.declaration.kind != DeclarationKind::Function + }) + .map(|f| f.id) + .collect::>(); + closure(&mut live, &call_graph(&functions)); + let mut required_only = candidates.difference(&live).copied().collect::>(); + required_only.sort_by_key(|id| id.index()); + Ok(Materialized { + functions, + values: rows, + required_only, + }) + })(); + match result { + Ok(view) => { + program.functions = view.functions.clone(); + program.resource_source.install_materialized(view); + Ok(()) + } + Err(message) => Err(vec![LowerError { span, message }]), + } +} + +pub(crate) fn proof_matches( + proof: &CheckedRequiredValue, + function: &Function, + expression: &Expression, + checked: &Arc, +) -> bool { + if !proof.belongs_to(checked) { + return false; + } + if let E::Constant { declaration } = expression.kind { + return proof.owner() == CheckedRequiredOwner::NamespaceConstant { declaration } + && proof.generic_index().is_none() + && proof.scoped_bindings().is_empty(); + } + let E::Comptime { + source_span, + bindings, + scopes, + .. + } = &expression.kind + else { + return false; + }; + if proof.source_span() != *source_span || !proof.is_explicit_comptime() { + return false; + } + let Ok(original) = proof.original_comptime() else { + return false; + }; + if !matches!(original, Expr::Comptime(_, span) if span == source_span) + || !proof.matches_original_comptime(original) + { + return false; + } + let owner_matches = match proof.owner() { + CheckedRequiredOwner::Function { definition, declaration } => { + function.source_definition == Some(definition) + && checked.module().items.iter().any(|item| { + matches!(item, Item::Function(value) + if value.name.span == declaration && value.span == function.span + && (checked.resolved().scope_table.def(definition).span == value.name.span + || checked.resolved().resolutions.get(&value.name.span) == Some(&definition))) + }) + && function.identity.declaration.kind == DeclarationKind::Function + } + CheckedRequiredOwner::Verify { declaration } => function.identity.declaration.kind == DeclarationKind::Verify + && checked.module().items.iter().any(|item| matches!(item, Item::Verify(v) if v.name.span == declaration && v.span == function.span)), + CheckedRequiredOwner::Property { declaration } => function.identity.declaration.kind == DeclarationKind::Property + && checked.module().items.iter().any(|item| matches!(item, Item::Property(v) if v.name.span == declaration && v.span == function.span)), + CheckedRequiredOwner::NamespaceConstant { .. } => false, + }; + if !owner_matches + || proof.type_arguments() != function.identity.type_arguments + || proof.specialization() != &function.identity.specialization + { + return false; + } + let generic = if function.identity.type_arguments.is_empty() { + None + } else { + let matches = checked + .checked() + .generic_function_instantiations + .iter() + .enumerate() + .filter(|(_, instance)| { + Some(instance.definition) == function.source_definition + && instance.concrete_args == function.identity.type_arguments + && instance.specialization == function.identity.specialization + }) + .map(|(index, _)| index) + .collect::>(); + let [index] = matches.as_slice() else { + return false; + }; + Some(*index) + }; + proof.generic_index() == generic + && proof.scoped_bindings().len() == bindings.len() + && scopes.len() == bindings.len() + && proof + .scoped_bindings() + .iter() + .zip(bindings) + .zip(scopes) + .all(|((proof, binding), scope)| { + proof.name() == binding.name + && proof.bound_type() == binding.ty + && proof.reflection() == &binding.reflection + && proof.owner() == scope.owner + && proof.index() == scope.index + && proof.selection() == scope.selection + }) +} + +fn derive_value( + proof: &CheckedRequiredValue, + ty: TypeId, + manifest: &crate::ResourceManifest, + checked: &Arc, + types: &TypeInterner, +) -> Result<(E, Option), String> { + if let Some(original) = proof.checked_hook(checked)? { + let hook = manifest + .hook_for_definition(original.definition) + .ok_or("required hook is absent from the original Resource manifest")?; + if hook.function_type() != ty + || hook.function_type() != original.function_type + || hook.resource_type() != original.resource_type + || hook.resource_kind().definition() != original.resource_definition + { + return Err( + "required hook changed its exact checked signature or nominal Resource identity" + .into(), + ); + } + hook.validate(types)?; + return Ok((E::ResourceHookValue { hook: hook.clone() }, Some(hook))); + } + if matches!(types.resolve(ty), Type::Secret(_) | Type::Refinement { .. }) { + return Err("checked required value is evaluated, but qualified native materialized value transport is pending".into()); + } + let value = proof.value().payload(); + let kind = match (types.resolve(ty), value) { + (Type::Int8 | Type::Int16 | Type::Int32 | Type::Int64 + | Type::Uint8 | Type::Uint16 | Type::Uint32 | Type::Uint64, Value::Int64(value)) => + E::Int(*value as i128), + (Type::Int8 | Type::Int16 | Type::Int32 | Type::Int64 + | Type::Uint8 | Type::Uint16 | Type::Uint32 | Type::Uint64, Value::Uint64(value)) => + E::Int(*value as i128), + (Type::Float32 | Type::Float64, Value::Float64(value)) => E::Float(*value), + (Type::String, Value::String(value)) => E::String(value.clone()), + (Type::Bool, Value::Bool(value)) => E::Bool(*value), + (Type::Nothing, Value::Nothing) => E::Nothing, + (Type::Optional(_), Value::OptionalNone) => E::OptionalNone, + _ => return Err("checked required value is evaluated, but its native materialized value transport is pending".into()), + }; + Ok((kind, None)) +} + +pub(crate) fn exported_functions( + program: &Program, + checked: &CheckedResourceProgram, +) -> Vec { + let mut spans = HashSet::new(); + for item in &checked.module().items { + match item { + Item::Function(value) if value.exported => { + spans.insert(value.name.span); + } + Item::Mutual(value) => { + for function in &value.declarations { + if function.exported { + spans.insert(function.name.span); + } + } + } + _ => {} + } + } + program + .functions + .iter() + .filter(|f| { + f.source_definition.is_some_and(|definition| { + spans.contains(&checked.resolved().scope_table.def(definition).span) + }) + }) + .map(|f| f.id) + .collect() +} +fn references(expression: &Expression, output: &mut HashSet) { + let mut copy = expression.clone(); + let _ = walk_expression(&mut copy, &mut |expression| { + match &expression.kind { + E::Call { function, .. } + | E::FunctionRef(function) + | E::ClosureRef { function, .. } + | E::FunctionAdapter { function, .. } => { + output.insert(*function); + } + E::ActorSpawn { + constructor: Some(function), + .. + } => { + output.insert(*function); + } + E::ActorMessage { handler, .. } => { + output.insert(*handler); + } + E::InterfaceCoerce { adapters, .. } => { + output.extend(adapters.iter().map(|a| a.function)) + } + _ => {} + } + Ok(()) + }); +} +fn call_graph(functions: &[Function]) -> HashMap> { + functions + .iter() + .map(|function| { + let mut body = function.body.clone(); + let mut targets = HashSet::new(); + let _ = walk_block(&mut body, &mut |expression| { + // This visitor already recurses; collecting one node avoids repeated traversal. + match &expression.kind { + E::Call { function, .. } + | E::FunctionRef(function) + | E::ClosureRef { function, .. } + | E::FunctionAdapter { function, .. } => { + targets.insert(*function); + } + E::ActorSpawn { + constructor: Some(function), + .. + } => { + targets.insert(*function); + } + E::ActorMessage { handler, .. } => { + targets.insert(*handler); + } + E::InterfaceCoerce { adapters, .. } => { + targets.extend(adapters.iter().map(|a| a.function)) + } + _ => {} + } + Ok(()) + }); + (function.id, targets) + }) + .collect() +} +fn closure(ids: &mut HashSet, graph: &HashMap>) { + let mut pending = ids.iter().copied().collect::>(); + while let Some(id) = pending.pop() { + if let Some(edges) = graph.get(&id) { + for edge in edges { + if ids.insert(*edge) { + pending.push(*edge); + } + } + } + } +} + +// Float equality authenticates exact bits, including NaN payloads and signed zero. +pub(crate) fn functions_equal(left: &[Function], right: &[Function]) -> bool { + if left.len() != right.len() { + return false; + } + left.iter().zip(right).all(|(left, right)| { + let mut left = left.clone(); + let mut right = right.clone(); + let a = clear_floats(&mut left.body); + let b = clear_floats(&mut right.body); + a == b && left == right + }) +} +pub(crate) fn expression_equal(left: &Expression, right: &Expression) -> bool { + let mut left = left.clone(); + let mut right = right.clone(); + let mut a = Vec::new(); + let mut b = Vec::new(); + let _ = walk_expression(&mut left, &mut |e| { + clear_float(e, &mut a); + Ok(()) + }); + let _ = walk_expression(&mut right, &mut |e| { + clear_float(e, &mut b); + Ok(()) + }); + a == b && left == right +} +fn clear_float(expression: &mut Expression, bits: &mut Vec) { + if let E::Float(value) = &mut expression.kind { + bits.push(value.to_bits()); + *value = 0.0; + } +} +fn clear_floats(block: &mut Block) -> Vec { + let mut bits = Vec::new(); + let _ = walk_block(block, &mut |expression| { + clear_float(expression, &mut bits); + Ok(()) + }); + bits +} + +pub(crate) fn walk_block( + block: &mut Block, + visit: &mut impl FnMut(&mut Expression) -> Result<(), String>, +) -> Result<(), String> { + for statement in &mut block.statements { + match &mut statement.kind { + S::Let { value, .. } + | S::Expression(value) + | S::HandleDefault(value) + | S::Respond(value) => walk_expression(value, visit)?, + S::Assign { target, value } => { + walk_expression(target, visit)?; + walk_expression(value, visit)?; + } + S::Return(value) => { + if let Some(value) = value { + walk_expression(value, visit)?; + } + } + S::If { + condition, + then_block, + else_block, + } => { + walk_expression(condition, visit)?; + walk_block(then_block, visit)?; + if let Some(block) = else_block { + walk_block(block, visit)?; + } + } + S::While { condition, body } => { + walk_expression(condition, visit)?; + walk_block(body, visit)?; + } + S::For { iterable, body, .. } => { + walk_expression(iterable, visit)?; + walk_block(body, visit)?; + } + S::Match { scrutinee, arms } => { + walk_expression(scrutinee, visit)?; + for arm in arms { + walk_block(&mut arm.body, visit)?; + } + } + S::Assert { condition, message } => { + walk_expression(condition, visit)?; + if let Some(value) = message { + walk_expression(value, visit)?; + } + } + S::Breakpoint { condition, .. } => { + if let Some(value) = condition { + walk_expression(value, visit)?; + } + } + S::Scope(block) => walk_block(block, visit)?, + S::ReflectedTypeDispatch { type_info, arms } => { + walk_expression(type_info, visit)?; + for arm in arms { + walk_block(&mut arm.body, visit)?; + } + } + S::Break | S::Continue | S::Trace(_) => {} + } + } + Ok(()) +} +fn walk_expression( + expression: &mut Expression, + visit: &mut impl FnMut(&mut Expression) -> Result<(), String>, +) -> Result<(), String> { + visit(expression)?; + match &mut expression.kind { + E::Binary { left, right, .. } => { + walk_expression(left, visit)?; + walk_expression(right, visit)?; + } + E::Unary { value, .. } + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::Comptime { value, .. } + | E::Declassify(value) + | E::Coarsen(value) + | E::RefinementValidated(value) + | E::InterfaceCoerce { value, .. } + | E::FunctionAdapter { value, .. } + | E::InterfaceType(value) + | E::RuntimeFailureMessage(value) + | E::StateIs { value, .. } + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::Field { base: value, .. } + | E::View(value) + | E::Clone(value) => walk_expression(value, visit)?, + E::Call { args, .. } + | E::Intrinsic { args, .. } + | E::ResourceInvoke { args, .. } + | E::ActorSpawn { args, .. } => { + for value in args { + walk_expression(value, visit)?; + } + } + E::IndirectCall { callee, args, .. } => { + for value in args { + walk_expression(value, visit)?; + } + walk_expression(callee, visit)?; + } + E::ActorMessage { actor, args, .. } => { + walk_expression(actor, visit)?; + for value in args { + walk_expression(value, visit)?; + } + } + E::StructConstruct { fields, .. } | E::BitfieldConstruct { fields, .. } => { + for value in fields { + walk_expression(value, visit)?; + } + } + E::MachineConstruct { payloads, .. } | E::EnumConstruct { payloads, .. } => { + for value in payloads { + walk_expression(value, visit)?; + } + } + E::MachineTransition { + source, payloads, .. + } => { + walk_expression(source, visit)?; + for value in payloads { + walk_expression(value, visit)?; + } + } + E::ListConstruct { elements } => { + for value in elements { + walk_expression(value, visit)?; + } + } + E::MapConstruct { entries } => { + for entry in entries { + walk_expression(&mut entry.key, visit)?; + walk_expression(&mut entry.value, visit)?; + } + } + E::Handle { + target, failure, .. + } => { + walk_expression(target, visit)?; + walk_block(failure, visit)?; + } + E::StringInterpolation(parts) => { + for part in parts { + if let StringSegment::Value(value) = part { + walk_expression(value, visit)?; + } + } + } + E::InlineFunction { body, .. } => walk_block(body, visit)?, + E::Int(_) + | E::Float(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::Local(_) + | E::Constant { .. } + | E::FunctionRef(_) + | E::ResourceHookValue { .. } + | E::ClosureRef { .. } + | E::OptionalNone + | E::RuntimeFailure(_) + | E::PropertyCaseContext(_) => {} + } + Ok(()) +} + +#[cfg(test)] +#[path = "resource_materialization_tests.rs"] +mod tests; diff --git a/crates/jett_hir/src/resource_materialization_tests.rs b/crates/jett_hir/src/resource_materialization_tests.rs new file mode 100644 index 00000000..42b1cc6f --- /dev/null +++ b/crates/jett_hir/src/resource_materialization_tests.rs @@ -0,0 +1,491 @@ +use super::*; +use jett_common::SourceOrigin; +use jett_comptime::ComptimeContext; +use jett_comptime::checked_types::CheckedExpressionTypes; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::CheckOptions; +use jett_types::{ReflectionMetadata, ResourceKernelRecipe}; +const SUPPORT: &str = + include_str!("../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); +const CLOSE: &str = + include_str!("../../jett_driver/tests/native_conformance/resource/33_comptime_hook_close.jett"); +fn checked(source: &str, release: bool) -> Arc { + let support = FileId::new(10_000); + let primary = FileId::new(0); + let mut parsed = jett_parser::parse(SUPPORT, support); + let project = jett_parser::parse(source, primary); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(project.errors.is_empty(), "{:?}", project.errors); + let declarations = parsed + .module + .items + .iter() + .filter_map(|item| match item { + Item::Resource(value) => Some(value.name.span), + _ => None, + }) + .collect::>(); + let [declaration] = declarations.as_slice() else { + panic!("one Resource declaration"); + }; + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: *declaration, + member: member.into(), + recipe, + }) + .collect::>(); + parsed.module.items.extend(project.module.items); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (support, SourceOrigin::Stdlib), + (primary, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("{error:?}: {:?}", error.diagnostics())), + ) +} +fn required(checked: &Arc) -> ExplicitComptimeValues { + let types = Arc::new(CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let result = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + Arc::new(ReflectionMetadata::new()), + types, + Arc::new(HashMap::new()), + ); + assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics); + assert!(result.debug_events.is_empty()); + result.values +} + +#[test] +fn required_source_hooks_and_absence_keep_original_archive_and_derive_exact_view() { + let sources = [ + CLOSE, + include_str!( + "../../jett_driver/tests/native_conformance/resource/34_comptime_hook_factory.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/35_comptime_hook_borrow.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/40_comptime_hook_absence.jett" + ), + ]; + for release in [false, true] { + for source in sources { + let checked = checked(source, release); + let values = required(&checked); + let mut program = crate::lower_checked_resource_program(&checked).unwrap(); + let original = program.resource_source.functions().to_vec(); + materialize_checked_required_values(&mut program, &values, &checked.checked().interner) + .unwrap(); + assert!(functions_equal( + program.resource_source.functions(), + &original + )); + assert!(functions_equal( + program.resource_source.execution_functions(), + &program.functions + )); + let count = program.resource_source.required_materializations().len(); + assert!(count > 0); + if source == CLOSE { + assert!( + program + .resource_source + .required_materializations() + .iter() + .any(|row| matches!(row.current().kind, E::Int(761))) + ); + } + for row in program.resource_source.required_materializations() { + assert!(matches!( + row.original().kind, + E::Comptime { .. } | E::Constant { .. } + )); + assert_eq!( + program.resource_source.materialized_hook( + row.function(), + row.original(), + row.current() + ), + row.hook() + ); + if let Some(hook) = row.hook() { + assert!( + matches!(&row.current().kind, E::ResourceHookValue { hook: current } if current == hook) + ); + assert_eq!(row.current().ty, hook.function_type()); + assert_eq!(row.current().span, row.original().span); + } + } + materialize_checked_required_values(&mut program, &values, &checked.checked().interner) + .unwrap(); + assert_eq!( + program.resource_source.required_materializations().len(), + count + ); + } + } +} + +#[test] +fn foreign_missing_and_public_mirror_values_never_materialize_resource_authority() { + for release in [false, true] { + let checked = checked(CLOSE, release); + let values = required(&checked); + let foreign = self::checked(CLOSE, release); + let mut program = crate::lower_checked_resource_program(&foreign).unwrap(); + assert!(program.resource_source.belongs_to(&foreign)); + assert!(!program.resource_source.belongs_to(&checked)); + let before = program.clone(); + assert!( + materialize_checked_required_values(&mut program, &values, &foreign.checked().interner) + .is_err() + ); + assert_eq!(program, before); + let mut program = crate::lower_checked_resource_program(&checked).unwrap(); + let proof = values + .checked_required_values(&checked) + .unwrap() + .into_iter() + .find(|v| v.checked_hook(&checked).unwrap().is_some()) + .unwrap(); + let mut mirror = ExplicitComptimeValues::default(); + mirror.insert( + proof.source_span(), + ComptimeContext::default(), + proof.value().clone(), + ); + assert!( + materialize_checked_required_values(&mut program, &mirror, &checked.checked().interner) + .is_err() + ); + assert!( + materialize_checked_required_values( + &mut program, + &ExplicitComptimeValues::default(), + &checked.checked().interner + ) + .is_err() + ); + let mut overwritten = values.clone(); + overwritten.insert( + proof.source_span(), + ComptimeContext::default(), + Value::Int64(99), + ); + materialize_checked_required_values( + &mut program, + &overwritten, + &checked.checked().interner, + ) + .unwrap(); + assert!( + program + .resource_source + .required_materializations() + .iter() + .any(|row| row.hook().is_some()) + ); + } +} + +#[test] +fn altered_current_bodies_headers_and_materialized_hooks_refuse_atomically() { + for release in [false, true] { + let checked = checked(CLOSE, release); + let values = required(&checked); + let original = crate::lower_checked_resource_program(&checked).unwrap(); + let main = original + .functions + .iter() + .position(|f| f.identity.declaration.name == "main") + .unwrap(); + let mut changed = original.clone(); + changed.functions[main] + .identity + .declaration + .name + .push_str("_forged"); + let before = changed.clone(); + assert!( + materialize_checked_required_values(&mut changed, &values, &checked.checked().interner) + .is_err() + ); + assert_eq!(changed, before); + let mut changed = original.clone(); + changed.functions[main].body.statements.pop(); + assert!( + materialize_checked_required_values(&mut changed, &values, &checked.checked().interner) + .is_err() + ); + let mut changed = original.clone(); + let mut replaced = false; + walk_block(&mut changed.functions[main].body, &mut |expression| { + if matches!(expression.kind, E::Comptime { .. }) && !replaced { + expression.kind = E::ResourceHookValue { + hook: original + .resource_manifest + .hooks() + .find(|h| h.recipe() == ResourceKernelRecipe::Finalize) + .unwrap(), + }; + replaced = true; + } + Ok(()) + }) + .unwrap(); + assert!(replaced); + assert!( + materialize_checked_required_values(&mut changed, &values, &checked.checked().interner) + .is_err() + ); + let mut baked = original.clone(); + materialize_checked_required_values(&mut baked, &values, &checked.checked().interner) + .unwrap(); + walk_block(&mut baked.functions[main].body, &mut |expression| { + if matches!(expression.kind, E::ResourceHookValue { .. }) { + expression.ty = TypeInterner::BOOL; + } + Ok(()) + }) + .unwrap(); + assert!( + materialize_checked_required_values(&mut baked, &values, &checked.checked().interner) + .is_err() + ); + } +} + +#[test] +fn required_only_generic_and_scoped_helpers_keep_original_body_and_exports() { + let sources = [ + include_str!( + "../../jett_driver/tests/native_conformance/resource/41_comptime_hook_pure_choice.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/42_comptime_hook_generic.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/43_comptime_hook_scoped.jett" + ), + ]; + for release in [false, true] { + for source in sources { + let checked = checked(source, release); + let values = required(&checked); + let mut program = crate::lower_checked_resource_program(&checked).unwrap(); + materialize_checked_required_values(&mut program, &values, &checked.checked().interner) + .unwrap(); + let choose = program + .functions + .iter() + .find(|f| f.identity.declaration.name == "choose") + .unwrap(); + assert!( + program + .resource_source + .required_only_function_ids() + .contains(&choose.id) + ); + assert!( + program + .resource_source + .functions() + .iter() + .any(|f| f.id == choose.id && f.body == choose.body) + ); + assert!( + program + .resource_source + .required_only_function_ids() + .iter() + .all(|id| !program.resource_source.exported_function_ids().contains(id)) + ); + let exported = source.replacen("function choose", "export function choose", 1); + let checked = self::checked(&exported, release); + let values = required(&checked); + let mut program = crate::lower_checked_resource_program(&checked).unwrap(); + materialize_checked_required_values(&mut program, &values, &checked.checked().interner) + .unwrap(); + let choose = program + .functions + .iter() + .find(|f| f.identity.declaration.name == "choose") + .unwrap(); + assert!( + program + .resource_source + .exported_function_ids() + .contains(&choose.id) + ); + assert!( + !program + .resource_source + .required_only_function_ids() + .contains(&choose.id) + ); + } + } +} + +#[test] +fn materialized_float_authentication_retains_nan_payload_and_signed_zero_bits() { + let span = Span::new(FileId::new(0), 1, 2); + let a = Expression { + kind: E::Float(f64::from_bits(0x7ff8_0000_0000_0001)), + ty: TypeInterner::FLOAT64, + span, + }; + let mut b = a.clone(); + assert!(expression_equal(&a, &b)); + b.kind = E::Float(f64::from_bits(0x7ff8_0000_0000_0002)); + assert!(!expression_equal(&a, &b)); + b.kind = E::Float(0.0); + let mut c = b.clone(); + c.kind = E::Float(-0.0); + assert!(!expression_equal(&b, &c)); +} + +#[test] +fn actual_generic_and_nested_scope_required_nodes_join_full_context() { + let source = include_str!( + "../../jett_comptime/src/resource_execution/fixtures/27_original_generic_scoped_required.jett" + ); + for release in [false, true] { + let checked = checked(source, release); + let values = required(&checked); + let mut program = crate::lower_checked_resource_program(&checked).unwrap(); + materialize_checked_required_values(&mut program, &values, &checked.checked().interner) + .unwrap(); + let rows = program.resource_source.required_materializations(); + let generic = rows + .iter() + .find(|row| row.proof.generic_index().is_some()) + .expect("actual generic body required value"); + let scoped = rows + .iter() + .find(|row| row.proof.scoped_bindings().len() == 2) + .expect("actual two-scope required value"); + for row in [generic, scoped] { + assert!(matches!(&row.current().kind, E::String(value) if value == "int64")); + let function = program + .resource_source + .functions() + .iter() + .find(|function| function.id == row.function()) + .unwrap(); + assert!(proof_matches( + &row.proof, + function, + row.original(), + &checked + )); + let mut copied = row.original().clone(); + if let E::Comptime { source_span, .. } = &mut copied.kind { + source_span.start += 1; + } + assert!(!proof_matches(&row.proof, function, &copied, &checked)); + } + let function = program + .resource_source + .functions() + .iter() + .find(|function| function.id == generic.function()) + .unwrap(); + let mut changed = function.clone(); + changed + .identity + .specialization + .type_info_kinds + .push((0, "forged".into())); + assert!(!proof_matches( + &generic.proof, + &changed, + generic.original(), + &checked + )); + let function = program + .resource_source + .functions() + .iter() + .find(|function| function.id == scoped.function()) + .unwrap(); + for field in [0, 1, 2] { + let mut changed = scoped.original().clone(); + let E::Comptime { scopes, .. } = &mut changed.kind else { + panic!("retained required node"); + }; + match field { + 0 => scopes[0].index += 1, + 1 => scopes[0].owner.start += 1, + _ => { + scopes[0].selection = + jett_typecheck::CheckedComptimeTypeSelection::ReflectedIteration(99) + } + } + assert!(!proof_matches(&scoped.proof, function, &changed, &checked)); + } + } +} + +#[test] +fn mutual_required_body_uses_retained_executable_declaration() { + let source = "namespace app\nmutual:\n function first(flag: bool) returns int64\n function second() returns int64\nfunction first(flag: bool) returns int64:\n int64 literal = comptime 7\n if flag:\n return second()\n return literal\nfunction second() returns int64:\n return first(false)\nexport function main(net: Network) returns nothing:\n int64 selected = first(true)\n return nothing\n"; + for release in [false, true] { + let checked = checked(source, release); + let values = required(&checked); + let proofs = values.checked_required_values(&checked).unwrap(); + let [proof] = proofs.as_slice() else { + panic!("one closed required literal in the mutual body"); + }; + let CheckedRequiredOwner::Function { + definition, + declaration, + } = proof.owner() + else { + panic!("actual required function owner"); + }; + assert_ne!( + checked.resolved().scope_table.def(definition).span, + declaration, + "resolver keeps the earlier mutual signature; proof retains the executable body" + ); + let mut program = crate::lower_checked_resource_program(&checked).unwrap(); + materialize_checked_required_values(&mut program, &values, &checked.checked().interner) + .unwrap(); + let [row] = program.resource_source.required_materializations() else { + panic!("one materialized required literal"); + }; + assert!(matches!(row.current().kind, E::Int(7))); + assert_eq!( + program + .functions + .iter() + .find(|function| function.id == row.function()) + .unwrap() + .source_definition, + Some(definition) + ); + } +} diff --git a/crates/jett_hir/src/resource_source.rs b/crates/jett_hir/src/resource_source.rs new file mode 100644 index 00000000..583f99e4 --- /dev/null +++ b/crates/jett_hir/src/resource_source.rs @@ -0,0 +1,372 @@ +//! Original checked HIR and type meanings; raw lowering cannot mint this archive. +use crate::{ + Expression, Function, FunctionId, FunctionIdentity, Program, ReflectedFieldDispatchProof, + ReflectedTypeArm, ResourceHookRef, ResourceManifest, +}; +use jett_comptime::CheckedRequiredValue; +use jett_typecheck::CheckedResourceProgram; +use jett_types::{Type, TypeId, TypeInterner}; +use std::{collections::HashMap, sync::Arc}; + +#[derive(Debug)] +enum Nominal { + Struct(jett_types::StructId, jett_types::StructDef), + Bitfield(jett_types::BitfieldId, jett_types::BitfieldDef), + Enum(jett_types::EnumId, jett_types::EnumDef), + Interface(jett_types::InterfaceId, jett_types::InterfaceDef), + Actor(jett_types::ActorId, jett_types::ActorDef), + Machine(jett_types::MachineId, jett_types::MachineDef), +} +impl Nominal { + fn capture(types: &TypeInterner, ty: &Type) -> Option { + Some(match ty { + Type::Struct(id) => Self::Struct(*id, types.resolve_struct(*id).clone()), + Type::Bitfield(id) => Self::Bitfield(*id, types.resolve_bitfield(*id).clone()), + Type::Enum(id) => Self::Enum(*id, types.resolve_enum(*id).clone()), + Type::Interface(id) => Self::Interface(*id, types.resolve_interface(*id).clone()), + Type::Actor(id) => Self::Actor(*id, types.resolve_actor(*id).clone()), + Type::Machine(id) | Type::MachineState { machine: id, .. } => { + Self::Machine(*id, types.resolve_machine(*id).clone()) + } + _ => return None, + }) + } + fn unchanged(&self, types: &TypeInterner) -> bool { + match self { + Self::Struct(id, original) => types.resolve_struct(*id) == original, + Self::Bitfield(id, original) => types.resolve_bitfield(*id) == original, + Self::Enum(id, original) => types.resolve_enum(*id) == original, + Self::Interface(id, original) => types.resolve_interface(*id) == original, + Self::Actor(id, original) => types.resolve_actor(*id) == original, + Self::Machine(id, original) => types.resolve_machine(*id) == original, + } + } +} +#[derive(Debug)] +struct TypeRow { + ty: TypeId, + original: Type, + arguments: Vec, + nominal: Option, +} +#[derive(Debug)] +struct Original { + checked: Arc, + manifest: ResourceManifest, + functions: Vec, + equality_methods: HashMap, + types: Vec, + exported: Vec, + reflected_fields: Vec, +} + +/// A checked required occurrence and the expression derived from its opaque value. +#[derive(Clone)] +pub struct RequiredMaterialization { + pub(crate) function: FunctionId, + pub(crate) identity: FunctionIdentity, + pub(crate) original: Expression, + pub(crate) current: Expression, + pub(crate) hook: Option, + pub(crate) proof: CheckedRequiredValue, +} +impl std::fmt::Debug for RequiredMaterialization { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RequiredMaterialization") + .field("function", &self.function) + .field("original", &self.original) + .field("current", &self.current) + .finish() + } +} +impl RequiredMaterialization { + pub fn function(&self) -> FunctionId { + self.function + } + pub fn identity(&self) -> &FunctionIdentity { + &self.identity + } + pub fn original(&self) -> &Expression { + &self.original + } + pub fn current(&self) -> &Expression { + &self.current + } + pub fn hook(&self) -> Option<&ResourceHookRef> { + self.hook.as_ref() + } +} +#[derive(Debug)] +pub(crate) struct Materialized { + pub(crate) functions: Vec, + pub(crate) values: Vec, + pub(crate) required_only: Vec, +} +/// Clone shares immutable records; empty/default supplies no Source authority. +#[derive(Clone, Default)] +pub struct ResourceSourceArchive { + data: Option>, + execution: Option>, + pub(crate) reflected_values: Option>, +} +impl std::fmt::Debug for ResourceSourceArchive { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ResourceSourceArchive") + .field("authenticated", &self.data.is_some()) + .finish() + } +} +impl PartialEq for ResourceSourceArchive { + fn eq(&self, other: &Self) -> bool { + match (&self.data, &other.data) { + (None, None) => true, + (Some(a), Some(b)) => { + Arc::ptr_eq(a, b) + && match (&self.reflected_values, &other.reflected_values) { + (None, None) => true, + (Some(a), Some(b)) => Arc::ptr_eq(a, b), + _ => false, + } + && match (&self.execution, &other.execution) { + (None, None) => true, + (Some(a), Some(b)) => Arc::ptr_eq(a, b), + _ => false, + } + } + _ => false, + } + } +} +impl Eq for ResourceSourceArchive {} +impl ResourceSourceArchive { + pub fn empty() -> Self { + Self::default() + } + // Called only at successful completion of the original checked lowering. + pub(crate) fn checked( + program: &Program, + checked: &Arc, + types: &TypeInterner, + ) -> Self { + let rows = types + .type_ids() + .map(|ty| TypeRow { + ty, + original: types.resolve(ty).clone(), + arguments: types.nominal_type_arguments(ty).to_vec(), + nominal: Nominal::capture(types, types.resolve(ty)), + }) + .collect(); + Self { + data: Some(Arc::new(Original { + checked: checked.clone(), + manifest: program.resource_manifest.clone(), + functions: program.functions.clone(), + equality_methods: program.equality_methods.clone(), + types: rows, + exported: super::resource_materialization::exported_functions(program, checked), + reflected_fields: super::reflected_field_sources::capture(program, checked), + })), + execution: None, + reflected_values: None, + } + } + /// Original checked functions stay immutable; this view contains only checked replacements. + pub fn execution_functions(&self) -> &[Function] { + if let Some(view) = &self.execution { + return view.functions.as_slice(); + } + self.reflected_values.as_ref().map_or_else( + || self.functions(), + |transition| transition.current_functions(), + ) + } + /// Exact checked session identity; equal source text cannot replace it. + pub fn belongs_to(&self, checked: &Arc) -> bool { + self.data + .as_ref() + .is_some_and(|data| Arc::ptr_eq(&data.checked, checked)) + } + pub fn required_materializations(&self) -> &[RequiredMaterialization] { + self.execution + .as_ref() + .map_or(&[], |view| view.values.as_slice()) + } + pub fn required_only_function_ids(&self) -> &[FunctionId] { + self.execution + .as_ref() + .map_or(&[], |view| view.required_only.as_slice()) + } + pub fn exported_function_ids(&self) -> &[FunctionId] { + self.data + .as_ref() + .map_or(&[], |data| data.exported.as_slice()) + } + pub fn materialized_hook( + &self, + function: FunctionId, + original: &Expression, + current: &Expression, + ) -> Option<&ResourceHookRef> { + self.required_materializations() + .iter() + .find(|row| { + row.function == function + && super::resource_materialization::expression_equal(&row.original, original) + && super::resource_materialization::expression_equal(&row.current, current) + })? + .hook() + } + pub(crate) fn checked_program(&self) -> Option<&Arc> { + self.data.as_ref().map(|data| &data.checked) + } + pub(crate) fn install_materialized(&mut self, view: Materialized) { + self.execution = Some(Arc::new(view)); + } + pub fn functions(&self) -> &[Function] { + self.data + .as_ref() + .map_or(&[], |data| data.functions.as_slice()) + } + pub fn manifest(&self) -> Option<&ResourceManifest> { + self.data.as_ref().map(|data| &data.manifest) + } + pub fn equality_methods(&self) -> Option<&HashMap> { + self.data.as_ref().map(|data| &data.equality_methods) + } + /// Readonly eligibility for an original reflected-field proof-only witness. + /// This does not admit operands or grant a Resource execution family or scope. + pub fn has_reflected_field_dispatch(&self, function: FunctionId) -> bool { + self.data.as_ref().is_some_and(|data| { + data.reflected_fields.iter().any(|proof| { + proof.function() == function + && proof.belongs_to(&data.checked) + && proof.validate_original() + }) + }) + } + + /// Exact original dispatch operands select a constructor-only Source proof. + /// Runtime TypeInfo values, field names, and caller-made arms grant no authority. + pub fn get_reflected_field_dispatch( + &self, + function: FunctionId, + type_info: &Expression, + arms: &[ReflectedTypeArm], + ) -> Option<&ReflectedFieldDispatchProof> { + let data = self.data.as_ref()?; + let mut matches = data.reflected_fields.iter().filter(|proof| { + proof.function() == function + && proof.belongs_to(&data.checked) + && proof.matches(data.functions.as_slice(), type_info, arms) + }); + let proof = matches.next()?; + matches.next().is_none().then_some(proof) + } + + /// Native normalization starts only from the unchanged checked HIR view. + pub fn validate_current(&self, program: &Program, types: &TypeInterner) -> Result<(), String> { + self.validate_types(types)?; + let data = self + .data + .as_ref() + .ok_or("Resource ownership has no original checked HIR archive")?; + if self != &program.resource_source + || self.manifest() != Some(&program.resource_manifest) + || self.equality_methods() != Some(&program.equality_methods) + || !super::resource_materialization::functions_equal( + self.execution_functions(), + &program.functions, + ) + || data + .reflected_fields + .iter() + .any(|proof| !proof.belongs_to(&data.checked) || !proof.validate_original()) + { + return Err( + "Resource original checked HIR or reflected-field Source proof changed".into(), + ); + } + Ok(()) + } + + /// Additional canonical IDs cannot alter any original type's meaning. + /// Structural/backend type validation remains mandatory before this check. + pub fn validate_types(&self, types: &TypeInterner) -> Result<(), String> { + let data = self + .data + .as_ref() + .ok_or("Resource ownership has no original checked HIR archive")?; + if types.len() < data.types.len() { + return Err("Resource original checked type archive lost an original TypeId".into()); + } + for row in &data.types { + if types.resolve(row.ty) != &row.original + || types.nominal_type_arguments(row.ty) != row.arguments.as_slice() + { + return Err( + "Resource original checked type meaning or nominal metadata changed".into(), + ); + } + if !types.contains_nominal_definition(types.resolve(row.ty)) { + return Err( + "Resource original checked type archive has a missing nominal definition" + .into(), + ); + } + if row + .nominal + .as_ref() + .is_some_and(|nominal| !nominal.unchanged(types)) + { + return Err( + "Resource original checked type meaning or nominal metadata changed".into(), + ); + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use jett_common::{FileId, SourceOrigin}; + #[test] + fn original_unused_nominal_row_with_missing_table_returns_error() { + for release in [false, true] { + let file = FileId::new(0); + let original = Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + jett_parser::parse( + "function main() returns nothing:\n return nothing\n", + file, + ), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions { release }, + ) + .unwrap(), + ); + let program = crate::lower_checked_resource_program(&original).unwrap(); + let types = &original.checked().interner; + assert!( + types + .type_ids() + .any(|ty| matches!(types.resolve(ty), Type::Enum(_))), + "original unused reflection enum row" + ); + let mut missing = TypeInterner::new(); + for ty in types.type_ids() { + assert_eq!(missing.intern(types.resolve(ty).clone()), ty); + } + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + program.resource_source.validate_types(&missing) + })); + assert_eq!( + result.unwrap().unwrap_err(), + "Resource original checked type archive has a missing nominal definition" + ); + } + } +} diff --git a/crates/jett_hir/src/snapshots/jett_hir__tests__hir_core.snap b/crates/jett_hir/src/snapshots/jett_hir__tests__hir_core.snap index 779e48ee..cf3f2aef 100644 --- a/crates/jett_hir/src/snapshots/jett_hir__tests__hir_core.snap +++ b/crates/jett_hir/src/snapshots/jett_hir__tests__hir_core.snap @@ -4,6 +4,12 @@ assertion_line: 7203 expression: program --- Program { + resource_manifest: ResourceManifest { + data: None, + }, + resource_source: ResourceSourceArchive { + authenticated: false, + }, functions: [ Function { id: FunctionId( diff --git a/crates/jett_hir/src/snapshots/jett_hir__tests__hir_enum_handle.snap b/crates/jett_hir/src/snapshots/jett_hir__tests__hir_enum_handle.snap index 57b08091..867fe3e3 100644 --- a/crates/jett_hir/src/snapshots/jett_hir__tests__hir_enum_handle.snap +++ b/crates/jett_hir/src/snapshots/jett_hir__tests__hir_enum_handle.snap @@ -4,6 +4,12 @@ assertion_line: 7225 expression: program --- Program { + resource_manifest: ResourceManifest { + data: None, + }, + resource_source: ResourceSourceArchive { + authenticated: false, + }, functions: [ Function { id: FunctionId( diff --git a/crates/jett_hir/src/type_validation.rs b/crates/jett_hir/src/type_validation.rs index 46cda8be..48dff1a4 100644 --- a/crates/jett_hir/src/type_validation.rs +++ b/crates/jett_hir/src/type_validation.rs @@ -22,6 +22,7 @@ pub fn validate_backend_types( interner: &TypeInterner, ) -> Result<(), Vec> { let mut validator = BackendTypeValidator { + resource_manifest: &program.resource_manifest, interner, functions: &program.functions, ownership_locals: Vec::new(), @@ -35,6 +36,19 @@ pub fn validate_backend_types( iteration_binders: HashSet::new(), errors: Vec::new(), }; + if let Err(message) = program.resource_manifest.validate(interner) { + validator.error(Span::new(jett_common::FileId::new(0), 0, 0), message); + } + if program.resource_source.checked_program().is_some() + && program + .functions + .iter() + .any(|function| !crate::reflected_callable_plans(function).is_empty()) + { + if let Err(message) = program.resource_source.validate_types(interner) { + validator.error(Span::new(jett_common::FileId::new(0), 0, 0), message); + } + } for function in &program.functions { validator.function(function); } @@ -86,6 +100,7 @@ enum DefinitionKey { } struct BackendTypeValidator<'a> { + resource_manifest: &'a crate::ResourceManifest, interner: &'a TypeInterner, functions: &'a [Function], ownership_locals: Vec, @@ -351,7 +366,8 @@ impl BackendTypeValidator<'_> { fn expression(&mut self, expression: &Expression, function_name: &str) { if let ExpressionKind::Call { ownership, .. } | ExpressionKind::IndirectCall { ownership, .. } - | ExpressionKind::Intrinsic { ownership, .. } = &expression.kind + | ExpressionKind::Intrinsic { ownership, .. } + | ExpressionKind::ResourceInvoke { ownership, .. } = &expression.kind { let mut metadata_types = Vec::new(); ownership.metadata_types(|ty| metadata_types.push(ty)); @@ -447,6 +463,27 @@ impl BackendTypeValidator<'_> { ); } } + ExpressionKind::ResourceHookValue { hook } => { + if !self.resource_manifest.contains_hook(hook) + || expression.ty != hook.function_type() + { + self.error( + expression.span, + "Resource descriptor differs from its original manifest or signature", + ); + } + } + ExpressionKind::ResourceInvoke { hook, args, .. } => { + if !self.resource_manifest.contains_hook(hook) { + self.error( + expression.span, + "Resource invocation belongs to another original manifest", + ); + } + for argument in args { + self.expression(argument, function_name); + } + } ExpressionKind::Call { args, .. } => { for argument in args { self.expression(argument, function_name); @@ -598,6 +635,21 @@ impl BackendTypeValidator<'_> { } } HandleKind::Result | HandleKind::Optional => {} + HandleKind::ReflectedReturn { plan, .. } => { + self.type_id( + plan.source_type, + expression.span, + "reflected callable source return".into(), + ); + self.type_id( + plan.requested_type, + expression.span, + "reflected callable requested return".into(), + ); + if plan.source_type != target.ty || plan.requested_type != expression.ty { + self.error(expression.span, "reflected callable return types changed"); + } + } } self.block(failure, function_name); } @@ -812,8 +864,12 @@ impl BackendTypeValidator<'_> { | Type::Nothing | Type::TypeConstruction | Type::Never - | Type::Capability(_) - | Type::Resource(_) => {} + | Type::Capability(_) => {} + Type::Resource(_) => { + if !self.resource_manifest.contains_type(type_id) { + self.error(span, "Resource type requires the original checked manifest; raw HIR lowering cannot authorize its layout"); + } + } } } @@ -1009,6 +1065,8 @@ mod tests { fn program_with(return_type: TypeId, statements: Vec) -> Program { let span = test_span(); Program { + resource_manifest: crate::ResourceManifest::empty(), + resource_source: crate::ResourceSourceArchive::empty(), equality_methods: Default::default(), functions: vec![Function { id: FunctionId(0), @@ -1203,6 +1261,7 @@ mod tests { let statement = Statement { kind: StatementKind::Expression(Expression { kind: ExpressionKind::Comptime { + scopes: Vec::new(), source_span: span, value: Box::new(Expression { kind: ExpressionKind::Int(7), diff --git a/crates/jett_mir/Cargo.toml b/crates/jett_mir/Cargo.toml index a99ebd84..861df99c 100644 --- a/crates/jett_mir/Cargo.toml +++ b/crates/jett_mir/Cargo.toml @@ -12,6 +12,7 @@ jett_typecheck.workspace = true jett_types.workspace = true [dev-dependencies] +jett_comptime.workspace = true jett_diagnostics.workspace = true jett_parser.workspace = true jett_resolve.workspace = true diff --git a/crates/jett_mir/src/breakpoint_regions.rs b/crates/jett_mir/src/breakpoint_regions.rs index c4a69f77..b7ec2f73 100644 --- a/crates/jett_mir/src/breakpoint_regions.rs +++ b/crates/jett_mir/src/breakpoint_regions.rs @@ -1024,6 +1024,20 @@ pub(super) fn blocks_equal(left: &[BasicBlock], right: &[BasicBlock]) -> bool { .zip(right) .all(|(left, right)| block_equal(left, right)) } +pub(super) fn expressions_equal(left: &Expression, right: &Expression) -> bool { + let (mut left, mut right) = (left.clone(), right.clone()); + let (mut left_bits, mut right_bits) = (Vec::new(), Vec::new()); + float_expression(&mut left, &mut left_bits); + float_expression(&mut right, &mut right_bits); + left == right && left_bits == right_bits +} +pub(super) fn hir_blocks_equal(left: &hir::Block, right: &hir::Block) -> bool { + let (mut left, mut right) = (left.clone(), right.clone()); + let (mut left_bits, mut right_bits) = (Vec::new(), Vec::new()); + float_hir_block(&mut left, &mut left_bits); + float_hir_block(&mut right, &mut right_bits); + left == right && left_bits == right_bits +} fn block_equal(left: &BasicBlock, right: &BasicBlock) -> bool { let (mut left, mut right) = (left.clone(), right.clone()); let (mut left_bits, mut right_bits) = (Vec::new(), Vec::new()); @@ -1035,7 +1049,12 @@ fn block_equal(left: &BasicBlock, right: &BasicBlock) -> bool { fn float_block(block: &mut BasicBlock, bits: &mut Vec) { for statement in &mut block.statements { match &mut statement.kind { - StatementKind::Let { value, .. } + StatementKind::ResourceConstructor(crate::ResourceConstructorNode::Stage { + value, + .. + }) + | StatementKind::ResourceCall(ResourceCallNode::Stage { value, .. }) + | StatementKind::Let { value, .. } | StatementKind::BeginCallView { value, .. } | StatementKind::CheckRefinement { call: value, .. } | StatementKind::Evaluate(value) @@ -1055,11 +1074,23 @@ fn float_block(block: &mut BasicBlock, bits: &mut Vec) { float_expression(value, bits); } } - StatementKind::OpenCallOwnerGeneration { .. } + StatementKind::ResourceCall( + ResourceCallNode::Begin { .. } + | ResourceCallNode::Invoke { .. } + | ResourceCallNode::End { .. }, + ) + | StatementKind::ResourceConstructor( + crate::ResourceConstructorNode::Begin { .. } + | crate::ResourceConstructorNode::Commit { .. } + | crate::ResourceConstructorNode::End { .. }, + ) + | StatementKind::ResourceLexicalExit(_) + | StatementKind::OpenCallOwnerGeneration { .. } | StatementKind::ReplaceCallOwnerGeneration { .. } | StatementKind::CloseCallOwnerGeneration { .. } | StatementKind::EndCallView { .. } | StatementKind::ReflectedContainerReady { .. } + | StatementKind::ReflectedValueReady { .. } | StatementKind::SequenceLength { .. } | StatementKind::SequenceGet { .. } | StatementKind::IterationBorrow { .. } @@ -1124,6 +1155,7 @@ fn float_expression(value: &mut Expression, bits: &mut Vec) { | E::View(value) | E::Clone(value) => float_expression(value, bits), E::Call { args, .. } + | E::ResourceInvoke { args, .. } | E::Intrinsic { args, .. } | E::ActorSpawn { args, .. } | E::StructConstruct { fields: args, .. } @@ -1177,6 +1209,7 @@ fn float_expression(value: &mut Expression, bits: &mut Vec) { | E::Nothing | E::Local(_) | E::Constant { .. } + | E::ResourceHookValue { .. } | E::FunctionRef(_) | E::ClosureRef { .. } | E::OptionalNone diff --git a/crates/jett_mir/src/call_ownership.rs b/crates/jett_mir/src/call_ownership.rs index 18adc816..d99760ab 100644 --- a/crates/jett_mir/src/call_ownership.rs +++ b/crates/jett_mir/src/call_ownership.rs @@ -8,7 +8,10 @@ use jett_hir::{self as hir, Expression, ExpressionKind as E, LocalId}; use jett_typecheck::{CheckedCallerEffect, CheckedOwnershipContext}; use jett_types::{Type, TypeId, TypeInterner}; -use crate::{BlockId, Function, Program, StatementKind as S, TerminatorKind as T}; +use crate::{ + BlockId, Function, Program, ResourceCallNode, ResourceConstructorNode, StatementKind as S, + TerminatorKind as T, +}; #[cfg(test)] mod tests; @@ -35,6 +38,7 @@ pub struct ArgumentAcquisition { /// Pointer equality is only a cache lookup, never ownership authority. #[derive(Debug)] pub struct CallerAcquisitions<'a> { + pub(crate) resource_pending: bool, owners: BTreeMap, calls: Vec<(&'a Expression, Vec)>, operands: Vec<(&'a Expression, LocalId)>, @@ -48,6 +52,7 @@ pub struct CallerAcquisitions<'a> { impl Default for CallerAcquisitions<'_> { fn default() -> Self { Self { + resource_pending: false, owners: BTreeMap::new(), calls: Vec::new(), operands: Vec::new(), @@ -198,6 +203,7 @@ pub fn validate_function<'a>( function: &'a Function, types: &TypeInterner, ) -> Result, String> { + program.resource_manifest.validate(types)?; ProgramValidation::new(program).validate_function(function, types) } @@ -242,7 +248,12 @@ impl<'p> ProgramValidation<'p> { function: &'a Function, types: &TypeInterner, ) -> Result, String> { - validate_function_with_signatures(function, types, &self.signatures) + validate_function_with_signatures( + function, + types, + &self.signatures, + &self._program.resource_manifest, + ) } } @@ -250,10 +261,11 @@ fn validate_function_with_signatures<'a>( function: &'a Function, types: &TypeInterner, signatures: &[hir::Function], + manifest: &hir::ResourceManifest, ) -> Result, String> { // Fresh exact site proof precedes lexical context selection. let breakpoint_sites = crate::breakpoint_regions::validate(function, types)?; - let mut validator = Validator::new(function, types, signatures)?; + let mut validator = Validator::new(function, types, signatures, manifest)?; let context = if function.debug_kind == hir::FunctionDebugKind::Inline { CheckedOwnershipContext::Ordinary } else { @@ -300,6 +312,7 @@ struct Validator<'a, 't, 's> { function: &'a Function, types: &'t TypeInterner, signatures: &'s [hir::Function], + manifest: &'s hir::ResourceManifest, locals: Vec, definitions: BTreeMap>>, predecessors: Vec>, @@ -317,7 +330,17 @@ impl<'a, 't, 's> Validator<'a, 't, 's> { function: &'a Function, types: &'t TypeInterner, signatures: &'s [hir::Function], + manifest: &'s hir::ResourceManifest, ) -> Result { + manifest.validate_type(types, function.return_type)?; + for parameter in &function.params { + manifest.validate_type(types, parameter.ty)?; + } + for local in &function.locals { + manifest.validate_type(types, local.ty)?; + manifest.validate_type(types, local.debug_ty)?; + } + crate::ordinary_borrowed_sums::validate(function, types)?; let iteration_scopes = crate::iteration_views::scopes(function, types)?; let locals = function .locals @@ -349,12 +372,20 @@ impl<'a, 't, 's> Validator<'a, 't, 's> { return Err("call ownership block IDs are not dense".into()); } for (index, statement) in block.statements.iter().enumerate() { + if let S::ResourceConstructor(node) = &statement.kind { + function.resource_constructor(node.constructor())?; + } let site = Site { block: block.id, statement: index, }; match &statement.kind { - S::Let { local, value } => { + S::ResourceCall(ResourceCallNode::Stage { + value, + ordinary: Some(local), + .. + }) + | S::Let { local, value } => { define(*local, site, statement.span, Definition::Let(value)) } S::BeginCallView { local, value } => { @@ -364,16 +395,41 @@ impl<'a, 't, 's> Validator<'a, 't, 's> { source, target, success, - } => define( - *target, - site, - statement.span, - Definition::SumTake { - source: *source, - success: *success, - }, - ), - S::CheckRefinement { local, .. } + } => { + let ordinary = + function.ordinary_borrowed_sum_projection(block.id, index)?; + let borrowed = function.resource_borrowed_sum_projection( + block.id, + crate::ResourcePosition::Statement(index), + )?; + let definition = if let Some(row) = ordinary { + if row.source() != *source + || (*success && row.output() != *target) + || (!*success && row.error() != Some(*target)) + { + return Err("call ownership ordinary borrowed Handle changed its extraction headers".into()); + } + Definition::Other + } else if let Some(row) = borrowed { + if row.source() != *source || (*success && row.output() != *target) { + return Err("call ownership borrowed Handle changed its current extraction headers".into()); + } + // A guarded view projection is never an owning producer. + Definition::Other + } else { + Definition::SumTake { + source: *source, + success: *success, + } + }; + define(*target, site, statement.span, definition); + } + S::ResourceCall(ResourceCallNode::Invoke { output: local, .. }) + | S::ResourceConstructor(ResourceConstructorNode::Commit { + output: local, + .. + }) + | S::CheckRefinement { local, .. } | S::SequenceLength { target: local, .. } | S::SequenceGet { target: local, .. } | S::SumTag { target: local, .. } => { @@ -464,6 +520,7 @@ impl<'a, 't, 's> Validator<'a, 't, 's> { function, types, signatures, + manifest, locals, definitions, predecessors, @@ -483,6 +540,36 @@ impl<'a, 't, 's> Validator<'a, 't, 's> { site: Site, context: CheckedOwnershipContext, ) -> Result<(), String> { + if let E::ResourceInvoke { + hook, + ownership, + args, + evaluation_order, + .. + } = &expression.kind + { + if !self.manifest.contains_hook(hook) { + return Err( + "Resource invocation belongs to another original checked manifest".into(), + ); + } + let locals = self.locals_at(site); + hir::validate_hir_invocation( + self.signatures, + &locals, + expression, + self.types, + context, + )?; + let hir::CallOwnership::Source(source) = ownership else { + return Err("Resource invocation cannot use Generated authority".into()); + }; + if source.arguments.len() != args.len() || evaluation_order.len() != args.len() { + return Err("Resource invocation source order or operand count changed".into()); + } + self.acquisitions.resource_pending = true; + return Ok(()); + } let (ownership, args, order) = match &expression.kind { E::Call { ownership, @@ -551,6 +638,17 @@ impl<'a, 't, 's> Validator<'a, 't, 's> { false, )?; hir::validate_invocation_target(self.signatures, expression, self.types, context)?; + let resource_original = crate::resource_ownership::original_call_at( + self.function, + self.manifest, + self.types, + expression, + site.block, + site.statement, + )?; + if resource_original { + self.acquisitions.resource_pending = true; + } match ownership { hir::CallOwnership::Source(source) => { let mut original = Vec::new(); @@ -559,7 +657,14 @@ impl<'a, 't, 's> Validator<'a, 't, 's> { let acquisition = self.source_acquisition(argument, value.ty); match argument.staging { hir::ArgumentStaging::Original => { - if argument.effect == CheckedCallerEffect::RelinquishOwned + let resource_actual = resource_original + && crate::resource_ownership::original_resource_actual( + self.manifest, + self.types, + argument.actual_type, + ); + if (argument.effect == CheckedCallerEffect::RelinquishOwned + && !resource_actual) || (argument.effect == CheckedCallerEffect::ObserveData && argument.physical_access == jett_typecheck::CheckedCalleeAccess::Owned) @@ -567,7 +672,7 @@ impl<'a, 't, 's> Validator<'a, 't, 's> { return Err("call ownership requires explicit owning staging at this physical boundary".into()); } self.original(value, argument, &source.bridge, site)?; - if consumes(argument.effect) { + if consumes(argument.effect) && !resource_actual { if let Some(binding) = acquisition.binding { self.acquisitions.operands.push((value, binding)); } @@ -2895,7 +3000,9 @@ impl<'a, 't, 's> Validator<'a, 't, 's> { context: CheckedOwnershipContext, ) -> Result<(), String> { match statement { - S::Let { value, .. } + S::ResourceCall(ResourceCallNode::Stage { value, .. }) + | S::ResourceConstructor(ResourceConstructorNode::Stage { value, .. }) + | S::Let { value, .. } | S::BeginCallView { value, .. } | S::Evaluate(value) | S::HandleDefault(value) => self.expression(value, site, context)?, @@ -2949,9 +3056,21 @@ impl<'a, 't, 's> Validator<'a, 't, 's> { site: Site, context: CheckedOwnershipContext, ) -> Result<(), String> { + self.manifest.validate_type(self.types, expression.ty)?; + if let E::ResourceHookValue { hook } = &expression.kind { + if !self.manifest.contains_hook(hook) || expression.ty != hook.function_type() { + return Err( + "Resource descriptor differs from its original checked manifest or signature" + .into(), + ); + } + } self.invocation(expression, site, context)?; match &expression.kind { - E::Call { args, .. } | E::Intrinsic { args, .. } | E::ActorSpawn { args, .. } => { + E::Call { args, .. } + | E::ResourceInvoke { args, .. } + | E::Intrinsic { args, .. } + | E::ActorSpawn { args, .. } => { for value in args { self.expression(value, site, context)?; } @@ -3048,6 +3167,7 @@ impl<'a, 't, 's> Validator<'a, 't, 's> { | E::Nothing | E::Local(_) | E::Constant { .. } + | E::ResourceHookValue { .. } | E::FunctionRef(_) | E::ClosureRef { .. } | E::OptionalNone diff --git a/crates/jett_mir/src/copy_values.rs b/crates/jett_mir/src/copy_values.rs index 3cb665f4..51cc917b 100644 --- a/crates/jett_mir/src/copy_values.rs +++ b/crates/jett_mir/src/copy_values.rs @@ -2,12 +2,48 @@ //! This deliberately rejects unextracted handlers rather than walking hidden //! control flow. Resources and other move-only values need a distinct //! move/borrow/drop analysis before their backend support can be enabled. -use crate::{ControlFlowGraph, Function, StatementKind, TerminatorKind}; +use crate::{ControlFlowGraph, Function, ResourceCallNode, StatementKind, TerminatorKind}; use jett_hir::{Expression, ExpressionKind, IntrinsicId, StringSegment}; use jett_types::{Type, TypeId, TypeInterner}; use std::collections::BTreeSet; type Set = BTreeSet; +#[derive(Clone, Copy)] +struct PlanningContext<'a> { + program: Option<&'a crate::Program>, + companion: Option<&'a crate::resource_ownership::CompanionContext<'a>>, +} +impl<'a> PlanningContext<'a> { + fn is_some(self) -> bool { + self.program.is_some() + } + fn is_none(self) -> bool { + self.program.is_none() + } + fn is_some_and(self, f: impl FnOnce(&'a crate::Program) -> bool) -> bool { + self.program.is_some_and(f) + } + fn and_then(self, f: impl FnOnce(&'a crate::Program) -> Option) -> Option { + self.program.and_then(f) + } + fn carrier_type(self, ty: TypeId) -> bool { + self.companion + .is_some_and(|context| context.carrier_type(ty)) + } + fn resource_type(self, ty: TypeId) -> bool { + self.companion + .is_some_and(|context| context.resource_type(ty)) + } + fn resource_expression(self, value: &Expression) -> bool { + self.companion + .is_some_and(|context| context.resource_expression(value)) + } + fn resource_local(self, local: &jett_hir::Local) -> bool { + self.companion + .is_some_and(|context| context.resource_local(local)) + } +} + #[derive(Debug)] pub struct CopyValuePlan { pub owned_locals: Vec, @@ -30,6 +66,42 @@ impl CopyValuePlan { types: &TypeInterner, program: Option<&crate::Program>, ) -> Result { + Self::analyze_with_context( + function, + types, + PlanningContext { + program, + companion: None, + }, + ) + } + pub(crate) fn analyze_companion( + context: &crate::resource_ownership::CompanionContext<'_>, + ) -> Result { + Self::analyze_with_context( + context.function, + context.types, + PlanningContext { + program: Some(context.program), + companion: Some(context), + }, + ) + } + fn analyze_with_context( + function: &Function, + types: &TypeInterner, + program: PlanningContext<'_>, + ) -> Result { + crate::ordinary_borrowed_sums::validate(function, types)?; + if program.companion.is_none() + && (crate::resource_type_pending(types, function.return_type) + || function + .locals + .iter() + .any(|local| crate::resource_type_pending(types, local.ty))) + { + return Err("pending ResourceOwnershipPlan: ordinary CopyValuePlan cannot plan Resource carriers or descriptors".into()); + } let generation_storage = crate::call_owner_generations::validate(function, types)?; if program.is_none() && !generation_storage.slots().is_empty() { return Err("generation storage requires complete program ownership validation".into()); @@ -47,7 +119,7 @@ impl CopyValuePlan { let mut max_temporaries = 0; for block in &function.blocks { let mut statements = Vec::new(); - for statement in &block.statements { + for (index, statement) in block.statements.iter().enumerate() { let mut reads = Set::new(); let mut temporaries = 0; let mut killed = None; @@ -62,7 +134,8 @@ impl CopyValuePlan { killed = Some(rhs_owner.index() as usize); Some(root.index() as usize) } - StatementKind::ReflectedContainerReady { source, .. } => { + StatementKind::ReflectedContainerReady { source, .. } + | StatementKind::ReflectedValueReady { source, .. } => { reads.insert(source.index() as usize); None } @@ -94,14 +167,27 @@ impl CopyValuePlan { reads.insert(source.index() as usize); if matches!(statement.kind, StatementKind::SumTake { .. }) { let ty = function.locals[target.index() as usize].ty; - temporaries += usize::from( - crate::move_values::is_copy_owned(types, ty) - || crate::move_values::is_linear(types, ty), - ); + let borrowed = matches!( + statement.kind, + StatementKind::SumTake { success: true, .. } + ) && function + .ordinary_borrowed_sum_projection(block.id, index)? + .is_some(); + if !borrowed { + temporaries += usize::from( + crate::move_values::is_copy_owned(types, ty) + || crate::move_values::is_linear(types, ty), + ); + } } Some(target.index() as usize) } - StatementKind::Let { local, value } + StatementKind::ResourceCall(ResourceCallNode::Stage { + value, + ordinary: Some(local), + .. + }) + | StatementKind::Let { local, value } | StatementKind::BeginCallView { local, value } => { let borrowed = function .local(*local) @@ -118,6 +204,71 @@ impl CopyValuePlan { )?; Some(local.index() as usize) } + StatementKind::ResourceCall(ResourceCallNode::Stage { + value, + ordinary: None, + .. + }) => { + if program.is_none() { + return Err( + "Resource call stage requires its fresh companion plan".into() + ); + } + visit(value, &mut reads, &mut temporaries, types, program, false)?; + None + } + StatementKind::ResourceCall(ResourceCallNode::Invoke { region, output }) => { + let record = function + .resource_call_region(*region) + .ok_or("Resource call output lacks its sealed region")?; + for actual in record.actuals() { + if let Some(local) = actual.ordinary() { + reads.insert(local.index() as usize); + } + } + let ty = function.locals[output.index() as usize].ty; + temporaries += usize::from( + crate::move_values::is_copy_owned(types, ty) + || crate::move_values::is_linear(types, ty), + ); + Some(output.index() as usize) + } + StatementKind::ResourceCall( + ResourceCallNode::Begin { .. } | ResourceCallNode::End { .. }, + ) => { + if program.is_none() { + return Err( + "Resource call region requires its fresh companion plan".into() + ); + } + None + } + StatementKind::ResourceConstructor(node) => { + if program.is_none() { + return Err( + "Resource constructor requires its fresh companion plan".into() + ); + } + function.resource_constructor(node.constructor())?; + match node { + crate::ResourceConstructorNode::Stage { value, .. } => { + visit(value, &mut reads, &mut temporaries, types, program, false)?; + None + } + crate::ResourceConstructorNode::Commit { output, .. } => { + Some(output.index() as usize) + } + _ => None, + } + } + StatementKind::ResourceLexicalExit(_) => { + if program.is_none() { + return Err( + "Resource lexical exit requires its fresh companion plan".into() + ); + } + None + } StatementKind::EndCallView { local } => { reads.insert(local.index() as usize); killed = Some(local.index() as usize); @@ -351,6 +502,7 @@ impl CopyValuePlan { .iter() .filter(|l| { l.view_source.is_none() + && !program.resource_local(l) && (crate::move_values::is_copy_owned(types, l.ty) || (program.is_some() && crate::move_values::is_linear(types, l.ty) @@ -427,11 +579,21 @@ fn visit( reads: &mut Set, temporaries: &mut usize, types: &TypeInterner, - program: Option<&crate::Program>, + program: PlanningContext<'_>, borrowed: bool, ) -> Result<(), String> { + if let Some(context) = program.companion { + if !context.contains(value) { + return Err( + "Resource companion expression is outside its exact current function".into(), + ); + } + } plan_type(types, value.ty, program)?; - if program.is_some() && crate::move_values::is_linear(types, value.ty) { + if program.is_some() + && !program.resource_expression(value) + && crate::move_values::is_linear(types, value.ty) + { *temporaries += usize::from(match &value.kind { ExpressionKind::Local(_) => !borrowed, ExpressionKind::Call { .. } @@ -489,7 +651,8 @@ fn visit( | ExpressionKind::Call { .. } | ExpressionKind::IndirectCall { .. } | ExpressionKind::Field { .. } - if crate::move_values::is_copy_owned(types, value.ty) => + if !program.resource_expression(value) + && crate::move_values::is_copy_owned(types, value.ty) => { *temporaries += 1 } @@ -569,9 +732,19 @@ fn visit( _ => {} } match &value.kind { + ExpressionKind::ResourceHookValue { .. } if program.companion.is_some() => {}, + ExpressionKind::ResourceInvoke { hook, args, evaluation_order, .. } if program.companion.is_some() => { + let Type::Function { view_params, .. } = types.resolve(hook.function_type()) else { return Err("Resource hook has no exact function signature".into()); }; + for ¶meter in evaluation_order { + visit(&args[parameter], reads, temporaries, types, program, view_params[parameter])?; + } + // Ordinary hook results still own normal runtime temporaries. + *temporaries += usize::from(!program.resource_expression(value) && (crate::move_values::is_copy_owned(types, value.ty) || crate::move_values::is_linear(types, value.ty))); + } + ExpressionKind::ResourceInvoke { .. } | ExpressionKind::ResourceHookValue { .. } => return Err("pending ResourceOwnershipPlan: ordinary expression liveness cannot own Resource operation or descriptor temporaries".into()), ExpressionKind::Local(l) => { reads.insert(l.index() as usize); - if program.is_some() && !borrowed && crate::move_values::is_linear(types, value.ty) { + if program.is_some() && !program.resource_expression(value) && !borrowed && crate::move_values::is_linear(types, value.ty) { // Test predicates clone local owners on each ordinary read; // reserving this slot for all native functions is conservative. *temporaries += 1; @@ -867,17 +1040,13 @@ fn copy_plan_type(types: &TypeInterner, ty: TypeId) -> Result<(), String> { )) } -fn plan_type( - types: &TypeInterner, - ty: TypeId, - program: Option<&crate::Program>, -) -> Result<(), String> { +fn plan_type(types: &TypeInterner, ty: TypeId, program: PlanningContext<'_>) -> Result<(), String> { plan_type_inner(types, ty, program, &mut BTreeSet::new()) } fn plan_type_inner( types: &TypeInterner, ty: TypeId, - program: Option<&crate::Program>, + program: PlanningContext<'_>, seen: &mut BTreeSet, ) -> Result<(), String> { if ty.index() as usize >= types.len() { @@ -886,6 +1055,65 @@ fn plan_type_inner( if !seen.insert(ty.index()) { return Ok(()); } + if program.carrier_type(ty) { + // The exact carrier graph owns these endpoints. Ordinary payload type + // checks remain recursive; no ordinary move/copy/drop permission is issued. + let mut children = Vec::new(); + match types.resolve(ty) { + Type::List(inner) | Type::Refinement { base: inner, .. } | Type::Optional(inner) => { + children.push(*inner) + } + Type::Map(key, value) | Type::Result(key, value) => children.extend([*key, *value]), + Type::Struct(id) => { + children.extend(types.resolve_struct(*id).fields.iter().map(|(_, ty)| *ty)) + } + Type::Enum(id) => children.extend( + types + .resolve_enum(*id) + .variants + .iter() + .flat_map(|variant| variant.fields.iter().map(|(_, ty)| *ty)), + ), + Type::Machine(id) | Type::MachineState { machine: id, .. } => children.extend( + types + .resolve_machine(*id) + .states + .iter() + .flat_map(|state| state.fields.iter().map(|(_, ty)| *ty)), + ), + _ => return Err("carrier companion type has no exact admitted graph family".into()), + } + for child in children { + plan_type_inner(types, child, program, seen)?; + } + return Ok(()); + } + if program.resource_type(ty) { + // The fresh custody proof owns only the carrier. Its ordinary companion + // and signature children keep their existing backend type checks. + match types.resolve(ty) { + Type::Resource(_) => return Ok(()), + Type::Optional(inner) => return plan_type_inner(types, *inner, program, seen), + Type::Result(ok, failure) => { + plan_type_inner(types, *ok, program, seen)?; + return plan_type_inner(types, *failure, program, seen); + } + Type::Function { + params, + return_type, + .. + } => { + for parameter in params { + plan_type_inner(types, *parameter, program, seen)?; + } + return plan_type_inner(types, *return_type, program, seen); + } + _ => return Err( + "pending Resource companion: qualified or aggregate custody type is unsupported" + .into(), + ), + } + } if program.is_some() { match types.resolve(ty) { Type::Secret(inner) | Type::Refinement { base: inner, .. } => { diff --git a/crates/jett_mir/src/generated_functions.rs b/crates/jett_mir/src/generated_functions.rs index facee1f0..5e98ec25 100644 --- a/crates/jett_mir/src/generated_functions.rs +++ b/crates/jett_mir/src/generated_functions.rs @@ -12,10 +12,14 @@ pub fn prepare_native_generated_functions(program: &mut Program, types: &TypeInt return; } for function in &mut program.functions { + let before = function.clone(); let _prepared = if function.debug_kind == hir::FunctionDebugKind::Inline { sequences::prune::unreachable(function) } else { sequences::prune::unused_locals(function) }; + if crate::reflected_callable::prepared(function, &before).is_err() { + *function = before; + } } } diff --git a/crates/jett_mir/src/handlers.rs b/crates/jett_mir/src/handlers.rs index a0f26390..7c7a19e9 100644 --- a/crates/jett_mir/src/handlers.rs +++ b/crates/jett_mir/src/handlers.rs @@ -4,6 +4,9 @@ use jett_hir::{ExpressionKind, HandleKind}; use jett_types::{Type, TypeInterner}; mod reflected; +mod resource_calls; +#[path = "handlers/resource_constructors.rs"] +mod resource_constructors; #[derive(Clone, Copy, PartialEq, Eq)] enum LoweringRequirement { @@ -26,7 +29,11 @@ fn expression_needs_lowering(expression: &Expression, requirement: LoweringRequi let includes_call_ownership = requirement == LoweringRequirement::HandlersAndCallOwnership; match &expression.kind { ExpressionKind::Handle { - kind: HandleKind::Result | HandleKind::Optional | HandleKind::Refinement { .. }, + kind: + HandleKind::Result + | HandleKind::Optional + | HandleKind::Refinement { .. } + | HandleKind::ReflectedReturn { .. }, .. } => true, ExpressionKind::View(value) | ExpressionKind::FunctionAdapter { value, .. } => needs(value), @@ -505,6 +512,13 @@ impl Builder<'_> { order: &[usize], ownership: &mut hir::CallOwnership, ) -> Option> { + if values + .iter() + .any(|value| crate::resource_type_pending(self.types, value.ty)) + { + self.resource_error = Some(LowerError { span: original_call.span, message: "pending ResourceOwnershipPlan: ordinary call-view staging cannot acquire Resource custody".into() }); + return None; + } // A staging attempt changes CFG, locals, handlers and active scopes. // Restore all of them if a later operand cannot prove its acquisition. let checkpoint = self.clone(); @@ -605,200 +619,209 @@ impl Builder<'_> { .begin_scope(original_call, self.types); let mut lowered = values.to_vec(); for &index in order { - if let Some(endpoint) = &handled[index] { - // Keep the complete original conversion and inner Handle header. - // This endpoint already produces an owner; no Clone is inserted. - let value = self.lower_value(endpoint); - if value.ty != endpoint.ty || value.span != endpoint.span { - return None; - } - let owner = self.temporary(value.ty, value.span); - self.push( - StatementKind::Let { - local: owner, - value, - }, - endpoint.span, - ); - let projection = Expression { - kind: ExpressionKind::View(Box::new(Expression { - kind: ExpressionKind::Local(owner), - ty: endpoint.ty, - span: endpoint.span, - })), - ty: values[index].ty, - span: values[index].span, - }; - lowered[index] = - self.stage_checked_call_view(index, None, owner, projection, ownership)?; - continue; - } - if inputs[index] == OrderedCallViewInput::Borrowed - && matches!(ownership, hir::CallOwnership::Generated(_)) - { - lowered[index] = - self.lower_generated_call_view(index, &values[index], ownership)?; - continue; + lowered[index] = self.lower_ordered_call_argument( + index, + &values[index], + handled[index].as_ref(), + inputs[index], + ownership, + )?; + } + Some(lowered) + } + + #[inline(never)] + fn lower_ordered_call_argument( + &mut self, + index: usize, + actual: &Expression, + handled: Option<&Expression>, + input: OrderedCallViewInput, + ownership: &mut hir::CallOwnership, + ) -> Option { + if let Some(endpoint) = handled { + // Keep the complete original conversion and inner Handle header. + // This endpoint already produces an owner; no Clone is inserted. + let value = self.lower_value(endpoint); + if value.ty != endpoint.ty || value.span != endpoint.span { + return None; } - if ownership.source_parameter_effect(index) - == Some(jett_typecheck::CheckedCallerEffect::ObserveData) - && ownership.parameter_physical_access(index) - == Some(jett_typecheck::CheckedCalleeAccess::Owned) - { - let snapshot = observation_snapshot(self.types, &values[index], ownership, index)?; - let value = match snapshot { - ObservationSnapshot::Direct => Expression { - kind: ExpressionKind::Clone(Box::new(self.lower_value(&values[index]))), - ty: values[index].ty, - span: values[index].span, - }, - ObservationSnapshot::Converted { - actual_type, - source_span, - } => self.lower_converted_observation_snapshot( - &values[index], - actual_type, - source_span, - )?, - }; - let owner = self.temporary(value.ty, value.span); - ownership.stage_observation(index, owner).ok()?; - self.push( - StatementKind::Let { - local: owner, - value, - }, - values[index].span, - ); - lowered[index] = Expression { + let owner = self.temporary(value.ty, value.span); + self.push( + StatementKind::Let { + local: owner, + value, + }, + endpoint.span, + ); + let projection = Expression { + kind: ExpressionKind::View(Box::new(Expression { kind: ExpressionKind::Local(owner), - ty: values[index].ty, - span: values[index].span, - }; - continue; - } - if inputs[index] == OrderedCallViewInput::SourceOwningTransfer { - let ExpressionKind::View(original) = &values[index].kind else { - return None; - }; - // Evaluate the full endpoint/producer, not a projected parent. - // An ordinary Let moves a linear owner; no snapshot is inserted. - let value = self.lower_value(original); - let owner = self.temporary(value.ty, value.span); - self.push( - StatementKind::Let { - local: owner, - value, - }, - original.span, - ); - let projection = Expression { - kind: ExpressionKind::View(Box::new(Expression { - kind: ExpressionKind::Local(owner), - ty: original.ty, - span: original.span, - })), - ty: values[index].ty, - span: values[index].span, - }; - lowered[index] = - self.stage_checked_call_view(index, Some(owner), owner, projection, ownership)?; - continue; - } - if inputs[index] == OrderedCallViewInput::Borrowed - && let hir::CallOwnership::Source(source) = ownership - && source.arguments[index].retained_snapshot_type() == Some(values[index].ty) - && can_snapshot_view(self.types, values[index].ty) - { - let endpoint = retained_snapshot_place(&values[index]) - .unwrap_or_else(|| self.lower_value(&values[index])); - let value = Expression { - kind: ExpressionKind::Clone(Box::new(endpoint)), - ty: values[index].ty, - span: values[index].span, - }; - let owner = self.temporary(value.ty, value.span); - self.push( - StatementKind::Let { - local: owner, - value, - }, - values[index].span, - ); - let projection = Expression { - kind: ExpressionKind::View(Box::new(Expression { - kind: ExpressionKind::Local(owner), - ty: values[index].ty, - span: values[index].span, - })), - ty: values[index].ty, - span: values[index].span, - }; - let loan = self.call_view_temporary(projection.ty, owner, projection.span); - ownership.stage_retained_snapshot(index, owner, loan).ok()?; - self.push( - StatementKind::BeginCallView { - local: loan, - value: projection.clone(), - }, - projection.span, - ); - self.call_view_scopes.last_mut()?.push(loan); - lowered[index] = Expression { - kind: ExpressionKind::View(Box::new(Expression { - kind: ExpressionKind::Local(loan), - ty: projection.ty, - span: projection.span, - })), + ty: endpoint.ty, + span: endpoint.span, + })), + ty: actual.ty, + span: actual.span, + }; + return Some(self.stage_checked_call_view(index, None, owner, projection, ownership)?); + } + if input == OrderedCallViewInput::Borrowed + && matches!(ownership, hir::CallOwnership::Generated(_)) + { + return Some(self.lower_generated_call_view(index, actual, ownership)?); + } + if ownership.source_parameter_effect(index) + == Some(jett_typecheck::CheckedCallerEffect::ObserveData) + && ownership.parameter_physical_access(index) + == Some(jett_typecheck::CheckedCalleeAccess::Owned) + { + let snapshot = observation_snapshot(self.types, actual, ownership, index)?; + let value = match snapshot { + ObservationSnapshot::Direct => Expression { + kind: ExpressionKind::Clone(Box::new(self.lower_value(actual))), + ty: actual.ty, + span: actual.span, + }, + ObservationSnapshot::Converted { + actual_type, + source_span, + } => self.lower_converted_observation_snapshot(actual, actual_type, source_span)?, + }; + let owner = self.temporary(value.ty, value.span); + ownership.stage_observation(index, owner).ok()?; + self.push( + StatementKind::Let { + local: owner, + value, + }, + actual.span, + ); + return Some(Expression { + kind: ExpressionKind::Local(owner), + ty: actual.ty, + span: actual.span, + }); + } + if input == OrderedCallViewInput::SourceOwningTransfer { + let ExpressionKind::View(original) = &actual.kind else { + return None; + }; + // Evaluate the full endpoint/producer, not a projected parent. + // An ordinary Let moves a linear owner; no snapshot is inserted. + let value = self.lower_value(original); + let owner = self.temporary(value.ty, value.span); + self.push( + StatementKind::Let { + local: owner, + value, + }, + original.span, + ); + let projection = Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(owner), + ty: original.ty, + span: original.span, + })), + ty: actual.ty, + span: actual.span, + }; + return Some(self.stage_checked_call_view( + index, + Some(owner), + owner, + projection, + ownership, + )?); + } + if input == OrderedCallViewInput::Borrowed + && let hir::CallOwnership::Source(source) = ownership + && source.arguments[index].retained_snapshot_type() == Some(actual.ty) + && can_snapshot_view(self.types, actual.ty) + { + let endpoint = + retained_snapshot_place(actual).unwrap_or_else(|| self.lower_value(actual)); + let value = Expression { + kind: ExpressionKind::Clone(Box::new(endpoint)), + ty: actual.ty, + span: actual.span, + }; + let owner = self.temporary(value.ty, value.span); + self.push( + StatementKind::Let { + local: owner, + value, + }, + actual.span, + ); + let projection = Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(owner), + ty: actual.ty, + span: actual.span, + })), + ty: actual.ty, + span: actual.span, + }; + let loan = self.call_view_temporary(projection.ty, owner, projection.span); + ownership.stage_retained_snapshot(index, owner, loan).ok()?; + self.push( + StatementKind::BeginCallView { + local: loan, + value: projection.clone(), + }, + projection.span, + ); + self.call_view_scopes.last_mut()?.push(loan); + return Some(Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(loan), ty: projection.ty, span: projection.span, - }; - continue; - } - if inputs[index] == OrderedCallViewInput::Borrowed - && let Some((source, projection)) = self.call_view_initializer(&values[index]) - { - lowered[index] = - self.stage_checked_call_view(index, None, source, projection, ownership)?; - continue; - } - if ownership.source_parameter_effect(index) - == Some(jett_typecheck::CheckedCallerEffect::TransferOwned) - && ownership.parameter_physical_access(index) - == Some(jett_typecheck::CheckedCalleeAccess::Owned) - { - // Acquire the endpoint at this source position. A later - // handler cannot turn a required transfer into a snapshot. - let value = self.lower_value(&values[index]); - let owner = self.temporary(value.ty, value.span); - self.push( - StatementKind::Let { - local: owner, - value, - }, - values[index].span, - ); - ownership.stage_acquisition(index, owner).ok()?; - lowered[index] = Expression { - kind: ExpressionKind::Local(owner), - ty: values[index].ty, - span: values[index].span, - }; - continue; - } - let single = - self.lower_ordered_owned_values(std::slice::from_ref(&values[index]), &[0])?; - lowered[index] = single.into_iter().next()?; - if matches!( - ownership.source_parameter_effect(index), - Some( - jett_typecheck::CheckedCallerEffect::TransferOwned - | jett_typecheck::CheckedCallerEffect::Copy - ) - ) && let ExpressionKind::Local(local) = lowered[index].kind - { - ownership.stage_acquisition(index, local).ok()?; - } + })), + ty: projection.ty, + span: projection.span, + }); + } + if input == OrderedCallViewInput::Borrowed + && let Some((source, projection)) = self.call_view_initializer(actual) + { + return Some(self.stage_checked_call_view(index, None, source, projection, ownership)?); + } + if ownership.source_parameter_effect(index) + == Some(jett_typecheck::CheckedCallerEffect::TransferOwned) + && ownership.parameter_physical_access(index) + == Some(jett_typecheck::CheckedCalleeAccess::Owned) + { + // Acquire the endpoint at this source position. A later + // handler cannot turn a required transfer into a snapshot. + let value = self.lower_value(actual); + let owner = self.temporary(value.ty, value.span); + self.push( + StatementKind::Let { + local: owner, + value, + }, + actual.span, + ); + ownership.stage_acquisition(index, owner).ok()?; + return Some(Expression { + kind: ExpressionKind::Local(owner), + ty: actual.ty, + span: actual.span, + }); + } + let single = self.lower_ordered_owned_values(std::slice::from_ref(actual), &[0])?; + let lowered = single.into_iter().next()?; + if matches!( + ownership.source_parameter_effect(index), + Some( + jett_typecheck::CheckedCallerEffect::TransferOwned + | jett_typecheck::CheckedCallerEffect::Copy + ) + ) && let ExpressionKind::Local(local) = lowered.kind + { + ownership.stage_acquisition(index, local).ok()?; } Some(lowered) } @@ -995,6 +1018,8 @@ impl Builder<'_> { view_source: None, span, }); + self.resource_capture + .local(&self.locals[id.index() as usize]); id } @@ -1011,6 +1036,8 @@ impl Builder<'_> { span, }); self.view_params.push(id); + self.resource_capture + .local(&self.locals[id.index() as usize]); id } @@ -1103,6 +1130,13 @@ impl Builder<'_> { values: &[Expression], order: &[usize], ) -> Option> { + if let Some(value) = values + .iter() + .find(|value| crate::resource_type_pending(self.types, value.ty)) + { + self.resource_error = Some(LowerError { span: value.span, message: "pending ResourceOwnershipPlan: ordinary owning operand staging cannot acquire Resource custody".into() }); + return None; + } if !valid_ordered_owned_values(self.types, &self.locals, values, order) { return None; } @@ -1139,13 +1173,94 @@ impl Builder<'_> { } pub(super) fn lower_value(&mut self, expression: &Expression) -> Expression { + if self + .resource_capture + .constructor_needed(expression, self.types) + { + return match self.lower_resource_constructor(expression) { + Ok(value) => value, + Err(message) => { + self.resource_error = Some(LowerError { + span: expression.span, + message, + }); + expression.clone() + } + }; + } + if self + .resource_capture + .normalized_call_needed(expression, self.types) + { + return match self.lower_resource_call(expression) { + Ok(value) => value, + Err(message) => { + self.resource_error = Some(LowerError { + span: expression.span, + message, + }); + expression.clone() + } + }; + } + match self + .resource_capture + .preserve_original_call(expression, self.types) + { + Ok(true) => return expression.clone(), + Err(message) => { + self.resource_error = Some(LowerError { + span: expression.span, + message, + }); + return expression.clone(); + } + Ok(false) => {} + } + if let ExpressionKind::ResourceInvoke { args, .. } = &expression.kind { + // Typed preservation only. Extracting actuals would require the dedicated owner plan. + if args.iter().any(needs_eager_lowering) { + self.resource_error = Some(LowerError { span: expression.span, message: "pending ResourceOwnershipPlan: Resource actual extraction requires dedicated custody staging".into() }); + } + return expression.clone(); + } + + if let Some(lowered) = self.try_lower_value_wrapper(expression) { + return lowered; + } + if let Some(lowered) = self.try_lower_value_binary(expression) { + return lowered; + } + if let Some(lowered) = self.try_lower_value_refinement(expression) { + return lowered; + } + if let Some(lowered) = self.try_lower_value_collection(expression) { + return lowered; + } + if let Some(lowered) = self.try_lower_value_sum(expression) { + return lowered; + } + if let Some(lowered) = self.try_lower_value_intrinsic(expression) { + return lowered; + } + if let Some(lowered) = self.try_lower_value_actor(expression) { + return lowered; + } + if let Some(lowered) = self.try_lower_value_call(expression) { + return lowered; + } + self.lower_sum_handle(expression) + } + + #[inline(never)] + fn try_lower_value_wrapper(&mut self, expression: &Expression) -> Option { if let ExpressionKind::FunctionAdapter { value, function } = &expression.kind { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::FunctionAdapter { value: Box::new(self.lower_value(value)), function: *function, }; - return lowered; + return Some(lowered); } if let ExpressionKind::InterfaceCoerce { value, adapters } = &expression.kind { let mut lowered = expression.clone(); @@ -1153,44 +1268,44 @@ impl Builder<'_> { value: Box::new(self.lower_value(value)), adapters: adapters.clone(), }; - return lowered; + return Some(lowered); } if let ExpressionKind::InterfaceType(value) = &expression.kind { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::InterfaceType(Box::new(self.lower_value(value))); - return lowered; + return Some(lowered); } if let ExpressionKind::RefinementValidated(value) = &expression.kind { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::RefinementValidated(Box::new(self.lower_value(value))); - return lowered; + return Some(lowered); } if let ExpressionKind::DisplayResult(value) = &expression.kind { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::DisplayResult(Box::new(self.lower_value(value))); - return lowered; + return Some(lowered); } if let ExpressionKind::EquatableResult(value) = &expression.kind { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::EquatableResult(Box::new(self.lower_value(value))); - return lowered; + return Some(lowered); } if let ExpressionKind::RuntimeFailureMessage(value) = &expression.kind { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::RuntimeFailureMessage(Box::new(self.lower_value(value))); - return lowered; + return Some(lowered); } if let ExpressionKind::View(value) = &expression.kind { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::View(Box::new(self.lower_value(value))); - return lowered; + return Some(lowered); } if let ExpressionKind::Clone(value) = &expression.kind && needs_eager_lowering(value) { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::Clone(Box::new(self.lower_value(value))); - return lowered; + return Some(lowered); } if let ExpressionKind::Run(value) = &expression.kind { let needs_snapshot = snapshotable_local(self.types, value); @@ -1204,7 +1319,7 @@ impl Builder<'_> { source }; lowered.kind = ExpressionKind::Run(Box::new(source)); - return lowered; + return Some(lowered); } } if let ExpressionKind::Join(value) = &expression.kind @@ -1212,7 +1327,7 @@ impl Builder<'_> { { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::Join(Box::new(self.lower_value(value))); - return lowered; + return Some(lowered); } if let ExpressionKind::Coarsen(value) = &expression.kind && (needs_eager_lowering(value) || snapshotable_local(self.types, value)) @@ -1225,7 +1340,7 @@ impl Builder<'_> { source }; lowered.kind = ExpressionKind::Coarsen(Box::new(source)); - return lowered; + return Some(lowered); } if let ExpressionKind::Declassify(value) = &expression.kind && (needs_eager_lowering(value) || snapshotable_local(self.types, value)) @@ -1238,7 +1353,7 @@ impl Builder<'_> { source }; lowered.kind = ExpressionKind::Declassify(Box::new(source)); - return lowered; + return Some(lowered); } if let ExpressionKind::Field { base, @@ -1253,7 +1368,7 @@ impl Builder<'_> { owner_type: *owner_type, field: *field, }; - return lowered; + return Some(lowered); } if let ExpressionKind::StateIs { value, state } = &expression.kind && needs_eager_lowering(value) @@ -1263,7 +1378,7 @@ impl Builder<'_> { value: Box::new(self.lower_value(value)), state: *state, }; - return lowered; + return Some(lowered); } if let ExpressionKind::Unary { op, value } = &expression.kind && needs_eager_lowering(value) @@ -1273,8 +1388,13 @@ impl Builder<'_> { op: *op, value: Box::new(self.lower_value(value)), }; - return lowered; + return Some(lowered); } + None + } + + #[inline(never)] + fn try_lower_value_binary(&mut self, expression: &Expression) -> Option { if let ExpressionKind::Binary { left, op, right } = &expression.kind && matches!(op, hir::BinaryOp::And | hir::BinaryOp::Or) && (needs_eager_lowering(left) || needs_eager_lowering(right)) @@ -1287,7 +1407,7 @@ impl Builder<'_> { op: *op, right: right.clone(), }; - return lowered; + return Some(lowered); } let span = expression.span; let left_local = self.temporary(TypeInterner::BOOL, left.span); @@ -1343,11 +1463,11 @@ impl Builder<'_> { ); self.close_to(continuation, span); self.current = continuation; - return Expression { + return Some(Expression { kind: ExpressionKind::Local(output), ty: TypeInterner::BOOL, span, - }; + }); } if let ExpressionKind::Binary { left, op, right } = &expression.kind && !matches!(op, hir::BinaryOp::And | hir::BinaryOp::Or) @@ -1392,7 +1512,69 @@ impl Builder<'_> { op: *op, right: Box::new(right_value), }; - return lowered; + return Some(lowered); + } + None + } + + #[inline(never)] + fn try_lower_value_refinement(&mut self, expression: &Expression) -> Option { + if let ExpressionKind::Handle { + target, + kind: HandleKind::ReflectedReturn { plan, .. }, + .. + } = &expression.kind + { + let value = self.lower_value(target); + let source = self.temporary(target.ty, expression.span); + self.push( + StatementKind::Let { + local: source, + value, + }, + expression.span, + ); + self.push( + StatementKind::ReflectedValueReady { + source, + inspect_pending: false, + }, + expression.span, + ); + let previous = self.reflected_produced_return; + self.reflected_produced_return = true; + self.lower_reflected_plan(source, plan, expression.span); + self.reflected_produced_return = previous; + let value = Expression { + kind: ExpressionKind::Local(source), + ty: target.ty, + span: expression.span, + }; + let mut ancestor = target.ty; + let mut coarsens = false; + while let Type::Refinement { base, .. } = self.types.resolve(ancestor) { + ancestor = *base; + if ancestor == expression.ty { + coarsens = true; + break; + } + } + return Some(Expression { + kind: if expression.ty == target.ty { + value.kind + } else if coarsens && matches!(&plan.action, hir::ReflectedFieldAction::Exact) { + ExpressionKind::Coarsen(Box::new(value)) + } else if matches!(self.types.resolve(expression.ty), Type::Refinement { .. }) { + ExpressionKind::RefinementValidated(Box::new(value)) + } else { + ExpressionKind::InterfaceCoerce { + value: Box::new(value), + adapters: Vec::new(), + } + }, + ty: expression.ty, + span: expression.span, + }); } if let ExpressionKind::Handle { target, @@ -1401,13 +1583,13 @@ impl Builder<'_> { failure, } = &expression.kind { - return self.lower_refinement_handle( + return Some(self.lower_refinement_handle( expression, target, predicates, *error_local, failure, - ); + )); } if let ExpressionKind::StructConstruct { struct_type, @@ -1419,13 +1601,13 @@ impl Builder<'_> { && refinement_predicates.len() == fields.len() && refinement_predicates.iter().any(|chain| !chain.is_empty()) { - return self.lower_refinement_struct_construct( + return Some(self.lower_refinement_struct_construct( expression, *struct_type, fields, evaluation_order, refinement_predicates, - ); + )); } if let ExpressionKind::Intrinsic { field_validation: Some(hir::ReflectedFieldValidation::Validate(plans)), @@ -1434,9 +1616,10 @@ impl Builder<'_> { { // Invalid plans stay visible for native contract rejection; never // turn missing or malformed proof metadata into an unchecked read. - return self - .lower_reflected_field_read(expression, plans) - .unwrap_or_else(|| expression.clone()); + return Some( + self.lower_reflected_field_read(expression, plans) + .unwrap_or_else(|| expression.clone()), + ); } if let ExpressionKind::Intrinsic { intrinsic: hir::IntrinsicId::TypeConstructFinish, @@ -1445,8 +1628,13 @@ impl Builder<'_> { } = &expression.kind && refinement_predicates.iter().any(|chain| !chain.is_empty()) { - return self.lower_refinement_builder_finish(expression, refinement_predicates); + return Some(self.lower_refinement_builder_finish(expression, refinement_predicates)); } + None + } + + #[inline(never)] + fn try_lower_value_collection(&mut self, expression: &Expression) -> Option { if let ExpressionKind::ListConstruct { elements } = &expression.kind && elements.iter().any(needs_eager_lowering) && let Some(elements) = @@ -1454,7 +1642,7 @@ impl Builder<'_> { { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::ListConstruct { elements }; - return lowered; + return Some(lowered); } if let ExpressionKind::StringInterpolation(segments) = &expression.kind && segments.iter().any(|segment| { @@ -1483,7 +1671,7 @@ impl Builder<'_> { .collect(); let mut lowered = expression.clone(); lowered.kind = ExpressionKind::StringInterpolation(segments); - return lowered; + return Some(lowered); } } if let ExpressionKind::MapConstruct { entries } = &expression.kind @@ -1507,7 +1695,7 @@ impl Builder<'_> { .collect(); let mut lowered = expression.clone(); lowered.kind = ExpressionKind::MapConstruct { entries }; - return lowered; + return Some(lowered); } } if let ExpressionKind::StructConstruct { @@ -1528,7 +1716,7 @@ impl Builder<'_> { validates_refinements: *validates_refinements, refinement_predicates: refinement_predicates.clone(), }; - return lowered; + return Some(lowered); } if let ExpressionKind::BitfieldConstruct { bitfield_type, @@ -1546,7 +1734,7 @@ impl Builder<'_> { evaluation_order: evaluation_order.clone(), validates_widths: *validates_widths, }; - return lowered; + return Some(lowered); } if let ExpressionKind::EnumConstruct { enum_type, @@ -1564,7 +1752,7 @@ impl Builder<'_> { payloads, evaluation_order: evaluation_order.clone(), }; - return lowered; + return Some(lowered); } if let ExpressionKind::MachineConstruct { state_type, @@ -1581,7 +1769,7 @@ impl Builder<'_> { state: *state, payloads, }; - return lowered; + return Some(lowered); } if let ExpressionKind::MachineTransition { source, @@ -1603,30 +1791,40 @@ impl Builder<'_> { target: *target, payloads: values, }; - return lowered; + return Some(lowered); } } + None + } + + #[inline(never)] + fn try_lower_value_sum(&mut self, expression: &Expression) -> Option { if let ExpressionKind::ResultOk(value) = &expression.kind && needs_eager_lowering(value) { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::ResultOk(Box::new(self.lower_value(value))); - return lowered; + return Some(lowered); } if let ExpressionKind::ResultFail(value) = &expression.kind && needs_eager_lowering(value) { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::ResultFail(Box::new(self.lower_value(value))); - return lowered; + return Some(lowered); } if let ExpressionKind::OptionalSome(value) = &expression.kind && needs_eager_lowering(value) { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::OptionalSome(Box::new(self.lower_value(value))); - return lowered; + return Some(lowered); } + None + } + + #[inline(never)] + fn try_lower_value_intrinsic(&mut self, expression: &Expression) -> Option { if let ExpressionKind::Intrinsic { intrinsic, type_arguments, @@ -1677,9 +1875,14 @@ impl Builder<'_> { evaluation_order: evaluation_order.clone(), ownership, }; - return self.finish_call_view_scope(lowered); + return Some(self.finish_call_view_scope(lowered)); } } + None + } + + #[inline(never)] + fn try_lower_value_actor(&mut self, expression: &Expression) -> Option { if let ExpressionKind::ActorSpawn { actor_type, args, @@ -1696,7 +1899,7 @@ impl Builder<'_> { evaluation_order: evaluation_order.clone(), constructor: *constructor, }; - return lowered; + return Some(lowered); } if let ExpressionKind::ActorMessage { actor, @@ -1734,8 +1937,13 @@ impl Builder<'_> { evaluation_order: evaluation_order.clone(), kind: *kind, }; - return lowered; + return Some(lowered); } + None + } + + #[inline(never)] + fn try_lower_value_call(&mut self, expression: &Expression) -> Option { if let ExpressionKind::IndirectCall { callee, args, @@ -1770,7 +1978,7 @@ impl Builder<'_> { evaluation_order, &mut ownership, ) else { - return expression.clone(); + return Some(expression.clone()); }; // Source evaluates call arguments before resolving a function value // held in a mutable local. A handler may rebind that local. @@ -1799,7 +2007,7 @@ impl Builder<'_> { evaluation_order: evaluation_order.clone(), ownership, }; - return self.finish_call_view_scope(lowered); + return Some(self.finish_call_view_scope(lowered)); } if let ExpressionKind::Call { function, @@ -1818,7 +2026,7 @@ impl Builder<'_> { evaluation_order, &mut ownership, ) else { - return expression.clone(); + return Some(expression.clone()); }; let mut lowered = expression.clone(); lowered.kind = ExpressionKind::Call { @@ -1827,8 +2035,265 @@ impl Builder<'_> { evaluation_order: evaluation_order.clone(), ownership, }; - return self.finish_call_view_scope(lowered); + return Some(self.finish_call_view_scope(lowered)); } + None + } + + #[inline(never)] + pub(super) fn lower_borrowed_sum_handle( + &mut self, + seed: resource_ownership::BorrowedSumSeed, + ) -> Expression { + let original = seed.original.clone(); + let ExpressionKind::View(handle) = &original.kind else { + unreachable!() + }; + let ExpressionKind::Handle { + target, + error_local, + failure, + .. + } = &handle.kind + else { + unreachable!() + }; + let span = handle.span; + let source = self.call_view_temporary(target.ty, seed.backing.id, span); + let output = self.call_view_temporary(handle.ty, seed.backing.id, span); + let tag = self.temporary(TypeInterner::BOOL, span); + let initialize = self + .resource_capture + .site( + self.current, + self.blocks[self.current.index() as usize].statements.len(), + ) + .expect("authenticated borrowed Handle capture"); + self.push( + StatementKind::Let { + local: source, + value: target.as_ref().clone(), + }, + span, + ); + let observe = self + .resource_capture + .site( + self.current, + self.blocks[self.current.index() as usize].statements.len(), + ) + .expect("authenticated borrowed Handle capture"); + self.push( + StatementKind::SumTag { + source, + target: tag, + }, + span, + ); + let success = self.new_block(span); + let failed = self.new_block(span); + let continuation = self.new_block(span); + self.terminate( + TerminatorKind::Branch { + condition: Expression { + kind: ExpressionKind::Local(tag), + ty: TypeInterner::BOOL, + span, + }, + then_block: success, + else_block: failed, + }, + span, + ); + self.current = success; + let project = self + .resource_capture + .site( + self.current, + self.blocks[self.current.index() as usize].statements.len(), + ) + .expect("authenticated borrowed Handle capture"); + self.push( + StatementKind::SumTake { + source, + target: output, + success: true, + }, + span, + ); + self.close_to(continuation, span); + self.current = failed; + let failure_site = error_local.map(|error| { + let site = self + .resource_capture + .site( + self.current, + self.blocks[self.current.index() as usize].statements.len(), + ) + .expect("authenticated borrowed Handle capture"); + self.push( + StatementKind::SumTake { + source, + target: error, + success: false, + }, + span, + ); + site + }); + self.lower_block(failure); + if self.open() { + self.terminate(TerminatorKind::Unreachable, span); + } + self.current = continuation; + let error = error_local + .and_then(|local| self.locals.get(local.index() as usize)) + .cloned(); + self.resource_capture.record_borrowed_sum( + seed, + self.locals[source.index() as usize].clone(), + self.locals[output.index() as usize].clone(), + self.locals[tag.index() as usize].clone(), + error, + initialize, + observe, + project, + failure_site, + failed, + continuation, + ); + Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(output), + ty: handle.ty, + span: handle.span, + })), + ty: original.ty, + span: original.span, + } + } + + #[inline(never)] + pub(super) fn lower_ordinary_borrowed_sum_handle( + &mut self, + seed: ordinary_borrowed_sums::Seed, + ) -> Expression { + let original = seed.original.clone(); + let ExpressionKind::View(handle) = &original.kind else { + unreachable!() + }; + let ExpressionKind::Handle { + target, + error_local, + failure, + .. + } = &handle.kind + else { + unreachable!() + }; + let span = handle.span; + let source = self.call_view_temporary(target.ty, seed.backing.id, span); + let output = self.call_view_temporary(handle.ty, seed.backing.id, span); + let tag = self.temporary(TypeInterner::BOOL, span); + let site = |builder: &Self| { + ordinary_borrowed_sums::OrdinarySumProjectionSite::new( + builder.current, + builder.blocks[builder.current.index() as usize] + .statements + .len(), + ) + }; + let initialize = site(self); + self.push( + StatementKind::Let { + local: source, + value: target.as_ref().clone(), + }, + span, + ); + let observe = site(self); + self.push( + StatementKind::SumTag { + source, + target: tag, + }, + span, + ); + let success = self.new_block(span); + let failed = self.new_block(span); + let continuation = self.new_block(span); + self.terminate( + TerminatorKind::Branch { + condition: Expression { + kind: ExpressionKind::Local(tag), + ty: TypeInterner::BOOL, + span, + }, + then_block: success, + else_block: failed, + }, + span, + ); + self.current = success; + let project = site(self); + self.push( + StatementKind::SumTake { + source, + target: output, + success: true, + }, + span, + ); + self.close_to(continuation, span); + self.current = failed; + let failure_site = error_local.map(|error| { + let at = site(self); + self.push( + StatementKind::SumTake { + source, + target: error, + success: false, + }, + span, + ); + at + }); + self.lower_block(failure); + if self.open() { + self.terminate(TerminatorKind::Unreachable, span); + } + self.current = continuation; + let alias_site = site(self); + let error = error_local + .and_then(|local| self.locals.get(local.index() as usize)) + .cloned(); + self.ordinary_sum_capture.record( + seed, + self.locals[source.index() as usize].clone(), + self.locals[output.index() as usize].clone(), + self.locals[tag.index() as usize].clone(), + error, + initialize, + observe, + project, + failure_site, + alias_site, + failed, + continuation, + ); + Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(output), + ty: handle.ty, + span: handle.span, + })), + ty: original.ty, + span: original.span, + } + } + + #[inline(never)] + fn lower_sum_handle(&mut self, expression: &Expression) -> Expression { let ExpressionKind::Handle { target, kind: HandleKind::Result | HandleKind::Optional, @@ -1998,7 +2463,16 @@ impl Builder<'_> { .. } = &mut raw.kind { - *field_validation = Some(hir::ReflectedFieldValidation::Read); + *field_validation = Some( + if plans + .iter() + .any(|plan| matches!(plan.action, hir::ReflectedFieldAction::CallableReturn(_))) + { + hir::ReflectedFieldValidation::ReadCallable(plans.to_vec()) + } else { + hir::ReflectedFieldValidation::Read + }, + ); *raw_args = args.clone(); *raw_ownership = ownership; } diff --git a/crates/jett_mir/src/handlers/reflected.rs b/crates/jett_mir/src/handlers/reflected.rs index dd3ea0af..df8b2867 100644 --- a/crates/jett_mir/src/handlers/reflected.rs +++ b/crates/jett_mir/src/handlers/reflected.rs @@ -8,18 +8,25 @@ enum Pass { Predicates, } -fn needed(plan: &hir::ReflectedFieldPlan, pass: Pass) -> bool { +fn needed(plan: &hir::ReflectedFieldPlan, pass: Pass, produced: bool) -> bool { use hir::ReflectedFieldAction as A; match (&plan.action, pass) { (A::Exact, _) => false, - (A::Predicates(_), Pass::Preflight) => false, + (A::CallableReturn(_), _) => false, + (A::Predicates(predicates), Pass::Preflight) => produced && !predicates.is_empty(), (A::Unsupported(_), Pass::Predicates) => false, - (A::Refine { base, .. }, Pass::Preflight) => needed(base, pass), + (A::Refine { base, predicates }, Pass::Preflight) => { + needed(base, pass, produced) || (produced && !predicates.is_empty()) + } (A::List(child) | A::Set(child) | A::Optional(child), Pass::Predicates) => { - needed(child, pass) + needed(child, pass, produced) + } + (A::Map { key, value }, Pass::Predicates) => { + needed(key, pass, produced) || needed(value, pass, produced) + } + (A::Result { ok, error }, Pass::Predicates) => { + needed(ok, pass, produced) || needed(error, pass, produced) } - (A::Map { key, value }, Pass::Predicates) => needed(key, pass) || needed(value, pass), - (A::Result { ok, error }, Pass::Predicates) => needed(ok, pass) || needed(error, pass), _ => true, } } @@ -53,12 +60,13 @@ impl Builder<'_> { pass: Pass, span: Span, ) { - if !self.open() || !needed(plan, pass) { + if !self.open() || !needed(plan, pass, self.reflected_produced_return) { return; } use hir::ReflectedFieldAction as A; match &plan.action { A::Exact => {} + A::CallableReturn(_) => {} A::Unsupported(reason) => { self.push( StatementKind::Evaluate(Expression { @@ -71,11 +79,48 @@ impl Builder<'_> { self.terminate(TerminatorKind::Unreachable, span); } A::Predicates(predicates) => { - self.reflected_predicates(source, plan.requested_type, predicates, span); + if matches!(pass, Pass::Preflight) { + self.push( + StatementKind::ReflectedValueReady { + source, + inspect_pending: true, + }, + span, + ); + } else { + let source_type = if self.reflected_produced_return { + self.locals[source.index() as usize].ty + } else { + plan.requested_type + }; + self.reflected_predicates(source, source_type, predicates, span); + } } A::Refine { base, predicates } => { - if needed(base, pass) { - let snapshot = self.temporary(base.requested_type, span); + if self.reflected_produced_return + && matches!(pass, Pass::Preflight) + && !predicates.is_empty() + { + self.push( + StatementKind::ReflectedValueReady { + source, + inspect_pending: true, + }, + span, + ); + } + if needed(base, pass, self.reflected_produced_return) { + let source_type = if self.reflected_produced_return { + self.locals[source.index() as usize].ty + } else { + plan.requested_type + }; + let base_type = if self.reflected_produced_return { + base.source_type + } else { + base.requested_type + }; + let snapshot = self.temporary(base_type, span); self.push( StatementKind::Let { local: snapshot, @@ -83,13 +128,13 @@ impl Builder<'_> { kind: ExpressionKind::Coarsen(Box::new(Expression { kind: ExpressionKind::Clone(Box::new(local( source, - plan.requested_type, + source_type, span, ))), - ty: plan.requested_type, + ty: source_type, span, })), - ty: base.requested_type, + ty: base_type, span, }, }, @@ -98,7 +143,12 @@ impl Builder<'_> { self.reflected_pass(snapshot, base, pass, span); } if self.open() && matches!(pass, Pass::Predicates) { - self.reflected_predicates(source, plan.requested_type, predicates, span); + let source_type = if self.reflected_produced_return { + self.locals[source.index() as usize].ty + } else { + plan.requested_type + }; + self.reflected_predicates(source, source_type, predicates, span); } } A::List(child) => self.reflected_sequence( @@ -158,7 +208,10 @@ impl Builder<'_> { span, ); } - if !children.iter().any(|(_, plan)| needed(plan, pass)) { + if !children + .iter() + .any(|(_, plan)| needed(plan, pass, self.reflected_produced_return)) + { return; } let cursor = self.temporary(TypeInterner::INT64, span); @@ -204,10 +257,15 @@ impl Builder<'_> { ); self.current = body; for (part, plan) in children { - if !self.open() || !needed(plan, pass) { + if !self.open() || !needed(plan, pass, self.reflected_produced_return) { continue; } - let child = self.temporary(plan.requested_type, span); + let child_type = if self.reflected_produced_return { + plan.source_type + } else { + plan.requested_type + }; + let child = self.temporary(child_type, span); self.push( StatementKind::SequenceGet { consume: false, @@ -260,7 +318,9 @@ impl Builder<'_> { span, ); } - if !needed(ok, pass) && !error.is_some_and(|plan| needed(plan, pass)) { + if !needed(ok, pass, self.reflected_produced_return) + && !error.is_some_and(|plan| needed(plan, pass, self.reflected_produced_return)) + { return; } let present = self.temporary(TypeInterner::BOOL, span); @@ -284,7 +344,9 @@ impl Builder<'_> { ); for (block, child, success) in [(accepted, Some(ok), true), (failed, error, false)] { self.current = block; - if let Some(child) = child.filter(|plan| needed(plan, pass)) { + if let Some(child) = + child.filter(|plan| needed(plan, pass, self.reflected_produced_return)) + { let source_type = self.locals[source.index() as usize].ty; let snapshot = self.temporary(source_type, span); self.push( @@ -298,7 +360,12 @@ impl Builder<'_> { }, span, ); - let payload = self.temporary(child.requested_type, span); + let child_type = if self.reflected_produced_return { + child.source_type + } else { + child.requested_type + }; + let payload = self.temporary(child_type, span); self.push( StatementKind::SumTake { source: snapshot, diff --git a/crates/jett_mir/src/handlers/resource_calls.rs b/crates/jett_mir/src/handlers/resource_calls.rs new file mode 100644 index 00000000..236fe675 --- /dev/null +++ b/crates/jett_mir/src/handlers/resource_calls.rs @@ -0,0 +1,172 @@ +//! Canonical constructor extraction; no public MIR graph may enter this path. +use super::*; +use crate::resource_ownership::constructors::ResourceOperationRegion; + +impl Builder<'_> { + pub(super) fn resource_site(&self) -> ResourceSite { + self.resource_capture + .site( + self.current, + self.blocks[self.current.index() as usize].statements.len(), + ) + .expect("authenticated Resource constructor") + } + + pub(super) fn lower_resource_call( + &mut self, + original: &Expression, + ) -> Result { + let checkpoint = self.clone(); + match self.try_lower_resource_call(original) { + Ok(value) => Ok(value), + Err(error) => { + *self = checkpoint; + Err(error) + } + } + } + + fn try_lower_resource_call(&mut self, original: &Expression) -> Result { + let ExpressionKind::Call { + args, + evaluation_order, + ownership: hir::CallOwnership::Source(source), + .. + } = &original.kind + else { + return Err("pending Resource call region: handled indirect/resource-hook invocation timing is unproved".into()); + }; + // Authenticate the original tuple before any extraction or generated Local. + let begin = self.resource_site(); + let region = self.resource_capture.begin_normalized_call( + original, + self.resource_call_scopes.last().copied(), + begin, + self.types, + )?; + self.push( + StatementKind::ResourceCall(ResourceCallNode::Begin { region }), + original.span, + ); + self.resource_call_scopes.push(region); + self.resource_operation_scopes + .push(ResourceOperationRegion::Call(region)); + for (source_index, ¶meter) in evaluation_order.iter().enumerate() { + let actual = args + .get(parameter) + .ok_or("Resource call source permutation is invalid")?; + let fact = source + .arguments + .iter() + .find(|fact| fact.parameter_index == parameter) + .ok_or("Resource call actual lost its original formal")?; + if fact.source_index != source_index { + return Err("Resource call actual changes its checked lexical order".into()); + } + let start = self.resource_site(); + let first_local = self.locals.len(); + let mut unsupported = false; + crate::resource_ownership::visit_expression(actual, &mut |value| { + unsupported |= matches!( + value.kind, + ExpressionKind::Handle { + kind: HandleKind::Refinement { .. }, + .. + } + ); + }); + if unsupported { + return Err( + "pending Resource call region: refinement handlers need their exact exit proof" + .into(), + ); + } + let endpoint = self.lower_value(actual); + if let Some(error) = &self.resource_error { + return Err(error.message.clone()); + } + let ordinary = if crate::resource_type_pending(self.types, endpoint.ty) { + None + } else { + Some(self.temporary(endpoint.ty, endpoint.span)) + }; + let stage = self.resource_site(); + self.resource_capture.normalized_actual( + region, + source_index, + parameter, + actual, + &endpoint, + start, + stage, + &self.locals[first_local..], + ordinary, + )?; + self.push( + StatementKind::ResourceCall(ResourceCallNode::Stage { + region, + source_index, + parameter, + value: endpoint, + ordinary, + }), + actual.span, + ); + } + let output = self.temporary(original.ty, original.span); + self.push( + StatementKind::ResourceCall(ResourceCallNode::Invoke { region, output }), + original.span, + ); + self.push( + StatementKind::ResourceCall(ResourceCallNode::End { + region, + outcome: ResourceCompletion::Normal, + }), + original.span, + ); + if self.resource_call_scopes.pop() != Some(region) + || self.resource_operation_scopes.pop() != Some(ResourceOperationRegion::Call(region)) + { + return Err("Resource call constructor lost its exact operation stack".into()); + } + Ok(Expression { + kind: ExpressionKind::Local(output), + ty: original.ty, + span: original.span, + }) + } + + /// Suspend only the active operation suffix before a real Source Return read. + pub(crate) fn abandon_resource_calls( + &mut self, + span: Span, + ) -> ( + Vec, + Vec, + Vec, + ) { + let calls = std::mem::take(&mut self.resource_call_scopes); + let constructors = std::mem::take(&mut self.resource_constructor_scopes); + let operations = std::mem::take(&mut self.resource_operation_scopes); + for operation in operations.iter().rev() { + match *operation { + ResourceOperationRegion::Call(region) => self.push( + StatementKind::ResourceCall(ResourceCallNode::End { + region, + outcome: ResourceCompletion::Abort, + }), + span, + ), + ResourceOperationRegion::Constructor(constructor) => self.push( + StatementKind::ResourceConstructor(ResourceConstructorNode::End { + constructor, + outcome: ResourceCompletion::Abort, + }), + span, + ), + } + } + (calls, constructors, operations) + } +} diff --git a/crates/jett_mir/src/handlers/resource_constructors.rs b/crates/jett_mir/src/handlers/resource_constructors.rs new file mode 100644 index 00000000..11765f6b --- /dev/null +++ b/crates/jett_mir/src/handlers/resource_constructors.rs @@ -0,0 +1,104 @@ +//! Checked occupied constructor staging; a general public MIR reseal cannot enter it. +use super::*; +use crate::resource_ownership::constructors::ResourceOperationRegion; + +impl Builder<'_> { + pub(super) fn lower_resource_constructor( + &mut self, + original: &Expression, + ) -> Result { + let checkpoint = self.clone(); + match self.try_lower_resource_constructor(original) { + Ok(v) => Ok(v), + Err(e) => { + *self = checkpoint; + Err(e) + } + } + } + + fn try_lower_resource_constructor( + &mut self, + original: &Expression, + ) -> Result { + let ExpressionKind::StructConstruct { + fields, + evaluation_order, + .. + } = &original.kind + else { + return Err("Resource constructor changed its original node".into()); + }; + let begin = self.resource_site(); + let constructor = self.resource_capture.begin_constructor_region( + original, + self.resource_operation_scopes.last().copied(), + begin, + self.types, + )?; + self.push( + StatementKind::ResourceConstructor(ResourceConstructorNode::Begin { constructor }), + original.span, + ); + self.resource_constructor_scopes.push(constructor); + self.resource_operation_scopes + .push(ResourceOperationRegion::Constructor(constructor)); + for (source_index, &field) in evaluation_order.iter().enumerate() { + let start = self.resource_site(); + let first_local = self.locals.len(); + let actual = fields + .get(field) + .ok_or("Resource constructor has a foreign field permutation")?; + let endpoint = self.lower_value(actual); + if let Some(error) = &self.resource_error { + return Err(error.message.clone()); + } + let stage = self.resource_site(); + self.resource_capture.constructor_field( + constructor, + source_index, + field, + actual, + &endpoint, + start, + stage, + &self.locals[first_local..], + )?; + self.push( + StatementKind::ResourceConstructor(ResourceConstructorNode::Stage { + constructor, + source_index, + field, + value: endpoint, + }), + actual.span, + ); + } + let output = self.temporary(original.ty, original.span); + self.push( + StatementKind::ResourceConstructor(ResourceConstructorNode::Commit { + constructor, + output, + }), + original.span, + ); + self.push( + StatementKind::ResourceConstructor(ResourceConstructorNode::End { + constructor, + outcome: ResourceCompletion::Normal, + }), + original.span, + ); + if self.resource_constructor_scopes.pop() != Some(constructor) + || self.resource_operation_scopes.pop() + != Some(ResourceOperationRegion::Constructor(constructor)) + { + return Err("Resource constructor lost its exact active operation suffix".into()); + } + Ok(Expression { + kind: ExpressionKind::Local(output), + ty: original.ty, + span: original.span, + }) + } +} diff --git a/crates/jett_mir/src/iteration_views.rs b/crates/jett_mir/src/iteration_views.rs index fa2fab52..78645d49 100644 --- a/crates/jett_mir/src/iteration_views.rs +++ b/crates/jett_mir/src/iteration_views.rs @@ -381,7 +381,18 @@ fn definitions(function: &Function, id: LocalId) -> usize { for block in &function.blocks { for statement in &block.statements { count += usize::from(match &statement.kind { - StatementKind::Let { local, .. } + StatementKind::ResourceConstructor(crate::ResourceConstructorNode::Commit { + output, + .. + }) => *output == id, + StatementKind::ResourceCall( + ResourceCallNode::Stage { + ordinary: Some(local), + .. + } + | ResourceCallNode::Invoke { output: local, .. }, + ) + | StatementKind::Let { local, .. } | StatementKind::BeginCallView { local, .. } | StatementKind::CheckRefinement { local, .. } => *local == id, StatementKind::SequenceGet { target, .. } diff --git a/crates/jett_mir/src/lib.rs b/crates/jett_mir/src/lib.rs index 3f30eb8a..b21b4aeb 100644 --- a/crates/jett_mir/src/lib.rs +++ b/crates/jett_mir/src/lib.rs @@ -4,6 +4,34 @@ mod analysis; mod breakpoint_regions; mod call_owner_generations; mod call_ownership; +mod ordinary_borrowed_sums; +pub use ordinary_borrowed_sums::{ + OrdinaryBorrowedSumProjection, OrdinarySumPayloadPath, OrdinarySumProjectionSite, +}; +mod resource_ownership; +pub use resource_ownership::{ + ResourceArgumentEffect, ResourceArgumentSyntax, ResourceBorrowedSumProjection, + ResourceCallActual, ResourceCallFormal, ResourceCallNode, ResourceCallOperand, + ResourceCallRegion, ResourceCallRegionId, ResourceCallResult, ResourceCarrierChild, + ResourceCarrierConstructor, ResourceCarrierEdge, ResourceCarrierField, + ResourceCarrierFunctionPlan, ResourceCarrierGeneration, ResourceCarrierIndex, + ResourceCarrierIteration, ResourceCarrierLoan, ResourceCarrierLoanId, + ResourceCarrierLoanSource, ResourceCarrierNode, ResourceCarrierObservation, + ResourceCarrierOperation, ResourceCarrierOperationId, ResourceCarrierOperationRole, + ResourceCarrierProjectionPath, ResourceCarrierShape, ResourceCarrierShapeId, + ResourceCarrierSlot, ResourceCarrierSlotId, ResourceCarrierState, ResourceCarrierValue, + ResourceCarrierVariant, ResourceCompanionPlan, ResourceCompletion, ResourceConstructorField, + ResourceConstructorId, ResourceConstructorNode, ResourceConstructorRegion, + ResourceEntryScopePlan, ResourceFrame, ResourceFrameId, ResourceFrameRole, + ResourceFunctionPlan, ResourceLexicalExit, ResourceLexicalExitId, ResourceLexicalExitKind, + ResourceLexicalExitPlan, ResourceLoan, ResourceLoanId, ResourceLoanSource, + ResourceNamedCallableProof, ResourceOccupancy, ResourceOperation, ResourceOperationId, + ResourceOperationRole, ResourceOwnerSlot, ResourceOwnerSlotId, ResourceOwnershipPlan, + ResourcePath, ResourcePosition, ResourceShape, ResourceSite, ResourceSlotStorage, + ResourceSumPayloadPath, validate_resource_ownership, validate_resource_ownership_for_entry, +}; +#[cfg(test)] +mod resource_manifest_tests; pub use call_owner_generations::{ CallGenerationSlot, CallGenerationStoragePlan, CallOwnerEscrowId, CallOwnerGenerationId, }; @@ -11,6 +39,8 @@ pub use call_ownership::{CallerAcquisitions, SourceAcquisition}; mod call_views; pub mod copy_values; mod generated_functions; +mod reflected_callable; +pub use reflected_callable::validate_reflected_callable_plans; mod handlers; mod iteration_views; pub use generated_functions::prepare_native_generated_functions; @@ -25,7 +55,7 @@ pub use jett_hir::{FunctionId, Local, LocalId, Param, ParamMode}; use jett_common::Span; use jett_hir::{self as hir, Expression, FieldId, FunctionIdentity, VariantId}; -use jett_types::{TypeId, TypeInterner}; +use jett_types::{Type, TypeId, TypeInterner}; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct BlockId(u32); @@ -38,6 +68,7 @@ impl BlockId { #[derive(Debug, Clone, PartialEq)] pub struct Program { + pub resource_manifest: hir::ResourceManifest, pub functions: Vec, pub equality_methods: std::collections::HashMap, } @@ -61,6 +92,9 @@ pub struct Function { original_view_iterations: Vec, breakpoint_regions: Vec, call_owner_generations: Vec, + resource_lowering: Option, + ordinary_borrowed_sums: Option, + reflected_callable: Option, } impl Function { @@ -174,6 +208,11 @@ impl SequenceSource { #[derive(Debug, Clone, PartialEq)] pub enum StatementKind { + /// Private constructor-owned lexical loan retirement; copied IDs grant nothing. + ResourceLexicalExit(ResourceLexicalExitId), + /// Constructor-authenticated Resource operation whose actuals span CFG blocks. + ResourceCall(ResourceCallNode), + ResourceConstructor(ResourceConstructorNode), /// Empty, internal generation escrow; this is not an initialized Jett value. OpenCallOwnerGeneration { generation: CallOwnerGenerationId, @@ -204,6 +243,11 @@ pub enum StatementKind { source: LocalId, kind: ReflectedContainerKind, }, + /// Private produced-return preflight, authenticated by its callable witness. + ReflectedValueReady { + source: LocalId, + inspect_pending: bool, + }, SequenceLength { source: SequenceSource, target: LocalId, @@ -347,6 +391,7 @@ pub fn validate(program: &Program) -> Result<(), Vec> { function, functions: &program.functions, function_count: program.functions.len(), + resource_manifest: &program.resource_manifest, errors: &mut errors, } .validate(); @@ -358,10 +403,62 @@ pub fn validate(program: &Program) -> Result<(), Vec> { } } +/// Conservative operational refusal until the separate Resource CFG plan exists. +pub(crate) fn resource_type_pending(types: &TypeInterner, ty: TypeId) -> bool { + fn visit( + types: &TypeInterner, + ty: TypeId, + seen: &mut std::collections::HashSet, + ) -> bool { + if ty.index() as usize >= types.len() { + return true; + } + if !seen.insert(ty) { + return false; + } + match types.resolve(ty) { + Type::Resource(_) => true, + Type::List(inner) + | Type::Set(inner) + | Type::Optional(inner) + | Type::Secret(inner) + | Type::Refinement { base: inner, .. } => visit(types, *inner, seen), + Type::Map(key, value) | Type::Result(key, value) => { + visit(types, *key, seen) || visit(types, *value, seen) + } + Type::Function { + params, + return_type, + .. + } => { + params.iter().any(|ty| visit(types, *ty, seen)) || visit(types, *return_type, seen) + } + Type::Struct(id) => types + .resolve_struct(*id) + .fields + .iter() + .any(|(_, ty)| visit(types, *ty, seen)), + Type::Enum(id) => types + .resolve_enum(*id) + .variants + .iter() + .any(|variant| variant.fields.iter().any(|(_, ty)| visit(types, *ty, seen))), + Type::Machine(id) | Type::MachineState { machine: id, .. } => types + .resolve_machine(*id) + .states + .iter() + .any(|state| state.fields.iter().any(|(_, ty)| visit(types, *ty, seen))), + _ => false, + } + } + visit(types, ty, &mut std::collections::HashSet::new()) +} + struct FunctionValidator<'function, 'errors> { function: &'function Function, functions: &'function [Function], function_count: usize, + resource_manifest: &'function hir::ResourceManifest, errors: &'errors mut Vec, } @@ -372,9 +469,25 @@ pub fn validate_call_ownership( types: &TypeInterner, ) -> Result<(), Vec> { validate(program)?; + validate_reflected_callable_plans(program, types)?; let mut errors = Vec::new(); + if let Err(message) = program.resource_manifest.validate(types) { + // Type bounds are a prerequisite for custody and invocation queries. + // A malformed type cannot establish that a Resource witness is needed. + return Err(vec![ValidationError { + span: Span::new(jett_common::FileId::new(0), 0, 0), + message, + }]); + } let validation = call_ownership::ProgramValidation::new(program); for function in &program.functions { + if let Err(message) = ordinary_borrowed_sums::validate(function, types) { + errors.push(ValidationError { + span: function.span, + message, + }); + continue; + } if let Err(message) = validation.validate_function(function, types) { errors.push(ValidationError { span: function.span, @@ -382,6 +495,13 @@ pub fn validate_call_ownership( }); } } + // Custody queries depend on valid invocation and original type metadata. + if !errors.is_empty() { + return Err(errors); + } + if let Err(mut witness_errors) = resource_ownership::validate_witnesses(program, types) { + errors.append(&mut witness_errors); + } if errors.is_empty() { Ok(()) } else { @@ -395,7 +515,17 @@ pub fn validate_caller_acquisitions<'a>( function: &'a Function, types: &TypeInterner, ) -> Result, String> { - call_ownership::validate_function(program, function, types) + let acquisitions = call_ownership::validate_function(program, function, types)?; + if acquisitions.resource_pending + || function + .locals + .iter() + .any(|local| resource_type_pending(types, local.ty)) + || resource_type_pending(types, function.return_type) + { + return Err("pending ResourceOwnershipPlan: typed Resource nodes grant no ordinary move, copy, borrow or drop authority".into()); + } + Ok(acquisitions) } impl FunctionValidator<'_, '_> { @@ -582,6 +712,28 @@ impl FunctionValidator<'_, '_> { fn statement(&mut self, statement: &Statement) { match &statement.kind { + StatementKind::ResourceLexicalExit(_) => {} + StatementKind::ResourceConstructor(node) => match node { + ResourceConstructorNode::Stage { value, .. } => self.expression(value), + ResourceConstructorNode::Commit { output, .. } => { + self.check_local(*output, statement.span, "Resource constructor output") + } + _ => {} + }, + StatementKind::ResourceCall(node) => match node { + ResourceCallNode::Stage { + value, ordinary, .. + } => { + self.expression(value); + if let Some(local) = ordinary { + self.check_local(*local, statement.span, "Resource call ordinary stage"); + } + } + ResourceCallNode::Invoke { output, .. } => { + self.check_local(*output, statement.span, "Resource call output") + } + ResourceCallNode::Begin { .. } | ResourceCallNode::End { .. } => {} + }, StatementKind::OpenCallOwnerGeneration { root, .. } => { self.check_local(*root, statement.span, "generation root") } @@ -592,7 +744,8 @@ impl FunctionValidator<'_, '_> { self.check_local(*rhs_owner, statement.span, "generation acquired RHS"); } StatementKind::CloseCallOwnerGeneration { .. } => {} - StatementKind::ReflectedContainerReady { source, .. } => { + StatementKind::ReflectedContainerReady { source, .. } + | StatementKind::ReflectedValueReady { source, .. } => { self.check_local(*source, statement.span, "reflected container source"); } StatementKind::SequenceLength { source, target } => { @@ -899,7 +1052,8 @@ impl FunctionValidator<'_, '_> { let ownership = match &expression.kind { hir::ExpressionKind::Call { ownership, .. } | hir::ExpressionKind::Intrinsic { ownership, .. } - | hir::ExpressionKind::IndirectCall { ownership, .. } => Some(ownership), + | hir::ExpressionKind::IndirectCall { ownership, .. } + | hir::ExpressionKind::ResourceInvoke { ownership, .. } => Some(ownership), _ => None, }; if let Some(ownership) = ownership { @@ -911,6 +1065,31 @@ impl FunctionValidator<'_, '_> { hir::ExpressionKind::Local(local) => { self.check_local(*local, expression.span, "expression"); } + hir::ExpressionKind::ResourceHookValue { hook } => { + if !self.resource_manifest.contains_hook(hook) { + self.error( + expression.span, + "MIR Resource descriptor has no original manifest", + ); + } + } + hir::ExpressionKind::ResourceInvoke { + hook, + args, + evaluation_order, + .. + } => { + if !self.resource_manifest.contains_hook(hook) { + self.error( + expression.span, + "MIR Resource invocation has no original manifest", + ); + } + self.check_evaluation_order(evaluation_order, args.len(), expression.span); + for value in args { + self.expression(value); + } + } hir::ExpressionKind::FunctionRef(function) => { self.check_function(*function, expression.span); } @@ -974,7 +1153,9 @@ impl FunctionValidator<'_, '_> { hir::is_reflected_field_intrinsic(*intrinsic), field_validation, ) { - (true, Some(hir::ReflectedFieldValidation::Read)) | (false, None) => {} + (true, Some(hir::ReflectedFieldValidation::Read)) + | (true, Some(hir::ReflectedFieldValidation::ReadCallable(_))) + | (false, None) => {} _ => self.error( expression.span, "reflected selector proof plans were not lowered", @@ -1146,6 +1327,18 @@ pub fn lower(program: &hir::Program, types: &TypeInterner) -> Result Result, _>>() .map_err(|error| vec![error])?, }; + reflected_callable::capture( + &mut lowered, + program, + &function_param_modes, + &resource_execution, + ) + .map_err(|message| { + vec![LowerError { + span: program + .functions + .first() + .map_or(Span::new(jett_common::FileId::new(0), 0, 0), |function| { + function.span + }), + message, + }] + })?; validate_call_ownership(&lowered, types).map_err(|errors| { errors .into_iter() @@ -1181,9 +1401,42 @@ fn lower_function( function: &hir::Function, types: &TypeInterner, function_param_modes: &std::collections::HashMap>, + resource_manifest: &hir::ResourceManifest, + resource_source: &hir::ResourceSourceArchive, + resource_execution: &resource_ownership::ResourceExecutionClosure, +) -> Result { + lower_function_body( + function, + types, + function_param_modes, + resource_manifest, + resource_source, + resource_execution, + ) +} + +fn lower_function_body( + function: &hir::Function, + types: &TypeInterner, + function_param_modes: &std::collections::HashMap>, + resource_manifest: &hir::ResourceManifest, + resource_source: &hir::ResourceSourceArchive, + resource_execution: &resource_ownership::ResourceExecutionClosure, ) -> Result { let mut builder = Builder::new(function.body.span, types, function_param_modes); builder.locals = function.locals.clone(); + builder.ordinary_sum_capture = ordinary_borrowed_sums::Capture::authenticated(function, types) + .map_err(|message| LowerError { + span: function.span, + message, + })?; + builder.resource_capture = resource_ownership::Capture::authenticated( + function, + resource_manifest, + resource_source, + types, + resource_execution, + ); builder.view_params = function .params .iter() @@ -1198,11 +1451,16 @@ fn lower_function( .map(|local| local.id), ); builder.lower_block(&function.body); + if let Some(error) = builder.resource_error.take() { + return Err(error); + } if builder.open() && function.return_type == jett_types::TypeInterner::NOTHING { builder.terminate(TerminatorKind::Return(None), function.body.span); } let breakpoint_capture = std::mem::take(&mut builder.breakpoint_capture); let generation_capture = std::mem::take(&mut builder.generation_capture); + let resource_capture = std::mem::take(&mut builder.resource_capture); + let ordinary_sum_capture = std::mem::take(&mut builder.ordinary_sum_capture); let mut lowered = Function { id: function.id, identity: function.identity.clone(), @@ -1220,6 +1478,9 @@ fn lower_function( original_view_iterations: Vec::new(), breakpoint_regions: Vec::new(), call_owner_generations: Vec::new(), + resource_lowering: None, + ordinary_borrowed_sums: None, + reflected_callable: None, }; lowered.breakpoint_regions = breakpoint_capture @@ -1235,23 +1496,46 @@ fn lower_function( span: function.span, message, })?; + lowered.resource_lowering = + resource_capture + .finish(&lowered) + .map_err(|message| LowerError { + span: function.span, + message, + })?; + lowered.ordinary_borrowed_sums = + ordinary_sum_capture + .finish(&lowered) + .map_err(|message| LowerError { + span: function.span, + message, + })?; iteration_views::capture_original(&mut lowered, types); Ok(lowered) } #[derive(Clone)] struct Builder<'a> { + resource_error: Option, types: &'a TypeInterner, function_param_modes: &'a std::collections::HashMap>, blocks: Vec, current: BlockId, - loops: Vec<(BlockId, BlockId, usize)>, + loops: Vec<(BlockId, BlockId, usize, usize)>, + lexical_scopes: Vec, + reflected_original_prefix: Option>, call_view_scopes: Vec>, + resource_call_scopes: Vec, + resource_constructor_scopes: Vec, + resource_operation_scopes: Vec, locals: Vec, handlers: Vec<(LocalId, BlockId)>, view_params: Vec, breakpoint_capture: breakpoint_regions::Capture, generation_capture: call_owner_generations::Capture, + resource_capture: resource_ownership::Capture, + ordinary_sum_capture: ordinary_borrowed_sums::Capture, + reflected_produced_return: bool, } impl<'a> Builder<'a> { @@ -1263,6 +1547,8 @@ impl<'a> Builder<'a> { Self { types, function_param_modes, + lexical_scopes: Vec::new(), + reflected_original_prefix: None, blocks: vec![BasicBlock { id: BlockId(0), statements: Vec::new(), @@ -1274,11 +1560,18 @@ impl<'a> Builder<'a> { current: BlockId(0), loops: Vec::new(), call_view_scopes: Vec::new(), + resource_call_scopes: Vec::new(), + resource_constructor_scopes: Vec::new(), + resource_operation_scopes: Vec::new(), locals: Vec::new(), handlers: Vec::new(), view_params: Vec::new(), + resource_error: None, breakpoint_capture: breakpoint_regions::Capture::default(), generation_capture: call_owner_generations::Capture::default(), + resource_capture: resource_ownership::Capture::default(), + ordinary_sum_capture: ordinary_borrowed_sums::Capture::default(), + reflected_produced_return: false, } } @@ -1294,6 +1587,7 @@ impl<'a> Builder<'a> { }); self.breakpoint_capture.block(id); self.generation_capture.block(id); + self.resource_capture.block(id, span); id } @@ -1308,6 +1602,7 @@ impl<'a> Builder<'a> { let value = Terminator { kind, span }; self.breakpoint_capture.terminator(self.current, &value); self.generation_capture.terminator(self.current, &value); + self.resource_capture.terminator(self.current, &value); self.blocks[self.current.index() as usize].terminator = value; } @@ -1318,6 +1613,20 @@ impl<'a> Builder<'a> { .statement(self.current, index, &value); self.generation_capture .statement(self.current, index, &value); + self.resource_capture.statement(self.current, index, &value); + if let StatementKind::ResourceConstructor(node) = &value.kind { + if let Some(site) = self.resource_capture.site(self.current, index) { + self.resource_capture + .constructor_node(site, node, &self.locals); + } + } + if let StatementKind::ResourceCall(node) = &value.kind { + let site = self + .resource_capture + .site(self.current, index) + .expect("authenticated Resource node"); + self.resource_capture.normalized_node(site, node); + } self.blocks[self.current.index() as usize] .statements .push(value); @@ -1330,18 +1639,103 @@ impl<'a> Builder<'a> { } fn lower_block(&mut self, block: &hir::Block) { + let lexical = match self + .resource_capture + .lexical_scope_under(block, self.reflected_original_prefix.as_deref()) + { + Ok(scope) => scope, + Err(message) => { + self.resource_error = Some(LowerError { + span: block.span, + message, + }); + None + } + }; + let entered = lexical.is_some(); + if let Some(scope) = lexical { + self.lexical_scopes.push(scope); + } for statement in &block.statements { if !self.open() { break; } self.lower_statement(statement); } + if entered { + if self.open() { + self.lower_lexical_exit( + ResourceLexicalExitKind::Fallthrough, + self.lexical_scopes.len() - 1, + None, + None, + block.span, + ); + } + self.lexical_scopes.pop(); + } + } + + fn lower_lexical_exit( + &mut self, + kind: ResourceLexicalExitKind, + floor: usize, + original: Option<&hir::Statement>, + target: Option, + span: Span, + ) { + let position = if kind == ResourceLexicalExitKind::Return { + ResourcePosition::Terminator + } else { + ResourcePosition::Statement(self.blocks[self.current.index() as usize].statements.len()) + }; + if let Some(id) = self.resource_capture.lexical_exit( + kind, + self.current, + position, + &self.lexical_scopes[floor..], + original, + target, + ) && kind != ResourceLexicalExitKind::Return + { + self.push(StatementKind::ResourceLexicalExit(id), span); + } } fn lower_statement(&mut self, statement: &hir::Statement) { match &statement.kind { hir::StatementKind::Let { local, value } => { - let value = if let Some(metadata) = self.locals.get(local.index() as usize) + let seed = match self + .resource_capture + .borrowed_sum_seed(*local, value, self.types) + { + Ok(seed) => seed, + Err(message) => { + self.resource_error = Some(LowerError { + span: statement.span, + message, + }); + None + } + }; + let ordinary_seed = match self.ordinary_sum_capture.seed(*local, value) { + Ok(seed) => seed, + Err(message) => { + self.resource_error = Some(LowerError { + span: statement.span, + message, + }); + None + } + }; + let value = if let Some(seed) = ordinary_seed { + self.lower_ordinary_borrowed_sum_handle(seed) + } else if let Some(seed) = seed { + if let Some(scope) = self.lexical_scopes.last_mut() { + scope.active.push(*local); + } + self.lower_borrowed_sum_handle(seed) + } else if let Some(metadata) = self.locals.get(local.index() as usize) && let Some(source) = metadata.view_source && let Some(origin) = self .locals @@ -1355,6 +1749,15 @@ impl<'a> Builder<'a> { self.types, ) .is_ok() + && (!resource_type_pending(self.types, metadata.ty) + || hir::borrowed_sum_view_initializer( + value, + source, + origin.ty, + metadata.ty, + self.types, + ) + .is_ok_and(|handle| handle.is_none())) { // Stable aliases contain only transparent reads. Taking an // owned snapshot would replace their checked backing local. @@ -1434,22 +1837,56 @@ impl<'a> Builder<'a> { hir::StatementKind::Return(value) => { // This branch abandons surrounding calls before the return // operand may move their owner. Other CFG branches retain them. + let (resource_scopes, constructor_scopes, operation_scopes) = + self.abandon_resource_calls(statement.span); self.end_call_views_since(0, statement.span); let abandoned = std::mem::take(&mut self.call_view_scopes); let generations = self.generation_capture.suspend(); let value = value.as_ref().map(|v| self.lower_value(v)); self.call_view_scopes = abandoned; self.generation_capture.restore(generations); + self.resource_call_scopes = resource_scopes; + self.resource_constructor_scopes = constructor_scopes; + self.resource_operation_scopes = operation_scopes; + self.lower_lexical_exit( + ResourceLexicalExitKind::Return, + 0, + Some(statement), + None, + statement.span, + ); self.terminate(TerminatorKind::Return(value), statement.span); } hir::StatementKind::Break => { - let (_, target, depth) = *self.loops.last().expect("validated break has a loop"); + if !self.resource_operation_scopes.is_empty() { + self.resource_error = Some(LowerError { span: statement.span, message: "pending Resource call region: Break needs its exact operation-depth exit proof".into() }); + } + let (_, target, depth, lexical_depth) = + *self.loops.last().expect("validated break has a loop"); self.end_call_views_since(depth, statement.span); + self.lower_lexical_exit( + ResourceLexicalExitKind::Break, + lexical_depth, + Some(statement), + Some(target), + statement.span, + ); self.terminate(TerminatorKind::Goto(target), statement.span); } hir::StatementKind::Continue => { - let (target, _, depth) = *self.loops.last().expect("validated continue has a loop"); + if !self.resource_operation_scopes.is_empty() { + self.resource_error = Some(LowerError { span: statement.span, message: "pending Resource call region: Continue needs its exact operation-depth exit proof".into() }); + } + let (target, _, depth, lexical_depth) = + *self.loops.last().expect("validated continue has a loop"); self.end_call_views_since(depth, statement.span); + self.lower_lexical_exit( + ResourceLexicalExitKind::Continue, + lexical_depth, + Some(statement), + Some(target), + statement.span, + ); self.terminate(TerminatorKind::Goto(target), statement.span); } hir::StatementKind::If { @@ -1566,8 +2003,12 @@ impl<'a> Builder<'a> { let body_block = self.new_block(body.span); let exit = self.new_block(statement_span); self.terminate(TerminatorKind::Goto(condition_block), statement_span); - self.loops - .push((condition_block, exit, self.call_view_scopes.len())); + self.loops.push(( + condition_block, + exit, + self.call_view_scopes.len(), + self.lexical_scopes.len(), + )); self.current = condition_block; let condition = self.lower_value(condition); self.terminate( @@ -1611,7 +2052,12 @@ impl<'a> Builder<'a> { }, statement_span, ); - self.loops.push((header, exit, self.call_view_scopes.len())); + self.loops.push(( + header, + exit, + self.call_view_scopes.len(), + self.lexical_scopes.len(), + )); self.current = body_block; self.lower_block(body); self.close_to(header, body.span); @@ -1685,6 +2131,13 @@ impl<'a> Builder<'a> { arms: &[hir::ReflectedTypeArm], statement_span: Span, ) { + if let Some(proof) = self + .resource_capture + .reflected_field_dispatch(type_info, arms) + { + self.lower_reflected_field_dispatch(proof, statement_span); + return; + } let arm_blocks = arms .iter() .map(|arm| self.new_block(arm.body.span)) @@ -1725,6 +2178,103 @@ impl<'a> Builder<'a> { self.current = join; } } + fn lower_reflected_field_dispatch( + &mut self, + proof: hir::ReflectedFieldDispatchProof, + statement_span: Span, + ) { + let Some(key) = self.locals.get(proof.key().index() as usize).cloned() else { + self.resource_error = Some(LowerError { + span: statement_span, + message: "Resource reflected binder lost its dense header".into(), + }); + return; + }; + let arms = proof.arms(); + let mut guards = vec![self.current]; + for _ in 1..arms.len() { + guards.push(self.new_block(statement_span)); + } + let dispatches = arms + .iter() + .map(|_| self.new_block(statement_span)) + .collect::>(); + let targets = arms + .iter() + .map(|arm| self.new_block(arm.body.span)) + .collect::>(); + let otherwise = self.new_block(statement_span); + let mut exits = Vec::new(); + for (index, arm) in arms.iter().enumerate() { + let condition = match resource_ownership::reflected_fields::ordinal_guard( + &proof, + proof.type_info(), + index, + ) { + Ok(condition) => condition, + Err(message) => { + self.resource_error = Some(LowerError { + span: statement_span, + message, + }); + return; + } + }; + self.current = guards[index]; + self.terminate( + TerminatorKind::Branch { + condition, + then_block: dispatches[index], + else_block: guards.get(index + 1).copied().unwrap_or(otherwise), + }, + statement_span, + ); + self.current = dispatches[index]; + self.terminate( + TerminatorKind::ReflectedTypeDispatch { + type_info: proof.type_info().clone(), + arms: vec![ReflectedTypeDispatchArm { + iteration_index: index, + bound_type: arm.bound_type, + reflection_identity: arm.reflection_identity.clone(), + target: targets[index], + }], + otherwise, + }, + proof.type_info().span, + ); + self.current = targets[index]; + let prefix = proof.arm_container_path(index).map(<[_]>::to_vec); + if prefix.is_none() { + self.resource_error = Some(LowerError { + span: statement_span, + message: "Resource reflected body lost its exact original Scope path".into(), + }); + return; + } + let previous = std::mem::replace(&mut self.reflected_original_prefix, prefix); + self.lower_block(&arm.body); + self.reflected_original_prefix = previous; + exits.push((self.current, self.open(), arm.body.span)); + } + if exits.iter().any(|(_, open, _)| *open) { + let join = self.new_block(statement_span); + for (exit, _, span) in exits { + self.current = exit; + self.close_to(join, span); + } + self.current = join; + } + if let Err(message) = self + .resource_capture + .capture_reflected_field_dispatch(proof, key, guards, dispatches, targets, otherwise) + { + self.resource_error = Some(LowerError { + span: statement_span, + message, + }); + } + } } #[cfg(test)] diff --git a/crates/jett_mir/src/move_values.rs b/crates/jett_mir/src/move_values.rs index 2b4f3417..34d80a96 100644 --- a/crates/jett_mir/src/move_values.rs +++ b/crates/jett_mir/src/move_values.rs @@ -1,7 +1,9 @@ //! Linear native places, call loans, and immutable nonowning local aliases. //! Definite availability is an intersection fixed point over actual CFG edges. use crate::copy_values::{CopyValuePlan, switch_bindings_on_edge}; -use crate::{ControlFlowGraph, Function, ParamMode, Program, StatementKind, TerminatorKind}; +use crate::{ + ControlFlowGraph, Function, ParamMode, Program, ResourceCallNode, StatementKind, TerminatorKind, +}; use jett_hir::{BinaryOp, Expression, ExpressionKind, IntrinsicId, StringSegment}; use jett_types::{Type, TypeId, TypeInterner}; use std::collections::{BTreeMap, BTreeSet}; @@ -167,10 +169,46 @@ impl MoveValuePlan { function: &Function, types: &TypeInterner, ) -> Result { + Self::analyze_inner(program, function, types, None) + } + pub(crate) fn analyze_companion( + context: &crate::resource_ownership::CompanionContext<'_>, + ) -> Result { + Self::analyze_inner( + context.program, + context.function, + context.types, + Some(context), + ) + } + fn analyze_inner( + program: &Program, + function: &Function, + types: &TypeInterner, + companion: Option<&crate::resource_ownership::CompanionContext<'_>>, + ) -> Result { + if companion.is_none() + && (crate::resource_type_pending(types, function.return_type) + || function + .locals + .iter() + .any(|local| crate::resource_type_pending(types, local.ty))) + { + return Err("pending ResourceOwnershipPlan: ordinary MoveValuePlan cannot transfer or drop Resource custody".into()); + } validate_local_view_initializers(function, types)?; let call_views = crate::call_views::validate(function, types)?; - let caller_acquisitions = crate::validate_caller_acquisitions(program, function, types)?; - let mut plan = CopyValuePlan::analyze_storage(function, types, Some(program))?; + // The private companion context already borrows the exact fresh custody + // proof. Re-run complete Source call validation without the public + // ordinary admission gate; every ordinary acquisition check is retained. + let caller_acquisitions = match companion { + Some(_) => crate::call_ownership::validate_function(program, function, types)?, + None => crate::validate_caller_acquisitions(program, function, types)?, + }; + let mut plan = match companion { + Some(context) => CopyValuePlan::analyze_companion(context)?, + None => CopyValuePlan::analyze_storage(function, types, Some(program))?, + }; if function.identity.declaration.kind == jett_hir::DeclarationKind::ActorHandler { // Captured state is written back after a return/respond terminator. // Keep its owning slots live even when the source body stops reading it. @@ -266,6 +304,7 @@ impl MoveValuePlan { caller_acquisitions: &caller_acquisitions, taking_binding: None, validate: false, + companion, } .block(id)?; changed |= next != outgoing[id.index() as usize] @@ -294,6 +333,7 @@ impl MoveValuePlan { caller_acquisitions: &caller_acquisitions, taking_binding: None, validate: true, + companion, } .block(id)?; } @@ -307,9 +347,49 @@ pub fn validate_local_view_initializers( function: &Function, types: &TypeInterner, ) -> Result<(), String> { + crate::ordinary_borrowed_sums::validate(function, types)?; let stages = crate::call_views::validate(function, types)?; let mut initialized = Set::new(); - for statement in function.blocks.iter().flat_map(|block| &block.statements) { + for (block, index, statement) in function.blocks.iter().flat_map(|block| { + block + .statements + .iter() + .enumerate() + .map(move |(index, statement)| (block.id, index, statement)) + }) { + if let StatementKind::SumTake { + source, + target, + success: true, + } = statement.kind + && let Some(row) = function.ordinary_borrowed_sum_projection(block, index)? + { + if row.source() != source || row.output() != target { + return Err( + "ordinary borrowed initializer changed its constructor projection".into(), + ); + } + initialized.insert(target.index() as usize); + continue; + } + if let StatementKind::SumTake { + source, + target, + success: true, + } = statement.kind + && let Some(row) = function.resource_borrowed_sum_projection( + block, + crate::ResourcePosition::Statement(index), + )? + { + if row.source() != source || row.output() != target { + return Err( + "borrowed Resource initializer changed its constructor projection".into(), + ); + } + initialized.insert(target.index() as usize); + continue; + } let (local, value) = match &statement.kind { StatementKind::Let { local, value } => { if stages.contains_key(&(local.index() as usize)) { @@ -329,7 +409,17 @@ pub fn validate_local_view_initializers( let origin = function .local(source) .ok_or("borrowed local initializer source is outside its function")?; - jett_hir::validate_local_view_initializer(value, source, origin.ty, definition.ty, types)?; + if !function.ordinary_borrowed_sum_alias(*local, value)? + && !function.resource_borrowed_sum_alias(*local, value)? + { + jett_hir::validate_local_view_initializer( + value, + source, + origin.ty, + definition.ty, + types, + )?; + } initialized.insert(local.index() as usize); } for local in &function.locals { @@ -356,11 +446,12 @@ struct Flow<'a> { caller_acquisitions: &'a crate::CallerAcquisitions<'a>, taking_binding: Option, validate: bool, + companion: Option<&'a crate::resource_ownership::CompanionContext<'a>>, } impl Flow<'_> { fn block(mut self, id: crate::BlockId) -> Result<(Set, Set, Set), String> { let block = &self.function.blocks[id.index() as usize]; - for statement in &block.statements { + for (index, statement) in block.statements.iter().enumerate() { match &statement.kind { StatementKind::OpenCallOwnerGeneration { root, .. } => { self.read(*root, "generation owner")?; @@ -412,7 +503,8 @@ impl Flow<'_> { self.state.remove(&(rhs_owner.index() as usize)); self.state.insert(root.index() as usize); } - StatementKind::ReflectedContainerReady { source, .. } => { + StatementKind::ReflectedContainerReady { source, .. } + | StatementKind::ReflectedValueReady { source, .. } => { self.read(*source, "reflected container source")?; } StatementKind::IterationBorrow { @@ -459,7 +551,67 @@ impl Flow<'_> { let root = self.read(*source, "sum source")?; if matches!(statement.kind, StatementKind::SumTake { .. }) { if self.function.is_view_local(*source) { - return Err("cannot take payload from borrowed sum".into()); + if let Some(row) = self + .function + .ordinary_borrowed_sum_projection(block.id, index)? + { + if row.source() != *source { + return Err( + "ordinary borrowed extraction changed its exact source" + .into(), + ); + } + if matches!( + statement.kind, + StatementKind::SumTake { success: true, .. } + ) { + if row.output() != *target + || !self.function.is_view_local(*target) + { + return Err( + "ordinary borrowed projection gained owning output" + .into(), + ); + } + self.aliases.insert(target.index() as usize); + } else { + if row.error() != Some(*target) { + return Err("ordinary borrowed extraction changed its failure local".into()); + } + self.require_owned_definition(*target)?; + } + self.state.insert(target.index() as usize); + continue; + } + let row = self + .function + .resource_borrowed_sum_projection( + block.id, + crate::ResourcePosition::Statement(index), + )? + .ok_or("cannot take payload from borrowed sum")?; + if row.source() != *source { + return Err( + "borrowed Resource extraction changed its current source" + .into(), + ); + } + if matches!( + statement.kind, + StatementKind::SumTake { success: true, .. } + ) { + if row.output() != *target || !self.function.is_view_local(*target) + { + return Err( + "borrowed Resource projection gained owning output".into(), + ); + } + self.aliases.insert(target.index() as usize); + } else { + self.require_owned_definition(*target)?; + } + self.state.insert(target.index() as usize); + continue; } self.reject_aliased_owner_change(root)?; self.state.remove(&source_id); @@ -476,6 +628,16 @@ impl Flow<'_> { self.aliases.insert(id); self.state.insert(id); } + StatementKind::ResourceLexicalExit(_) => { + let exit = self + .function + .resource_lexical_exit(block.id, crate::ResourcePosition::Statement(index))? + .ok_or("Resource lexical marker lost its private constructor proof")?; + for local in exit.current_locals(self.function)? { + self.aliases.remove(&(local.index() as usize)); + self.state.remove(&(local.index() as usize)); + } + } StatementKind::EndCallView { local } => { let id = local.index() as usize; if !self.call_views.contains_key(&id) @@ -487,7 +649,12 @@ impl Flow<'_> { self.aliases.remove(&id); self.state.remove(&id); } - StatementKind::Let { local, value } => { + StatementKind::ResourceCall(ResourceCallNode::Stage { + value, + ordinary: Some(local), + .. + }) + | StatementKind::Let { local, value } => { let definition = self .function .local(*local) @@ -497,13 +664,17 @@ impl Flow<'_> { .function .local(source) .ok_or("borrowed local initializer source is outside its function")?; - jett_hir::validate_local_view_initializer( - value, - source, - origin.ty, - definition.ty, - self.types, - )?; + if !self.function.ordinary_borrowed_sum_alias(*local, value)? + && !self.function.resource_borrowed_sum_alias(*local, value)? + { + jett_hir::validate_local_view_initializer( + value, + source, + origin.ty, + definition.ty, + self.types, + )?; + } self.expr(value, true)?; self.aliases.insert(local.index() as usize); } else { @@ -518,6 +689,17 @@ impl Flow<'_> { result?; } self.state.insert(local.index() as usize); + // A copied primitive/String final binding owns its value. Its + // constructor-only read intermediates cannot keep the shell borrowed. + if let Some(intermediates) = self + .function + .ordinary_copied_sum_intermediates(*local, value)? + { + for intermediate in intermediates { + self.aliases.remove(&(intermediate.index() as usize)); + self.state.remove(&(intermediate.index() as usize)); + } + } } StatementKind::CheckRefinement { local, call, .. } => { self.require_owned_definition(*local)?; @@ -546,6 +728,63 @@ impl Flow<'_> { } self.state.insert(local.index() as usize); } + StatementKind::ResourceCall(ResourceCallNode::Stage { + value, + ordinary: None, + .. + }) => { + if self.companion.is_none() { + return Err("Resource call stage requires its fresh companion plan".into()); + } + self.expr(value, false)?; + } + StatementKind::ResourceCall(ResourceCallNode::Invoke { region, output }) => { + if self.companion.is_none() { + return Err("Resource call invoke requires its fresh companion plan".into()); + } + let record = self + .function + .resource_call_region(*region) + .ok_or("Resource call invoke lacks its sealed region")?; + for actual in record.actuals() { + if let Some(local) = actual.ordinary() { + self.read(local, "Resource call arrived ordinary endpoint")?; + let header = self + .function + .local(local) + .ok_or("Resource call endpoint header missing")?; + if is_linear(self.types, header.ty) + || is_copy_owned(self.types, header.ty) + { + self.state.remove(&(local.index() as usize)); + } + } + } + self.require_owned_definition(*output)?; + self.state.insert(output.index() as usize); + } + StatementKind::ResourceCall( + ResourceCallNode::Begin { .. } | ResourceCallNode::End { .. }, + ) => { + if self.companion.is_none() { + return Err("Resource call region requires its fresh companion plan".into()); + } + } + StatementKind::ResourceConstructor(node) => { + if self.companion.is_none() { + return Err("Resource constructor requires its fresh companion plan".into()); + } + self.function.resource_constructor(node.constructor())?; + match node { + crate::ResourceConstructorNode::Stage { value, .. } => { + self.expr(value, false)? + } + crate::ResourceConstructorNode::Commit { output, .. } => { + self.state.insert(output.index() as usize); + } + _ => {} + } + } StatementKind::Evaluate(value) => self.expr(value, false)?, StatementKind::Assert { condition, message } => { self.expr(condition, false)?; @@ -653,6 +892,72 @@ impl Flow<'_> { } fn expr_inner(&mut self, value: &Expression, borrowed: bool) -> Result<(), String> { + if let Some(context) = self.companion { + if !context.contains(value) { + return Err("Resource companion move is outside its current function".into()); + } + if context.resource_expression(value) { + if context.carrier_expression(value) { + match &value.kind { + ExpressionKind::ListConstruct { elements } => { + for child in elements { + self.expr(child, false)?; + } + return Ok(()); + } + ExpressionKind::MapConstruct { entries } => { + for entry in entries { + self.expr(&entry.key, false)?; + self.expr(&entry.value, false)?; + } + return Ok(()); + } + ExpressionKind::StructConstruct { + fields, + evaluation_order, + .. + } => { + for index in evaluation_order { + self.expr(&fields[*index], false)?; + } + return Ok(()); + } + ExpressionKind::EnumConstruct { + payloads, + evaluation_order, + .. + } => { + for index in evaluation_order { + self.expr(&payloads[*index], false)?; + } + return Ok(()); + } + ExpressionKind::MachineConstruct { payloads, .. } => { + for child in payloads { + self.expr(child, false)?; + } + return Ok(()); + } + ExpressionKind::Field { base, .. } => return self.expr(base, true), + ExpressionKind::InterfaceCoerce { value, adapters } + if adapters.is_empty() => + { + return self.expr(value, borrowed); + } + _ => {} + } + } + match &value.kind { + ExpressionKind::Local(local) => { self.read(*local, "Resource companion initialization")?; return Ok(()); } + ExpressionKind::View(inner) => return self.expr(inner, true), + ExpressionKind::Clone(inner) if context.descriptor_expression(value) => return self.expr(inner, false), + ExpressionKind::ResourceHookValue { .. } | ExpressionKind::OptionalNone => return Ok(()), + ExpressionKind::ResultOk(inner) | ExpressionKind::ResultFail(inner) | ExpressionKind::OptionalSome(inner) => return self.expr(inner, false), + ExpressionKind::Call { .. } | ExpressionKind::IndirectCall { .. } | ExpressionKind::ResourceInvoke { .. } => {}, + _ => return Err("pending Resource companion: expression requires its dedicated custody transport".into()), + } + } + } match &value.kind { ExpressionKind::Local(local) => { let id = local.index() as usize; @@ -939,6 +1244,13 @@ impl Flow<'_> { } } } + ExpressionKind::ResourceInvoke { hook, args, evaluation_order, .. } if self.companion.is_some() => { + let Type::Function { view_params, .. } = self.types.resolve(hook.function_type()) else { return Err("Resource hook has no exact function signature".into()); }; + let saved = self.loans.clone(); + for ¶meter in evaluation_order { self.expr(&args[parameter], view_params[parameter])?; } + self.loans = saved; + } + ExpressionKind::ResourceHookValue { .. } | ExpressionKind::ResourceInvoke { .. } => return Err("pending ResourceOwnershipPlan: ordinary move expression cannot own Resource descriptors or invokes".into()), ExpressionKind::Int(_) | ExpressionKind::Float(_) | ExpressionKind::Bool(_) diff --git a/crates/jett_mir/src/ordinary_borrowed_sums.rs b/crates/jett_mir/src/ordinary_borrowed_sums.rs new file mode 100644 index 00000000..8a4d3e5a --- /dev/null +++ b/crates/jett_mir/src/ordinary_borrowed_sums.rs @@ -0,0 +1,1114 @@ +//! Constructor-certified ordinary Handle views. These records grant no Resource custody. +use super::*; +use hir::{ExpressionKind as E, StatementKind as H}; +use std::sync::Arc; + +#[cfg(test)] +mod tests; + +// Erased interfaces/callables and actors do not expose all runtime payload +// types through a sum's TypeId. Their ordinary projection proof remains pending. +fn ordinary_type_pending(types: &TypeInterner, ty: TypeId) -> bool { + fn visit( + types: &TypeInterner, + ty: TypeId, + seen: &mut std::collections::HashSet, + ) -> bool { + if ty.index() as usize >= types.len() { + return true; + } + if !seen.insert(ty) { + return false; + } + match types.resolve(ty) { + Type::Resource(_) + | Type::Interface(_) + | Type::Function { .. } + | Type::Actor(_) + | Type::Capability(_) + | Type::TypeConstruction + | Type::Error => true, + Type::List(inner) + | Type::Set(inner) + | Type::Optional(inner) + | Type::Secret(inner) + | Type::Refinement { base: inner, .. } => visit(types, *inner, seen), + Type::Map(key, value) | Type::Result(key, value) => { + visit(types, *key, seen) || visit(types, *value, seen) + } + Type::Struct(id) => types + .resolve_struct(*id) + .fields + .iter() + .any(|(_, ty)| visit(types, *ty, seen)), + Type::Bitfield(id) => types + .resolve_bitfield(*id) + .fields + .iter() + .any(|field| visit(types, field.ty, seen)), + Type::Enum(id) => types + .resolve_enum(*id) + .variants + .iter() + .any(|variant| variant.fields.iter().any(|(_, ty)| visit(types, *ty, seen))), + Type::Machine(id) | Type::MachineState { machine: id, .. } => types + .resolve_machine(*id) + .states + .iter() + .any(|state| state.fields.iter().any(|(_, ty)| visit(types, *ty, seen))), + _ => false, + } + } + visit(types, ty, &mut std::collections::HashSet::new()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OrdinarySumPayloadPath { + OptionalSome, + ResultOk, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct OrdinarySumProjectionSite { + block: BlockId, + statement_index: usize, +} +impl OrdinarySumProjectionSite { + pub fn block(self) -> BlockId { + self.block + } + pub fn statement_index(self) -> usize { + self.statement_index + } + pub(super) fn new(block: BlockId, statement_index: usize) -> Self { + Self { + block, + statement_index, + } + } +} + +#[derive(Debug, Clone)] +pub struct OrdinaryBorrowedSumProjection { + original_path: Vec, + original: Expression, + original_alias: Local, + original_backing: Local, + alias: Local, + backing: Local, + source: Local, + output: Local, + tag: Local, + error: Option, + initialize: OrdinarySumProjectionSite, + observe: OrdinarySumProjectionSite, + project: OrdinarySumProjectionSite, + failure: Option, + alias_site: OrdinarySumProjectionSite, + failed: BlockId, + continuation: BlockId, + path: OrdinarySumPayloadPath, +} +impl OrdinaryBorrowedSumProjection { + pub fn alias(&self) -> LocalId { + self.alias.id + } + pub fn backing(&self) -> LocalId { + self.backing.id + } + pub fn source(&self) -> LocalId { + self.source.id + } + pub fn output(&self) -> LocalId { + self.output.id + } + pub fn tag(&self) -> LocalId { + self.tag.id + } + pub fn error(&self) -> Option { + self.error.as_ref().map(|local| local.id) + } + pub fn sum_type(&self) -> TypeId { + self.backing.ty + } + pub fn payload_type(&self) -> TypeId { + self.alias.ty + } + pub fn path(&self) -> OrdinarySumPayloadPath { + self.path + } + pub fn original_handle(&self) -> &Expression { + let E::View(handle) = &self.original.kind else { + unreachable!() + }; + handle + } + pub fn initialize_site(&self) -> OrdinarySumProjectionSite { + self.initialize + } + pub fn observe_site(&self) -> OrdinarySumProjectionSite { + self.observe + } + pub fn success_site(&self) -> OrdinarySumProjectionSite { + self.project + } + pub fn failure_site(&self) -> Option { + self.failure + } + + fn same(&self, other: &Self) -> bool { + self.original_path == other.original_path + && breakpoint_regions::expressions_equal(&self.original, &other.original) + && self.original_alias == other.original_alias + && self.original_backing == other.original_backing + && self.alias == other.alias + && self.backing == other.backing + && self.source == other.source + && self.output == other.output + && self.tag == other.tag + && self.error == other.error + && self.initialize == other.initialize + && self.observe == other.observe + && self.project == other.project + && self.failure == other.failure + && self.alias_site == other.alias_site + && self.failed == other.failed + && self.continuation == other.continuation + && self.path == other.path + } + fn statement<'a>( + &self, + function: &'a Function, + site: OrdinarySumProjectionSite, + ) -> Result<&'a Statement, String> { + function + .blocks + .get(site.block.index() as usize) + .filter(|block| block.id == site.block) + .and_then(|block| block.statements.get(site.statement_index)) + .ok_or_else(|| "ordinary borrowed Handle lost its exact statement site".into()) + } + fn current(&self, function: &Function) -> Result<(), String> { + for header in [ + &self.alias, + &self.backing, + &self.source, + &self.output, + &self.tag, + ] + .into_iter() + .chain(self.error.iter()) + { + if function.local(header.id) != Some(header) { + return Err("ordinary borrowed Handle changed a sealed local header".into()); + } + } + if (self.alias.mutable && self.original_alias.view_source.is_some()) + || self.backing.mutable + || self.source.mutable + || self.output.mutable + || self.alias.view_source != self.original_alias.view_source.map(|_| self.backing.id) + || self.source.view_source != Some(self.backing.id) + || self.output.view_source != Some(self.backing.id) + || self.source.ty != self.backing.ty + || self.output.ty != self.alias.ty + || self.tag.ty != TypeInterner::BOOL + || self.tag.view_source.is_some() + { + return Err("ordinary borrowed Handle lost its immutable nonowning headers".into()); + } + if !matches!(&self.statement(function, self.initialize)?.kind, StatementKind::Let { local, value } + if *local == self.source.id && value.ty == self.source.ty + && matches!(&value.kind, E::View(inner) if inner.ty == self.source.ty + && matches!(inner.kind, E::Local(backing) if backing == self.backing.id))) + || !matches!(self.statement(function, self.observe)?.kind, StatementKind::SumTag { source, target } + if source == self.source.id && target == self.tag.id) + || !matches!(self.statement(function, self.project)?.kind, StatementKind::SumTake { source, target, success: true } + if source == self.source.id && target == self.output.id) + || !matches!(&self.statement(function, self.alias_site)?.kind, StatementKind::Let { local, value } + if *local == self.alias.id && value.ty == self.alias.ty + && matches!(&value.kind, E::View(inner) if inner.ty == self.output.ty + && matches!(inner.kind, E::Local(output) if output == self.output.id))) + { + return Err( + "ordinary borrowed Handle changed its exact initializer or projection".into(), + ); + } + let branch = &function.blocks[self.observe.block.index() as usize]; + if self.initialize.block != self.observe.block + || self.initialize.statement_index >= self.observe.statement_index + || self.alias_site.block != self.continuation + || !matches!(&branch.terminator.kind, TerminatorKind::Branch { condition, then_block, else_block } + if condition.ty == TypeInterner::BOOL && matches!(condition.kind, E::Local(tag) if tag == self.tag.id) + && *then_block == self.project.block && *else_block == self.failed && then_block != else_block) + || !matches!(function.blocks[self.project.block.index() as usize].terminator.kind, + TerminatorKind::Goto(target) if target == self.continuation) + { + return Err("ordinary borrowed Handle lost its exact selecting tag edge".into()); + } + match (&self.error, self.failure) { + (Some(error), Some(site)) + if error.ty == TypeInterner::STRING + && error.view_source.is_none() + && !error.mutable + && site.block == self.failed + && matches!(self.statement(function, site)?.kind, StatementKind::SumTake { source, target, success: false } + if source == self.source.id && target == error.id) => {} + (None, None) if self.path == OrdinarySumPayloadPath::OptionalSome => {} + _ => { + return Err( + "ordinary borrowed Handle changed its owning String failure companion".into(), + ); + } + } + let cfg = ControlFlowGraph::analyze(function).map_err(|errors| format!("{errors:?}"))?; + if !cfg.reverse_postorder().contains(&self.initialize.block) + || !cfg.reverse_postorder().contains(&self.project.block) + || !cfg.reverse_postorder().contains(&self.alias_site.block) + || cfg.predecessors(self.project.block) != [self.observe.block] + || cfg.predecessors(self.failed) != [self.observe.block] + { + return Err( + "ordinary borrowed Handle is disconnected from its constructor guard".into(), + ); + } + let definitions = function.blocks.iter().flat_map(|block| &block.statements); + let alias_count = definitions + .clone() + .filter(|statement| { + matches!(statement.kind, + StatementKind::Let { local, .. } if local == self.alias.id) + }) + .count(); + let output_count = definitions + .filter(|statement| { + matches!(statement.kind, + StatementKind::SumTake { target, .. } if target == self.output.id) + }) + .count(); + if alias_count != 1 || output_count != 1 { + return Err( + "ordinary borrowed Handle has copied or missing current declarations".into(), + ); + } + Ok(()) + } + fn remap_blocks(&mut self, map: &[Option]) -> Result<(), String> { + let remap = |id: &mut BlockId| -> Result<(), String> { + *id = map + .get(id.index() as usize) + .copied() + .flatten() + .ok_or("ordinary borrowed Handle canonical map removed its guard")?; + Ok(()) + }; + for site in [ + &mut self.initialize, + &mut self.observe, + &mut self.project, + &mut self.alias_site, + ] + .into_iter() + .chain(self.failure.iter_mut()) + { + remap(&mut site.block)?; + } + remap(&mut self.failed)?; + remap(&mut self.continuation) + } + fn remap_locals(&mut self, map: &[Option]) -> Result<(), String> { + for header in [ + &mut self.alias, + &mut self.backing, + &mut self.source, + &mut self.output, + &mut self.tag, + ] + .into_iter() + .chain(self.error.iter_mut()) + { + header.id = map + .get(header.id.index() as usize) + .copied() + .flatten() + .ok_or("ordinary borrowed Handle canonical map removed a sealed header")?; + if let Some(source) = &mut header.view_source { + *source = map + .get(source.index() as usize) + .copied() + .flatten() + .ok_or("ordinary borrowed Handle canonical map removed its backing")?; + } + } + Ok(()) + } + fn shift(&mut self, block: BlockId, count: usize) { + for site in [ + &mut self.initialize, + &mut self.observe, + &mut self.project, + &mut self.alias_site, + ] + .into_iter() + .chain(self.failure.iter_mut()) + { + if site.block == block { + site.statement_index += count; + } + } + } +} + +#[derive(Debug, Clone)] +struct Declaration { + path: Vec, + value: Expression, + alias: Local, + backing: Local, + path_kind: OrdinarySumPayloadPath, +} +#[derive(Default)] +struct Inventory { + declarations: Vec, +} +fn child(path: &[usize], kind: usize, index: usize) -> Vec { + let mut child = path.to_vec(); + child.extend([kind, index]); + child +} +impl Inventory { + fn block( + &mut self, + block: &hir::Block, + path: Vec, + function: &hir::Function, + types: &TypeInterner, + ) -> Result<(), String> { + for (index, statement) in block.statements.iter().enumerate() { + let at = child(&path, 0, index); + if let H::Let { local, value } = &statement.kind { + let alias = function + .locals + .get(local.index() as usize) + .filter(|header| header.id == *local) + .ok_or("ordinary borrowed Handle has no original local header")?; + let written_source = match &value.kind { + E::View(handle) => match &handle.kind { + E::Handle { target, .. } => match &target.kind { + E::View(backing) => match backing.kind { + E::Local(source) => Some(source), + _ => None, + }, + _ => None, + }, + _ => None, + }, + _ => None, + }; + if let Some(source) = written_source { + let backing = function + .locals + .get(source.index() as usize) + .filter(|header| header.id == source) + .ok_or("ordinary borrowed Handle has no original backing header")?; + if !ordinary_type_pending(types, backing.ty) + && let Some(handle) = hir::borrowed_sum_view_initializer( + value, source, backing.ty, alias.ty, types, + )? + { + let copyable = + jett_typecheck::ownership::is_implicitly_copyable(types, alias.ty); + if backing.mutable || (alias.mutable && !copyable) { + return Err( + "ordinary borrowed Handle requires an immutable backing and view alias".into() + ); + } + if alias.view_source != if copyable { None } else { Some(source) } { + return Err( + "ordinary borrowed Handle changed its checked alias ownership mode" + .into(), + ); + } + let E::Handle { kind, .. } = &handle.kind else { + unreachable!() + }; + let path_kind = match kind { + hir::HandleKind::Optional => OrdinarySumPayloadPath::OptionalSome, + hir::HandleKind::Result => { + if !matches!(types.resolve(backing.ty), Type::Result(_, failure) if *failure == TypeInterner::STRING) + { + return Err("ordinary borrowed Handle failure companion requires String".into()); + } + OrdinarySumPayloadPath::ResultOk + } + _ => { + return Err( + "ordinary borrowed Handle has an unsupported sum kind".into() + ); + } + }; + if self.declarations.iter().any(|old| old.alias.id == *local) { + return Err( + "ordinary borrowed Handle lacks a unique original declaration" + .into(), + ); + } + self.declarations.push(Declaration { + path: at.clone(), + value: value.clone(), + alias: alias.clone(), + backing: backing.clone(), + path_kind, + }); + } + } + } + let mut values = Vec::new(); + match &statement.kind { + H::Let { value, .. } + | H::HandleDefault(value) + | H::Expression(value) + | H::Respond(value) => values.push(value), + H::Assign { target, value } => values.extend([target, value]), + H::Return(value) => values.extend(value.iter()), + H::If { + condition, + then_block, + else_block, + } => { + values.push(condition); + self.block(then_block, child(&at, 2, 0), function, types)?; + if let Some(block) = else_block { + self.block(block, child(&at, 2, 1), function, types)?; + } + } + H::While { condition, body } => { + values.push(condition); + self.block(body, child(&at, 2, 0), function, types)?; + } + H::For { iterable, body, .. } => { + values.push(iterable); + self.block(body, child(&at, 2, 0), function, types)?; + } + H::Match { scrutinee, arms } => { + values.push(scrutinee); + for (index, arm) in arms.iter().enumerate() { + self.block(&arm.body, child(&at, 2, index), function, types)?; + } + } + H::Assert { condition, message } => { + values.push(condition); + values.extend(message.iter()); + } + H::Breakpoint { condition, .. } => values.extend(condition.iter()), + H::Scope(block) => self.block(block, child(&at, 2, 0), function, types)?, + H::ReflectedTypeDispatch { type_info, arms } => { + values.push(type_info); + for (index, arm) in arms.iter().enumerate() { + self.block(&arm.body, child(&at, 2, index), function, types)?; + } + } + H::Break | H::Continue | H::Trace(_) => {} + } + for (index, value) in values.into_iter().enumerate() { + self.expression(value, child(&at, 1, index), function, types)?; + } + } + Ok(()) + } + fn expression( + &mut self, + value: &Expression, + path: Vec, + function: &hir::Function, + types: &TypeInterner, + ) -> Result<(), String> { + let mut children: Vec<&Expression> = Vec::new(); + match &value.kind { + E::Binary { left, right, .. } => children.extend([left.as_ref(), right.as_ref()]), + E::Unary { value, .. } + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::Declassify(value) + | E::Coarsen(value) + | E::RefinementValidated(value) + | E::InterfaceType(value) + | E::RuntimeFailureMessage(value) + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::View(value) + | E::Clone(value) + | E::Comptime { value, .. } + | E::InterfaceCoerce { value, .. } + | E::FunctionAdapter { value, .. } + | E::StateIs { value, .. } => children.push(value), + E::Call { args, .. } + | E::ResourceInvoke { args, .. } + | E::Intrinsic { args, .. } + | E::ActorSpawn { args, .. } => children.extend(args), + E::IndirectCall { callee, args, .. } => { + children.push(callee); + children.extend(args); + } + E::ActorMessage { actor, args, .. } => { + children.push(actor); + children.extend(args); + } + E::StructConstruct { fields, .. } | E::BitfieldConstruct { fields, .. } => { + children.extend(fields) + } + E::MachineConstruct { payloads, .. } | E::EnumConstruct { payloads, .. } => { + children.extend(payloads) + } + E::MachineTransition { + source, payloads, .. + } => { + children.push(source); + children.extend(payloads); + } + E::ListConstruct { elements } => children.extend(elements), + E::MapConstruct { entries } => { + for entry in entries { + children.extend([&entry.key, &entry.value]); + } + } + E::Handle { + target, failure, .. + } => { + children.push(target); + self.block(failure, child(&path, 3, 0), function, types)?; + } + E::StringInterpolation(parts) => { + for part in parts { + if let hir::StringSegment::Value(value) = part { + children.push(value); + } + } + } + E::Field { base, .. } => children.push(base), + E::InlineFunction { .. } + | E::Int(_) + | E::Float(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::Local(_) + | E::Constant { .. } + | E::FunctionRef(_) + | E::ResourceHookValue { .. } + | E::ClosureRef { .. } + | E::OptionalNone + | E::RuntimeFailure(_) + | E::PropertyCaseContext(_) => {} + } + for (index, value) in children.into_iter().enumerate() { + self.expression(value, child(&path, 1, index), function, types)?; + } + Ok(()) + } +} + +#[derive(Debug, Clone)] +struct CurrentSeal { + id: FunctionId, + identity: FunctionIdentity, + debug_kind: hir::FunctionDebugKind, + params: Vec, + capture_count: usize, + return_type: TypeId, + span: Span, + locals: Vec, + entry: BlockId, + blocks: Vec, + rows: Vec, +} +impl CurrentSeal { + fn new(function: &Function, rows: &[OrdinaryBorrowedSumProjection]) -> Self { + Self { + id: function.id, + identity: function.identity.clone(), + debug_kind: function.debug_kind.clone(), + params: function.params.clone(), + capture_count: function.capture_count, + return_type: function.return_type, + span: function.span, + locals: function.locals.clone(), + entry: function.entry, + blocks: function.blocks.clone(), + rows: rows.to_vec(), + } + } + fn matches(&self, function: &Function) -> bool { + self.id == function.id + && self.identity == function.identity + && self.debug_kind == function.debug_kind + && self.params == function.params + && self.capture_count == function.capture_count + && self.return_type == function.return_type + && self.span == function.span + && self.locals == function.locals + && self.entry == function.entry + && breakpoint_regions::blocks_equal(&self.blocks, &function.blocks) + } +} +#[derive(Debug, Clone)] +pub(super) struct Witness { + original: Arc, + declarations: Arc>, + rows: Vec, + sealed: Arc, +} +impl PartialEq for Witness { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.original, &other.original) + && Arc::ptr_eq(&self.declarations, &other.declarations) + && Arc::ptr_eq(&self.sealed, &other.sealed) + && same_rows(&self.rows, &other.rows) + } +} +fn same_rows( + left: &[OrdinaryBorrowedSumProjection], + right: &[OrdinaryBorrowedSumProjection], +) -> bool { + left.len() == right.len() && left.iter().zip(right).all(|(left, right)| left.same(right)) +} +impl Witness { + fn current(&self, function: &Function) -> Result<(), String> { + if !self.sealed.matches(function) || !same_rows(&self.rows, &self.sealed.rows) { + return Err( + "ordinary borrowed Handle differs from its independently sealed constructor graph" + .into(), + ); + } + if self.original.id != function.id + || self.original.identity != function.identity + || self.rows.len() != self.declarations.len() + { + return Err("ordinary borrowed Handle lost its original function or complete declaration inventory".into()); + } + for (index, row) in self.rows.iter().enumerate() { + row.current(function)?; + if self.rows[..index] + .iter() + .any(|old| old.original_alias.id == row.original_alias.id) + { + return Err("ordinary borrowed Handle duplicated an original declaration".into()); + } + if self + .declarations + .iter() + .filter(|declaration| { + declaration.path == row.original_path + && declaration.alias == row.original_alias + && declaration.backing == row.original_backing + && declaration.path_kind == row.path + && breakpoint_regions::expressions_equal(&declaration.value, &row.original) + }) + .count() + != 1 + || self + .original + .locals + .get(row.original_alias.id.index() as usize) + != Some(&row.original_alias) + || self + .original + .locals + .get(row.original_backing.id.index() as usize) + != Some(&row.original_backing) + || row.original_alias.ty != row.alias.ty + || row.original_backing.ty != row.backing.ty + { + return Err( + "ordinary borrowed Handle changed its exact original declaration association" + .into(), + ); + } + } + Ok(()) + } +} + +#[derive(Default, Clone)] +pub(super) struct Capture { + original: Option>, + declarations: Arc>, + rows: Vec, +} +pub(super) struct Seed { + pub original: Expression, + pub backing: Local, + declaration: Declaration, +} +impl Capture { + pub(super) fn authenticated( + function: &hir::Function, + types: &TypeInterner, + ) -> Result { + let mut inventory = Inventory::default(); + inventory.block(&function.body, Vec::new(), function, types)?; + if inventory.declarations.is_empty() { + return Ok(Self::default()); + } + Ok(Self { + original: Some(Arc::new(function.clone())), + declarations: Arc::new(inventory.declarations), + rows: Vec::new(), + }) + } + pub(super) fn seed(&self, local: LocalId, value: &Expression) -> Result, String> { + let Some(declaration) = self + .declarations + .iter() + .find(|declaration| declaration.alias.id == local) + else { + return Ok(None); + }; + if !breakpoint_regions::expressions_equal(&declaration.value, value) + || self.rows.iter().any(|row| row.original_alias.id == local) + { + return Err( + "ordinary borrowed Handle changed or copied its original initializer".into(), + ); + } + Ok(Some(Seed { + original: declaration.value.clone(), + backing: declaration.backing.clone(), + declaration: declaration.clone(), + })) + } + #[allow(clippy::too_many_arguments)] + pub(super) fn record( + &mut self, + seed: Seed, + source: Local, + output: Local, + tag: Local, + error: Option, + initialize: OrdinarySumProjectionSite, + observe: OrdinarySumProjectionSite, + project: OrdinarySumProjectionSite, + failure: Option, + alias_site: OrdinarySumProjectionSite, + failed: BlockId, + continuation: BlockId, + ) { + self.rows.push(OrdinaryBorrowedSumProjection { + original_path: seed.declaration.path, + original: seed.original, + original_alias: seed.declaration.alias.clone(), + original_backing: seed.declaration.backing.clone(), + alias: seed.declaration.alias, + backing: seed.declaration.backing, + source, + output, + tag, + error, + initialize, + observe, + project, + failure, + alias_site, + failed, + continuation, + path: seed.declaration.path_kind, + }); + } + pub(super) fn finish(self, function: &Function) -> Result, String> { + let Some(original) = self.original else { + return Ok(None); + }; + let sealed = Arc::new(CurrentSeal::new(function, &self.rows)); + let witness = Witness { + original, + declarations: self.declarations, + rows: self.rows, + sealed, + }; + witness.current(function)?; + Ok(Some(witness)) + } +} + +pub(super) fn has_records(function: &Function) -> bool { + function.ordinary_borrowed_sums.is_some() +} +pub(super) fn validate_current(function: &Function) -> Result<(), String> { + function + .ordinary_borrowed_sums + .as_ref() + .map_or(Ok(()), |witness| witness.current(function)) +} +pub(super) fn validate(function: &Function, types: &TypeInterner) -> Result<(), String> { + validate_current(function)?; + for block in &function.blocks { + for (index, statement) in block.statements.iter().enumerate() { + if let StatementKind::SumTag { source, .. } | StatementKind::SumTake { source, .. } = + statement.kind + && function.is_view_local(source) + && let Some(header) = function.local(source) + && !resource_type_pending(types, header.ty) + && function + .ordinary_borrowed_sum_projection(block.id, index)? + .is_none() + { + return Err( + "ordinary borrowed sum operation has no constructor-certified projection" + .into(), + ); + } + } + } + let Some(witness) = &function.ordinary_borrowed_sums else { + return Ok(()); + }; + for declaration in witness.declarations.iter() { + if ordinary_type_pending(types, declaration.backing.ty) + || hir::borrowed_sum_view_initializer( + &declaration.value, + declaration.backing.id, + declaration.backing.ty, + declaration.alias.ty, + types, + )? + .is_none() + { + return Err( + "ordinary borrowed Handle changed its Resource-free original sum type".into(), + ); + } + let copyable = + jett_typecheck::ownership::is_implicitly_copyable(types, declaration.alias.ty); + if declaration.alias.view_source + != if copyable { + None + } else { + Some(declaration.backing.id) + } + { + return Err( + "ordinary borrowed Handle changed its checked final copy or view mode".into(), + ); + } + if declaration.path_kind == OrdinarySumPayloadPath::ResultOk + && !matches!(types.resolve(declaration.backing.ty), Type::Result(_, failure) if *failure == TypeInterner::STRING) + { + return Err("ordinary borrowed Handle changed its exact String failure type".into()); + } + } + Ok(()) +} +impl Function { + /// Read only after the complete constructor graph and original association are authenticated. + pub fn ordinary_borrowed_sum_projection( + &self, + block: BlockId, + statement_index: usize, + ) -> Result, String> { + let Some(witness) = &self.ordinary_borrowed_sums else { + return Ok(None); + }; + witness.current(self)?; + let site = OrdinarySumProjectionSite::new(block, statement_index); + Ok(witness + .rows + .iter() + .find(|row| row.observe == site || row.project == site || row.failure == Some(site))) + } + pub fn ordinary_borrowed_sum_alias( + &self, + local: LocalId, + value: &Expression, + ) -> Result { + let Some(witness) = &self.ordinary_borrowed_sums else { + return Ok(false); + }; + witness.current(self)?; + Ok(witness.rows.iter().any(|row| row.alias.id == local + && matches!(&self.blocks[row.alias_site.block.index() as usize].statements[row.alias_site.statement_index].kind, + StatementKind::Let { local: defined, value: current } if *defined == local && std::ptr::eq(current, value)))) + } + pub(crate) fn ordinary_copied_sum_intermediates( + &self, + local: LocalId, + value: &Expression, + ) -> Result, String> { + if !self.ordinary_borrowed_sum_alias(local, value)? { + return Ok(None); + } + let witness = self + .ordinary_borrowed_sums + .as_ref() + .expect("authenticated ordinary alias"); + Ok(witness + .rows + .iter() + .find(|row| row.alias.id == local && row.original_alias.view_source.is_none()) + .map(|row| [row.source.id, row.output.id])) + } +} + +fn transition( + function: &mut Function, + before: &Function, + expected: &Function, + rows: Vec, +) -> Result<(), String> { + let original = before + .ordinary_borrowed_sums + .as_ref() + .ok_or("ordinary borrowed Handle has no before witness")?; + original.current(before)?; + if function.ordinary_borrowed_sums.as_ref() != Some(original) + || !CurrentSeal::new(expected, &rows).matches(function) + { + return Err( + "ordinary borrowed Handle preparation differs from its exact authenticated edit".into(), + ); + } + // Identity canonical edits preserve the private proof identity as well as the graph. + if original.sealed.matches(function) && same_rows(&rows, &original.rows) { + original.current(function)?; + return Ok(()); + } + let mut witness = original.clone(); + witness.rows = rows; + witness.sealed = Arc::new(CurrentSeal::new(expected, &witness.rows)); + witness.current(function)?; + function.ordinary_borrowed_sums = Some(witness); + Ok(()) +} +pub(super) fn remap_blocks( + function: &mut Function, + before: &Function, + map: &[Option], +) -> Result<(), String> { + let Some(witness) = &before.ordinary_borrowed_sums else { + return Ok(()); + }; + witness.current(before)?; + let cfg = ControlFlowGraph::analyze(before).map_err(|errors| format!("{errors:?}"))?; + let mut canonical = vec![None; before.blocks.len()]; + let mut next = 0; + for block in &before.blocks { + if cfg.reverse_postorder().contains(&block.id) { + canonical[block.id.index() as usize] = Some(BlockId(next)); + next += 1; + } + } + if map != canonical { + return Err("ordinary borrowed Handle block map is not exact reachable compaction".into()); + } + let mut expected = before.clone(); + expected + .blocks + .retain(|block| map[block.id.index() as usize].is_some()); + let remap = |id: &mut BlockId| { + *id = map[id.index() as usize].expect("reachable edge remains reachable"); + }; + remap(&mut expected.entry); + for block in &mut expected.blocks { + remap(&mut block.id); + sequences::prune::block_targets(&mut block.terminator.kind, &remap); + } + let mut rows = witness.rows.clone(); + for row in &mut rows { + row.remap_blocks(map)?; + } + transition(function, before, &expected, rows) +} +pub(super) fn remap_locals( + function: &mut Function, + before: &Function, + map: &[Option], +) -> Result<(), String> { + let Some(witness) = &before.ordinary_borrowed_sums else { + return Ok(()); + }; + witness.current(before)?; + if map.len() != before.locals.len() { + return Err("ordinary borrowed Handle local map has a foreign width".into()); + } + let mut next = 0; + for id in map.iter().flatten() { + if id.index() as usize != next { + return Err("ordinary borrowed Handle local map is not dense compaction".into()); + } + next += 1; + } + let mut expected = before.clone(); + expected + .locals + .retain(|local| map[local.id.index() as usize].is_some()); + let mut valid = true; + let mut remap = |id: &mut LocalId| { + if let Some(mapped) = map.get(id.index() as usize).copied().flatten() { + *id = mapped; + } else { + valid = false; + } + }; + for parameter in &mut expected.params { + remap(&mut parameter.local); + } + for local in &mut expected.locals { + remap(&mut local.id); + if let Some(source) = &mut local.view_source { + remap(source); + } + } + let mut floor = |floor: &mut u32| { + if (*floor as usize) <= map.len() { + *floor = map[..*floor as usize] + .iter() + .filter(|id| id.is_some()) + .count() as u32; + } + }; + for block in &mut expected.blocks { + sequences::prune::block_locals(block, &mut remap, &mut floor); + } + if !valid { + return Err("ordinary borrowed Handle local map removed an execution read".into()); + } + let mut rows = witness.rows.clone(); + for row in &mut rows { + row.remap_locals(map)?; + } + transition(function, before, &expected, rows) +} +pub(super) fn sequence_transition( + function: &mut Function, + before: &Function, + edit: &breakpoint_regions::SequenceEdit, + types: &TypeInterner, +) -> Result<(), String> { + let Some(witness) = &before.ordinary_borrowed_sums else { + return Ok(()); + }; + validate(before, types)?; + // The existing finite edit validator checks every block, prefix and redirected + // edge against the exact original ForEach. The shadow grants no breakpoint proof. + let mut shadow = function.clone(); + breakpoint_regions::sequence_transition(&mut shadow, before, edit.clone(), types)?; + if function.locals.len() < before.locals.len() + || function.locals[..before.locals.len()] != before.locals + || function.params != before.params + || function.entry != before.entry + { + return Err( + "ordinary borrowed Handle sequence preparation changed existing headers".into(), + ); + } + let mut rows = witness.rows.clone(); + if let Some((block, prefix)) = &edit.prefix { + for row in &mut rows { + row.shift(*block, prefix.len()); + } + } + transition(function, before, &shadow, rows) +} diff --git a/crates/jett_mir/src/ordinary_borrowed_sums/tests.rs b/crates/jett_mir/src/ordinary_borrowed_sums/tests.rs new file mode 100644 index 00000000..6cfcabb9 --- /dev/null +++ b/crates/jett_mir/src/ordinary_borrowed_sums/tests.rs @@ -0,0 +1,1278 @@ +use super::*; +use crate::copy_values::CopyValuePlan; +use crate::move_values::MoveValuePlan; +use jett_common::{FileId, SourceOrigin}; +use jett_hir::{ExpressionKind as E, HandleKind}; +use jett_types::{Type, TypeInterner}; +use std::collections::HashMap; + +const SOURCES: [&str; 12] = [ + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/01_optional_branches.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/02_result_branches.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/03_owned_scoped_forwarded.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/04_named_prefix_return.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/05_optional_terminal_prefix.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/06_result_terminal_prefix.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/07_outer_pending_some.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/08_outer_pending_none.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/09_outer_pending_ok.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/10_outer_pending_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/11_pending_optional_payload.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/12_pending_result_payload.jett" + ), +]; + +fn lowered(source: &str, release: bool) -> (crate::Program, TypeInterner) { + let project = FileId::new(0); + let list = FileId::new(jett_common::STDLIB_FILE_ID_START); + let bytes = FileId::new(jett_common::STDLIB_FILE_ID_START + 1); + let mut parsed = jett_parser::parse(include_str!("../../../../stdlib/list.jett"), list); + let mut byte_module = jett_parser::parse(include_str!("../../../../stdlib/bytes.jett"), bytes); + let mut primary = jett_parser::parse(source, project); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(byte_module.errors.is_empty(), "{:?}", byte_module.errors); + assert!(primary.errors.is_empty(), "{:?}", primary.errors); + parsed.module.items.append(&mut byte_module.module.items); + parsed.module.items.append(&mut primary.module.items); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check_with_options( + &parsed.module, + &resolved, + jett_typecheck::CheckOptions { release }, + ); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let high = jett_hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([ + (project, SourceOrigin::Project), + (list, SourceOrigin::Stdlib), + (bytes, SourceOrigin::Stdlib), + ]), + ) + .expect("genuine frozen Source and shipped stdlib HIR"); + ( + crate::lower(&high, &checked.interner).expect("genuine borrowed Handle MIR"), + checked.interner, + ) +} + +fn guarded_function(program: &crate::Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.ordinary_borrowed_sums.is_some() + }) + .expect("original ordinary borrowed declaration") +} + +fn guarded_function_mut(program: &mut crate::Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.ordinary_borrowed_sums.is_some() + }) + .expect("original ordinary borrowed declaration") +} + +fn assert_plans(program: &crate::Program, types: &TypeInterner, fixture: usize) -> usize { + crate::validate_call_ownership(program, types).unwrap(); + let mut list_rows = 0; + for function in &program.functions { + let Some(witness) = &function.ordinary_borrowed_sums else { + continue; + }; + let copy = CopyValuePlan::analyze_storage(function, types, Some(program)) + .unwrap_or_else(|error| panic!("Source{fixture:02} Copy: {error}")); + let plan = MoveValuePlan::analyze(program, function, types) + .unwrap_or_else(|error| panic!("Source{fixture:02} Move: {error}")); + assert_eq!(plan.owned_locals, copy.owned_locals); + for row in &witness.rows { + list_rows += usize::from(matches!(types.resolve(row.payload_type()), Type::List(_))); + assert_eq!(function.local(row.backing()).unwrap().ty, row.sum_type()); + for local in [row.source(), row.output()] { + let header = function.local(local).unwrap(); + assert!(!header.mutable); + assert_eq!(header.view_source, Some(row.backing())); + assert!(function.is_view_local(local)); + assert!(!plan.owned_locals.contains(&(local.index() as usize))); + assert!(!copy.owned_locals.contains(&(local.index() as usize))); + } + let alias = function.local(row.alias()).unwrap(); + assert_eq!(alias.mutable, row.original_alias.mutable); + if alias.view_source.is_some() { + assert!( + !alias.mutable, + "a true borrowed final alias remains immutable" + ); + } + assert_eq!( + alias.view_source, + row.original_alias.view_source.map(|_| row.backing()) + ); + let owned_alias = alias.view_source.is_none() && alias.ty == TypeInterner::STRING; + assert_eq!( + plan.owned_locals.contains(&(alias.id.index() as usize)), + owned_alias + ); + assert_eq!( + copy.owned_locals.contains(&(alias.id.index() as usize)), + owned_alias + ); + assert_eq!(function.local(row.source()).unwrap().ty, row.sum_type()); + assert_eq!(function.local(row.output()).unwrap().ty, row.payload_type()); + assert_eq!(function.local(row.alias()).unwrap().ty, row.payload_type()); + let initialization = row.initialize_site(); + assert!( + matches!(&function.blocks[initialization.block().index() as usize].statements[initialization.statement_index()].kind, StatementKind::Let { local, value } if *local == row.source() && matches!(&value.kind, E::View(inner) if matches!(inner.kind, E::Local(backing) if backing == row.backing()))) + ); + let observed = row.observe_site(); + assert!( + matches!(function.blocks[observed.block().index() as usize].statements[observed.statement_index()].kind, StatementKind::SumTag { source, target } if source == row.source() && target == row.tag()) + ); + let projected = row.success_site(); + assert!( + matches!(function.blocks[projected.block().index() as usize].statements[projected.statement_index()].kind, StatementKind::SumTake { source, target, success: true } if source == row.source() && target == row.output()) + ); + assert_eq!( + function + .ordinary_borrowed_sum_projection( + projected.block(), + projected.statement_index() + ) + .unwrap() + .unwrap() + .alias(), + row.alias() + ); + let alias_value = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + StatementKind::Let { local, value } if *local == row.alias() => Some(value), + _ => None, + }) + .unwrap(); + assert!( + function + .ordinary_borrowed_sum_alias(row.alias(), alias_value) + .unwrap() + ); + let E::Handle { kind, .. } = &row.original_handle().kind else { + panic!("complete original Handle") + }; + match ( + types.resolve(row.sum_type()), + kind, + row.error(), + row.failure_site(), + ) { + (Type::Optional(payload), HandleKind::Optional, None, None) => { + assert_eq!(*payload, row.payload_type()) + } + (Type::Result(payload, failure), HandleKind::Result, Some(error), Some(site)) => { + assert_eq!(*payload, row.payload_type()); + assert_eq!(*failure, TypeInterner::STRING); + let header = function.local(error).unwrap(); + assert_eq!(header.ty, TypeInterner::STRING); + assert!(header.view_source.is_none()); + assert!(plan.owned_locals.contains(&(error.index() as usize))); + assert!(copy.owned_locals.contains(&(error.index() as usize))); + assert!(copy.temporary_slots > 0); + assert!( + matches!(function.blocks[site.block().index() as usize].statements[site.statement_index()].kind, StatementKind::SumTake { source, target, success: false } if source == row.source() && target == error) + ); + assert_eq!( + function + .ordinary_borrowed_sum_projection(site.block(), site.statement_index()) + .unwrap() + .unwrap() + .alias(), + row.alias() + ); + } + _ => panic!("exact sum, payload and owned failure companion"), + } + for (planner, live_in, live_out, live_after) in [ + ( + "Move", + &plan.live_in, + &plan.live_out, + &plan.live_after_statement, + ), + ( + "Copy", + ©.live_in, + ©.live_out, + ©.live_after_statement, + ), + ] { + let mut observed_live = false; + for live in live_in + .iter() + .chain(live_out) + .chain(live_after.iter().flatten()) + { + if (alias.view_source.is_some() + && live.contains(&(row.alias().index() as usize))) + || live.contains(&(row.output().index() as usize)) + || live.contains(&(row.source().index() as usize)) + { + observed_live = true; + assert!( + live.contains(&(row.backing().index() as usize)), + "Source{fixture:02} {planner} backing must outlive its payload view" + ); + } + } + assert!( + observed_live, + "Source{fixture:02} {planner} visits a live projection" + ); + } + assert_eq!( + plan.owned_locals + .contains(&(row.backing().index() as usize)), + !function.is_view_local(row.backing()) + ); + } + } + list_rows +} + +#[test] +fn ordinary_borrowed_sums_all_frozen_sources_keep_nonowning_payload_and_owned_failure_plans() { + let expected = [2, 2, 9, 2, 1, 1, 1, 1, 1, 1, 1, 1]; + for release in [false, true] { + for (index, source) in SOURCES.into_iter().enumerate() { + let (mut program, types) = lowered(source, release); + if index != 2 { + assert_eq!(assert_plans(&program, &types, index + 1), expected[index]); + } else { + crate::validate_call_ownership(&program, &types).unwrap(); + } + crate::prepare_native_sequences(&mut program, &types); + for function in &mut program.functions { + assert!( + crate::sequences::prune::unreachable(function), + "Source{:02} release={release}", + index + 1 + ); + assert!( + crate::sequences::prune::unused_locals(function), + "Source{:02} local remap release={release}", + index + 1 + ); + } + assert_eq!(assert_plans(&program, &types, index + 1), expected[index]); + } + } +} + +#[test] +fn ordinary_borrowed_sums_owned_scoped_forwarded_source_keeps_original_owner_across_real_loop_preparation() + { + for release in [false, true] { + let (mut program, types) = lowered(SOURCES[2], release); + let has_for = |program: &crate::Program| { + program + .functions + .iter() + .filter(|function| function.identity.declaration.namespace == "app") + .flat_map(|function| &function.blocks) + .any(|block| matches!(block.terminator.kind, TerminatorKind::ForEach { .. })) + }; + assert!(has_for(&program)); + crate::validate_call_ownership(&program, &types).unwrap(); + crate::prepare_native_sequences(&mut program, &types); + assert!( + !has_for(&program), + "Source03 must complete its actual For lowering" + ); + for function in &program.functions { + if function.identity.declaration.namespace != "app" + || !matches!( + function.identity.declaration.name.as_str(), + "inspect_optional" | "inspect_result" + ) + { + continue; + } + let named = |name: &str| { + function + .locals + .iter() + .find(|local| local.name == name) + .unwrap() + .id + }; + let source = named("source"); + let alias = named("alias"); + let forwarded = named("forwarded"); + let copied = named("copied"); + assert_eq!(function.local(alias).unwrap().view_source, Some(source)); + assert_eq!(function.local(forwarded).unwrap().view_source, Some(alias)); + assert!(function.local(copied).unwrap().view_source.is_none()); + let plan = MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!(plan.owned_locals.contains(&(source.index() as usize))); + assert!(plan.owned_locals.contains(&(copied.index() as usize))); + assert!(!plan.owned_locals.contains(&(alias.index() as usize))); + assert!(!plan.owned_locals.contains(&(forwarded.index() as usize))); + let mut used = false; + for live in plan.live_in.iter().chain(&plan.live_out).chain( + plan.live_after_statement + .iter() + .flat_map(|block| block.iter()), + ) { + if live.contains(&(forwarded.index() as usize)) { + used = true; + assert!(live.contains(&(alias.index() as usize))); + assert!(live.contains(&(source.index() as usize))); + } + } + assert!(used); + } + assert_eq!(assert_plans(&program, &types, 3), 9); + } +} + +#[test] +fn ordinary_borrowed_sums_scalar_count_preserves_checked_copyable_alias_mode() { + for release in [false, true] { + let (mut program, types) = lowered(SOURCES[2], release); + crate::prepare_native_sequences(&mut program, &types); + assert_eq!(assert_plans(&program, &types, 3), 9); + let scalar_rows = program + .functions + .iter() + .filter_map(|function| { + function + .ordinary_borrowed_sums + .as_ref() + .map(|witness| (function, witness)) + }) + .flat_map(|(function, witness)| witness.rows.iter().map(move |row| (function, row))) + .filter(|(_, row)| row.payload_type() == TypeInterner::INT64) + .collect::>(); + assert_eq!( + scalar_rows.len(), + 1, + "Source03 count[int64] has its own constructor record" + ); + let (function, row) = scalar_rows[0]; + assert_eq!(function.identity.declaration.name, "count"); + assert!(row.original_alias.view_source.is_none()); + assert!(function.local(row.alias()).unwrap().view_source.is_none()); + assert_eq!( + function.local(row.output()).unwrap().view_source, + Some(row.backing()) + ); + assert_eq!( + function.local(row.source()).unwrap().view_source, + Some(row.backing()) + ); + let move_plan = MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!( + !move_plan + .owned_locals + .contains(&(row.alias().index() as usize)) + ); + assert!( + !move_plan + .owned_locals + .contains(&(row.output().index() as usize)) + ); + } +} + +const STRING_COPY: &str = r#"namespace app +function inspect(view source: optional[string]) returns string: + string alias = view((view source) handle: + return "absent" + ) + return alias +function main() returns string: + optional[string] source = some("present") + return inspect(view source) +"#; + +#[test] +fn ordinary_borrowed_sums_string_success_copies_into_owned_final_alias() { + for release in [false, true] { + let (mut program, types) = lowered(STRING_COPY, release); + assert_eq!(assert_plans(&program, &types, 0), 0); + let function = guarded_function(&program); + let row = &function.ordinary_borrowed_sums.as_ref().unwrap().rows[0]; + assert_eq!(row.payload_type(), TypeInterner::STRING); + assert!(row.original_alias.view_source.is_none()); + assert!(function.local(row.alias()).unwrap().view_source.is_none()); + assert!(!function.is_view_local(row.alias())); + assert!(function.is_view_local(row.output())); + let copy = CopyValuePlan::analyze_storage(function, &types, Some(&program)).unwrap(); + let moved = MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!(copy.owned_locals.contains(&(row.alias().index() as usize))); + assert!(moved.owned_locals.contains(&(row.alias().index() as usize))); + assert!(!copy.owned_locals.contains(&(row.output().index() as usize))); + assert!( + !moved + .owned_locals + .contains(&(row.output().index() as usize)) + ); + assert!( + copy.temporary_slots > 0, + "final String View takes its ordinary owned copy" + ); + crate::prepare_native_sequences(&mut program, &types); + for function in &mut program.functions { + assert!(crate::sequences::prune::unreachable(function)); + } + assert_eq!(assert_plans(&program, &types, 0), 0); + } +} + +const CALLBACK_COMPACTION: &str = r#"namespace app +function inspect(view source: optional[list[int64]]) returns int64: + list[int64] alias = view((view source) handle: + return 0 + int64 dead = 7 + return 0 + ) + int64 retained = 3 + function(int64) returns int64 callback = function(value: int64) returns int64: return retained + value + return list.sum[int64](view alias) + callback(2) +function main() returns int64: + optional[list[int64]] source = some(list(4, 5)) + return inspect(view source) +"#; + +const COPY_THEN_MOVE_OPTIONAL: &str = r#"namespace app +function consume(source: optional[int64]) returns int64: + return source handle: return 0 +function main() returns int64: + optional[int64] source = some(5) + int64 alias = view((view source) handle: return 0) + int64 owned = consume(source) + return alias + owned +"#; + +const COPY_THEN_MOVE_RESULT: &str = r#"namespace app +function consume(source: result[string, string]) returns string: + return source handle error: return error +function main() returns string: + result[string, string] source = ok("present") + string alias = view((view source) handle error: return error) + string owned = consume(source) + return "{alias}:{owned}" +"#; + +// Exact genuine Source15 from Root's mutable-copy reference control. +const MUTABLE_COPIED_PAYLOADS: &str = r#"namespace app +function copied_int(view stdout: Stdout) returns nothing: + optional[int64] source = some(3) + mutable int64 copied = view((view source) handle: + return nothing + ) + copied = copied + 1 + int64 original = source handle: + return nothing + Stdout.write(view stdout, "integer:{copied}:{original}\n") + return nothing +function copied_string(view stdout: Stdout) returns nothing: + result[string, string] source = ok("kept") + mutable string copied = view((view source) handle error: + return nothing + ) + copied = "changed" + string original = source handle error: + return nothing + Stdout.write(view stdout, "string:{copied}:{original}\n") + return nothing +function main(stdout: Stdout) returns nothing: + copied_int(view stdout) + copied_string(view stdout) + return nothing +"#; + +#[test] +fn ordinary_borrowed_sums_mutable_copied_payloads_rebind_before_original_owning_unwrap() { + for release in [false, true] { + let (mut program, types) = lowered(MUTABLE_COPIED_PAYLOADS, release); + for prepared in [false, true] { + if prepared { + crate::prepare_native_sequences(&mut program, &types); + for function in &mut program.functions { + assert!(crate::sequences::prune::unreachable(function)); + assert!(crate::sequences::prune::unused_locals(function)); + } + } + assert_eq!(assert_plans(&program, &types, 15), 0); + let mut copied_types = Vec::new(); + for function in &program.functions { + let Some(witness) = &function.ordinary_borrowed_sums else { + continue; + }; + assert_eq!(witness.rows.len(), 1); + let row = &witness.rows[0]; + copied_types.push(row.payload_type()); + assert!(row.original_alias.mutable); + assert!(row.original_alias.view_source.is_none()); + assert!(function.local(row.alias()).unwrap().mutable); + assert!(function.local(row.alias()).unwrap().view_source.is_none()); + assert!(!function.local(row.backing()).unwrap().mutable); + assert!(!function.is_view_local(row.backing())); + for intermediate in [row.source(), row.output()] { + let header = function.local(intermediate).unwrap(); + assert!(!header.mutable); + assert_eq!(header.view_source, Some(row.backing())); + } + let continuation = &function.blocks[row.alias_site.block.index() as usize]; + let (assigned_at, replacement) = continuation.statements.iter().enumerate().find_map(|(index, statement)| match &statement.kind { + StatementKind::Assign { target, value } if matches!(target.kind, E::Local(local) if local == row.alias()) => Some((index, value)), + _ => None, + }).expect("the exact Source rebinds its final copied payload"); + assert!(assigned_at > row.alias_site.statement_index); + if row.payload_type() == TypeInterner::INT64 { + assert_eq!(function.identity.declaration.name, "copied_int"); + assert!( + matches!(&replacement.kind, E::Binary { left, right, .. } if matches!(left.kind, E::Local(local) if local == row.alias()) && matches!(right.kind, E::Int(1))) + ); + } else { + assert_eq!(row.payload_type(), TypeInterner::STRING); + assert_eq!(function.identity.declaration.name, "copied_string"); + assert!(matches!(&replacement.kind, E::String(value) if value == "changed")); + } + let later_owning_take = function + .blocks + .iter() + .find_map(|block| { + block + .statements + .iter() + .find_map(|statement| match statement.kind { + StatementKind::SumTake { + source, + target, + success: true, + } if source != row.source() + && !function.is_view_local(source) + && function.local(source).unwrap().ty == row.sum_type() + && function.local(target).unwrap().ty == row.payload_type() => + { + Some((block.id, source)) + } + _ => None, + }) + }) + .expect("later bare owning Handle unwraps its ordinary shell snapshot"); + assert_ne!(later_owning_take.0, row.project.block); + let cfg = ControlFlowGraph::analyze(function).unwrap(); + assert!(cfg.reverse_postorder().contains(&later_owning_take.0)); + let copy = + CopyValuePlan::analyze_storage(function, &types, Some(&program)).unwrap(); + let moved = MoveValuePlan::analyze(&program, function, &types) + .expect("rebinding a copied payload does not create or preserve a source loan"); + for live_after in [©.live_after_statement, &moved.live_after_statement] { + let after_rebind = + &live_after[row.alias_site.block.index() as usize][assigned_at]; + assert!(after_rebind.contains(&(row.alias().index() as usize))); + assert!(!after_rebind.contains(&(row.source().index() as usize))); + assert!(!after_rebind.contains(&(row.output().index() as usize))); + } + } + copied_types.sort_by_key(|ty| ty.index()); + let mut expected = vec![TypeInterner::INT64, TypeInterner::STRING]; + expected.sort_by_key(|ty| ty.index()); + assert_eq!(copied_types, expected); + } + } +} + +#[test] +fn ordinary_borrowed_sums_genuine_mutable_list_payload_still_refuses_native_borrow() { + let source = r#"namespace app +function inspect(view source: optional[list[int64]]) returns nothing: + mutable list[int64] alias = view((view source) handle: return nothing) + trace alias + return nothing +function main() returns nothing: + optional[list[int64]] source = some(list(3)) + inspect(view source) + return nothing +"#; + for release in [false, true] { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check_with_options( + &parsed.module, + &resolved, + jett_typecheck::CheckOptions { release }, + ); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let errors = jett_hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .expect_err("genuine move-only payload View cannot declare a mutable borrowed final alias"); + assert!( + errors.iter().any(|error| error + .message + .contains("native borrowed alias requires immutable bindings")), + "{errors:?}" + ); + } +} + +#[test] +fn ordinary_borrowed_sums_copied_final_alias_retires_projection_loans_before_original_shell_transfer() + { + for release in [false, true] { + for source in [COPY_THEN_MOVE_OPTIONAL, COPY_THEN_MOVE_RESULT] { + let (mut program, types) = lowered(source, release); + for prepared in [false, true] { + if prepared { + crate::prepare_native_sequences(&mut program, &types); + for function in &mut program.functions { + assert!(crate::sequences::prune::unreachable(function)); + assert!(crate::sequences::prune::unused_locals(function)); + } + } + assert_eq!(assert_plans(&program, &types, 0), 0); + let function = guarded_function(&program); + let witness = function.ordinary_borrowed_sums.as_ref().unwrap(); + assert_eq!(witness.rows.len(), 1); + let row = &witness.rows[0]; + assert!(row.original_alias.view_source.is_none()); + assert!(function.local(row.alias()).unwrap().view_source.is_none()); + assert!( + !function.is_view_local(row.backing()), + "the original sum shell is owned" + ); + assert_eq!( + function.local(row.source()).unwrap().view_source, + Some(row.backing()) + ); + assert_eq!( + function.local(row.output()).unwrap().view_source, + Some(row.backing()) + ); + let (call_block, call_index, call) = function.blocks.iter().find_map(|block| { + block.statements.iter().enumerate().find_map(|(index, statement)| match &statement.kind { + StatementKind::Let { value, .. } if matches!(value.kind, E::Call { function: target, .. } if program.functions[target.index() as usize].identity.declaration.name == "consume") => Some((block.id, index, value)), + _ => None, + }) + }).expect("actual owning helper invocation"); + assert_eq!(call_block, row.alias_site.block); + assert!( + call_index > row.alias_site.statement_index, + "copy precedes the source transfer" + ); + let acquisitions = + crate::validate_caller_acquisitions(&program, function, &types).unwrap(); + let [acquired] = acquisitions + .arguments(call) + .expect("current owning call acquisition") + else { + panic!("one original owned shell argument") + }; + assert_eq!(acquired.parameter_index, 0); + assert_eq!(acquired.source.binding, Some(row.backing())); + assert_eq!( + acquired.source.effect, + jett_typecheck::CheckedCallerEffect::TransferOwned + ); + assert_eq!(acquired.source.actual_type, row.sum_type()); + assert_eq!(acquired.source.storage_type, row.sum_type()); + let E::Call { + function: target, + args, + .. + } = &call.kind + else { + panic!("owning helper call") + }; + assert_eq!( + program.functions[target.index() as usize].params[0].mode, + ParamMode::Owned + ); + assert_eq!(acquisitions.argument_binding(&args[0]), Some(row.backing())); + let copy = + CopyValuePlan::analyze_storage(function, &types, Some(&program)).unwrap(); + let moved = MoveValuePlan::analyze(&program, function, &types).expect("a copied final alias leaves no active projection loan that can block moving its original shell"); + for live_after in [©.live_after_statement, &moved.live_after_statement] { + let live = &live_after[row.alias_site.block.index() as usize] + [row.alias_site.statement_index]; + assert!( + live.contains(&(row.alias().index() as usize)), + "the final copy is used after source transfer" + ); + assert!( + live.contains(&(row.backing().index() as usize)), + "the original backing is acquired by the later call" + ); + assert!(!live.contains(&(row.source().index() as usize))); + assert!(!live.contains(&(row.output().index() as usize))); + } + } + } + } +} + +#[test] +fn ordinary_borrowed_sums_genuine_callback_capture_survives_local_compaction() { + for release in [false, true] { + let (mut program, types) = lowered(CALLBACK_COMPACTION, release); + assert_eq!(assert_plans(&program, &types, 0), 1); + let original = guarded_function(&program).clone(); + let dead = original + .locals + .iter() + .find(|local| local.name == "dead") + .unwrap() + .id; + let retained = original + .locals + .iter() + .find(|local| local.name == "retained") + .unwrap() + .id; + let callback = original + .locals + .iter() + .find(|local| local.name == "callback") + .unwrap() + .id; + let inline_parameter = original + .locals + .iter() + .find(|local| local.name == "value") + .unwrap() + .id; + assert!(dead.index() < retained.index()); + assert!(retained.index() < inline_parameter.index()); + assert!(inline_parameter.index() < callback.index()); + assert!(!original.blocks.iter().flat_map(|block| &block.statements).any(|statement| matches!(statement.kind, StatementKind::Let { local, .. } if local == dead))); + let lifted = program + .functions + .iter() + .filter(|function| function.debug_kind == hir::FunctionDebugKind::Inline) + .cloned() + .collect::>(); + assert_eq!(lifted.len(), 1); + assert_eq!(lifted[0].capture_count, 1); + assert_eq!(lifted[0].params[0].name, "retained"); + assert!(crate::sequences::prune::unused_locals( + guarded_function_mut(&mut program) + )); + let function = guarded_function(&program); + assert_eq!(function.locals.len(), original.locals.len() - 2); + assert!(!function.locals.iter().any(|local| local.name == "dead")); + assert!(!function.locals.iter().any(|local| local.name == "value")); + let moved_retained = function + .locals + .iter() + .find(|local| local.name == "retained") + .unwrap() + .id; + let moved_callback = function + .locals + .iter() + .find(|local| local.name == "callback") + .unwrap() + .id; + assert_eq!(moved_retained.index(), retained.index() - 1); + assert_eq!(moved_callback.index(), callback.index() - 2); + assert!(function.blocks.iter().flat_map(|block| &block.statements).any(|statement| matches!(&statement.kind, StatementKind::Let { local, value } if *local == moved_callback && matches!(&value.kind, E::ClosureRef { function, captures } if *function == lifted[0].id && captures == &[moved_retained])))); + assert_eq!( + program + .functions + .iter() + .filter(|function| function.debug_kind == hir::FunctionDebugKind::Inline) + .cloned() + .collect::>(), + lifted, + "caller compaction preserves separately lifted callback ABI" + ); + assert_eq!(assert_plans(&program, &types, 0), 1); + let compacted = program.clone(); + assert!(crate::sequences::prune::unused_locals( + guarded_function_mut(&mut program) + )); + assert!( + program == compacted, + "second unused_locals must preserve the exact compacted callback program; release={release}" + ); + } +} + +const COMPOSED_ABSENT: &str = r#"namespace app +function inspect[T](view source: optional[list[int64]], absent: optional[T]) returns int64: + list[int64] alias = view((view source) handle: return 0) + T impossible = absent handle: return list.sum[int64](view alias) + trace impossible + return list.sum[int64](view alias) +function main() returns int64: + optional[list[int64]] source = some(list(4, 5)) + return inspect(view source, none) +"#; + +#[test] +fn ordinary_borrowed_sums_unrelated_optional_never_preparation_keeps_complete_valid_function() { + for release in [false, true] { + let (mut program, types) = lowered(COMPOSED_ABSENT, release); + assert_eq!(assert_plans(&program, &types, 0), 1); + let original = guarded_function(&program).clone(); + assert!( + original + .locals + .iter() + .any(|local| local.ty == TypeInterner::NEVER) + ); + let absent_site = original.blocks.iter().find_map(|block| block.statements.iter().find_map(|statement| match statement.kind { + StatementKind::SumTake { source, success: true, .. } if matches!(types.resolve(original.local(source).unwrap().ty), Type::Optional(payload) if *payload == TypeInterner::NEVER) => Some(block.id), + _ => None, + })).unwrap(); + crate::prepare_native_uninhabited_sums(&mut program, &types); + let prepared = guarded_function(&program); + crate::validate_call_ownership(&program, &types).unwrap(); + assert_eq!(assert_plans(&program, &types, 0), 1); + if prepared != &original { + assert!(!prepared.blocks.iter().flat_map(|block| &block.statements).any(|statement| matches!(statement.kind, StatementKind::SumTake { source, success: true, .. } if matches!(types.resolve(prepared.local(source).unwrap().ty), Type::Optional(payload) if *payload == TypeInterner::NEVER)))); + assert!( + ControlFlowGraph::analyze(prepared) + .unwrap() + .reverse_postorder() + .iter() + .all(|id| (id.index() as usize) < prepared.blocks.len()) + ); + } else { + assert!( + ControlFlowGraph::analyze(prepared) + .unwrap() + .reverse_postorder() + .contains(&absent_site), + "a rollback preserves the complete original graph" + ); + } + let once = program.clone(); + crate::prepare_native_uninhabited_sums(&mut program, &types); + assert_eq!(program, once); + } +} + +fn assert_refused(program: &crate::Program, types: &TypeInterner) { + assert!(crate::validate_call_ownership(program, types).is_err()); + let function = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "inspect" + }) + .unwrap(); + assert!(CopyValuePlan::analyze_storage(function, types, Some(program)).is_err()); + assert!(MoveValuePlan::analyze(program, function, types).is_err()); + if let Some(witness) = &function.ordinary_borrowed_sums { + if let Some(row) = witness.rows.first() { + assert!( + function + .ordinary_borrowed_sum_projection( + row.project.block, + row.project.statement_index + ) + .is_err() + ); + } + let mut changed = function.clone(); + assert!(!crate::sequences::prune::unreachable(&mut changed)); + assert_eq!( + changed, *function, + "failed block pruning rolls back the exact malformed input" + ); + assert!(!crate::sequences::prune::unused_locals(&mut changed)); + assert_eq!( + changed, *function, + "failed local compaction rolls back the exact malformed input" + ); + } + let mut changed = program.clone(); + crate::prepare_native_sequences(&mut changed, types); + assert_eq!( + changed, *program, + "validation refusal happens before preparation can hide the mutation" + ); +} + +fn reseal_only_blocks(function: &mut Function) { + let blocks = function.blocks.clone(); + Arc::make_mut(&mut function.ordinary_borrowed_sums.as_mut().unwrap().sealed).blocks = blocks; +} + +#[test] +fn ordinary_borrowed_sums_changed_headers_tag_edge_and_failure_are_refused() { + for source in [SOURCES[0], SOURCES[1]] { + let (program, types) = lowered(source, false); + assert_eq!(assert_plans(&program, &types, 0), 2); + for corruption in 0..9 { + let mut changed = program.clone(); + let function = guarded_function_mut(&mut changed); + let row = function.ordinary_borrowed_sums.as_ref().unwrap().rows[0].clone(); + match corruption { + 0 => function.locals[row.source().index() as usize].view_source = None, + 1 => { + function.locals[row.output().index() as usize].view_source = Some(row.source()) + } + 2 => function.locals[row.alias().index() as usize].mutable = true, + 3 => function.locals[row.backing().index() as usize].ty = row.payload_type(), + 4 => function.locals[row.tag().index() as usize].ty = TypeInterner::INT64, + 5 => { + function.blocks[row.observe.block.index() as usize].statements + [row.observe.statement_index] + .kind = StatementKind::SumTag { + source: row.backing(), + target: row.tag(), + } + } + 6 => { + let TerminatorKind::Branch { condition, .. } = &mut function.blocks + [row.observe.block.index() as usize] + .terminator + .kind + else { + panic!("genuine selecting edge") + }; + condition.kind = E::Bool(true); + } + 7 => { + let TerminatorKind::Branch { + then_block, + else_block, + .. + } = &mut function.blocks[row.observe.block.index() as usize] + .terminator + .kind + else { + panic!("genuine selecting edge") + }; + std::mem::swap(then_block, else_block); + } + _ => { + if let Some(error) = row.error() { + function.locals[error.index() as usize].view_source = Some(row.backing()); + } else { + function.blocks[row.project.block.index() as usize].statements + [row.project.statement_index] + .kind = StatementKind::SumTake { + source: row.source(), + target: row.output(), + success: false, + }; + } + } + } + assert_refused(&changed, &types); + } + } +} + +#[test] +fn ordinary_borrowed_sums_copied_missing_and_disconnected_projection_survive_cache_reseal_refusal() +{ + let (program, types) = lowered(SOURCES[1], false); + assert_eq!(assert_plans(&program, &types, 0), 2); + for corruption in 0..4 { + let mut changed = program.clone(); + let function = guarded_function_mut(&mut changed); + let row = function.ordinary_borrowed_sums.as_ref().unwrap().rows[0].clone(); + assert_eq!(function.local(row.output()).unwrap().ty, row.payload_type()); + assert_eq!( + function.local(row.output()).unwrap().view_source, + Some(row.backing()) + ); + match corruption { + 0 => { + let copied = function.blocks[row.project.block.index() as usize].statements + [row.project.statement_index] + .clone(); + function.blocks[row.project.block.index() as usize] + .statements + .push(copied); + } + 1 => { + function.blocks[row.project.block.index() as usize] + .statements + .remove(row.project.statement_index); + } + 2 => { + let TerminatorKind::Branch { then_block, .. } = &mut function.blocks + [row.observe.block.index() as usize] + .terminator + .kind + else { + panic!("genuine selecting edge") + }; + *then_block = row.continuation; + let cfg = ControlFlowGraph::analyze(function).unwrap(); + assert!(!cfg.reverse_postorder().contains(&row.project.block)); + assert!( + matches!(function.blocks[row.project.block.index() as usize].statements[row.project.statement_index].kind, StatementKind::SumTake { source, target, success: true } if source == row.source() && target == row.output()) + ); + } + _ => { + let site = row.failure.unwrap(); + function.blocks[site.block.index() as usize].statements[site.statement_index] + .kind = StatementKind::SumTake { + source: row.source(), + target: row.error().unwrap(), + success: true, + }; + } + } + assert_refused(&changed, &types); + reseal_only_blocks(guarded_function_mut(&mut changed)); + assert_refused(&changed, &types); + } +} + +#[test] +fn ordinary_borrowed_sums_private_inventory_and_original_association_resist_row_reseal() { + let (program, types) = lowered(SOURCES[0], false); + for corruption in 0..6 { + let mut changed = program.clone(); + let function = guarded_function_mut(&mut changed); + let witness = function.ordinary_borrowed_sums.as_mut().unwrap(); + let independent = witness.sealed.clone(); + match corruption { + 0 => { + witness.rows.pop(); + } + 1 => { + witness.rows.push(witness.rows[0].clone()); + } + 2 => witness.rows[0].original_path.push(usize::MAX), + 3 => { + let sibling = witness.rows[1].clone(); + witness.rows[0].original_path = sibling.original_path; + witness.rows[0].original_alias = sibling.original_alias; + witness.rows[0].original_backing = sibling.original_backing; + witness.rows[0].original = sibling.original; + } + 4 => witness.rows[0].path = OrdinarySumPayloadPath::ResultOk, + _ => { + let (left, right) = witness.rows.split_at_mut(1); + std::mem::swap(&mut left[0].original_path, &mut right[0].original_path); + std::mem::swap(&mut left[0].original, &mut right[0].original); + std::mem::swap(&mut left[0].original_alias, &mut right[0].original_alias); + std::mem::swap( + &mut left[0].original_backing, + &mut right[0].original_backing, + ); + assert_ne!(left[0].original_alias.id, right[0].original_alias.id); + } + } + assert!(Arc::ptr_eq(&witness.sealed, &independent)); + assert!(!same_rows(&witness.rows, &independent.rows)); + assert_refused(&changed, &types); + if corruption == 5 { + // Both original keys remain genuine and unique; their association + // with current sites is independent private authority, not a cache. + reseal_only_blocks(guarded_function_mut(&mut changed)); + assert_refused(&changed, &types); + continue; + } + let witness = guarded_function_mut(&mut changed) + .ordinary_borrowed_sums + .as_mut() + .unwrap(); + let rows = witness.rows.clone(); + Arc::make_mut(&mut witness.sealed).rows = rows; + assert!(same_rows(&witness.rows, &witness.sealed.rows)); + assert_refused(&changed, &types); + } +} + +#[test] +fn ordinary_borrowed_sums_missing_and_foreign_witnesses_cannot_authorize_typed_views() { + let (program, types) = lowered(SOURCES[0], false); + let (foreign, _) = lowered(SOURCES[1], false); + for replacement in [ + None, + guarded_function(&foreign).ordinary_borrowed_sums.clone(), + ] { + let mut changed = program.clone(); + let function = guarded_function_mut(&mut changed); + let row = function.ordinary_borrowed_sums.as_ref().unwrap().rows[0].clone(); + function.ordinary_borrowed_sums = replacement; + assert_eq!( + function.local(row.output()).unwrap().view_source, + Some(row.backing()) + ); + assert!( + matches!(function.blocks[row.project.block.index() as usize].statements[row.project.statement_index].kind, StatementKind::SumTake { source, target, success: true } if source == row.source() && target == row.output()) + ); + assert_refused(&changed, &types); + } +} + +#[test] +fn ordinary_borrowed_sums_alias_query_requires_the_exact_current_initializer_reference() { + let (program, types) = lowered(SOURCES[0], false); + assert_eq!(assert_plans(&program, &types, 0), 2); + let function = guarded_function(&program); + let row = &function.ordinary_borrowed_sums.as_ref().unwrap().rows[0]; + let StatementKind::Let { value, .. } = &function.blocks[row.alias_site.block.index() as usize] + .statements[row.alias_site.statement_index] + .kind + else { + panic!("genuine alias initializer") + }; + assert!( + function + .ordinary_borrowed_sum_alias(row.alias(), value) + .unwrap() + ); + assert!( + !function + .ordinary_borrowed_sum_alias(row.alias(), &value.clone()) + .unwrap() + ); + assert!( + !function + .ordinary_borrowed_sum_alias(row.output(), value) + .unwrap() + ); +} + +#[test] +fn ordinary_borrowed_sums_malformed_maps_and_edits_leave_input_unchanged() { + let (mut program, types) = lowered(SOURCES[0], false); + for function in &mut program.functions { + assert!(crate::sequences::prune::unreachable(function)); + } + assert_eq!(assert_plans(&program, &types, 0), 2); + let before = guarded_function(&program); + let block_map = before + .blocks + .iter() + .map(|block| Some(block.id)) + .collect::>(); + let local_map = before + .locals + .iter() + .map(|local| Some(local.id)) + .collect::>(); + for corruption in 0..6 { + let mut function = before.clone(); + let result = match corruption { + 0 => { + let mut map = block_map.clone(); + map.pop(); + remap_blocks(&mut function, before, &map) + } + 1 => { + let mut map = block_map.clone(); + map[before.entry.index() as usize] = None; + remap_blocks(&mut function, before, &map) + } + 2 => { + let mut map = block_map.clone(); + map.swap(0, 1); + remap_blocks(&mut function, before, &map) + } + 3 => { + let mut map = local_map.clone(); + map.pop(); + remap_locals(&mut function, before, &map) + } + 4 => { + let mut map = local_map.clone(); + map.swap(0, 1); + remap_locals(&mut function, before, &map) + } + _ => { + let witness = before.ordinary_borrowed_sums.as_ref().unwrap(); + let mut map = local_map.clone(); + map[witness.rows[0].backing().index() as usize] = None; + let mut next = 0; + for local in map.iter_mut().flatten() { + *local = LocalId::new(next); + next += 1; + } + remap_locals(&mut function, before, &map) + } + }; + assert!(result.is_err(), "malformed canonical map {corruption}"); + assert_eq!(function, *before); + validate(&function, &types).unwrap(); + } + let mut changed = before.clone(); + let row = &before.ordinary_borrowed_sums.as_ref().unwrap().rows[0]; + changed.locals[row.output().index() as usize].view_source = None; + let malformed = changed.clone(); + assert!(remap_blocks(&mut changed, before, &block_map).is_err()); + assert_eq!(changed, malformed); + assert!(remap_locals(&mut changed, before, &local_map).is_err()); + assert_eq!(changed, malformed); +} diff --git a/crates/jett_mir/src/reflected_callable.rs b/crates/jett_mir/src/reflected_callable.rs new file mode 100644 index 00000000..d9cc326e --- /dev/null +++ b/crates/jett_mir/src/reflected_callable.rs @@ -0,0 +1,269 @@ +//! Independent replay and sealed graphs for checked lazy reflected adapters. +//! These records grant no Resource family, entry scope or runtime capability. + +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; + +use super::*; + +#[derive(Debug)] +struct Original { + hir: hir::Program, + modes: HashMap>, + execution: resource_ownership::ResourceExecutionClosure, + required: HashSet, +} + +#[derive(Debug, Clone)] +struct Graph { + id: FunctionId, + identity: FunctionIdentity, + debug_kind: hir::FunctionDebugKind, + params: Vec, + capture_count: usize, + return_type: TypeId, + locals: Vec, + entry: BlockId, + blocks: Vec, + span: Span, +} + +impl PartialEq for Graph { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + && self.identity == other.identity + && self.debug_kind == other.debug_kind + && self.params == other.params + && self.capture_count == other.capture_count + && self.return_type == other.return_type + && self.locals == other.locals + && self.entry == other.entry + && self.span == other.span + && breakpoint_regions::blocks_equal(&self.blocks, &other.blocks) + } +} + +impl Graph { + fn capture(function: &Function) -> Self { + Self { + id: function.id, + identity: function.identity.clone(), + debug_kind: function.debug_kind.clone(), + params: function.params.clone(), + capture_count: function.capture_count, + return_type: function.return_type, + locals: function.locals.clone(), + entry: function.entry, + blocks: function.blocks.clone(), + span: function.span, + } + } +} + +#[derive(Debug, Clone)] +pub(super) struct Witness { + original: Arc, + source: Arc, + current: Arc, +} + +impl PartialEq for Witness { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.original, &other.original) + && Arc::ptr_eq(&self.source, &other.source) + && Arc::ptr_eq(&self.current, &other.current) + } +} + +pub(super) fn capture( + program: &mut Program, + original: &hir::Program, + modes: &HashMap>, + execution: &resource_ownership::ResourceExecutionClosure, +) -> Result<(), String> { + let mut required = HashSet::new(); + let mut has_adapters = false; + for function in &original.functions { + for plan in hir::reflected_callable_plans(function) { + if !plan.validate_original() { + return Err("reflected callable lacks original HIR constructor evidence".into()); + } + has_adapters = true; + required.insert(function.id); + required.insert(plan.function()); + required.insert( + plan.original_owner() + .ok_or("reflected callable original owner is missing")?, + ); + for predicate in plan.original_predicates() { + required.insert(predicate.id); + } + } + } + if !has_adapters { + return Ok(()); + } + // Original descriptor targets and capture producers remain authenticated, + // including helpers whose signatures alone cannot identify their bodies. + required.extend(original.functions.iter().map(|function| function.id)); + let original = Arc::new(Original { + hir: original.clone(), + modes: modes.clone(), + execution: execution.clone(), + required, + }); + for function in &mut program.functions { + if original.required.contains(&function.id) { + let graph = Arc::new(Graph::capture(function)); + function.reflected_callable = Some(Witness { + original: original.clone(), + source: graph.clone(), + current: graph, + }); + } + } + Ok(()) +} + +/// Called only beside canonical preparation after the complete input gate. +/// A changed public graph cannot become new authority through this function. +pub(super) fn prepared(function: &mut Function, before: &Function) -> Result<(), String> { + let Some(previous) = &before.reflected_callable else { + if function.reflected_callable.is_some() { + return Err("reflected callable witness appeared during preparation".into()); + } + return Ok(()); + }; + if previous.current.as_ref() != &Graph::capture(before) + || function.reflected_callable.as_ref() != Some(previous) + { + return Err("reflected callable preparation has no exact original current graph".into()); + } + let current = Arc::new(Graph::capture(function)); + let mut witness = previous.clone(); + witness.current = current; + function.reflected_callable = Some(witness); + Ok(()) +} + +fn private_expression(value: &Expression) -> bool { + use hir::ExpressionKind as E; + matches!( + &value.kind, + E::Intrinsic { + field_validation: Some(hir::ReflectedFieldValidation::ReadCallable(_)), + .. + } + ) || matches!(&value.kind, E::IndirectCall { ownership: hir::CallOwnership::Generated(generated), .. } + if matches!(generated.operation, hir::GeneratedOperation::ReflectedCallableAdapter { .. })) +} + +fn has_private_operations(function: &Function) -> bool { + function.blocks.iter().any(|block| { + let mut found = block + .statements + .iter() + .any(|statement| matches!(statement.kind, StatementKind::ReflectedValueReady { .. })); + resource_ownership::walk::mir_block(block, &mut |expression| { + found |= private_expression(expression) + }); + found + }) +} + +pub fn validate_reflected_callable_plans( + program: &Program, + types: &TypeInterner, +) -> Result<(), Vec> { + let mut errors = Vec::new(); + let mut originals = Vec::>::new(); + for function in &program.functions { + let Some(witness) = &function.reflected_callable else { + let mut copied_adapter = false; + for block in &function.blocks { + resource_ownership::walk::mir_block(block, &mut |value| { + if let hir::ExpressionKind::FunctionAdapter { + function: target, .. + } = &value.kind + { + copied_adapter |= program + .functions + .get(target.index() as usize) + .is_some_and(has_private_operations); + } + }); + } + if has_private_operations(function) || copied_adapter { + errors.push(ValidationError { + span: function.span, + message: "reflected callable MIR operation has no original private witness" + .into(), + }); + } + continue; + }; + if !originals + .iter() + .any(|original| Arc::ptr_eq(original, &witness.original)) + { + originals.push(witness.original.clone()); + } + if witness.current.as_ref() != &Graph::capture(function) || witness.source.id != function.id + { + errors.push(ValidationError { + span: function.span, + message: "reflected callable current body, tuple, readiness or context changed" + .into(), + }); + continue; + } + let Some(original) = witness + .original + .hir + .functions + .get(function.id.index() as usize) + else { + errors.push(ValidationError { + span: function.span, + message: "reflected callable MIR lost its original function".into(), + }); + continue; + }; + match super::lower_function_body(original, types, &witness.original.modes, &witness.original.hir.resource_manifest, &witness.original.hir.resource_source, &witness.original.execution) { + Ok(reconstructed) if Graph::capture(&reconstructed) == *witness.source => {} + _ => errors.push(ValidationError { span: function.span, message: "reflected callable original adapter/predicate/tuple CFG cannot be reconstructed".into() }), + } + } + for original in originals { + if let Err(hir_errors) = hir::validate(&original.hir) + .and_then(|()| hir::validate_backend_types(&original.hir, types)) + { + errors.extend(hir_errors.into_iter().map(|error| ValidationError { + span: error.span, + message: error.message, + })); + } + for required in &original.required { + if !program + .functions + .get(required.index() as usize) + .is_some_and(|function| { + function + .reflected_callable + .as_ref() + .is_some_and(|witness| Arc::ptr_eq(&witness.original, &original)) + }) + { + errors.push(ValidationError { span: original.hir.functions[required.index() as usize].span, message: "reflected callable original owner, descriptor or predicate witness was removed".into() }); + } + } + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_mir/src/reflected_callable/tests.rs b/crates/jett_mir/src/reflected_callable/tests.rs new file mode 100644 index 00000000..2e9f9b24 --- /dev/null +++ b/crates/jett_mir/src/reflected_callable/tests.rs @@ -0,0 +1,450 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; + +fn fixture( + source_return: &str, + requested_return: &str, +) -> (Program, Arc) { + let source = format!( + "namespace app\ntype Positive = int64 where value > 0\ntype Higher = Positive where value > 10\nfunction five() returns int64:\n return 5\nstruct Holder:\n callback: function() returns {source_return}\nfunction inspect(view holder: Holder, view field: TypeField) returns function() returns {requested_return}:\n return type.field_value[Holder, function() returns {requested_return}](view holder, view field)\n" + ); + source_fixture(&source) +} + +fn source_fixture(source: &str) -> (Program, Arc) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let checked = Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + parsed, + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions { release: false }, + ) + .unwrap(), + ); + let hir = hir::lower_checked_ordinary_program(&checked).expect("ordinary original checked HIR"); + let mir = lower(&hir, &checked.checked().interner).expect("canonical reflected MIR"); + (mir, checked) +} + +fn adapter(program: &Program) -> usize { + program + .functions + .iter() + .position(|function| { + function.blocks.iter().any(|block| { + block.statements.iter().any(|statement| { + matches!(statement.kind, StatementKind::ReflectedValueReady { .. }) + }) + }) + }) + .expect("private adapter") +} + +#[test] +fn read_is_lazy_and_invocation_orders_actual_result_preflight_predicate_publication() { + let (program, checked) = fixture("int64", "Positive"); + validate_reflected_callable_plans(&program, &checked.checked().interner).unwrap(); + let reader = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + assert!( + !reader + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!( + statement.kind, + StatementKind::CheckRefinement { .. } | StatementKind::ReflectedValueReady { .. } + )) + ); + let mut reads = 0usize; + for block in &reader.blocks { + resource_ownership::walk::mir_block(block, &mut |value| { + if matches!( + value.kind, + hir::ExpressionKind::Intrinsic { + field_validation: Some(hir::ReflectedFieldValidation::ReadCallable(_)), + .. + } + ) { + reads += 1; + } + }); + } + assert_eq!(reads, 1); + let function = &program.functions[adapter(&program)]; + let statements = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .collect::>(); + let call = statements + .iter() + .position(|statement| { + matches!( + statement.kind, + StatementKind::Let { + value: Expression { + kind: hir::ExpressionKind::IndirectCall { .. }, + .. + }, + .. + } + ) + }) + .unwrap(); + assert_eq!( + statements + .iter() + .filter(|statement| matches!( + statement.kind, + StatementKind::Let { + value: Expression { + kind: hir::ExpressionKind::IndirectCall { .. }, + .. + }, + .. + } + )) + .count(), + 1, + "actual source invocation once" + ); + let ready = statements + .iter() + .position(|statement| { + matches!( + statement.kind, + StatementKind::ReflectedValueReady { + inspect_pending: true, + .. + } + ) + }) + .unwrap(); + let predicate = statements + .iter() + .position(|statement| matches!(statement.kind, StatementKind::CheckRefinement { .. })) + .unwrap(); + assert!(call < ready && ready < predicate); + assert!(function.blocks.iter().any(|block| matches!( + &block.terminator.kind, + TerminatorKind::Return(Some(Expression { + kind: hir::ExpressionKind::RefinementValidated(_), + .. + })) + ))); +} + +#[test] +fn ancestor_transport_has_no_new_pending_inspection_or_predicate() { + let (program, checked) = fixture("Higher", "Positive"); + let function = &program.functions[adapter(&program)]; + assert!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!( + statement.kind, + StatementKind::ReflectedValueReady { + inspect_pending: false, + .. + } + )) + ); + assert!( + !function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!( + statement.kind, + StatementKind::ReflectedValueReady { + inspect_pending: true, + .. + } | StatementKind::CheckRefinement { .. } + )) + ); + let returned = function + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("ancestor adapter return"); + let hir::ExpressionKind::Coarsen(inner) = &returned.kind else { + panic!("ancestor proof must be transported by typed coarsening"); + }; + assert!( + matches!(checked.checked().interner.resolve(inner.ty), Type::Refinement { base, .. } if *base == returned.ty) + ); + validate_reflected_callable_plans(&program, &checked.checked().interner).unwrap(); +} + +#[test] +fn pending_scalar_and_unit_ancestor_returns_transport_the_original_proof_and_cannot_relabel_it() { + for (base, value) in [("int64", "0"), ("nothing", "nothing")] { + let source = format!( + "namespace app\ntype Ready = {base} where true\ntype Higher = Ready where true\nstruct Holder:\n callback: function() returns Higher\nfunction pending_source() returns Higher:\n Higher proved = {value} handle error:\n return pending_source()\n return run run proved\nfunction inspect(view holder: Holder, view field: TypeField) returns function() returns Ready:\n return type.field_value[Holder, function() returns Ready](view holder, view field)\n" + ); + let (mut program, checked) = source_fixture(&source); + let types = &checked.checked().interner; + let index = adapter(&program); + for prepared in [false, true] { + if prepared { + prepare_native_sequences(&mut program, types); + prepare_native_uninhabited_sums(&mut program, types); + prepare_native_generated_functions(&mut program, types); + } + validate_reflected_callable_plans(&program, types).unwrap(); + let function = &program.functions[index]; + assert!( + !function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!( + statement.kind, + StatementKind::CheckRefinement { .. } + | StatementKind::ReflectedValueReady { + inspect_pending: true, + .. + } + )) + ); + let mut joins = 0; + let mut new_proofs = 0; + for block in &function.blocks { + resource_ownership::walk::mir_block(block, &mut |expression| { + joins += usize::from(matches!(expression.kind, hir::ExpressionKind::Join(_))); + new_proofs += usize::from(matches!( + expression.kind, + hir::ExpressionKind::RefinementValidated(_) + )); + }); + } + assert_eq!(joins, 0, "{base} pending layers stay intact"); + assert_eq!( + new_proofs, 0, + "{base} existing ancestor proof is not revalidated" + ); + let returned = function + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("transported ancestor return"); + let hir::ExpressionKind::Coarsen(inner) = &returned.kind else { + panic!("{base} exact ancestor transport needs Coarsen"); + }; + assert!(matches!(inner.kind, hir::ExpressionKind::Local(_))); + assert!( + matches!(types.resolve(inner.ty), Type::Refinement { base, .. } if *base == returned.ty) + ); + let mut forged = program.clone(); + let returned = forged.functions[index] + .blocks + .iter_mut() + .find_map(|block| match &mut block.terminator.kind { + TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .unwrap(); + let hir::ExpressionKind::Coarsen(inner) = &returned.kind else { + unreachable!() + }; + returned.kind = hir::ExpressionKind::RefinementValidated(inner.clone()); + assert!( + validate_reflected_callable_plans(&forged, types).is_err(), + "{base} public return relabel cannot refresh the private original graph" + ); + } + } +} + +#[test] +fn recursive_produced_return_completes_all_occupied_preflight_before_any_predicate() { + let (program, checked) = fixture("list[optional[int64]]", "list[optional[Positive]]"); + let function = &program.functions[adapter(&program)]; + let mut visited = HashSet::new(); + let mut pending = vec![(function.entry, false)]; + while let Some((block, predicate_seen)) = pending.pop() { + if !visited.insert((block, predicate_seen)) { + continue; + } + let block = &function.blocks[block.index() as usize]; + let mut seen = predicate_seen; + for statement in &block.statements { + if matches!(statement.kind, StatementKind::CheckRefinement { .. }) { + seen = true; + } + assert!( + !(seen + && matches!( + statement.kind, + StatementKind::ReflectedValueReady { + inspect_pending: true, + .. + } | StatementKind::ReflectedContainerReady { .. } + )), + "preflight must not run after an earlier predicate" + ); + } + match &block.terminator.kind { + TerminatorKind::Goto(next) => pending.push((*next, seen)), + TerminatorKind::Branch { + then_block, + else_block, + .. + } => { + pending.push((*then_block, seen)); + pending.push((*else_block, seen)); + } + _ => {} + } + } + assert!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!( + statement.kind, + StatementKind::ReflectedValueReady { + inspect_pending: true, + .. + } + )) + ); + validate_reflected_callable_plans(&program, &checked.checked().interner).unwrap(); +} + +#[test] +fn graph_and_tuple_and_body_and_witness_forgery_cannot_reseal() { + let (original, checked) = fixture("int64", "Positive"); + let types = &checked.checked().interner; + for mutation in 0..7 { + let mut program = original.clone(); + let index = adapter(&program); + match mutation { + 0 => { + let statement = program.functions[index] + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find(|statement| { + matches!( + statement.kind, + StatementKind::ReflectedValueReady { + inspect_pending: true, + .. + } + ) + }) + .unwrap(); + let StatementKind::ReflectedValueReady { + inspect_pending, .. + } = &mut statement.kind + else { + unreachable!() + }; + *inspect_pending = false; + } + 1 => { + program.functions[index].reflected_callable = None; + } + 2 => { + for function in &mut program.functions { + function.reflected_callable = None; + } + } + 3 => { + let function = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "five") + .unwrap(); + let TerminatorKind::Return(Some(value)) = &mut function.blocks + [function.entry.index() as usize] + .terminator + .kind + else { + panic!("source return") + }; + value.kind = hir::ExpressionKind::Int(0); + } + 4 => { + program.functions[index].identity.declaration.namespace = "other".into(); + } + 5 => { + let statement = program.functions[index] + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find(|statement| { + matches!(statement.kind, StatementKind::CheckRefinement { .. }) + }) + .unwrap(); + let StatementKind::CheckRefinement { call, .. } = &mut statement.kind else { + unreachable!() + }; + let hir::ExpressionKind::Call { args, .. } = &mut call.kind else { + panic!("predicate tuple") + }; + args[0].kind = hir::ExpressionKind::Int(7); + } + _ => { + let function = program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.kind + == hir::DeclarationKind::RefinementPredicate + && function.identity.declaration.namespace == "app" + }) + .unwrap(); + let returned = function + .blocks + .iter_mut() + .find_map(|block| match &mut block.terminator.kind { + TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("original predicate result"); + returned.kind = hir::ExpressionKind::Bool(false); + } + } + assert!( + validate_reflected_callable_plans(&program, types).is_err(), + "forgery {mutation}" + ); + prepare_native_generated_functions(&mut program, types); + prepare_native_sequences(&mut program, types); + prepare_native_uninhabited_sums(&mut program, types); + assert!( + validate_reflected_callable_plans(&program, types).is_err(), + "preparation cannot reseal forgery {mutation}" + ); + } +} + +#[test] +fn canonical_preparation_preserves_private_original_and_current_graphs() { + let (mut program, checked) = fixture("int64", "Positive"); + let types = &checked.checked().interner; + prepare_native_generated_functions(&mut program, types); + validate_reflected_callable_plans(&program, types).unwrap(); + prepare_native_sequences(&mut program, types); + validate_reflected_callable_plans(&program, types).unwrap(); + prepare_native_uninhabited_sums(&mut program, types); + validate_reflected_callable_plans(&program, types).unwrap(); +} diff --git a/crates/jett_mir/src/resource_manifest_tests.rs b/crates/jett_mir/src/resource_manifest_tests.rs new file mode 100644 index 00000000..db102974 --- /dev/null +++ b/crates/jett_mir/src/resource_manifest_tests.rs @@ -0,0 +1,241 @@ +use crate::{StatementKind as S, TerminatorKind as T}; +use jett_common::{FileId, SourceOrigin}; +use jett_hir::{CallOwnership, Expression, ExpressionKind as E}; +use jett_parser::{ast::Item, parse}; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; +use jett_types::{ResourceKernelRecipe, TypeInterner}; +use std::collections::HashMap; +use std::sync::Arc; +const SUPPORT: &str = r#"namespace resource_probe +export resource TestHandle +export function create(view net: Network, label: int64) returns result[TestHandle, string]: + return kernel_create(label: label, net: view net) +export function close(token: TestHandle) returns nothing: + kernel_close(token) + return nothing +export function descriptor() returns function(TestHandle) returns nothing: + return kernel_close +"#; +const SOURCE: &str = r#"namespace app +function factory(view net: Network, label: int64) returns result[resource_probe.TestHandle, string]: + use resource_probe + return resource_probe.create(view net, label) +"#; +fn checked(release: bool) -> Arc { + let stdlib = FileId::new(10_000); + let project = FileId::new(0); + let mut parsed = parse(SUPPORT, stdlib); + let source = parse(SOURCE, project); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(source.errors.is_empty(), "{:?}", source.errors); + let declaration = parsed + .module + .items + .iter() + .find_map(|item| { + if let Item::Resource(resource) = item { + Some(resource.name.span) + } else { + None + } + }) + .unwrap(); + parsed.module.items.extend(source.module.items); + parsed.errors.extend(source.errors); + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: declaration, + member: member.into(), + recipe, + }) + .collect::>(); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (stdlib, SourceOrigin::Stdlib), + (project, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("{error:?}; {:?}", error.diagnostics())), + ) +} +fn invoke(program: &crate::Program) -> &Expression { + program + .functions + .iter() + .flat_map(|function| &function.blocks) + .find_map(|block| { + if let T::Return(Some(value)) = &block.terminator.kind + && matches!(value.kind, E::ResourceInvoke { .. }) + { + return Some(value); + } + block + .statements + .iter() + .find_map(|statement| match &statement.kind { + S::Let { value, .. } | S::Evaluate(value) + if matches!(value.kind, E::ResourceInvoke { .. }) => + { + Some(value) + } + _ => None, + }) + }) + .expect("Source ResourceInvoke survives typed lowering") +} +fn invoke_mut(program: &mut crate::Program) -> &mut Expression { + program + .functions + .iter_mut() + .flat_map(|function| &mut function.blocks) + .find_map(|block| { + if let T::Return(Some(value)) = &mut block.terminator.kind + && matches!(value.kind, E::ResourceInvoke { .. }) + { + return Some(value); + } + block + .statements + .iter_mut() + .find_map(|statement| match &mut statement.kind { + S::Let { value, .. } | S::Evaluate(value) + if matches!(value.kind, E::ResourceInvoke { .. }) => + { + Some(value) + } + _ => None, + }) + }) + .unwrap() +} +#[test] +fn resource_manifest_survives_mir_without_ordinary_owner_authority() { + for release in [false, true] { + let original = checked(release); + let types = &original.checked().interner; + let hir = jett_hir::lower_checked_resource_program(&original).unwrap(); + let mut mir = crate::lower(&hir, types).unwrap(); + assert_eq!(mir.resource_manifest, hir.resource_manifest); + let E::ResourceInvoke { + hook, + evaluation_order, + ownership: CallOwnership::Source(source), + .. + } = &invoke(&mir).kind + else { + panic!("original Source hook"); + }; + assert_eq!(hook.recipe(), ResourceKernelRecipe::NetworkFactory); + assert_eq!(evaluation_order, &[1, 0]); + assert!(mir.resource_manifest.contains_hook(hook)); + assert!( + source + .arguments + .iter() + .all(|argument| matches!(argument.staging, jett_hir::ArgumentStaging::Original)) + ); + crate::validate_call_ownership(&mir, types).unwrap(); + let operations = mir.functions.iter().filter(|function| function.blocks.iter().any(|block| { + matches!(&block.terminator.kind, T::Return(Some(Expression { kind: E::ResourceInvoke { hook, .. }, .. })) if hook.recipe() == ResourceKernelRecipe::NetworkFactory) + })).collect::>(); + let [function] = operations.as_slice() else { + panic!("one exact original factory body"); + }; + assert!( + crate::validate_caller_acquisitions(&mir, function, types) + .unwrap_err() + .contains("pending ResourceOwnershipPlan") + ); + assert!( + crate::move_values::MoveValuePlan::analyze(&mir, function, types) + .unwrap_err() + .contains("pending ResourceOwnershipPlan") + ); + assert!( + crate::copy_values::CopyValuePlan::analyze(function, types) + .unwrap_err() + .contains("pending ResourceOwnershipPlan") + ); + crate::prepare_native_sequences(&mut mir, types); + crate::validate_call_ownership(&mir, types).unwrap(); + assert_eq!(mir.resource_manifest, hir.resource_manifest); + } +} +#[test] +fn resource_manifest_mir_refuses_foreign_hook_and_original_packet_corruption() { + for release in [false, true] { + let original = checked(release); + let types = &original.checked().interner; + let hir = jett_hir::lower_checked_resource_program(&original).unwrap(); + let mir = crate::lower(&hir, types).unwrap(); + let foreign = jett_hir::lower_checked_resource_program(&checked(release)).unwrap(); + for mutation in 0..6 { + let mut changed = mir.clone(); + if mutation == 0 { + changed.resource_manifest = jett_hir::ResourceManifest::empty(); + } else if mutation == 5 { + let descriptor = changed + .functions + .iter_mut() + .flat_map(|function| &mut function.blocks) + .find_map(|block| { + if let T::Return(Some(value)) = &mut block.terminator.kind + && matches!(value.kind, E::ResourceHookValue { .. }) + { + Some(value) + } else { + None + } + }) + .expect("exact original descriptor result"); + descriptor.ty = TypeInterner::STRING; + } else { + let expression = invoke_mut(&mut changed); + let E::ResourceInvoke { + hook, + args, + evaluation_order, + ownership: CallOwnership::Source(source), + } = &mut expression.kind + else { + unreachable!() + }; + match mutation { + 1 => { + *hook = foreign + .resource_manifest + .hooks() + .find(|hook| hook.recipe() == ResourceKernelRecipe::NetworkFactory) + .unwrap() + } + 2 => *evaluation_order = vec![0, 1], + 3 => { + source.arguments[0].staging = jett_hir::ArgumentStaging::Copied { + value: jett_hir::LocalId::new(0), + } + } + _ => args[1].ty = TypeInterner::STRING, + } + } + let errors = crate::validate_call_ownership(&changed, types).unwrap_err(); + if mutation == 5 { + assert!( + errors + .iter() + .any(|error| error.message.contains("Resource descriptor differs")), + "{errors:?}" + ); + } + } + } +} diff --git a/crates/jett_mir/src/resource_ownership/borrowed_sums.rs b/crates/jett_mir/src/resource_ownership/borrowed_sums.rs new file mode 100644 index 00000000..e63d6ea0 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/borrowed_sums.rs @@ -0,0 +1,448 @@ +//! Constructor-owned, nonowning Handle projections. Public sum syntax is inert. +use super::*; +use hir::ExpressionKind as E; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum ResourceSumPayloadPath { + OptionalSome, + ResultOk, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ResourceBorrowedSumProjection { + original_path: Vec, + original: Expression, + original_alias: Local, + original_backing: Local, + alias: Local, + backing: Local, + source: Local, + output: Local, + tag: Local, + error: Option, + initialize: ResourceSite, + observe: ResourceSite, + project: ResourceSite, + failure: Option, + failed: BlockId, + continuation: BlockId, + path: ResourceSumPayloadPath, +} +impl ResourceBorrowedSumProjection { + pub fn original_initializer(&self) -> &Expression { + &self.original + } + pub fn original_handle(&self) -> &Expression { + let E::View(handle) = &self.original.kind else { + unreachable!() + }; + handle + } + pub fn alias(&self) -> LocalId { + self.alias.id + } + pub fn backing(&self) -> LocalId { + self.backing.id + } + pub fn source(&self) -> LocalId { + self.source.id + } + pub fn output(&self) -> LocalId { + self.output.id + } + pub fn tag(&self) -> LocalId { + self.tag.id + } + pub fn path(&self) -> ResourceSumPayloadPath { + self.path + } + pub fn success_site(&self) -> ResourceSite { + self.project + } + pub fn failure_site(&self) -> Option { + self.failure + } + pub(super) fn original_local(&self) -> LocalId { + self.original_alias.id + } + pub(super) fn initialize_site(&self) -> ResourceSite { + self.initialize + } + pub(super) fn retirement_locals(&self) -> [LocalId; 3] { + [self.alias.id, self.output.id, self.source.id] + } + + fn current(&self, function: &Function) -> Result<(), String> { + let statement = |site: ResourceSite| -> Result<&Statement, String> { + let ResourcePosition::Statement(index) = site.position else { + return Err("borrowed Resource Handle site is not a statement".into()); + }; + if site.function != function.id { + return Err("borrowed Resource Handle has a foreign function".into()); + } + function + .blocks + .get(site.block.index() as usize) + .filter(|block| block.id == site.block) + .and_then(|block| block.statements.get(index)) + .ok_or_else(|| "borrowed Resource Handle lost its exact current site".into()) + }; + for header in [ + &self.alias, + &self.backing, + &self.source, + &self.output, + &self.tag, + ] + .into_iter() + .chain(self.error.iter()) + { + if function.local(header.id) != Some(header) { + return Err("borrowed Resource Handle changed its sealed local header".into()); + } + } + if self.source.mutable + || self.output.mutable + || self.alias.mutable + || self.source.view_source != Some(self.backing.id) + || self.output.view_source != Some(self.backing.id) + || self.alias.view_source != Some(self.backing.id) + || self.source.ty != self.backing.ty + || self.output.ty != self.alias.ty + || self.tag.ty != TypeInterner::BOOL + || self.tag.view_source.is_some() + { + return Err("borrowed Resource Handle lost its immutable nonowning headers".into()); + } + let initialized = statement(self.initialize)?; + if !matches!(&initialized.kind, StatementKind::Let { local, value } + if *local == self.source.id && value.ty == self.source.ty + && matches!(&value.kind, E::View(inner) if inner.ty == self.source.ty && matches!(inner.kind, E::Local(backing) if backing == self.backing.id))) + || !matches!(statement(self.observe)?.kind, StatementKind::SumTag { source, target } if source == self.source.id && target == self.tag.id) + || !matches!(statement(self.project)?.kind, StatementKind::SumTake { source, target, success: true } if source == self.source.id && target == self.output.id) + { + return Err( + "borrowed Resource Handle changed its initializer, tag or projection".into(), + ); + } + let branch = &function.blocks[self.observe.block.index() as usize]; + if self.observe.block != self.initialize.block + || !matches!(&branch.terminator.kind, TerminatorKind::Branch { condition, then_block, else_block } + if condition.ty == TypeInterner::BOOL && matches!(condition.kind, E::Local(tag) if tag == self.tag.id) + && *then_block == self.project.block && *else_block == self.failed && then_block != else_block) + || !matches!(function.blocks[self.project.block.index() as usize].terminator.kind, TerminatorKind::Goto(target) if target == self.continuation) + { + return Err("borrowed Resource Handle lost its exact selecting CFG guard".into()); + } + match (&self.error, self.failure) { + (Some(error), Some(site)) + if site.block == self.failed + && matches!(statement(site)?.kind, StatementKind::SumTake { source, target, success: false } if source == self.source.id && target == error.id) => + {} + (None, None) => {} + _ => { + return Err( + "borrowed Resource Handle lost its exact failure companion read".into(), + ); + } + } + let alias_count = function.blocks.iter().flat_map(|block| &block.statements).filter(|statement| { + matches!(&statement.kind, StatementKind::Let { local, value } if *local == self.alias.id + && value.ty == self.alias.ty && matches!(&value.kind, E::View(inner) if inner.ty == self.output.ty && matches!(inner.kind, E::Local(output) if output == self.output.id))) + }).count(); + let projection_count = function.blocks.iter().flat_map(|block| &block.statements).filter(|statement| { + matches!(statement.kind, StatementKind::SumTake { target, success: true, .. } if target == self.output.id) + }).count(); + if alias_count != 1 || projection_count != 1 { + return Err( + "borrowed Resource Handle has copied or missing current projections".into(), + ); + } + let cfg = ControlFlowGraph::analyze(function).map_err(|error| format!("{error:?}"))?; + if !cfg.reverse_postorder().contains(&self.observe.block) + || !cfg.reverse_postorder().contains(&self.project.block) + || cfg.predecessors(self.project.block) != [self.observe.block] + || cfg.predecessors(self.failed) != [self.observe.block] + { + return Err( + "borrowed Resource Handle is disconnected from its exact selecting edge".into(), + ); + } + Ok(()) + } + fn remap_blocks(&mut self, map: &[Option]) -> Result<(), String> { + let remap = |id: &mut BlockId| -> Result<(), String> { + *id = + map.get(id.index() as usize).copied().flatten().ok_or( + "borrowed Resource Handle canonical map removed an authenticated edge", + )?; + Ok(()) + }; + for site in [&mut self.initialize, &mut self.observe, &mut self.project] + .into_iter() + .chain(self.failure.iter_mut()) + { + remap(&mut site.block)?; + } + remap(&mut self.failed)?; + remap(&mut self.continuation) + } + fn remap_locals(&mut self, remap: &mut impl FnMut(&mut LocalId)) { + for header in [ + &mut self.alias, + &mut self.backing, + &mut self.source, + &mut self.output, + &mut self.tag, + ] + .into_iter() + .chain(self.error.iter_mut()) + { + remap(&mut header.id); + if let Some(source) = &mut header.view_source { + remap(source); + } + } + } +} + +pub(crate) struct BorrowedSumSeed { + pub(crate) original_path: Vec, + pub(crate) original: Expression, + pub(crate) alias: Local, + pub(crate) backing: Local, + pub(crate) path: ResourceSumPayloadPath, +} +impl Capture { + pub(crate) fn borrowed_sum_seed( + &self, + local: LocalId, + expression: &Expression, + types: &TypeInterner, + ) -> Result, String> { + let Some(witness) = &self.witness else { + return Ok(None); + }; + if !resource_type_pending(types, expression.ty) { + return Ok(None); + } + let mut found = None; + for declaration in lexical_borrows::declarations(witness) { + if declaration.local != local { + continue; + } + let value = &declaration.value; + if !crate::breakpoint_regions::expressions_equal(value, expression) { + return Err( + "borrowed Resource declaration changed its exact archived initializer".into(), + ); + } + let alias = witness + .original + .locals + .get(local.index() as usize) + .filter(|header| header.id == local) + .ok_or("borrowed Resource Handle original alias is absent")?; + let Some(source) = alias.view_source else { + continue; + }; + let backing = witness + .original + .locals + .get(source.index() as usize) + .filter(|header| header.id == source) + .ok_or("borrowed Resource Handle original backing is absent")?; + let Some(handle) = + hir::borrowed_sum_view_initializer(value, source, backing.ty, alias.ty, types)? + else { + continue; + }; + let E::Handle { kind, .. } = &handle.kind else { + unreachable!() + }; + let path = match kind { + hir::HandleKind::Optional => ResourceSumPayloadPath::OptionalSome, + hir::HandleKind::Result => ResourceSumPayloadPath::ResultOk, + _ => return Err("borrowed Resource Handle has an unsupported sum kind".into()), + }; + if found.is_some() || alias.mutable { + return Err( + "borrowed Resource Handle has no unique immutable original binding".into(), + ); + } + found = Some(BorrowedSumSeed { + original_path: declaration.path, + original: value.clone(), + alias: alias.clone(), + backing: backing.clone(), + path, + }); + } + Ok(found) + } + #[allow(clippy::too_many_arguments)] + pub(crate) fn record_borrowed_sum( + &mut self, + seed: BorrowedSumSeed, + source: Local, + output: Local, + tag: Local, + error: Option, + initialize: ResourceSite, + observe: ResourceSite, + project: ResourceSite, + failure: Option, + failed: BlockId, + continuation: BlockId, + ) { + if let Some(witness) = &mut self.witness { + witness.borrowed_sums.push(ResourceBorrowedSumProjection { + original_path: seed.original_path, + original: seed.original, + original_alias: seed.alias.clone(), + original_backing: seed.backing.clone(), + alias: seed.alias, + backing: seed.backing, + source, + output, + tag, + error, + initialize, + observe, + project, + failure, + failed, + continuation, + path: seed.path, + }); + } + } +} +pub(super) fn current( + witness: &ResourceLoweringWitness, + function: &Function, +) -> Result<(), String> { + if witness.borrowed_sums != witness.borrowed_seal { + return Err( + "borrowed Resource Handle changed its independent original/current association seal" + .into(), + ); + } + let expected = lexical_borrows::declarations(witness); + if expected.len() != witness.borrowed_sums.len() { + return Err( + "borrowed Resource Handle lost its complete archived projection inventory".into(), + ); + } + for (index, row) in witness.borrowed_sums.iter().enumerate() { + row.current(function)?; + if witness.borrowed_sums[..index] + .iter() + .any(|old| old.original_alias.id == row.original_alias.id) + { + return Err("borrowed Resource Handle original binding is duplicated".into()); + } + let count = expected + .iter() + .filter(|declaration| { + declaration.local == row.original_alias.id + && declaration.path == row.original_path + && crate::breakpoint_regions::expressions_equal( + &declaration.value, + &row.original, + ) + }) + .count(); + if count != 1 + || row.original_alias.mutable + || row.original_alias.view_source != Some(row.original_backing.id) + || row.original_alias.ty != row.original.ty + || row.original_alias.ty != row.alias.ty + || row.original_backing.ty != row.backing.ty + || witness + .original + .locals + .get(row.original_alias.id.index() as usize) + != Some(&row.original_alias) + || witness + .original + .locals + .get(row.original_backing.id.index() as usize) + != Some(&row.original_backing) + { + return Err("borrowed Resource Handle lost its exact archived original binding".into()); + } + } + Ok(()) +} +pub(super) fn seal(witness: &mut ResourceLoweringWitness) { + witness.borrowed_seal = witness.borrowed_sums.clone(); +} +pub(super) fn remap_blocks( + rows: &mut [ResourceBorrowedSumProjection], + map: &[Option], +) -> Result<(), String> { + for row in rows { + row.remap_blocks(map)?; + } + Ok(()) +} +pub(super) fn remap_locals( + rows: &mut [ResourceBorrowedSumProjection], + remap: &mut impl FnMut(&mut LocalId), +) { + for row in rows { + row.remap_locals(remap); + } +} +impl Function { + /// Readonly exact current statement projection; public copied sum nodes grant nothing. + pub fn resource_borrowed_sum_projection( + &self, + block: BlockId, + position: ResourcePosition, + ) -> Result, String> { + let Some(witness) = &self.resource_lowering else { + return Ok(None); + }; + witness.current(self)?; + let site = ResourceSite { + function: self.id, + block, + position, + }; + Ok(witness + .borrowed_sums + .iter() + .find(|row| row.observe == site || row.project == site || row.failure == Some(site))) + } + pub(crate) fn resource_borrowed_sum_alias( + &self, + local: LocalId, + value: &Expression, + ) -> Result { + let Some(witness) = &self.resource_lowering else { + return Ok(false); + }; + witness.current(self)?; + Ok(witness.borrowed_sums.iter().any(|row| row.alias.id == local && matches!(&value.kind, E::View(inner) if matches!(inner.kind, E::Local(output) if output == row.output.id)) + && self.blocks.iter().flat_map(|block| &block.statements).any(|statement| matches!(&statement.kind, StatementKind::Let { local: defined, value: current } if *defined == local && std::ptr::eq(current, value))))) + } +} + +// Only an authenticated finite sequence prefix moves these current sites. +pub(super) fn shift_prefix(witness: &mut ResourceLoweringWitness, block: BlockId, count: usize) { + for row in &mut witness.borrowed_sums { + for site in [&mut row.initialize, &mut row.observe, &mut row.project] + .into_iter() + .chain(row.failure.iter_mut()) + { + if site.block == block + && let ResourcePosition::Statement(index) = &mut site.position + { + *index += count; + } + } + } + seal(witness); +} diff --git a/crates/jett_mir/src/resource_ownership/borrowed_sums_tests.rs b/crates/jett_mir/src/resource_ownership/borrowed_sums_tests.rs new file mode 100644 index 00000000..fff4b730 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/borrowed_sums_tests.rs @@ -0,0 +1,328 @@ +use super::tests::{checked_support, lowered}; +use super::*; + +const SUPPORT: &str = + include_str!("../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); +const SOME: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/47_view_optional_some_written.jett" +); +const SOURCES: [&str; 12] = [ + include_str!( + "../../../jett_driver/tests/native_conformance/resource/45_view_optional_none_written.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/46_view_optional_none_bare.jett" + ), + SOME, + include_str!( + "../../../jett_driver/tests/native_conformance/resource/48_view_optional_some_bare.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/49_view_result_fail_written.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/50_view_result_fail_bare.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/51_view_result_ok_written.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/52_view_result_ok_bare.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/53_view_optional_written_argument_abort.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/54_view_result_bare_argument_abort.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/55_view_optional_written_borrow_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/56_view_result_bare_borrow_fail.jett" + ), +]; + +fn observer(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name.starts_with("observe_") + }) + .unwrap() +} +fn observer_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name.starts_with("observe_") + }) + .unwrap() +} + +#[test] +fn resource_borrowed_sums_loan_order_preserves_exact_tag_path_and_parent() { + let projected = |parent, tag, path| ResourceLoanSource::ProjectedSumPayload { + parent: ResourceLoanId(parent), + tag: LocalId::new(tag), + path, + }; + let sources = [ + ResourceLoanSource::Owner(ResourceOwnerSlotId(0)), + ResourceLoanSource::IncomingViewFormal { + scope: ResourceFrameId(0), + parameter: 0, + }, + ResourceLoanSource::IncomingViewFormal { + scope: ResourceFrameId(0), + parameter: 1, + }, + ResourceLoanSource::IncomingViewFormal { + scope: ResourceFrameId(1), + parameter: 0, + }, + projected(0, 0, ResourceSumPayloadPath::OptionalSome), + projected(0, 1, ResourceSumPayloadPath::OptionalSome), + projected(0, 0, ResourceSumPayloadPath::ResultOk), + projected(1, 0, ResourceSumPayloadPath::OptionalSome), + ]; + for left in sources { + for right in sources { + assert_eq!(left.cmp(&right).is_eq(), left == right); + } + } + assert_eq!( + sources.into_iter().collect::>().len(), + sources.len() + ); +} + +#[test] +fn resource_borrowed_sums_preserve_nonowning_headers_and_all_source_call_shapes() { + for release in [false, true] { + for (fixture, source) in SOURCES.into_iter().enumerate() { + let checked = checked_support(source, SUPPORT, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + for function in &mut program.functions { + assert!( + crate::sequences::prune::unreachable(function), + "borrowed canonical fixture {} release={release}", + fixture + 45 + ); + } + let ownership = validate_resource_ownership(&program, types).unwrap_or_else(|errors| { + panic!( + "borrowed fixture {} release={release}: {errors:?}", + fixture + 45 + ) + }); + let current = observer(&program); + let plan = ownership.function(current.id).unwrap(); + let projected = plan + .operations() + .iter() + .find_map(|operation| match operation.role() { + ResourceOperationRole::ProjectSumView { + source, + destination, + tag, + path, + } => Some((*source, *destination, *tag, *path, operation.site())), + _ => None, + }) + .expect("exact guarded borrowed payload projection"); + let (parent, child, tag, path, site) = projected; + let row = current + .resource_borrowed_sum_projection(site.block(), site.position()) + .unwrap() + .unwrap(); + assert_eq!(row.tag(), tag); + assert_eq!(row.path(), path); + assert!(matches!( + plan.loans()[parent.index()].source(), + ResourceLoanSource::IncomingViewFormal { .. } + )); + assert!(plan.loans()[parent.index()].shape().conditional()); + assert_eq!( + plan.loans()[child.index()].source(), + ResourceLoanSource::ProjectedSumPayload { parent, tag, path } + ); + assert!(!plan.loans()[child.index()].shape().conditional()); + for local in [row.source(), row.output(), row.alias()] { + assert!(current.is_view_local(local)); + assert!(!current.local(local).unwrap().mutable); + assert!(!plan.owner_slots().iter().any(|slot| matches!(slot.storage(), ResourceSlotStorage::Local { header } if header.id == local))); + } + assert!(!plan.operations().iter().any(|operation| matches!(operation.role(), ResourceOperationRole::EndSumBorrow { loan } if *loan == parent))); + assert!(plan.operations().iter().any(|operation| matches!(operation.role(), ResourceOperationRole::EndBorrow { loan } if *loan == child))); + if fixture >= 4 && fixture != 8 && fixture != 10 { + assert!(plan.operations().iter().any(|operation| matches!(operation.role(), ResourceOperationRole::ReadFailureCompanion { failure, .. } if *failure == TypeInterner::STRING))); + } + for function in ownership.functions() { + let companion = ResourceCompanionPlan::analyze(&ownership, function.function()) + .unwrap_or_else(|error| { + panic!( + "borrowed companion fixture {} release={release}: {error}", + fixture + 45 + ) + }); + let current = &program.functions[function.function().index() as usize]; + assert!( + current + .locals + .iter() + .filter(|local| resource_type_pending(types, local.ty)) + .all(|local| !companion + .storage() + .owned_locals + .contains(&(local.id.index() as usize))) + ); + } + let main = ownership + .functions() + .iter() + .find(|function| { + function.identity().declaration.namespace == "app" + && function.identity().declaration.name == "main" + }) + .unwrap(); + assert!(main.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::BorrowSum { .. } + | ResourceOperationRole::PrepareSourceSumBorrow { .. } + ))); + assert!(main.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::EndSumBorrow { .. } + ))); + if fixture == 8 || fixture == 9 { + assert!(main.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::PrepareSourceSumBorrow { .. } + ))); + } + } + } +} + +#[test] +fn resource_borrowed_sums_refuse_header_tag_and_disconnected_projection_resealing() { + for release in [false, true] { + let checked = checked_support(SOME, SUPPORT, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + let current = observer(&baseline); + let row = current + .resource_lowering + .as_ref() + .unwrap() + .borrowed_sums + .first() + .unwrap(); + let output = row.output(); + let source = row.source(); + let backing = row.backing(); + let site = row.success_site(); + for mutation in 0..8 { + let mut forged = baseline.clone(); + let current = observer_mut(&mut forged); + match mutation { + 0 => current.locals[output.index() as usize].view_source = None, + 1 => current.locals[output.index() as usize].mutable = true, + 2 => { + let ResourcePosition::Statement(index) = site.position() else { + unreachable!() + }; + let StatementKind::SumTake { source, .. } = + &mut current.blocks[site.block().index() as usize].statements[index].kind + else { + unreachable!() + }; + *source = backing; + } + 3 => { + let ResourcePosition::Statement(index) = site.position() else { + unreachable!() + }; + let StatementKind::SumTake { success, .. } = + &mut current.blocks[site.block().index() as usize].statements[index].kind + else { + unreachable!() + }; + *success = false; + } + 4 => { + let row = current + .resource_lowering + .as_ref() + .unwrap() + .borrowed_sums + .first() + .unwrap(); + let tag = row.tag(); + let branch = current.blocks.iter_mut().find(|block| matches!(&block.terminator.kind, TerminatorKind::Branch { condition, .. } if matches!(condition.kind, hir::ExpressionKind::Local(local) if local == tag))).unwrap(); + let TerminatorKind::Branch { + then_block, + else_block, + .. + } = &mut branch.terminator.kind + else { + unreachable!() + }; + *then_block = *else_block; + } + 5 => { + let ResourcePosition::Statement(index) = site.position() else { + unreachable!() + }; + let statement = + current.blocks[site.block().index() as usize].statements[index].clone(); + current.blocks.push(BasicBlock { + id: BlockId(current.blocks.len() as u32), + statements: vec![statement], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: current.span, + }, + }); + } + 6 => current + .resource_lowering + .as_mut() + .unwrap() + .borrowed_sums + .clear(), + 7 => { + current.locals[source.index() as usize].ty = + current.locals[output.index() as usize].ty + } + _ => unreachable!(), + } + // Public general cache resealing must not replace independent transport. + let witness = current.resource_lowering.as_mut().unwrap(); + witness.blocks = current.blocks.clone(); + witness.locals = current.locals.clone(); + assert!( + validate_resource_ownership(&forged, types).is_err(), + "mutation={mutation} release={release}" + ); + assert!( + !crate::sequences::prune::unreachable(observer_mut(&mut forged)), + "canonical mutation={mutation} release={release}" + ); + } + let mut missing = baseline.clone(); + observer_mut(&mut missing).resource_lowering = None; + assert!(validate_resource_ownership(&missing, types).is_err()); + let mut foreign = baseline.clone(); + let other = checked_support(SOME, SUPPORT, release); + observer_mut(&mut foreign).resource_lowering = + observer(&lowered(&other)).resource_lowering.clone(); + assert!(validate_resource_ownership(&foreign, types).is_err()); + } +} diff --git a/crates/jett_mir/src/resource_ownership/carriers.rs b/crates/jett_mir/src/resource_ownership/carriers.rs new file mode 100644 index 00000000..2dd658f9 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/carriers.rs @@ -0,0 +1,468 @@ +//! Constructor-issued typed aggregate transport. No graph row grants leaf custody. +//! Every runtime root is validated to contain zero selected Resource leaves before +//! it is committed; latent occupied shape metadata is retained without execution. +use super::*; +use hir::ExpressionKind as E; +use std::sync::Arc; + +mod flow; +mod graph; +mod iterations; +mod planner; + +identity!(ResourceCarrierShapeId); +identity!(ResourceCarrierSlotId); +identity!(ResourceCarrierLoanId); +identity!(ResourceCarrierOperationId); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResourceCarrierField { + pub ordinal: usize, + pub name: String, + pub shape: ResourceCarrierShapeId, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResourceCarrierVariant { + pub ordinal: usize, + pub name: String, + pub discriminant: i64, + pub fields: Vec, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResourceCarrierState { + pub ordinal: usize, + pub name: String, + pub fields: Vec, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResourceCarrierNode { + Ordinary, + Resource { + kind: hir::ResourceKindRef, + }, + Optional { + payload: ResourceCarrierShapeId, + }, + Result { + success: ResourceCarrierShapeId, + failure: ResourceCarrierShapeId, + }, + List { + element: ResourceCarrierShapeId, + }, + Map { + key: ResourceCarrierShapeId, + value: ResourceCarrierShapeId, + }, + Struct { + owner: jett_types::StructId, + arguments: Vec, + fields: Vec, + }, + Enum { + owner: jett_types::EnumId, + arguments: Vec, + variants: Vec, + }, + Machine { + owner: jett_types::MachineId, + states: Vec, + transitions: Vec<(usize, usize)>, + }, + MachineState { + machine: ResourceCarrierShapeId, + state: usize, + }, + Refinement { + name: String, + base: ResourceCarrierShapeId, + }, +} +#[derive(Debug, Clone)] +pub struct ResourceCarrierShape { + id: ResourceCarrierShapeId, + ty: TypeId, + node: ResourceCarrierNode, + contains_resource: bool, + refinement_predicate: Option, +} +impl ResourceCarrierShape { + pub fn id(&self) -> ResourceCarrierShapeId { + self.id + } + pub fn ty(&self) -> TypeId { + self.ty + } + pub fn node(&self) -> &ResourceCarrierNode { + &self.node + } + pub fn contains_resource(&self) -> bool { + self.contains_resource + } + /// Original checked declaration/body association; metadata grants no proof + /// that a raw base value satisfies the refinement at runtime. + pub fn refinement_predicate(&self) -> Option<&hir::Function> { + self.refinement_predicate.as_ref() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceCarrierGeneration { + /// Runtime activation issues a fresh generation on each successful install. + FreshOnInstall, +} +#[derive(Debug, Clone)] +pub struct ResourceCarrierSlot { + id: ResourceCarrierSlotId, + frame: ResourceFrameId, + shape: ResourceCarrierShapeId, + storage: ResourceSlotStorage, +} +impl ResourceCarrierSlot { + pub fn id(&self) -> ResourceCarrierSlotId { + self.id + } + pub fn frame(&self) -> ResourceFrameId { + self.frame + } + pub fn shape(&self) -> ResourceCarrierShapeId { + self.shape + } + pub fn storage(&self) -> &ResourceSlotStorage { + &self.storage + } + pub fn generation(&self) -> ResourceCarrierGeneration { + ResourceCarrierGeneration::FreshOnInstall + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceCarrierIndex { + Constant(usize), + Local(LocalId), + CurrentIteration { header: BlockId }, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceCarrierEdge { + SwitchVariant { + source: BlockId, + variant: VariantId, + target: BlockId, + }, + SwitchOtherwise { + source: BlockId, + target: BlockId, + }, + IterationBody { + source: BlockId, + target: BlockId, + }, +} +#[derive(Debug, Clone)] +pub struct ResourceCarrierIteration { + pub header: BlockId, + pub body: BlockId, + pub exit: BlockId, + pub source: ResourceCarrierSlotId, + pub binders: Vec, + pub cursor: Option, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResourceCarrierProjectionPath { + Field { + owner: TypeId, + field: FieldId, + }, + Variant { + owner: TypeId, + variant: VariantId, + field: usize, + }, + State { + owner: TypeId, + state: usize, + field: usize, + }, + Element { + index: ResourceCarrierIndex, + }, + MapKey { + index: ResourceCarrierIndex, + }, + MapValue { + index: ResourceCarrierIndex, + }, + OptionalSome, + ResultOk, + ResultFail, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceCarrierLoanSource { + Slot(ResourceCarrierSlotId), + Loan(ResourceCarrierLoanId), + IncomingViewFormal { + scope: ResourceFrameId, + parameter: usize, + }, +} +#[derive(Debug, Clone)] +pub struct ResourceCarrierLoan { + id: ResourceCarrierLoanId, + frame: ResourceFrameId, + shape: ResourceCarrierShapeId, + source: ResourceCarrierLoanSource, + path: Vec, +} +impl ResourceCarrierLoan { + pub fn id(&self) -> ResourceCarrierLoanId { + self.id + } + pub fn frame(&self) -> ResourceFrameId { + self.frame + } + pub fn shape(&self) -> ResourceCarrierShapeId { + self.shape + } + pub fn source(&self) -> ResourceCarrierLoanSource { + self.source + } + pub fn path(&self) -> &[ResourceCarrierProjectionPath] { + &self.path + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceCarrierValue { + Ordinary { ty: TypeId }, + Owned { slot: ResourceCarrierSlotId }, + Borrowed { loan: ResourceCarrierLoanId }, + LeafOwned { slot: ResourceOwnerSlotId }, + LeafBorrowed { loan: ResourceLoanId }, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceCarrierConstructor { + List, + Map, + Struct, + Enum { variant: VariantId }, + Machine { state: usize }, + OptionalNone, + OptionalSome, + ResultOk, + ResultFail, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResourceCarrierChild { + pub index: usize, + pub shape: ResourceCarrierShapeId, + pub value: ResourceCarrierValue, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResourceCarrierObservation { + Length, + Tag, + State { + state: usize, + }, + Ordinary { + path: Vec, + ty: TypeId, + }, +} +#[derive(Debug, Clone)] +pub enum ResourceCarrierOperationRole { + BeginConstructor { + destination: ResourceCarrierSlotId, + constructor: ResourceCarrierConstructor, + }, + ConstructorChild { + destination: ResourceCarrierSlotId, + child: ResourceCarrierChild, + }, + CommitConstructor { + destination: ResourceCarrierSlotId, + }, + Transfer { + source: ResourceCarrierSlotId, + destination: ResourceCarrierSlotId, + }, + QualifyMachine { + source: ResourceCarrierSlotId, + destination: ResourceCarrierSlotId, + machine: ResourceCarrierShapeId, + state: usize, + }, + Borrow { + loan: ResourceCarrierLoanId, + }, + EndBorrow { + loan: ResourceCarrierLoanId, + }, + Observe { + source: ResourceCarrierLoanId, + observation: ResourceCarrierObservation, + target: Option, + }, + Project { + source: ResourceCarrierLoanId, + destination: ResourceCarrierLoanId, + }, + Extract { + source: ResourceCarrierLoanId, + path: Vec, + destination: ResourceCarrierSlotId, + }, + AdaptSum { + source: ResourceCarrierLoanId, + path: Vec, + destination: ResourceOwnerSlotId, + loan: Option, + lease_frame: ResourceFrameId, + }, + ExtractOrdinary { + source: ResourceCarrierLoanId, + path: Vec, + target: LocalId, + ty: TypeId, + }, + BeginIteration { + source: ResourceCarrierValue, + destination: ResourceCarrierSlotId, + body: BlockId, + exit: BlockId, + }, + RetireIteration { + source: ResourceCarrierSlotId, + binders: Vec, + }, + PublishReturn { + source: ResourceCarrierSlotId, + }, + Retire { + source: ResourceCarrierSlotId, + }, +} +#[derive(Debug, Clone)] +pub struct ResourceCarrierOperation { + id: ResourceCarrierOperationId, + site: ResourceSite, + frame: ResourceFrameId, + role: ResourceCarrierOperationRole, + expression: Option, + edge: Option, +} +impl ResourceCarrierOperation { + pub fn id(&self) -> ResourceCarrierOperationId { + self.id + } + pub fn site(&self) -> ResourceSite { + self.site + } + pub fn frame(&self) -> ResourceFrameId { + self.frame + } + pub fn role(&self) -> &ResourceCarrierOperationRole { + &self.role + } + pub fn edge(&self) -> Option { + self.edge + } + pub fn is_expression_operation(&self) -> bool { + self.expression.is_some() + } + pub fn is_for_expression(&self, expression: &Expression) -> bool { + self.expression == Some(std::ptr::from_ref(expression).addr()) + } +} +#[derive(Debug)] +pub struct ResourceCarrierFunctionPlan { + shapes: Arc>, + slots: Vec, + loans: Vec, + operations: Vec, + iterations: Vec, +} +impl ResourceCarrierFunctionPlan { + pub fn shapes(&self) -> &[ResourceCarrierShape] { + &self.shapes + } + pub fn shape_for_type(&self, ty: TypeId) -> Option<&ResourceCarrierShape> { + self.shapes + .get(usize::try_from(ty.index()).ok()?) + .filter(|row| row.ty == ty) + } + pub fn shape(&self, id: ResourceCarrierShapeId) -> Option<&ResourceCarrierShape> { + self.shapes.get(id.index()).filter(|row| row.id == id) + } + pub fn slots(&self) -> &[ResourceCarrierSlot] { + &self.slots + } + pub fn loans(&self) -> &[ResourceCarrierLoan] { + &self.loans + } + pub fn operations(&self) -> &[ResourceCarrierOperation] { + &self.operations + } + pub fn iterations(&self) -> &[ResourceCarrierIteration] { + &self.iterations + } + pub fn operations_for_expression<'a>( + &'a self, + expression: &'a Expression, + ) -> impl Iterator { + self.operations + .iter() + .filter(move |operation| operation.is_for_expression(expression)) + } + pub fn operations_at( + &self, + site: ResourceSite, + ) -> impl Iterator { + self.operations + .iter() + .filter(move |operation| operation.site == site && operation.edge.is_none()) + } + pub fn operations_on_edge( + &self, + edge: ResourceCarrierEdge, + ) -> impl Iterator { + self.operations + .iter() + .filter(move |operation| operation.edge == Some(edge)) + } +} + +/// Leaf and function descriptor types retain their existing protocol. +pub(super) fn carrier_type(types: &TypeInterner, ty: TypeId) -> bool { + if !resource_type_pending(types, ty) { + return false; + } + match types.resolve(ty) { + Type::Resource(_) | Type::Function { .. } => false, + Type::Optional(inner) if matches!(types.resolve(*inner), Type::Resource(_)) => false, + Type::Result(ok, fail) + if matches!(types.resolve(*ok), Type::Resource(_)) + && !resource_type_pending(types, *fail) => + { + false + } + _ => true, + } +} +pub(super) fn required(function: &Function, types: &TypeInterner) -> bool { + carrier_type(types, function.return_type) + || function + .locals + .iter() + .any(|local| carrier_type(types, local.ty)) + || function.blocks.iter().any(|block| { + let mut found = false; + walk::mir_block(block, &mut |value| found |= carrier_type(types, value.ty)); + found + }) +} +pub(super) fn analyze( + program: &Program, + function: &Function, + types: &TypeInterner, +) -> Result { + planner::analyze(program, function, types) +} diff --git a/crates/jett_mir/src/resource_ownership/carriers/flow.rs b/crates/jett_mir/src/resource_ownership/carriers/flow.rs new file mode 100644 index 00000000..0be18003 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/carriers/flow.rs @@ -0,0 +1,1527 @@ +//! Independent current-CFG zero-leaf custody proof. IDs describe exact sealed +//! endpoints, never values that can grant native permission on their own. +use super::*; +use std::collections::VecDeque; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Origin { + Formal(usize), + Constructor(ResourceCarrierOperationId), + Extraction(ResourceCarrierOperationId), + Source(ResourceOperationId), + LeafAbsence(ResourceOperationId), +} +#[derive(Debug, Clone, PartialEq, Eq)] +enum Slot { + Vacant, + Building { + origin: Origin, + expected: Vec, + order: Vec, + arrived: usize, + }, + Live { + origin: Origin, + taken: Vec>, + }, + Moved, +} +#[derive(Debug, Clone, PartialEq, Eq)] +enum Leaf { + Vacant, + Empty { + origin: Origin, + }, + Occupied { + origin: Origin, + }, + Adapter { + operation: ResourceCarrierOperationId, + }, + Moved, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum Root { + Slot(ResourceCarrierSlotId), + Incoming(ResourceCarrierLoanId), +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Anchor { + root: Root, + origin: Origin, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Selector { + Sum(bool), + Variant(usize), + State(usize), + Iteration(BlockId), +} +#[derive(Debug, Clone, PartialEq, Eq)] +struct Selection { + anchor: Anchor, + path: Vec, + selector: Selector, +} +#[derive(Debug, Clone, PartialEq, Eq)] +struct Probe { + anchor: Anchor, + path: Vec, + shape: ResourceCarrierShapeId, +} +#[derive(Debug, Clone, PartialEq, Eq)] +struct State { + slots: Vec, + leaves: Vec, + loans: BTreeSet, + leaf_loans: BTreeSet, + frames: Vec, + returned: bool, + selections: Vec, + tags: BTreeMap, + switches: BTreeMap, + retired_projection: Option<(ResourceCarrierLoanId, ResourceCarrierSlotId, Origin)>, +} +struct Analysis<'a> { + function: &'a Function, + plan: &'a ResourceFunctionPlan, + carriers: &'a ResourceCarrierFunctionPlan, +} +impl Analysis<'_> { + fn frame(&self, state: &mut State, frame: ResourceFrameId) -> Result<(), String> { + if frame == ResourceFrameId(0) { + if state.frames.last().copied() != Some(frame) { + return Err("carrier operation lost its exact live Scope prefix".into()); + } + return Ok(()); + } + let row = self + .plan + .frames + .get(frame.index()) + .filter(|row| row.id == frame) + .ok_or("carrier operation frame missing")?; + if row.role == ResourceFrameRole::Return { + if row.parent.is_some() { + return Err("carrier provisional Return frame has a foreign parent".into()); + } + return Ok(()); + } + if state.frames.last().copied() == Some(frame) { + return Ok(()); + } + let parent = row + .parent + .ok_or("carrier Operation frame lost its parent")?; + if state.frames.last().copied() != Some(parent) { + return Err("carrier operation changed its exact frame prefix".into()); + } + state.frames.push(frame); + Ok(()) + } + fn live<'s>(&self, state: &'s State, slot: ResourceCarrierSlotId) -> Result<&'s Slot, String> { + let value = state + .slots + .get(slot.index()) + .ok_or("carrier slot outside its exact plan")?; + if !matches!(value, Slot::Live { .. }) { + return Err("carrier source is uninitialized, moved, or incompletely constructed at this CFG site".into()); + } + Ok(value) + } + fn root(&self, loan: ResourceCarrierLoanId) -> Result, String> { + let mut current = loan; + let mut seen = BTreeSet::new(); + loop { + if !seen.insert(current) { + return Err("carrier loan parent graph is cyclic".into()); + } + let row = self + .carriers + .loans + .get(current.index()) + .filter(|row| row.id == current) + .ok_or("carrier loan missing")?; + match row.source { + ResourceCarrierLoanSource::Slot(slot) => return Ok(Some(slot)), + ResourceCarrierLoanSource::Loan(parent) => current = parent, + ResourceCarrierLoanSource::IncomingViewFormal { .. } => return Ok(None), + } + } + } + fn unleased(&self, state: &State, slot: ResourceCarrierSlotId) -> Result<(), String> { + for loan in &state.loans { + if self.root(*loan)? == Some(slot) { + return Err( + "carrier move/retirement has an exact live parent or child lease".into(), + ); + } + } + for loan in &state.leaf_loans { + if let ResourceLoanSource::CarrierSumProjection { operation } = + self.plan.loans[loan.index()].source + { + let ResourceCarrierOperationRole::AdaptSum { source, .. } = + self.carriers.operations[operation.index()].role + else { + return Err("carrier leaf loan lost its exact adapter role".into()); + }; + if self.root(source)? == Some(slot) { + return Err("carrier move/retirement has a live projected sum lease".into()); + } + } + } + Ok(()) + } + fn install( + &self, + state: &mut State, + slot: ResourceCarrierSlotId, + origin: Origin, + ) -> Result<(), String> { + if !matches!( + state.slots.get(slot.index()), + Some(Slot::Vacant | Slot::Moved) + ) { + return Err( + "carrier destination is already live outside an authenticated binder reset".into(), + ); + } + state.slots[slot.index()] = Slot::Live { + origin, + taken: Vec::new(), + }; + Ok(()) + } + fn take(&self, state: &mut State, slot: ResourceCarrierSlotId) -> Result { + self.live(state, slot)?; + self.unleased(state, slot)?; + state + .selections + .retain(|fact| fact.anchor.root != Root::Slot(slot)); + state + .tags + .retain(|_, probe| probe.anchor.root != Root::Slot(slot)); + state + .switches + .retain(|_, probe| probe.anchor.root != Root::Slot(slot)); + Ok(std::mem::replace( + &mut state.slots[slot.index()], + Slot::Moved, + )) + } + fn take_complete( + &self, + state: &mut State, + slot: ResourceCarrierSlotId, + ) -> Result { + if !matches!(self.live(state, slot)?, Slot::Live { taken, .. } if taken.is_empty()) { + return Err("carrier whole-root transfer, adoption, Source transport or publication has extracted children".into()); + } + self.take(state, slot) + } + fn view_complete(&self, state: &State, loan: ResourceCarrierLoanId) -> Result<(), String> { + self.loan_live(state, loan)?; + if let Some(root) = self.root(loan)? { + let path = self.absolute_path(loan, &[])?; + let Slot::Live { taken, .. } = self.live(state, root)? else { + return Err("carrier view Source root is not installed".into()); + }; + if taken + .iter() + .any(|prior| prior.starts_with(&path) || path.starts_with(prior)) + { + return Err( + "carrier view Source formal cannot receive an incomplete selected subtree" + .into(), + ); + } + } + Ok(()) + } + fn loan_live(&self, state: &State, loan: ResourceCarrierLoanId) -> Result<(), String> { + if !state.loans.contains(&loan) { + return Err( + "carrier observation/projection uses an ended or unreached exact loan".into(), + ); + } + if let Some(root) = self.root(loan)? { + self.live(state, root)?; + } + if self.projected_shape(state, loan, &[])? != self.carriers.loans[loan.index()].shape { + return Err( + "carrier active loan changed its exact parent generation/path shape".into(), + ); + } + Ok(()) + } + fn absolute_path( + &self, + loan: ResourceCarrierLoanId, + suffix: &[ResourceCarrierProjectionPath], + ) -> Result, String> { + let mut ancestors = Vec::new(); + let mut current = loan; + let mut seen = BTreeSet::new(); + loop { + if !seen.insert(current) { + return Err("carrier path parent graph is cyclic".into()); + } + let row = self + .carriers + .loans + .get(current.index()) + .ok_or("carrier path loan missing")?; + ancestors.push(row.path.clone()); + match row.source { + ResourceCarrierLoanSource::Loan(parent) => current = parent, + ResourceCarrierLoanSource::Slot(_) + | ResourceCarrierLoanSource::IncomingViewFormal { .. } => break, + } + } + let mut path = Vec::new(); + for ancestor in ancestors.into_iter().rev() { + path.extend(ancestor); + } + path.extend_from_slice(suffix); + Ok(path) + } + fn anchor(&self, state: &State, loan: ResourceCarrierLoanId) -> Result { + let mut current = loan; + let mut seen = BTreeSet::new(); + loop { + if !seen.insert(current) { + return Err("carrier anchor loan graph is cyclic".into()); + } + let row = self + .carriers + .loans + .get(current.index()) + .ok_or("carrier anchor loan missing")?; + match row.source { + ResourceCarrierLoanSource::Slot(slot) => { + let Slot::Live { origin, .. } = self.live(state, slot)? else { + return Err("carrier anchor is not live".into()); + }; + return Ok(Anchor { + root: Root::Slot(slot), + origin: *origin, + }); + } + ResourceCarrierLoanSource::Loan(parent) => current = parent, + ResourceCarrierLoanSource::IncomingViewFormal { parameter, .. } => { + return Ok(Anchor { + root: Root::Incoming(current), + origin: Origin::Formal(parameter), + }); + } + } + } + } + fn selection( + &self, + state: &State, + anchor: Anchor, + path: &[ResourceCarrierProjectionPath], + selector: Selector, + ) -> bool { + state + .selections + .iter() + .any(|fact| fact.anchor == anchor && fact.path == path && fact.selector == selector) + } + fn select(&self, state: &mut State, probe: &Probe, selector: Selector) { + state + .selections + .retain(|fact| fact.anchor != probe.anchor || fact.path != probe.path); + state.selections.push(Selection { + anchor: probe.anchor, + path: probe.path.clone(), + selector, + }); + } + fn projected_shape( + &self, + state: &State, + loan: ResourceCarrierLoanId, + suffix: &[ResourceCarrierProjectionPath], + ) -> Result { + let anchor = self.anchor(state, loan)?; + let mut current = match anchor.root { + Root::Slot(slot) => self.carriers.slots[slot.index()].shape, + Root::Incoming(loan) => self.carriers.loans[loan.index()].shape, + }; + let path = self.absolute_path(loan, suffix)?; + if let Root::Slot(slot) = anchor.root { + let Slot::Live { taken, .. } = self.live(state, slot)? else { + return Err("carrier path root is not installed".into()); + }; + if taken.iter().any(|prior| path.starts_with(prior)) { + return Err("carrier projection reads an already extracted selected path".into()); + } + } + let mut prefix = Vec::new(); + for part in &path { + let shape = self + .carriers + .shape(current) + .ok_or("carrier selected path graph row missing")?; + current = match (part, &shape.node) { + (ResourceCarrierProjectionPath::Field { owner, field }, ResourceCarrierNode::Struct { fields, .. }) if *owner == shape.ty => fields.get(field.index() as usize).ok_or("carrier field outside its exact nominal definition")?.shape, + (ResourceCarrierProjectionPath::Variant { owner, variant, field }, ResourceCarrierNode::Enum { variants, .. }) if *owner == shape.ty => { + if !self.selection(state, anchor, &prefix, Selector::Variant(variant.index() as usize)) { return Err("carrier variant projection lacks its exact current selecting Switch edge".into()); } + variants.get(variant.index() as usize).and_then(|variant| variant.fields.get(*field)).ok_or("carrier variant payload outside its exact graph")?.shape + } + (ResourceCarrierProjectionPath::State { owner, state: selected, field }, ResourceCarrierNode::MachineState { machine, state: actual }) if *owner == shape.ty && selected == actual => { + let ResourceCarrierNode::Machine { states, .. } = &self.carriers.shape(*machine).ok_or("carrier machine graph missing")?.node else { return Err("carrier state owner is not a machine".into()); }; + states.get(*selected).and_then(|state| state.fields.get(*field)).ok_or("carrier state field outside its exact graph")?.shape + } + (ResourceCarrierProjectionPath::State { owner, state: selected, field }, ResourceCarrierNode::Machine { states, .. }) => { + let qualified = self.carriers.shape_for_type(*owner).ok_or("carrier field qualification graph missing")?; + if !matches!(qualified.node, ResourceCarrierNode::MachineState { machine, state } if machine == current && state == *selected) || !self.selection(state, anchor, &prefix, Selector::State(*selected)) { return Err("carrier state projection lacks its exact current StateIs edge/owner qualification".into()); } + states.get(*selected).and_then(|state| state.fields.get(*field)).ok_or("carrier state field outside its exact graph")?.shape + } + (ResourceCarrierProjectionPath::OptionalSome, ResourceCarrierNode::Optional { payload }) => { + if !self.selection(state, anchor, &prefix, Selector::Sum(true)) { return Err("carrier Optional payload lacks its exact selecting typed SumTag edge".into()); } + *payload + } + (ResourceCarrierProjectionPath::ResultOk, ResourceCarrierNode::Result { success, .. }) => { + if !self.selection(state, anchor, &prefix, Selector::Sum(true)) { return Err("carrier Result ok payload lacks its exact selecting typed SumTag edge".into()); } + *success + } + (ResourceCarrierProjectionPath::ResultFail, ResourceCarrierNode::Result { failure, .. }) => { + if !self.selection(state, anchor, &prefix, Selector::Sum(false)) { return Err("carrier Result fail payload lacks its exact selecting typed SumTag edge".into()); } + *failure + } + (ResourceCarrierProjectionPath::Element { index }, ResourceCarrierNode::List { element }) => { self.iteration_index(state, anchor, &prefix, *index)?; *element } + (ResourceCarrierProjectionPath::MapKey { index }, ResourceCarrierNode::Map { key, .. }) => { self.iteration_index(state, anchor, &prefix, *index)?; *key } + (ResourceCarrierProjectionPath::MapValue { index }, ResourceCarrierNode::Map { value, .. }) => { self.iteration_index(state, anchor, &prefix, *index)?; *value } + _ => return Err("carrier selected path changes its exact graph family, nominal owner or qualification".into()), + }; + prefix.push(part.clone()); + } + Ok(current) + } + fn iteration_index( + &self, + state: &State, + anchor: Anchor, + path: &[ResourceCarrierProjectionPath], + index: ResourceCarrierIndex, + ) -> Result<(), String> { + let Root::Slot(slot) = anchor.root else { + return Err( + "pending Resource carrier: borrowed iterator index has no owning loop root".into(), + ); + }; + let row = self + .carriers + .iterations + .iter() + .find(|row| { + row.source == slot + && match index { + ResourceCarrierIndex::Local(cursor) => row.cursor == Some(cursor), + ResourceCarrierIndex::CurrentIteration { header } => { + row.cursor.is_none() && row.header == header + } + ResourceCarrierIndex::Constant(_) => false, + } + }) + .ok_or("carrier dynamic index is outside its exact original/current iterator cursor")?; + if !path.is_empty() || !self.selection(state, anchor, path, Selector::Iteration(row.header)) + { + return Err( + "carrier indexed extraction lacks its exact selected current iterator body edge" + .into(), + ); + } + Ok(()) + } + fn probe(&self, state: &State, loan: ResourceCarrierLoanId) -> Result { + Ok(Probe { + anchor: self.anchor(state, loan)?, + path: self.absolute_path(loan, &[])?, + shape: self.projected_shape(state, loan, &[])?, + }) + } + fn extract( + &self, + state: &mut State, + source: ResourceCarrierLoanId, + path: &[ResourceCarrierProjectionPath], + ) -> Result<(), String> { + let root = self + .root(source)? + .ok_or("carrier owning extraction cannot consume an incoming view")?; + let origin = match self.live(state, root)? { + Slot::Live { origin, .. } => *origin, + _ => return Err("carrier extraction lost its installed root".into()), + }; + if state.retired_projection.take() != Some((source, root, origin)) { + return Err("carrier owning extraction lacks its immediately retired exact projection lease/generation".into()); + } + self.unleased(state, root)?; + self.projected_shape(state, source, path)?; + let key = self.absolute_path(source, path)?; + let Slot::Live { taken, .. } = &mut state.slots[root.index()] else { + return Err("carrier extraction root is not installed".into()); + }; + if taken + .iter() + .any(|prior| prior.starts_with(&key) || key.starts_with(prior)) + { + return Err("carrier selected path was already extracted at this generation".into()); + } + taken.push(key); + Ok(()) + } + fn constructor( + &self, + operation: &ResourceCarrierOperation, + destination: ResourceCarrierSlotId, + constructor: &ResourceCarrierConstructor, + ) -> Result<(Vec, Vec), String> { + let shape = self + .carriers + .shape(self.carriers.slots[destination.index()].shape) + .ok_or("carrier constructor graph row missing")?; + let mut dynamic = None; + let mut written_order = None; + if !operation.is_expression_operation() { + let witness = self + .function + .resource_lowering + .as_ref() + .ok_or("carrier constructor lost its checked Source witness")?; + if let Some(region) = witness + .constructors + .iter() + .find(|r| r.begin() == operation.site) + { + self.function.resource_constructor(region.id())?; + written_order = Some(region.evaluation_order().to_vec()); + } + } + let block = self + .function + .blocks + .get(operation.site.block.index() as usize) + .ok_or("carrier constructor site block missing")?; + walk::mir_block(block, &mut |expression| { + if !operation.is_for_expression(expression) { + return; + } + match &expression.kind { + E::ListConstruct { elements } => dynamic = Some(elements.len()), + E::MapConstruct { entries } => dynamic = entries.len().checked_mul(2), + E::StructConstruct { + evaluation_order, .. + } + | E::EnumConstruct { + evaluation_order, .. + } => written_order = Some(evaluation_order.clone()), + _ => {} + } + }); + let expected = match (constructor, &shape.node) { + (ResourceCarrierConstructor::List, ResourceCarrierNode::List { element }) => { + vec![ + *element; + dynamic.ok_or("carrier list constructor lost its exact current arity")? + ] + } + (ResourceCarrierConstructor::Map, ResourceCarrierNode::Map { key, value }) => (0 + ..dynamic.ok_or("carrier map constructor lost its exact current arity")?) + .map(|index| if index % 2 == 0 { *key } else { *value }) + .collect(), + (ResourceCarrierConstructor::Struct, ResourceCarrierNode::Struct { fields, .. }) => { + fields.iter().map(|field| field.shape).collect() + } + ( + ResourceCarrierConstructor::Enum { variant }, + ResourceCarrierNode::Enum { variants, .. }, + ) => variants + .get(variant.index() as usize) + .ok_or("carrier constructor variant outside its exact graph")? + .fields + .iter() + .map(|field| field.shape) + .collect(), + ( + ResourceCarrierConstructor::Machine { state }, + ResourceCarrierNode::Machine { states, .. }, + ) => states + .get(*state) + .ok_or("carrier constructor state outside its exact graph")? + .fields + .iter() + .map(|field| field.shape) + .collect(), + ( + ResourceCarrierConstructor::Machine { state }, + ResourceCarrierNode::MachineState { + machine, + state: selected, + }, + ) if state == selected => { + let ResourceCarrierNode::Machine { states, .. } = &self + .carriers + .shape(*machine) + .ok_or("carrier qualified machine graph missing")? + .node + else { + return Err("carrier qualified owner is not a machine".into()); + }; + states + .get(*state) + .ok_or("carrier qualified state outside its exact graph")? + .fields + .iter() + .map(|field| field.shape) + .collect() + } + (ResourceCarrierConstructor::OptionalNone, ResourceCarrierNode::Optional { .. }) => { + Vec::new() + } + ( + ResourceCarrierConstructor::OptionalSome, + ResourceCarrierNode::Optional { payload }, + ) => vec![*payload], + (ResourceCarrierConstructor::ResultOk, ResourceCarrierNode::Result { success, .. }) => { + vec![*success] + } + ( + ResourceCarrierConstructor::ResultFail, + ResourceCarrierNode::Result { failure, .. }, + ) => vec![*failure], + _ => { + return Err( + "carrier constructor role differs from its exact typed graph family".into(), + ); + } + }; + let order = written_order.unwrap_or_else(|| (0..expected.len()).collect()); + if order.len() != expected.len() + || order.iter().copied().collect::>() != (0..expected.len()).collect() + { + return Err( + "carrier constructor has a noncanonical lexical arrival permutation".into(), + ); + } + Ok((expected, order)) + } + fn carrier( + &self, + state: &mut State, + operation: &ResourceCarrierOperation, + ) -> Result<(), String> { + if !matches!( + operation.role, + ResourceCarrierOperationRole::PublishReturn { .. } + ) { + self.frame(state, operation.frame)?; + } + if !matches!( + operation.role, + ResourceCarrierOperationRole::Extract { .. } + | ResourceCarrierOperationRole::ExtractOrdinary { .. } + | ResourceCarrierOperationRole::AdaptSum { loan: None, .. } + ) { + state.retired_projection = None; + } + match &operation.role { + ResourceCarrierOperationRole::BeginConstructor { + destination, + constructor, + } => { + if !matches!(state.slots[destination.index()], Slot::Vacant | Slot::Moved) { + return Err("carrier constructor overwrites a live root".into()); + } + let (expected, order) = self.constructor(operation, *destination, constructor)?; + state.slots[destination.index()] = Slot::Building { + origin: Origin::Constructor(operation.id), + expected, + order, + arrived: 0, + }; + } + ResourceCarrierOperationRole::ConstructorChild { destination, child } => { + let Slot::Building { + expected, + order, + arrived, + .. + } = &state.slots[destination.index()] + else { + return Err("carrier constructor child precedes its exact Begin".into()); + }; + if order.get(*arrived) != Some(&child.index) + || expected.get(child.index) != Some(&child.shape) + { + return Err("carrier constructor child changes its exact lexical prefix or declared graph node".into()); + } + match child.value { + ResourceCarrierValue::Ordinary { ty } => { + let shape = self + .carriers + .shape(child.shape) + .ok_or("carrier ordinary child graph missing")?; + if shape.ty != ty || shape.contains_resource { + return Err("carrier constructor cannot adopt custody through an ordinary payload channel".into()); + } + } + ResourceCarrierValue::Owned { slot } => { + if self.carriers.slots[slot.index()].shape != child.shape { + return Err("carrier constructor adoption changes its child's exact installed graph node".into()); + } + self.take_complete(state, slot)?; + } + ResourceCarrierValue::LeafOwned { slot } => { + let endpoint = self + .plan + .slots + .get(slot.index()) + .ok_or("carrier child lost its exact leaf holder")?; + let node = &self + .carriers + .shape(child.shape) + .ok_or("carrier child lost its exact leaf graph")? + .node; + let valid = match (&endpoint.shape, node, state.leaves.get(slot.index())) { + ( + ResourceShape::Plain { kind }, + ResourceCarrierNode::Resource { kind: child_kind }, + Some(Leaf::Occupied { .. }), + ) => kind == child_kind, + ( + ResourceShape::Optional { .. } | ResourceShape::Result { .. }, + _, + Some(Leaf::Empty { .. }), + ) => true, + _ => false, + }; + if !valid { + return Err("carrier child lacks its exact initialized occupied or absent leaf producer".into()); + } + if state.leaf_loans.iter().any(|loan| { + self.plan.loans[loan.index()].source == ResourceLoanSource::Owner(slot) + }) { + return Err( + "carrier child adoption has an exact live leaf lease".into() + ); + } + state.leaves[slot.index()] = Leaf::Moved; + } + _ => return Err("carrier constructor cannot adopt any borrowed child".into()), + } + let Slot::Building { arrived, .. } = &mut state.slots[destination.index()] else { + return Err("carrier constructor child lost its arrived prefix".into()); + }; + *arrived += 1; + } + ResourceCarrierOperationRole::CommitConstructor { destination } => { + let Slot::Building { + origin, + expected, + arrived, + .. + } = &state.slots[destination.index()] + else { + return Err("carrier constructor Commit lacks its exact arrived prefix".into()); + }; + if *arrived != expected.len() { + return Err( + "carrier constructor Commit precedes its complete exact acquired prefix" + .into(), + ); + } + state.slots[destination.index()] = Slot::Live { + origin: *origin, + taken: Vec::new(), + }; + } + ResourceCarrierOperationRole::Transfer { + source, + destination, + } => { + let value = self.take_complete(state, *source)?; + if !matches!(state.slots[destination.index()], Slot::Vacant | Slot::Moved) { + return Err("carrier transfer overwrites a live destination".into()); + } + state.slots[destination.index()] = value; + } + ResourceCarrierOperationRole::QualifyMachine { + source, + destination, + machine, + state: selected, + } => { + let source_node = &self + .carriers + .shape(self.carriers.slots[source.index()].shape) + .ok_or("carrier qualification source graph row missing")? + .node; + if !matches!(source_node, ResourceCarrierNode::MachineState { machine: owner, state } if owner == machine && state == selected) + || self.carriers.slots[destination.index()].shape != *machine + { + return Err( + "carrier qualification changes its exact machine owner or checked state" + .into(), + ); + } + let value = self.take_complete(state, *source)?; + if !matches!(state.slots[destination.index()], Slot::Vacant | Slot::Moved) { + return Err("carrier qualification overwrites a live destination".into()); + } + state.slots[destination.index()] = value; + } + ResourceCarrierOperationRole::Borrow { loan } => { + let row = self + .carriers + .loans + .get(loan.index()) + .ok_or("carrier Borrow lost its exact loan row")?; + match row.source { + ResourceCarrierLoanSource::Slot(slot) => { self.live(state, slot)?; } + ResourceCarrierLoanSource::Loan(parent) => self.loan_live(state, parent)?, + ResourceCarrierLoanSource::IncomingViewFormal { .. } => return Err("carrier incoming formal is a resident caller lease, not a child Borrow activation".into()), + } + if self.projected_shape(state, *loan, &[])? != row.shape { + return Err("carrier Borrow declared graph node differs from its exact parent/path projection".into()); + } + if !state.loans.insert(*loan) { + return Err("carrier Borrow repeats a live lease".into()); + } + } + ResourceCarrierOperationRole::EndBorrow { loan } => { + if state.loans.iter().any(|child| { + self.carriers.loans[child.index()].source + == ResourceCarrierLoanSource::Loan(*loan) + }) { + return Err("carrier parent lease ends before its exact child".into()); + } + if state.leaf_loans.iter().any(|child| matches!(self.plan.loans[child.index()].source, + ResourceLoanSource::CarrierSumProjection { operation } if matches!(self.carriers.operations[operation.index()].role, ResourceCarrierOperationRole::AdaptSum { source, .. } if source == *loan))) { return Err("carrier parent lease ends before its sum adapter child".into()); } + if !state.loans.remove(loan) { + return Err("carrier EndBorrow has no reached activation".into()); + } + if let Some(root) = self.root(*loan)? { + if let Slot::Live { origin, .. } = self.live(state, root)? { + state.retired_projection = Some((*loan, root, *origin)); + } + } + } + ResourceCarrierOperationRole::Observe { + source, + observation, + target, + } => { + self.loan_live(state, *source)?; + let probe = self.probe(state, *source)?; + let node = &self + .carriers + .shape(probe.shape) + .ok_or("carrier observation graph row missing")? + .node; + match observation { + ResourceCarrierObservation::Length + if matches!( + node, + ResourceCarrierNode::List { .. } | ResourceCarrierNode::Map { .. } + ) => {} + ResourceCarrierObservation::Tag + if matches!( + node, + ResourceCarrierNode::Optional { .. } + | ResourceCarrierNode::Result { .. } + | ResourceCarrierNode::Enum { .. } + ) => + { + if let Some(target) = target { + state.tags.insert(target.index(), probe); + } else if matches!( + self.function.blocks[operation.site.block.index() as usize] + .terminator + .kind, + TerminatorKind::Switch { .. } + ) { + state.switches.insert(operation.site.block.index(), probe); + } + } + ResourceCarrierObservation::State { state: selected } => { + let machine = match node { + ResourceCarrierNode::Machine { states, .. } => states, + ResourceCarrierNode::MachineState { machine, .. } => match &self + .carriers + .shape(*machine) + .ok_or("carrier state observation graph owner missing")? + .node + { + ResourceCarrierNode::Machine { states, .. } => states, + _ => { + return Err( + "carrier state observation owner is not a machine".into() + ); + } + }, + _ => { + return Err( + "carrier state observation has a different graph family".into(), + ); + } + }; + if *selected >= machine.len() { + return Err( + "carrier StateIs selector outside its exact machine graph".into() + ); + } + } + ResourceCarrierObservation::Ordinary { path, ty } => { + let shape = self + .carriers + .shape(self.projected_shape(state, *source, path)?) + .ok_or("carrier ordinary projection row missing")?; + if shape.ty != *ty || shape.contains_resource { + return Err( + "carrier observation cannot expose custody as ordinary payload" + .into(), + ); + } + } + _ => { + return Err( + "carrier observation role changes its exact typed graph family".into(), + ); + } + } + } + ResourceCarrierOperationRole::Project { + source, + destination, + } => { + self.loan_live(state, *source)?; + self.loan_live(state, *destination)?; + } + ResourceCarrierOperationRole::Extract { + source, + path, + destination, + } => { + if self.projected_shape(state, *source, path)? + != self.carriers.slots[destination.index()].shape + { + return Err("carrier extraction changes its exact selected graph node".into()); + } + self.extract(state, *source, path)?; + self.install(state, *destination, Origin::Extraction(operation.id))?; + } + ResourceCarrierOperationRole::ExtractOrdinary { + source, path, ty, .. + } => { + let shape = self + .carriers + .shape(self.projected_shape(state, *source, path)?) + .ok_or("carrier ordinary extraction graph row missing")?; + if shape.ty != *ty || shape.contains_resource { + return Err( + "carrier extraction cannot export custody through ordinary payload storage" + .into(), + ); + } + self.extract(state, *source, path)?; + } + ResourceCarrierOperationRole::AdaptSum { + source, + path, + destination, + loan, + .. + } => { + let projected = self + .carriers + .shape(self.projected_shape(state, *source, path)?) + .ok_or("carrier sum adapter graph row missing")?; + let expected = match &projected.node { + ResourceCarrierNode::Optional { payload } => match &self.carriers.shape(*payload).ok_or("carrier sum payload graph missing")?.node { ResourceCarrierNode::Resource { kind } => ResourceShape::Optional { kind: kind.clone() }, _ => return Err("carrier adapter Optional does not have a sole exact legacy Resource leaf".into()) }, + ResourceCarrierNode::Result { success, failure } => match &self.carriers.shape(*success).ok_or("carrier sum success graph missing")?.node { ResourceCarrierNode::Resource { kind } => ResourceShape::Result { kind: kind.clone(), failure: self.carriers.shape(*failure).ok_or("carrier sum failure graph missing")?.ty }, _ => return Err("carrier adapter Result does not have a sole exact legacy Resource leaf".into()) }, + _ => return Err("carrier adapter selected graph node is not an existing leaf sum".into()), + }; + if self.plan.slots[destination.index()].shape != expected { + return Err( + "carrier adapter changes its exact legacy sum kind/failure shape".into(), + ); + } + if loan.is_some() { + self.loan_live(state, *source)?; + } else { + self.extract(state, *source, path)?; + } + if !matches!( + state.leaves[destination.index()], + Leaf::Vacant | Leaf::Moved + ) { + return Err( + "carrier sum adapter overwrites an installed shell outside a binder reset" + .into(), + ); + } + state.leaves[destination.index()] = if loan.is_some() { + Leaf::Adapter { + operation: operation.id, + } + } else { + Leaf::Empty { + origin: Origin::Extraction(operation.id), + } + }; + } + ResourceCarrierOperationRole::Retire { source } => { + self.take(state, *source)?; + } + ResourceCarrierOperationRole::PublishReturn { source } => { + self.live(state, *source)?; + if !state.returned + || !state.frames.is_empty() + || self.plan.provisional_return().map(ResourceFrame::id) + != Some(self.carriers.slots[source.index()].frame) + { + return Err( + "carrier Return publication precedes exact successful Scope cleanup".into(), + ); + } + self.take_complete(state, *source)?; + } + ResourceCarrierOperationRole::RetireIteration { source, binders } => { + self.reset_iteration(state, *source, binders)? + } + ResourceCarrierOperationRole::BeginIteration { + source, + destination, + .. + } => { + let ResourceCarrierValue::Owned { slot } = source else { + return Err("carrier consuming iterator lacks its original owning root".into()); + }; + let value = self.take_complete(state, *slot)?; + if !matches!(state.slots[destination.index()], Slot::Vacant | Slot::Moved) { + return Err("carrier iterator root installs twice".into()); + } + state.slots[destination.index()] = value; + } + } + Ok(()) + } + fn reset_iteration( + &self, + state: &mut State, + source: ResourceCarrierSlotId, + binders: &[LocalId], + ) -> Result<(), String> { + self.live(state, source)?; + self.unleased(state, source)?; + state.selections.retain(|fact| { + fact.anchor.root != Root::Slot(source) + || !matches!(fact.selector, Selector::Iteration(_)) + }); + for binder in binders { + for slot in &self.carriers.slots { + if matches!(&slot.storage, ResourceSlotStorage::Local { header } if header.id == *binder) + { + self.unleased(state, slot.id)?; + if matches!(state.slots[slot.id.index()], Slot::Building { .. }) { + return Err( + "carrier iteration binder has an incomplete arrived prefix".into() + ); + } + state.slots[slot.id.index()] = Slot::Vacant; + } + } + for slot in &self.plan.slots { + if matches!(&slot.storage, ResourceSlotStorage::Local { header } if header.id == *binder) + { + if state.leaf_loans.iter().any(|loan| { + self.plan.loans[loan.index()].source == ResourceLoanSource::Owner(slot.id) + }) { + return Err("carrier iteration binder reset has a live sum lease".into()); + } + state.leaves[slot.id.index()] = Leaf::Vacant; + } + } + } + let Slot::Live { taken, .. } = &mut state.slots[source.index()] else { + return Err("carrier iterator reset lost its installed root".into()); + }; + // Only the exact canonical iterator cursor advances these dynamic paths. + // The physical runtime retains holes and generation checks for every index. + let iteration = self + .carriers + .iterations + .iter() + .find(|row| row.source == source && row.binders == binders) + .ok_or("carrier iteration reset has no exact original/current loop association")?; + taken.retain(|path| { + !path.iter().any(|part| match part { + ResourceCarrierProjectionPath::Element { index } + | ResourceCarrierProjectionPath::MapKey { index } + | ResourceCarrierProjectionPath::MapValue { index } => match index { + ResourceCarrierIndex::Local(cursor) => iteration.cursor == Some(*cursor), + ResourceCarrierIndex::CurrentIteration { header } => { + iteration.cursor.is_none() && iteration.header == *header + } + ResourceCarrierIndex::Constant(_) => false, + }, + _ => false, + }) + }); + Ok(()) + } + fn operation(&self, state: &mut State, operation: &ResourceOperation) -> Result<(), String> { + if let ResourceOperationRole::Carrier { operation: id } = operation.role { + return self.carrier( + state, + self.carriers + .operations + .get(id.index()) + .ok_or("carrier operation reference missing")?, + ); + } + if !matches!( + operation.role, + ResourceOperationRole::CompleteReturnAfterCleanup { .. } + ) { + self.frame(state, operation.frame)?; + } + state.retired_projection = None; + match &operation.role { + ResourceOperationRole::CreateAbsentSum { destination } => { + if !matches!(state.leaves[destination.index()], Leaf::Vacant | Leaf::Moved) { return Err("carrier absence sum installs into a live shell".into()); } + state.leaves[destination.index()] = Leaf::Empty { origin: Origin::LeafAbsence(operation.id) }; + } + ResourceOperationRole::Transfer { source, destination } => { + let exact = self.plan.slots[source.index()].shape == self.plan.slots[destination.index()].shape; + let initialized = match (&self.plan.slots[source.index()].shape, &state.leaves[source.index()]) { + (ResourceShape::Plain { .. }, Leaf::Occupied { .. }) => true, + (ResourceShape::Optional { .. } | ResourceShape::Result { .. }, Leaf::Empty { .. }) => true, + _ => false, + }; + if !exact || !initialized || !matches!(state.leaves[destination.index()], Leaf::Vacant | Leaf::Moved) { return Err("carrier leaf transfer lacks exact initialized source/vacant destination".into()); } + if state.leaf_loans.iter().any(|loan| self.plan.loans[loan.index()].source == ResourceLoanSource::Owner(*source)) { return Err("carrier leaf transfer uses a source with an exact live sum lease".into()); } + state.leaves[destination.index()] = std::mem::replace(&mut state.leaves[source.index()], Leaf::Moved); + } + ResourceOperationRole::BorrowSum { loan } => { + match self.plan.loans[loan.index()].source { + ResourceLoanSource::Owner(slot) if matches!(state.leaves[slot.index()], Leaf::Empty { .. }) => {} + ResourceLoanSource::CarrierSumProjection { operation } => { + let ResourceCarrierOperationRole::AdaptSum { source, destination, loan: Some(expected), .. } = &self.carriers.operations[operation.index()].role else { return Err("carrier sum Borrow lost its exact adapter row".into()); }; + if *expected != *loan || state.leaves[destination.index()] != (Leaf::Adapter { operation }) { return Err("carrier sum Borrow changes its exact adapted shell".into()); } + self.loan_live(state, *source)?; + } + _ => return Err("carrier sum Borrow has no exact zero-leaf source".into()), + } + if !state.leaf_loans.insert(*loan) { return Err("carrier sum Borrow repeats a live activation".into()); } + } + ResourceOperationRole::EndSumBorrow { loan } => { + if !state.leaf_loans.remove(loan) { return Err("carrier sum EndBorrow has no exact activation".into()); } + if let ResourceLoanSource::CarrierSumProjection { operation } = self.plan.loans[loan.index()].source { + let ResourceCarrierOperationRole::AdaptSum { destination, .. } = self.carriers.operations[operation.index()].role else { return Err("carrier sum retirement lost adapter destination".into()); }; + state.leaves[destination.index()] = Leaf::Moved; + } + } + ResourceOperationRole::InvokeSourceFunction { operands, result, .. } => { + for operand in operands { + match operand { + ResourceCallOperand::CarrierOwned { slot, .. } => { + if self.carriers.slots[slot.index()].frame != operation.frame { return Err("carrier Source owned actual bypassed its exact Operation holder".into()); } + self.take_complete(state, *slot)?; + } + ResourceCallOperand::CarrierBorrowed { loan, .. } => self.view_complete(state, *loan)?, + ResourceCallOperand::Owned { slot, .. } => { + let initialized = match (&self.plan.slots[slot.index()].shape, &state.leaves[slot.index()]) { + (ResourceShape::Plain { .. }, Leaf::Occupied { .. }) => true, + (ResourceShape::Optional { .. } | ResourceShape::Result { .. }, Leaf::Empty { .. }) => true, + _ => false, + }; + if !initialized { return Err("carrier Source leaf actual lacks an exact initialized producer".into()); } + state.leaves[slot.index()] = Leaf::Moved; + } + ResourceCallOperand::Borrowed { loan, .. } if state.leaf_loans.contains(loan) => {} + ResourceCallOperand::Borrowed { .. } => return Err("carrier Source uses an ended sum lease".into()), + ResourceCallOperand::Ordinary { .. } => {} + } + } + match result { + ResourceCallResult::Carrier { slot } => self.install(state, *slot, Origin::Source(operation.id))?, + ResourceCallResult::Owned { .. } => return Err("pending Resource carrier: live/conditional leaf Source result needs the dedicated leaf flow".into()), + ResourceCallResult::Ordinary { .. } => {} + } + } + ResourceOperationRole::Complete { outcome } => { + let frame = operation.frame; + if state.frames.last().copied() != Some(frame) { return Err("carrier completion changed its exact pending prefix".into()); } + if state.loans.iter().any(|loan| self.carriers.loans[loan.index()].frame == frame && !matches!(self.carriers.loans[loan.index()].source, ResourceCarrierLoanSource::IncomingViewFormal { .. })) || state.leaf_loans.iter().any(|loan| self.plan.loans[loan.index()].frame == frame && !matches!(self.plan.loans[loan.index()].source, ResourceLoanSource::IncomingViewFormal { .. })) { + return Err("carrier completion has an unretired exact owned loan".into()); + } + for slot in &self.carriers.slots { + if slot.frame == frame { + if matches!(state.slots[slot.id.index()], Slot::Building { .. }) && *outcome != ResourceCompletion::Abort { return Err("carrier normal completion has an incomplete acquired prefix".into()); } + state.slots[slot.id.index()] = if frame == ResourceFrameId(0) { Slot::Moved } else { Slot::Vacant }; + } + } + for slot in &self.plan.slots { if slot.frame == frame { state.leaves[slot.id.index()] = if frame == ResourceFrameId(0) { Leaf::Moved } else { Leaf::Vacant }; } } + state.frames.pop(); + if frame == ResourceFrameId(0) { state.returned = *outcome == ResourceCompletion::Return; } + } + ResourceOperationRole::CompleteReturnAfterCleanup { source } => { + if !state.returned || !state.frames.is_empty() || self.plan.provisional_return().map(ResourceFrame::id) != Some(self.plan.slots[source.index()].frame) || !matches!(state.leaves[source.index()], Leaf::Empty { .. }) { return Err("carrier leaf Return publication lacks exact successful Scope cleanup and empty provisional holder".into()); } + state.leaves[source.index()] = Leaf::Moved; + } + ResourceOperationRole::Drop { source, occupancy } => { + let initialized = matches!((&self.plan.slots[source.index()].shape, &state.leaves[source.index()], occupancy), + (ResourceShape::Plain { .. },Leaf::Occupied { .. },ResourceOccupancy::Occupied) + | (ResourceShape::Optional { .. } | ResourceShape::Result { .. },Leaf::Empty { .. },ResourceOccupancy::Empty)); + if !initialized || state.leaf_loans.iter().any(|loan| self.plan.loans[loan.index()].source == ResourceLoanSource::Owner(*source)) { return Err("carrier leaf discard lacks its initialized exact unleased endpoint".into()); } + state.leaves[source.index()] = Leaf::Moved; + } + _ => return Err("pending Resource carrier: this leaf/control operation needs its dedicated independent flow proof".into()), + } + Ok(()) + } + fn site(&self, state: &mut State, site: ResourceSite) -> Result<(), String> { + for operation in &self.plan.operations { + if operation.site == site { + if let ResourceOperationRole::Carrier { operation: id } = operation.role { + if self.carriers.operations[id.index()].edge.is_some() { + continue; + } + } + self.operation(state, operation)?; + } + } + state.retired_projection = None; + Ok(()) + } + fn edge(&self, state: &mut State, edge: ResourceCarrierEdge) -> Result<(), String> { + for operation in self.carriers.operations_on_edge(edge) { + self.carrier(state, operation)?; + } + Ok(()) + } + fn branch( + &self, + state: &mut State, + condition: &Expression, + selected: bool, + block: BlockId, + ) -> Result<(), String> { + if let E::Local(tag) = condition.kind + && let Some(probe) = state.tags.get(&tag.index()).cloned() + { + if condition.ty != TypeInterner::BOOL + || !matches!( + self.carriers + .shape(probe.shape) + .ok_or("carrier tag guard graph missing")? + .node, + ResourceCarrierNode::Optional { .. } | ResourceCarrierNode::Result { .. } + ) + { + return Err( + "carrier selecting Branch differs from its exact typed sum Tag destination" + .into(), + ); + } + self.select(state, &probe, Selector::Sum(selected)); + } + if selected && let E::StateIs { .. } = condition.kind { + let observations = self + .carriers + .operations_for_expression(condition) + .filter_map(|operation| match operation.role { + ResourceCarrierOperationRole::Observe { + source, + observation: ResourceCarrierObservation::State { state }, + .. + } if operation.site.block == block => Some((source, state)), + _ => None, + }) + .collect::>(); + let [(loan, state_id)] = observations.as_slice() else { + return Err( + "carrier StateIs Branch has no unique exact current state observation".into(), + ); + }; + let probe = self.probe(state, *loan)?; + self.select(state, &probe, Selector::State(*state_id)); + } + if selected + && let Some(iteration) = self + .carriers + .iterations + .iter() + .find(|row| row.header == block && row.cursor.is_some()) + { + let anchor = match self.live(state, iteration.source)? { + Slot::Live { origin, .. } => Anchor { + root: Root::Slot(iteration.source), + origin: *origin, + }, + _ => return Err("carrier iterator selecting edge lost its installed root".into()), + }; + let probe = Probe { + anchor, + path: Vec::new(), + shape: self.carriers.slots[iteration.source.index()].shape, + }; + self.select(state, &probe, Selector::Iteration(block)); + } + Ok(()) + } + fn select_switch( + &self, + state: &mut State, + source: BlockId, + variant: VariantId, + ) -> Result<(), String> { + let probe = + state.switches.get(&source.index()).cloned().ok_or( + "carrier Switch selected an edge without its exact reached Tag observation", + )?; + let ResourceCarrierNode::Enum { variants, .. } = &self + .carriers + .shape(probe.shape) + .ok_or("carrier Switch graph row missing")? + .node + else { + return Err("carrier Switch observation is not an enum".into()); + }; + if variant.index() as usize >= variants.len() { + return Err("carrier Switch edge selected a foreign graph variant".into()); + } + self.select(state, &probe, Selector::Variant(variant.index() as usize)); + Ok(()) + } + fn select_iteration(&self, state: &mut State, header: BlockId) -> Result<(), String> { + let row = self + .carriers + .iterations + .iter() + .find(|row| row.header == header && row.cursor.is_none()) + .ok_or("carrier raw iterator edge has no exact Source/current association")?; + let anchor = match self.live(state, row.source)? { + Slot::Live { origin, .. } => Anchor { + root: Root::Slot(row.source), + origin: *origin, + }, + _ => return Err("carrier raw iterator selecting edge lost its installed root".into()), + }; + let probe = Probe { + anchor, + path: Vec::new(), + shape: self.carriers.slots[row.source.index()].shape, + }; + self.select(state, &probe, Selector::Iteration(header)); + Ok(()) + } + fn normalized_entry(&self, state: &mut State, block: BlockId) -> Result<(), String> { + for iteration in &self.carriers.iterations { + if iteration.header != block { + continue; + } + if iteration.cursor.is_none() + && !matches!(state.slots[iteration.source.index()], Slot::Live { .. }) + { + self.site( + state, + ResourceSite { + function: self.function.id, + block, + position: ResourcePosition::Terminator, + }, + )?; + } + self.reset_iteration(state, iteration.source, &iteration.binders)?; + } + Ok(()) + } +} + +pub(super) fn validate( + function: &Function, + _types: &TypeInterner, + plan: &ResourceFunctionPlan, + carriers: &ResourceCarrierFunctionPlan, +) -> Result<(), String> { + let analysis = Analysis { + function, + plan, + carriers, + }; + let mut initial = State { + slots: vec![Slot::Vacant; carriers.slots.len()], + leaves: vec![Leaf::Vacant; plan.slots.len()], + loans: BTreeSet::new(), + leaf_loans: BTreeSet::new(), + frames: vec![ResourceFrameId(0)], + returned: false, + selections: Vec::new(), + tags: BTreeMap::new(), + switches: BTreeMap::new(), + retired_projection: None, + }; + for (parameter, param) in function.params.iter().enumerate() { + for slot in &carriers.slots { + if matches!(&slot.storage, ResourceSlotStorage::Local { header } if header.id == param.local) + { + analysis.install(&mut initial, slot.id, Origin::Formal(parameter))?; + } + } + if param.mode == ParamMode::Owned { + for slot in &plan.slots { + if matches!(&slot.storage, ResourceSlotStorage::Local { header } if header.id == param.local) + && matches!(slot.shape, ResourceShape::Plain { .. }) + { + initial.leaves[slot.id.index()] = Leaf::Occupied { + origin: Origin::Formal(parameter), + }; + } + } + } + } + for loan in &carriers.loans { + if matches!( + loan.source, + ResourceCarrierLoanSource::IncomingViewFormal { .. } + ) { + initial.loans.insert(loan.id); + } + } + for loan in &plan.loans { + if matches!(loan.source, ResourceLoanSource::IncomingViewFormal { .. }) { + initial.leaf_loans.insert(loan.id); + } + } + analysis.normalized_entry(&mut initial, function.entry)?; + let mut states = vec![None; function.blocks.len()]; + states[usize::try_from(function.entry.index()) + .map_err(|_| "carrier entry index is not representable")?] = Some(initial); + let mut queue = VecDeque::from([function.entry]); + let limit = function + .blocks + .len() + .saturating_mul( + carriers + .slots + .len() + .saturating_add(carriers.loans.len()) + .saturating_add(1), + ) + .saturating_mul(8) + .saturating_add(1); + let mut steps = 0usize; + while let Some(block_id) = queue.pop_front() { + steps = steps + .checked_add(1) + .ok_or("carrier fixed point step overflow")?; + if steps > limit { + return Err( + "pending Resource carrier: exact custody fixed point exceeds its finite bound" + .into(), + ); + } + let index = usize::try_from(block_id.index()) + .map_err(|_| "carrier block index is not representable")?; + let mut state = states[index] + .as_ref() + .ok_or("carrier reachable block has no exact incoming state")? + .clone(); + let block = &function.blocks[index]; + for statement in 0..block.statements.len() { + analysis.site( + &mut state, + ResourceSite { + function: function.id, + block: block_id, + position: ResourcePosition::Statement(statement), + }, + )?; + } + let raw_iterator = carriers + .iterations + .iter() + .any(|row| row.header == block_id && row.cursor.is_none()); + if !raw_iterator { + analysis.site( + &mut state, + ResourceSite { + function: function.id, + block: block_id, + position: ResourcePosition::Terminator, + }, + )?; + } + let mut outputs = Vec::new(); + match &block.terminator.kind { + TerminatorKind::Goto(target) => outputs.push((*target, state)), + TerminatorKind::Branch { condition, then_block, else_block } => { + let mut yes = state.clone(); analysis.branch(&mut yes, condition, true, block_id)?; + analysis.branch(&mut state, condition, false, block_id)?; + outputs.push((*then_block, yes)); outputs.push((*else_block, state)); + } + TerminatorKind::ForEach { body, exit, .. } => { + let mut selected = state.clone(); + if carriers.iterations.iter().any(|row| row.header == block_id) { + analysis.select_iteration(&mut selected, block_id)?; + analysis.edge(&mut selected, ResourceCarrierEdge::IterationBody { source: block_id, target: *body })?; + } + outputs.push((*body, selected)); outputs.push((*exit, state)); + } + TerminatorKind::Switch { variants, otherwise, .. } => { + for (variant, target, _) in variants { + let mut selected = state.clone(); + if carriers.operations_on_edge(ResourceCarrierEdge::SwitchVariant { source: block_id, variant: *variant, target: *target }).next().is_some() { + analysis.select_switch(&mut selected, block_id, *variant)?; + analysis.edge(&mut selected, ResourceCarrierEdge::SwitchVariant { source: block_id, variant: *variant, target: *target })?; + } + outputs.push((*target, selected)); + } + if let Some(target) = otherwise { analysis.edge(&mut state, ResourceCarrierEdge::SwitchOtherwise { source: block_id, target: *target })?; outputs.push((*target, state)); } + } + TerminatorKind::Return(_) | TerminatorKind::Unreachable => {} + _ => return Err("pending Resource carrier: this terminator requires its independent custody edge proof".into()), + } + for (target, mut output) in outputs { + analysis.normalized_entry(&mut output, target)?; + let index = usize::try_from(target.index()) + .map_err(|_| "carrier successor index is not representable")?; + if let Some(current) = &mut states[index] { + // Selecting facts can be forgotten at joins. Custody, generation, + // arrived prefix and exact active lease sets must agree independently. + let mut merged = current.clone(); + merged + .selections + .retain(|fact| output.selections.contains(fact)); + merged + .tags + .retain(|key, probe| output.tags.get(key) == Some(probe)); + merged + .switches + .retain(|key, probe| output.switches.get(key) == Some(probe)); + output.selections = merged.selections.clone(); + output.tags = merged.tags.clone(); + output.switches = merged.switches.clone(); + if merged != output { + return Err("pending Resource carrier: CFG join changed exact root generation, selected paths, operation prefix, or loans".into()); + } + if *current != merged { + *current = merged; + queue.push_back(target); + } + } else { + states[index] = Some(output); + queue.push_back(target); + } + } + } + Ok(()) +} diff --git a/crates/jett_mir/src/resource_ownership/carriers/graph.rs b/crates/jett_mir/src/resource_ownership/carriers/graph.rs new file mode 100644 index 00000000..fc29e969 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/carriers/graph.rs @@ -0,0 +1,206 @@ +//! Exact checked type graph; recursion uses canonical TypeId backreferences. +use super::*; + +fn id(types: &TypeInterner, ty: TypeId) -> Result { + let index = + usize::try_from(ty.index()).map_err(|_| "carrier TypeId index is not representable")?; + if index >= types.len() { + return Err("carrier child type is outside its checked interner".into()); + } + Ok(ResourceCarrierShapeId(index)) +} +fn fields( + types: &TypeInterner, + fields: &[(String, TypeId)], +) -> Result, String> { + fields + .iter() + .enumerate() + .map(|(ordinal, (name, ty))| { + Ok(ResourceCarrierField { + ordinal, + name: name.clone(), + shape: id(types, *ty)?, + }) + }) + .collect() +} +fn arguments(types: &TypeInterner, ty: TypeId) -> Result, String> { + types + .nominal_type_arguments(ty) + .iter() + .map(|ty| id(types, *ty)) + .collect() +} +fn machine_type( + types: &TypeInterner, + owner: jett_types::MachineId, +) -> Result { + let ty = types + .type_ids() + .find(|ty| matches!(types.resolve(*ty), Type::Machine(id) if *id == owner)) + .ok_or("qualified carrier machine lost its unqualified checked owner")?; + id(types, ty) +} +fn node( + manifest: &hir::ResourceManifest, + types: &TypeInterner, + ty: TypeId, +) -> Result { + Ok(match types.resolve(ty) { + Type::Resource(_) => ResourceCarrierNode::Resource { + kind: manifest + .kind_for_type(ty) + .ok_or("carrier Resource node has no exact manifest kind")?, + }, + Type::Optional(payload) => ResourceCarrierNode::Optional { + payload: id(types, *payload)?, + }, + Type::Result(success, failure) => ResourceCarrierNode::Result { + success: id(types, *success)?, + failure: id(types, *failure)?, + }, + Type::List(element) => ResourceCarrierNode::List { + element: id(types, *element)?, + }, + Type::Map(key, value) => ResourceCarrierNode::Map { + key: id(types, *key)?, + value: id(types, *value)?, + }, + Type::Struct(owner) => ResourceCarrierNode::Struct { + owner: *owner, + arguments: arguments(types, ty)?, + fields: fields(types, &types.resolve_struct(*owner).fields)?, + }, + Type::Enum(owner) => ResourceCarrierNode::Enum { + owner: *owner, + arguments: arguments(types, ty)?, + variants: types + .resolve_enum(*owner) + .variants + .iter() + .enumerate() + .map(|(ordinal, variant)| { + Ok(ResourceCarrierVariant { + ordinal, + name: variant.name.clone(), + discriminant: variant.discriminant, + fields: fields(types, &variant.fields)?, + }) + }) + .collect::>()?, + }, + Type::Machine(owner) => { + let definition = types.resolve_machine(*owner); + ResourceCarrierNode::Machine { + owner: *owner, + states: definition + .states + .iter() + .enumerate() + .map(|(ordinal, state)| { + Ok(ResourceCarrierState { + ordinal, + name: state.name.clone(), + fields: fields(types, &state.fields)?, + }) + }) + .collect::>()?, + transitions: definition + .transitions + .iter() + .map(|edge| { + Ok::<_, String>(( + usize::try_from(edge.from.index()) + .map_err(|_| "carrier state index is not representable")?, + usize::try_from(edge.to.index()) + .map_err(|_| "carrier state index is not representable")?, + )) + }) + .collect::>()?, + } + } + Type::MachineState { machine, state } => ResourceCarrierNode::MachineState { + machine: machine_type(types, *machine)?, + state: usize::try_from(state.index()) + .map_err(|_| "carrier state index is not representable")?, + }, + Type::Refinement { name, base } => ResourceCarrierNode::Refinement { + name: name.clone(), + base: id(types, *base)?, + }, + // Ordinary graph terminals are metadata only. In particular, descriptor + // signatures never become leaf owners by mentioning Resource types. + Type::Function { .. } => ResourceCarrierNode::Ordinary, + _ if !resource_type_pending(types, ty) => ResourceCarrierNode::Ordinary, + _ => { + return Err( + "pending Resource carrier: this checked type family needs its dedicated graph node" + .into(), + ); + } + }) +} +pub(super) fn capture( + source: &hir::ResourceSourceArchive, + manifest: &hir::ResourceManifest, + types: &TypeInterner, +) -> Result>, String> { + source.validate_types(types)?; + manifest.validate(types)?; + if source.manifest() != Some(manifest) { + return Err("carrier shape graph belongs to another checked manifest".into()); + } + let mut rows = Vec::with_capacity(types.len()); + for ty in types.type_ids() { + let shape = id(types, ty)?; + if shape.index() != rows.len() { + return Err("carrier type graph lost canonical dense ordinals".into()); + } + rows.push(ResourceCarrierShape { + id: shape, + ty, + node: node(manifest, types, ty)?, + contains_resource: !matches!(types.resolve(ty), Type::Function { .. }) + && resource_type_pending(types, ty), + refinement_predicate: predicate(source, types, ty)?, + }); + } + Ok(Arc::new(rows)) +} + +fn predicate( + source: &hir::ResourceSourceArchive, + types: &TypeInterner, + ty: TypeId, +) -> Result, String> { + let Type::Refinement { name, base } = types.resolve(ty) else { + return Ok(None); + }; + let mut input = *base; + while let Type::Refinement { base, .. } = types.resolve(input) { + input = *base; + } + if let Type::Secret(inner) = types.resolve(input) { + input = *inner; + } + let mut candidates = source.functions().iter().filter(|function| { + hir::refinement_predicate_declaration_matches(&function.identity.declaration, name) + && function.source_definition.is_some() + && function.capture_count == 0 + && function.params.len() == 1 + && function.params[0].ty == input + && function.params[0].mode == ParamMode::Owned + && !function.params[0].mutable + && function.return_type == TypeInterner::BOOL + }); + let selected = candidates + .next() + .ok_or("carrier refinement graph lost its original checked predicate association")?; + if candidates.next().is_some() { + return Err( + "carrier refinement graph has an ambiguous original predicate association".into(), + ); + } + Ok(Some(selected.clone())) +} diff --git a/crates/jett_mir/src/resource_ownership/carriers/iterations.rs b/crates/jett_mir/src/resource_ownership/carriers/iterations.rs new file mode 100644 index 00000000..e1ae4297 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/carriers/iterations.rs @@ -0,0 +1,716 @@ +//! Private association of canonical prepared iterators with their original For. +//! These rows describe current sites; they issue no owner, lease or Source permit. +use super::*; +use hir::{BinaryOp, StatementKind as H}; + +struct SourceFor<'a> { + key: LocalId, + value: Option, + by_view: bool, + iterable: &'a Expression, + span: Span, +} + +#[derive(Default)] +struct Inventory<'a> { + loops: Vec>, + binders: BTreeSet, +} + +impl<'a> Inventory<'a> { + fn block(&mut self, block: &'a hir::Block) -> Result<(), String> { + for statement in &block.statements { + match &statement.kind { + H::For { + key, + value, + by_view, + iterable, + body, + } => { + for binder in std::iter::once(*key).chain(value.iter().copied()) { + if !self.binders.insert(binder.index()) { + return Err( + "carrier iterator duplicated an original Source binder".into() + ); + } + } + self.loops.push(SourceFor { + key: *key, + value: *value, + by_view: *by_view, + iterable, + span: statement.span, + }); + self.expression(iterable)?; + self.block(body)?; + } + H::If { + condition, + then_block, + else_block, + } => { + self.expression(condition)?; + self.block(then_block)?; + if let Some(block) = else_block { + self.block(block)?; + } + } + H::While { condition, body } => { + self.expression(condition)?; + self.block(body)?; + } + H::Match { scrutinee, arms } => { + self.expression(scrutinee)?; + for arm in arms { + self.block(&arm.body)?; + } + } + H::Scope(block) => self.block(block)?, + H::ReflectedTypeDispatch { type_info, arms } => { + self.expression(type_info)?; + for arm in arms { + self.block(&arm.body)?; + } + } + H::Let { value, .. } + | H::Expression(value) + | H::HandleDefault(value) + | H::Respond(value) => self.expression(value)?, + H::Assign { target, value } => { + self.expression(target)?; + self.expression(value)?; + } + H::Return(value) => { + if let Some(value) = value { + self.expression(value)?; + } + } + H::Assert { condition, message } => { + self.expression(condition)?; + if let Some(message) = message { + self.expression(message)?; + } + } + H::Breakpoint { condition, .. } => { + if let Some(condition) = condition { + self.expression(condition)?; + } + } + H::Break | H::Continue | H::Trace(_) => {} + } + } + Ok(()) + } + + fn expression(&mut self, expression: &'a Expression) -> Result<(), String> { + match &expression.kind { + E::Handle { + target, failure, .. + } => { + self.expression(target)?; + self.block(failure)?; + } + E::Binary { left, right, .. } => { + self.expression(left)?; + self.expression(right)?; + } + E::Unary { value, .. } + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::Declassify(value) + | E::Coarsen(value) + | E::RefinementValidated(value) + | E::InterfaceType(value) + | E::RuntimeFailureMessage(value) + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::View(value) + | E::Clone(value) + | E::Comptime { value, .. } + | E::InterfaceCoerce { value, .. } + | E::FunctionAdapter { value, .. } + | E::StateIs { value, .. } => self.expression(value)?, + E::Call { args, .. } + | E::ResourceInvoke { args, .. } + | E::Intrinsic { args, .. } + | E::ActorSpawn { args, .. } => { + for value in args { + self.expression(value)?; + } + } + E::IndirectCall { callee, args, .. } => { + self.expression(callee)?; + for value in args { + self.expression(value)?; + } + } + E::ActorMessage { actor, args, .. } => { + self.expression(actor)?; + for value in args { + self.expression(value)?; + } + } + E::StructConstruct { fields, .. } | E::BitfieldConstruct { fields, .. } => { + for value in fields { + self.expression(value)?; + } + } + E::EnumConstruct { payloads, .. } | E::MachineConstruct { payloads, .. } => { + for value in payloads { + self.expression(value)?; + } + } + E::MachineTransition { + source, payloads, .. + } => { + self.expression(source)?; + for value in payloads { + self.expression(value)?; + } + } + E::ListConstruct { elements } => { + for value in elements { + self.expression(value)?; + } + } + E::MapConstruct { entries } => { + for entry in entries { + self.expression(&entry.key)?; + self.expression(&entry.value)?; + } + } + E::StringInterpolation(parts) => { + for part in parts { + if let hir::StringSegment::Value(value) = part { + self.expression(value)?; + } + } + } + E::Field { base, .. } => self.expression(base)?, + // A separate callable body belongs to its own function/local archive. + E::InlineFunction { .. } + | E::Int(_) + | E::Float(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::Local(_) + | E::Constant { .. } + | E::FunctionRef(_) + | E::ResourceHookValue { .. } + | E::ClosureRef { .. } + | E::OptionalNone + | E::RuntimeFailure(_) + | E::PropertyCaseContext(_) => {} + } + Ok(()) + } +} + +fn dense_block(function: &Function, id: BlockId) -> Result<&BasicBlock, String> { + let index = + usize::try_from(id.index()).map_err(|_| "carrier iterator block is not representable")?; + function + .blocks + .get(index) + .filter(|block| block.id == id) + .ok_or_else(|| "carrier iterator lost its dense current block".into()) +} + +fn local_expression(value: &Expression, local: LocalId, ty: TypeId, span: Span) -> bool { + value.ty == ty + && value.span == span + && matches!(&value.kind, E::Local(current) if *current == local) +} + +fn temporary(function: &Function, id: LocalId, ty: TypeId, span: Span) -> Result<(), String> { + let local = function + .local(id) + .ok_or("carrier iterator temporary lost its dense header")?; + if local.ty != ty + || local.debug_ty != ty + || local.span != span + || !local.mutable + || local.view_source.is_some() + || function.is_view_local(id) + || function.parameter_for_local(id).is_some() + { + return Err("carrier iterator changed its exact temporary declaration".into()); + } + Ok(()) +} + +fn source_expression( + witness: &ResourceLoweringWitness, + original: &Expression, + current: &Expression, +) -> bool { + if crate::breakpoint_regions::expressions_equal(original, current) { + return true; + } + let direct = witness + .calls + .iter() + .filter(|row| { + crate::breakpoint_regions::expressions_equal(&row.original, original) + && crate::breakpoint_regions::expressions_equal(&row.current, current) + }) + .count(); + let normalized = if let E::Local(local) = ¤t.kind { + witness + .regions + .iter() + .filter(|row| { + row.output() == Some(*local) + && crate::breakpoint_regions::expressions_equal(row.original(), original) + && original.ty == current.ty + }) + .count() + } else { + 0 + }; + direct + normalized == 1 +} + +fn binding( + function: &Function, + original: &hir::Function, + id: LocalId, + ty: TypeId, +) -> Result<(), String> { + let index = + usize::try_from(id.index()).map_err(|_| "carrier iterator binder is not representable")?; + let archived = original + .locals + .get(index) + .filter(|local| local.id == id) + .ok_or("carrier iterator binder has no exact original Source header")?; + let current = function + .local(id) + .ok_or("carrier iterator binder lost its current header")?; + if archived.ty != ty + || current.ty != ty + || current.debug_ty != ty + || archived.mutable + || current.mutable + || archived.view_source.is_some() + || current.view_source.is_some() + || function.is_view_local(id) + || function.parameter_for_local(id).is_some() + { + return Err("carrier iterator changed its exact original owning binder".into()); + } + Ok(()) +} + +fn source_slot( + carriers: &ResourceCarrierFunctionPlan, + header: &Local, +) -> Result { + let matches = carriers.slots().iter().filter(|slot| { + matches!(slot.storage(), ResourceSlotStorage::Local { header: current } if current == header) + && carriers.shape(slot.shape()).is_some_and(|shape| { + shape.ty() == header.ty && shape.contains_resource() + }) + }).collect::>(); + let [slot] = matches.as_slice() else { + return Err("carrier iterator has no unique exact local source slot".into()); + }; + Ok(slot.id()) +} + +fn extraction( + statement: &Statement, + source: LocalId, + cursor: LocalId, + target: LocalId, + part: SequencePart, + span: Span, +) -> bool { + statement.span == span + && matches!(&statement.kind, StatementKind::SequenceGet { + consume: true, + source: SequenceSource::Local(current_source), + index, + target: current_target, + part: current_part, + } if *current_source == source && *index == cursor && *current_target == target && *current_part == part) +} + +fn advance(statement: &Statement, cursor: LocalId, span: Span) -> bool { + let StatementKind::Assign { target, value } = &statement.kind else { + return false; + }; + let E::Binary { + left, + op: BinaryOp::Add, + right, + } = &value.kind + else { + return false; + }; + statement.span == span + && value.ty == TypeInterner::INT64 + && value.span == span + && local_expression(target, cursor, TypeInterner::INT64, span) + && local_expression(left, cursor, TypeInterner::INT64, span) + && right.ty == TypeInterner::INT64 + && right.span == span + && matches!(&right.kind, E::Int(1)) +} + +fn preheader( + function: &Function, + cfg: &ControlFlowGraph, + header: BlockId, +) -> Result { + let mut outside = BTreeSet::new(); + let mut pending = vec![function.entry]; + while let Some(block) = pending.pop() { + if block == header || !outside.insert(block.index()) { + continue; + } + pending.extend_from_slice(cfg.successors(block)); + } + let candidates = cfg + .predecessors(header) + .iter() + .copied() + .filter(|block| outside.contains(&block.index())) + .collect::>(); + let [preheader] = candidates.as_slice() else { + return Err("carrier iterator lost its unique exact preheader".into()); + }; + if !matches!(&dense_block(function, *preheader)?.terminator.kind, TerminatorKind::Goto(target) if *target == header) + { + return Err("carrier iterator preheader bypasses its exact header".into()); + } + Ok(*preheader) +} + +struct Prepared { + row: ResourceCarrierIteration, + preheader: BlockId, + length_site: usize, +} + +fn prepared( + function: &Function, + types: &TypeInterner, + carriers: &ResourceCarrierFunctionPlan, + witness: &ResourceLoweringWitness, + originals: &[SourceFor<'_>], + cfg: &ControlFlowGraph, + header: &BasicBlock, +) -> Result, String> { + let TerminatorKind::Branch { + condition, + then_block: body, + else_block: exit, + } = &header.terminator.kind + else { + return Ok(None); + }; + let body_block = dense_block(function, *body)?; + let Some(first) = body_block.statements.first() else { + return Ok(None); + }; + let StatementKind::SequenceGet { + source, + index: cursor, + target: key, + .. + } = &first.kind + else { + return Ok(None); + }; + let Some(ty) = source.ty(function) else { + return Err("carrier iterator lost its source type".into()); + }; + if ty.index() as usize >= types.len() { + return Err("carrier iterator source type is out of range".into()); + } + if !carrier_type(types, ty) { + return Ok(None); + } + let SequenceSource::Local(source) = source else { + return Err( + "pending Resource carrier: projected iteration needs its bounded parent lease".into(), + ); + }; + let source_header = function + .local(*source) + .ok_or("carrier iterator source has no dense local header")?; + let (key_type, value_type, key_part) = match types.resolve(ty) { + Type::List(element) => (*element, None, SequencePart::Element), + Type::Map(key, value) => (*key, Some(*value), SequencePart::Key), + _ => return Err("carrier iterator source is not its exact list or map".into()), + }; + let value = if value_type.is_some() { + let second = body_block + .statements + .get(1) + .ok_or("carrier map iterator lost its value extraction")?; + let StatementKind::SequenceGet { target, .. } = &second.kind else { + return Err("carrier map iterator reordered its exact value extraction".into()); + }; + Some(*target) + } else { + None + }; + let candidates = originals + .iter() + .filter(|row| row.key == *key && row.value == value && row.iterable.ty == ty) + .collect::>(); + let [original] = candidates.as_slice() else { + return Err("carrier iterator has no unique exact original Source For tuple".into()); + }; + if original.by_view + || matches!(&original.iterable.kind, E::View(_) | E::Field { .. }) + || matches!(&original.iterable.kind, E::Local(local) if function.is_view_local(*local)) + { + return Err( + "pending Resource carrier: borrowed iteration needs its exact Source lease".into(), + ); + } + binding(function, &witness.original, *key, key_type)?; + if let Some((binding_id, binding_type)) = value.zip(value_type) { + binding(function, &witness.original, binding_id, binding_type)?; + } + let span = original.iterable.span; + if key_type == TypeInterner::NEVER || value_type == Some(TypeInterner::NEVER) { + return Err( + "pending Resource carrier: uninhabited iteration needs its exact transport".into(), + ); + } + let E::Binary { + left, + op: BinaryOp::Less, + right, + } = &condition.kind + else { + return Err("carrier iterator changed its exact selecting comparison".into()); + }; + let E::Local(length) = &right.kind else { + return Err("carrier iterator lost its exact length local".into()); + }; + if condition.ty != TypeInterner::BOOL + || condition.span != span + || header.terminator.span != original.span + || !local_expression(left, *cursor, TypeInterner::INT64, span) + || !local_expression(right, *length, TypeInterner::INT64, span) + || *body == header.id + || *exit == header.id + || body == exit + || *cursor == *length + || *cursor == *source + || *length == *source + { + return Err("carrier iterator changed its exact selecting endpoints".into()); + } + temporary(function, *cursor, TypeInterner::INT64, span)?; + temporary(function, *length, TypeInterner::INT64, span)?; + temporary(function, *source, ty, span)?; + if !extraction(first, *source, *cursor, *key, key_part, span) { + return Err( + "carrier iterator changed its exact consuming key or element extraction".into(), + ); + } + let mut binders = vec![*key]; + if let Some(value) = value { + if value == *key { + return Err("carrier map iterator duplicated its original binder".into()); + } + let second = body_block + .statements + .get(1) + .ok_or("carrier map extraction disappeared")?; + if !extraction(second, *source, *cursor, value, SequencePart::Value, span) { + return Err("carrier map iterator changed Key then Value extraction".into()); + } + binders.push(value); + } + if binders + .iter() + .any(|binder| [*source, *cursor, *length].contains(binder)) + { + return Err("carrier iterator aliased its source or cursor with a Source binder".into()); + } + let increment = body_block + .statements + .get(binders.len()) + .ok_or("carrier iterator advance is absent")?; + if !advance(increment, *cursor, span) { + return Err("carrier iterator changed its exact cursor advance".into()); + } + if cfg + .predecessors(*body) + .iter() + .any(|predecessor| *predecessor != header.id) + { + return Err("carrier iterator body has an entry outside its exact selecting header".into()); + } + let preheader_id = preheader(function, cfg, header.id)?; + let preheader = dense_block(function, preheader_id)?; + let start = preheader + .statements + .len() + .checked_sub(3) + .ok_or("carrier iterator initialization is incomplete")?; + let [source_init, zero, length_init] = &preheader.statements[start..] else { + return Err("carrier iterator initialization changed its exact arity".into()); + }; + let StatementKind::Let { + local: source_local, + value: source_value, + } = &source_init.kind + else { + return Err("carrier iterator lost its owning source initializer".into()); + }; + if source_init.span != span + || *source_local != *source + || source_value.ty != ty + || !source_expression(witness, original.iterable, source_value) + || zero.span != span + || !matches!(&zero.kind, StatementKind::Let { local, value } if *local == *cursor + && value.ty == TypeInterner::INT64 && value.span == span && matches!(&value.kind, E::Int(0))) + || length_init.span != span + || !matches!(&length_init.kind, StatementKind::SequenceLength { + source: SequenceSource::Local(current_source), target + } if *current_source == *source && *target == *length) + { + return Err( + "carrier iterator changed its exact Source, zero or length initialization".into(), + ); + } + Ok(Some(Prepared { + row: ResourceCarrierIteration { + header: header.id, + body: *body, + exit: *exit, + source: source_slot(carriers, source_header)?, + binders, + cursor: Some(*cursor), + }, + preheader: preheader_id, + length_site: start + 2, + })) +} + +pub(super) fn capture( + function: &Function, + types: &TypeInterner, + carriers: &ResourceCarrierFunctionPlan, +) -> Result, String> { + let witness = function + .resource_lowering + .as_ref() + .ok_or("carrier iterator has no exact Source witness")?; + witness.source.validate_types(types)?; + witness.current(function)?; + let mut inventory = Inventory::default(); + inventory.block(&witness.original.body)?; + let cfg = ControlFlowGraph::analyze(function) + .map_err(|errors| format!("carrier iterator CFG is malformed: {errors:?}"))?; + let mut rows = Vec::new(); + let mut binders = BTreeSet::new(); + let mut sources = BTreeSet::new(); + let mut cursors = BTreeSet::new(); + let mut lengths = BTreeSet::new(); + let mut gets = BTreeSet::new(); + // Raw For rows remain the planner's operation metadata. They cannot share + // their original binders with a second prepared loop. + for block in &function.blocks { + if let TerminatorKind::ForEach { + key, + value, + iterable, + .. + } = &block.terminator.kind + && (iterable.ty.index() as usize) < types.len() + && carrier_type(types, iterable.ty) + { + for binder in std::iter::once(*key).chain(value.iter().copied()) { + if !binders.insert(binder.index()) { + return Err("carrier raw iterator reused its Source binder".into()); + } + } + } + } + for block in &function.blocks { + let Some(prepared) = prepared( + function, + types, + carriers, + witness, + &inventory.loops, + &cfg, + block, + )? + else { + continue; + }; + if !sources.insert(prepared.row.source.index()) + || !cursors.insert( + prepared + .row + .cursor + .ok_or("prepared carrier iterator has no cursor")? + .index(), + ) + || !lengths.insert((prepared.preheader.index(), prepared.length_site)) + { + return Err( + "carrier prepared iterator duplicated its source, cursor or initialization".into(), + ); + } + for (ordinal, binder) in prepared.row.binders.iter().enumerate() { + if !binders.insert(binder.index()) || !gets.insert((prepared.row.body.index(), ordinal)) + { + return Err( + "carrier prepared iterator reused its Source binder or extraction".into(), + ); + } + } + rows.push(prepared.row); + } + for block in &function.blocks { + for (position, statement) in block.statements.iter().enumerate() { + let source = match &statement.kind { + StatementKind::SequenceLength { source, .. } + | StatementKind::SequenceGet { source, .. } + | StatementKind::IterationBorrow { source, .. } => source, + _ => continue, + }; + let ty = source + .ty(function) + .ok_or("carrier sequence statement lost its exact source type")?; + if ty.index() as usize >= types.len() { + return Err("carrier sequence source type is out of range".into()); + } + if !carrier_type(types, ty) { + continue; + } + let site = (block.id.index(), position); + match &statement.kind { + StatementKind::SequenceLength { .. } if lengths.contains(&site) => {} + StatementKind::SequenceGet { .. } if gets.contains(&site) => {} + StatementKind::IterationBorrow { .. } => { + return Err("pending Resource carrier: borrowed iteration needs its exact current lease".into()); + } + _ => { + return Err( + "carrier sequence statement is outside its exact prepared Source iterator" + .into(), + ); + } + } + } + } + Ok(rows) +} diff --git a/crates/jett_mir/src/resource_ownership/carriers/planner.rs b/crates/jett_mir/src/resource_ownership/carriers/planner.rs new file mode 100644 index 00000000..4b36b7fb --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/carriers/planner.rs @@ -0,0 +1,1726 @@ +//! Fresh projection of the complete authenticated current graph. The original +//! checker owns Source move legality; runtime generations own physical custody. +use super::*; +use jett_typecheck::{ + CheckedCalleeAccess as Access, CheckedCallerEffect as Effect, CheckedCallerSyntax as Syntax, +}; + +struct Planner<'p> { + program: &'p Program, + function: &'p Function, + types: &'p TypeInterner, + plan: ResourceFunctionPlan, + carriers: ResourceCarrierFunctionPlan, + carrier_locals: Vec>, + leaf_locals: Vec>, + site: ResourceSite, + expression: Option, + ordinal: usize, + frame: ResourceFrameId, + call_frames: BTreeMap, + iteration_slots: BTreeMap, + edge: Option, + ended_loans: BTreeSet, + constructor_frames: BTreeMap, + constructor_slots: BTreeMap, + incoming_frames: Vec<(ResourceSite, ResourceFrameId)>, +} +impl Planner<'_> { + fn shape_id(&self, ty: TypeId) -> Result { + self.carriers + .shape_for_type(ty) + .map(ResourceCarrierShape::id) + .ok_or_else(|| "carrier operation lost its exact checked type graph row".into()) + } + fn slot( + &mut self, + ty: TypeId, + storage: ResourceSlotStorage, + frame: ResourceFrameId, + ) -> Result { + let shape = self.shape_id(ty)?; + if !self + .carriers + .shape(shape) + .is_some_and(ResourceCarrierShape::contains_resource) + { + return Err( + "carrier slot cannot grant custody to ordinary data or a descriptor".into(), + ); + } + let id = ResourceCarrierSlotId(self.carriers.slots.len()); + self.carriers.slots.push(ResourceCarrierSlot { + id, + frame, + shape, + storage, + }); + Ok(id) + } + fn temporary(&mut self, ty: TypeId) -> Result { + self.slot( + ty, + ResourceSlotStorage::Expression { + site: self.site, + ordinal: self.ordinal, + }, + self.frame, + ) + } + fn leaf_slot( + &mut self, + ty: TypeId, + storage: ResourceSlotStorage, + frame: ResourceFrameId, + ) -> Result { + let shape = shape(&self.program.resource_manifest, self.types, ty)? + .ok_or("carrier leaf bridge has no exact existing leaf shape")?; + let id = ResourceOwnerSlotId(self.plan.slots.len()); + self.plan.slots.push(ResourceOwnerSlot { + id, + frame, + shape, + storage, + }); + Ok(id) + } + fn operation(&mut self, role: ResourceOperationRole) { + let id = ResourceOperationId(self.plan.operations.len()); + self.plan.operations.push(ResourceOperation { + id, + frame: self.frame, + site: self.site, + ordinal: self.ordinal, + role, + expression: self.expression, + named_indirect: None, + indirect_hook: None, + }); + } + fn carrier_operation( + &mut self, + role: ResourceCarrierOperationRole, + ) -> ResourceCarrierOperationId { + let id = ResourceCarrierOperationId(self.carriers.operations.len()); + self.carriers.operations.push(ResourceCarrierOperation { + id, + site: self.site, + frame: self.frame, + role, + expression: self.expression, + edge: self.edge, + }); + self.operation(ResourceOperationRole::Carrier { operation: id }); + id + } + fn frame(&mut self, role: ResourceFrameRole) -> ResourceFrameId { + let id = ResourceFrameId(self.plan.frames.len()); + self.plan.frames.push(ResourceFrame { + id, + role, + site: self.site, + parent: Some(self.frame), + ordinal: self.ordinal, + }); + id + } + fn end_loan(&mut self, loan: ResourceCarrierLoanId) { + if self.ended_loans.insert(loan) { + self.carrier_operation(ResourceCarrierOperationRole::EndBorrow { loan }); + } + } + fn loan( + &mut self, + value: ResourceCarrierValue, + ty: TypeId, + path: Vec, + ) -> Result { + let source = match value { + ResourceCarrierValue::Owned { slot } => ResourceCarrierLoanSource::Slot(slot), + ResourceCarrierValue::Borrowed { loan } => ResourceCarrierLoanSource::Loan(loan), + _ => { + return Err( + "carrier projection cannot borrow an ordinary or single-leaf value".into(), + ); + } + }; + let id = ResourceCarrierLoanId(self.carriers.loans.len()); + let shape = self.shape_id(ty)?; + self.carriers.loans.push(ResourceCarrierLoan { + id, + frame: self.frame, + shape, + source, + path, + }); + self.carrier_operation(ResourceCarrierOperationRole::Borrow { loan: id }); + Ok(id) + } + fn value_type(&self, value: ResourceCarrierValue) -> Result { + match value { + ResourceCarrierValue::Ordinary { ty } => Ok(ty), + ResourceCarrierValue::Owned { slot } => self + .carriers + .slots + .get(slot.index()) + .and_then(|slot| self.carriers.shape(slot.shape)) + .map(ResourceCarrierShape::ty) + .ok_or_else(|| "carrier input lost its installed slot shape".into()), + ResourceCarrierValue::Borrowed { loan } => self + .carriers + .loans + .get(loan.index()) + .and_then(|loan| self.carriers.shape(loan.shape)) + .map(ResourceCarrierShape::ty) + .ok_or_else(|| "carrier input lost its installed loan shape".into()), + ResourceCarrierValue::LeafOwned { slot } => match &self + .plan + .slots + .get(slot.index()) + .ok_or("carrier leaf input slot missing")? + .storage + { + ResourceSlotStorage::Local { header } => Ok(header.ty), + _ => { + let wanted = &self.plan.slots[slot.index()].shape; + self.types + .type_ids() + .find(|ty| { + shape(&self.program.resource_manifest, self.types, *ty) + .ok() + .flatten() + .as_ref() + == Some(wanted) + }) + .ok_or_else(|| "carrier leaf input lost its exact shape type".into()) + } + }, + ResourceCarrierValue::LeafBorrowed { .. } => { + Err("carrier constructor cannot adopt a borrowed leaf".into()) + } + } + } + fn begin_constructor( + &mut self, + expression: &Expression, + constructor: ResourceCarrierConstructor, + ) -> Result { + let destination = self.temporary(expression.ty)?; + self.carrier_operation(ResourceCarrierOperationRole::BeginConstructor { + destination, + constructor, + }); + Ok(destination) + } + fn child( + &mut self, + destination: ResourceCarrierSlotId, + index: usize, + expected: TypeId, + value: ResourceCarrierValue, + ) -> Result<(), String> { + if self.value_type(value)? != expected { + return Err("carrier constructor child changes its exact checked type".into()); + } + if matches!( + value, + ResourceCarrierValue::Borrowed { .. } | ResourceCarrierValue::LeafBorrowed { .. } + ) { + return Err("carrier owning constructor cannot adopt a resident loan".into()); + } + let shape = self.shape_id(expected)?; + self.carrier_operation(ResourceCarrierOperationRole::ConstructorChild { + destination, + child: ResourceCarrierChild { + index, + shape, + value, + }, + }); + Ok(()) + } + fn commit(&mut self, destination: ResourceCarrierSlotId) -> ResourceCarrierValue { + self.carrier_operation(ResourceCarrierOperationRole::CommitConstructor { destination }); + ResourceCarrierValue::Owned { slot: destination } + } + fn exact_order(&self, order: &[usize], count: usize) -> Result<(), String> { + let mut seen = BTreeSet::new(); + if order.len() != count + || order + .iter() + .any(|index| *index >= count || !seen.insert(*index)) + { + return Err("carrier constructor changed its checked lexical evaluation order".into()); + } + Ok(()) + } + fn expression( + &mut self, + expression: &Expression, + owned: bool, + ) -> Result { + let previous = self + .expression + .replace(std::ptr::from_ref(expression).addr()); + self.ordinal = self + .ordinal + .checked_add(1) + .ok_or("carrier expression ordinal overflow")?; + let result = self.expression_inner(expression, owned); + self.expression = previous; + result + } + // A checked branch can narrow a machine Local without changing its + // physical storage. Only observational places may read that same owner; + // owning Local reads still use expression_inner's exact header check. + fn observational_place( + &mut self, + expression: &Expression, + ) -> Result { + let previous = self + .expression + .replace(std::ptr::from_ref(expression).addr()); + self.ordinal = self + .ordinal + .checked_add(1) + .ok_or("carrier expression ordinal overflow")?; + let result = match &expression.kind { + E::Local(local) => { + let header = self + .function + .local(*local) + .ok_or("carrier observation lost its exact dense local header")?; + match ( + self.types.resolve(header.ty), + self.types.resolve(expression.ty), + ) { + (Type::Machine(owner), Type::MachineState { machine, .. }) + if owner == machine => + { + let index = usize::try_from(local.index()) + .map_err(|_| "carrier local index is not representable")?; + let value = self.carrier_locals.get(index).copied().flatten().ok_or( + "carrier observation has no dedicated physical slot or formal loan", + )?; + if !matches!( + value, + ResourceCarrierValue::Owned { .. } + | ResourceCarrierValue::Borrowed { .. } + ) || self.value_type(value)? != header.ty + { + Err( + "carrier observation changed its physical local custody type" + .into(), + ) + } else { + Ok(value) + } + } + _ => self.expression_inner(expression, false), + } + } + E::View(inner) => { + if expression.ty != inner.ty { + Err("carrier observational View changed its exact checked type".into()) + } else { + self.observational_place(inner) + } + } + _ => self.expression_inner(expression, false), + }; + self.expression = previous; + result + } + fn expression_inner( + &mut self, + expression: &Expression, + owned: bool, + ) -> Result { + match &expression.kind { + E::Local(local) => { + let header = self.function.local(*local).ok_or("carrier read lost its exact dense local header")?; + if header.ty != expression.ty { return Err("carrier local read changed its checked type".into()); } + let index = usize::try_from(local.index()).map_err(|_| "carrier local index is not representable")?; + if let Some(value) = self.carrier_locals[index] { + if owned && matches!(value, ResourceCarrierValue::Borrowed { .. }) { return Err("carrier owning read cannot consume an incoming or resident view".into()); } + return Ok(value); + } + if let Some(value) = self.leaf_locals[index] { + if owned && matches!(value, ResourceCarrierValue::LeafBorrowed { .. }) { return Err("carrier leaf adapter cannot adopt a borrowed view".into()); } + return Ok(value); + } + if resource_type_pending(self.types, expression.ty) && !matches!(self.types.resolve(expression.ty), Type::Function { .. }) { + return Err("carrier read has no dedicated slot or formal loan".into()); + } + Ok(ResourceCarrierValue::Ordinary { ty: expression.ty }) + } + E::View(inner) => { + if owned && resource_type_pending(self.types, expression.ty) { return Err("carrier View cannot be adopted by owning storage".into()); } + self.expression(inner, false) + } + E::Call { function, args, evaluation_order, ownership } => self.call(expression, *function, args, evaluation_order, ownership), + E::InterfaceCoerce { value, adapters } if carrier_type(self.types, expression.ty) && adapters.is_empty() => { + if !self.compatible(value.ty, expression.ty) { return Err("carrier coercion changes its exact checked qualification".into()); } + let input = self.expression(value, owned)?; + if value.ty == expression.ty { return Ok(input); } + let ResourceCarrierValue::Owned { slot: source } = input else { return Err("pending Resource carrier: borrowed qualification needs its exact readonly graph projection".into()); }; + let destination = self.temporary(expression.ty)?; + self.transfer(source, destination)?; + Ok(ResourceCarrierValue::Owned { slot: destination }) + } + E::Field { base, owner_type, field } if carrier_type(self.types, base.ty) => self.field(expression, base, *owner_type, *field, owned), + E::StateIs { value, state } if carrier_type(self.types, value.ty) => { + let input = self.observational_place(value)?; + let physical_type = self.value_type(input)?; + let source = self.loan(input, physical_type, Vec::new())?; + let state = usize::try_from(state.index()).map_err(|_| "carrier state selector is not representable")?; + self.carrier_operation(ResourceCarrierOperationRole::Observe { source, observation: ResourceCarrierObservation::State { state }, target: None }); + self.end_loan(source); + Ok(ResourceCarrierValue::Ordinary { ty: expression.ty }) + } + E::Intrinsic { intrinsic, args, evaluation_order, .. } => { + self.exact_order(evaluation_order, args.len())?; + if matches!(intrinsic, hir::IntrinsicId::ListLength | hir::IntrinsicId::MapLength) && args.len() == 1 && carrier_type(self.types, args[0].ty) { + let input = self.expression(&args[0], false)?; + let source = self.loan(input, args[0].ty, Vec::new())?; + self.carrier_operation(ResourceCarrierOperationRole::Observe { source, observation: ResourceCarrierObservation::Length, target: None }); + self.end_loan(source); + return Ok(ResourceCarrierValue::Ordinary { ty: expression.ty }); + } + if args.iter().any(|arg| resource_type_pending(self.types, arg.ty)) || carrier_type(self.types, expression.ty) { + return Err("pending Resource carrier: this intrinsic requires its dedicated custody operation".into()); + } + for index in evaluation_order { self.expression(&args[*index], true)?; } + Ok(ResourceCarrierValue::Ordinary { ty: expression.ty }) + } + E::ListConstruct { elements } if carrier_type(self.types, expression.ty) => { + let Type::List(element) = self.types.resolve(expression.ty) else { return Err("carrier list constructor lost its exact list type".into()); }; + let element = *element; + let destination = self.begin_constructor(expression, ResourceCarrierConstructor::List)?; + for (index, child) in elements.iter().enumerate() { + if child.ty != element { return Err("carrier list child changes its exact element type".into()); } + let value = self.expression(child, true)?; + self.child(destination, index, element, value)?; + } + Ok(self.commit(destination)) + } + E::MapConstruct { entries } if carrier_type(self.types, expression.ty) => { + let Type::Map(key, value) = self.types.resolve(expression.ty) else { return Err("carrier map constructor lost its exact map type".into()); }; + let (key, value) = (*key, *value); + if resource_type_pending(self.types, key) { return Err("carrier map key must preserve the checked primitive key contract".into()); } + let destination = self.begin_constructor(expression, ResourceCarrierConstructor::Map)?; + for (index, entry) in entries.iter().enumerate() { + if entry.key.ty != key || entry.value.ty != value { return Err("carrier map entry changes its exact key/value types".into()); } + let key_value = self.expression(&entry.key, true)?; + self.child(destination, index.checked_mul(2).ok_or("carrier map ordinal overflow")?, key, key_value)?; + let payload = self.expression(&entry.value, true)?; + self.child(destination, index.checked_mul(2).and_then(|n| n.checked_add(1)).ok_or("carrier map ordinal overflow")?, value, payload)?; + } + Ok(self.commit(destination)) + } + E::StructConstruct { struct_type, fields, evaluation_order, .. } if carrier_type(self.types, expression.ty) => { + if *struct_type != expression.ty { return Err("carrier record changed its exact nominal type".into()); } + let Type::Struct(owner) = self.types.resolve(*struct_type) else { return Err("carrier record lost its nominal definition".into()); }; + let definition = self.types.resolve_struct(*owner); + let expected = definition.fields.iter().map(|(_, ty)| *ty).collect::>(); + if expected.len() != fields.len() { return Err("carrier record changed its declared field count".into()); } + self.exact_order(evaluation_order, fields.len())?; + let destination = self.begin_constructor(expression, ResourceCarrierConstructor::Struct)?; + for index in evaluation_order { + let value = self.expression(&fields[*index], true)?; + self.child(destination, *index, expected[*index], value)?; + } + Ok(self.commit(destination)) + } + E::EnumConstruct { enum_type, variant, payloads, evaluation_order } if carrier_type(self.types, expression.ty) => { + if *enum_type != expression.ty { return Err("carrier enum changed its exact nominal owner".into()); } + let Type::Enum(owner) = self.types.resolve(*enum_type) else { return Err("carrier enum lost its exact definition".into()); }; + let expected = self.types.resolve_enum(*owner).variants.get(usize::try_from(variant.index()).map_err(|_| "carrier variant index is not representable")?) + .ok_or("carrier enum selected a foreign variant")?.fields.iter().map(|(_, ty)| *ty).collect::>(); + if expected.len() != payloads.len() { return Err("carrier enum changed its selected payload count".into()); } + self.exact_order(evaluation_order, payloads.len())?; + let destination = self.begin_constructor(expression, ResourceCarrierConstructor::Enum { variant: *variant })?; + for index in evaluation_order { + let value = self.expression(&payloads[*index], true)?; + self.child(destination, *index, expected[*index], value)?; + } + Ok(self.commit(destination)) + } + E::MachineConstruct { state_type, state, payloads } if carrier_type(self.types, expression.ty) => self.machine(expression, *state_type, *state, payloads), + E::OptionalNone if !carrier_type(self.types, expression.ty) && shape(&self.program.resource_manifest, self.types, expression.ty)?.is_some() => { + let destination = self.leaf_slot(expression.ty, ResourceSlotStorage::Expression { site: self.site, ordinal: self.ordinal }, self.frame)?; + self.operation(ResourceOperationRole::CreateAbsentSum { destination }); + Ok(ResourceCarrierValue::LeafOwned { slot: destination }) + } + E::OptionalNone if carrier_type(self.types, expression.ty) => { + let destination = self.begin_constructor(expression, ResourceCarrierConstructor::OptionalNone)?; + Ok(self.commit(destination)) + } + E::OptionalSome(inner) | E::ResultOk(inner) | E::ResultFail(inner) if carrier_type(self.types, expression.ty) => { + let (constructor, expected) = match (&expression.kind, self.types.resolve(expression.ty)) { + (E::OptionalSome(_), Type::Optional(payload)) => (ResourceCarrierConstructor::OptionalSome, *payload), + (E::ResultOk(_), Type::Result(ok, _)) => (ResourceCarrierConstructor::ResultOk, *ok), + (E::ResultFail(_), Type::Result(_, fail)) => (ResourceCarrierConstructor::ResultFail, *fail), + _ => return Err("carrier sum changed its exact selected arm type".into()), + }; + let destination = self.begin_constructor(expression, constructor)?; + let value = self.expression(inner, true)?; + self.child(destination, 0, expected, value)?; + Ok(self.commit(destination)) + } + E::RefinementValidated(_) if carrier_type(self.types, expression.ty) => Err("pending Resource carrier: occupied or refined construction needs its original checked predicate transition".into()), + E::ResourceInvoke { .. } | E::IndirectCall { .. } if resource_type_pending(self.types, expression.ty) => Err("pending Resource carrier: live leaf producers remain under their dedicated custody plan".into()), + E::Clone(_) if resource_type_pending(self.types, expression.ty) => Err("carrier physical copies cannot create an owning root".into()), + _ if carrier_type(self.types, expression.ty) => Err("pending Resource carrier: this expression needs its exact constructor or projection transport".into()), + _ => { + let mut has_resource_child = false; + walk::expression(expression, &mut |child| { + if !std::ptr::eq(child, expression) && resource_type_pending(self.types, child.ty) && !matches!(self.types.resolve(child.ty), Type::Function { .. }) { + has_resource_child = true; + } + }); + if has_resource_child { + // Common ordinary bool/scalar operators contain an exact typed + // observation. Recurse through their direct operands only. + match &expression.kind { + E::Binary { left, right, .. } => { self.expression(left, false)?; self.expression(right, false)?; } + E::Unary { value, .. } => { self.expression(value, false)?; } + _ => return Err("pending Resource carrier: nested ordinary observation needs its dedicated operation".into()), + } + } + Ok(ResourceCarrierValue::Ordinary { ty: expression.ty }) + } + } + } + fn machine( + &mut self, + expression: &Expression, + state_type: TypeId, + state: hir::StateId, + payloads: &[Expression], + ) -> Result { + let Type::MachineState { + machine, + state: selected, + } = self.types.resolve(state_type) + else { + return Err("carrier machine constructor lost its exact checked state type".into()); + }; + if selected.index() != state.index() { + return Err("carrier machine constructor changed its checked state selector".into()); + } + match self.types.resolve(expression.ty) { + Type::Machine(owner) if owner == machine => {} + Type::MachineState { + machine: owner, + state: actual, + } if owner == machine && actual == selected => {} + _ => { + return Err( + "carrier machine constructor changed its checked owner or qualification".into(), + ); + } + } + let fields = self + .types + .resolve_machine(*machine) + .state(*selected) + .ok_or("carrier machine lost its selected state definition")? + .fields + .iter() + .map(|(_, ty)| *ty) + .collect::>(); + if fields.len() != payloads.len() { + return Err("carrier machine changed its selected payload count".into()); + } + let state = usize::try_from(state.index()) + .map_err(|_| "carrier state selector is not representable")?; + let destination = + self.begin_constructor(expression, ResourceCarrierConstructor::Machine { state })?; + for (index, (expected, child)) in fields.into_iter().zip(payloads).enumerate() { + let value = self.expression(child, true)?; + self.child(destination, index, expected, value)?; + } + Ok(self.commit(destination)) + } + fn field( + &mut self, + expression: &Expression, + base: &Expression, + owner_type: TypeId, + field: FieldId, + owned: bool, + ) -> Result { + let expected = match self.types.resolve(owner_type) { + Type::Struct(owner) => self + .types + .resolve_struct(*owner) + .fields + .get( + usize::try_from(field.index()) + .map_err(|_| "carrier field selector is not representable")?, + ) + .map(|(_, ty)| *ty), + Type::MachineState { machine, state } => self + .types + .resolve_machine(*machine) + .state(*state) + .and_then(|state| state.fields.get(usize::try_from(field.index()).ok()?)) + .map(|(_, ty)| *ty), + _ => None, + } + .ok_or("carrier field lost its exact checked nominal owner/ordinal")?; + if expected != expression.ty { + return Err("carrier field changes its declared exact payload type".into()); + } + let input = match self.types.resolve(owner_type) { + Type::MachineState { .. } => self.observational_place(base)?, + _ => self.expression(base, false)?, + }; + let physical_type = self.value_type(input)?; + let source = self.loan(input, physical_type, Vec::new())?; + let path = match self.types.resolve(owner_type) { + Type::Struct(_) => ResourceCarrierProjectionPath::Field { + owner: owner_type, + field, + }, + Type::MachineState { state, .. } => ResourceCarrierProjectionPath::State { + owner: owner_type, + state: usize::try_from(state.index()) + .map_err(|_| "carrier state selector is not representable")?, + field: usize::try_from(field.index()) + .map_err(|_| "carrier field selector is not representable")?, + }, + _ => return Err("carrier field lost its exact checked projection family".into()), + }; + let result = self.project(expression.ty, source, vec![path], owned, None)?; + if owned { + self.end_loan(source); + } + Ok(result) + } + fn project( + &mut self, + ty: TypeId, + source: ResourceCarrierLoanId, + path: Vec, + owned: bool, + target: Option, + ) -> Result { + if carrier_type(self.types, ty) { + if owned { + let destination = match target { + Some(local) => match self.carrier_locals[usize::try_from(local.index()) + .map_err(|_| "carrier binder index is not representable")?] + { + Some(ResourceCarrierValue::Owned { slot }) => slot, + _ => { + return Err( + "carrier extracted binder lacks its exact destination slot".into(), + ); + } + }, + None => self.temporary(ty)?, + }; + self.end_loan(source); + self.carrier_operation(ResourceCarrierOperationRole::Extract { + source, + path, + destination, + }); + Ok(ResourceCarrierValue::Owned { slot: destination }) + } else { + let destination = + self.loan(ResourceCarrierValue::Borrowed { loan: source }, ty, path)?; + // The single Borrow row already authenticates the exact parent + // and path; no second Project row may mint the same loan again. + Ok(ResourceCarrierValue::Borrowed { loan: destination }) + } + } else if let Some(shape) = shape(&self.program.resource_manifest, self.types, ty)? { + if !matches!( + shape, + ResourceShape::Optional { .. } | ResourceShape::Result { .. } + ) { + return Err("pending Resource carrier: projected live Resource leaves need their exact child custody transport".into()); + } + let destination = match target { + Some(local) if owned => match self.leaf_locals[usize::try_from(local.index()) + .map_err(|_| "leaf binder index is not representable")?] + { + Some(ResourceCarrierValue::LeafOwned { slot }) => slot, + _ => { + return Err( + "carrier leaf extraction lacks its dedicated destination slot".into(), + ); + } + }, + _ => self.leaf_slot( + ty, + ResourceSlotStorage::Expression { + site: self.site, + ordinal: self.ordinal, + }, + self.frame, + )?, + }; + let loan = if owned { + None + } else { + Some(ResourceLoanId(self.plan.loans.len())) + }; + if owned { + self.end_loan(source); + } + let operation = self.carrier_operation(ResourceCarrierOperationRole::AdaptSum { + source, + path, + destination, + loan, + lease_frame: self.frame, + }); + if let Some(id) = loan { + self.plan.loans.push(ResourceLoan { + id, + frame: self.frame, + shape, + source: ResourceLoanSource::CarrierSumProjection { operation }, + parameter: None, + }); + self.operation(ResourceOperationRole::BorrowSum { loan: id }); + Ok(ResourceCarrierValue::LeafBorrowed { loan: id }) + } else { + Ok(ResourceCarrierValue::LeafOwned { slot: destination }) + } + } else { + if owned && let Some(target) = target { + self.end_loan(source); + self.carrier_operation(ResourceCarrierOperationRole::ExtractOrdinary { + source, + path, + target, + ty, + }); + } else { + self.carrier_operation(ResourceCarrierOperationRole::Observe { + source, + observation: ResourceCarrierObservation::Ordinary { path, ty }, + target, + }); + } + self.end_loan(source); + Ok(ResourceCarrierValue::Ordinary { ty }) + } + } + fn call( + &mut self, + expression: &Expression, + function: FunctionId, + args: &[Expression], + order: &[usize], + ownership: &hir::CallOwnership, + ) -> Result { + let callee = self + .program + .functions + .get( + usize::try_from(function.index()) + .map_err(|_| "carrier callee index is not representable")?, + ) + .filter(|callee| callee.id == function) + .ok_or("carrier Source call lost its exact callee")?; + self.exact_order(order, args.len())?; + let relevant = has_execution_records(callee, self.types) + || args + .iter() + .any(|arg| resource_type_pending(self.types, arg.ty)) + || resource_type_pending(self.types, expression.ty); + if !relevant { + for index in order { + self.expression(&args[*index], true)?; + } + return Ok(ResourceCarrierValue::Ordinary { ty: expression.ty }); + } + let hir::CallOwnership::Source(source) = ownership else { + return Err("carrier call cannot use Generated authority".into()); + }; + if source.arguments.len() != args.len() || callee.params.len() != args.len() { + return Err("carrier call changed its original complete Source tuple".into()); + } + let parent = self.frame; + let call_ordinal = self.ordinal; + let frame = self.frame(ResourceFrameRole::Operation); + self.frame = frame; + let loan_start = self.carriers.loans.len(); + let leaf_loan_start = self.plan.loans.len(); + let mut operands = Vec::new(); + for parameter in order { + let fact = source + .arguments + .iter() + .find(|fact| fact.parameter_index == *parameter) + .ok_or("carrier actual lost its original Source formal tuple")?; + let arg = &args[*parameter]; + if !resource_type_pending(self.types, arg.ty) + || matches!(self.types.resolve(arg.ty), Type::Function { .. }) + { + self.expression(arg, true)?; + operands.push(ResourceCallOperand::Ordinary { + parameter: *parameter, + ty: arg.ty, + }); + continue; + } + if fact.staging != hir::ArgumentStaging::Original + || matches!(fact.effect, Effect::Copy | Effect::ObserveData) + { + return Err( + "carrier actual cannot use ordinary staging or copying authority".into(), + ); + } + let retaining = fact.effect == Effect::RetainBorrow; + if retaining + && (fact.syntax != Syntax::WrittenView || fact.callee_access != Access::View) + { + return Err( + "carrier retained actual changed its written View/formal association".into(), + ); + } + if !retaining && fact.syntax != Syntax::Bare { + return Err("carrier owned actual changed its checked bare spelling".into()); + } + let input = if !retaining && fact.physical_access == Access::View { + if let E::View(inner) = &arg.kind { + inner.as_ref() + } else { + arg + } + } else { + arg + }; + let value = self.expression(input, !retaining)?; + match value { + ResourceCarrierValue::Owned { .. } + if retaining && fact.callee_access == Access::View => + { + let loan = self.loan(value, arg.ty, Vec::new())?; + operands.push(ResourceCallOperand::CarrierBorrowed { + parameter: *parameter, + loan, + }); + } + ResourceCarrierValue::Owned { slot: source } => { + let slot = self.slot( + arg.ty, + ResourceSlotStorage::Argument { + site: self.site, + call_ordinal, + parameter: *parameter, + }, + frame, + )?; + self.transfer(source, slot)?; + if fact.callee_access == Access::Owned { + operands.push(ResourceCallOperand::CarrierOwned { + parameter: *parameter, + slot, + }); + } else { + let loan = + self.loan(ResourceCarrierValue::Owned { slot }, arg.ty, Vec::new())?; + operands.push(ResourceCallOperand::CarrierBorrowed { + parameter: *parameter, + loan, + }); + } + } + ResourceCarrierValue::Borrowed { .. } if fact.callee_access == Access::View => { + let loan = self.loan(value, arg.ty, Vec::new())?; + operands.push(ResourceCallOperand::CarrierBorrowed { + parameter: *parameter, + loan, + }); + } + ResourceCarrierValue::LeafOwned { slot: source } + if fact.callee_access == Access::Owned => + { + let slot = self.leaf_slot( + arg.ty, + ResourceSlotStorage::Argument { + site: self.site, + call_ordinal, + parameter: *parameter, + }, + frame, + )?; + self.operation(ResourceOperationRole::Transfer { + source, + destination: slot, + }); + operands.push(ResourceCallOperand::Owned { + parameter: *parameter, + slot, + }); + } + ResourceCarrierValue::LeafOwned { slot: source } + if fact.callee_access == Access::View => + { + let slot = if retaining { + source + } else { + let destination = self.leaf_slot( + arg.ty, + ResourceSlotStorage::Argument { + site: self.site, + call_ordinal, + parameter: *parameter, + }, + frame, + )?; + self.operation(ResourceOperationRole::Transfer { + source, + destination, + }); + destination + }; + let loan = ResourceLoanId(self.plan.loans.len()); + let shape = self.plan.slots[slot.index()].shape.clone(); + self.plan.loans.push(ResourceLoan { + id: loan, + frame, + shape, + source: ResourceLoanSource::Owner(slot), + parameter: Some(*parameter), + }); + self.operation(ResourceOperationRole::BorrowSum { loan }); + operands.push(ResourceCallOperand::Borrowed { + parameter: *parameter, + loan, + }); + } + ResourceCarrierValue::LeafBorrowed { loan } + if fact.callee_access == Access::View => + { + operands.push(ResourceCallOperand::Borrowed { + parameter: *parameter, + loan, + }) + } + _ => { + return Err( + "carrier actual changes its checked owning/view endpoint role".into(), + ); + } + } + } + let (result, value) = if carrier_type(self.types, expression.ty) { + let slot = self.slot( + expression.ty, + ResourceSlotStorage::Expression { + site: self.site, + ordinal: self.ordinal, + }, + parent, + )?; + ( + ResourceCallResult::Carrier { slot }, + ResourceCarrierValue::Owned { slot }, + ) + } else if shape(&self.program.resource_manifest, self.types, expression.ty)?.is_some() { + let slot = self.leaf_slot( + expression.ty, + ResourceSlotStorage::Expression { + site: self.site, + ordinal: self.ordinal, + }, + parent, + )?; + ( + ResourceCallResult::Owned { slot }, + ResourceCarrierValue::LeafOwned { slot }, + ) + } else { + ( + ResourceCallResult::Ordinary { ty: expression.ty }, + ResourceCarrierValue::Ordinary { ty: expression.ty }, + ) + }; + let completed_ordinal = self.ordinal; + self.ordinal = call_ordinal; + self.operation(ResourceOperationRole::InvokeSourceFunction { + function, + source: source.clone(), + formals: source + .arguments + .iter() + .map(ResourceCallFormal::original) + .collect(), + evaluation_order: order.to_vec(), + operands, + result, + }); + self.ordinal = completed_ordinal; + for index in (leaf_loan_start..self.plan.loans.len()).rev() { + let loan = ResourceLoanId(index); + if self.plan.loans[index].frame == frame { + self.operation(ResourceOperationRole::EndSumBorrow { loan }); + } + } + for index in (loan_start..self.carriers.loans.len()).rev() { + let loan = ResourceCarrierLoanId(index); + if self.carriers.loans[index].frame == frame { + self.end_loan(loan); + } + } + self.operation(ResourceOperationRole::Complete { + outcome: ResourceCompletion::Normal, + }); + self.frame = parent; + Ok(value) + } + fn compatible(&self, actual: TypeId, expected: TypeId) -> bool { + actual == expected + || matches!((self.types.resolve(actual), self.types.resolve(expected)), + (Type::MachineState { machine, .. }, Type::Machine(owner)) if machine == owner) + } + fn transfer( + &mut self, + source: ResourceCarrierSlotId, + destination: ResourceCarrierSlotId, + ) -> Result<(), String> { + let actual = self + .carriers + .shape(self.carriers.slots[source.index()].shape) + .ok_or("carrier transfer source shape missing")? + .ty; + let expected = self + .carriers + .shape(self.carriers.slots[destination.index()].shape) + .ok_or("carrier transfer destination shape missing")? + .ty; + if actual == expected { + self.carrier_operation(ResourceCarrierOperationRole::Transfer { + source, + destination, + }); + } else if let (Type::MachineState { machine, state }, Type::Machine(owner)) = + (self.types.resolve(actual), self.types.resolve(expected)) + { + if machine != owner { + return Err("carrier qualification changes its exact machine owner".into()); + } + let machine = self.shape_id(expected)?; + let state = usize::try_from(state.index()) + .map_err(|_| "carrier state selector is not representable")?; + self.carrier_operation(ResourceCarrierOperationRole::QualifyMachine { + source, + destination, + machine, + state, + }); + } else { + return Err( + "carrier transfer changes exact graph node without a checked qualification".into(), + ); + } + Ok(()) + } + fn store( + &mut self, + local: LocalId, + input: ResourceCarrierValue, + ty: TypeId, + ) -> Result<(), String> { + let header = self + .function + .local(local) + .ok_or("carrier storage lost its exact current local header")?; + if !self.compatible(ty, header.ty) { + return Err("carrier storage changes its exact nominal/generic/state type".into()); + } + let index = usize::try_from(local.index()) + .map_err(|_| "carrier destination index is not representable")?; + if carrier_type(self.types, header.ty) { + if self.function.is_view_local(local) { + let loan = self.loan(input, header.ty, Vec::new())?; + self.carrier_locals[index] = Some(ResourceCarrierValue::Borrowed { loan }); + } else { + let Some(ResourceCarrierValue::Owned { slot: destination }) = + self.carrier_locals[index] + else { + return Err("carrier destination lacks its dedicated owning slot".into()); + }; + let ResourceCarrierValue::Owned { slot: source } = input else { + return Err( + "carrier owned storage cannot adopt a borrowed or ordinary value".into(), + ); + }; + if source != destination { + self.transfer(source, destination)?; + } + } + } else if shape(&self.program.resource_manifest, self.types, header.ty)?.is_some() { + if self.function.is_view_local(local) { + match input { + ResourceCarrierValue::LeafBorrowed { .. } => { self.leaf_locals[index] = Some(input); } + _ => return Err("pending Resource carrier: this resident leaf alias needs its lexical constructor transport".into()), + } + } else { + let Some(ResourceCarrierValue::LeafOwned { slot: destination }) = + self.leaf_locals[index] + else { + return Err("carrier leaf storage lacks its dedicated owning slot".into()); + }; + let ResourceCarrierValue::LeafOwned { slot: source } = input else { + return Err( + "carrier leaf storage cannot adopt a borrowed or aggregate value".into(), + ); + }; + if source != destination { + self.operation(ResourceOperationRole::Transfer { + source, + destination, + }); + } + } + } else if !matches!(input, ResourceCarrierValue::Ordinary { .. }) { + return Err("carrier custody cannot enter ordinary companion storage".into()); + } + Ok(()) + } + fn source_loan( + &mut self, + source: &SequenceSource, + ) -> Result<(ResourceCarrierLoanId, TypeId), String> { + let ty = source + .ty(self.function) + .ok_or("carrier sequence lost its exact current source type")?; + let root = usize::try_from(source.root().index()) + .map_err(|_| "carrier sequence root is not representable")?; + let input = self + .carrier_locals + .get(root) + .and_then(|value| *value) + .ok_or("carrier sequence source lacks a dedicated carrier slot or loan")?; + let path = match source { + SequenceSource::Local(_) => Vec::new(), + SequenceSource::Projected { path, .. } => path + .iter() + .map(|field| ResourceCarrierProjectionPath::Field { + owner: field.owner_type, + field: field.field, + }) + .collect(), + }; + let loan = self.loan(input, ty, path)?; + Ok((loan, ty)) + } + fn sequence( + &mut self, + consume: bool, + source: &SequenceSource, + index: LocalId, + target: LocalId, + part: SequencePart, + ) -> Result<(), String> { + if self.function.local(index).map(|header| header.ty) != Some(TypeInterner::INT64) { + return Err("carrier sequence index lost its exact Int64 header".into()); + } + let (source, ty) = self.source_loan(source)?; + let (expected, path) = match (self.types.resolve(ty), part) { + (Type::List(element), SequencePart::Element) => ( + *element, + ResourceCarrierProjectionPath::Element { + index: ResourceCarrierIndex::Local(index), + }, + ), + (Type::Map(key, _), SequencePart::Key) => ( + *key, + ResourceCarrierProjectionPath::MapKey { + index: ResourceCarrierIndex::Local(index), + }, + ), + (Type::Map(_, value), SequencePart::Value) => ( + *value, + ResourceCarrierProjectionPath::MapValue { + index: ResourceCarrierIndex::Local(index), + }, + ), + _ => { + return Err( + "carrier sequence extraction changed its checked list/map component".into(), + ); + } + }; + if self.function.local(target).map(|header| header.ty) != Some(expected) { + return Err("carrier sequence extraction changes its exact current binder type".into()); + } + self.project(expected, source, vec![path], consume, Some(target))?; + self.end_loan(source); + Ok(()) + } + fn finish(&mut self, outcome: ResourceCompletion) { + // Dynamic activation tracks acquisition order. These rows specify exact + // eligible endpoints; cleanup does not invent local-number drop order. + self.operation(ResourceOperationRole::Complete { outcome }); + } + fn block(&mut self, block: &BasicBlock) -> Result<(), String> { + for (index, statement) in block.statements.iter().enumerate() { + self.site = ResourceSite { + function: self.function.id, + block: block.id, + position: ResourcePosition::Statement(index), + }; + self.ordinal = 0; + self.expression = None; + self.frame = self + .incoming_frames + .iter() + .find(|(site, _)| *site == self.site) + .map(|(_, frame)| *frame) + .ok_or("carrier statement has no authenticated incoming constructor prefix")?; + self.plan.execution_frames.push((self.site, self.frame)); + match &statement.kind { + StatementKind::ResourceConstructor(node) => { + let region = self.function.resource_constructor(node.constructor())?; + let frame = *self.constructor_frames.get(®ion.id()).ok_or("carrier constructor lost its private Operation frame")?; + let destination = *self.constructor_slots.get(®ion.id()).ok_or("carrier constructor lost its private building root")?; + match node { + ResourceConstructorNode::Begin { .. } => { + self.frame = frame; + self.carrier_operation(ResourceCarrierOperationRole::BeginConstructor { destination, constructor: ResourceCarrierConstructor::Struct }); + } + ResourceConstructorNode::Stage { source_index, field, value, .. } => { + if self.frame != frame || region.fields().get(*source_index).is_none_or(|f| f.stage() != self.site || f.field() != *field) { + return Err("carrier constructor Stage differs from its exact current field occurrence".into()); + } + let input = self.expression(value, true)?; + self.child(destination, *field, region.field_types()[*field], input)?; + } + ResourceConstructorNode::Commit { output, .. } => { + if self.frame != frame || region.commit().is_none_or(|(site, header)| site != self.site || header.id != *output) { + return Err("carrier constructor Commit differs from its exact output occurrence".into()); + } + self.carrier_operation(ResourceCarrierOperationRole::CommitConstructor { destination }); + self.store(*output, ResourceCarrierValue::Owned { slot: destination }, region.original().ty)?; + } + ResourceConstructorNode::End { outcome, .. } => { + if self.frame != frame || !region.exits().contains(&(self.site, *outcome)) { + return Err("carrier constructor End differs from its exact reached prefix".into()); + } + self.finish(*outcome); + } + } + } + StatementKind::Let { local, value } => { + let input = self.expression(value, !self.function.is_view_local(*local))?; + self.store(*local, input, value.ty)?; + } + StatementKind::Assign { target, value } => { + if resource_type_pending(self.types, target.ty) { return Err("pending Resource carrier: replacement needs its established acquisition/destruction transition".into()); } + self.expression(target, false)?; + self.expression(value, true)?; + } + StatementKind::Evaluate(value) | StatementKind::HandleDefault(value) => { + let input = self.expression(value, true)?; + match input { + ResourceCarrierValue::Owned { slot } => { self.carrier_operation(ResourceCarrierOperationRole::Retire { source: slot }); } + ResourceCarrierValue::LeafOwned { slot } => self.operation(ResourceOperationRole::Drop { source: slot, occupancy: if matches!(self.plan.slots[slot.index()].shape,ResourceShape::Plain { .. }) { ResourceOccupancy::Occupied } else { ResourceOccupancy::Empty } }), + ResourceCarrierValue::Borrowed { .. } | ResourceCarrierValue::LeafBorrowed { .. } => return Err("carrier borrowed result cannot be discarded as an owning endpoint".into()), + ResourceCarrierValue::Ordinary { .. } => {} + } + } + StatementKind::SequenceLength { source, target } if source.ty(self.function).is_some_and(|ty| carrier_type(self.types, ty)) => { + if self.function.local(*target).map(|header| header.ty) != Some(TypeInterner::INT64) { return Err("carrier sequence length lost its exact Int64 destination".into()); } + let (source, _) = self.source_loan(source)?; + self.carrier_operation(ResourceCarrierOperationRole::Observe { source, observation: ResourceCarrierObservation::Length, target: Some(*target) }); + self.end_loan(source); + } + StatementKind::SequenceGet { consume, source, index, target, part } if source.ty(self.function).is_some_and(|ty| carrier_type(self.types, ty)) => self.sequence(*consume, source, *index, *target, *part)?, + StatementKind::SumTag { source, target } => { + let ty = self.function.local(*source).ok_or("carrier SumTag source header missing")?.ty; + if carrier_type(self.types, ty) { + let input = self.carrier_locals[usize::try_from(source.index()).map_err(|_| "carrier sum source index is not representable")?].ok_or("carrier SumTag source slot missing")?; + let loan = self.loan(input, ty, Vec::new())?; + self.carrier_operation(ResourceCarrierOperationRole::Observe { source: loan, observation: ResourceCarrierObservation::Tag, target: Some(*target) }); + self.end_loan(loan); + } else if let Some(ResourceCarrierValue::LeafBorrowed { loan }) = self.leaf_locals[usize::try_from(source.index()).map_err(|_| "carrier leaf source index is not representable")?] { + self.operation(ResourceOperationRole::ObserveSumView { source: loan, target: *target }); + } + } + StatementKind::SumTake { source, target, success } => { + let ty = self.function.local(*source).ok_or("carrier SumTake source header missing")?.ty; + let output = self.function.local(*target).ok_or("carrier SumTake destination header missing")?.ty; + if carrier_type(self.types, ty) { + let (expected, path) = match (self.types.resolve(ty), success) { + (Type::Optional(inner), true) => (*inner, ResourceCarrierProjectionPath::OptionalSome), + (Type::Result(ok, _), true) => (*ok, ResourceCarrierProjectionPath::ResultOk), + (Type::Result(_, fail), false) => (*fail, ResourceCarrierProjectionPath::ResultFail), + _ => return Err("carrier SumTake selected a non-payload arm".into()), + }; + if output != expected { return Err("carrier SumTake changed its exact selected payload type".into()); } + let input = self.carrier_locals[usize::try_from(source.index()).map_err(|_| "carrier sum source index is not representable")?].ok_or("carrier SumTake source slot missing")?; + let loan = self.loan(input, ty, Vec::new())?; + self.project(output, loan, vec![path], !self.function.is_view_local(*target), Some(*target))?; + self.end_loan(loan); + if !self.function.is_view_local(*target) && let ResourceCarrierValue::Owned { slot } = input { + self.carrier_operation(ResourceCarrierOperationRole::Retire { source: slot }); + } + } else if resource_type_pending(self.types, ty) { + return Err("pending Resource carrier: nested leaf Handle needs its original borrowed-sum selecting edge".into()); + } + } + StatementKind::Assert { condition, message } => { self.expression(condition, false)?; if let Some(message) = message { self.expression(message, false)?; } } + StatementKind::CheckRefinement { call, .. } => { self.expression(call, false)?; } + StatementKind::Breakpoint { condition, bindings } => { + if bindings.iter().any(|local| self.function.local(*local).is_some_and(|local| resource_type_pending(self.types, local.ty))) { return Err("pending Resource carrier: debugger custody observation transport is unproved".into()); } + if let Some(condition) = condition { self.expression(condition, false)?; } + } + StatementKind::BeginCallView { local, value } => { + if resource_type_pending(self.types, value.ty) || self.function.local(*local).is_some_and(|header| resource_type_pending(self.types, header.ty)) { return Err("ordinary BeginCallView cannot mint a carrier loan".into()); } + self.expression(value, false)?; + } + StatementKind::ResourceCall(_) => return Err("pending Resource carrier: cross-block call requires its complete prepared carrier prefix".into()), + StatementKind::ResourceLexicalExit(_) => return Err("pending Resource carrier: lexical carrier aliases require their exact private exit rows".into()), + StatementKind::IterationBorrow { source, .. } if source.ty(self.function).is_some_and(|ty| carrier_type(self.types, ty)) => return Err("pending Resource carrier: borrowed iteration needs its exact bounded parent lease".into()), + _ => {} + } + } + self.site = ResourceSite { + function: self.function.id, + block: block.id, + position: ResourcePosition::Terminator, + }; + self.ordinal = 0; + self.expression = None; + self.frame = self + .incoming_frames + .iter() + .find(|(site, _)| *site == self.site) + .map(|(_, frame)| *frame) + .ok_or("carrier terminator has no authenticated incoming constructor prefix")?; + self.plan.execution_frames.push((self.site, self.frame)); + match &block.terminator.kind { + TerminatorKind::Return(value) => { + if let Some(value) = value { + let input = self.expression(value, true)?; + if !self.compatible(value.ty, self.function.return_type) { return Err("carrier Return changes its declared exact nominal/state type".into()); } + match input { + ResourceCarrierValue::Owned { slot: source } => { + let frame = self.plan.provisional_return().ok_or("carrier Return has no exact provisional frame")?.id(); + let destination = self.slot(self.function.return_type, ResourceSlotStorage::Return { site: self.site }, frame)?; + self.transfer(source, destination)?; + self.frame = ResourceFrameId(0); + self.finish(ResourceCompletion::Return); + self.carrier_operation(ResourceCarrierOperationRole::PublishReturn { source: destination }); + } + ResourceCarrierValue::LeafOwned { slot: source } => { + let frame = self.plan.provisional_return().ok_or("carrier leaf Return has no exact provisional frame")?.id(); + let destination = self.leaf_slot(self.function.return_type, ResourceSlotStorage::Return { site: self.site }, frame)?; + self.operation(ResourceOperationRole::Transfer { source, destination }); + self.frame = ResourceFrameId(0); + self.finish(ResourceCompletion::Return); + self.operation(ResourceOperationRole::CompleteReturnAfterCleanup { source: destination }); + } + ResourceCarrierValue::Ordinary { .. } => self.finish(ResourceCompletion::Return), + _ => return Err("carrier Return cannot publish a borrowed value".into()), + } + } else { self.finish(ResourceCompletion::Return); } + } + TerminatorKind::Branch { condition, .. } => { self.expression(condition, false)?; } + TerminatorKind::ForEach { key, value, by_view, iterable, body, exit } if carrier_type(self.types, iterable.ty) => { + control_flow::carrier_sequence(self.function, self.types, *key, *value, *by_view, iterable)?; + let input = self.expression(iterable, true)?; + let destination = self.temporary(iterable.ty)?; + self.iteration_slots.insert(block.id.index(), destination); + self.carrier_operation(ResourceCarrierOperationRole::BeginIteration { source: input, destination, body: *body, exit: *exit }); + let mut binders = vec![*key]; + if let Some(value) = value { binders.push(*value); } + self.carriers.iterations.push(ResourceCarrierIteration { header: block.id, body: *body, exit: *exit, source: destination, binders: binders.clone(), cursor: None }); + self.site = ResourceSite { function: self.function.id, block: *body, position: ResourcePosition::Statement(0) }; + self.edge = Some(ResourceCarrierEdge::IterationBody { source: block.id, target: *body }); + for (ordinal, binder) in binders.iter().enumerate() { + let ty = self.function.local(*binder).ok_or("carrier For lost its exact original binder")?.ty; + let source = self.loan(ResourceCarrierValue::Owned { slot: destination }, iterable.ty, Vec::new())?; + let index = ResourceCarrierIndex::CurrentIteration { header: block.id }; + let path = match self.types.resolve(iterable.ty) { + Type::List(_) => ResourceCarrierProjectionPath::Element { index }, + Type::Map(..) if ordinal == 0 => ResourceCarrierProjectionPath::MapKey { index }, + Type::Map(..) => ResourceCarrierProjectionPath::MapValue { index }, + _ => return Err("carrier For lost its checked list/map component".into()), + }; + self.project(ty, source, vec![path], true, Some(*binder))?; + self.end_loan(source); + } + self.edge = None; + self.site = ResourceSite { function: self.function.id, block: block.id, position: ResourcePosition::Terminator }; + } + TerminatorKind::Switch { scrutinee, variants, otherwise } if carrier_type(self.types, scrutinee.ty) => { + let input = self.expression(scrutinee, true)?; + let tag_loan = self.loan(input, scrutinee.ty, Vec::new())?; + self.carrier_operation(ResourceCarrierOperationRole::Observe { source: tag_loan, observation: ResourceCarrierObservation::Tag, target: None }); + self.end_loan(tag_loan); + let Type::Enum(owner) = self.types.resolve(scrutinee.ty) else { return Err("carrier Switch lost its checked enum owner".into()); }; + let mut seen = BTreeSet::new(); + for (variant, target, bindings) in variants { + let index = usize::try_from(variant.index()).map_err(|_| "carrier Switch selector is not representable")?; + let fields = self.types.resolve_enum(*owner).variants.get(index).ok_or("carrier Switch selected a foreign variant")?.fields.iter().map(|(_, ty)| *ty).collect::>(); + if !seen.insert(index) || fields.len() != bindings.len() { return Err("carrier Switch changed its exact selected binder count".into()); } + self.site = ResourceSite { function: self.function.id, block: *target, position: ResourcePosition::Statement(0) }; + self.edge = Some(ResourceCarrierEdge::SwitchVariant { source: block.id, variant: *variant, target: *target }); + for (ordinal, (ty, binding)) in fields.into_iter().zip(bindings).enumerate() { + if self.function.local(*binding).map(|header| header.ty) != Some(ty) { return Err("carrier Switch changed its exact selected binder header".into()); } + let path = vec![ResourceCarrierProjectionPath::Variant { owner: scrutinee.ty, variant: *variant, field: ordinal }]; + // Runtime extraction is selected by the exact Switch edge. + self.site = ResourceSite { function: self.function.id, block: *target, position: ResourcePosition::Statement(0) }; + let source = self.loan(input, scrutinee.ty, Vec::new())?; + self.project(ty, source, path, !self.function.is_view_local(*binding), Some(*binding))?; + self.end_loan(source); + } + if let ResourceCarrierValue::Owned { slot } = input { self.carrier_operation(ResourceCarrierOperationRole::Retire { source: slot }); } + self.edge = None; + self.site = ResourceSite { function: self.function.id, block: block.id, position: ResourcePosition::Terminator }; + } + if let Some(target) = otherwise { + self.site = ResourceSite { function: self.function.id, block: *target, position: ResourcePosition::Statement(0) }; + self.edge = Some(ResourceCarrierEdge::SwitchOtherwise { source: block.id, target: *target }); + if let ResourceCarrierValue::Owned { slot } = input { self.carrier_operation(ResourceCarrierOperationRole::Retire { source: slot }); } + self.edge = None; + self.site = ResourceSite { function: self.function.id, block: block.id, position: ResourcePosition::Terminator }; + } + } + TerminatorKind::Unreachable => self.finish(ResourceCompletion::Abort), + TerminatorKind::Respond(_) | TerminatorKind::ReflectedTypeDispatch { .. } => return Err("pending Resource carrier: this control family needs its independent current-node proof".into()), + _ => {} + } + Ok(()) + } +} + +pub(super) fn analyze( + program: &Program, + function: &Function, + types: &TypeInterner, +) -> Result { + let witness = function + .resource_lowering + .as_ref() + .ok_or("carrier plan has no original checked Source constructor witness")?; + witness.source.validate_types(types)?; + witness.current(function)?; + if function.capture_count != 0 { + return Err("pending Resource carrier: aggregate captures need their exact environment custody transport".into()); + } + let cfg = ControlFlowGraph::analyze(function) + .map_err(|errors| format!("carrier CFG is malformed: {errors:?}"))?; + let site = ResourceSite { + function: function.id, + block: function.entry, + position: ResourcePosition::Terminator, + }; + let returned = resource_type_pending(types, function.return_type) + && !matches!(types.resolve(function.return_type), Type::Function { .. }); + let return_frame = returned.then_some(ResourceFrameId(1)); + let mut frames = vec![ResourceFrame { + id: ResourceFrameId(0), + role: ResourceFrameRole::Scope, + site, + parent: return_frame, + ordinal: 0, + }]; + if let Some(id) = return_frame { + frames.push(ResourceFrame { + id, + role: ResourceFrameRole::Return, + site, + parent: None, + ordinal: 0, + }); + } + let plan = ResourceFunctionPlan { + function: function.id, + identity: function.identity.clone(), + parameters: function.params.clone(), + return_type: function.return_type, + frames, + slots: Vec::new(), + loans: Vec::new(), + operations: Vec::new(), + execution_frames: Vec::new(), + lexical_exits: Vec::new(), + named_callable_producers: Vec::new(), + named_callable_values: named_callables::current_values(witness, function)?, + descriptor_return: witness.descriptors.returned().cloned(), + descriptor_values: returned_descriptors::current_values(witness, function)?, + descriptor_locals: witness + .descriptors + .locals() + .map(|(local, hook)| (local, hook.clone())) + .collect(), + carriers: None, + }; + let mut planner = Planner { + program, + function, + types, + plan, + carriers: ResourceCarrierFunctionPlan { + shapes: graph::capture(&witness.source, &program.resource_manifest, types)?, + slots: Vec::new(), + loans: Vec::new(), + operations: Vec::new(), + iterations: Vec::new(), + }, + carrier_locals: vec![None; function.locals.len()], + leaf_locals: vec![None; function.locals.len()], + site, + expression: None, + ordinal: 0, + frame: ResourceFrameId(0), + call_frames: BTreeMap::new(), + iteration_slots: BTreeMap::new(), + edge: None, + ended_loans: BTreeSet::new(), + constructor_frames: BTreeMap::new(), + constructor_slots: BTreeMap::new(), + incoming_frames: Vec::new(), + }; + for region in &witness.constructors { + let parent = match region.parent() { + None => ResourceFrameId(0), + Some(constructors::ResourceOperationRegion::Constructor(parent)) => *planner.constructor_frames.get(&parent) + .ok_or("carrier constructor has a missing exact parent Operation")?, + Some(constructors::ResourceOperationRegion::Call(_)) => return Err("pending Resource carrier: constructor under a staged Source call needs its authenticated mixed-prefix transport".into()), + }; + planner.site = region.begin(); + planner.frame = parent; + let frame = planner.frame(ResourceFrameRole::Operation); + planner.constructor_frames.insert(region.id(), frame); + let slot = planner.slot( + region.original().ty, + ResourceSlotStorage::Expression { + site: region.begin(), + ordinal: 0, + }, + frame, + )?; + planner.constructor_slots.insert(region.id(), slot); + } + planner.incoming_frames = constructor_frames(function, &cfg, &planner.constructor_frames)?; + for local in &function.locals { + let index = usize::try_from(local.id.index()) + .map_err(|_| "carrier local index is not representable")?; + if carrier_type(types, local.ty) && !function.is_view_local(local.id) { + let slot = planner.slot( + local.ty, + ResourceSlotStorage::Local { + header: local.clone(), + }, + ResourceFrameId(0), + )?; + planner.carrier_locals[index] = Some(ResourceCarrierValue::Owned { slot }); + } else if !carrier_type(types, local.ty) + && shape(&program.resource_manifest, types, local.ty)?.is_some() + && !function.is_view_local(local.id) + { + let slot = planner.leaf_slot( + local.ty, + ResourceSlotStorage::Local { + header: local.clone(), + }, + ResourceFrameId(0), + )?; + planner.leaf_locals[index] = Some(ResourceCarrierValue::LeafOwned { slot }); + } + } + for (parameter, param) in function.params.iter().enumerate() { + if param.mode != ParamMode::View { + continue; + } + let index = usize::try_from(param.local.index()) + .map_err(|_| "carrier formal index is not representable")?; + if carrier_type(types, param.ty) { + let id = ResourceCarrierLoanId(planner.carriers.loans.len()); + let shape = planner.shape_id(param.ty)?; + planner.carriers.loans.push(ResourceCarrierLoan { + id, + frame: ResourceFrameId(0), + shape, + source: ResourceCarrierLoanSource::IncomingViewFormal { + scope: ResourceFrameId(0), + parameter, + }, + path: Vec::new(), + }); + planner.carrier_locals[index] = Some(ResourceCarrierValue::Borrowed { loan: id }); + } else if let Some(shape) = shape(&program.resource_manifest, types, param.ty)? { + let id = ResourceLoanId(planner.plan.loans.len()); + planner.plan.loans.push(ResourceLoan { + id, + frame: ResourceFrameId(0), + shape, + source: ResourceLoanSource::IncomingViewFormal { + scope: ResourceFrameId(0), + parameter, + }, + parameter: None, + }); + planner.leaf_locals[index] = Some(ResourceCarrierValue::LeafBorrowed { loan: id }); + } + } + let mut reachable = vec![false; function.blocks.len()]; + let mut pending = vec![function.entry]; + while let Some(block) = pending.pop() { + let index = usize::try_from(block.index()) + .map_err(|_| "carrier block index is not representable")?; + if reachable[index] { + continue; + } + reachable[index] = true; + pending.extend_from_slice(cfg.successors(block)); + } + for block in &function.blocks { + if reachable[usize::try_from(block.id.index()) + .map_err(|_| "carrier block index is not representable")?] + { + planner.block(block)?; + } else if walk::mir_block_has_resource(block) + || walk::mir_block_has_custody(block, function, types) + { + return Err( + "carrier custody operation is disconnected from its authenticated entry CFG".into(), + ); + } + } + // Prepared sequence rows are accepted only after their complete canonical + // current loop and original Source For tuple have been joined privately. + let prepared = iterations::capture(function, types, &planner.carriers)?; + planner.carriers.iterations.extend(prepared); + let resets = planner.carriers.iterations.clone(); + for iteration in resets { + planner.site = ResourceSite { + function: function.id, + block: iteration.header, + position: ResourcePosition::Terminator, + }; + planner.frame = ResourceFrameId(0); + planner.expression = None; + planner.edge = None; + planner.carrier_operation(ResourceCarrierOperationRole::RetireIteration { + source: iteration.source, + binders: iteration.binders, + }); + } + flow::validate(function, types, &planner.plan, &planner.carriers)?; + planner.plan.carriers = Some(planner.carriers); + Ok(planner.plan) +} + +/// Reconstruct active constructor prefixes through actual CFG edges. Numeric +/// block order and a public marker are never frame authority. +fn constructor_frames( + function: &Function, + cfg: &ControlFlowGraph, + frames: &BTreeMap, +) -> Result, String> { + let mut incoming: Vec>> = vec![None; function.blocks.len()]; + incoming[function.entry.index() as usize] = Some(Vec::new()); + let mut queue = std::collections::VecDeque::from([function.entry]); + let mut sites = Vec::new(); + while let Some(id) = queue.pop_front() { + let block = function + .blocks + .get(id.index() as usize) + .filter(|b| b.id == id) + .ok_or("carrier constructor CFG selected a foreign block")?; + let mut active = incoming[id.index() as usize] + .clone() + .ok_or("carrier constructor CFG lost its incoming prefix")?; + let frame = |active: &[ResourceConstructorId]| -> Result { + active + .last() + .map(|id| { + frames + .get(id) + .copied() + .ok_or_else(|| "carrier constructor prefix lost its private frame".into()) + }) + .unwrap_or(Ok(ResourceFrameId(0))) + }; + for (index, statement) in block.statements.iter().enumerate() { + let site = ResourceSite { + function: function.id, + block: id, + position: ResourcePosition::Statement(index), + }; + sites.push((site, frame(&active)?)); + if let StatementKind::ResourceConstructor(node) = &statement.kind { + let region = function.resource_constructor(node.constructor())?; + match node { + ResourceConstructorNode::Begin { constructor } => { + let expected = active + .last() + .copied() + .map(constructors::ResourceOperationRegion::Constructor); + if region.begin() != site + || region.parent() != expected + || active.contains(constructor) + { + return Err("carrier constructor Begin differs from its exact active parent prefix".into()); + } + active.push(*constructor); + } + ResourceConstructorNode::End { + constructor, + outcome, + } => { + if active.pop() != Some(*constructor) + || !region.exits().contains(&(site, *outcome)) + { + return Err( + "carrier constructor End is not the exact active suffix".into() + ); + } + } + _ if active.last().copied() != Some(node.constructor()) => { + return Err( + "carrier constructor field or Commit has no reached exact prefix" + .into(), + ); + } + _ => {} + } + } + } + sites.push(( + ResourceSite { + function: function.id, + block: id, + position: ResourcePosition::Terminator, + }, + frame(&active)?, + )); + if matches!( + block.terminator.kind, + TerminatorKind::Return(_) | TerminatorKind::Unreachable + ) && !active.is_empty() + { + return Err("carrier terminal path leaves an uncompleted constructor prefix".into()); + } + for successor in cfg.successors(id) { + let next = incoming + .get_mut(successor.index() as usize) + .ok_or("carrier constructor CFG has a foreign successor")?; + match next { + Some(previous) if *previous != active => { + return Err("carrier constructor CFG joins different operation prefixes".into()); + } + Some(_) => {} + None => { + *next = Some(active.clone()); + queue.push_back(*successor); + } + } + } + } + Ok(sites) +} diff --git a/crates/jett_mir/src/resource_ownership/carriers_tests.rs b/crates/jett_mir/src/resource_ownership/carriers_tests.rs new file mode 100644 index 00000000..a2531b78 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/carriers_tests.rs @@ -0,0 +1,1731 @@ +use super::carriers::{ + ResourceCarrierConstructor, ResourceCarrierEdge, ResourceCarrierFunctionPlan, + ResourceCarrierIndex, ResourceCarrierLoanSource, ResourceCarrierNode, + ResourceCarrierObservation, ResourceCarrierOperationRole, ResourceCarrierProjectionPath, + ResourceCarrierShape, ResourceCarrierShapeId, +}; +use super::tests::{SUPPORT, checked_support}; +use super::*; +use jett_comptime::checked_types::CheckedExpressionTypes; +use jett_typecheck::CheckedResourceProgram; +use std::{collections::HashMap, sync::Arc}; + +const SOURCE: &str = include_str!("fixtures/17_absent_aggregate_shapes.jett"); +const CONSTRUCTORS: [&str; 12] = [ + "empty_tokens", + "absent_tokens", + "empty_map", + "absent_map", + "empty_result_tokens", + "failed_result_tokens", + "absent_envelope", + "absent_box", + "empty_choice", + "absent_choice", + "absent_state", + "absent_exact_state", +]; +const CONSUMING_SUFFIX: &str = r#" +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false +export function consuming_absent_list() returns int64: + list[optional[resource_probe.TestHandle]] values = absent_tokens() + mutable int64 absent_count = 0 + for candidate in values: + if source17_absent_optional(view candidate): + absent_count = absent_count + 1 + return absent_count +export function consuming_absent_map() returns int64: + map[string, optional[resource_probe.TestHandle]] values = absent_map() + mutable int64 absent_count = 0 + for key, candidate in values: + if key != "empty": + return -1 + if source17_absent_optional(view candidate): + absent_count = absent_count + 1 + return absent_count +export function borrowed_absent_fallback() returns int64: + TokenEnvelope value = absent_envelope() + if source17_absent_optional(view value.fallback): + return value.marker + return 0 +export function matching_absent_choice() returns int64: + TokenChoice value = absent_choice() + match value: + vacant(marker, tokens): + return marker + other: + return 0 + return -1 +"#; + +fn materialized(checked: &Arc) -> hir::Program { + let expression_types = Arc::new(CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let required = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + expression_types, + Arc::new(HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "{:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + required.values.checked_required_values(checked).unwrap(); + assert!(!required.values.is_empty()); + assert!( + required + .values + .values() + .all(|value| matches!(value, jett_comptime::Value::Int64(17))) + ); + let mut program = hir::lower_checked_resource_program(checked).unwrap(); + hir::materialize_checked_required_values( + &mut program, + &required.values, + &checked.checked().interner, + ) + .unwrap(); + hir::complete_value_conversions(&mut program, &checked.checked().interner).unwrap(); + program +} + +fn lowered(checked: &Arc) -> Program { + let program = materialized(checked); + lower(&program, &checked.checked().interner).unwrap() +} + +fn named<'a>(plan: &'a ResourceOwnershipPlan<'_>, name: &str) -> &'a ResourceFunctionPlan { + plan.functions() + .iter() + .find(|function| { + function.identity().declaration.namespace == "app" + && function.identity().declaration.name == name + }) + .unwrap_or_else(|| panic!("retained app.{name} Resource plan")) +} + +fn function<'a>(program: &'a Program, name: &str) -> &'a Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap_or_else(|| panic!("retained app.{name} MIR function")) +} + +fn function_mut<'a>(program: &'a mut Program, name: &str) -> &'a mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap_or_else(|| panic!("retained app.{name} MIR function")) +} + +fn carrier(plan: &ResourceFunctionPlan) -> &ResourceCarrierFunctionPlan { + plan.carriers() + .expect("typed aggregate transport, including latent occupied arms") +} + +fn result_shape(plan: &ResourceFunctionPlan) -> &ResourceCarrierShape { + carrier(plan).shape_for_type(plan.return_type()).unwrap() +} + +fn child(plan: &ResourceCarrierFunctionPlan, id: ResourceCarrierShapeId) -> &ResourceCarrierShape { + plan.shape(id) + .expect("a checked graph edge retains its exact target type") +} + +fn consuming_program(release: bool) -> (Arc, Program) { + let source = format!("{SOURCE}{CONSUMING_SUFFIX}"); + assert!(source.starts_with(SOURCE)); + let checked = checked_support(&source, SUPPORT, release); + let program = lowered(&checked); + (checked, program) +} + +fn original_for( + program: &Program, + name: &str, +) -> (BlockId, BlockId, BlockId, Vec, TypeId) { + function(program, name) + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + TerminatorKind::ForEach { + key, + value, + iterable, + body, + exit, + .. + } => { + let mut binders = vec![*key]; + if let Some(value) = value { + binders.push(*value); + } + Some((block.id, *body, *exit, binders, iterable.ty)) + } + _ => None, + }) + .expect("one original consuming For header") +} + +fn assert_leaf_binder(plan: &ResourceFunctionPlan, function: &Function, local: LocalId) { + let header = function.local(local).unwrap(); + assert!( + plan.owner_slots() + .iter() + .any(|slot| matches!(slot.storage(), + ResourceSlotStorage::Local { header: captured } if captured == header + ) && matches!(slot.shape(), ResourceShape::Optional { .. })), + "the original Optional[Resource] binder needs its exact dedicated leaf-sum slot" + ); +} + +fn assert_optional_view_call(plan: &ResourceFunctionPlan, helper: FunctionId) { + let loan = plan + .operations() + .iter() + .find_map(|operation| match operation.role() { + ResourceOperationRole::InvokeSourceFunction { + function, operands, .. + } if *function == helper => match operands.as_slice() { + [ResourceCallOperand::Borrowed { parameter: 0, loan }] => Some(*loan), + _ => None, + }, + _ => None, + }) + .expect("the real Optional view helper uses the bounded leaf-sum call protocol"); + assert!(matches!( + plan.loans()[loan.index()].source(), + ResourceLoanSource::Owner(_) + )); + assert!( + plan.operations() + .iter() + .any(|operation| matches!(operation.role(), + ResourceOperationRole::BorrowSum { loan: actual } if *actual == loan + )) + ); + assert!( + plan.operations() + .iter() + .any(|operation| matches!(operation.role(), + ResourceOperationRole::EndSumBorrow { loan: actual } if *actual == loan + )) + ); +} + +#[test] +fn resource_carrier_consuming_source17_for_rows_preserve_raw_and_prepared_cursor_and_binder_transport() + { + for release in [false, true] { + let (checked, raw) = consuming_program(release); + let types = &checked.checked().interner; + let helper = function(&raw, "source17_absent_optional").id; + let raw_plan = validate_resource_ownership(&raw, types).unwrap(); + let mut prepared = raw.clone(); + crate::prepare_native_sequences(&mut prepared, types); + crate::validate(&prepared).unwrap(); + let prepared_plan = validate_resource_ownership(&prepared, types).unwrap(); + for name in ["consuming_absent_list", "consuming_absent_map"] { + let original = original_for(&raw, name); + let raw_function = named(&raw_plan, name); + let raw_carriers = carrier(raw_function); + let [raw_iteration] = raw_carriers.iterations() else { + panic!("one original For iteration row") + }; + assert_eq!( + (raw_iteration.header, raw_iteration.body, raw_iteration.exit), + (original.0, original.1, original.2) + ); + assert_eq!(raw_iteration.binders, original.3); + assert_eq!(raw_iteration.cursor, None); + assert_eq!( + child( + raw_carriers, + raw_carriers.slots()[raw_iteration.source.index()].shape() + ) + .ty(), + original.4 + ); + let raw_edge = ResourceCarrierEdge::IterationBody { + source: original.0, + target: original.1, + }; + assert!( + raw_carriers.operations_on_edge(raw_edge).any( + |operation| matches!(operation.role(), + ResourceCarrierOperationRole::AdaptSum { path, loan: None, .. } + if matches!(path.as_slice(), [ResourceCarrierProjectionPath::Element { + index: ResourceCarrierIndex::CurrentIteration { header } + }] | [ResourceCarrierProjectionPath::MapValue { + index: ResourceCarrierIndex::CurrentIteration { header } + }] if *header == original.0) + ) + ), + "raw consuming For extracts an absent leaf-sum shell on its exact selected body edge" + ); + assert!( + raw_function.loans().iter().all(|loan| !matches!( + loan.source(), + ResourceLoanSource::CarrierSumProjection { .. } + )), + "an owned For binder is an owner, not a borrowed carrier fallback" + ); + let prepared_function = named(&prepared_plan, name); + let graph = carrier(prepared_function); + let [iteration] = graph.iterations() else { + panic!("one prepared For iteration row") + }; + assert_eq!( + (iteration.header, iteration.body, iteration.exit), + (original.0, original.1, original.2) + ); + assert_eq!(iteration.binders, original.3); + let cursor = iteration + .cursor + .expect("the exact canonical sequence cursor survives in the carrier plan"); + let current = function(&prepared, name); + assert_eq!(current.local(cursor).unwrap().ty, TypeInterner::INT64); + assert_eq!( + child(graph, graph.slots()[iteration.source.index()].shape()).ty(), + original.4 + ); + let gets = current.blocks[usize::try_from(iteration.body.index()).unwrap()] + .statements + .iter() + .filter_map(|statement| match &statement.kind { + StatementKind::SequenceGet { + source, + index, + target, + part, + .. + } => Some((source, index, target, part)), + _ => None, + }) + .collect::>(); + assert_eq!(gets.len(), iteration.binders.len()); + for ((source, index, target, _), binder) in gets.iter().zip(&iteration.binders) { + assert_eq!(**index, cursor); + assert_eq!(**target, *binder); + assert_eq!(source.ty(current), Some(original.4)); + } + if name == "consuming_absent_map" { + assert_eq!(*gets[0].3, SequencePart::Key); + assert_eq!(*gets[1].3, SequencePart::Value); + assert_eq!( + current.local(iteration.binders[0]).unwrap().ty, + TypeInterner::STRING + ); + } else { + assert_eq!(*gets[0].3, SequencePart::Element); + } + assert!( + !graph.operations().iter().any(|operation| matches!( + operation.edge(), + Some(ResourceCarrierEdge::IterationBody { .. }) + )), + "prepared extraction runs at canonical SequenceGet statements" + ); + assert!( + graph + .operations() + .iter() + .any(|operation| matches!(operation.role(), + ResourceCarrierOperationRole::AdaptSum { path, loan: None, .. } + if matches!(path.as_slice(), [ResourceCarrierProjectionPath::Element { + index: ResourceCarrierIndex::Local(actual) + }] | [ResourceCarrierProjectionPath::MapValue { + index: ResourceCarrierIndex::Local(actual) + }] if *actual == cursor) + )) + ); + let binder = *iteration.binders.last().unwrap(); + assert_leaf_binder( + raw_function, + function(&raw, name), + *raw_iteration.binders.last().unwrap(), + ); + assert_leaf_binder(prepared_function, current, binder); + assert_optional_view_call(raw_function, helper); + assert_optional_view_call(prepared_function, helper); + assert!(prepared_function.loans().iter().all(|loan| !matches!( + loan.source(), + ResourceLoanSource::CarrierSumProjection { .. } + ))); + } + } +} + +#[test] +fn resource_carrier_source17_borrowed_fallback_adapts_a_parent_bound_sum_loan() { + for release in [false, true] { + let (checked, raw) = consuming_program(release); + let types = &checked.checked().interner; + for prepare in [false, true] { + let mut program = raw.clone(); + if prepare { + crate::prepare_native_sequences(&mut program, types); + } + let plan = validate_resource_ownership(&program, types).unwrap(); + let function = named(&plan, "borrowed_absent_fallback"); + let graph = carrier(function); + let owner_type = self::function(&program, "absent_envelope").return_type; + let projected = function + .loans() + .iter() + .filter_map(|loan| match loan.source() { + ResourceLoanSource::CarrierSumProjection { operation } => { + Some((loan, operation)) + } + _ => None, + }) + .collect::>(); + let [(loan, operation)] = projected.as_slice() else { + panic!("one exact borrowed fallback adapter") + }; + let adapter = &graph.operations()[operation.index()]; + let ResourceCarrierOperationRole::AdaptSum { + source, + path, + loan: adapted, + lease_frame, + .. + } = adapter.role() + else { + panic!("normal leaf-sum loan is issued only by its carrier projection operation") + }; + assert_eq!(*adapted, Some(loan.id())); + assert_eq!(*lease_frame, loan.frame()); + assert_eq!(adapter.frame(), loan.frame()); + assert!( + matches!(path.as_slice(), [ResourceCarrierProjectionPath::Field { owner, field }] + if *owner == owner_type && field.index() == 1) + ); + assert_eq!( + child(graph, graph.loans()[source.index()].shape()).ty(), + owner_type + ); + let helper = self::function(&program, "source17_absent_optional").id; + assert!(function.operations().iter().any(|operation| matches!(operation.role(), + ResourceOperationRole::InvokeSourceFunction { function: callee, operands, .. } + if *callee == helper && matches!(operands.as_slice(), + [ResourceCallOperand::Borrowed { parameter: 0, loan: actual }] if *actual == loan.id()) + ))); + } + } +} + +#[test] +fn resource_carrier_source17_vacant_match_extracts_exact_ordinary_and_carrier_binders_on_the_selected_edge() + { + for release in [false, true] { + let (checked, raw) = consuming_program(release); + let types = &checked.checked().interner; + for prepare in [false, true] { + let mut program = raw.clone(); + if prepare { + crate::prepare_native_sequences(&mut program, types); + } + let plan = validate_resource_ownership(&program, types).unwrap(); + let mir = function(&program, "matching_absent_choice"); + let (header, choice_type, variant, target, bindings) = mir + .blocks + .iter() + .find_map(|block| { + let TerminatorKind::Switch { + scrutinee, + variants, + .. + } = &block.terminator.kind + else { + return None; + }; + variants + .iter() + .find(|(variant, _, _)| variant.index() == 1) + .map(|(variant, target, bindings)| { + (block.id, scrutinee.ty, *variant, *target, bindings) + }) + }) + .unwrap(); + let [marker, tokens] = bindings.as_slice() else { + panic!("vacant's two original typed binders") + }; + assert_eq!(mir.local(*marker).unwrap().ty, TypeInterner::INT64); + assert!(matches!( + types.resolve(mir.local(*tokens).unwrap().ty), + Type::List(_) + )); + let graph = carrier(named(&plan, "matching_absent_choice")); + let edge = ResourceCarrierEdge::SwitchVariant { + source: header, + variant, + target, + }; + assert!(graph.operations_on_edge(edge).any(|operation| matches!(operation.role(), + ResourceCarrierOperationRole::ExtractOrdinary { path, target: actual, ty, .. } + if *actual == *marker && *ty == TypeInterner::INT64 + && matches!(path.as_slice(), [ResourceCarrierProjectionPath::Variant { owner, variant: selected, field: 0 }] + if *owner == choice_type && *selected == variant) + ))); + assert!(graph.operations_on_edge(edge).any(|operation| matches!(operation.role(), + ResourceCarrierOperationRole::Extract { path, destination, .. } + if matches!(path.as_slice(), [ResourceCarrierProjectionPath::Variant { owner, variant: selected, field: 1 }] + if *owner == choice_type && *selected == variant) + && matches!(graph.slots()[destination.index()].storage(), + ResourceSlotStorage::Local { header } if header == mir.local(*tokens).unwrap()) + )), "vacant tokens are a typed owned carrier binder even when their selected list is empty"); + } + } +} + +#[test] +fn resource_carrier_prepared_consuming_sequence_mutations_cannot_reuse_the_original_transport_seal() +{ + for release in [false, true] { + let (checked, mut program) = consuming_program(release); + let types = &checked.checked().interner; + validate_resource_ownership(&program, types).unwrap(); + crate::prepare_native_sequences(&mut program, types); + validate_resource_ownership(&program, types).unwrap(); + for mutation in 0..5 { + let mut changed = program.clone(); + let function = function_mut(&mut changed, "consuming_absent_map"); + let original_source = function + .locals + .iter() + .find(|local| local.name == "values") + .unwrap() + .id; + let count = function + .locals + .iter() + .find(|local| local.name == "absent_count") + .unwrap() + .id; + let body = function + .blocks + .iter_mut() + .find(|block| { + block.statements.iter().any(|statement| { + matches!( + statement.kind, + StatementKind::SequenceGet { + part: SequencePart::Value, + .. + } + ) + }) + }) + .unwrap(); + let key_position = body + .statements + .iter() + .position(|statement| { + matches!( + statement.kind, + StatementKind::SequenceGet { + part: SequencePart::Key, + .. + } + ) + }) + .unwrap(); + let value_position = body + .statements + .iter() + .position(|statement| { + matches!( + statement.kind, + StatementKind::SequenceGet { + part: SequencePart::Value, + .. + } + ) + }) + .unwrap(); + if mutation == 2 { + body.statements.swap(key_position, value_position); + } else { + let key_target = match body.statements[key_position].kind { + StatementKind::SequenceGet { target, .. } => target, + _ => unreachable!(), + }; + let StatementKind::SequenceGet { + consume, + source, + index, + target, + .. + } = &mut body.statements[value_position].kind + else { + unreachable!() + }; + match mutation { + 0 => *index = count, + 1 => *source = SequenceSource::Local(original_source), + 3 => *consume = false, + 4 => *target = key_target, + _ => unreachable!(), + } + } + assert!( + validate_resource_ownership(&changed, types).is_err(), + "prepared index/source/key-value order/consume/binder mutation {mutation} cannot issue a new carrier proof" + ); + } + } +} + +#[test] +fn resource_carrier_whole_source17_preserves_all_exported_constructor_shapes() { + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + crate::validate(&program).unwrap(); + let plan = validate_resource_ownership(&program, types).unwrap(); + for name in CONSTRUCTORS { + let function = named(&plan, name); + let graph = carrier(function); + let shape = result_shape(function); + assert_eq!(shape.ty(), self::function(&program, name).return_type); + assert!( + shape.contains_resource(), + "empty values retain the type of {name}" + ); + assert!( + graph.operations().iter().any(|operation| matches!( + operation.role(), + ResourceCarrierOperationRole::PublishReturn { .. } + )), + "{name} publishes an exact typed return root" + ); + } + // This unused public view formal must not disappear because the selected + // constructor examples have no occupied children. Required controls also + // remain in the source family after real required-value materialization. + function(&program, "refined_shape_contract"); + function(&program, "absent_required_controls"); + let first = carrier(named(&plan, "empty_tokens")); + let second = carrier(named(&plan, "absent_exact_state")); + for shape in first.shapes() { + let same = second.shape_for_type(shape.ty()).unwrap(); + assert_eq!(shape.id(), same.id()); + assert_eq!(shape.node(), same.node()); + assert_eq!(shape.contains_resource(), same.contains_resource()); + } + } +} + +#[test] +fn resource_carrier_source17_collection_edges_keep_resource_and_ordinary_sum_arms() { + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let program = lowered(&checked); + let plan = validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let empty = named(&plan, "empty_tokens"); + let ResourceCarrierNode::List { element } = result_shape(empty).node() else { + panic!("empty_tokens keeps list[TestHandle]"); + }; + let ResourceCarrierNode::Resource { kind } = child(carrier(empty), *element).node() else { + panic!("empty list retains the original Resource leaf kind"); + }; + assert_eq!( + Some(kind.clone()), + program.resource_manifest.kind_for_type(kind.ty()) + ); + let resource_ty = kind.ty(); + let absent = named(&plan, "absent_tokens"); + let ResourceCarrierNode::List { element } = result_shape(absent).node() else { + panic!("absent_tokens keeps list[optional[TestHandle]]"); + }; + let ResourceCarrierNode::Optional { payload } = child(carrier(absent), *element).node() + else { + panic!("two None elements retain Optional layout"); + }; + assert_eq!(child(carrier(absent), *payload).ty(), resource_ty); + let absent_map = named(&plan, "absent_map"); + let ResourceCarrierNode::Map { key, value } = result_shape(absent_map).node() else { + panic!("absent_map keeps both key and value types"); + }; + assert_eq!(child(carrier(absent_map), *key).ty(), TypeInterner::STRING); + assert!(!child(carrier(absent_map), *key).contains_resource()); + assert!(matches!( + child(carrier(absent_map), *value).node(), + ResourceCarrierNode::Optional { .. } + )); + for name in ["empty_result_tokens", "failed_result_tokens"] { + let result = named(&plan, name); + let ResourceCarrierNode::Result { success, failure } = result_shape(result).node() + else { + panic!("both selected result arms retain one result type"); + }; + assert!(matches!( + child(carrier(result), *success).node(), + ResourceCarrierNode::List { .. } + )); + assert!(child(carrier(result), *success).contains_resource()); + assert_eq!(child(carrier(result), *failure).ty(), TypeInterner::STRING); + assert!(!child(carrier(result), *failure).contains_resource()); + } + assert_eq!( + result_shape(named(&plan, "empty_result_tokens")).id(), + result_shape(named(&plan, "failed_result_tokens")).id() + ); + } +} + +#[test] +fn resource_carrier_source17_nominal_generic_inactive_and_refined_layouts_are_complete() { + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + let plan = validate_resource_ownership(&program, types).unwrap(); + let envelope = named(&plan, "absent_envelope"); + let Type::Struct(expected_owner) = types.resolve(envelope.return_type()) else { + panic!("checked TokenEnvelope nominal owner"); + }; + let ResourceCarrierNode::Struct { + owner, + arguments, + fields, + } = result_shape(envelope).node() + else { + panic!("TokenEnvelope is not erased into an ordinary record"); + }; + assert_eq!(owner, expected_owner); + assert!(arguments.is_empty()); + assert_eq!( + fields + .iter() + .map(|field| (field.ordinal, field.name.as_str())) + .collect::>(), + [(0, "tokens"), (1, "fallback"), (2, "marker")] + ); + assert!(child(carrier(envelope), fields[0].shape).contains_resource()); + assert!(child(carrier(envelope), fields[1].shape).contains_resource()); + assert_eq!( + child(carrier(envelope), fields[2].shape).ty(), + TypeInterner::INT64 + ); + let boxed = named(&plan, "absent_box"); + let ResourceCarrierNode::Struct { + arguments, fields, .. + } = result_shape(boxed).node() + else { + panic!("concrete TokenBox[TestHandle] layout"); + }; + assert_eq!(arguments.len(), 1); + assert!(matches!( + child(carrier(boxed), arguments[0]).node(), + ResourceCarrierNode::Resource { .. } + )); + assert_eq!(fields.len(), 1); + assert_eq!(fields[0].name, "items"); + assert!(matches!( + child(carrier(boxed), fields[0].shape).node(), + ResourceCarrierNode::List { .. } + )); + let choice = named(&plan, "empty_choice"); + let ResourceCarrierNode::Enum { + owner, variants, .. + } = result_shape(choice).node() + else { + panic!("empty TokenChoice retains every variant"); + }; + let Type::Enum(expected_owner) = types.resolve(choice.return_type()) else { + panic!("checked TokenChoice nominal owner"); + }; + assert_eq!(owner, expected_owner); + assert_eq!( + variants + .iter() + .map(|variant| (variant.ordinal, variant.name.as_str())) + .collect::>(), + [(0, "empty"), (1, "vacant"), (2, "occupied")] + ); + assert_eq!( + variants + .iter() + .map(|variant| variant.discriminant) + .collect::>(), + [0, 1, 2] + ); + assert!(variants[0].fields.is_empty()); + assert_eq!(variants[1].fields.len(), 2); + assert_eq!(variants[1].fields[0].name, "marker"); + assert_eq!(variants[1].fields[1].name, "tokens"); + assert!(child(carrier(choice), variants[1].fields[1].shape).contains_resource()); + assert!(matches!( + child(carrier(choice), variants[2].fields[0].shape).node(), + ResourceCarrierNode::Resource { .. } + )); + let state = named(&plan, "absent_state"); + let ResourceCarrierNode::Machine { + states, + transitions, + .. + } = result_shape(state).node() + else { + panic!("unqualified TokenState retains every state"); + }; + assert_eq!( + states + .iter() + .map(|state| (state.ordinal, state.name.as_str())) + .collect::>(), + [(0, "vacant"), (1, "waiting"), (2, "occupied")] + ); + assert_eq!(transitions, &[(0, 1), (0, 2)]); + assert_eq!(states[0].fields[0].name, "fallback"); + assert_eq!(states[0].fields[1].name, "marker"); + assert!(matches!( + child(carrier(state), states[2].fields[0].shape).node(), + ResourceCarrierNode::Resource { .. } + )); + let exact = named(&plan, "absent_exact_state"); + let ResourceCarrierNode::MachineState { + machine, + state: qualified, + } = result_shape(exact).node() + else { + panic!("TokenState at vacant retains its qualification"); + }; + assert_eq!(*qualified, 0); + assert_eq!(child(carrier(exact), *machine).ty(), state.return_type()); + assert_ne!(exact.return_type(), state.return_type()); + let refined = named(&plan, "refined_shape_contract"); + assert_eq!(refined.parameters()[0].mode, ParamMode::View); + let graph = carrier(refined); + let shape = graph.shape_for_type(refined.parameters()[0].ty).unwrap(); + let ResourceCarrierNode::Struct { fields, .. } = shape.node() else { + panic!("unused RefinedEnvelope view formal retains its original fields"); + }; + assert_eq!( + fields + .iter() + .map(|field| field.name.as_str()) + .collect::>(), + ["marker", "tokens"] + ); + let ResourceCarrierNode::Refinement { name, base } = child(graph, fields[0].shape).node() + else { + panic!("PositiveMarker is not widened to int64"); + }; + assert!(name.ends_with("PositiveMarker")); + assert_eq!(child(graph, *base).ty(), TypeInterner::INT64); + assert!(!child(graph, fields[0].shape).contains_resource()); + let predicate = child(graph, fields[0].shape) + .refinement_predicate() + .expect("the refinement graph retains its original checked predicate body"); + assert!(hir::refinement_predicate_declaration_matches( + &predicate.identity.declaration, + name + )); + assert_eq!(predicate.return_type, TypeInterner::BOOL); + assert_eq!(predicate.params.len(), 1); + assert_eq!(predicate.params[0].ty, TypeInterner::INT64); + let archived = function(&program, "refined_shape_contract") + .resource_lowering + .as_ref() + .unwrap() + .source + .execution_functions() + .iter() + .find(|function| function.id == predicate.id) + .unwrap(); + assert!(crate::breakpoint_regions::hir_blocks_equal( + &predicate.body, + &archived.body + )); + assert_eq!(predicate.locals, archived.locals); + assert!(child(graph, fields[1].shape).contains_resource()); + assert!(graph.loans().iter().any(|loan| matches!( + loan.source(), + ResourceCarrierLoanSource::IncomingViewFormal { parameter: 0, .. } + ))); + assert!( + !graph.operations().iter().any(|operation| matches!( + operation.role(), + ResourceCarrierOperationRole::BeginConstructor { .. } + )), + "an unused view formal is metadata plus a bounded lease, never a minted owner" + ); + } +} + +#[test] +fn resource_carrier_constructor_children_keep_lexical_order_and_exact_current_occurrences() { + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let program = lowered(&checked); + let plan = validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let envelope = named(&plan, "absent_envelope"); + let graph = carrier(envelope); + let destination = graph + .operations() + .iter() + .find_map(|operation| match operation.role() { + ResourceCarrierOperationRole::BeginConstructor { + destination, + constructor: ResourceCarrierConstructor::Struct, + } => Some(*destination), + _ => None, + }) + .unwrap(); + let children = graph + .operations() + .iter() + .filter_map(|operation| match operation.role() { + ResourceCarrierOperationRole::ConstructorChild { + destination: actual, + child, + } if *actual == destination => Some(child.index), + _ => None, + }) + .collect::>(); + assert_eq!(children, [0, 1, 2]); + let began = graph.operations().iter().position(|operation| matches!(operation.role(), + ResourceCarrierOperationRole::BeginConstructor { destination: actual, .. } if *actual == destination + )).unwrap(); + let committed = graph.operations().iter().position(|operation| matches!(operation.role(), + ResourceCarrierOperationRole::CommitConstructor { destination: actual } if *actual == destination + )).unwrap(); + assert!(began < committed); + let current = function(&program, "absent_envelope"); + let region = staged_constructor(current); + assert_eq!(region.evaluation_order(), [0, 1, 2]); + assert!(matches!( + region.original().kind, + hir::ExpressionKind::StructConstruct { .. } + )); + let (commit, _) = region.commit().unwrap(); + assert!(graph.operations_at(commit).any(|operation| matches!( + operation.role(), ResourceCarrierOperationRole::CommitConstructor { destination: actual } + if *actual == destination + ))); + for field in region.fields() { + assert!(graph.operations_at(field.stage()).any(|operation| matches!( + operation.role(), ResourceCarrierOperationRole::ConstructorChild { destination: actual, child } + if *actual == destination && child.index == field.field() + ))); + } + let first = ®ion.fields()[0]; + let ResourcePosition::Statement(index) = first.stage().position() else { + unreachable!() + }; + let StatementKind::ResourceConstructor(ResourceConstructorNode::Stage { + value: expression, + .. + }) = ¤t.blocks[first.stage().block().index() as usize].statements[index].kind + else { + unreachable!() + }; + assert!( + graph.operations_for_expression(expression).next().is_some(), + "the exact current list field retains its constructor-issued expression occurrence" + ); + let copied = (*expression).clone(); + assert_eq!( + graph.operations_for_expression(&copied).count(), + 0, + "a copied public expression has no constructor-issued current occurrence" + ); + for operation in graph.operations() { + assert!( + envelope + .frames() + .iter() + .any(|frame| frame.id() == operation.frame()) + ); + assert_eq!(operation.site().function(), envelope.function()); + } + } +} + +fn staged_constructor(function: &Function) -> &ResourceConstructorRegion { + let ids = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| match &statement.kind { + StatementKind::ResourceConstructor(ResourceConstructorNode::Begin { constructor }) => { + Some(*constructor) + } + _ => None, + }) + .collect::>(); + let [id] = ids.as_slice() else { + panic!("one exact staged struct constructor") + }; + function.resource_constructor(*id).unwrap() +} + +#[test] +fn resource_carrier_source_call_owned_view_and_ordinary_projection_keep_original_formal_tuples() { + const SUFFIX: &str = r#" +function forward_envelope(value: TokenEnvelope) returns TokenEnvelope: + return value +function inspect_envelope(view value: TokenEnvelope) returns int64: + return value.marker +export function carrier_transport() returns int64: + TokenEnvelope initial = absent_envelope() + int64 marker = inspect_envelope(view initial) + TokenEnvelope forwarded = forward_envelope(initial) + int64 second = inspect_envelope(view forwarded) + return marker + second +"#; + for release in [false, true] { + let source = format!("{SOURCE}{SUFFIX}"); + assert!(source.starts_with(SOURCE)); + let checked = checked_support(&source, SUPPORT, release); + let program = lowered(&checked); + let plan = validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let scenario = named(&plan, "carrier_transport"); + let owning = function(&program, "forward_envelope").id; + let viewing = function(&program, "inspect_envelope").id; + let envelope_type = function(&program, "absent_envelope").return_type; + let owning_call = scenario + .operations() + .iter() + .find_map(|operation| match operation.role() { + ResourceOperationRole::InvokeSourceFunction { + function, + source, + formals, + operands, + result, + .. + } if *function == owning => Some((source, formals, operands, result)), + _ => None, + }) + .unwrap(); + assert_eq!(owning_call.0.arguments.len(), 1); + assert_eq!(owning_call.1.len(), 1); + assert_eq!(owning_call.1[0].parameter(), 0); + assert_eq!(owning_call.1[0].source_index(), 0); + assert_eq!(owning_call.1[0].actual_type(), envelope_type); + assert_eq!(owning_call.1[0].parameter_type(), envelope_type); + assert_eq!(owning_call.1[0].access(), ParamMode::Owned); + assert_eq!(owning_call.1[0].syntax(), ResourceArgumentSyntax::Bare); + assert_eq!( + owning_call.1[0].effect(), + ResourceArgumentEffect::TransferOwned + ); + assert!(matches!( + owning_call.2.as_slice(), + [ResourceCallOperand::CarrierOwned { parameter: 0, .. }] + )); + assert!(matches!(owning_call.3, ResourceCallResult::Carrier { .. })); + let view_calls = scenario + .operations() + .iter() + .filter_map(|operation| match operation.role() { + ResourceOperationRole::InvokeSourceFunction { + function, + source, + formals, + operands, + .. + } if *function == viewing => Some((source, formals, operands)), + _ => None, + }) + .collect::>(); + assert_eq!(view_calls.len(), 2); + for (source, formals, operands) in view_calls { + assert_eq!(source.arguments.len(), 1); + assert_eq!( + source.arguments[0].effect, + jett_typecheck::CheckedCallerEffect::RetainBorrow + ); + assert_eq!(formals.len(), 1); + assert_eq!(formals[0].parameter(), 0); + assert_eq!(formals[0].source_index(), 0); + assert_eq!(formals[0].actual_type(), envelope_type); + assert_eq!(formals[0].parameter_type(), envelope_type); + assert_eq!(formals[0].access(), ParamMode::View); + assert_eq!(formals[0].syntax(), ResourceArgumentSyntax::WrittenView); + assert_eq!(formals[0].effect(), ResourceArgumentEffect::RetainBorrow); + assert!(matches!( + operands.as_slice(), + [ResourceCallOperand::CarrierBorrowed { parameter: 0, .. }] + )); + } + let inspected = named(&plan, "inspect_envelope"); + let graph = carrier(inspected); + let owner = inspected.parameters()[0].ty; + assert!(graph.loans().iter().any(|loan| matches!( + loan.source(), + ResourceCarrierLoanSource::IncomingViewFormal { parameter: 0, .. } + ))); + assert!(graph.operations().iter().any(|operation| matches!(operation.role(), + ResourceCarrierOperationRole::Observe { + observation: ResourceCarrierObservation::Ordinary { path, ty }, .. + } if *ty == TypeInterner::INT64 + && matches!(path.as_slice(), [ResourceCarrierProjectionPath::Field { owner: actual, field }] + if *actual == owner && field.index() == 2) + )), "marker observation retains its nominal parent and exact field ordinal"); + assert!( + carrier(named(&plan, "forward_envelope")) + .operations() + .iter() + .any(|operation| matches!( + operation.role(), + ResourceCarrierOperationRole::PublishReturn { .. } + )) + ); + } +} + +fn constructor(function: &Function, wanted: impl Fn(&Expression) -> bool) -> Option<&Expression> { + for block in &function.blocks { + for statement in &block.statements { + if let StatementKind::Let { value, .. } | StatementKind::Evaluate(value) = + &statement.kind + && let Some(value) = matching_expression(value, &wanted) + { + return Some(value); + } + } + if let TerminatorKind::Return(Some(value)) = &block.terminator.kind + && let Some(value) = matching_expression(value, &wanted) + { + return Some(value); + } + } + None +} + +fn constructor_mut( + function: &mut Function, + wanted: impl Fn(&Expression) -> bool, +) -> &mut Expression { + for block in &mut function.blocks { + for statement in &mut block.statements { + if let StatementKind::Let { value, .. } | StatementKind::Evaluate(value) = + &mut statement.kind + && let Some(value) = matching_expression_mut(value, &wanted) + { + return value; + } + } + if let TerminatorKind::Return(Some(value)) = &mut block.terminator.kind + && let Some(value) = matching_expression_mut(value, &wanted) + { + return value; + } + } + panic!("exact original constructor occurrence"); +} + +fn matching_expression<'a>( + value: &'a Expression, + wanted: &impl Fn(&Expression) -> bool, +) -> Option<&'a Expression> { + if wanted(value) { + return Some(value); + } + match &value.kind { + hir::ExpressionKind::View(inner) | hir::ExpressionKind::RefinementValidated(inner) => { + matching_expression(inner, wanted) + } + hir::ExpressionKind::InterfaceCoerce { value, .. } + | hir::ExpressionKind::Comptime { value, .. } => matching_expression(value, wanted), + _ => None, + } +} + +fn matching_expression_mut<'a>( + value: &'a mut Expression, + wanted: &impl Fn(&Expression) -> bool, +) -> Option<&'a mut Expression> { + if wanted(value) { + return Some(value); + } + match &mut value.kind { + hir::ExpressionKind::View(inner) | hir::ExpressionKind::RefinementValidated(inner) => { + matching_expression_mut(inner, wanted) + } + hir::ExpressionKind::InterfaceCoerce { value, .. } + | hir::ExpressionKind::Comptime { value, .. } => matching_expression_mut(value, wanted), + _ => None, + } +} + +#[test] +fn resource_carrier_public_mir_field_tag_type_return_and_local_changes_cannot_issue_transport() { + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + validate_resource_ownership(&program, types).unwrap(); + for mutation in 0..8 { + let mut changed = program.clone(); + match mutation { + 0 | 1 | 2 => { + let other_type = function(&changed, "absent_box").return_type; + let current = function_mut(&mut changed, "absent_envelope"); + let (commit, output) = staged_constructor(current).commit().unwrap(); + let output = output.id; + if mutation == 2 { + current.locals[output.index() as usize].ty = other_type; + let ResourcePosition::Statement(index) = commit.position() else { + unreachable!() + }; + assert!( + matches!(current.blocks[commit.block().index() as usize].statements[index].kind, + StatementKind::ResourceConstructor(ResourceConstructorNode::Commit { output: id, .. }) if id == output) + ); + } else { + let mut altered = 0; + for node in current + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + { + if let StatementKind::ResourceConstructor( + ResourceConstructorNode::Stage { + source_index, + field, + .. + }, + ) = &mut node.kind + { + let (selected, second) = if mutation == 0 { + (field, 1) + } else { + (source_index, 2) + }; + if *selected == 0 { + *selected = second; + altered += 1; + } else if *selected == second { + *selected = 0; + altered += 1; + } + } + } + assert_eq!(altered, 2, "exact staged field/order permutation"); + } + } + 3 => { + let value = + constructor_mut(function_mut(&mut changed, "absent_choice"), |value| { + matches!(value.kind, hir::ExpressionKind::EnumConstruct { .. }) + }); + let hir::ExpressionKind::EnumConstruct { variant, .. } = &mut value.kind else { + unreachable!() + }; + *variant = hir::VariantId::new(2); + } + 4 => { + let value = + constructor_mut(function_mut(&mut changed, "absent_state"), |value| { + matches!(value.kind, hir::ExpressionKind::MachineConstruct { .. }) + }); + let hir::ExpressionKind::MachineConstruct { state, .. } = &mut value.kind + else { + unreachable!() + }; + *state = hir::StateId::new(1); + } + 5 => { + let other_type = function(&changed, "absent_tokens").return_type; + function_mut(&mut changed, "empty_tokens").return_type = other_type; + } + 6 => { + let other_type = function(&changed, "absent_envelope").return_type; + let function = function_mut(&mut changed, "refined_shape_contract"); + let local = function.params[0].local; + function.locals[usize::try_from(local.index()).unwrap()].ty = other_type; + } + 7 => { + let other_type = function(&changed, "empty_map").return_type; + let value = + constructor_mut(function_mut(&mut changed, "absent_map"), |value| { + matches!(value.kind, hir::ExpressionKind::MapConstruct { .. }) + }); + value.ty = other_type; + } + _ => unreachable!(), + } + assert!( + validate_witnesses(&changed, types).is_err(), + "original constructor witness, mutation {mutation}" + ); + assert!( + validate_resource_ownership(&changed, types).is_err(), + "no carrier authority, mutation {mutation}" + ); + } + } +} + +#[test] +fn resource_carrier_foreign_checked_body_and_copied_witness_cannot_replace_the_current_archive() { + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let foreign = checked_support( + &SOURCE.replace("marker: 17", "marker: 19"), + SUPPORT, + release, + ); + let types = &checked.checked().interner; + let program = lowered(&checked); + let foreign_program = lowered(&foreign); + validate_resource_ownership(&program, types).unwrap(); + validate_resource_ownership(&foreign_program, &foreign.checked().interner).unwrap(); + let foreign_function = function(&foreign_program, "absent_envelope"); + for mutation in 0..3 { + let mut changed = program.clone(); + let function = function_mut(&mut changed, "absent_envelope"); + match mutation { + 0 => function.blocks = foreign_function.blocks.clone(), + 1 => function.resource_lowering = foreign_function.resource_lowering.clone(), + 2 => *function = foreign_function.clone(), + _ => unreachable!(), + } + assert!( + validate_resource_ownership(&changed, types).is_err(), + "foreign body/witness {mutation}" + ); + } + } +} + +fn copied_types(original: &TypeInterner) -> TypeInterner { + let mut types = TypeInterner::new(); + macro_rules! copy_nominals { + ($variant:ident, $resolve:ident, $add:ident) => { + let definitions = original + .type_ids() + .filter_map(|ty| match original.resolve(ty) { + Type::$variant(id) => Some((id.index(), (*id, original.$resolve(*id).clone()))), + _ => None, + }) + .collect::>(); + for (id, definition) in definitions.into_values() { + assert_eq!(types.$add(definition), id); + } + }; + } + copy_nominals!(Struct, resolve_struct, add_struct); + copy_nominals!(Bitfield, resolve_bitfield, add_bitfield); + copy_nominals!(Enum, resolve_enum, add_enum); + copy_nominals!(Interface, resolve_interface, add_interface); + copy_nominals!(Actor, resolve_actor, add_actor); + copy_nominals!(Machine, resolve_machine, add_machine); + for ty in original.type_ids() { + assert_eq!(types.intern(original.resolve(ty).clone()), ty); + } + for ty in original.type_ids() { + let arguments = original.nominal_type_arguments(ty); + if !arguments.is_empty() { + types + .register_nominal_type_arguments(ty, arguments.to_vec()) + .unwrap(); + } + } + types +} + +#[test] +fn resource_carrier_stored_nominal_definitions_cannot_erase_latent_payloads_or_refined_fields() { + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let original_types = &checked.checked().interner; + let hir = materialized(&checked); + let program = lower(&hir, original_types).unwrap(); + let mut types = copied_types(original_types); + hir.resource_source.validate_types(&types).unwrap(); + types.intern(Type::Optional(TypeInterner::UINT64)); + hir.resource_source.validate_types(&types).unwrap(); + let Type::Struct(envelope_id) = + types.resolve(function(&program, "absent_envelope").return_type) + else { + unreachable!() + }; + let envelope_id = *envelope_id; + let envelope = types.resolve_struct(envelope_id).clone(); + let mut erased = envelope.clone(); + erased.fields[0].1 = TypeInterner::STRING; + types.update_struct(envelope_id, erased); + assert!(hir.resource_source.validate_types(&types).is_err()); + assert!(validate_resource_ownership(&program, &types).is_err()); + types.update_struct(envelope_id, envelope); + let Type::Enum(choice_id) = types.resolve(function(&program, "empty_choice").return_type) + else { + unreachable!() + }; + let choice_id = *choice_id; + let choice = types.resolve_enum(choice_id).clone(); + let mut erased = choice.clone(); + erased.variants[2].fields.clear(); + types.update_enum(choice_id, erased); + assert!( + hir.resource_source.validate_types(&types).is_err(), + "inactive occupied variant remains archived" + ); + assert!(validate_resource_ownership(&program, &types).is_err()); + types.update_enum(choice_id, choice); + let Type::Machine(machine_id) = + types.resolve(function(&program, "absent_state").return_type) + else { + unreachable!() + }; + let machine_id = *machine_id; + let machine = types.resolve_machine(machine_id).clone(); + let mut erased = machine.clone(); + erased.states[2].fields.clear(); + types.update_machine(machine_id, erased); + assert!( + hir.resource_source.validate_types(&types).is_err(), + "inactive occupied machine state remains archived" + ); + types.update_machine(machine_id, machine); + let refined = function(&program, "refined_shape_contract"); + let Type::Struct(refined_id) = types.resolve(refined.params[0].ty) else { + unreachable!() + }; + let refined_id = *refined_id; + let mut erased = types.resolve_struct(refined_id).clone(); + erased.fields[0].1 = TypeInterner::INT64; + types.update_struct(refined_id, erased); + assert!( + hir.resource_source.validate_types(&types).is_err(), + "the unused formal does not erase PositiveMarker" + ); + assert!(validate_resource_ownership(&program, &types).is_err()); + } +} + +const MACHINE_OBSERVATION_SUFFIX: &str = r#" +function source17_machine_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false +export function observe_general_machine() returns bool: + TokenState value = absent_state() + if value at vacant: + if value.marker != "empty": + return false + return source17_machine_absent_optional(view value.fallback) + return false +export function observe_exact_machine() returns bool: + TokenState at vacant value = absent_exact_state() + if value at vacant: + if value.marker != "empty": + return false + return source17_machine_absent_optional(view value.fallback) + return false +export function observe_machine_view(view value: TokenState) returns bool: + if value at vacant: + if value.marker != "empty": + return false + return source17_machine_absent_optional(view value.fallback) + return false +"#; + +fn machine_observation_program(release: bool) -> (Arc, Program) { + let source = format!("{SOURCE}{MACHINE_OBSERVATION_SUFFIX}"); + assert!(source.starts_with(SOURCE)); + let checked = checked_support(&source, SUPPORT, release); + let program = lowered(&checked); + (checked, program) +} + +#[test] +fn resource_carrier_machine_state_observation_preserves_physical_root_and_guarded_field_paths() { + for release in [false, true] { + let (checked, raw) = machine_observation_program(release); + let types = &checked.checked().interner; + for prepare in [false, true] { + let mut program = raw.clone(); + if prepare { + crate::prepare_native_sequences(&mut program, types); + } + crate::validate(&program).unwrap(); + let plan = validate_resource_ownership(&program, types).unwrap(); + for name in [ + "observe_general_machine", + "observe_exact_machine", + "observe_machine_view", + ] { + let mir = function(&program, name); + let local = mir + .locals + .iter() + .find(|local| local.name == "value") + .unwrap(); + let graph = carrier(named(&plan, name)); + let state_observations = graph + .operations() + .iter() + .filter_map(|operation| match operation.role() { + ResourceCarrierOperationRole::Observe { + source, + observation: ResourceCarrierObservation::State { state: 0 }, + .. + } => Some((*source, operation)), + _ => None, + }) + .collect::>(); + let [(source, operation)] = state_observations.as_slice() else { + panic!("one exact vacant StateIs observation for {name}"); + }; + assert_eq!( + child(graph, graph.loans()[source.index()].shape()).ty(), + local.ty + ); + let condition = mir + .blocks + .iter() + .find_map(|block| { + let TerminatorKind::Branch { condition, .. } = &block.terminator.kind + else { + return None; + }; + matches!(condition.kind, hir::ExpressionKind::StateIs { .. }) + .then_some(condition) + }) + .unwrap(); + assert!( + operation.is_for_expression(condition), + "the row retains the StateIs occurrence" + ); + let assert_state_path = |source: super::carriers::ResourceCarrierLoanId, + path: &[ResourceCarrierProjectionPath], + field: usize| { + assert_eq!( + child(graph, graph.loans()[source.index()].shape()).ty(), + local.ty + ); + let [ + ResourceCarrierProjectionPath::State { + owner, + state, + field: actual, + }, + ] = path + else { + panic!("machine field uses its checked State path"); + }; + assert_eq!((*state, *actual), (0, field)); + let Type::MachineState { machine, state } = types.resolve(*owner) else { + panic!("field path retains its qualified nominal owner"); + }; + assert_eq!(state.index(), 0); + assert!( + matches!(types.resolve(local.ty), + Type::Machine(physical) if physical == machine + ) || matches!(types.resolve(local.ty), + Type::MachineState { machine: physical, state: selected } + if physical == machine && selected.index() == 0 + ) + ); + }; + let marker = graph + .operations() + .iter() + .find_map(|operation| match operation.role() { + ResourceCarrierOperationRole::Observe { + source, + observation: ResourceCarrierObservation::Ordinary { path, ty }, + .. + } if *ty == TypeInterner::STRING => Some((*source, path)), + _ => None, + }) + .unwrap(); + assert_state_path(marker.0, marker.1, 1); + let adapter = graph + .operations() + .iter() + .find_map(|operation| match operation.role() { + ResourceCarrierOperationRole::AdaptSum { + source, + path, + loan: Some(loan), + .. + } => Some((*source, path, *loan, operation.id())), + _ => None, + }) + .unwrap(); + assert_state_path(adapter.0, adapter.1, 0); + assert!( + matches!(named(&plan, name).loans()[adapter.2.index()].source(), + ResourceLoanSource::CarrierSumProjection { operation } if operation == adapter.3 + ) + ); + if name == "observe_machine_view" { + let ResourceCarrierLoanSource::Loan(parent) = + graph.loans()[source.index()].source() + else { + panic!("view observation borrows its physical incoming loan"); + }; + assert!(matches!( + graph.loans()[parent.index()].source(), + ResourceCarrierLoanSource::IncomingViewFormal { parameter: 0, .. } + )); + assert!( + !graph.slots().iter().any(|slot| matches!(slot.storage(), + ResourceSlotStorage::Local { header } if header.id == local.id + )), + "a view formal never becomes an owned carrier slot" + ); + } + } + let mir = function(&program, "observe_general_machine"); + let mut narrowed_local_base = false; + for block in &mir.blocks { + walk::mir_block(block, &mut |value| { + if let hir::ExpressionKind::Field { + base, owner_type, .. + } = &value.kind + { + narrowed_local_base |= base.ty == *owner_type + && matches!(types.resolve(base.ty), Type::MachineState { .. }); + } + }); + } + assert!( + narrowed_local_base, + "the positive source exercises the checked branch-narrowed place" + ); + for name in CONSTRUCTORS { + function(&program, name); + } + function(&program, "refined_shape_contract"); + function(&program, "absent_required_controls"); + } + } +} + +fn first_machine_field_mut(value: &mut Expression) -> Option<&mut Expression> { + if matches!(value.kind, hir::ExpressionKind::Field { .. }) { + return Some(value); + } + match &mut value.kind { + hir::ExpressionKind::Binary { left, right, .. } => { + first_machine_field_mut(left).or_else(|| first_machine_field_mut(right)) + } + hir::ExpressionKind::Unary { value, .. } | hir::ExpressionKind::View(value) => { + first_machine_field_mut(value) + } + _ => None, + } +} + +#[test] +fn resource_carrier_machine_observation_current_guard_and_narrowed_place_mutations_refuse() { + for release in [false, true] { + let (checked, program) = machine_observation_program(release); + let types = &checked.checked().interner; + validate_resource_ownership(&program, types).unwrap(); + for mutation in 0..5 { + let mut changed = program.clone(); + let mir = function_mut(&mut changed, "observe_general_machine"); + let local = mir + .locals + .iter() + .find(|local| local.name == "value") + .unwrap() + .clone(); + if mutation < 2 { + let branch = mir + .blocks + .iter_mut() + .find_map(|block| match &mut block.terminator.kind { + TerminatorKind::Branch { + condition, + then_block, + else_block, + } if matches!(condition.kind, hir::ExpressionKind::StateIs { .. }) => { + Some((condition, then_block, else_block)) + } + _ => None, + }) + .unwrap(); + if mutation == 0 { + let hir::ExpressionKind::StateIs { state, .. } = &mut branch.0.kind else { + unreachable!(); + }; + *state = hir::StateId::new(1); + } else { + std::mem::swap(branch.1, branch.2); + } + } else { + let field = mir + .blocks + .iter_mut() + .find_map(|block| { + let TerminatorKind::Branch { condition, .. } = &mut block.terminator.kind + else { + return None; + }; + first_machine_field_mut(condition) + }) + .unwrap(); + let hir::ExpressionKind::Field { + base, owner_type, .. + } = &mut field.kind + else { + unreachable!(); + }; + assert!(matches!( + types.resolve(*owner_type), + Type::MachineState { .. } + )); + match mutation { + 2 => *owner_type = local.ty, + 3 => { + assert_ne!(base.ty, local.ty); + base.ty = local.ty; + } + 4 => { + let qualified = *owner_type; + mir.locals + .iter_mut() + .find(|header| header.id == local.id) + .unwrap() + .ty = qualified; + } + _ => unreachable!(), + } + } + assert!( + validate_witnesses(&changed, types).is_err(), + "machine observational mutation {mutation} cannot replace its authenticated current occurrence" + ); + assert!( + validate_resource_ownership(&changed, types).is_err(), + "machine observational mutation {mutation} cannot issue a fresh carrier permission" + ); + } + } +} diff --git a/crates/jett_mir/src/resource_ownership/companion.rs b/crates/jett_mir/src/resource_ownership/companion.rs new file mode 100644 index 00000000..e14b6056 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/companion.rs @@ -0,0 +1,215 @@ +//! Ordinary ownership remains independent of the fresh Resource custody proof. +use super::*; +use crate::copy_values::CopyValuePlan; +use std::collections::HashSet; +#[cfg(test)] +mod tests; + +/// Holds the fresh plan for the entire lifetime of its ordinary storage proof. +#[derive(Debug)] +pub struct ResourceCompanionPlan<'a, 'p> { + ownership: &'a ResourceOwnershipPlan<'p>, + function: FunctionId, + storage: CopyValuePlan, + caller_acquisitions: crate::CallerAcquisitions<'p>, +} +impl<'a, 'p> ResourceCompanionPlan<'a, 'p> { + pub fn analyze( + ownership: &'a ResourceOwnershipPlan<'p>, + function: FunctionId, + ) -> Result { + let context = CompanionContext::new(ownership, function)?; + let storage = crate::move_values::MoveValuePlan::analyze_companion(&context)?; + let caller_acquisitions = crate::call_ownership::validate_function( + context.program, + context.function, + context.types, + )?; + Ok(Self { + ownership, + function, + storage, + caller_acquisitions, + }) + } + pub fn ownership(&self) -> &'a ResourceOwnershipPlan<'p> { + self.ownership + } + pub fn function(&self) -> FunctionId { + self.function + } + pub fn storage(&self) -> &CopyValuePlan { + &self.storage + } + /// Complete Source acquisitions borrowed from this exact immutable program. + pub fn caller_acquisitions(&self) -> &crate::CallerAcquisitions<'p> { + &self.caller_acquisitions + } +} + +pub(crate) struct CompanionContext<'p> { + pub(crate) program: &'p Program, + pub(crate) function: &'p Function, + pub(crate) types: &'p TypeInterner, + expressions: BTreeSet, + named_values: BTreeSet, + descriptor_values: BTreeSet, + carrier_expressions: BTreeSet, + carrier_types: HashSet, + named_locals: HashSet, +} +impl<'p> CompanionContext<'p> { + fn new(ownership: &ResourceOwnershipPlan<'p>, function: FunctionId) -> Result { + let plan = ownership + .function(function) + .ok_or("Resource companion has no exact fresh function plan")?; + let current = ownership + .program + .functions + .get(function.index() as usize) + .filter(|current| current.id == function) + .ok_or("Resource companion lost its current function")?; + let mut expressions = BTreeSet::new(); + let mut remember = |value: &Expression| { + walk::expression(value, &mut |value| { + expressions.insert(std::ptr::from_ref(value).addr()); + }) + }; + for block in ¤t.blocks { + for statement in &block.statements { + match &statement.kind { + StatementKind::ResourceConstructor(ResourceConstructorNode::Stage { + value, + .. + }) + | StatementKind::ResourceCall(ResourceCallNode::Stage { value, .. }) + | StatementKind::Let { value, .. } + | StatementKind::BeginCallView { value, .. } + | StatementKind::Evaluate(value) + | StatementKind::HandleDefault(value) => remember(value), + StatementKind::Assign { target, value } => { + remember(target); + remember(value); + } + StatementKind::Assert { condition, message } => { + remember(condition); + if let Some(message) = message { + remember(message); + } + } + StatementKind::CheckRefinement { call, .. } => remember(call), + StatementKind::Breakpoint { condition, .. } => { + if let Some(condition) = condition { + remember(condition); + } + } + _ => {} // These exact typed statements are checked by both CFG analyses. + } + } + match &block.terminator.kind { + TerminatorKind::Return(Some(value)) + | TerminatorKind::Respond(value) + | TerminatorKind::Branch { + condition: value, .. + } + | TerminatorKind::Switch { + scrutinee: value, .. + } + | TerminatorKind::ReflectedTypeDispatch { + type_info: value, .. + } => remember(value), + _ => {} + } + } + let mut carrier_expressions = BTreeSet::new(); + if let Some(carriers) = plan.carriers() { + for block in ¤t.blocks { + walk::mir_block(block, &mut |value| { + let direct = carriers.operations_for_expression(value).next().is_some() + || plan.operations_for_expression(value).any(|operation| { + matches!( + operation.role(), + ResourceOperationRole::InvokeSourceFunction { + result: ResourceCallResult::Carrier { .. }, + .. + } + ) + }); + let local = matches!(value.kind, hir::ExpressionKind::Local(id) if carriers.slots().iter().any(|slot| matches!(slot.storage(), ResourceSlotStorage::Local { header } if header.id == id)) || current.is_view_local(id) && carriers.shape_for_type(value.ty).is_some_and(ResourceCarrierShape::contains_resource)); + if direct + || local + || matches!( + value.kind, + hir::ExpressionKind::View(_) + | hir::ExpressionKind::InterfaceCoerce { .. } + ) + { + carrier_expressions.insert(std::ptr::from_ref(value).addr()); + } + }); + } + } + let carrier_types = plan.carriers().map_or_else(HashSet::new, |carriers| { + carriers + .shapes() + .iter() + .filter(|shape| shape.contains_resource()) + .map(ResourceCarrierShape::ty) + .collect() + }); + Ok(Self { + carrier_expressions, + carrier_types, + program: ownership.program, + function: current, + types: ownership.types, + expressions, + descriptor_values: plan + .descriptor_values + .iter() + .map(|(address, _)| *address) + .collect(), + named_values: plan + .named_callable_values + .iter() + .map(|(address, _)| *address) + .collect(), + named_locals: current + .resource_lowering + .as_ref() + .map_or_else(HashSet::new, |witness| { + witness + .named_callables + .iter() + .map(named_callables::NamedCallableBinding::local) + .collect() + }), + }) + } + pub(crate) fn carrier_type(&self, ty: TypeId) -> bool { + self.carrier_types.contains(&ty) && carriers::carrier_type(self.types, ty) + } + pub(crate) fn carrier_expression(&self, value: &Expression) -> bool { + self.carrier_expressions + .contains(&std::ptr::from_ref(value).addr()) + } + pub(crate) fn contains(&self, value: &Expression) -> bool { + self.expressions.contains(&std::ptr::from_ref(value).addr()) + } + pub(crate) fn resource_type(&self, ty: TypeId) -> bool { + resource_type_pending(self.types, ty) + } + pub(crate) fn descriptor_expression(&self, value: &Expression) -> bool { + self.descriptor_values + .contains(&std::ptr::from_ref(value).addr()) + } + pub(crate) fn resource_expression(&self, value: &Expression) -> bool { + self.resource_type(value.ty) + && !self + .named_values + .contains(&std::ptr::from_ref(value).addr()) + } + pub(crate) fn resource_local(&self, local: &Local) -> bool { + self.resource_type(local.ty) && !self.named_locals.contains(&local.id) + } +} diff --git a/crates/jett_mir/src/resource_ownership/companion/tests.rs b/crates/jett_mir/src/resource_ownership/companion/tests.rs new file mode 100644 index 00000000..f5db840e --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/companion/tests.rs @@ -0,0 +1,137 @@ +use super::super::tests::{checked, lowered}; +use super::*; +const SOURCE: &str = include_str!("../fixtures/connected_entry.jett"); +#[test] +fn resource_companions_keep_ordinary_failure_storage_and_public_custody_refusals() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let program = lowered(&checked); + let types = &checked.checked().interner; + let ownership = validate_resource_ownership(&program, types).unwrap(); + for function_plan in ownership.functions() { + let current = &program.functions[function_plan.function().index() as usize]; + let companion = ResourceCompanionPlan::analyze(&ownership, current.id).unwrap(); + assert!(std::ptr::eq(companion.ownership(), &ownership)); + assert_eq!(companion.function(), current.id); + for local in ¤t.locals { + if resource_type_pending(types, local.ty) { + assert!( + !companion + .storage() + .owned_locals + .contains(&(local.id.index() as usize)) + ); + } + } + if current + .locals + .iter() + .any(|local| custody_type(types, local.ty)) + { + assert!( + crate::move_values::MoveValuePlan::analyze(&program, current, types) + .unwrap_err() + .contains("pending ResourceOwnershipPlan") + ); + assert!( + CopyValuePlan::analyze(current, types) + .unwrap_err() + .contains("pending ResourceOwnershipPlan") + ); + } + for statement in current.blocks.iter().flat_map(|block| &block.statements) { + if let StatementKind::SumTake { + target, + success: false, + .. + } = statement.kind + { + assert_eq!(current.local(target).unwrap().ty, TypeInterner::STRING); + assert!( + companion + .storage() + .owned_locals + .contains(&(target.index() as usize)) + ); + } + } + } + let lifecycle = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "lifecycle" + }) + .unwrap(); + let ordinary_result = lifecycle + .locals + .iter() + .find(|local| local.name == "observed") + .unwrap(); + assert_eq!( + types.resolve(ordinary_result.ty), + &Type::Result(TypeInterner::INT64, TypeInterner::STRING) + ); + let companion = ResourceCompanionPlan::analyze(&ownership, lifecycle.id).unwrap(); + assert!( + companion + .storage() + .owned_locals + .contains(&(ordinary_result.id.index() as usize)) + ); + assert!(companion.storage().temporary_slots > 0); + } +} +#[test] +fn resource_role_projection_requires_the_exact_current_expression_occurrence() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let program = lowered(&checked); + let ownership = validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let mut found = 0; + for function_plan in ownership.functions() { + let current = &program.functions[function_plan.function().index() as usize]; + let context = CompanionContext::new(&ownership, current.id).unwrap(); + for statement in current.blocks.iter().flat_map(|block| &block.statements) { + let value = match &statement.kind { + StatementKind::Let { value, .. } | StatementKind::Evaluate(value) => value, + _ => continue, + }; + walk::expression(value, &mut |value| { + assert!(context.contains(value)); + let copied = value.clone(); + assert!(!context.contains(&copied)); + assert_eq!(function_plan.operations_for_expression(&copied).count(), 0); + for operation in function_plan.operations_for_expression(value) { + assert_eq!(operation.site().function(), current.id); + found += usize::from(matches!( + operation.role(), + ResourceOperationRole::InvokeHook { .. } + | ResourceOperationRole::InvokeSourceFunction { .. } + )); + } + }); + } + } + assert!(found >= 3); + } +} +#[test] +fn resource_companion_function_selection_cannot_adopt_an_unrelated_ordinary_body() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let program = lowered(&checked); + let ownership = validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let outside = program + .functions + .iter() + .find(|function| ownership.function(function.id).is_none()) + .expect("unused ordinary support function"); + assert!( + ResourceCompanionPlan::analyze(&ownership, outside.id) + .unwrap_err() + .contains("exact fresh function plan") + ); + } +} diff --git a/crates/jett_mir/src/resource_ownership/constructors.rs b/crates/jett_mir/src/resource_ownership/constructors.rs new file mode 100644 index 00000000..6769a706 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/constructors.rs @@ -0,0 +1,510 @@ +//! Constructor-issued struct operation regions, separate from Source call authority. +use super::*; +use jett_hir::ExpressionKind; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ResourceConstructorId(usize); +impl ResourceConstructorId { + pub fn index(self) -> usize { + self.0 + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ResourceConstructorNode { + Begin { + constructor: ResourceConstructorId, + }, + Stage { + constructor: ResourceConstructorId, + source_index: usize, + field: usize, + value: Expression, + }, + Commit { + constructor: ResourceConstructorId, + output: LocalId, + }, + End { + constructor: ResourceConstructorId, + outcome: ResourceCompletion, + }, +} +impl ResourceConstructorNode { + pub fn constructor(&self) -> ResourceConstructorId { + match self { + Self::Begin { constructor } + | Self::Stage { constructor, .. } + | Self::Commit { constructor, .. } + | Self::End { constructor, .. } => *constructor, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ResourceOperationRegion { + Call(ResourceCallRegionId), + Constructor(ResourceConstructorId), +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ResourceConstructorField { + source_index: usize, + field: usize, + original: Expression, + endpoint: Expression, + start: ResourceSite, + stage: ResourceSite, + locals: Vec, +} +impl ResourceConstructorField { + pub fn source_index(&self) -> usize { + self.source_index + } + pub fn field(&self) -> usize { + self.field + } + pub fn original(&self) -> &Expression { + &self.original + } + pub fn endpoint(&self) -> &Expression { + &self.endpoint + } + pub fn start(&self) -> ResourceSite { + self.start + } + pub fn stage(&self) -> ResourceSite { + self.stage + } + pub fn locals(&self) -> &[Local] { + &self.locals + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ResourceConstructorRegion { + id: ResourceConstructorId, + original: Expression, + order: Vec, + field_types: Vec, + parent: Option, + begin: ResourceSite, + fields: Vec, + commit: Option<(ResourceSite, Local)>, + exits: Vec<(ResourceSite, ResourceCompletion)>, +} +impl ResourceConstructorRegion { + pub fn id(&self) -> ResourceConstructorId { + self.id + } + pub fn original(&self) -> &Expression { + &self.original + } + pub fn evaluation_order(&self) -> &[usize] { + &self.order + } + pub fn field_types(&self) -> &[TypeId] { + &self.field_types + } + pub(crate) fn parent(&self) -> Option { + self.parent + } + pub fn begin(&self) -> ResourceSite { + self.begin + } + pub fn fields(&self) -> &[ResourceConstructorField] { + &self.fields + } + pub fn commit(&self) -> Option<(ResourceSite, &Local)> { + self.commit.as_ref().map(|(s, l)| (*s, l)) + } + pub fn exits(&self) -> &[(ResourceSite, ResourceCompletion)] { + &self.exits + } + + pub(super) fn current(&self, function: &Function) -> Result<(), String> { + let ExpressionKind::StructConstruct { + fields, + evaluation_order, + struct_type, + .. + } = &self.original.kind + else { + return Err("Resource constructor lost its original nominal expression".into()); + }; + if *struct_type != self.original.ty + || *evaluation_order != self.order + || fields.len() != self.field_types.len() + || fields + .iter() + .zip(&self.field_types) + .any(|(field, ty)| field.ty != *ty) + || self.fields.iter().any(|field| { + fields.get(field.field).is_none_or(|original| { + !crate::breakpoint_regions::expressions_equal(original, &field.original) + }) + }) + { + return Err("Resource constructor changed its exact original field relation".into()); + } + let node = |site: ResourceSite| -> Result<&ResourceConstructorNode, String> { + let ResourcePosition::Statement(index) = site.position else { + return Err("Resource constructor lost its exact statement site".into()); + }; + let statement = function + .blocks + .get(site.block.index() as usize) + .filter(|b| b.id == site.block) + .and_then(|b| b.statements.get(index)) + .ok_or("Resource constructor statement disappeared")?; + let StatementKind::ResourceConstructor(node) = &statement.kind else { + return Err("Resource constructor statement changed its node family".into()); + }; + if site.function != function.id || node.constructor() != self.id { + return Err("Resource constructor has a foreign occurrence ID".into()); + } + Ok(node) + }; + if !matches!(node(self.begin)?, ResourceConstructorNode::Begin { .. }) + || self.fields.len() != self.order.len() + { + return Err("Resource constructor lost its complete original field tuple".into()); + } + for (source_index, field) in self.fields.iter().enumerate() { + if field.source_index != source_index + || self.order.get(source_index) != Some(&field.field) + || self.field_types.get(field.field) != Some(&field.endpoint.ty) + || !matches!(node(field.stage)?, ResourceConstructorNode::Stage { source_index:i, field:f, value, .. } + if *i == source_index && *f == field.field && crate::breakpoint_regions::expressions_equal(value, &field.endpoint)) + || field.locals.iter().any(|l| function.local(l.id) != Some(l)) + { + return Err( + "Resource constructor changed an exact field endpoint or generated header" + .into(), + ); + } + } + let (site, output) = self + .commit + .as_ref() + .ok_or("Resource constructor has no exact Commit")?; + if function.local(output.id) != Some(output) + || output.ty != self.original.ty + || !output.mutable + || output.view_source.is_some() + || !matches!(node(*site)?, ResourceConstructorNode::Commit { output:id, .. } if *id == output.id) + { + return Err("Resource constructor Commit changed its owned output header".into()); + } + for (site, outcome) in &self.exits { + if !matches!(node(*site)?, ResourceConstructorNode::End { outcome:found, .. } if found == outcome) + { + return Err("Resource constructor lost its exact completion occurrence".into()); + } + } + if self + .exits + .iter() + .filter(|(_, o)| *o == ResourceCompletion::Normal) + .count() + != 1 + || function + .blocks + .iter() + .flat_map(|b| &b.statements) + .filter(|s| { + matches!(&s.kind, + StatementKind::ResourceConstructor(n) if n.constructor() == self.id) + }) + .count() + != self.fields.len() + self.exits.len() + 2 + { + return Err("Resource constructor has missing or copied operation nodes".into()); + } + Ok(()) + } + + pub(super) fn remap_blocks(&mut self, map: &[Option]) -> Result<(), String> { + let remap = |site: &mut ResourceSite| -> Result<(), String> { + site.block = map + .get(site.block.index() as usize) + .copied() + .flatten() + .ok_or("canonical selection removed a sealed Resource constructor endpoint")?; + Ok(()) + }; + remap(&mut self.begin)?; + for field in &mut self.fields { + remap(&mut field.start)?; + remap(&mut field.stage)?; + } + if let Some((site, _)) = &mut self.commit { + remap(site)?; + } + for (site, _) in &mut self.exits { + remap(site)?; + } + Ok(()) + } + + pub(super) fn remap_locals(&mut self, remap: &mut impl FnMut(&mut LocalId)) { + for field in &mut self.fields { + for header in &mut field.locals { + remap(&mut header.id); + if let Some(s) = &mut header.view_source { + remap(s); + } + } + let mut wrapper = BasicBlock { + id: BlockId(0), + statements: vec![Statement { + kind: StatementKind::Evaluate(field.endpoint.clone()), + span: field.endpoint.span, + }], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: field.endpoint.span, + }, + }; + crate::sequences::prune::block_locals(&mut wrapper, remap, &mut |_| {}); + if let StatementKind::Evaluate(value) = wrapper.statements.remove(0).kind { + field.endpoint = value; + } + } + if let Some((_, output)) = &mut self.commit { + remap(&mut output.id); + } + } + + pub(super) fn shift_prefix(&mut self, block: BlockId, count: usize) { + let shift = |site: &mut ResourceSite| { + if site.block == block + && let ResourcePosition::Statement(i) = &mut site.position + { + *i += count; + } + }; + shift(&mut self.begin); + for f in &mut self.fields { + shift(&mut f.start); + shift(&mut f.stage); + } + if let Some((site, _)) = &mut self.commit { + shift(site); + } + for (site, _) in &mut self.exits { + shift(site); + } + } +} + +pub(super) fn records_equal( + a: &[ResourceConstructorRegion], + b: &[ResourceConstructorRegion], +) -> bool { + a.len() == b.len() + && a.iter().zip(b).all(|(a, b)| { + a.id == b.id + && crate::breakpoint_regions::expressions_equal(&a.original, &b.original) + && a.order == b.order + && a.field_types == b.field_types + && a.parent == b.parent + && a.begin == b.begin + && a.commit == b.commit + && a.exits == b.exits + && a.fields.len() == b.fields.len() + && a.fields.iter().zip(&b.fields).all(|(a, b)| { + a.source_index == b.source_index + && a.field == b.field + && a.start == b.start + && a.stage == b.stage + && a.locals == b.locals + && crate::breakpoint_regions::expressions_equal(&a.original, &b.original) + && crate::breakpoint_regions::expressions_equal(&a.endpoint, &b.endpoint) + }) + }) +} + +impl Capture { + pub(crate) fn constructor_needed(&self, value: &Expression, types: &TypeInterner) -> bool { + self.witness.is_some() + && matches!(&value.kind, ExpressionKind::StructConstruct { fields, .. } + if fields.iter().any(|v| resource_type_pending(types, v.ty))) + } + + pub(crate) fn begin_constructor_region( + &mut self, + original: &Expression, + parent: Option, + begin: ResourceSite, + types: &TypeInterner, + ) -> Result { + let w = self + .witness + .as_mut() + .ok_or("Resource constructor lacks checked Source authentication")?; + w.source.validate_types(types)?; + let ExpressionKind::StructConstruct { + struct_type, + fields, + evaluation_order, + validates_refinements: false, + refinement_predicates, + .. + } = &original.kind + else { + return Err( + "occupied refined constructor needs its exact checked predicate transition".into(), + ); + }; + if !refinement_predicates.is_empty() || original.ty != *struct_type { + return Err("Resource constructor changed its exact nominal type".into()); + } + let Type::Struct(id) = types.resolve(*struct_type) else { + return Err("Resource constructor lacks exact struct header".into()); + }; + let field_types = types + .resolve_struct(*id) + .fields + .iter() + .map(|(_, t)| *t) + .collect::>(); + let mut occurrences = 0; + walk::hir_block(&w.original.body, &mut |v| { + occurrences += usize::from(crate::breakpoint_regions::expressions_equal(v, original)) + }); + if occurrences != 1 + || fields.len() != field_types.len() + || fields.iter().zip(&field_types).any(|(f, t)| f.ty != *t) + || evaluation_order.len() != fields.len() + || evaluation_order.iter().copied().collect::>() + != (0..fields.len()).collect() + || w.constructors + .iter() + .any(|r| crate::breakpoint_regions::expressions_equal(&r.original, original)) + { + return Err( + "Resource constructor differs from its unique complete original checked struct" + .into(), + ); + } + let id = ResourceConstructorId(w.constructors.len()); + if matches!(parent, Some(ResourceOperationRegion::Constructor(p)) if p.index() >= id.index()) + { + return Err("Resource constructor has no private parent region".into()); + } + w.constructors.push(ResourceConstructorRegion { + id, + original: original.clone(), + order: evaluation_order.clone(), + field_types, + parent, + begin, + fields: vec![], + commit: None, + exits: vec![], + }); + Ok(id) + } + + pub(crate) fn constructor_field( + &mut self, + id: ResourceConstructorId, + source_index: usize, + field: usize, + original: &Expression, + endpoint: &Expression, + start: ResourceSite, + stage: ResourceSite, + locals: &[Local], + ) -> Result<(), String> { + let r = self + .witness + .as_mut() + .and_then(|w| w.constructors.get_mut(id.index())) + .ok_or("Resource field lost its private constructor")?; + if r.fields.len() != source_index + || r.order.get(source_index) != Some(&field) + || r.field_types.get(field) != Some(&endpoint.ty) + { + return Err("Resource field changed its original lexical occurrence/type".into()); + } + r.fields.push(ResourceConstructorField { + source_index, + field, + original: original.clone(), + endpoint: endpoint.clone(), + start, + stage, + locals: locals.to_vec(), + }); + Ok(()) + } + + pub(crate) fn constructor_node( + &mut self, + site: ResourceSite, + node: &ResourceConstructorNode, + locals: &[Local], + ) { + let Some(r) = self + .witness + .as_mut() + .and_then(|w| w.constructors.get_mut(node.constructor().index())) + else { + return; + }; + match node { + ResourceConstructorNode::Commit { output, .. } => { + if let Some(h) = locals + .get(output.index() as usize) + .filter(|h| h.id == *output) + { + r.commit = Some((site, h.clone())); + } + } + ResourceConstructorNode::End { outcome, .. } => r.exits.push((site, *outcome)), + _ => {} + } + } +} + +impl Function { + pub fn resource_constructor( + &self, + id: ResourceConstructorId, + ) -> Result<&ResourceConstructorRegion, String> { + let w = self + .resource_lowering + .as_ref() + .ok_or("Resource constructor lost original checked witness")?; + w.current(self)?; + w.constructors + .get(id.index()) + .filter(|r| r.id == id) + .ok_or_else(|| "Resource constructor has a foreign private occurrence".into()) + } +} + +pub(crate) fn validate_nodes(function: &Function) -> Result<(), String> { + let has_nodes = function.blocks.iter().any(|block| { + block + .statements + .iter() + .any(|statement| matches!(statement.kind, StatementKind::ResourceConstructor(_))) + }); + let has_inventory = function.resource_lowering.as_ref().is_some_and(|witness| { + !witness.constructors.is_empty() || !witness.constructor_seal.is_empty() + }); + if !has_nodes && !has_inventory { + return Ok(()); + } + function + .resource_lowering + .as_ref() + .ok_or("Resource constructor canonical preparation lost its original checked witness")? + .current(function) +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_mir/src/resource_ownership/constructors/tests.rs b/crates/jett_mir/src/resource_ownership/constructors/tests.rs new file mode 100644 index 00000000..2bf127db --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/constructors/tests.rs @@ -0,0 +1,395 @@ +use super::*; +use crate::resource_ownership::tests::{checked, lowered}; + +const SOURCES: [&str; 3] = [ + include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/33_owned_struct_scope_drop_entry.jett" + ), + include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/34_owned_struct_reverse_entry.jett" + ), + include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/35_owned_struct_abandoned_prefix_entry.jett" + ), +]; + +// Historical originals retain their exact invalid viewed-entry Source bytes. +const INVALID_VIEWED_ENTRY_SOURCES: [&str; 3] = [ + include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/30_owned_struct_scope_drop.jett" + ), + include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/31_owned_struct_reverse.jett" + ), + include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/32_owned_struct_abandoned_prefix.jett" + ), +]; + +#[test] +fn resource_occupied_struct_historical_viewed_entries_keep_exact_e0503_refusal() { + for release in [false, true] { + for source in INVALID_VIEWED_ENTRY_SOURCES { + let stdlib = jett_common::FileId::new(10_000); + let project = jett_common::FileId::new(0); + let mut parsed = jett_parser::parse(crate::resource_ownership::tests::SUPPORT, stdlib); + let primary = jett_parser::parse(source, project); + assert!(parsed.errors.is_empty()); + assert!(primary.errors.is_empty()); + let declarations = parsed + .module + .items + .iter() + .filter_map(|item| match item { + jett_parser::ast::Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .collect::>(); + let [declaration] = declarations.as_slice() else { + panic!("historical Source must use the original sole checked support Resource"); + }; + let catalog = [ + ( + "kernel_create", + jett_types::ResourceKernelRecipe::NetworkFactory, + ), + ( + "kernel_borrow", + jett_types::ResourceKernelRecipe::NetworkBorrow, + ), + ("kernel_close", jett_types::ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| jett_resolve::ResourceKernelSpec { + resource_declaration: *declaration, + member: member.to_owned(), + recipe, + }) + .collect::>(); + parsed.module.items.extend(primary.module.items); + let checked = jett_typecheck::CheckedResourceProgram::prepare( + parsed, + std::collections::HashMap::from([ + (stdlib, jett_common::SourceOrigin::Stdlib), + (project, jett_common::SourceOrigin::Project), + ]), + &catalog, + jett_typecheck::CheckOptions { release }, + ); + let error = match checked { + Err(error) => error, + Ok(_) => panic!("historical viewed-main Source was admitted (release={release})"), + }; + let diagnostics = error + .diagnostics() + .expect("historical E0503 checker diagnostics"); + assert!( + diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 503 + && diagnostic.span.file == project + && diagnostic.message + == "`main` must own runtime capability parameter `net`; remove `view`"), + "historical viewed-main Source lost exact E0503 (release={release}): {:?}", + diagnostics + .iter() + .map(|diagnostic| ( + diagnostic.code.code(), + diagnostic.message.as_str(), + diagnostic.span + )) + .collect::>() + ); + } + } +} + +fn helper(program: &Program) -> &Function { + program + .functions + .iter() + .find(|f| { + f.identity.declaration.namespace == "app" + && matches!( + f.identity.declaration.name.as_str(), + "retire_owned" | "retire_pair" + ) + }) + .unwrap() +} +fn helper_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|f| { + f.identity.declaration.namespace == "app" + && matches!( + f.identity.declaration.name.as_str(), + "retire_owned" | "retire_pair" + ) + }) + .unwrap() +} +fn reseal_general(function: &mut Function) { + let w = function.resource_lowering.as_mut().unwrap(); + w.blocks = function.blocks.clone(); + w.locals = function.locals.clone(); + w.parameters = function.params.clone(); +} +fn refused(program: &Program, types: &TypeInterner) { + assert!( + validate_resource_ownership(program, types).is_err(), + "forged constructor ownership was admitted" + ); + let mut selected = helper(program).clone(); + let before = selected.clone(); + assert!( + !crate::sequences::prune::unreachable(&mut selected), + "forged constructor prune accepted; private witness present={}", + before.resource_lowering.is_some() + ); + assert!( + crate::breakpoint_regions::blocks_equal(&selected.blocks, &before.blocks), + "refused prune must preserve public graph" + ); + let mut selected = before.clone(); + assert!(!crate::sequences::prune::unused_locals(&mut selected)); + assert_eq!(selected.locals, before.locals); + assert!(crate::breakpoint_regions::blocks_equal( + &selected.blocks, + &before.blocks + )); +} + +#[test] +fn resource_occupied_struct_constructor_preserves_owned_formals_lexical_order_and_operation_output() +{ + for release in [false, true] { + for (index, source) in SOURCES.into_iter().enumerate() { + let checked = checked(source, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + for canonical in [false, true] { + if canonical { + crate::prepare_native_sequences(&mut program, types); + crate::prepare_native_uninhabited_sums(&mut program, types); + crate::prepare_native_generated_functions(&mut program, types); + } + crate::validate(&program).unwrap(); + validate_call_ownership(&program, types).unwrap(); + let function = helper(&program); + let witness = function.resource_lowering.as_ref().unwrap(); + assert_eq!(witness.constructors.len(), 1); + let region = function + .resource_constructor(witness.constructors[0].id()) + .unwrap(); + assert_eq!( + region.evaluation_order(), + if index == 1 { + &[1, 0, 2][..] + } else { + &[0, 1][..] + } + ); + let plan = validate_resource_ownership(&program, types).unwrap(); + let selected = plan.function(function.id).unwrap(); + let carriers = selected.carriers().unwrap(); + let begin = carriers + .operations() + .iter() + .find(|operation| { + operation.site() == region.begin() + && matches!( + operation.role(), + ResourceCarrierOperationRole::BeginConstructor { .. } + ) + }) + .unwrap(); + let ResourceCarrierOperationRole::BeginConstructor { destination, .. } = + begin.role() + else { + unreachable!() + }; + let frame = begin.frame(); + assert_eq!( + selected.frames()[frame.index()].role(), + ResourceFrameRole::Operation + ); + assert_eq!( + selected.frames()[frame.index()].parent(), + Some(selected.root_scope().id()) + ); + for (parameter, param) in function.params.iter().enumerate() { + assert_eq!(param.mode, ParamMode::Owned); + let slot = selected.owner_slots().iter().find(|slot| matches!(slot.storage(),ResourceSlotStorage::Local { header } if header.id == param.local)).unwrap(); + assert!(matches!(slot.shape(), ResourceShape::Plain { .. })); + assert!(carriers.operations().iter().any(|operation| matches!(operation.role(),ResourceCarrierOperationRole::ConstructorChild { child, .. } if child.value == ResourceCarrierValue::LeafOwned { slot:slot.id() })) ,"formal {parameter} must be adopted through its exact plain holder"); + } + let commit = carriers.operations().iter().find(|operation| matches!(operation.role(),ResourceCarrierOperationRole::CommitConstructor { destination:found } if found == destination)).unwrap(); + let (_, output) = region.commit().unwrap(); + assert!( + output.mutable, + "constructor output keeps its exact generated temporary header" + ); + let output_slot = carriers.slots().iter().find(|slot| matches!(slot.storage(),ResourceSlotStorage::Local { header } if header.id == output.id)).unwrap(); + assert_eq!(output_slot.frame(), selected.root_scope().id()); + let transfer = carriers.operations().iter().find(|operation| operation.site() == commit.site() && matches!(operation.role(),ResourceCarrierOperationRole::Transfer { source,destination:to } if source == destination && *to == output_slot.id())).unwrap(); + assert!(transfer.id().index() > commit.id().index()); + assert_eq!(transfer.frame(), frame); + for (site, outcome) in region.exits() { + assert!(selected.operations().iter().any(|operation| operation.site() == *site && operation.frame() == frame && matches!(operation.role(),ResourceOperationRole::Complete { outcome:found } if found == outcome))); + } + if index == 2 { + assert_eq!( + region + .exits() + .iter() + .filter(|(_, o)| *o == ResourceCompletion::Abort) + .count(), + 1 + ); + assert_ne!( + region.fields()[0].stage().block(), + region.fields()[1].stage().block() + ); + for field in region.fields() { + assert_eq!( + selected + .execution_frame(field.stage().block(), field.stage().position()), + Some(frame) + ); + } + } + assert!( + crate::copy_values::CopyValuePlan::analyze_storage( + function, + types, + Some(&program) + ) + .is_err() + ); + assert!( + crate::move_values::MoveValuePlan::analyze(&program, function, types).is_err() + ); + ResourceCompanionPlan::analyze(&plan, function.id).unwrap(); + } + } + } +} + +#[test] +fn resource_occupied_struct_constructor_refuses_changed_fields_copied_nodes_and_lost_proof_before_prune() + { + for release in [false, true] { + let checked = checked(SOURCES[1], release); + let types = &checked.checked().interner; + let program = lowered(&checked); + validate_resource_ownership(&program, types).unwrap(); + for mutation in 0..8 { + let mut forged = program.clone(); + let function = helper_mut(&mut forged); + let region = function.resource_lowering.as_ref().unwrap().constructors[0].clone(); + match mutation { + 0 => { + let site = region.fields()[0].stage(); + let ResourcePosition::Statement(i) = site.position() else { + unreachable!() + }; + let StatementKind::ResourceConstructor(ResourceConstructorNode::Stage { + field, + .. + }) = &mut function.blocks[site.block().index() as usize].statements[i].kind + else { + unreachable!() + }; + *field = 0; + } + 1 => { + let site = region.begin(); + let ResourcePosition::Statement(i) = site.position() else { + unreachable!() + }; + let copy = function.blocks[site.block().index() as usize].statements[i].clone(); + function.blocks[site.block().index() as usize] + .statements + .insert(i + 1, copy); + } + 2 => { + function.resource_lowering.as_mut().unwrap().constructors[0].fields[0] + .original = region.fields[1].original.clone(); + } + 3 => { + function.resource_lowering.as_mut().unwrap().constructors[0].fields[0].start = + region.fields[1].start; + } + 4 => { + function.resource_lowering.as_mut().unwrap().constructors[0] + .order + .swap(0, 1); + } + 5 => { + let (_, output) = region.commit().unwrap(); + function.locals[output.id.index() as usize].view_source = + Some(function.params[0].local); + } + 6 => { + function.resource_lowering = None; + refused(&forged, types); + continue; + } + 7 => { + let (_, output) = region.commit().unwrap(); + assert!(function.locals[output.id.index() as usize].mutable); + function.locals[output.id.index() as usize].mutable = false; + } + _ => unreachable!(), + } + reseal_general(function); + refused(&forged, types); + } + } +} + +#[test] +fn resource_occupied_struct_constructor_disconnected_stage_and_wrong_prefix_remain_refused() { + let checked = checked(SOURCES[2], false); + let types = &checked.checked().interner; + let program = lowered(&checked); + for disconnect in [false, true] { + let mut forged = program.clone(); + let function = helper_mut(&mut forged); + let region = function.resource_lowering.as_ref().unwrap().constructors[0].clone(); + let site = region.fields()[0].stage(); + let ResourcePosition::Statement(i) = site.position() else { + unreachable!() + }; + if disconnect { + let id = BlockId(u32::try_from(function.blocks.len()).unwrap()); + function.blocks.push(BasicBlock { + id, + statements: vec![ + function.blocks[site.block().index() as usize].statements[i].clone(), + ], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: function.span, + }, + }); + } else { + let end = region + .exits() + .iter() + .find(|(_, o)| *o == ResourceCompletion::Abort) + .unwrap() + .0; + let ResourcePosition::Statement(i) = end.position() else { + unreachable!() + }; + function.blocks[end.block().index() as usize] + .statements + .remove(i); + } + reseal_general(function); + refused(&forged, types); + } +} diff --git a/crates/jett_mir/src/resource_ownership/control_flow.rs b/crates/jett_mir/src/resource_ownership/control_flow.rs new file mode 100644 index 00000000..12ef2ed2 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/control_flow.rs @@ -0,0 +1,573 @@ +//! Control-flow transport preserves the independently proven custody state. +//! Sequence preparation may only replace an exact original ForEach by its canonical +//! consuming-list or map edit. The carrier planner owns custody and retirement. +use super::*; +use hir::{BinaryOp, ExpressionKind as E}; + +// Erased storage is not evidence of absent custody, even if its nominal +// signature mentions no Resource. This is a bounded compiler proof, not a +// restriction on valid Source programs. +fn resource_free(types: &TypeInterner, ty: TypeId, seen: &mut BTreeSet) -> bool { + if ty.index() as usize >= types.len() { + return false; + } + if !seen.insert(ty.index()) { + return true; + } + match types.resolve(ty) { + Type::Resource(_) + | Type::Interface(_) + | Type::Actor(_) + | Type::Function { .. } + | Type::Capability(_) + | Type::TypeConstruction + | Type::Error => false, + Type::List(inner) + | Type::Set(inner) + | Type::Optional(inner) + | Type::Secret(inner) + | Type::Refinement { base: inner, .. } => resource_free(types, *inner, seen), + Type::Map(key, value) | Type::Result(key, value) => { + resource_free(types, *key, seen) && resource_free(types, *value, seen) + } + Type::Struct(id) => types + .resolve_struct(*id) + .fields + .iter() + .all(|(_, ty)| resource_free(types, *ty, seen)), + Type::Bitfield(id) => types + .resolve_bitfield(*id) + .fields + .iter() + .all(|field| resource_free(types, field.ty, seen)), + Type::Enum(id) => types.resolve_enum(*id).variants.iter().all(|variant| { + variant + .fields + .iter() + .all(|(_, ty)| resource_free(types, *ty, seen)) + }), + Type::Machine(id) | Type::MachineState { machine: id, .. } => { + types.resolve_machine(*id).states.iter().all(|state| { + state + .fields + .iter() + .all(|(_, ty)| resource_free(types, *ty, seen)) + }) + } + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::String + | Type::Bool + | Type::Bytes + | Type::Nothing + | Type::Never => true, + } +} +fn ordinary_type(types: &TypeInterner, ty: TypeId) -> Result<(), String> { + if !resource_free(types, ty, &mut BTreeSet::new()) { + return Err("pending ResourceOwnershipPlan: control-flow value needs its dedicated Resource carrier transport".into()); + } + Ok(()) +} +fn header<'a>( + function: &'a Function, + types: &TypeInterner, + local: LocalId, +) -> Result<&'a Local, String> { + let header = function + .local(local) + .ok_or("Resource control-flow binder lost its dense local header")?; + ordinary_type(types, header.ty)?; + Ok(header) +} +pub(super) fn consuming_list( + function: &Function, + types: &TypeInterner, + key: LocalId, + value: Option, + by_view: bool, + iterable: &Expression, +) -> Result { + ordinary_type(types, iterable.ty)?; + let Type::List(element) = types.resolve(iterable.ty) else { + return Err("pending ResourceOwnershipPlan: this iterator needs its dedicated ordinary sequence normalization".into()); + }; + ordinary_type(types, *element)?; + if by_view || value.is_some() || *element == TypeInterner::NEVER { + return Err("pending ResourceOwnershipPlan: borrowed or uninhabited iteration needs its exact sequence transport".into()); + } + let binding = header(function, types, key)?; + if binding.ty != *element || binding.view_source.is_some() { + return Err("Resource consuming iterator changed its exact ordinary element binder".into()); + } + Ok(*element) +} +fn carrier_binding( + function: &Function, + types: &TypeInterner, + local: LocalId, + expected: TypeId, +) -> Result<(), String> { + if expected.index() as usize >= types.len() || expected == TypeInterner::NEVER { + return Err("pending ResourceOwnershipPlan: uninhabited carrier iteration needs its exact sequence transport".into()); + } + let binding = function + .local(local) + .ok_or("Resource carrier iterator lost its dense binder header")?; + if binding.ty != expected + || binding.debug_ty != expected + || binding.mutable + || binding.view_source.is_some() + || function.is_view_local(local) + || function.parameter_for_local(local).is_some() + { + return Err("Resource carrier iterator changed its exact owning binder declaration".into()); + } + Ok(()) +} +/// Validate only the exact typed consuming iterator. This creates no carrier, +/// owner, lease or absence proof; the independent carrier planner supplies them. +pub(super) fn carrier_sequence( + function: &Function, + types: &TypeInterner, + key: LocalId, + value: Option, + by_view: bool, + iterable: &Expression, +) -> Result<(), String> { + function + .resource_lowering + .as_ref() + .ok_or("Resource carrier iterator has no authenticated Source witness")? + .current(function)?; + let occurrences = function + .blocks + .iter() + .filter(|block| { + matches!(&block.terminator.kind, TerminatorKind::ForEach { + key: current_key, + value: current_value, + by_view: current_view, + iterable: current_iterable, + .. + } if *current_key == key + && *current_value == value + && *current_view == by_view + && crate::breakpoint_regions::expressions_equal(current_iterable, iterable)) + }) + .count(); + if occurrences != 1 { + return Err("Resource carrier iterator lost its unique exact current ForEach".into()); + } + if iterable.ty.index() as usize >= types.len() { + return Err("Resource carrier iterator lost its exact iterable type".into()); + } + if by_view + || matches!(&iterable.kind, E::View(_) | E::Field { .. }) + || matches!(&iterable.kind, E::Local(local) if function.is_view_local(*local)) + { + return Err("pending ResourceOwnershipPlan: borrowed carrier iteration needs its exact sequence transport".into()); + } + match types.resolve(iterable.ty) { + Type::List(element) => { + if value.is_some() { + return Err("Resource carrier list changed its sole element binder".into()); + } + carrier_binding(function, types, key, *element) + } + Type::Map(key_type, value_type) => { + let value = value.ok_or("pending ResourceOwnershipPlan: key-only carrier map iteration needs its exact residual-value transport")?; + if key == value { + return Err("Resource carrier map reused its key and value binder".into()); + } + // Checked map keys have no Resource custody; their ordinary storage + // remains separate from the value carrier extracted next. + ordinary_type(types, *key_type)?; + carrier_binding(function, types, key, *key_type)?; + carrier_binding(function, types, value, *value_type) + } + _ => Err("pending ResourceOwnershipPlan: this carrier iterator needs its dedicated sequence normalization".into()), + } +} +pub(super) fn ordinary_switch( + function: &Function, + types: &TypeInterner, + scrutinee: &Expression, + variants: &[(VariantId, BlockId, Vec)], + otherwise: Option, +) -> Result<(), String> { + ordinary_type(types, scrutinee.ty)?; + let Type::Enum(id) = types.resolve(scrutinee.ty) else { + return Err("Resource switch requires its exact ordinary enum discriminant".into()); + }; + let definition = types.resolve_enum(*id); + let mut seen = BTreeSet::new(); + for (variant, _, bindings) in variants { + let index = variant.index() as usize; + let fields = &definition + .variants + .get(index) + .ok_or("Resource switch selected a foreign enum variant")? + .fields; + if !seen.insert(index) || fields.len() != bindings.len() { + return Err("Resource switch changed its distinct ordinary variant bindings".into()); + } + for ((_, expected), binding) in fields.iter().zip(bindings) { + ordinary_type(types, *expected)?; + if header(function, types, *binding)?.ty != *expected { + return Err("Resource switch changed its exact ordinary payload type".into()); + } + } + } + if otherwise.is_none() && seen.len() != definition.variants.len() { + return Err("Resource switch lost its checked exhaustive variant edges".into()); + } + Ok(()) +} +fn sequence_element( + function: &Function, + types: &TypeInterner, + source: &SequenceSource, +) -> Result { + let SequenceSource::Local(local) = source else { + return Err( + "pending ResourceOwnershipPlan: projected sequence needs its exact aggregate transport" + .into(), + ); + }; + let source = header(function, types, *local)?; + let Type::List(element) = types.resolve(source.ty) else { + return Err("Resource sequence statement changed its exact ordinary list source".into()); + }; + ordinary_type(types, *element)?; + Ok(*element) +} +pub(super) fn sequence_length( + function: &Function, + types: &TypeInterner, + source: &SequenceSource, + target: LocalId, +) -> Result<(), String> { + sequence_element(function, types, source)?; + if header(function, types, target)?.ty != TypeInterner::INT64 { + return Err("Resource sequence length changed its ordinary integer destination".into()); + } + Ok(()) +} +pub(super) fn sequence_get( + function: &Function, + types: &TypeInterner, + consume: bool, + source: &SequenceSource, + index: LocalId, + target: LocalId, + part: SequencePart, +) -> Result<(), String> { + let element = sequence_element(function, types, source)?; + if !consume + || part != SequencePart::Element + || header(function, types, index)?.ty != TypeInterner::INT64 + || header(function, types, target)?.ty != element + { + return Err( + "Resource consuming sequence changed its exact ordinary element extraction".into(), + ); + } + Ok(()) +} +fn local(id: LocalId, ty: TypeId, span: Span) -> Expression { + Expression { + kind: E::Local(id), + ty, + span, + } +} +fn temporary(function: &mut Function, ty: TypeId, span: Span) -> LocalId { + let id = LocalId::new(function.locals.len() as u32); + function.locals.push(Local { + id, + name: format!("$iterator{}", id.index()), + ty, + debug_ty: ty, + debug_type_name: None, + mutable: true, + view_source: None, + span, + }); + id +} +/// Reconstruct rather than trusting the edit transcript to choose statements, +/// headers, guards or edges. The before graph must still match all independent seals. +fn canonical_sequence( + before: &Function, + edit: &crate::breakpoint_regions::SequenceEdit, + types: &TypeInterner, +) -> Result<(Function, BlockId, usize), String> { + let block = before + .blocks + .get(edit.header.index() as usize) + .filter(|block| block.id == edit.header) + .ok_or("Resource sequence transition lost its exact original header")?; + let TerminatorKind::ForEach { + key, + value: value_binding, + by_view, + iterable, + body, + exit, + } = &block.terminator.kind + else { + return Err("Resource sequence transition has no exact original ForEach".into()); + }; + if iterable.ty.index() as usize >= types.len() { + return Err("Resource sequence transition lost its exact iterable type".into()); + } + if resource_type_pending(types, iterable.ty) + || matches!(types.resolve(iterable.ty), Type::Map(..)) + { + carrier_sequence(before, types, *key, *value_binding, *by_view, iterable)?; + } else { + consuming_list(before, types, *key, *value_binding, *by_view, iterable)?; + } + let cfg = ControlFlowGraph::analyze(before) + .map_err(|errors| format!("Resource sequence CFG is malformed: {errors:?}"))?; + let mut outside = vec![false; before.blocks.len()]; + let mut pending = vec![before.entry]; + while let Some(current) = pending.pop() { + if current == edit.header || outside[current.index() as usize] { + continue; + } + outside[current.index() as usize] = true; + pending.extend_from_slice(cfg.successors(current)); + } + let preheaders = cfg + .predecessors(edit.header) + .iter() + .copied() + .filter(|id| outside[id.index() as usize]) + .collect::>(); + let [preheader] = preheaders.as_slice() else { + return Err("Resource sequence transition has no unique original preheader".into()); + }; + if !matches!(before.blocks[preheader.index() as usize].terminator.kind, TerminatorKind::Goto(target) if target == edit.header) + { + return Err("Resource sequence transition changed its exact preheader edge".into()); + } + let mut expected = before.clone(); + let span = iterable.span; + let cursor = temporary(&mut expected, TypeInterner::INT64, span); + let length = temporary(&mut expected, TypeInterner::INT64, span); + let source = temporary(&mut expected, iterable.ty, span); + let value = if let E::View(inner) = &iterable.kind { + *inner.clone() + } else { + iterable.clone() + }; + let initialization = vec![ + Statement { + kind: StatementKind::Let { + local: source, + value, + }, + span, + }, + Statement { + kind: StatementKind::Let { + local: cursor, + value: Expression { + kind: E::Int(0), + ty: TypeInterner::INT64, + span, + }, + }, + span, + }, + Statement { + kind: StatementKind::SequenceLength { + source: SequenceSource::Local(source), + target: length, + }, + span, + }, + ]; + let mut prefix = vec![ + Statement { + kind: StatementKind::SequenceGet { + consume: true, + source: SequenceSource::Local(source), + index: cursor, + target: *key, + part: if matches!(types.resolve(iterable.ty), Type::Map(..)) { + SequencePart::Key + } else { + SequencePart::Element + }, + }, + span, + }, + Statement { + kind: StatementKind::Assign { + target: local(cursor, TypeInterner::INT64, span), + value: Expression { + kind: E::Binary { + left: Box::new(local(cursor, TypeInterner::INT64, span)), + op: BinaryOp::Add, + right: Box::new(Expression { + kind: E::Int(1), + ty: TypeInterner::INT64, + span, + }), + }, + ty: TypeInterner::INT64, + span, + }, + }, + span, + }, + ]; + if let Some(value) = value_binding { + prefix.insert( + 1, + Statement { + kind: StatementKind::SequenceGet { + consume: true, + source: SequenceSource::Local(source), + index: cursor, + target: *value, + part: SequencePart::Value, + }, + span, + }, + ); + } + let prefix_length = prefix.len(); + let terminator = Terminator { + kind: TerminatorKind::Branch { + condition: Expression { + kind: E::Binary { + left: Box::new(local(cursor, TypeInterner::INT64, span)), + op: BinaryOp::Less, + right: Box::new(local(length, TypeInterner::INT64, span)), + }, + ty: TypeInterner::BOOL, + span, + }, + then_block: *body, + else_block: *exit, + }, + span: block.terminator.span, + }; + if !crate::breakpoint_regions::terminator_equal(&edit.before, &block.terminator) + || !crate::breakpoint_regions::terminator_equal(&edit.after, &terminator) + || !edit.redirects.is_empty() + || !edit.blocks.is_empty() + || edit.append.len() != 1 + || edit.append[0].0 != *preheader + || edit.prefix.as_ref().map(|(id, _)| *id) != Some(*body) + { + return Err( + "Resource sequence transcript differs from the canonical consuming iterator edit" + .into(), + ); + } + let same_statements = |left: &[Statement], right: &[Statement]| { + left.len() == right.len() + && left + .iter() + .zip(right) + .all(|(left, right)| crate::breakpoint_regions::statement_equal(left, right)) + }; + if !same_statements(&edit.append[0].1, &initialization) + || !same_statements( + &edit + .prefix + .as_ref() + .ok_or("Resource sequence prefix disappeared")? + .1, + &prefix, + ) + { + return Err( + "Resource sequence transcript changed its exact source, cursor or extraction order" + .into(), + ); + } + expected.blocks[preheader.index() as usize] + .statements + .extend(initialization); + let statements = &mut expected.blocks[body.index() as usize].statements; + let mut all = prefix; + all.append(statements); + *statements = all; + expected.blocks[edit.header.index() as usize].terminator = terminator; + Ok((expected, *body, prefix_length)) +} +pub(crate) fn sequence_transition( + function: &mut Function, + before: &Function, + edit: &crate::breakpoint_regions::SequenceEdit, + types: &TypeInterner, +) -> Result<(), String> { + let Some(original) = before.resource_lowering.as_ref() else { + return Ok(()); + }; + original.current(before)?; + if !function + .resource_lowering + .as_ref() + .is_some_and(|current| current.same(original)) + { + return Err("Resource sequence transition changed its exact before-witness".into()); + } + if !has_execution_records(before, types) && !original.reflected_fields.is_empty() { + return reflected_fields::ordinary_sequence_transition(function, before, edit, types); + } + let (expected, body, prefix_length) = canonical_sequence(before, edit, types)?; + if function.id != expected.id + || function.identity != expected.identity + || function.params != expected.params + || function.locals != expected.locals + || function.capture_count != expected.capture_count + || function.return_type != expected.return_type + || function.entry != expected.entry + || function.span != expected.span + || function.debug_kind != expected.debug_kind + || !crate::breakpoint_regions::blocks_equal(&function.blocks, &expected.blocks) + { + return Err( + "Resource sequence transition changed more than its exact reconstructed canonical edit" + .into(), + ); + } + let mut witness = original.clone(); + witness.locals = expected.locals.clone(); + witness.blocks = expected.blocks.clone(); + borrowed_sums::shift_prefix(&mut witness, body, prefix_length); + lexical_borrows::shift_prefix(&mut witness, body, prefix_length); + for region in &mut witness.regions { + region.shift_prefix(body, prefix_length); + } + for region in &mut witness.constructors { + region.shift_prefix(body, prefix_length); + } + let mut constructor_seal = witness.constructor_seal.to_vec(); + for region in &mut constructor_seal { + region.shift_prefix(body, prefix_length); + } + witness.constructor_seal = constructor_seal.into(); + // The independent body advances only after reconstructing the same finite edit. + if original.descriptors.has_body() { + witness.descriptors.seal_body(&expected); + } + witness.current(function)?; + function.resource_lowering = Some(witness); + Ok(()) +} diff --git a/crates/jett_mir/src/resource_ownership/control_flow_tests.rs b/crates/jett_mir/src/resource_ownership/control_flow_tests.rs new file mode 100644 index 00000000..19a283fe --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/control_flow_tests.rs @@ -0,0 +1,596 @@ +use super::tests::{checked_support, lowered}; +use super::*; +use crate::breakpoint_regions::SequenceEdit; +use hir::ExpressionKind as E; + +const SUPPORT: &str = + include_str!("../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); +const SOURCES: [&str; 5] = [ + include_str!( + "../../../jett_driver/tests/native_conformance/resource/control_flow/01_for_owned_optional_reuse.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/control_flow/02_for_owned_optional_break.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/control_flow/03_for_owned_result_continue.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/control_flow/04_match_owned_optional_first.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/control_flow/05_match_owned_optional_second.jett" + ), +]; +fn main(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "main" + }) + .unwrap() +} +fn main_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "main" + }) + .unwrap() +} +fn edit(before: &Function, prepared: &Function) -> SequenceEdit { + let block = before + .blocks + .iter() + .find(|block| matches!(block.terminator.kind, TerminatorKind::ForEach { .. })) + .unwrap(); + let TerminatorKind::ForEach { body, .. } = block.terminator.kind else { + unreachable!() + }; + let preheader = before + .blocks + .iter() + .find(|candidate| { + matches!(candidate.terminator.kind, TerminatorKind::Goto(target) if target == block.id) + && prepared.blocks[candidate.id.index() as usize] + .statements + .len() + > candidate.statements.len() + }) + .unwrap(); + SequenceEdit { + header: block.id, + before: block.terminator.clone(), + after: prepared.blocks[block.id.index() as usize] + .terminator + .clone(), + append: vec![( + preheader.id, + prepared.blocks[preheader.id.index() as usize].statements[preheader.statements.len()..] + .to_vec(), + )], + prefix: Some(( + body, + prepared.blocks[body.index() as usize].statements[..2].to_vec(), + )), + redirects: Vec::new(), + blocks: Vec::new(), + } +} +fn site_can_reach(function: &Function, start: ResourceSite, end: ResourceSite) -> bool { + let cfg = ControlFlowGraph::analyze(function).unwrap(); + let position = |site: ResourceSite| match site.position() { + ResourcePosition::Statement(index) => index, + ResourcePosition::Terminator => function.blocks[site.block().index() as usize] + .statements + .len(), + }; + let mut pending = vec![(start.block(), position(start) + 1)]; + let mut seen = BTreeSet::new(); + while let Some((block, floor)) = pending.pop() { + if !seen.insert((block.index(), floor)) { + continue; + } + if block == end.block() && position(end) >= floor { + return true; + } + pending.extend(cfg.successors(block).iter().map(|target| (*target, 0))); + } + false +} +#[test] +fn resource_control_flow_original_for_and_match_retire_exact_loans_before_backing_reuse() { + for release in [false, true] { + for (index, source) in SOURCES.iter().enumerate() { + let checked = checked_support(source, SUPPORT, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + validate_resource_ownership(&program, types).unwrap(); + let original = main(&program).clone(); + let original_rows = original + .resource_lowering + .as_ref() + .unwrap() + .borrowed_sums + .clone(); + crate::prepare_native_sequences(&mut program, types); + let function = main(&program); + assert!( + !function + .blocks + .iter() + .any(|block| matches!(block.terminator.kind, TerminatorKind::ForEach { .. })), + "Source{} release={release}", + index + 1 + ); + let ownership = validate_resource_ownership(&program, types).unwrap(); + let plan = ownership.function(function.id).unwrap(); + let rows = &function.resource_lowering.as_ref().unwrap().borrowed_sums; + assert_eq!(rows.len(), if index < 3 { 1 } else { 2 }); + for (old, row) in original_rows.iter().zip(rows) { + assert_eq!(old.original_initializer(), row.original_initializer()); + assert_eq!(old.alias(), row.alias()); + assert_eq!(old.backing(), row.backing()); + for local in row.retirement_locals() { + assert!(function.is_view_local(local)); + assert!(!plan.owner_slots().iter().any(|slot| matches!(slot.storage(), ResourceSlotStorage::Local { header } if header.id == local))); + } + let backing = plan.owner_slots().iter().find(|slot| matches!(slot.storage(), ResourceSlotStorage::Local { header } if header.id == row.backing())).unwrap().id(); + let projection = plan + .operations() + .iter() + .find(|operation| { + operation.site() == row.success_site() + && matches!( + operation.role(), + ResourceOperationRole::ProjectSumView { .. } + ) + }) + .unwrap(); + let ResourceOperationRole::ProjectSumView { + source: parent, + destination: child, + .. + } = projection.role() + else { + unreachable!() + }; + let expected_kind = match index { + 1 => ResourceLexicalExitKind::Break, + 2 => ResourceLexicalExitKind::Continue, + _ => ResourceLexicalExitKind::Fallthrough, + }; + let exits = function + .blocks + .iter() + .flat_map(|block| { + (0..block.statements.len()) + .map(ResourcePosition::Statement) + .chain(std::iter::once(ResourcePosition::Terminator)) + .filter_map(move |position| { + let exit = plan.lexical_exit(block.id, position)?; + (exit.kind() == expected_kind + && exit.clear_locals().contains(&row.alias())) + .then_some(( + ResourceSite { + function: function.id, + block: block.id, + position, + }, + exit, + )) + }) + }) + .collect::>(); + assert!(!exits.is_empty()); + for (_, exit) in &exits { + let operations = exit + .operation_ids() + .iter() + .map(|id| &plan.operations()[id.index()]) + .collect::>(); + let child_end = operations.iter().position(|operation| matches!(operation.role(), ResourceOperationRole::EndBorrow { loan } if loan == child)).unwrap(); + let parent_end = operations.iter().position(|operation| matches!(operation.role(), ResourceOperationRole::EndSumBorrow { loan } if loan == parent)).unwrap(); + assert!( + child_end < parent_end, + "Source{} child retires before the parent shell lease", + index + 1 + ); + assert!(!exit.clear_locals().contains(&row.backing())); + assert!( + exit.clear_locals() + .iter() + .all(|local| row.retirement_locals().contains(local)), + "an arm cannot retire its sibling alias" + ); + } + let moves = plan.operations().iter().filter(|operation| matches!(operation.role(), ResourceOperationRole::Transfer { source, .. } if *source == backing)).collect::>(); + assert_eq!( + moves.len(), + 1, + "original held is reused once after the borrowed scope" + ); + for (site, _) in exits { + assert!( + site_can_reach(function, site, moves[0].site()), + "the original backing is reused after this actual lexical boundary" + ); + } + for operation in plan.operations() { + if matches!(operation.role(), ResourceOperationRole::Drop { source, .. } if *source == backing) + { + assert!( + !site_can_reach(function, operation.site(), moves[0].site()), + "aborting cleanup cannot consume the backing on a path to its later owning unwrap" + ); + } + } + } + let once = program.clone(); + crate::prepare_native_sequences(&mut program, types); + assert_eq!( + program, once, + "the no-op preparation must preserve every authenticated witness" + ); + } + } +} +#[test] +fn resource_control_flow_canonical_sequence_refuses_forged_transcripts_and_custody_edits() { + for release in [false, true] { + let checked = checked_support(SOURCES[0], SUPPORT, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + let before = main(&baseline); + let mut prepared = baseline.clone(); + crate::prepare_native_sequences(&mut prepared, types); + let good = main(&prepared); + let canonical = edit(before, good); + let mut replay = good.clone(); + replay.resource_lowering = before.resource_lowering.clone(); + control_flow::sequence_transition(&mut replay, before, &canonical, types).unwrap(); + assert_eq!(&replay, good); + for mutation in 0..10 { + let mut candidate = good.clone(); + candidate.resource_lowering = before.resource_lowering.clone(); + let mut transcript = canonical.clone(); + let (body, prefix) = transcript.prefix.as_mut().unwrap(); + let body = *body; + match mutation { + 0 => { + prefix.swap(0, 1); + candidate.blocks[body.index() as usize] + .statements + .swap(0, 1); + } + 1 => { + let TerminatorKind::Branch { condition, .. } = &mut transcript.after.kind + else { + unreachable!() + }; + let E::Binary { op, .. } = &mut condition.kind else { + unreachable!() + }; + *op = hir::BinaryOp::LessEqual; + candidate.blocks[transcript.header.index() as usize].terminator = + transcript.after.clone(); + } + 2 => { + let StatementKind::SequenceGet { consume, .. } = &mut prefix[0].kind else { + unreachable!() + }; + *consume = false; + candidate.blocks[body.index() as usize].statements[0] = prefix[0].clone(); + } + 3 => { + let StatementKind::SequenceGet { target, .. } = &mut prefix[0].kind else { + unreachable!() + }; + *target = before.resource_lowering.as_ref().unwrap().borrowed_sums[0].backing(); + candidate.blocks[body.index() as usize].statements[0] = prefix[0].clone(); + } + 4 => { + let duplicate = candidate + .blocks + .iter() + .flat_map(|block| &block.statements) + .find(|statement| { + matches!(statement.kind, StatementKind::ResourceLexicalExit(_)) + }) + .unwrap() + .clone(); + prefix.push(duplicate.clone()); + candidate.blocks[body.index() as usize] + .statements + .insert(2, duplicate); + } + 5 => { + let TerminatorKind::Branch { else_block, .. } = &mut transcript.after.kind + else { + unreachable!() + }; + *else_block = body; + candidate.blocks[transcript.header.index() as usize].terminator = + transcript.after.clone(); + } + 6 => { + transcript.append[0].1.swap(0, 2); + let (preheader, values) = &transcript.append[0]; + let start = before.blocks[preheader.index() as usize].statements.len(); + candidate.blocks[preheader.index() as usize].statements[start..] + .clone_from_slice(values); + } + 7 => { + candidate.locals.last_mut().unwrap().ty = before + .local( + before.resource_lowering.as_ref().unwrap().borrowed_sums[0].backing(), + ) + .unwrap() + .ty; + } + 8 => { + let statement = candidate + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find(|statement| { + matches!(statement.kind, StatementKind::SumTake { success: true, .. }) + }) + .unwrap(); + let StatementKind::SumTake { source, .. } = &mut statement.kind else { + unreachable!() + }; + *source = before.resource_lowering.as_ref().unwrap().borrowed_sums[0].backing(); + } + 9 => { + transcript.prefix = None; + } + _ => unreachable!(), + } + let unchanged = candidate.resource_lowering.clone(); + assert!( + control_flow::sequence_transition(&mut candidate, before, &transcript, types) + .is_err(), + "mutation={mutation} release={release}" + ); + assert_eq!( + candidate.resource_lowering, unchanged, + "rejected edit grants no replacement witness" + ); + } + } +} +#[test] +fn resource_control_flow_prepared_for_and_match_refuse_resealed_duplicate_or_moved_loan_sites() { + for release in [false, true] { + for index in [0, 1, 2, 3, 4] { + let checked = checked_support(SOURCES[index], SUPPORT, release); + let types = &checked.checked().interner; + let mut baseline = lowered(&checked); + crate::prepare_native_sequences(&mut baseline, types); + validate_resource_ownership(&baseline, types).unwrap(); + for mutation in 0..4 { + let mut candidate = baseline.clone(); + let function = main_mut(&mut candidate); + let row = function.resource_lowering.as_ref().unwrap().borrowed_sums[0].clone(); + let project = row.success_site(); + let ResourcePosition::Statement(position) = project.position() else { + unreachable!() + }; + match mutation { + 0 => { + let duplicate = function.blocks[project.block().index() as usize] + .statements[position] + .clone(); + function.blocks[project.block().index() as usize] + .statements + .insert(position + 1, duplicate); + } + 1 => { + function.blocks[project.block().index() as usize] + .statements + .remove(position); + } + 2 => { + let exit = function + .blocks + .iter_mut() + .find(|block| { + block.statements.iter().any(|statement| { + matches!(statement.kind, StatementKind::ResourceLexicalExit(_)) + }) + }) + .unwrap(); + let boundary = exit + .statements + .iter() + .position(|statement| { + matches!(statement.kind, StatementKind::ResourceLexicalExit(_)) + }) + .unwrap(); + let duplicate = exit.statements[boundary].clone(); + exit.statements.insert(0, duplicate); + } + 3 => { + let block = function + .blocks + .iter_mut() + .find(|block| { + matches!( + block.terminator.kind, + TerminatorKind::Switch { .. } | TerminatorKind::Branch { .. } + ) + }) + .unwrap(); + match &mut block.terminator.kind { + TerminatorKind::Switch { variants, .. } => { + let target = variants[0].1; + variants[0].1 = variants[1].1; + variants[1].1 = target; + } + TerminatorKind::Branch { + then_block, + else_block, + .. + } => { + std::mem::swap(then_block, else_block); + } + _ => unreachable!(), + } + } + _ => unreachable!(), + } + let witness = function.resource_lowering.as_mut().unwrap(); + witness.blocks = function.blocks.clone(); + witness.locals = function.locals.clone(); + assert!( + validate_resource_ownership(&candidate, types).is_err(), + "Source{} mutation={mutation} release={release}", + index + 1 + ); + assert!( + !crate::sequences::prune::unreachable(main_mut(&mut candidate)), + "canonical pruning cannot rehabilitate a forged loan graph" + ); + } + } + } +} + +// Draft append to resource_ownership/control_flow_tests.rs after Source registration. +// UNEXECUTED: Root must first compile/check both new Source programs and derive +// provider transcripts from the reference executor. No proposed outcome is acceptance. +const MULTI_LOOP_SOURCES: [&str; 2] = [ + include_str!( + "../../../jett_driver/tests/native_conformance/resource/control_flow/07_consecutive_for_owned_optional_reuse.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/control_flow/08_nested_for_owned_optional_reuse.jett" + ), +]; +#[test] +fn resource_control_flow_repeated_sequence_transitions_preserve_all_sibling_and_nested_loans() { + for release in [false, true] { + for (index, source) in MULTI_LOOP_SOURCES.iter().enumerate() { + let checked = checked_support(source, SUPPORT, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + validate_resource_ownership(&program, types).unwrap(); + let original = main(&program).clone(); + let original_witness = original.resource_lowering.as_ref().unwrap(); + assert_eq!( + original + .blocks + .iter() + .filter(|block| matches!(block.terminator.kind, TerminatorKind::ForEach { .. })) + .count(), + 2 + ); + assert_eq!(original_witness.borrowed_sums.len(), 2); + crate::prepare_native_sequences(&mut program, types); + let function = main(&program); + assert!( + !function + .blocks + .iter() + .any(|block| matches!(block.terminator.kind, TerminatorKind::ForEach { .. })) + ); + let plan = validate_resource_ownership(&program, types).unwrap(); + let custody = plan.function(function.id).unwrap(); + let witness = function.resource_lowering.as_ref().unwrap(); + assert_eq!(witness.borrowed_sums.len(), 2); + for (before, after) in original_witness + .borrowed_sums + .iter() + .zip(&witness.borrowed_sums) + { + assert_eq!(before.original_initializer(), after.original_initializer()); + assert_eq!(before.alias(), after.alias()); + assert_eq!(before.backing(), after.backing()); + } + let rows = &witness.borrowed_sums; + assert_eq!(rows[0].backing(), rows[1].backing()); + assert_ne!(rows[0].alias(), rows[1].alias()); + if index == 0 { + let first_alias = function.local(rows[0].alias()).unwrap(); + let second_alias = function.local(rows[1].alias()).unwrap(); + assert_eq!(first_alias.name, "token"); + assert_eq!(second_alias.name, "token"); + assert_ne!(first_alias.span, second_alias.span); + } + assert_eq!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| matches!( + statement.kind, + StatementKind::SequenceGet { consume: true, .. } + )) + .count(), + 2 + ); + let projects = custody + .operations() + .iter() + .filter_map(|operation| { + let ResourceOperationRole::ProjectSumView { + source, + destination, + .. + } = operation.role() + else { + return None; + }; + Some((operation, *source, *destination)) + }) + .collect::>(); + assert_eq!(projects.len(), 2); + for (position, row) in rows.iter().enumerate() { + let (_, parent, child) = projects + .iter() + .find(|(operation, _, _)| operation.site() == row.success_site()) + .unwrap(); + let exits = function + .blocks + .iter() + .flat_map(|block| { + (0..block.statements.len()) + .map(ResourcePosition::Statement) + .chain(std::iter::once(ResourcePosition::Terminator)) + .filter_map(move |site| custody.lexical_exit(block.id, site)) + }) + .filter(|exit| { + exit.kind() == ResourceLexicalExitKind::Fallthrough + && exit.clear_locals().contains(&row.alias()) + }) + .collect::>(); + assert!(!exits.is_empty()); + for exit in exits { + let roles = exit + .operation_ids() + .iter() + .map(|id| custody.operations()[id.index()].role()) + .collect::>(); + let child_end = roles.iter().position(|role| matches!(role, ResourceOperationRole::EndBorrow { loan } if loan == child)).unwrap(); + let parent_end = roles.iter().position(|role| matches!(role, ResourceOperationRole::EndSumBorrow { loan } if loan == parent)).unwrap(); + assert!(child_end < parent_end); + assert!(!exit.clear_locals().contains(&row.backing())); + if index == 0 || position == 1 { + assert!( + !exit.clear_locals().contains(&rows[1 - position].alias()), + "sibling fallthrough or nested inner exit cannot retire another live scope" + ); + } + } + } + let once = program.clone(); + crate::prepare_native_sequences(&mut program, types); + assert_eq!(program, once); + } + } +} diff --git a/crates/jett_mir/src/resource_ownership/entry_scope.rs b/crates/jett_mir/src/resource_ownership/entry_scope.rs new file mode 100644 index 00000000..88e1cbfa --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/entry_scope.rs @@ -0,0 +1,163 @@ +//! A selected native wrapper Scope does not make its ordinary body a Source family. +use super::*; + +/// Wrapper-only metadata borrowed from a fully authenticated current Program. +#[derive(Debug)] +pub struct ResourceEntryScopePlan { + function: FunctionId, + identity: hir::FunctionIdentity, + parameters: Vec, + return_type: TypeId, + root_scope: ResourceFrame, + completion: ResourceOperation, +} +impl ResourceEntryScopePlan { + pub fn function(&self) -> FunctionId { + self.function + } + pub fn identity(&self) -> &hir::FunctionIdentity { + &self.identity + } + pub fn parameters(&self) -> &[Param] { + &self.parameters + } + pub fn return_type(&self) -> TypeId { + self.return_type + } + pub fn root_scope(&self) -> &ResourceFrame { + &self.root_scope + } + pub fn completion(&self) -> &ResourceOperation { + &self.completion + } +} + +pub(super) fn capture_needed( + function: &hir::Function, + source: &hir::ResourceSourceArchive, +) -> bool { + source.manifest().is_some() + && function.capture_count == 0 + && function.return_type == TypeInterner::NOTHING + && function + .params + .iter() + .all(|param| param.ty == TypeInterner::NETWORK) +} + +/// Selects an authenticated wrapper Scope without changing execution-family membership. +pub fn validate_resource_ownership_for_entry<'p>( + program: &'p Program, + types: &'p TypeInterner, + entry: FunctionId, +) -> Result, Vec> { + crate::validate(program)?; + let mut plan = validate_resource_ownership(program, types)?; + let span = program + .functions + .first() + .map_or(Span::new(jett_common::FileId::new(0), 0, 0), |function| { + function.span + }); + let selected = program + .functions + .get(entry.index() as usize) + .filter(|function| function.id == entry) + .ok_or_else(|| { + vec![ValidationError { + span, + message: "native Resource entry has no exact current function".into(), + }] + })?; + let refusal = |message: &str| { + vec![ValidationError { + span: selected.span, + message: message.into(), + }] + }; + if plan.required_only_function_ids().contains(&entry) { + return Err(refusal( + "required-only Resource helper cannot be a native entry", + )); + } + let witness = selected.resource_lowering.as_ref().ok_or_else(|| { + refusal("native Resource entry lacks its initially authenticated Source witness") + })?; + let original = plan + .original_source(entry) + .ok_or_else(|| refusal("native Resource entry has no exact original Source function"))?; + if !capture_needed(original, &witness.source) + || !capture_needed(&witness.original, &witness.source) + || selected.identity != original.identity + || selected.capture_count != 0 + || selected.return_type != original.return_type + || selected.params.len() != original.params.len() + || !selected + .params + .iter() + .zip(&original.params) + .all(|(current, archived)| { + current.name == archived.name + && current.ty == archived.ty + && current.mode == archived.mode + && current.mutable == archived.mutable + && current.span == archived.span + }) + { + return Err(refusal( + "native Resource entry changed its closed Network/Nothing Source header", + )); + } + if plan.function(entry).is_some() { + return Ok(plan); + } + if has_execution_records(selected, types) { + return Err(refusal( + "native entry cannot replace a missing Source execution-family plan", + )); + } + ControlFlowGraph::analyze(selected).map_err(|errors| { + errors + .into_iter() + .map(|error| ValidationError { + span: selected.span, + message: format!("native Resource entry CFG: {}", error.message), + }) + .collect::>() + })?; + let site = ResourceSite { + function: entry, + block: selected.entry, + position: ResourcePosition::Terminator, + }; + let frame = ResourceFrameId(0); + plan.entry_scope = Some(ResourceEntryScopePlan { + function: entry, + identity: selected.identity.clone(), + parameters: selected.params.clone(), + return_type: selected.return_type, + root_scope: ResourceFrame { + id: frame, + role: ResourceFrameRole::Scope, + site, + parent: None, + ordinal: 0, + }, + completion: ResourceOperation { + id: ResourceOperationId(0), + frame, + site, + ordinal: 0, + role: ResourceOperationRole::Complete { + outcome: ResourceCompletion::Return, + }, + expression: None, + named_indirect: None, + indirect_hook: None, + }, + }); + Ok(plan) +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_mir/src/resource_ownership/entry_scope/tests.rs b/crates/jett_mir/src/resource_ownership/entry_scope/tests.rs new file mode 100644 index 00000000..f92e532e --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/entry_scope/tests.rs @@ -0,0 +1,912 @@ +use super::*; +use crate::resource_ownership::tests::{checked, checked_support, lowered}; +use jett_common::{FileId, SourceOrigin}; +use jett_comptime::checked_types::CheckedExpressionTypes; +use jett_parser::parse; +use jett_typecheck::CheckedResourceProgram; +use std::{collections::HashMap, sync::Arc}; + +include!( + "../../../../jett_codegen_cranelift/src/emit/resource_execution/tests/carrier_wrapper_sources.rs" +); + +const WHOLE_SOURCE: &str = include_str!("../fixtures/17_absent_aggregate_shapes.jett"); +const FIXTURE_SUPPORT: &str = + include_str!("../../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); +const PURE: &str = r#"namespace app +function helper() returns int64: + return 7 +function ordinary_probe(view net: Network) returns nothing: + int64 answer = helper() + if answer != 7: + return nothing + return nothing + int64 dead = 9 + return nothing +function peer(view net: Network) returns nothing: + return nothing +"#; + +fn named<'a>(program: &'a Program, name: &str) -> &'a Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap_or_else(|| panic!("checked app.{name}")) +} + +fn named_mut<'a>(program: &'a mut Program, name: &str) -> &'a mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap_or_else(|| panic!("checked app.{name}")) +} + +fn materialized(checked: &Arc) -> Program { + let expression_types = Arc::new(CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let required = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + expression_types, + Arc::new(HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "{:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + required.values.checked_required_values(checked).unwrap(); + assert!(!required.values.is_empty()); + let types = &checked.checked().interner; + let mut hir = hir::lower_checked_resource_program(checked).unwrap(); + hir::materialize_checked_required_values(&mut hir, &required.values, types).unwrap(); + hir::complete_value_conversions(&mut hir, types).unwrap(); + crate::lower(&hir, types).unwrap() +} + +fn prepare(program: &mut Program, types: &TypeInterner) { + crate::prepare_native_sequences(program, types); + crate::prepare_native_uninhabited_sums(program, types); + crate::prepare_native_generated_functions(program, types); +} + +fn assert_bridge_metadata(program: &Program, types: &TypeInterner, name: &str) { + let selected = named(program, name); + let ordinary = validate_resource_ownership(program, types).unwrap(); + assert!(ordinary.entry_scope().is_none()); + assert!(ordinary.function(selected.id).is_none()); + let ownership = validate_resource_ownership_for_entry(program, types, selected.id).unwrap(); + let bridge = ownership + .entry_scope() + .expect("selected wrapper-only Scope"); + assert_eq!(bridge.function(), selected.id); + assert_eq!(bridge.identity(), &selected.identity); + assert_eq!(bridge.parameters(), selected.params.as_slice()); + assert_eq!(bridge.return_type(), TypeInterner::NOTHING); + assert_eq!(bridge.root_scope().role(), ResourceFrameRole::Scope); + assert_eq!(bridge.root_scope().parent(), None); + assert_eq!(bridge.root_scope().site().function(), selected.id); + assert_eq!(bridge.root_scope().site().block(), selected.entry); + assert_eq!( + bridge.root_scope().site().position(), + ResourcePosition::Terminator + ); + assert_eq!(bridge.completion().frame(), bridge.root_scope().id()); + assert_eq!(bridge.completion().site(), bridge.root_scope().site()); + assert!(matches!( + bridge.completion().role(), + ResourceOperationRole::Complete { .. } + )); + assert!(!bridge.completion().is_expression_operation()); + assert!(bridge.completion().named_indirect_target().is_none()); + assert!(bridge.completion().indirect_hook_target().is_none()); + assert!(ownership.function(selected.id).is_none()); + assert!( + !ownership + .functions() + .iter() + .any(|function| function.function() == selected.id) + ); + assert!(ownership.original_source(selected.id).is_some()); + assert!(!has_execution_records(selected, types)); + validate_call_ownership(program, types).unwrap(); +} + +fn assert_bridge(program: &Program, types: &TypeInterner, name: &str) { + assert_bridge_metadata(program, types, name); + let selected = named(program, name); + let copy = + crate::copy_values::CopyValuePlan::analyze_storage(selected, types, Some(program)).unwrap(); + let storage = crate::move_values::MoveValuePlan::analyze(program, selected, types).unwrap(); + assert_eq!(copy.owned_locals, storage.owned_locals); + validate_call_ownership(program, types).unwrap(); +} + +fn reseal_general_cache(function: &mut Function) { + let parameters = function.params.clone(); + let locals = function.locals.clone(); + let blocks = function.blocks.clone(); + let entry = function.entry; + let witness = function.resource_lowering.as_mut().unwrap(); + witness.parameters = parameters; + witness.locals = locals; + witness.blocks = blocks; + witness.entry = entry; +} + +fn assert_refused_before_prune( + mut program: Program, + types: &TypeInterner, + name: &str, + locally_exact_foreign: bool, +) { + let selected = named(&program, name).id; + assert!(validate_resource_ownership_for_entry(&program, types, selected).is_err()); + let before = program.clone(); + crate::prepare_native_sequences(&mut program, types); + assert!( + program == before, + "refused canonical pruning must preserve the graph" + ); + assert!(validate_resource_ownership_for_entry(&program, types, selected).is_err()); + if named(&program, name).resource_lowering.is_some() { + let function = named(&program, name); + let witness = function.resource_lowering.as_ref().unwrap(); + assert_eq!(witness.current(function).is_ok(), locally_exact_foreign); + if locally_exact_foreign { + // A sibling's changed Source makes the whole archive foreign even + // when this function's local body is still exactly authenticated. + let original = witness.original.clone(); + let source = witness.source.clone(); + assert!(function.locals.iter().any(|local| local.name == "dead")); + assert!(crate::sequences::prune::unreachable(named_mut( + &mut program, + name + ))); + let function = named(&program, name); + assert!(!function.locals.iter().any(|local| local.name == "dead")); + let witness = function.resource_lowering.as_ref().unwrap(); + witness.current(function).unwrap(); + assert!(original_function_equal(&witness.original, &original)); + assert!(witness.source == source); + let errors = match validate_resource_ownership(&program, types) { + Err(errors) => errors, + Ok(_) => panic!("local pruning admitted a foreign Source archive"), + }; + assert!( + errors.iter().any(|error| error + .message + .contains("mixes foreign original/materialized archives")), + "{errors:?}" + ); + } else { + assert!(!crate::sequences::prune::unreachable(named_mut( + &mut program, + name + ))); + assert!(program == before); + } + } + assert!(validate_resource_ownership_for_entry(&program, types, selected).is_err()); +} + +#[test] +fn ordinary_entry_pure_checked_candidates_have_initial_body_seals_without_comptime_or_name_authority() + { + for release in [false, true] { + let checked = checked(PURE, release); + let types = &checked.checked().interner; + let hir = hir::lower_checked_resource_program(&checked).unwrap(); + assert!(hir.resource_source.required_materializations().is_empty()); + let mut has_comptime = false; + for function in hir + .functions + .iter() + .filter(|function| function.identity.declaration.namespace == "app") + { + walk::hir_block(&function.body, &mut |value| { + has_comptime |= matches!(value.kind, hir::ExpressionKind::Comptime { .. }); + }); + } + assert!(!has_comptime); + let baseline = crate::lower(&hir, types).unwrap(); + for name in ["ordinary_probe", "peer"] { + let function = named(&baseline, name); + let witness = function + .resource_lowering + .as_ref() + .expect("initial proof-only capture"); + assert!(witness.descriptors.has_body()); + witness.descriptors.current(function).unwrap(); + assert_bridge(&baseline, types, name); + } + assert!( + validate_resource_ownership(&baseline, types) + .unwrap() + .function(named(&baseline, "helper").id) + .is_none() + ); + let probe = named(&baseline, "ordinary_probe"); + assert!(probe.locals.iter().any(|local| local.name == "dead")); + let before_locals = probe.locals.len(); + let mut compacted = baseline.clone(); + prepare(&mut compacted, types); + assert!(named(&compacted, "ordinary_probe").locals.len() < before_locals); + assert_bridge(&compacted, types, "ordinary_probe"); + assert_bridge(&compacted, types, "peer"); + } +} + +#[test] +fn ordinary_entry_default_archive_hir_remains_ordinary_and_cannot_mint_an_entry_scope() { + let source = "namespace app\nfunction ordinary_probe(view net: Network) returns nothing:\n return nothing\n"; + let file = FileId::new(0); + let parsed = parse(source, file); + assert!(parsed.errors.is_empty()); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let origins = HashMap::from([(file, SourceOrigin::Project)]); + let hir = hir::lower(&parsed.module, &resolved, &checked, &origins).unwrap(); + assert!(hir.resource_source.manifest().is_none()); + let program = crate::lower(&hir, &checked.interner).unwrap(); + let selected = named(&program, "ordinary_probe"); + assert!(selected.resource_lowering.is_none()); + let ordinary = validate_resource_ownership(&program, &checked.interner).unwrap(); + assert!(ordinary.function(selected.id).is_none()); + assert!(ordinary.entry_scope().is_none()); + assert!( + validate_resource_ownership_for_entry(&program, &checked.interner, selected.id).is_err() + ); +} + +#[test] +fn ordinary_entry_consuming_list_for_and_later_ordinary_caller_preserve_the_initial_proof() { + const SOURCE: &str = r#"namespace app +function main() returns nothing: + list[int64] values = list(1, 2) + mutable int64 total = 0 + for number in values: + total = total + number + return nothing +function reenter() returns nothing: + main() + return nothing +"#; + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + let main = named(&program, "main"); + let selected = main.id; + assert!( + main.resource_lowering + .as_ref() + .unwrap() + .descriptors + .has_body() + ); + assert!( + main.blocks + .iter() + .any(|block| matches!(block.terminator.kind, TerminatorKind::ForEach { .. })) + ); + let initial = validate_resource_ownership_for_entry(&program, types, selected).unwrap(); + assert!(initial.entry_scope().is_some()); + assert!(initial.function(selected).is_none()); + assert!(initial.function(named(&program, "reenter").id).is_none()); + prepare(&mut program, types); + assert!( + !named(&program, "main") + .blocks + .iter() + .any(|block| matches!(block.terminator.kind, TerminatorKind::ForEach { .. })) + ); + assert_bridge(&program, types, "main"); + assert_bridge(&program, types, "reenter"); + let main = named(&program, "main"); + let storage = crate::move_values::MoveValuePlan::analyze(&program, main, types).unwrap(); + let source = main + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| { + if let StatementKind::SequenceGet { + consume: true, + source, + .. + } = &statement.kind + { + Some(source.root()) + } else { + None + } + }) + .expect("actual canonical consuming-list element extraction"); + assert!( + matches!(types.resolve(main.local(source).unwrap().ty), Type::List(inner) if *inner == TypeInterner::INT64) + ); + assert!(storage.owned_locals.contains(&(source.index() as usize))); + } +} + +#[test] +fn ordinary_entry_source17_wrapper14_keeps_required_helper_and_main_ordinary_before_and_after_preparation() + { + assert_eq!(WRAPPERS.len(), 14); + let (name, suffix) = WRAPPERS[13]; + assert_eq!(name, "14_required_primitive_controls"); + let source = format!("{WHOLE_SOURCE}{suffix}"); + for release in [false, true] { + let checked = checked_support(&source, FIXTURE_SUPPORT, release); + let types = &checked.checked().interner; + let baseline = materialized(&checked); + for prepared in [false, true] { + let mut program = baseline.clone(); + if prepared { + prepare(&mut program, types); + } + assert_bridge(&program, types, "main"); + let main = named(&program, "main"); + let helper = named(&program, "absent_required_controls"); + let ownership = + validate_resource_ownership_for_entry(&program, types, main.id).unwrap(); + assert!(ownership.function(helper.id).is_none()); + assert!(!has_execution_records(helper, types)); + crate::copy_values::CopyValuePlan::analyze(helper, types).unwrap(); + crate::move_values::MoveValuePlan::analyze(&program, helper, types).unwrap(); + let archived = ownership.original_source(helper.id).unwrap(); + let mut required = false; + walk::hir_block(&archived.body, &mut |value| { + required |= matches!(value.kind, hir::ExpressionKind::Comptime { .. }); + }); + assert!( + required, + "the complete original required-value helper remains archived" + ); + for constructor in [ + "empty_tokens", + "absent_tokens", + "empty_map", + "absent_map", + "empty_result_tokens", + "failed_result_tokens", + "absent_envelope", + "absent_box", + "empty_choice", + "absent_choice", + "absent_state", + "absent_exact_state", + ] { + named(&program, constructor); + } + named(&program, "refined_shape_contract"); + } + let ownership = validate_resource_ownership(&baseline, types).unwrap(); + assert!(!ownership.required_only_function_ids().is_empty()); + for &required_only in ownership.required_only_function_ids() { + let errors = + validate_resource_ownership_for_entry(&baseline, types, required_only).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("required-only")) + ); + } + } +} + +#[test] +fn ordinary_entry_missing_foreign_or_copied_witness_refuses_before_canonical_pruning() { + for release in [false, true] { + let checked = checked(PURE, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + let foreign = lowered(&self::checked( + &PURE.replace("return 7", "return 8"), + release, + )); + for mutation in 0..4 { + let mut changed = baseline.clone(); + let replacement = match mutation { + 0 => None, + 1 => named(&foreign, "ordinary_probe").resource_lowering.clone(), + 2 => named(&baseline, "peer").resource_lowering.clone(), + 3 => None, + _ => unreachable!(), + }; + named_mut(&mut changed, "ordinary_probe").resource_lowering = replacement; + if mutation == 3 { + named_mut(&mut changed, "ordinary_probe").capture_count = 1; + crate::validate(&changed).unwrap(); + let errors = match validate_resource_ownership(&changed, types) { + Err(errors) => errors, + Ok(_) => panic!("missing original entry witness was accepted"), + }; + assert!( + errors + .iter() + .any(|error| error.message.contains("constructor witness")), + "{:?}", + errors + ); + } + assert_refused_before_prune(changed, types, "ordinary_probe", mutation == 1); + } + } +} + +#[test] +fn ordinary_entry_changed_headers_and_dense_identity_cannot_be_resealed_into_authority() { + for release in [false, true] { + let checked = checked(PURE, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + let selected = named(&baseline, "ordinary_probe").id; + for mutation in 0..7 { + let mut changed = baseline.clone(); + let function = named_mut(&mut changed, "ordinary_probe"); + match mutation { + 0 => function.params[0].mode = ParamMode::Owned, + 1 => function.params[0].mutable = true, + 2 => function.params[0].ty = TypeInterner::INT64, + 3 => function.return_type = TypeInterner::INT64, + 4 => function.capture_count = 1, + 5 => function.identity.declaration.name = "replacement".into(), + 6 => function.id = named(&baseline, "peer").id, + _ => unreachable!(), + } + reseal_general_cache(function); + assert!( + validate_resource_ownership_for_entry(&changed, types, selected).is_err(), + "mutation={mutation}; release={release}" + ); + let before = changed.clone(); + let function = changed + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.name + == if mutation == 5 { + "replacement" + } else { + "ordinary_probe" + } + }) + .unwrap(); + assert!(!crate::sequences::prune::unreachable(function)); + assert!(changed == before); + } + assert!( + validate_resource_ownership_for_entry(&baseline, types, FunctionId::new(u32::MAX)) + .is_err() + ); + } +} + +#[test] +fn ordinary_entry_independent_body_seal_refuses_resealed_whole_body_and_disconnected_replacement() { + for release in [false, true] { + let checked = checked(PURE, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + for mutation in 0..3 { + let mut changed = baseline.clone(); + let function = named_mut(&mut changed, "ordinary_probe"); + match mutation { + 0 => { + let value = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| { + if let StatementKind::Let { value, .. } = &mut statement.kind { + Some(value) + } else { + None + } + }) + .unwrap(); + value.kind = hir::ExpressionKind::Int(99); + } + 1 => function.blocks[usize::try_from(function.entry.index()).unwrap()] + .statements + .clear(), + 2 => { + let mut copied = + function.blocks[usize::try_from(function.entry.index()).unwrap()].clone(); + copied.id = BlockId(u32::try_from(function.blocks.len()).unwrap()); + copied.terminator.kind = TerminatorKind::Return(None); + function.blocks.push(copied); + } + _ => unreachable!(), + } + reseal_general_cache(function); + assert_refused_before_prune(changed, types, "ordinary_probe", false); + } + } +} + +#[test] +fn ordinary_entry_independent_graph_is_mandatory_even_when_general_cache_is_exact() { + for release in [false, true] { + let checked = checked(PURE, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + let function = named_mut(&mut program, "ordinary_probe"); + assert!( + function + .resource_lowering + .as_ref() + .unwrap() + .descriptors + .has_body() + ); + function.resource_lowering.as_mut().unwrap().descriptors = + returned_descriptors::DescriptorWitness::default(); + assert_refused_before_prune(program, types, "ordinary_probe", false); + } +} + +#[test] +fn ordinary_entry_execution_family_retains_its_existing_body_scope_without_an_extra_bridge() { + const SOURCE: &str = include_str!("../fixtures/connected_entry.jett"); + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + let selected = named(&program, "main").id; + let plan = validate_resource_ownership_for_entry(&program, types, selected).unwrap(); + assert!(plan.entry_scope().is_none()); + assert!(plan.function(selected).is_some()); + } +} + +const RAW_REFINEMENTS: &str = r#"namespace app +type NonZeroInt = int64 where value != 0 +type Positive = int64 where value > 0 +function quotient(value: int64, divisor: NonZeroInt) returns int64: + return value / divisor +function pure_entry() returns nothing: + list[int64] values = list(1, 2) + mutable int64 total = 0 + for number in values: + total = total + number + return nothing +function ordinary_probe(view net: Network) returns nothing: + pure_entry() + return nothing +"#; + +#[test] +fn ordinary_entry_default_archive_refinement_predicates_remain_ordinary_through_preparation() { + for release in [false, true] { + let file = FileId::new(0); + let parsed = parse(RAW_REFINEMENTS, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check_with_options( + &parsed.module, + &resolved, + jett_typecheck::CheckOptions { release }, + ); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{:?}", + checked.diagnostics + ); + let origins = HashMap::from([(file, SourceOrigin::Project)]); + let hir = hir::lower(&parsed.module, &resolved, &checked, &origins).unwrap(); + assert!(hir.resource_source.manifest().is_none()); + assert_eq!( + hir.functions + .iter() + .filter(|function| { + function.identity.declaration.kind == hir::DeclarationKind::RefinementPredicate + }) + .count(), + 2 + ); + let types = &checked.interner; + let baseline = crate::lower(&hir, types).unwrap(); + assert!( + named(&baseline, "pure_entry") + .blocks + .iter() + .any(|block| { matches!(block.terminator.kind, TerminatorKind::ForEach { .. }) }) + ); + for prepared in [false, true] { + let mut program = baseline.clone(); + if prepared { + prepare(&mut program, types); + assert!(!named(&program, "pure_entry").blocks.iter().any(|block| { + matches!(block.terminator.kind, TerminatorKind::ForEach { .. }) + })); + } + assert!( + program + .functions + .iter() + .all(|function| function.resource_lowering.is_none()) + ); + validate_call_ownership(&program, types).unwrap(); + let ordinary = validate_resource_ownership(&program, types).unwrap(); + assert!(ordinary.functions().is_empty()); + assert!(ordinary.entry_scope().is_none()); + for name in ["pure_entry", "ordinary_probe"] { + assert!( + validate_resource_ownership_for_entry( + &program, + types, + named(&program, name).id + ) + .is_err() + ); + } + } + } +} + +#[test] +fn ordinary_entry_checked_refinement_archive_retains_proofs_and_refuses_missing_authority() { + for release in [false, true] { + let checked = checked(RAW_REFINEMENTS, release); + let types = &checked.checked().interner; + let hir = hir::lower_checked_resource_program(&checked).unwrap(); + assert!(hir.resource_source.manifest().is_some()); + assert!(!hir.resource_manifest.kinds().is_empty()); + let baseline = crate::lower(&hir, types).unwrap(); + let predicates = baseline + .functions + .iter() + .filter(|function| { + function.identity.declaration.kind == hir::DeclarationKind::RefinementPredicate + }) + .collect::>(); + assert_eq!(predicates.len(), 2); + for predicate in predicates { + let witness = predicate + .resource_lowering + .as_ref() + .expect("checked refinement proof"); + assert_eq!(&witness.source, &hir.resource_source); + witness.current(predicate).unwrap(); + assert!(!has_execution_records(predicate, types)); + } + for prepared in [false, true] { + let mut program = baseline.clone(); + if prepared { + prepare(&mut program, types); + } + for name in ["pure_entry", "ordinary_probe"] { + if prepared { + assert_bridge(&program, types, name); + } else { + // The initial proof exists before executable For staging. + // Storage/ABI analysis follows certified preparation above. + assert_bridge_metadata(&program, types, name); + } + } + let selected = named(&program, "pure_entry").id; + named_mut(&mut program, "pure_entry").resource_lowering = None; + let errors = match validate_resource_ownership(&program, types) { + Err(errors) => errors, + Ok(_) => panic!("lost checked entry witness was accepted"), + }; + assert!( + errors + .iter() + .any(|error| error.message.contains("constructor witness")) + ); + assert!(validate_resource_ownership_for_entry(&program, types, selected).is_err()); + } + // Removing the checked archive cannot turn a real Resource manifest + // into the ordinary raw-HIR path before initial constructor capture. + let mut no_archive = hir.clone(); + no_archive.resource_source = hir::ResourceSourceArchive::empty(); + let errors = match crate::lower(&no_archive, types) { + Err(errors) => errors, + Ok(_) => panic!("raw HIR with a Resource manifest was accepted"), + }; + assert!( + errors + .iter() + .any(|error| error.message.contains("no original checked HIR archive")) + ); + } +} + +#[test] +fn ordinary_entry_raw_body_only_resource_descriptor_retains_mandatory_custody_capture() { + for release in [false, true] { + let checked = checked( + "namespace app\nfunction pure_entry() returns nothing:\n return nothing\n", + release, + ); + let types = &checked.checked().interner; + let hir = hir::lower_checked_resource_program(&checked).unwrap(); + let mut function = hir + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "pure_entry" + }) + .unwrap() + .clone(); + let descriptor = hir + .functions + .iter() + .find_map(|candidate| { + let mut found = None; + walk::hir_block(&candidate.body, &mut |value| { + if matches!(value.kind, hir::ExpressionKind::ResourceHookValue { .. }) { + found = Some(value.clone()); + } + }); + found + }) + .expect("genuine checked descriptor expression"); + assert!(!resource_type_pending(types, function.return_type)); + assert!( + function + .locals + .iter() + .all(|local| !resource_type_pending(types, local.ty)) + ); + function.body.statements.insert( + 0, + hir::Statement { + span: descriptor.span, + kind: hir::StatementKind::Expression(descriptor), + }, + ); + let no_archive = hir::ResourceSourceArchive::empty(); + let execution = ResourceExecutionClosure::default(); + assert!(runtime_custody_needed(&function, types, &execution)); + let capture = Capture::authenticated( + &function, + &hir.resource_manifest, + &no_archive, + types, + &execution, + ); + assert!( + capture.witness.is_some(), + "raw body-only custody must not be made ordinary" + ); + let mut raw = hir; + function.id = FunctionId::new(0); + raw.functions = vec![function]; + raw.resource_source = no_archive; + let errors = match crate::lower(&raw, types) { + Err(errors) => errors, + Ok(_) => panic!("raw body-only Resource descriptor was accepted"), + }; + assert!( + errors + .iter() + .any(|error| error.message.contains("no original checked HIR archive")) + ); + } +} + +#[test] +fn ordinary_entry_checked_scalar_refinement_lost_foreign_body_or_header_proofs_refuse() { + for release in [false, true] { + let checked = checked(RAW_REFINEMENTS, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + let selected = baseline + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "Positive" + && function.identity.declaration.kind + == hir::DeclarationKind::RefinementPredicate + }) + .expect("genuine generated scalar refinement predicate") + .id; + let foreign = lowered(&self::checked( + &RAW_REFINEMENTS.replace("value > 0", "value > 1"), + release, + )); + for mutation in 0..5 { + let mut changed = baseline.clone(); + let function = changed + .functions + .iter_mut() + .find(|function| function.id == selected) + .unwrap(); + match mutation { + 0 => function.resource_lowering = None, + 1 => { + function.resource_lowering = None; + function.identity.declaration.kind = hir::DeclarationKind::Function; + } + 2 => { + let value = function + .blocks + .iter_mut() + .find_map(|block| { + if let TerminatorKind::Return(Some(value)) = &mut block.terminator.kind + { + Some(value) + } else { + None + } + }) + .expect("actual refinement return operand"); + assert_eq!(value.ty, TypeInterner::BOOL); + value.kind = hir::ExpressionKind::Bool(false); + } + 3 => function.return_type = TypeInterner::NOTHING, + 4 => { + function.resource_lowering = foreign + .functions + .iter() + .find(|function| function.id == selected) + .unwrap() + .resource_lowering + .clone(); + } + _ => unreachable!(), + } + let errors = match validate_resource_ownership(&changed, types) { + Err(errors) => errors, + Ok(_) => panic!("refinement mutation {mutation} was accepted; release={release}"), + }; + if mutation < 2 { + assert!( + errors + .iter() + .any(|error| error.span == function_span(&baseline, selected) + && error.message.contains("constructor witness")) + ); + } + let before = changed.clone(); + crate::prepare_native_sequences(&mut changed, types); + assert!( + changed == before, + "invalid refinement proof was canonically resealed" + ); + } + } +} + +fn function_span(program: &Program, id: FunctionId) -> jett_common::Span { + program + .functions + .iter() + .find(|function| function.id == id) + .unwrap() + .span +} diff --git a/crates/jett_mir/src/resource_ownership/execution_closure.rs b/crates/jett_mir/src/resource_ownership/execution_closure.rs new file mode 100644 index 00000000..5a9a9866 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/execution_closure.rs @@ -0,0 +1,97 @@ +//! Derived only after the whole original checked HIR archive is authenticated. +//! A current body or signature alone can never add a function to this family. +use super::*; + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct ResourceExecutionClosure { + functions: BTreeSet, +} +impl ResourceExecutionClosure { + pub(super) fn from_archive( + archive: &hir::ResourceSourceArchive, + types: &TypeInterner, + ) -> Result { + let mut family = Self::from_original(archive.execution_functions(), types)?; + family.functions.retain(|index| { + !archive + .required_only_function_ids() + .iter() + .any(|function| function.index() == *index) + }); + Ok(family) + } + + pub(super) fn contains(&self, function: FunctionId) -> bool { + self.functions.contains(&function.index()) + } + pub(super) fn direct_call(&self, value: &Expression) -> bool { + matches!(&value.kind, hir::ExpressionKind::Call { function, .. } if self.contains(*function)) + } + pub(super) fn from_original( + functions: &[hir::Function], + types: &TypeInterner, + ) -> Result { + let mut family = Self::default(); + for (index, function) in functions.iter().enumerate() { + if function.id.index() as usize != index { + return Err( + "Resource original call closure has no exact dense function identity".into(), + ); + } + let mut needed = custody_type(types, function.return_type) + || function + .locals + .iter() + .any(|local| custody_type(types, local.ty)); + walk::hir_block(&function.body, &mut |value| { + needed |= custody_type(types, value.ty) + || matches!( + value.kind, + hir::ExpressionKind::ResourceHookValue { .. } + | hir::ExpressionKind::ResourceInvoke { .. } + ); + }); + if needed { + family.functions.insert(function.id.index()); + } + } + loop { + let mut predecessors = Vec::new(); + for function in functions { + if family.contains(function.id) { + continue; + } + let mut calls = false; + let mut malformed = false; + walk::hir_block(&function.body, &mut |value| { + if let hir::ExpressionKind::Call { + function: target, + ownership, + .. + } = &value.kind + && family.contains(*target) + { + calls = true; + malformed |= !matches!(ownership, hir::CallOwnership::Source(_)); + } + }); + if malformed { + return Err( + "Resource execution predecessor has no original Source call certificate" + .into(), + ); + } + if calls { + predecessors.push(function.id); + } + } + if predecessors.is_empty() { + break; + } + family + .functions + .extend(predecessors.into_iter().map(|function| function.index())); + } + Ok(family) + } +} diff --git a/crates/jett_mir/src/resource_ownership/execution_closure_tests.rs b/crates/jett_mir/src/resource_ownership/execution_closure_tests.rs new file mode 100644 index 00000000..671271aa --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/execution_closure_tests.rs @@ -0,0 +1,136 @@ +use super::tests::{checked, lowered}; +use super::*; +const SOURCE: &str = include_str!("fixtures/connected_entry.jett"); +fn named<'a>(program: &'a Program, name: &str) -> &'a Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap() +} +#[test] +fn resource_original_execution_closure_captures_resource_free_source_predecessors() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + let plan = validate_resource_ownership(&program, types).unwrap(); + let entry = named(&program, "main"); + assert_eq!(entry.return_type, TypeInterner::NOTHING); + assert!( + entry + .locals + .iter() + .all(|local| !custody_type(types, local.ty)) + ); + let entry_plan = plan + .function(entry.id) + .expect("exact original predecessor Scope"); + assert_eq!(entry_plan.root_scope().role(), ResourceFrameRole::Scope); + assert!(entry_plan.provisional_return().is_none()); + let operations = entry_plan + .operations() + .iter() + .filter_map(|operation| { + if let ResourceOperationRole::InvokeSourceFunction { + function, formals, .. + } = operation.role() + { + Some((*function, formals)) + } else { + None + } + }) + .collect::>(); + assert_eq!(operations.len(), 1); + assert_eq!(operations[0].0, named(&program, "lifecycle").id); + assert_eq!(operations[0].1.len(), 1); + assert_eq!(operations[0].1[0].parameter_type(), TypeInterner::NETWORK); + assert_eq!(operations[0].1[0].access(), ParamMode::View); + } +} +#[test] +fn resource_execution_closure_cannot_be_removed_or_recaptured_from_edited_public_graphs() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + let entry_id = named(&program, "main").id; + let mut changed = program.clone(); + changed.functions[entry_id.index() as usize].resource_lowering = None; + assert!( + validate_resource_ownership(&changed, types) + .unwrap_err() + .iter() + .any(|error| error + .message + .contains("initially authenticated Source constructor witness")) + ); + let mut hir = hir::lower_checked_resource_program(&checked).unwrap(); + let index = hir + .functions + .iter() + .position(|function| function.id == entry_id) + .unwrap(); + let original = hir.functions[index].body.clone(); + hir.functions[index].body.statements.clear(); + assert!( + lower(&hir, types) + .unwrap_err() + .iter() + .any(|error| error.message.contains("original checked HIR archive")) + ); + hir.functions[index].body = original; + let closure = authenticate_original(&hir, types).unwrap(); + assert!(closure.contains(entry_id)); + } +} +#[test] +fn resource_sum_shell_constructors_and_failure_extraction_have_exact_distinct_plan_roles() { + const ABSENT: &str = "namespace app\nfunction discard(value: optional[resource_probe.TestHandle]) returns nothing:\n return nothing\nfunction main(net: Network) returns nothing:\n use resource_probe\n discard(resource_probe.empty())\n resource_probe.TestHandle token = resource_probe.create(view net, 3) handle error:\n return nothing\n resource_probe.close(token)\n return nothing\n"; + for release in [false, true] { + let checked = checked(ABSENT, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + let plan = validate_resource_ownership(&program, types).unwrap(); + assert!( + plan.functions() + .iter() + .flat_map(|function| function.operations()) + .any(|operation| matches!( + operation.role(), + ResourceOperationRole::CreateAbsentSum { .. } + )) + ); + assert!(plan.functions().iter().flat_map(|function| function.operations()).any(|operation| matches!(operation.role(), ResourceOperationRole::TakeFailureCompanion { failure, target, .. } if *failure == TypeInterner::STRING && target.ty == *failure))); + for function in plan.functions() { + for operation in function.operations() { + if let ResourceOperationRole::CreateAbsentSum { destination } = operation.role() { + assert!(matches!( + function.owner_slots()[destination.index()].shape(), + ResourceShape::Optional { .. } + )); + } + } + } + } +} + +#[test] +fn resource_free_nested_call_to_custody_family_cannot_silently_use_the_ordinary_abi() { + const SOURCE: &str = "namespace app\nfunction label(view net: Network) returns int64:\n use resource_probe\n resource_probe.TestHandle token = resource_probe.create(view net, 1) handle error:\n return 0\n resource_probe.close(token)\n return 1\nfunction main(net: Network) returns nothing:\n string text = \"{label(view net)}\"\n return nothing\n"; + for release in [false, true] { + let checked = checked(SOURCE, release); + let program = lowered(&checked); + let errors = + validate_resource_ownership(&program, &checked.checked().interner).unwrap_err(); + assert!(errors.iter().any(|error| { + error.message.contains( + "nested Resource evaluation requires exact source-order canonical staging", + ) + })); + } +} diff --git a/crates/jett_mir/src/resource_ownership/execution_frames_tests.rs b/crates/jett_mir/src/resource_ownership/execution_frames_tests.rs new file mode 100644 index 00000000..943ebfe9 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/execution_frames_tests.rs @@ -0,0 +1,160 @@ +use super::tests::{checked, lowered}; +use super::*; + +const SOURCES: &[&str] = &[ + include_str!( + "../../../jett_driver/tests/native_conformance/resource/13_first_handle_failure.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/14_first_handle_success.jett" + ), +]; + +#[test] +fn staged_scope_sum_sites_and_extracts_use_exact_active_execution_frame() { + for release in [false, true] { + for &source in SOURCES { + let checked = checked(source, release); + let program = lowered(&checked); + let main = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .unwrap(); + let plan = validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let flow = plan.function(main.id).unwrap(); + let begin = flow + .operations() + .iter() + .find(|operation| { + matches!( + operation.role(), + ResourceOperationRole::BeginSourceFunction { .. } + ) + }) + .unwrap(); + let active = begin.frame(); + let scope = flow.root_scope().id(); + assert_ne!(active, scope); + assert_eq!( + flow.execution_frame(begin.site().block(), begin.site().position()), + Some(scope) + ); + let mut tags = 0; + let mut takes = 0; + for block in &main.blocks { + for (index, statement) in block.statements.iter().enumerate() { + if let StatementKind::SumTag { source, .. } = statement.kind { + let Some(slot) = flow.owner_slots().iter().find(|slot| + matches!(slot.storage(), ResourceSlotStorage::Local { header } if header.id == source)) else { continue }; + tags += 1; + assert_eq!(slot.frame(), scope); + assert_eq!( + flow.execution_frame(block.id, ResourcePosition::Statement(index)), + Some(active) + ); + let transfer = flow.operations().iter().find(|operation| + matches!(operation.role(), ResourceOperationRole::Transfer { destination, .. } if *destination == slot.id())).unwrap(); + assert_eq!(transfer.frame(), active); + } + if let StatementKind::SumTake { + source, success, .. + } = statement.kind + { + let Some(shell) = flow.owner_slots().iter().find(|slot| + matches!(slot.storage(), ResourceSlotStorage::Local { header } if header.id == source)) else { continue }; + takes += 1; + let operation = flow + .operations() + .iter() + .find(|operation| { + operation.site().block() == block.id + && operation.site().position() + == ResourcePosition::Statement(index) + && matches!( + operation.role(), + ResourceOperationRole::SumTake { .. } + | ResourceOperationRole::TakeFailureCompanion { .. } + ) + }) + .unwrap(); + assert_eq!(operation.frame(), active); + assert_eq!( + flow.execution_frame(block.id, ResourcePosition::Statement(index)), + Some(active) + ); + assert_eq!(shell.frame(), scope); + if success { + let ResourceOperationRole::SumTake { destination, .. } = + operation.role() + else { + panic!("success extraction") + }; + assert_eq!(flow.owner_slots()[destination.index()].frame(), scope); + } + } + } + if matches!(block.terminator.kind, TerminatorKind::Return(_)) { + assert_eq!( + flow.execution_frame(block.id, ResourcePosition::Terminator), + Some(scope) + ); + } + } + assert_eq!((tags, takes), (1, 2)); + assert_eq!( + flow.execution_frame(BlockId(u32::MAX), ResourcePosition::Terminator), + None + ); + assert_eq!( + flow.execution_frame(main.entry, ResourcePosition::Statement(usize::MAX)), + None + ); + } + } +} + +#[test] +fn staged_sum_frame_projection_cannot_authorize_copied_or_altered_current_graph() { + for release in [false, true] { + let checked = checked(SOURCES[0], release); + let original = lowered(&checked); + let main = original + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .unwrap(); + let (block, index) = main + .blocks + .iter() + .find_map(|block| { + block + .statements + .iter() + .enumerate() + .find_map(|(index, statement)| { + matches!(statement.kind, StatementKind::SumTag { .. }) + .then_some((block.id, index)) + }) + }) + .unwrap(); + for copied in [false, true] { + let mut changed = original.clone(); + let current = &mut changed.functions[main.id.index() as usize]; + if copied { + let statement = current.blocks[block.index() as usize].statements[index].clone(); + current.blocks[block.index() as usize] + .statements + .insert(index, statement); + } else { + let StatementKind::SumTag { source, .. } = + &mut current.blocks[block.index() as usize].statements[index].kind + else { + unreachable!() + }; + *source = current.params[0].local; + } + assert!(validate_resource_ownership(&changed, &checked.checked().interner).is_err()); + } + } +} diff --git a/crates/jett_mir/src/resource_ownership/fixtures/17_absent_aggregate_shapes.jett b/crates/jett_mir/src/resource_ownership/fixtures/17_absent_aggregate_shapes.jett new file mode 100644 index 00000000..3a1d96cf --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/fixtures/17_absent_aggregate_shapes.jett @@ -0,0 +1,78 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker diff --git a/crates/jett_mir/src/resource_ownership/fixtures/connected_entry.jett b/crates/jett_mir/src/resource_ownership/fixtures/connected_entry.jett new file mode 100644 index 00000000..ecccd22d --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/fixtures/connected_entry.jett @@ -0,0 +1,14 @@ +namespace app +function pass_owner(token: resource_probe.TestHandle) returns resource_probe.TestHandle: + return token +function lifecycle(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1) handle error: + return nothing + resource_probe.TestHandle moved = pass_owner(token) + result[int64, string] observed = resource_probe.borrow(view net, view moved) + resource_probe.close(moved) + return nothing +function main(net: Network) returns nothing: + lifecycle(view net) + return nothing diff --git a/crates/jett_mir/src/resource_ownership/flow.rs b/crates/jett_mir/src/resource_ownership/flow.rs new file mode 100644 index 00000000..f811b279 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/flow.rs @@ -0,0 +1,1499 @@ +//! Independent current-CFG occupancy proof. Abstract states are metadata, +//! never runtime tokens and never an ordinary linearity classifier. +use super::*; +mod normalized; +use hir::ExpressionKind as E; +use jett_typecheck::{ + CheckedCalleeAccess as Access, CheckedCallerEffect as Effect, CheckedCallerSyntax as Syntax, +}; +use normalized::{ActiveCall, PreparedCall}; +use std::collections::VecDeque; + +#[derive(Clone, Debug, PartialEq, Eq)] +struct Value { + shape: ResourceShape, + occupancy: ResourceOccupancy, + owner: Option, + loan: Option, +} +#[derive(Clone, Debug, PartialEq, Eq)] +struct State { + values: Vec>, + moved: Vec, + descriptors: Vec>, + named_callables: Vec>, + tags: BTreeMap, + guards: BTreeMap, + leases: BTreeSet, + aborted: bool, + calls: Vec, +} +type Key = (u32, u8, usize, usize); +fn key(site: ResourceSite, ordinal: usize) -> Key { + match site.position { + ResourcePosition::Statement(index) => (site.block.index(), 0, index, ordinal), + ResourcePosition::Terminator => (site.block.index(), 1, 0, ordinal), + } +} +struct Analysis<'p> { + program: &'p Program, + function: &'p Function, + types: &'p TypeInterner, + plan: ResourceFunctionPlan, + local_slots: Vec>, + expression_slots: BTreeMap<(Key, Option), ResourceOwnerSlotId>, + operation_frames: BTreeMap, + loan_sites: BTreeMap<(Key, usize, ResourceLoanSource), ResourceLoanId>, + site: ResourceSite, + ordinal: usize, + expression: Option, + emit: bool, + active_frame: ResourceFrameId, + return_frame: Option, + normalized_calls: BTreeMap, +} +impl<'p> Analysis<'p> { + fn record_execution_frame(&mut self) -> Result<(), String> { + if self.emit { + if self + .plan + .execution_frames + .iter() + .any(|(site, _)| *site == self.site) + { + return Err("Resource execution frame has duplicate current-site authority".into()); + } + self.plan + .execution_frames + .push((self.site, self.active_frame)); + } + Ok(()) + } + fn operation(&mut self, frame: ResourceFrameId, role: ResourceOperationRole) { + if self.emit { + let id = ResourceOperationId(self.plan.operations.len()); + self.plan.operations.push(ResourceOperation { + id, + frame, + site: self.site, + ordinal: self.ordinal, + role, + expression: self.expression, + named_indirect: None, + indirect_hook: None, + }); + } + } + fn frame(&mut self, ordinal: usize, role: ResourceFrameRole) -> ResourceFrameId { + let entry = key(self.site, ordinal); + if let Some(id) = self.operation_frames.get(&entry) { + return *id; + } + let id = ResourceFrameId(self.plan.frames.len()); + self.plan.frames.push(ResourceFrame { + id, + role, + site: self.site, + parent: Some(self.active_frame), + ordinal, + }); + self.operation_frames.insert(entry, id); + id + } + fn result_slot( + &mut self, + ordinal: usize, + shape: ResourceShape, + frame: ResourceFrameId, + ) -> ResourceOwnerSlotId { + let entry = (key(self.site, ordinal), None); + if let Some(id) = self.expression_slots.get(&entry) { + return *id; + } + let id = ResourceOwnerSlotId(self.plan.slots.len()); + self.plan.slots.push(ResourceOwnerSlot { + id, + frame, + shape, + storage: ResourceSlotStorage::Expression { + site: self.site, + ordinal, + }, + }); + self.expression_slots.insert(entry, id); + id + } + fn argument_slot( + &mut self, + ordinal: usize, + parameter: usize, + shape: ResourceShape, + frame: ResourceFrameId, + ) -> ResourceOwnerSlotId { + let entry = (key(self.site, ordinal), Some(parameter)); + if let Some(id) = self.expression_slots.get(&entry) { + return *id; + } + let id = ResourceOwnerSlotId(self.plan.slots.len()); + self.plan.slots.push(ResourceOwnerSlot { + id, + frame, + shape, + storage: ResourceSlotStorage::Argument { + site: self.site, + call_ordinal: ordinal, + parameter, + }, + }); + self.expression_slots.insert(entry, id); + id + } + fn loan( + &mut self, + ordinal: usize, + parameter: usize, + frame: ResourceFrameId, + source: ResourceLoanSource, + ) -> Result { + let entry = (key(self.site, ordinal), parameter, source); + if let Some(id) = self.loan_sites.get(&entry) { + return Ok(*id); + } + let id = ResourceLoanId(self.plan.loans.len()); + let shape = match source { + ResourceLoanSource::CarrierSumProjection { .. } => return Err( + "carrier sum adapters are issued only by the exact carrier projection constructor" + .into(), + ), + ResourceLoanSource::Owner(owner) => self.plan.slots[owner.index()].shape.clone(), + ResourceLoanSource::ProjectedSumPayload { parent, .. } => ResourceShape::Plain { + kind: self.plan.loans[parent.index()].shape.kind().clone(), + }, + ResourceLoanSource::IncomingViewFormal { .. } => { + unreachable!("incoming formal loans are initialized from their exact typed header") + } + }; + self.plan.loans.push(ResourceLoan { + id, + frame, + source, + shape, + parameter: (parameter != usize::MAX).then_some(parameter), + }); + self.loan_sites.insert(entry, id); + Ok(id) + } + fn borrow(&mut self, frame: ResourceFrameId, loan: ResourceLoanId) { + let role = if self.plan.loans[loan.index()].shape.conditional() { + ResourceOperationRole::BorrowSum { loan } + } else { + ResourceOperationRole::Borrow { loan } + }; + self.operation(frame, role); + } + fn end_borrow(&mut self, frame: ResourceFrameId, loan: ResourceLoanId) { + let role = if self.plan.loans[loan.index()].shape.conditional() { + ResourceOperationRole::EndSumBorrow { loan } + } else { + ResourceOperationRole::EndBorrow { loan } + }; + self.operation(frame, role); + } + fn clear(&self, state: &mut State, local: LocalId) { + state + .tags + .retain(|target, source| *target != local.index() && *source != local); + state.guards.remove(&local.index()); + state.descriptors[local.index() as usize] = None; + state.named_callables[local.index() as usize] = None; + } + fn descriptor_value(&self, state: &State, value: &Expression) -> Option { + let expected = self.plan.descriptor_value(value)?; + match &value.kind { + E::Local(local) => (state.descriptors[local.index() as usize].as_ref() + == Some(expected)) + .then(|| expected.clone()), + E::Clone(inner) => (self.descriptor_value(state, inner).as_ref() == Some(expected)) + .then(|| expected.clone()), + E::Call { .. } | E::ResourceHookValue { .. } => Some(expected.clone()), + _ => None, + } + } + fn named_binding(&self, state: &State, local: LocalId, value: &Expression) -> Option { + let witness = self.function.resource_lowering.as_ref()?; + witness + .named_callables + .iter() + .enumerate() + .find_map(|(index, binding)| { + if binding.local() != local + || !crate::breakpoint_regions::expressions_equal(binding.initializer(), value) + { + return None; + } + if let Some(parent) = binding.parent() { + let parent_local = witness.named_callables[parent].local(); + (state.named_callables[parent_local.index() as usize] == Some(parent)) + .then_some(index) + } else { + matches!(value.kind, E::FunctionRef(_)).then_some(index) + } + }) + } + fn named_indirect_target( + &self, + state: &State, + expression: &Expression, + ) -> Result, String> { + let E::IndirectCall { + callee, + ownership: hir::CallOwnership::Source(source), + .. + } = &expression.kind + else { + return Ok(None); + }; + let E::Local(local) = callee.kind else { + return Ok(None); + }; + let Some(index) = state.named_callables[local.index() as usize] else { + return Ok(None); + }; + let witness = self + .function + .resource_lowering + .as_ref() + .ok_or("Resource named call lost its constructor witness")?; + let binding = witness + .named_callables + .get(index) + .ok_or("Resource named call has no exact private producer")?; + let proof = binding.proof(); + let association = witness + .calls + .iter() + .find(|association| { + crate::breakpoint_regions::expressions_equal(&association.current, expression) + }) + .ok_or("Resource named call has no exact original Indirect occurrence")?; + let E::IndirectCall { + callee: original_callee, + .. + } = &association.original.kind + else { + return Err("Resource named call changed its original invocation kind".into()); + }; + if binding.local() != local + || !binding.original_callee_matches(original_callee) + || callee.ty != proof.signature_type() + || source.target + != (hir::CallTarget::Indirect { + signature_type: proof.signature_type(), + }) + || source.bridge != hir::CallBridge::Direct + || !source.generated_operands.is_empty() + { + return Err("Resource named call differs from its exact immutable callee or original Source signature".into()); + } + let target = self + .program + .functions + .get(proof.function().index() as usize) + .filter(|target| target.id == proof.function()) + .ok_or("Resource named call lost its selected declaration")?; + let selected = target + .resource_lowering + .as_ref() + .ok_or("Resource named call has no selected original Source body")?; + if selected.source != witness.source || target.identity != *proof.identity() { + return Err("Resource named call selected a foreign archive or declaration".into()); + } + Ok(Some(proof.clone())) + } + fn read( + &self, + state: &mut State, + local: LocalId, + taking: bool, + ) -> Result, String> { + let index = local.index() as usize; + let metadata = self + .function + .local(local) + .ok_or("Resource read has no exact dense local header")?; + if shape(&self.program.resource_manifest, self.types, metadata.ty)?.is_none() { + return Ok(None); + } + let value = state.values[index] + .as_ref() + .ok_or("Resource owner is uninitialized, moved or retired at this current CFG site")? + .clone(); + if taking { + if value.loan.is_some() { + return Err( + "Resource borrowed formal or alias cannot become an owned value".into(), + ); + } + let owner = value.owner; + if state.leases.iter().any(|loan| { + Some(self.plan.loans[loan.0].source) == owner.map(ResourceLoanSource::Owner) + }) { + return Err("Resource owner cannot move, close or replace while its exact lease remains live".into()); + } + state.values[index] = None; + state.moved[index] = true; + self.clear(state, local); + } + Ok(Some(value)) + } + fn store( + &mut self, + state: &mut State, + local: LocalId, + value: Option, + replace: bool, + self_rebind: bool, + ) -> Result<(), String> { + let index = local.index() as usize; + let metadata = self + .function + .local(local) + .ok_or("Resource destination has no exact local header")?; + let expected = shape(&self.program.resource_manifest, self.types, metadata.ty)?; + let Some(expected) = expected else { + if value.is_some() { + return Err("Resource custody cannot be stored in an ordinary destination".into()); + } + self.clear(state, local); + return Ok(()); + }; + let mut value = + value.ok_or("Resource destination has no checked owning or resident-loan producer")?; + if value.shape != expected { + return Err("Resource destination changes its nominal kind or active shape".into()); + } + if self.function.is_view_local(local) { + if replace { + return Err( + "Resource view alias replacement requires its separate lease transport".into(), + ); + } + if !value.shape.conditional() && value.occupancy != ResourceOccupancy::Occupied { + return Err("pending ResourceOwnershipPlan: conditional sum borrowing needs exact occupied-arm lease transport".into()); + } + if value.loan.is_none() { + let source = value + .owner + .ok_or("Resource alias has no exact owned backing")?; + let loan = self.loan( + self.ordinal, + usize::MAX, + ResourceFrameId(0), + ResourceLoanSource::Owner(source), + )?; + state.leases.insert(loan); + self.borrow(ResourceFrameId(0), loan); + value.loan = Some(loan); + } + state.values[index] = Some(value); + state.moved[index] = false; + self.clear(state, local); + return Ok(()); + } + if value.loan.is_some() { + return Err("Resource owning storage cannot adopt a borrowed carrier".into()); + } + let destination = self.local_slots[index] + .ok_or("Resource owning destination lacks its dedicated slot")?; + if replace && !metadata.mutable { + return Err("Resource assignment requires its exact mutable owning header".into()); + } + if let Some(old) = state.values[index].as_ref() { + if !replace || !metadata.mutable { + return Err( + "Resource live destination requires its exact mutable replacement constructor" + .into(), + ); + } + if state.leases.iter().any(|loan| { + self.plan.loans[loan.0].source == ResourceLoanSource::Owner(destination) + }) { + return Err("Resource replacement has a live old-holder lease".into()); + } + self.operation( + ResourceFrameId(0), + ResourceOperationRole::Replace { + destination, + replacement: value + .owner + .ok_or("Resource replacement lacks its evaluated RHS owner slot")?, + old: old.occupancy, + }, + ); + self.operation( + ResourceFrameId(0), + ResourceOperationRole::Drop { + source: destination, + occupancy: old.occupancy, + }, + ); + } + if let Some(source) = value.owner { + if source != destination { + self.operation( + self.active_frame, + ResourceOperationRole::Transfer { + source, + destination, + }, + ); + } else if replace { + if !self_rebind + || !matches!(value.shape, ResourceShape::Plain { .. }) + || value.occupancy != ResourceOccupancy::Occupied + || state.values[index].is_some() + { + return Err("pending ResourceOwnershipPlan: self-rebind needs the exact occupied plain Local reseat".into()); + } + self.operation( + ResourceFrameId(0), + ResourceOperationRole::SelfRebind { slot: destination }, + ); + } + } + value.owner = Some(destination); + state.values[index] = Some(value); + state.moved[index] = false; + self.clear(state, local); + Ok(()) + } + fn finish(&mut self, state: &mut State, frame: ResourceFrameId, outcome: ResourceCompletion) { + // Incoming formal records denote resident caller leases, not child-owned tokens. + let loans = state.leases.iter().copied().collect::>(); + for loan in loans.into_iter().rev() { + self.end_borrow(frame, loan); + state.leases.remove(&loan); + } + for value in state.values.iter_mut().rev() { + if let Some(value) = value.take() { + if value.loan.is_none() + && let Some(source) = value.owner + { + self.operation( + frame, + ResourceOperationRole::Drop { + source, + occupancy: value.occupancy, + }, + ); + } + } + } + self.operation(frame, ResourceOperationRole::Complete { outcome }); + } + fn lexical_exit(&mut self, state: &mut State) -> Result<(), String> { + let Some(exit) = self + .function + .resource_lexical_exit(self.site.block, self.site.position)? + else { + return Err("Resource lexical retirement has no exact constructor exit".into()); + }; + let witness = self + .function + .resource_lowering + .as_ref() + .ok_or("Resource lexical retirement has no Source witness")?; + let mut clear_locals = Vec::new(); + let mut loans = BTreeSet::new(); + for original in exit.declarations() { + let row = witness + .borrowed_sums + .iter() + .find(|row| row.original_local() == original) + .ok_or("Resource lexical retirement lost an exact original alias")?; + for local in row.retirement_locals() { + if !clear_locals.contains(&local) { + clear_locals.push(local); + } + if let Some(value) = &state.values[local.index() as usize] + && let Some(loan) = value.loan + && state.leases.contains(&loan) + { + // A forwarded incoming/outer lease is never adopted by this declaration. + let same_site = |site: Key, current: ResourceSite| { + let expected = key(current, 0); + (site.0, site.1, site.2) == (expected.0, expected.1, expected.2) + }; + let created_here = self.loan_sites.iter().any(|((site, _, _), id)| { + *id == loan + && (same_site(*site, row.initialize_site()) + || same_site(*site, row.success_site())) + }); + if created_here { + loans.insert(loan); + } + } + } + } + let start = self.plan.operations.len(); + for loan in loans.into_iter().rev() { + if state.leases.iter().any(|child| matches!(self.plan.loans[child.index()].source, ResourceLoanSource::ProjectedSumPayload { parent, .. } if parent == loan)) { + return Err("Resource lexical parent retirement has a live child outside its exact exit".into()); + } + self.end_borrow(self.active_frame, loan); + state.leases.remove(&loan); + } + for local in &clear_locals { + state.values[local.index() as usize] = None; + state.moved[local.index() as usize] = false; + self.clear(state, *local); + } + if self.emit { + self.plan.lexical_exits.push(ResourceLexicalExitPlan::new( + self.site, + exit.kind(), + (start..self.plan.operations.len()) + .map(ResourceOperationId) + .collect(), + clear_locals, + )); + } + Ok(()) + } + fn expression( + &mut self, + state: &mut State, + expression: &Expression, + taking: bool, + ) -> Result, String> { + let previous = self + .expression + .replace(std::ptr::from_ref(expression).addr()); + let result = self.expression_inner(state, expression, taking); + self.expression = previous; + result + } + fn expression_inner( + &mut self, + state: &mut State, + expression: &Expression, + taking: bool, + ) -> Result, String> { + if state.aborted { + return Ok(None); + } + self.ordinal += 1; + let ordinal = self.ordinal; + let expected = shape(&self.program.resource_manifest, self.types, expression.ty)?; + match &expression.kind { + E::Local(local) => { + if self.plan.descriptor_value(expression).is_some() && self.descriptor_value(state, expression).is_none() { + return Err("Resource descriptor has no live exact producer at this current CFG read".into()); + } + self.read(state, *local, taking) + } + E::Clone(inner) if self.plan.descriptor_value(expression).is_some() => { + if self.descriptor_value(state, expression).is_none() { return Err("Resource descriptor alias lost its current exact producer".into()); } + self.expression(state, inner, false) + } + E::View(inner) => { + if taking && expected.is_some() { return Err("Resource View cannot be adopted as an owned result".into()); } + self.expression(state, inner, false) + } + E::ResourceHookValue { hook } => { + if !self.program.resource_manifest.contains_hook(hook) || expression.ty != hook.function_type() { return Err("Resource descriptor differs from its exact original manifest signature".into()); } + self.operation(ResourceFrameId(0), ResourceOperationRole::Descriptor { hook: hook.clone() }); + Ok(None) + } + E::ResourceInvoke { hook, args, evaluation_order, ownership } => self.invocation(state, expression, Some(hook.clone()), None, args, evaluation_order, ownership, ordinal, None), + E::Call { function, args, evaluation_order, ownership } => self.invocation(state, expression, None, Some(*function), args, evaluation_order, ownership, ordinal, None), + E::IndirectCall { callee, args, evaluation_order, ownership } => { + let hook = self.descriptor_value(state, callee); + if expected.is_some() || args.iter().any(|arg| resource_type_pending(self.types, arg.ty)) { + if let Some(hook) = hook { + if callee.ty != hook.function_type() { return Err("Resource indirect descriptor has a different exact signature".into()); } + self.invocation(state, expression, Some(hook), None, args, evaluation_order, ownership, ordinal, None) + } else if let Some(proof) = self.named_indirect_target(state, expression)? { + self.invocation(state, expression, None, Some(proof.function()), args, evaluation_order, ownership, ordinal, Some(proof)) + } else { + Err("pending ResourceOwnershipPlan: indirect custody call lacks an exact descriptor producer".into()) + } + } else { + for &index in evaluation_order { self.expression(state, &args[index], true)?; } + if !state.aborted { self.expression(state, callee, false)?; } + Ok(None) + } + } + E::OptionalNone if expected.is_some() => { + let shape = expected.unwrap(); + if !matches!(shape, ResourceShape::Optional { .. }) { return Err("Resource absence is not its exact Optional shape".into()); } + let destination = self.result_slot(ordinal, shape.clone(), self.active_frame); + self.operation(self.active_frame, ResourceOperationRole::CreateAbsentSum { destination }); + Ok(Some(Value { shape, occupancy: ResourceOccupancy::Empty, owner: Some(destination), loan: None })) + }, + E::OptionalSome(inner) | E::ResultOk(inner) if expected.is_some() => { + let payload = self.expression(state, inner, true)?; + if state.aborted { return Ok(None); } + let mut value = payload.ok_or("Resource sum payload has no exact owning producer")?; + if value.loan.is_some() || value.shape.kind() != expected.as_ref().unwrap().kind() { return Err("Resource sum payload is borrowed or from another nominal kind".into()); } + let shape = expected.unwrap(); + let destination = self.result_slot(ordinal, shape.clone(), self.active_frame); + let source = value.owner.ok_or("Resource sum adoption has no sole owning payload")?; + self.operation(self.active_frame, ResourceOperationRole::SumAdopt { source, destination }); + value.shape = shape; + value.owner = Some(destination); + Ok(Some(value)) + } + E::ResultFail(inner) if expected.is_some() => { + if self.expression(state, inner, true)?.is_some() { return Err("Resource result failure companion cannot carry custody".into()); } + if state.aborted { return Ok(None); } + let shape = expected.unwrap(); + let ResourceShape::Result { failure, .. } = &shape else { return Err("Resource failure is not its exact Result shape".into()); }; + if inner.ty != *failure { return Err("Resource failure companion changes its declared exact type".into()); } + let destination = self.result_slot(ordinal, shape.clone(), self.active_frame); + self.operation(self.active_frame, ResourceOperationRole::CreateFailureSum { destination, failure: *failure }); + Ok(Some(Value { shape, occupancy: ResourceOccupancy::Empty, owner: Some(destination), loan: None })) + } + E::RuntimeFailure(_) | E::RuntimeFailureMessage(_) => { + if let E::RuntimeFailureMessage(message) = &expression.kind { self.expression(state, message, false)?; } + state.aborted = true; + Ok(None) + } + _ if expected.is_some() => Err("pending ResourceOwnershipPlan: Resource expression needs its dedicated canonical producer or conversion transport".into()), + _ => { + let mut has_resource = false; + walk::expression(expression, &mut |value| { + if !std::ptr::eq(value, expression) { + has_resource |= resource_type_pending(self.types, value.ty) && !matches!(self.types.resolve(value.ty), Type::Function { .. }); + if let E::Call { function, .. } = value.kind { + has_resource |= self.program.functions.get(function.index() as usize).is_some_and(|callee| callee.id == function && has_execution_records(callee, self.types)); + } + } + }); + if has_resource { return Err("pending ResourceOwnershipPlan: nested Resource evaluation requires exact source-order canonical staging".into()); } + Ok(None) + } + } + } + fn invocation( + &mut self, + state: &mut State, + expression: &Expression, + hook: Option, + function: Option, + args: &[Expression], + order: &[usize], + ownership: &hir::CallOwnership, + ordinal: usize, + named_indirect: Option, + ) -> Result, String> { + let resource = hook.is_some() + || function.is_some_and(|id| { + self.program + .functions + .get(id.index() as usize) + .is_some_and(|callee| { + callee.id == id && has_execution_records(callee, self.types) + }) + }) + || shape(&self.program.resource_manifest, self.types, expression.ty)?.is_some() + || args.iter().any(|arg| { + shape(&self.program.resource_manifest, self.types, arg.ty) + .ok() + .flatten() + .is_some() + }); + if !resource { + for &index in order { + self.expression(state, &args[index], true)?; + } + return Ok(None); + } + let hir::CallOwnership::Source(source) = ownership else { + return Err("Resource invocation cannot use Generated authority".into()); + }; + if source.arguments.len() != args.len() || order.len() != args.len() { + return Err("Resource invocation changed its original source tuple count".into()); + } + if let Some(hook) = &hook { + if !self.program.resource_manifest.contains_hook(hook) { + return Err("Resource invocation hook belongs to another checked program".into()); + } + } + if let Some(function) = function { + let callee = self + .program + .functions + .get(function.index() as usize) + .filter(|callee| callee.id == function) + .ok_or("Resource Source callee has no exact function")?; + if !has_execution_records(callee, self.types) { + return Err("Resource Source callee lacks its original custody witness".into()); + } + } + let frame = self.frame(ordinal, ResourceFrameRole::Operation); + let parent = self.active_frame; + self.active_frame = frame; + let mut operands = Vec::new(); + let mut loans = Vec::new(); + let mut temporary_owners = Vec::new(); + for ¶meter in order { + let arg = &args[parameter]; + let fact = source + .arguments + .iter() + .find(|fact| fact.parameter_index == parameter) + .ok_or("Resource actual has no exact original formal tuple")?; + let Some(_) = shape(&self.program.resource_manifest, self.types, arg.ty)? else { + self.expression(state, arg, true)?; + operands.push(ResourceCallOperand::Ordinary { + parameter, + ty: arg.ty, + }); + if state.aborted { + break; + } + continue; + }; + if fact.staging != hir::ArgumentStaging::Original { + return Err("pending ResourceOwnershipPlan: resource actual uses an ordinary staging record".into()); + } + let retaining = fact.effect == Effect::RetainBorrow; + if !retaining && !matches!(fact.effect, Effect::RelinquishOwned | Effect::TransferOwned) + { + return Err("Resource custody cannot use Copy or ObserveData".into()); + } + let input = if !retaining + && fact.syntax == Syntax::Bare + && fact.physical_access == Access::View + { + if let E::View(inner) = &arg.kind { + inner.as_ref() + } else { + arg + } + } else { + arg + }; + let evaluated = self.expression(state, input, !retaining)?; + if state.aborted { + break; + } + let mut value = + evaluated.ok_or("Resource actual has no exact live owner or resident lease")?; + if retaining { + if fact.syntax != Syntax::WrittenView || fact.callee_access != Access::View { + return Err( + "Resource retained loan differs from written View/formal access".into(), + ); + } + let loan = if let Some(loan) = value.loan { + self.operation( + frame, + ResourceOperationRole::BoundedBorrowUse { loan, parameter }, + ); + loan + } else { + if !value.shape.conditional() && value.occupancy != ResourceOccupancy::Occupied + { + return Err("pending ResourceOwnershipPlan: sum View actual needs its occupied-arm loan transport".into()); + } + let owner = value + .owner + .ok_or("Resource retained actual has no exact holder")?; + let loan = + self.loan(ordinal, parameter, frame, ResourceLoanSource::Owner(owner))?; + state.leases.insert(loan); + loans.push(loan); + self.borrow(frame, loan); + loan + }; + operands.push(ResourceCallOperand::Borrowed { parameter, loan }); + } else { + if value.loan.is_some() { + return Err("Resource owned actual cannot consume a resident loan".into()); + } + let temporary = self.argument_slot(ordinal, parameter, value.shape.clone(), frame); + if let Some(owner) = value.owner { + self.operation( + frame, + ResourceOperationRole::Transfer { + source: owner, + destination: temporary, + }, + ); + } + value.owner = Some(temporary); + if fact.callee_access == Access::View { + if !value.shape.conditional() && value.occupancy != ResourceOccupancy::Occupied + { + return Err("pending ResourceOwnershipPlan: bare sum to View needs its dedicated occupied transport".into()); + } + let loan = self.loan( + ordinal, + parameter, + frame, + ResourceLoanSource::Owner(temporary), + )?; + loans.push(loan); + self.borrow(frame, loan); + operands.push(ResourceCallOperand::Borrowed { parameter, loan }); + temporary_owners.push((temporary, value.occupancy)); + } else { + operands.push(ResourceCallOperand::Owned { + parameter, + slot: temporary, + }); + } + } + } + if !state.aborted + && let Some(proof) = &named_indirect + { + let E::IndirectCall { callee, .. } = &expression.kind else { + return Err( + "Resource named body proof is not attached to an original Indirect call".into(), + ); + }; + self.expression(state, callee, false)?; + if self.named_indirect_target(state, expression)?.as_ref() != Some(proof) { + return Err( + "Resource named callee lost its exact provenance after actual evaluation" + .into(), + ); + } + } + if !state.aborted + && hook.is_some() + && let E::IndirectCall { callee, .. } = &expression.kind + { + self.expression(state, callee, false)?; + if !state.aborted && self.descriptor_value(state, callee).as_ref() != hook.as_ref() { + return Err("Resource indirect hook lost its exact descriptor provenance after actual evaluation".into()); + } + if source.target + != (hir::CallTarget::Indirect { + signature_type: callee.ty, + }) + || source.bridge != hir::CallBridge::Direct + || !source.generated_operands.is_empty() + { + return Err( + "Resource indirect hook changed its original Indirect Source authority".into(), + ); + } + } + let outcome_shape = shape(&self.program.resource_manifest, self.types, expression.ty)?; + let output = outcome_shape + .as_ref() + .map(|shape| self.result_slot(ordinal, shape.clone(), parent)); + if !state.aborted { + let result = match output { + Some(slot) => ResourceCallResult::Owned { slot }, + None => ResourceCallResult::Ordinary { ty: expression.ty }, + }; + if let Some(hook) = hook { + self.operation( + frame, + ResourceOperationRole::InvokeHook { + hook: hook.clone(), + source: source.clone(), + evaluation_order: order.to_vec(), + operands: operands.clone(), + result, + }, + ); + if self.emit && matches!(expression.kind, E::IndirectCall { .. }) { + self.plan + .operations + .last_mut() + .ok_or("Resource indirect hook lost its Invoke operation")? + .indirect_hook = Some(hook.clone()); + } + match hook.recipe() { + jett_types::ResourceKernelRecipe::NetworkFactory => { + let destination = output + .ok_or("Resource factory has no exact success-bearing destination")?; + self.operation(frame, ResourceOperationRole::Acquire { hook, destination }); + } + jett_types::ResourceKernelRecipe::Finalize => { + let owner = operands + .iter() + .find_map(|operand| { + if let ResourceCallOperand::Owned { slot, .. } = operand { + Some(*slot) + } else { + None + } + }) + .ok_or("Resource Close has no consumed owning actual")?; + self.operation( + frame, + ResourceOperationRole::Close { + hook, + source: owner, + }, + ); + } + jett_types::ResourceKernelRecipe::NetworkBorrow => {} + } + } else { + self.operation( + frame, + ResourceOperationRole::InvokeSourceFunction { + function: function + .ok_or("Resource call has neither an exact hook nor a Source callee")?, + formals: source + .arguments + .iter() + .map(ResourceCallFormal::original) + .collect(), + source: source.clone(), + evaluation_order: order.to_vec(), + operands, + result, + }, + ); + if self.emit + && let Some(proof) = named_indirect + { + let invocation = self + .plan + .operations + .last_mut() + .ok_or("Resource named call has no invocation operation")?; + invocation.named_indirect = Some(proof); + } + } + } else { + // Every earlier owned actual must retain a cleanup destination even if the callee never runs. + for operand in &operands { + if let ResourceCallOperand::Owned { slot, .. } = operand { + temporary_owners.push((*slot, ResourceOccupancy::Occupied)); + } + } + } + for loan in loans.into_iter().rev() { + state.leases.remove(&loan); + self.end_borrow(frame, loan); + } + for (source, occupancy) in temporary_owners.into_iter().rev() { + self.operation(frame, ResourceOperationRole::Drop { source, occupancy }); + } + self.operation( + frame, + ResourceOperationRole::Complete { + outcome: if state.aborted { + ResourceCompletion::Abort + } else { + ResourceCompletion::Normal + }, + }, + ); + self.active_frame = parent; + Ok(if state.aborted { + None + } else { + outcome_shape.map(|shape| Value { + occupancy: if shape.conditional() { + ResourceOccupancy::Conditional + } else { + ResourceOccupancy::Occupied + }, + shape, + owner: output, + loan: None, + }) + }) + } + fn block( + &mut self, + block: &BasicBlock, + mut state: State, + ) -> Result, String> { + self.active_frame = state + .calls + .last() + .map_or(ResourceFrameId(0), |active| active.frame); + for (index, statement) in block.statements.iter().enumerate() { + self.site = ResourceSite { + function: self.function.id, + block: block.id, + position: ResourcePosition::Statement(index), + }; + self.ordinal = 0; + if state.aborted { + break; + } + self.record_execution_frame()?; + match &statement.kind { + StatementKind::ResourceLexicalExit(_) => self.lexical_exit(&mut state)?, + StatementKind::ResourceConstructor(_) => return Err("occupied constructor requires its authenticated carrier flow".into()), + StatementKind::ResourceCall(node) => self.normalized_node(&mut state, node)?, + StatementKind::Let { local, value } => { + let named = self.named_binding(&state, *local, value); + let descriptor = self.descriptor_value(&state, value); + let taking = !self.function.is_view_local(*local); + let result = self.expression(&mut state, value, taking)?; + if !state.aborted { + self.store(&mut state, *local, result, false, false)?; + let expected_descriptor = self.function.resource_lowering.as_ref().and_then(|witness| witness.descriptors.binding(*local, value)); + if descriptor.as_ref() != expected_descriptor { return Err("Resource descriptor binding lost its exact original immutable initializer".into()); } + state.descriptors[local.index() as usize] = descriptor; + state.named_callables[local.index() as usize] = named; + if self.emit && let Some(index) = named + && matches!(value.kind, E::FunctionRef(_)) + { + let proof = self.function.resource_lowering.as_ref() + .and_then(|witness| witness.named_callables.get(index)) + .ok_or("Resource named producer lost its constructor record")?.proof().clone(); + self.plan.named_callable_producers.push((std::ptr::from_ref(value).addr(), proof)); + } + } + } + StatementKind::Assign { target, value } => { + let E::Local(local) = target.kind else { return Err("pending ResourceOwnershipPlan: projected replacement needs its aggregate transport".into()); }; + let result = self.expression(&mut state, value, true)?; + if !state.aborted { + let self_rebind = matches!(value.kind, E::Local(source) if source == local) + && target.ty == value.ty + && self.function.local(local).map(|header| header.ty) == Some(value.ty); + self.store(&mut state, local, result, true, self_rebind)?; + } + } + StatementKind::Evaluate(value) | StatementKind::HandleDefault(value) => { + if let Some(value) = self.expression(&mut state, value, true)? { + if value.loan.is_some() { return Err("Resource borrowed result cannot be discarded as owned".into()); } + if let Some(source) = value.owner { self.operation(ResourceFrameId(0), ResourceOperationRole::Drop { source, occupancy: value.occupancy }); } + } + } + StatementKind::SumTag { source, target } => { + if let Some(value) = self.read(&mut state, *source, false)? { + if !value.shape.conditional() { return Err("Resource tag needs an exact supported sum".into()); } + if self.function.local(*target).map(|local| local.ty) != Some(TypeInterner::BOOL) { return Err("Resource sum tag is not its exact Bool destination".into()); } + if let Some(loan) = value.loan { + let row = self.function.resource_borrowed_sum_projection(self.site.block, self.site.position)?.ok_or("borrowed Resource sum tag lacks its original Handle constructor relation")?; + if row.source() != *source || row.tag() != *target { return Err("borrowed Resource sum tag changed its exact source/output headers".into()); } + self.operation(self.active_frame, ResourceOperationRole::ObserveSumView { source: loan, target: *target }); + } + state.tags.insert(target.index(), *source); + } + } + StatementKind::SumTake { source, target, success } => { + if let Some(value) = self.read(&mut state, *source, false)? { + let (tag, selected) = state.guards.get(&source.index()).copied().ok_or("Resource SumTake has no exact selecting typed tag edge")?; + if selected != *success || (selected && value.occupancy != ResourceOccupancy::Occupied) || (!selected && value.occupancy != ResourceOccupancy::Empty) { return Err("Resource SumTake differs from its selected occupied arm".into()); } + if let Some(parent) = value.loan { + let row = self.function.resource_borrowed_sum_projection(self.site.block, self.site.position)?.ok_or("borrowed Resource sum extraction lacks its exact constructor relation")?; + if row.source() != *source || row.tag() != tag { return Err("borrowed Resource sum extraction changed its exact tag/header relation".into()); } + if selected { + if row.output() != *target || row.success_site() != self.site || !self.function.is_view_local(*target) || self.local_slots[target.index() as usize].is_some() { return Err("borrowed Resource payload gained an owning or foreign output".into()); } + let path = row.path(); + let loan = self.loan(self.ordinal, usize::MAX, ResourceFrameId(0), ResourceLoanSource::ProjectedSumPayload { parent, tag, path })?; + state.leases.insert(loan); + self.store(&mut state, *target, Some(Value { shape: ResourceShape::Plain { kind: value.shape.kind().clone() }, occupancy: ResourceOccupancy::Occupied, owner: None, loan: Some(loan) }), false, false)?; + self.operation(self.active_frame, ResourceOperationRole::ProjectSumView { source: parent, destination: loan, tag, path }); + } else { + let ResourceShape::Result { failure, .. } = value.shape else { return Err("borrowed Optional has no failure companion".into()); }; + let header = self.function.local(*target).ok_or("borrowed Resource failure read has no header")?.clone(); + if failure != TypeInterner::STRING || header.ty != failure || header.view_source.is_some() || row.failure_site() != Some(self.site) { return Err("borrowed Resource failure read lacks its exact String companion".into()); } + self.operation(self.active_frame, ResourceOperationRole::ReadFailureCompanion { source: parent, target: header, failure, tag }); + } + continue; + } + if selected { + let owner = value.owner.ok_or("Resource sum payload lacks its holder")?; + let mut payload = self.read(&mut state, *source, true)?.ok_or("Resource sum source was consumed before its exact take")?; + payload.shape = ResourceShape::Plain { kind: payload.shape.kind().clone() }; + payload.owner = Some(self.local_slots[target.index() as usize].ok_or("Resource sum success has no exact output slot")?); + self.store(&mut state, *target, Some(payload), false, false)?; + self.operation(self.active_frame, ResourceOperationRole::SumTake { source: owner, destination: self.local_slots[target.index() as usize].ok_or("Resource sum success has no dedicated output slot")?, tag, success: true }); + } else { + let ResourceShape::Result { failure, .. } = &value.shape else { return Err("pending ResourceOwnershipPlan: Optional empty-arm extraction has no ordinary companion".into()); }; + let header = self.function.local(*target).ok_or("Resource failure take has no exact destination")?.clone(); + if header.ty != *failure { return Err("Resource failure take changes its exact companion type".into()); } + let owner = value.owner.ok_or("Resource failure companion has no exact sum shell holder")?; + self.operation(self.active_frame, ResourceOperationRole::TakeFailureCompanion { source: owner, target: header, failure: *failure, tag }); + self.read(&mut state, *source, true)?; + if shape(&self.program.resource_manifest, self.types, self.function.local(*target).ok_or("Resource failure take has no destination")?.ty)?.is_some() { return Err("Resource failure companion cannot become an occupied owner".into()); } + } + } + } + StatementKind::Assert { condition, message } => { + self.expression(&mut state, condition, false)?; + if let Some(message) = message { self.expression(&mut state, message, false)?; } + } + StatementKind::Breakpoint { condition, bindings } => { + if bindings.iter().any(|local| state.values[local.index() as usize].is_some()) { return Err("pending ResourceOwnershipPlan: Resource debugger observation transport is unproved".into()); } + if let Some(condition) = condition { self.expression(&mut state, condition, false)?; } + } + StatementKind::Trace(local) => { + if state.values[local.index() as usize].is_some() { return Err("Resource trace cannot manufacture observation or copied custody".into()); } + } + StatementKind::BeginCallView { local, value } => { + if shape(&self.program.resource_manifest, self.types, self.function.local(*local).ok_or("loan local missing")?.ty)?.is_some() || resource_type_pending(self.types, value.ty) { return Err("pending ResourceOwnershipPlan: ordinary BeginCallView is not a Resource lease constructor".into()); } + } + StatementKind::EndCallView { .. } | StatementKind::OpenCallOwnerGeneration { .. } | StatementKind::ReplaceCallOwnerGeneration { .. } | StatementKind::CloseCallOwnerGeneration { .. } => {} + StatementKind::CheckRefinement { call, .. } => { self.expression(&mut state, call, false)?; } + StatementKind::SequenceLength { source, target } => { + control_flow::sequence_length(self.function, self.types, source, *target)?; + self.clear(&mut state, *target); + } + StatementKind::SequenceGet { consume, source, index, target, part } => { + control_flow::sequence_get(self.function, self.types, *consume, source, *index, *target, *part)?; + self.clear(&mut state, *target); + } + StatementKind::ReflectedContainerReady { .. } | StatementKind::ReflectedValueReady { .. } | StatementKind::IterationBorrow { .. } => return Err("pending ResourceOwnershipPlan: sequence or aggregate custody needs its dedicated normalization".into()), + } + } + self.site = ResourceSite { + function: self.function.id, + block: block.id, + position: ResourcePosition::Terminator, + }; + self.ordinal = 0; + if state.aborted { + if !state.calls.is_empty() { + return Err("pending Resource call region: infrastructure abort needs its exact acquired-prefix retirement".into()); + } + self.finish(&mut state, ResourceFrameId(0), ResourceCompletion::Abort); + return Ok(Vec::new()); + } + self.record_execution_frame()?; + match &block.terminator.kind { + TerminatorKind::Return(value) => { + if !state.calls.is_empty() { return Err("Resource Source Return reaches an unretired normalized operation suffix".into()); } + let mut outgoing = None; + if let Some(value) = value { + let descriptor = self.descriptor_value(&state, value); + let returned = self.expression(&mut state, value, true)?; + if !state.aborted && let Some(expected) = self.plan.descriptor_return() { + let witnessed = self.function.resource_lowering.as_ref().is_some_and(|witness| witness.descriptors.return_matches(value)); + if descriptor.as_ref() != Some(expected) || !witnessed { return Err("Resource descriptor Return differs from its exact current producer and archived return summary".into()); } + } + if let Some(returned) = returned { + if returned.loan.is_some() { return Err("Resource resident lease cannot escape as an owning return".into()); } + let expected = shape(&self.program.resource_manifest, self.types, self.function.return_type)?.ok_or("Resource return has an ordinary declared result")?; + if returned.shape != expected { return Err("Resource return changes its exact shape or kind".into()); } + if self.function.resource_lexical_exit(self.site.block, self.site.position)?.is_some() { self.lexical_exit(&mut state)?; } + let frame = self.return_frame.ok_or("Resource return lacks its initially selected provisional frame")?; + let destination = self.result_slot(usize::MAX, expected, frame); + self.plan.slots[destination.0].storage = ResourceSlotStorage::Return { site: self.site }; + if let Some(source) = returned.owner { self.operation(frame, ResourceOperationRole::Transfer { source, destination }); } + outgoing = Some(destination); + } else if !state.aborted && shape(&self.program.resource_manifest, self.types, self.function.return_type)?.is_some() { return Err("Resource return has no exact owned output".into()); } + } + if outgoing.is_none() && !state.aborted && self.function.resource_lexical_exit(self.site.block, self.site.position)?.is_some() { self.lexical_exit(&mut state)?; } + let completion = if state.aborted { ResourceCompletion::Abort } else { ResourceCompletion::Return }; + self.finish(&mut state, ResourceFrameId(0), completion); + if let Some(source) = outgoing { self.operation(self.return_frame.ok_or("Resource return frame disappeared")?, ResourceOperationRole::CompleteReturnAfterCleanup { source }); } + Ok(Vec::new()) + } + TerminatorKind::Unreachable => { if !state.calls.is_empty() { return Err("pending Resource call region: unreachable exit needs its exact operation suffix retirement".into()); } self.finish(&mut state, ResourceFrameId(0), ResourceCompletion::Abort); Ok(Vec::new()) } + TerminatorKind::Goto(target) => Ok(vec![(*target, state)]), + TerminatorKind::Branch { condition, then_block, else_block } => { + self.expression(&mut state, condition, false)?; + if state.aborted { self.finish(&mut state, ResourceFrameId(0), ResourceCompletion::Abort); return Ok(Vec::new()); } + let mut yes = state.clone(); let mut no = state; + if let E::Local(tag) = condition.kind && let Some(source) = yes.tags.get(&tag.index()).copied() { + let value = yes.values[source.index() as usize].as_ref().ok_or("Resource tag source was consumed before its selecting edge")?; + if !value.shape.conditional() { return Err("Resource selecting edge lost its exact sum tag source".into()); } + // Plan both authenticated CFG edges. A known empty/occupied + // producer does not remove either runtime SumTag edge; + // the exact selecting guard still gates each SumTake. + if let Some(value) = &mut yes.values[source.index() as usize] { value.occupancy = ResourceOccupancy::Occupied; } + if let Some(value) = &mut no.values[source.index() as usize] { value.occupancy = ResourceOccupancy::Empty; } + yes.guards.insert(source.index(), (tag, true)); no.guards.insert(source.index(), (tag, false)); + } + Ok(vec![(*then_block, yes), (*else_block, no)]) + } + TerminatorKind::ForEach { key, value, by_view, iterable, body, exit } => { + control_flow::consuming_list(self.function, self.types, *key, *value, *by_view, iterable)?; + if self.expression(&mut state, iterable, true)?.is_some() { + return Err("Resource sequence cannot acquire or transport element custody".into()); + } + if state.aborted { self.finish(&mut state, ResourceFrameId(0), ResourceCompletion::Abort); return Ok(Vec::new()); } + self.clear(&mut state, *key); + Ok(vec![(*body, state.clone()), (*exit, state)]) + } + TerminatorKind::Switch { scrutinee, variants, otherwise } => { + control_flow::ordinary_switch(self.function, self.types, scrutinee, variants, *otherwise)?; + if self.expression(&mut state, scrutinee, true)?.is_some() { + return Err("Resource switch cannot acquire or transport discriminant custody".into()); + } + if state.aborted { self.finish(&mut state, ResourceFrameId(0), ResourceCompletion::Abort); return Ok(Vec::new()); } + let mut successors = Vec::new(); + for (_, target, bindings) in variants { + let mut branch = state.clone(); + for local in bindings { self.clear(&mut branch, *local); } + successors.push((*target, branch)); + } + if let Some(target) = otherwise { successors.push((*target, state)); } + Ok(successors) + } + TerminatorKind::ReflectedTypeDispatch { type_info, arms, otherwise } => { + let witness = self.function.resource_lowering.as_ref() + .ok_or("Resource reflected body lacks its original Source witness")?; + if !reflected_fields::admits(witness, block.id) || arms.len() != 1 { + return Err("pending ResourceOwnershipPlan: selected control-flow custody normalization is unproved".into()); + } + if self.expression(&mut state, type_info, false)?.is_some() { + return Err("Resource reflected selector cannot carry Resource custody".into()); + } + if state.aborted { + self.finish(&mut state, ResourceFrameId(0), ResourceCompletion::Abort); + return Ok(Vec::new()); + } + Ok(vec![(arms[0].target, state.clone()), (*otherwise, state)]) + } + TerminatorKind::Respond(_) => Err("pending ResourceOwnershipPlan: selected control-flow custody normalization is unproved".into()), + } + } +} + +fn join(left: &mut State, right: &State) -> Result { + let before = left.clone(); + if left.calls != right.calls { + return Err("Resource CFG join changed its exact operation stack, arrived prefix or retirement obligations".into()); + } + if left.leases != right.leases { + return Err( + "Resource CFG join cannot extend or silently end a loan from only one path".into(), + ); + } + for index in 0..left.values.len() { + match (&mut left.values[index], &right.values[index]) { + (Some(a), Some(b)) if a.shape == b.shape && a.owner == b.owner && a.loan == b.loan => { + if a.occupancy != b.occupancy { a.occupancy = ResourceOccupancy::Conditional; } + } + (None, None) => {} + _ => return Err("pending ResourceOwnershipPlan: moved/uninitialized versus live CFG join requires conditional cleanup proof".into()), + } + left.moved[index] |= right.moved[index]; + if left.descriptors[index] != right.descriptors[index] { + left.descriptors[index] = None; + } + if left.named_callables[index] != right.named_callables[index] { + left.named_callables[index] = None; + } + } + left.tags + .retain(|id, source| right.tags.get(id) == Some(source)); + left.guards + .retain(|id, guard| right.guards.get(id) == Some(guard)); + Ok(*left != before) +} + +pub(super) fn analyze( + program: &Program, + function: &Function, + types: &TypeInterner, +) -> Result { + let original = &function + .resource_lowering + .as_ref() + .ok_or("Resource plan lacks its authenticated Source")? + .original; + let admitted = function + .resource_lowering + .as_ref() + .ok_or("Resource plan has no constructor witness")? + .borrowed_sums + .iter() + .map(|row| row.original_local()) + .collect::>(); + if walk::nested_resource_declaration(&original.body, original, types, false, &admitted) { + return Err("pending ResourceOwnershipPlan: nested Resource declarations need exact constructor Scope records".into()); + } + if function.capture_count != 0 { + return Err( + "pending ResourceOwnershipPlan: Resource-bearing capture transport is unproved".into(), + ); + } + ControlFlowGraph::analyze(function) + .map_err(|errors| format!("Resource CFG is malformed: {errors:?}"))?; + let site = ResourceSite { + function: function.id, + block: function.entry, + position: ResourcePosition::Terminator, + }; + let return_frame = + shape(&program.resource_manifest, types, function.return_type)?.map(|_| ResourceFrameId(1)); + let mut frames = vec![ResourceFrame { + id: ResourceFrameId(0), + role: ResourceFrameRole::Scope, + site, + parent: return_frame, + ordinal: 0, + }]; + if let Some(id) = return_frame { + frames.push(ResourceFrame { + id, + role: ResourceFrameRole::Return, + site, + parent: None, + ordinal: 0, + }); + } + let plan = ResourceFunctionPlan { + carriers: None, + function: function.id, + identity: function.identity.clone(), + parameters: function.params.clone(), + return_type: function.return_type, + frames, + slots: Vec::new(), + loans: Vec::new(), + operations: Vec::new(), + execution_frames: Vec::new(), + lexical_exits: Vec::new(), + descriptor_return: function + .resource_lowering + .as_ref() + .and_then(|witness| witness.descriptors.returned()) + .cloned(), + descriptor_values: returned_descriptors::current_values( + function + .resource_lowering + .as_ref() + .ok_or("Resource descriptors lost their constructor witness")?, + function, + )?, + descriptor_locals: function + .resource_lowering + .as_ref() + .map_or_else(Vec::new, |witness| { + witness + .descriptors + .locals() + .map(|(local, hook)| (local, hook.clone())) + .collect() + }), + named_callable_producers: Vec::new(), + named_callable_values: named_callables::current_values( + function + .resource_lowering + .as_ref() + .ok_or("Resource named values lost their constructor witness")?, + function, + )?, + }; + let mut analysis = Analysis { + program, + function, + types, + plan, + local_slots: vec![None; function.locals.len()], + expression_slots: BTreeMap::new(), + operation_frames: BTreeMap::new(), + loan_sites: BTreeMap::new(), + site, + ordinal: 0, + expression: None, + emit: false, + active_frame: ResourceFrameId(0), + return_frame, + normalized_calls: BTreeMap::new(), + }; + let mut initial = State { + values: vec![None; function.locals.len()], + moved: vec![false; function.locals.len()], + descriptors: vec![None; function.locals.len()], + named_callables: vec![None; function.locals.len()], + tags: BTreeMap::new(), + guards: BTreeMap::new(), + leases: BTreeSet::new(), + aborted: false, + calls: Vec::new(), + }; + for local in &function.locals { + if let Some(shape) = shape(&program.resource_manifest, types, local.ty)? + && !function.is_view_local(local.id) + { + let id = ResourceOwnerSlotId(analysis.plan.slots.len()); + analysis.local_slots[local.id.index() as usize] = Some(id); + analysis.plan.slots.push(ResourceOwnerSlot { + id, + frame: ResourceFrameId(0), + shape, + storage: ResourceSlotStorage::Local { + header: local.clone(), + }, + }); + } + } + for (parameter, param) in function.params.iter().enumerate() { + if let Some(shape) = shape(&program.resource_manifest, types, param.ty)? { + let mut value = Value { + occupancy: if shape.conditional() { + ResourceOccupancy::Conditional + } else { + ResourceOccupancy::Occupied + }, + shape, + owner: analysis.local_slots[param.local.index() as usize], + loan: None, + }; + if param.mode == ParamMode::View { + let loan = ResourceLoanId(analysis.plan.loans.len()); + analysis.plan.loans.push(ResourceLoan { + id: loan, + frame: ResourceFrameId(0), + shape: value.shape.clone(), + parameter: None, + source: ResourceLoanSource::IncomingViewFormal { + scope: ResourceFrameId(0), + parameter, + }, + }); + value.loan = Some(loan); + } + initial.values[param.local.index() as usize] = Some(value); + } + } + let mut states = vec![None; function.blocks.len()]; + states[function.entry.index() as usize] = Some(initial); + let mut queue = VecDeque::from([function.entry]); + let mut steps = 0usize; + let limit = function + .blocks + .len() + .saturating_mul(function.locals.len().saturating_add(1)) + .saturating_mul(8) + .saturating_add(1); + while let Some(block) = queue.pop_front() { + steps += 1; + if steps > limit { + return Err("pending ResourceOwnershipPlan: custody fixed point exceeds the finite normalization bound".into()); + } + let input = states[block.index() as usize] + .as_ref() + .ok_or("Resource reachable block has no exact incoming state")? + .clone(); + for (target, output) in analysis.block(&function.blocks[block.index() as usize], input)? { + let changed = if let Some(current) = &mut states[target.index() as usize] { + join(current, &output)? + } else { + states[target.index() as usize] = Some(output); + true + }; + if changed { + queue.push_back(target); + } + } + } + analysis.emit = true; + for block in &function.blocks { + if let Some(state) = &states[block.id.index() as usize] { + analysis.block(block, state.clone())?; + } else if walk::mir_block_has_resource(block) + || walk::mir_block_has_custody(block, function, types) + { + return Err( + "Resource custody operation is disconnected from its authenticated entry CFG" + .into(), + ); + } + } + Ok(analysis.plan) +} diff --git a/crates/jett_mir/src/resource_ownership/flow/normalized.rs b/crates/jett_mir/src/resource_ownership/flow/normalized.rs new file mode 100644 index 00000000..fe6a6a86 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/flow/normalized.rs @@ -0,0 +1,507 @@ +//! Cross-block normalized Source operation proof. Every identity is constructor sealed. +use super::*; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(super) struct ActiveCall { + region: ResourceCallRegionId, + pub(super) frame: ResourceFrameId, + parent: ResourceFrameId, + next: usize, + loans: Vec, + owners: Vec<(ResourceOwnerSlotId, ResourceOccupancy)>, + invoked: bool, +} + +#[derive(Clone)] +pub(super) struct PreparedCall { + frame: ResourceFrameId, + operands: Vec, + result: ResourceCallResult, + created_loans: Vec, +} + +impl Analysis<'_> { + fn prepare_normalized( + &mut self, + state: &State, + region: &ResourceCallRegion, + ) -> Result { + if let Some(prepared) = self.normalized_calls.get(®ion.id()) { + return Ok(prepared.clone()); + } + let frame = self.frame(0, ResourceFrameRole::Operation); + let begin = self.site; + let mut operands = Vec::new(); + let mut created_loans = Vec::new(); + for actual in region.actuals() { + let fact = region + .source() + .arguments + .iter() + .find(|fact| fact.parameter_index == actual.parameter()) + .ok_or("Resource normalized actual has no exact original Source tuple")?; + self.site = actual.stage(); + let Some(shape) = shape( + &self.program.resource_manifest, + self.types, + actual.endpoint().ty, + )? + else { + if actual.ordinary().is_none() { + return Err( + "Resource normalized ordinary actual lacks its exact initialized endpoint" + .into(), + ); + } + operands.push(ResourceCallOperand::Ordinary { + parameter: actual.parameter(), + ty: actual.endpoint().ty, + }); + continue; + }; + if actual.ordinary().is_some() || fact.staging != hir::ArgumentStaging::Original { + return Err( + "Resource normalized actual gained ordinary storage or staging authority" + .into(), + ); + } + match fact.effect { + Effect::TransferOwned | Effect::RelinquishOwned => { + if fact.syntax != Syntax::Bare { + return Err( + "Resource normalized owned actual changes its bare Source syntax" + .into(), + ); + } + let slot = self.argument_slot(0, actual.parameter(), shape, frame); + if fact.callee_access == Access::View { + let loan = self.loan( + 0, + actual.parameter(), + frame, + ResourceLoanSource::Owner(slot), + )?; + created_loans.push(loan); + operands.push(ResourceCallOperand::Borrowed { + parameter: actual.parameter(), + loan, + }); + } else { + operands.push(ResourceCallOperand::Owned { + parameter: actual.parameter(), + slot, + }); + } + } + Effect::RetainBorrow => { + if fact.syntax != Syntax::WrittenView || fact.callee_access != Access::View { + return Err( + "Resource normalized retained actual changed its written View tuple" + .into(), + ); + } + let E::View(inner) = &actual.endpoint().kind else { + return Err("pending Resource call region: retained Resource expression needs a proved stable place endpoint".into()); + }; + let E::Local(local) = inner.kind else { + return Err("pending Resource call region: retained Resource expression needs its exact Local holder".into()); + }; + let value = state + .values + .get(local.index() as usize) + .and_then(Option::as_ref) + .ok_or( + "Resource normalized retained holder is not live at its initial region", + )?; + let loan = if let Some(loan) = value.loan { + loan + } else { + let owner = value + .owner + .ok_or("Resource normalized retained holder lacks its exact owner")?; + let loan = self.loan( + 0, + actual.parameter(), + frame, + ResourceLoanSource::Owner(owner), + )?; + created_loans.push(loan); + loan + }; + operands.push(ResourceCallOperand::Borrowed { + parameter: actual.parameter(), + loan, + }); + } + Effect::Copy | Effect::ObserveData => { + return Err("Resource normalized custody cannot use Copy or ObserveData".into()); + } + } + } + self.site = begin; + let (_, output) = region + .invocation() + .ok_or("Resource normalized Source invocation lacks its exact output")?; + let result = match shape( + &self.program.resource_manifest, + self.types, + region.original().ty, + )? { + Some(_) => ResourceCallResult::Owned { + slot: self.local_slots[output.index() as usize] + .ok_or("Resource normalized output lacks its exact dedicated slot")?, + }, + None => ResourceCallResult::Ordinary { + ty: region.original().ty, + }, + }; + let prepared = PreparedCall { + frame, + operands, + result, + created_loans, + }; + self.normalized_calls.insert(region.id(), prepared.clone()); + Ok(prepared) + } + + pub(super) fn normalized_node( + &mut self, + state: &mut State, + node: &ResourceCallNode, + ) -> Result<(), String> { + let region = self + .function + .resource_call_region(node.region()) + .ok_or("Resource normalized node has no constructor-owned region")? + .clone(); + match node { + ResourceCallNode::Begin { .. } => { + if state.calls.last().map(|active| active.region) != region.parent() { + return Err( + "Resource normalized Begin changed its exact parent operation stack".into(), + ); + } + let prepared = self.prepare_normalized(state, ®ion)?; + let parent = self.active_frame; + if self.plan.frames[prepared.frame.index()].parent != Some(parent) { + return Err("Resource normalized Begin changed its frame parent".into()); + } + self.operation( + prepared.frame, + ResourceOperationRole::BeginSourceFunction { + region: region.id(), + function: region.function(), + source: region.source().clone(), + formals: region + .source() + .arguments + .iter() + .map(ResourceCallFormal::original) + .collect(), + evaluation_order: region.evaluation_order().to_vec(), + operands: prepared.operands.clone(), + result: prepared.result, + }, + ); + for &loan in &prepared.created_loans { + let parameter = self.plan.loans[loan.index()] + .parameter + .ok_or("Resource prepared Source loan lost its parameter")?; + let role = if self.plan.loans[loan.index()].shape.conditional() { + ResourceOperationRole::PrepareSourceSumBorrow { loan, parameter } + } else { + ResourceOperationRole::PrepareSourceBorrow { loan, parameter } + }; + self.operation(prepared.frame, role); + } + state.calls.push(ActiveCall { + region: region.id(), + frame: prepared.frame, + parent, + next: 0, + loans: Vec::new(), + owners: Vec::new(), + invoked: false, + }); + self.active_frame = prepared.frame; + } + ResourceCallNode::Stage { + source_index, + parameter, + value, + ordinary, + .. + } => { + let mut active = state + .calls + .pop() + .ok_or("Resource normalized Stage has no active operation")?; + if active.region != region.id() + || active.next != *source_index + || active.invoked + || region.evaluation_order().get(*source_index) != Some(parameter) + { + return Err("Resource normalized Stage changed its exact arrived prefix".into()); + } + let prepared = self + .normalized_calls + .get(®ion.id()) + .ok_or("Resource normalized Stage lacks its prepared call")? + .clone(); + let operand = prepared + .operands + .get(*source_index) + .ok_or("Resource normalized Stage lost its formal operand")? + .clone(); + let fact = region + .source() + .arguments + .iter() + .find(|fact| fact.parameter_index == *parameter) + .ok_or("Resource normalized Stage lost its Source fact")?; + let retaining = fact.effect == Effect::RetainBorrow; + let input = if !retaining && fact.syntax == Syntax::Bare { + match &value.kind { + E::View(inner) => inner.as_ref(), + _ => value, + } + } else { + value + }; + let evaluated = self.expression(state, input, !retaining)?; + if state.aborted { + return Err("pending Resource call region: infrastructure abort within a staged actual needs its exact prefix exit".into()); + } + match &operand { + ResourceCallOperand::CarrierOwned { .. } + | ResourceCallOperand::CarrierBorrowed { .. } => { + return Err( + "carrier operand reached the legacy single-leaf normalized flow".into(), + ); + } + ResourceCallOperand::Ordinary { ty, .. } => { + if evaluated.is_some() + || ordinary + .and_then(|local| self.function.local(local)) + .map(|header| header.ty) + != Some(*ty) + { + return Err("Resource normalized ordinary endpoint changed its exact type/header".into()); + } + } + ResourceCallOperand::Owned { .. } | ResourceCallOperand::Borrowed { .. } + if !retaining => + { + let value = evaluated + .ok_or("Resource normalized Stage has no live owning producer")?; + if value.loan.is_some() + || (!value.shape.conditional() + && value.occupancy != ResourceOccupancy::Occupied) + { + return Err( + "Resource normalized owned Stage is not its exact occupied owner" + .into(), + ); + } + let slot = match &operand { + ResourceCallOperand::CarrierOwned { .. } + | ResourceCallOperand::CarrierBorrowed { .. } => return Err( + "carrier operand reached the legacy single-leaf normalized flow" + .into(), + ), + ResourceCallOperand::Owned { slot, .. } => *slot, + ResourceCallOperand::Borrowed { loan, .. } => { + match self.plan.loans[loan.index()].source { + ResourceLoanSource::Owner(slot) => slot, + ResourceLoanSource::IncomingViewFormal { .. } + | ResourceLoanSource::CarrierSumProjection { .. } + | ResourceLoanSource::ProjectedSumPayload { .. } => return Err( + "Resource owned Stage changed into a resident formal loan" + .into(), + ), + } + } + ResourceCallOperand::Ordinary { .. } => { + return Err("Resource owned Stage lost its custody operand".into()); + } + }; + if value.shape != self.plan.slots[slot.index()].shape { + return Err( + "Resource normalized Stage changes its sealed owner shape".into() + ); + } + self.operation( + active.frame, + ResourceOperationRole::Transfer { + source: value.owner.ok_or( + "Resource normalized Stage lacks an exact source owner", + )?, + destination: slot, + }, + ); + active.owners.push((slot, value.occupancy)); + if let ResourceCallOperand::Borrowed { loan, .. } = operand { + state.leases.insert(loan); + active.loans.push(loan); + self.borrow(active.frame, loan); + } + } + ResourceCallOperand::Borrowed { loan, .. } => { + let value = evaluated + .ok_or("Resource normalized retained Stage lost its holder")?; + if value.shape != self.plan.loans[loan.index()].shape { + return Err( + "Resource normalized retained Stage changes its sealed loan shape" + .into(), + ); + } + if let Some(resident) = value.loan { + if resident != *loan { + return Err( + "Resource normalized retained Stage changed its forwarded loan" + .into(), + ); + } + self.operation( + active.frame, + ResourceOperationRole::BoundedBorrowUse { + loan: *loan, + parameter: *parameter, + }, + ); + } else { + if value.owner.map(ResourceLoanSource::Owner) + != Some(self.plan.loans[loan.index()].source) + { + return Err("Resource normalized retained Stage changed its original holder".into()); + } + state.leases.insert(*loan); + active.loans.push(*loan); + self.borrow(active.frame, *loan); + } + } + ResourceCallOperand::Owned { .. } => { + return Err( + "Resource normalized retained Stage cannot own its formal".into() + ); + } + } + self.operation( + active.frame, + ResourceOperationRole::StageSourceActual { + region: region.id(), + source_index: *source_index, + parameter: *parameter, + operand, + ordinary: *ordinary, + }, + ); + active.next += 1; + state.calls.push(active); + } + ResourceCallNode::Invoke { output, .. } => { + let active = state + .calls + .last_mut() + .ok_or("Resource normalized Invoke lacks its active operation")?; + if active.region != region.id() + || active.invoked + || active.next != region.evaluation_order().len() + { + return Err( + "Resource normalized Invoke has an incomplete or reused actual prefix" + .into(), + ); + } + active.invoked = true; + let frame = active.frame; + let prepared = self + .normalized_calls + .get(®ion.id()) + .ok_or("Resource normalized Invoke lost its prepared tuple")? + .clone(); + self.operation( + frame, + ResourceOperationRole::InvokeSourceFunction { + function: region.function(), + formals: region + .source() + .arguments + .iter() + .map(ResourceCallFormal::original) + .collect(), + source: region.source().clone(), + evaluation_order: region.evaluation_order().to_vec(), + operands: prepared.operands, + result: prepared.result, + }, + ); + let value = shape( + &self.program.resource_manifest, + self.types, + region.original().ty, + )? + .map(|shape| -> Result { + Ok(Value { + occupancy: if shape.conditional() { + ResourceOccupancy::Conditional + } else { + ResourceOccupancy::Occupied + }, + shape, + owner: match prepared.result { + ResourceCallResult::Owned { slot } => Some(slot), + ResourceCallResult::Ordinary { .. } => None, + ResourceCallResult::Carrier { .. } => { + return Err( + "carrier result reached the legacy single-leaf normalized flow" + .into(), + ); + } + }, + loan: None, + }) + }) + .transpose()?; + self.store(state, *output, value, false, false)?; + } + ResourceCallNode::End { outcome, .. } => { + let active = state + .calls + .pop() + .ok_or("Resource normalized End lacks its active operation")?; + if active.region != region.id() + || (*outcome == ResourceCompletion::Normal) != active.invoked + || *outcome == ResourceCompletion::Return + { + return Err( + "Resource normalized End changed its exact invoked/abandoned outcome" + .into(), + ); + } + for loan in active.loans.iter().rev() { + state.leases.remove(loan); + self.end_borrow(active.frame, *loan); + } + let prepared = self + .normalized_calls + .get(®ion.id()) + .ok_or("Resource normalized End lost its exact prepared operands")?; + let owners = active.owners.iter().rev().filter(|(slot, _)| *outcome == ResourceCompletion::Abort || prepared.operands.iter().any(|operand| matches!(operand, ResourceCallOperand::Borrowed { loan, .. } if self.plan.loans[loan.index()].source == ResourceLoanSource::Owner(*slot)))).copied().collect::>(); + for (source, occupancy) in owners { + self.operation( + active.frame, + ResourceOperationRole::Drop { source, occupancy }, + ); + } + self.operation( + active.frame, + ResourceOperationRole::Complete { outcome: *outcome }, + ); + self.active_frame = active.parent; + } + } + Ok(()) + } +} diff --git a/crates/jett_mir/src/resource_ownership/lexical_borrows.rs b/crates/jett_mir/src/resource_ownership/lexical_borrows.rs new file mode 100644 index 00000000..c6028faf --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/lexical_borrows.rs @@ -0,0 +1,566 @@ +//! Private lexical declaration and retirement custody. Source paths never remap. +use super::*; +use hir::{ExpressionKind as E, StatementKind as H}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ResourceLexicalExitId(usize); +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceLexicalExitKind { + Fallthrough, + Return, + Break, + Continue, +} +#[derive(Debug, Clone, PartialEq)] +pub struct ResourceLexicalExit { + id: ResourceLexicalExitId, + kind: ResourceLexicalExitKind, + site: ResourceSite, + pub(super) scopes: Vec, + original_exit: Option, + target: Option, +} +impl ResourceLexicalExit { + pub fn kind(&self) -> ResourceLexicalExitKind { + self.kind + } + pub(super) fn declarations(&self) -> impl Iterator + '_ { + self.scopes + .iter() + .rev() + .flat_map(|scope| scope.active.iter().rev().copied()) + } + pub(crate) fn current_locals(&self, function: &Function) -> Result, String> { + let witness = function + .resource_lowering + .as_ref() + .ok_or("Resource lexical exit has no exact function witness")?; + let mut locals = Vec::new(); + for original in self.declarations() { + let row = witness + .borrowed_sums + .iter() + .find(|row| row.original_local() == original) + .ok_or("Resource lexical exit lost its current declaration headers")?; + for local in row.retirement_locals() { + if !locals.contains(&local) { + locals.push(local); + } + } + } + Ok(locals) + } +} +#[derive(Debug, Clone)] +pub struct ResourceLexicalExitPlan { + site: ResourceSite, + kind: ResourceLexicalExitKind, + operation_ids: Vec, + clear_locals: Vec, +} +impl ResourceLexicalExitPlan { + pub fn kind(&self) -> ResourceLexicalExitKind { + self.kind + } + pub fn operation_ids(&self) -> &[ResourceOperationId] { + &self.operation_ids + } + pub fn clear_locals(&self) -> &[LocalId] { + &self.clear_locals + } + pub(super) fn new( + site: ResourceSite, + kind: ResourceLexicalExitKind, + operation_ids: Vec, + clear_locals: Vec, + ) -> Self { + Self { + site, + kind, + operation_ids, + clear_locals, + } + } +} +impl ResourceFunctionPlan { + pub fn lexical_exit( + &self, + block: BlockId, + position: ResourcePosition, + ) -> Option<&ResourceLexicalExitPlan> { + self.lexical_exits.iter().find(|exit| { + exit.site + == ResourceSite { + function: self.function, + block, + position, + } + }) + } +} +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct LexicalScope { + path: Vec, + pub(crate) active: Vec, +} +#[derive(Clone)] +pub(super) struct Declaration { + pub(super) path: Vec, + pub(super) scope: Vec, + pub(super) local: LocalId, + pub(super) value: Expression, +} +struct OriginalScope { + path: Vec, + body: hir::Block, +} +#[derive(Default)] +struct Inventory { + declarations: Vec, + scopes: Vec, +} +fn child(path: &[usize], kind: usize, index: usize) -> Vec { + let mut next = path.to_vec(); + next.extend([kind, index]); + next +} +impl Inventory { + fn block(&mut self, block: &hir::Block, path: Vec, manifest: &hir::ResourceManifest) { + self.scopes.push(OriginalScope { + path: path.clone(), + body: block.clone(), + }); + for (index, statement) in block.statements.iter().enumerate() { + let at = child(&path, 0, index); + if let H::Let { local, value } = &statement.kind + && manifest.kind_for_type(value.ty).is_some() + && matches!(&value.kind, E::View(handle) if matches!(handle.kind, E::Handle { .. })) + { + self.declarations.push(Declaration { + path: at.clone(), + scope: path.clone(), + local: *local, + value: value.clone(), + }); + } + let mut values = Vec::new(); + match &statement.kind { + H::Let { value, .. } + | H::HandleDefault(value) + | H::Expression(value) + | H::Respond(value) => values.push(value), + H::Assign { target, value } => values.extend([target, value]), + H::Return(value) => values.extend(value.iter()), + H::If { + condition, + then_block, + else_block, + } => { + values.push(condition); + self.block(then_block, child(&at, 2, 0), manifest); + if let Some(block) = else_block { + self.block(block, child(&at, 2, 1), manifest); + } + } + H::While { condition, body } => { + values.push(condition); + self.block(body, child(&at, 2, 0), manifest); + } + H::For { iterable, body, .. } => { + values.push(iterable); + self.block(body, child(&at, 2, 0), manifest); + } + H::Match { scrutinee, arms } => { + values.push(scrutinee); + for (arm, value) in arms.iter().enumerate() { + self.block(&value.body, child(&at, 2, arm), manifest); + } + } + H::Assert { condition, message } => { + values.push(condition); + values.extend(message.iter()); + } + H::Breakpoint { condition, .. } => values.extend(condition.iter()), + H::Scope(block) => self.block(block, child(&at, 2, 0), manifest), + H::ReflectedTypeDispatch { type_info, arms } => { + values.push(type_info); + for (arm, value) in arms.iter().enumerate() { + self.block(&value.body, child(&at, 2, arm), manifest); + } + } + H::Break | H::Continue | H::Trace(_) => {} + } + for (index, value) in values.into_iter().enumerate() { + self.expression(value, child(&at, 1, index), manifest); + } + } + } + fn expression( + &mut self, + value: &Expression, + path: Vec, + manifest: &hir::ResourceManifest, + ) { + let mut children: Vec<&Expression> = Vec::new(); + match &value.kind { + E::Binary { left, right, .. } => children.extend([left.as_ref(), right.as_ref()]), + E::Unary { value, .. } + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::Declassify(value) + | E::Coarsen(value) + | E::RefinementValidated(value) + | E::InterfaceType(value) + | E::RuntimeFailureMessage(value) + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::View(value) + | E::Clone(value) + | E::Comptime { value, .. } + | E::InterfaceCoerce { value, .. } + | E::FunctionAdapter { value, .. } + | E::StateIs { value, .. } => children.push(value), + E::Call { args, .. } + | E::ResourceInvoke { args, .. } + | E::Intrinsic { args, .. } + | E::ActorSpawn { args, .. } => children.extend(args), + E::IndirectCall { callee, args, .. } => { + children.push(callee); + children.extend(args); + } + E::ActorMessage { actor, args, .. } => { + children.push(actor); + children.extend(args); + } + E::StructConstruct { fields, .. } | E::BitfieldConstruct { fields, .. } => { + children.extend(fields) + } + E::MachineConstruct { payloads, .. } | E::EnumConstruct { payloads, .. } => { + children.extend(payloads) + } + E::MachineTransition { + source, payloads, .. + } => { + children.push(source); + children.extend(payloads); + } + E::ListConstruct { elements } => children.extend(elements), + E::MapConstruct { entries } => { + for entry in entries { + children.extend([&entry.key, &entry.value]); + } + } + E::Handle { + target, failure, .. + } => { + children.push(target); + self.block(failure, child(&path, 3, 0), manifest); + } + E::StringInterpolation(parts) => { + for part in parts { + if let hir::StringSegment::Value(value) = part { + children.push(value); + } + } + } + E::Field { base, .. } => children.push(base), + // A separate callable body has its own checked function/local archive. + E::InlineFunction { .. } + | E::Int(_) + | E::Float(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::Local(_) + | E::Constant { .. } + | E::FunctionRef(_) + | E::ResourceHookValue { .. } + | E::ClosureRef { .. } + | E::OptionalNone + | E::RuntimeFailure(_) + | E::PropertyCaseContext(_) => {} + } + for (index, value) in children.into_iter().enumerate() { + self.expression(value, child(&path, 1, index), manifest); + } + } +} +fn inventory(witness: &ResourceLoweringWitness) -> Inventory { + let mut inventory = Inventory::default(); + inventory.block(&witness.original.body, Vec::new(), &witness.manifest); + inventory +} +pub(super) fn declarations(witness: &ResourceLoweringWitness) -> Vec { + inventory(witness).declarations +} +impl Capture { + pub(crate) fn lexical_scope(&self, block: &hir::Block) -> Result, String> { + self.lexical_scope_under(block, None) + } + pub(crate) fn lexical_scope_under( + &self, + block: &hir::Block, + original_prefix: Option<&[usize]>, + ) -> Result, String> { + let Some(witness) = &self.witness else { + return Ok(None); + }; + let inventory = inventory(witness); + if inventory.declarations.is_empty() { + return Ok(None); + } + let mut scopes = inventory.scopes.iter().filter(|scope| { + original_prefix.is_none_or(|prefix| scope.path.starts_with(prefix)) + && crate::breakpoint_regions::hir_blocks_equal(&scope.body, block) + }); + let scope = scopes + .next() + .ok_or("Resource lexical block is absent from its exact original archive")?; + if scopes.next().is_some() { + return Err( + "pending Resource lexical block has ambiguous original specialization identity" + .into(), + ); + } + Ok(Some(LexicalScope { + path: scope.path.clone(), + active: Vec::new(), + })) + } + pub(crate) fn lexical_exit( + &mut self, + kind: ResourceLexicalExitKind, + block: BlockId, + position: ResourcePosition, + scopes: &[LexicalScope], + original_exit: Option<&hir::Statement>, + target: Option, + ) -> Option { + if scopes.iter().all(|scope| scope.active.is_empty()) { + return None; + } + let witness = self.witness.as_mut()?; + let site = ResourceSite { + function: witness.original.id, + block, + position, + }; + let id = ResourceLexicalExitId(witness.lexical_exits.len()); + witness.lexical_exits.push(ResourceLexicalExit { + id, + kind, + site, + scopes: scopes.to_vec(), + original_exit: original_exit.cloned(), + target, + }); + Some(id) + } +} +pub(super) fn seal(witness: &mut ResourceLoweringWitness) { + witness.lexical_seal = witness.lexical_exits.clone(); +} +pub(super) fn current( + witness: &ResourceLoweringWitness, + function: &Function, +) -> Result<(), String> { + if witness.lexical_exits != witness.lexical_seal { + return Err( + "Resource lexical exits changed their independent constructor membership seal".into(), + ); + } + let inventory = inventory(witness); + for declaration in &inventory.declarations { + if !witness + .lexical_exits + .iter() + .any(|exit| exit.declarations().any(|local| local == declaration.local)) + { + return Err("Resource lexical exits lost the complete original declaration retirement inventory".into()); + } + } + for (index, exit) in witness.lexical_exits.iter().enumerate() { + if exit.id.0 != index || exit.site.function != function.id { + return Err( + "Resource lexical exit has a foreign or duplicated constructor identity".into(), + ); + } + let mut keys = BTreeSet::new(); + for scope in &exit.scopes { + if !inventory + .scopes + .iter() + .any(|original| original.path == scope.path) + { + return Err("Resource lexical exit lost its original lexical block".into()); + } + for key in &scope.active { + if !keys.insert(key.index()) + || !inventory + .declarations + .iter() + .any(|row| row.local == *key && row.scope == scope.path) + { + return Err( + "Resource lexical exit changed its exact original declaration membership" + .into(), + ); + } + } + } + if let Some(original_exit) = &exit.original_exit { + let scope = exit + .scopes + .last() + .and_then(|scope| { + inventory + .scopes + .iter() + .find(|original| original.path == scope.path) + }) + .ok_or("Resource lexical exit lost its exact original containing block")?; + let matches = scope + .body + .statements + .iter() + .filter(|statement| { + let left = hir::Block { + statements: vec![(*statement).clone()], + span: original_exit.span, + }; + let right = hir::Block { + statements: vec![original_exit.clone()], + span: original_exit.span, + }; + crate::breakpoint_regions::hir_blocks_equal(&left, &right) + }) + .count(); + let kind = matches!( + (exit.kind, &original_exit.kind), + (ResourceLexicalExitKind::Return, H::Return(_)) + | (ResourceLexicalExitKind::Break, H::Break) + | (ResourceLexicalExitKind::Continue, H::Continue) + ); + if matches != 1 || !kind { + return Err( + "Resource lexical exit changed its exact archived control statement".into(), + ); + } + } else if exit.kind != ResourceLexicalExitKind::Fallthrough { + return Err("Resource lexical control exit lost its archived statement".into()); + } + let block = function + .blocks + .get(exit.site.block.index() as usize) + .filter(|block| block.id == exit.site.block) + .ok_or("Resource lexical exit lost its current block")?; + match (exit.kind, exit.site.position) { + (ResourceLexicalExitKind::Return, ResourcePosition::Terminator) + if matches!(block.terminator.kind, TerminatorKind::Return(_)) + && matches!( + exit.original_exit.as_ref().map(|statement| &statement.kind), + Some(H::Return(_)) + ) => {} + ( + ResourceLexicalExitKind::Fallthrough + | ResourceLexicalExitKind::Break + | ResourceLexicalExitKind::Continue, + ResourcePosition::Statement(index), + ) if matches!(block.statements.get(index).map(|statement| &statement.kind), Some(StatementKind::ResourceLexicalExit(id)) if *id == exit.id) => { + if let Some(target) = exit.target { + if index + 1 != block.statements.len() + || !matches!(block.terminator.kind, TerminatorKind::Goto(current) if current == target) + { + return Err( + "Resource lexical loop exit changed its exact boundary target".into(), + ); + } + } + } + _ => { + return Err( + "Resource lexical exit lost its exact constructor node or Return boundary" + .into(), + ); + } + } + let copies = function.blocks.iter().flat_map(|block| &block.statements).filter(|statement| matches!(statement.kind, StatementKind::ResourceLexicalExit(id) if id == exit.id)).count(); + if copies != usize::from(exit.kind != ResourceLexicalExitKind::Return) { + return Err("Resource lexical exit has copied or missing current nodes".into()); + } + } + for block in &function.blocks { + for (index, statement) in block.statements.iter().enumerate() { + if let StatementKind::ResourceLexicalExit(id) = statement.kind + && !witness.lexical_exits.iter().any(|exit| { + exit.id == id + && exit.site + == ResourceSite { + function: function.id, + block: block.id, + position: ResourcePosition::Statement(index), + } + }) + { + return Err("Resource lexical exit node has no exact constructor authority".into()); + } + } + } + Ok(()) +} +pub(super) fn remap_blocks( + witness: &mut ResourceLoweringWitness, + map: &[Option], +) -> Result<(), String> { + for exit in &mut witness.lexical_exits { + exit.site.block = map + .get(exit.site.block.index() as usize) + .copied() + .flatten() + .ok_or("Resource canonical map removed an authenticated lexical exit")?; + if let Some(target) = &mut exit.target { + *target = map + .get(target.index() as usize) + .copied() + .flatten() + .ok_or("Resource canonical map removed an exact lexical loop target")?; + } + } + seal(witness); + Ok(()) +} +impl Function { + pub fn resource_lexical_exit( + &self, + block: BlockId, + position: ResourcePosition, + ) -> Result, String> { + let Some(witness) = &self.resource_lowering else { + return Ok(None); + }; + witness.current(self)?; + Ok(witness.lexical_exits.iter().find(|exit| { + exit.site + == ResourceSite { + function: self.id, + block, + position, + } + })) + } +} + +pub(super) fn shift_prefix(witness: &mut ResourceLoweringWitness, block: BlockId, count: usize) { + for exit in &mut witness.lexical_exits { + if exit.site.block == block + && let ResourcePosition::Statement(index) = &mut exit.site.position + { + *index += count; + } + } + seal(witness); +} diff --git a/crates/jett_mir/src/resource_ownership/lexical_borrows_tests.rs b/crates/jett_mir/src/resource_ownership/lexical_borrows_tests.rs new file mode 100644 index 00000000..b51acb79 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/lexical_borrows_tests.rs @@ -0,0 +1,782 @@ +use super::tests::{checked_support, lowered}; +use super::*; + +const SUPPORT: &str = + include_str!("../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); +const SOURCES: [&str; 39] = [ + include_str!( + "../../../jett_driver/tests/native_conformance/resource/57_nested_view_optional_some_written.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/58_if_optional_written_some.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/59_if_optional_written_none.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/60_if_optional_bare_some.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/61_if_optional_bare_none.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/62_if_result_written_ok.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/63_if_result_written_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/64_if_result_bare_ok.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/65_if_result_bare_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/66_scope_optional_written_some.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/67_scope_optional_written_none.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/68_scope_optional_bare_some.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/69_scope_optional_bare_none.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/70_scope_result_written_ok.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/71_scope_result_written_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/72_scope_result_bare_ok.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/73_scope_result_bare_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/74_while_optional_written_some.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/75_while_optional_written_none.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/76_while_optional_bare_some.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/77_while_optional_bare_none.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/78_while_result_written_ok.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/79_while_result_written_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/80_while_result_bare_ok.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/81_while_result_bare_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/82_if_owned_optional_reuse.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/83_if_owned_result_reuse.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/84_scope_owned_optional_reuse.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/85_scope_owned_result_reuse.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/86_while_owned_optional_reuse.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/87_while_owned_result_reuse.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/88_if_sibling_optional_bindings.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/89_scope_sibling_result_bindings.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/90_scope_optional_written_borrow_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/91_scope_result_bare_borrow_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/92_while_optional_bare_borrow_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/93_while_result_written_borrow_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/94_while_owned_optional_break.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/95_while_owned_result_continue.jett" + ), +]; + +fn borrowed_function(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function + .resource_lowering + .as_ref() + .is_some_and(|witness| !witness.borrowed_sums.is_empty()) + }) + .expect("fixture contains its original borrowed declaration") +} + +fn borrowed_function_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function + .resource_lowering + .as_ref() + .is_some_and(|witness| !witness.borrowed_sums.is_empty()) + }) + .expect("fixture contains its original borrowed declaration") +} + +fn exits<'a>( + function: &Function, + plan: &'a ResourceFunctionPlan, +) -> Vec<(BlockId, ResourcePosition, &'a ResourceLexicalExitPlan)> { + let mut exits = Vec::new(); + for block in &function.blocks { + let positions = (0..block.statements.len()) + .map(ResourcePosition::Statement) + .chain(std::iter::once(ResourcePosition::Terminator)); + for position in positions { + if let Some(current) = function + .resource_lexical_exit(block.id, position) + .expect("constructor-owned lexical exit query") + { + let exit = plan + .lexical_exit(block.id, position) + .expect("fresh custody analysis retains the exact lexical exit"); + assert_eq!(exit.kind(), current.kind()); + for id in exit.operation_ids() { + let operation = &plan.operations()[id.index()]; + assert_eq!(operation.id(), *id); + assert_eq!(operation.site().block(), block.id); + assert_eq!(operation.site().position(), position); + assert!(matches!( + operation.role(), + ResourceOperationRole::EndBorrow { .. } + | ResourceOperationRole::EndSumBorrow { .. } + )); + } + let distinct = exit + .clear_locals() + .iter() + .map(|local| local.index()) + .collect::>(); + assert_eq!(distinct.len(), exit.clear_locals().len()); + assert!( + exit.clear_locals() + .iter() + .all(|local| function.local(*local).is_some()) + ); + exits.push((block.id, position, exit)); + } else { + assert!(plan.lexical_exit(block.id, position).is_none()); + } + } + } + exits +} + +fn ends_loan( + plan: &ResourceFunctionPlan, + exit: &ResourceLexicalExitPlan, + loan: ResourceLoanId, +) -> bool { + exit.operation_ids().iter().any(|id| { + matches!(plan.operations()[id.index()].role(), ResourceOperationRole::EndBorrow { loan: current } | ResourceOperationRole::EndSumBorrow { loan: current } if *current == loan) + }) +} + +// A lexical boundary may share a block with later statements or precede a loop +// backedge. Search instruction boundaries, rather than comparing block IDs. +fn site_can_reach(function: &Function, start: ResourceSite, end: ResourceSite) -> bool { + let cfg = ControlFlowGraph::analyze(function).unwrap(); + let position = |site: ResourceSite| match site.position() { + ResourcePosition::Statement(index) => index, + ResourcePosition::Terminator => function.blocks[site.block().index() as usize] + .statements + .len(), + }; + let mut pending = vec![(start.block(), position(start) + 1)]; + let mut seen = BTreeSet::new(); + while let Some((block, floor)) = pending.pop() { + if !seen.insert((block.index(), floor)) { + continue; + } + if block == end.block() && position(end) >= floor { + return true; + } + pending.extend(cfg.successors(block).iter().map(|target| (*target, 0))); + } + false +} +fn operation_precedes( + function: &Function, + start: &ResourceOperation, + end: &ResourceOperation, +) -> bool { + if start.site() == end.site() { + start.id().index() < end.id().index() + } else { + site_can_reach(function, start.site(), end.site()) + } +} +fn consumes_slot(role: &ResourceOperationRole, slot: ResourceOwnerSlotId) -> bool { + match role { + ResourceOperationRole::Transfer { source, .. } | ResourceOperationRole::SumTake { source, .. } + | ResourceOperationRole::TakeFailureCompanion { source, .. } | ResourceOperationRole::Close { source, .. } + | ResourceOperationRole::Drop { source, .. } | ResourceOperationRole::SumAdopt { source, .. } => *source == slot, + ResourceOperationRole::Replace { destination, replacement, .. } => *destination == slot || *replacement == slot, + ResourceOperationRole::SelfRebind { slot: current } => *current == slot, + ResourceOperationRole::InvokeSourceFunction { operands, .. } | ResourceOperationRole::InvokeHook { operands, .. } => operands.iter().any(|operand| matches!(operand, ResourceCallOperand::Owned { slot: current, .. } if *current == slot)), + _ => false, + } +} + +fn assert_forged_refused(mut program: Program, types: &TypeInterner, control: &str) { + let function = borrowed_function_mut(&mut program); + let witness = function.resource_lowering.as_mut().unwrap(); + witness.blocks = function.blocks.clone(); + witness.locals = function.locals.clone(); + assert!( + validate_resource_ownership(&program, types).is_err(), + "{control}" + ); + assert!( + !crate::sequences::prune::unreachable(borrowed_function_mut(&mut program)), + "canonical {control}" + ); +} + +#[test] +fn resource_lexical_borrows_shared_sources_keep_exact_exit_cleanup_after_canonical_pruning() { + for release in [false, true] { + let mut projections = 0; + for (index, source) in SOURCES.into_iter().enumerate() { + let fixture = index + 57; + let checked = checked_support(source, SUPPORT, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + for function in &mut program.functions { + assert!( + crate::sequences::prune::unreachable(function), + "Source{fixture} release={release}" + ); + } + let ownership = validate_resource_ownership(&program, types) + .unwrap_or_else(|errors| panic!("Source{fixture} release={release}: {errors:?}")); + let function = borrowed_function(&program); + let plan = ownership.function(function.id).unwrap(); + let exits = exits(function, plan); + assert!(!exits.is_empty(), "Source{fixture} release={release}"); + let rows = &function.resource_lowering.as_ref().unwrap().borrowed_sums; + assert_eq!( + rows.len(), + if fixture == 88 || fixture == 89 { 2 } else { 1 } + ); + projections += rows.len(); + for operation in plan.operations() { + let ResourceOperationRole::ProjectSumView { + source: parent, + destination: child, + .. + } = operation.role() + else { + continue; + }; + let row = function + .resource_borrowed_sum_projection( + operation.site().block(), + operation.site().position(), + ) + .unwrap() + .unwrap(); + let cleanup = exits + .iter() + .filter(|(_, _, exit)| ends_loan(plan, exit, *child)) + .collect::>(); + assert!(!cleanup.is_empty(), "Source{fixture} child={child:?}"); + for (_, _, exit) in cleanup { + for local in [row.alias(), row.source(), row.output()] { + assert!( + exit.clear_locals().contains(&local), + "Source{fixture} stale local={local:?}" + ); + assert!(function.is_view_local(local)); + assert!(!plan.owner_slots().iter().any(|slot| matches!(slot.storage(), ResourceSlotStorage::Local { header } if header.id == local))); + } + } + if matches!( + plan.loans()[parent.index()].source(), + ResourceLoanSource::IncomingViewFormal { .. } + ) { + assert!(!plan.operations().iter().any(|operation| matches!(operation.role(), ResourceOperationRole::EndSumBorrow { loan } if loan == parent)), "Source{fixture} must preserve its resident caller lease"); + } + } + let expected = match fixture { + 57 => ResourceLexicalExitKind::Return, + 94 => ResourceLexicalExitKind::Break, + 95 => ResourceLexicalExitKind::Continue, + _ => ResourceLexicalExitKind::Fallthrough, + }; + assert!(exits.iter().any(|(_, _, exit)| exit.kind() == expected && !exit.operation_ids().is_empty()), "Source{fixture} lacks {expected:?} cleanup"); + } + assert_eq!(projections, 41); + } +} + +#[test] +fn resource_lexical_borrows_owned_backing_survives_child_first_scope_and_loop_exits() { + for release in [false, true] { + for fixture in [82, 83, 84, 85, 86, 87, 94, 95] { + let checked = checked_support(SOURCES[fixture - 57], SUPPORT, release); + let program = lowered(&checked); + let ownership = + validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let function = borrowed_function(&program); + let plan = ownership.function(function.id).unwrap(); + let exits = exits(function, plan); + for operation in plan.operations() { + let ResourceOperationRole::ProjectSumView { + source: parent, + destination: child, + .. + } = operation.role() + else { + continue; + }; + let ResourceLoanSource::Owner(backing_slot) = plan.loans()[parent.index()].source() + else { + panic!("Source{fixture} must borrow its retained local sum") + }; + let row = function + .resource_borrowed_sum_projection( + operation.site().block(), + operation.site().position(), + ) + .unwrap() + .unwrap(); + assert!( + matches!(plan.owner_slots()[backing_slot.index()].storage(), ResourceSlotStorage::Local { header } if header.id == row.backing()) + ); + let normal = exits + .iter() + .find(|(_, _, exit)| { + ends_loan(plan, exit, *child) + && exit.kind() != ResourceLexicalExitKind::Return + }) + .expect("original inner body retires normally before backing reuse"); + assert!(!normal.2.clear_locals().contains(&row.backing())); + let child_end = normal.2.operation_ids().iter().position(|id| matches!(plan.operations()[id.index()].role(), ResourceOperationRole::EndBorrow { loan } if loan == child)).unwrap(); + let parent_end = normal.2.operation_ids().iter().position(|id| matches!(plan.operations()[id.index()].role(), ResourceOperationRole::EndSumBorrow { loan } if loan == parent)).expect("locally created parent shell lease retires"); + assert!( + child_end < parent_end, + "Source{fixture} child before parent" + ); + let transfers = plan.operations().iter().filter(|operation| matches!(operation.role(), ResourceOperationRole::Transfer { source, .. } if *source == backing_slot)).collect::>(); + assert_eq!( + transfers.len(), + 1, + "Source{fixture} moves its original held shell exactly once" + ); + let moved = transfers[0]; + let ResourceOperationRole::Transfer { + destination: shell, .. + } = moved.role() + else { + unreachable!() + }; + let exit_site = ResourceSite { + function: function.id, + block: normal.0, + position: normal.1, + }; + assert!( + site_can_reach(function, exit_site, moved.site()), + "Source{fixture} must reuse held after its actual lexical boundary" + ); + let ResourcePosition::Statement(index) = moved.site().position() else { + panic!("Source{fixture} owning Handle begins at its exact source Let") + }; + let ResourceSlotStorage::Local { + header: shell_header, + } = plan.owner_slots()[shell.index()].storage() + else { + panic!("Source{fixture} owning Handle has a dedicated generated source holder") + }; + assert!( + matches!(&function.blocks[moved.site().block().index() as usize].statements[index].kind, StatementKind::Let { local, value } if *local == shell_header.id && matches!(value.kind, hir::ExpressionKind::Local(backing) if backing == row.backing())), + "Source{fixture} transfers that exact original backing, without a snapshot or replacement" + ); + assert_eq!( + plan.owner_slots()[shell.index()].shape(), + plan.owner_slots()[backing_slot.index()].shape() + ); + for candidate in plan.operations() { + if candidate.id() != moved.id() + && consumes_slot(candidate.role(), backing_slot) + && operation_precedes(function, candidate, moved) + { + panic!( + "Source{fixture} consumes backing={} before its later unwrap at operation={}", + backing_slot.index(), + candidate.id().index() + ); + } + } + let takes = plan.operations().iter().filter(|operation| matches!(operation.role(), ResourceOperationRole::SumTake { source, success: true, .. } if source == shell)).collect::>(); + assert_eq!( + takes.len(), + 1, + "Source{fixture} extracts the transferred shell once" + ); + let take = takes[0]; + assert!(operation_precedes(function, moved, take)); + let ResourceOperationRole::SumTake { + destination: payload, + .. + } = take.role() + else { + unreachable!() + }; + let mut slot = *payload; + let mut previous = take; + let mut visited = BTreeSet::new(); + loop { + assert!( + visited.insert(slot.index()), + "Source{fixture} final payload transfer chain is acyclic" + ); + let closes = plan.operations().iter().filter(|operation| match operation.role() { + ResourceOperationRole::Close { source, .. } => *source == slot, + ResourceOperationRole::InvokeSourceFunction { function: callee, operands, .. } => { + let declaration = &program.functions[callee.index() as usize].identity.declaration; + declaration.namespace == "resource_probe" && declaration.name == "close" && operands.iter().any(|operand| matches!(operand, ResourceCallOperand::Owned { slot: current, .. } if *current == slot)) + } + _ => false, + }).collect::>(); + if !closes.is_empty() { + assert_eq!( + closes.len(), + 1, + "Source{fixture} closes the extracted payload exactly once" + ); + assert!(operation_precedes(function, previous, closes[0])); + break; + } + let next = plan.operations().iter().filter(|operation| matches!(operation.role(), ResourceOperationRole::Transfer { source, .. } if *source == slot)).collect::>(); + assert_eq!( + next.len(), + 1, + "Source{fixture} carries payload={} into one exact owning continuation", + slot.index() + ); + assert!(operation_precedes(function, previous, next[0])); + let ResourceOperationRole::Transfer { destination, .. } = next[0].role() else { + unreachable!() + }; + slot = *destination; + previous = next[0]; + } + if matches!(fixture, 86 | 87 | 94 | 95) { + let block = &function.blocks[normal.0.index() as usize]; + assert!( + matches!(block.terminator.kind, TerminatorKind::Goto(_)), + "Source{fixture} cleanup precedes its backedge/break/continue" + ); + } + } + } + } +} + +#[test] +fn resource_lexical_borrows_sibling_names_keep_separate_original_keys_and_backings() { + for release in [false, true] { + for fixture in [88, 89] { + let checked = checked_support(SOURCES[fixture - 57], SUPPORT, release); + let program = lowered(&checked); + let ownership = + validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let function = borrowed_function(&program); + let rows = &function.resource_lowering.as_ref().unwrap().borrowed_sums; + assert_eq!(rows.len(), 2); + assert_eq!( + function.local(rows[0].alias()).unwrap().name, + function.local(rows[1].alias()).unwrap().name + ); + assert_ne!(rows[0].alias(), rows[1].alias()); + assert_ne!(rows[0].backing(), rows[1].backing()); + let plan = ownership.function(function.id).unwrap(); + let exits = exits(function, plan); + for (index, row) in rows.iter().enumerate() { + assert!(exits.iter().any(|(_, _, exit)| exit.kind() + == ResourceLexicalExitKind::Fallthrough + && exit.clear_locals().contains(&row.alias()) + && !exit.clear_locals().contains(&rows[1 - index].alias()))); + } + } + } +} + +#[test] +fn resource_lexical_borrows_return_cleanup_is_after_the_observation_operand() { + for release in [false, true] { + let checked = checked_support(SOURCES[0], SUPPORT, release); + let program = lowered(&checked); + let ownership = validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let function = borrowed_function(&program); + let plan = ownership.function(function.id).unwrap(); + let exits = exits(function, plan); + let child = plan + .operations() + .iter() + .find_map(|operation| match operation.role() { + ResourceOperationRole::ProjectSumView { destination, .. } => Some(*destination), + _ => None, + }) + .unwrap(); + assert!(plan.operations().iter().any(|operation| matches!(operation.role(), ResourceOperationRole::BoundedBorrowUse { loan, .. } if *loan == child))); + let returns = exits + .iter() + .filter(|(_, _, exit)| { + exit.kind() == ResourceLexicalExitKind::Return && ends_loan(plan, exit, child) + }) + .collect::>(); + assert!(!returns.is_empty()); + for (block, position, _) in returns { + assert_eq!(*position, ResourcePosition::Terminator); + assert!(matches!( + function.blocks[block.index() as usize].terminator.kind, + TerminatorKind::Return(_) + )); + } + } +} + +#[test] +fn resource_lexical_borrows_refuse_copied_missing_and_swapped_exit_nodes_after_general_reseal() { + for release in [false, true] { + let checked = checked_support(SOURCES[31], SUPPORT, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + let function = borrowed_function(&baseline); + let nodes = function + .blocks + .iter() + .flat_map(|block| { + block + .statements + .iter() + .enumerate() + .filter_map(move |(index, statement)| { + matches!(statement.kind, StatementKind::ResourceLexicalExit(_)) + .then_some((block.id, index)) + }) + }) + .collect::>(); + assert!(nodes.len() >= 2); + for mutation in 0..4 { + let mut forged = baseline.clone(); + let function = borrowed_function_mut(&mut forged); + let (block, index) = nodes[0]; + match mutation { + 0 => { + let statement = + function.blocks[block.index() as usize].statements[index].clone(); + function.blocks.push(BasicBlock { + id: BlockId(function.blocks.len() as u32), + statements: vec![statement], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: function.span, + }, + }); + } + 1 => { + function.blocks[block.index() as usize] + .statements + .remove(index); + } + 2 => { + let other = + &function.blocks[nodes[1].0.index() as usize].statements[nodes[1].1].kind; + let StatementKind::ResourceLexicalExit(other) = other else { + unreachable!() + }; + let other = *other; + function.blocks[block.index() as usize].statements[index].kind = + StatementKind::ResourceLexicalExit(other); + } + 3 => { + let returning = function + .blocks + .iter_mut() + .find(|block| matches!(block.terminator.kind, TerminatorKind::Return(_))) + .unwrap(); + returning.terminator.kind = TerminatorKind::Goto(function.entry); + } + _ => unreachable!(), + } + assert_forged_refused( + forged, + types, + &format!("exit mutation={mutation} release={release}"), + ); + } + } +} + +#[test] +fn resource_lexical_borrows_refuse_duplicate_original_keys_sibling_backing_swaps_and_disconnected_takes() + { + for release in [false, true] { + for fixture in [88, 89] { + let checked = checked_support(SOURCES[fixture - 57], SUPPORT, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + let function = borrowed_function(&baseline); + let rows = &function.resource_lowering.as_ref().unwrap().borrowed_sums; + let source = rows[0].source(); + let other_backing = rows[1].backing(); + let project = rows[0].success_site(); + for mutation in 0..4 { + let mut forged = baseline.clone(); + let function = borrowed_function_mut(&mut forged); + match mutation { + 0 => { + let witness = function.resource_lowering.as_mut().unwrap(); + witness.borrowed_sums[1] = witness.borrowed_sums[0].clone(); + } + 1 => { + function + .resource_lowering + .as_mut() + .unwrap() + .borrowed_sums + .pop(); + } + 2 => { + let statement = function.blocks.iter_mut().flat_map(|block| &mut block.statements).find(|statement| matches!(statement.kind, StatementKind::Let { local, .. } if local == source)).unwrap(); + let StatementKind::Let { value, .. } = &mut statement.kind else { + unreachable!() + }; + let hir::ExpressionKind::View(inner) = &mut value.kind else { + unreachable!() + }; + inner.kind = hir::ExpressionKind::Local(other_backing); + } + 3 => { + let ResourcePosition::Statement(index) = project.position() else { + unreachable!() + }; + let statement = function.blocks[project.block().index() as usize] + .statements[index] + .clone(); + function.blocks.push(BasicBlock { + id: BlockId(function.blocks.len() as u32), + statements: vec![statement], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: function.span, + }, + }); + } + _ => unreachable!(), + } + assert_forged_refused( + forged, + types, + &format!("Source{fixture} key mutation={mutation} release={release}"), + ); + } + } + } +} + +#[test] +fn resource_lexical_borrows_refuse_changed_exit_membership_with_unchanged_nodes_and_sites() { + for release in [false, true] { + let checked = checked_support(SOURCES[31], SUPPORT, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + let witness = borrowed_function(&baseline) + .resource_lowering + .as_ref() + .unwrap(); + let candidates = witness + .lexical_exits + .iter() + .enumerate() + .filter_map(|(index, exit)| { + (exit.kind() == ResourceLexicalExitKind::Fallthrough + && exit.scopes.iter().any(|scope| !scope.active.is_empty())) + .then_some(index) + }) + .collect::>(); + assert_eq!(candidates.len(), 2); + let [left, right] = [candidates[0], candidates[1]]; + for mutation in 0..3 { + let mut forged = baseline.clone(); + let function = borrowed_function_mut(&mut forged); + let original_blocks = function.blocks.clone(); + let witness = function.resource_lowering.as_mut().unwrap(); + let independent = witness.lexical_seal.clone(); + match mutation { + 0 => { + let left_scopes = witness.lexical_exits[left].scopes.clone(); + witness.lexical_exits[left].scopes = + witness.lexical_exits[right].scopes.clone(); + witness.lexical_exits[right].scopes = left_scopes; + } + 1 => { + for scope in &mut witness.lexical_exits[left].scopes { + scope.active.clear(); + } + } + 2 => { + let scope = witness.lexical_exits[left] + .scopes + .iter_mut() + .find(|scope| !scope.active.is_empty()) + .unwrap(); + scope.active.push(scope.active[0]); + } + _ => unreachable!(), + } + assert_eq!(witness.lexical_seal, independent); + assert!(crate::breakpoint_regions::blocks_equal( + &function.blocks, + &original_blocks + )); + assert_forged_refused( + forged, + types, + &format!("membership mutation={mutation} release={release}"), + ); + } + } +} diff --git a/crates/jett_mir/src/resource_ownership/mod.rs b/crates/jett_mir/src/resource_ownership/mod.rs new file mode 100644 index 00000000..cde6eb95 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/mod.rs @@ -0,0 +1,1708 @@ +//! Initial Source-authenticated custody analysis, separate from ordinary Move/Copy. +//! A successful plan is metadata proof only; the Resource ABI remains mandatory. +use super::*; +use std::collections::{BTreeMap, BTreeSet}; + +macro_rules! identity { + ($name:ident) => { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct $name(usize); + impl $name { + pub fn index(self) -> usize { + self.0 + } + } + }; +} +mod borrowed_sums; +mod lexical_borrows; +pub(crate) use lexical_borrows::LexicalScope; +pub use lexical_borrows::{ + ResourceLexicalExit, ResourceLexicalExitId, ResourceLexicalExitKind, ResourceLexicalExitPlan, +}; +#[cfg(test)] +mod borrowed_sums_tests; +pub(crate) mod constructors; +#[cfg(test)] +mod execution_frames_tests; +#[cfg(test)] +mod lexical_borrows_tests; +mod normalized_calls; +pub(crate) use borrowed_sums::BorrowedSumSeed; +pub use borrowed_sums::{ResourceBorrowedSumProjection, ResourceSumPayloadPath}; +pub use constructors::{ + ResourceConstructorField, ResourceConstructorId, ResourceConstructorNode, + ResourceConstructorRegion, +}; +#[cfg(test)] +mod normalized_calls_tests; +pub use normalized_calls::{ + ResourceCallActual, ResourceCallNode, ResourceCallRegion, ResourceCallRegionId, +}; +mod carriers; +pub use carriers::{ + ResourceCarrierChild, ResourceCarrierConstructor, ResourceCarrierEdge, ResourceCarrierField, + ResourceCarrierFunctionPlan, ResourceCarrierGeneration, ResourceCarrierIndex, + ResourceCarrierIteration, ResourceCarrierLoan, ResourceCarrierLoanId, + ResourceCarrierLoanSource, ResourceCarrierNode, ResourceCarrierObservation, + ResourceCarrierOperation, ResourceCarrierOperationId, ResourceCarrierOperationRole, + ResourceCarrierProjectionPath, ResourceCarrierShape, ResourceCarrierShapeId, + ResourceCarrierSlot, ResourceCarrierSlotId, ResourceCarrierState, ResourceCarrierValue, + ResourceCarrierVariant, +}; +#[cfg(test)] +mod carriers_tests; +mod companion; +mod control_flow; +#[cfg(test)] +mod control_flow_tests; +pub(crate) use companion::CompanionContext; +pub use companion::ResourceCompanionPlan; +mod entry_scope; +mod execution_closure; +pub use entry_scope::{ResourceEntryScopePlan, validate_resource_ownership_for_entry}; +mod named_callables; +mod returned_descriptors; +pub use named_callables::ResourceNamedCallableProof; +#[cfg(test)] +mod execution_closure_tests; +mod flow; +pub(crate) mod reflected_fields; +pub(super) use execution_closure::ResourceExecutionClosure; +#[cfg(test)] +mod original_calls_tests; +#[cfg(test)] +mod tests; +pub(crate) mod walk; + +identity!(ResourceFrameId); +identity!(ResourceOwnerSlotId); +identity!(ResourceLoanId); +identity!(ResourceOperationId); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourcePosition { + Statement(usize), + Terminator, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ResourceSite { + function: FunctionId, + block: BlockId, + position: ResourcePosition, +} +impl ResourceSite { + pub fn function(self) -> FunctionId { + self.function + } + pub fn block(self) -> BlockId { + self.block + } + pub fn position(self) -> ResourcePosition { + self.position + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResourceShape { + Plain { + kind: hir::ResourceKindRef, + }, + Optional { + kind: hir::ResourceKindRef, + }, + Result { + kind: hir::ResourceKindRef, + failure: TypeId, + }, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourcePath { + Some, + Ok, +} +impl ResourceShape { + pub fn kind(&self) -> &hir::ResourceKindRef { + match self { + Self::Plain { kind } | Self::Optional { kind } | Self::Result { kind, .. } => kind, + } + } + pub fn active_path(&self) -> &[ResourcePath] { + match self { + Self::Plain { .. } => &[], + Self::Optional { .. } => &[ResourcePath::Some], + Self::Result { .. } => &[ResourcePath::Ok], + } + } + fn conditional(&self) -> bool { + !matches!(self, Self::Plain { .. }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceFrameRole { + Scope, + Operation, + Return, +} +#[derive(Debug, Clone)] +pub struct ResourceFrame { + id: ResourceFrameId, + role: ResourceFrameRole, + site: ResourceSite, + parent: Option, + ordinal: usize, +} +impl ResourceFrame { + pub fn id(&self) -> ResourceFrameId { + self.id + } + pub fn role(&self) -> ResourceFrameRole { + self.role + } + pub fn site(&self) -> ResourceSite { + self.site + } + pub fn parent(&self) -> Option { + self.parent + } + /// Expression occurrence within the exact containing-function statement or terminator. + pub fn ordinal(&self) -> usize { + self.ordinal + } +} + +#[derive(Debug, Clone)] +pub enum ResourceSlotStorage { + Local { + header: Local, + }, + Expression { + site: ResourceSite, + ordinal: usize, + }, + Argument { + site: ResourceSite, + call_ordinal: usize, + parameter: usize, + }, + Return { + site: ResourceSite, + }, +} +#[derive(Debug, Clone)] +pub struct ResourceOwnerSlot { + id: ResourceOwnerSlotId, + frame: ResourceFrameId, + shape: ResourceShape, + storage: ResourceSlotStorage, +} +impl ResourceOwnerSlot { + pub fn id(&self) -> ResourceOwnerSlotId { + self.id + } + pub fn frame(&self) -> ResourceFrameId { + self.frame + } + pub fn shape(&self) -> &ResourceShape { + &self.shape + } + pub fn storage(&self) -> &ResourceSlotStorage { + &self.storage + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceLoanSource { + CarrierSumProjection { + operation: ResourceCarrierOperationId, + }, + Owner(ResourceOwnerSlotId), + IncomingViewFormal { + scope: ResourceFrameId, + parameter: usize, + }, + ProjectedSumPayload { + parent: ResourceLoanId, + tag: LocalId, + path: ResourceSumPayloadPath, + }, +} +impl ResourceLoanSource { + // Preserve every typed identity in the private loan-site map. LocalId does + // not need a global ordering contract to distinguish selecting tag sites. + fn ordering_key(self) -> (u8, usize, usize, u32, u8) { + match self { + Self::CarrierSumProjection { operation } => (3, operation.index(), 0, 0, 0), + Self::Owner(owner) => (0, owner.index(), 0, 0, 0), + Self::IncomingViewFormal { scope, parameter } => (1, scope.index(), parameter, 0, 0), + Self::ProjectedSumPayload { parent, tag, path } => ( + 2, + parent.index(), + 0, + tag.index(), + match path { + ResourceSumPayloadPath::OptionalSome => 0, + ResourceSumPayloadPath::ResultOk => 1, + }, + ), + } + } +} +impl PartialOrd for ResourceLoanSource { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for ResourceLoanSource { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.ordering_key().cmp(&other.ordering_key()) + } +} +#[derive(Debug, Clone)] +pub struct ResourceLoan { + id: ResourceLoanId, + frame: ResourceFrameId, + source: ResourceLoanSource, + shape: ResourceShape, + parameter: Option, +} +impl ResourceLoan { + pub fn shape(&self) -> &ResourceShape { + &self.shape + } + pub fn id(&self) -> ResourceLoanId { + self.id + } + pub fn frame(&self) -> ResourceFrameId { + self.frame + } + /// Sealed call parameter, independent of any reached invocation. + pub fn parameter(&self) -> Option { + self.parameter + } + pub fn source(&self) -> ResourceLoanSource { + self.source + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceOccupancy { + Occupied, + Conditional, + Empty, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceCompletion { + Normal, + Return, + Abort, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResourceCallOperand { + CarrierOwned { + parameter: usize, + slot: ResourceCarrierSlotId, + }, + CarrierBorrowed { + parameter: usize, + loan: ResourceCarrierLoanId, + }, + Ordinary { + parameter: usize, + ty: TypeId, + }, + Owned { + parameter: usize, + slot: ResourceOwnerSlotId, + }, + Borrowed { + parameter: usize, + loan: ResourceLoanId, + }, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceCallResult { + Carrier { slot: ResourceCarrierSlotId }, + Ordinary { ty: TypeId }, + Owned { slot: ResourceOwnerSlotId }, +} + +/// Readonly projection of the original checked Source tuple, not a minting API. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceArgumentSyntax { + Bare, + WrittenView, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceArgumentEffect { + Copy, + TransferOwned, + RelinquishOwned, + RetainBorrow, + ObserveData, +} +#[derive(Debug, Clone)] +pub struct ResourceCallFormal { + parameter: usize, + source_index: usize, + actual_type: TypeId, + parameter_type: TypeId, + syntax: ResourceArgumentSyntax, + effect: ResourceArgumentEffect, + access: ParamMode, +} +impl ResourceCallFormal { + pub fn parameter(&self) -> usize { + self.parameter + } + pub fn source_index(&self) -> usize { + self.source_index + } + pub fn actual_type(&self) -> TypeId { + self.actual_type + } + pub fn parameter_type(&self) -> TypeId { + self.parameter_type + } + pub fn syntax(&self) -> ResourceArgumentSyntax { + self.syntax + } + pub fn effect(&self) -> ResourceArgumentEffect { + self.effect + } + pub fn access(&self) -> ParamMode { + self.access + } + fn original(argument: &hir::ArgumentOwnership) -> Self { + use jett_typecheck::{ + CheckedCalleeAccess as A, CheckedCallerEffect as E, CheckedCallerSyntax as S, + }; + Self { + parameter: argument.parameter_index, + source_index: argument.source_index, + actual_type: argument.actual_type, + parameter_type: argument.parameter_type, + syntax: match argument.syntax { + S::Bare => ResourceArgumentSyntax::Bare, + S::WrittenView => ResourceArgumentSyntax::WrittenView, + }, + effect: match argument.effect { + E::Copy => ResourceArgumentEffect::Copy, + E::TransferOwned => ResourceArgumentEffect::TransferOwned, + E::RelinquishOwned => ResourceArgumentEffect::RelinquishOwned, + E::RetainBorrow => ResourceArgumentEffect::RetainBorrow, + E::ObserveData => ResourceArgumentEffect::ObserveData, + }, + access: match argument.callee_access { + A::Owned => ParamMode::Owned, + A::View => ParamMode::View, + }, + } + } +} + +/// Closed metadata roles. None is a public constructor for a validated plan. +#[derive(Debug, Clone)] +pub enum ResourceOperationRole { + Carrier { + operation: ResourceCarrierOperationId, + }, + BeginSourceFunction { + region: ResourceCallRegionId, + function: FunctionId, + source: hir::SourceCallOwnership, + formals: Vec, + evaluation_order: Vec, + operands: Vec, + result: ResourceCallResult, + }, + /// One installed Borrow authority row; execution activates it only at Stage. + PrepareSourceBorrow { + loan: ResourceLoanId, + parameter: usize, + }, + PrepareSourceSumBorrow { + loan: ResourceLoanId, + parameter: usize, + }, + StageSourceActual { + region: ResourceCallRegionId, + source_index: usize, + parameter: usize, + operand: ResourceCallOperand, + ordinary: Option, + }, + Acquire { + hook: hir::ResourceHookRef, + destination: ResourceOwnerSlotId, + }, + Transfer { + source: ResourceOwnerSlotId, + destination: ResourceOwnerSlotId, + }, + Borrow { + loan: ResourceLoanId, + }, + BorrowSum { + loan: ResourceLoanId, + }, + EndSumBorrow { + loan: ResourceLoanId, + }, + ObserveSumView { + source: ResourceLoanId, + target: LocalId, + }, + ProjectSumView { + source: ResourceLoanId, + destination: ResourceLoanId, + tag: LocalId, + path: ResourceSumPayloadPath, + }, + ReadFailureCompanion { + source: ResourceLoanId, + target: Local, + failure: TypeId, + tag: LocalId, + }, + BoundedBorrowUse { + loan: ResourceLoanId, + parameter: usize, + }, + EndBorrow { + loan: ResourceLoanId, + }, + InvokeHook { + hook: hir::ResourceHookRef, + source: hir::SourceCallOwnership, + evaluation_order: Vec, + operands: Vec, + result: ResourceCallResult, + }, + InvokeSourceFunction { + function: FunctionId, + formals: Vec, + source: hir::SourceCallOwnership, + evaluation_order: Vec, + operands: Vec, + result: ResourceCallResult, + }, + /// Move a sole plain payload into a fresh conditional shell. + SumAdopt { + source: ResourceOwnerSlotId, + destination: ResourceOwnerSlotId, + }, + CreateAbsentSum { + destination: ResourceOwnerSlotId, + }, + CreateFailureSum { + destination: ResourceOwnerSlotId, + failure: TypeId, + }, + TakeFailureCompanion { + source: ResourceOwnerSlotId, + target: Local, + failure: TypeId, + tag: LocalId, + }, + SumTake { + source: ResourceOwnerSlotId, + destination: ResourceOwnerSlotId, + tag: LocalId, + success: bool, + }, + Close { + hook: hir::ResourceHookRef, + source: ResourceOwnerSlotId, + }, + Replace { + destination: ResourceOwnerSlotId, + replacement: ResourceOwnerSlotId, + old: ResourceOccupancy, + }, + /// Exact successful mutable Local = Local reseat; no runtime effect or row. + SelfRebind { + slot: ResourceOwnerSlotId, + }, + /// Scope completion obligations are retired by the dynamic core log, never local-number order. + Drop { + source: ResourceOwnerSlotId, + occupancy: ResourceOccupancy, + }, + Complete { + outcome: ResourceCompletion, + }, + CompleteReturnAfterCleanup { + source: ResourceOwnerSlotId, + }, + Descriptor { + hook: hir::ResourceHookRef, + }, +} +#[derive(Debug, Clone)] +pub struct ResourceOperation { + id: ResourceOperationId, + frame: ResourceFrameId, + site: ResourceSite, + ordinal: usize, + role: ResourceOperationRole, + // Address identity is private, borrowed from this fresh plan's immutable Program. + expression: Option, + named_indirect: Option, + indirect_hook: Option, +} +impl ResourceOperation { + pub fn id(&self) -> ResourceOperationId { + self.id + } + pub fn frame(&self) -> ResourceFrameId { + self.frame + } + pub fn site(&self) -> ResourceSite { + self.site + } + pub fn ordinal(&self) -> usize { + self.ordinal + } + pub fn role(&self) -> &ResourceOperationRole { + &self.role + } + /// Separates constructor-captured expression roles from statement/terminator roles. + pub fn is_expression_operation(&self) -> bool { + self.expression.is_some() + } + /// Exact reached Indirect Source body selection, never target-name authority. + pub fn indirect_hook_target(&self) -> Option<&hir::ResourceHookRef> { + self.indirect_hook.as_ref() + } + pub fn named_indirect_target(&self) -> Option<&ResourceNamedCallableProof> { + self.named_indirect.as_ref() + } +} + +#[derive(Debug)] +pub struct ResourceFunctionPlan { + carriers: Option, + function: FunctionId, + identity: FunctionIdentity, + parameters: Vec, + return_type: TypeId, + frames: Vec, + slots: Vec, + loans: Vec, + operations: Vec, + execution_frames: Vec<(ResourceSite, ResourceFrameId)>, + lexical_exits: Vec, + named_callable_producers: Vec<(usize, ResourceNamedCallableProof)>, + named_callable_values: Vec<(usize, ResourceNamedCallableProof)>, + descriptor_return: Option, + descriptor_values: Vec<(usize, hir::ResourceHookRef)>, + descriptor_locals: Vec<(LocalId, hir::ResourceHookRef)>, +} +impl ResourceFunctionPlan { + pub fn carriers(&self) -> Option<&ResourceCarrierFunctionPlan> { + self.carriers.as_ref() + } + pub fn function(&self) -> FunctionId { + self.function + } + pub fn identity(&self) -> &FunctionIdentity { + &self.identity + } + pub fn parameters(&self) -> &[Param] { + &self.parameters + } + pub fn return_type(&self) -> TypeId { + self.return_type + } + pub fn root_scope(&self) -> &ResourceFrame { + &self.frames[0] + } + pub fn provisional_return(&self) -> Option<&ResourceFrame> { + self.root_scope() + .parent + .and_then(|id| self.frames.get(id.index())) + .filter(|frame| frame.role == ResourceFrameRole::Return) + } + pub fn entry(&self) -> BlockId { + self.frames[0].site.block + } + pub fn frames(&self) -> &[ResourceFrame] { + &self.frames + } + pub fn owner_slots(&self) -> &[ResourceOwnerSlot] { + &self.slots + } + pub fn loans(&self) -> &[ResourceLoan] { + &self.loans + } + pub fn operations(&self) -> &[ResourceOperation] { + &self.operations + } + /// Incoming execution frame at an exact reachable current site, captured + /// only after the custody CFG fixed point and original witness validate. + pub fn execution_frame( + &self, + block: BlockId, + position: ResourcePosition, + ) -> Option { + let site = ResourceSite { + function: self.function, + block, + position, + }; + self.execution_frames + .iter() + .find_map(|(current, frame)| (*current == site).then_some(*frame)) + } + /// Exact current occurrence, never a span/type/signature lookup. + pub fn operations_for_expression( + &self, + expression: &Expression, + ) -> impl Iterator { + let address = std::ptr::from_ref(expression).addr(); + self.operations + .iter() + .filter(move |operation| operation.expression == Some(address)) + } + /// Only an authenticated original FunctionRef producer at this exact current occurrence. + /// Exact retained hook Return summary, rechecked against current reachable returns. + pub fn descriptor_return(&self) -> Option<&hir::ResourceHookRef> { + self.descriptor_return.as_ref() + } + /// Installation-owned metadata at this exact current expression occurrence. + pub fn descriptor_value(&self, expression: &Expression) -> Option<&hir::ResourceHookRef> { + let address = std::ptr::from_ref(expression).addr(); + self.descriptor_values + .iter() + .find_map(|(current, hook)| (*current == address).then_some(hook)) + } + pub fn descriptor_local(&self, local: LocalId) -> Option<&hir::ResourceHookRef> { + self.descriptor_locals + .iter() + .find_map(|(current, hook)| (*current == local).then_some(hook)) + } + pub fn named_callable_producer( + &self, + expression: &Expression, + ) -> Option<&ResourceNamedCallableProof> { + let address = std::ptr::from_ref(expression).addr(); + self.named_callable_producers + .iter() + .find_map(|(current, proof)| (*current == address).then_some(proof)) + } + /// Exact original producer, immutable alias initializer or reached callee descriptor value. + /// Callable signatures alone never make a physical Resource owner. + pub fn named_callable_value( + &self, + expression: &Expression, + ) -> Option<&ResourceNamedCallableProof> { + let address = std::ptr::from_ref(expression).addr(); + self.named_callable_values + .iter() + .find_map(|(current, proof)| (*current == address).then_some(proof)) + } +} +#[derive(Debug)] +pub struct ResourceOwnershipPlan<'p> { + program: &'p Program, + types: &'p TypeInterner, + functions: Vec, + required_only_functions: Vec, + entry_scope: Option, +} +impl<'p> ResourceOwnershipPlan<'p> { + pub fn program(&self) -> &'p Program { + self.program + } + pub fn types(&self) -> &'p TypeInterner { + self.types + } + /// Classification under this immutable validated graph; no ordinary carrier authority. + pub fn type_requires_custody(&self, ty: TypeId) -> bool { + resource_type_pending(self.types, ty) + } + pub fn carrier_shapes(&self) -> &[ResourceCarrierShape] { + self.functions + .iter() + .find_map(|function| function.carriers.as_ref()) + .map_or(&[], ResourceCarrierFunctionPlan::shapes) + } + pub fn functions(&self) -> &[ResourceFunctionPlan] { + &self.functions + } + /// Wrapper-only Scope metadata; it grants no Source execution-family membership. + pub fn entry_scope(&self) -> Option<&ResourceEntryScopePlan> { + self.entry_scope.as_ref() + } + /// Exact checked helpers omitted only as implicit native roots. This grants + /// no runtime body, carrier or invocation authority for those functions. + pub fn required_only_function_ids(&self) -> &[FunctionId] { + &self.required_only_functions + } + pub fn original_source(&self, id: FunctionId) -> Option<&'p hir::Function> { + self.program + .functions + .get(id.index() as usize) + .filter(|function| function.id == id) + .and_then(|function| function.resource_lowering.as_ref()) + .and_then(|witness| { + witness + .source + .functions() + .get(id.index() as usize) + .filter(|function| function.id == id) + }) + } + pub fn function(&self, id: FunctionId) -> Option<&ResourceFunctionPlan> { + self.functions + .iter() + .find(|function| function.function == id) + } +} + +#[derive(Debug, Clone, PartialEq)] +struct OriginalCallAssociation { + original: Expression, + current: Expression, +} +impl OriginalCallAssociation { + fn same(&self, other: &Self) -> bool { + crate::breakpoint_regions::expressions_equal(&self.original, &other.original) + && crate::breakpoint_regions::expressions_equal(&self.current, &other.current) + } +} +#[derive(Debug, Clone, PartialEq)] +pub(super) struct ResourceLoweringWitness { + original: hir::Function, + calls: Vec, + regions: Vec, + constructors: Vec, + constructor_seal: std::sync::Arc<[ResourceConstructorRegion]>, + reflected_fields: Vec, + reflected_seal: Vec, + borrowed_sums: Vec, + borrowed_seal: Vec, + lexical_exits: Vec, + lexical_seal: Vec, + named_callables: Vec, + descriptors: returned_descriptors::DescriptorWitness, + source: hir::ResourceSourceArchive, + execution: ResourceExecutionClosure, + manifest: hir::ResourceManifest, + parameters: Vec, + locals: Vec, + blocks: Vec, + entry: BlockId, +} + +#[derive(Clone, Default)] +pub(super) struct Capture { + witness: Option, +} +impl Capture { + pub(super) fn site(&self, block: BlockId, index: usize) -> Option { + self.function().map(|function| ResourceSite { + function, + block, + position: ResourcePosition::Statement(index), + }) + } + pub(super) fn function(&self) -> Option { + self.witness.as_ref().map(|witness| witness.original.id) + } + /// Only the checked lowering entry calls this, before any operand extraction. + pub(super) fn authenticated( + function: &hir::Function, + manifest: &hir::ResourceManifest, + source: &hir::ResourceSourceArchive, + types: &TypeInterner, + execution: &ResourceExecutionClosure, + ) -> Self { + // A raw ordinary predicate has no Resource archive authority. + // Keep custody detection independent so malformed raw Resource bodies + // still follow the existing mandatory refusal path. + let needed = (source.manifest().is_some() + && function.identity.declaration.kind == hir::DeclarationKind::RefinementPredicate) + || source.has_reflected_field_dispatch(function.id) + || runtime_custody_needed(function, types, execution) + || source.required_only_function_ids().contains(&function.id) + || source + .required_materializations() + .iter() + .any(|row| row.function() == function.id) + || entry_scope::capture_needed(function, source); + Self { + witness: needed.then(|| ResourceLoweringWitness { + original: function.clone(), + calls: Vec::new(), + regions: Vec::new(), + constructors: Vec::new(), + constructor_seal: std::sync::Arc::from([]), + reflected_fields: Vec::new(), + reflected_seal: Vec::new(), + borrowed_sums: Vec::new(), + borrowed_seal: Vec::new(), + lexical_exits: Vec::new(), + lexical_seal: Vec::new(), + named_callables: named_callables::capture(function, source, types, execution), + descriptors: returned_descriptors::capture(function, source), + source: source.clone(), + execution: execution.clone(), + manifest: manifest.clone(), + parameters: function.params.clone(), + locals: function.locals.clone(), + blocks: vec![BasicBlock { + id: BlockId(0), + statements: Vec::new(), + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: function.body.span, + }, + }], + entry: BlockId(0), + }), + } + } + pub(super) fn preserve_original_call( + &mut self, + expression: &Expression, + types: &TypeInterner, + ) -> Result { + let Some(witness) = &self.witness else { + return Ok(false); + }; + let (args, ownership) = match &expression.kind { + hir::ExpressionKind::Call { + args, ownership, .. + } + | hir::ExpressionKind::IndirectCall { + args, ownership, .. + } + | hir::ExpressionKind::ResourceInvoke { + args, ownership, .. + } => (args, ownership), + _ => return Ok(false), + }; + let occupied = |ty| { + resource_type_pending(types, ty) && !matches!(types.resolve(ty), Type::Function { .. }) + }; + if !occupied(expression.ty) + && !args.iter().any(|value| occupied(value.ty)) + && !matches!(expression.kind, hir::ExpressionKind::ResourceInvoke { .. }) + && !witness.execution.direct_call(expression) + { + return Ok(false); + } + let hir::CallOwnership::Source(source) = ownership else { + return Err( + "Resource original call preservation requires exact Source authority".into(), + ); + }; + if source + .arguments + .iter() + .any(|argument| argument.staging != hir::ArgumentStaging::Original) + { + return Err("Resource original call preservation changed its initial staging".into()); + } + let mut handled = false; + for value in args { + walk::expression(value, &mut |value| { + handled |= matches!(value.kind, hir::ExpressionKind::Handle { .. }) + }); + } + if let hir::ExpressionKind::IndirectCall { callee, .. } = &expression.kind { + walk::expression(callee, &mut |value| { + handled |= matches!(value.kind, hir::ExpressionKind::Handle { .. }) + }); + } + if handled { + return Err("pending ResourceOwnershipPlan: eager handled actuals need the dedicated custody CFG normalizer".into()); + } + let mut occurrences = 0usize; + walk::hir_block(&witness.original.body, &mut |candidate| { + occurrences += usize::from(crate::breakpoint_regions::expressions_equal( + candidate, expression, + )); + }); + if occurrences != 1 { + return Err( + "Resource preservation has no unique exact original Source expression".into(), + ); + } + let mut candidates = Vec::new(); + walk::expression(expression, &mut |candidate| { + if source_custody_call(candidate, types, &witness.execution) { + candidates.push(candidate.clone()); + } + }); + let witness = self + .witness + .as_mut() + .ok_or("Resource association lost its initial witness")?; + for candidate in candidates { + let mut original_count = 0; + walk::hir_block(&witness.original.body, &mut |original| { + original_count += usize::from(crate::breakpoint_regions::expressions_equal( + original, &candidate, + )); + }); + if original_count != 1 { + return Err("Resource call association has no unique original Source tuple".into()); + } + if !witness.calls.iter().any(|record| { + crate::breakpoint_regions::expressions_equal(&record.current, &candidate) + }) { + witness.calls.push(OriginalCallAssociation { + original: candidate.clone(), + current: candidate, + }); + } + } + Ok(true) + } + pub(super) fn block(&mut self, block: BlockId, span: Span) { + if let Some(witness) = &mut self.witness { + witness.blocks.push(BasicBlock { + id: block, + statements: Vec::new(), + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span, + }, + }); + } + } + pub(super) fn local(&mut self, local: &Local) { + if let Some(witness) = &mut self.witness { + witness.locals.push(local.clone()); + } + } + pub(super) fn statement(&mut self, block: BlockId, index: usize, value: &Statement) { + if let Some(witness) = &mut self.witness { + if let Some(destination) = witness.blocks.get_mut(block.index() as usize) { + // Sequential constructor writes only; a mismatch remains unsealable. + if destination.id == block && destination.statements.len() == index { + destination.statements.push(value.clone()); + } + } + } + } + pub(super) fn terminator(&mut self, block: BlockId, value: &Terminator) { + if let Some(witness) = &mut self.witness { + if let Some(destination) = witness + .blocks + .get_mut(block.index() as usize) + .filter(|destination| destination.id == block) + { + destination.terminator = value.clone(); + } + } + } + pub(super) fn finish( + mut self, + function: &Function, + ) -> Result, String> { + if let Some(witness) = &mut self.witness { + witness.constructor_seal = witness.constructors.clone().into(); + lexical_borrows::seal(witness); + borrowed_sums::seal(witness); + reflected_fields::seal(witness); + } + if let Some(witness) = &mut self.witness + && (!witness.constructors.is_empty() + || !witness.borrowed_sums.is_empty() + || entry_scope::capture_needed(&witness.original, &witness.source)) + { + witness.descriptors.seal_body(function); + } + if let Some(witness) = &self.witness { + witness.current(function)?; + } + if let Some(witness) = &mut self.witness { + if witness + .source + .required_only_function_ids() + .contains(&function.id) + || !witness.constructors.is_empty() + || !witness.borrowed_sums.is_empty() + || entry_scope::capture_needed(&witness.original, &witness.source) + || witness + .source + .required_materializations() + .iter() + .any(|row| row.function() == function.id) + { + witness.descriptors.seal_body(function); + } else { + witness.descriptors.seal(function); + } + } + Ok(self.witness) + } +} +impl ResourceLoweringWitness { + fn current(&self, function: &Function) -> Result<(), String> { + if function.id != self.original.id + || function.identity != self.original.identity + || function.debug_kind != self.original.debug_kind + || function.params != self.parameters + || function.capture_count != self.original.capture_count + || function.return_type != self.original.return_type + || function.span != self.original.span + || function.entry != self.entry + || function.locals != self.locals + || !crate::breakpoint_regions::blocks_equal(&function.blocks, &self.blocks) + { + return Err("Resource ownership differs from its initially authenticated Source or constructor-emitted graph".into()); + } + self.descriptors.current(function)?; + if (!self.constructors.is_empty() + || !self.borrowed_sums.is_empty() + || entry_scope::capture_needed(&self.original, &self.source)) + && !self.descriptors.has_body() + { + return Err("Resource Source proof lost its independent constructor body seal".into()); + } + if !constructors::records_equal(&self.constructors, &self.constructor_seal) { + return Err( + "Resource constructor lost its independent original/current occurrence seal".into(), + ); + } + for constructor in &self.constructors { + constructor.current(function)?; + let mut occurrences = 0; + walk::hir_block(&self.original.body, &mut |value| { + occurrences += usize::from(crate::breakpoint_regions::expressions_equal( + value, + constructor.original(), + )) + }); + if occurrences != 1 { + return Err( + "Resource constructor lost its unique original checked occurrence".into(), + ); + } + } + borrowed_sums::current(self, function)?; + lexical_borrows::current(self, function)?; + reflected_fields::current(self, function)?; + for region in &self.regions { + region.current(function)?; + let mut original_count = 0; + walk::hir_block(&self.original.body, &mut |value| { + original_count += usize::from(crate::breakpoint_regions::expressions_equal( + value, + region.original(), + )) + }); + if original_count != 1 { + return Err( + "Resource normalized call lost its unique original checked Source occurrence" + .into(), + ); + } + } + for (index, record) in self.calls.iter().enumerate() { + if self.calls[..index].iter().any(|earlier| { + crate::breakpoint_regions::expressions_equal(&earlier.original, &record.original) + || crate::breakpoint_regions::expressions_equal( + &earlier.current, + &record.current, + ) + }) { + return Err("Resource original call association is duplicated".into()); + } + let (mut original_count, mut current_count) = (0usize, 0usize); + walk::hir_block(&self.original.body, &mut |value| { + original_count += usize::from(crate::breakpoint_regions::expressions_equal( + value, + &record.original, + )) + }); + for block in &self.blocks { + walk::mir_block(block, &mut |value| { + current_count += usize::from(crate::breakpoint_regions::expressions_equal( + value, + &record.current, + )) + }); + } + if original_count != 1 || current_count != 1 { + return Err("Resource original call association lost its unique source or current occurrence".into()); + } + } + Ok(()) + } + fn same(&self, other: &Self) -> bool { + self.original.id == other.original.id + && self.original.identity == other.original.identity + && self.original.source_definition == other.original.source_definition + && self.original.params == other.original.params + && self.original.locals == other.original.locals + && self.original.return_type == other.original.return_type + && self.original.capture_count == other.original.capture_count + && self.original.span == other.original.span + && self.original.debug_kind == other.original.debug_kind + && crate::breakpoint_regions::hir_blocks_equal( + &self.original.body, + &other.original.body, + ) + && self.calls.len() == other.calls.len() + && self + .calls + .iter() + .zip(&other.calls) + .all(|(left, right)| left.same(right)) + && self.reflected_fields == other.reflected_fields + && self.reflected_seal == other.reflected_seal + && constructors::records_equal(&self.constructors, &other.constructors) + && std::sync::Arc::ptr_eq(&self.constructor_seal, &other.constructor_seal) + && self.regions.len() == other.regions.len() + && self.borrowed_sums == other.borrowed_sums + && self.borrowed_seal == other.borrowed_seal + && self.lexical_exits == other.lexical_exits + && self.lexical_seal == other.lexical_seal + && self + .regions + .iter() + .zip(&other.regions) + .all(|(a, b)| a.same(b)) + && self.named_callables == other.named_callables + && self.descriptors == other.descriptors + && self.source == other.source + && self.execution == other.execution + && self.manifest == other.manifest + && self.parameters == other.parameters + && self.locals == other.locals + && self.entry == other.entry + && crate::breakpoint_regions::blocks_equal(&self.blocks, &other.blocks) + } +} + +fn original_function_equal(left: &hir::Function, right: &hir::Function) -> bool { + left.id == right.id + && left.identity == right.identity + && left.debug_kind == right.debug_kind + && left.source_definition == right.source_definition + && left.params == right.params + && left.capture_count == right.capture_count + && left.return_type == right.return_type + && left.locals == right.locals + && left.span == right.span + && crate::breakpoint_regions::hir_blocks_equal(&left.body, &right.body) +} +/// Executed before constructor capture; edited public HIR can never be re-sealed. +pub(super) fn authenticate_original( + program: &hir::Program, + types: &TypeInterner, +) -> Result { + let archive = &program.resource_source; + if archive.manifest().is_none() { + if !program.resource_manifest.kinds().is_empty() + || program.resource_manifest.hooks().next().is_some() + || program.functions.iter().any(|function| { + resource_type_pending(types, function.return_type) + || function + .locals + .iter() + .any(|local| resource_type_pending(types, local.ty)) + }) + { + return Err("Resource lowering has no original checked HIR archive".into()); + } + return Ok(ResourceExecutionClosure::default()); + } + archive.validate_types(types)?; + if archive.manifest() != Some(&program.resource_manifest) + || archive.equality_methods() != Some(&program.equality_methods) + || archive.execution_functions().len() != program.functions.len() + || !archive + .execution_functions() + .iter() + .zip(&program.functions) + .all(|(left, right)| original_function_equal(left, right)) + { + return Err("Resource lowering differs from its original checked HIR archive".into()); + } + ResourceExecutionClosure::from_archive(archive, types) +} + +pub(super) fn visit_expression(value: &Expression, visit: &mut impl FnMut(&Expression)) { + walk::expression(value, visit); +} + +fn custody_type(types: &TypeInterner, ty: TypeId) -> bool { + resource_type_pending(types, ty) && !matches!(types.resolve(ty), Type::Function { .. }) +} +/// The original runtime classification, independent of required-value records. +fn runtime_custody_needed( + function: &hir::Function, + types: &TypeInterner, + execution: &ResourceExecutionClosure, +) -> bool { + let mut needed = execution.contains(function.id) + || resource_type_pending(types, function.return_type) + || function + .locals + .iter() + .any(|local| resource_type_pending(types, local.ty)); + walk::hir_block(&function.body, &mut |value| { + needed |= matches!( + value.kind, + hir::ExpressionKind::ResourceHookValue { .. } + | hir::ExpressionKind::ResourceInvoke { .. } + ) || custody_type(types, value.ty); + }); + needed +} +/// A proof-only required row seals a body without granting Resource execution. +/// The immutable archived body and checked execution family select this role. +pub(super) fn has_execution_records(function: &Function, types: &TypeInterner) -> bool { + function + .resource_lowering + .as_ref() + .is_some_and(|witness| runtime_custody_needed(&witness.original, types, &witness.execution)) +} +fn source_custody_call( + value: &Expression, + types: &TypeInterner, + execution: &ResourceExecutionClosure, +) -> bool { + let (args, ownership) = match &value.kind { + hir::ExpressionKind::Call { + args, ownership, .. + } + | hir::ExpressionKind::IndirectCall { + args, ownership, .. + } + | hir::ExpressionKind::ResourceInvoke { + args, ownership, .. + } => (args, ownership), + _ => return false, + }; + matches!(ownership, hir::CallOwnership::Source(source) if source.arguments.iter().all(|argument| argument.staging == hir::ArgumentStaging::Original)) + && (custody_type(types, value.ty) + || args.iter().any(|arg| custody_type(types, arg.ty)) + || matches!(value.kind, hir::ExpressionKind::ResourceInvoke { .. }) + || execution.direct_call(value)) +} +/// A readonly association lookup, never a constructor or ordinary owner claim. +pub(super) fn original_call_at( + function: &Function, + manifest: &hir::ResourceManifest, + types: &TypeInterner, + expression: &Expression, + block: BlockId, + statement: usize, +) -> Result { + let Some(witness) = function.resource_lowering.as_ref() else { + return Ok(false); + }; + if !witness + .calls + .iter() + .any(|record| crate::breakpoint_regions::expressions_equal(&record.current, expression)) + { + return Ok(false); + } + witness.source.validate_types(types)?; + if witness.execution != ResourceExecutionClosure::from_archive(&witness.source, types)? { + return Err( + "Resource call execution closure differs from its original checked bodies".into(), + ); + } + witness.manifest.validate(types)?; + witness.current(function)?; + if &witness.manifest != manifest + || witness.source.manifest() != Some(manifest) + || !witness + .source + .execution_functions() + .iter() + .any(|original| original_function_equal(original, &witness.original)) + { + return Err( + "Resource call association differs from its original checked function or manifest" + .into(), + ); + } + let current = function + .blocks + .get(block.index() as usize) + .filter(|value| value.id == block) + .ok_or("Resource call association has no exact current block")?; + let mut pointers = 0; + walk::mir_site(current, statement, &mut |value| { + pointers += usize::from(std::ptr::eq(value, expression)) + }); + if pointers != 1 { + return Err( + "Resource call association differs from its exact current expression site".into(), + ); + } + Ok(true) +} +/// The finite connected actual shapes; descriptors and ordinary data remain ordinary. +pub(super) fn original_resource_actual( + manifest: &hir::ResourceManifest, + types: &TypeInterner, + ty: TypeId, +) -> bool { + match types.resolve(ty) { + Type::Resource(_) => manifest.contains_type(ty), + Type::Optional(inner) => { + matches!(types.resolve(*inner), Type::Resource(_)) && manifest.contains_type(*inner) + } + Type::Result(ok, fail) => { + matches!(types.resolve(*ok), Type::Resource(_)) + && manifest.contains_type(*ok) + && !resource_type_pending(types, *fail) + } + _ => false, + } +} + +pub(super) fn validate_witnesses( + program: &Program, + types: &TypeInterner, +) -> Result<(), Vec> { + let mut errors = Vec::new(); + let source = program + .functions + .iter() + .find_map(|function| function.resource_lowering.as_ref()) + .map(|witness| &witness.source); + let execution = program + .functions + .iter() + .find_map(|function| function.resource_lowering.as_ref()) + .map(|witness| { + witness.source.validate_types(types)?; + ResourceExecutionClosure::from_archive(&witness.source, types) + }) + .transpose() + .map_err(|message| { + vec![ValidationError { + span: program + .functions + .first() + .map_or(Span::new(jett_common::FileId::new(0), 0, 0), |function| { + function.span + }), + message, + }] + })? + .unwrap_or_default(); + for function in &program.functions { + let needed = execution.contains(function.id) + || source.is_some_and(|archive| { + archive + .execution_functions() + .iter() + .find(|original| original.id == function.id) + .is_some_and(|original| { + original.identity.declaration.kind + == hir::DeclarationKind::RefinementPredicate + || entry_scope::capture_needed(original, archive) + }) + }) + || source + .is_some_and(|archive| archive.required_only_function_ids().contains(&function.id)) + || source.is_some_and(|archive| { + archive + .required_materializations() + .iter() + .any(|row| row.function() == function.id) + }) + || resource_type_pending(types, function.return_type) + || function + .locals + .iter() + .any(|local| resource_type_pending(types, local.ty)) + || function.blocks.iter().any(|block| { + walk::mir_block_has_resource(block) + || walk::mir_block_has_custody(block, function, types) + }); + match &function.resource_lowering { + Some(witness) => { + let result = witness + .source + .validate_types(types) + .and_then(|()| witness.manifest.validate(types)) + .and_then(|()| { + if source != Some(&witness.source) { + return Err("Resource ownership mixes foreign original/materialized archives".into()); + } + if witness.execution != execution { + return Err("Resource execution closure differs from the original checked call graph".into()); + } + if witness.source.manifest() != Some(&witness.manifest) + || !witness.source.execution_functions().iter().any(|original| { + original_function_equal(original, &witness.original) + }) + { + return Err( + "Resource ownership lost its original checked HIR archive join" + .into(), + ); + } + if witness.manifest != program.resource_manifest { + return Err( + "Resource ownership witness belongs to another checked manifest" + .into(), + ); + } + witness.current(function)?; + named_callables::validate(witness, function, types)?; + returned_descriptors::validate(witness, function) + }); + if let Err(message) = result { + errors.push(ValidationError { + span: function.span, + message, + }); + } + } + None if needed => errors.push(ValidationError { + span: function.span, + message: + "Resource ownership has no initially authenticated Source constructor witness" + .into(), + }), + None => {} + } + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } +} + +/// Fresh typed/CFG proof. This is not a runtime registration or ABI admission. +pub fn validate_resource_ownership<'p>( + program: &'p Program, + types: &'p TypeInterner, +) -> Result, Vec> { + validate_call_ownership(program, types)?; + validate_witnesses(program, types)?; + let required_only_functions = program + .functions + .iter() + .find_map(|function| function.resource_lowering.as_ref()) + .map_or_else(Vec::new, |witness| { + witness.source.required_only_function_ids().to_vec() + }); + let mut functions = Vec::new(); + let mut errors = Vec::new(); + for function in &program.functions { + if !has_execution_records(function, types) || required_only_functions.contains(&function.id) + { + continue; + } + let analyzed = if carriers::required(function, types) { + carriers::analyze(program, function, types) + } else { + flow::analyze(program, function, types) + }; + match analyzed { + Ok(plan) => functions.push(plan), + Err(message) => errors.push(ValidationError { + span: function.span, + message, + }), + } + } + if errors.is_empty() { + Ok(ResourceOwnershipPlan { + program, + types, + functions, + required_only_functions, + entry_scope: None, + }) + } else { + Err(errors) + } +} + +fn shape( + manifest: &hir::ResourceManifest, + types: &TypeInterner, + ty: TypeId, +) -> Result, String> { + if ty.index() as usize >= types.len() { + return Err("Resource ownership type is outside the exact interner".into()); + } + let kind = |ty| { + manifest.kind_for_type(ty).ok_or_else(|| { + "Resource ownership kind has no original manifest declaration".to_string() + }) + }; + Ok(match types.resolve(ty) { + Type::Resource(_) => Some(ResourceShape::Plain { kind: kind(ty)? }), + Type::Optional(inner) if matches!(types.resolve(*inner), Type::Resource(_)) => Some(ResourceShape::Optional { kind: kind(*inner)? }), + Type::Result(ok, fail) if matches!(types.resolve(*ok), Type::Resource(_)) && !resource_type_pending(types, *fail) => Some(ResourceShape::Result { kind: kind(*ok)?, failure: *fail }), + Type::Function { .. } => None, // A signature is descriptor data, never occupied custody. + _ if resource_type_pending(types, ty) => return Err("pending ResourceOwnershipPlan: occupied Resource layout needs its dedicated aggregate or qualification transport".into()), + _ => None, + }) +} + +pub(super) fn has_records(function: &Function) -> bool { + function.resource_lowering.is_some() +} +pub(super) fn validate_current(function: &Function) -> Result<(), String> { + function + .resource_lowering + .as_ref() + .map_or(Ok(()), |witness| witness.current(function)) +} + +pub(super) use control_flow::sequence_transition; + +/// Only the canonical unreachable-block deletion supplies this exact map. +pub(super) fn remap_blocks( + function: &mut Function, + before: &Function, + map: &[Option], +) -> Result<(), String> { + let Some(original) = before.resource_lowering.as_ref() else { + return Ok(()); + }; + original.current(before)?; + let witness = function + .resource_lowering + .as_mut() + .ok_or("Resource canonical remap lost its constructor witness")?; + if !witness.same(original) { + return Err("Resource canonical remap changed its before-witness".into()); + } + witness.blocks.retain(|block| { + map.get(block.id.index() as usize) + .copied() + .flatten() + .is_some() + }); + let remap = |id: &mut BlockId| { + if let Some(mapped) = map.get(id.index() as usize).copied().flatten() { + *id = mapped; + } + }; + for region in &mut witness.regions { + region.remap_blocks(map)?; + } + for constructor in &mut witness.constructors { + constructor.remap_blocks(map)?; + } + let mut constructor_seal = witness.constructor_seal.to_vec(); + for constructor in &mut constructor_seal { + constructor.remap_blocks(map)?; + } + witness.constructor_seal = constructor_seal.into(); + borrowed_sums::remap_blocks(&mut witness.borrowed_sums, map)?; + borrowed_sums::seal(witness); + lexical_borrows::remap_blocks(witness, map)?; + reflected_fields::remap_blocks(witness, map)?; + returned_descriptors::remap_blocks(&mut witness.descriptors, map)?; + remap(&mut witness.entry); + for block in &mut witness.blocks { + remap(&mut block.id); + crate::sequences::prune::block_targets(&mut block.terminator.kind, &remap); + } + function + .resource_lowering + .as_ref() + .ok_or("Resource canonical remap lost its witness")? + .current(function) +} + +pub(super) fn remap_locals( + function: &mut Function, + before: &Function, + map: &[Option], +) -> Result<(), String> { + let Some(original) = before.resource_lowering.as_ref() else { + return Ok(()); + }; + original.current(before)?; + let witness = function + .resource_lowering + .as_mut() + .ok_or("Resource canonical local remap lost its constructor witness")?; + if !witness.same(original) { + return Err("Resource canonical local remap changed its before-witness".into()); + } + // Source IDs in original are archival. Only the independently captured current graph is remapped. + let mut valid = true; + let mut remap = |id: &mut LocalId| { + if let Some(mapped) = map.get(id.index() as usize).copied().flatten() { + *id = mapped; + } else { + valid = false; + } + }; + for parameter in &mut witness.parameters { + remap(&mut parameter.local); + } + witness.locals.retain(|local| { + map.get(local.id.index() as usize) + .copied() + .flatten() + .is_some() + }); + for local in &mut witness.locals { + remap(&mut local.id); + if let Some(source) = &mut local.view_source { + remap(source); + } + } + for block in &mut witness.blocks { + crate::sequences::prune::block_locals(block, &mut remap, &mut |_| {}); + } + for region in &mut witness.regions { + region.remap_locals(&mut remap); + } + for constructor in &mut witness.constructors { + constructor.remap_locals(&mut remap); + } + let mut constructor_seal = witness.constructor_seal.to_vec(); + for constructor in &mut constructor_seal { + constructor.remap_locals(&mut remap); + } + witness.constructor_seal = constructor_seal.into(); + borrowed_sums::remap_locals(&mut witness.borrowed_sums, &mut remap); + borrowed_sums::seal(witness); + named_callables::remap_locals(&mut witness.named_callables, &mut remap); + reflected_fields::remap_locals(witness, &mut remap); + returned_descriptors::remap_locals(&mut witness.descriptors, map, &mut remap); + for record in &mut witness.calls { + let mut block = BasicBlock { + id: BlockId(0), + statements: vec![Statement { + kind: StatementKind::Evaluate(record.current.clone()), + span: record.current.span, + }], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: record.current.span, + }, + }; + crate::sequences::prune::block_locals(&mut block, &mut remap, &mut |_| {}); + let StatementKind::Evaluate(value) = block.statements.remove(0).kind else { + return Err("Resource association metadata remap changed its wrapper".into()); + }; + record.current = value; + } + if !valid { + return Err("Resource canonical local remap lost an active current reference".into()); + } + function + .resource_lowering + .as_ref() + .ok_or("Resource canonical remap lost its witness")? + .current(function) +} diff --git a/crates/jett_mir/src/resource_ownership/named_callables.rs b/crates/jett_mir/src/resource_ownership/named_callables.rs new file mode 100644 index 00000000..fda1805b --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/named_callables.rs @@ -0,0 +1,451 @@ +//! Immutable named callable provenance, derived from the whole checked archive. +//! Descriptor storage stays ordinary; this proof only selects a Source body. +use super::*; +use hir::ExpressionKind as E; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResourceNamedCallableProof { + function: FunctionId, + signature_type: TypeId, + identity: FunctionIdentity, +} +impl ResourceNamedCallableProof { + pub fn function(&self) -> FunctionId { + self.function + } + pub fn signature_type(&self) -> TypeId { + self.signature_type + } + pub fn identity(&self) -> &FunctionIdentity { + &self.identity + } +} + +#[derive(Debug, Clone, PartialEq)] +pub(super) struct NamedCallableBinding { + original_header: Local, + original_value: Expression, + current_header: Local, + current_value: Expression, + parent: Option, + root: usize, + proof: ResourceNamedCallableProof, +} + +fn alias(value: &Expression) -> Option { + match &value.kind { + E::Local(local) => Some(*local), + E::Clone(inner) if inner.ty == value.ty && inner.span == value.span => alias(inner), + _ => None, + } +} + +fn target( + archive: &hir::ResourceSourceArchive, + function: FunctionId, + signature_type: TypeId, + types: &TypeInterner, +) -> Option { + let declaration = archive.functions().get(function.index() as usize)?; + let Type::Function { + params, + view_params, + return_type, + } = types.resolve(signature_type) + else { + return None; + }; + if declaration.id != function + || declaration.source_definition.is_none() + || !matches!(declaration.debug_kind, hir::FunctionDebugKind::Named(_)) + || declaration.identity.declaration.kind != hir::DeclarationKind::Function + || !declaration.identity.type_arguments.is_empty() + || !declaration.identity.scoped_type_bindings.is_empty() + || declaration.identity.specialization != Default::default() + || declaration.capture_count != 0 + || declaration.params.len() != params.len() + || view_params.len() != params.len() + || declaration.return_type != *return_type + || declaration + .params + .iter() + .zip(params) + .zip(view_params) + .any(|((parameter, ty), view)| { + parameter.ty != *ty || (parameter.mode == ParamMode::View) != *view + }) + { + return None; + } + Some(ResourceNamedCallableProof { + function, + signature_type, + identity: declaration.identity.clone(), + }) +} + +fn original_callee(value: &Expression) -> Option<&Expression> { + let E::IndirectCall { + callee, + ownership: hir::CallOwnership::Source(source), + .. + } = &value.kind + else { + return None; + }; + let hir::CallTarget::Indirect { signature_type } = source.target else { + return None; + }; + (callee.ty == signature_type + && source.bridge == hir::CallBridge::Direct + && source.generated_operands.is_empty() + && source + .arguments + .iter() + .all(|argument| argument.staging == hir::ArgumentStaging::Original)) + .then_some(callee) +} + +/// The first supported producer is an original root-scope immutable Let. +/// Unsupported producers/escapes retain the existing pending ABI refusal. +pub(super) fn capture( + original: &hir::Function, + archive: &hir::ResourceSourceArchive, + types: &TypeInterner, + execution: &ResourceExecutionClosure, +) -> Vec { + let mut bindings: Vec = Vec::new(); + let mut permitted = BTreeSet::new(); + for statement in &original.body.statements { + let hir::StatementKind::Let { local, value } = &statement.kind else { + continue; + }; + let Some(header) = original + .locals + .get(local.index() as usize) + .filter(|header| { + header.id == *local + && !header.mutable + && header.view_source.is_none() + && header.ty == value.ty + }) + else { + continue; + }; + let (proof, parent, root) = if let E::FunctionRef(function) = value.kind { + let Some(proof) = target(archive, function, value.ty, types) else { + continue; + }; + (proof, None, bindings.len()) + } else if let Some(source) = alias(value) { + let Some((index, parent)) = bindings + .iter() + .enumerate() + .find(|(_, binding)| binding.original_header.id == source) + else { + continue; + }; + if parent.proof.signature_type != value.ty { + continue; + } + walk::expression(value, &mut |leaf| { + if matches!(leaf.kind, E::Local(id) if id == source) { + permitted.insert(std::ptr::from_ref(leaf).addr()); + } + }); + (parent.proof.clone(), Some(index), parent.root) + } else { + continue; + }; + bindings.push(NamedCallableBinding { + original_header: header.clone(), + original_value: value.clone(), + current_header: header.clone(), + current_value: value.clone(), + parent, + root, + proof, + }); + } + let mut reached = BTreeSet::new(); + walk::hir_block(&original.body, &mut |value| { + if let Some(callee) = original_callee(value) + && let E::Local(local) = callee.kind + && let Some(binding) = bindings + .iter() + .find(|binding| binding.original_header.id == local) + && binding.proof.signature_type == callee.ty + && source_custody_call(value, types, execution) + { + permitted.insert(std::ptr::from_ref(callee).addr()); + reached.insert(binding.root); + } + }); + let mut escaped = BTreeSet::new(); + metadata_escapes(&original.body, &bindings, &mut escaped); + walk::hir_block(&original.body, &mut |value| { + for binding in &bindings { + let escapes = match &value.kind { + E::Local(local) => { + *local == binding.original_header.id + && !permitted.contains(&std::ptr::from_ref(value).addr()) + } + E::ClosureRef { captures, .. } => captures.contains(&binding.original_header.id), + _ => false, + }; + if escapes { + escaped.insert(binding.root); + } + } + }); + // Remap only private binding indexes. Archival Local/Function IDs stay exact. + let mut map = vec![None; bindings.len()]; + let mut next = 0; + for (index, binding) in bindings.iter().enumerate() { + if reached.contains(&binding.root) && !escaped.contains(&binding.root) { + map[index] = Some(next); + next += 1; + } + } + bindings + .into_iter() + .filter_map(|mut binding| { + binding.root = map[binding.root]?; + binding.parent = binding.parent.and_then(|parent| map[parent]); + Some(binding) + }) + .collect() +} + +fn header_same(original: &Local, current: &Local) -> bool { + let mut restored = current.clone(); + restored.id = original.id; + restored == *original +} + +pub(super) fn validate( + witness: &ResourceLoweringWitness, + function: &Function, + types: &TypeInterner, +) -> Result<(), String> { + let expected = capture( + &witness.original, + &witness.source, + types, + &witness.execution, + ); + if expected.len() != witness.named_callables.len() { + return Err("Resource named callable lost its original producer/use proof".into()); + } + for (binding, expected) in witness.named_callables.iter().zip(&expected) { + if binding.original_header != expected.original_header + || !crate::breakpoint_regions::expressions_equal( + &binding.original_value, + &expected.original_value, + ) + || binding.parent != expected.parent + || binding.root != expected.root + || binding.proof != expected.proof + || !header_same(&binding.original_header, &binding.current_header) + || function.local(binding.current_header.id) != Some(&binding.current_header) + { + return Err("Resource named callable differs from its exact archived declaration or immutable binding".into()); + } + let mut restored = binding.current_value.clone(); + let mut valid = true; + remap_expression(&mut restored, &mut |local| { + if let Some(original) = witness + .named_callables + .iter() + .find(|candidate| candidate.current_header.id == *local) + { + *local = original.original_header.id; + } else { + valid = false; + } + }); + if !valid + || !crate::breakpoint_regions::expressions_equal(&binding.original_value, &restored) + { + return Err("Resource named callable changed its exact original initializer".into()); + } + let count = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| { + matches!(&statement.kind, StatementKind::Let { local, value } + if *local == binding.current_header.id + && crate::breakpoint_regions::expressions_equal(value, &binding.current_value)) + }) + .count(); + if count != 1 { + return Err("Resource named callable has no unique current immutable producer".into()); + } + } + current_values(witness, function)?; + Ok(()) +} + +fn metadata_escapes( + block: &hir::Block, + bindings: &[NamedCallableBinding], + escaped: &mut BTreeSet, +) { + for statement in &block.statements { + let locals: &[LocalId] = match &statement.kind { + hir::StatementKind::Trace(local) => std::slice::from_ref(local), + hir::StatementKind::Breakpoint { bindings, .. } => bindings, + _ => &[], + }; + for binding in bindings { + if locals.contains(&binding.original_header.id) { + escaped.insert(binding.root); + } + } + match &statement.kind { + hir::StatementKind::If { + then_block, + else_block, + .. + } => { + metadata_escapes(then_block, bindings, escaped); + if let Some(block) = else_block { + metadata_escapes(block, bindings, escaped); + } + } + hir::StatementKind::While { body, .. } + | hir::StatementKind::For { body, .. } + | hir::StatementKind::Scope(body) => metadata_escapes(body, bindings, escaped), + hir::StatementKind::Match { arms, .. } => { + for arm in arms { + metadata_escapes(&arm.body, bindings, escaped); + } + } + hir::StatementKind::ReflectedTypeDispatch { arms, .. } => { + for arm in arms { + metadata_escapes(&arm.body, bindings, escaped); + } + } + _ => {} + } + } +} + +/// Original aliases/callees are rejoined to exact current occurrences, independently +/// of the constructor graph equality check. A copied/resealed graph adds no use role. +pub(super) fn current_values( + witness: &ResourceLoweringWitness, + function: &Function, +) -> Result, String> { + let mut values = BTreeMap::new(); + for binding in &witness.named_callables { + for statement in function.blocks.iter().flat_map(|block| &block.statements) { + if let StatementKind::Let { local, value } = &statement.kind + && *local == binding.current_header.id + && crate::breakpoint_regions::expressions_equal(value, &binding.current_value) + { + walk::expression(value, &mut |value| { + values.insert(std::ptr::from_ref(value).addr(), binding.proof.clone()); + }); + } + } + } + for block in &function.blocks { + walk::mir_block(block, &mut |value| { + if let E::IndirectCall { callee, .. } = &value.kind + && let E::Local(local) = callee.kind + && let Some(binding) = witness.named_callables.iter().find(|binding| binding.current_header.id == local) + && witness.calls.iter().any(|record| { + crate::breakpoint_regions::expressions_equal(&record.current, value) + && matches!(&record.original.kind, E::IndirectCall { callee, .. } if binding.original_callee_matches(callee)) + }) + { + values.insert(std::ptr::from_ref(callee.as_ref()).addr(), binding.proof.clone()); + } + }); + } + let mut escaped = false; + for block in &function.blocks { + walk::mir_block(block, &mut |value| { + for binding in &witness.named_callables { + escaped |= match &value.kind { + E::Local(local) => { + *local == binding.current_header.id + && !values.contains_key(&std::ptr::from_ref(value).addr()) + } + E::ClosureRef { captures, .. } => captures.contains(&binding.current_header.id), + _ => false, + }; + } + }); + for statement in &block.statements { + let locals: &[LocalId] = match &statement.kind { + StatementKind::Trace(local) => std::slice::from_ref(local), + StatementKind::Breakpoint { bindings, .. } => bindings, + _ => &[], + }; + escaped |= witness + .named_callables + .iter() + .any(|binding| locals.contains(&binding.current_header.id)); + } + } + if escaped { + return Err( + "Resource named callable has an unproved current escape or callee occurrence".into(), + ); + } + Ok(values.into_iter().collect()) +} + +pub(super) fn remap_locals( + bindings: &mut [NamedCallableBinding], + remap: &mut impl FnMut(&mut LocalId), +) { + for binding in bindings { + remap(&mut binding.current_header.id); + remap_expression(&mut binding.current_value, remap); + } +} + +fn remap_expression(value: &mut Expression, remap: &mut impl FnMut(&mut LocalId)) { + let mut wrapper = BasicBlock { + id: BlockId(0), + statements: vec![Statement { + kind: StatementKind::Evaluate(value.clone()), + span: value.span, + }], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: value.span, + }, + }; + crate::sequences::prune::block_locals(&mut wrapper, remap, &mut |_| {}); + if let StatementKind::Evaluate(remapped) = wrapper.statements.remove(0).kind { + *value = remapped; + } +} + +impl NamedCallableBinding { + pub(super) fn original_callee_matches(&self, callee: &Expression) -> bool { + callee.ty == self.proof.signature_type + && matches!(callee.kind, E::Local(local) if local == self.original_header.id) + } + pub(super) fn local(&self) -> LocalId { + self.current_header.id + } + pub(super) fn parent(&self) -> Option { + self.parent + } + pub(super) fn initializer(&self) -> &Expression { + &self.current_value + } + pub(super) fn proof(&self) -> &ResourceNamedCallableProof { + &self.proof + } +} + +#[cfg(test)] +#[path = "named_callables_tests.rs"] +mod tests; diff --git a/crates/jett_mir/src/resource_ownership/named_callables_tests.rs b/crates/jett_mir/src/resource_ownership/named_callables_tests.rs new file mode 100644 index 00000000..4f293dbe --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/named_callables_tests.rs @@ -0,0 +1,337 @@ +use super::*; +use crate::resource_ownership::tests::{checked, lowered}; + +const SOURCE: &str = r#"namespace app +function wrong_target(token: resource_probe.TestHandle) returns nothing: + return nothing +function close_selected(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +function scenario(view net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = close_selected + resource_probe.TestHandle token = resource_probe.create(view net, 701) handle error: + return nothing + dispose(token) + return nothing +"#; + +fn scenario(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "scenario" + }) + .unwrap() +} +fn scenario_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "scenario" + }) + .unwrap() +} +fn producer(function: &Function) -> &Expression { + let signature = function.resource_lowering.as_ref().unwrap().named_callables[0] + .proof + .signature_type(); + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + StatementKind::Let { value, .. } + if value.ty == signature && matches!(value.kind, E::FunctionRef(_)) => + { + Some(value) + } + _ => None, + }) + .unwrap() +} +fn indirect(function: &Function) -> &Expression { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + StatementKind::Evaluate(value) if matches!(value.kind, E::IndirectCall { .. }) => { + Some(value) + } + _ => None, + }) + .unwrap() +} +fn initializer_mut(function: &mut Function) -> &mut Expression { + function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + StatementKind::Let { value, .. } if matches!(value.kind, E::FunctionRef(_)) => { + Some(value) + } + _ => None, + }) + .unwrap() +} + +#[test] +fn resource_named_indirect_keeps_original_source_and_exact_producer_use() { + for release in [false, true] { + for selected in ["close_selected", "wrong_target"] { + let source = + SOURCE.replace("dispose = close_selected", &format!("dispose = {selected}")); + let original = checked(&source, release); + let types = &original.checked().interner; + let program = lowered(&original); + let function = scenario(&program); + let ownership = validate_resource_ownership(&program, types).unwrap(); + let plan = ownership.function(function.id).unwrap(); + let descriptor = producer(function); + let proof = plan.named_callable_producer(descriptor).unwrap(); + assert_eq!(proof.identity().declaration.name, selected); + assert_eq!( + proof.function(), + match descriptor.kind { + E::FunctionRef(id) => id, + _ => unreachable!(), + } + ); + assert_eq!(proof.signature_type(), descriptor.ty); + assert!(plan.named_callable_producer(&descriptor.clone()).is_none()); + assert_eq!(plan.named_callable_value(descriptor), Some(proof)); + assert!(plan.named_callable_value(&descriptor.clone()).is_none()); + let call = indirect(function); + let operation = plan + .operations_for_expression(call) + .find(|operation| operation.named_indirect_target().is_some()) + .unwrap(); + assert_eq!(operation.named_indirect_target(), Some(proof)); + let ResourceOperationRole::InvokeSourceFunction { + function: target, + source, + evaluation_order, + .. + } = operation.role() + else { + panic!("selected Source operation"); + }; + assert_eq!(*target, proof.function()); + assert_eq!( + source.target, + hir::CallTarget::Indirect { + signature_type: proof.signature_type() + } + ); + assert_eq!(source.bridge, hir::CallBridge::Direct); + assert_eq!(evaluation_order, &[0]); + assert!( + plan.operations_for_expression(&call.clone()) + .next() + .is_none() + ); + let E::IndirectCall { callee, .. } = &call.kind else { + unreachable!() + }; + assert_eq!(plan.named_callable_value(callee), Some(proof)); + assert!( + plan.named_callable_value(&callee.as_ref().clone()) + .is_none() + ); + let companion = ResourceCompanionPlan::analyze(&ownership, function.id).unwrap(); + assert!(companion.storage().temporary_slots >= 2); + for binding in &function.resource_lowering.as_ref().unwrap().named_callables { + assert!( + companion + .storage() + .owned_locals + .contains(&(binding.local().index() as usize)) + ); + } + } + } +} + +#[test] +fn resource_named_indirect_retains_immutable_alias_chain_and_canonical_local_maps() { + let source = SOURCE.replace( + " function(resource_probe.TestHandle) returns nothing dispose = close_selected", + " function(int64) returns int64 unused_callback = function(ignored: int64) returns int64: return ignored\n function(resource_probe.TestHandle) returns nothing selected = close_selected\n function(resource_probe.TestHandle) returns nothing dispose = selected", + ); + for release in [false, true] { + let original = checked(&source, release); + let types = &original.checked().interner; + let mut program = lowered(&original); + let before = scenario(&program) + .resource_lowering + .as_ref() + .unwrap() + .named_callables + .clone(); + assert_eq!(before.len(), 2); + crate::prepare_native_generated_functions(&mut program, types); + let function = scenario(&program); + let after = &function.resource_lowering.as_ref().unwrap().named_callables; + assert_eq!(after[0].original_header, before[0].original_header); + assert_eq!(after[1].original_header, before[1].original_header); + assert!(after[0].current_header.id.index() < before[0].current_header.id.index()); + assert!(after[1].current_header.id.index() < before[1].current_header.id.index()); + let ownership = validate_resource_ownership(&program, types).unwrap(); + let plan = ownership.function(function.id).unwrap(); + let proof = plan.named_callable_producer(producer(function)).unwrap(); + assert_eq!(proof.identity().declaration.name, "close_selected"); + assert_eq!( + plan.operations_for_expression(indirect(function)) + .filter(|operation| operation.named_indirect_target() == Some(proof)) + .count(), + 1 + ); + let companion = ResourceCompanionPlan::analyze(&ownership, function.id).unwrap(); + for binding in after { + assert!( + companion + .storage() + .owned_locals + .contains(&(binding.local().index() as usize)) + ); + } + } +} + +#[test] +fn resource_named_indirect_rejects_target_initializer_binding_and_reseal_swaps() { + for release in [false, true] { + let original = checked(SOURCE, release); + let types = &original.checked().interner; + let program = lowered(&original); + let wrong = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "wrong_target") + .unwrap() + .id; + for mutation in 0..7 { + let mut changed = program.clone(); + let function = scenario_mut(&mut changed); + match mutation { + 0 => initializer_mut(function).kind = E::FunctionRef(wrong), + 1 => { + function.resource_lowering.as_mut().unwrap().named_callables[0] + .proof + .function = wrong + } + 2 => { + function.resource_lowering.as_mut().unwrap().named_callables[0] + .current_header + .mutable = true + } + 3 => { + function.resource_lowering.as_mut().unwrap().named_callables[0] + .current_header + .id = function.params[0].local + } + 4 | 5 => { + initializer_mut(function).kind = E::FunctionRef(wrong); + let blocks = function.blocks.clone(); + let changed_initializer = producer(function).clone(); + let witness = function.resource_lowering.as_mut().unwrap(); + witness.blocks = blocks; + if mutation == 5 { + witness.named_callables[0].current_value = changed_initializer; + } + } + 6 => { + let binding = function.resource_lowering.as_ref().unwrap().named_callables[0] + .current_header + .clone(); + function.blocks[0].statements.push(Statement { + kind: StatementKind::Evaluate(Expression { + kind: E::Local(binding.id), + ty: binding.ty, + span: binding.span, + }), + span: binding.span, + }); + let blocks = function.blocks.clone(); + function.resource_lowering.as_mut().unwrap().blocks = blocks; + } + _ => unreachable!(), + } + assert!( + validate_resource_ownership(&changed, types).is_err(), + "mutation {mutation}" + ); + } + let mut changed_hir = hir::lower_checked_resource_program(&original).unwrap(); + let caller = changed_hir + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "scenario") + .unwrap(); + let initializer = caller + .body + .statements + .iter_mut() + .find_map(|statement| match &mut statement.kind { + hir::StatementKind::Let { value, .. } + if matches!(value.kind, E::FunctionRef(_)) => + { + Some(value) + } + _ => None, + }) + .unwrap(); + initializer.kind = E::FunctionRef(wrong); + assert!(lower(&changed_hir, types).is_err()); + } +} + +#[test] +fn resource_named_indirect_does_not_mint_for_mutable_or_escaped_original_binding() { + for release in [false, true] { + let original = checked(SOURCE, release); + let types = &original.checked().interner; + let mut hir = hir::lower_checked_resource_program(&original).unwrap(); + let execution = authenticate_original(&hir, types).unwrap(); + let caller = hir + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "scenario") + .unwrap(); + let binding = caller + .locals + .iter_mut() + .find(|local| local.name == "dispose") + .unwrap(); + binding.mutable = true; + assert!(capture(caller, &hir.resource_source, types, &execution).is_empty()); + let mut hir = hir::lower_checked_resource_program(&original).unwrap(); + let caller = hir + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "scenario") + .unwrap(); + let binding = caller + .locals + .iter() + .find(|local| local.name == "dispose") + .unwrap() + .clone(); + caller.body.statements.push(hir::Statement { + kind: hir::StatementKind::Return(Some(Expression { + kind: E::Local(binding.id), + ty: binding.ty, + span: binding.span, + })), + span: binding.span, + }); + assert!(capture(caller, &hir.resource_source, types, &execution).is_empty()); + } +} diff --git a/crates/jett_mir/src/resource_ownership/normalized_calls.rs b/crates/jett_mir/src/resource_ownership/normalized_calls.rs new file mode 100644 index 00000000..2149d0b1 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/normalized_calls.rs @@ -0,0 +1,439 @@ +//! Constructor-only authority for a Source call whose actuals span CFG blocks. +use super::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ResourceCallRegionId(usize); +impl ResourceCallRegionId { + pub fn index(self) -> usize { + self.0 + } +} + +/// Public syntax is inert without the exact private constructor witness. +#[derive(Debug, Clone, PartialEq)] +pub enum ResourceCallNode { + Begin { + region: ResourceCallRegionId, + }, + Stage { + region: ResourceCallRegionId, + source_index: usize, + parameter: usize, + value: Expression, + ordinary: Option, + }, + Invoke { + region: ResourceCallRegionId, + output: LocalId, + }, + End { + region: ResourceCallRegionId, + outcome: ResourceCompletion, + }, +} +impl ResourceCallNode { + pub fn region(&self) -> ResourceCallRegionId { + match self { + Self::Begin { region } + | Self::Stage { region, .. } + | Self::Invoke { region, .. } + | Self::End { region, .. } => *region, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ResourceCallActual { + source_index: usize, + parameter: usize, + original: Expression, + endpoint: Expression, + start: ResourceSite, + stage: ResourceSite, + locals: Vec, + ordinary: Option, +} +impl ResourceCallActual { + pub fn source_index(&self) -> usize { + self.source_index + } + pub fn parameter(&self) -> usize { + self.parameter + } + pub fn original(&self) -> &Expression { + &self.original + } + pub fn endpoint(&self) -> &Expression { + &self.endpoint + } + pub fn start(&self) -> ResourceSite { + self.start + } + pub fn stage(&self) -> ResourceSite { + self.stage + } + pub fn locals(&self) -> &[Local] { + &self.locals + } + pub fn ordinary(&self) -> Option { + self.ordinary + } +} + +/// Archival originals and current emitted endpoints intentionally have separate IDs. +#[derive(Debug, Clone, PartialEq)] +pub struct ResourceCallRegion { + id: ResourceCallRegionId, + original: Expression, + source: hir::SourceCallOwnership, + function: FunctionId, + order: Vec, + parent: Option, + begin: ResourceSite, + actuals: Vec, + invocation: Option<(ResourceSite, LocalId)>, + exits: Vec<(ResourceSite, ResourceCompletion)>, +} +impl ResourceCallRegion { + pub fn id(&self) -> ResourceCallRegionId { + self.id + } + pub fn original(&self) -> &Expression { + &self.original + } + pub fn source(&self) -> &hir::SourceCallOwnership { + &self.source + } + pub fn function(&self) -> FunctionId { + self.function + } + pub fn evaluation_order(&self) -> &[usize] { + &self.order + } + pub fn parent(&self) -> Option { + self.parent + } + pub fn begin(&self) -> ResourceSite { + self.begin + } + pub fn actuals(&self) -> &[ResourceCallActual] { + &self.actuals + } + pub fn invocation(&self) -> Option<(ResourceSite, LocalId)> { + self.invocation + } + pub fn output(&self) -> Option { + self.invocation.map(|(_, output)| output) + } + pub fn ordinary(&self, parameter: usize) -> Option { + self.actuals + .iter() + .find(|actual| actual.parameter == parameter) + .and_then(|actual| actual.ordinary) + } + pub fn exits(&self) -> &[(ResourceSite, ResourceCompletion)] { + &self.exits + } + pub(super) fn same(&self, other: &Self) -> bool { + self.id == other.id + && self.source == other.source + && self.function == other.function + && self.order == other.order + && self.parent == other.parent + && self.begin == other.begin + && self.invocation == other.invocation + && self.exits == other.exits + && crate::breakpoint_regions::expressions_equal(&self.original, &other.original) + && self.actuals.len() == other.actuals.len() + && self.actuals.iter().zip(&other.actuals).all(|(a, b)| { + a.source_index == b.source_index + && a.parameter == b.parameter + && a.start == b.start + && a.stage == b.stage + && a.locals == b.locals + && a.ordinary == b.ordinary + && crate::breakpoint_regions::expressions_equal(&a.original, &b.original) + && crate::breakpoint_regions::expressions_equal(&a.endpoint, &b.endpoint) + }) + } + pub(super) fn current(&self, function: &Function) -> Result<(), String> { + let node = |site: ResourceSite| -> Result<&ResourceCallNode, String> { + let ResourcePosition::Statement(index) = site.position else { + return Err("Resource call region site is not a statement".into()); + }; + let statement = function + .blocks + .get(site.block.index() as usize) + .filter(|block| block.id == site.block) + .and_then(|block| block.statements.get(index)) + .ok_or("Resource call region site is missing")?; + let StatementKind::ResourceCall(node) = &statement.kind else { + return Err("Resource call region site changed its typed node".into()); + }; + if site.function != function.id || node.region() != self.id { + return Err("Resource call region changed its function or opaque reference".into()); + } + Ok(node) + }; + if !matches!(node(self.begin)?, ResourceCallNode::Begin { .. }) + || self.order.len() != self.source.arguments.len() + || self.actuals.len() != self.order.len() + { + return Err( + "Resource call region lost its exact begin or complete Source tuple".into(), + ); + } + for (source_index, actual) in self.actuals.iter().enumerate() { + if actual.source_index != source_index + || self.order[source_index] != actual.parameter + || !matches!(node(actual.stage)?, ResourceCallNode::Stage { source_index: index, parameter, value, ordinary, .. } + if *index == source_index && *parameter == actual.parameter && *ordinary == actual.ordinary + && crate::breakpoint_regions::expressions_equal(value, &actual.endpoint)) + || actual + .locals + .iter() + .any(|header| function.local(header.id) != Some(header)) + { + return Err( + "Resource call actual lost its lexical endpoint or generated headers".into(), + ); + } + } + let (site, output) = self + .invocation + .ok_or("Resource call region has no exact invocation")?; + if !matches!(node(site)?, ResourceCallNode::Invoke { output: found, .. } if *found == output) + || function.local(output).map(|local| local.ty) != Some(self.original.ty) + { + return Err("Resource call region changed its invocation output".into()); + } + for (site, outcome) in &self.exits { + if !matches!(node(*site)?, ResourceCallNode::End { outcome: found, .. } if found == outcome) + { + return Err("Resource call region lost an exact completion site".into()); + } + } + let count = function.blocks.iter().flat_map(|block| &block.statements) + .filter(|statement| matches!(&statement.kind, StatementKind::ResourceCall(value) if value.region() == self.id)).count(); + if count != 2 + self.actuals.len() + self.exits.len() + || !self + .exits + .iter() + .any(|(_, outcome)| *outcome == ResourceCompletion::Normal) + { + return Err( + "Resource call region has copied, disconnected or missing typed nodes".into(), + ); + } + Ok(()) + } + pub(super) fn shift_prefix(&mut self, block: BlockId, count: usize) { + let shift = |site: &mut ResourceSite| { + if site.block == block + && let ResourcePosition::Statement(index) = &mut site.position + { + *index += count; + } + }; + shift(&mut self.begin); + for actual in &mut self.actuals { + shift(&mut actual.start); + shift(&mut actual.stage); + } + if let Some((site, _)) = &mut self.invocation { + shift(site); + } + for (site, _) in &mut self.exits { + shift(site); + } + } + pub(super) fn remap_blocks(&mut self, map: &[Option]) -> Result<(), String> { + let remap = |site: &mut ResourceSite| -> Result<(), String> { + site.block = map.get(site.block.index() as usize).copied().flatten() + .ok_or("pending Resource call region: canonical selection removed a sealed evaluation or completion site")?; + Ok(()) + }; + remap(&mut self.begin)?; + for actual in &mut self.actuals { + remap(&mut actual.start)?; + remap(&mut actual.stage)?; + } + if let Some((site, _)) = &mut self.invocation { + remap(site)?; + } + for (site, _) in &mut self.exits { + remap(site)?; + } + Ok(()) + } + pub(super) fn remap_locals(&mut self, remap: &mut impl FnMut(&mut LocalId)) { + for actual in &mut self.actuals { + for header in &mut actual.locals { + remap(&mut header.id); + if let Some(source) = &mut header.view_source { + remap(source); + } + } + if let Some(local) = &mut actual.ordinary { + remap(local); + } + let mut wrapper = BasicBlock { + id: BlockId(0), + statements: vec![Statement { + kind: StatementKind::Evaluate(actual.endpoint.clone()), + span: actual.endpoint.span, + }], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: actual.endpoint.span, + }, + }; + crate::sequences::prune::block_locals(&mut wrapper, remap, &mut |_| {}); + if let StatementKind::Evaluate(endpoint) = wrapper.statements.remove(0).kind { + actual.endpoint = endpoint; + } + } + if let Some((_, output)) = &mut self.invocation { + remap(output); + } + } +} + +impl Capture { + pub(crate) fn normalized_call_needed(&self, value: &Expression, types: &TypeInterner) -> bool { + self.witness.as_ref().is_some_and(|witness| { + source_custody_call(value, types, &witness.execution) && { + let mut handled = false; + walk::expression(value, &mut |node| { + handled |= matches!(node.kind, hir::ExpressionKind::Handle { .. }) + }); + handled + } + }) + } + pub(crate) fn begin_normalized_call( + &mut self, + original: &Expression, + parent: Option, + begin: ResourceSite, + types: &TypeInterner, + ) -> Result { + let witness = self + .witness + .as_mut() + .ok_or("Resource call region lacks initial Source authentication")?; + witness.source.validate_types(types)?; + let hir::ExpressionKind::Call { + function, + args, + evaluation_order, + ownership: hir::CallOwnership::Source(source), + } = &original.kind + else { + return Err("pending Resource call region: indirect/hook handled custody calls require their separate invocation proof".into()); + }; + let mut occurrences = 0; + walk::hir_block(&witness.original.body, &mut |value| { + occurrences += usize::from(crate::breakpoint_regions::expressions_equal( + value, original, + )) + }); + if occurrences != 1 + || source.arguments.len() != args.len() + || source + .arguments + .iter() + .any(|fact| fact.staging != hir::ArgumentStaging::Original) + || evaluation_order.len() != args.len() + || evaluation_order + .iter() + .copied() + .collect::>() + .len() + != args.len() + || evaluation_order + .iter() + .any(|parameter| *parameter >= args.len()) + { + return Err( + "Resource call region differs from its unique complete original Source call".into(), + ); + } + let id = ResourceCallRegionId(witness.regions.len()); + if parent.is_some_and(|parent| parent.index() >= id.index()) { + return Err("Resource call region has no constructor-owned parent".into()); + } + witness.regions.push(ResourceCallRegion { + id, + original: original.clone(), + source: source.clone(), + function: *function, + order: evaluation_order.clone(), + parent, + begin, + actuals: Vec::new(), + invocation: None, + exits: Vec::new(), + }); + Ok(id) + } + pub(crate) fn normalized_actual( + &mut self, + region: ResourceCallRegionId, + source_index: usize, + parameter: usize, + original: &Expression, + endpoint: &Expression, + start: ResourceSite, + stage: ResourceSite, + locals: &[Local], + ordinary: Option, + ) -> Result<(), String> { + let record = self + .witness + .as_mut() + .and_then(|witness| witness.regions.get_mut(region.index())) + .ok_or("Resource actual lost its constructor region")?; + if record.actuals.len() != source_index + || record.order.get(source_index) != Some(¶meter) + { + return Err("Resource actual changed its exact lexical occurrence".into()); + } + record.actuals.push(ResourceCallActual { + source_index, + parameter, + original: original.clone(), + endpoint: endpoint.clone(), + start, + stage, + locals: locals.to_vec(), + ordinary, + }); + Ok(()) + } + pub(crate) fn normalized_node(&mut self, site: ResourceSite, node: &ResourceCallNode) { + let Some(record) = self + .witness + .as_mut() + .and_then(|witness| witness.regions.get_mut(node.region().index())) + else { + return; + }; + match node { + ResourceCallNode::Invoke { output, .. } => record.invocation = Some((site, *output)), + ResourceCallNode::End { outcome, .. } => record.exits.push((site, *outcome)), + ResourceCallNode::Begin { .. } | ResourceCallNode::Stage { .. } => {} + } + } +} + +impl Function { + /// Validity is granted only by fresh whole-program ownership validation. + pub fn resource_call_region(&self, id: ResourceCallRegionId) -> Option<&ResourceCallRegion> { + self.resource_lowering + .as_ref()? + .regions + .get(id.index()) + .filter(|region| region.id == id) + } +} diff --git a/crates/jett_mir/src/resource_ownership/normalized_calls_tests.rs b/crates/jett_mir/src/resource_ownership/normalized_calls_tests.rs new file mode 100644 index 00000000..02140647 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/normalized_calls_tests.rs @@ -0,0 +1,511 @@ +use super::tests::{checked, lowered}; +use super::*; +use hir::ExpressionKind as E; + +const ORIGINAL: &str = include_str!( + "../../../jett_comptime/src/resource_execution/fixtures/05_later_named_argument_failure_v2.jett" +); +const MINIMAL: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/09_later_handle_minimal.jett" +); +const SUCCESS: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/11_later_handle_success.jett" +); +const RETAINED: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/12_later_handle_written_view.jett" +); +const FIRST: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/13_first_handle_failure.jett" +); + +fn outer(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function + .resource_lowering + .as_ref() + .is_some_and(|witness| !witness.regions.is_empty()) + }) + .expect("one authenticated outer call producer") +} + +#[test] +fn normalized_source_call_preserves_original_tuple_and_constructor_cfg_in_both_profiles() { + for release in [false, true] { + for source in [ORIGINAL, MINIMAL, SUCCESS, RETAINED, FIRST] { + let checked = checked(source, release); + let types = &checked.checked().interner; + let hir = hir::lower_checked_resource_program(&checked).unwrap(); + let mut originals = Vec::new(); + for function in &hir.functions { + walk::hir_block(&function.body, &mut |value| { + if let E::Call { + function: target, + evaluation_order, + ownership: hir::CallOwnership::Source(source), + .. + } = &value.kind + && hir.functions[target.index() as usize] + .identity + .declaration + .name + == "accept_staged" + { + assert_eq!(evaluation_order, &[1, 0, 2]); + assert!( + source + .arguments + .iter() + .all(|fact| fact.staging == hir::ArgumentStaging::Original) + ); + originals.push(value.clone()); + } + }); + } + assert_eq!(originals.len(), 1); + let mir = lower(&hir, types).unwrap(); + let function = outer(&mir); + let witness = function.resource_lowering.as_ref().unwrap(); + assert_eq!(witness.regions.len(), 1); + let region = &witness.regions[0]; + assert!(crate::breakpoint_regions::expressions_equal( + region.original(), + &originals[0] + )); + assert_eq!(region.evaluation_order(), &[1, 0, 2]); + assert_eq!( + region + .actuals() + .iter() + .map(ResourceCallActual::parameter) + .collect::>(), + [1, 0, 2] + ); + assert!(region.actuals()[0].ordinary().is_none()); + assert!(region.actuals()[1].ordinary().is_some()); + assert!(region.actuals()[2].ordinary().is_some()); + assert!( + !function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!(statement.kind, StatementKind::BeginCallView { .. })) + ); + let plan = validate_resource_ownership(&mir, types).unwrap(); + ResourceCompanionPlan::analyze(&plan, function.id).unwrap(); + let flow = plan.function(function.id).unwrap(); + let begins = flow + .operations() + .iter() + .filter(|operation| { + matches!( + operation.role(), + ResourceOperationRole::BeginSourceFunction { .. } + ) + }) + .collect::>(); + assert_eq!(begins.len(), 1); + let frame = begins[0].frame(); + assert_eq!( + flow.frames()[frame.index()].parent(), + Some(ResourceFrameId(0)) + ); + let stages = flow + .operations() + .iter() + .filter(|operation| { + matches!( + operation.role(), + ResourceOperationRole::StageSourceActual { .. } + ) + }) + .collect::>(); + assert_eq!(stages.len(), 3); + assert!(stages.iter().all( + |operation| operation.frame() == frame && !operation.is_expression_operation() + )); + assert_eq!( + flow.operations() + .iter() + .filter(|operation| operation.frame() == frame + && matches!( + operation.role(), + ResourceOperationRole::InvokeSourceFunction { .. } + )) + .count(), + 1 + ); + let prepared = flow + .operations() + .iter() + .filter_map(|operation| match operation.role() { + ResourceOperationRole::PrepareSourceBorrow { loan, parameter } => { + Some((*loan, *parameter)) + } + _ => None, + }) + .collect::>(); + assert_eq!(prepared.len(), 1); + assert_eq!(prepared[0].1, 1); + assert_eq!(flow.loans()[prepared[0].0.index()].parameter(), Some(1)); + for (site, outcome) in region.exits() { + if *outcome != ResourceCompletion::Abort { + continue; + } + let end = flow + .operations() + .iter() + .find(|operation| { + operation.site() == *site + && operation.frame() == frame + && matches!( + operation.role(), + ResourceOperationRole::Complete { + outcome: ResourceCompletion::Abort + } + ) + }) + .unwrap(); + assert!(!end.is_expression_operation()); + let ResourcePosition::Statement(index) = site.position() else { + panic!("typed abandoned statement"); + }; + assert!(matches!( + function.blocks[site.block().index() as usize] + .terminator + .kind, + TerminatorKind::Return(_) + )); + assert!( + index + < function.blocks[site.block().index() as usize] + .statements + .len() + ); + } + } + } +} + +#[test] +fn normalized_first_handled_actual_has_a_proved_empty_abandoned_prefix() { + for release in [false, true] { + let checked = checked(FIRST, release); + let mir = lowered(&checked); + let function = outer(&mir); + let region = &function.resource_lowering.as_ref().unwrap().regions[0]; + let plan = validate_resource_ownership(&mir, &checked.checked().interner).unwrap(); + let flow = plan.function(function.id).unwrap(); + let first = region.actuals()[0].start().block(); + let TerminatorKind::Branch { + else_block: failed, .. + } = function.blocks[first.index() as usize].terminator.kind + else { + panic!("first exact Handle branch"); + }; + let abort = region + .exits() + .iter() + .find(|(site, outcome)| site.block() == failed && *outcome == ResourceCompletion::Abort) + .unwrap() + .0; + assert!( + !flow + .operations() + .iter() + .any(|operation| operation.site() == abort + && matches!( + operation.role(), + ResourceOperationRole::EndBorrow { .. } + | ResourceOperationRole::Drop { .. } + )) + ); + } +} + +#[test] +fn normalized_call_public_node_and_cfg_edits_cannot_copy_or_reseal_authority() { + for release in [false, true] { + let checked = checked(MINIMAL, release); + let types = &checked.checked().interner; + let original = lowered(&checked); + let index = original + .functions + .iter() + .position(|function| function.id == outer(&original).id) + .unwrap(); + let region = outer(&original).resource_lowering.as_ref().unwrap().regions[0].clone(); + for mutation in 0..9 { + let mut edited = original.clone(); + let function = &mut edited.functions[index]; + let actual = region.actuals()[0].stage(); + let ResourcePosition::Statement(stage) = actual.position() else { + panic!("stage statement"); + }; + match mutation { + 0 => { + if let StatementKind::ResourceCall(ResourceCallNode::Stage { + source_index, + .. + }) = + &mut function.blocks[actual.block().index() as usize].statements[stage].kind + { + *source_index += 1; + } + } + 1 => { + if let StatementKind::ResourceCall(ResourceCallNode::Stage { + parameter, .. + }) = + &mut function.blocks[actual.block().index() as usize].statements[stage].kind + { + *parameter = 0; + } + } + 2 => { + if let StatementKind::ResourceCall(ResourceCallNode::Stage { value, .. }) = + &mut function.blocks[actual.block().index() as usize].statements[stage].kind + { + value.kind = E::Clone(Box::new(value.clone())); + } + } + 3 => { + let statement = + function.blocks[actual.block().index() as usize].statements[stage].clone(); + function.blocks[actual.block().index() as usize] + .statements + .insert(stage, statement); + } + 4 => { + let ResourcePosition::Statement(begin) = region.begin().position() else { + panic!("begin statement"); + }; + function.blocks[region.begin().block().index() as usize] + .statements + .remove(begin); + } + 5 => { + let (site, _) = region.invocation().unwrap(); + let ResourcePosition::Statement(invoke) = site.position() else { + panic!("invoke statement"); + }; + function.blocks[site.block().index() as usize] + .statements + .remove(invoke); + } + 6 => { + let (site, _) = region + .exits() + .iter() + .find(|(_, outcome)| *outcome == ResourceCompletion::Abort) + .unwrap(); + let ResourcePosition::Statement(end) = site.position() else { + panic!("end statement"); + }; + function.blocks[site.block().index() as usize] + .statements + .remove(end); + } + 7 => function.entry = region.actuals()[1].stage().block(), + 8 => { + let block = + &mut function.blocks[region.actuals()[1].start().block().index() as usize]; + if let TerminatorKind::Branch { + then_block, + else_block, + .. + } = &mut block.terminator.kind + { + std::mem::swap(then_block, else_block); + } else { + panic!("exact Handle selector"); + } + } + _ => unreachable!(), + } + assert!( + validate_resource_ownership(&edited, types).is_err(), + "mutation {mutation} must lose constructor authority" + ); + } + } +} + +#[test] +fn normalized_call_canonical_compaction_preserves_archival_original_and_current_sites() { + for release in [false, true] { + let checked = checked(MINIMAL, release); + let mut mir = lowered(&checked); + let id = outer(&mir).id; + let original = outer(&mir).resource_lowering.as_ref().unwrap().regions[0] + .original() + .clone(); + let function = mir + .functions + .iter_mut() + .find(|function| function.id == id) + .unwrap(); + assert!(crate::sequences::prune::unreachable(function)); + assert!(crate::sequences::prune::unused_locals(function)); + assert!(crate::breakpoint_regions::expressions_equal( + function.resource_lowering.as_ref().unwrap().regions[0].original(), + &original + )); + validate_resource_ownership(&mir, &checked.checked().interner).unwrap(); + } +} + +#[test] +fn normalized_call_original_source_edits_refuse_before_constructor_extraction() { + for release in [false, true] { + let checked = checked(MINIMAL, release); + let types = &checked.checked().interner; + let original = hir::lower_checked_resource_program(&checked).unwrap(); + for mutation in 0..4 { + let mut edited = original.clone(); + let function = edited + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "main") + .unwrap(); + let call = function + .body + .statements + .iter_mut() + .find_map(|statement| match &mut statement.kind { + hir::StatementKind::Expression(value) + if matches!(value.kind, E::Call { .. }) => + { + Some(value) + } + _ => None, + }) + .expect("original complete Source call"); + let call_span = call.span; + let E::Call { + args, + evaluation_order, + ownership: hir::CallOwnership::Source(source), + .. + } = &mut call.kind + else { + panic!("Source call"); + }; + match mutation { + 0 => evaluation_order.swap(0, 1), + 1 => { + if let hir::CallerOrigin::Binding(binding) = &mut source.arguments[1].origin { + binding.local = function.params[0].local; + } else { + panic!("original Resource Binding fact"); + } + } + 2 => source.arguments[1].syntax = jett_typecheck::CheckedCallerSyntax::WrittenView, + 3 => args[1].kind = E::Clone(Box::new(args[1].clone())), + _ => unreachable!(), + } + let errors = lower(&edited, types).unwrap_err(); + let expected_message = match mutation { + 0 => { + "call ownership lexical evaluation order disagrees with its source permutation" + } + 1 | 2 => "call ownership disagrees with its original source witness", + 3 => "call ownership binding does not match its actual typed operand", + _ => unreachable!(), + }; + assert_eq!(errors.len(), 1, "release={release}, mutation={mutation}"); + assert_eq!(errors[0].span, call_span); + assert_eq!(errors[0].message, expected_message); + } + } +} + +#[test] +fn normalized_handle_default_joins_the_same_operation_before_one_invocation() { + for release in [false, true] { + let source = MINIMAL.replace("\r\n", "\n").replace( + "marker: fail_marker() handle error:\n return nothing\n , net: view net", + "marker: (fail_marker() handle error: default 7), net: view net", + ); + assert_ne!(source, MINIMAL, "exact canonical original marker Handle"); + let checked = checked(&source, release); + let types = &checked.checked().interner; + let mir = lowered(&checked); + let function = outer(&mir); + let region = &function.resource_lowering.as_ref().unwrap().regions[0]; + assert_eq!(region.exits().len(), 1); + assert_eq!(region.exits()[0].1, ResourceCompletion::Normal); + let plan = validate_resource_ownership(&mir, types).unwrap(); + ResourceCompanionPlan::analyze(&plan, function.id).unwrap(); + let flow = plan.function(function.id).unwrap(); + let begin = flow + .operations() + .iter() + .find(|operation| { + matches!( + operation.role(), + ResourceOperationRole::BeginSourceFunction { .. } + ) + }) + .unwrap(); + assert_eq!( + flow.operations() + .iter() + .filter(|operation| operation.frame() == begin.frame() + && matches!( + operation.role(), + ResourceOperationRole::InvokeSourceFunction { .. } + )) + .count(), + 1 + ); + assert!( + !flow + .operations() + .iter() + .any(|operation| operation.frame() == begin.frame() + && matches!( + operation.role(), + ResourceOperationRole::Complete { + outcome: ResourceCompletion::Abort + } + )) + ); + } +} + +#[test] +fn normalized_abandonment_retires_only_its_prefix_before_another_owner_return() { + const SOURCE: &str = r#"namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +function scenario(view net: Network, token: resource_probe.TestHandle, other: resource_probe.TestHandle) returns resource_probe.TestHandle: + accept_staged(token: token, marker: fail_marker() handle error: + return other + , net: view net) + return other +"#; + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let mir = lowered(&checked); + let function = outer(&mir); + let region = &function.resource_lowering.as_ref().unwrap().regions[0]; + let plan = validate_resource_ownership(&mir, types).unwrap(); + ResourceCompanionPlan::analyze(&plan, function.id).unwrap(); + let flow = plan.function(function.id).unwrap(); + let other = function.params[2].local; + let other_slot = flow.owner_slots().iter().find(|slot| matches!(slot.storage(), ResourceSlotStorage::Local { header } if header.id == other)).unwrap().id(); + for (site, outcome) in region.exits() { + if *outcome != ResourceCompletion::Abort { + continue; + } + assert!(!flow.operations().iter().any(|operation| operation.site() == *site && matches!(operation.role(), ResourceOperationRole::Drop { source, .. } if *source == other_slot))); + assert!(flow.operations().iter().any(|operation| operation.site().block() == site.block() && operation.site().position() == ResourcePosition::Terminator && matches!(operation.role(), ResourceOperationRole::Transfer { source, .. } if *source == other_slot))); + } + } +} diff --git a/crates/jett_mir/src/resource_ownership/original_calls_tests.rs b/crates/jett_mir/src/resource_ownership/original_calls_tests.rs new file mode 100644 index 00000000..18f15f7f --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/original_calls_tests.rs @@ -0,0 +1,226 @@ +use super::tests::{checked, lowered}; +use super::*; +use jett_typecheck::CheckedCallerEffect; + +const SOURCE: &str = r#"namespace app +function read_once(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 2) handle error: + return nothing + result[int64, string] observed = read_once(token, view net) + return nothing +"#; +fn scenario(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "scenario" + }) + .unwrap() +} +fn original_call(function: &Function) -> (&Expression, BlockId, usize) { + for block in &function.blocks { + for (index, statement) in block.statements.iter().enumerate() { + let (StatementKind::Let { value, .. } | StatementKind::Evaluate(value)) = + &statement.kind + else { + continue; + }; + let hir::ExpressionKind::Call { + ownership: hir::CallOwnership::Source(source), + .. + } = &value.kind + else { + continue; + }; + if source + .arguments + .iter() + .any(|argument| argument.effect == CheckedCallerEffect::RelinquishOwned) + { + return (value, block.id, index); + } + } + } + panic!("exact original bare Resource to View Source call"); +} +#[test] +fn resource_original_call_association_keeps_custody_pending_without_ordinary_owner_claim() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + let function = scenario(&program); + let (call, block, index) = original_call(function); + assert!( + original_call_at( + function, + &program.resource_manifest, + types, + call, + block, + index + ) + .unwrap() + ); + let acquisitions = + crate::call_ownership::validate_function(&program, function, types).unwrap(); + assert!(acquisitions.resource_pending); + let hir::ExpressionKind::Call { args, .. } = &call.kind else { + unreachable!() + }; + assert_eq!(acquisitions.argument_binding(&args[0]), None); + assert!(acquisitions.arguments(call).unwrap().is_empty()); + validate_resource_ownership(&program, types).unwrap(); + assert!( + crate::validate_caller_acquisitions(&program, function, types) + .unwrap_err() + .contains("pending ResourceOwnershipPlan") + ); + let copied_query = call.clone(); + assert!( + original_call_at( + function, + &program.resource_manifest, + types, + &copied_query, + block, + index + ) + .unwrap_err() + .contains("exact current expression site") + ); + assert!( + original_call_at( + function, + &program.resource_manifest, + types, + call, + block, + index + 1 + ) + .is_err() + ); + } +} +#[test] +fn resource_original_call_association_rejects_lost_duplicated_and_changed_source_current_pairs() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + validate_resource_ownership(&program, types).unwrap(); + for mutation in 0..7 { + let mut changed = program.clone(); + let function = changed + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "scenario" + }) + .unwrap(); + let witness = function.resource_lowering.as_mut().unwrap(); + match mutation { + 0 => witness.calls.clear(), + 1 => witness.calls.push(witness.calls[0].clone()), + 2 => witness.calls[0].original.span.start += 1, + 3 => witness.calls[0].current.span.start += 1, + 4 => { + let block = function.blocks.iter_mut().find(|block| block.statements.iter().any(|statement| + matches!(&statement.kind, StatementKind::Let { value, .. } if matches!(&value.kind, hir::ExpressionKind::Call { ownership: hir::CallOwnership::Source(source), .. } if source.arguments.iter().any(|argument| argument.effect == CheckedCallerEffect::RelinquishOwned))))).unwrap(); + let statement = block.statements.iter().find(|statement| + matches!(&statement.kind, StatementKind::Let { value, .. } if matches!(&value.kind, hir::ExpressionKind::Call { ownership: hir::CallOwnership::Source(source), .. } if source.arguments.iter().any(|argument| argument.effect == CheckedCallerEffect::RelinquishOwned)))).unwrap().clone(); + block.statements.push(statement); + } + 5 | 6 => { + let statement = function.blocks.iter_mut().flat_map(|block| &mut block.statements).find(|statement| + matches!(&statement.kind, StatementKind::Let { value, .. } if matches!(&value.kind, hir::ExpressionKind::Call { ownership: hir::CallOwnership::Source(source), .. } if source.arguments.iter().any(|argument| argument.effect == CheckedCallerEffect::RelinquishOwned)))).unwrap(); + let StatementKind::Let { value, .. } = &mut statement.kind else { + unreachable!() + }; + let hir::ExpressionKind::Call { + evaluation_order, + ownership: hir::CallOwnership::Source(source), + .. + } = &mut value.kind + else { + unreachable!() + }; + if mutation == 5 { + evaluation_order.reverse(); + } else { + source.arguments[0].source_span.start += 1; + } + } + _ => unreachable!(), + } + assert!( + validate_resource_ownership(&changed, types).is_err(), + "mutation {mutation}" + ); + } + let mut prepared = program.clone(); + crate::prepare_native_generated_functions(&mut prepared, types); + validate_resource_ownership(&prepared, types).unwrap(); + let (call, block, index) = original_call(scenario(&prepared)); + assert!( + original_call_at( + scenario(&prepared), + &prepared.resource_manifest, + types, + call, + block, + index + ) + .unwrap() + ); + } +} +#[test] +fn resource_original_call_association_captures_unnamed_nested_optional_custody_result() { + const SOURCE: &str = r#"namespace app +function accept(value: optional[resource_probe.TestHandle]) returns nothing: + return nothing +function scenario() returns nothing: + use resource_probe + accept(resource_probe.empty()) + return nothing +"#; + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + let function = scenario(&program); + assert!( + function + .locals + .iter() + .all(|local| !custody_type(types, local.ty)) + ); + assert!(!custody_type(types, function.return_type)); + let witness = function + .resource_lowering + .as_ref() + .expect("unnamed Resource expression retains original constructor authority"); + assert_eq!( + witness.calls.len(), + 2, + "outer Source consumer and original nested optional producer" + ); + validate_resource_ownership(&program, types).unwrap(); + let mut changed = program.clone(); + changed + .functions + .iter_mut() + .find(|function| function.id == scenario(&program).id) + .unwrap() + .resource_lowering = None; + assert!(validate_call_ownership(&changed, types).is_err()); + } +} diff --git a/crates/jett_mir/src/resource_ownership/reflected_field_source.jett b/crates/jett_mir/src/resource_ownership/reflected_field_source.jett new file mode 100644 index 00000000..44c19d0e --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/reflected_field_source.jett @@ -0,0 +1,24 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_mir/src/resource_ownership/reflected_fields.rs b/crates/jett_mir/src/resource_ownership/reflected_fields.rs new file mode 100644 index 00000000..b5e00841 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/reflected_fields.rs @@ -0,0 +1,353 @@ +//! Exact ordinal guards derived only from the original checked field-loop archive. +use super::*; +use hir::{BinaryOp, ExpressionKind as E}; + +#[derive(Debug, Clone, PartialEq)] +pub(super) struct FieldDispatch { + original: hir::ReflectedFieldDispatchProof, + key: Local, + type_info: Expression, + guards: Vec, + dispatches: Vec, + targets: Vec, + paths: Vec>, + otherwise: BlockId, +} + +pub(crate) fn ordinal_guard( + proof: &hir::ReflectedFieldDispatchProof, + type_info: &Expression, + index: usize, +) -> Result { + let E::Field { + base, + owner_type, + field, + } = &type_info.kind + else { + return Err("Resource field dispatch lost its checked field selector".into()); + }; + if *field != proof.type_info_field() { + return Err("Resource field dispatch changed its TypeInfo member".into()); + } + let index = i128::try_from(index) + .map_err(|_| "Resource field ordinal exceeds the checked scalar representation")?; + Ok(Expression { + kind: E::Binary { + left: Box::new(Expression { + kind: E::Field { + base: base.clone(), + owner_type: *owner_type, + field: proof.field_index(), + }, + ty: TypeInterner::INT64, + span: type_info.span, + }), + op: BinaryOp::Equal, + right: Box::new(Expression { + kind: E::Int(index), + ty: TypeInterner::INT64, + span: type_info.span, + }), + }, + ty: TypeInterner::BOOL, + span: type_info.span, + }) +} + +impl Capture { + pub(crate) fn reflected_field_dispatch( + &self, + type_info: &Expression, + arms: &[hir::ReflectedTypeArm], + ) -> Option { + let witness = self.witness.as_ref()?; + witness + .source + .get_reflected_field_dispatch(witness.original.id, type_info, arms) + .cloned() + } + + pub(crate) fn capture_reflected_field_dispatch( + &mut self, + original: hir::ReflectedFieldDispatchProof, + key: Local, + guards: Vec, + dispatches: Vec, + targets: Vec, + otherwise: BlockId, + ) -> Result<(), String> { + let Some(witness) = &mut self.witness else { + return Err("Resource field dispatch has no original Source constructor".into()); + }; + let paths = (0..original.arms().len()) + .map(|index| { + original + .arm_container_path(index) + .map(<[_]>::to_vec) + .ok_or("Resource field dispatch lost its ordinal Scope path") + }) + .collect::, _>>()?; + witness.reflected_fields.push(FieldDispatch { + type_info: original.type_info().clone(), + original, + key, + guards, + dispatches, + targets, + paths, + otherwise, + }); + Ok(()) + } +} + +impl FieldDispatch { + fn current( + &self, + witness: &ResourceLoweringWitness, + function: &Function, + ) -> Result<(), String> { + let proof = witness + .source + .get_reflected_field_dispatch( + function.id, + self.original.type_info(), + self.original.arms(), + ) + .ok_or("Resource field dispatch lost its exact checked archive membership")?; + if proof != &self.original + || proof.function() != function.id + || self.guards.len() != proof.arms().len() + || self.dispatches.len() != proof.arms().len() + || self.targets.len() != proof.arms().len() + || self.paths.len() != proof.arms().len() + || function.local(self.key.id) != Some(&self.key) + { + return Err( + "Resource field dispatch changed its sealed ordinal/header inventory".into(), + ); + } + let E::Field { base, .. } = &self.type_info.kind else { + return Err("Resource field dispatch lost its current field binder".into()); + }; + if !matches!(base.kind, E::Local(key) if key == self.key.id) || base.ty != self.key.ty { + return Err("Resource field dispatch changed its exact current field binder".into()); + } + let block = |id: BlockId| { + function + .blocks + .get(id.index() as usize) + .filter(|block| block.id == id) + .ok_or("Resource field dispatch lost its current block") + }; + let mut sites = BTreeSet::new(); + for (index, arm) in proof.arms().iter().enumerate() { + if proof.iteration_index(index) != Some(index) + || proof.arm_container_path(index) != Some(self.paths[index].as_slice()) + || !sites.insert(self.guards[index]) + || !sites.insert(self.dispatches[index]) + || !sites.insert(self.targets[index]) + { + return Err( + "Resource field dispatch reordered ordinals or changed original Scope paths" + .into(), + ); + } + let next = self + .guards + .get(index + 1) + .copied() + .unwrap_or(self.otherwise); + let expected = ordinal_guard(proof, &self.type_info, index)?; + if !matches!(&block(self.guards[index])?.terminator.kind, + TerminatorKind::Branch { condition, then_block, else_block } + if crate::breakpoint_regions::expressions_equal(condition, &expected) + && *then_block == self.dispatches[index] && *else_block == next) + { + return Err( + "Resource field dispatch changed its exact ordinal selecting guard".into(), + ); + } + let expected = ReflectedTypeDispatchArm { + iteration_index: index, + bound_type: arm.bound_type, + reflection_identity: arm.reflection_identity.clone(), + target: self.targets[index], + }; + if !matches!(&block(self.dispatches[index])?.terminator.kind, + TerminatorKind::ReflectedTypeDispatch { type_info, arms, otherwise } + if crate::breakpoint_regions::expressions_equal(type_info, &self.type_info) + && arms.as_slice() == [expected] && *otherwise == self.otherwise) + { + return Err( + "Resource field dispatch changed its exact one-arm body/reflection join".into(), + ); + } + } + if !sites.insert(self.otherwise) + || !matches!( + block(self.otherwise)?.terminator.kind, + TerminatorKind::Unreachable + ) + { + return Err("Resource field dispatch changed its refusing metadata boundary".into()); + } + Ok(()) + } +} + +pub(super) fn seal(witness: &mut ResourceLoweringWitness) { + witness.reflected_seal = witness.reflected_fields.clone(); +} +pub(super) fn current( + witness: &ResourceLoweringWitness, + function: &Function, +) -> Result<(), String> { + if witness.reflected_fields != witness.reflected_seal { + return Err( + "Resource field dispatch changed its independent constructor membership seal".into(), + ); + } + for (index, row) in witness.reflected_fields.iter().enumerate() { + if witness.reflected_fields[..index] + .iter() + .any(|previous| previous.original == row.original) + { + return Err("Resource field dispatch duplicated an original checked occurrence".into()); + } + row.current(witness, function)?; + } + Ok(()) +} +pub(super) fn admits(witness: &ResourceLoweringWitness, block: BlockId) -> bool { + witness + .reflected_fields + .iter() + .any(|row| row.dispatches.contains(&block)) +} + +/// Ordinary proof-only helpers retain the established ordinary sequence edit. +/// The finite edit cannot introduce Resource data or a runtime execution role. +pub(super) fn ordinary_sequence_transition( + function: &mut Function, + before: &Function, + edit: &crate::breakpoint_regions::SequenceEdit, + types: &TypeInterner, +) -> Result<(), String> { + let original = before + .resource_lowering + .as_ref() + .ok_or("Ordinary reflected sequence lost its original Source proof")?; + original.current(before)?; + if has_execution_records(before, types) + || original.reflected_fields.is_empty() + || !original.calls.is_empty() + || !original.regions.is_empty() + || !original.borrowed_sums.is_empty() + || !original.lexical_exits.is_empty() + || !function + .resource_lowering + .as_ref() + .is_some_and(|current| current.same(original)) + { + return Err("Ordinary reflected sequence cannot change a Resource custody witness".into()); + } + // This exact finite ordinary edit validator is also used by ordinary + // borrowed-sum and call-generation proof transitions. The shadow cannot + // install a new breakpoint, Scope, owner, provider or reflected proof. + let mut expected = function.clone(); + crate::breakpoint_regions::sequence_transition(&mut expected, before, edit.clone(), types)?; + if function.id != before.id + || function.identity != before.identity + || function.params != before.params + || function.capture_count != before.capture_count + || function.return_type != before.return_type + || function.entry != before.entry + || function.span != before.span + || function.debug_kind != before.debug_kind + || function.locals.len() < before.locals.len() + || function.locals[..before.locals.len()] != before.locals + || function.locals[before.locals.len()..].iter().any(|local| { + local.ty.index() as usize >= types.len() || resource_type_pending(types, local.ty) + }) + || function.blocks.iter().any(|block| { + walk::mir_block_has_resource(block) + || walk::mir_block_has_custody(block, function, types) + }) + { + return Err( + "Ordinary reflected sequence changed original headers or introduced custody".into(), + ); + } + let mut witness = original.clone(); + witness.locals = function.locals.clone(); + witness.blocks = function.blocks.clone(); + if let Some((body, prefix)) = &edit.prefix { + lexical_borrows::shift_prefix(&mut witness, *body, prefix.len()); + } + if original.descriptors.has_body() { + witness.descriptors.seal_body(function); + } + witness.current(function)?; + function.resource_lowering = Some(witness); + Ok(()) +} + +pub(super) fn remap_blocks( + witness: &mut ResourceLoweringWitness, + map: &[Option], +) -> Result<(), String> { + let remap = |id: &mut BlockId| -> Result<(), String> { + *id = map + .get(id.index() as usize) + .copied() + .flatten() + .ok_or("Resource field dispatch lost a live canonical ordinal block")?; + Ok(()) + }; + for row in &mut witness.reflected_fields { + for id in row + .guards + .iter_mut() + .chain(&mut row.dispatches) + .chain(&mut row.targets) + { + remap(id)?; + } + remap(&mut row.otherwise)?; + } + seal(witness); + Ok(()) +} +pub(super) fn remap_locals( + witness: &mut ResourceLoweringWitness, + remap: &mut impl FnMut(&mut LocalId), +) { + for row in &mut witness.reflected_fields { + remap(&mut row.key.id); + if let Some(id) = &mut row.key.view_source { + remap(id); + } + let mut wrapper = BasicBlock { + id: BlockId(0), + statements: vec![Statement { + kind: StatementKind::Evaluate(row.type_info.clone()), + span: row.type_info.span, + }], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: row.type_info.span, + }, + }; + crate::sequences::prune::block_locals(&mut wrapper, remap, &mut |_| {}); + if let StatementKind::Evaluate(value) = wrapper.statements.remove(0).kind { + row.type_info = value; + } + } + seal(witness); +} + +#[cfg(test)] +#[path = "reflected_fields_tests.rs"] +mod tests; diff --git a/crates/jett_mir/src/resource_ownership/reflected_fields_tests.rs b/crates/jett_mir/src/resource_ownership/reflected_fields_tests.rs new file mode 100644 index 00000000..b5bf7025 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/reflected_fields_tests.rs @@ -0,0 +1,379 @@ +use super::super::tests::{checked_support, lowered}; +use super::*; + +const SUPPORT: &str = + include_str!("../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); +const SOURCE: &str = include_str!("reflected_field_source.jett"); + +fn main(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "main" + }) + .unwrap() +} +fn main_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "main" + }) + .unwrap() +} + +fn reseal_current_graph(function: &mut Function) { + let copy = function.clone(); + let witness = function.resource_lowering.as_mut().unwrap(); + witness.blocks = copy.blocks.clone(); + witness.descriptors.seal_body(©); + seal(witness); +} + +#[test] +fn reflected_field_original_equal_types_keep_distinct_checked_ordinals_and_scopes() { + let indexed = SOURCE.replace(" total = total + observed", " if field.index == 0:\n total = total + observed\n else:\n total = total + observed + 1").replace("total != 34", "total != 35"); + let named = SOURCE.replace(" total = total + observed", " if field.name == \"first\":\n total = total + observed\n else:\n total = total + observed + 1").replace("total != 34", "total != 35"); + for release in [false, true] { + for source in [SOURCE, indexed.as_str(), named.as_str()] { + let checked = checked_support(source, SUPPORT, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + let function = main(&program); + let witness = function.resource_lowering.as_ref().unwrap(); + let [row] = witness.reflected_fields.as_slice() else { + panic!("exact original field binding"); + }; + assert_eq!(row.original.iteration_index(0), Some(0)); + assert_eq!(row.original.iteration_index(1), Some(1)); + assert_eq!( + row.original.arms()[0].bound_type, + row.original.arms()[1].bound_type + ); + assert_eq!( + row.original.arms()[0].reflection_identity, + row.original.arms()[1].reflection_identity + ); + assert_ne!(row.paths[0], row.paths[1]); + assert_ne!(row.targets[0], row.targets[1]); + assert_eq!(witness.borrowed_sums.len(), 2); + assert_ne!( + witness.borrowed_sums[0].alias(), + witness.borrowed_sums[1].alias() + ); + assert_eq!( + witness.borrowed_sums[0].backing(), + witness.borrowed_sums[1].backing() + ); + for dispatch in &row.dispatches { + let TerminatorKind::ReflectedTypeDispatch { arms, .. } = + &function.blocks[dispatch.index() as usize].terminator.kind + else { + panic!("selected one-arm dispatch"); + }; + assert_eq!(arms.len(), 1); + } + validate_resource_ownership(&program, types).unwrap(); + crate::prepare_native_sequences(&mut program, types); + validate_resource_ownership(&program, types).unwrap(); + let once = program.clone(); + crate::prepare_native_sequences(&mut program, types); + assert_eq!(program, once); + } + } +} + +#[test] +fn reflected_field_reordered_equal_type_guards_and_redirected_targets_cannot_reseal() { + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let types = &checked.checked().interner; + let original = lowered(&checked); + for mutation in 0..3 { + let mut edited = original.clone(); + let function = main_mut(&mut edited); + let row = function + .resource_lowering + .as_ref() + .unwrap() + .reflected_fields[0] + .clone(); + assert_eq!( + row.original.arms()[0].bound_type, + row.original.arms()[1].bound_type + ); + match mutation { + 0 => { + for (index, guard) in row.guards.iter().enumerate() { + let TerminatorKind::Branch { condition, .. } = + &mut function.blocks[guard.index() as usize].terminator.kind + else { + unreachable!(); + }; + *condition = + ordinal_guard(&row.original, &row.type_info, 1 - index).unwrap(); + } + } + 1 => { + let TerminatorKind::Branch { then_block, .. } = &mut function.blocks + [row.guards[0].index() as usize] + .terminator + .kind + else { + unreachable!(); + }; + *then_block = row.dispatches[1]; + } + 2 => { + let TerminatorKind::ReflectedTypeDispatch { arms, .. } = &mut function.blocks + [row.dispatches[0].index() as usize] + .terminator + .kind + else { + unreachable!(); + }; + arms[0].target = row.targets[1]; + } + _ => unreachable!(), + } + reseal_current_graph(function); + let expected = if mutation == 2 { + "exact one-arm body/reflection join" + } else { + "exact ordinal selecting guard" + }; + let reflected_message = + super::current(function.resource_lowering.as_ref().unwrap(), function).unwrap_err(); + assert!(reflected_message.contains(expected), "{reflected_message}"); + let message = validate_resource_ownership(&edited, types) + .unwrap_err() + .into_iter() + .map(|error| error.message) + .collect::>() + .join("\n"); + assert!( + message.contains(expected) + || (mutation != 0 + && message.contains( + "borrowed Resource Handle is disconnected from its exact selecting edge", + )), + "{message}" + ); + } + } +} + +#[test] +fn reflected_field_stale_scope_path_cannot_be_replaced_by_equal_type_body() { + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let types = &checked.checked().interner; + let mut edited = lowered(&checked); + let function = main_mut(&mut edited); + let witness = function.resource_lowering.as_mut().unwrap(); + witness.reflected_fields[0].paths[1] = witness.reflected_fields[0].paths[0].clone(); + seal(witness); + let message = validate_resource_ownership(&edited, types) + .unwrap_err() + .into_iter() + .map(|error| error.message) + .collect::>() + .join("\n"); + assert!( + message.contains("reordered ordinals or changed original Scope paths"), + "{message}" + ); + } +} + +#[test] +fn reflected_field_public_hir_reordering_has_no_original_source_authority() { + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let types = &checked.checked().interner; + let original = hir::lower_checked_resource_program(&checked).unwrap(); + for mutation in 0..3 { + let mut edited = original.clone(); + let function = edited + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "main") + .unwrap(); + let statement = function + .body + .statements + .iter_mut() + .find(|statement| matches!(statement.kind, hir::StatementKind::For { .. })) + .unwrap(); + let hir::StatementKind::For { iterable, body, .. } = &mut statement.kind else { + unreachable!(); + }; + if mutation == 0 { + let hir::StatementKind::ReflectedTypeDispatch { arms, .. } = + &mut body.statements[0].kind + else { + unreachable!(); + }; + assert_eq!(arms[0].bound_type, arms[1].bound_type); + arms.swap(0, 1); + } else { + let hir::ExpressionKind::ListConstruct { elements } = &mut iterable.kind else { + unreachable!(); + }; + if mutation == 1 { + elements.swap(0, 1); + } else { + let hir::ExpressionKind::StructConstruct { fields, .. } = &mut elements[1].kind + else { + unreachable!(); + }; + let hir::ExpressionKind::StructConstruct { fields, .. } = &mut fields[9].kind + else { + unreachable!(); + }; + fields[0].kind = hir::ExpressionKind::String("foreign-int64".into()); + } + } + let message = crate::lower(&edited, types) + .unwrap_err() + .into_iter() + .map(|error| error.message) + .collect::>() + .join("\n"); + assert!( + message.contains("original checked HIR archive"), + "{message}" + ); + } + } +} + +#[test] +fn reflected_field_plain_helper_gets_only_exact_archived_proof_without_runtime_scope() { + let helper = "function field_ordinal_total() returns int64:\n mutable int64 total = 0\n for field in type.fields[IterationShape]():\n comptime type Element = field.type_info:\n Element observed = 17\n if field.index == 0:\n total = total + observed\n else:\n total = total + observed + 1\n return total\n"; + let source = SOURCE.replace( + "export function main", + &format!("{helper}export function main"), + ); + for release in [false, true] { + let checked = checked_support(&source, SUPPORT, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "field_ordinal_total") + .unwrap(); + let witness = function.resource_lowering.as_ref().unwrap(); + assert_eq!(witness.reflected_fields.len(), 1); + assert_eq!(witness.reflected_fields[0].original.arms().len(), 2); + assert_eq!( + witness.reflected_fields[0].original.arms()[0].bound_type, + witness.reflected_fields[0].original.arms()[1].bound_type + ); + assert!(!super::super::has_execution_records(function, types)); + let id = function.id; + let plan = validate_resource_ownership(&program, types).unwrap(); + assert!(plan.function(id).is_none()); + crate::prepare_native_sequences(&mut program, types); + validate_resource_ownership(&program, types).unwrap(); + + let mut edited = program.clone(); + let function = edited + .functions + .iter_mut() + .find(|function| function.id == id) + .unwrap(); + let row = function + .resource_lowering + .as_ref() + .unwrap() + .reflected_fields[0] + .clone(); + let TerminatorKind::Branch { condition, .. } = &mut function.blocks + [row.guards[0].index() as usize] + .terminator + .kind + else { + unreachable!(); + }; + *condition = ordinal_guard(&row.original, &row.type_info, 1).unwrap(); + reseal_current_graph(function); + let message = validate_resource_ownership(&edited, types) + .unwrap_err() + .into_iter() + .map(|error| error.message) + .collect::>() + .join("\n"); + assert!( + message.contains("exact ordinal selecting guard"), + "{message}" + ); + } +} + +#[test] +fn reflected_field_plain_helper_preserves_mixed_ordinary_sequence_preparation() { + let head = "function field_ordinal_total() returns int64:\n mutable int64 total = 0\n for field in type.fields[IterationShape]():\n comptime type Element = field.type_info:\n Element observed = 17\n total = total + observed\n"; + let ordinary = [ + " for letter in \"ab\":\n total = total + 1\n", + " for key, value in map(1: 2):\n total = total + key + value\n", + " list[int64] values = list(1, 2)\n for observed in view values:\n total = total + observed\n", + " for impossible in list():\n total = total + 1\n", + ]; + for release in [false, true] { + for body in ordinary { + let helper = format!("{head}{body} return total\n"); + let source = SOURCE.replace( + "export function main", + &format!("{helper}export function main"), + ); + let checked = checked_support(&source, SUPPORT, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "field_ordinal_total") + .unwrap(); + assert_eq!( + function + .resource_lowering + .as_ref() + .unwrap() + .reflected_fields + .len(), + 1 + ); + assert!(!super::super::has_execution_records(function, types)); + let id = function.id; + crate::prepare_native_sequences(&mut program, types); + validate_resource_ownership(&program, types).unwrap(); + let function = program + .functions + .iter() + .find(|function| function.id == id) + .unwrap(); + assert!( + !function + .blocks + .iter() + .any(|block| matches!(block.terminator.kind, TerminatorKind::ForEach { .. })), + "ordinary sequence preparation rolled back: {body}" + ); + assert!( + validate_resource_ownership(&program, types) + .unwrap() + .function(id) + .is_none() + ); + let once = program.clone(); + crate::prepare_native_sequences(&mut program, types); + assert_eq!(program, once); + } + } +} diff --git a/crates/jett_mir/src/resource_ownership/required_descriptors_tests.rs b/crates/jett_mir/src/resource_ownership/required_descriptors_tests.rs new file mode 100644 index 00000000..7828ac2b --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/required_descriptors_tests.rs @@ -0,0 +1,650 @@ +use super::*; +use crate::resource_ownership::tests::checked; +use crate::resource_ownership::tests::checked_support; +use jett_comptime::ExplicitComptimeValues; +use jett_comptime::checked_types::CheckedExpressionTypes; +use jett_typecheck::CheckedResourceProgram; +use std::{collections::HashMap, sync::Arc}; + +const FIXTURE_SUPPORT: &str = + include_str!("../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); +const PURE_CHOICE: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/41_comptime_hook_pure_choice.jett" +); +const ABSENCE: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/40_comptime_hook_absence.jett" +); + +fn required(checked: &Arc) -> ExplicitComptimeValues { + let expression_types = Arc::new(CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let captured = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + expression_types, + Arc::new(HashMap::new()), + ); + assert!( + captured.diagnostics.is_empty(), + "{:?}", + captured.diagnostics + ); + assert!(captured.debug_events.is_empty()); + captured.values +} + +fn materialized(checked: &Arc) -> hir::Program { + let mut program = hir::lower_checked_resource_program(checked).unwrap(); + hir::materialize_checked_required_values( + &mut program, + &required(checked), + &checked.checked().interner, + ) + .unwrap(); + program +} + +fn named<'a>(program: &'a Program, name: &str) -> &'a Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap() +} + +fn named_mut<'a>(program: &'a mut Program, name: &str) -> &'a mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap() +} + +#[test] +fn resource_required_descriptors_preserve_checked_values_and_exact_current_occurrences() { + let sources = [ + include_str!( + "../../../jett_driver/tests/native_conformance/resource/33_comptime_hook_close.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/34_comptime_hook_factory.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/35_comptime_hook_borrow.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/36_comptime_hook_alias.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/37_comptime_hook_relay.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/38_comptime_hook_unused.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/39_immediate_comptime_hook_close.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/40_comptime_hook_absence.jett" + ), + PURE_CHOICE, + include_str!( + "../../../jett_driver/tests/native_conformance/resource/42_comptime_hook_generic.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/43_comptime_hook_scoped.jett" + ), + ]; + for release in [false, true] { + for (fixture, source) in sources.into_iter().enumerate() { + let checked = checked_support(source, FIXTURE_SUPPORT, release); + let types = &checked.checked().interner; + let hir = materialized(&checked); + let program = crate::lower(&hir, types).unwrap(); + let ownership = validate_resource_ownership(&program, types).unwrap_or_else(|errors| { + panic!( + "required fixture {} release={release}: {errors:?}; main CFG={:#?}", + fixture + 33, + named(&program, "main").blocks + ) + }); + assert_eq!( + ownership.required_only_function_ids(), + hir.resource_source.required_only_function_ids() + ); + let function = named(&program, "main"); + let plan = ownership.function(function.id).unwrap(); + let original = ownership.original_source(function.id).unwrap(); + let mut original_required = false; + walk::hir_block(&original.body, &mut |value| { + original_required |= matches!(value.kind, E::Comptime { .. }); + }); + assert!(original_required); + let witness = function.resource_lowering.as_ref().unwrap(); + assert!(witness.descriptors.graph.is_some()); + for row in hir.resource_source.required_materializations() { + if row.function() != function.id || row.hook().is_none() { + continue; + } + assert!(witness.descriptors.values.iter().any(|value| { + crate::breakpoint_regions::expressions_equal(&value.original, row.original()) + && crate::breakpoint_regions::expressions_equal( + &value.current, + row.current(), + ) + && Some(&value.hook) == row.hook() + })); + // An archive value is data, never the current MIR occurrence. + assert!(plan.descriptor_value(row.current()).is_none()); + } + for block in &function.blocks { + walk::mir_block(block, &mut |value| { + if let E::ResourceHookValue { hook } = &value.kind { + assert_eq!(plan.descriptor_value(value), Some(hook)); + assert!(plan.descriptor_value(&value.clone()).is_none()); + } + }); + } + let companion = ResourceCompanionPlan::analyze(&ownership, function.id).unwrap(); + for local in &function.locals { + if plan.descriptor_local(local.id).is_some() { + assert!( + !companion + .storage() + .owned_locals + .contains(&(local.id.index() as usize)) + ); + assert!(!plan.owner_slots().iter().any(|slot| matches!( + slot.storage(), ResourceSlotStorage::Local { header } if header.id == local.id + ))); + } + } + for required_only in ownership.required_only_function_ids() { + let helper = &program.functions[required_only.index() as usize]; + assert_eq!(helper.id, *required_only); + assert!(ownership.function(*required_only).is_none()); + assert!(ownership.original_source(*required_only).is_some()); + let descriptors = &helper.resource_lowering.as_ref().unwrap().descriptors; + assert!(descriptors.graph.is_some()); + assert!(descriptors.returned.is_none()); + assert!(descriptors.values.is_empty()); + assert!(descriptors.bindings.is_empty()); + } + for plan in ownership.functions() { + ResourceCompanionPlan::analyze(&ownership, plan.function()).unwrap(); + } + } + } +} + +#[test] +fn resource_required_descriptors_reject_unmaterialized_and_foreign_archive_substitution() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let original = hir::lower_checked_resource_program(&checked).unwrap(); + let mut current = materialized(&checked); + current.resource_source = original.resource_source; + assert!(crate::lower(¤t, types).is_err()); + + let baseline = crate::lower(&materialized(&checked), types).unwrap(); + validate_resource_ownership(&baseline, types).unwrap(); + let foreign = self::checked(SOURCE, release); + let foreign_hir = materialized(&foreign); + let foreign_mir = crate::lower(&foreign_hir, &foreign.checked().interner).unwrap(); + let mut changed = baseline.clone(); + let function = named_mut(&mut changed, "scenario"); + function.resource_lowering.as_mut().unwrap().source = named(&foreign_mir, "scenario") + .resource_lowering + .as_ref() + .unwrap() + .source + .clone(); + assert!(validate_resource_ownership(&changed, types).is_err()); + } +} + +#[test] +fn resource_required_only_helpers_refuse_resealed_control_flow_and_runtime_authority() { + for release in [false, true] { + let checked = checked_support(PURE_CHOICE, FIXTURE_SUPPORT, release); + let types = &checked.checked().interner; + let baseline = crate::lower(&materialized(&checked), types).unwrap(); + let ownership = validate_resource_ownership(&baseline, types).unwrap(); + let helper_id = named(&baseline, "choose").id; + assert!(ownership.required_only_function_ids().contains(&helper_id)); + let mut changed = baseline.clone(); + let helper = named_mut(&mut changed, "choose"); + let branch = helper + .blocks + .iter_mut() + .find(|block| matches!(block.terminator.kind, TerminatorKind::Branch { .. })) + .unwrap(); + if let TerminatorKind::Branch { + then_block, + else_block, + .. + } = &mut branch.terminator.kind + { + *then_block = *else_block; + } + helper.resource_lowering.as_mut().unwrap().blocks = helper.blocks.clone(); + assert!(!crate::sequences::prune::unreachable(helper)); + assert!(validate_resource_ownership(&changed, types).is_err()); + + let mut changed = baseline.clone(); + let hook = named(&baseline, "main") + .resource_lowering + .as_ref() + .unwrap() + .descriptors + .bindings[0] + .value + .hook + .clone(); + named_mut(&mut changed, "choose") + .resource_lowering + .as_mut() + .unwrap() + .descriptors + .returned = Some(hook); + assert!(validate_resource_ownership(&changed, types).is_err()); + + let mut changed = baseline.clone(); + named_mut(&mut changed, "choose").resource_lowering = None; + assert!(validate_resource_ownership(&changed, types).is_err()); + } +} + +#[test] +fn resource_required_scalar_body_refuses_resealed_branch_before_canonical_pruning() { + const SCALAR: &str = r#"namespace app +export function main(net: Network) returns nothing: + use resource_probe + bool stop = comptime true + if stop: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = resource_probe.create(view net, 761) handle error: + return nothing + resource_probe.close(token) + return nothing +"#; + for release in [false, true] { + let checked = checked(SCALAR, release); + let types = &checked.checked().interner; + let hir = materialized(&checked); + let mut changed = crate::lower(&hir, types).unwrap(); + validate_resource_ownership(&changed, types).unwrap(); + let main = named_mut(&mut changed, "main"); + let descriptors = &main.resource_lowering.as_ref().unwrap().descriptors; + assert!(descriptors.values.is_empty()); + assert!(descriptors.returned.is_none()); + assert!( + hir.resource_source + .required_materializations() + .iter() + .any(|row| { + row.function() == main.id && matches!(row.current().kind, E::Bool(true)) + }) + ); + let branch = main + .blocks + .iter_mut() + .find(|block| matches!(block.terminator.kind, TerminatorKind::Branch { .. })) + .unwrap(); + if let TerminatorKind::Branch { + then_block, + else_block, + .. + } = &mut branch.terminator.kind + { + assert_ne!(*then_block, *else_block); + *then_block = *else_block; + } + main.resource_lowering.as_mut().unwrap().blocks = main.blocks.clone(); + assert!(validate_resource_ownership(&changed, types).is_err()); + let accepted = crate::sequences::prune::unreachable(named_mut(&mut changed, "main")); + assert!(!accepted); + assert!(validate_resource_ownership(&changed, types).is_err()); + } +} + +#[test] +fn resource_required_absence_plans_exact_tag_arms_and_refuses_disconnected_custody() { + for release in [false, true] { + let checked = checked_support(ABSENCE, FIXTURE_SUPPORT, release); + let types = &checked.checked().interner; + let baseline = crate::lower(&materialized(&checked), types).unwrap(); + let ownership = validate_resource_ownership(&baseline, types).unwrap(); + let main = named(&baseline, "main"); + let plan = ownership.function(main.id).unwrap(); + assert!(plan.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::CreateAbsentSum { .. } + ))); + assert!(plan.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::SumTake { success: true, .. } + ))); + let close = baseline + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "resource_probe" + && function.identity.declaration.name == "close" + }) + .unwrap(); + assert!(plan.operations().iter().any(|operation| matches!( + operation.role(), ResourceOperationRole::InvokeSourceFunction { function, .. } + if *function == close.id + ))); + assert!( + ownership + .function(close.id) + .unwrap() + .operations() + .iter() + .any(|operation| matches!(operation.role(), ResourceOperationRole::Close { .. })) + ); + let mut changed = baseline.clone(); + let main = named_mut(&mut changed, "main"); + let branch = main + .blocks + .iter_mut() + .find(|block| matches!(block.terminator.kind, TerminatorKind::Branch { .. })) + .unwrap(); + if let TerminatorKind::Branch { condition, .. } = &mut branch.terminator.kind { + condition.kind = E::Bool(false); + } + let error = match crate::resource_ownership::flow::analyze( + &changed, + named(&changed, "main"), + types, + ) { + Err(error) => error, + Ok(_) => panic!("wrong selecting tag admitted; release={release}"), + }; + assert!( + error.contains("Resource SumTake has no exact selecting typed tag edge"), + "{error}" + ); + + let mut changed = baseline.clone(); + let main = named_mut(&mut changed, "main"); + let mut disconnected = main + .blocks + .iter() + .find(|block| { + let mut close_call = false; + walk::mir_block(block, &mut |value| { + close_call |= + matches!(value.kind, E::Call { function, .. } if function == close.id); + }); + close_call + }) + .unwrap() + .clone(); + assert!(walk::mir_block_has_custody(&disconnected, main, types)); + disconnected.id = BlockId(main.blocks.len() as u32); + let disconnected_id = disconnected.id; + main.blocks.push(disconnected); + assert!( + !ControlFlowGraph::analyze(main) + .unwrap() + .reverse_postorder() + .contains(&disconnected_id) + ); + let error = match crate::resource_ownership::flow::analyze( + &changed, + named(&changed, "main"), + types, + ) { + Err(error) => error, + Ok(_) => panic!("disconnected Resource close admitted; release={release}"), + }; + // The Return's lexical-exit query authenticates the entire current graph + // before flow reaches its final disconnected-custody walk. + assert_eq!( + error, + "Resource ownership differs from its initially authenticated Source or constructor-emitted graph" + ); + assert!(validate_resource_ownership(&changed, types).is_err()); + } +} + +#[test] +fn resource_required_absence_refuses_disconnected_sum_take_header() { + for release in [false, true] { + let checked = checked_support(ABSENCE, FIXTURE_SUPPORT, release); + let types = &checked.checked().interner; + let baseline = crate::lower(&materialized(&checked), types).unwrap(); + validate_resource_ownership(&baseline, types).unwrap(); + let mut changed = baseline.clone(); + let main = named_mut(&mut changed, "main"); + let mut disconnected = main + .blocks + .iter() + .find(|block| { + block.statements.iter().any(|statement| { + matches!(statement.kind, StatementKind::SumTake { success: true, .. }) + }) + }) + .unwrap() + .clone(); + assert!(walk::mir_block_has_custody(&disconnected, main, types)); + disconnected.id = BlockId(main.blocks.len() as u32); + let disconnected_id = disconnected.id; + main.blocks.push(disconnected); + assert!( + !ControlFlowGraph::analyze(main) + .unwrap() + .reverse_postorder() + .contains(&disconnected_id) + ); + let error = match crate::resource_ownership::flow::analyze( + &changed, + named(&changed, "main"), + types, + ) { + Err(error) => error, + Ok(_) => panic!("disconnected Resource SumTake header admitted; release={release}"), + }; + // The whole-body authentication refuses this disconnected typed header + // before flow reaches its final disconnected-custody walk. + assert_eq!( + error, + "Resource ownership differs from its initially authenticated Source or constructor-emitted graph" + ); + assert!(validate_resource_ownership(&changed, types).is_err()); + } +} + +#[test] +fn resource_required_header_detector_preserves_ordinary_sum_classification() { + const ORDINARY: &str = r#"namespace app +function ordinary() returns int64: + optional[int64] maybe = none + int64 number = maybe handle: + default 0 + return number +"#; + for release in [false, true] { + let checked = checked(ORDINARY, release); + let types = &checked.checked().interner; + let hir = hir::lower_checked_resource_program(&checked).unwrap(); + let program = crate::lower(&hir, types).unwrap(); + let function = named(&program, "ordinary"); + let mut header_count = 0; + for block in &function.blocks { + if block.statements.iter().any(|statement| { + matches!( + statement.kind, + StatementKind::SumTag { .. } | StatementKind::SumTake { .. } + ) + }) { + header_count += 1; + assert!(!walk::mir_block_has_custody(block, function, types)); + } + } + assert!(header_count > 0); + assert!( + validate_resource_ownership(&program, types) + .unwrap() + .function(function.id) + .is_none() + ); + } +} + +#[test] +fn resource_required_scalar_helper_refuses_changed_value_before_canonical_pruning() { + const HELPER: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/44_comptime_scalar_helper.jett" + ); + for release in [false, true] { + let checked = checked(HELPER, release); + let types = &checked.checked().interner; + let hir = materialized(&checked); + let baseline = crate::lower(&hir, types).unwrap(); + let ownership = validate_resource_ownership(&baseline, types).unwrap(); + let helper = named(&baseline, "selector"); + assert!(helper.resource_lowering.is_some()); + assert!(!has_execution_records(helper, types)); + assert!( + helper + .resource_lowering + .as_ref() + .unwrap() + .descriptors + .graph + .is_some() + ); + assert!(ownership.function(helper.id).is_none()); + assert!(!ownership.required_only_function_ids().contains(&helper.id)); + assert!( + hir.resource_source + .required_materializations() + .iter() + .any(|row| { + row.function() == helper.id && matches!(row.current().kind, E::Bool(false)) + }) + ); + let mut compacted = baseline.clone(); + assert!(crate::sequences::prune::unreachable(named_mut( + &mut compacted, + "selector" + ))); + let compacted_plan = validate_resource_ownership(&compacted, types).unwrap(); + assert!(compacted_plan.function(helper.id).is_none()); + let original_helper = compacted_plan.original_source(helper.id).unwrap(); + assert!( + original_helper + .body + .statements + .iter() + .any(|statement| matches!( + &statement.kind, hir::StatementKind::Return(Some(value)) + if matches!(value.kind, E::Comptime { .. }) + )) + ); + let mut missing = baseline.clone(); + named_mut(&mut missing, "selector").resource_lowering = None; + assert!(validate_resource_ownership(&missing, types).is_err()); + let mut changed = baseline.clone(); + let helper = named_mut(&mut changed, "selector"); + let witness_present = helper.resource_lowering.is_some(); + let returned = helper + .blocks + .iter_mut() + .find_map(|block| match &mut block.terminator.kind { + TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .unwrap(); + assert!(matches!(returned.kind, E::Bool(false))); + returned.kind = E::Bool(true); + helper.resource_lowering.as_mut().unwrap().blocks = helper.blocks.clone(); + let validation_accepted = validate_resource_ownership(&changed, types).is_ok(); + let transform_accepted = + crate::sequences::prune::unreachable(named_mut(&mut changed, "selector")); + let ownership_accepted = validate_resource_ownership(&changed, types).is_ok(); + assert!( + !validation_accepted && !transform_accepted && !ownership_accepted, + "required scalar helper escaped authentication: witness present={witness_present}, validation accepted={validation_accepted}, transform accepted={transform_accepted}, ownership accepted={ownership_accepted}, release={release}" + ); + } +} + +const SOURCE: &str = r#"namespace app +function scenario(view net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = comptime resource_probe.descriptor() + resource_probe.TestHandle token = resource_probe.create(view net, 761) handle error: + return nothing + dispose(token) + return nothing +"#; + +#[test] +fn resource_required_descriptor_public_hook_substitution_does_not_mint_materialization() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let mut program = hir::lower_checked_resource_program(&checked).unwrap(); + let archive = program.resource_source.clone(); + let hook = program + .resource_manifest + .hooks() + .find(|hook| hook.recipe() == jett_types::ResourceKernelRecipe::Finalize) + .unwrap() + .clone(); + let function = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "scenario") + .unwrap(); + let value = function + .body + .statements + .iter_mut() + .find_map(|statement| match &mut statement.kind { + hir::StatementKind::Let { value, .. } + if matches!(value.kind, E::Comptime { .. }) => + { + Some(value) + } + _ => None, + }) + .unwrap(); + assert_eq!(value.ty, hook.function_type()); + value.kind = E::ResourceHookValue { hook }; + assert_eq!(program.resource_source, archive); + assert!(archive.functions().iter().any(|function| { + let mut original_required = false; + walk::hir_block(&function.body, &mut |value| { + original_required |= matches!(value.kind, E::Comptime { .. }); + }); + original_required + })); + let errors = crate::lower(&program, types).unwrap_err(); + assert!(errors.iter().any(|error| { + error + .message + .contains("Resource lowering differs from its original checked HIR archive") + })); + } +} diff --git a/crates/jett_mir/src/resource_ownership/returned_descriptors.rs b/crates/jett_mir/src/resource_ownership/returned_descriptors.rs new file mode 100644 index 00000000..9db9829c --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/returned_descriptors.rs @@ -0,0 +1,722 @@ +//! Exact returned hook metadata, retained independently of the current CFG. +//! A callable type never selects a hook; only its authenticated producer does. +use super::*; +use hir::ExpressionKind as E; + +#[derive(Debug, Clone, PartialEq)] +pub(super) struct DescriptorValue { + original: Expression, + current: Expression, + hook: hir::ResourceHookRef, +} +#[derive(Debug, Clone, PartialEq)] +pub(super) struct DescriptorBinding { + original: Local, + current: Local, + value: DescriptorValue, +} +#[derive(Debug, Clone, Default, PartialEq)] +pub(super) struct DescriptorWitness { + graph: Option, + returned: Option, + returns: Vec, + bindings: Vec, + values: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +struct DescriptorGraph { + parameters: Vec, + locals: Vec, + blocks: Vec, + entry: BlockId, +} +fn effects_only( + block: &hir::Block, + function: &hir::Function, + manifest: &hir::ResourceManifest, +) -> bool { + let metadata_type = |ty| manifest.hooks().any(|hook| hook.function_type() == ty); + let metadata_local = |local: LocalId| { + function + .locals + .get(local.index() as usize) + .is_some_and(|header| header.id == local && metadata_type(header.ty)) + }; + for statement in &block.statements { + match &statement.kind { + hir::StatementKind::Return(_) + | hir::StatementKind::Respond(_) + | hir::StatementKind::Break + | hir::StatementKind::Continue => return false, + hir::StatementKind::If { + then_block, + else_block, + .. + } => { + if !effects_only(then_block, function, manifest) + || else_block + .as_ref() + .is_some_and(|block| !effects_only(block, function, manifest)) + { + return false; + } + } + hir::StatementKind::Scope(block) => { + if !effects_only(block, function, manifest) { + return false; + } + } + hir::StatementKind::While { .. } + | hir::StatementKind::For { .. } + | hir::StatementKind::Match { .. } + | hir::StatementKind::ReflectedTypeDispatch { .. } => return false, + hir::StatementKind::Trace(local) if metadata_local(*local) => return false, + hir::StatementKind::Breakpoint { bindings, .. } + if bindings.iter().any(|local| metadata_local(*local)) => + { + return false; + } + _ => {} + } + } + let mut valid = true; + walk::hir_block(block, &mut |value| { + valid &= !metadata_type(value.ty) + && !matches!( + value.kind, + E::ResourceHookValue { .. } | E::InlineFunction { .. } | E::ClosureRef { .. } + ); + if let E::Handle { failure, .. } = &value.kind { + valid &= effects_only(failure, function, manifest); + } + }); + valid +} +fn original_value( + value: &Expression, + locals: &BTreeMap, + summaries: &[Option], + manifest: &hir::ResourceManifest, + function: &hir::Function, + archive: &hir::ResourceSourceArchive, +) -> Option { + if let Some(hook) = required_hook(function, archive, value) { + return (manifest.contains_hook(hook) && hook.function_type() == value.ty) + .then(|| hook.clone()); + } + let hook = match &value.kind { + E::ResourceHookValue { hook } if manifest.contains_hook(hook) => hook.clone(), + E::Local(local) => locals.get(&local.index())?.clone(), + E::Clone(inner) if inner.ty == value.ty && inner.span == value.span => { + original_value(inner, locals, summaries, manifest, function, archive)? + } + E::Call { + function, + ownership: hir::CallOwnership::Source(source), + .. + } if (matches!(&source.target, hir::CallTarget::Function(id) if id == function) + || matches!(&source.target, hir::CallTarget::Declaration { .. })) + && source.bridge == hir::CallBridge::Direct + && source.generated_operands.is_empty() + && source + .arguments + .iter() + .all(|argument| argument.staging == hir::ArgumentStaging::Original) => + { + summaries.get(function.index() as usize)?.clone()? + } + _ => return None, + }; + (hook.function_type() == value.ty).then_some(hook) +} + +fn required_original<'a>( + function: &hir::Function, + archive: &'a hir::ResourceSourceArchive, + value: &Expression, +) -> Option<&'a Expression> { + archive.required_materializations().iter().find_map(|row| { + let hook = row.hook()?; + (row.function() == function.id + && row.identity() == &function.identity + && crate::breakpoint_regions::expressions_equal(row.current(), value) + && archive.materialized_hook(function.id, row.original(), value) == Some(hook)) + .then_some(row.original()) + }) +} + +fn required_hook<'a>( + function: &hir::Function, + archive: &'a hir::ResourceSourceArchive, + value: &Expression, +) -> Option<&'a hir::ResourceHookRef> { + let original = required_original(function, archive, value)?; + archive.materialized_hook(function.id, original, value) +} + +fn descriptor_value( + function: &hir::Function, + archive: &hir::ResourceSourceArchive, + expression: &Expression, + hook: &hir::ResourceHookRef, +) -> DescriptorValue { + DescriptorValue { + original: required_original(function, archive, expression) + .unwrap_or(expression) + .clone(), + current: expression.clone(), + hook: hook.clone(), + } +} +fn immutable_local<'a>( + function: &'a hir::Function, + local: LocalId, + value: &Expression, +) -> Option<&'a Local> { + function + .locals + .get(local.index() as usize) + .filter(|header| { + header.id == local + && !header.mutable + && header.view_source.is_none() + && header.ty == value.ty + }) +} +fn root_summary( + function: &hir::Function, + summaries: &[Option], + manifest: &hir::ResourceManifest, + archive: &hir::ResourceSourceArchive, +) -> Option { + if archive.required_only_function_ids().contains(&function.id) + || function.capture_count != 0 + || function.source_definition.is_none() + || !function.identity.type_arguments.is_empty() + || !function.identity.scoped_type_bindings.is_empty() + || function.identity.specialization != Default::default() + { + return None; + } + let mut locals = BTreeMap::new(); + let mut returned = None; + let mut returns = 0; + for statement in &function.body.statements { + match &statement.kind { + hir::StatementKind::Let { local, value } + if immutable_local(function, *local, value).is_some() => + { + if let Some(hook) = + original_value(value, &locals, summaries, manifest, function, archive) + { + locals.insert(local.index(), hook); + } + } + hir::StatementKind::Return(Some(value)) => { + let hook = original_value(value, &locals, summaries, manifest, function, archive)?; + if function.return_type != hook.function_type() + || returned.as_ref().is_some_and(|old| old != &hook) + { + return None; + } + returned = Some(hook); + returns += 1; + } + hir::StatementKind::If { + then_block, + else_block, + .. + } => { + if !effects_only(then_block, function, manifest) + || else_block + .as_ref() + .is_some_and(|block| !effects_only(block, function, manifest)) + { + return None; + } + } + // Nested descriptor exits still need their own retained return transport. + hir::StatementKind::While { .. } + | hir::StatementKind::For { .. } + | hir::StatementKind::Scope(_) + | hir::StatementKind::Match { .. } + | hir::StatementKind::ReflectedTypeDispatch { .. } + | hir::StatementKind::Return(None) => return None, + _ => {} + } + } + (returns == 1).then_some(returned).flatten() +} +fn summaries(archive: &hir::ResourceSourceArchive) -> Vec> { + let mut summaries = vec![None; archive.functions().len()]; + let Some(manifest) = archive.manifest() else { + return summaries; + }; + // Relay chains are finite. Cycles cannot fabricate a descriptor base producer. + for _ in 0..archive.functions().len() { + let next: Vec<_> = archive + .execution_functions() + .iter() + .map(|function| root_summary(function, &summaries, manifest, archive)) + .collect(); + if next == summaries { + break; + } + summaries = next; + } + summaries +} +fn remember( + values: &mut Vec, + expression: &Expression, + hook: &hir::ResourceHookRef, + function: &hir::Function, + archive: &hir::ResourceSourceArchive, +) { + if !values + .iter() + .any(|value| crate::breakpoint_regions::expressions_equal(&value.current, expression)) + { + values.push(descriptor_value(function, archive, expression, hook)); + } + if let E::Clone(inner) = &expression.kind { + remember(values, inner, hook, function, archive); + } +} + +pub(super) fn capture( + function: &hir::Function, + archive: &hir::ResourceSourceArchive, +) -> DescriptorWitness { + if archive.required_only_function_ids().contains(&function.id) { + // The full lowering witness still authenticates this checked body. Its + // required-only role supplies no runtime descriptor occurrence or plan. + return DescriptorWitness::default(); + } + let summaries = summaries(archive); + let mut witness = DescriptorWitness { + returned: summaries + .get(function.id.index() as usize) + .cloned() + .flatten(), + ..Default::default() + }; + let Some(manifest) = archive.manifest() else { + return witness; + }; + let mut locals = BTreeMap::new(); + for statement in &function.body.statements { + if let hir::StatementKind::Let { local, value } = &statement.kind + && let Some(header) = immutable_local(function, *local, value) + && let Some(hook) = + original_value(value, &locals, &summaries, manifest, function, archive) + { + remember(&mut witness.values, value, &hook, function, archive); + locals.insert(local.index(), hook.clone()); + witness.bindings.push(DescriptorBinding { + original: header.clone(), + current: header.clone(), + value: descriptor_value(function, archive, value, &hook), + }); + } + if let hir::StatementKind::Return(Some(value)) = &statement.kind + && let Some(hook) = + original_value(value, &locals, &summaries, manifest, function, archive) + && witness.returned.as_ref() == Some(&hook) + { + remember(&mut witness.values, value, &hook, function, archive); + witness + .returns + .push(descriptor_value(function, archive, value, &hook)); + } + } + walk::hir_block(&function.body, &mut |value| { + if let E::IndirectCall { + callee, + ownership: hir::CallOwnership::Source(source), + .. + } = &value.kind + && source.target + == (hir::CallTarget::Indirect { + signature_type: callee.ty, + }) + && source.bridge == hir::CallBridge::Direct + && source.generated_operands.is_empty() + && let Some(hook) = + original_value(callee, &locals, &summaries, manifest, function, archive) + { + remember(&mut witness.values, callee, &hook, function, archive); + } + }); + witness +} + +fn remap_expression(value: &mut Expression, remap: &mut impl FnMut(&mut LocalId)) { + let mut wrapper = BasicBlock { + id: BlockId(0), + statements: vec![Statement { + kind: StatementKind::Evaluate(value.clone()), + span: value.span, + }], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: value.span, + }, + }; + crate::sequences::prune::block_locals(&mut wrapper, remap, &mut |_| {}); + if let StatementKind::Evaluate(remapped) = wrapper.statements.remove(0).kind { + *value = remapped; + } +} + +pub(super) fn remap_locals( + witness: &mut DescriptorWitness, + map: &[Option], + remap: &mut impl FnMut(&mut LocalId), +) { + if let Some(graph) = &mut witness.graph { + for parameter in &mut graph.parameters { + remap(&mut parameter.local); + } + graph.locals.retain(|local| { + map.get(local.id.index() as usize) + .copied() + .flatten() + .is_some() + }); + for local in &mut graph.locals { + remap(&mut local.id); + if let Some(source) = &mut local.view_source { + remap(source); + } + } + for block in &mut graph.blocks { + crate::sequences::prune::block_locals(block, remap, &mut |_| {}); + } + } + for binding in &mut witness.bindings { + remap(&mut binding.current.id); + remap_expression(&mut binding.value.current, remap); + } + for value in witness.values.iter_mut().chain(&mut witness.returns) { + remap_expression(&mut value.current, remap); + } +} +pub(super) fn remap_blocks( + witness: &mut DescriptorWitness, + map: &[Option], +) -> Result<(), String> { + let Some(graph) = &mut witness.graph else { + return Ok(()); + }; + // Preflight every retained edge before deleting or renumbering any evidence. + // An unmapped original target must never survive as a coincidentally equal new ID. + let missing = std::cell::Cell::new(false); + let check = |id: &mut BlockId| { + if map.get(id.index() as usize).copied().flatten().is_none() { + missing.set(true); + } + }; + let mut entry = graph.entry; + check(&mut entry); + for block in &graph.blocks { + if map + .get(block.id.index() as usize) + .copied() + .flatten() + .is_some() + { + let mut terminator = block.terminator.kind.clone(); + crate::sequences::prune::block_targets(&mut terminator, &check); + } + } + if missing.get() { + return Err( + "Resource descriptor canonical block map removes a retained entry or edge target" + .into(), + ); + } + let remap = |id: &mut BlockId| { + if let Some(mapped) = map.get(id.index() as usize).copied().flatten() { + *id = mapped; + } + }; + graph.blocks.retain(|block| { + map.get(block.id.index() as usize) + .copied() + .flatten() + .is_some() + }); + remap(&mut graph.entry); + for block in &mut graph.blocks { + remap(&mut block.id); + crate::sequences::prune::block_targets(&mut block.terminator.kind, &remap); + } + Ok(()) +} +fn mapped_value_valid(value: &DescriptorValue, witness: &ResourceLoweringWitness) -> bool { + if matches!(value.original.kind, E::Comptime { .. } | E::Constant { .. }) { + return witness.source.materialized_hook( + witness.original.id, + &value.original, + &value.current, + ) == Some(&value.hook); + } + if matches!(value.original.kind, E::Call { .. }) { + return witness.calls.iter().any(|call| { + crate::breakpoint_regions::expressions_equal(&call.original, &value.original) + && crate::breakpoint_regions::expressions_equal(&call.current, &value.current) + }); + } + let mut restored = value.current.clone(); + let mut valid = true; + remap_expression(&mut restored, &mut |local| { + if let Some(binding) = witness + .descriptors + .bindings + .iter() + .find(|binding| binding.current.id == *local) + { + *local = binding.original.id; + } else { + valid = false; + } + }); + valid && crate::breakpoint_regions::expressions_equal(&restored, &value.original) +} +fn header_same(original: &Local, current: &Local) -> bool { + let mut restored = current.clone(); + restored.id = original.id; + restored == *original +} + +pub(super) fn validate( + witness: &ResourceLoweringWitness, + function: &Function, +) -> Result<(), String> { + let expected = capture(&witness.original, &witness.source); + let current = &witness.descriptors; + current.current(function)?; + if (!witness.borrowed_sums.is_empty() + || super::entry_scope::capture_needed(&witness.original, &witness.source) + || witness + .source + .required_materializations() + .iter() + .any(|row| row.function() == function.id)) + && current.graph.is_none() + { + return Err( + "required Resource materialization lost its authenticated current body transport" + .into(), + ); + } + if witness + .source + .required_only_function_ids() + .contains(&function.id) + { + // No runtime operation or callable authority is projected for a + // required-only body. Source/header/type/call authentication ran first. + return (current.graph.is_some() + && current.returned.is_none() + && current.values.is_empty() + && current.bindings.is_empty() + && current.returns.is_empty()) + .then_some(()) + .ok_or_else(|| { + "required-only Resource helper acquired runtime descriptor authority".into() + }); + } + if current.graph.is_none() && (!current.values.is_empty() || current.returned.is_some()) { + return Err("Resource descriptor lost its authenticated current body transport".into()); + } + if current.returned != expected.returned + || current.bindings.len() != expected.bindings.len() + || current.values.len() != expected.values.len() + || current.returns.len() != expected.returns.len() + { + return Err("Resource descriptor lost its exact archived return or producer family".into()); + } + for (binding, original) in current.bindings.iter().zip(&expected.bindings) { + if binding.original != original.original + || binding.value.original != original.value.original + || binding.value.hook != original.value.hook + || !header_same(&binding.original, &binding.current) + || function.local(binding.current.id) != Some(&binding.current) + || !mapped_value_valid(&binding.value, witness) + { + return Err( + "Resource descriptor changed its exact immutable producer header or Source tuple" + .into(), + ); + } + let count = function.blocks.iter().flat_map(|block| &block.statements).filter(|statement| { + matches!(&statement.kind, StatementKind::Let { local, value } + if *local == binding.current.id && crate::breakpoint_regions::expressions_equal(value, &binding.value.current)) + }).count(); + if count != 1 { + return Err("Resource descriptor has no unique current immutable producer".into()); + } + } + for (value, original) in current.values.iter().zip(&expected.values) { + if value.original != original.original + || value.hook != original.hook + || !mapped_value_valid(value, witness) + { + return Err( + "Resource descriptor differs from its archived hook, producer or alias occurrence" + .into(), + ); + } + } + for (value, original) in current.returns.iter().zip(&expected.returns) { + if value.original != original.original + || value.hook != original.hook + || !mapped_value_valid(value, witness) + { + return Err("Resource descriptor differs from its exact retained Return".into()); + } + let count = function + .blocks + .iter() + .filter(|block| { + matches!(&block.terminator.kind, TerminatorKind::Return(Some(actual)) + if crate::breakpoint_regions::expressions_equal(actual, &value.current)) + }) + .count(); + if count != 1 { + return Err("Resource descriptor return has no unique current Return transport".into()); + } + } + current_values(witness, function)?; + Ok(()) +} + +pub(super) fn current_values( + witness: &ResourceLoweringWitness, + function: &Function, +) -> Result, String> { + let mut values = BTreeMap::new(); + for proof in &witness.descriptors.values { + let mut count = 0; + for block in &function.blocks { + walk::mir_block(block, &mut |value| { + if crate::breakpoint_regions::expressions_equal(value, &proof.current) { + values.insert(std::ptr::from_ref(value).addr(), proof.hook.clone()); + count += 1; + } + }); + } + if count != 1 { + return Err( + "Resource descriptor lost its unique original/current value occurrence".into(), + ); + } + } + let mut escaped = false; + for block in &function.blocks { + walk::mir_block(block, &mut |value| { + for binding in &witness.descriptors.bindings { + escaped |= match &value.kind { + E::Local(local) => { + *local == binding.current.id + && !values.contains_key(&std::ptr::from_ref(value).addr()) + } + E::ClosureRef { captures, .. } => captures.contains(&binding.current.id), + _ => false, + }; + } + escaped |= matches!(value.kind, E::ResourceHookValue { .. }) + && !values.contains_key(&std::ptr::from_ref(value).addr()); + }); + for statement in &block.statements { + let locals: &[LocalId] = match &statement.kind { + StatementKind::Trace(local) => std::slice::from_ref(local), + StatementKind::Breakpoint { bindings, .. } => bindings, + StatementKind::Assign { target, .. } if matches!(target.kind, E::Local(_)) => { + if let E::Local(local) = &target.kind { + std::slice::from_ref(local) + } else { + &[] + } + } + _ => &[], + }; + escaped |= witness + .descriptors + .bindings + .iter() + .any(|binding| locals.contains(&binding.current.id)); + } + } + if escaped { + return Err( + "Resource descriptor has an unproved current escape, mutation or producer".into(), + ); + } + Ok(values.into_iter().collect()) +} +impl DescriptorWitness { + pub(super) fn has_body(&self) -> bool { + self.graph.is_some() + } + pub(super) fn current(&self, function: &Function) -> Result<(), String> { + if let Some(graph) = &self.graph + && (graph.parameters != function.params + || graph.locals != function.locals + || graph.entry != function.entry + || !crate::breakpoint_regions::blocks_equal(&graph.blocks, &function.blocks)) + { + return Err("Resource descriptor body or headers differ from their authenticated current Source transport".into()); + } + Ok(()) + } + + pub(super) fn seal(&mut self, function: &Function) { + if self.returned.is_some() || !self.values.is_empty() { + self.seal_body(function); + } + } + pub(super) fn seal_body(&mut self, function: &Function) { + self.graph = Some(DescriptorGraph { + parameters: function.params.clone(), + locals: function.locals.clone(), + blocks: function.blocks.clone(), + entry: function.entry, + }); + } + pub(super) fn returned(&self) -> Option<&hir::ResourceHookRef> { + self.returned.as_ref() + } + pub(super) fn locals(&self) -> impl Iterator { + self.bindings + .iter() + .map(|binding| (binding.current.id, &binding.value.hook)) + } + pub(super) fn binding( + &self, + local: LocalId, + value: &Expression, + ) -> Option<&hir::ResourceHookRef> { + self.bindings.iter().find_map(|binding| { + (binding.current.id == local + && crate::breakpoint_regions::expressions_equal(&binding.value.current, value)) + .then_some(&binding.value.hook) + }) + } + pub(super) fn return_matches(&self, value: &Expression) -> bool { + self.returns + .iter() + .any(|proof| crate::breakpoint_regions::expressions_equal(&proof.current, value)) + } +} + +#[cfg(test)] +#[path = "returned_descriptors_tests.rs"] +mod tests; + +#[cfg(test)] +#[path = "required_descriptors_tests.rs"] +mod required_tests; diff --git a/crates/jett_mir/src/resource_ownership/returned_descriptors_tests.rs b/crates/jett_mir/src/resource_ownership/returned_descriptors_tests.rs new file mode 100644 index 00000000..b2d470ca --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/returned_descriptors_tests.rs @@ -0,0 +1,514 @@ +use super::*; +use crate::resource_ownership::tests::{checked, lowered}; + +const SOURCE: &str = r#"namespace app +function relay() returns function(resource_probe.TestHandle) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing selected = resource_probe.descriptor() + function(resource_probe.TestHandle) returns nothing alias = selected + return alias +function scenario(view net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing selected = relay() + function(resource_probe.TestHandle) returns nothing dispose = selected + resource_probe.TestHandle token = resource_probe.create(view net, 721) handle error: + return nothing + dispose(token) + return nothing +"#; +fn scenario(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "scenario" + }) + .unwrap() +} +fn scenario_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "scenario" + }) + .unwrap() +} +fn indirect(function: &Function) -> &Expression { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + StatementKind::Evaluate(value) if matches!(value.kind, E::IndirectCall { .. }) => { + Some(value) + } + _ => None, + }) + .unwrap() +} +#[test] +fn resource_returned_hook_preserves_alias_relay_and_indirect_authority() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + let ownership = validate_resource_ownership(&program, types).unwrap(); + let function = scenario(&program); + let plan = ownership.function(function.id).unwrap(); + let call = indirect(function); + let operation = plan + .operations_for_expression(call) + .find(|operation| operation.indirect_hook_target().is_some()) + .unwrap(); + let hook = operation.indirect_hook_target().unwrap(); + assert_eq!(hook.recipe(), jett_types::ResourceKernelRecipe::Finalize); + let ResourceOperationRole::InvokeHook { + source, operands, .. + } = operation.role() + else { + panic!("exact indirect hook operation"); + }; + assert_eq!( + source.target, + hir::CallTarget::Indirect { + signature_type: hook.function_type() + } + ); + assert_eq!(source.bridge, hir::CallBridge::Direct); + assert!(matches!( + &operands[..], + [ResourceCallOperand::Owned { parameter: 0, .. }] + )); + let E::IndirectCall { callee, .. } = &call.kind else { + unreachable!(); + }; + assert_eq!(plan.descriptor_value(callee), Some(hook)); + assert!(plan.descriptor_value(&callee.as_ref().clone()).is_none()); + assert!( + plan.operations_for_expression(&call.clone()) + .next() + .is_none() + ); + let companion = ResourceCompanionPlan::analyze(&ownership, function.id).unwrap(); + for local in &function.locals { + if plan.descriptor_local(local.id).is_some() { + assert!( + !companion + .storage() + .owned_locals + .contains(&(local.id.index() as usize)) + ); + assert!(!plan.owner_slots().iter().any(|slot| matches!(slot.storage(), ResourceSlotStorage::Local { header } if header.id == local.id))); + } + } + for name in ["relay", "descriptor"] { + let returned = ownership + .functions() + .iter() + .find(|plan| plan.identity().declaration.name == name) + .unwrap(); + assert_eq!(returned.descriptor_return(), Some(hook)); + ResourceCompanionPlan::analyze(&ownership, returned.function()).unwrap(); + } + } +} +#[test] +fn resource_returned_hook_supports_immediate_and_unused_metadata() { + for release in [false, true] { + for body in [ + " resource_probe.TestHandle token = resource_probe.create(view net, 721) handle error:\n return nothing\n resource_probe.descriptor()(token)\n return nothing\n", + " function(resource_probe.TestHandle) returns nothing unused = resource_probe.descriptor()\n return nothing\n", + ] { + let source = format!( + "namespace app\nfunction scenario(view net: Network) returns nothing:\n use resource_probe\n{body}" + ); + let checked = checked(&source, release); + let program = lowered(&checked); + let ownership = + validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + for plan in ownership.functions() { + ResourceCompanionPlan::analyze(&ownership, plan.function()).unwrap(); + } + } + } +} +#[test] +fn resource_returned_hook_refuses_changed_retained_returns_and_resealed_escapes() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + let mut changed = baseline.clone(); + let function = scenario_mut(&mut changed); + let binding = function + .resource_lowering + .as_ref() + .unwrap() + .descriptors + .bindings[0] + .clone(); + let escape = Expression { + kind: E::Local(binding.current.id), + ty: binding.current.ty, + span: binding.value.current.span, + }; + function.blocks[function.entry.index() as usize] + .statements + .push(Statement { + span: escape.span, + kind: StatementKind::Evaluate(escape), + }); + function.resource_lowering.as_mut().unwrap().blocks = function.blocks.clone(); + assert!(validate_resource_ownership(&changed, types).is_err()); + let mut changed = baseline.clone(); + let function = changed + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "relay") + .unwrap(); + let block = function + .blocks + .iter_mut() + .find(|block| matches!(block.terminator.kind, TerminatorKind::Return(Some(_)))) + .unwrap(); + if let TerminatorKind::Return(Some(value)) = &mut block.terminator.kind { + value.span = function.span; + } + function.resource_lowering.as_mut().unwrap().blocks = function.blocks.clone(); + assert!(validate_resource_ownership(&changed, types).is_err()); + let mut changed = baseline.clone(); + let function = scenario_mut(&mut changed); + function + .resource_lowering + .as_mut() + .unwrap() + .descriptors + .returned = baseline + .functions + .iter() + .find(|function| function.identity.declaration.name == "descriptor") + .unwrap() + .resource_lowering + .as_ref() + .unwrap() + .descriptors + .returned + .clone(); + assert!(validate_resource_ownership(&changed, types).is_err()); + } +} +use crate::resource_ownership::tests::{SUPPORT, checked_support}; +const DESCRIPTOR_SUPPORT: &str = r#"export function factory_descriptor() returns function(view Network, int64) returns result[TestHandle, string]: + return kernel_create +export function borrow_descriptor() returns function(view Network, view TestHandle) returns result[int64, string]: + return kernel_borrow +export function select_create(view net: Network, view anchor: TestHandle, fail_now: bool) returns function(view Network, int64) returns result[TestHandle, string]: + result[int64, string] observed = borrow(view net, view anchor) + if fail_now: + int64 ignored = terminal_label() + return kernel_create +"#; +const ALL_RECIPES: &str = r#"namespace app +function scenario(view net: Network) returns nothing: + use resource_probe + function(view Network, int64) returns result[resource_probe.TestHandle, string] acquire = resource_probe.factory_descriptor() + function(view Network, view resource_probe.TestHandle) returns result[int64, string] observe = resource_probe.borrow_descriptor() + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.descriptor() + resource_probe.TestHandle token = acquire(view net, 731) handle error: + return nothing + result[int64, string] observed = observe(view net, view token) + dispose(token) + return nothing +"#; +#[test] +fn resource_returned_hook_proves_every_recipe_and_modes() { + for release in [false, true] { + let support = format!("{SUPPORT}{DESCRIPTOR_SUPPORT}"); + let checked = checked_support(ALL_RECIPES, &support, release); + let program = lowered(&checked); + let ownership = validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let plan = ownership.function(scenario(&program).id).unwrap(); + let hooks: Vec<_> = plan + .operations() + .iter() + .filter_map(|operation| { + operation + .indirect_hook_target() + .map(|hook| (hook.recipe(), operation)) + }) + .collect(); + assert_eq!(hooks.len(), 3); + for (recipe, operation) in hooks { + let ResourceOperationRole::InvokeHook { + operands, result, .. + } = operation.role() + else { + panic!("exact hook operation"); + }; + match recipe { + jett_types::ResourceKernelRecipe::NetworkFactory => { + assert!(matches!(result, ResourceCallResult::Owned { .. })); + assert!( + operands + .iter() + .all(|operand| matches!(operand, ResourceCallOperand::Ordinary { .. })) + ); + } + jett_types::ResourceKernelRecipe::NetworkBorrow => { + assert!(matches!(result, ResourceCallResult::Ordinary { .. })); + assert!(operands.iter().any(|operand| matches!( + operand, + ResourceCallOperand::Borrowed { parameter: 1, .. } + ))); + } + jett_types::ResourceKernelRecipe::Finalize => { + assert!(matches!( + &operands[..], + [ResourceCallOperand::Owned { parameter: 0, .. }] + )); + } + } + } + for plan in ownership.functions() { + ResourceCompanionPlan::analyze(&ownership, plan.function()).unwrap(); + } + } +} +#[test] +fn resource_returned_hook_authenticates_callee_effects_after_actuals() { + for release in [false, true] { + let support = format!("{SUPPORT}{DESCRIPTOR_SUPPORT}"); + let source = r#"namespace app +function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle anchor = resource_probe.create(view net, 732) handle error: + return nothing + resource_probe.TestHandle token = resource_probe.select_create(view net, view anchor, false)(view net, 733) handle error: + return nothing + resource_probe.close(token) + resource_probe.close(anchor) + return nothing +"#; + let checked = checked_support(source, &support, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + let ownership = validate_resource_ownership(&baseline, types).unwrap(); + for plan in ownership.functions() { + ResourceCompanionPlan::analyze(&ownership, plan.function()).unwrap(); + } + let selected = baseline + .functions + .iter() + .find(|function| function.identity.declaration.name == "select_create") + .unwrap(); + assert_eq!( + ownership + .function(selected.id) + .unwrap() + .descriptor_return() + .unwrap() + .recipe(), + jett_types::ResourceKernelRecipe::NetworkFactory + ); + let mut changed = baseline.clone(); + let selected = changed + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "select_create") + .unwrap(); + let block = selected + .blocks + .iter_mut() + .find(|block| matches!(block.terminator.kind, TerminatorKind::Branch { .. })) + .unwrap(); + if let TerminatorKind::Branch { condition, .. } = &mut block.terminator.kind { + condition.kind = E::Bool(true); + } + selected.resource_lowering.as_mut().unwrap().blocks = selected.blocks.clone(); + assert!(validate_resource_ownership(&changed, types).is_err()); + } +} +#[test] +fn resource_returned_hook_shared_all_recipe_and_evaluation_order_sources_validate() { + const SUPPORT: &str = + include_str!("../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); + const SOURCES: [&str; 6] = [ + include_str!( + "../../../jett_driver/tests/native_conformance/resource/27_returned_hook_factory.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/28_returned_hook_borrow.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/29_immediate_returned_hook_close.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/30_immediate_returned_hook_factory.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/31_immediate_returned_hook_callee_failure.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/32_immediate_returned_hook_actual_failure.jett" + ), + ]; + for release in [false, true] { + for (index, source) in SOURCES.iter().enumerate() { + let checked = checked_support(source, SUPPORT, release); + let program = lowered(&checked); + let ownership = validate_resource_ownership(&program, &checked.checked().interner) + .unwrap_or_else(|errors| { + panic!("shared Source {} release {release}: {errors:?}", index + 27) + }); + let entry = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "main" + }) + .unwrap(); + assert!( + ownership + .function(entry.id) + .unwrap() + .operations() + .iter() + .any(|operation| operation.indirect_hook_target().is_some()) + ); + for plan in ownership.functions() { + ResourceCompanionPlan::analyze(&ownership, plan.function()).unwrap(); + } + } + } +} +#[test] +fn resource_returned_hook_refuses_resealed_body_before_canonical_pruning() { + const SUPPORT: &str = + include_str!("../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); + const SOURCE: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/30_immediate_returned_hook_factory.jett" + ); + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let types = &checked.checked().interner; + let mut changed = lowered(&checked); + validate_resource_ownership(&changed, types).unwrap(); + let selected = changed + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "select_create") + .unwrap(); + let branch = selected + .blocks + .iter_mut() + .find(|block| matches!(block.terminator.kind, TerminatorKind::Branch { .. })) + .unwrap(); + if let TerminatorKind::Branch { + then_block, + else_block, + .. + } = &mut branch.terminator.kind + { + *then_block = *else_block; + } + selected.resource_lowering.as_mut().unwrap().blocks = selected.blocks.clone(); + assert!( + validate_resource_ownership(&changed, types).is_err(), + "untransformed copied body must be refused" + ); + let selected = changed + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "select_create") + .unwrap(); + let transform_accepted = crate::sequences::prune::unreachable(selected); + let ownership_accepted = validate_resource_ownership(&changed, types).is_ok(); + assert!( + !transform_accepted && !ownership_accepted, + "canonical prune consumed a copied/resealed descriptor body: transform accepted={transform_accepted}, final ownership accepted={ownership_accepted}, release={release}" + ); + } +} +#[test] +fn resource_returned_hook_refuses_resealed_headers_before_canonical_local_pruning() { + const SUPPORT: &str = + include_str!("../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); + const SOURCE: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/30_immediate_returned_hook_factory.jett" + ); + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let types = &checked.checked().interner; + let mut changed = lowered(&checked); + let selected = changed + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "select_create") + .unwrap(); + let mut unproved = selected.locals[0].clone(); + unproved.id = LocalId::new(u32::try_from(selected.locals.len()).unwrap()); + unproved.name = "unproved_unused_header".into(); + unproved.ty = TypeInterner::BOOL; + unproved.debug_ty = TypeInterner::BOOL; + unproved.debug_type_name = None; + unproved.view_source = None; + unproved.mutable = false; + selected.locals.push(unproved); + selected.resource_lowering.as_mut().unwrap().locals = selected.locals.clone(); + assert!(validate_resource_ownership(&changed, types).is_err()); + let selected = changed + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "select_create") + .unwrap(); + let transform_accepted = crate::sequences::prune::unused_locals(selected); + let ownership_accepted = validate_resource_ownership(&changed, types).is_ok(); + assert!( + !transform_accepted && !ownership_accepted, + "canonical local prune consumed copied/resealed descriptor headers: transform accepted={transform_accepted}, final ownership accepted={ownership_accepted}, release={release}" + ); + } +} +#[test] +fn resource_returned_hook_refuses_unmapped_retained_descriptor_targets() { + const SUPPORT: &str = + include_str!("../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); + const SOURCE: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/30_immediate_returned_hook_factory.jett" + ); + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let program = lowered(&checked); + let selected = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "select_create") + .unwrap(); + let mut proof = selected + .resource_lowering + .as_ref() + .unwrap() + .descriptors + .clone(); + let branch = selected + .blocks + .iter() + .find(|block| matches!(block.terminator.kind, TerminatorKind::Branch { .. })) + .unwrap(); + let TerminatorKind::Branch { then_block, .. } = branch.terminator.kind else { + unreachable!(); + }; + let mut map: Vec<_> = selected.blocks.iter().map(|block| Some(block.id)).collect(); + map[then_block.index() as usize] = None; + let before = proof.clone(); + assert!(remap_blocks(&mut proof, &map).is_err()); + assert_eq!( + proof, before, + "rejected mapping must preserve the retained evidence" + ); + } +} diff --git a/crates/jett_mir/src/resource_ownership/tests.rs b/crates/jett_mir/src/resource_ownership/tests.rs new file mode 100644 index 00000000..1c4dc9c9 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/tests.rs @@ -0,0 +1,735 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; +use jett_hir::ExpressionKind; +use jett_parser::{ast::Item, parse}; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::CheckedCallerEffect as Effect; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; +use jett_types::ResourceKernelRecipe; +use std::{collections::HashMap, sync::Arc}; + +pub(super) const SUPPORT: &str = r#"namespace resource_probe +export resource TestHandle +export function create(view net: Network, label: int64) returns result[TestHandle, string]: + return kernel_create(label: label, net: view net) +export function borrow(view net: Network, view token: TestHandle) returns result[int64, string]: + return kernel_borrow(view net, view token) +export function close(token: TestHandle) returns nothing: + kernel_close(token) + return nothing +export function empty() returns optional[TestHandle]: + return none +export function descriptor() returns function(TestHandle) returns nothing: + return kernel_close +export function terminal_label() returns int64: + list[int64] numbers = list(1) + list[int64] removed = list.__remove_at[int64](numbers, -1) + return 1 +"#; +const SOURCE: &str = r#"namespace app +function pass_owner(token: resource_probe.TestHandle) returns resource_probe.TestHandle: + return token +function pass_view(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +function consume_pair(label: int64, token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1) handle error: + return nothing + resource_probe.TestHandle moved = pass_owner(token) + result[int64, string] observed = pass_view(view moved, view net) + resource_probe.close(moved) + return nothing +"#; +pub(super) fn checked(source: &str, release: bool) -> Arc { + checked_support(source, SUPPORT, release) +} +pub(super) fn checked_support( + source: &str, + support: &str, + release: bool, +) -> Arc { + let stdlib = FileId::new(10_000); + let project = FileId::new(0); + let mut parsed = parse(support, stdlib); + let primary = parse(source, project); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(primary.errors.is_empty(), "{:?}", primary.errors); + let declaration = parsed + .module + .items + .iter() + .find_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .unwrap(); + parsed.module.items.extend(primary.module.items); + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: declaration, + member: member.into(), + recipe, + }) + .collect::>(); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (stdlib, SourceOrigin::Stdlib), + (project, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("{error:?}; {:?}", error.diagnostics())), + ) +} +pub(super) fn lowered(checked: &Arc) -> Program { + let hir = hir::lower_checked_resource_program(checked).unwrap(); + lower(&hir, &checked.checked().interner).unwrap() +} +fn named<'a>(plan: &'a ResourceOwnershipPlan<'_>, name: &str) -> &'a ResourceFunctionPlan { + plan.functions() + .iter() + .find(|function| { + function.identity().declaration.namespace == "app" + && function.identity().declaration.name == name + }) + .expect("exact source test function") +} +#[test] +fn resource_ownership_source_factory_move_return_borrow_close_and_sum_cfg() { + for release in [false, true] { + let original = checked(SOURCE, release); + let types = &original.checked().interner; + let mir = lowered(&original); + let plan = validate_resource_ownership(&mir, types).unwrap(); + let scenario = named(&plan, "scenario"); + assert!(scenario.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::SumTake { success: true, .. } + ))); + assert!( + scenario + .operations() + .iter() + .any(|operation| matches!(operation.role(), ResourceOperationRole::Borrow { .. })) + ); + assert!(scenario.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::InvokeSourceFunction { .. } + ))); + let retained = named(&plan, "pass_view"); + assert!(retained.loans().iter().any(|loan| matches!( + loan.source(), + ResourceLoanSource::IncomingViewFormal { parameter: 0, .. } + ))); + assert!(retained.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::BoundedBorrowUse { parameter: 1, .. } + ))); + assert!( + !retained + .operations() + .iter() + .any(|operation| matches!(operation.role(), ResourceOperationRole::Acquire { .. })) + ); + let returned = named(&plan, "pass_owner"); + let return_frame = returned + .frames() + .iter() + .find(|frame| frame.role() == ResourceFrameRole::Return) + .unwrap(); + assert_eq!(returned.frames()[0].parent(), Some(return_frame.id())); + assert!(returned.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::CompleteReturnAfterCleanup { .. } + ))); + assert!( + plan.functions() + .iter() + .flat_map(|function| function.operations()) + .any(|operation| matches!(operation.role(), ResourceOperationRole::Close { .. })) + ); + // Metadata proof does not grant ordinary Move/Copy or native Resource admission. + let function = &mir.functions[scenario.function().index() as usize]; + assert!( + crate::move_values::MoveValuePlan::analyze(&mir, function, types) + .unwrap_err() + .contains("pending ResourceOwnershipPlan") + ); + assert!( + crate::copy_values::CopyValuePlan::analyze(function, types) + .unwrap_err() + .contains("pending ResourceOwnershipPlan") + ); + } +} +#[test] +fn resource_ownership_source_bare_to_view_consumes_one_call_holder_and_ends_its_loan() { + const SOURCE: &str = r#"namespace app +function read_once(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 2) handle error: + return nothing + result[int64, string] observed = read_once(token, view net) + return nothing +"#; + for release in [false, true] { + let original = checked(SOURCE, release); + let mir = lowered(&original); + let plan = validate_resource_ownership(&mir, &original.checked().interner).unwrap(); + let function = named(&plan, "scenario"); + let operations = function.operations(); + let (invoke, frame, loan) = operations + .iter() + .enumerate() + .find_map(|(index, operation)| { + let ResourceOperationRole::InvokeSourceFunction { + source, operands, .. + } = operation.role() + else { + return None; + }; + if !source + .arguments + .iter() + .any(|argument| argument.effect == Effect::RelinquishOwned) + { + return None; + } + let loan = operands.iter().find_map(|operand| { + if let ResourceCallOperand::Borrowed { parameter: 0, loan } = operand { + Some(*loan) + } else { + None + } + })?; + Some((index, operation.frame(), loan)) + }) + .expect("original bare owner to View formal"); + let source = function.loans()[loan.index()].source(); + let ResourceLoanSource::Owner(holder) = source else { + panic!("no owner from resident formal"); + }; + assert_eq!(function.owner_slots()[holder.index()].frame(), frame); + assert!(matches!( + function.owner_slots()[holder.index()].storage(), + ResourceSlotStorage::Argument { parameter: 0, .. } + )); + let ended = operations.iter().position(|operation| matches!(operation.role(), ResourceOperationRole::EndBorrow { loan: actual } if *actual == loan)).unwrap(); + let dropped = operations.iter().position(|operation| matches!(operation.role(), ResourceOperationRole::Drop { source, .. } if *source == holder)).unwrap(); + assert!(invoke < ended && ended < dropped); + } +} +#[test] +fn resource_ownership_source_named_order_holds_earlier_owner_for_later_terminal_actual() { + const SOURCE: &str = r#"namespace app +function consume_pair(label: int64, token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 3) handle error: + return nothing + consume_pair(token: token, label: resource_probe.terminal_label()) + return nothing +"#; + for release in [false, true] { + let original = checked(SOURCE, release); + let mir = lowered(&original); + let plan = validate_resource_ownership(&mir, &original.checked().interner).unwrap(); + let function = named(&plan, "scenario"); + let invocation = function.operations().iter().find(|operation| matches!(operation.role(), ResourceOperationRole::InvokeSourceFunction { evaluation_order, .. } if evaluation_order == &[1, 0])).unwrap(); + let ResourceOperationRole::InvokeSourceFunction { operands, .. } = invocation.role() else { + unreachable!() + }; + let holder = operands + .iter() + .find_map(|operand| { + if let ResourceCallOperand::Owned { parameter: 1, slot } = operand { + Some(*slot) + } else { + None + } + }) + .unwrap(); + assert_eq!( + function.owner_slots()[holder.index()].frame(), + invocation.frame() + ); + assert!(function.operations().iter().take(invocation.id().index()).any(|operation| matches!(operation.role(), ResourceOperationRole::Transfer { destination, .. } if *destination == holder))); + assert!( + function + .frames() + .iter() + .any(|frame| frame.id() == invocation.frame() + && frame.parent() == Some(ResourceFrameId(0))) + ); + // The actual failing Source helper is not executed here; its caller holder + // remains an operation cleanup destination, distinct from callee custody. + } +} +#[test] +fn resource_ownership_source_current_graph_and_original_witness_mutations_fail_closed() { + for release in [false, true] { + let original = checked(SOURCE, release); + let types = &original.checked().interner; + let mir = lowered(&original); + validate_resource_ownership(&mir, types).unwrap(); + for mutation in 0..7 { + let mut changed = mir.clone(); + let function = changed + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "scenario" + }) + .unwrap(); + match mutation { + 0 => function.resource_lowering = None, + 1 => function.span.start += 1, + 2 => function.locals[0].mutable = !function.locals[0].mutable, + 3 => function.blocks[0].terminator.span.start += 1, + 4 => function.blocks[0].statements[0].span.start += 1, + 5 => function.identity.declaration.name.push_str("_foreign"), + 6 => changed.resource_manifest = hir::ResourceManifest::empty(), + _ => unreachable!(), + } + assert!( + validate_resource_ownership(&changed, types).is_err(), + "mutation {mutation}" + ); + } + } +} +#[test] +fn resource_ownership_source_checked_compaction_keeps_archive_and_dense_current_graph() { + for release in [false, true] { + let original = checked(SOURCE, release); + let types = &original.checked().interner; + let mut mir = lowered(&original); + let archives = mir + .functions + .iter() + .filter_map(|function| { + function + .resource_lowering + .as_ref() + .map(|witness| (function.id, witness.original.clone())) + }) + .collect::>(); + crate::prepare_native_generated_functions(&mut mir, types); + validate_resource_ownership(&mir, types).unwrap(); + for (id, archived) in archives { + let witness = mir.functions[id.index() as usize] + .resource_lowering + .as_ref() + .unwrap(); + assert!(crate::breakpoint_regions::hir_blocks_equal( + &archived.body, + &witness.original.body + )); + assert_eq!(archived.locals, witness.original.locals); + for (index, local) in mir.functions[id.index() as usize].locals.iter().enumerate() { + assert_eq!(local.id.index() as usize, index); + } + } + } +} +#[test] +fn resource_ownership_bitwise_source_archive_and_constructor_capture_reject_replaced_nodes() { + let original = checked(SOURCE, false); + let types = &original.checked().interner; + let hir = hir::lower_checked_resource_program(&original).unwrap(); + let function = hir + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "resource_probe" + && function.identity.declaration.name == "create" + }) + .unwrap(); + let mut capture = Capture::authenticated( + function, + &hir.resource_manifest, + &hir.resource_source, + types, + &authenticate_original(&hir, types).unwrap(), + ); + let hir::StatementKind::Return(Some(value)) = &function.body.statements[0].kind else { + panic!("source factory return"); + }; + assert!(capture.preserve_original_call(value, types).unwrap()); + let mut replaced = value.clone(); + replaced.span.start += 1; + assert!(capture.preserve_original_call(&replaced, types).is_err()); + let mut a = Expression { + kind: hir::ExpressionKind::Float(f64::from_bits(0x7ff8_0000_0000_0011)), + ty: TypeInterner::FLOAT64, + span: value.span, + }; + assert!(crate::breakpoint_regions::expressions_equal(&a, &a.clone())); + let mut b = a.clone(); + b.kind = hir::ExpressionKind::Float(f64::from_bits(0x7ff8_0000_0000_0012)); + assert!(!crate::breakpoint_regions::expressions_equal(&a, &b)); + a.kind = hir::ExpressionKind::Float(0.0); + b.kind = hir::ExpressionKind::Float(-0.0); + assert!(!crate::breakpoint_regions::expressions_equal(&a, &b)); +} +#[test] +fn resource_ownership_pending_nested_region_keeps_source_validity_and_no_fake_root_lifetime() { + const SOURCE: &str = r#"namespace app +function scenario(view net: Network) returns nothing: + use resource_probe + if true: + resource_probe.TestHandle token = resource_probe.create(view net, 4) handle error: + return nothing + resource_probe.close(token) + return nothing +"#; + for release in [false, true] { + let original = checked(SOURCE, release); + let mir = lowered(&original); + assert!( + validate_resource_ownership(&mir, &original.checked().interner) + .unwrap_err() + .iter() + .any(|error| error.message.contains("nested Resource declarations")) + ); + // This is an implementation prerequisite, never a checker language exclusion. + validate_call_ownership(&mir, &original.checked().interner).unwrap(); + } +} + +#[test] +fn resource_ownership_original_hir_archive_cannot_authenticate_edited_body_or_empty_archive() { + for release in [false, true] { + let original = checked(SOURCE, release); + let types = &original.checked().interner; + let initial = hir::lower_checked_resource_program(&original).unwrap(); + for mutation in 0..3 { + let mut changed = initial.clone(); + if mutation == 0 { + let function = changed + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "scenario" + }) + .unwrap(); + let hir::StatementKind::Let { value, .. } = &mut function.body.statements[0].kind + else { + panic!("original token declaration"); + }; + let hir::ExpressionKind::Handle { target, .. } = &mut value.kind else { + panic!("original factory handle"); + }; + let hir::ExpressionKind::Call { args, .. } = &mut target.kind else { + panic!("original factory Source call"); + }; + let hir::ExpressionKind::Int(number) = &mut args[1].kind else { + panic!("original factory label occurrence"); + }; + *number += 1; + } else if mutation == 1 { + changed.resource_source = hir::ResourceSourceArchive::empty(); + } else { + changed.functions[0].debug_kind = hir::FunctionDebugKind::Inline; + } + // Existing public HIR gates do not themselves mint an original-body archive. + hir::validate(&changed).unwrap(); + hir::validate_backend_types(&changed, types).unwrap(); + assert!( + lower(&changed, types) + .unwrap_err() + .iter() + .any(|error| error.message.contains("original checked HIR archive")) + ); + } + } +} +#[test] +fn resource_ownership_original_type_graph_detects_resource_field_removal_and_accepts_additions() { + const SOURCE: &str = r#"namespace app +struct Holder: + token: optional[resource_probe.TestHandle] +function inspect(holder: Holder) returns nothing: + return nothing +"#; + for release in [false, true] { + let original = checked(SOURCE, release); + let original_types = &original.checked().interner; + let hir = hir::lower_checked_resource_program(&original).unwrap(); + // Rebuild original nominal tables before copying their stable TypeIds. + let mut types = TypeInterner::new(); + let inspected = hir + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "inspect" + }) + .unwrap(); + let Type::Struct(id) = original_types.resolve(inspected.params[0].ty) else { + panic!("the checked inspect formal must be the Source Holder"); + }; + let id = *id; + let definition = original_types.resolve_struct(id).clone(); + assert!(matches!( + original_types.resolve(definition.fields[0].1), + Type::Optional(_) + )); + macro_rules! copy_nominals { + ($variant:ident, $resolve:ident, $add:ident) => { + let definitions = original_types + .type_ids() + .filter_map(|ty| match original_types.resolve(ty) { + Type::$variant(definition_id) => Some(( + definition_id.index(), + ( + *definition_id, + original_types.$resolve(*definition_id).clone(), + ), + )), + _ => None, + }) + .collect::>(); + for (definition_id, original_definition) in definitions.into_values() { + assert_eq!(types.$add(original_definition), definition_id); + } + }; + } + copy_nominals!(Struct, resolve_struct, add_struct); + copy_nominals!(Bitfield, resolve_bitfield, add_bitfield); + copy_nominals!(Enum, resolve_enum, add_enum); + copy_nominals!(Interface, resolve_interface, add_interface); + copy_nominals!(Actor, resolve_actor, add_actor); + copy_nominals!(Machine, resolve_machine, add_machine); + for ty in original_types.type_ids() { + assert_eq!(types.intern(original_types.resolve(ty).clone()), ty); + } + for ty in original_types.type_ids() { + let args = original_types.nominal_type_arguments(ty); + if !args.is_empty() { + types + .register_nominal_type_arguments(ty, args.to_vec()) + .unwrap(); + } + } + hir.resource_source.validate_types(&types).unwrap(); + types.intern(Type::Optional(TypeInterner::UINT64)); + hir.resource_source.validate_types(&types).unwrap(); + let mut erased = definition.clone(); + erased.fields.clear(); + types.update_struct(id, erased); + assert!( + hir.resource_source + .validate_types(&types) + .unwrap_err() + .contains("type meaning or nominal metadata") + ); + assert!( + lower(&hir, &types) + .unwrap_err() + .iter() + .any(|error| error.message.contains("type meaning or nominal metadata")) + ); + let mut foreign = definition; + foreign.fields[0].1 = TypeInterner::STRING; + types.update_struct(id, foreign); + assert!(hir.resource_source.validate_types(&types).is_err()); + } +} + +const MUTABLE_REPLACEMENT_SOURCE: &str = include_str!( + "../../../jett_codegen_cranelift/src/emit/resource_execution/replacement/mutable_assignment.jett" +); + +#[test] +fn resource_ownership_replacement_seals_the_actual_rhs_and_original_mutable_site() { + for release in [false, true] { + let original = checked(MUTABLE_REPLACEMENT_SOURCE, release); + let program = lowered(&original); + let plan = validate_resource_ownership(&program, &original.checked().interner).unwrap(); + for name in ["replace_live", "failed_rhs_keeps_owner"] { + let function = named(&plan, name); + let replacements = function + .operations() + .iter() + .filter(|operation| { + matches!(operation.role(), ResourceOperationRole::Replace { .. }) + }) + .collect::>(); + assert_eq!(replacements.len(), 1, "{name}"); + let operation = replacements[0]; + let ResourceOperationRole::Replace { + destination, + replacement, + old, + } = operation.role() + else { + unreachable!(); + }; + assert_ne!(destination, replacement); + assert_eq!(*old, ResourceOccupancy::Occupied); + assert!(!operation.is_expression_operation()); + let current = &program.functions[function.function().index() as usize]; + let ResourcePosition::Statement(index) = operation.site().position() else { + panic!("assignment statement site"); + }; + let StatementKind::Assign { target, value } = + ¤t.blocks[operation.site().block().index() as usize].statements[index].kind + else { + panic!("exact original assignment"); + }; + let (ExpressionKind::Local(target), ExpressionKind::Local(rhs)) = + (&target.kind, &value.kind) + else { + panic!("canonical handled Source RHS and local destination"); + }; + let ResourceSlotStorage::Local { header } = + function.owner_slots()[destination.index()].storage() + else { + panic!("original mutable owning header"); + }; + assert_eq!(header.id, *target); + assert!(header.mutable); + assert_eq!( + function.owner_slots()[destination.index()].shape(), + function.owner_slots()[replacement.index()].shape() + ); + let ResourceSlotStorage::Local { header: rhs_header } = + function.owner_slots()[replacement.index()].storage() + else { + panic!("evaluated canonical RHS local"); + }; + assert_eq!(rhs_header.id, *rhs); + assert!(function.operations().iter().any(|other| other.site() == operation.site() + && matches!(other.role(), ResourceOperationRole::Transfer { source, destination: selected } + if source == replacement && selected == destination))); + } + } +} + +#[test] +fn resource_ownership_self_rebind_and_closed_destination_keep_distinct_exact_roles() { + for release in [false, true] { + let original = checked(MUTABLE_REPLACEMENT_SOURCE, release); + let program = lowered(&original); + let plan = validate_resource_ownership(&program, &original.checked().interner).unwrap(); + let function = named(&plan, "rebind_self"); + let reseats = function + .operations() + .iter() + .filter(|operation| { + matches!(operation.role(), ResourceOperationRole::SelfRebind { .. }) + }) + .collect::>(); + assert_eq!(reseats.len(), 1); + let operation = reseats[0]; + let ResourceOperationRole::SelfRebind { slot } = operation.role() else { + unreachable!() + }; + let ResourceSlotStorage::Local { header } = function.owner_slots()[slot.index()].storage() + else { + panic!("owning local") + }; + assert!(header.mutable); + let current = &program.functions[function.function().index() as usize]; + let ResourcePosition::Statement(index) = operation.site().position() else { + panic!("assignment") + }; + let StatementKind::Assign { target, value } = + ¤t.blocks[operation.site().block().index() as usize].statements[index].kind + else { + panic!("self assignment") + }; + assert!(matches!(target.kind, ExpressionKind::Local(local) if local == header.id)); + assert!(matches!(value.kind, ExpressionKind::Local(local) if local == header.id)); + assert!( + !function + .operations() + .iter() + .any(|other| other.site() == operation.site() + && matches!( + other.role(), + ResourceOperationRole::Replace { .. } + | ResourceOperationRole::Transfer { .. } + | ResourceOperationRole::Drop { .. } + )) + ); + let closed = named(&plan, "rebind_after_close"); + assert!(!closed.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::Replace { .. } | ResourceOperationRole::SelfRebind { .. } + ))); + let current = &program.functions[closed.function().index() as usize]; + assert!(closed.operations().iter().any(|operation| { + let ResourcePosition::Statement(index) = operation.site().position() else { + return false; + }; + matches!( + current.blocks[operation.site().block().index() as usize].statements[index].kind, + StatementKind::Assign { .. } + ) && matches!(operation.role(), ResourceOperationRole::Transfer { .. }) + })); + } +} + +#[test] +fn resource_ownership_replacement_and_reseat_reject_changed_public_source_or_header() { + for release in [false, true] { + let original = checked(MUTABLE_REPLACEMENT_SOURCE, release); + let program = lowered(&original); + let types = &original.checked().interner; + for mutation in 0..3 { + let mut changed = program.clone(); + let function = changed + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "rebind_self" + }) + .unwrap(); + let assignment = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find(|statement| matches!(statement.kind, StatementKind::Assign { .. })) + .unwrap(); + let StatementKind::Assign { target, value } = &mut assignment.kind else { + unreachable!() + }; + let ExpressionKind::Local(local) = target.kind else { + unreachable!() + }; + if mutation == 0 { + function.locals[local.index() as usize].mutable = false; + } else if mutation == 1 { + value.kind = ExpressionKind::View(Box::new(value.clone())); + } else { + value.span.start += 1; + } + assert!(validate_resource_ownership(&changed, types).is_err()); + assert!(validate_witnesses(&changed, types).is_err()); + } + } +} diff --git a/crates/jett_mir/src/resource_ownership/walk.rs b/crates/jett_mir/src/resource_ownership/walk.rs new file mode 100644 index 00000000..0f775c5f --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/walk.rs @@ -0,0 +1,437 @@ +//! Dependency-only traversal; visits neither mint custody nor change context. +use super::*; +use hir::{ExpressionKind as E, StatementKind as H}; + +pub(super) fn expression(value: &Expression, visit: &mut impl FnMut(&Expression)) { + visit(value); + match &value.kind { + E::Binary { left, right, .. } => { + expression(left, visit); + expression(right, visit); + } + E::Unary { value, .. } + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::Declassify(value) + | E::Coarsen(value) + | E::RefinementValidated(value) + | E::InterfaceType(value) + | E::RuntimeFailureMessage(value) + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::View(value) + | E::Clone(value) + | E::Comptime { value, .. } + | E::InterfaceCoerce { value, .. } + | E::FunctionAdapter { value, .. } + | E::StateIs { value, .. } => expression(value, visit), + E::Call { args, .. } + | E::ResourceInvoke { args, .. } + | E::Intrinsic { args, .. } + | E::ActorSpawn { args, .. } => { + for value in args { + expression(value, visit); + } + } + E::IndirectCall { callee, args, .. } => { + expression(callee, visit); + for value in args { + expression(value, visit); + } + } + E::ActorMessage { actor, args, .. } => { + expression(actor, visit); + for value in args { + expression(value, visit); + } + } + E::StructConstruct { fields, .. } | E::BitfieldConstruct { fields, .. } => { + for value in fields { + expression(value, visit); + } + } + E::MachineConstruct { payloads, .. } | E::EnumConstruct { payloads, .. } => { + for value in payloads { + expression(value, visit); + } + } + E::MachineTransition { + source, payloads, .. + } => { + expression(source, visit); + for value in payloads { + expression(value, visit); + } + } + E::ListConstruct { elements } => { + for value in elements { + expression(value, visit); + } + } + E::MapConstruct { entries } => { + for entry in entries { + expression(&entry.key, visit); + expression(&entry.value, visit); + } + } + E::Handle { + target, failure, .. + } => { + expression(target, visit); + hir_block(failure, visit); + } + E::StringInterpolation(parts) => { + for part in parts { + if let hir::StringSegment::Value(value) = part { + expression(value, visit); + } + } + } + E::InlineFunction { body, .. } => hir_block(body, visit), + E::Field { base, .. } => expression(base, visit), + E::Int(_) + | E::Float(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::Local(_) + | E::Constant { .. } + | E::FunctionRef(_) + | E::ResourceHookValue { .. } + | E::ClosureRef { .. } + | E::OptionalNone + | E::RuntimeFailure(_) + | E::PropertyCaseContext(_) => {} + } +} +pub(super) fn hir_block(block: &hir::Block, visit: &mut impl FnMut(&Expression)) { + for statement in &block.statements { + match &statement.kind { + H::Let { value, .. } + | H::HandleDefault(value) + | H::Expression(value) + | H::Respond(value) => expression(value, visit), + H::Assign { target, value } => { + expression(target, visit); + expression(value, visit); + } + H::Return(value) => { + if let Some(value) = value { + expression(value, visit); + } + } + H::If { + condition, + then_block, + else_block, + } => { + expression(condition, visit); + hir_block(then_block, visit); + if let Some(block) = else_block { + hir_block(block, visit); + } + } + H::While { condition, body } => { + expression(condition, visit); + hir_block(body, visit); + } + H::For { iterable, body, .. } => { + expression(iterable, visit); + hir_block(body, visit); + } + H::Match { scrutinee, arms } => { + expression(scrutinee, visit); + for arm in arms { + hir_block(&arm.body, visit); + } + } + H::Assert { condition, message } => { + expression(condition, visit); + if let Some(value) = message { + expression(value, visit); + } + } + H::Breakpoint { condition, .. } => { + if let Some(value) = condition { + expression(value, visit); + } + } + H::Scope(block) => hir_block(block, visit), + H::ReflectedTypeDispatch { type_info, arms } => { + expression(type_info, visit); + for arm in arms { + hir_block(&arm.body, visit); + } + } + H::Break | H::Continue | H::Trace(_) => {} + } + } +} +pub(crate) fn mir_block(block: &BasicBlock, visit: &mut impl FnMut(&Expression)) { + for statement in &block.statements { + match &statement.kind { + StatementKind::ResourceConstructor(ResourceConstructorNode::Stage { + value, .. + }) + | StatementKind::ResourceCall(ResourceCallNode::Stage { value, .. }) + | StatementKind::Let { value, .. } + | StatementKind::BeginCallView { value, .. } + | StatementKind::CheckRefinement { call: value, .. } + | StatementKind::Evaluate(value) + | StatementKind::HandleDefault(value) => expression(value, visit), + StatementKind::Assign { target, value } => { + expression(target, visit); + expression(value, visit); + } + StatementKind::Assert { condition, message } => { + expression(condition, visit); + if let Some(value) = message { + expression(value, visit); + } + } + StatementKind::Breakpoint { condition, .. } => { + if let Some(value) = condition { + expression(value, visit); + } + } + StatementKind::ResourceCall( + ResourceCallNode::Begin { .. } + | ResourceCallNode::Invoke { .. } + | ResourceCallNode::End { .. }, + ) + | StatementKind::ResourceConstructor( + ResourceConstructorNode::Begin { .. } + | ResourceConstructorNode::Commit { .. } + | ResourceConstructorNode::End { .. }, + ) + | StatementKind::ResourceLexicalExit(_) + | StatementKind::OpenCallOwnerGeneration { .. } + | StatementKind::ReplaceCallOwnerGeneration { .. } + | StatementKind::CloseCallOwnerGeneration { .. } + | StatementKind::EndCallView { .. } + | StatementKind::ReflectedContainerReady { .. } + | StatementKind::ReflectedValueReady { .. } + | StatementKind::SequenceLength { .. } + | StatementKind::SequenceGet { .. } + | StatementKind::IterationBorrow { .. } + | StatementKind::SumTag { .. } + | StatementKind::SumTake { .. } + | StatementKind::Trace(_) => {} + } + } + match &block.terminator.kind { + TerminatorKind::Return(value) => { + if let Some(value) = value { + expression(value, visit); + } + } + TerminatorKind::Respond(value) + | TerminatorKind::Branch { + condition: value, .. + } + | TerminatorKind::Switch { + scrutinee: value, .. + } + | TerminatorKind::ForEach { + iterable: value, .. + } + | TerminatorKind::ReflectedTypeDispatch { + type_info: value, .. + } => expression(value, visit), + TerminatorKind::Goto(_) | TerminatorKind::Unreachable => {} + } +} +pub(super) fn mir_block_has_custody( + block: &BasicBlock, + function: &Function, + types: &TypeInterner, +) -> bool { + let mut found = false; + mir_block(block, &mut |value| { + found |= resource_type_pending(types, value.ty) + && !matches!(types.resolve(value.ty), Type::Function { .. }); + }); + // Sums, sequence operations and Switch binders hold typed local headers + // rather than expression nodes. + // Normalized calls also own an exact operation prefix at header-only sites. + let occupied = |id| { + function + .local(id) + .is_some_and(|local| custody_type(types, local.ty)) + }; + found + || block + .statements + .iter() + .any(|statement| match &statement.kind { + StatementKind::SumTag { source, target } + | StatementKind::SumTake { source, target, .. } => { + occupied(*source) || occupied(*target) + } + StatementKind::SequenceLength { source, target } + | StatementKind::SequenceGet { source, target, .. } => { + occupied(source.root()) || occupied(*target) + } + StatementKind::IterationBorrow { source, .. } => occupied(source.root()), + StatementKind::ResourceCall(_) | StatementKind::ResourceConstructor(_) => true, + _ => false, + }) + || match &block.terminator.kind { + TerminatorKind::Switch { variants, .. } => variants + .iter() + .any(|(_, _, bindings)| bindings.iter().any(|local| occupied(*local))), + _ => false, + } +} +pub(super) fn mir_block_has_resource(block: &BasicBlock) -> bool { + let mut found = false; + mir_block(block, &mut |value| { + found |= matches!( + value.kind, + E::ResourceInvoke { .. } | E::ResourceHookValue { .. } + ); + }); + found +} + +/// Source scope admission is finite: no resource declaration gets a guessed root lifetime. +pub(super) fn nested_resource_declaration( + block: &hir::Block, + function: &hir::Function, + types: &TypeInterner, + nested: bool, + admitted: &[LocalId], +) -> bool { + for statement in &block.statements { + if let H::Let { local, .. } = &statement.kind + && nested + && !admitted.contains(local) + && function + .locals + .get(local.index() as usize) + .is_some_and(|local| resource_type_pending(types, local.ty)) + { + return true; + } + let found = match &statement.kind { + H::If { + then_block, + else_block, + .. + } => { + nested_resource_declaration(then_block, function, types, true, admitted) + || else_block.as_ref().is_some_and(|block| { + nested_resource_declaration(block, function, types, true, admitted) + }) + } + H::While { body, .. } | H::For { body, .. } | H::Scope(body) => { + nested_resource_declaration(body, function, types, true, admitted) + } + H::Match { arms, .. } => arms + .iter() + .any(|arm| nested_resource_declaration(&arm.body, function, types, true, admitted)), + H::ReflectedTypeDispatch { arms, .. } => arms + .iter() + .any(|arm| nested_resource_declaration(&arm.body, function, types, true, admitted)), + _ => false, + }; + if found { + return true; + } + let mut inside = false; + let single = hir::Block { + statements: vec![statement.clone()], + span: statement.span, + }; + hir_block(&single, &mut |value| { + if let E::Handle { failure, .. } = &value.kind { + inside |= nested_resource_declaration(failure, function, types, true, admitted); + } + }); + if inside { + return true; + } + } + false +} + +/// Visit only expressions physically owned by this exact current statement/terminator. +pub(super) fn mir_site(block: &BasicBlock, position: usize, visit: &mut impl FnMut(&Expression)) { + if let Some(statement) = block.statements.get(position) { + match &statement.kind { + StatementKind::ResourceConstructor(ResourceConstructorNode::Stage { + value, .. + }) + | StatementKind::ResourceCall(ResourceCallNode::Stage { value, .. }) + | StatementKind::Let { value, .. } + | StatementKind::BeginCallView { value, .. } + | StatementKind::CheckRefinement { call: value, .. } + | StatementKind::Evaluate(value) + | StatementKind::HandleDefault(value) => expression(value, visit), + StatementKind::Assign { target, value } => { + expression(target, visit); + expression(value, visit); + } + StatementKind::Assert { condition, message } => { + expression(condition, visit); + if let Some(value) = message { + expression(value, visit); + } + } + StatementKind::Breakpoint { condition, .. } => { + if let Some(value) = condition { + expression(value, visit); + } + } + StatementKind::ResourceCall( + ResourceCallNode::Begin { .. } + | ResourceCallNode::Invoke { .. } + | ResourceCallNode::End { .. }, + ) + | StatementKind::ResourceConstructor( + ResourceConstructorNode::Begin { .. } + | ResourceConstructorNode::Commit { .. } + | ResourceConstructorNode::End { .. }, + ) + | StatementKind::ResourceLexicalExit(_) + | StatementKind::OpenCallOwnerGeneration { .. } + | StatementKind::ReplaceCallOwnerGeneration { .. } + | StatementKind::CloseCallOwnerGeneration { .. } + | StatementKind::EndCallView { .. } + | StatementKind::ReflectedContainerReady { .. } + | StatementKind::ReflectedValueReady { .. } + | StatementKind::SequenceLength { .. } + | StatementKind::SequenceGet { .. } + | StatementKind::IterationBorrow { .. } + | StatementKind::SumTag { .. } + | StatementKind::SumTake { .. } + | StatementKind::Trace(_) => {} + } + } else if position == block.statements.len() { + match &block.terminator.kind { + TerminatorKind::Return(value) => { + if let Some(value) = value { + expression(value, visit); + } + } + TerminatorKind::Respond(value) + | TerminatorKind::Branch { + condition: value, .. + } + | TerminatorKind::Switch { + scrutinee: value, .. + } + | TerminatorKind::ForEach { + iterable: value, .. + } + | TerminatorKind::ReflectedTypeDispatch { + type_info: value, .. + } => expression(value, visit), + TerminatorKind::Goto(_) | TerminatorKind::Unreachable => {} + } + } +} diff --git a/crates/jett_mir/src/sequences.rs b/crates/jett_mir/src/sequences.rs index b5f9cdd2..eeb26215 100644 --- a/crates/jett_mir/src/sequences.rs +++ b/crates/jett_mir/src/sequences.rs @@ -71,7 +71,10 @@ pub fn prepare_native_sequences(program: &mut Program, types: &TypeInterner) { return; } for function in &mut program.functions { - let before_pass = (!function.breakpoint_regions.is_empty() + let before_pass = (crate::resource_ownership::has_records(function) + || function.reflected_callable.is_some() + || crate::ordinary_borrowed_sums::has_records(function) + || !function.breakpoint_regions.is_empty() || crate::call_owner_generations::has_records(function)) .then(|| function.clone()); let mut valid_regions = true; @@ -146,8 +149,10 @@ pub fn prepare_native_sequences(program: &mut Program, types: &TypeInterner) { { continue; } - let before_iteration = (!function.breakpoint_regions.is_empty() - || crate::call_owner_generations::has_records(function)) + let before_iteration = (crate::resource_ownership::has_records(function) + || !function.breakpoint_regions.is_empty() + || crate::call_owner_generations::has_records(function) + || crate::ordinary_borrowed_sums::has_records(function)) .then(|| function.clone()); let mut region_edit = crate::breakpoint_regions::SequenceEdit { header, @@ -250,13 +255,20 @@ pub fn prepare_native_sequences(program: &mut Program, types: &TypeInterner) { function.blocks[index].terminator.kind = TerminatorKind::Goto(exit); region_edit.after = function.blocks[index].terminator.clone(); if let Some(before_iteration) = &before_iteration - && (crate::call_owner_generations::sequence_transition( + && (crate::resource_ownership::sequence_transition( function, before_iteration, ®ion_edit, types, ) .is_err() + || crate::call_owner_generations::sequence_transition( + function, + before_iteration, + ®ion_edit, + types, + ) + .is_err() || crate::breakpoint_regions::sequence_transition( function, before_iteration, @@ -410,13 +422,27 @@ pub fn prepare_native_sequences(program: &mut Program, types: &TypeInterner) { region_edit.after = old.clone(); } if let Some(before_iteration) = &before_iteration - && (crate::call_owner_generations::sequence_transition( + && (crate::resource_ownership::sequence_transition( function, before_iteration, ®ion_edit, types, ) .is_err() + || crate::ordinary_borrowed_sums::sequence_transition( + function, + before_iteration, + ®ion_edit, + types, + ) + .is_err() + || crate::call_owner_generations::sequence_transition( + function, + before_iteration, + ®ion_edit, + types, + ) + .is_err() || crate::breakpoint_regions::sequence_transition( function, before_iteration, @@ -440,10 +466,18 @@ pub fn prepare_native_sequences(program: &mut Program, types: &TypeInterner) { ); } } - if !valid_regions || (removed_uninhabited_body && !prune::unreachable(function)) { + if !valid_regions + || crate::ordinary_borrowed_sums::validate_current(function).is_err() + || crate::resource_ownership::validate_current(function).is_err() + || (removed_uninhabited_body && !prune::unreachable(function)) + { if let Some(before_pass) = before_pass { *function = before_pass; } + } else if let Some(before_pass) = before_pass { + if crate::reflected_callable::prepared(function, &before_pass).is_err() { + *function = before_pass; + } } } } diff --git a/crates/jett_mir/src/sequences/prune.rs b/crates/jett_mir/src/sequences/prune.rs index 36d9f02b..e401f9e7 100644 --- a/crates/jett_mir/src/sequences/prune.rs +++ b/crates/jett_mir/src/sequences/prune.rs @@ -4,8 +4,13 @@ use super::*; pub(crate) fn unreachable(function: &mut Function) -> bool { + if crate::resource_ownership::constructors::validate_nodes(function).is_err() { + return false; + } let before = (!function.breakpoint_regions.is_empty() - || crate::call_owner_generations::has_records(function)) + || crate::call_owner_generations::has_records(function) + || crate::resource_ownership::has_records(function) + || crate::ordinary_borrowed_sums::has_records(function)) .then(|| function.clone()); let Ok(cfg) = ControlFlowGraph::analyze(function) else { return false; @@ -50,6 +55,8 @@ pub(crate) fn unreachable(function: &mut Function) -> bool { if before.as_ref().is_some_and(|before| { crate::call_owner_generations::remap_blocks(function, before, &blocks).is_err() + || crate::resource_ownership::remap_blocks(function, before, &blocks).is_err() + || crate::ordinary_borrowed_sums::remap_blocks(function, before, &blocks).is_err() }) || crate::breakpoint_regions::remap_blocks(function, &blocks).is_err() || !unused_locals(function) { @@ -64,8 +71,13 @@ pub(crate) fn unreachable(function: &mut Function) -> bool { /// Remove absent frame slots without changing any original control-flow block. /// Every parameter, binding, read, capture and debug or loan reference survives. pub(crate) fn unused_locals(function: &mut Function) -> bool { + if crate::resource_ownership::constructors::validate_nodes(function).is_err() { + return false; + } let before = (!function.breakpoint_regions.is_empty() - || crate::call_owner_generations::has_records(function)) + || crate::call_owner_generations::has_records(function) + || crate::resource_ownership::has_records(function) + || crate::ordinary_borrowed_sums::has_records(function)) .then(|| function.clone()); let mut used = vec![false; function.locals.len()]; for parameter in &function.params { @@ -137,7 +149,10 @@ pub(crate) fn unused_locals(function: &mut Function) -> bool { function .original_view_iterations .retain_mut(|record| record.remap_locals(&locals)); - if crate::call_owner_generations::remap_locals(function, &locals).is_err() + if before.as_ref().is_some_and(|before| { + crate::resource_ownership::remap_locals(function, before, &locals).is_err() + || crate::ordinary_borrowed_sums::remap_locals(function, before, &locals).is_err() + }) || crate::call_owner_generations::remap_locals(function, &locals).is_err() || crate::breakpoint_regions::remap_locals(function, &locals).is_err() { if let Some(before) = before { @@ -220,6 +235,26 @@ fn visit_block_locals( ) { for statement in &mut block.statements { match &mut statement.kind { + StatementKind::ResourceLexicalExit(_) => {} + StatementKind::ResourceConstructor(node) => match node { + crate::ResourceConstructorNode::Stage { value, .. } => { + expression(value, visit, floor, metadata) + } + crate::ResourceConstructorNode::Commit { output, .. } => visit(output), + _ => {} + }, + StatementKind::ResourceCall(node) => match node { + ResourceCallNode::Stage { + value, ordinary, .. + } => { + if let Some(local) = ordinary { + visit(local); + } + expression(value, visit, floor, metadata); + } + ResourceCallNode::Invoke { output, .. } => visit(output), + ResourceCallNode::Begin { .. } | ResourceCallNode::End { .. } => {} + }, StatementKind::OpenCallOwnerGeneration { root, .. } => visit(root), StatementKind::ReplaceCallOwnerGeneration { root, rhs_owner, .. @@ -228,7 +263,8 @@ fn visit_block_locals( visit(rhs_owner); } StatementKind::CloseCallOwnerGeneration { .. } => {} - StatementKind::ReflectedContainerReady { source, .. } => visit(source), + StatementKind::ReflectedContainerReady { source, .. } + | StatementKind::ReflectedValueReady { source, .. } => visit(source), StatementKind::SequenceLength { source, target } => { source_local(source, visit); visit(target); @@ -339,7 +375,8 @@ fn expression( let packet = match &mut value.kind { ExpressionKind::Call { ownership, .. } | ExpressionKind::IndirectCall { ownership, .. } - | ExpressionKind::Intrinsic { ownership, .. } => Some(ownership), + | ExpressionKind::Intrinsic { ownership, .. } + | ExpressionKind::ResourceInvoke { ownership, .. } => Some(ownership), _ => None, }; if let Some(packet) = packet { @@ -387,6 +424,7 @@ fn expression( | ExpressionKind::View(value) | ExpressionKind::Clone(value) => expression(value, visit, floor, metadata), ExpressionKind::Call { args, .. } + | ExpressionKind::ResourceInvoke { args, .. } | ExpressionKind::Intrinsic { args, .. } | ExpressionKind::ActorSpawn { args, .. } | ExpressionKind::StructConstruct { fields: args, .. } @@ -458,6 +496,7 @@ fn expression( | ExpressionKind::Bool(_) | ExpressionKind::Nothing | ExpressionKind::Constant { .. } + | ExpressionKind::ResourceHookValue { .. } | ExpressionKind::FunctionRef(_) | ExpressionKind::OptionalNone | ExpressionKind::RuntimeFailure(_) diff --git a/crates/jett_mir/src/uninhabited_sums.rs b/crates/jett_mir/src/uninhabited_sums.rs index af587c93..841e71f6 100644 --- a/crates/jett_mir/src/uninhabited_sums.rs +++ b/crates/jett_mir/src/uninhabited_sums.rs @@ -106,7 +106,10 @@ pub fn prepare_native_uninhabited_sums(program: &mut Program, types: &TypeIntern return; }; for (function, (absent_plans, present_plans)) in program.functions.iter_mut().zip(plans) { - let before = (!function.breakpoint_regions.is_empty() + let before = (crate::resource_ownership::has_records(function) + || function.reflected_callable.is_some() + || crate::ordinary_borrowed_sums::has_records(function) + || !function.breakpoint_regions.is_empty() || crate::call_owner_generations::has_records(function)) .then(|| function.clone()); let mut changed = false; @@ -186,10 +189,18 @@ pub fn prepare_native_uninhabited_sums(program: &mut Program, types: &TypeIntern crate::call_owner_generations::sum_transition(function, before, types).is_ok() && crate::breakpoint_regions::sum_transition(function, before, types).is_ok() }); - if !regions_valid || !sequences::prune::unreachable(function) { + if !regions_valid + || crate::ordinary_borrowed_sums::validate_current(function).is_err() + || crate::resource_ownership::validate_current(function).is_err() + || !sequences::prune::unreachable(function) + { if let Some(before) = before { *function = before; } + } else if let Some(before) = before { + if crate::reflected_callable::prepared(function, &before).is_err() { + *function = before; + } } } } diff --git a/crates/jett_native_launcher/src/lib.rs b/crates/jett_native_launcher/src/lib.rs index 49c65a79..c469e966 100644 --- a/crates/jett_native_launcher/src/lib.rs +++ b/crates/jett_native_launcher/src/lib.rs @@ -54,6 +54,13 @@ struct RuntimeCall { trait RuntimeLifecycle { fn create(&mut self, out_context: *mut JettRuntimeContextV1) -> RuntimeCall; fn destroy(&mut self, context: *mut JettRuntimeContextV1) -> RuntimeCall; + fn finish_entry( + &mut self, + _context: *mut JettRuntimeContextV1, + outcome: EntryOutcome, + ) -> EntryOutcome { + outcome + } fn failure(&mut self, _context: *mut JettRuntimeContextV1) -> Option { None } @@ -65,6 +72,40 @@ trait RuntimeLifecycle { #[derive(Default)] struct NativeRuntime; +fn resource_failure(context: *mut JettRuntimeContextV1) -> Option { + let mut result = MaybeUninit::uninit(); + // SAFETY: the caller retains the stationary live context and distinct output. + let status = unsafe { + jett_runtime::native_abi::jett_rt_v1_resource_program_failure(context, result.as_mut_ptr()) + }; + (status != JettRuntimeStatusV1::OK).then(|| RuntimeCall { + status, + // SAFETY: the non-overlapping valid result is initialized on return. + result: unsafe { result.assume_init() }, + }) +} + +unsafe fn copy_failure( + context: *mut JettRuntimeContextV1, + buffer: *mut u8, + capacity: u64, + out_length: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + // SAFETY: the caller supplies the same live context and exclusive copy outputs. + unsafe { + if resource_failure(context).is_some() { + jett_runtime::native_abi::jett_rt_v1_resource_program_failure_copy( + context, buffer, capacity, out_length, out_result, + ) + } else { + jett_runtime::native_abi::values::jett_rt_v1_value_failure_copy( + context, buffer, capacity, out_length, out_result, + ) + } + } +} + impl RuntimeLifecycle for NativeRuntime { fn create(&mut self, out_context: *mut JettRuntimeContextV1) -> RuntimeCall { let mut result = MaybeUninit::::uninit(); @@ -87,6 +128,9 @@ impl RuntimeLifecycle for NativeRuntime { } fn failure(&mut self, context: *mut JettRuntimeContextV1) -> Option { + if let Some(failure) = resource_failure(context) { + return Some(failure); + } let mut result = MaybeUninit::uninit(); // SAFETY: launch_with retains the stationary context until destruction. let status = unsafe { @@ -108,7 +152,7 @@ impl RuntimeLifecycle for NativeRuntime { // SAFETY: the live context and distinct outputs remain valid through // this call. A null, zero-capacity buffer queries the required length. let status = unsafe { - jett_runtime::native_abi::values::jett_rt_v1_value_failure_copy( + copy_failure( context, std::ptr::null_mut(), 0, @@ -128,7 +172,7 @@ impl RuntimeLifecycle for NativeRuntime { // SAFETY: `message` owns `length` writable bytes, and all output // records are distinct. The runtime copies while the context is live. let status = unsafe { - jett_runtime::native_abi::values::jett_rt_v1_value_failure_copy( + copy_failure( context, message.as_mut_ptr(), required, @@ -139,6 +183,35 @@ impl RuntimeLifecycle for NativeRuntime { (status == JettRuntimeStatusV1::OK && copied == required).then_some(message) } + fn finish_entry( + &mut self, + context: *mut JettRuntimeContextV1, + outcome: EntryOutcome, + ) -> EntryOutcome { + let (body, panicked) = match outcome { + EntryOutcome::Status(status) => (status, 0), + EntryOutcome::Panicked => (JettRuntimeStatusV1::PANIC.code(), 1), + }; + let mut selected_kind = 0; + // SAFETY: entry control returned; context and separate output remain live. + let status = unsafe { + jett_runtime::native_abi::jett_rt_v1_resource_program_unwind( + context, + body, + panicked, + &mut selected_kind, + ) + }; + if status == JettRuntimeStatusV1::OK { + return outcome; + } + if panicked != 0 && selected_kind == 2 { + EntryOutcome::Panicked + } else { + EntryOutcome::Status(status.code()) + } + } + fn destroy(&mut self, context: *mut JettRuntimeContextV1) -> RuntimeCall { let mut result = MaybeUninit::::uninit(); // SAFETY: `launch_with` calls destroy only for the same initialized, @@ -202,6 +275,11 @@ where Ok(status) => EntryOutcome::Status(status), Err(()) => EntryOutcome::Panicked, }; + let entry_outcome = + match catch_operation(|| runtime.finish_entry(context.as_mut_ptr(), entry_outcome)) { + Ok(outcome) => outcome, + Err(()) => EntryOutcome::Panicked, + }; let entry_failure = runtime.failure(context.as_mut_ptr()); let entry_message = entry_failure diff --git a/crates/jett_runtime/Cargo.toml b/crates/jett_runtime/Cargo.toml index 6810804b..ecc8b9f5 100644 --- a/crates/jett_runtime/Cargo.toml +++ b/crates/jett_runtime/Cargo.toml @@ -18,3 +18,7 @@ minifb = "=0.28.0" [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.61.2", features = ["Win32_Globalization"] } + +# Declare this compiler-test cfg without enabling it in this crate or dependencies. +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ["cfg(jett_resource_native_test_archive)"] } diff --git a/crates/jett_runtime/src/lib.rs b/crates/jett_runtime/src/lib.rs index 05326d2e..bab2f83d 100644 --- a/crates/jett_runtime/src/lib.rs +++ b/crates/jett_runtime/src/lib.rs @@ -9,6 +9,7 @@ pub mod graphics; pub mod math; pub mod native_abi; pub mod random; +mod resource_custody; pub mod uuid; use std::any::Any; diff --git a/crates/jett_runtime/src/native_abi.rs b/crates/jett_runtime/src/native_abi.rs index a2eef797..97788d58 100644 --- a/crates/jett_runtime/src/native_abi.rs +++ b/crates/jett_runtime/src/native_abi.rs @@ -54,6 +54,11 @@ use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock}; use crate::{ResourceRegistry, discard_panic_payload}; +mod resource; +pub use resource::{ + jett_rt_v1_resource_program_failure, jett_rt_v1_resource_program_failure_copy, + jett_rt_v1_resource_program_unwind, +}; pub mod values; /// The native runtime ABI version implemented by this module. @@ -190,13 +195,15 @@ const _: () = { struct NativeContextState { values: values::NativeValues, - _resources: ResourceRegistry, + resources: ResourceRegistry, + resource_state: Option, } impl NativeContextState { fn new() -> Self { Self { - _resources: ResourceRegistry::new(), + resources: ResourceRegistry::new(), + resource_state: None, values: values::NativeValues::default(), } } @@ -517,9 +524,14 @@ pub unsafe extern "C" fn jett_rt_v1_context_destroy( let Some(mut state) = entry.wait_for_leases_and_take_state() else { return JettRuntimeResultV1::failure(JettRuntimeStatusV1::PANIC, PANIC_MESSAGE); }; + let resource_cleanup_failed = if let Some(resource) = &mut state.resource_state { + resource.safety_unwind(&mut state.values, &mut state.resources) + } else { + false + }; let graphics_cleanup_failed = state.values.release_graphics_session().is_err(); let actor_cleanup_failed = state.values.release_actors().is_err(); - if graphics_cleanup_failed || actor_cleanup_failed { + if resource_cleanup_failed || graphics_cleanup_failed || actor_cleanup_failed { state.values.cleanup_failed = true; } let leaked = !state.values.is_empty(); @@ -718,7 +730,7 @@ mod tests { state .as_mut() .unwrap() - ._resources + .resources .insert( ResourceTypeId::new(1), (), @@ -924,7 +936,7 @@ mod tests { lock_unpoisoned(&lease.entry.state) .as_mut() .unwrap() - ._resources + .resources .insert( ResourceTypeId::new(1), (), diff --git a/crates/jett_runtime/src/native_abi/resource.rs b/crates/jett_runtime/src/native_abi/resource.rs new file mode 100644 index 00000000..a700f545 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource.rs @@ -0,0 +1,1253 @@ +//! Private typed native Resource protocol. No Source or C operation entry is minted here. + +use super::*; +use crate::resource_custody::{ + BorrowedResourceToken, CustodyError, JettResourceCallResultV1, NativeFrameRole, + NativeLayoutInstallation, NativeLoanSource, NativeOperation, NativeParent, NativePayloadStep, + NativeRecipe, NativeShape, NativeSourceInvocation, NativeSourceResult, NativeSourceValue, + NativeSumLoanSource, OwnedResourceToken, PreparedResourceAcquisition, RESOURCE_DOMAIN_ERROR, + RESOURCE_DOMAIN_OK, RegisteredNativeLayout, ResourceCleanupFailure, ResourceCleanupOutcome, + ResourceCustody, ResourceFrameKind, ResourceFrameToken, ResourceLayoutError, ResourcePurpose, + ResourceTransitionFailure, +}; +use crate::{AuthorityProvenance, RegistryError}; + +mod borrowed_sums; +mod carriers; +mod carriers_sum; +mod leaves; +mod operations; +#[cfg(test)] +pub(super) mod ordinary_reservation_faults; +mod program_entry; +mod provider; +pub use program_entry::{ + jett_rt_v1_resource_program_failure, jett_rt_v1_resource_program_failure_copy, + jett_rt_v1_resource_program_unwind, +}; +mod source; +#[cfg(test)] +use ordinary_reservation_faults::{ReservationFaults, ReservationSite}; +#[cfg(all(test, jett_resource_native_test_archive))] +mod test_archive_api; +#[cfg(all(test, jett_resource_native_test_archive))] +mod test_archive_main; +#[cfg(test)] +pub(super) use provider::{DecodedScript, NativeTestEvent}; +use provider::{InstalledProvider, NativeProviderIdentity, NativeRestrictionIdentity}; + +const MAX_ATTEMPTS: usize = 64; +const REFUSED: &[u8] = b"native Resource protocol refused"; +const CLEANUP_FAILED: &[u8] = b"native Resource cleanup failed"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum NativeResourceError { + Context, + AlreadyInstalled, + MissingInstallation, + InvalidEntry, + InvalidHandle, + WrongFamily, + WrongFrame, + WrongOperation, + WrongGrant, + WrongPurpose, + ActiveAttempt, + BodyFailed, + SourceBodyStatus(JettRuntimeStatusV1), + UnsupportedSourceBoundary, + UnsupportedCarrierCustody, + UnsupportedCarrierObservation, + UnprovedCarrierRefinement, + Capacity, + Layout(ResourceLayoutError), + Custody(CustodyError), + Registry(RegistryError), + Cleanup(ResourceCleanupFailure), + Ordinary(JettRuntimeStatusV1), + OrdinaryStorage(JettRuntimeStatusV1), +} +type ResourceResult = Result; +impl From for NativeResourceError { + fn from(error: ResourceLayoutError) -> Self { + Self::Layout(error) + } +} +impl From for NativeResourceError { + fn from(error: CustodyError) -> Self { + Self::Custody(error) + } +} +impl From for NativeResourceError { + fn from(error: RegistryError) -> Self { + Self::Registry(error) + } +} +impl NativeResourceError { + pub(super) fn status(self) -> JettRuntimeStatusV1 { + match self { + Self::Capacity => JettRuntimeStatusV1::RESOURCE_EXHAUSTED, + Self::Context => JettRuntimeStatusV1::INVALID_CONTEXT, + Self::Ordinary(status) + | Self::OrdinaryStorage(status) + | Self::SourceBodyStatus(status) => status, + Self::Cleanup(ResourceCleanupFailure::FinalizerPanic) => JettRuntimeStatusV1::PANIC, + _ => JettRuntimeStatusV1::INVALID_ARGUMENT, + } + } +} +fn ordinary_error(error: (JettRuntimeStatusV1, &'static [u8])) -> NativeResourceError { + NativeResourceError::OrdinaryStorage(error.0) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(super) struct ResourceHandleId(NonZeroU64); +impl ResourceHandleId { + pub(super) fn new(raw: u64) -> ResourceResult { + NonZeroU64::new(raw) + .map(Self) + .ok_or(NativeResourceError::InvalidHandle) + } + pub(super) fn raw(self) -> u64 { + self.0.get() + } +} +fn next_handle(ids: &mut values::ReservedNativeIdentities) -> ResourceResult { + ResourceHandleId::new(ids.take().map_err(ordinary_error)?) +} + +/// This key and state borrow always come from the same stationary context lease. +pub(super) struct AuthenticatedResourceContext { + key: ContextRegistryKey, + lease: NativeContextLease, +} +impl AuthenticatedResourceContext { + pub(super) fn acquire(context: *const JettRuntimeContextV1) -> ResourceResult { + let key = context_key(context).map_err(|_| NativeResourceError::Context)?; + let lease = acquire_context(key).map_err(|_| NativeResourceError::Context)?; + Ok(Self { key, lease }) + } + pub(super) fn with_state( + &self, + operation: impl FnOnce(ContextRegistryKey, &mut NativeContextState) -> ResourceResult, + ) -> ResourceResult { + let mut state = lock_unpoisoned(&self.lease.entry.state); + let state = state.as_mut().ok_or(NativeResourceError::Context)?; + if state + .resource_state + .as_ref() + .is_some_and(|resource| resource.context != self.key) + { + return Err(NativeResourceError::Context); + } + operation(self.key, state) + } + pub(super) fn with_resource( + &self, + operation: impl FnOnce( + &mut NativeResourceState, + &mut values::NativeValues, + &mut ResourceRegistry, + ) -> ResourceResult, + ) -> ResourceResult { + self.with_state(|_, state| { + let resource = state + .resource_state + .as_mut() + .ok_or(NativeResourceError::MissingInstallation)?; + match catch_unwind(AssertUnwindSafe(|| { + operation(resource, &mut state.values, &mut state.resources) + })) { + Ok(Ok(value)) => Ok(value), + Ok(Err(error)) => { + resource.record_failure(error); + Err(error) + } + Err(panic) => { + discard_panic_payload(panic); + if let Some(attempt) = &mut resource.attempt { + attempt.body.get_or_insert(NativeBodyFailure::HostPanic); + } + Err(NativeResourceError::BodyFailed) + } + } + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct NativeEntry { + pub(super) function: u32, + pub(super) signature: u32, + pub(super) scope: u32, +} + +struct NativeFrameEntry { + template: u32, + attempt: ResourceHandleId, + parent: Option, + token: ResourceFrameToken, + incoming: Vec>, +} +struct NativeResidentLoan { + sum: bool, + parent_loan: ResourceHandleId, + caller_frame: ResourceHandleId, + invoke_operation: u32, + callee_parameter: u32, +} +struct NativeOwnerEntry { + frame: ResourceHandleId, + slot: u32, + token: OwnedResourceToken, +} +struct NativeLoanEntry { + frame: ResourceHandleId, + borrow_operation: u32, + source_slot: u32, + source_frame: ResourceHandleId, + token: BorrowedResourceToken, + parent_sum: Option, +} +struct NativeSumLoan { + frame: ResourceHandleId, + borrow_operation: u32, + shell: ResourceHandleId, + source_frame: ResourceHandleId, + source_slot: u32, + shape: u32, + token: Option, +} +struct NativePreparedCall { + operation: u32, + target_operation: u32, + hook: u32, + frame: ResourceHandleId, + descriptor: Option, +} +struct NativePreparedEntry { + destination_frame: ResourceHandleId, + call: ResourceHandleId, + frame: ResourceHandleId, + destination: u32, + kind: u32, + network: ResourceHandleId, + token: PreparedResourceAcquisition, + publication_ids: values::ReservedNativeIdentities, +} +struct NativeNetworkGrant { + context: ContextRegistryKey, + layout: Arc, + provider: NativeProviderIdentity, + restriction: NativeRestrictionIdentity, + ordinary_network: u64, + provenance: AuthorityProvenance, +} +enum NativeResourceEntry { + Program { layout: Arc }, + Frame(NativeFrameEntry), + Owner(NativeOwnerEntry), + Loan(NativeLoanEntry), + SumLoan(NativeSumLoan), + Prepared(NativePreparedEntry), + Call(NativePreparedCall), + Descriptor { hook: u32, signature: u32 }, + NetworkGrant(NativeNetworkGrant), + Sum(NativeResourceSum), + SourceCall(source::NativeSourceCall), + CarrierRoot(carriers::NativeCarrierRoot), + CarrierLoan(carriers::NativeCarrierLoan), + CarrierBuilder(carriers::NativeCarrierBuilder), +} +struct NativeResourceSum { + frame: ResourceHandleId, + slot: u32, + shape: u32, + payload: NativeSumPayload, +} +enum NativeSumPayload { + None, + Some(OwnedResourceToken), + Ok(OwnedResourceToken), + Fail { shape: u32, string: u64 }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum AttemptPhase { + Running, + Completing, +} +/// Solely issued by the exact installed Resource entry constructor. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct ResourceOrdinaryOwner { + context: ContextRegistryKey, + program: ResourceHandleId, + entry: NativeEntry, + root: ResourceHandleId, + attempt: ResourceHandleId, + purpose: ResourcePurpose, + ordinal: usize, +} +impl ResourceOrdinaryOwner { + pub(super) fn ordinal(self) -> usize { + self.ordinal + } + pub(super) fn within_attempt_budget(self) -> bool { + self.ordinal < MAX_ATTEMPTS + } + pub(super) fn continues_installation(self, previous: Self) -> bool { + self.context == previous.context + && self.program == previous.program + && self.entry == previous.entry + && self.attempt.raw() > previous.attempt.raw() + && self.root != previous.root + && previous.ordinal.checked_add(1) == Some(self.ordinal) + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct ResourceOrdinaryCompletion { + owner: ResourceOrdinaryOwner, + completion: NativeResourceCompletion, + diagnostic_ready: bool, +} +impl ResourceOrdinaryCompletion { + pub(super) fn owner(self) -> ResourceOrdinaryOwner { + self.owner + } + pub(super) fn matches_owner(self) -> bool { + self.completion.attempt == self.owner.attempt.raw() + && self.completion.reserved == 0 + && self.completion.selected_kind <= 3 + } + pub(super) fn diagnostic_ready(self) -> bool { + self.diagnostic_ready && self.matches_owner() + } +} +struct NativeAttempt { + id: ResourceHandleId, + ordinary_owner: ResourceOrdinaryOwner, + entry: NativeEntry, + purpose: ResourcePurpose, + root_frame: ResourceHandleId, + root_body_status: Option, + body: Option, + cleanup: Option, + phase: AttemptPhase, +} +enum NativeBodyFailure { + Resource(NativeResourceError), + HostPanic, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct NativeResourceCompletion { + pub(super) attempt: u64, + pub(super) body_status: u32, + pub(super) cleanup_status: u32, + pub(super) selected_kind: u32, + pub(super) reserved: u32, +} +struct NativeCompletedAttempt { + completion: NativeResourceCompletion, + terminal_status: JettRuntimeStatusV1, + ordinary: ResourceOrdinaryCompletion, + resource_message: Vec, + ordinary_message: Vec, + report_error: Option, +} + +#[repr(C)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub(super) struct NativeResourceCounts { + pub(super) owners: u64, + pub(super) loans: u64, + pub(super) frames: u64, + pub(super) registry: u64, + pub(super) owner_handles: u64, + pub(super) loan_handles: u64, + pub(super) frame_handles: u64, + pub(super) provisional: u64, + pub(super) ordinary_empty: u32, + pub(super) reserved: u32, +} + +pub(super) struct NativeResourceState { + context: ContextRegistryKey, + installation: NativeLayoutInstallation, + layout: Arc, + installed_wire: Vec, + entry_owner: program_entry::EntryOwner, + program_handle: ResourceHandleId, + custody: ResourceCustody, + handles: HashMap, + carrier_adapters: HashMap, + active_frames: Vec, + attempt: Option, + completed: Vec, + #[cfg(test)] + reservation_faults: ReservationFaults, + provider: InstalledProvider, + entry: NativeEntry, + network: Option, +} + +impl NativeResourceState { + fn validate_installation(&self) -> ResourceResult<()> { + if !self + .installation + .installed() + .is_some_and(|layout| Arc::ptr_eq(layout, &self.layout)) + { + return Err(NativeResourceError::MissingInstallation); + } + match self.handles.get(&self.program_handle) { + Some(NativeResourceEntry::Program { layout }) if Arc::ptr_eq(layout, &self.layout) => { + Ok(()) + } + _ => Err(NativeResourceError::WrongFamily), + } + } + fn reserve(&mut self, count: usize) -> ResourceResult { + self.handles + .try_reserve(count) + .map_err(|_| NativeResourceError::Capacity)?; + values::reserve_native_identities( + u64::try_from(count).map_err(|_| NativeResourceError::Capacity)?, + ) + .map_err(ordinary_error) + } + fn running(&self, ordinary: &values::NativeValues) -> ResourceResult<&NativeAttempt> { + self.validate_installation()?; + let attempt = self + .attempt + .as_ref() + .ok_or(NativeResourceError::InvalidEntry)?; + ordinary + .validate_resource_ordinary_attempt(attempt.ordinary_owner) + .map_err(ordinary_error)?; + if attempt.phase != AttemptPhase::Running + || attempt.body.is_some() + || attempt.cleanup.is_some() + { + return Err(NativeResourceError::BodyFailed); + } + if let Some(failure) = ordinary.resource_failure() { + return Err(NativeResourceError::Ordinary(failure.status)); + } + if ordinary.cleanup_failed { + return Err(NativeResourceError::BodyFailed); + } + Ok(attempt) + } + fn frame(&self, handle: ResourceHandleId) -> ResourceResult<&NativeFrameEntry> { + let NativeResourceEntry::Frame(frame) = self + .handles + .get(&handle) + .ok_or(NativeResourceError::InvalidHandle)? + else { + return Err(NativeResourceError::WrongFamily); + }; + if self.attempt.as_ref().map(|attempt| attempt.id) != Some(frame.attempt) + || !self.active_frames.contains(&handle) + { + return Err(NativeResourceError::WrongFrame); + } + Ok(frame) + } + fn operation(&self, ordinal: u32) -> ResourceResult<&NativeOperation> { + self.layout + .operations() + .get(ordinal as usize) + .map(|record| record.operation()) + .ok_or(NativeResourceError::WrongOperation) + } + fn operation_frame(&self, ordinal: u32, frame: ResourceHandleId) -> ResourceResult<()> { + if self.active_frames.last() != Some(&frame) { + return Err(NativeResourceError::WrongFrame); + } + let template = self.frame(frame)?.template; + let operation = self.operation(ordinal)?; + let actual = match operation { + NativeOperation::Acquire { frame, .. } + | NativeOperation::Transfer { frame, .. } + | NativeOperation::Borrow { frame, .. } + | NativeOperation::BoundedBorrowUse { frame, .. } + | NativeOperation::EndBorrow { frame, .. } + | NativeOperation::InvokeBorrow { frame, .. } + | NativeOperation::Close { frame, .. } + | NativeOperation::Drop { frame, .. } + | NativeOperation::SumAdopt { frame, .. } + | NativeOperation::SumTake { frame, .. } + | NativeOperation::SumDrop { frame, .. } + | NativeOperation::Replace { frame, .. } + | NativeOperation::Complete { frame, .. } + | NativeOperation::InvokeSourceFunction { frame, .. } + | NativeOperation::TakeFailureCompanion { frame, .. } + | NativeOperation::PublishReturn { frame, .. } + | NativeOperation::CreateAbsentSum { frame, .. } + | NativeOperation::CreateFailureSum { frame, .. } + | NativeOperation::BorrowSum { frame, .. } + | NativeOperation::ObserveSumView { frame, .. } + | NativeOperation::ProjectSumView { frame, .. } + | NativeOperation::ReadFailureCompanion { frame, .. } + | NativeOperation::EndSumBorrow { frame, .. } => *frame, + NativeOperation::Descriptor { .. } | NativeOperation::InvokeDescriptor { .. } => { + return Err(NativeResourceError::WrongOperation); + } + NativeOperation::Carrier { record } => self + .layout + .carriers() + .operations + .get(*record as usize) + .ok_or(NativeResourceError::WrongOperation)? + .operation + .frame(), + }; + if template != actual { + return Err(NativeResourceError::WrongFrame); + } + Ok(()) + } + fn cleanup(&mut self, outcome: ResourceCleanupOutcome) -> ResourceResult<()> { + if let Some(error) = outcome.failure() { + if let Some(attempt) = &mut self.attempt { + attempt.cleanup.get_or_insert(error); + } + Err(NativeResourceError::Cleanup(error)) + } else { + Ok(()) + } + } + fn record_failure(&mut self, error: NativeResourceError) { + if let Some(attempt) = &mut self.attempt { + match error { + NativeResourceError::Ordinary(_) => {} // current ordinary storage remains authoritative + NativeResourceError::Cleanup(error) => { + attempt.cleanup.get_or_insert(error); + } + other => { + attempt + .body + .get_or_insert(NativeBodyFailure::Resource(other)); + } + } + } + } + + pub(super) fn begin_entry( + &mut self, + ordinary: &mut values::NativeValues, + registry: &ResourceRegistry, + entry: NativeEntry, + purpose: ResourcePurpose, + ) -> ResourceResult { + self.validate_installation()?; + if entry != self.entry || self.attempt.is_some() { + return Err(NativeResourceError::ActiveAttempt); + } + #[cfg(test)] + if self.completed.len() >= self.provider.attempts() { + return Err(NativeResourceError::InvalidEntry); + } + if self.completed.len() >= MAX_ATTEMPTS + || self.custody.live_owners() != 0 + || self.custody.live_loans() != 0 + || self.custody.active_frames() != 0 + || registry.live_count() != 0 + || !self.active_frames.is_empty() + || !self.only_installation_handles() + { + return Err(NativeResourceError::InvalidEntry); + } + if let Some((status, _)) = ordinary.session_failure() { + return Err(NativeResourceError::Ordinary(status)); + } + if ordinary.cleanup_failed || !ordinary.is_empty() { + return Err(NativeResourceError::BodyFailed); + } + let mut ids = self.reserve(2)?; + #[cfg(test)] + self.reservation_faults.checkpoint( + ReservationSite::BeginActiveFrames, + self.active_frames.len() == self.active_frames.capacity(), + )?; + self.active_frames + .try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + #[cfg(test)] + self.reservation_faults.checkpoint( + ReservationSite::BeginHistory, + self.completed.len() == self.completed.capacity(), + )?; + self.completed + .try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + let id = next_handle(&mut ids)?; + let root_frame = next_handle(&mut ids)?; + let ordinary_owner = ResourceOrdinaryOwner { + context: self.context, + program: self.program_handle, + entry, + root: root_frame, + attempt: id, + purpose, + ordinal: self.completed.len(), + }; + let predecessor = self.completed.last().map(|completed| completed.ordinary); + let prepared_ordinary = ordinary + .prepare_resource_ordinary_attempt(ordinary_owner, predecessor) + .map_err(ordinary_error)?; + let token = self + .custody + .begin_frame(ResourceFrameKind::Scope, purpose)?; + self.handles.insert( + root_frame, + NativeResourceEntry::Frame(NativeFrameEntry { + template: entry.scope, + attempt: id, + parent: None, + token, + incoming: Vec::new(), + }), + ); + self.active_frames.push(root_frame); + self.attempt = Some(NativeAttempt { + id, + ordinary_owner, + entry, + purpose, + root_frame, + root_body_status: None, + body: None, + cleanup: None, + phase: AttemptPhase::Running, + }); + prepared_ordinary.publish(); + Ok(id) + } + fn only_installation_handles(&self) -> bool { + self.carrier_adapters.is_empty() + && self.custody.live_carrier_roots() == 0 + && self.handles.values().all(|entry| { + matches!( + entry, + NativeResourceEntry::Program { .. } + | NativeResourceEntry::NetworkGrant(_) + | NativeResourceEntry::Descriptor { .. } + ) + }) + } + pub(super) fn entry_network( + &self, + attempt: ResourceHandleId, + parameter: u32, + ) -> ResourceResult { + self.validate_installation()?; + self.runtime_purpose()?; + let running = self + .attempt + .as_ref() + .ok_or(NativeResourceError::InvalidEntry)?; + if running.id != attempt || running.phase != AttemptPhase::Running { + return Err(NativeResourceError::InvalidEntry); + } + let formal = self.layout.signatures()[self.entry.signature as usize] + .parameters() + .get(parameter as usize) + .ok_or(NativeResourceError::InvalidEntry)?; + if !matches!( + self.layout.shapes().get(formal.shape() as usize), + Some(NativeShape::Network) + ) { + return Err(NativeResourceError::InvalidEntry); + } + match self + .handles + .get(&self.network.ok_or(NativeResourceError::WrongGrant)?) + { + Some(NativeResourceEntry::NetworkGrant(grant)) => Ok(grant.ordinary_network), + _ => Err(NativeResourceError::WrongGrant), + } + } + /// Observe only the sole retired root Scope of this exact Runtime attempt. + /// Body/ordinary/cleanup failures are independent and must not gate this read. + pub(super) fn entry_outcome( + &self, + scope: ResourceHandleId, + function: u32, + signature: u32, + template: u32, + ) -> ResourceResult { + self.validate_installation()?; + self.runtime_purpose()?; + let attempt = self + .attempt + .as_ref() + .ok_or(NativeResourceError::InvalidEntry)?; + let selected = NativeEntry { + function, + signature, + scope: template, + }; + if attempt.phase != AttemptPhase::Running + || attempt.entry != self.entry + || selected != self.entry + || attempt.root_frame != scope + { + return Err(NativeResourceError::InvalidEntry); + } + let row = self + .layout + .frames() + .get(template as usize) + .ok_or(NativeResourceError::WrongFrame)?; + if row.role() != NativeFrameRole::Scope + || row.site().function() != function + || row.signature() != signature + || self.handles.contains_key(&scope) + || self.active_frames.contains(&scope) + { + return Err(NativeResourceError::WrongFrame); + } + attempt + .root_body_status + .ok_or(NativeResourceError::WrongFrame) + } + + pub(super) fn complete_entry( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + attempt_id: ResourceHandleId, + body_status: u32, + host_panic: bool, + ) -> ResourceResult { + let attempt = self + .attempt + .as_mut() + .ok_or(NativeResourceError::InvalidEntry)?; + if attempt.id != attempt_id || attempt.phase != AttemptPhase::Running { + return Err(NativeResourceError::InvalidEntry); + } + ordinary + .validate_resource_ordinary_attempt(attempt.ordinary_owner) + .map_err(ordinary_error)?; + attempt.phase = AttemptPhase::Completing; + if host_panic { + attempt.body.get_or_insert(NativeBodyFailure::HostPanic); + } + if self.layout.wire_version() == 3 { + // Typed carrier companions and parent leases retire while their frames are live. + while let Some(frame) = self.active_frames.last().copied() { + if let Err(error) = self.retire_frame(ordinary, registry, frame) { + match error { + NativeResourceError::Cleanup(error) => { + self.attempt + .as_mut() + .ok_or(NativeResourceError::InvalidEntry)? + .cleanup + .get_or_insert(error); + } + _ => { + ordinary.cleanup_failed = true; + } + } + if self.active_frames.last() == Some(&frame) { + // Refused retirement retains the exact obligations; no success receipt. + return Err(error); + } + } + } + if !self.only_installation_handles() { + // Frame retirement must discharge every holder and staging receipt. + // Keep any skipped variant observable instead of erasing its obligation. + ordinary.cleanup_failed = true; + return Err(NativeResourceError::BodyFailed); + } + } + // Unwind the real core before retiring token shells or copying diagnostics. + let cleanup = self.custody.unwind_all(registry); + if let Some(error) = cleanup.failure() { + self.attempt + .as_mut() + .ok_or(NativeResourceError::InvalidEntry)? + .cleanup + .get_or_insert(error); + } + let mut companion_failed = false; + self.handles.retain(|_, entry| { + if let NativeResourceEntry::Sum(sum) = entry { + if let NativeSumPayload::Fail { shape, string } = &sum.payload { + companion_failed |= ordinary + .drop_resource_typed_companion(&self.layout, *shape, *string) + .is_err(); + } + } + matches!( + entry, + NativeResourceEntry::Program { .. } + | NativeResourceEntry::NetworkGrant(_) + | NativeResourceEntry::Descriptor { .. } + ) + }); + self.active_frames.clear(); + self.carrier_adapters.clear(); + if companion_failed { + ordinary.cleanup_failed = true; + } + if self.custody.live_owners() != 0 + || self.custody.live_loans() != 0 + || self.custody.active_frames() != 0 + || registry.live_count() != 0 + { + return Err(NativeResourceError::BodyFailed); + } + if !ordinary.is_empty() { + // Resource retirement cannot hide an ordinary frame leak. Keep the + // Completing attempt and its error; neither can authorize reentry. + ordinary.cleanup_failed = true; + return Err(NativeResourceError::BodyFailed); + } + let attempt = self + .attempt + .take() + .ok_or(NativeResourceError::InvalidEntry)?; + let ordinary_failure = ordinary.resource_failure(); + let recorded_body_failure = attempt.body.is_some() || ordinary_failure.is_some(); + let cleanup_status = attempt + .cleanup + .map(|error| NativeResourceError::Cleanup(error).status().code()) + .unwrap_or(if ordinary.cleanup_failed { + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + } else { + 0 + }); + let selected_kind = if cleanup_status != 0 { + 3 + } else if host_panic || matches!(attempt.body, Some(NativeBodyFailure::HostPanic)) { + 2 + } else if body_status != 0 || recorded_body_failure { + 1 + } else { + 0 + }; + let completion = NativeResourceCompletion { + attempt: attempt_id.raw(), + body_status, + cleanup_status, + selected_kind, + reserved: 0, + }; + // Retain the original body field above. A refused leaf or an ordinary + // first error can also fail a body whose generated status was zero. + let terminal_status = if cleanup_status != 0 { + JettRuntimeStatusV1(cleanup_status) + } else if selected_kind == 2 { + JettRuntimeStatusV1::PANIC + } else if body_status != 0 { + JettRuntimeStatusV1(body_status) + } else if let Some(failure) = &attempt.body { + match failure { + NativeBodyFailure::Resource(error) => error.status(), + NativeBodyFailure::HostPanic => JettRuntimeStatusV1::PANIC, + } + } else if let Some(failure) = &ordinary_failure { + failure.status + } else { + JettRuntimeStatusV1::OK + }; + let resource_message = if attempt.cleanup.is_some() { + Some(CLEANUP_FAILED) + } else if ordinary.cleanup_failed { + Some(b"native value cleanup failed".as_slice()) + } else { + match attempt.body { + Some(NativeBodyFailure::HostPanic) => Some(PANIC_MESSAGE), + Some(NativeBodyFailure::Resource(_)) => Some(REFUSED), + None => None, + } + }; + let diagnostic: ResourceResult<(Vec, Vec)> = (|| { + #[cfg(not(test))] + let resource_message = copy_diagnostic(None, resource_message.unwrap_or_default())?; + #[cfg(test)] + let resource_message = copy_diagnostic( + None, + resource_message.unwrap_or_default(), + &mut self.reservation_faults, + ReservationSite::ResourceDiagnostic, + )?; + let ordinary_message = if let Some(failure) = ordinary_failure { + #[cfg(not(test))] + { + copy_diagnostic(failure.prefix, failure.message)? + } + #[cfg(test)] + { + copy_diagnostic( + failure.prefix, + failure.message, + &mut self.reservation_faults, + ReservationSite::OrdinaryDiagnostic, + )? + } + } else { + Vec::new() + }; + Ok((resource_message, ordinary_message)) + })(); + let (resource_message, ordinary_message, report_error) = match diagnostic { + Ok((resource, ordinary)) => (resource, ordinary, None), + Err(error) => (Vec::new(), Vec::new(), Some(error)), + }; + let ordinary_completion = ResourceOrdinaryCompletion { + owner: attempt.ordinary_owner, + completion, + diagnostic_ready: report_error.is_none(), + }; + ordinary + .seal_resource_ordinary_attempt(ordinary_completion) + .map_err(ordinary_error)?; + // A report failure retains a completed tombstone; cleanup and retirement are not retried. + self.completed.push(NativeCompletedAttempt { + completion, + terminal_status, + ordinary: ordinary_completion, + resource_message, + ordinary_message, + report_error, + }); + if let Some(error) = report_error { + return Err(error); + } + Ok(completion) + } + pub(super) fn completed_attempt( + &self, + attempt: ResourceHandleId, + ) -> ResourceResult { + let completed = self + .completed + .iter() + .find(|entry| entry.completion.attempt == attempt.raw()) + .ok_or(NativeResourceError::InvalidEntry)?; + if let Some(error) = completed.report_error { + return Err(error); + } + Ok(completed.completion) + } + pub(super) fn completed_messages( + &self, + attempt: ResourceHandleId, + ) -> ResourceResult<(&[u8], &[u8])> { + let completed = self + .completed + .iter() + .find(|entry| entry.completion.attempt == attempt.raw()) + .ok_or(NativeResourceError::InvalidEntry)?; + if let Some(error) = completed.report_error { + return Err(error); + } + Ok((&completed.resource_message, &completed.ordinary_message)) + } + #[cfg(test)] + pub(super) fn script_remaining(&self) -> usize { + self.provider.remaining() + } + #[cfg(test)] + pub(super) fn observe_events( + &self, + observe: impl FnOnce(&[provider::NativeTestEvent]) -> T, + ) -> T { + self.provider.observe_events(observe) + } + pub(super) fn counts( + &self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + ) -> NativeResourceCounts { + let mut counts = NativeResourceCounts { + owners: self.custody.live_owners() as u64, + loans: self.custody.live_loans() as u64, + frames: self.custody.active_frames() as u64, + registry: registry.live_count() as u64, + ordinary_empty: u32::from(ordinary.is_empty()), + ..NativeResourceCounts::default() + }; + for entry in self.handles.values() { + match entry { + NativeResourceEntry::Owner(_) => counts.owner_handles += 1, + NativeResourceEntry::Sum(NativeResourceSum { + payload: NativeSumPayload::Some(_) | NativeSumPayload::Ok(_), + .. + }) => counts.owner_handles += 1, + NativeResourceEntry::Sum(_) if self.layout.wire_version() == 3 => { + counts.owner_handles += 1; + } + NativeResourceEntry::Loan(_) | NativeResourceEntry::SumLoan(_) => { + counts.loan_handles += 1 + } + NativeResourceEntry::CarrierRoot(_) => counts.owner_handles += 1, + NativeResourceEntry::CarrierLoan(_) => counts.loan_handles += 1, + NativeResourceEntry::CarrierBuilder(_) => counts.provisional += 1, + NativeResourceEntry::SourceCall(_) + | NativeResourceEntry::Call(_) + | NativeResourceEntry::Prepared(_) + if self.layout.wire_version() == 3 => + { + counts.provisional += 1; + } + NativeResourceEntry::Frame(frame) => { + counts.frame_handles += 1; + if self.layout.frames()[frame.template as usize].role() + == NativeFrameRole::Return + { + counts.provisional += 1; + } + } + _ => {} + } + } + if self.layout.wire_version() == 3 { + // A sealed ended adapter still owns its shell until frame retirement. + counts.provisional += self.carrier_adapters.len() as u64; + // A private root has exactly one native envelope. Report any orphan + // as a live obligation without changing the matched-envelope counts. + let envelopes = self + .handles + .values() + .filter(|entry| match entry { + NativeResourceEntry::CarrierRoot(root) => root.custody.is_some(), + NativeResourceEntry::CarrierBuilder(builder) => builder.custody.is_some(), + _ => false, + }) + .count(); + counts.provisional += + self.custody.live_carrier_roots().saturating_sub(envelopes) as u64; + } + counts + } + pub(super) fn safety_unwind( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + ) -> bool { + let mut failed = false; + if self.layout.wire_version() == 3 { + while let Some(frame) = self.active_frames.last().copied() { + if self.retire_frame(ordinary, registry, frame).is_err() { + failed = true; + if self.active_frames.last() == Some(&frame) { + break; + } + } + } + } + let outcome = self.custody.unwind_all(registry); + failed |= outcome.failure().is_some(); + if self.layout.wire_version() == 3 { + // A skipped holder variant cannot turn context backstop cleanup into success. + failed |= !self.only_installation_handles(); + failed |= self.safety_retire_carriers(ordinary).is_err(); + } + for entry in self.handles.values() { + if let NativeResourceEntry::Sum(NativeResourceSum { + payload: NativeSumPayload::Fail { string, .. }, + .. + }) = entry + { + failed |= ordinary.drop_resource_ordinary_companion(*string).is_err(); + } + } + self.handles.clear(); + self.carrier_adapters.clear(); + self.active_frames.clear(); + self.attempt = None; + failed + || self.custody.live_owners() != 0 + || self.custody.live_carrier_roots() != 0 + || self.custody.live_loans() != 0 + || registry.live_count() != 0 + } +} + +fn copy_diagnostic( + prefix: Option<&[u8]>, + message: &[u8], + #[cfg(test)] faults: &mut ReservationFaults, + #[cfg(test)] site: ReservationSite, +) -> ResourceResult> { + let prefix = prefix.unwrap_or_default(); + let length = prefix + .len() + .checked_add(message.len()) + .ok_or(NativeResourceError::Capacity)?; + if length > 64 * 1024 { + return Err(NativeResourceError::Capacity); + } + let mut bytes = Vec::new(); + #[cfg(test)] + faults.checkpoint(site, length != 0)?; + bytes + .try_reserve_exact(length) + .map_err(|_| NativeResourceError::Capacity)?; + bytes.extend_from_slice(prefix); + bytes.extend_from_slice(message); + Ok(bytes) +} + +fn validate_entry(layout: &RegisteredNativeLayout, entry: NativeEntry) -> ResourceResult { + let frame = layout + .frames() + .get(entry.scope as usize) + .ok_or(NativeResourceError::InvalidEntry)?; + let signature = layout + .signatures() + .get(entry.signature as usize) + .ok_or(NativeResourceError::InvalidEntry)?; + if frame.role() != NativeFrameRole::Scope + || frame.site().function() != entry.function + || frame.signature() != entry.signature + || frame.parents() != [NativeParent::Root] + || !matches!( + layout.shapes().get(signature.result() as usize), + Some(NativeShape::Nothing) + ) + { + return Err(NativeResourceError::InvalidEntry); + } + let mut network = false; + for formal in signature.parameters() { + match layout.shapes().get(formal.shape() as usize) { + Some(NativeShape::Network) => network = true, + _ => return Err(NativeResourceError::InvalidEntry), + } + } + Ok(network) +} + +fn install_candidate( + key: ContextRegistryKey, + state: &mut NativeContextState, + bytes: &[u8], + entry: NativeEntry, + mut provider: InstalledProvider, +) -> ResourceResult { + if state.resource_state.is_some() { + return Err(NativeResourceError::AlreadyInstalled); + } + if state.values.resource_failure().is_some() + || state.values.cleanup_failed + || !state.values.is_empty() + || state.resources.live_count() != 0 + { + return Err(NativeResourceError::BodyFailed); + } + let mut installation = NativeLayoutInstallation::new(&state.resources); + let layout = installation.install(&state.resources, bytes)?; + let needs_network = validate_entry(&layout, entry)?; + let pair_network = needs_network && provider.enabled(); + // Own the exact authenticated bytes before publishing provider/grant state. + let mut installed_wire = Vec::new(); + installed_wire + .try_reserve_exact(bytes.len()) + .map_err(|_| NativeResourceError::Capacity)?; + installed_wire.extend_from_slice(bytes); + let custody = ResourceCustody::new(layout.program().clone(), &state.resources)?; + let mut handles = HashMap::new(); + handles + .try_reserve(2) + .map_err(|_| NativeResourceError::Capacity)?; + let mut ids = values::reserve_native_identities(5).map_err(ordinary_error)?; + let program_handle = next_handle(&mut ids)?; + let network_handle = if pair_network { + Some(next_handle(&mut ids)?) + } else { + None + }; + let provider_id = NativeProviderIdentity::new(ids.take().map_err(ordinary_error)?)?; + let restriction = NativeRestrictionIdentity::new(ids.take().map_err(ordinary_error)?)?; + provider.bind(provider_id, network_handle)?; + let staged = if pair_network { + Some( + state + .values + .prepare_resource_network(&mut ids) + .map_err(ordinary_error)?, + ) + } else { + None + }; + handles.insert( + program_handle, + NativeResourceEntry::Program { + layout: layout.clone(), + }, + ); + // No fallible publication follows this commit while the authenticated lease owns the mutex. + if let (Some(handle), Some(staged)) = (network_handle, staged) { + let ordinary_network = state.values.commit_resource_network(staged); + handles.insert( + handle, + NativeResourceEntry::NetworkGrant(NativeNetworkGrant { + context: key, + layout: layout.clone(), + provider: provider_id, + restriction, + ordinary_network, + provenance: AuthorityProvenance::new(provider_id.raw(), restriction.raw()), + }), + ); + } + state.resource_state = Some(NativeResourceState { + context: key, + installation, + layout, + installed_wire, + entry_owner: program_entry::EntryOwner::External, + program_handle, + custody, + handles, + carrier_adapters: HashMap::new(), + active_frames: Vec::new(), + attempt: None, + completed: Vec::new(), + #[cfg(test)] + reservation_faults: ReservationFaults::default(), + provider, + entry, + network: network_handle, + }); + Ok(program_handle) +} + +pub(super) fn install_disabled( + authenticated: &AuthenticatedResourceContext, + bytes: &[u8], + entry: NativeEntry, +) -> ResourceResult { + authenticated.with_state(|key, state| { + install_candidate(key, state, bytes, entry, InstalledProvider::Disabled) + }) +} +#[cfg(test)] +pub(super) fn install_scripted( + authenticated: &AuthenticatedResourceContext, + bytes: &[u8], + entry: NativeEntry, + script: DecodedScript, +) -> ResourceResult { + authenticated.with_state(|key, state| { + install_candidate( + key, + state, + bytes, + entry, + InstalledProvider::scripted(script)?, + ) + }) +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_runtime/src/native_abi/resource/borrowed_sum_tests.rs b/crates/jett_runtime/src/native_abi/resource/borrowed_sum_tests.rs new file mode 100644 index 00000000..3b1a75b5 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/borrowed_sum_tests.rs @@ -0,0 +1,569 @@ +//! Native protocol controls. Genuine Source acceptance is a separate driver gate. +use super::super::*; +use super::{assert_empty, context, events, script, words}; + +struct Fixture { + bytes: Vec, + result: bool, + create: u32, + source_written: u32, + source_bare: u32, + observe: u32, + project: u32, + read: Option, + end_child: u32, + invoke: u32, + complete_inner: u32, + complete_scope: u32, + complete_root: u32, + take: u32, + drop_written: u32, + drop_bare: u32, + take_failure: Option, + caller_project: u32, + caller_end: u32, + close: u32, +} +fn fixture(result: bool, corrupt: u8) -> Fixture { + let shape = if result { 6 } else { 5 }; + let path = if result { 2 } else { 1 }; + let mut signatures = vec![ + vec![0, 2, 6, 0, 2, 1, 1], + vec![1, 2, 7, 0, 2, 4, 2], + vec![2, 1, 3, 4, 1], + vec![3, 1, 3, 0, 2], + vec![4, 2, 3, 0, 2, shape, 2], + ]; + if corrupt == 1 { + *signatures[4].last_mut().unwrap() = 1; + } + let shapes = vec![ + vec![0, 6], + vec![1, 1, 64, 1], + vec![2, 4], + vec![3, 5], + vec![4, 7, 0], + vec![5, 8, 4], + vec![6, 9, 4, 2], + vec![7, 9, 1, 2], + ]; + let frames = vec![ + vec![0, 1, 0, 0, 0, 0, 3, 1, 0, 0], + vec![1, 2, 0, 0, 0, 1, 3, 1, 1, 0], + vec![2, 1, 1, 0, 0, 2, 4, 1, 1, 1], + vec![3, 2, 1, 0, 0, 3, 4, 1, 1, 2], + ]; + let slots = vec![ + vec![0, 0, shape, 1, path], + vec![1, 1, shape, 1, path], + vec![2, 1, 6, 1, 2], + vec![3, 1, 4, 0], + ]; + let mut ops: Vec<(u32, Vec)> = Vec::new(); + let mut add = |function, row| { + let id = ops.len() as u32; + ops.push((function, row)); + id + }; + add(0, vec![1, 1, 0, 2]); // 0 factory + add(0, vec![10, 1, 2, 3]); // 1 unwrap owning factory result + add(0, vec![9, 1, 3, 0]); // 2 adopt owner into caller sum + add(0, vec![2, 1, 0, 1]); // 3 whole bare argument holder transfer + let create = add( + 0, + if result { + vec![20, 1, 1, 2] + } else { + vec![19, 1, 1] + }, + ); + add(0, vec![2, 1, 1, 0]); // 5 preserve initial None/Fail in caller Scope + add(0, vec![13, 1]); // 6 complete operation + add(0, vec![21, 1, 0, 1]); // 7 written shell lease + add(0, vec![21, 1, 1, 1]); // 8 bare argument holder lease + add(0, vec![25, 1, 7]); // 9 end written shell lease + add(0, vec![25, 1, 8]); // 10 end bare shell lease + let source_row = |bare| { + let mut row = vec![16, 1, 1, 4, 2, 0, 2, 0, 1, 2]; + row.extend([0, 0, 0, 0, 2, 4, 2, 1]); + row.extend([ + 1, + 1, + shape, + shape, + if bare { 1 } else { 2 }, + if bare { 3 } else { 4 }, + 2, + 4, + 1, + if bare { + if corrupt == 6 { 7 } else { 8 } + } else { + 7 + }, + ]); + row.extend([1, 3]); + row + }; + let source_written = add(0, source_row(false)); + let source_bare = add(0, source_row(true)); + let observe = add(1, vec![22, 2, 2, 2, if corrupt == 2 { 0 } else { 1 }]); + let project = add( + 1, + vec![ + 23, + 2, + 2, + 2, + 1, + if corrupt == 3 { + if path == 1 { 2 } else { 1 } + } else { + path + }, + 2, + ], + ); + let end_child = add(1, vec![5, 2, project]); + let read = result.then(|| add(1, vec![24, 2, 2, 2, 1, if corrupt == 4 { 1 } else { 2 }])); + let invoke = add( + if corrupt == 7 { 0 } else { 1 }, + vec![ + 6, + if corrupt == 7 { 1 } else { 3 }, + 1, + 3, + if corrupt == 5 { 7 } else { project }, + ], + ); + let complete_inner = add(1, vec![13, 3]); + let complete_scope = add(1, vec![13, 2]); + let complete_root = add(0, vec![13, 0]); + let take = add(0, vec![10, 1, 0, 3]); + let drop_written = add(0, vec![11, 1, 0]); + let drop_bare = add(0, vec![11, 1, 1]); + let take_failure = result.then(|| add(0, vec![17, 1, 0, 2])); + let caller_project = add(0, vec![23, 1, 1, 7, path, 1]); + let caller_end = add(0, vec![5, 1, caller_project]); + let close = add(0, vec![7, 1, 2, 3]); + let mut bytes = b"JTRSC001".to_vec(); + words(&mut bytes, &[2, 0]); + bytes.extend(0u64.to_le_bytes()); + words( + &mut bytes, + &[ + 1, + 3, + signatures.len() as u32, + shapes.len() as u32, + frames.len() as u32, + slots.len() as u32, + ops.len() as u32, + 0, + ], + ); + words(&mut bytes, &[0]); + for row in [[0, 0, 1, 0], [1, 0, 2, 1], [2, 0, 3, 2]] { + words(&mut bytes, &row); + } + for row in signatures + .into_iter() + .chain(shapes) + .chain(frames) + .chain(slots) + { + words(&mut bytes, &row); + } + for (index, (function, row)) in ops.into_iter().enumerate() { + words( + &mut bytes, + &[index as u32, function, 0, 0, 100 + index as u32], + ); + words(&mut bytes, &row); + } + let len = bytes.len() as u64; + bytes[16..24].copy_from_slice(&len.to_le_bytes()); + Fixture { + bytes, + result, + create, + source_written, + source_bare, + observe, + project, + read, + end_child, + invoke, + complete_inner, + complete_scope, + complete_root, + take, + drop_written, + drop_bare, + take_failure, + caller_project, + caller_end, + close, + } +} +fn entry() -> NativeEntry { + NativeEntry { + function: 0, + signature: 3, + scope: 0, + } +} +fn setup( + auth: &AuthenticatedResourceContext, + f: &Fixture, + occupied: bool, + rows: &[(u32, i64, i64, &str)], +) -> ( + ResourceHandleId, + ResourceHandleId, + ResourceHandleId, + ResourceHandleId, + u64, +) { + install_scripted(auth, &f.bytes, entry(), script(rows, 1)).unwrap(); + auth.with_resource(|s, v, r| { + let attempt = s.begin_entry(v, r, entry(), ResourcePurpose::Runtime)?; + let root = s.root_frame(attempt)?; + let op = s.begin_operation_frame(v, 1, root)?; + let net = s.entry_network(attempt, 0)?; + let shell = if occupied { + let call = s.prepare_hook(v, 0, op, None)?; + let prepared = s.prepare_factory(v, r, call, net)?; + let value = s.factory(v, r, prepared, net, 801)?; + let owner = s.sum_take(v, r, 1, op, ResourceHandleId::new(value.value)?, op)?; + s.sum_adopt(v, r, 2, op, owner, root)? + } else { + let shell = if f.result { + let mut ids = v + .prepare_resource_ordinary_output() + .map_err(ordinary_error)?; + let text = v + .publish_resource_error("empty".to_owned(), &mut ids) + .map_err(ordinary_error)?; + s.failure_sum(v, op, f.create, text)? + } else { + s.absent_sum(v, op, f.create)? + }; + s.transfer(v, r, 5, op, shell, root)? + }; + Ok((attempt, root, op, shell, net)) + }) + .unwrap() +} +fn finish( + auth: &AuthenticatedResourceContext, + attempt: ResourceHandleId, + root: ResourceHandleId, + op: ResourceHandleId, + f: &Fixture, +) { + auth.with_resource(|s, v, r| { + s.end_operation_frame(v, r, op)?; + s.scope_complete(v, r, root, f.complete_root, 0)?; + let outcome = s.complete_entry(v, r, attempt, 0, false)?; + assert_eq!( + ( + outcome.body_status, + outcome.cleanup_status, + outcome.selected_kind + ), + (0, 0, 0) + ); + Ok(()) + }) + .unwrap(); + assert_empty(auth); +} + +#[test] +fn native_resource_borrowed_sum_absent_and_failure_views_keep_shell_and_copy_failure() { + for result in [false, true] { + context(|auth| { + let f = fixture(result, 0); + let (attempt, root, op, shell, net) = setup(auth, &f, false, &[]); + auth.with_resource(|s, v, r| { + let parent = s.sum_borrow(v, r, 7, op, shell)?; + assert_eq!(s.custody.live_owners(), 0); + assert_eq!(s.custody.live_loans(), 0); + assert_eq!(s.counts(v, r).loan_handles, 1); + assert_eq!( + s.transfer(v, r, 3, op, shell, op), + Err(CustodyError::ActiveBorrow.into()) + ); + assert_eq!( + s.sum_drop(v, r, f.drop_written, op, shell), + Err(CustodyError::ActiveBorrow.into()) + ); + let call = s.source_prepare(v, op, f.source_written)?; + s.source_actual(v, r, call, 0, 0, net)?; + s.source_actual(v, r, call, 1, 1, parent.raw())?; + let scope = s.source_enter(v, r, call)?; + assert_eq!(s.source_parameter(v, scope, 1)?, parent.raw()); + assert_eq!(s.sum_view_tag(v, r, f.observe, scope, parent)?, 0); + assert!(s.sum_view_project(v, r, f.project, scope, parent).is_err()); + if let Some(read) = f.read { + for _ in 0..2 { + let copy = s.sum_failure_read(v, r, read, scope, parent)?; + v.drop_resource_typed_companion(&s.layout, 2, copy) + .map_err(ordinary_error)?; + assert!(s.handles.contains_key(&shell)); + } + } + s.scope_complete(v, r, scope, f.complete_scope, 0)?; + s.source_status(v, call)?; + s.sum_borrow_end(r, 9, op, parent)?; + s.carrier(shell, 0, r)?; + if let Some(take) = f.take_failure { + let text = s.failure_companion_take(v, op, take, shell)?; + v.drop_resource_typed_companion(&s.layout, 2, text) + .map_err(ordinary_error)?; + } else { + s.sum_drop(v, r, f.drop_written, op, shell)?; + } + Ok(()) + }) + .unwrap(); + finish(auth, attempt, root, op, &f); + assert!(events(auth).is_empty()); + }); + } +} + +#[test] +fn native_resource_borrowed_sum_occupied_written_and_bare_keep_one_owner_across_hook_outcomes() { + for result in [false, true] { + for bare in [false, true] { + for failed in [false, true] { + context(|auth| { + let f = fixture(result, 0); + let rows = if failed { + vec![(1, 801, 0, ""), (5, 801, 0, "borrow failed")] + } else { + vec![(1, 801, 0, ""), (4, 801, 17, "")] + }; + let (attempt, root, op, original, net) = setup(auth, &f, true, &rows); + auth.with_resource(|s, v, r| { + let shell = if bare { + s.transfer(v, r, 3, op, original, op)? + } else { + original + }; + let parent = s.sum_borrow(v, r, if bare { 8 } else { 7 }, op, shell)?; + let call = s.source_prepare( + v, + op, + if bare { + f.source_bare + } else { + f.source_written + }, + )?; + s.source_actual(v, r, call, 0, 0, net)?; + s.source_actual(v, r, call, 1, 1, parent.raw())?; + let scope = s.source_enter(v, r, call)?; + assert_eq!(s.custody.live_owners(), 1); + assert_eq!(s.sum_view_tag(v, r, f.observe, scope, parent)?, 1); + let child = s.sum_view_project(v, r, f.project, scope, parent)?; + let inner = s.begin_operation_frame(v, 3, scope)?; + let hook = s.prepare_hook(v, f.invoke, inner, None)?; + let output = s.invoke_borrow(v, r, hook, net, child)?; + v.drop_resource_typed_companion(&s.layout, 7, output) + .map_err(ordinary_error)?; + s.end_operation_frame(v, r, inner)?; + s.borrow_end(f.end_child, scope, child)?; + s.scope_complete(v, r, scope, f.complete_scope, 0)?; + s.source_status(v, call)?; + s.sum_borrow_end(r, if bare { 10 } else { 9 }, op, parent)?; + if bare { + assert!(!s.handles.contains_key(&original)); + s.sum_drop(v, r, f.drop_bare, op, shell)?; + } else { + s.carrier(original, 0, r)?; + let owner = s.sum_take(v, r, f.take, op, original, op)?; + s.close_or_drop(r, f.close, op, owner)?; + } + Ok(()) + }) + .unwrap(); + finish(auth, attempt, root, op, &f); + assert_eq!( + events(auth), + vec![(801, 1), (801, if failed { 4 } else { 3 }), (801, 5)] + ); + }); + } + } + } +} + +#[test] +fn native_resource_borrowed_sum_child_prevents_parent_end_and_owning_adoption_until_retired() { + for result in [false, true] { + context(|auth| { + let f = fixture(result, 0); + let (attempt, root, op, shell, _) = setup(auth, &f, true, &[(1, 801, 0, "")]); + auth.with_resource(|s, v, r| { + let parent = s.sum_borrow(v, r, 7, op, shell)?; + let child = s.sum_view_project(v, r, f.caller_project, op, parent)?; + assert_eq!( + s.sum_borrow_end(r, 9, op, parent), + Err(CustodyError::ActiveBorrow.into()) + ); + assert_eq!( + s.sum_take(v, r, f.take, op, shell, op), + Err(CustodyError::ActiveBorrow.into()) + ); + assert_eq!( + s.sum_drop(v, r, f.drop_written, op, shell), + Err(CustodyError::ActiveBorrow.into()) + ); + assert_eq!(s.custody.live_owners(), 1); + assert_eq!(s.custody.live_loans(), 2); + assert!( + s.exact_loan( + child, + NativeLoanSource::ExistingBorrow { + operation: f.caller_project + } + ) + .is_err() + ); + s.borrow_end(f.caller_end, op, child)?; + s.sum_borrow_end(r, 9, op, parent)?; + assert!(s.sum_borrow_end(r, 9, op, parent).is_err()); + assert!( + s.exact_loan( + child, + NativeLoanSource::ProjectedSumPayload { + operation: f.caller_project + } + ) + .is_err() + ); + let owner = s.sum_take(v, r, f.take, op, shell, op)?; + s.close_or_drop(r, f.close, op, owner)?; + Ok(()) + }) + .unwrap(); + finish(auth, attempt, root, op, &f); + assert_eq!(events(auth), vec![(801, 1), (801, 5)]); + }); + } +} + +#[test] +fn native_resource_borrowed_sum_abandoned_prefix_retires_leases_before_holder_cleanup() { + for occupied in [false, true] { + for result in [false, true] { + context(|auth| { + let f = fixture(result, 0); + let rows = if occupied { + vec![(1, 801, 0, "")] + } else { + vec![] + }; + let (attempt, root, op, original, net) = setup(auth, &f, occupied, &rows); + auth.with_resource(|s, v, r| { + let shell = s.transfer(v, r, 3, op, original, op)?; + let parent = s.sum_borrow(v, r, 8, op, shell)?; + let call = s.source_prepare(v, op, f.source_bare)?; + s.source_actual(v, r, call, 0, 0, net)?; + s.source_actual(v, r, call, 1, 1, parent.raw())?; + // No SourceEnter or provider invocation: suffix retirement handles the prefix. + s.end_operation_frame(v, r, op)?; + assert_eq!(s.counts(v, r).loan_handles, 0); + assert_eq!(s.custody.live_owners(), 0); + s.scope_complete(v, r, root, f.complete_root, 0)?; + assert_eq!(s.complete_entry(v, r, attempt, 0, false)?.selected_kind, 0); + Ok(()) + }) + .unwrap(); + assert_empty(auth); + assert_eq!( + events(auth), + if occupied { + vec![(801, 1), (801, 5)] + } else { + vec![] + } + ); + }); + } + } +} + +#[test] +fn native_resource_borrowed_sum_registration_rejects_wrong_formal_path_companion_and_issuer() { + for corrupt in 1..=7 { + context(|auth| { + let f = fixture(true, corrupt); + assert!( + install_disabled(auth, &f.bytes, entry()).is_err(), + "corruption {corrupt}" + ); + auth.with_state(|_, state| { + assert!(state.resource_state.is_none()); + Ok(()) + }) + .unwrap(); + }); + } + context(|auth| { + let mut f = fixture(false, 0); + f.bytes[8..12].copy_from_slice(&1u32.to_le_bytes()); + assert!(install_disabled(auth, &f.bytes, entry()).is_err()); + }); +} + +#[test] +fn native_resource_borrowed_sum_leaf_output_preflight_is_before_lookup_or_effects() { + let mut ctx = Box::new(JettRuntimeContextV1::retired()); + let mut result = JettRuntimeResultV1::ok(); + assert_eq!( + unsafe { jett_rt_v1_context_create(JETT_RUNTIME_ABI_VERSION_V1, &mut *ctx, &mut result) }, + JettRuntimeStatusV1::OK + ); + let pointer = &*ctx as *const JettRuntimeContextV1; + let mut sentinel = 0x5555u64; + unsafe { + assert_eq!( + leaves::jett_rt_v1_resource_sum_borrow(pointer, 0, 0, 0, std::ptr::null_mut()), + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + assert_eq!( + leaves::jett_rt_v1_resource_sum_view_tag(pointer, 0, 0, 0, pointer.cast_mut().cast()), + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + assert_eq!( + leaves::jett_rt_v1_resource_sum_view_project( + pointer, + 0, + 0, + 0, + (&mut sentinel as *mut u64).cast::().add(1).cast() + ), + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + assert_eq!( + leaves::jett_rt_v1_resource_sum_failure_read(pointer, 0, 0, 0, std::ptr::null_mut()), + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + } + assert_eq!(sentinel, 0x5555); + let auth = AuthenticatedResourceContext::acquire(pointer).unwrap(); + auth.with_state(|_, state| { + assert!(state.resource_state.is_none()); + Ok(()) + }) + .unwrap(); + drop(auth); + assert_eq!( + unsafe { jett_rt_v1_context_destroy(&mut *ctx, &mut result) }, + JettRuntimeStatusV1::OK + ); +} diff --git a/crates/jett_runtime/src/native_abi/resource/borrowed_sums.rs b/crates/jett_runtime/src/native_abi/resource/borrowed_sums.rs new file mode 100644 index 00000000..313b72e3 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/borrowed_sums.rs @@ -0,0 +1,420 @@ +//! Nonowning conditional carriers. Shell identity stays separate from payload custody. +use super::*; + +impl NativeResourceState { + pub(super) fn validate_sum_parent( + &self, + handle: ResourceHandleId, + registry: &ResourceRegistry, + ) -> ResourceResult<()> { + self.sum_lease(handle, registry).map(|_| ()) + } + pub(super) fn projected_sum_parent( + &self, + frame: ResourceHandleId, + handle: ResourceHandleId, + source: NativeSumLoanSource, + ) -> ResourceResult<()> { + let Some(NativeResourceEntry::SumLoan(loan)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + self.frame(loan.frame)?; + self.frame(loan.source_frame)?; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&loan.shell) else { + return Err(NativeResourceError::InvalidHandle); + }; + if sum.slot != loan.source_slot || sum.frame != loan.source_frame || sum.shape != loan.shape + { + return Err(NativeResourceError::WrongOperation); + } + if self.carrier_adapters.contains_key(&loan.shell) { + self.carrier_adapter_validate(loan.shell, handle)?; + } + match source { + NativeSumLoanSource::ExistingBorrow { operation } => { + if operation != loan.borrow_operation + || !matches!( + self.operation(operation)?, + NativeOperation::BorrowSum { .. } + ) + { + return Err(NativeResourceError::WrongOperation); + } + if self.activation_frame(frame, self.frame(loan.frame)?.template)? != loan.frame { + return Err(NativeResourceError::WrongFrame); + } + } + NativeSumLoanSource::IncomingViewFormal { scope, parameter } => { + self.resident_sum_at(self.activation_frame(frame, scope)?, handle, parameter)?; + } + NativeSumLoanSource::CarrierProjected { operation } => { + self.carrier_adapter_source(frame, handle, operation)?; + } + } + Ok(()) + } + fn sum_lease( + &self, + handle: ResourceHandleId, + registry: &ResourceRegistry, + ) -> ResourceResult<&NativeSumLoan> { + let Some(NativeResourceEntry::SumLoan(loan)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + self.frame(loan.frame)?; + self.frame(loan.source_frame)?; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&loan.shell) else { + return Err(NativeResourceError::InvalidHandle); + }; + if sum.frame != loan.source_frame || sum.slot != loan.source_slot || sum.shape != loan.shape + { + return Err(NativeResourceError::WrongOperation); + } + if self.carrier_adapters.contains_key(&loan.shell) { + self.carrier_adapter_validate(loan.shell, handle)?; + } + self.carrier_frame_at(loan.frame, loan.shell, loan.source_slot)?; + match (&sum.payload, &loan.token) { + (NativeSumPayload::Some(owner) | NativeSumPayload::Ok(owner), Some(token)) => { + if self.custody.validate_owned(owner, registry)? + != self.custody.validate_borrowed(token, registry)? + { + return Err(NativeResourceError::WrongOperation); + } + } + (NativeSumPayload::None | NativeSumPayload::Fail { .. }, None) => {} + _ => return Err(NativeResourceError::WrongFamily), + } + Ok(loan) + } + pub(super) fn source_sum_loan( + &self, + frame: ResourceHandleId, + handle: ResourceHandleId, + source: NativeSumLoanSource, + shape: Option, + registry: &ResourceRegistry, + ) -> ResourceResult<&NativeSumLoan> { + let loan = self.sum_lease(handle, registry)?; + if shape.is_some_and(|shape| shape != loan.shape) { + return Err(NativeResourceError::WrongFamily); + } + match source { + NativeSumLoanSource::ExistingBorrow { operation } => { + if loan.borrow_operation != operation + || !matches!( + self.operation(operation)?, + NativeOperation::BorrowSum { .. } + ) + { + return Err(NativeResourceError::WrongOperation); + } + if self.activation_frame(frame, self.frame(loan.frame)?.template)? != loan.frame { + return Err(NativeResourceError::WrongFrame); + } + } + NativeSumLoanSource::IncomingViewFormal { scope, parameter } => { + let scope = self.activation_frame(frame, scope)?; + self.resident_sum_at(scope, handle, parameter)?; + } + NativeSumLoanSource::CarrierProjected { operation } => { + self.carrier_adapter_source(frame, handle, operation)?; + } + } + Ok(loan) + } + pub(super) fn sum_unborrowed(&self, shell: ResourceHandleId) -> ResourceResult<()> { + if self + .handles + .values() + .any(|entry| matches!(entry, NativeResourceEntry::SumLoan(loan) if loan.shell == shell)) + { + return Err(CustodyError::ActiveBorrow.into()); + } + Ok(()) + } + pub(super) fn frame_shells_unborrowed(&self, frame: ResourceHandleId) -> ResourceResult<()> { + for entry in self.handles.values() { + if let NativeResourceEntry::SumLoan(loan) = entry { + if loan.source_frame == frame && loan.frame != frame { + return Err(CustodyError::ActiveBorrow.into()); + } + } + } + Ok(()) + } + pub(super) fn sum_borrow( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + shell: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.runtime_purpose()?; + self.operation_frame(operation, frame)?; + let (source, lease_frame) = match self.operation(operation)? { + NativeOperation::BorrowSum { + source_sum_slot, + lease_frame, + .. + } => (*source_sum_slot, *lease_frame), + _ => return Err(NativeResourceError::WrongOperation), + }; + if lease_frame != self.frame(frame)?.template { + return Err(NativeResourceError::WrongFrame); + } + self.carrier(shell, source, registry)?; + self.carrier_frame_at(frame, shell, source)?; + let mut ids = self.reserve(1)?; + let handle = next_handle(&mut ids)?; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&shell) else { + return Err(NativeResourceError::WrongFamily); + }; + let (source_frame, shape) = (sum.frame, sum.shape); + let token = match &sum.payload { + NativeSumPayload::Some(owner) | NativeSumPayload::Ok(owner) => { + let Some(NativeResourceEntry::Frame(header)) = self.handles.get(&frame) else { + return Err(NativeResourceError::WrongFrame); + }; + Some(self.custody.begin_borrow(owner, &header.token, registry)?) + } + NativeSumPayload::Fail { shape, string } => { + ordinary + .validate_resource_ordinary(*string, &self.layout, *shape) + .map_err(ordinary_error)?; + None + } + NativeSumPayload::None => None, + }; + self.handles.insert( + handle, + NativeResourceEntry::SumLoan(NativeSumLoan { + frame, + borrow_operation: operation, + shell, + source_frame, + source_slot: source, + shape, + token, + }), + ); + Ok(handle) + } + pub(super) fn sum_view_tag( + &self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.operation_frame(operation, frame)?; + let source = match self.operation(operation)? { + NativeOperation::ObserveSumView { source, .. } => *source, + _ => return Err(NativeResourceError::WrongOperation), + }; + let loan = self.source_sum_loan(frame, handle, source, None, registry)?; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&loan.shell) else { + return Err(NativeResourceError::WrongFamily); + }; + Ok(u32::from(matches!( + sum.payload, + NativeSumPayload::Some(_) | NativeSumPayload::Ok(_) + ))) + } + pub(super) fn sum_view_project( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.runtime_purpose()?; + self.operation_frame(operation, frame)?; + let (source, path, lease_frame) = match self.operation(operation)? { + NativeOperation::ProjectSumView { + source, + path, + lease_frame, + .. + } => (*source, *path, *lease_frame), + _ => return Err(NativeResourceError::WrongOperation), + }; + if lease_frame != self.frame(frame)?.template { + return Err(NativeResourceError::WrongFrame); + } + let parent = self.source_sum_loan(frame, handle, source, None, registry)?; + let (shell, source_slot, source_frame) = + (parent.shell, parent.source_slot, parent.source_frame); + let mut ids = self.reserve(1)?; + let output = next_handle(&mut ids)?; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&shell) else { + return Err(NativeResourceError::WrongFamily); + }; + let owner = match (&sum.payload, path) { + (NativeSumPayload::Some(token), NativePayloadStep::Some) + | (NativeSumPayload::Ok(token), NativePayloadStep::Ok) => token, + _ => return Err(NativeResourceError::WrongOperation), + }; + let Some(NativeResourceEntry::Frame(header)) = self.handles.get(&frame) else { + return Err(NativeResourceError::WrongFrame); + }; + let token = self.custody.begin_borrow(owner, &header.token, registry)?; + self.handles.insert( + output, + NativeResourceEntry::Loan(NativeLoanEntry { + frame, + borrow_operation: operation, + source_slot, + source_frame, + token, + parent_sum: Some(handle), + }), + ); + Ok(output) + } + pub(super) fn sum_failure_read( + &mut self, + ordinary: &mut values::NativeValues, + registry: &ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.operation_frame(operation, frame)?; + let (source, shape) = match self.operation(operation)? { + NativeOperation::ReadFailureCompanion { + source, + failure_shape, + .. + } => (*source, *failure_shape), + _ => return Err(NativeResourceError::WrongOperation), + }; + let parent = self.source_sum_loan(frame, handle, source, None, registry)?; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&parent.shell) else { + return Err(NativeResourceError::WrongFamily); + }; + let NativeSumPayload::Fail { + shape: actual, + string, + } = sum.payload + else { + return Err(NativeResourceError::WrongOperation); + }; + if actual != shape { + return Err(NativeResourceError::WrongOperation); + } + ordinary + .clone_resource_string_companion(&self.layout, shape, string) + .map_err(ordinary_error) + } + fn sum_borrow_end_inner(&mut self, handle: ResourceHandleId) -> ResourceResult<()> { + if self.handles.values().any(|entry| matches!(entry, NativeResourceEntry::Loan(loan) if loan.parent_sum == Some(handle))) { return Err(CustodyError::ActiveBorrow.into()); } + let Some(NativeResourceEntry::SumLoan(loan)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if self.carrier_adapters.contains_key(&loan.shell) { + self.carrier_adapter_validate(loan.shell, handle)?; + } + let Some(NativeResourceEntry::SumLoan(mut loan)) = self.handles.remove(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if let Some(token) = &mut loan.token { + if let Err(error) = self.custody.end_borrow(token) { + self.handles + .insert(handle, NativeResourceEntry::SumLoan(loan)); + return Err(error.into()); + } + } + if let Some(origin) = self.carrier_adapters.get(&loan.shell) { + if origin.sum_loan != handle { + self.handles + .insert(handle, NativeResourceEntry::SumLoan(loan)); + return Err(NativeResourceError::WrongOperation); + } + let carrier_loan = origin.loan; + if let Err(error) = self.carrier_end_loan(loan.frame, carrier_loan) { + self.handles + .insert(handle, NativeResourceEntry::SumLoan(loan)); + return Err(error); + } + // Retain the shell's seal until its lease frame retires. Ending a + // view does not authorize adoption by an ordinary legacy slot. + } + Ok(()) + } + pub(super) fn sum_borrow_end( + &mut self, + registry: &ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + handle: ResourceHandleId, + ) -> ResourceResult<()> { + self.operation_frame(operation, frame)?; + let borrow = match self.operation(operation)? { + NativeOperation::EndSumBorrow { borrow, .. } => *borrow, + _ => return Err(NativeResourceError::WrongOperation), + }; + let source = match self.operation(borrow)? { + NativeOperation::BorrowSum { .. } => { + NativeSumLoanSource::ExistingBorrow { operation: borrow } + } + NativeOperation::Carrier { .. } => { + NativeSumLoanSource::CarrierProjected { operation: borrow } + } + _ => return Err(NativeResourceError::WrongOperation), + }; + let loan = self.source_sum_loan(frame, handle, source, None, registry)?; + if loan.frame != frame { + return Err(NativeResourceError::WrongFrame); + } + self.sum_borrow_end_inner(handle) + } + /// Complete only this frame's child projections, then its parent shell leases. + /// Incoming parent handles belong to the caller and are never adopted here. + pub(super) fn retire_sum_views(&mut self, frame: ResourceHandleId) -> ResourceResult<()> { + self.frame_shells_unborrowed(frame)?; + if !self.handles.values().any(|entry| matches!(entry, NativeResourceEntry::Loan(loan) if loan.frame == frame && loan.parent_sum.is_some()) || matches!(entry, NativeResourceEntry::SumLoan(loan) if loan.frame == frame)) { return Ok(()); } + let mut children = Vec::new(); + let mut parents = Vec::new(); + children + .try_reserve(self.handles.len()) + .map_err(|_| NativeResourceError::Capacity)?; + parents + .try_reserve(self.handles.len()) + .map_err(|_| NativeResourceError::Capacity)?; + for (&id, entry) in &self.handles { + match entry { + NativeResourceEntry::Loan(loan) + if loan.frame == frame && loan.parent_sum.is_some() => + { + children.push(id) + } + NativeResourceEntry::SumLoan(loan) if loan.frame == frame => parents.push(id), + _ => {} + } + } + let mut first: Option = None; + for handle in children.into_iter().rev() { + let Some(NativeResourceEntry::Loan(mut loan)) = self.handles.remove(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if let Err(error) = self.custody.end_borrow(&mut loan.token) { + self.handles.insert(handle, NativeResourceEntry::Loan(loan)); + first.get_or_insert(error.into()); + } + } + for handle in parents.into_iter().rev() { + if let Err(error) = self.sum_borrow_end_inner(handle) { + first.get_or_insert(error); + } + } + first.map_or(Ok(()), Err) + } +} diff --git a/crates/jett_runtime/src/native_abi/resource/carriers.rs b/crates/jett_runtime/src/native_abi/resource/carriers.rs new file mode 100644 index 00000000..74b45753 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/carriers.rs @@ -0,0 +1,1263 @@ +//! Opaque v3 zero-leaf trees. Live leaf custody is deliberately not synthesized. +use super::*; +use crate::resource_custody::{CarrierRootToken, OwnedResourceToken}; +#[path = "carriers/occupied.rs"] +mod occupied; +use crate::resource_custody::{ + NativeCarrierChildSource as ChildSource, NativeCarrierDestination as Destination, + NativeCarrierLoanSource as LoanSource, NativeCarrierNode as Node, + NativeCarrierObservation as Observation, NativeCarrierOperation as Op, + NativeCarrierPath as Path, NativeCarrierSelector as Selector, NativeCarrierSource as Source, +}; + +pub(super) struct NativeCarrierTree { + pub(super) node: u32, + pub(super) selector: u32, + pub(super) ordinary: Option<(u32, u64)>, + pub(super) owner: Option, + pub(super) children: Vec>, + pub(super) acquisition: Vec, +} +pub(super) struct NativeCarrierRoot { + pub(super) frame: ResourceHandleId, + pub(super) slot: u32, + pub(super) generation: u64, + pub(super) tree: NativeCarrierTree, + pub(super) custody: Option, +} +pub(super) struct NativeCarrierLoan { + pub(super) frame: ResourceHandleId, + pub(super) operation: u32, + pub(super) root: ResourceHandleId, + pub(super) generation: u64, + pub(super) path: Vec, + pub(super) parent: Option, + pub(super) incoming: Option<(u32, u32, ResourceHandleId)>, +} +pub(super) struct NativeCarrierBuilder { + pub(super) frame: ResourceHandleId, + pub(super) operation: u32, + pub(super) destination: u32, + pub(super) selector: u32, + pub(super) children: Vec>, + pub(super) acquisition: Vec, + pub(super) next: usize, + pub(super) custody: Option, +} +pub(super) struct NativeCarrierSumOrigin { + pub(super) loan: ResourceHandleId, + pub(super) operation: u32, + pub(super) sum_loan: ResourceHandleId, +} +fn index(selector: Selector, dynamic: u64) -> ResourceResult { + usize::try_from(match selector { + Selector::Static(i) => u64::from(i), + Selector::Dynamic => dynamic, + }) + .map_err(|_| NativeResourceError::WrongOperation) +} +fn normalized(path: &[Path], dynamic: u64) -> ResourceResult> { + let mut out = Vec::new(); + out.try_reserve_exact(path.len()) + .map_err(|_| NativeResourceError::Capacity)?; + for p in path { + out.push(match *p { + Path::List(s) => Path::List(Selector::Static( + u32::try_from(index(s, dynamic)?) + .map_err(|_| NativeResourceError::WrongOperation)?, + )), + Path::MapKey(s) => Path::MapKey(Selector::Static( + u32::try_from(index(s, dynamic)?) + .map_err(|_| NativeResourceError::WrongOperation)?, + )), + Path::MapValue(s) => Path::MapValue(Selector::Static( + u32::try_from(index(s, dynamic)?) + .map_err(|_| NativeResourceError::WrongOperation)?, + )), + p => p, + }); + } + Ok(out) +} +fn child_index(tree: &NativeCarrierTree, step: Path) -> ResourceResult { + Ok(match step { + Path::Field(i) => i as usize, + Path::EnumPayload { variant, field } if variant == tree.selector => field as usize, + Path::MachinePayload { state, field } if state == tree.selector => field as usize, + Path::List(s) => index(s, 0)?, + Path::MapKey(s) => index(s, 0)? + .checked_mul(2) + .ok_or(NativeResourceError::Capacity)?, + Path::MapValue(s) => index(s, 0)? + .checked_mul(2) + .and_then(|i| i.checked_add(1)) + .ok_or(NativeResourceError::Capacity)?, + Path::Some | Path::Ok if tree.selector == 1 => 0, + Path::Fail if tree.selector == 0 => 0, + _ => return Err(NativeResourceError::WrongOperation), + }) +} +pub(super) fn selected<'a>( + mut tree: &'a NativeCarrierTree, + path: &[Path], +) -> ResourceResult<&'a NativeCarrierTree> { + for p in path { + let i = child_index(tree, *p)?; + tree = tree + .children + .get(i) + .and_then(Option::as_ref) + .ok_or(NativeResourceError::WrongOperation)?; + } + Ok(tree) +} +pub(super) fn take_selected( + tree: &mut NativeCarrierTree, + path: &[Path], +) -> ResourceResult { + let (last, prefix) = path + .split_last() + .ok_or(NativeResourceError::WrongOperation)?; + let mut current = tree; + for p in prefix { + let i = child_index(current, *p)?; + current = current + .children + .get_mut(i) + .and_then(Option::as_mut) + .ok_or(NativeResourceError::WrongOperation)?; + } + let i = child_index(current, *last)?; + current + .children + .get_mut(i) + .and_then(Option::take) + .ok_or(NativeResourceError::WrongOperation) +} +impl NativeResourceState { + pub(super) fn carrier_operation( + &self, + operation: u32, + frame: ResourceHandleId, + ) -> ResourceResult { + if self.layout.wire_version() != 3 { + return Err(NativeResourceError::UnsupportedSourceBoundary); + } + let NativeOperation::Carrier { record } = *self.operation(operation)? else { + return Err(NativeResourceError::WrongOperation); + }; + let row = self + .layout + .carriers() + .operations + .get(record as usize) + .ok_or(NativeResourceError::WrongOperation)?; + if row.site != self.layout.operations()[operation as usize].site() { + return Err(NativeResourceError::WrongOperation); + } + self.operation_frame(operation, frame)?; + if self.frame(frame)?.template != row.operation.frame() { + return Err(NativeResourceError::WrongFrame); + } + Ok(row.operation.clone()) + } + pub(super) fn carrier_slot_empty( + &self, + frame: ResourceHandleId, + slot: u32, + ) -> ResourceResult<()> { + let s = self + .layout + .carriers() + .slots + .get(slot as usize) + .ok_or(NativeResourceError::WrongOperation)?; + if s.frame != self.frame(frame)?.template { + return Err(NativeResourceError::WrongFrame); + } + if self.handles.values().any(|e|matches!(e,NativeResourceEntry::CarrierRoot(r) if r.frame==frame&&r.slot==slot)||matches!(e,NativeResourceEntry::CarrierBuilder(b) if b.frame==frame&&b.destination==slot)){return Err(NativeResourceError::WrongOperation);} + Ok(()) + } + fn validate_tree( + &self, + ordinary: &values::NativeValues, + tree: &NativeCarrierTree, + ) -> ResourceResult<()> { + let mut pending = Vec::new(); + pending + .try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + pending.push(tree); + while let Some(t) = pending.pop() { + let n = self.layout.carriers().node(t.node)?; + if !matches!(n, Node::Resource { .. }) && t.owner.is_some() { + return Err(NativeResourceError::WrongFamily); + } + if t.acquisition + .iter() + .enumerate() + .any(|(i, p)| *p >= t.children.len() || t.acquisition[..i].contains(p)) + || t.children + .iter() + .enumerate() + .any(|(i, c)| c.is_some() && !t.acquisition.contains(&i)) + { + return Err(NativeResourceError::WrongOperation); + } + match n { + Node::Resource { .. } => { + if t.selector != 0 + || t.owner.is_none() + || t.ordinary.is_some() + || !t.children.is_empty() + || !t.acquisition.is_empty() + { + return Err(NativeResourceError::WrongFamily); + } + continue; + } + Node::Refinement { .. } => { + return Err(NativeResourceError::UnprovedCarrierRefinement); + } + Node::Ordinary { shape } => { + if !t.children.is_empty() || t.ordinary.map(|v| v.0) != Some(*shape) { + return Err(NativeResourceError::WrongFamily); + } + ordinary + .validate_resource_ordinary( + t.ordinary.ok_or(NativeResourceError::WrongFamily)?.1, + &self.layout, + *shape, + ) + .map_err(ordinary_error)?; + continue; + } + Node::List { element } => { + if t.selector != 0 + || t.ordinary.is_some() + || t.children.iter().flatten().any(|c| c.node != *element) + { + return Err(NativeResourceError::WrongFamily); + } + } + Node::Map { key, value } => { + if t.selector != 0 + || t.ordinary.is_some() + || t.children.len() % 2 != 0 + || t.children.iter().enumerate().any(|(i, c)| { + c.as_ref() + .is_some_and(|c| c.node != if i % 2 == 0 { *key } else { *value }) + }) + { + return Err(NativeResourceError::WrongFamily); + } + } + _ => { + let children = self + .layout + .carriers() + .selected_children(t.node, t.selector)?; + if t.ordinary.is_some() + || t.children.len() != children.len() + || t.children + .iter() + .zip(children) + .any(|(c, n)| c.as_ref().is_some_and(|c| c.node != n)) + { + return Err(NativeResourceError::WrongFamily); + } + } + } + pending + .try_reserve(t.children.len()) + .map_err(|_| NativeResourceError::Capacity)?; + pending.extend(t.children.iter().flatten()); + } + Ok(()) + } + pub(super) fn validate_complete_tree( + &self, + ordinary: &values::NativeValues, + tree: &NativeCarrierTree, + ) -> ResourceResult<()> { + self.validate_tree(ordinary, tree)?; + let mut pending = Vec::new(); + pending + .try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + pending.push(tree); + while let Some(t) = pending.pop() { + if t.children.iter().any(Option::is_none) { + return Err(NativeResourceError::WrongOperation); + } + pending + .try_reserve(t.children.len()) + .map_err(|_| NativeResourceError::Capacity)?; + pending.extend(t.children.iter().flatten()); + } + Ok(()) + } + pub(super) fn carrier_root( + &self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + handle: ResourceHandleId, + slot: u32, + ) -> ResourceResult<&NativeCarrierRoot> { + let Some(NativeResourceEntry::CarrierRoot(root)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + let row = self + .layout + .carriers() + .slots + .get(slot as usize) + .ok_or(NativeResourceError::WrongOperation)?; + if root.slot != slot + || root.generation != handle.raw() + || root.tree.node != row.node + || root.frame != self.activation_frame(frame, row.frame)? + { + return Err(NativeResourceError::WrongFrame); + } + self.frame(root.frame)?; + self.validate_tree(ordinary, &root.tree)?; + self.validate_carrier_ownership(root.custody.as_ref(), &root.tree)?; + if let Some(token) = &root.custody { + self.custody.validate_carrier_location( + token, + &self.frame(root.frame)?.token, + &self.layout.carrier_slot(slot)?, + false, + )?; + } + Ok(root) + } + pub(super) fn carrier_loan( + &self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + handle: ResourceHandleId, + source: LoanSource, + ) -> ResourceResult<&NativeCarrierLoan> { + let Some(NativeResourceEntry::CarrierLoan(loan)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + self.frame(loan.frame)?; + let Some(NativeResourceEntry::CarrierRoot(root)) = self.handles.get(&loan.root) else { + return Err(NativeResourceError::InvalidHandle); + }; + if root.generation != loan.generation { + return Err(NativeResourceError::InvalidHandle); + } + self.frame(root.frame)?; + self.validate_tree(ordinary, &root.tree)?; + self.validate_carrier_ownership(root.custody.as_ref(), &root.tree)?; + if let Some(token) = &root.custody { + self.custody.validate_carrier_location( + token, + &self.frame(root.frame)?.token, + &self.layout.carrier_slot(root.slot)?, + false, + )?; + } + self.validate_tree(ordinary, selected(&root.tree, &loan.path)?)?; + if let Some(parent) = loan.parent { + let Some(NativeResourceEntry::CarrierLoan(parent_loan)) = self.handles.get(&parent) + else { + return Err(NativeResourceError::InvalidHandle); + }; + if parent_loan.root != loan.root + || parent_loan.generation != loan.generation + || !loan.path.starts_with(&parent_loan.path) + { + return Err(NativeResourceError::WrongOperation); + } + } + match source { + LoanSource::ExistingBorrow { operation } => { + if loan.operation != operation + || loan.incoming.is_some() + || self.activation_frame(frame, self.frame(loan.frame)?.template)? != loan.frame + { + return Err(NativeResourceError::WrongOperation); + } + } + LoanSource::IncomingViewFormal { scope, parameter } => { + let Some((expected, p, call)) = loan.incoming else { + return Err(NativeResourceError::WrongOperation); + }; + let scope_frame = self.activation_frame(frame, scope)?; + if expected != scope || p != parameter || loan.frame != scope_frame { + return Err(NativeResourceError::WrongFrame); + } + let active = self.source_call(call)?; + if active.scope != Some(scope_frame) || active.phase != source::SourcePhase::Entered + { + return Err(NativeResourceError::WrongFrame); + } + } + } + Ok(loan) + } + pub(super) fn carrier_location( + &self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + handle: ResourceHandleId, + source: Source, + path: &[Path], + dynamic: u64, + ) -> ResourceResult<(ResourceHandleId, u64, Vec, Option)> { + let (root, generation, mut full, parent) = match source { + Source::Slot(slot) => { + let r = self.carrier_root(ordinary, frame, handle, slot)?; + (handle, r.generation, Vec::new(), None) + } + Source::Loan(source) => { + let l = self.carrier_loan(ordinary, frame, handle, source)?; + (l.root, l.generation, l.path.clone(), Some(handle)) + } + }; + let tail = normalized(path, dynamic)?; + full.try_reserve(tail.len()) + .map_err(|_| NativeResourceError::Capacity)?; + full.extend(tail); + let Some(NativeResourceEntry::CarrierRoot(r)) = self.handles.get(&root) else { + return Err(NativeResourceError::WrongFamily); + }; + self.layout.carriers().path_node(r.tree.node, &full)?; + selected(&r.tree, &full)?; + Ok((root, generation, full, parent)) + } + pub(super) fn carrier_unborrowed(&self, root: ResourceHandleId) -> ResourceResult<()> { + if self + .handles + .values() + .any(|e| matches!(e,NativeResourceEntry::CarrierLoan(l) if l.root==root)) + { + return Err(CustodyError::ActiveBorrow.into()); + } + Ok(()) + } + pub(super) fn carrier_construct_begin( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + operation: u32, + ) -> ResourceResult { + self.running(ordinary)?; + let Op::Construct { + destination, + selector, + children, + .. + } = self.carrier_operation(operation, frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + self.carrier_slot_empty(frame, destination)?; + let mut values = Vec::new(); + let mut acquisition = Vec::new(); + values + .try_reserve_exact(children.len()) + .map_err(|_| NativeResourceError::Capacity)?; + acquisition + .try_reserve_exact(children.len()) + .map_err(|_| NativeResourceError::Capacity)?; + values.resize_with(children.len(), || None); + let mut ids = self.reserve(1)?; + let handle = next_handle(&mut ids)?; + self.handles.insert( + handle, + NativeResourceEntry::CarrierBuilder(NativeCarrierBuilder { + frame, + operation, + destination, + selector, + children: values, + acquisition, + next: 0, + custody: None, + }), + ); + Ok(handle) + } + pub(super) fn carrier_child( + &mut self, + ordinary: &values::NativeValues, + builder: ResourceHandleId, + ordinal: u32, + bits: u64, + ) -> ResourceResult<()> { + self.running(ordinary)?; + let Some(NativeResourceEntry::CarrierBuilder(b)) = self.handles.get(&builder) else { + return Err(NativeResourceError::WrongFamily); + }; + let (frame, operation, next) = (b.frame, b.operation, b.next); + let Op::Construct { children, .. } = self.carrier_operation(operation, frame)? else { + return Err(NativeResourceError::WrongOperation); + }; + if ordinal as usize != next { + return Err(NativeResourceError::WrongOperation); + } + let child = children + .get(next) + .ok_or(NativeResourceError::WrongOperation)?; + if b.children + .get(child.index as usize) + .ok_or(NativeResourceError::WrongOperation)? + .is_some() + { + return Err(NativeResourceError::WrongOperation); + } + let tree = match child.source { + ChildSource::Ordinary { shape } => { + ordinary + .validate_resource_ordinary(bits, &self.layout, shape) + .map_err(ordinary_error)?; + NativeCarrierTree { + node: child.node, + selector: 0, + ordinary: Some((shape, bits)), + owner: None, + children: Vec::new(), + acquisition: Vec::new(), + } + } + ChildSource::Move { slot } => { + let handle = ResourceHandleId::new(bits)?; + let root = self.carrier_root(ordinary, frame, handle, slot)?; + self.validate_complete_tree(ordinary, &root.tree)?; + self.carrier_unborrowed(handle)?; + if root.custody.is_some() { + return Err(NativeResourceError::UnsupportedCarrierCustody); + } + let Some(NativeResourceEntry::CarrierRoot(r)) = self.handles.remove(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + r.tree + } + ChildSource::LeafOwner { .. } => { + return Err(NativeResourceError::UnsupportedCarrierCustody); + } + ChildSource::LeafSum { slot } => self.carrier_take_leaf_sum( + ordinary, + frame, + ResourceHandleId::new(bits)?, + slot, + child.node, + )?, + }; + let Some(NativeResourceEntry::CarrierBuilder(b)) = self.handles.get_mut(&builder) else { + return Err(NativeResourceError::WrongFamily); + }; + let target = b + .children + .get_mut(child.index as usize) + .ok_or(NativeResourceError::WrongOperation)?; + *target = Some(tree); + b.acquisition.push(child.index as usize); + b.next += 1; + Ok(()) + } + pub(super) fn carrier_commit( + &mut self, + ordinary: &values::NativeValues, + builder: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + let Some(NativeResourceEntry::CarrierBuilder(b)) = self.handles.get(&builder) else { + return Err(NativeResourceError::WrongFamily); + }; + let Op::Construct { children, .. } = self.carrier_operation(b.operation, b.frame)? else { + return Err(NativeResourceError::WrongOperation); + }; + self.validate_carrier_builder(ordinary, b)?; + if b.next != children.len() || b.children.iter().any(Option::is_none) { + return Err(NativeResourceError::WrongOperation); + } + let node = self.layout.carriers().slots[b.destination as usize].node; + for child in b.children.iter().flatten() { + self.validate_complete_tree(ordinary, child)?; + } + let is_ordinary = matches!(self.layout.carriers().node(node)?, Node::Ordinary { .. }); + let mut ids = self.reserve(1)?; + let output = next_handle(&mut ids)?; + let Some(NativeResourceEntry::CarrierBuilder(mut b)) = self.handles.remove(&builder) else { + return Err(NativeResourceError::WrongFamily); + }; + if let Some(root) = &b.custody { + if let Err(error) = self.custody.complete_carrier_root(root) { + self.handles + .insert(builder, NativeResourceEntry::CarrierBuilder(b)); + return Err(error.into()); + } + } + let tree = if is_ordinary { + b.children + .get_mut(0) + .and_then(Option::take) + .ok_or(NativeResourceError::WrongFamily)? + } else { + NativeCarrierTree { + node, + selector: b.selector, + ordinary: None, + owner: None, + children: b.children, + acquisition: b.acquisition, + } + }; + self.handles.insert( + output, + NativeResourceEntry::CarrierRoot(NativeCarrierRoot { + frame: b.frame, + slot: b.destination, + generation: output.raw(), + custody: b.custody, + tree, + }), + ); + Ok(output) + } + pub(super) fn carrier_move_reserved( + &mut self, + ordinary: &values::NativeValues, + handle: ResourceHandleId, + source: u32, + destination: u32, + source_frame: ResourceHandleId, + frame: ResourceHandleId, + output: ResourceHandleId, + ) -> ResourceResult { + let root = self.carrier_root(ordinary, source_frame, handle, source)?; + if root.custody.is_some() { + return Err(NativeResourceError::UnsupportedCarrierCustody); + } + self.validate_complete_tree(ordinary, &root.tree)?; + self.carrier_unborrowed(handle)?; + self.carrier_slot_empty(frame, destination)?; + let expected = self.layout.carriers().slots[destination as usize].node; + let Some(NativeResourceEntry::CarrierRoot(root)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if root.tree.node != expected { + return Err(NativeResourceError::WrongFamily); + } + let Some(NativeResourceEntry::CarrierRoot(mut root)) = self.handles.remove(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + root.frame = frame; + root.slot = destination; + root.generation = output.raw(); + self.handles + .insert(output, NativeResourceEntry::CarrierRoot(root)); + Ok(output) + } + pub(super) fn carrier_transfer( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + let Op::Transfer { + source, + destination, + .. + } = self.carrier_operation(operation, frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + let target = self.activation_frame( + frame, + self.layout.carriers().slots[destination as usize].frame, + )?; + let mut ids = self.reserve(1)?; + let output = next_handle(&mut ids)?; + self.carrier_move_reserved(ordinary, handle, source, destination, frame, target, output) + } + pub(super) fn carrier_qualify( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + let Op::QualifyMachine { + source, + destination, + machine, + state, + .. + } = self.carrier_operation(operation, frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + let root = self.carrier_root(ordinary, frame, handle, source)?; + self.validate_complete_tree(ordinary, &root.tree)?; + self.carrier_unborrowed(handle)?; + if root.custody.is_some() { + return Err(NativeResourceError::UnsupportedCarrierCustody); + } + if root.tree.selector != state + || self.layout.carriers().node(root.tree.node)? + != &(Node::MachineState { machine, state }) + || self.layout.carriers().slots[destination as usize].node != machine + { + return Err(NativeResourceError::WrongFamily); + } + let target = self.activation_frame( + frame, + self.layout.carriers().slots[destination as usize].frame, + )?; + self.carrier_slot_empty(target, destination)?; + let mut ids = self.reserve(1)?; + let output = next_handle(&mut ids)?; + let Some(NativeResourceEntry::CarrierRoot(mut root)) = self.handles.remove(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + root.tree.node = machine; + root.slot = destination; + root.frame = target; + root.generation = output.raw(); + self.handles + .insert(output, NativeResourceEntry::CarrierRoot(root)); + Ok(output) + } + pub(super) fn carrier_borrow( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + dynamic: u64, + ) -> ResourceResult { + self.running(ordinary)?; + let Op::Borrow { + source, + path, + lease_frame, + .. + } = self.carrier_operation(operation, frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + if lease_frame != self.frame(frame)?.template { + return Err(NativeResourceError::WrongFrame); + } + let (root, generation, path, parent) = + self.carrier_location(ordinary, frame, handle, source, &path, dynamic)?; + let mut ids = self.reserve(1)?; + let output = next_handle(&mut ids)?; + self.handles.insert( + output, + NativeResourceEntry::CarrierLoan(NativeCarrierLoan { + frame, + operation, + root, + generation, + path, + parent, + incoming: None, + }), + ); + Ok(output) + } + pub(super) fn carrier_end_loan( + &mut self, + frame: ResourceHandleId, + handle: ResourceHandleId, + ) -> ResourceResult<()> { + let Some(NativeResourceEntry::CarrierLoan(loan)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if loan.frame != frame + || self + .handles + .values() + .any(|e| matches!(e,NativeResourceEntry::CarrierLoan(l) if l.parent==Some(handle))) + || self + .carrier_adapters + .values() + .any(|a| a.loan == handle && self.handles.contains_key(&a.sum_loan)) + { + return Err(CustodyError::ActiveBorrow.into()); + } + self.handles.remove(&handle); + Ok(()) + } + pub(super) fn carrier_end( + &mut self, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + ) -> ResourceResult<()> { + let Op::EndBorrow { borrow, .. } = self.carrier_operation(operation, frame)? else { + return Err(NativeResourceError::WrongOperation); + }; + let Some(NativeResourceEntry::CarrierLoan(l)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if l.operation != borrow || l.incoming.is_some() { + return Err(NativeResourceError::WrongOperation); + } + self.carrier_end_loan(frame, handle) + } + pub(super) fn carrier_observe( + &self, + ordinary: &mut values::NativeValues, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + dynamic: u64, + ) -> ResourceResult { + self.running(ordinary)?; + let Op::Observe { + source, + path, + observation, + .. + } = self.carrier_operation(operation, frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + let (root, _, path, _) = + self.carrier_location(ordinary, frame, handle, source, &path, dynamic)?; + let Some(NativeResourceEntry::CarrierRoot(r)) = self.handles.get(&root) else { + return Err(NativeResourceError::WrongFamily); + }; + let tree = selected(&r.tree, &path)?; + match observation { + Observation::Length => Ok(u64::try_from( + match self.layout.carriers().node(tree.node)? { + Node::List { .. } => tree.children.len(), + Node::Map { .. } => tree.children.len() / 2, + _ => return Err(NativeResourceError::WrongFamily), + }, + ) + .map_err(|_| NativeResourceError::Capacity)?), + Observation::Tag => match self.layout.carriers().node(tree.node)? { + Node::Enum { variants, .. } => Ok(variants + .get(tree.selector as usize) + .ok_or(NativeResourceError::WrongFamily)? + .discriminant as u64), + Node::Optional { .. } | Node::Result { .. } => Ok(u64::from(tree.selector)), + _ => Err(NativeResourceError::WrongFamily), + }, + Observation::State => Ok(u64::from(tree.selector)), + Observation::Ordinary { shape, clone } => { + let (actual, bits) = tree.ordinary.ok_or(NativeResourceError::WrongFamily)?; + if actual != shape { + return Err(NativeResourceError::WrongFamily); + } + self.carrier_observe_ordinary(ordinary, shape, clone, bits) + } + } + } + fn carrier_observe_ordinary( + &self, + ordinary: &mut values::NativeValues, + shape: u32, + clone: bool, + bits: u64, + ) -> ResourceResult { + let required = Observation::ordinary_clone_policy(self.layout.shapes(), shape) + .ok_or(NativeResourceError::UnsupportedCarrierObservation)?; + if clone != required { + return Err(NativeResourceError::WrongOperation); + } + ordinary + .validate_resource_ordinary(bits, &self.layout, shape) + .map_err(ordinary_error)?; + if required { + ordinary + .clone_resource_string_companion(&self.layout, shape, bits) + .map_err(ordinary_error) + } else { + Ok(bits) + } + } + pub(super) fn carrier_extract( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + dynamic: u64, + ) -> ResourceResult { + self.running(ordinary)?; + let Op::Extract { + source, + path, + destination, + .. + } = self.carrier_operation(operation, frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + let (_, _, path, _) = self.carrier_location( + ordinary, + frame, + handle, + Source::Slot(source), + &path, + dynamic, + )?; + self.carrier_unborrowed(handle)?; + let Some(NativeResourceEntry::CarrierRoot(root)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if root.custody.is_some() { + return Err(NativeResourceError::UnsupportedCarrierCustody); + } + let projected = selected(&root.tree, &path)?; + self.validate_complete_tree(ordinary, projected)?; + match destination { + Destination::Slot(slot) => { + if projected.node != self.layout.carriers().slots[slot as usize].node { + return Err(NativeResourceError::WrongFamily); + } + } + Destination::Ordinary { shape } => { + if projected.ordinary.map(|v| v.0) != Some(shape) { + return Err(NativeResourceError::WrongFamily); + } + } + } + if path.is_empty() { + return Err(NativeResourceError::WrongOperation); + } + let output = match destination { + Destination::Slot(slot) => { + let target = self + .activation_frame(frame, self.layout.carriers().slots[slot as usize].frame)?; + self.carrier_slot_empty(target, slot)?; + let mut ids = self.reserve(1)?; + Some((next_handle(&mut ids)?, target, slot)) + } + Destination::Ordinary { .. } => None, + }; + let Some(NativeResourceEntry::CarrierRoot(root)) = self.handles.get_mut(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + let tree = take_selected(&mut root.tree, &path)?; + if let Some((out, target, slot)) = output { + self.handles.insert( + out, + NativeResourceEntry::CarrierRoot(NativeCarrierRoot { + frame: target, + slot, + generation: out.raw(), + custody: None, + tree, + }), + ); + Ok(out.raw()) + } else { + let Destination::Ordinary { shape } = destination else { + return Err(NativeResourceError::WrongFamily); + }; + let (actual, bits) = tree.ordinary.ok_or(NativeResourceError::WrongFamily)?; + if actual != shape { + return Err(NativeResourceError::WrongFamily); + } + Ok(bits) + } + } + fn carrier_take_leaf_sum( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + handle: ResourceHandleId, + slot: u32, + node: u32, + ) -> ResourceResult { + self.carrier_frame_at(frame, handle, slot)?; + self.sum_unborrowed(handle)?; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if sum.slot != slot + || self.layout.slots().get(slot as usize).map(|s| s.shape()) != Some(sum.shape) + || self.carrier_adapters.contains_key(&handle) + || !self.layout.carrier_equivalent_sum(node, sum.shape) + { + return Err(NativeResourceError::WrongFamily); + } + let (selector, child) = match sum.payload { + NativeSumPayload::None => (0, None), + NativeSumPayload::Fail { shape, string } => { + ordinary + .validate_resource_ordinary(string, &self.layout, shape) + .map_err(ordinary_error)?; + let Node::Result { fail, .. } = self.layout.carriers().node(node)? else { + return Err(NativeResourceError::WrongFamily); + }; + ( + 0, + Some(NativeCarrierTree { + node: *fail, + selector: 0, + ordinary: Some((shape, string)), + owner: None, + children: Vec::new(), + acquisition: Vec::new(), + }), + ) + } + _ => return Err(NativeResourceError::UnsupportedCarrierCustody), + }; + let mut children = Vec::new(); + let mut acquisition = Vec::new(); + children + .try_reserve_exact(usize::from(child.is_some())) + .map_err(|_| NativeResourceError::Capacity)?; + acquisition + .try_reserve_exact(usize::from(child.is_some())) + .map_err(|_| NativeResourceError::Capacity)?; + if let Some(child) = child { + children.push(Some(child)); + acquisition.push(0); + } + self.handles.remove(&handle); + Ok(NativeCarrierTree { + node, + selector, + ordinary: None, + owner: None, + children, + acquisition, + }) + } + fn prepare_carrier_cleanup<'a>( + &self, + trees: impl Iterator, + ) -> ResourceResult> { + let mut scan = Vec::new(); + for tree in trees { + scan.try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + scan.push(tree); + } + let mut count = 0usize; + while let Some(tree) = scan.pop() { + count = count.checked_add(1).ok_or(NativeResourceError::Capacity)?; + scan.try_reserve(tree.children.len()) + .map_err(|_| NativeResourceError::Capacity)?; + scan.extend(tree.children.iter().flatten()); + } + let mut pending = Vec::new(); + pending + .try_reserve_exact(count) + .map_err(|_| NativeResourceError::Capacity)?; + Ok(pending) + } + fn drop_carrier_tree_prepared( + &self, + ordinary: &mut values::NativeValues, + tree: NativeCarrierTree, + mut pending: Vec, + ) -> ResourceResult<()> { + pending.push(tree); + let mut first = None; + while let Some(mut tree) = pending.pop() { + if let Some((shape, bits)) = tree.ordinary { + if let Err(e) = ordinary + .drop_resource_typed_companion(&self.layout, shape, bits) + .map_err(ordinary_error) + { + first.get_or_insert(e); + } + } + // LIFO visiting makes the latest lexical acquisition retire first. + for i in tree.acquisition { + if let Some(child) = tree.children.get_mut(i).and_then(Option::take) { + pending.push(child); + } + } + } + first.map_or(Ok(()), Err) + } + pub(super) fn carrier_retire( + &mut self, + ordinary: &mut values::NativeValues, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + ) -> ResourceResult<()> { + let Op::Retire { source, .. } = self.carrier_operation(operation, frame)? else { + return Err(NativeResourceError::WrongOperation); + }; + let root = self.carrier_root(ordinary, frame, handle, source)?; + if root.custody.is_some() { + return Err(NativeResourceError::UnsupportedCarrierCustody); + } + let pending = self.prepare_carrier_cleanup(std::iter::once(&root.tree))?; + self.carrier_unborrowed(handle)?; + let Some(NativeResourceEntry::CarrierRoot(root)) = self.handles.remove(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + self.drop_carrier_tree_prepared(ordinary, root.tree, pending) + } + pub(super) fn carrier_reset_iteration( + &mut self, + ordinary: &mut values::NativeValues, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + ) -> ResourceResult<()> { + self.running(ordinary)?; + let Op::ResetIteration { + source, + carrier_binders, + leaf_sum_binders, + .. + } = self.carrier_operation(operation, frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + self.carrier_root(ordinary, frame, handle, source)?; + self.carrier_unborrowed(handle)?; + let mut holders = Vec::new(); + holders + .try_reserve_exact(carrier_binders.len() + leaf_sum_binders.len()) + .map_err(|_| NativeResourceError::Capacity)?; + for slot in carrier_binders { + let target = + self.activation_frame(frame, self.layout.carriers().slots[slot as usize].frame)?; + if self.handles.values().any(|e|matches!(e,NativeResourceEntry::CarrierBuilder(b) if b.frame==target&&b.destination==slot)){return Err(NativeResourceError::WrongOperation);} + if let Some(id) = self.handles.iter().find_map(|(id, e)| { + matches!(e,NativeResourceEntry::CarrierRoot(r) if r.frame==target&&r.slot==slot) + .then_some(*id) + }) { + let root = self.carrier_root(ordinary, frame, id, slot)?; + self.carrier_unborrowed(id)?; + if root.custody.is_some() { + return Err(NativeResourceError::UnsupportedCarrierCustody); + } + holders.push(( + id, + Some(self.prepare_carrier_cleanup(std::iter::once(&root.tree))?), + )); + } + } + for slot in leaf_sum_binders { + let target = + self.activation_frame(frame, self.layout.slots()[slot as usize].frame())?; + let id = self.handles.iter().find_map(|(id, e)| { + matches!(e,NativeResourceEntry::Sum(s) if s.frame==target&&s.slot==slot) + .then_some(*id) + }); + if let Some(id)=id{ + self.carrier_frame_at(frame,id,slot)?;self.sum_unborrowed(id)?; + let Some(NativeResourceEntry::Sum(sum))=self.handles.get(&id) else{return Err(NativeResourceError::WrongFamily);}; + if self.carrier_adapters.contains_key(&id)||sum.shape!=self.layout.slots()[slot as usize].shape(){return Err(NativeResourceError::WrongFamily);} + match sum.payload{NativeSumPayload::None=>{},NativeSumPayload::Fail{shape,string}=>ordinary.validate_resource_ordinary(string,&self.layout,shape).map_err(ordinary_error)?,_=>return Err(NativeResourceError::UnsupportedCarrierCustody)} + holders.push((id,None)); + }else if self.handles.values().any(|e|matches!(e,NativeResourceEntry::Owner(o) if o.frame==target&&o.slot==slot)||matches!(e,NativeResourceEntry::Prepared(p) if p.destination_frame==target&&p.destination==slot)){return Err(NativeResourceError::UnsupportedCarrierCustody);} + } + // Issued identities reflect the lexical acquisition order across both slot families. + holders.sort_by_key(|(id, _)| std::cmp::Reverse(id.raw())); + let mut first = None; + for (id, pending) in holders { + let result = match self.handles.remove(&id) { + Some(NativeResourceEntry::CarrierRoot(root)) => self.drop_carrier_tree_prepared( + ordinary, + root.tree, + pending.ok_or(NativeResourceError::WrongFamily)?, + ), + Some(NativeResourceEntry::Sum(NativeResourceSum { + payload: NativeSumPayload::Fail { shape, string }, + .. + })) => ordinary + .drop_resource_typed_companion(&self.layout, shape, string) + .map_err(ordinary_error), + Some(NativeResourceEntry::Sum(NativeResourceSum { + payload: NativeSumPayload::None, + .. + })) => Ok(()), + _ => Err(NativeResourceError::WrongFamily), + }; + if let Err(error) = result { + first.get_or_insert(error); + } + } + first.map_or(Ok(()), Err) + } + pub(super) fn safety_retire_carriers( + &mut self, + ordinary: &mut values::NativeValues, + ) -> ResourceResult<()> { + // The context has already been retired and waited for its leases. This + // backstop never publishes data or grants a reusable Source loan. + self.handles + .retain(|_, entry| !matches!(entry, NativeResourceEntry::CarrierLoan(_))); + let mut holders = Vec::new(); + holders + .try_reserve(self.handles.len()) + .map_err(|_| NativeResourceError::Capacity)?; + holders.extend(self.handles.iter().filter_map(|(id, entry)| { + matches!( + entry, + NativeResourceEntry::CarrierRoot(_) | NativeResourceEntry::CarrierBuilder(_) + ) + .then_some(*id) + })); + holders.sort_by_key(|id| std::cmp::Reverse(id.raw())); + let mut first = None; + for id in holders { + let pending = match self.handles.get(&id) { + Some(NativeResourceEntry::CarrierRoot(root)) => { + self.prepare_carrier_cleanup(std::iter::once(&root.tree))? + } + Some(NativeResourceEntry::CarrierBuilder(builder)) => { + let mut pending = + self.prepare_carrier_cleanup(builder.children.iter().flatten())?; + pending + .try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + pending + } + _ => continue, + }; + let (tree, custody) = match self.handles.remove(&id) { + Some(NativeResourceEntry::CarrierRoot(root)) => (root.tree, root.custody), + Some(NativeResourceEntry::CarrierBuilder(builder)) => ( + NativeCarrierTree { + node: self + .layout + .carriers() + .slots + .get(builder.destination as usize) + .ok_or(NativeResourceError::WrongOperation)? + .node, + selector: builder.selector, + ordinary: None, + owner: None, + children: builder.children, + acquisition: builder.acquisition, + }, + builder.custody, + ), + _ => continue, + }; + if let Some(token) = custody.as_ref() { + if let Err(error) = self.custody.retire_carrier_metadata(token) { + first.get_or_insert(NativeResourceError::Custody(error)); + } + } + if let Err(error) = self.drop_carrier_tree_prepared(ordinary, tree, pending) { + first.get_or_insert(error); + } + } + first.map_or(Ok(()), Err) + } +} + +#[cfg(test)] +#[path = "carriers_tests.rs"] +mod tests; diff --git a/crates/jett_runtime/src/native_abi/resource/carriers/occupied.rs b/crates/jett_runtime/src/native_abi/resource/carriers/occupied.rs new file mode 100644 index 00000000..37e131c4 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/carriers/occupied.rs @@ -0,0 +1,683 @@ +//! Occupied carrier transport joins typed paths to the Core ownership ledger. +use super::*; + +fn child_path( + graph: &crate::resource_custody::NativeCarrierLayout, + node: u32, + selector: u32, + index: usize, +) -> ResourceResult { + let i = u32::try_from(index).map_err(|_| NativeResourceError::Capacity)?; + Ok(match graph.node(node)? { + Node::Struct { .. } => Path::Field(i), + Node::Optional { .. } if selector == 1 && index == 0 => Path::Some, + Node::Result { .. } if index == 0 => { + if selector == 1 { + Path::Ok + } else { + Path::Fail + } + } + Node::Enum { .. } => Path::EnumPayload { + variant: selector, + field: i, + }, + Node::Machine { .. } => Path::MachinePayload { + state: selector, + field: i, + }, + Node::MachineState { state, .. } if *state == selector => Path::MachinePayload { + state: selector, + field: i, + }, + Node::List { .. } => Path::List(Selector::Static(i)), + Node::Map { .. } if index % 2 == 0 => Path::MapKey(Selector::Static(i / 2)), + Node::Map { .. } => Path::MapValue(Selector::Static(i / 2)), + _ => return Err(NativeResourceError::WrongFamily), + }) +} + +fn copied_path(path: &[Path], next: Option) -> ResourceResult> { + let mut out = Vec::new(); + out.try_reserve_exact(path.len() + usize::from(next.is_some())) + .map_err(|_| NativeResourceError::Capacity)?; + out.extend_from_slice(path); + out.extend(next); + Ok(out) +} + +fn owners<'a>( + graph: &crate::resource_custody::NativeCarrierLayout, + tree: &'a NativeCarrierTree, + path: &[Path], + out: &mut Vec<(&'a OwnedResourceToken, Vec)>, +) -> ResourceResult<()> { + if let Some(owner) = &tree.owner { + out.try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + out.push((owner, copied_path(path, None)?)); + } + for i in &tree.acquisition { + if let Some(child) = tree.children.get(*i).and_then(Option::as_ref) { + owners( + graph, + child, + &copied_path(path, Some(child_path(graph, tree.node, tree.selector, *i)?))?, + out, + )?; + } + } + Ok(()) +} + +// Borrow each distinct child once in physical order, then sort its rank vector +// into lexical acquisition order. No owner is copied or inferred from bits. +fn owners_mut<'a>( + graph: &crate::resource_custody::NativeCarrierLayout, + tree: &'a mut NativeCarrierTree, + from: &[Path], + prefix: &[Path], + rank: &[usize], + out: &mut Vec<(&'a mut OwnedResourceToken, Vec, Vec, Vec)>, +) -> ResourceResult<()> { + if let Some(owner) = &mut tree.owner { + let mut order = Vec::new(); + order + .try_reserve_exact(rank.len()) + .map_err(|_| NativeResourceError::Capacity)?; + order.extend_from_slice(rank); + let mut to = copied_path(prefix, None)?; + to.try_reserve(from.len()) + .map_err(|_| NativeResourceError::Capacity)?; + to.extend_from_slice(from); + out.try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + out.push((owner, copied_path(from, None)?, to, order)); + } + for (i, child) in tree.children.iter_mut().enumerate() { + if let Some(child) = child { + let order = tree + .acquisition + .iter() + .position(|p| *p == i) + .ok_or(NativeResourceError::WrongOperation)?; + let mut next_rank = Vec::new(); + next_rank + .try_reserve_exact(rank.len() + 1) + .map_err(|_| NativeResourceError::Capacity)?; + next_rank.extend_from_slice(rank); + next_rank.push(order); + owners_mut( + graph, + child, + &copied_path(from, Some(child_path(graph, tree.node, tree.selector, i)?))?, + prefix, + &next_rank, + out, + )?; + } + } + Ok(()) +} + +fn retirement_owners<'a>( + graph: &crate::resource_custody::NativeCarrierLayout, + tree: &'a mut NativeCarrierTree, +) -> ResourceResult)>> { + let mut ordered = Vec::new(); + owners_mut(graph, tree, &[], &[], &[], &mut ordered)?; + ordered.sort_by(|a, b| a.3.cmp(&b.3)); + let mut result = Vec::new(); + result + .try_reserve_exact(ordered.len()) + .map_err(|_| NativeResourceError::Capacity)?; + result.extend(ordered.into_iter().map(|(owner, from, _, _)| (owner, from))); + Ok(result) +} + +impl NativeResourceState { + pub(super) fn validate_carrier_ownership( + &self, + root: Option<&CarrierRootToken>, + tree: &NativeCarrierTree, + ) -> ResourceResult<()> { + let mut tokens = Vec::new(); + owners(self.layout.carriers(), tree, &[], &mut tokens)?; + match root { + Some(root) => self + .custody + .validate_carrier_inventory(root, &tokens) + .map_err(Into::into), + None if tokens.is_empty() => Ok(()), + None => Err(NativeResourceError::WrongFamily), + } + } + + pub(super) fn validate_carrier_builder( + &self, + ordinary: &values::NativeValues, + builder: &NativeCarrierBuilder, + ) -> ResourceResult<()> { + let Op::Construct { children, .. } = + self.carrier_operation(builder.operation, builder.frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + if builder.next > children.len() + || builder.acquisition.len() != builder.next + || builder + .acquisition + .iter() + .zip(&children) + .any(|(i, c)| *i != c.index as usize) + || builder.children.iter().enumerate().any(|(i, c)| { + c.is_some() + != children[..builder.next] + .iter() + .any(|c| c.index as usize == i) + }) + { + return Err(NativeResourceError::WrongOperation); + } + let node = self.layout.carriers().slots[builder.destination as usize].node; + let mut tokens = Vec::new(); + for i in &builder.acquisition { + let tree = builder.children[*i] + .as_ref() + .ok_or(NativeResourceError::WrongOperation)?; + if children[..builder.next] + .iter() + .find(|c| c.index as usize == *i) + .is_none_or(|c| c.node != tree.node) + { + return Err(NativeResourceError::WrongFamily); + } + self.validate_complete_tree(ordinary, tree)?; + let prefix = if matches!(self.layout.carriers().node(node)?, Node::Ordinary { .. }) { + Vec::new() + } else { + vec![child_path( + self.layout.carriers(), + node, + builder.selector, + *i, + )?] + }; + owners(self.layout.carriers(), tree, &prefix, &mut tokens)?; + } + match &builder.custody { + Some(root) => { + self.custody.validate_carrier_location( + root, + &self.frame(builder.frame)?.token, + &self.layout.carrier_slot(builder.destination)?, + true, + )?; + self.custody.validate_carrier_inventory(root, &tokens)?; + } + None if tokens.is_empty() => {} + None => return Err(NativeResourceError::WrongFamily), + } + Ok(()) + } + + fn builder_root(&mut self, builder: &mut NativeCarrierBuilder) -> ResourceResult<()> { + if builder.custody.is_none() { + let slot = self.layout.carrier_slot(builder.destination)?; + let Some(NativeResourceEntry::Frame(frame)) = self.handles.get(&builder.frame) else { + return Err(NativeResourceError::WrongFrame); + }; + builder.custody = Some(self.custody.begin_carrier_root(&frame.token, &slot)?); + } + Ok(()) + } + + pub(in crate::native_abi::resource) fn carrier_child_with_registry( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + builder_id: ResourceHandleId, + ordinal: u32, + bits: u64, + ) -> ResourceResult<()> { + self.running(ordinary)?; + let Some(NativeResourceEntry::CarrierBuilder(builder)) = self.handles.get(&builder_id) + else { + return Err(NativeResourceError::WrongFamily); + }; + self.validate_carrier_builder(ordinary, builder)?; + let Op::Construct { children, .. } = + self.carrier_operation(builder.operation, builder.frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + if ordinal as usize != builder.next { + return Err(NativeResourceError::WrongOperation); + } + let child = children + .get(builder.next) + .ok_or(NativeResourceError::WrongOperation)? + .clone(); + if !matches!(child.source, ChildSource::LeafOwner { .. }) { + // Whole occupied carrier children use the same typed root migration. + if let ChildSource::Move { slot } = child.source { + let input = ResourceHandleId::new(bits)?; + let root = self.carrier_root(ordinary, builder.frame, input, slot)?; + self.validate_complete_tree(ordinary, &root.tree)?; + if root.custody.is_some() { + return self.carrier_occupied_child( + ordinary, registry, builder_id, ordinal, input, child, + ); + } + } + return self.carrier_child(ordinary, builder_id, ordinal, bits); + } + let ChildSource::LeafOwner { slot } = child.source else { + unreachable!() + }; + let input = ResourceHandleId::new(bits)?; + self.carrier_frame_at(builder.frame, input, slot)?; + let owner = self.owner(input, slot)?; + let Node::Resource { kind } = self.layout.carriers().node(child.node)? else { + return Err(NativeResourceError::WrongFamily); + }; + let shape = self.layout.slots()[slot as usize].shape(); + if self.layout.shapes().get(shape as usize) != Some(&NativeShape::Resource { kind: *kind }) + { + return Err(NativeResourceError::WrongFamily); + } + self.custody.validate_owned(&owner.token, registry)?; + let Some(NativeResourceEntry::CarrierBuilder(mut builder)) = + self.handles.remove(&builder_id) + else { + return Err(NativeResourceError::WrongFamily); + }; + let result = (|| { + self.builder_root(&mut builder)?; + let node = self.layout.carriers().slots[builder.destination as usize].node; + let path = [child_path( + self.layout.carriers(), + node, + builder.selector, + child.index as usize, + )?]; + let Some(NativeResourceEntry::Owner(owner)) = self.handles.get_mut(&input) else { + return Err(NativeResourceError::WrongFamily); + }; + self.custody.transfer_into_carrier( + &mut owner.token, + builder + .custody + .as_ref() + .ok_or(NativeResourceError::WrongFamily)?, + &path, + registry, + )?; + let Some(NativeResourceEntry::Owner(owner)) = self.handles.remove(&input) else { + unreachable!() + }; + builder.children[child.index as usize] = Some(NativeCarrierTree { + node: child.node, + selector: 0, + ordinary: None, + owner: Some(owner.token), + children: Vec::new(), + acquisition: Vec::new(), + }); + builder.acquisition.push(child.index as usize); + builder.next += 1; + Ok(()) + })(); + self.handles + .insert(builder_id, NativeResourceEntry::CarrierBuilder(builder)); + result + } + + fn migrate_carrier_tree( + &mut self, + tree: &mut NativeCarrierTree, + source: &CarrierRootToken, + destination: &CarrierRootToken, + prefix: &[Path], + registry: &ResourceRegistry, + complete: bool, + ) -> ResourceResult<()> { + let mut ordered = Vec::new(); + owners_mut(self.layout.carriers(), tree, &[], prefix, &[], &mut ordered)?; + ordered.sort_by(|a, b| a.3.cmp(&b.3)); + let mut transfers = Vec::new(); + transfers + .try_reserve_exact(ordered.len()) + .map_err(|_| NativeResourceError::Capacity)?; + transfers.extend( + ordered + .into_iter() + .map(|(token, from, to, _)| (token, from, to)), + ); + self.custody.move_carrier_inventory( + source, + destination, + &mut transfers, + registry, + complete, + )?; + Ok(()) + } + + fn carrier_occupied_child( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + builder_id: ResourceHandleId, + _ordinal: u32, + input: ResourceHandleId, + child: crate::resource_custody::NativeCarrierChild, + ) -> ResourceResult<()> { + self.carrier_unborrowed(input)?; + let Some(NativeResourceEntry::CarrierBuilder(mut builder)) = + self.handles.remove(&builder_id) + else { + return Err(NativeResourceError::WrongFamily); + }; + let result = (|| { + self.builder_root(&mut builder)?; + let node = self.layout.carriers().slots[builder.destination as usize].node; + let path = [child_path( + self.layout.carriers(), + node, + builder.selector, + child.index as usize, + )?]; + let Some(NativeResourceEntry::CarrierRoot(mut root)) = self.handles.remove(&input) + else { + return Err(NativeResourceError::WrongFamily); + }; + let result = match (root.custody.as_ref(), builder.custody.as_ref()) { + (Some(source), Some(destination)) => self.migrate_carrier_tree( + &mut root.tree, + source, + destination, + &path, + registry, + false, + ), + _ => Err(NativeResourceError::WrongFamily), + }; + if let Err(error) = result { + self.handles + .insert(input, NativeResourceEntry::CarrierRoot(root)); + return Err(error); + } + builder.children[child.index as usize] = Some(root.tree); + builder.acquisition.push(child.index as usize); + builder.next += 1; + Ok(()) + })(); + self.handles + .insert(builder_id, NativeResourceEntry::CarrierBuilder(builder)); + result + } + + pub(in crate::native_abi::resource) fn carrier_move_reserved_with_registry( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + input: ResourceHandleId, + source: u32, + destination: u32, + source_frame: ResourceHandleId, + frame: ResourceHandleId, + output: ResourceHandleId, + ) -> ResourceResult { + let root = self.carrier_root(ordinary, source_frame, input, source)?; + self.validate_complete_tree(ordinary, &root.tree)?; + self.carrier_unborrowed(input)?; + self.carrier_slot_empty(frame, destination)?; + if root.tree.node != self.layout.carriers().slots[destination as usize].node { + return Err(NativeResourceError::WrongFamily); + } + if root.custody.is_none() { + return self.carrier_move_reserved( + ordinary, + input, + source, + destination, + source_frame, + frame, + output, + ); + } + let slot = self.layout.carrier_slot(destination)?; + let Some(NativeResourceEntry::Frame(header)) = self.handles.get(&frame) else { + return Err(NativeResourceError::WrongFrame); + }; + let destination_root = self.custody.begin_carrier_root(&header.token, &slot)?; + let Some(NativeResourceEntry::CarrierRoot(mut root)) = self.handles.remove(&input) else { + unreachable!() + }; + let result = match root.custody.as_ref() { + Some(source_root) => self.migrate_carrier_tree( + &mut root.tree, + source_root, + &destination_root, + &[], + registry, + true, + ), + None => Err(NativeResourceError::WrongFamily), + }; + if let Err(error) = result { + self.handles + .insert(input, NativeResourceEntry::CarrierRoot(root)); + self.custody.retire_carrier_metadata(&destination_root)?; + return Err(error); + } + root.custody = Some(destination_root); + root.frame = frame; + root.slot = destination; + root.generation = output.raw(); + self.handles + .insert(output, NativeResourceEntry::CarrierRoot(root)); + Ok(output) + } + + pub(in crate::native_abi::resource) fn carrier_transfer_with_registry( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + frame: ResourceHandleId, + operation: u32, + input: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + let Op::Transfer { + source, + destination, + .. + } = self.carrier_operation(operation, frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + let target = self.activation_frame( + frame, + self.layout.carriers().slots[destination as usize].frame, + )?; + let mut ids = self.reserve(1)?; + let output = next_handle(&mut ids)?; + self.carrier_move_reserved_with_registry( + ordinary, + registry, + input, + source, + destination, + frame, + target, + output, + ) + } + + pub(in crate::native_abi::resource) fn carrier_retire_with_registry( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + frame: ResourceHandleId, + operation: u32, + input: ResourceHandleId, + ) -> ResourceResult<()> { + let Op::Retire { source, .. } = self.carrier_operation(operation, frame)? else { + return Err(NativeResourceError::WrongOperation); + }; + let root = self.carrier_root(ordinary, frame, input, source)?; + self.carrier_unborrowed(input)?; + if root.custody.is_none() { + return self.carrier_retire(ordinary, frame, operation, input); + } + let mut tokens = Vec::new(); + owners(self.layout.carriers(), &root.tree, &[], &mut tokens)?; + self.custody.preflight_carrier_retirement( + root.custody + .as_ref() + .ok_or(NativeResourceError::WrongFamily)?, + &tokens, + registry, + )?; + let pending = self.prepare_carrier_cleanup(std::iter::once(&root.tree))?; + let Some(NativeResourceEntry::CarrierRoot(mut root)) = self.handles.remove(&input) else { + unreachable!() + }; + let preparation = retirement_owners(self.layout.carriers(), &mut root.tree); + let cleanup = match preparation { + Ok(mut owners) => match root.custody.as_ref() { + Some(custody) => { + self.custody + .close_carrier_inventory(custody, &mut owners, registry) + } + None => { + self.handles + .insert(input, NativeResourceEntry::CarrierRoot(root)); + return Err(NativeResourceError::WrongFamily); + } + }, + Err(error) => { + self.handles + .insert(input, NativeResourceEntry::CarrierRoot(root)); + return Err(error); + } + }; + if root + .custody + .as_ref() + .is_none_or(|custody| !self.custody.carrier_retirement_completed(custody)) + { + self.handles + .insert(input, NativeResourceEntry::CarrierRoot(root)); + return match self.cleanup(cleanup) { + Err(error) => Err(error), + Ok(()) => Err(NativeResourceError::WrongFamily), + }; + } + // Core marks each owner retired before invoking its trusted finalizer. + let core_result = self.cleanup(cleanup); + let ordinary_result = self.drop_carrier_tree_prepared(ordinary, root.tree, pending); + core_result?; + ordinary_result + } + + pub(in crate::native_abi::resource) fn prepare_carrier_frame( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + ) -> ResourceResult { + if self.handles.values().any(|e| matches!(e, NativeResourceEntry::CarrierLoan(l) if l.frame != frame + && matches!(self.handles.get(&l.root), Some(NativeResourceEntry::CarrierRoot(r)) if r.frame == frame))) { + return Err(CustodyError::ActiveBorrow.into()); + } + loop { + let next = self.handles.iter().find_map(|(id, e)| + (matches!(e, NativeResourceEntry::CarrierLoan(l) if l.frame == frame) + && !self.handles.values().any(|e| matches!(e, NativeResourceEntry::CarrierLoan(l) if l.parent == Some(*id)))).then_some(*id)); + let Some(id) = next else { + break; + }; + self.carrier_end_loan(frame, id)?; + } + if self + .handles + .values() + .any(|e| matches!(e, NativeResourceEntry::CarrierLoan(l) if l.frame == frame)) + { + return Err(CustodyError::ActiveBorrow.into()); + } + let mut holders = Vec::new(); + holders + .try_reserve_exact(self.handles.len()) + .map_err(|_| NativeResourceError::Capacity)?; + for (id, entry) in &self.handles { + let pending = match entry { + NativeResourceEntry::CarrierRoot(root) if root.frame == frame => { + self.carrier_root(ordinary, frame, *id, root.slot)?; + self.prepare_carrier_cleanup(std::iter::once(&root.tree))? + } + NativeResourceEntry::CarrierBuilder(builder) if builder.frame == frame => { + self.validate_carrier_builder(ordinary, builder)?; + let mut pending = + self.prepare_carrier_cleanup(builder.children.iter().flatten())?; + pending + .try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + pending + } + _ => continue, + }; + holders.push((*id, pending)); + } + holders.sort_by_key(|(id, _)| std::cmp::Reverse(id.raw())); + Ok(PreparedCarrierFrame { frame, holders }) + } + + pub(in crate::native_abi::resource) fn retire_carrier_frame_prepared( + &mut self, + ordinary: &mut values::NativeValues, + prepared: PreparedCarrierFrame, + ) -> ResourceResult<()> { + // This runs only after unified Core end_frame has discharged every scalar + // and carrier ticket in one acquisition order. It never finalizes owners. + let mut first = None; + for (id, pending) in prepared.holders { + match self.handles.get(&id) { + Some(NativeResourceEntry::CarrierRoot(r)) if r.frame == prepared.frame => { + if let Some(root) = &r.custody { + self.custody.retire_carrier_metadata(root)?; + } + } + Some(NativeResourceEntry::CarrierBuilder(b)) if b.frame == prepared.frame => { + if let Some(root) = &b.custody { + self.custody.retire_carrier_metadata(root)?; + } + } + _ => return Err(NativeResourceError::WrongFrame), + } + let tree = match self.handles.remove(&id) { + Some(NativeResourceEntry::CarrierRoot(r)) => r.tree, + Some(NativeResourceEntry::CarrierBuilder(b)) => NativeCarrierTree { + node: self.layout.carriers().slots[b.destination as usize].node, + selector: b.selector, + ordinary: None, + owner: None, + children: b.children, + acquisition: b.acquisition, + }, + _ => unreachable!(), + }; + if let Err(error) = self.drop_carrier_tree_prepared(ordinary, tree, pending) { + first.get_or_insert(error); + } + } + first.map_or(Ok(()), Err) + } +} + +pub(in crate::native_abi::resource) struct PreparedCarrierFrame { + frame: ResourceHandleId, + holders: Vec<(ResourceHandleId, Vec)>, +} diff --git a/crates/jett_runtime/src/native_abi/resource/carriers_sum.rs b/crates/jett_runtime/src/native_abi/resource/carriers_sum.rs new file mode 100644 index 00000000..0d111f88 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/carriers_sum.rs @@ -0,0 +1,392 @@ +//! V3 adapters keep absent legacy sums attached to their carrier projection. +use super::carriers::{ + NativeCarrierLoan, NativeCarrierSumOrigin, NativeCarrierTree, selected, take_selected, +}; +use super::*; +use crate::resource_custody::{ + NativeCarrierLoanSource, NativeCarrierNode, NativeCarrierOperation, NativeCarrierPath, + NativeCarrierSelector, NativeCarrierSource, +}; + +#[derive(Clone, Copy)] +enum AbsentCompanion { + None, + Fail { shape: u32, bits: u64 }, +} + +fn adapter_projection_matches( + expected: &[NativeCarrierPath], + actual: &[NativeCarrierPath], +) -> bool { + expected.len() == actual.len() + && expected + .iter() + .zip(actual) + .all(|(expected, actual)| match (expected, actual) { + ( + NativeCarrierPath::List(NativeCarrierSelector::Dynamic), + NativeCarrierPath::List(NativeCarrierSelector::Static(_)), + ) + | ( + NativeCarrierPath::MapKey(NativeCarrierSelector::Dynamic), + NativeCarrierPath::MapKey(NativeCarrierSelector::Static(_)), + ) + | ( + NativeCarrierPath::MapValue(NativeCarrierSelector::Dynamic), + NativeCarrierPath::MapValue(NativeCarrierSelector::Static(_)), + ) => true, + _ => expected == actual, + }) +} + +impl NativeResourceState { + fn carrier_absence(&self, tree: &NativeCarrierTree) -> ResourceResult { + match self.layout.carriers().node(tree.node)? { + NativeCarrierNode::Optional { .. } + if tree.selector == 0 && tree.children.is_empty() && tree.ordinary.is_none() => + { + Ok(AbsentCompanion::None) + } + NativeCarrierNode::Result { fail, .. } + if tree.selector == 0 && tree.children.len() == 1 && tree.ordinary.is_none() => + { + let child = tree.children[0] + .as_ref() + .ok_or(NativeResourceError::WrongOperation)?; + let NativeCarrierNode::Ordinary { shape } = self.layout.carriers().node(*fail)? + else { + return Err(NativeResourceError::UnsupportedCarrierCustody); + }; + if child.node != *fail || !child.children.is_empty() { + return Err(NativeResourceError::WrongFamily); + } + let (actual, bits) = child.ordinary.ok_or(NativeResourceError::WrongFamily)?; + if actual != *shape { + return Err(NativeResourceError::WrongFamily); + } + Ok(AbsentCompanion::Fail { + shape: *shape, + bits, + }) + } + NativeCarrierNode::Optional { .. } | NativeCarrierNode::Result { .. } => { + Err(NativeResourceError::UnsupportedCarrierCustody) + } + _ => Err(NativeResourceError::WrongFamily), + } + } + + /// This checks the private origin as well as the public legacy sum metadata. + /// An ended adapter remains sealed, but cannot be used as a live view. + pub(super) fn carrier_adapter_validate( + &self, + shell: ResourceHandleId, + sum_loan: ResourceHandleId, + ) -> ResourceResult<()> { + let origin = self + .carrier_adapters + .get(&shell) + .ok_or(NativeResourceError::WrongOperation)?; + if origin.sum_loan != sum_loan { + return Err(NativeResourceError::WrongOperation); + } + let Some(NativeResourceEntry::SumLoan(sum_lease)) = self.handles.get(&sum_loan) else { + return Err(NativeResourceError::InvalidHandle); + }; + let Some(NativeResourceEntry::CarrierLoan(loan)) = self.handles.get(&origin.loan) else { + return Err(NativeResourceError::InvalidHandle); + }; + let Some(NativeResourceEntry::CarrierRoot(root)) = self.handles.get(&loan.root) else { + return Err(NativeResourceError::InvalidHandle); + }; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&shell) else { + return Err(NativeResourceError::InvalidHandle); + }; + self.frame(loan.frame)?; + self.frame(root.frame)?; + if loan.generation != root.generation + || root.generation != loan.root.raw() + || loan.operation != origin.operation + || loan.incoming.is_some() + || sum_lease.shell != shell + || sum_lease.token.is_some() + || sum_lease.frame != loan.frame + || sum.frame != loan.frame + || sum_lease.source_frame != sum.frame + || sum_lease.source_slot != sum.slot + || sum_lease.shape != sum.shape + || sum_lease.borrow_operation != origin.operation + { + return Err(NativeResourceError::WrongOperation); + } + if let Some(parent) = loan.parent { + let Some(NativeResourceEntry::CarrierLoan(parent)) = self.handles.get(&parent) else { + return Err(NativeResourceError::InvalidHandle); + }; + self.frame(parent.frame)?; + if parent.root != loan.root + || parent.generation != loan.generation + || !loan.path.starts_with(&parent.path) + { + return Err(NativeResourceError::WrongOperation); + } + } + let NativeOperation::Carrier { record } = *self.operation(origin.operation)? else { + return Err(NativeResourceError::WrongOperation); + }; + let row = self + .layout + .carriers() + .operations + .get(usize::try_from(record).map_err(|_| NativeResourceError::WrongOperation)?) + .ok_or(NativeResourceError::WrongOperation)?; + let NativeCarrierOperation::AdaptSum { + frame: template, + source, + path, + sum_slot, + lease_frame, + borrowed: true, + } = &row.operation + else { + return Err(NativeResourceError::WrongOperation); + }; + // A caller lease remains valid while its callee is the active frame. + // Checking an origin must not require the issuing frame to be on top. + if self.layout.wire_version() != 3 + || row.site + != self.layout.operations()[usize::try_from(origin.operation) + .map_err(|_| NativeResourceError::WrongOperation)?] + .site() + || sum.slot != *sum_slot + || template != lease_frame + || *lease_frame != self.frame(loan.frame)?.template + { + return Err(NativeResourceError::WrongOperation); + } + let tail = match source { + NativeCarrierSource::Slot(slot) => { + if root.slot != *slot || loan.parent.is_some() { + return Err(NativeResourceError::WrongOperation); + } + loan.path.as_slice() + } + NativeCarrierSource::Loan(source) => { + let parent_handle = loan.parent.ok_or(NativeResourceError::WrongOperation)?; + let Some(NativeResourceEntry::CarrierLoan(parent)) = + self.handles.get(&parent_handle) + else { + return Err(NativeResourceError::InvalidHandle); + }; + match source { + NativeCarrierLoanSource::ExistingBorrow { operation } => { + if parent.operation != *operation || parent.incoming.is_some() { + return Err(NativeResourceError::WrongOperation); + } + } + NativeCarrierLoanSource::IncomingViewFormal { scope, parameter } => { + let Some((actual_scope, actual_parameter, call)) = parent.incoming else { + return Err(NativeResourceError::WrongOperation); + }; + let active_call = self.source_call(call)?; + if actual_scope != *scope + || actual_parameter != *parameter + || self.frame(parent.frame)?.template != *scope + || active_call.scope != Some(parent.frame) + || active_call.phase != source::SourcePhase::Entered + { + return Err(NativeResourceError::WrongOperation); + } + } + } + loan.path + .get(parent.path.len()..) + .ok_or(NativeResourceError::WrongOperation)? + } + }; + if !adapter_projection_matches(path, tail) { + return Err(NativeResourceError::WrongOperation); + } + let tree = selected(&root.tree, &loan.path)?; + if !self.layout.carrier_equivalent_sum(tree.node, sum.shape) { + return Err(NativeResourceError::WrongFamily); + } + match (self.carrier_absence(tree)?, &sum.payload) { + (AbsentCompanion::None, NativeSumPayload::None) => Ok(()), + ( + AbsentCompanion::Fail { shape, bits }, + NativeSumPayload::Fail { + shape: actual, + string, + }, + ) if shape == *actual && bits == *string => Ok(()), + _ => Err(NativeResourceError::WrongFamily), + } + } + + pub(super) fn carrier_adapter_source( + &self, + frame: ResourceHandleId, + handle: ResourceHandleId, + operation: u32, + ) -> ResourceResult<()> { + let Some(NativeResourceEntry::SumLoan(loan)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if self.layout.wire_version() != 3 || loan.borrow_operation != operation { + return Err(NativeResourceError::WrongOperation); + } + self.carrier_adapter_validate(loan.shell, handle)?; + if self.activation_frame(frame, self.frame(loan.frame)?.template)? != loan.frame { + return Err(NativeResourceError::WrongFrame); + } + Ok(()) + } + + pub(super) fn carrier_adapt( + &mut self, + ordinary: &mut values::NativeValues, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + dynamic: u64, + ) -> ResourceResult { + self.running(ordinary)?; + let NativeCarrierOperation::AdaptSum { + source, + path, + sum_slot, + lease_frame, + borrowed, + .. + } = self.carrier_operation(operation, frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + if lease_frame != self.frame(frame)?.template { + return Err(NativeResourceError::WrongFrame); + } + // A borrowed adapter's shell is owned solely by this lease frame. + self.slot_empty(frame, sum_slot)?; + let shape = self.layout.slots() + [usize::try_from(sum_slot).map_err(|_| NativeResourceError::WrongOperation)?] + .shape(); + let (root_handle, generation, full_path, parent) = + self.carrier_location(ordinary, frame, handle, source, &path, dynamic)?; + let Some(NativeResourceEntry::CarrierRoot(root)) = self.handles.get(&root_handle) else { + return Err(NativeResourceError::WrongFamily); + }; + let tree = selected(&root.tree, &full_path)?; + if !self.layout.carrier_equivalent_sum(tree.node, shape) { + return Err(NativeResourceError::WrongFamily); + } + let companion = self.carrier_absence(tree)?; + if let AbsentCompanion::Fail { shape, bits } = companion { + ordinary + .validate_resource_ordinary(bits, &self.layout, shape) + .map_err(ordinary_error)?; + } + if borrowed { + self.carrier_adapters + .try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + let mut ids = self.reserve(3)?; + let shell = next_handle(&mut ids)?; + let sum_loan = next_handle(&mut ids)?; + let carrier_loan = next_handle(&mut ids)?; + // The only owned companion copied by this bridge is an exact String. + let payload = match companion { + AbsentCompanion::None => NativeSumPayload::None, + AbsentCompanion::Fail { shape, bits } => NativeSumPayload::Fail { + shape, + string: ordinary + .clone_resource_string_companion(&self.layout, shape, bits) + .map_err(ordinary_error)?, + }, + }; + self.handles.insert( + carrier_loan, + NativeResourceEntry::CarrierLoan(NativeCarrierLoan { + frame, + operation, + root: root_handle, + generation, + path: full_path, + parent, + incoming: None, + }), + ); + self.handles.insert( + shell, + NativeResourceEntry::Sum(NativeResourceSum { + frame, + slot: sum_slot, + shape, + payload, + }), + ); + self.handles.insert( + sum_loan, + NativeResourceEntry::SumLoan(NativeSumLoan { + frame, + borrow_operation: operation, + shell, + source_frame: frame, + source_slot: sum_slot, + shape, + token: None, + }), + ); + self.carrier_adapters.insert( + shell, + NativeCarrierSumOrigin { + loan: carrier_loan, + operation, + sum_loan, + }, + ); + Ok(sum_loan) + } else { + if !matches!(source, NativeCarrierSource::Slot(_)) || parent.is_some() { + return Err(NativeResourceError::WrongOperation); + } + self.carrier_unborrowed(root_handle)?; + let mut ids = self.reserve(1)?; + let shell = next_handle(&mut ids)?; + if full_path.is_empty() { + let Some(NativeResourceEntry::CarrierRoot(_)) = self.handles.remove(&root_handle) + else { + return Err(NativeResourceError::WrongFamily); + }; + } else { + let Some(NativeResourceEntry::CarrierRoot(root)) = + self.handles.get_mut(&root_handle) + else { + return Err(NativeResourceError::WrongFamily); + }; + let _tree = take_selected(&mut root.tree, &full_path)?; + } + let payload = match companion { + AbsentCompanion::None => NativeSumPayload::None, + AbsentCompanion::Fail { shape, bits } => NativeSumPayload::Fail { + shape, + string: bits, + }, + }; + self.handles.insert( + shell, + NativeResourceEntry::Sum(NativeResourceSum { + frame, + slot: sum_slot, + shape, + payload, + }), + ); + Ok(shell) + } + } +} + +#[cfg(test)] +#[path = "carriers_sum_tests.rs"] +mod tests; diff --git a/crates/jett_runtime/src/native_abi/resource/carriers_sum_tests.rs b/crates/jett_runtime/src/native_abi/resource/carriers_sum_tests.rs new file mode 100644 index 00000000..38eb6a87 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/carriers_sum_tests.rs @@ -0,0 +1,432 @@ +use super::*; +use crate::resource_custody::{ + NativeCarrierChildSource, NativeCarrierOperation as CarrierOp, NativeCarrierPath as Path, + NativeCarrierSelector as Selector, NativeCarrierSource, +}; + +fn carrier_op(state: &NativeResourceState, select: impl Fn(&CarrierOp) -> bool) -> u32 { + state + .layout + .operations() + .iter() + .enumerate() + .find_map(|(ordinal, row)| { + let NativeOperation::Carrier { record } = row.operation() else { + return None; + }; + select(&state.layout.carriers().operations[usize::try_from(*record).unwrap()].operation) + .then(|| u32::try_from(ordinal).unwrap()) + }) + .expect("fixture carrier operation") +} + +fn legacy_op(state: &NativeResourceState, select: impl Fn(&NativeOperation) -> bool) -> u32 { + state + .layout + .operations() + .iter() + .enumerate() + .find_map(|(ordinal, row)| select(row.operation()).then(|| u32::try_from(ordinal).unwrap())) + .expect("fixture legacy operation") +} + +fn entry() -> NativeEntry { + NativeEntry { + function: 0, + signature: 0, + scope: 0, + } +} + +fn bridge_context(operation: impl FnOnce(&AuthenticatedResourceContext)) { + super::super::tests::context(|auth| { + install_disabled( + auth, + &crate::resource_custody::carrier_runtime_fixture_bytes(), + entry(), + ) + .unwrap(); + operation(auth); + }); +} + +fn start_record( + state: &mut NativeResourceState, + ordinary: &mut values::NativeValues, + registry: &ResourceRegistry, +) -> ResourceResult<(ResourceHandleId, ResourceHandleId, ResourceHandleId)> { + let attempt = state.begin_entry(ordinary, registry, entry(), ResourcePurpose::Runtime)?; + let root = state.root_frame(attempt)?; + let frame = state.begin_operation_frame(ordinary, 1, root)?; + let none_op = carrier_op(state, |operation| { + matches!(operation, + CarrierOp::Construct { destination: 0, selector: 0, children, .. } if children.is_empty()) + }); + let none_builder = state.carrier_construct_begin(ordinary, frame, none_op)?; + let none = state.carrier_commit(ordinary, none_builder)?; + let record_op = carrier_op(state, |operation| { + matches!(operation, + CarrierOp::Construct { destination: 2, children, .. } + if children.iter().any(|child| matches!(child.source, NativeCarrierChildSource::Move { slot: 0 }))) + }); + let builder = state.carrier_construct_begin(ordinary, frame, record_op)?; + state.carrier_child(ordinary, builder, 0, 7)?; + state.carrier_child(ordinary, builder, 1, none.raw())?; + let record = state.carrier_commit(ordinary, builder)?; + let transfer = carrier_op(state, |operation| { + matches!( + operation, + CarrierOp::Transfer { + source: 2, + destination: 9, + .. + } + ) + }); + let record = state.carrier_transfer(ordinary, frame, transfer, record)?; + Ok((attempt, frame, record)) +} + +fn finish( + state: &mut NativeResourceState, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + attempt: ResourceHandleId, + frame: ResourceHandleId, +) -> ResourceResult<()> { + state.end_operation_frame(ordinary, registry, frame)?; + assert!(state.carrier_adapters.is_empty()); + assert!(!state.handles.values().any(|entry| matches!( + entry, + NativeResourceEntry::CarrierRoot(_) + | NativeResourceEntry::CarrierLoan(_) + | NativeResourceEntry::CarrierBuilder(_) + | NativeResourceEntry::Sum(_) + | NativeResourceEntry::SumLoan(_) + ))); + assert_eq!( + state + .complete_entry(ordinary, registry, attempt, 0, false)? + .selected_kind, + 0 + ); + assert_eq!( + state.counts(ordinary, registry), + NativeResourceCounts { + ordinary_empty: 1, + ..NativeResourceCounts::default() + } + ); + Ok(()) +} + +#[test] +fn adapter_projection_rejects_a_different_same_shape_sibling() { + assert!(!adapter_projection_matches( + &[Path::Field(0)], + &[Path::Field(1)] + )); + assert!(!adapter_projection_matches( + &[Path::EnumPayload { + variant: 0, + field: 0 + }], + &[Path::EnumPayload { + variant: 1, + field: 0 + }], + )); + assert!(!adapter_projection_matches( + &[Path::MachinePayload { state: 0, field: 0 }], + &[Path::MachinePayload { state: 1, field: 0 }], + )); +} + +#[test] +fn adapter_dynamic_selection_keeps_its_collection_projection_kind() { + assert!(adapter_projection_matches( + &[Path::Field(2), Path::MapValue(Selector::Dynamic)], + &[Path::Field(2), Path::MapValue(Selector::Static(5))], + )); + assert!(!adapter_projection_matches( + &[Path::Field(2), Path::MapValue(Selector::Dynamic)], + &[Path::Field(2), Path::MapKey(Selector::Static(5))], + )); + assert!(!adapter_projection_matches( + &[Path::List(Selector::Static(4))], + &[Path::List(Selector::Static(5))], + )); + assert!(!adapter_projection_matches( + &[Path::List(Selector::Dynamic)], + &[Path::List(Selector::Static(5)), Path::Some], + )); +} + +#[test] +fn borrowed_adapter_survives_a_source_scope_and_pins_then_releases_the_exact_root() { + bridge_context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame, record) = start_record(state, ordinary, registry)?; + let adapt = carrier_op(state, |operation| { + matches!( + operation, + CarrierOp::AdaptSum { + source: NativeCarrierSource::Slot(9), + borrowed: true, + .. + } + ) + }); + let loan = state.carrier_adapt(ordinary, frame, adapt, record, 0)?; + let Some(NativeResourceEntry::SumLoan(sum)) = state.handles.get(&loan) else { + panic!("adapter sum loan"); + }; + let shell = sum.shell; + let origin_loan = state.carrier_adapters[&shell].loan; + assert!(matches!(state.handles.get(&origin_loan), + Some(NativeResourceEntry::CarrierLoan(loan)) if loan.root == record)); + let reverse = carrier_op(state, |operation| { + matches!( + operation, + CarrierOp::Transfer { + source: 9, + destination: 2, + .. + } + ) + }); + assert_eq!( + state.carrier_transfer(ordinary, frame, reverse, record), + Err(NativeResourceError::Custody(CustodyError::ActiveBorrow)) + ); + + let invoke = legacy_op(state, |operation| { + matches!( + operation, + NativeOperation::InvokeSourceFunction { callee: 1, .. } + ) + }); + let call = state.source_prepare(ordinary, frame, invoke)?; + state.source_actual(ordinary, registry, call, 0, 0, loan.raw())?; + let scope = state.source_enter(ordinary, registry, call)?; + assert_eq!(state.source_parameter(ordinary, scope, 0)?, loan.raw()); + let tag = legacy_op(state, |operation| { + matches!( + operation, + NativeOperation::ObserveSumView { + frame: 2, + source: NativeSumLoanSource::IncomingViewFormal { + scope: 2, + parameter: 0, + }, + } + ) + }); + assert_eq!(state.sum_view_tag(ordinary, registry, tag, scope, loan)?, 0); + let complete = legacy_op(state, |operation| { + matches!(operation, NativeOperation::Complete { frame: 2 }) + }); + state.scope_complete(ordinary, registry, scope, complete, 0)?; + state.source_status(ordinary, call)?; + + let end = legacy_op(state, |operation| { + matches!(operation, + NativeOperation::EndSumBorrow { borrow, .. } if *borrow == adapt) + }); + state.sum_borrow_end(registry, end, frame, loan)?; + assert!(!state.handles.contains_key(&loan)); + assert!(!state.handles.contains_key(&origin_loan)); + assert!(state.carrier_adapters.contains_key(&shell)); + let before_refusals = state.counts(ordinary, registry); + assert_eq!(before_refusals.loan_handles, 0); + assert_eq!(before_refusals.owner_handles, 2); + assert_eq!(before_refusals.provisional, 2); + assert_eq!(before_refusals.frame_handles, 2); + assert_eq!(before_refusals.registry, 0); + assert!(matches!(state.handles.get(&shell), + Some(NativeResourceEntry::Sum(sum)) + if sum.frame == frame && sum.slot == 0 && sum.shape == 5 + && matches!(sum.payload, NativeSumPayload::None))); + // The seal denies the owning family even after its exact loan ends. + assert_eq!( + state.carrier(shell, 0, registry), + Err(NativeResourceError::WrongFamily) + ); + assert_eq!( + state.sum_tag(shell), + Err(NativeResourceError::WrongOperation) + ); + assert_eq!( + state.sum_borrow_end(registry, end, frame, loan), + Err(NativeResourceError::WrongFamily) + ); + assert_eq!(state.counts(ordinary, registry), before_refusals); + assert!(state.carrier_adapters.contains_key(&shell)); + assert_eq!( + state.carrier_root(ordinary, frame, record, 9)?.generation, + record.raw() + ); + state.carrier_transfer(ordinary, frame, reverse, record)?; + finish(state, ordinary, registry, attempt, frame) + }) + .unwrap(); + }); +} + +#[test] +fn owned_adapter_extraction_returns_to_a_carrier_once() { + bridge_context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame, record) = start_record(state, ordinary, registry)?; + let adapt = carrier_op(state, |operation| { + matches!( + operation, + CarrierOp::AdaptSum { + source: NativeCarrierSource::Slot(9), + borrowed: false, + .. + } + ) + }); + let shell = state.carrier_adapt(ordinary, frame, adapt, record, 0)?; + assert_eq!(state.sum_tag(shell)?, 0); + assert!(!state.carrier_adapters.contains_key(&shell)); + assert_eq!( + state.carrier_adapt(ordinary, frame, adapt, record, 0), + Err(NativeResourceError::WrongOperation) + ); + let construct = carrier_op(state, |operation| { + matches!(operation, + CarrierOp::Construct { destination: 2, children, .. } + if children.iter().any(|child| matches!(child.source, + NativeCarrierChildSource::LeafSum { slot: 0 }))) + }); + let builder = state.carrier_construct_begin(ordinary, frame, construct)?; + state.carrier_child(ordinary, builder, 0, 19)?; + state.carrier_child(ordinary, builder, 1, shell.raw())?; + let restored = state.carrier_commit(ordinary, builder)?; + assert!(!state.handles.contains_key(&shell)); + assert_eq!( + state.sum_tag(shell), + Err(NativeResourceError::InvalidHandle) + ); + state.carrier_root(ordinary, frame, restored, 2)?; + finish(state, ordinary, registry, attempt, frame) + }) + .unwrap(); + }); +} + +#[test] +fn borrowed_adapter_refuses_forged_projection_and_legacy_owned_child_adoption() { + bridge_context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame, record) = start_record(state, ordinary, registry)?; + let adapt = carrier_op(state, |operation| { + matches!( + operation, + CarrierOp::AdaptSum { + source: NativeCarrierSource::Slot(9), + borrowed: true, + .. + } + ) + }); + let loan = state.carrier_adapt(ordinary, frame, adapt, record, 0)?; + let Some(NativeResourceEntry::SumLoan(sum)) = state.handles.get(&loan) else { + panic!("adapter sum loan"); + }; + let shell = sum.shell; + let origin_loan = state.carrier_adapters[&shell].loan; + let Some(NativeResourceEntry::CarrierLoan(origin)) = + state.handles.get_mut(&origin_loan) + else { + panic!("adapter carrier loan"); + }; + origin.path[0] = Path::Field(0); + assert_eq!( + state.carrier_adapter_validate(shell, loan), + Err(NativeResourceError::WrongOperation) + ); + assert!(state.handles.contains_key(&loan)); + assert!(state.handles.contains_key(&record)); + let Some(NativeResourceEntry::CarrierLoan(origin)) = + state.handles.get_mut(&origin_loan) + else { + panic!("adapter carrier loan"); + }; + origin.path[0] = Path::Field(1); + state.carrier_adapter_validate(shell, loan)?; + let end = legacy_op(state, |operation| { + matches!(operation, + NativeOperation::EndSumBorrow { borrow, .. } if *borrow == adapt) + }); + state.sum_borrow_end(registry, end, frame, loan)?; + + let construct = carrier_op(state, |operation| { + matches!(operation, + CarrierOp::Construct { destination: 2, children, .. } + if children.iter().any(|child| matches!(child.source, + NativeCarrierChildSource::LeafSum { slot: 0 }))) + }); + let builder = state.carrier_construct_begin(ordinary, frame, construct)?; + state.carrier_child(ordinary, builder, 0, 23)?; + assert_eq!( + state.carrier_child(ordinary, builder, 1, shell.raw()), + Err(NativeResourceError::WrongFamily) + ); + assert!(state.carrier_adapters.contains_key(&shell)); + assert!(state.handles.contains_key(&shell)); + assert!(state.handles.contains_key(&record)); + finish(state, ordinary, registry, attempt, frame) + }) + .unwrap(); + }); +} + +#[test] +fn nested_adapter_keeps_each_parent_lease_until_its_child_ends() { + bridge_context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame, record) = start_record(state, ordinary, registry)?; + let full_op = carrier_op(state, |operation| matches!(operation, + CarrierOp::Borrow { source: NativeCarrierSource::Slot(9), path, .. } + if path.is_empty())); + let full = state.carrier_borrow(ordinary, frame, full_op, record, 0)?; + let child_op = carrier_op(state, |operation| matches!(operation, + CarrierOp::Borrow { + source: NativeCarrierSource::Loan(NativeCarrierLoanSource::ExistingBorrow { operation: parent }), + path, .. + } if *parent == full_op && path.as_slice() == &[Path::Field(1)])); + let child = state.carrier_borrow(ordinary, frame, child_op, full, 0)?; + let adapt = carrier_op(state, |operation| matches!(operation, + CarrierOp::AdaptSum { + source: NativeCarrierSource::Loan(NativeCarrierLoanSource::ExistingBorrow { operation: parent }), + path, borrowed: true, .. + } if *parent == child_op && path.is_empty())); + let loan = state.carrier_adapt(ordinary, frame, adapt, child, 0)?; + let child_end = carrier_op(state, |operation| matches!(operation, + CarrierOp::EndBorrow { borrow, .. } if *borrow == child_op)); + let full_end = carrier_op(state, |operation| matches!(operation, + CarrierOp::EndBorrow { borrow, .. } if *borrow == full_op)); + assert_eq!(state.carrier_end(frame, child_end, child), + Err(NativeResourceError::Custody(CustodyError::ActiveBorrow))); + assert_eq!(state.carrier_end(frame, full_end, full), + Err(NativeResourceError::Custody(CustodyError::ActiveBorrow))); + let tag = legacy_op(state, |operation| matches!(operation, + NativeOperation::ObserveSumView { + source: NativeSumLoanSource::CarrierProjected { operation: origin }, .. + } if *origin == adapt)); + assert_eq!(state.sum_view_tag(ordinary, registry, tag, frame, loan)?, 0); + let end = legacy_op(state, |operation| matches!(operation, + NativeOperation::EndSumBorrow { borrow, .. } if *borrow == adapt)); + state.sum_borrow_end(registry, end, frame, loan)?; + state.carrier_end(frame, child_end, child)?; + state.carrier_end(frame, full_end, full)?; + let reverse = carrier_op(state, |operation| matches!(operation, + CarrierOp::Transfer { source: 9, destination: 2, .. })); + state.carrier_transfer(ordinary, frame, reverse, record)?; + finish(state, ordinary, registry, attempt, frame) + }).unwrap(); + }); +} diff --git a/crates/jett_runtime/src/native_abi/resource/carriers_tests.rs b/crates/jett_runtime/src/native_abi/resource/carriers_tests.rs new file mode 100644 index 00000000..dcd07962 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/carriers_tests.rs @@ -0,0 +1,562 @@ +//! Host protocol negative controls. These tests are not Source admission evidence. +use super::*; + +fn entry() -> NativeEntry { + NativeEntry { + function: 0, + signature: 0, + scope: 0, + } +} +fn context(test: impl FnOnce(&AuthenticatedResourceContext)) { + super::super::tests::context(|auth| { + install_disabled( + auth, + &crate::resource_custody::carrier_runtime_fixture_bytes(), + entry(), + ) + .unwrap(); + test(auth); + }); +} +fn refused_cleanup_context(test: impl FnOnce(&AuthenticatedResourceContext)) { + let mut context = Box::new(JettRuntimeContextV1::retired()); + let status = unsafe { + jett_rt_v1_context_create( + JETT_RUNTIME_ABI_VERSION_V1, + &mut *context, + &mut JettRuntimeResultV1::ok(), + ) + }; + assert_eq!(status, JettRuntimeStatusV1::OK); + let authenticated = AuthenticatedResourceContext::acquire(&*context).unwrap(); + install_disabled( + &authenticated, + &crate::resource_custody::carrier_runtime_fixture_bytes(), + entry(), + ) + .unwrap(); + test(&authenticated); + drop(authenticated); + let status = + unsafe { jett_rt_v1_context_destroy(&mut *context, &mut JettRuntimeResultV1::ok()) }; + assert_eq!(status, JettRuntimeStatusV1::INVALID_ARGUMENT); +} +fn op(state: &NativeResourceState, predicate: impl Fn(&Op) -> bool) -> u32 { + state + .layout + .operations() + .iter() + .enumerate() + .find_map(|(i, row)| { + let NativeOperation::Carrier { record } = row.operation() else { + return None; + }; + predicate(&state.layout.carriers().operations[*record as usize].operation) + .then(|| u32::try_from(i).unwrap()) + }) + .expect("fixture carrier operation") +} +fn start( + state: &mut NativeResourceState, + ordinary: &mut values::NativeValues, + registry: &ResourceRegistry, +) -> ResourceResult<(ResourceHandleId, ResourceHandleId)> { + let attempt = state.begin_entry(ordinary, registry, entry(), ResourcePurpose::Runtime)?; + let root = state.root_frame(attempt)?; + let frame = state.begin_operation_frame(ordinary, 1, root)?; + Ok((attempt, frame)) +} +fn none( + state: &mut NativeResourceState, + ordinary: &mut values::NativeValues, + frame: ResourceHandleId, + slot: u32, +) -> ResourceResult { + let operation = op( + state, + |r| matches!(r,Op::Construct{destination,selector:0,children,..} if *destination==slot&&children.is_empty()), + ); + let builder = state.carrier_construct_begin(ordinary, frame, operation)?; + state.carrier_commit(ordinary, builder) +} +fn record( + state: &mut NativeResourceState, + ordinary: &mut values::NativeValues, + frame: ResourceHandleId, + marker: u64, +) -> ResourceResult { + let optional = none(state, ordinary, frame, 0)?; + let operation = op( + state, + |r| matches!(r,Op::Construct{destination:2,children,..} if children.iter().any(|c|matches!(c.source,ChildSource::Move{slot:0}))), + ); + let builder = state.carrier_construct_begin(ordinary, frame, operation)?; + state.carrier_child(ordinary, builder, 0, marker)?; + state.carrier_child(ordinary, builder, 1, optional.raw())?; + state.carrier_commit(ordinary, builder) +} +fn finish( + state: &mut NativeResourceState, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + attempt: ResourceHandleId, +) -> ResourceResult<()> { + let result = state.complete_entry(ordinary, registry, attempt, 0, false)?; + assert_eq!(result.selected_kind, 0); + assert_eq!( + state.counts(ordinary, registry), + NativeResourceCounts { + ordinary_empty: 1, + ..NativeResourceCounts::default() + } + ); + Ok(()) +} + +#[test] +fn staged_constructor_checks_lexical_prefix_before_consuming_children() { + context(|auth| { + auth.with_resource(|state,ordinary,registry|{ + let (attempt,frame)=start(state,ordinary,registry)?;let optional=none(state,ordinary,frame,0)?; + let constructor=op(state,|r|matches!(r,Op::Construct{destination:2,children,..} if children.iter().any(|c|matches!(c.source,ChildSource::Move{slot:0})))); + let builder=state.carrier_construct_begin(ordinary,frame,constructor)?; + assert_eq!(state.carrier_child(ordinary,builder,1,optional.raw()),Err(NativeResourceError::WrongOperation)); + assert_eq!(state.carrier_commit(ordinary,builder),Err(NativeResourceError::WrongOperation)); + assert!(state.handles.contains_key(&optional));state.carrier_child(ordinary,builder,0,41)?; + state.carrier_child(ordinary,builder,1,optional.raw())?;let root=state.carrier_commit(ordinary,builder)?; + assert!(!state.handles.contains_key(&builder));assert!(!state.handles.contains_key(&optional)); + assert_eq!(state.carrier_commit(ordinary,builder),Err(NativeResourceError::WrongFamily)); + assert_eq!(state.carrier_root(ordinary,frame,root,2)?.tree.node,8);assert_eq!(registry.live_count(),0); + finish(state,ordinary,registry,attempt) + }).unwrap(); + }); +} + +#[test] +fn transfer_issues_a_fresh_generation_and_preserves_the_exact_nominal_stamp() { + context(|auth| { + auth.with_resource(|state,ordinary,registry|{ + let (attempt,frame)=start(state,ordinary,registry)?;let input=record(state,ordinary,frame,17)?; + let transfer=op(state,|r|matches!(r,Op::Transfer{source:2,destination:9,..})); + let output=state.carrier_transfer(ordinary,frame,transfer,input)?;assert_ne!(input,output); + assert_eq!(state.carrier_root(ordinary,frame,input,2).err(),Some(NativeResourceError::WrongFamily)); + let observer=op(state,|r|matches!(r,Op::Observe{source:Source::Slot(9),path,observation:Observation::Ordinary{shape:0,clone:false},..} if path==&[Path::Field(0)])); + assert_eq!(state.carrier_observe(ordinary,frame,observer,output,0)?,17); + assert_eq!(state.carrier_root(ordinary,frame,output,3).err(),Some(NativeResourceError::WrongFrame)); + let Some(NativeResourceEntry::CarrierRoot(root))=state.handles.get_mut(&output) else{panic!("root")};root.tree.node=16; + assert_eq!(state.carrier_root(ordinary,frame,output,9).err(),Some(NativeResourceError::WrongFrame)); + let Some(NativeResourceEntry::CarrierRoot(root))=state.handles.get_mut(&output) else{panic!("root")};root.tree.node=8; + finish(state,ordinary,registry,attempt) + }).unwrap(); + }); +} + +fn rejected_fake_leaf( + state: &mut NativeResourceState, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, +) -> ResourceResult<(ResourceHandleId, ResourceHandleId, ResourceHandleId)> { + let (attempt, frame) = start(state, ordinary, registry)?; + let mut ids = state.reserve(1)?; + let fake = next_handle(&mut ids)?; + state.handles.insert( + fake, + NativeResourceEntry::CarrierRoot(NativeCarrierRoot { + frame, + slot: 10, + generation: fake.raw(), + custody: None, + tree: NativeCarrierTree { + node: 3, + selector: 0, + ordinary: None, + owner: None, + children: vec![], + acquisition: vec![], + }, + }), + ); + let constructor = op(state, |r| { + matches!( + r, + Op::Construct { + destination: 0, + selector: 1, + .. + } + ) + }); + let builder = state.carrier_construct_begin(ordinary, frame, constructor)?; + assert_eq!( + state.carrier_child(ordinary, builder, 0, fake.raw()), + Err(NativeResourceError::WrongFamily) + ); + let Some(NativeResourceEntry::CarrierBuilder(builder)) = state.handles.get(&builder) else { + panic!("builder") + }; + assert_eq!(builder.next, 0); + assert!(builder.acquisition.is_empty()); + assert!(state.handles.contains_key(&fake)); + assert_eq!(registry.live_count(), 0); + Ok((attempt, frame, fake)) +} + +#[test] +fn selected_live_leaf_refuses_before_a_fake_handle_is_consumed() { + refused_cleanup_context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame, fake) = rejected_fake_leaf(state, ordinary, registry)?; + assert_eq!( + finish(state, ordinary, registry, attempt), + Err(NativeResourceError::WrongFamily) + ); + assert!(state.handles.contains_key(&fake)); + assert_eq!(state.active_frames.last(), Some(&frame)); + assert_eq!( + state.attempt.as_ref().unwrap().phase, + AttemptPhase::Completing + ); + assert!(ordinary.cleanup_failed); + assert_eq!(registry.live_count(), 0); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn restored_private_fake_leaf_allows_clean_builder_completion() { + context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame, fake) = rejected_fake_leaf(state, ordinary, registry)?; + assert!(matches!( + state.carrier_root(ordinary, frame, fake, 10), + Err(NativeResourceError::WrongFamily) + )); + assert!(matches!( + state.handles.remove(&fake), + Some(NativeResourceEntry::CarrierRoot(_)) + )); + finish(state, ordinary, registry, attempt) + }) + .unwrap(); + }); +} + +#[test] +fn partial_string_prefix_retires_once_after_a_later_stage_refuses() { + context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame) = start(state, ordinary, registry)?; + let mut ids = ordinary + .prepare_resource_ordinary_output() + .map_err(ordinary_error)?; + let text = ordinary + .publish_resource_error("kept prefix".to_owned(), &mut ids) + .map_err(ordinary_error)?; + let constructor = op(state, |r| { + matches!( + r, + Op::Construct { + destination: 1, + selector: 0, + .. + } + ) + }); + let builder = state.carrier_construct_begin(ordinary, frame, constructor)?; + state.carrier_child(ordinary, builder, 0, text)?; + assert_eq!( + state.carrier_child(ordinary, builder, 1, 0), + Err(NativeResourceError::WrongOperation) + ); + assert!(!ordinary.is_empty()); + finish(state, ordinary, registry, attempt)?; + assert!( + ordinary + .drop_resource_typed_companion(&state.layout, 1, text) + .is_err() + ); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn latent_generic_state_and_refinement_metadata_never_synthesizes_live_custody() { + context(|auth| { + auth.with_resource(|state,ordinary,registry|{ + let (attempt,_frame)=start(state,ordinary,registry)?; + let scalar=||NativeCarrierTree{node:0,selector:0,ordinary:Some((0,17)),owner:None,children:vec![],acquisition:vec![]}; + let absent=||NativeCarrierTree{node:4,selector:0,ordinary:None,owner:None,children:vec![],acquisition:vec![]}; + let generic=NativeCarrierTree{node:13,selector:0,ordinary:None,owner:None,children:vec![Some(absent())],acquisition:vec![0]}; + state.validate_complete_tree(ordinary,&generic)?; + assert!(matches!(state.layout.carriers().node(13)?,Node::Struct{arguments,..} if arguments==&[3])); + assert!(matches!(state.layout.carriers().node(14)?,Node::Struct{arguments,..} if arguments==&[4])); + let state_value=NativeCarrierTree{node:11,selector:0,ordinary:None,owner:None,children:vec![Some(scalar())],acquisition:vec![0]};state.validate_complete_tree(ordinary,&state_value)?; + let wrong_state=NativeCarrierTree{selector:1,..state_value};assert!(state.validate_complete_tree(ordinary,&wrong_state).is_err()); + let refined=NativeCarrierTree{node:12,selector:0,ordinary:None,owner:None,children:vec![Some(scalar())],acquisition:vec![0]}; + assert_eq!(state.validate_complete_tree(ordinary,&refined),Err(NativeResourceError::UnprovedCarrierRefinement)); + assert_eq!(registry.live_count(),0);finish(state,ordinary,registry,attempt) + }).unwrap(); + }); +} + +#[test] +fn extraction_tombstones_each_path_once_and_iteration_retains_original_length() { + context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame) = start(state, ordinary, registry)?; + let a = none(state, ordinary, frame, 0)?; + let b = none(state, ordinary, frame, 11)?; + let constructor = op( + state, + |r| matches!(r,Op::Construct{destination:6,children,..} if children.len()==2), + ); + let builder = state.carrier_construct_begin(ordinary, frame, constructor)?; + state.carrier_child(ordinary, builder, 0, a.raw())?; + state.carrier_child(ordinary, builder, 1, b.raw())?; + let list = state.carrier_commit(ordinary, builder)?; + let length = op(state, |r| { + matches!( + r, + Op::Observe { + source: Source::Slot(6), + observation: Observation::Length, + .. + } + ) + }); + let extract = op(state, |r| { + matches!( + r, + Op::Extract { + source: 6, + destination: Destination::Slot(12), + .. + } + ) + }); + let reset = op(state, |r| matches!(r, Op::ResetIteration { source: 6, .. })); + state.carrier_reset_iteration(ordinary, frame, reset, list)?; + let first = state.carrier_extract(ordinary, frame, extract, list, 0)?; + assert_eq!(state.carrier_observe(ordinary, frame, length, list, 0)?, 2); + assert_eq!( + state.carrier_extract(ordinary, frame, extract, list, 0), + Err(NativeResourceError::WrongOperation) + ); + state.carrier_reset_iteration(ordinary, frame, reset, list)?; + assert!(!state.handles.contains_key(&ResourceHandleId::new(first)?)); + let second = state.carrier_extract(ordinary, frame, extract, list, 1)?; + assert_ne!(first, second); + state.carrier_reset_iteration(ordinary, frame, reset, list)?; + assert_eq!(state.carrier_observe(ordinary, frame, length, list, 0)?, 2); + assert_eq!( + state.carrier_extract(ordinary, frame, extract, list, 1), + Err(NativeResourceError::WrongOperation) + ); + let Some(NativeResourceEntry::CarrierRoot(root)) = state.handles.get(&list) else { + panic!("iterator") + }; + assert_eq!( + state.validate_complete_tree(ordinary, &root.tree), + Err(NativeResourceError::WrongOperation) + ); + finish(state, ordinary, registry, attempt) + }) + .unwrap(); + }); +} + +#[test] +fn checked_machine_widening_preserves_state_and_enum_tag_uses_its_discriminant() { + context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame) = start(state, ordinary, registry)?; + let constructor = op(state, |r| { + matches!( + r, + Op::Construct { + destination: 13, + .. + } + ) + }); + let builder = state.carrier_construct_begin(ordinary, frame, constructor)?; + state.carrier_child(ordinary, builder, 0, 17)?; + let qualified = state.carrier_commit(ordinary, builder)?; + let qualify = op(state, |r| { + matches!( + r, + Op::QualifyMachine { + source: 13, + destination: 14, + machine: 10, + state: 0, + .. + } + ) + }); + let general = state.carrier_qualify(ordinary, frame, qualify, qualified)?; + assert_ne!(general, qualified); + assert_eq!( + state.carrier_root(ordinary, frame, qualified, 13).err(), + Some(NativeResourceError::WrongFamily) + ); + assert_eq!( + state.carrier_root(ordinary, frame, general, 14)?.tree.node, + 10 + ); + let observe_state = op(state, |r| { + matches!( + r, + Op::Observe { + source: Source::Slot(14), + observation: Observation::State, + .. + } + ) + }); + assert_eq!( + state.carrier_observe(ordinary, frame, observe_state, general, 0)?, + 0 + ); + assert_eq!( + state.carrier_qualify(ordinary, frame, qualify, general), + Err(NativeResourceError::WrongFrame) + ); + let constructor = op(state, |r| { + matches!( + r, + Op::Construct { + destination: 15, + .. + } + ) + }); + let builder = state.carrier_construct_begin(ordinary, frame, constructor)?; + state.carrier_child(ordinary, builder, 0, 17)?; + let enumeration = state.carrier_commit(ordinary, builder)?; + let tag = op(state, |r| { + matches!( + r, + Op::Observe { + source: Source::Slot(15), + observation: Observation::Tag, + .. + } + ) + }); + assert_eq!( + state.carrier_observe(ordinary, frame, tag, enumeration, 0)?, + (-7i64) as u64 + ); + finish(state, ordinary, registry, attempt) + }) + .unwrap(); + }); +} + +#[test] +fn forged_scalar_clone_flag_refuses_without_changing_the_parent_or_obligation() { + context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame) = start(state, ordinary, registry)?; + let parent = record(state, ordinary, frame, 17)?; + let before = state.counts(ordinary, registry); + assert_eq!( + state.carrier_observe_ordinary(ordinary, 0, true, 17), + Err(NativeResourceError::WrongOperation) + ); + assert_eq!(state.counts(ordinary, registry), before); + let marker = state + .carrier_root(ordinary, frame, parent, 2)? + .tree + .children[0] + .as_ref() + .unwrap() + .ordinary + .unwrap(); + assert_eq!(marker, (0, 17)); + assert_eq!(state.carrier_observe_ordinary(ordinary, 0, false, 17)?, 17); + assert_eq!(state.carrier_observe_ordinary(ordinary, 3, false, 1)?, 1); + assert_eq!(state.carrier_observe_ordinary(ordinary, 2, false, 0)?, 0); + finish(state, ordinary, registry, attempt) + }) + .unwrap(); + }); +} + +#[test] +fn forged_string_alias_refuses_and_a_real_clone_retires_independently() { + context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame) = start(state, ordinary, registry)?; + let mut ids = ordinary + .prepare_resource_ordinary_output() + .map_err(ordinary_error)?; + let text = ordinary + .publish_resource_error("parent-owned text".to_owned(), &mut ids) + .map_err(ordinary_error)?; + let constructor = op(state, |row| { + matches!( + row, + Op::Construct { + destination: 1, + selector: 0, + .. + } + ) + }); + let builder = state.carrier_construct_begin(ordinary, frame, constructor)?; + state.carrier_child(ordinary, builder, 0, text)?; + let parent = state.carrier_commit(ordinary, builder)?; + let before = state.counts(ordinary, registry); + assert_eq!( + state.carrier_observe_ordinary(ordinary, 1, false, text), + Err(NativeResourceError::WrongOperation) + ); + assert_eq!(state.counts(ordinary, registry), before); + let cloned = state.carrier_observe_ordinary(ordinary, 1, true, text)?; + ordinary + .drop_resource_typed_companion(&state.layout, 1, cloned) + .map_err(ordinary_error)?; + ordinary + .validate_resource_ordinary(text, &state.layout, 1) + .map_err(ordinary_error)?; + assert!(state.carrier_root(ordinary, frame, parent, 1).is_ok()); + finish(state, ordinary, registry, attempt)?; + assert!( + ordinary + .drop_resource_typed_companion(&state.layout, 1, text) + .is_err() + ); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn unsupported_owning_observation_flags_refuse_before_inspecting_a_fake_handle() { + context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, _) = start(state, ordinary, registry)?; + let before = state.counts(ordinary, registry); + for shape in [5, 6] { + for clone in [false, true] { + assert_eq!( + state.carrier_observe_ordinary(ordinary, shape, clone, u64::MAX), + Err(NativeResourceError::UnsupportedCarrierObservation) + ); + } + } + assert_eq!(state.counts(ordinary, registry), before); + assert!(ordinary.is_empty()); + finish(state, ordinary, registry, attempt) + }) + .unwrap(); + }); +} diff --git a/crates/jett_runtime/src/native_abi/resource/entry_outcome_tests.rs b/crates/jett_runtime/src/native_abi/resource/entry_outcome_tests.rs new file mode 100644 index 00000000..0e83ff79 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/entry_outcome_tests.rs @@ -0,0 +1,416 @@ +//! Root observation controls; distinct from Source/native linked acceptance. +use super::*; + +#[derive(Debug, PartialEq, Eq)] +struct Observation { + counts: NativeResourceCounts, + body: Option, + cleanup: Option, + root_body: Option, + ordinary: Option, + script_remaining: usize, +} + +fn observation(auth: &AuthenticatedResourceContext) -> Observation { + auth.with_state(|_, state| { + let resource = state.resource_state.as_ref().unwrap(); + let attempt = resource.attempt.as_ref().unwrap(); + Ok(Observation { + counts: resource.counts(&state.values, &state.resources), + body: attempt.body.as_ref().map(|failure| match failure { + NativeBodyFailure::Resource(error) => error.status().code(), + NativeBodyFailure::HostPanic => JettRuntimeStatusV1::PANIC.code(), + }), + cleanup: attempt.cleanup, + root_body: attempt.root_body_status, + ordinary: state + .values + .resource_failure() + .map(|failure| failure.status.code()), + script_remaining: resource.script_remaining(), + }) + }) + .unwrap() +} + +fn observe(auth: &AuthenticatedResourceContext, scope: ResourceHandleId) -> ResourceResult { + auth.with_state(|_, state| { + state + .resource_state + .as_ref() + .unwrap() + .entry_outcome(scope, 0, 3, 0) + }) +} + +fn pointer(auth: &AuthenticatedResourceContext) -> *const JettRuntimeContextV1 { + auth.key.owner_address as *const JettRuntimeContextV1 +} + +fn begin( + auth: &AuthenticatedResourceContext, +) -> (ResourceHandleId, ResourceHandleId, ResourceHandleId) { + auth.with_resource(|s, v, r| { + let attempt = s.begin_entry(v, r, entry(), ResourcePurpose::Runtime)?; + let root = s.root_frame(attempt)?; + let op = s.begin_operation_frame(v, 1, root)?; + Ok((attempt, root, op)) + }) + .unwrap() +} + +#[test] +fn native_resource_entry_outcome_c_refusals_are_readonly_and_original_body_survives_first_error() { + context(|auth| { + let f = fixture(5, false, false); + let (attempt, root, op, _) = setup(auth, &f, &[]); + let initial = observation(auth); + let mut out = 9001; + assert_ne!( + unsafe { + leaves::jett_rt_v1_resource_entry_outcome( + pointer(auth), + root.raw(), + 0, + 3, + 0, + &mut out, + ) + }, + JettRuntimeStatusV1::OK + ); + assert_eq!(out, 9001); + assert_eq!(observation(auth), initial); + + let child = auth + .with_resource(|s, v, r| { + let shell = s.absent_sum(v, op, f.create.unwrap())?; + let call = s.source_prepare(v, op, f.source)?; + s.source_actual(v, r, call, 0, 0, shell.raw())?; + let child = s.source_enter(v, r, call)?; + let parameter = ResourceHandleId::new(s.source_parameter(v, child, 0)?)?; + let inner = s.begin_operation_frame(v, 4, child)?; + let dest = s.destination_frame(inner, f.transfer_return)?; + let provisional = s.transfer(v, r, f.transfer_return, inner, parameter, dest)?; + s.end_operation_frame(v, r, inner)?; + s.scope_complete(v, r, child, f.complete_scope, 0)?; + let returned = s.return_publish(v, r, child, f.publish.unwrap(), provisional)?; + s.source_status(v, call)?; + s.sum_drop(v, r, f.close, op, returned)?; + Ok(child) + }) + .unwrap(); + let before_child_query = observation(auth); + assert_ne!( + unsafe { + leaves::jett_rt_v1_resource_entry_outcome( + pointer(auth), + child.raw(), + 1, + 4, + 3, + &mut out, + ) + }, + JettRuntimeStatusV1::OK + ); + assert_eq!(out, 9001); + assert_eq!(observation(auth), before_child_query); + + auth.with_resource(|s, v, r| s.end_operation_frame(v, r, op)) + .unwrap(); + assert_ne!(unsafe { values::jett_rt_v1_assert_fail(pointer(auth)) }, 0); + assert_ne!( + unsafe { + leaves::jett_rt_v1_resource_scope_complete( + pointer(auth), + root.raw(), + f.complete_root, + 71, + ) + }, + JettRuntimeStatusV1::OK + ); + let completed_root = observation(auth); + assert_eq!(completed_root.root_body, Some(71)); + assert!(completed_root.body.is_some() && completed_root.ordinary.is_some()); + assert_eq!(completed_root.counts.frames, 0); + for (scope, function, signature, template) in [ + (root, 1, 3, 0), + (root, 0, 4, 0), + (root, 0, 3, 3), + (child, 0, 3, 0), + ] { + assert_ne!( + unsafe { + leaves::jett_rt_v1_resource_entry_outcome( + pointer(auth), + scope.raw(), + function, + signature, + template, + &mut out, + ) + }, + JettRuntimeStatusV1::OK + ); + assert_eq!(out, 9001); + assert_eq!(observation(auth), completed_root); + } + for _ in 0..2 { + assert_eq!( + unsafe { + leaves::jett_rt_v1_resource_entry_outcome( + pointer(auth), + root.raw(), + 0, + 3, + 0, + &mut out, + ) + }, + JettRuntimeStatusV1::OK + ); + assert_eq!(out, 71); + assert_eq!(observation(auth), completed_root); + } + auth.with_resource(|s, v, r| { + let completion = s.complete_entry(v, r, attempt, out, false)?; + assert_eq!((completion.body_status, completion.selected_kind), (71, 1)); + assert_eq!(s.completed_messages(attempt)?.1, b"assertion failed"); + Ok(()) + }) + .unwrap(); + out = 9001; + assert_ne!( + unsafe { + leaves::jett_rt_v1_resource_entry_outcome( + pointer(auth), + root.raw(), + 0, + 3, + 0, + &mut out, + ) + }, + JettRuntimeStatusV1::OK + ); + assert_eq!(out, 9001); + }); +} + +#[test] +fn native_resource_entry_outcome_zero_is_independent_of_cleanup_panic_and_c_output_preflight() { + context(|auth| { + let f = fixture(4, false, false); + let (attempt, root, op, network) = setup(auth, &f, &[(3, 501, 0, "")]); + let owner = acquire(auth, op, network); + // Move the owner through the real helper Return into the root holder; + // otherwise Operation cleanup would finalize it before root completion. + auth.with_resource(|s, v, r| { + let call = s.source_prepare(v, op, f.source)?; + s.source_actual(v, r, call, 0, 0, owner.raw())?; + let child = s.source_enter(v, r, call)?; + let parameter = ResourceHandleId::new(s.source_parameter(v, child, 0)?)?; + let inner = s.begin_operation_frame(v, 4, child)?; + let destination = s.destination_frame(inner, f.transfer_return)?; + let provisional = s.transfer(v, r, f.transfer_return, inner, parameter, destination)?; + s.end_operation_frame(v, r, inner)?; + s.scope_complete(v, r, child, f.complete_scope, 0)?; + s.return_publish(v, r, child, f.publish.unwrap(), provisional)?; + s.source_status(v, call) + }) + .unwrap(); + let before = observation(auth); + assert_eq!(before.counts.owners, 1); + for out in [ + ptr::null_mut::(), + pointer(auth).cast_mut().cast::(), + pointer(auth) + .cast::() + .wrapping_add(1) + .cast_mut() + .cast::(), + (usize::MAX - 3) as *mut u32, + ] { + assert_eq!( + unsafe { + leaves::jett_rt_v1_resource_entry_outcome( + pointer(auth), + root.raw(), + 0, + 3, + 0, + out, + ) + }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + assert_eq!(observation(auth), before); + } + auth.with_resource(|s, v, r| { + s.end_operation_frame(v, r, op)?; + assert_eq!( + s.scope_complete(v, r, root, f.complete_root, 0), + Err(NativeResourceError::Cleanup( + ResourceCleanupFailure::FinalizerPanic + )) + ); + Ok(()) + }) + .unwrap(); + let after = observation(auth); + assert_eq!(after.root_body, Some(0)); + assert_eq!(after.cleanup, Some(ResourceCleanupFailure::FinalizerPanic)); + assert_eq!( + ( + after.counts.owners, + after.counts.frames, + after.counts.registry + ), + (0, 0, 0) + ); + let mut out = 9001; + assert_eq!( + unsafe { + leaves::jett_rt_v1_resource_entry_outcome( + pointer(auth), + root.raw(), + 0, + 3, + 0, + &mut out, + ) + }, + JettRuntimeStatusV1::OK + ); + assert_eq!(out, 0); + assert_eq!(observation(auth), after); + auth.with_resource(|s, v, r| { + let completion = s.complete_entry(v, r, attempt, out, false)?; + assert_eq!((completion.body_status, completion.selected_kind), (0, 3)); + assert_ne!(completion.cleanup_status, 0); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn native_resource_entry_outcome_is_retained_only_after_actual_root_retirement() { + context(|auth| { + let f = fixture(5, false, false); + let (attempt, root, op, _) = setup(auth, &f, &[]); + auth.with_resource(|s, v, r| { + s.end_operation_frame(v, r, op)?; + let mut foreign = ResourceRegistry::new(); + assert!(matches!( + s.scope_complete(v, &mut foreign, root, f.complete_root, 0), + Err(NativeResourceError::Cleanup(_)) + )); + assert!(s.frame(root).is_ok()); + assert_eq!(s.attempt.as_ref().unwrap().root_body_status, None); + Ok(()) + }) + .unwrap(); + let before = observation(auth); + assert!(observe(auth, root).is_err()); + assert_eq!(observation(auth), before); + auth.with_resource(|s, v, r| { + // The prior cleanup refusal remains selected; this does not reset it. + assert_eq!( + s.scope_complete(v, r, root, f.complete_root, 0), + Err(NativeResourceError::BodyFailed) + ); + assert!(s.frame(root).is_err()); + Ok(()) + }) + .unwrap(); + assert_eq!(observe(auth, root), Ok(0)); + auth.with_resource(|s, v, r| { + let completion = s.complete_entry(v, r, attempt, 0, false)?; + assert_eq!(completion.selected_kind, 3); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn native_resource_entry_outcome_refuses_foreign_and_previous_attempt_roots() { + context(|auth| { + let f = fixture(5, false, false); + install_scripted(auth, &f.bytes, entry(), script(&[], 2)).unwrap(); + let (first, first_root, first_op) = begin(auth); + auth.with_resource(|s, v, r| { + s.end_operation_frame(v, r, first_op)?; + s.scope_complete(v, r, first_root, f.complete_root, 0)?; + Ok(()) + }) + .unwrap(); + assert_eq!(observe(auth, first_root), Ok(0)); + auth.with_resource(|s, v, r| s.complete_entry(v, r, first, 0, false)) + .unwrap(); + let (second, second_root, second_op) = begin(auth); + let current = observation(auth); + assert!(observe(auth, first_root).is_err()); + assert_eq!(observation(auth), current); + auth.with_resource(|s, v, r| { + s.end_operation_frame(v, r, second_op)?; + s.scope_complete(v, r, second_root, f.complete_root, 0)?; + Ok(()) + }) + .unwrap(); + let retired = observation(auth); + context(|foreign_auth| { + let (foreign_attempt, foreign_root, foreign_op, _) = setup(foreign_auth, &f, &[]); + foreign_auth + .with_resource(|s, v, r| { + s.end_operation_frame(v, r, foreign_op)?; + s.scope_complete(v, r, foreign_root, f.complete_root, 0)?; + Ok(()) + }) + .unwrap(); + let foreign_before = observation(foreign_auth); + assert_ne!(foreign_root, second_root); + assert!(observe(auth, foreign_root).is_err()); + assert!(observe(foreign_auth, second_root).is_err()); + assert_eq!(observation(auth), retired); + assert_eq!(observation(foreign_auth), foreign_before); + assert_eq!(observe(foreign_auth, foreign_root), Ok(0)); + foreign_auth + .with_resource(|s, v, r| s.complete_entry(v, r, foreign_attempt, 0, false)) + .unwrap(); + }); + assert!(observe(auth, first_root).is_err()); + assert_eq!(observe(auth, second_root), Ok(0)); + auth.with_resource(|s, v, r| s.complete_entry(v, r, second, 0, false)) + .unwrap(); + assert!(observe(auth, second_root).is_err()); + }); +} + +#[test] +fn native_resource_entry_outcome_refuses_required_worker_purpose_without_latching() { + context(|auth| { + let f = fixture(5, false, false); + install_scripted(auth, &f.bytes, entry(), script(&[], 1)).unwrap(); + let (attempt, root) = auth + .with_resource(|s, v, r| { + let attempt = s.begin_entry(v, r, entry(), ResourcePurpose::ExplicitComptime)?; + let root = s.root_frame(attempt)?; + s.scope_complete(v, r, root, f.complete_root, 0)?; + Ok((attempt, root)) + }) + .unwrap(); + let before = observation(auth); + assert_eq!(observe(auth, root), Err(NativeResourceError::WrongPurpose)); + assert_eq!(observation(auth), before); + auth.with_resource(|s, v, r| { + let completion = s.complete_entry(v, r, attempt, 0, false)?; + assert_eq!(completion.selected_kind, 0); + Ok(()) + }) + .unwrap(); + }); +} diff --git a/crates/jett_runtime/src/native_abi/resource/leaves.rs b/crates/jett_runtime/src/native_abi/resource/leaves.rs new file mode 100644 index 00000000..71c1509f --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/leaves.rs @@ -0,0 +1,788 @@ +//! Exact compiler-internal Resource leaves. No provider installation or key adoption. +use super::*; +#[path = "leaves/carriers.rs"] +mod carrier_leaves; + +/// Addresses cannot establish lifetime/exclusivity; those remain the unsafe C +/// caller contract. Reject null/misaligned/overflow/known context overlaps before effect. +pub(super) fn output_ok(context: *const JettRuntimeContextV1, out: *mut T) -> bool { + let start = out.addr(); + let Some(end) = start.checked_add(std::mem::size_of::()) else { + return false; + }; + let ctx = context.addr(); + let Some(ctx_end) = ctx.checked_add(std::mem::size_of::()) else { + return false; + }; + !out.is_null() && start % std::mem::align_of::() == 0 && (end <= ctx || ctx_end <= start) +} +pub(super) fn disjoint(a: *mut A, b: *mut B) -> bool { + let Some(a_end) = a.addr().checked_add(std::mem::size_of::()) else { + return false; + }; + let Some(b_end) = b.addr().checked_add(std::mem::size_of::()) else { + return false; + }; + a_end <= b.addr() || b_end <= a.addr() +} +fn status( + context: *const JettRuntimeContextV1, + op: impl FnOnce( + &mut NativeResourceState, + &mut values::NativeValues, + &mut ResourceRegistry, + ) -> ResourceResult<()>, +) -> JettRuntimeStatusV1 { + match catch_unwind(AssertUnwindSafe(|| { + AuthenticatedResourceContext::acquire(context)?.with_resource(op) + })) { + Ok(Ok(())) => JettRuntimeStatusV1::OK, + Ok(Err(error)) => error.status(), + Err(panic) => { + discard_panic_payload(panic); + JettRuntimeStatusV1::PANIC + } + } +} +unsafe fn output( + context: *const JettRuntimeContextV1, + out: *mut T, + op: impl FnOnce( + &mut NativeResourceState, + &mut values::NativeValues, + &mut ResourceRegistry, + ) -> ResourceResult, +) -> JettRuntimeStatusV1 { + if !output_ok(context, out) { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + } + status(context, |state, ordinary, registry| { + let value = op(state, ordinary, registry)?; + // SAFETY: the C caller guarantees live exclusive storage; shape/overlap was checked above. + unsafe { + ptr::write(out, value); + } + Ok(()) + }) +} +fn id(raw: u64) -> ResourceResult { + ResourceHandleId::new(raw) +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_scope_validate( + context: *const JettRuntimeContextV1, + scope: u64, + function: u32, + signature: u32, + template: u32, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| { + s.scope_validate(v, id(scope)?, function, signature, template) + }) +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_operation_begin( + context: *const JettRuntimeContextV1, + parent: u64, + template: u32, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.begin_operation_frame(v, template, id(parent)?) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_operation_complete( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| { + s.operation_frame(operation, id(frame)?)?; + if !matches!(s.operation(operation)?, NativeOperation::Complete { .. }) { + return Err(NativeResourceError::WrongOperation); + } + s.end_operation_frame(v, r, id(frame)?) + }) +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_hook_prepare( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + descriptor: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.prepare_hook( + v, + operation, + id(frame)?, + if descriptor == 0 { + None + } else { + Some(id(descriptor)?) + }, + ) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_factory_prepare( + context: *const JettRuntimeContextV1, + call: u64, + network: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.prepare_factory(v, r, id(call)?, network) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_factory_commit( + context: *const JettRuntimeContextV1, + prepared: u64, + network: u64, + label: i64, + out_value: *mut JettResourceCallResultV1, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.factory(v, r, id(prepared)?, network, label) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_borrow_begin( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + owner: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.borrow_begin(v, r, operation, id(frame)?, id(owner)?) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_borrow_commit( + context: *const JettRuntimeContextV1, + call: u64, + network: u64, + loan: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.invoke_borrow(v, r, id(call)?, network, id(loan)?) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_borrow_end( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + loan: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| { + s.borrow_end(operation, id(frame)?, id(loan)?) + }) +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_close( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + owner: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| { + s.close_or_drop(r, operation, id(frame)?, id(owner)?) + }) +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_descriptor_close( + context: *const JettRuntimeContextV1, + call: u64, + owner: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| { + let call_id = id(call)?; + let call = s.checked_call(call_id)?; + let frame = call.frame; + let operation = call.target_operation; + if call.descriptor.is_none() { + return Err(NativeResourceError::WrongOperation); + } + s.close_or_drop(r, operation, frame, id(owner)?)?; + s.handles.remove(&call_id); + Ok(()) + }) +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_descriptor( + context: *const JettRuntimeContextV1, + operation: u32, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.descriptor(v, operation).map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_transfer( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + value: u64, + destination_frame: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + if s.destination_frame(id(frame)?, operation)? != id(destination_frame)? { + return Err(NativeResourceError::WrongFrame); + } + s.transfer( + v, + r, + operation, + id(frame)?, + id(value)?, + id(destination_frame)?, + ) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_destination_frame( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.running(v)?; + s.destination_frame(id(frame)?, operation) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_sum_adopt( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + owner: u64, + destination_frame: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + if s.destination_frame(id(frame)?, operation)? != id(destination_frame)? { + return Err(NativeResourceError::WrongFrame); + } + s.sum_adopt( + v, + r, + operation, + id(frame)?, + id(owner)?, + id(destination_frame)?, + ) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_sum_tag( + context: *const JettRuntimeContextV1, + frame: u64, + value: u64, + out_value: *mut u32, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.running(v)?; + s.frame(id(frame)?)?; + if s.active_frames.last() != Some(&id(frame)?) { + return Err(NativeResourceError::WrongFrame); + } + s.sum_tag(id(value)?) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_sum_take( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + value: u64, + destination_frame: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + if s.destination_frame(id(frame)?, operation)? != id(destination_frame)? { + return Err(NativeResourceError::WrongFrame); + } + s.sum_take( + v, + r, + operation, + id(frame)?, + id(value)?, + id(destination_frame)?, + ) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_sum_drop( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + value: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| { + s.sum_drop(v, r, operation, id(frame)?, id(value)?) + }) +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_failure_companion_take( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + sum: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.failure_companion_take(v, id(frame)?, operation, id(sum)?) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_absent_sum( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.absent_sum(v, id(frame)?, operation) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_failure_sum( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + companion: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.failure_sum(v, id(frame)?, operation, companion) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_replace( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + old: u64, + replacement: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.replace(v, r, operation, id(frame)?, id(old)?, id(replacement)?) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_source_prepare( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.source_prepare(v, id(frame)?, operation) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_source_actual( + context: *const JettRuntimeContextV1, + call: u64, + source_index: u32, + parameter: u32, + value: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| { + s.source_actual(v, r, id(call)?, source_index, parameter, value) + }) +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_source_enter( + context: *const JettRuntimeContextV1, + call: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.source_enter(v, r, id(call)?).map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_source_parameter( + context: *const JettRuntimeContextV1, + scope: u64, + parameter: u32, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.source_parameter(v, id(scope)?, parameter) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_source_status( + context: *const JettRuntimeContextV1, + call: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| s.source_status(v, id(call)?)) +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_scope_complete( + context: *const JettRuntimeContextV1, + scope: u64, + operation: u32, + body_status: u32, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| { + s.scope_complete(v, r, id(scope)?, operation, body_status) + }) +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_return_publish( + context: *const JettRuntimeContextV1, + scope: u64, + operation: u32, + provisional: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.return_publish(v, r, id(scope)?, operation, id(provisional)?) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Output records are aligned, live, exclusive and mutually disjoint. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_entry_scope( + context: *const JettRuntimeContextV1, + out_scope: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + unsafe { + entry_lookup(context, out_scope, out_result, |s| { + s.validate_installation()?; + s.runtime_purpose()?; + let attempt = s + .attempt + .as_ref() + .ok_or(NativeResourceError::InvalidEntry)?; + if attempt.phase != AttemptPhase::Running || s.entry != attempt.entry { + return Err(NativeResourceError::InvalidEntry); + } + let root = s.frame(attempt.root_frame)?; + if root.parent.is_some() || root.template != s.entry.scope { + return Err(NativeResourceError::WrongFrame); + } + Ok(attempt.root_frame.raw()) + }) + } +} +/// # Safety +/// Output records are aligned, live, exclusive and mutually disjoint. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_entry_network( + context: *const JettRuntimeContextV1, + parameter: u32, + out_capability: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + unsafe { + entry_lookup(context, out_capability, out_result, |s| { + let attempt = s + .attempt + .as_ref() + .ok_or(NativeResourceError::InvalidEntry)? + .id; + s.entry_network(attempt, parameter) + }) + } +} +unsafe fn entry_lookup( + context: *const JettRuntimeContextV1, + out: *mut u64, + out_result: *mut JettRuntimeResultV1, + lookup: impl FnOnce(&NativeResourceState) -> ResourceResult, +) -> JettRuntimeStatusV1 { + if !output_ok(context, out) || !output_ok(context, out_result) || !disjoint(out, out_result) { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + } + complete_call(out_result, || { + let result = AuthenticatedResourceContext::acquire(context).and_then(|context| { + context.with_resource(|s, v, _| { + s.running(v)?; + lookup(s) + }) + }); + match result { + Ok(value) => { + unsafe { ptr::write(out, value) }; + JettRuntimeResultV1::ok() + } + Err(error) => JettRuntimeResultV1::failure(error.status(), REFUSED), + } + }) +} + +/// # Safety +/// Context is stationary and output storage is aligned, live and exclusive. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_entry_outcome( + context: *const JettRuntimeContextV1, + root_scope: u64, + function: u32, + signature: u32, + template: u32, + out_body: *mut u32, +) -> JettRuntimeStatusV1 { + if !output_ok(context, out_body) { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + } + // Refusal is observation only: with_resource would latch it as a body fault. + let outcome = catch_unwind(AssertUnwindSafe(|| { + AuthenticatedResourceContext::acquire(context)?.with_state(|_, state| { + state + .resource_state + .as_ref() + .ok_or(NativeResourceError::MissingInstallation)? + .entry_outcome(id(root_scope)?, function, signature, template) + }) + })); + match outcome { + Ok(Ok(body)) => { + // SAFETY: the caller supplies live exclusive storage; preflight + // refused known overlap/range/alignment faults before any state read. + unsafe { ptr::write(out_body, body) }; + JettRuntimeStatusV1::OK + } + Ok(Err(error)) => error.status(), + Err(panic) => { + discard_panic_payload(panic); + JettRuntimeStatusV1::PANIC + } + } +} + +/// # Safety +/// Context and output satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_sum_borrow( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + sum: u64, + out_loan: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_loan, |s, v, r| { + s.sum_borrow(v, r, operation, id(frame)?, id(sum)?) + .map(ResourceHandleId::raw) + }) + } +} +/// # Safety +/// Context and output satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_sum_view_tag( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + loan: u64, + out_tag: *mut u32, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_tag, |s, v, r| { + s.sum_view_tag(v, r, operation, id(frame)?, id(loan)?) + }) + } +} +/// # Safety +/// Context and output satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_sum_view_project( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + loan: u64, + out_child: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_child, |s, v, r| { + s.sum_view_project(v, r, operation, id(frame)?, id(loan)?) + .map(ResourceHandleId::raw) + }) + } +} +/// # Safety +/// Context and output satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_sum_failure_read( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + loan: u64, + out_companion: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_companion, |s, v, r| { + s.sum_failure_read(v, r, operation, id(frame)?, id(loan)?) + }) + } +} +/// # Safety +/// Context satisfies the stationary-context contract. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_sum_borrow_end( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + loan: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, _v, r| { + s.sum_borrow_end(r, operation, id(frame)?, id(loan)?) + }) +} diff --git a/crates/jett_runtime/src/native_abi/resource/leaves/carriers.rs b/crates/jett_runtime/src/native_abi/resource/leaves/carriers.rs new file mode 100644 index 00000000..ca6f9031 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/leaves/carriers.rs @@ -0,0 +1,206 @@ +//! ABI v1 opaque handles; each operation is joined to an installed v3 carrier row. +use super::*; + +/// # Safety +/// Context and output obey the stationary-context and exclusive-output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_construct_begin( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + out: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out, |s, v, _| { + s.carrier_construct_begin(v, id(frame)?, operation) + .map(ResourceHandleId::raw) + }) + } +} +/// # Safety +/// Context obeys the stationary-context contract; bits conveys the row's owned child. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_child( + context: *const JettRuntimeContextV1, + builder: u64, + ordinal: u32, + bits: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| { + s.carrier_child_with_registry(v, r, id(builder)?, ordinal, bits) + }) +} +/// # Safety +/// Context and output obey the stationary-context and exclusive-output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_commit( + context: *const JettRuntimeContextV1, + builder: u64, + out: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out, |s, v, _| { + s.carrier_commit(v, id(builder)?).map(ResourceHandleId::raw) + }) + } +} +/// # Safety +/// Context and output obey the stationary-context and exclusive-output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_transfer( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + input: u64, + out: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out, |s, v, r| { + s.carrier_transfer_with_registry(v, r, id(frame)?, operation, id(input)?) + .map(ResourceHandleId::raw) + }) + } +} +/// # Safety +/// Context and output obey the stationary-context and exclusive-output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_qualify( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + input: u64, + out: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out, |s, v, _| { + s.carrier_qualify(v, id(frame)?, operation, id(input)?) + .map(ResourceHandleId::raw) + }) + } +} +/// # Safety +/// Context and output obey the stationary-context and exclusive-output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_borrow( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + input: u64, + selection: u64, + out: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out, |s, v, _| { + s.carrier_borrow(v, id(frame)?, operation, id(input)?, selection) + .map(ResourceHandleId::raw) + }) + } +} +/// # Safety +/// Context and output obey the stationary-context and exclusive-output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_observe( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + input: u64, + selection: u64, + out: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out, |s, v, _| { + s.carrier_observe(v, id(frame)?, operation, id(input)?, selection) + }) + } +} +/// # Safety +/// Context and output obey the stationary-context and exclusive-output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_extract( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + input: u64, + selection: u64, + out: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out, |s, v, _| { + s.carrier_extract(v, id(frame)?, operation, id(input)?, selection) + }) + } +} +/// # Safety +/// Context and output obey the stationary-context and exclusive-output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_adapt( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + input: u64, + selection: u64, + out: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out, |s, v, _| { + s.carrier_adapt(v, id(frame)?, operation, id(input)?, selection) + .map(ResourceHandleId::raw) + }) + } +} +/// # Safety +/// Context obeys the stationary-context contract. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_end( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + input: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, _, _| { + s.carrier_end(id(frame)?, operation, id(input)?) + }) +} +/// # Safety +/// Context obeys the stationary-context contract. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_retire( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + input: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| { + s.carrier_retire_with_registry(v, r, id(frame)?, operation, id(input)?) + }) +} +/// # Safety +/// Context obeys the stationary-context contract. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_reset_iteration( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + input: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, v, _| { + s.carrier_reset_iteration(v, id(frame)?, operation, id(input)?) + }) +} +/// # Safety +/// Context and output obey the stationary-context and exclusive-output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_publish( + context: *const JettRuntimeContextV1, + scope: u64, + operation: u32, + input: u64, + out: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out, |s, v, r| { + s.carrier_publish(v, r, id(scope)?, operation, id(input)?) + .map(ResourceHandleId::raw) + }) + } +} diff --git a/crates/jett_runtime/src/native_abi/resource/operations.rs b/crates/jett_runtime/src/native_abi/resource/operations.rs new file mode 100644 index 00000000..c99dc490 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/operations.rs @@ -0,0 +1,1043 @@ +use super::*; + +impl NativeResourceState { + pub(super) fn root_frame(&self, attempt: ResourceHandleId) -> ResourceResult { + let running = self + .attempt + .as_ref() + .ok_or(NativeResourceError::InvalidEntry)?; + if running.id != attempt || running.phase != AttemptPhase::Running { + return Err(NativeResourceError::InvalidEntry); + } + Ok(running.root_frame) + } + pub(super) fn begin_operation_frame( + &mut self, + ordinary: &values::NativeValues, + template: u32, + parent: ResourceHandleId, + ) -> ResourceResult { + let attempt = self.running(ordinary)?.id; + if self.active_frames.last() != Some(&parent) { + return Err(NativeResourceError::WrongFrame); + } + let parent_template = self.frame(parent)?.template; + let frame = self + .layout + .frames() + .get(template as usize) + .ok_or(NativeResourceError::WrongFrame)?; + if frame.role() != NativeFrameRole::Operation { + // A Source invocation/Return constructor must supply its separate checked handoff. + return Err(NativeResourceError::UnsupportedSourceBoundary); + } + if !frame + .parents() + .contains(&NativeParent::Frame(parent_template)) + { + return Err(NativeResourceError::WrongFrame); + } + let mut ids = self.reserve(1)?; + self.active_frames + .try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + let handle = next_handle(&mut ids)?; + let purpose = self + .attempt + .as_ref() + .ok_or(NativeResourceError::InvalidEntry)? + .purpose; + let token = self + .custody + .begin_frame(ResourceFrameKind::Operation, purpose)?; + self.handles.insert( + handle, + NativeResourceEntry::Frame(NativeFrameEntry { + template, + attempt, + parent: Some(parent), + token, + incoming: Vec::new(), + }), + ); + self.active_frames.push(handle); + Ok(handle) + } + pub(super) fn end_operation_frame( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + frame: ResourceHandleId, + ) -> ResourceResult<()> { + if self.active_frames.last() != Some(&frame) { + return Err(NativeResourceError::WrongFrame); + } + if self.layout.frames()[self.frame(frame)?.template as usize].role() + != NativeFrameRole::Operation + { + return Err(NativeResourceError::WrongFrame); + } + self.retire_frame(ordinary, registry, frame) + } + pub(super) fn retire_frame( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + frame: ResourceHandleId, + ) -> ResourceResult<()> { + if self.active_frames.last() != Some(&frame) { + return Err(NativeResourceError::WrongFrame); + } + let Some(NativeResourceEntry::Frame(_)) = self.handles.get(&frame) else { + return Err(NativeResourceError::WrongFrame); + }; + self.frame_shells_unborrowed(frame)?; + let view_retirement = self.retire_sum_views(frame); + let carrier_retirement = if self.layout.wire_version() == 3 { + view_retirement?; + // End metadata leases and reserve companion cleanup while generations live. + Some(self.prepare_carrier_frame(ordinary, frame)?) + } else { + None + }; + let Some(NativeResourceEntry::Frame(header)) = self.handles.get(&frame) else { + return Err(NativeResourceError::WrongFrame); + }; + let cleanup = self.custody.end_frame(&header.token, registry); + if self.custody.active_frames() == self.active_frames.len() { + // A refused cleanup has not retired this frame. Keep its tokens available. + return self.cleanup(cleanup); + } + let core_result = self.cleanup(cleanup); + let carrier_result = if let Some(prepared) = carrier_retirement { + self.retire_carrier_frame_prepared(ordinary, prepared) + } else { + Ok(()) + }; + let mut ordinary_failed = false; + self.handles.retain(|_, entry| match entry { + NativeResourceEntry::Frame(_) => true, + NativeResourceEntry::Owner(owner) => owner.frame != frame, + NativeResourceEntry::Loan(loan) => loan.frame != frame, + NativeResourceEntry::SumLoan(loan) => loan.frame != frame, + NativeResourceEntry::Prepared(prepared) => prepared.frame != frame, + NativeResourceEntry::Call(call) => call.frame != frame, + NativeResourceEntry::SourceCall(call) => call.frame != frame, + NativeResourceEntry::Sum(sum) if sum.frame == frame => { + if let NativeSumPayload::Fail { shape, string } = &sum.payload { + ordinary_failed |= ordinary + .drop_resource_typed_companion(&self.layout, *shape, *string) + .is_err(); + } + false + } + _ => true, + }); + self.handles.remove(&frame); + self.active_frames.pop(); + self.carrier_adapters + .retain(|shell, _| self.handles.contains_key(shell)); + if ordinary_failed { + ordinary.cleanup_failed = true; + } + core_result?; + carrier_result?; + view_retirement + } + pub(super) fn runtime_purpose(&self) -> ResourceResult<()> { + if self.attempt.as_ref().map(|attempt| attempt.purpose) != Some(ResourcePurpose::Runtime) { + return Err(NativeResourceError::WrongPurpose); + } + Ok(()) + } + pub(super) fn owner( + &self, + handle: ResourceHandleId, + slot: u32, + ) -> ResourceResult<&NativeOwnerEntry> { + match self.handles.get(&handle) { + Some(NativeResourceEntry::Owner(owner)) if owner.slot == slot => { + self.frame(owner.frame)?; + let current = *self + .active_frames + .last() + .ok_or(NativeResourceError::WrongFrame)?; + self.carrier_frame_at(current, handle, slot)?; + Ok(owner) + } + Some(NativeResourceEntry::Owner(_)) => Err(NativeResourceError::WrongOperation), + Some(_) => Err(NativeResourceError::WrongFamily), + None => Err(NativeResourceError::InvalidHandle), + } + } + pub(super) fn slot_frame(&self, slot: u32, frame: ResourceHandleId) -> ResourceResult<()> { + let layout_slot = self + .layout + .slots() + .get(slot as usize) + .ok_or(NativeResourceError::WrongOperation)?; + if layout_slot.frame() != self.frame(frame)?.template { + return Err(NativeResourceError::WrongFrame); + } + Ok(()) + } + fn grant( + &self, + ordinary: &values::NativeValues, + network_bits: u64, + ) -> ResourceResult<(ResourceHandleId, AuthorityProvenance)> { + let handle = self.network.ok_or(NativeResourceError::WrongGrant)?; + let Some(NativeResourceEntry::NetworkGrant(grant)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongGrant); + }; + if grant.context != self.context + || !Arc::ptr_eq(&grant.layout, &self.layout) + || !self.provider.matches(grant.provider, handle) + || grant.ordinary_network != network_bits + || !ordinary.validates_resource_network(network_bits) + || grant.provenance + != AuthorityProvenance::new(grant.provider.raw(), grant.restriction.raw()) + { + return Err(NativeResourceError::WrongGrant); + } + Ok((handle, grant.provenance)) + } + pub(super) fn descriptor( + &mut self, + ordinary: &values::NativeValues, + operation: u32, + ) -> ResourceResult { + self.running(ordinary)?; + let hook = match self.operation(operation)? { + NativeOperation::Descriptor { hook } => *hook, + _ => return Err(NativeResourceError::WrongOperation), + }; + let signature = self + .layout + .hooks() + .get(hook as usize) + .ok_or(NativeResourceError::WrongOperation)? + .signature(); + let mut ids = self.reserve(1)?; + let handle = next_handle(&mut ids)?; + self.handles + .insert(handle, NativeResourceEntry::Descriptor { hook, signature }); + Ok(handle) + } + pub(super) fn prepare_hook( + &mut self, + ordinary: &values::NativeValues, + operation: u32, + frame: ResourceHandleId, + descriptor: Option, + ) -> ResourceResult { + self.running(ordinary)?; + self.runtime_purpose()?; + let (target, hook) = match (self.operation(operation)?, descriptor) { + ( + NativeOperation::Acquire { hook, .. } + | NativeOperation::InvokeBorrow { hook, .. } + | NativeOperation::Close { hook, .. }, + None, + ) => (operation, *hook), + ( + NativeOperation::InvokeDescriptor { + hook, + signature, + target, + }, + Some(handle), + ) => { + match self.handles.get(&handle) { + Some(NativeResourceEntry::Descriptor { + hook: actual_hook, + signature: actual_signature, + }) if actual_hook == hook && actual_signature == signature => {} + _ => return Err(NativeResourceError::WrongFamily), + } + (*target, *hook) + } + _ => return Err(NativeResourceError::WrongOperation), + }; + self.operation_frame(target, frame)?; + let mut ids = self.reserve(1)?; + let handle = next_handle(&mut ids)?; + self.handles.insert( + handle, + NativeResourceEntry::Call(NativePreparedCall { + operation, + target_operation: target, + hook, + frame, + descriptor, + }), + ); + Ok(handle) + } + pub(super) fn checked_call( + &self, + handle: ResourceHandleId, + ) -> ResourceResult<&NativePreparedCall> { + let Some(NativeResourceEntry::Call(call)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + self.operation_frame(call.target_operation, call.frame)?; + match (self.operation(call.operation)?, call.descriptor) { + ( + NativeOperation::InvokeDescriptor { + hook, + signature, + target, + }, + Some(descriptor), + ) if *target == call.target_operation && *hook == call.hook => { + match self.handles.get(&descriptor) { + Some(NativeResourceEntry::Descriptor { + hook: actual, + signature: actual_signature, + }) if actual == hook && actual_signature == signature => {} + _ => return Err(NativeResourceError::WrongFamily), + } + } + ( + NativeOperation::Acquire { hook, .. } + | NativeOperation::InvokeBorrow { hook, .. } + | NativeOperation::Close { hook, .. }, + None, + ) if *hook == call.hook && call.operation == call.target_operation => {} + _ => return Err(NativeResourceError::WrongOperation), + } + Ok(call) + } + pub(super) fn prepare_factory( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + call_handle: ResourceHandleId, + network_bits: u64, + ) -> ResourceResult { + self.running(ordinary)?; + self.runtime_purpose()?; + let call = self.checked_call(call_handle)?; + let frame = call.frame; + let hook = call.hook; + let destination = match self.operation(call.target_operation)? { + NativeOperation::Acquire { destination, .. } => *destination, + _ => return Err(NativeResourceError::WrongOperation), + }; + let hook = self + .layout + .hooks() + .get(hook as usize) + .ok_or(NativeResourceError::WrongOperation)?; + if hook.recipe() != NativeRecipe::NetworkFactory { + return Err(NativeResourceError::WrongOperation); + } + let kind = hook.kind(); + let destination_frame = + self.destination_frame(frame, self.checked_call(call_handle)?.target_operation)?; + self.slot_empty(destination_frame, destination)?; + let slot = &self.layout.slots()[destination as usize]; + if slot.shape() != self.layout.signatures()[hook.signature() as usize].result() + || slot.path() != [NativePayloadStep::Ok] + { + return Err(NativeResourceError::WrongOperation); + } + let network = self.grant(ordinary, network_bits)?.0; + self.layout.preflight_physical_acquisition(registry)?; + // Both Resource result branches and ordinary error storage are reserved before the provider. + let publication_ids = self.reserve(1)?; + let mut prepared_ids = self.reserve(1)?; + let handle = next_handle(&mut prepared_ids)?; + ordinary + .prepare_resource_ordinary_output() + .map_err(ordinary_error)?; // capacity only; unused identity range is burned + let frame_token = match self.handles.get(&destination_frame) { + Some(NativeResourceEntry::Frame(frame)) => &frame.token, + _ => return Err(NativeResourceError::WrongFrame), + }; + let holder = self + .custody + .holder(frame_token, &self.layout.slot(destination)?)?; + let token = self + .custody + .prepare_acquisition(&self.layout.kind(kind)?, &holder)?; + self.handles.insert( + handle, + NativeResourceEntry::Prepared(NativePreparedEntry { + call: call_handle, + frame, + destination, + destination_frame, + kind, + network, + token, + publication_ids, + }), + ); + Ok(handle) + } + pub(super) fn factory( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + prepared_handle: ResourceHandleId, + network_bits: u64, + label: i64, + ) -> ResourceResult { + self.running(ordinary)?; + self.runtime_purpose()?; + let grant = self.grant(ordinary, network_bits)?; + let Some(NativeResourceEntry::Prepared(prepared)) = self.handles.get(&prepared_handle) + else { + return Err(NativeResourceError::WrongFamily); + }; + let call = self.checked_call(prepared.call)?; + let actual_destination = match self.operation(call.target_operation)? { + NativeOperation::Acquire { destination, .. } => *destination, + _ => return Err(NativeResourceError::WrongOperation), + }; + if prepared.network != grant.0 + || prepared.frame != call.frame + || prepared.destination != actual_destination + || self.layout.hooks()[call.hook as usize].kind() != prepared.kind + { + return Err(NativeResourceError::WrongOperation); + } + self.layout.preflight_physical_acquisition(registry)?; + let mut ordinary_ids = ordinary + .prepare_resource_ordinary_output() + .map_err(ordinary_error)?; + // Rejoin occupancy immediately before the effect; this is not token reconstruction. + self.slot_frame(prepared.destination, prepared.destination_frame)?; + let destination_frame = self.destination_frame(call.frame, call.target_operation)?; + if destination_frame != prepared.destination_frame { + return Err(NativeResourceError::WrongFrame); + } + if self.handles.values().any(|entry| matches!(entry, NativeResourceEntry::Owner(owner) + if owner.frame == destination_frame && owner.slot == prepared.destination) + || matches!(entry, NativeResourceEntry::Sum(sum) if sum.frame == destination_frame && sum.slot == prepared.destination)) { + return Err(NativeResourceError::WrongOperation); + } + let frame = match self.handles.get(&prepared.destination_frame) { + Some(NativeResourceEntry::Frame(frame)) => &frame.token, + _ => return Err(NativeResourceError::WrongFrame), + }; + let holder = self + .custody + .holder(frame, &self.layout.slot(prepared.destination)?)?; + self.custody + .prepare_acquisition(&self.layout.kind(prepared.kind)?, &holder)?; + let result = catch_unwind(AssertUnwindSafe(|| self.provider.construct(label))); + let result = match result { + Ok(Ok(result)) => result, + Ok(Err(error)) => { + self.record_failure(error); + return Err(error); + } + Err(panic) => { + discard_panic_payload(panic); + if let Some(attempt) = &mut self.attempt { + attempt.body.get_or_insert(NativeBodyFailure::HostPanic); + } + return Err(NativeResourceError::BodyFailed); + } + }; + let Some(NativeResourceEntry::Prepared(mut prepared)) = + self.handles.remove(&prepared_handle) + else { + return Err(NativeResourceError::WrongFamily); + }; + self.handles.remove(&prepared.call); + let handle = next_handle(&mut prepared.publication_ids)?; + let shape = self.layout.slots()[prepared.destination as usize].shape(); + let (domain, payload) = match result { + Ok(constructed) => { + let provider::ConstructedPayload { payload, finalizer } = constructed; + let token = match self.custody.commit_acquisition( + registry, + prepared.token, + grant.1, + payload, + finalizer, + ) { + Ok(token) => token, + Err(ResourceTransitionFailure::Rejected(error)) => { + let error = NativeResourceError::Custody(error); + self.record_failure(error); + return Err(error); + } + Err(ResourceTransitionFailure::Cleanup(error)) => { + let error = NativeResourceError::Cleanup(error); + self.record_failure(error); + return Err(error); + } + }; + (RESOURCE_DOMAIN_OK, NativeSumPayload::Ok(token)) + } + Err(error) => { + let fail_shape = match self.layout.shapes().get(shape as usize) { + Some(NativeShape::Result { fail, .. }) => *fail, + _ => return Err(NativeResourceError::WrongOperation), + }; + let string = ordinary + .publish_resource_error(error, &mut ordinary_ids) + .map_err(ordinary_error)?; + ( + RESOURCE_DOMAIN_ERROR, + NativeSumPayload::Fail { + shape: fail_shape, + string, + }, + ) + } + }; + self.handles.insert( + handle, + NativeResourceEntry::Sum(NativeResourceSum { + frame: prepared.destination_frame, + slot: prepared.destination, + shape, + payload, + }), + ); + Ok(JettResourceCallResultV1 { + domain, + reserved: 0, + value: handle.raw(), + }) + } + pub(super) fn sum_tag(&self, handle: ResourceHandleId) -> ResourceResult { + if self.carrier_adapters.contains_key(&handle) { + return Err(NativeResourceError::WrongOperation); + } + match self.handles.get(&handle) { + Some(NativeResourceEntry::Sum(sum)) => { + self.frame(sum.frame)?; + let current = *self + .active_frames + .last() + .ok_or(NativeResourceError::WrongFrame)?; + self.carrier_frame_at(current, handle, sum.slot)?; + Ok(match &sum.payload { + NativeSumPayload::None | NativeSumPayload::Fail { .. } => 0, + _ => 1, + }) + } + Some(_) => Err(NativeResourceError::WrongFamily), + None => Err(NativeResourceError::InvalidHandle), + } + } + pub(super) fn sum_adopt( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + owner_handle: ResourceHandleId, + destination_frame: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.operation_frame(operation, frame)?; + let (source, destination) = match self.operation(operation)? { + NativeOperation::SumAdopt { + source, + destination, + .. + } => (*source, *destination), + _ => return Err(NativeResourceError::WrongOperation), + }; + self.owner(owner_handle, source)?; + self.slot_empty(destination_frame, destination)?; + let slot = &self.layout.slots()[destination as usize]; + let shape = slot.shape(); + let step = match slot.path() { + [NativePayloadStep::Some] => NativePayloadStep::Some, + [NativePayloadStep::Ok] => NativePayloadStep::Ok, + _ => return Err(NativeResourceError::WrongOperation), + }; + let mut ids = self.reserve(1)?; + let handle = next_handle(&mut ids)?; + let holder = self.custody.holder( + &self.frame(destination_frame)?.token, + &self.layout.slot(destination)?, + )?; + let Some(NativeResourceEntry::Owner(mut owner)) = self.handles.remove(&owner_handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if let Err(error) = self.custody.transfer(&mut owner.token, &holder, registry) { + self.handles + .insert(owner_handle, NativeResourceEntry::Owner(owner)); + return Err(error.into()); + } + let payload = if step == NativePayloadStep::Some { + NativeSumPayload::Some(owner.token) + } else { + NativeSumPayload::Ok(owner.token) + }; + self.handles.insert( + handle, + NativeResourceEntry::Sum(NativeResourceSum { + frame: destination_frame, + slot: destination, + shape, + payload, + }), + ); + Ok(handle) + } + pub(super) fn replace( + &mut self, + ordinary: &values::NativeValues, + registry: &mut ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + old_handle: ResourceHandleId, + replacement_handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.runtime_purpose()?; + self.operation_frame(operation, frame)?; + let (old_slot, replacement_slot) = match self.operation(operation)? { + NativeOperation::Replace { + old, replacement, .. + } => (*old, *replacement), + _ => return Err(NativeResourceError::WrongOperation), + }; + if old_handle == replacement_handle { + return Err(NativeResourceError::WrongOperation); + } + let old_frame = self.owner(old_handle, old_slot)?.frame; + self.owner(replacement_handle, replacement_slot)?; + let holder = self + .custody + .holder(&self.frame(old_frame)?.token, &self.layout.slot(old_slot)?)?; + let (Some(NativeResourceEntry::Owner(old)), Some(NativeResourceEntry::Owner(replacement))) = ( + self.handles.get(&old_handle), + self.handles.get(&replacement_handle), + ) else { + return Err(NativeResourceError::WrongFamily); + }; + self.custody + .preflight_replacement(&old.token, &replacement.token, &holder, registry)?; + let mut ids = self.reserve(1)?; + let handle = next_handle(&mut ids)?; + let Some(NativeResourceEntry::Owner(mut old)) = self.handles.remove(&old_handle) else { + return Err(NativeResourceError::WrongFamily); + }; + let cleanup = self.custody.close(&mut old.token, registry); + if self.custody.validate_owned(&old.token, registry).is_ok() { + self.handles + .insert(old_handle, NativeResourceEntry::Owner(old)); + // The RHS still has its one independent owner, available for exact scope cleanup. + return self + .cleanup(cleanup) + .and(Err(NativeResourceError::WrongOperation)); + } + let Some(NativeResourceEntry::Owner(mut replacement)) = + self.handles.remove(&replacement_handle) + else { + return Err(NativeResourceError::WrongFamily); + }; + if cleanup.failure().is_some() { + let replacement_cleanup = self.custody.close(&mut replacement.token, registry); + if self + .custody + .validate_owned(&replacement.token, registry) + .is_ok() + { + self.handles + .insert(replacement_handle, NativeResourceEntry::Owner(replacement)); + } + self.cleanup(cleanup)?; + return self + .cleanup(replacement_cleanup) + .and(Err(NativeResourceError::BodyFailed)); + } + if let Err(error) = self + .custody + .transfer(&mut replacement.token, &holder, registry) + { + self.handles + .insert(replacement_handle, NativeResourceEntry::Owner(replacement)); + return Err(error.into()); + } + replacement.frame = old_frame; + replacement.slot = old_slot; + self.handles + .insert(handle, NativeResourceEntry::Owner(replacement)); + Ok(handle) + } + pub(super) fn sum_take( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + sum_handle: ResourceHandleId, + destination_frame: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + if self.carrier_adapters.contains_key(&sum_handle) { + return Err(NativeResourceError::WrongOperation); + } + self.operation_frame(operation, frame)?; + let (source, destination) = match self.operation(operation)? { + NativeOperation::SumTake { + source, + destination, + .. + } => (*source, *destination), + _ => return Err(NativeResourceError::WrongOperation), + }; + self.slot_empty(destination_frame, destination)?; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&sum_handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if sum.slot != source { + return Err(NativeResourceError::WrongOperation); + } + self.carrier_frame_at(frame, sum_handle, source)?; + self.sum_unborrowed(sum_handle)?; + let token = match &sum.payload { + NativeSumPayload::Some(token) | NativeSumPayload::Ok(token) => token, + _ => return Err(NativeResourceError::WrongOperation), + }; + self.custody.validate_owned(token, registry)?; + let mut ids = self.reserve(1)?; + let new_handle = next_handle(&mut ids)?; + let holder = self.custody.holder( + &self.frame(destination_frame)?.token, + &self.layout.slot(destination)?, + )?; + let Some(NativeResourceEntry::Sum(mut sum)) = self.handles.remove(&sum_handle) else { + return Err(NativeResourceError::WrongFamily); + }; + let token = match &mut sum.payload { + NativeSumPayload::Some(token) | NativeSumPayload::Ok(token) => token, + _ => { + self.handles + .insert(sum_handle, NativeResourceEntry::Sum(sum)); + return Err(NativeResourceError::WrongOperation); + } + }; + if let Err(error) = self.custody.transfer(token, &holder, registry) { + self.handles + .insert(sum_handle, NativeResourceEntry::Sum(sum)); + return Err(error.into()); + } + let token = match sum.payload { + NativeSumPayload::Some(token) | NativeSumPayload::Ok(token) => token, + _ => return Err(NativeResourceError::WrongOperation), + }; + self.handles.insert( + new_handle, + NativeResourceEntry::Owner(NativeOwnerEntry { + frame: destination_frame, + slot: destination, + token, + }), + ); + Ok(new_handle) + } + pub(super) fn transfer( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + owner_handle: ResourceHandleId, + destination_frame: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.operation_frame(operation, frame)?; + let (source, destination) = match self.operation(operation)? { + NativeOperation::Transfer { + source, + destination, + .. + } => (*source, *destination), + _ => return Err(NativeResourceError::WrongOperation), + }; + self.carrier_frame_at(frame, owner_handle, source)?; + self.move_carrier( + registry, + owner_handle, + source, + destination, + destination_frame, + ) + } + + pub(super) fn borrow_begin( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + owner_handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.runtime_purpose()?; + self.operation_frame(operation, frame)?; + let (source, lease_frame) = match self.operation(operation)? { + NativeOperation::Borrow { + source, + lease_frame, + .. + } => (*source, *lease_frame), + _ => return Err(NativeResourceError::WrongOperation), + }; + if self.frame(frame)?.template != lease_frame { + return Err(NativeResourceError::WrongFrame); + } + self.owner(owner_handle, source)?; + let mut ids = self.reserve(1)?; + let handle = next_handle(&mut ids)?; + let Some(NativeResourceEntry::Owner(owner)) = self.handles.get(&owner_handle) else { + return Err(NativeResourceError::WrongFamily); + }; + let source_frame = owner.frame; + let Some(NativeResourceEntry::Frame(frame_header)) = self.handles.get(&frame) else { + return Err(NativeResourceError::WrongFrame); + }; + let token = self + .custody + .begin_borrow(&owner.token, &frame_header.token, registry)?; + self.handles.insert( + handle, + NativeResourceEntry::Loan(NativeLoanEntry { + frame, + borrow_operation: operation, + source_slot: source, + source_frame, + token, + parent_sum: None, + }), + ); + Ok(handle) + } + pub(super) fn exact_loan( + &self, + handle: ResourceHandleId, + source: NativeLoanSource, + ) -> ResourceResult<&NativeLoanEntry> { + let Some(NativeResourceEntry::Loan(loan)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + match source { + NativeLoanSource::ExistingBorrow { operation } + if operation == loan.borrow_operation + && loan.parent_sum.is_none() + && matches!(self.operation(operation)?, NativeOperation::Borrow { .. }) => + { + let current = *self + .active_frames + .last() + .ok_or(NativeResourceError::WrongFrame)?; + if self.activation_frame(current, self.frame(loan.frame)?.template)? != loan.frame { + return Err(NativeResourceError::WrongFrame); + } + } + NativeLoanSource::ProjectedSumPayload { operation } + if operation == loan.borrow_operation && loan.parent_sum.is_some() => + { + let NativeOperation::ProjectSumView { source, .. } = self.operation(operation)? + else { + return Err(NativeResourceError::WrongOperation); + }; + let current = *self + .active_frames + .last() + .ok_or(NativeResourceError::WrongFrame)?; + if self.activation_frame(current, self.frame(loan.frame)?.template)? != loan.frame { + return Err(NativeResourceError::WrongFrame); + } + let parent = loan.parent_sum.ok_or(NativeResourceError::WrongFamily)?; + // The issuer's own incoming/formal association remains required. + self.projected_sum_parent(loan.frame, parent, *source)?; + } + NativeLoanSource::IncomingViewFormal { scope, parameter } => { + self.validate_resident(handle, scope, parameter)?; + } + _ => return Err(NativeResourceError::WrongOperation), + } + self.frame(loan.frame)?; + self.frame(loan.source_frame)?; + Ok(loan) + } + pub(super) fn invoke_borrow( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + call_handle: ResourceHandleId, + network_bits: u64, + loan_handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.runtime_purpose()?; + let call = self.checked_call(call_handle)?; + let (hook, source) = match self.operation(call.target_operation)? { + NativeOperation::InvokeBorrow { hook, source, .. } => (*hook, *source), + _ => return Err(NativeResourceError::WrongOperation), + }; + let hook = self + .layout + .hooks() + .get(hook as usize) + .ok_or(NativeResourceError::WrongOperation)?; + if hook.recipe() != NativeRecipe::NetworkBorrow { + return Err(NativeResourceError::WrongOperation); + } + let kind = hook.kind(); + let loan = self.exact_loan(loan_handle, source)?; + if self.layout.slot_kind(loan.source_slot)? != kind { + return Err(NativeResourceError::WrongOperation); + } + let key = self.custody.validate_borrowed(&loan.token, registry)?; + let provenance = self.grant(ordinary, network_bits)?.1; + let kind_id = self.layout.kind_id(kind)?; + let mut output_ids = ordinary + .prepare_resource_ordinary_output() + .map_err(ordinary_error)?; + let result = catch_unwind(AssertUnwindSafe(|| { + registry.access::(key, kind_id, &provenance, |payload| { + self.provider.borrow(payload) + }) + })); + self.handles.remove(&call_handle); + let result = match result { + Ok(Ok(Ok(result))) => result, + Ok(Ok(Err(error))) => { + self.record_failure(error); + return Err(error); + } + Ok(Err(error)) => { + let error = NativeResourceError::Registry(error); + self.record_failure(error); + return Err(error); + } + Err(panic) => { + discard_panic_payload(panic); + if let Some(attempt) = &mut self.attempt { + attempt.body.get_or_insert(NativeBodyFailure::HostPanic); + } + return Err(NativeResourceError::BodyFailed); + } + }; + ordinary + .publish_resource_borrow_result(result, &mut output_ids) + .map_err(ordinary_error) + } + pub(super) fn borrow_end( + &mut self, + operation: u32, + frame: ResourceHandleId, + handle: ResourceHandleId, + ) -> ResourceResult<()> { + self.operation_frame(operation, frame)?; + let borrow = match self.operation(operation)? { + NativeOperation::EndBorrow { borrow, .. } => *borrow, + _ => return Err(NativeResourceError::WrongOperation), + }; + let source = match self.operation(borrow)? { + NativeOperation::Borrow { .. } => { + NativeLoanSource::ExistingBorrow { operation: borrow } + } + NativeOperation::ProjectSumView { .. } => { + NativeLoanSource::ProjectedSumPayload { operation: borrow } + } + _ => return Err(NativeResourceError::WrongOperation), + }; + self.exact_loan(handle, source)?; + let Some(NativeResourceEntry::Loan(mut loan)) = self.handles.remove(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if let Err(error) = self.custody.end_borrow(&mut loan.token) { + self.handles.insert(handle, NativeResourceEntry::Loan(loan)); + return Err(error.into()); + } + Ok(()) + } + pub(super) fn close_or_drop( + &mut self, + registry: &mut ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + owner_handle: ResourceHandleId, + ) -> ResourceResult<()> { + self.runtime_purpose()?; + self.operation_frame(operation, frame)?; + let source = match self.operation(operation)? { + NativeOperation::Close { hook, source, .. } + if self + .layout + .hooks() + .get(*hook as usize) + .is_some_and(|hook| hook.recipe() == NativeRecipe::Finalize) => + { + *source + } + NativeOperation::Drop { source, .. } => *source, + _ => return Err(NativeResourceError::WrongOperation), + }; + self.owner(owner_handle, source)?; + let Some(NativeResourceEntry::Owner(mut owner)) = self.handles.remove(&owner_handle) else { + return Err(NativeResourceError::WrongFamily); + }; + let cleanup = self.custody.close(&mut owner.token, registry); + if self.custody.validate_owned(&owner.token, registry).is_ok() { + self.handles + .insert(owner_handle, NativeResourceEntry::Owner(owner)); + } + self.cleanup(cleanup) + } + pub(super) fn sum_drop( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + handle: ResourceHandleId, + ) -> ResourceResult<()> { + if self.carrier_adapters.contains_key(&handle) { + return Err(NativeResourceError::WrongOperation); + } + self.operation_frame(operation, frame)?; + let source = match self.operation(operation)? { + NativeOperation::SumDrop { source, .. } => *source, + _ => return Err(NativeResourceError::WrongOperation), + }; + match self.handles.get(&handle) { + Some(NativeResourceEntry::Sum(sum)) if sum.slot == source => {} + _ => return Err(NativeResourceError::WrongOperation), + } + self.carrier_frame_at(frame, handle, source)?; + self.sum_unborrowed(handle)?; + let Some(NativeResourceEntry::Sum(mut sum)) = self.handles.remove(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + let cleanup = match &mut sum.payload { + NativeSumPayload::Ok(token) | NativeSumPayload::Some(token) => { + self.custody.close(token, registry) + } + NativeSumPayload::Fail { shape, string } => { + if let Err(error) = + ordinary.drop_resource_typed_companion(&self.layout, *shape, *string) + { + self.handles.insert(handle, NativeResourceEntry::Sum(sum)); + ordinary.cleanup_failed = true; + return Err(ordinary_error(error)); + } + ResourceCleanupOutcome::default() + } + NativeSumPayload::None => ResourceCleanupOutcome::default(), + }; + if let NativeSumPayload::Ok(token) | NativeSumPayload::Some(token) = &sum.payload { + if self.custody.validate_owned(token, registry).is_ok() { + self.handles.insert(handle, NativeResourceEntry::Sum(sum)); + } + } + self.cleanup(cleanup) + } +} diff --git a/crates/jett_runtime/src/native_abi/resource/ordinary_attempt_owner_tests.rs b/crates/jett_runtime/src/native_abi/resource/ordinary_attempt_owner_tests.rs new file mode 100644 index 00000000..eccb493e --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/ordinary_attempt_owner_tests.rs @@ -0,0 +1,187 @@ +//! Private forged-proof negatives; these mint no public Source or ABI authority. +use super::*; + +#[test] +fn ordinary_attempt_owner_checks_every_retained_identity_component() { + context(|auth| { + install(auth, &[], 1); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + auth.with_state(|_, state| { + let owner = state + .resource_state + .as_ref() + .unwrap() + .attempt + .as_ref() + .unwrap() + .ordinary_owner; + state + .values + .validate_resource_ordinary_attempt(owner) + .unwrap(); + let mut wrong_context = owner; + wrong_context.context.token = wrong_context.context.token.wrapping_add(1); + let mut wrong_program = owner; + wrong_program.program = ResourceHandleId::new(owner.program.raw() + 1).unwrap(); + let mut wrong_entry = owner; + wrong_entry.entry.function += 1; + let mut wrong_signature = owner; + wrong_signature.entry.signature += 1; + let mut wrong_scope = owner; + wrong_scope.entry.scope += 1; + let mut wrong_root = owner; + wrong_root.root = ResourceHandleId::new(owner.root.raw() + 1).unwrap(); + let mut wrong_attempt = owner; + wrong_attempt.attempt = ResourceHandleId::new(owner.attempt.raw() + 1).unwrap(); + let mut wrong_purpose = owner; + wrong_purpose.purpose = ResourcePurpose::Property; + let mut wrong_ordinal = owner; + wrong_ordinal.ordinal += 1; + for forged in [ + wrong_context, + wrong_program, + wrong_entry, + wrong_signature, + wrong_scope, + wrong_root, + wrong_attempt, + wrong_purpose, + wrong_ordinal, + ] { + assert!( + state + .values + .validate_resource_ordinary_attempt(forged) + .is_err() + ); + state + .values + .validate_resource_ordinary_attempt(owner) + .unwrap(); + } + Ok(()) + }) + .unwrap(); + assert_eq!(complete(auth, attempt, 0).selected_kind, 0); + assert_empty(auth); + }); +} + +#[test] +fn malformed_ordinary_completion_cannot_seal_or_replace_the_running_channel() { + context(|auth| { + install(auth, &[], 1); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + auth.with_state(|_, state| { + let owner = state + .resource_state + .as_ref() + .unwrap() + .attempt + .as_ref() + .unwrap() + .ordinary_owner; + let base = ResourceOrdinaryCompletion { + owner, + completion: NativeResourceCompletion { + attempt: owner.attempt.raw(), + body_status: 0, + cleanup_status: 0, + selected_kind: 0, + reserved: 0, + }, + diagnostic_ready: true, + }; + let mut wrong_attempt = base; + wrong_attempt.completion.attempt += 1; + let mut wrong_reserved = base; + wrong_reserved.completion.reserved = 1; + let mut wrong_channel = base; + wrong_channel.completion.selected_kind = 4; + let mut wrong_owner = base; + wrong_owner.owner.ordinal += 1; + for forged in [wrong_attempt, wrong_reserved, wrong_channel, wrong_owner] { + assert!(state.values.seal_resource_ordinary_attempt(forged).is_err()); + state + .values + .validate_resource_ordinary_attempt(owner) + .unwrap(); + } + assert!(state.resource_state.as_ref().unwrap().completed.is_empty()); + Ok(()) + }) + .unwrap(); + complete(auth, attempt, 0); + assert_empty(auth); + }); +} + +#[test] +fn sealed_ordinary_predecessor_requires_the_exact_saved_completion_and_ordinal() { + context(|auth| { + install(auth, &[], 2); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + let first = complete(auth, attempt, 0); + auth.with_state(|_, state| { + let resource = state.resource_state.as_ref().unwrap(); + let previous = resource.completed.last().unwrap().ordinary; + let owner = previous.owner; + assert!( + state + .values + .validate_resource_ordinary_attempt(owner) + .is_err() + ); + assert!( + state + .values + .seal_resource_ordinary_attempt(previous) + .is_err() + ); + let mut next = owner; + next.attempt = ResourceHandleId::new(owner.attempt.raw() + 10).unwrap(); + next.root = ResourceHandleId::new(owner.root.raw() + 10).unwrap(); + next.ordinal += 1; + let mut wrong_saved = previous; + wrong_saved.completion.body_status = 1; + assert!( + state + .values + .prepare_resource_ordinary_attempt(next, Some(wrong_saved)) + .is_err() + ); + let mut wrong_ordinal = next; + wrong_ordinal.ordinal += 1; + assert!( + state + .values + .prepare_resource_ordinary_attempt(wrong_ordinal, Some(previous)) + .is_err() + ); + let mut foreign = next; + foreign.context.token = foreign.context.token.wrapping_add(1); + assert!( + state + .values + .prepare_resource_ordinary_attempt(foreign, Some(previous)) + .is_err() + ); + let mut wrong_entry = next; + wrong_entry.entry.signature += 1; + assert!( + state + .values + .prepare_resource_ordinary_attempt(wrong_entry, Some(previous)) + .is_err() + ); + assert_eq!(resource.completed_attempt(attempt)?, first); + assert!(resource.completed_messages(attempt)?.1.is_empty()); + Ok(()) + }) + .unwrap(); + let (second, _, _, _) = start(auth, ResourcePurpose::Runtime); + assert!(second.raw() > attempt.raw()); + complete(auth, second, 0); + assert_empty(auth); + }); +} diff --git a/crates/jett_runtime/src/native_abi/resource/ordinary_error_reentry_tests.rs b/crates/jett_runtime/src/native_abi/resource/ordinary_error_reentry_tests.rs new file mode 100644 index 00000000..dfe5cf60 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/ordinary_error_reentry_tests.rs @@ -0,0 +1,694 @@ +//! Host lifetime controls. These are not Source admission or linked acceptance. +//! Registered as a child of `resource/tests.rs` to reuse its genuine fixture. +use super::*; +use std::ptr; + +const BOUNDS: &[u8] = b"list.__remove_at: index -1 out of bounds"; + +fn pointer(auth: &AuthenticatedResourceContext) -> *const JettRuntimeContextV1 { + auth.key.owner_address as *const JettRuntimeContextV1 +} + +fn current_message(auth: &AuthenticatedResourceContext) -> Vec { + let mut length = 0; + let mut result = JettRuntimeResultV1::ok(); + assert_eq!( + unsafe { + values::jett_rt_v1_value_failure_copy( + pointer(auth), + ptr::null_mut(), + 0, + &mut length, + &mut result, + ) + }, + JettRuntimeStatusV1::OK + ); + let mut bytes = vec![0; usize::try_from(length).unwrap()]; + assert_eq!( + unsafe { + values::jett_rt_v1_value_failure_copy( + pointer(auth), + bytes.as_mut_ptr(), + length, + &mut length, + &mut result, + ) + }, + JettRuntimeStatusV1::OK + ); + bytes +} + +fn saved( + auth: &AuthenticatedResourceContext, + attempt: ResourceHandleId, +) -> (NativeResourceCompletion, Vec, Vec) { + auth.with_resource(|state, _, _| { + let completion = state.completed_attempt(attempt)?; + let (resource, ordinary) = state.completed_messages(attempt)?; + Ok((completion, resource.to_vec(), ordinary.to_vec())) + }) + .unwrap() +} + +fn set_prefix(auth: &AuthenticatedResourceContext, prefix: &[u8]) { + unsafe { + let text = values::jett_rt_v1_string_literal( + pointer(auth), + prefix.as_ptr(), + u64::try_from(prefix.len()).unwrap(), + ); + assert_ne!(text, 0); + assert_eq!(values::jett_rt_v1_property_case_set(pointer(auth), text), 0); + assert_eq!(values::jett_rt_v1_string_release(pointer(auth), text), 0); + } +} + +fn bounds_failure(auth: &AuthenticatedResourceContext, index: i64) { + unsafe { + let list = values::jett_rt_v1_list_new(pointer(auth), 0); + assert_ne!(list, 0); + assert_eq!(values::jett_rt_v1_list_append(pointer(auth), list, 1), list); + assert_eq!( + values::jett_rt_v1_list_remove_at(pointer(auth), list, index), + 0 + ); + assert_eq!( + values::jett_rt_v1_value_status(pointer(auth)), + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + assert_eq!(values::jett_rt_v1_value_drop(pointer(auth), list), 0); + } +} + +fn dynamic_failure(auth: &AuthenticatedResourceContext, message: &[u8]) { + unsafe { + let text = values::jett_rt_v1_string_literal( + pointer(auth), + message.as_ptr(), + u64::try_from(message.len()).unwrap(), + ); + assert_ne!(text, 0); + assert_ne!( + values::jett_rt_v1_assert_fail_message(pointer(auth), text), + 0 + ); + assert_eq!(values::jett_rt_v1_string_release(pointer(auth), text), 0); + } +} + +fn successful_leaf(auth: &AuthenticatedResourceContext) { + unsafe { + let list = values::jett_rt_v1_list_new(pointer(auth), 0); + assert_ne!(list, 0); + assert_eq!( + values::jett_rt_v1_list_append(pointer(auth), list, 17), + list + ); + assert_eq!(values::jett_rt_v1_list_length(pointer(auth), list), 1); + assert_eq!(values::jett_rt_v1_value_drop(pointer(auth), list), 0); + assert_eq!(values::jett_rt_v1_value_status(pointer(auth)), 0); + } +} + +fn begin_only(auth: &AuthenticatedResourceContext) -> ResourceResult { + // Deliberately use with_state: a refused host preflight is not a Source body fault. + auth.with_state(|_, state| { + let resource = state.resource_state.as_mut().unwrap(); + resource.begin_entry( + &mut state.values, + &state.resources, + entry(), + ResourcePurpose::Runtime, + ) + }) +} + +fn assert_no_effects(auth: &AuthenticatedResourceContext) { + assert_empty(auth); + assert!(events(auth).is_empty()); + auth.with_resource(|state, _, _| { + assert_eq!(state.script_remaining(), 0); + Ok(()) + }) + .unwrap(); +} + +fn poisoned_context(operation: impl FnOnce(&AuthenticatedResourceContext)) { + let mut record = Box::new(JettRuntimeContextV1::retired()); + let mut result = JettRuntimeResultV1::ok(); + assert_eq!( + unsafe { + jett_rt_v1_context_create(JETT_RUNTIME_ABI_VERSION_V1, &mut *record, &mut result) + }, + JettRuntimeStatusV1::OK + ); + let authenticated = AuthenticatedResourceContext::acquire(&*record).unwrap(); + operation(&authenticated); + drop(authenticated); + assert_eq!( + unsafe { jett_rt_v1_context_destroy(&mut *record, &mut result) }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + assert_eq!(result.status, JettRuntimeStatusV1::INVALID_ARGUMENT); + // No failure-taking operation or cleanup-poison reset is used for teardown. +} + +#[test] +fn ordinary_error_reentry_signed_bounds_twice_keeps_both_records_and_grant() { + context(|auth| { + install(auth, &[], 2); + let (first, _, _, grant) = start(auth, ResourcePurpose::Runtime); + bounds_failure(auth, -1); + assert_eq!(current_message(auth), BOUNDS); + let first_completion = complete(auth, first, 1); + assert_eq!( + ( + first_completion.body_status, + first_completion.cleanup_status, + first_completion.selected_kind + ), + (1, 0, 1) + ); + let first_saved = saved(auth, first); + assert_eq!(first_saved.2, BOUNDS); + assert_eq!(current_message(auth), BOUNDS); + assert_no_effects(auth); + + let (second, _, _, second_grant) = start(auth, ResourcePurpose::Runtime); + assert!(second.raw() > first.raw()); + assert_eq!(second_grant, grant); + assert!(current_message(auth).is_empty()); + assert_eq!(unsafe { values::jett_rt_v1_value_status(pointer(auth)) }, 0); + bounds_failure(auth, -1); + let second_completion = complete(auth, second, 1); + assert_eq!( + ( + second_completion.body_status, + second_completion.cleanup_status, + second_completion.selected_kind + ), + (1, 0, 1) + ); + assert_eq!(saved(auth, second).2, BOUNDS); + assert_eq!(saved(auth, first), first_saved); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_distinct_signed_errors_capture_distinct_prefixes() { + context(|auth| { + install(auth, &[], 2); + let (first, _, _, grant) = start(auth, ResourcePurpose::Runtime); + set_prefix(auth, b"property 'first' trial 2: "); + bounds_failure(auth, -1); + complete(auth, first, 17); + let first_saved = saved(auth, first); + assert_eq!(first_saved.0.body_status, 17); + assert_eq!( + first_saved.2, + b"property 'first' trial 2: list.__remove_at: index -1 out of bounds" + ); + + let (second, _, _, second_grant) = start(auth, ResourcePurpose::Runtime); + assert_eq!(grant, second_grant); + set_prefix(auth, b"property 'second' trial 9: "); + bounds_failure(auth, 7); + complete(auth, second, 23); + assert_eq!( + saved(auth, second).2, + b"property 'second' trial 9: list.__remove_at: index 7 out of bounds" + ); + assert_eq!(saved(auth, first), first_saved); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_failure_then_success_then_different_failure_runs_real_leaves() { + context(|auth| { + install(auth, &[], 3); + let (first, _, _, grant) = start(auth, ResourcePurpose::Runtime); + bounds_failure(auth, -1); + complete(auth, first, 1); + let first_saved = saved(auth, first); + let (second, _, _, second_grant) = start(auth, ResourcePurpose::Runtime); + assert_eq!(grant, second_grant); + successful_leaf(auth); + let success = complete(auth, second, 0); + assert_eq!( + ( + success.body_status, + success.cleanup_status, + success.selected_kind + ), + (0, 0, 0) + ); + assert!(current_message(auth).is_empty()); + let success_saved = saved(auth, second); + assert!(success_saved.1.is_empty() && success_saved.2.is_empty()); + + let (third, _, _, third_grant) = start(auth, ResourcePurpose::Runtime); + assert!(first.raw() < second.raw() && second.raw() < third.raw()); + assert_eq!(grant, third_grant); + dynamic_failure(auth, b"third terminal message"); + complete(auth, third, 29); + assert_eq!(saved(auth, third).2, b"third terminal message"); + assert_eq!(saved(auth, first), first_saved); + assert_eq!(saved(auth, second), success_saved); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_success_then_error_does_not_reuse_success_channel() { + context(|auth| { + install(auth, &[], 2); + let (first, _, _, _) = start(auth, ResourcePurpose::Runtime); + successful_leaf(auth); + complete(auth, first, 0); + let first_saved = saved(auth, first); + let (second, _, _, _) = start(auth, ResourcePurpose::Runtime); + bounds_failure(auth, -1); + complete(auth, second, 1); + assert_eq!(saved(auth, second).2, BOUNDS); + assert_eq!(saved(auth, first), first_saved); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_first_failure_blocks_later_normal_leaf_without_replacing_message() { + context(|auth| { + install(auth, &[], 1); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + let replacement = b"must never replace the first diagnostic"; + let text = unsafe { + values::jett_rt_v1_string_literal( + pointer(auth), + replacement.as_ptr(), + u64::try_from(replacement.len()).unwrap(), + ) + }; + assert_ne!(text, 0); + bounds_failure(auth, -1); + assert_ne!( + unsafe { values::jett_rt_v1_assert_fail_message(pointer(auth), text) }, + 0 + ); + assert_eq!(unsafe { values::jett_rt_v1_list_new(pointer(auth), 0) }, 0); + assert_eq!(current_message(auth), BOUNDS); + assert_eq!( + unsafe { values::jett_rt_v1_string_release(pointer(auth), text) }, + 0 + ); + complete(auth, attempt, 31); + assert_eq!(saved(auth, attempt).2, BOUNDS); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_pre_entry_session_failure_is_not_issued_an_attempt() { + context(|auth| { + install(auth, &[], 2); + dynamic_failure(auth, b"session before first entry"); + let message = current_message(auth); + assert!(begin_only(auth).is_err()); + assert!(begin_only(auth).is_err()); + assert_eq!(current_message(auth), message); + auth.with_resource(|state, _, _| { + assert!(state.attempt.is_none() && state.completed.is_empty()); + Ok(()) + }) + .unwrap(); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_between_entry_error_is_session_owned_and_old_receipt_survives() { + context(|auth| { + install(auth, &[], 2); + let (first, _, _, _) = start(auth, ResourcePurpose::Runtime); + successful_leaf(auth); + complete(auth, first, 0); + let first_saved = saved(auth, first); + dynamic_failure(auth, b"session between entries"); + assert_eq!(current_message(auth), b"session between entries"); + assert!(begin_only(auth).is_err()); + assert_eq!(saved(auth, first), first_saved); + assert_eq!(current_message(auth), b"session between entries"); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_refused_begin_preserves_sealed_error_and_can_retry_after_owner_drop() { + context(|auth| { + install(auth, &[], 2); + let (first, _, _, _) = start(auth, ResourcePurpose::Runtime); + bounds_failure(auth, -1); + complete(auth, first, 1); + let first_saved = saved(auth, first); + let text = + unsafe { values::jett_rt_v1_string_literal(pointer(auth), b"resident".as_ptr(), 8) }; + assert_ne!(text, 0); + assert!(begin_only(auth).is_err()); + assert_eq!(saved(auth, first), first_saved); + assert_eq!(current_message(auth), BOUNDS); + assert_eq!( + unsafe { values::jett_rt_v1_string_release(pointer(auth), text) }, + 0 + ); + let second = begin_only(auth).unwrap(); + successful_leaf(auth); + assert_eq!(complete(auth, second, 0).selected_kind, 0); + assert_eq!(saved(auth, first), first_saved); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_wrong_entry_active_and_duplicate_completion_do_not_advance_history() { + context(|auth| { + install(auth, &[], 2); + let (first, _, _, _) = start(auth, ResourcePurpose::Runtime); + bounds_failure(auth, -1); + auth.with_state(|_, state| { + let resource = state.resource_state.as_mut().unwrap(); + assert!( + resource + .begin_entry( + &mut state.values, + &state.resources, + entry(), + ResourcePurpose::Runtime + ) + .is_err() + ); + let wrong = ResourceHandleId::new(first.raw().checked_add(1).unwrap())?; + assert!( + resource + .complete_entry(&mut state.values, &mut state.resources, wrong, 0, false) + .is_err() + ); + assert!(resource.completed.is_empty()); + Ok(()) + }) + .unwrap(); + assert_eq!(current_message(auth), BOUNDS); + complete(auth, first, 1); + let first_saved = saved(auth, first); + auth.with_state(|_, state| { + let resource = state.resource_state.as_mut().unwrap(); + let mut wrong_entry = entry(); + wrong_entry.signature = wrong_entry.signature.checked_add(1).unwrap(); + assert!( + resource + .begin_entry( + &mut state.values, + &state.resources, + wrong_entry, + ResourcePurpose::Runtime + ) + .is_err() + ); + assert!( + resource + .complete_entry(&mut state.values, &mut state.resources, first, 0, false) + .is_err() + ); + assert_eq!(resource.completed.len(), 1); + Ok(()) + }) + .unwrap(); + assert_eq!(saved(auth, first), first_saved); + let second = begin_only(auth).unwrap(); + successful_leaf(auth); + complete(auth, second, 0); + assert_eq!(saved(auth, first), first_saved); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_foreign_attempt_cannot_complete_or_query_this_context() { + context(|first_auth| { + context(|second_auth| { + install(first_auth, &[], 1); + install(second_auth, &[], 1); + let (first, _, _, _) = start(first_auth, ResourcePurpose::Runtime); + let (second, _, _, _) = start(second_auth, ResourcePurpose::Runtime); + assert_ne!(first, second); + bounds_failure(first_auth, -1); + dynamic_failure(second_auth, b"second context error"); + for (auth, foreign) in [(first_auth, second), (second_auth, first)] { + auth.with_state(|_, state| { + let resource = state.resource_state.as_mut().unwrap(); + assert!( + resource + .complete_entry( + &mut state.values, + &mut state.resources, + foreign, + 0, + false + ) + .is_err() + ); + assert!(resource.completed_attempt(foreign).is_err()); + assert!(resource.completed_messages(foreign).is_err()); + Ok(()) + }) + .unwrap(); + } + complete(first_auth, first, 1); + complete(second_auth, second, 1); + assert_eq!(saved(first_auth, first).2, BOUNDS); + assert_eq!(saved(second_auth, second).2, b"second context error"); + assert_no_effects(first_auth); + assert_no_effects(second_auth); + }); + }); +} + +#[test] +fn ordinary_error_reentry_exhausted_budget_preserves_the_last_completed_error() { + context(|auth| { + install(auth, &[], 1); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + bounds_failure(auth, -1); + complete(auth, attempt, 1); + let before = saved(auth, attempt); + assert!(begin_only(auth).is_err()); + assert!(begin_only(auth).is_err()); + assert_eq!(saved(auth, attempt), before); + assert_eq!(current_message(auth), BOUNDS); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_maximum_retained_history_is_bounded_and_exact() { + context(|auth| { + install(auth, &[], u32::try_from(MAX_ATTEMPTS).unwrap()); + let mut history = Vec::new(); + for _ in 0..MAX_ATTEMPTS { + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + assert_eq!(unsafe { values::jett_rt_v1_value_status(pointer(auth)) }, 0); + bounds_failure(auth, -1); + complete(auth, attempt, 1); + let observation = saved(auth, attempt); + assert_eq!(observation.2, BOUNDS); + history.push((attempt, observation)); + } + assert_eq!(history.len(), MAX_ATTEMPTS); + assert!(begin_only(auth).is_err()); + assert!(begin_only(auth).is_err()); + for (attempt, observation) in history { + assert_eq!(saved(auth, attempt), observation); + } + assert_eq!(current_message(auth), BOUNDS); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_diagnostic_overflow_retains_tombstone_without_cleanup_retry() { + context(|auth| { + install(auth, &[], 2); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + let message = vec![b'x'; 64 * 1024 + 1]; + dynamic_failure(auth, &message); + assert_eq!(current_message(auth), message); + auth.with_state(|_, state| { + let resource = state.resource_state.as_mut().unwrap(); + assert_eq!( + resource.complete_entry(&mut state.values, &mut state.resources, attempt, 1, false), + Err(NativeResourceError::Capacity) + ); + assert_eq!(resource.completed.len(), 1); + assert_eq!( + resource.completed_attempt(attempt), + Err(NativeResourceError::Capacity) + ); + assert_eq!( + resource.completed_messages(attempt), + Err(NativeResourceError::Capacity) + ); + assert!( + resource + .complete_entry(&mut state.values, &mut state.resources, attempt, 1, false) + .is_err() + ); + Ok(()) + }) + .unwrap(); + assert!(begin_only(auth).is_err()); + assert_eq!(current_message(auth), message); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_real_cleanup_poison_preserves_first_error_and_blocks_constructor() { + poisoned_context(|auth| { + install(auth, &[], 2); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + let list = unsafe { values::jett_rt_v1_list_new(pointer(auth), 0) }; + assert_ne!(list, 0); + assert_eq!( + unsafe { values::jett_rt_v1_list_append(pointer(auth), list, 1) }, + list + ); + assert_eq!( + unsafe { values::jett_rt_v1_list_remove_at(pointer(auth), list, -1) }, + 0 + ); + assert_eq!( + unsafe { values::jett_rt_v1_value_drop(pointer(auth), list) }, + 0 + ); + assert_ne!( + unsafe { values::jett_rt_v1_value_drop(pointer(auth), list) }, + 0 + ); + assert_eq!(current_message(auth), BOUNDS); + let completion = complete(auth, attempt, 1); + assert_eq!(completion.selected_kind, 3); + assert_ne!(completion.cleanup_status, 0); + let before = saved(auth, attempt); + assert_eq!(before.2, BOUNDS); + assert!(begin_only(auth).is_err()); + assert_eq!(saved(auth, attempt), before); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_sealed_take_refuses_and_poison_cannot_erase_history() { + poisoned_context(|auth| { + install(auth, &[], 2); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + bounds_failure(auth, -1); + complete(auth, attempt, 1); + let before = saved(auth, attempt); + assert_eq!( + unsafe { values::jett_rt_v1_failure_take_prefixed_text(pointer(auth), ptr::null(), 0) }, + 0 + ); + assert_eq!(saved(auth, attempt), before); + // The invalid between-entry take belongs to the sticky session channel. + // Its poison may take observation precedence; the sealed body stays exact. + let session_message = current_message(auth); + assert!(!session_message.is_empty()); + assert_eq!(before.2, BOUNDS); + assert!(begin_only(auth).is_err()); + assert_eq!(saved(auth, attempt), before); + assert_eq!(current_message(auth), session_message); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_active_refinement_capture_remains_data_before_completion() { + context(|auth| { + install(auth, &[], 2); + let (first, _, _, _) = start(auth, ResourcePurpose::Runtime); + bounds_failure(auth, -1); + let captured = unsafe { + values::jett_rt_v1_failure_take_prefixed_text(pointer(auth), b"handled: ".as_ptr(), 9) + }; + assert_ne!(captured, 0); + assert_eq!(unsafe { values::jett_rt_v1_value_status(pointer(auth)) }, 0); + let expected = b"handled: list.__remove_at: index -1 out of bounds"; + let expected_text = unsafe { + values::jett_rt_v1_string_literal( + pointer(auth), + expected.as_ptr(), + u64::try_from(expected.len()).unwrap(), + ) + }; + assert_ne!(expected_text, 0); + assert_eq!( + unsafe { values::jett_rt_v1_string_equal(pointer(auth), captured, expected_text) }, + 1 + ); + assert_eq!( + unsafe { values::jett_rt_v1_string_release(pointer(auth), captured) }, + 0 + ); + assert_eq!( + unsafe { values::jett_rt_v1_string_release(pointer(auth), expected_text) }, + 0 + ); + successful_leaf(auth); + assert_eq!(complete(auth, first, 0).selected_kind, 0); + let first_saved = saved(auth, first); + assert!(first_saved.2.is_empty()); + let (second, _, _, _) = start(auth, ResourcePurpose::Runtime); + bounds_failure(auth, -1); + complete(auth, second, 1); + assert_eq!(saved(auth, second).2, BOUNDS); + assert_eq!(saved(auth, first), first_saved); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_historical_resource_cleanup_panic_still_allows_clean_next_entry() { + context(|auth| { + install(auth, &[(3, 41, 0, ""), (1, 42, 0, "")], 2); + let (first, _, frame, grant) = start(auth, ResourcePurpose::Runtime); + construct(auth, frame, grant, 41); + let first_completion = complete(auth, first, 0); + assert_eq!( + ( + first_completion.body_status, + first_completion.cleanup_status, + first_completion.selected_kind + ), + (0, JettRuntimeStatusV1::PANIC.code(), 3) + ); + let first_saved = saved(auth, first); + assert_eq!(first_saved.1, CLEANUP_FAILED); + assert!(first_saved.2.is_empty()); + assert_empty(auth); + + let (second, _, frame, second_grant) = start(auth, ResourcePurpose::Runtime); + assert_eq!(grant, second_grant); + construct(auth, frame, second_grant, 42); + assert_eq!(complete(auth, second, 0).selected_kind, 0); + assert_eq!(saved(auth, first), first_saved); + assert_eq!(events(auth), vec![(41, 1), (41, 5), (42, 1), (42, 5)]); + assert_empty(auth); + auth.with_resource(|state, _, _| { + assert_eq!(state.script_remaining(), 0); + Ok(()) + }) + .unwrap(); + }); +} diff --git a/crates/jett_runtime/src/native_abi/resource/ordinary_reservation_failure_tests.rs b/crates/jett_runtime/src/native_abi/resource/ordinary_reservation_failure_tests.rs new file mode 100644 index 00000000..20bf34df --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/ordinary_reservation_failure_tests.rs @@ -0,0 +1,816 @@ +//! Deterministic host allocation-refusal controls, not linked Source acceptance. +use super::*; +use crate::native_abi::resource::ordinary_reservation_faults::ReservationSite; +use std::ptr; + +const BOUNDS: &[u8] = b"list.__remove_at: index -1 out of bounds"; +type History = Vec<(NativeResourceCompletion, Vec, Vec)>; + +fn pointer(auth: &AuthenticatedResourceContext) -> *const JettRuntimeContextV1 { + auth.key.owner_address as *const JettRuntimeContextV1 +} + +fn message(auth: &AuthenticatedResourceContext) -> Vec { + let mut length = 0; + let mut result = JettRuntimeResultV1::ok(); + assert_eq!( + unsafe { + values::jett_rt_v1_value_failure_copy( + pointer(auth), + ptr::null_mut(), + 0, + &mut length, + &mut result, + ) + }, + JettRuntimeStatusV1::OK + ); + let mut bytes = vec![0; usize::try_from(length).unwrap()]; + assert_eq!( + unsafe { + values::jett_rt_v1_value_failure_copy( + pointer(auth), + bytes.as_mut_ptr(), + length, + &mut length, + &mut result, + ) + }, + JettRuntimeStatusV1::OK + ); + bytes +} + +fn prefix(auth: &AuthenticatedResourceContext, bytes: &[u8]) { + unsafe { + let text = values::jett_rt_v1_string_literal( + pointer(auth), + bytes.as_ptr(), + u64::try_from(bytes.len()).unwrap(), + ); + assert_ne!(text, 0); + assert_eq!(values::jett_rt_v1_property_case_set(pointer(auth), text), 0); + assert_eq!(values::jett_rt_v1_string_release(pointer(auth), text), 0); + } +} + +fn bounds(auth: &AuthenticatedResourceContext) { + unsafe { + let list = values::jett_rt_v1_list_new(pointer(auth), 0); + assert_ne!(list, 0); + assert_eq!(values::jett_rt_v1_list_append(pointer(auth), list, 1), list); + assert_eq!( + values::jett_rt_v1_list_remove_at(pointer(auth), list, -1), + 0 + ); + assert_eq!( + values::jett_rt_v1_value_status(pointer(auth)), + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + assert_eq!(values::jett_rt_v1_value_drop(pointer(auth), list), 0); + } +} + +fn dynamic_failure(auth: &AuthenticatedResourceContext, bytes: &[u8]) { + unsafe { + let text = values::jett_rt_v1_string_literal( + pointer(auth), + bytes.as_ptr(), + u64::try_from(bytes.len()).unwrap(), + ); + assert_ne!(text, 0); + assert_ne!( + values::jett_rt_v1_assert_fail_message(pointer(auth), text), + 0 + ); + assert_eq!(values::jett_rt_v1_string_release(pointer(auth), text), 0); + } +} + +fn begin_raw(auth: &AuthenticatedResourceContext) -> ResourceResult { + // Constructor preflight refusal is not an entered Source body failure. + auth.with_state(|_, state| { + state.resource_state.as_mut().unwrap().begin_entry( + &mut state.values, + &state.resources, + entry(), + ResourcePurpose::Runtime, + ) + }) +} + +fn complete_raw( + auth: &AuthenticatedResourceContext, + attempt: ResourceHandleId, + body: u32, +) -> ResourceResult { + auth.with_state(|_, state| { + state.resource_state.as_mut().unwrap().complete_entry( + &mut state.values, + &mut state.resources, + attempt, + body, + false, + ) + }) +} + +fn history(auth: &AuthenticatedResourceContext) -> History { + auth.with_resource(|state, _, _| { + Ok(state + .completed + .iter() + .map(|row| { + ( + row.completion, + row.resource_message.clone(), + row.ordinary_message.clone(), + ) + }) + .collect()) + }) + .unwrap() +} + +fn channel_state( + auth: &AuthenticatedResourceContext, +) -> ( + usize, + Option, + Option, +) { + auth.with_state(|_, state| Ok(state.values.test_resource_ordinary_channel_state())) + .unwrap() +} + +fn arm(auth: &AuthenticatedResourceContext, site: ReservationSite) { + auth.with_state(|_, state| { + match site { + ReservationSite::OrdinaryChannel => { + state.values.test_arm_resource_ordinary_reservation() + } + ReservationSite::BeginActiveFrames + | ReservationSite::BeginHistory + | ReservationSite::ResourceDiagnostic + | ReservationSite::OrdinaryDiagnostic => state + .resource_state + .as_mut() + .unwrap() + .reservation_faults + .arm(site), + } + Ok(()) + }) + .unwrap(); +} + +fn fault_snapshot( + auth: &AuthenticatedResourceContext, + site: ReservationSite, +) -> (usize, usize, bool) { + auth.with_state(|_, state| { + Ok(match site { + ReservationSite::OrdinaryChannel => { + state.values.test_resource_ordinary_reservation_snapshot() + } + ReservationSite::BeginActiveFrames + | ReservationSite::BeginHistory + | ReservationSite::ResourceDiagnostic + | ReservationSite::OrdinaryDiagnostic => state + .resource_state + .as_ref() + .unwrap() + .reservation_faults + .snapshot(site), + }) + }) + .unwrap() +} + +fn assert_injected(before: (usize, usize, bool), after: (usize, usize, bool)) { + assert!(before.2); + assert_eq!(after, (before.0 + 1, before.1 + 1, false)); +} + +fn expected_begin_error(site: ReservationSite) -> NativeResourceError { + match site { + ReservationSite::OrdinaryChannel => { + NativeResourceError::OrdinaryStorage(JettRuntimeStatusV1::RESOURCE_EXHAUSTED) + } + ReservationSite::BeginActiveFrames | ReservationSite::BeginHistory => { + NativeResourceError::Capacity + } + ReservationSite::ResourceDiagnostic | ReservationSite::OrdinaryDiagnostic => { + panic!("not an entry preflight site") + } + } +} + +fn assert_idle(auth: &AuthenticatedResourceContext, remaining: usize) { + assert_empty(auth); + auth.with_resource(|state, _, _| { + assert!(state.attempt.is_none()); + assert!(state.only_installation_handles()); + assert_eq!(state.script_remaining(), remaining); + Ok(()) + }) + .unwrap(); +} + +fn fill_to_growth(auth: &AuthenticatedResourceContext, site: ReservationSite) -> History { + let (first, _, _, grant) = start(auth, ResourcePurpose::Runtime); + bounds(auth); + assert_eq!(complete(auth, first, 1).selected_kind, 1); + let capacity = auth + .with_state(|_, state| { + Ok(match site { + ReservationSite::BeginHistory => { + state.resource_state.as_ref().unwrap().completed.capacity() + } + ReservationSite::OrdinaryChannel => { + state.values.test_resource_ordinary_channel_capacity().1 + } + ReservationSite::BeginActiveFrames + | ReservationSite::ResourceDiagnostic + | ReservationSite::OrdinaryDiagnostic => panic!("not a growable history site"), + }) + }) + .unwrap(); + // Derived from actual Vec capacity, never a fixed assumed growth factor. + assert!(capacity >= 1 && capacity < MAX_ATTEMPTS); + for _ in 1..capacity { + let (attempt, _, _, next_grant) = start(auth, ResourcePurpose::Runtime); + assert_eq!(next_grant, grant); + bounds(auth); + assert_eq!(complete(auth, attempt, 1).selected_kind, 1); + } + auth.with_state(|_, state| { + match site { + ReservationSite::BeginHistory => { + let history = &state.resource_state.as_ref().unwrap().completed; + assert_eq!(history.len(), history.capacity()); + } + ReservationSite::OrdinaryChannel => { + let (length, capacity) = state.values.test_resource_ordinary_channel_capacity(); + assert_eq!(length, capacity); + } + ReservationSite::BeginActiveFrames + | ReservationSite::ResourceDiagnostic + | ReservationSite::OrdinaryDiagnostic => panic!("not a growable history site"), + } + Ok(()) + }) + .unwrap(); + assert_idle(auth, 0); + history(auth) +} + +#[test] +fn ordinary_reservation_first_begin_refusals_publish_no_owner_or_provider_effect() { + for site in [ + ReservationSite::BeginActiveFrames, + ReservationSite::BeginHistory, + ReservationSite::OrdinaryChannel, + ] { + context(|auth| { + install(auth, &[(1, 811, 0, "")], 1); + arm(auth, site); + let before = fault_snapshot(auth, site); + assert_eq!(begin_raw(auth), Err(expected_begin_error(site))); + assert_injected(before, fault_snapshot(auth, site)); + assert_eq!(channel_state(auth), (0, None, None)); + assert!(history(auth).is_empty()); + assert!(message(auth).is_empty()); + assert!(events(auth).is_empty()); + assert_idle(auth, 1); + assert!(complete_raw(auth, ResourceHandleId::new(123_456).unwrap(), 0).is_err()); + assert_eq!(channel_state(auth), (0, None, None)); + let (attempt, _, frame, grant) = start(auth, ResourcePurpose::Runtime); + construct(auth, frame, grant, 811); + assert_eq!(complete(auth, attempt, 0).selected_kind, 0); + assert_eq!(events(auth), vec![(811, 1), (811, 5)]); + assert_idle(auth, 0); + }); + } +} + +#[test] +fn ordinary_reservation_next_history_or_channel_growth_preserves_every_sealed_error() { + for site in [ + ReservationSite::BeginHistory, + ReservationSite::OrdinaryChannel, + ] { + context(|auth| { + install(auth, &[], u32::try_from(MAX_ATTEMPTS).unwrap()); + let saved = fill_to_growth(auth, site); + let saved_channel = channel_state(auth); + let saved_message = message(auth); + assert_eq!(saved_message, BOUNDS); + arm(auth, site); + let before = fault_snapshot(auth, site); + assert_eq!(begin_raw(auth), Err(expected_begin_error(site))); + assert_injected(before, fault_snapshot(auth, site)); + assert_eq!(history(auth), saved); + assert_eq!(channel_state(auth), saved_channel); + assert_eq!(message(auth), saved_message); + assert_idle(auth, 0); + let (next, _, _, _) = start(auth, ResourcePurpose::Runtime); + assert!(message(auth).is_empty()); + assert_eq!(complete(auth, next, 0).selected_kind, 0); + let current = history(auth); + assert_eq!(¤t[..saved.len()], &saved); + assert_eq!(current.len(), saved.len() + 1); + assert!(events(auth).is_empty()); + assert_idle(auth, 0); + }); + } +} + +#[test] +fn ordinary_reservation_invalid_predecessor_never_reaches_or_consumes_channel_allocation() { + context(|auth| { + install(auth, &[], u32::try_from(MAX_ATTEMPTS).unwrap()); + let saved = fill_to_growth(auth, ReservationSite::OrdinaryChannel); + let saved_channel = channel_state(auth); + let saved_message = message(auth); + arm(auth, ReservationSite::OrdinaryChannel); + let before = fault_snapshot(auth, ReservationSite::OrdinaryChannel); + auth.with_state(|_, state| { + let previous = state + .resource_state + .as_ref() + .unwrap() + .completed + .last() + .unwrap() + .ordinary; + let mut forged = previous.owner; + forged.attempt = ResourceHandleId::new(forged.attempt.raw() + 10).unwrap(); + forged.root = ResourceHandleId::new(forged.root.raw() + 10).unwrap(); + forged.ordinal += 1; + forged.context.token = forged.context.token.wrapping_add(1); + assert!( + state + .values + .prepare_resource_ordinary_attempt(forged, Some(previous)) + .is_err() + ); + let mut wrong_saved = previous; + wrong_saved.completion.body_status += 1; + let mut next = previous.owner; + next.attempt = ResourceHandleId::new(next.attempt.raw() + 10).unwrap(); + next.root = ResourceHandleId::new(next.root.raw() + 10).unwrap(); + next.ordinal += 1; + assert!( + state + .values + .prepare_resource_ordinary_attempt(next, Some(wrong_saved)) + .is_err() + ); + Ok(()) + }) + .unwrap(); + assert_eq!( + fault_snapshot(auth, ReservationSite::OrdinaryChannel), + before + ); + assert_eq!(channel_state(auth), saved_channel); + assert_eq!(history(auth), saved); + assert_eq!(message(auth), saved_message); + assert_eq!( + begin_raw(auth), + Err(expected_begin_error(ReservationSite::OrdinaryChannel)) + ); + assert_injected( + before, + fault_snapshot(auth, ReservationSite::OrdinaryChannel), + ); + assert_eq!(channel_state(auth), saved_channel); + assert_eq!(history(auth), saved); + assert_idle(auth, 0); + }); +} + +#[test] +fn ordinary_reservation_wrong_or_active_entry_cannot_consume_prepublication_fault() { + context(|auth| { + install(auth, &[], 1); + arm(auth, ReservationSite::BeginActiveFrames); + let before = fault_snapshot(auth, ReservationSite::BeginActiveFrames); + auth.with_state(|_, state| { + let mut wrong = entry(); + wrong.signature += 1; + assert_eq!( + state.resource_state.as_mut().unwrap().begin_entry( + &mut state.values, + &state.resources, + wrong, + ResourcePurpose::Runtime, + ), + Err(NativeResourceError::ActiveAttempt) + ); + Ok(()) + }) + .unwrap(); + assert_eq!( + fault_snapshot(auth, ReservationSite::BeginActiveFrames), + before + ); + assert_eq!(begin_raw(auth), Err(NativeResourceError::Capacity)); + assert_injected( + before, + fault_snapshot(auth, ReservationSite::BeginActiveFrames), + ); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + arm(auth, ReservationSite::BeginHistory); + let active_before = fault_snapshot(auth, ReservationSite::BeginHistory); + let channel = channel_state(auth); + assert_eq!(begin_raw(auth), Err(NativeResourceError::ActiveAttempt)); + assert_eq!(channel_state(auth), channel); + assert_eq!( + fault_snapshot(auth, ReservationSite::BeginHistory), + active_before + ); + assert_eq!(complete(auth, attempt, 0).selected_kind, 0); + assert_eq!( + fault_snapshot(auth, ReservationSite::BeginHistory), + active_before + ); + assert_idle(auth, 0); + }); +} + +#[test] +fn ordinary_reservation_sealing_uses_preflight_storage_and_keeps_pending_growth_faults() { + context(|auth| { + install(auth, &[(1, 821, 0, "")], 1); + let (attempt, _, frame, grant) = start(auth, ResourcePurpose::Runtime); + construct(auth, frame, grant, 821); + prefix(auth, b"case 17: "); + bounds(auth); + let original_message = message(auth); + arm(auth, ReservationSite::BeginHistory); + arm(auth, ReservationSite::OrdinaryChannel); + let history_fault = fault_snapshot(auth, ReservationSite::BeginHistory); + let channel_fault = fault_snapshot(auth, ReservationSite::OrdinaryChannel); + let completion = complete(auth, attempt, 1); + assert_eq!( + ( + completion.body_status, + completion.cleanup_status, + completion.selected_kind + ), + (1, 0, 1) + ); + assert_eq!( + fault_snapshot(auth, ReservationSite::BeginHistory), + history_fault + ); + assert_eq!( + fault_snapshot(auth, ReservationSite::OrdinaryChannel), + channel_fault + ); + assert_eq!(channel_state(auth).2.unwrap().completion, completion); + assert_eq!(history(auth)[0].2, original_message); + assert_eq!(message(auth), original_message); + assert_eq!(events(auth), vec![(821, 1), (821, 5)]); + let saved = history(auth); + assert!(complete_raw(auth, attempt, 0).is_err()); + assert_eq!(history(auth), saved); + assert_eq!( + fault_snapshot(auth, ReservationSite::BeginHistory), + history_fault + ); + assert_eq!( + fault_snapshot(auth, ReservationSite::OrdinaryChannel), + channel_fault + ); + assert_idle(auth, 0); + }); +} + +fn assert_diagnostic_tombstone( + auth: &AuthenticatedResourceContext, + attempt: ResourceHandleId, + cleanup: u32, + selected_kind: u32, + original_message: &[u8], +) { + assert_eq!(message(auth), original_message); + let selected = channel_state(auth).2.unwrap(); + assert!(!selected.diagnostic_ready()); + assert_eq!( + ( + selected.completion.body_status, + selected.completion.cleanup_status, + selected.completion.selected_kind + ), + (1, cleanup, selected_kind) + ); + auth.with_resource(|state, ordinary, _| { + let row = state.completed.last().unwrap(); + assert_eq!(row.completion.attempt, attempt.raw()); + assert_eq!(row.ordinary, selected); + assert_eq!(row.report_error, Some(NativeResourceError::Capacity)); + assert!(row.resource_message.is_empty() && row.ordinary_message.is_empty()); + assert_eq!( + state.completed_attempt(attempt), + Err(NativeResourceError::Capacity) + ); + assert_eq!( + state.completed_messages(attempt), + Err(NativeResourceError::Capacity) + ); + assert!(!ordinary.cleanup_failed); + Ok(()) + }) + .unwrap(); + let saved = history(auth); + let before_events = events(auth); + assert!(complete_raw(auth, attempt, 0).is_err()); + assert!(begin_raw(auth).is_err()); + assert_eq!(history(auth), saved); + assert_eq!(events(auth), before_events); + assert_eq!(channel_state(auth).2, Some(selected)); + assert_eq!(message(auth), original_message); + assert_idle(auth, 0); +} + +#[test] +fn ordinary_reservation_ordinary_diagnostic_failure_seals_after_real_resource_retirement() { + context(|auth| { + install(auth, &[(1, 831, 0, "")], 3); + let (first, _, _, first_grant) = start(auth, ResourcePurpose::Runtime); + prefix(auth, b"earlier case: "); + bounds(auth); + assert_eq!(complete(auth, first, 1).selected_kind, 1); + let earlier_history = history(auth); + let earlier_proof = channel_state(auth).2.unwrap(); + assert_idle(auth, 1); + let (attempt, _, frame, grant) = start(auth, ResourcePurpose::Runtime); + assert_eq!(first_grant, grant); + construct(auth, frame, grant, 831); + prefix(auth, b"case 23: "); + bounds(auth); + let original_message = message(auth); + let expected = [b"case 23: ".as_slice(), BOUNDS].concat(); + assert_eq!(original_message, expected); + arm(auth, ReservationSite::OrdinaryDiagnostic); + let before = fault_snapshot(auth, ReservationSite::OrdinaryDiagnostic); + assert_eq!( + complete_raw(auth, attempt, 1), + Err(NativeResourceError::Capacity) + ); + assert_injected( + before, + fault_snapshot(auth, ReservationSite::OrdinaryDiagnostic), + ); + assert_eq!(events(auth), vec![(831, 1), (831, 5)]); + assert_diagnostic_tombstone(auth, attempt, 0, 1, &original_message); + assert_eq!(&history(auth)[..earlier_history.len()], &earlier_history); + auth.with_resource(|state, _, _| { + assert_eq!(state.completed[0].ordinary, earlier_proof); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn ordinary_reservation_either_diagnostic_failure_keeps_cleanup_selection_and_first_bounds_error() { + for site in [ + ReservationSite::ResourceDiagnostic, + ReservationSite::OrdinaryDiagnostic, + ] { + context(|auth| { + install(auth, &[(3, 841, 0, "")], 2); + let (attempt, _, frame, grant) = start(auth, ResourcePurpose::Runtime); + construct(auth, frame, grant, 841); + bounds(auth); + assert_eq!(message(auth), BOUNDS); + arm(auth, site); + let before = fault_snapshot(auth, site); + assert_eq!( + complete_raw(auth, attempt, 1), + Err(NativeResourceError::Capacity) + ); + assert_injected(before, fault_snapshot(auth, site)); + assert_eq!(events(auth), vec![(841, 1), (841, 5)]); + assert_diagnostic_tombstone( + auth, + attempt, + JettRuntimeStatusV1::PANIC.code(), + 3, + BOUNDS, + ); + }); + } +} + +#[test] +fn ordinary_reservation_empty_diagnostics_do_not_request_or_consume_allocator_fault() { + context(|auth| { + install(auth, &[], 1); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + arm(auth, ReservationSite::ResourceDiagnostic); + let before = fault_snapshot(auth, ReservationSite::ResourceDiagnostic); + let completion = complete(auth, attempt, 0); + assert_eq!(completion.selected_kind, 0); + assert_eq!( + fault_snapshot(auth, ReservationSite::ResourceDiagnostic), + before + ); + assert!(channel_state(auth).2.unwrap().diagnostic_ready()); + let saved = history(auth); + assert!(saved[0].1.is_empty() && saved[0].2.is_empty()); + assert!(message(auth).is_empty()); + assert!(events(auth).is_empty()); + assert_idle(auth, 0); + }); +} + +#[test] +fn ordinary_reservation_length_refusal_precedes_and_preserves_pending_allocator_fault() { + context(|auth| { + install(auth, &[], 2); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + let original = vec![b'x'; 64 * 1024 + 1]; + dynamic_failure(auth, &original); + arm(auth, ReservationSite::OrdinaryDiagnostic); + let before = fault_snapshot(auth, ReservationSite::OrdinaryDiagnostic); + assert_eq!( + complete_raw(auth, attempt, 1), + Err(NativeResourceError::Capacity) + ); + assert_eq!( + fault_snapshot(auth, ReservationSite::OrdinaryDiagnostic), + before + ); + assert_diagnostic_tombstone(auth, attempt, 0, 1, &original); + assert_eq!( + fault_snapshot(auth, ReservationSite::OrdinaryDiagnostic), + before + ); + assert!(events(auth).is_empty()); + }); +} + +#[test] +fn ordinary_reservation_faults_are_owned_by_one_context_and_never_poison_another() { + context(|first| { + context(|second| { + install(first, &[], 1); + install(second, &[], 1); + arm(first, ReservationSite::OrdinaryChannel); + let first_before = fault_snapshot(first, ReservationSite::OrdinaryChannel); + let (second_attempt, _, _, _) = start(second, ResourcePurpose::Runtime); + assert_eq!(complete(second, second_attempt, 0).selected_kind, 0); + assert_eq!( + fault_snapshot(first, ReservationSite::OrdinaryChannel), + first_before + ); + assert_eq!( + fault_snapshot(second, ReservationSite::OrdinaryChannel).1, + 0 + ); + assert_eq!( + begin_raw(first), + Err(expected_begin_error(ReservationSite::OrdinaryChannel)) + ); + assert_injected( + first_before, + fault_snapshot(first, ReservationSite::OrdinaryChannel), + ); + assert_eq!(channel_state(first), (0, None, None)); + let (first_attempt, _, _, _) = start(first, ResourcePurpose::Runtime); + assert_eq!(complete(first, first_attempt, 0).selected_kind, 0); + assert!(message(first).is_empty() && message(second).is_empty()); + assert!(events(first).is_empty() && events(second).is_empty()); + assert_idle(first, 0); + assert_idle(second, 0); + }); + }); +} + +#[test] +fn ordinary_reservation_custody_failure_after_preparation_publishes_no_channel_or_frame() { + use crate::resource_custody::FrameReservationSite; + for site in [ + FrameReservationSite::Frames, + FrameReservationSite::ActiveFrames, + ] { + context(|auth| { + install(auth, &[], 1); + let before = auth + .with_state(|_, state| { + let custody = &mut state.resource_state.as_mut().unwrap().custody; + custody.test_arm_frame_reservation(site); + Ok(custody.test_frame_reservation_snapshot(site)) + }) + .unwrap(); + assert_eq!( + begin_raw(auth), + Err(NativeResourceError::Custody( + CustodyError::CapacityExhausted + )) + ); + let after = auth + .with_state(|_, state| { + Ok(state + .resource_state + .as_ref() + .unwrap() + .custody + .test_frame_reservation_snapshot(site)) + }) + .unwrap(); + assert_injected(before, after); + // The ordinary Vec reservation succeeded before the real core failure, + // but dropping the prepared borrow did not publish its owner or record. + assert_eq!( + fault_snapshot(auth, ReservationSite::OrdinaryChannel), + (1, 0, false) + ); + assert_eq!(channel_state(auth), (0, None, None)); + assert!(history(auth).is_empty()); + assert!(message(auth).is_empty()); + assert!(events(auth).is_empty()); + assert_idle(auth, 0); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + assert_eq!( + fault_snapshot(auth, ReservationSite::OrdinaryChannel), + (1, 0, false) + ); + assert_eq!(channel_state(auth).0, 1); + assert_eq!(complete(auth, attempt, 0).selected_kind, 0); + assert!(channel_state(auth).2.unwrap().diagnostic_ready()); + assert_idle(auth, 0); + }); + } +} + +#[test] +fn ordinary_reservation_earlier_history_growth_does_not_make_failed_diagnostics_reenterable() { + context(|auth| { + install(auth, &[], u32::try_from(MAX_ATTEMPTS).unwrap()); + let (first, _, _, _) = start(auth, ResourcePurpose::Runtime); + let capacity = auth + .with_resource(|state, _, _| Ok(state.completed.capacity())) + .unwrap(); + assert!(capacity >= 1 && capacity < MAX_ATTEMPTS); + let mut prepared_first = Some(first); + let mut tombstone = None; + for ordinal in 0..capacity { + let attempt = prepared_first + .take() + .unwrap_or_else(|| start(auth, ResourcePurpose::Runtime).0); + bounds(auth); + if ordinal + 1 == capacity { + arm(auth, ReservationSite::OrdinaryDiagnostic); + assert_eq!( + complete_raw(auth, attempt, 1), + Err(NativeResourceError::Capacity) + ); + tombstone = Some(attempt); + } else { + assert_eq!(complete(auth, attempt, 1).selected_kind, 1); + } + } + let tombstone = tombstone.unwrap(); + let saved = history(auth); + let saved_channel = channel_state(auth); + let original_message = message(auth); + assert_eq!(original_message, BOUNDS); + assert!(!saved_channel.2.unwrap().diagnostic_ready()); + auth.with_resource(|state, _, _| { + assert_eq!(state.completed.len(), state.completed.capacity()); + assert_eq!( + state.completed_attempt(tombstone), + Err(NativeResourceError::Capacity) + ); + Ok(()) + }) + .unwrap(); + arm(auth, ReservationSite::BeginHistory); + let before = fault_snapshot(auth, ReservationSite::BeginHistory); + // Preserve the real constructor order: Resource history reserves earlier + // than exact ordinary predecessor validation. Both refusals are inert. + assert_eq!(begin_raw(auth), Err(NativeResourceError::Capacity)); + assert_injected(before, fault_snapshot(auth, ReservationSite::BeginHistory)); + assert_eq!(history(auth), saved); + assert_eq!(channel_state(auth), saved_channel); + assert_eq!(message(auth), original_message); + assert_idle(auth, 0); + assert_eq!( + begin_raw(auth), + Err(NativeResourceError::OrdinaryStorage( + JettRuntimeStatusV1::INVALID_ARGUMENT + )) + ); + assert_eq!(history(auth), saved); + assert_eq!(channel_state(auth), saved_channel); + assert_eq!(message(auth), original_message); + assert!(events(auth).is_empty()); + assert_idle(auth, 0); + }); +} diff --git a/crates/jett_runtime/src/native_abi/resource/ordinary_reservation_faults.rs b/crates/jett_runtime/src/native_abi/resource/ordinary_reservation_faults.rs new file mode 100644 index 00000000..e086ea78 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/ordinary_reservation_faults.rs @@ -0,0 +1,71 @@ +//! Private unit-test faults at real fallible ordinary-entry reservation sites. +//! This module, its instance state and every checkpoint are absent without cfg(test). +use super::{NativeResourceError, ResourceResult}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(in crate::native_abi) enum ReservationSite { + BeginActiveFrames, + BeginHistory, + OrdinaryChannel, + ResourceDiagnostic, + OrdinaryDiagnostic, +} + +impl ReservationSite { + fn index(self) -> usize { + match self { + Self::BeginActiveFrames => 0, + Self::BeginHistory => 1, + Self::OrdinaryChannel => 2, + Self::ResourceDiagnostic => 3, + Self::OrdinaryDiagnostic => 4, + } + } +} + +#[derive(Default)] +pub(in crate::native_abi) struct ReservationFaults { + armed: Option, + growth_requests: [usize; 5], + injected_refusals: [usize; 5], +} + +impl ReservationFaults { + pub(in crate::native_abi) fn arm(&mut self, site: ReservationSite) { + assert!( + self.armed.is_none(), + "only one allocation fault may be pending per state" + ); + self.armed = Some(site); + } + + // A reserve that reuses capacity cannot produce an allocator failure. The + // hook is directly before try_reserve and follows the guards preceding that + // actual reservation. The ordinary-channel hook follows the exact predecessor proof. + pub(in crate::native_abi) fn checkpoint( + &mut self, + site: ReservationSite, + allocation_needed: bool, + ) -> ResourceResult<()> { + if !allocation_needed { + return Ok(()); + } + let index = site.index(); + self.growth_requests[index] += 1; + if self.armed == Some(site) { + self.armed = None; + self.injected_refusals[index] += 1; + return Err(NativeResourceError::Capacity); + } + Ok(()) + } + + pub(in crate::native_abi) fn snapshot(&self, site: ReservationSite) -> (usize, usize, bool) { + let index = site.index(); + ( + self.growth_requests[index], + self.injected_refusals[index], + self.armed == Some(site), + ) + } +} diff --git a/crates/jett_runtime/src/native_abi/resource/program_entry.rs b/crates/jett_runtime/src/native_abi/resource/program_entry.rs new file mode 100644 index 00000000..8031f99a --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/program_entry.rs @@ -0,0 +1,383 @@ +//! Generated-object startup. No Source provider or runtime-purpose selector. +use super::*; +use std::mem::{align_of, size_of}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum EntryOwner { + External, + Generated, +} + +#[repr(C)] +struct ObjectManifest { + layout: *const u8, + layout_length: u64, + function: u32, + signature: u32, + scope: u32, + reserved: u32, +} +const _: () = assert!(size_of::() == 32 && align_of::() == 8); + +#[derive(Clone, Copy)] +struct Region { + start: usize, + end: usize, +} +fn region(pointer: *const T, count: usize) -> ResourceResult { + let length = size_of::() + .checked_mul(count) + .filter(|length| *length <= isize::MAX as usize) + .ok_or(NativeResourceError::Capacity)?; + let start = pointer.addr(); + if pointer.is_null() || start % align_of::() != 0 { + return Err(NativeResourceError::WrongOperation); + } + let end = start + .checked_add(length) + .ok_or(NativeResourceError::Capacity)?; + Ok(Region { start, end }) +} +fn separate(regions: &[Region]) -> ResourceResult<()> { + for (index, left) in regions.iter().enumerate() { + for right in ®ions[index + 1..] { + if left.start < right.end && right.start < left.end { + return Err(NativeResourceError::WrongOperation); + } + } + } + Ok(()) +} + +fn begin( + authenticated: &AuthenticatedResourceContext, + wire: &[u8], + entry: NativeEntry, +) -> ResourceResult { + authenticated.with_state(|key, state| { + if state.resource_state.is_none() { + install_candidate(key, state, wire, entry, InstalledProvider::Disabled)?; + state + .resource_state + .as_mut() + .ok_or(NativeResourceError::MissingInstallation)? + .entry_owner = EntryOwner::Generated; + } + let resource = state + .resource_state + .as_mut() + .ok_or(NativeResourceError::MissingInstallation)?; + resource.validate_installation()?; + if resource.entry != entry || resource.installed_wire != wire { + return Err(NativeResourceError::InvalidEntry); + } + if resource.entry_owner == EntryOwner::External { + // The matched test main already owns begin/complete. This is never + // an alternate production installation or attempt-adoption path. + #[cfg(all(test, jett_resource_native_test_archive))] + if matches!(resource.provider, InstalledProvider::Scripted(_)) + && resource.attempt.as_ref().is_some_and(|attempt| { + attempt.phase == AttemptPhase::Running + && attempt.entry == entry + && attempt.purpose == ResourcePurpose::Runtime + }) + { + return Ok(0); + } + return Err(NativeResourceError::ActiveAttempt); + } + let attempt = resource.begin_entry( + &mut state.values, + &state.resources, + entry, + ResourcePurpose::Runtime, + )?; + Ok(attempt.raw()) + }) +} + +/// # Safety +/// The stationary context is live. Manifest/layout remain readable for this +/// call; output records are distinct, aligned, exclusive writable storage. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_program_begin( + context: *const JettRuntimeContextV1, + manifest: *const ObjectManifest, + out_attempt: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + let regions = (|| { + let records = [ + region(context, 1)?, + region(manifest, 1)?, + region(out_attempt, 1)?, + region(out_result, 1)?, + ]; + separate(&records)?; + // SAFETY: caller supplies readable storage; known overlaps were refused. + let manifest = unsafe { &*manifest }; + let length = + usize::try_from(manifest.layout_length).map_err(|_| NativeResourceError::Capacity)?; + let layout = region(manifest.layout, length)?; + for record in records { + separate(&[layout, record])?; + } + if manifest.reserved != 0 || length == 0 { + return Err(NativeResourceError::InvalidEntry); + } + Ok(length) + })(); + let Ok(length) = regions else { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + }; + complete_call(out_result, || { + // SAFETY: caller keeps the validated readable regions stable for the call. + let manifest = unsafe { &*manifest }; + let wire = unsafe { std::slice::from_raw_parts(manifest.layout, length) }; + let entry = NativeEntry { + function: manifest.function, + signature: manifest.signature, + scope: manifest.scope, + }; + match AuthenticatedResourceContext::acquire(context) + .and_then(|authenticated| begin(&authenticated, wire, entry)) + { + Ok(attempt) => { + // SAFETY: validated exclusive output; no effect follows the write. + unsafe { ptr::write(out_attempt, attempt) }; + JettRuntimeResultV1::ok() + } + Err(error) => JettRuntimeResultV1::failure(error.status(), REFUSED), + } + }) +} + +fn complete( + authenticated: &AuthenticatedResourceContext, + attempt: ResourceHandleId, + body_status: u32, + panicked: bool, +) -> ResourceResult { + authenticated.with_state(|_, state| { + let resource = state + .resource_state + .as_mut() + .ok_or(NativeResourceError::MissingInstallation)?; + resource.validate_installation()?; + if resource.entry_owner != EntryOwner::Generated { + return Err(NativeResourceError::WrongOperation); + } + resource.runtime_purpose()?; + resource.complete_entry( + &mut state.values, + &mut state.resources, + attempt, + body_status, + panicked, + )?; + let completed = resource + .completed + .last() + .ok_or(NativeResourceError::InvalidEntry)?; + if completed.completion.attempt != attempt.raw() { + return Err(NativeResourceError::InvalidEntry); + } + Ok(completed.terminal_status) + }) +} + +fn status_call( + operation: impl FnOnce() -> ResourceResult, +) -> JettRuntimeStatusV1 { + match catch_unwind(AssertUnwindSafe(operation)) { + Ok(Ok(status)) => status, + Ok(Err(error)) => error.status(), + Err(panic) => { + discard_panic_payload(panic); + JettRuntimeStatusV1::PANIC + } + } +} + +/// Retire precisely the generated attempt, preserving original body status. +/// # Safety +/// `context` is the live stationary context from the successful begin call. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_program_complete( + context: *const JettRuntimeContextV1, + attempt: u64, + body_status: u32, +) -> JettRuntimeStatusV1 { + status_call(|| { + complete( + &AuthenticatedResourceContext::acquire(context)?, + ResourceHandleId::new(attempt)?, + body_status, + false, + ) + }) +} + +/// Launcher fallback after a recoverable entry panic. Ordinary contexts and +/// externally managed test attempts are no-ops. Completing custody is never retried. +/// # Safety +/// `context` is live and stationary; call only after entry control has returned. +/// `out_selected_kind` is distinct aligned exclusive writable storage. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_program_unwind( + context: *const JettRuntimeContextV1, + body_status: u32, + panicked: u32, + out_selected_kind: *mut u32, +) -> JettRuntimeStatusV1 { + if !leaves::output_ok(context, out_selected_kind) { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + } + status_call(|| { + if panicked > 1 { + return Err(NativeResourceError::WrongOperation); + } + let authenticated = AuthenticatedResourceContext::acquire(context)?; + let attempt = authenticated.with_state(|_, state| { + let Some(resource) = state.resource_state.as_ref() else { + return Ok(None); + }; + if resource.entry_owner != EntryOwner::Generated { + return Ok(None); + } + match resource.attempt.as_ref() { + Some(attempt) if attempt.phase == AttemptPhase::Running => Ok(Some(attempt.id)), + Some(_) => Err(NativeResourceError::ActiveAttempt), + None => Ok(None), + } + })?; + let status = match attempt { + Some(attempt) => complete(&authenticated, attempt, body_status, panicked != 0)?, + None => JettRuntimeStatusV1::OK, + }; + let kind = if attempt.is_some() { + observation(context, |completed| { + Ok(completed.map(|c| c.completion.selected_kind).unwrap_or(0)) + })? + } else { + 0 + }; + // SAFETY: the caller supplies checked exclusive output storage. + unsafe { ptr::write(out_selected_kind, kind) }; + Ok(status) + }) +} + +fn observation( + context: *const JettRuntimeContextV1, + read: impl FnOnce(Option<&NativeCompletedAttempt>) -> ResourceResult, +) -> ResourceResult { + AuthenticatedResourceContext::acquire(context)?.with_state(|_, state| { + let completed = state + .resource_state + .as_ref() + .filter(|resource| { + resource.entry_owner == EntryOwner::Generated && resource.attempt.is_none() + }) + .and_then(|resource| resource.completed.last()); + if let Some(error) = completed.and_then(|completed| completed.report_error) { + return Err(error); + } + read(completed) + }) +} + +/// Read the retained terminal outcome without changing any first-error channel. +/// # Safety +/// Live stationary context and distinct aligned writable result record. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_program_failure( + context: *const JettRuntimeContextV1, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + if !leaves::output_ok(context, out_result) { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + } + complete_call(out_result, || { + match observation(context, |completed| { + Ok(completed + .map(|c| c.terminal_status) + .unwrap_or(JettRuntimeStatusV1::OK)) + }) { + Ok(status) if status == JettRuntimeStatusV1::OK => JettRuntimeResultV1::ok(), + Ok(status) => JettRuntimeResultV1::failure(status, REFUSED), + Err(error) => JettRuntimeResultV1::failure(error.status(), REFUSED), + } + }) +} + +fn message(completed: &NativeCompletedAttempt) -> &[u8] { + let message = + if completed.completion.selected_kind >= 2 || completed.ordinary_message.is_empty() { + &completed.resource_message + } else { + &completed.ordinary_message + }; + if message.is_empty() && completed.terminal_status != JettRuntimeStatusV1::OK { + b"native Resource entry failed" + } else { + message + } +} + +/// Copy retained diagnostics while their context is alive. Null/zero queries length. +/// # Safety +/// Live stationary context; output records and any buffer are writable, distinct, +/// and exclusive for the duration of the call. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_program_failure_copy( + context: *const JettRuntimeContextV1, + buffer: *mut u8, + capacity: u64, + out_length: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + let checked = (|| { + let records = [ + region(context, 1)?, + region(out_length, 1)?, + region(out_result, 1)?, + ]; + separate(&records)?; + let capacity = usize::try_from(capacity).map_err(|_| NativeResourceError::Capacity)?; + if !buffer.is_null() { + let buffer = region(buffer, capacity)?; + for record in records { + separate(&[buffer, record])?; + } + } else if capacity != 0 { + return Err(NativeResourceError::WrongOperation); + } + Ok(capacity) + })(); + let Ok(capacity) = checked else { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + }; + complete_call(out_result, || { + match observation(context, |completed| { + let message = completed.map(message).unwrap_or_default(); + if !buffer.is_null() && capacity < message.len() { + return Err(NativeResourceError::Capacity); + } + // SAFETY: validated exclusive regions; borrowed bytes remain pinned by the lease. + unsafe { + ptr::write(out_length, message.len() as u64); + if !buffer.is_null() { + ptr::copy_nonoverlapping(message.as_ptr(), buffer, message.len()); + } + } + Ok(()) + }) { + Ok(()) => JettRuntimeResultV1::ok(), + Err(error) => JettRuntimeResultV1::failure(error.status(), REFUSED), + } + }) +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_runtime/src/native_abi/resource/program_entry/tests.rs b/crates/jett_runtime/src/native_abi/resource/program_entry/tests.rs new file mode 100644 index 00000000..6cb578f5 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/program_entry/tests.rs @@ -0,0 +1,395 @@ +//! Startup controls, not a real provider or normal-archive execution receipt. +use super::*; +use crate::native_abi::resource::tests::{context, entry, layout_with_entry_network}; + +fn pointer(authenticated: &AuthenticatedResourceContext) -> *const JettRuntimeContextV1 { + authenticated.key.owner_address as *const JettRuntimeContextV1 +} +fn manifest(wire: &[u8]) -> ObjectManifest { + let entry = entry(); + ObjectManifest { + layout: wire.as_ptr(), + layout_length: wire.len() as u64, + function: entry.function, + signature: entry.signature, + scope: entry.scope, + reserved: 0, + } +} +fn retained( + authenticated: &AuthenticatedResourceContext, +) -> (usize, NativeResourceCompletion, JettRuntimeStatusV1) { + authenticated + .with_state(|_, state| { + let resource = state.resource_state.as_ref().unwrap(); + let completed = resource.completed.last().unwrap(); + Ok(( + resource.completed.len(), + completed.completion, + completed.terminal_status, + )) + }) + .unwrap() +} + +#[test] +fn generated_startup_retains_wire_and_disabled_refusal() { + context(|auth| { + let wire = layout_with_entry_network(true); + let object = manifest(&wire); + let mut attempt = 99; + let mut result = JettRuntimeResultV1::ok(); + assert_eq!( + unsafe { + jett_rt_v1_resource_program_begin(pointer(auth), &object, &mut attempt, &mut result) + }, + JettRuntimeStatusV1::OK + ); + assert_ne!(attempt, 0); + auth.with_state(|_, state| { + let resource = state.resource_state.as_ref().unwrap(); + assert_eq!(resource.installed_wire, wire); + assert!(!resource.provider.enabled()); + assert!(resource.network.is_none()); + assert_eq!( + resource.entry_network(ResourceHandleId::new(attempt)?, 0), + Err(NativeResourceError::WrongGrant) + ); + assert_eq!( + resource.attempt.as_ref().unwrap().purpose, + ResourcePurpose::Runtime + ); + Ok(()) + }) + .unwrap(); + assert_eq!( + unsafe { jett_rt_v1_resource_program_complete(pointer(auth), attempt, 0) }, + JettRuntimeStatusV1::OK + ); + assert_eq!(retained(auth).1.body_status, 0); + }); +} + +#[test] +fn exact_reentry_preserves_history_and_refuses_swapped_manifest() { + context(|auth| { + let wire = layout_with_entry_network(false); + let first = begin(auth, &wire, entry()).unwrap(); + assert_eq!( + complete(auth, ResourceHandleId::new(first).unwrap(), 5, false) + .unwrap() + .code(), + 5 + ); + let mut swapped = wire.clone(); + *swapped.last_mut().unwrap() ^= 1; + assert!(matches!( + begin(auth, &swapped, entry()), + Err(NativeResourceError::InvalidEntry) + )); + assert_eq!(retained(auth).0, 1); + assert_eq!(retained(auth).1.body_status, 5); + let second = begin(auth, &wire, entry()).unwrap(); + assert_ne!(first, second); + assert_eq!( + complete(auth, ResourceHandleId::new(second).unwrap(), 0, false).unwrap(), + JettRuntimeStatusV1::OK + ); + assert_eq!(retained(auth).0, 2); + auth.with_state(|_, state| { + let resource = state.resource_state.as_ref().unwrap(); + assert_eq!(resource.completed[0].completion.body_status, 5); + assert_eq!(resource.completed[0].terminal_status.code(), 5); + assert_eq!(resource.custody.live_owners(), 0); + assert_eq!(resource.custody.live_loans(), 0); + assert_eq!(state.resources.live_count(), 0); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn active_reentry_and_wrong_context_do_not_select_or_retire_an_attempt() { + context(|first_context| { + context(|second_context| { + let wire = layout_with_entry_network(false); + let attempt = begin(first_context, &wire, entry()).unwrap(); + assert!(matches!( + begin(first_context, &wire, entry()), + Err(NativeResourceError::ActiveAttempt) + )); + let second = begin(second_context, &wire, entry()).unwrap(); + assert!( + complete( + second_context, + ResourceHandleId::new(attempt).unwrap(), + 0, + false + ) + .is_err() + ); + assert_eq!( + complete( + first_context, + ResourceHandleId::new(attempt).unwrap(), + 0, + false + ) + .unwrap(), + JettRuntimeStatusV1::OK + ); + assert_eq!( + complete( + second_context, + ResourceHandleId::new(second).unwrap(), + 0, + false + ) + .unwrap(), + JettRuntimeStatusV1::OK + ); + assert!( + complete( + first_context, + ResourceHandleId::new(attempt).unwrap(), + 0, + false + ) + .is_err() + ); + assert_eq!(retained(first_context).0, 1); + }) + }); +} + +#[test] +fn ordinary_first_error_is_copied_without_rewriting_body_or_poisoning_reentry() { + context(|auth| { + let wire = layout_with_entry_network(false); + let attempt = begin(auth, &wire, entry()).unwrap(); + assert_ne!(unsafe { values::jett_rt_v1_assert_fail(pointer(auth)) }, 0); + assert_eq!( + complete(auth, ResourceHandleId::new(attempt).unwrap(), 0, false).unwrap(), + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + let (_, completed, _) = retained(auth); + assert_eq!(completed.body_status, 0); + assert_eq!(completed.selected_kind, 1); + let mut bytes = [0_u8; 64]; + let mut length = 0; + let mut result = JettRuntimeResultV1::ok(); + assert_eq!( + unsafe { + jett_rt_v1_resource_program_failure_copy( + pointer(auth), + bytes.as_mut_ptr(), + bytes.len() as u64, + &mut length, + &mut result, + ) + }, + JettRuntimeStatusV1::OK + ); + assert_eq!(&bytes[..length as usize], b"assertion failed"); + let next = begin(auth, &wire, entry()).unwrap(); + assert_eq!( + complete(auth, ResourceHandleId::new(next).unwrap(), 0, false).unwrap(), + JettRuntimeStatusV1::OK + ); + assert_eq!(retained(auth).0, 2); + }); +} + +#[test] +fn first_resource_failure_and_cleanup_priority_retain_original_completion() { + context(|auth| { + let wire = layout_with_entry_network(false); + let attempt = begin(auth, &wire, entry()).unwrap(); + auth.with_state(|_, state| { + let resource = state.resource_state.as_mut().unwrap(); + resource.record_failure(NativeResourceError::Capacity); + resource.record_failure(NativeResourceError::WrongGrant); + Ok(()) + }) + .unwrap(); + assert_eq!( + complete(auth, ResourceHandleId::new(attempt).unwrap(), 0, false).unwrap(), + JettRuntimeStatusV1::RESOURCE_EXHAUSTED + ); + assert_eq!(retained(auth).1.body_status, 0); + let next = begin(auth, &wire, entry()).unwrap(); + auth.with_state(|_, state| { + state + .resource_state + .as_mut() + .unwrap() + .record_failure(NativeResourceError::Cleanup( + ResourceCleanupFailure::FinalizerPanic, + )); + Ok(()) + }) + .unwrap(); + assert_eq!( + complete(auth, ResourceHandleId::new(next).unwrap(), 5, true).unwrap(), + JettRuntimeStatusV1::PANIC + ); + assert_eq!(retained(auth).1.body_status, 5); + assert_eq!( + retained(auth).1.cleanup_status, + JettRuntimeStatusV1::PANIC.code() + ); + assert_eq!(retained(auth).1.selected_kind, 3); + }); +} + +#[test] +fn recoverable_panic_completes_once_and_ordinary_context_is_noop() { + context(|auth| { + let mut kind = 99; + assert_eq!( + unsafe { jett_rt_v1_resource_program_unwind(pointer(auth), 255, 1, &mut kind) }, + JettRuntimeStatusV1::OK + ); + assert_eq!(kind, 0); + let wire = layout_with_entry_network(false); + begin(auth, &wire, entry()).unwrap(); + assert_eq!( + unsafe { jett_rt_v1_resource_program_unwind(pointer(auth), 255, 1, &mut kind) }, + JettRuntimeStatusV1::PANIC + ); + assert_eq!(kind, 2); + assert_eq!(retained(auth).1.body_status, 255); + assert_eq!(retained(auth).0, 1); + assert_eq!( + unsafe { jett_rt_v1_resource_program_unwind(pointer(auth), 255, 1, &mut kind) }, + JettRuntimeStatusV1::OK + ); + assert_eq!(retained(auth).0, 1); + }); +} + +#[test] +fn context_overlap_and_reserved_manifest_refuse_before_installation() { + context(|auth| { + let wire = layout_with_entry_network(false); + let mut object = manifest(&wire); + let mut attempt = 99; + let mut result = JettRuntimeResultV1::ok(); + assert_eq!( + unsafe { + jett_rt_v1_resource_program_begin( + pointer(auth), + &object, + &mut attempt, + pointer(auth).cast_mut().cast(), + ) + }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + object.reserved = 1; + assert_eq!( + unsafe { + jett_rt_v1_resource_program_begin(pointer(auth), &object, &mut attempt, &mut result) + }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + assert_eq!(attempt, 99); + auth.with_state(|_, state| { + assert!(state.resource_state.is_none()); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn external_nonruntime_attempts_cannot_be_adopted_by_generated_startup() { + for purpose in [ + ResourcePurpose::ExplicitComptime, + ResourcePurpose::Verify, + ResourcePurpose::Property, + ] { + context(|auth| { + let wire = layout_with_entry_network(false); + install_disabled(auth, &wire, entry()).unwrap(); + auth.with_state(|_, state| { + let resource = state.resource_state.as_mut().unwrap(); + resource.begin_entry(&mut state.values, &state.resources, entry(), purpose)?; + Ok(()) + }) + .unwrap(); + assert!(matches!( + begin(auth, &wire, entry()), + Err(NativeResourceError::ActiveAttempt) + )); + let mut kind = 99; + assert_eq!( + unsafe { jett_rt_v1_resource_program_unwind(pointer(auth), 0, 0, &mut kind) }, + JettRuntimeStatusV1::OK + ); + assert_eq!(kind, 0); + auth.with_state(|_, state| { + let resource = state.resource_state.as_mut().unwrap(); + let attempt = resource.attempt.as_ref().unwrap().id; + assert_eq!(resource.attempt.as_ref().unwrap().purpose, purpose); + resource.complete_entry( + &mut state.values, + &mut state.resources, + attempt, + 0, + false, + )?; + Ok(()) + }) + .unwrap(); + }); + } +} + +#[cfg(all(test, jett_resource_native_test_archive))] +#[test] +fn private_scripted_handoff_borrows_only_the_exact_runtime_attempt() { + use crate::native_abi::resource::tests::script; + for purpose in [ + ResourcePurpose::Runtime, + ResourcePurpose::ExplicitComptime, + ResourcePurpose::Verify, + ResourcePurpose::Property, + ] { + context(|auth| { + let wire = layout_with_entry_network(false); + install_scripted(auth, &wire, entry(), script(&[], 1)).unwrap(); + let attempt = auth + .with_state(|_, state| { + let resource = state.resource_state.as_mut().unwrap(); + resource.begin_entry(&mut state.values, &state.resources, entry(), purpose) + }) + .unwrap(); + if purpose == ResourcePurpose::Runtime { + assert_eq!(begin(auth, &wire, entry()).unwrap(), 0); + } else { + assert!(matches!( + begin(auth, &wire, entry()), + Err(NativeResourceError::ActiveAttempt) + )); + } + assert!(complete(auth, attempt, 0, false).is_err()); + auth.with_state(|_, state| { + let resource = state.resource_state.as_mut().unwrap(); + assert_eq!(resource.entry_owner, EntryOwner::External); + assert_eq!(resource.attempt.as_ref().unwrap().id, attempt); + resource.complete_entry( + &mut state.values, + &mut state.resources, + attempt, + 0, + false, + )?; + assert_eq!(resource.completed.len(), 1); + Ok(()) + }) + .unwrap(); + }); + } +} diff --git a/crates/jett_runtime/src/native_abi/resource/provider.rs b/crates/jett_runtime/src/native_abi/resource/provider.rs new file mode 100644 index 00000000..81ffb184 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/provider.rs @@ -0,0 +1,366 @@ +use super::*; +#[cfg(test)] +use std::collections::VecDeque; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct NativeProviderIdentity(NonZeroU64); +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct NativeRestrictionIdentity(NonZeroU64); +impl NativeProviderIdentity { + pub(super) fn new(raw: u64) -> ResourceResult { + NonZeroU64::new(raw) + .map(Self) + .ok_or(NativeResourceError::WrongGrant) + } + pub(super) fn raw(self) -> u64 { + self.0.get() + } +} +impl NativeRestrictionIdentity { + pub(super) fn new(raw: u64) -> ResourceResult { + NonZeroU64::new(raw) + .map(Self) + .ok_or(NativeResourceError::WrongGrant) + } + pub(super) fn raw(self) -> u64 { + self.0.get() + } +} + +pub(super) struct NativePayload { + label: i64, +} +pub(super) struct ConstructedPayload { + pub(super) payload: NativePayload, + pub(super) finalizer: Box, +} +pub(super) enum InstalledProvider { + Disabled, + #[cfg(test)] + Scripted(ScriptedProvider), +} +impl InstalledProvider { + pub(super) fn enabled(&self) -> bool { + match self { + Self::Disabled => false, + #[cfg(test)] + Self::Scripted(_) => true, + } + } + pub(super) fn bind( + &mut self, + identity: NativeProviderIdentity, + grant: Option, + ) -> ResourceResult<()> { + match self { + Self::Disabled => { + let _ = (identity, grant); + Ok(()) + } + #[cfg(test)] + Self::Scripted(provider) => { + provider.identity = Some(identity); + provider.grant = grant; + Ok(()) + } + } + } + pub(super) fn matches( + &self, + identity: NativeProviderIdentity, + grant: ResourceHandleId, + ) -> bool { + match self { + Self::Disabled => { + let _ = (identity, grant); + false + } + #[cfg(test)] + Self::Scripted(provider) => { + provider.identity == Some(identity) && provider.grant == Some(grant) + } + } + } + pub(super) fn construct( + &mut self, + label: i64, + ) -> ResourceResult> { + match self { + Self::Disabled => { + let _ = label; + Err(NativeResourceError::WrongGrant) + } + #[cfg(test)] + Self::Scripted(provider) => provider.construct(label), + } + } + pub(super) fn borrow( + &mut self, + payload: &mut NativePayload, + ) -> ResourceResult> { + match self { + Self::Disabled => { + let _ = payload; + Err(NativeResourceError::WrongGrant) + } + #[cfg(test)] + Self::Scripted(provider) => provider.borrow(payload.label), + } + } + #[cfg(test)] + pub(super) fn scripted(script: DecodedScript) -> ResourceResult { + let mut events = Vec::new(); + events + .try_reserve_exact( + script + .operations + .len() + .checked_mul(2) + .ok_or(NativeResourceError::Capacity)?, + ) + .map_err(|_| NativeResourceError::Capacity)?; + Ok(Self::Scripted(ScriptedProvider { + identity: None, + grant: None, + operations: script.operations, + telemetry: Arc::new(Mutex::new(Telemetry { + events, + next_sequence: 1, + })), + attempts: script.attempts, + })) + } + #[cfg(test)] + pub(super) fn attempts(&self) -> usize { + match self { + Self::Disabled => MAX_ATTEMPTS, + Self::Scripted(provider) => provider.attempts, + } + } + #[cfg(test)] + pub(super) fn remaining(&self) -> usize { + match self { + Self::Disabled => 0, + Self::Scripted(provider) => provider.operations.len(), + } + } + #[cfg(test)] + pub(super) fn observe_events(&self, operation: impl FnOnce(&[NativeTestEvent]) -> T) -> T { + match self { + Self::Disabled => operation(&[]), + Self::Scripted(provider) => operation(&lock_unpoisoned(&provider.telemetry).events), + } + } +} + +#[repr(C)] +#[cfg(test)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(in crate::native_abi) struct NativeTestEvent { + pub(in crate::native_abi) sequence: u64, + pub(in crate::native_abi) label: i64, + pub(in crate::native_abi) kind: u32, + pub(in crate::native_abi) reserved: u32, +} +#[cfg(test)] +struct Telemetry { + events: Vec, + next_sequence: u64, +} +#[cfg(test)] +impl Telemetry { + fn event(&mut self, label: i64, kind: u32) { + // Full event capacity and the small script bound were checked at installation. + self.events.push(NativeTestEvent { + sequence: self.next_sequence, + label, + kind, + reserved: 0, + }); + self.next_sequence += 1; + } +} +#[cfg(test)] +pub(super) struct ScriptedProvider { + identity: Option, + grant: Option, + operations: VecDeque, + telemetry: Arc>, + attempts: usize, +} +#[cfg(test)] +enum ScriptOperation { + ConstructOk(i64), + ConstructFail(i64, String), + ConstructFinalizerPanic(i64), + BorrowOk(i64, i64), + BorrowFail(i64, String), + BorrowPanic(i64), +} +#[cfg(test)] +impl ScriptedProvider { + fn construct(&mut self, label: i64) -> ResourceResult> { + match self.operations.front() { + Some( + ScriptOperation::ConstructOk(expected) + | ScriptOperation::ConstructFail(expected, _) + | ScriptOperation::ConstructFinalizerPanic(expected), + ) if *expected == label => {} + _ => return Err(NativeResourceError::WrongOperation), + } + let row = self + .operations + .pop_front() + .ok_or(NativeResourceError::WrongOperation)?; + let panic_on_drop = matches!(&row, ScriptOperation::ConstructFinalizerPanic(_)); + match row { + ScriptOperation::ConstructFail(_, error) => { + lock_unpoisoned(&self.telemetry).event(label, 2); + Ok(Err(error)) + } + ScriptOperation::ConstructOk(_) | ScriptOperation::ConstructFinalizerPanic(_) => { + lock_unpoisoned(&self.telemetry).event(label, 1); + let telemetry = self.telemetry.clone(); + Ok(Ok(ConstructedPayload { + payload: NativePayload { label }, + finalizer: Box::new(move |payload| { + lock_unpoisoned(&telemetry).event(payload.label, 5); + if panic_on_drop { + panic!("scripted native Resource finalizer panic"); + } + }), + })) + } + _ => Err(NativeResourceError::WrongOperation), + } + } + fn borrow(&mut self, label: i64) -> ResourceResult> { + match self.operations.front() { + Some( + ScriptOperation::BorrowOk(expected, _) + | ScriptOperation::BorrowFail(expected, _) + | ScriptOperation::BorrowPanic(expected), + ) if *expected == label => {} + _ => return Err(NativeResourceError::WrongOperation), + } + let kind = if matches!( + self.operations.front(), + Some(ScriptOperation::BorrowFail(..)) + ) { + 4 + } else { + 3 + }; + // The reference records Borrowed before popping and panicking. + lock_unpoisoned(&self.telemetry).event(label, kind); + match self + .operations + .pop_front() + .ok_or(NativeResourceError::WrongOperation)? + { + ScriptOperation::BorrowOk(_, result) => Ok(Ok(result)), + ScriptOperation::BorrowFail(_, error) => Ok(Err(error)), + ScriptOperation::BorrowPanic(_) => panic!("scripted native Resource borrow panic"), + _ => Err(NativeResourceError::WrongOperation), + } + } +} + +#[cfg(test)] +pub(in crate::native_abi) struct DecodedScript { + attempts: usize, + operations: VecDeque, +} +#[cfg(test)] +impl DecodedScript { + pub(in crate::native_abi) fn decode(bytes: &[u8]) -> ResourceResult { + if bytes.len() > 1024 * 1024 { + return Err(NativeResourceError::Capacity); + } + let mut reader = ScriptReader { bytes, offset: 0 }; + if reader.bytes(8)? != b"JTRST001" + || reader.u32()? != 1 + || reader.u32()? != 0 + || reader.u64()? != bytes.len() as u64 + { + return Err(NativeResourceError::WrongOperation); + } + let attempts = reader.u32()? as usize; + let count = reader.u32()? as usize; + if !(1..=MAX_ATTEMPTS).contains(&attempts) || count > 4096 { + return Err(NativeResourceError::Capacity); + } + let mut operations = VecDeque::new(); + operations + .try_reserve(count) + .map_err(|_| NativeResourceError::Capacity)?; + for _ in 0..count { + let tag = reader.u32()?; + let label = reader.u64()? as i64; + operations.push_back(match tag { + 1 => ScriptOperation::ConstructOk(label), + 2 => ScriptOperation::ConstructFail(label, reader.string()?), + 3 => ScriptOperation::ConstructFinalizerPanic(label), + 4 => ScriptOperation::BorrowOk(label, reader.u64()? as i64), + 5 => ScriptOperation::BorrowFail(label, reader.string()?), + 6 => ScriptOperation::BorrowPanic(label), + _ => return Err(NativeResourceError::WrongOperation), + }); + } + if reader.offset != bytes.len() { + return Err(NativeResourceError::WrongOperation); + } + Ok(Self { + attempts, + operations, + }) + } +} +#[cfg(test)] +struct ScriptReader<'a> { + bytes: &'a [u8], + offset: usize, +} +#[cfg(test)] +impl<'a> ScriptReader<'a> { + fn bytes(&mut self, count: usize) -> ResourceResult<&'a [u8]> { + let end = self + .offset + .checked_add(count) + .ok_or(NativeResourceError::Capacity)?; + let bytes = self + .bytes + .get(self.offset..end) + .ok_or(NativeResourceError::WrongOperation)?; + self.offset = end; + Ok(bytes) + } + fn u32(&mut self) -> ResourceResult { + let bytes: [u8; 4] = self + .bytes(4)? + .try_into() + .map_err(|_| NativeResourceError::WrongOperation)?; + Ok(u32::from_le_bytes(bytes)) + } + fn u64(&mut self) -> ResourceResult { + let bytes: [u8; 8] = self + .bytes(8)? + .try_into() + .map_err(|_| NativeResourceError::WrongOperation)?; + Ok(u64::from_le_bytes(bytes)) + } + fn string(&mut self) -> ResourceResult { + let length = self.u32()? as usize; + if length > 64 * 1024 { + return Err(NativeResourceError::Capacity); + } + let text = std::str::from_utf8(self.bytes(length)?) + .map_err(|_| NativeResourceError::WrongOperation)?; + let mut owned = String::new(); + owned + .try_reserve_exact(text.len()) + .map_err(|_| NativeResourceError::Capacity)?; + owned.push_str(text); + Ok(owned) + } +} diff --git a/crates/jett_runtime/src/native_abi/resource/source.rs b/crates/jett_runtime/src/native_abi/resource/source.rs new file mode 100644 index 00000000..c75c36f4 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/source.rs @@ -0,0 +1,1634 @@ +//! Active Source handoffs. Immutable rows describe joins; only live core tokens own values. +use super::*; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(super) enum SourcePhase { + Prepared, + Entered, + ScopeCompleted, + Completed, + Aborted, +} +#[derive(Clone, Copy)] +struct SourceCompletion { + scope: ResourceHandleId, + return_frame: Option, + original_body_status: u32, + failure: Option, +} + +pub(super) struct NativeSourceCall { + pub(super) frame: ResourceHandleId, + operation: u32, + actuals: Vec>, + parameters: Vec, + next_source: usize, + pub(super) scope: Option, + return_frame: Option, + destination: Option, + body_status: u32, + pub(super) phase: SourcePhase, + completion: Option, +} + +impl NativeResourceState { + pub(super) fn source_call( + &self, + handle: ResourceHandleId, + ) -> ResourceResult<&NativeSourceCall> { + match self.handles.get(&handle) { + Some(NativeResourceEntry::SourceCall(call)) => Ok(call), + _ => Err(NativeResourceError::WrongFamily), + } + } + fn source_row( + &self, + operation: u32, + ) -> ResourceResult<(u32, u32, u32, NativeSourceInvocation)> { + match self.operation(operation)? { + NativeOperation::InvokeSourceFunction { + callee, + signature, + callee_scope, + source: Some(source), + .. + } if matches!(self.layout.wire_version(), 2 | 3) => { + Ok((*callee, *signature, *callee_scope, source.clone())) + } + _ => Err(NativeResourceError::UnsupportedSourceBoundary), + } + } + fn call_for_scope(&self, scope: ResourceHandleId) -> ResourceResult { + let mut found = None; + for (&id, entry) in &self.handles { + if let NativeResourceEntry::SourceCall(call) = entry { + if call.scope == Some(scope) + && matches!( + call.phase, + SourcePhase::Entered | SourcePhase::ScopeCompleted + ) + { + if found.replace(id).is_some() { + return Err(NativeResourceError::WrongFrame); + } + } + } + } + found.ok_or(NativeResourceError::WrongFrame) + } + /// Resolve within this activation only. A recursive caller with the same + /// template/function is separated by the current Scope boundary. + pub(super) fn activation_frame( + &self, + start: ResourceHandleId, + template: u32, + ) -> ResourceResult { + let function = self.layout.frames()[self.frame(start)?.template as usize] + .site() + .function(); + let mut current = start; + loop { + let header = self.frame(current)?; + let row = &self.layout.frames()[header.template as usize]; + if row.site().function() != function { + return Err(NativeResourceError::WrongFrame); + } + if header.template == template { + return Ok(current); + } + let parent = header.parent.ok_or(NativeResourceError::WrongFrame)?; + if row.role() == NativeFrameRole::Scope { + let parent_header = self.frame(parent)?; + let parent_row = &self.layout.frames()[parent_header.template as usize]; + if parent_row.role() == NativeFrameRole::Return + && parent_row.site().function() == function + && parent_header.template == template + { + return Ok(parent); + } + return Err(NativeResourceError::WrongFrame); + } + current = parent; + } + } + pub(super) fn validate_resident( + &self, + loan: ResourceHandleId, + scope: u32, + parameter: u32, + ) -> ResourceResult<()> { + let start = *self + .active_frames + .last() + .ok_or(NativeResourceError::WrongFrame)?; + let scope_handle = self.activation_frame(start, scope)?; + self.resident_at(scope_handle, loan, parameter) + } + fn resident_at( + &self, + scope: ResourceHandleId, + loan: ResourceHandleId, + parameter: u32, + ) -> ResourceResult<()> { + let header = self.frame(scope)?; + let resident = header + .incoming + .get(parameter as usize) + .and_then(Option::as_ref) + .ok_or(NativeResourceError::WrongOperation)?; + if resident.sum || resident.parent_loan != loan || resident.callee_parameter != parameter { + return Err(NativeResourceError::WrongOperation); + } + self.frame(resident.caller_frame)?; + let call = self.source_call(self.call_for_scope(scope)?)?; + if call.operation != resident.invoke_operation + || call.frame != resident.caller_frame + || call.phase != SourcePhase::Entered + { + return Err(NativeResourceError::WrongFrame); + } + match self.handles.get(&loan) { + Some(NativeResourceEntry::Loan(value)) => { + self.frame(value.frame)?; + self.frame(value.source_frame)?; + Ok(()) + } + _ => Err(NativeResourceError::WrongFamily), + } + } + pub(super) fn resident_sum_at( + &self, + scope: ResourceHandleId, + loan: ResourceHandleId, + parameter: u32, + ) -> ResourceResult<()> { + let header = self.frame(scope)?; + let resident = header + .incoming + .get(parameter as usize) + .and_then(Option::as_ref) + .ok_or(NativeResourceError::WrongOperation)?; + if !resident.sum || resident.parent_loan != loan || resident.callee_parameter != parameter { + return Err(NativeResourceError::WrongOperation); + } + self.frame(resident.caller_frame)?; + let call = self.source_call(self.call_for_scope(scope)?)?; + if call.operation != resident.invoke_operation + || call.frame != resident.caller_frame + || call.phase != SourcePhase::Entered + { + return Err(NativeResourceError::WrongFrame); + } + let (_, _, _, row) = self.source_row(call.operation)?; + if !matches!( + row.formals + .get(parameter as usize) + .map(|formal| formal.value), + Some(NativeSourceValue::ResidentSumView { .. }) + ) || !matches!( + self.handles.get(&loan), + Some(NativeResourceEntry::SumLoan(_)) + ) { + return Err(NativeResourceError::WrongFamily); + } + Ok(()) + } + /// Carrier views have a private child lease. The incoming formal header + /// retains its caller lease, and the child is bound to this exact call. + pub(super) fn resident_carrier_at( + &self, + ordinary: &values::NativeValues, + scope: ResourceHandleId, + loan: ResourceHandleId, + parameter: u32, + ) -> ResourceResult<()> { + let header = self.frame(scope)?; + let resident = header + .incoming + .get(parameter as usize) + .and_then(Option::as_ref) + .ok_or(NativeResourceError::WrongOperation)?; + let call_handle = self.call_for_scope(scope)?; + let call = self.source_call(call_handle)?; + if resident.sum + || resident.callee_parameter != parameter + || call.operation != resident.invoke_operation + || call.frame != resident.caller_frame + || call.phase != SourcePhase::Entered + || call.parameters.get(parameter as usize) != Some(&loan.raw()) + || call.actuals.get(parameter as usize).copied().flatten() + != Some(resident.parent_loan.raw()) + { + return Err(NativeResourceError::WrongOperation); + } + self.frame(resident.caller_frame)?; + let (_, _, template, row) = self.source_row(call.operation)?; + let formal = row + .formals + .get(parameter as usize) + .ok_or(NativeResourceError::WrongOperation)?; + let NativeSourceValue::CarrierView { source } = formal.value else { + return Err(NativeResourceError::WrongFamily); + }; + let parent = self.carrier_loan( + ordinary, + resident.caller_frame, + resident.parent_loan, + source, + )?; + let Some(NativeResourceEntry::CarrierLoan(child)) = self.handles.get(&loan) else { + return Err(NativeResourceError::WrongFamily); + }; + if child.frame != scope + || child.incoming != Some((template, parameter, call_handle)) + || child.parent != Some(resident.parent_loan) + || child.root != parent.root + || child.generation != parent.generation + || child.path != parent.path + { + return Err(NativeResourceError::WrongOperation); + } + self.source_carrier_view( + ordinary, + scope, + loan, + crate::resource_custody::NativeCarrierLoanSource::IncomingViewFormal { + scope: template, + parameter, + }, + formal.callee_shape, + ) + } + fn source_carrier_owned( + &self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + handle: ResourceHandleId, + slot: u32, + shape: u32, + ) -> ResourceResult<()> { + let root = self.carrier_root(ordinary, frame, handle, slot)?; + if root.frame != frame + || !matches!(self.layout.shapes().get(shape as usize), + Some(NativeShape::Carrier { node }) if *node == root.tree.node) + { + return Err(NativeResourceError::WrongFamily); + } + self.validate_complete_tree(ordinary, &root.tree) + } + fn source_carrier_view( + &self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + handle: ResourceHandleId, + source: crate::resource_custody::NativeCarrierLoanSource, + shape: u32, + ) -> ResourceResult<()> { + let loan = self.carrier_loan(ordinary, frame, handle, source)?; + let Some(NativeResourceEntry::CarrierRoot(root)) = self.handles.get(&loan.root) else { + return Err(NativeResourceError::InvalidHandle); + }; + let tree = carriers::selected(&root.tree, &loan.path)?; + if !matches!(self.layout.shapes().get(shape as usize), + Some(NativeShape::Carrier { node }) if *node == tree.node) + { + return Err(NativeResourceError::WrongFamily); + } + self.validate_complete_tree(ordinary, tree) + } + fn source_loan( + &self, + frame: ResourceHandleId, + handle: ResourceHandleId, + source: NativeLoanSource, + registry: &ResourceRegistry, + ) -> ResourceResult<()> { + match source { + NativeLoanSource::ProjectedSumPayload { .. } => { + let loan = self.exact_loan(handle, source)?; + self.custody.validate_borrowed(&loan.token, registry)?; + let parent = loan.parent_sum.ok_or(NativeResourceError::WrongFamily)?; + self.validate_sum_parent(parent, registry)?; + let mut ancestor = frame; + while ancestor != loan.frame { + ancestor = self + .frame(ancestor)? + .parent + .ok_or(NativeResourceError::WrongFrame)?; + } + } + NativeLoanSource::ExistingBorrow { operation } => { + let loan = self.exact_loan(handle, source)?; + let mut ancestor = frame; + loop { + if ancestor == loan.frame { + break; + } + ancestor = self + .frame(ancestor)? + .parent + .ok_or(NativeResourceError::WrongFrame)?; + } + if self.layout.operations()[operation as usize] + .site() + .function() + != self.layout.frames()[self.frame(frame)?.template as usize] + .site() + .function() + { + return Err(NativeResourceError::WrongOperation); + } + self.custody.validate_borrowed(&loan.token, registry)?; + } + NativeLoanSource::IncomingViewFormal { scope, parameter } => { + let scope = self.activation_frame(frame, scope)?; + self.resident_at(scope, handle, parameter)?; + let Some(NativeResourceEntry::Loan(loan)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + self.custody.validate_borrowed(&loan.token, registry)?; + } + } + Ok(()) + } + fn ordinary_actual( + &self, + ordinary: &values::NativeValues, + value: u64, + shape: u32, + ) -> ResourceResult<()> { + if let Some(NativeShape::HookDescriptor { hook }) = self.layout.shapes().get(shape as usize) + { + let signature = self.layout.hooks()[*hook as usize].signature(); + return match self.handles.get(&ResourceHandleId::new(value)?) { + Some(NativeResourceEntry::Descriptor { + hook: actual, + signature: actual_signature, + }) if actual == hook && *actual_signature == signature => Ok(()), + _ => Err(NativeResourceError::WrongFamily), + }; + } + ordinary + .validate_resource_ordinary(value, &self.layout, shape) + .map_err(ordinary_error) + } + pub(super) fn carrier( + &self, + handle: ResourceHandleId, + slot: u32, + registry: &ResourceRegistry, + ) -> ResourceResult<()> { + if self.carrier_adapters.contains_key(&handle) { + return Err(NativeResourceError::WrongFamily); + } + match self.handles.get(&handle) { + Some(NativeResourceEntry::Owner(owner)) if owner.slot == slot => { + self.frame(owner.frame)?; + if !matches!( + self.layout.shapes()[self.layout.slots()[slot as usize].shape() as usize], + NativeShape::Resource { .. } + ) { + return Err(NativeResourceError::WrongFamily); + } + self.custody.validate_owned(&owner.token, registry)?; + } + Some(NativeResourceEntry::Sum(sum)) + if sum.slot == slot && sum.shape == self.layout.slots()[slot as usize].shape() => + { + self.frame(sum.frame)?; + match &sum.payload { + NativeSumPayload::Some(token) | NativeSumPayload::Ok(token) => { + self.custody.validate_owned(token, registry)?; + } + NativeSumPayload::None | NativeSumPayload::Fail { .. } => {} + } + } + _ => return Err(NativeResourceError::WrongFamily), + } + Ok(()) + } + pub(super) fn carrier_frame_at( + &self, + current: ResourceHandleId, + handle: ResourceHandleId, + slot: u32, + ) -> ResourceResult<()> { + let actual = match self.handles.get(&handle) { + Some(NativeResourceEntry::Owner(owner)) if owner.slot == slot => owner.frame, + Some(NativeResourceEntry::Sum(sum)) if sum.slot == slot => sum.frame, + _ => return Err(NativeResourceError::WrongFamily), + }; + let expected = + self.activation_frame(current, self.layout.slots()[slot as usize].frame())?; + if actual != expected { + return Err(NativeResourceError::WrongFrame); + } + Ok(()) + } + pub(super) fn slot_empty(&self, frame: ResourceHandleId, slot: u32) -> ResourceResult<()> { + self.slot_frame(slot, frame)?; + if self.handles.values().any(|entry| match entry { + NativeResourceEntry::Owner(owner) => owner.frame == frame && owner.slot == slot, + NativeResourceEntry::Sum(sum) => sum.frame == frame && sum.slot == slot, + NativeResourceEntry::Prepared(value) => { + value.frame == frame && value.destination == slot + } + _ => false, + }) { + return Err(NativeResourceError::WrongOperation); + } + Ok(()) + } + /// This private join is used only after an exact operation or Source boundary + /// has selected both slots. It never turns an empty shell into an owner. + pub(super) fn move_carrier( + &mut self, + registry: &ResourceRegistry, + handle: ResourceHandleId, + source: u32, + destination: u32, + frame: ResourceHandleId, + ) -> ResourceResult { + let mut ids = self.reserve(1)?; + let output = next_handle(&mut ids)?; + self.move_carrier_reserved(registry, handle, source, destination, frame, output) + } + fn move_carrier_reserved( + &mut self, + registry: &ResourceRegistry, + handle: ResourceHandleId, + source: u32, + destination: u32, + frame: ResourceHandleId, + output: ResourceHandleId, + ) -> ResourceResult { + self.carrier(handle, source, registry)?; + self.sum_unborrowed(handle)?; + self.slot_empty(frame, destination)?; + let source_row = &self.layout.slots()[source as usize]; + let destination_row = &self.layout.slots()[destination as usize]; + if source_row.shape() != destination_row.shape() + || source_row.path() != destination_row.path() + { + return Err(NativeResourceError::WrongOperation); + } + let holder = self + .custody + .holder(&self.frame(frame)?.token, &self.layout.slot(destination)?)?; + let mut entry = self + .handles + .remove(&handle) + .ok_or(NativeResourceError::InvalidHandle)?; + let outcome = match &mut entry { + NativeResourceEntry::Owner(owner) => { + self.custody.transfer(&mut owner.token, &holder, registry) + } + NativeResourceEntry::Sum(sum) => match &mut sum.payload { + NativeSumPayload::Some(token) | NativeSumPayload::Ok(token) => { + self.custody.transfer(token, &holder, registry) + } + NativeSumPayload::None | NativeSumPayload::Fail { .. } => Ok(()), + }, + _ => { + self.handles.insert(handle, entry); + return Err(NativeResourceError::WrongFamily); + } + }; + if let Err(error) = outcome { + self.handles.insert(handle, entry); + return Err(error.into()); + } + match &mut entry { + NativeResourceEntry::Owner(owner) => { + owner.frame = frame; + owner.slot = destination; + } + NativeResourceEntry::Sum(sum) => { + sum.frame = frame; + sum.slot = destination; + } + _ => unreachable!(), + } + self.handles.insert(output, entry); + Ok(output) + } + pub(super) fn destination_frame( + &self, + frame: ResourceHandleId, + operation: u32, + ) -> ResourceResult { + self.operation_frame(operation, frame)?; + let destination = match self.operation(operation)? { + NativeOperation::Acquire { destination, .. } + | NativeOperation::Transfer { destination, .. } + | NativeOperation::SumAdopt { destination, .. } + | NativeOperation::SumTake { destination, .. } => *destination, + _ => return Err(NativeResourceError::WrongOperation), + }; + self.activation_frame(frame, self.layout.slots()[destination as usize].frame()) + } + pub(super) fn scope_validate( + &self, + ordinary: &values::NativeValues, + scope: ResourceHandleId, + function: u32, + signature: u32, + template: u32, + ) -> ResourceResult<()> { + self.running(ordinary)?; + let header = self.frame(scope)?; + let row = &self.layout.frames()[header.template as usize]; + if self.active_frames.last() != Some(&scope) + || header.template != template + || row.role() != NativeFrameRole::Scope + || row.site().function() != function + || row.signature() != signature + { + return Err(NativeResourceError::WrongFrame); + } + if header.parent.is_none() { + if self.attempt.as_ref().map(|a| a.root_frame) != Some(scope) { + return Err(NativeResourceError::WrongFrame); + } + } else { + let call = self.source_call(self.call_for_scope(scope)?)?; + let (callee, sig, target, _) = self.source_row(call.operation)?; + if call.phase != SourcePhase::Entered + || callee != function + || sig != signature + || target != template + { + return Err(NativeResourceError::WrongFrame); + } + } + Ok(()) + } + pub(super) fn source_prepare( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + operation: u32, + ) -> ResourceResult { + self.running(ordinary)?; + self.operation_frame(operation, frame)?; + let (_, _, _, row) = self.source_row(operation)?; + let destination = match row.result { + NativeSourceResult::Owned { + caller_destination_frame, + caller_destination_slot, + .. + } => { + let destination = self.activation_frame(frame, caller_destination_frame)?; + self.slot_empty(destination, caller_destination_slot)?; + Some(destination) + } + NativeSourceResult::Ordinary { .. } => None, + NativeSourceResult::Carrier { + caller_destination_frame, + caller_destination_slot, + .. + } => { + let destination = self.activation_frame(frame, caller_destination_frame)?; + self.carrier_slot_empty(destination, caller_destination_slot)?; + Some(destination) + } + }; + let mut actuals = Vec::new(); + let mut parameters = Vec::new(); + actuals + .try_reserve_exact(row.formals.len()) + .map_err(|_| NativeResourceError::Capacity)?; + parameters + .try_reserve_exact(row.formals.len()) + .map_err(|_| NativeResourceError::Capacity)?; + actuals.resize(row.formals.len(), None); + parameters.resize(row.formals.len(), 0); + let mut ids = self.reserve(1)?; + let handle = next_handle(&mut ids)?; + self.handles.insert( + handle, + NativeResourceEntry::SourceCall(NativeSourceCall { + frame, + operation, + actuals, + parameters, + next_source: 0, + scope: None, + return_frame: None, + destination, + body_status: 0, + phase: SourcePhase::Prepared, + completion: None, + }), + ); + Ok(handle) + } + pub(super) fn source_actual( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + handle: ResourceHandleId, + source_index: u32, + parameter: u32, + value: u64, + ) -> ResourceResult<()> { + self.running(ordinary)?; + let call = self.source_call(handle)?; + self.operation_frame(call.operation, call.frame)?; + let (_, _, _, row) = self.source_row(call.operation)?; + if call.phase != SourcePhase::Prepared + || call.next_source != source_index as usize + || row.evaluation_order.get(source_index as usize) != Some(¶meter) + { + return Err(NativeResourceError::WrongOperation); + } + let formal = row + .formals + .get(parameter as usize) + .ok_or(NativeResourceError::WrongOperation)?; + if formal.source_index != source_index || call.actuals[parameter as usize].is_some() { + return Err(NativeResourceError::WrongOperation); + } + match formal.value { + NativeSourceValue::Ordinary => { + self.ordinary_actual(ordinary, value, formal.actual_shape)? + } + NativeSourceValue::Owned { + caller_argument_slot, + .. + } => { + let id = ResourceHandleId::new(value)?; + self.carrier(id, caller_argument_slot, registry)?; + match self.handles.get(&id) { + Some(NativeResourceEntry::Owner(owner)) if owner.frame == call.frame => {} + Some(NativeResourceEntry::Sum(sum)) if sum.frame == call.frame => {} + _ => return Err(NativeResourceError::WrongFrame), + } + } + NativeSourceValue::ResidentView { source } => { + self.source_loan(call.frame, ResourceHandleId::new(value)?, source, registry)? + } + NativeSourceValue::ResidentSumView { source } => { + self.source_sum_loan( + call.frame, + ResourceHandleId::new(value)?, + source, + Some(formal.actual_shape), + registry, + )?; + } + NativeSourceValue::CarrierOwned { + caller_argument_slot, + .. + } => { + self.source_carrier_owned( + ordinary, + call.frame, + ResourceHandleId::new(value)?, + caller_argument_slot, + formal.actual_shape, + )?; + } + NativeSourceValue::CarrierView { source } => { + self.source_carrier_view( + ordinary, + call.frame, + ResourceHandleId::new(value)?, + source, + formal.actual_shape, + )?; + } + } + let Some(NativeResourceEntry::SourceCall(call)) = self.handles.get_mut(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + call.actuals[parameter as usize] = Some(value); + call.next_source += 1; + Ok(()) + } + fn begin_source_frame( + &mut self, + template: u32, + parent: ResourceHandleId, + role: NativeFrameRole, + handle: ResourceHandleId, + ) -> ResourceResult { + if self.active_frames.last() != Some(&parent) { + return Err(NativeResourceError::WrongFrame); + } + let attempt = self + .attempt + .as_ref() + .ok_or(NativeResourceError::InvalidEntry)?; + let purpose = attempt.purpose; + let attempt_id = attempt.id; + let row = &self.layout.frames()[template as usize]; + if row.role() != role + || !row + .parents() + .contains(&NativeParent::Frame(self.frame(parent)?.template)) + { + return Err(NativeResourceError::WrongFrame); + } + self.active_frames + .try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + let kind = match role { + NativeFrameRole::Scope => ResourceFrameKind::Scope, + NativeFrameRole::Return => ResourceFrameKind::Return, + _ => return Err(NativeResourceError::WrongFrame), + }; + let token = self.custody.begin_frame(kind, purpose)?; + self.handles.insert( + handle, + NativeResourceEntry::Frame(NativeFrameEntry { + template, + attempt: attempt_id, + parent: Some(parent), + token, + incoming: Vec::new(), + }), + ); + self.active_frames.push(handle); + Ok(handle) + } + pub(super) fn source_enter( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + let call = self.source_call(handle)?; + let frame = call.frame; + let operation = call.operation; + self.operation_frame(operation, frame)?; + let (_, _, template, row) = self.source_row(operation)?; + if call.phase != SourcePhase::Prepared + || call.next_source != row.formals.len() + || call.actuals.iter().any(Option::is_none) + { + return Err(NativeResourceError::WrongOperation); + } + // Allocate every adapter-owned carrier before opening a callee or moving a token. + let mut actuals = Vec::new(); + actuals + .try_reserve_exact(call.actuals.len()) + .map_err(|_| NativeResourceError::Capacity)?; + actuals.extend(call.actuals.iter().map(|v| v.unwrap_or(0))); + let mut ids = self.reserve(2 + actuals.len())?; + let reserved_return = next_handle(&mut ids)?; + let reserved_scope = next_handle(&mut ids)?; + let mut outputs = Vec::new(); + outputs + .try_reserve_exact(actuals.len()) + .map_err(|_| NativeResourceError::Capacity)?; + let mut incoming = Vec::new(); + incoming + .try_reserve_exact(actuals.len()) + .map_err(|_| NativeResourceError::Capacity)?; + let mut carrier_views = Vec::new(); + carrier_views + .try_reserve_exact(actuals.len()) + .map_err(|_| NativeResourceError::Capacity)?; + for formal in &row.formals { + outputs.push(next_handle(&mut ids)?); + let staged = if let NativeSourceValue::CarrierView { source } = formal.value { + let actual = ResourceHandleId::new(actuals[formal.parameter as usize])?; + self.source_carrier_view(ordinary, frame, actual, source, formal.actual_shape)?; + let parent = self.carrier_loan(ordinary, frame, actual, source)?; + let mut path = Vec::new(); + path.try_reserve_exact(parent.path.len()) + .map_err(|_| NativeResourceError::Capacity)?; + path.extend_from_slice(&parent.path); + Some(carriers::NativeCarrierLoan { + frame, + operation: parent.operation, + root: parent.root, + generation: parent.generation, + path, + parent: Some(actual), + incoming: Some((template, formal.parameter, handle)), + }) + } else { + None + }; + carrier_views.push(staged); + incoming.push( + if matches!( + formal.value, + NativeSourceValue::ResidentView { .. } + | NativeSourceValue::ResidentSumView { .. } + | NativeSourceValue::CarrierView { .. } + ) { + Some(NativeResidentLoan { + sum: matches!(formal.value, NativeSourceValue::ResidentSumView { .. }), + parent_loan: ResourceHandleId::new(actuals[formal.parameter as usize])?, + caller_frame: frame, + invoke_operation: operation, + callee_parameter: formal.parameter, + }) + } else { + None + }, + ); + } + self.active_frames + .try_reserve(2) + .map_err(|_| NativeResourceError::Capacity)?; + for formal in &row.formals { + let actual = actuals[formal.parameter as usize]; + match formal.value { + NativeSourceValue::Owned { + caller_argument_slot, + .. + } => self.carrier( + ResourceHandleId::new(actual)?, + caller_argument_slot, + registry, + )?, + NativeSourceValue::ResidentView { source } => { + self.source_loan(frame, ResourceHandleId::new(actual)?, source, registry)? + } + NativeSourceValue::ResidentSumView { source } => { + self.source_sum_loan( + frame, + ResourceHandleId::new(actual)?, + source, + Some(formal.actual_shape), + registry, + )?; + } + NativeSourceValue::Ordinary => { + self.ordinary_actual(ordinary, actual, formal.actual_shape)? + } + NativeSourceValue::CarrierOwned { + caller_argument_slot, + .. + } => { + self.source_carrier_owned( + ordinary, + frame, + ResourceHandleId::new(actual)?, + caller_argument_slot, + formal.actual_shape, + )?; + } + NativeSourceValue::CarrierView { source } => { + self.source_carrier_view( + ordinary, + frame, + ResourceHandleId::new(actual)?, + source, + formal.actual_shape, + )?; + } + } + } + let return_frame = if let Some(template) = row.callee_return { + Some(self.begin_source_frame( + template, + frame, + NativeFrameRole::Return, + reserved_return, + )?) + } else { + None + }; + let scope_result = self.begin_source_frame( + template, + return_frame.unwrap_or(frame), + NativeFrameRole::Scope, + reserved_scope, + ); + let scope = match scope_result { + Ok(scope) => scope, + Err(error) => { + if let Some(return_frame) = return_frame { + self.retire_frame(ordinary, registry, return_frame)?; + } + return Err(error); + } + }; + if let Some(NativeResourceEntry::SourceCall(call)) = self.handles.get_mut(&handle) { + call.scope = Some(scope); + call.return_frame = return_frame; + call.phase = SourcePhase::Entered; + } + let Some(NativeResourceEntry::Frame(header)) = self.handles.get_mut(&scope) else { + return Err(NativeResourceError::WrongFrame); + }; + header.incoming = incoming; + // All holders are checked empty and all core transfer bookkeeping is reserved + // before the first owner leaves its caller argument slot. + let preflight: ResourceResult<()> = (|| { + let mut slots = Vec::new(); + slots + .try_reserve_exact(row.formals.len()) + .map_err(|_| NativeResourceError::Capacity)?; + let mut token_handles = Vec::new(); + token_handles + .try_reserve_exact(row.formals.len()) + .map_err(|_| NativeResourceError::Capacity)?; + let mut owned_handles = std::collections::HashSet::new(); + owned_handles + .try_reserve(row.formals.len()) + .map_err(|_| NativeResourceError::Capacity)?; + for formal in &row.formals { + if let NativeSourceValue::Owned { + callee_parameter_slot, + .. + } = formal.value + { + self.slot_empty(scope, callee_parameter_slot)?; + let actual = ResourceHandleId::new(actuals[formal.parameter as usize])?; + if !owned_handles.insert(actual) { + return Err(NativeResourceError::WrongOperation); + } + self.sum_unborrowed(actual)?; + match self.handles.get(&actual) { + Some(NativeResourceEntry::Owner(_)) + | Some(NativeResourceEntry::Sum(NativeResourceSum { + payload: NativeSumPayload::Some(_) | NativeSumPayload::Ok(_), + .. + })) => { + token_handles.push(actual); + slots.push(self.layout.slot(callee_parameter_slot)?); + } + _ => {} // None/Fail shells have no owner generation to transfer. + } + } else if let NativeSourceValue::CarrierOwned { + caller_argument_slot, + callee_parameter_slot, + } = formal.value + { + let actual = ResourceHandleId::new(actuals[formal.parameter as usize])?; + if !owned_handles.insert(actual) { + return Err(NativeResourceError::WrongOperation); + } + self.source_carrier_owned( + ordinary, + frame, + actual, + caller_argument_slot, + formal.actual_shape, + )?; + self.carrier_unborrowed(actual)?; + self.carrier_slot_empty(scope, callee_parameter_slot)?; + let destination = self + .layout + .carriers() + .slots + .get(callee_parameter_slot as usize) + .ok_or(NativeResourceError::WrongOperation)?; + if !matches!(self.layout.shapes().get(formal.callee_shape as usize), + Some(NativeShape::Carrier { node }) if *node == destination.node) + { + return Err(NativeResourceError::WrongFamily); + } + } + } + let mut transfers = Vec::new(); + transfers + .try_reserve_exact(slots.len()) + .map_err(|_| NativeResourceError::Capacity)?; + for (index, handle) in token_handles.iter().enumerate() { + let token = match self.handles.get(handle) { + Some(NativeResourceEntry::Owner(owner)) => &owner.token, + Some(NativeResourceEntry::Sum(NativeResourceSum { + payload: NativeSumPayload::Some(token) | NativeSumPayload::Ok(token), + .. + })) => token, + _ => return Err(NativeResourceError::WrongFamily), + }; + transfers.push((token, &slots[index])); + } + let Some(NativeResourceEntry::Frame(header)) = self.handles.get(&scope) else { + return Err(NativeResourceError::WrongFrame); + }; + self.custody + .preflight_transfer_batch(&header.token, &transfers, registry)?; + Ok(()) + })(); + if let Err(error) = preflight { + self.abort_source(ordinary, registry, handle)?; + return Err(error); + } + for formal in &row.formals { + let actual = actuals[formal.parameter as usize]; + let acquired = match formal.value { + NativeSourceValue::Owned { + caller_argument_slot, + callee_parameter_slot, + } => self + .move_carrier_reserved( + registry, + ResourceHandleId::new(actual)?, + caller_argument_slot, + callee_parameter_slot, + scope, + outputs[formal.parameter as usize], + ) + .map(ResourceHandleId::raw), + NativeSourceValue::ResidentView { .. } + | NativeSourceValue::ResidentSumView { .. } => Ok(actual), + NativeSourceValue::Ordinary => Ok(actual), + NativeSourceValue::CarrierOwned { + caller_argument_slot, + callee_parameter_slot, + } => self + .carrier_move_reserved_with_registry( + ordinary, + registry, + ResourceHandleId::new(actual)?, + caller_argument_slot, + callee_parameter_slot, + frame, + scope, + outputs[formal.parameter as usize], + ) + .map(ResourceHandleId::raw), + NativeSourceValue::CarrierView { .. } => carrier_views + .get_mut(formal.parameter as usize) + .and_then(Option::take) + .ok_or(NativeResourceError::WrongOperation) + .map(|mut lease| { + lease.frame = scope; + let output = outputs[formal.parameter as usize]; + self.handles + .insert(output, NativeResourceEntry::CarrierLoan(lease)); + output.raw() + }), + }; + match acquired { + Ok(value) => { + let Some(NativeResourceEntry::SourceCall(call)) = self.handles.get_mut(&handle) + else { + return Err(NativeResourceError::WrongFamily); + }; + call.parameters[formal.parameter as usize] = value; + } + Err(error) => { + self.abort_source(ordinary, registry, handle)?; + return Err(error); + } + } + } + Ok(scope) + } + fn abort_source( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + handle: ResourceHandleId, + ) -> ResourceResult<()> { + let call = self.source_call(handle)?; + let scope = call.scope; + let return_frame = call.return_frame; + let mut first = None; + for frame in [scope, return_frame].into_iter().flatten() { + if let Err(error) = self.retire_frame(ordinary, registry, frame) { + first.get_or_insert(error); + } + } + if let Some(NativeResourceEntry::SourceCall(call)) = self.handles.get_mut(&handle) { + call.phase = SourcePhase::Aborted; + } + first.map_or(Ok(()), Err) + } + pub(super) fn source_parameter( + &self, + ordinary: &values::NativeValues, + scope: ResourceHandleId, + parameter: u32, + ) -> ResourceResult { + self.running(ordinary)?; + self.frame(scope)?; + let call = self.source_call(self.call_for_scope(scope)?)?; + if call.phase != SourcePhase::Entered { + return Err(NativeResourceError::WrongFrame); + } + let value = call + .parameters + .get(parameter as usize) + .copied() + .ok_or(NativeResourceError::WrongOperation)?; + let (_, _, _, row) = self.source_row(call.operation)?; + let formal = row + .formals + .get(parameter as usize) + .ok_or(NativeResourceError::WrongOperation)?; + match formal.value { + NativeSourceValue::Owned { + callee_parameter_slot, + .. + } => match self.handles.get(&ResourceHandleId::new(value)?) { + Some(NativeResourceEntry::Owner(owner)) + if owner.frame == scope && owner.slot == callee_parameter_slot => {} + Some(NativeResourceEntry::Sum(sum)) + if sum.frame == scope && sum.slot == callee_parameter_slot => {} + _ => return Err(NativeResourceError::WrongFamily), + }, + NativeSourceValue::ResidentView { .. } => { + self.resident_at(scope, ResourceHandleId::new(value)?, parameter)? + } + NativeSourceValue::ResidentSumView { .. } => { + self.resident_sum_at(scope, ResourceHandleId::new(value)?, parameter)?; + } + NativeSourceValue::Ordinary => { + self.ordinary_actual(ordinary, value, formal.callee_shape)? + } + NativeSourceValue::CarrierOwned { + callee_parameter_slot, + .. + } => { + self.source_carrier_owned( + ordinary, + scope, + ResourceHandleId::new(value)?, + callee_parameter_slot, + formal.callee_shape, + )?; + } + NativeSourceValue::CarrierView { .. } => { + self.resident_carrier_at( + ordinary, + scope, + ResourceHandleId::new(value)?, + parameter, + )?; + } + } + Ok(value) + } + fn record_source_completion( + &mut self, + handle: ResourceHandleId, + failure: Option, + ) -> ResourceResult<()> { + let call = self.source_call(handle)?; + let scope = call.scope.ok_or(NativeResourceError::WrongFrame)?; + if !matches!(call.phase, SourcePhase::Completed | SourcePhase::Aborted) + || self.handles.contains_key(&scope) + || self.active_frames.contains(&scope) + || call.return_frame.is_some_and(|frame| { + self.handles.contains_key(&frame) || self.active_frames.contains(&frame) + }) + { + // Refused retirement and pending publication carry no completion authority. + return Ok(()); + } + let completion = SourceCompletion { + scope, + return_frame: call.return_frame, + original_body_status: call.body_status, + failure, + }; + let Some(NativeResourceEntry::SourceCall(call)) = self.handles.get_mut(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + call.completion = Some(completion); + Ok(()) + } + + pub(super) fn source_status( + &self, + ordinary: &values::NativeValues, + handle: ResourceHandleId, + ) -> ResourceResult<()> { + let call = self.source_call(handle)?; + self.operation_frame(call.operation, call.frame)?; + self.source_row(call.operation)?; + let completion = call.completion.ok_or(NativeResourceError::BodyFailed)?; + if call.scope != Some(completion.scope) + || call.return_frame != completion.return_frame + || call.body_status != completion.original_body_status + || self.handles.contains_key(&completion.scope) + || self.active_frames.contains(&completion.scope) + || completion.return_frame.is_some_and(|frame| { + self.handles.contains_key(&frame) || self.active_frames.contains(&frame) + }) + { + return Err(NativeResourceError::WrongFrame); + } + match call.phase { + SourcePhase::Aborted => { + Err(completion.failure.ok_or(NativeResourceError::BodyFailed)?) + } + SourcePhase::Completed + if completion.original_body_status == 0 && completion.failure.is_none() => + { + self.running(ordinary)?; + Ok(()) + } + _ => Err(NativeResourceError::BodyFailed), + } + } + + #[cfg(test)] + pub(super) fn source_completed_body_status( + &self, + handle: ResourceHandleId, + ) -> ResourceResult> { + Ok(self + .source_call(handle)? + .completion + .map(|completion| completion.original_body_status)) + } + pub(super) fn scope_complete( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + scope: ResourceHandleId, + operation: u32, + body_status: u32, + ) -> ResourceResult<()> { + self.operation_frame(operation, scope)?; + if !matches!(self.operation(operation)?, NativeOperation::Complete { .. }) + || self.layout.frames()[self.frame(scope)?.template as usize].role() + != NativeFrameRole::Scope + { + return Err(NativeResourceError::WrongOperation); + } + let root = self.frame(scope)?.parent.is_none(); + if root { + let attempt = self + .attempt + .as_ref() + .ok_or(NativeResourceError::InvalidEntry)?; + let header = self.frame(scope)?; + let row = &self.layout.frames()[header.template as usize]; + if attempt.phase != AttemptPhase::Running + || attempt.root_frame != scope + || attempt.entry != self.entry + || attempt.root_body_status.is_some() + || header.template != self.entry.scope + || row.site().function() != self.entry.function + || row.signature() != self.entry.signature + { + return Err(NativeResourceError::InvalidEntry); + } + } + let call = if !root { + Some(self.call_for_scope(scope)?) + } else { + None + }; + if let Some(handle) = call { + let Some(NativeResourceEntry::SourceCall(call)) = self.handles.get_mut(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + call.body_status = body_status; + } + let mut cleanup = self.retire_frame(ordinary, registry, scope); + // A refused cleanup can keep its frame live. Retain the original + // status only after this exact root has actually retired, even if its + // finalizer failed. Do not substitute the selected cleanup/body error. + if root && !self.handles.contains_key(&scope) && !self.active_frames.contains(&scope) { + let attempt = self + .attempt + .as_mut() + .ok_or(NativeResourceError::InvalidEntry)?; + attempt.root_body_status = Some(body_status); + } + let body = if body_status != 0 { + Err(NativeResourceError::BodyFailed) + } else { + self.running(ordinary).map(|_| ()) + }; + if let Some(handle) = call { + let return_frame = self.source_call(handle)?.return_frame; + if cleanup.is_err() || body.is_err() { + if let Some(frame) = return_frame { + if let Err(error) = self.retire_frame(ordinary, registry, frame) { + if cleanup.is_ok() { + cleanup = Err(error); + } + } + } + if let Some(NativeResourceEntry::SourceCall(call)) = self.handles.get_mut(&handle) { + call.phase = SourcePhase::Aborted; + } + } else if let Some(NativeResourceEntry::SourceCall(call)) = + self.handles.get_mut(&handle) + { + call.phase = if return_frame.is_some() { + SourcePhase::ScopeCompleted + } else { + SourcePhase::Completed + }; + } + } + if let Some(handle) = call { + // Preserve the original nonzero body independently of selected cleanup. + // A clean body whose real Scope cleanup failed must still stop its caller. + let failure = if body_status != 0 { + Some(NativeResourceError::SourceBodyStatus(JettRuntimeStatusV1( + body_status, + ))) + } else { + cleanup + .as_ref() + .err() + .copied() + .or_else(|| body.as_ref().err().copied()) + }; + self.record_source_completion(handle, failure)?; + } + cleanup?; + body + } + pub(super) fn return_publish( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + scope: ResourceHandleId, + operation: u32, + provisional: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + let handle = self.call_for_scope(scope)?; + let call = self.source_call(handle)?; + if call.phase != SourcePhase::ScopeCompleted || call.body_status != 0 { + return Err(NativeResourceError::WrongFrame); + } + let (_, _, template, row) = self.source_row(call.operation)?; + let source = match self.operation(operation)? { + NativeOperation::PublishReturn { + frame, + source_return_slot, + } if *frame == template => *source_return_slot, + _ => return Err(NativeResourceError::WrongOperation), + }; + let NativeSourceResult::Owned { + caller_destination_slot, + permitted_return_slots, + .. + } = row.result + else { + return Err(NativeResourceError::WrongOperation); + }; + if !permitted_return_slots.contains(&source) { + return Err(NativeResourceError::WrongOperation); + } + let return_frame = call.return_frame.ok_or(NativeResourceError::WrongFrame)?; + let destination = call.destination.ok_or(NativeResourceError::WrongFrame)?; + if self.active_frames.last() != Some(&return_frame) { + return Err(NativeResourceError::WrongFrame); + } + self.carrier(provisional, source, registry)?; + self.carrier_frame_at(return_frame, provisional, source)?; + if self.handles.iter().any(|(&id, entry)| { + id != provisional + && match entry { + NativeResourceEntry::Owner(owner) => owner.frame == return_frame, + NativeResourceEntry::Sum(sum) => sum.frame == return_frame, + NativeResourceEntry::Loan(loan) => loan.frame == return_frame, + NativeResourceEntry::SumLoan(loan) => loan.frame == return_frame, + NativeResourceEntry::Prepared(prepared) => prepared.frame == return_frame, + NativeResourceEntry::Call(call) => call.frame == return_frame, + NativeResourceEntry::SourceCall(call) => call.frame == return_frame, + NativeResourceEntry::CarrierRoot(root) => root.frame == return_frame, + NativeResourceEntry::CarrierLoan(loan) => loan.frame == return_frame, + NativeResourceEntry::CarrierBuilder(builder) => builder.frame == return_frame, + _ => false, + } + }) { + return Err(NativeResourceError::WrongOperation); + } + let output = self.move_carrier( + registry, + provisional, + source, + caller_destination_slot, + destination, + )?; + self.retire_frame(ordinary, registry, return_frame)?; + let Some(NativeResourceEntry::SourceCall(call)) = self.handles.get_mut(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + call.phase = SourcePhase::Completed; + self.record_source_completion(handle, None)?; + Ok(output) + } + pub(super) fn carrier_publish( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + scope: ResourceHandleId, + operation: u32, + provisional: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + if self.layout.wire_version() != 3 { + return Err(NativeResourceError::UnsupportedSourceBoundary); + } + let handle = self.call_for_scope(scope)?; + let call = self.source_call(handle)?; + if call.phase != SourcePhase::ScopeCompleted + || call.body_status != 0 + || self.handles.contains_key(&scope) + || self.active_frames.contains(&scope) + { + return Err(NativeResourceError::WrongFrame); + } + let (callee, signature, template, row) = self.source_row(call.operation)?; + let NativeOperation::Carrier { record } = self.operation(operation)? else { + return Err(NativeResourceError::WrongOperation); + }; + let carrier_record = self + .layout + .carriers() + .operations + .get(*record as usize) + .ok_or(NativeResourceError::WrongOperation)?; + let crate::resource_custody::NativeCarrierOperation::PublishReturn { frame, source } = + &carrier_record.operation + else { + return Err(NativeResourceError::WrongOperation); + }; + let (frame, source) = (*frame, *source); + if frame != template + || carrier_record.site != self.layout.operations()[operation as usize].site() + || carrier_record.site.function() != callee + { + return Err(NativeResourceError::WrongOperation); + } + let NativeSourceResult::Carrier { + shape, + caller_destination_frame, + caller_destination_slot, + callee_return_frame, + permitted_return_slots, + } = row.result + else { + return Err(NativeResourceError::WrongOperation); + }; + if !permitted_return_slots.contains(&source) { + return Err(NativeResourceError::WrongOperation); + } + let return_frame = call.return_frame.ok_or(NativeResourceError::WrongFrame)?; + let destination = call.destination.ok_or(NativeResourceError::WrongFrame)?; + let caller_frame = call.frame; + let return_header = self.frame(return_frame)?; + let return_row = &self.layout.frames()[return_header.template as usize]; + if self.active_frames.last() != Some(&return_frame) + || return_header.template != callee_return_frame + || return_header.parent != Some(caller_frame) + || return_row.role() != NativeFrameRole::Return + || return_row.site().function() != callee + || return_row.signature() != signature + || row.callee_return != Some(callee_return_frame) + || self.activation_frame(caller_frame, caller_destination_frame)? != destination + { + return Err(NativeResourceError::WrongFrame); + } + self.source_carrier_owned(ordinary, return_frame, provisional, source, shape)?; + self.carrier_unborrowed(provisional)?; + self.carrier_slot_empty(destination, caller_destination_slot)?; + if self.handles.iter().any(|(&id, entry)| { + id != provisional + && match entry { + NativeResourceEntry::Owner(value) => value.frame == return_frame, + NativeResourceEntry::Sum(value) => value.frame == return_frame, + NativeResourceEntry::Loan(value) => value.frame == return_frame, + NativeResourceEntry::SumLoan(value) => value.frame == return_frame, + NativeResourceEntry::Prepared(value) => value.frame == return_frame, + NativeResourceEntry::Call(value) => value.frame == return_frame, + NativeResourceEntry::SourceCall(value) => value.frame == return_frame, + NativeResourceEntry::CarrierRoot(value) => value.frame == return_frame, + NativeResourceEntry::CarrierLoan(value) => value.frame == return_frame, + NativeResourceEntry::CarrierBuilder(value) => value.frame == return_frame, + _ => false, + } + }) { + return Err(NativeResourceError::WrongOperation); + } + // Publication remains provisional until the exact Return frame retires. + let mut ids = self.reserve(1)?; + let output = next_handle(&mut ids)?; + let output = self.carrier_move_reserved_with_registry( + ordinary, + registry, + provisional, + source, + caller_destination_slot, + return_frame, + destination, + output, + )?; + self.retire_frame(ordinary, registry, return_frame)?; + let Some(NativeResourceEntry::SourceCall(call)) = self.handles.get_mut(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + call.phase = SourcePhase::Completed; + self.record_source_completion(handle, None)?; + Ok(output) + } + pub(super) fn absent_sum( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + operation: u32, + ) -> ResourceResult { + self.running(ordinary)?; + self.operation_frame(operation, frame)?; + let slot = match self.operation(operation)? { + NativeOperation::CreateAbsentSum { + destination_slot, .. + } => *destination_slot, + _ => return Err(NativeResourceError::WrongOperation), + }; + self.slot_empty(frame, slot)?; + let mut ids = self.reserve(1)?; + let output = next_handle(&mut ids)?; + self.handles.insert( + output, + NativeResourceEntry::Sum(NativeResourceSum { + frame, + slot, + shape: self.layout.slots()[slot as usize].shape(), + payload: NativeSumPayload::None, + }), + ); + Ok(output) + } + pub(super) fn failure_sum( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + operation: u32, + companion: u64, + ) -> ResourceResult { + self.running(ordinary)?; + self.operation_frame(operation, frame)?; + let (slot, shape) = match self.operation(operation)? { + NativeOperation::CreateFailureSum { + destination_slot, + failure_shape, + .. + } => (*destination_slot, *failure_shape), + _ => return Err(NativeResourceError::WrongOperation), + }; + ordinary + .validate_resource_ordinary(companion, &self.layout, shape) + .map_err(ordinary_error)?; + self.slot_empty(frame, slot)?; + let mut ids = self.reserve(1)?; + let output = next_handle(&mut ids)?; + self.handles.insert( + output, + NativeResourceEntry::Sum(NativeResourceSum { + frame, + slot, + shape: self.layout.slots()[slot as usize].shape(), + payload: NativeSumPayload::Fail { + shape, + string: companion, + }, + }), + ); + Ok(output) + } + pub(super) fn failure_companion_take( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.operation_frame(operation, frame)?; + let (source, shape) = match self.operation(operation)? { + NativeOperation::TakeFailureCompanion { + source_sum_slot, + failure_shape, + .. + } => (*source_sum_slot, *failure_shape), + _ => return Err(NativeResourceError::WrongOperation), + }; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if sum.slot != source { + return Err(NativeResourceError::WrongOperation); + } + self.carrier_frame_at(frame, handle, source)?; + self.sum_unborrowed(handle)?; + let NativeSumPayload::Fail { + shape: actual, + string, + } = sum.payload + else { + return Err(NativeResourceError::WrongFamily); + }; + if actual != shape { + return Err(NativeResourceError::WrongOperation); + } + ordinary + .validate_resource_ordinary(string, &self.layout, shape) + .map_err(ordinary_error)?; + self.handles.remove(&handle); + Ok(string) + } +} diff --git a/crates/jett_runtime/src/native_abi/resource/source_tests.rs b/crates/jett_runtime/src/native_abi/resource/source_tests.rs new file mode 100644 index 00000000..5a25e097 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/source_tests.rs @@ -0,0 +1,768 @@ +//! Malformed-host and real core transition controls, distinct from Source-native acceptance. +use super::super::*; +use super::{context, script, words}; + +struct Fixture { + bytes: Vec, + source: u32, + transfer_return: u32, + complete_scope: u32, + publish: Option, + complete_op: u32, + complete_root: u32, + complete_inner: u32, + close: u32, + create: Option, + borrow: Option, + invoke_borrow: Option, + end_borrow: Option, + take_fail: Option, +} +fn fixture(shape: u32, resident: bool, reorder: bool) -> Fixture { + let signatures = vec![ + vec![0, 2, 6, 0, 2, 1, 1], + vec![1, 2, 7, 0, 2, 4, 2], + vec![2, 1, 3, 4, 1], + vec![3, 1, 3, 0, 2], + if resident { + vec![4, 2, 3, 4, 2, 0, 2] + } else { + vec![4, 1, shape, shape, 1] + }, + ]; + let shapes = vec![ + vec![0, 6], + vec![1, 1, 64, 1], + vec![2, 4], + vec![3, 5], + vec![4, 7, 0], + vec![5, 8, 4], + vec![6, 9, 4, 2], + vec![7, 9, 1, 2], + ]; + let parent = if resident { 1 } else { 2 }; + let frames = vec![ + vec![0, 1, 0, 0, 0, 0, 3, 1, 0, 0], + vec![1, 2, 0, 0, 0, 1, 3, 1, 1, 0], + vec![2, 3, 1, 0, 0, 2, 4, 1, 1, 1], + vec![3, 1, 1, 0, 0, 3, 4, 1, 1, parent], + vec![4, 2, 1, 0, 0, 4, 4, 1, 1, 3], + ]; + let path = if shape == 5 { + vec![1, 1] + } else if shape == 6 { + vec![1, 2] + } else { + vec![0] + }; + let mut slots = Vec::new(); + for (id, frame, ty) in [ + (0, 0, shape), + (1, 1, shape), + (2, 1, 6), + (3, 2, shape), + (4, 3, shape), + (5, 4, shape), + (6, 1, 4), + (7, 0, 4), + ] { + let mut row = vec![id, frame, ty]; + row.extend(if ty == 6 { + vec![1, 2] + } else if ty == 4 { + vec![0] + } else { + path.clone() + }); + slots.push(row); + } + let mut ops: Vec<(u32, Vec)> = Vec::new(); + let mut add = |function, row: Vec| { + let id = ops.len() as u32; + ops.push((function, row)); + id + }; + add(0, vec![1, 1, 0, 2]); // factory -> typed Result slot + add(0, vec![10, 1, 2, 6]); // occupied result -> independent plain slot + add( + 0, + vec![2, 1, 6, if !resident && shape == 4 { 1 } else { 7 }], + ); + let borrow = if resident { + Some(add(0, vec![3, 1, 7, 1])) + } else { + None + }; + let mut row = vec![16, 1, 1, 4, 3]; + if resident { + row.push(0); + row.extend([ + 2, + if reorder { 1 } else { 0 }, + if reorder { 0 } else { 1 }, + 2, + ]); + row.extend([ + 0, + if reorder { 1 } else { 0 }, + 4, + 4, + 2, + 4, + 2, + 3, + 1, + borrow.unwrap(), + ]); + row.extend([1, if reorder { 0 } else { 1 }, 0, 0, 1, 1, 2, 1]); + row.extend([1, 3]); + } else { + row.extend([1, 2, 1, 0, 1, 0, 0, shape, shape, 1, 2, 1, 2, 1, 4]); + row.extend([2, shape, 0, 0, 2, 1, 3]); + } + let source = add(0, row); + let transfer_return = if resident { + u32::MAX + } else { + add(1, vec![2, 4, 4, 3]) + }; + let invoke_borrow = if resident { + Some(add(1, vec![6, 4, 1, 2, 3, 0])) + } else { + None + }; + let complete_inner = add(1, vec![13, 4]); + let complete_scope = add(1, vec![13, 3]); + let publish = if resident { + None + } else { + Some(add(1, vec![18, 3, 3])) + }; + let end_borrow = borrow.map(|borrow| add(0, vec![5, 1, borrow])); + let complete_op = add(0, vec![13, 1]); + let complete_root = add(0, vec![13, 0]); + let close = if !resident && shape != 4 { + add(0, vec![11, 1, 0]) + } else { + add(0, vec![7, 1, 2, if resident { 7 } else { 0 }]) + }; + let create = if shape == 5 { + Some(add(0, vec![19, 1, 1])) + } else if shape == 6 { + Some(add(0, vec![20, 1, 1, 2])) + } else { + None + }; + let take_fail = if shape == 6 { + Some(add(0, vec![17, 1, 0, 2])) + } else { + None + }; + let mut bytes = b"JTRSC001".to_vec(); + words(&mut bytes, &[2, 0]); + bytes.extend_from_slice(&0u64.to_le_bytes()); + words( + &mut bytes, + &[ + 1, + 3, + signatures.len() as u32, + shapes.len() as u32, + frames.len() as u32, + slots.len() as u32, + ops.len() as u32, + 0, + ], + ); + words(&mut bytes, &[0]); + for row in [[0, 0, 1, 0], [1, 0, 2, 1], [2, 0, 3, 2]] { + words(&mut bytes, &row); + } + for row in signatures + .into_iter() + .chain(shapes) + .chain(frames) + .chain(slots) + { + words(&mut bytes, &row); + } + for (id, (function, row)) in ops.into_iter().enumerate() { + words(&mut bytes, &[id as u32, function, 0, 0, 100 + id as u32]); + words(&mut bytes, &row); + } + let len = bytes.len() as u64; + bytes[16..24].copy_from_slice(&len.to_le_bytes()); + Fixture { + bytes, + source, + transfer_return, + complete_scope, + publish, + complete_op, + complete_root, + complete_inner, + close, + create, + borrow, + invoke_borrow, + end_borrow, + take_fail, + } +} +fn entry() -> NativeEntry { + NativeEntry { + function: 0, + signature: 3, + scope: 0, + } +} +fn setup( + auth: &AuthenticatedResourceContext, + f: &Fixture, + rows: &[(u32, i64, i64, &str)], +) -> (ResourceHandleId, ResourceHandleId, ResourceHandleId, u64) { + install_scripted(auth, &f.bytes, entry(), script(rows, 1)).unwrap(); + auth.with_resource(|s, v, r| { + let attempt = s.begin_entry(v, r, entry(), ResourcePurpose::Runtime)?; + let root = s.root_frame(attempt)?; + let op = s.begin_operation_frame(v, 1, root)?; + let network = s.entry_network(attempt, 0)?; + Ok((attempt, root, op, network)) + }) + .unwrap() +} +fn acquire( + auth: &AuthenticatedResourceContext, + op: ResourceHandleId, + network: u64, +) -> ResourceHandleId { + auth.with_resource(|s, v, r| { + let call = s.prepare_hook(v, 0, op, None)?; + let prepared = s.prepare_factory(v, r, call, network)?; + let result = s.factory(v, r, prepared, network, 501)?; + let value = s.sum_take(v, r, 1, op, ResourceHandleId::new(result.value)?, op)?; + s.transfer(v, r, 2, op, value, s.destination_frame(op, 2)?) + }) + .unwrap() +} +fn finish(auth: &AuthenticatedResourceContext, attempt: ResourceHandleId) { + auth.with_resource(|s, v, r| { + let completion = s.complete_entry(v, r, attempt, 0, false)?; + assert_eq!(completion.selected_kind, 0); + let counts = s.counts(v, r); + assert_eq!( + ( + counts.owners, + counts.loans, + counts.frames, + counts.registry, + counts.provisional + ), + (0, 0, 0, 0, 0) + ); + Ok(()) + }) + .unwrap(); +} + +#[test] +fn native_resource_source_owned_activation_parameter_return_and_publication_are_clone_free() { + context(|auth| { + let f = fixture(4, false, false); + let (attempt, root, op, network) = setup(auth, &f, &[(1, 501, 0, "")]); + let owner = acquire(auth, op, network); + auth.with_resource(|s, v, r| { + let call = s.source_prepare(v, op, f.source)?; + s.source_actual(v, r, call, 0, 0, owner.raw())?; + let scope = s.source_enter(v, r, call)?; + s.scope_validate(v, scope, 1, 4, 3)?; + assert!(!s.handles.contains_key(&owner)); + let parameter = ResourceHandleId::new(s.source_parameter(v, scope, 0)?)?; + let inner = s.begin_operation_frame(v, 4, scope)?; + let return_frame = s.destination_frame(inner, f.transfer_return)?; + let provisional = + s.transfer(v, r, f.transfer_return, inner, parameter, return_frame)?; + s.end_operation_frame(v, r, inner)?; + s.scope_complete(v, r, scope, f.complete_scope, 0)?; + assert_eq!(s.counts(v, r).provisional, 1); + // Same slot/type from a different activation cannot satisfy Return publication. + let NativeResourceEntry::Owner(header) = s.handles.get_mut(&provisional).unwrap() + else { + panic!("plain provisional owner") + }; + let recorded_frame = header.frame; + header.frame = root; + assert_eq!( + s.return_publish(v, r, scope, f.publish.unwrap(), provisional), + Err(NativeResourceError::WrongFrame) + ); + let NativeResourceEntry::Owner(header) = s.handles.get_mut(&provisional).unwrap() + else { + panic!("unchanged owner") + }; + header.frame = recorded_frame; + let output = s.return_publish(v, r, scope, f.publish.unwrap(), provisional)?; + assert!(!s.handles.contains_key(&provisional)); + s.source_status(v, call)?; + assert_eq!(s.owner(output, 0)?.frame, root); + s.close_or_drop(r, f.close, op, output)?; + s.end_operation_frame(v, r, op)?; + Ok(()) + }) + .unwrap(); + finish(auth, attempt); + auth.with_resource(|s, _, _| { + s.observe_events(|events| { + assert_eq!( + events.iter().map(|e| e.kind).collect::>(), + vec![1, 5] + ) + }); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn native_resource_source_named_actual_order_and_resident_view_preserve_one_parent_loan() { + context(|auth| { + let f = fixture(4, true, true); + let (attempt, _, op, network) = setup(auth, &f, &[(1, 501, 0, ""), (4, 501, 7, "")]); + let owner = acquire(auth, op, network); + auth.with_resource(|s, v, r| { + let loan = s.borrow_begin(v, r, f.borrow.unwrap(), op, owner)?; + let call = s.source_prepare(v, op, f.source)?; + // Formal zero is the SECOND source actual. No evaluation occurs inside these leaves. + s.source_actual(v, r, call, 0, 1, network)?; + s.source_actual(v, r, call, 1, 0, loan.raw())?; + let scope = s.source_enter(v, r, call)?; + assert_eq!(s.source_parameter(v, scope, 0)?, loan.raw()); + let inner = s.begin_operation_frame(v, 4, scope)?; + let hook = s.prepare_hook(v, f.invoke_borrow.unwrap(), inner, None)?; + let result = s.invoke_borrow(v, r, hook, network, loan)?; + v.drop_resource_ordinary_companion(result) + .map_err(ordinary_error)?; + s.end_operation_frame(v, r, inner)?; + s.scope_complete(v, r, scope, f.complete_scope, 0)?; + s.source_status(v, call)?; + assert_eq!(s.counts(v, r).loans, 1); + s.borrow_end(f.end_borrow.unwrap(), op, loan)?; + s.close_or_drop(r, f.close, op, owner)?; + s.end_operation_frame(v, r, op)?; + Ok(()) + }) + .unwrap(); + finish(auth, attempt); + }); +} + +#[test] +fn native_resource_source_absent_shell_moves_through_formal_and_return_without_owner_recovery() { + context(|auth| { + let f = fixture(5, false, false); + let (attempt, _, op, _) = setup(auth, &f, &[]); + auth.with_resource(|s, v, r| { + let shell = s.absent_sum(v, op, f.create.unwrap())?; + let call = s.source_prepare(v, op, f.source)?; + s.source_actual(v, r, call, 0, 0, shell.raw())?; + let scope = s.source_enter(v, r, call)?; + let parameter = ResourceHandleId::new(s.source_parameter(v, scope, 0)?)?; + let inner = s.begin_operation_frame(v, 4, scope)?; + let destination = s.destination_frame(inner, f.transfer_return)?; + let value = s.transfer(v, r, f.transfer_return, inner, parameter, destination)?; + assert_eq!(s.sum_tag(value)?, 0); + assert_eq!(s.custody.live_owners(), 0); + s.end_operation_frame(v, r, inner)?; + s.scope_complete(v, r, scope, f.complete_scope, 0)?; + let value = s.return_publish(v, r, scope, f.publish.unwrap(), value)?; + s.source_status(v, call)?; + s.sum_drop(v, r, f.close, op, value)?; + s.end_operation_frame(v, r, op)?; + Ok(()) + }) + .unwrap(); + finish(auth, attempt); + }); +} + +#[test] +fn native_resource_source_failure_shell_companion_moves_once_and_never_adopts_a_resource_owner() { + context(|auth| { + let f = fixture(6, false, false); + let (attempt, _, op, _) = setup(auth, &f, &[]); + auth.with_resource(|s, v, r| { + let mut ids = v + .prepare_resource_ordinary_output() + .map_err(ordinary_error)?; + let text = v + .publish_resource_error("ordinary domain failure".to_owned(), &mut ids) + .map_err(ordinary_error)?; + let shell = s.failure_sum(v, op, f.create.unwrap(), text)?; + let call = s.source_prepare(v, op, f.source)?; + s.source_actual(v, r, call, 0, 0, shell.raw())?; + let scope = s.source_enter(v, r, call)?; + let parameter = ResourceHandleId::new(s.source_parameter(v, scope, 0)?)?; + let inner = s.begin_operation_frame(v, 4, scope)?; + let dest = s.destination_frame(inner, f.transfer_return)?; + let value = s.transfer(v, r, f.transfer_return, inner, parameter, dest)?; + s.end_operation_frame(v, r, inner)?; + s.scope_complete(v, r, scope, f.complete_scope, 0)?; + let value = s.return_publish(v, r, scope, f.publish.unwrap(), value)?; + let taken = s.failure_companion_take(v, op, f.take_fail.unwrap(), value)?; + assert_eq!(taken, text); + assert_eq!(s.custody.live_owners(), 0); + assert!(!s.handles.contains_key(&value)); + v.drop_resource_ordinary_companion(taken) + .map_err(ordinary_error)?; + s.end_operation_frame(v, r, op)?; + Ok(()) + }) + .unwrap(); + finish(auth, attempt); + }); +} + +#[test] +fn native_resource_source_missing_or_duplicate_actuals_cannot_open_a_callee() { + context(|auth| { + let f = fixture(4, false, false); + let (attempt, _, op, network) = setup(auth, &f, &[(1, 501, 0, "")]); + let owner = acquire(auth, op, network); + // Inspect direct refused transitions without latching the separate body channel. + auth.with_resource(|s, v, r| { + let call = s.source_prepare(v, op, f.source)?; + assert_eq!( + s.source_enter(v, r, call), + Err(NativeResourceError::WrongOperation) + ); + assert_eq!(s.custody.active_frames(), 2); + s.source_actual(v, r, call, 0, 0, owner.raw())?; + assert_eq!( + s.source_actual(v, r, call, 0, 0, owner.raw()), + Err(NativeResourceError::WrongOperation) + ); + assert_eq!(s.custody.live_owners(), 1); + assert!(s.handles.contains_key(&owner)); + s.end_operation_frame(v, r, op)?; + Ok(()) + }) + .unwrap(); + finish(auth, attempt); + }); +} + +#[test] +fn native_resource_source_failed_scope_cancels_provisional_return_before_any_publication() { + context(|auth| { + let f = fixture(4, false, false); + let (attempt, _, op, network) = setup(auth, &f, &[(1, 501, 0, "")]); + let owner = acquire(auth, op, network); + auth.with_resource(|s, v, r| { + let call = s.source_prepare(v, op, f.source)?; + s.source_actual(v, r, call, 0, 0, owner.raw())?; + let scope = s.source_enter(v, r, call)?; + let parameter = ResourceHandleId::new(s.source_parameter(v, scope, 0)?)?; + let inner = s.begin_operation_frame(v, 4, scope)?; + let dest = s.destination_frame(inner, f.transfer_return)?; + let provisional = s.transfer(v, r, f.transfer_return, inner, parameter, dest)?; + s.end_operation_frame(v, r, inner)?; + assert_eq!( + s.scope_complete(v, r, scope, f.complete_scope, 71), + Err(NativeResourceError::BodyFailed) + ); + assert!(!s.handles.contains_key(&provisional)); + assert_eq!(s.counts(v, r).provisional, 0); + assert_eq!(s.source_completed_body_status(call)?, Some(71)); + let original = NativeResourceError::SourceBodyStatus(JettRuntimeStatusV1(71)); + assert_eq!(s.source_status(v, call), Err(original)); + assert_eq!(original.status().code(), 71); + // A different current activation or tuple cannot borrow this outcome. + let root = s.root_frame(attempt)?; + s.active_frames.push(root); + assert_eq!( + s.source_status(v, call), + Err(NativeResourceError::WrongFrame) + ); + s.active_frames.pop(); + let Some(NativeResourceEntry::Frame(header)) = s.handles.get_mut(&op) else { + panic!("exact live caller operation") + }; + let template = header.template; + header.template = 0; + assert_eq!( + s.source_status(v, call), + Err(NativeResourceError::WrongFrame) + ); + let Some(NativeResourceEntry::Frame(header)) = s.handles.get_mut(&op) else { + panic!("unchanged live caller operation") + }; + header.template = template; + assert_eq!(s.source_status(v, call), Err(original)); + assert!( + s.return_publish(v, r, scope, f.publish.unwrap(), provisional) + .is_err() + ); + s.end_operation_frame(v, r, op)?; + assert_eq!( + s.source_status(v, call), + Err(NativeResourceError::WrongFamily) + ); + let completion = s.complete_entry(v, r, attempt, 71, false)?; + assert_eq!((completion.body_status, completion.selected_kind), (71, 1)); + assert_eq!(s.counts(v, r).owners, 0); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn native_resource_source_c_outputs_refuse_overlap_before_opening_any_operation_or_provider() { + let mut raw = Box::new(JettRuntimeContextV1::retired()); + let mut result = JettRuntimeResultV1::ok(); + let ptr = &mut *raw as *mut JettRuntimeContextV1; + assert_eq!( + unsafe { jett_rt_v1_context_create(JETT_RUNTIME_ABI_VERSION_V1, ptr, &mut result) }, + JettRuntimeStatusV1::OK + ); + let auth = AuthenticatedResourceContext::acquire(ptr).unwrap(); + let f = fixture(4, false, false); + let (attempt, root, _, _) = setup(&auth, &f, &[(1, 501, 0, "")]); + // A writable result overlapping the immutable Context cannot be an output carrier. + assert_eq!( + unsafe { + leaves::jett_rt_v1_resource_operation_begin(ptr, root.raw(), 1, ptr.cast::()) + }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + assert_eq!( + unsafe { + leaves::jett_rt_v1_resource_entry_scope( + ptr, + &mut 0u64, + ptr.cast::(), + ) + }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + auth.with_resource(|s, v, r| { + assert_eq!(s.custody.active_frames(), 2); + assert_eq!(s.script_remaining(), 1); + let completion = s.complete_entry(v, r, attempt, 0, false)?; + assert_eq!(completion.selected_kind, 0); + Ok(()) + }) + .unwrap(); + drop(auth); + assert_eq!( + unsafe { jett_rt_v1_context_destroy(ptr, &mut result) }, + JettRuntimeStatusV1::OK + ); +} + +#[test] +fn native_resource_source_entry_getters_return_real_root_and_paired_network_without_new_frames() { + let mut raw = Box::new(JettRuntimeContextV1::retired()); + let mut result = JettRuntimeResultV1::ok(); + let ptr = &mut *raw as *mut JettRuntimeContextV1; + assert_eq!( + unsafe { jett_rt_v1_context_create(JETT_RUNTIME_ABI_VERSION_V1, ptr, &mut result) }, + JettRuntimeStatusV1::OK + ); + let auth = AuthenticatedResourceContext::acquire(ptr).unwrap(); + let f = fixture(4, false, false); + let (attempt, root, _, network) = setup(&auth, &f, &[]); + let mut scope = 0; + let mut capability = 0; + assert_eq!( + unsafe { leaves::jett_rt_v1_resource_entry_scope(ptr, &mut scope, &mut result) }, + JettRuntimeStatusV1::OK + ); + assert_eq!( + unsafe { leaves::jett_rt_v1_resource_entry_network(ptr, 0, &mut capability, &mut result) }, + JettRuntimeStatusV1::OK + ); + assert_eq!((scope, capability), (root.raw(), network)); + auth.with_resource(|s, v, r| { + assert_eq!(s.custody.active_frames(), 2); + s.complete_entry(v, r, attempt, 0, false)?; + Ok(()) + }) + .unwrap(); + drop(auth); + assert_eq!( + unsafe { jett_rt_v1_context_destroy(ptr, &mut result) }, + JettRuntimeStatusV1::OK + ); +} + +#[test] +fn native_resource_source_return_cleanup_panic_precedes_nonzero_observed_body_status() { + context(|auth| { + let f = fixture(4, false, false); + let (attempt, _, op, network) = setup(auth, &f, &[(3, 501, 0, "")]); + let owner = acquire(auth, op, network); + auth.with_resource(|s, v, r| { + let call = s.source_prepare(v, op, f.source)?; + s.source_actual(v, r, call, 0, 0, owner.raw())?; + let scope = s.source_enter(v, r, call)?; + let parameter = ResourceHandleId::new(s.source_parameter(v, scope, 0)?)?; + let inner = s.begin_operation_frame(v, 4, scope)?; + let dest = s.destination_frame(inner, f.transfer_return)?; + let provisional = s.transfer(v, r, f.transfer_return, inner, parameter, dest)?; + s.end_operation_frame(v, r, inner)?; + assert_eq!( + s.scope_complete(v, r, scope, f.complete_scope, 71), + Err(NativeResourceError::Cleanup( + ResourceCleanupFailure::FinalizerPanic + )) + ); + assert!(!s.handles.contains_key(&provisional)); + assert_eq!(s.counts(v, r).provisional, 0); + assert!( + s.return_publish(v, r, scope, f.publish.unwrap(), provisional) + .is_err() + ); + s.end_operation_frame(v, r, op)?; + let completion = s.complete_entry(v, r, attempt, 71, false)?; + assert_eq!((completion.body_status, completion.selected_kind), (71, 3)); + let counts = s.counts(v, r); + assert_eq!( + ( + counts.owners, + counts.loans, + counts.frames, + counts.registry, + counts.provisional + ), + (0, 0, 0, 0, 0) + ); + Ok(()) + }) + .unwrap(); + }); +} + +#[path = "entry_outcome_tests.rs"] +mod entry_outcome_tests; + +#[test] +fn native_resource_source_clean_body_cleanup_panic_has_exact_completed_failure_status() { + context(|auth| { + let f = fixture(4, false, false); + let (attempt, _, op, network) = setup(auth, &f, &[(3, 501, 0, "")]); + let owner = acquire(auth, op, network); + auth.with_resource(|s, v, r| { + let call = s.source_prepare(v, op, f.source)?; + s.source_actual(v, r, call, 0, 0, owner.raw())?; + let scope = s.source_enter(v, r, call)?; + let parameter = ResourceHandleId::new(s.source_parameter(v, scope, 0)?)?; + let cleanup = NativeResourceError::Cleanup(ResourceCleanupFailure::FinalizerPanic); + assert_eq!( + s.scope_complete(v, r, scope, f.complete_scope, 0), + Err(cleanup) + ); + assert_eq!(s.source_completed_body_status(call)?, Some(0)); + assert_eq!(s.source_status(v, call), Err(cleanup)); + assert_eq!(cleanup.status(), JettRuntimeStatusV1::PANIC); + assert!(!s.handles.contains_key(&scope)); + assert!(!s.handles.contains_key(¶meter)); + assert!( + s.return_publish(v, r, scope, f.publish.unwrap(), parameter) + .is_err() + ); + s.end_operation_frame(v, r, op)?; + let completion = s.complete_entry(v, r, attempt, 255, false)?; + assert_eq!( + ( + completion.body_status, + completion.cleanup_status, + completion.selected_kind + ), + (255, 255, 3) + ); + let counts = s.counts(v, r); + assert_eq!( + ( + counts.owners, + counts.loans, + counts.frames, + counts.registry, + counts.provisional + ), + (0, 0, 0, 0, 0) + ); + s.observe_events(|events| { + assert_eq!( + events.iter().map(|event| event.kind).collect::>(), + [1, 5] + ) + }); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn native_resource_source_unfinished_and_merely_retired_frames_have_no_completion_receipt() { + context(|auth| { + let f = fixture(4, false, false); + let (attempt, root, op, network) = setup(auth, &f, &[(1, 501, 0, "")]); + let owner = acquire(auth, op, network); + auth.with_resource(|s, v, r| { + assert_eq!( + s.source_prepare(v, op, f.close), + Err(NativeResourceError::UnsupportedSourceBoundary) + ); + assert_eq!( + s.source_status(v, root), + Err(NativeResourceError::WrongFamily) + ); + let call = s.source_prepare(v, op, f.source)?; + assert_eq!(s.source_completed_body_status(call)?, None); + assert_eq!( + s.source_status(v, call), + Err(NativeResourceError::BodyFailed) + ); + assert_eq!( + s.source_enter(v, r, call), + Err(NativeResourceError::WrongOperation) + ); + assert_eq!( + s.source_status(v, call), + Err(NativeResourceError::BodyFailed) + ); + s.source_actual(v, r, call, 0, 0, owner.raw())?; + let scope = s.source_enter(v, r, call)?; + assert_eq!(s.source_completed_body_status(call)?, None); + assert_eq!( + s.source_status(v, call), + Err(NativeResourceError::WrongFrame) + ); + s.retire_frame(v, r, scope)?; + let return_frame = *s.active_frames.last().unwrap(); + assert_ne!(return_frame, op); + s.retire_frame(v, r, return_frame)?; + // Physical retirement alone never mints a trusted Source completion. + assert_eq!(s.source_completed_body_status(call)?, None); + assert_eq!( + s.source_status(v, call), + Err(NativeResourceError::BodyFailed) + ); + assert!( + s.return_publish(v, r, scope, f.publish.unwrap(), owner) + .is_err() + ); + s.end_operation_frame(v, r, op)?; + assert_eq!( + s.source_status(v, call), + Err(NativeResourceError::WrongFamily) + ); + Ok(()) + }) + .unwrap(); + finish(auth, attempt); + }); +} diff --git a/crates/jett_runtime/src/native_abi/resource/test_archive_api.rs b/crates/jett_runtime/src/native_abi/resource/test_archive_api.rs new file mode 100644 index 00000000..5a755620 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/test_archive_api.rs @@ -0,0 +1,400 @@ +//! Private matched-archive exports. Parent wiring requires BOTH private cfgs. +//! Observations borrow the live context and never clear its failure channels. + +use super::*; +use std::mem::{align_of, offset_of, size_of}; + +const MAX_BYTES: usize = 1024 * 1024; +const MAX_EVENTS: usize = 8192; + +#[repr(C)] +pub(super) struct ObjectManifest { + layout: *const u8, + layout_length: u64, + entry_function: u32, + entry_signature: u32, + entry_scope: u32, + reserved: u32, +} + +#[repr(C)] +pub(super) struct ArchiveInfo { + runtime_abi: u32, + test_protocol: u32, + layout_wire: u32, + pointer_bits: u32, + archive_profile: u32, + reserved: [u32; 3], +} + +const _: () = { + assert!(size_of::() == 32 && align_of::() == 8); + assert!(offset_of!(ObjectManifest, entry_function) == 16); + assert!(size_of::() == 32 && align_of::() == 4); + assert!(size_of::() == 24); + assert!(size_of::() == 72); + assert!(size_of::() == 24); +}; + +#[derive(Clone, Copy)] +struct Region { + start: usize, + end: usize, +} + +fn region(pointer: *const T, count: usize) -> ResourceResult { + if pointer.is_null() || (pointer as usize) % align_of::() != 0 { + return Err(NativeResourceError::WrongOperation); + } + let length = size_of::() + .checked_mul(count) + .filter(|length| *length <= isize::MAX as usize) + .ok_or(NativeResourceError::Capacity)?; + let start = pointer as usize; + let end = start + .checked_add(length) + .ok_or(NativeResourceError::Capacity)?; + Ok(Region { start, end }) +} + +fn disjoint(regions: &[Region]) -> ResourceResult<()> { + for (index, left) in regions.iter().enumerate() { + for right in ®ions[index + 1..] { + if left.start < right.end && right.start < left.end { + return Err(NativeResourceError::WrongOperation); + } + } + } + Ok(()) +} + +fn checked_length(length: u64, maximum: usize) -> ResourceResult { + usize::try_from(length) + .ok() + .filter(|length| *length <= maximum) + .ok_or(NativeResourceError::Capacity) +} + +fn result_call( + out_result: *mut JettRuntimeResultV1, + operation: impl FnOnce() -> ResourceResult<()>, +) -> JettRuntimeStatusV1 { + if region(out_result, 1).is_err() { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + } + complete_call(out_result, || match operation() { + Ok(()) => JettRuntimeResultV1::ok(), + Err(error) => JettRuntimeResultV1::failure(error.status(), REFUSED), + }) +} + +fn outputs( + context: *const JettRuntimeContextV1, + output: *mut T, + out_result: *mut JettRuntimeResultV1, +) -> ResourceResult<()> { + disjoint(&[ + region(context, 1)?, + region(output, 1)?, + region(out_result, 1)?, + ]) +} + +fn context_call( + context: *const JettRuntimeContextV1, + output: *mut T, + out_result: *mut JettRuntimeResultV1, + operation: impl FnOnce(&AuthenticatedResourceContext) -> ResourceResult<()>, +) -> JettRuntimeStatusV1 { + // Do not let complete_call write through a pointer overlapping the context. + if outputs(context, output, out_result).is_err() { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + } + result_call(out_result, || { + operation(&AuthenticatedResourceContext::acquire(context)?) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_test_archive_info( + out_info: *mut ArchiveInfo, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + if region(out_info, 1) + .and_then(|info| disjoint(&[info, region(out_result, 1)?])) + .is_err() + { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + } + result_call(out_result, || { + // This shared codec constant is a prerequisite of the v2 runtime packet. + unsafe { + ptr::write( + out_info, + ArchiveInfo { + runtime_abi: JETT_RUNTIME_ABI_VERSION_V1, + test_protocol: 1, + layout_wire: crate::resource_custody::NATIVE_RESOURCE_LAYOUT_WIRE_VERSION, + pointer_bits: usize::BITS, + archive_profile: u32::from(!cfg!(debug_assertions)), + reserved: [0; 3], + }, + ); + } + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_test_install( + context: *const JettRuntimeContextV1, + manifest: *const ObjectManifest, + script: *const u8, + script_length: u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + // Validate result/context separation before the result writer is entered. + if outputs(context, manifest.cast_mut(), out_result).is_err() { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + } + let manifest = unsafe { &*manifest }; + let lengths = (|| { + let layout_length = checked_length(manifest.layout_length, MAX_BYTES)?; + let script_length = checked_length(script_length, MAX_BYTES)?; + if layout_length == 0 || script_length == 0 { + return Err(NativeResourceError::WrongOperation); + } + disjoint(&[ + region(context, 1)?, + region(manifest, 1)?, + region(out_result, 1)?, + region(manifest.layout, layout_length)?, + region(script, script_length)?, + ])?; + Ok((layout_length, script_length)) + })(); + let (layout_length, script_length) = match lengths { + Ok(lengths) => lengths, + // A refusal must not overwrite any readonly input through out_result. + Err(_) => return JettRuntimeStatusV1::INVALID_ARGUMENT, + }; + result_call(out_result, || { + let authenticated = AuthenticatedResourceContext::acquire(context)?; + if manifest.reserved != 0 { + return Err(NativeResourceError::InvalidEntry); + } + let layout = unsafe { slice::from_raw_parts(manifest.layout, layout_length) }; + let version = layout.get(8..12).ok_or(NativeResourceError::InvalidEntry)?; + if ![2u32, 3].iter().any(|known| version == known.to_le_bytes()) { + return Err(NativeResourceError::InvalidEntry); + } + let script = + DecodedScript::decode(unsafe { slice::from_raw_parts(script, script_length) })?; + install_scripted( + &authenticated, + layout, + NativeEntry { + function: manifest.entry_function, + signature: manifest.entry_signature, + scope: manifest.entry_scope, + }, + script, + )?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_test_entry_begin( + context: *const JettRuntimeContextV1, + out_attempt: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + context_call(context, out_attempt, out_result, |authenticated| { + authenticated.with_state(|_, state| { + let resource = state + .resource_state + .as_mut() + .ok_or(NativeResourceError::MissingInstallation)?; + let attempt = resource.begin_entry( + &mut state.values, + &state.resources, + resource.entry, + ResourcePurpose::Runtime, + )?; + unsafe { ptr::write(out_attempt, attempt.raw()) }; + Ok(()) + }) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_test_entry_complete( + context: *const JettRuntimeContextV1, + attempt: u64, + body_status: u32, + body_panicked: u32, + out_completion: *mut NativeResourceCompletion, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + context_call(context, out_completion, out_result, |authenticated| { + if body_panicked > 1 { + return Err(NativeResourceError::WrongOperation); + } + let attempt = ResourceHandleId::new(attempt)?; + authenticated.with_state(|_, state| { + let resource = state + .resource_state + .as_mut() + .ok_or(NativeResourceError::MissingInstallation)?; + let completion = resource.complete_entry( + &mut state.values, + &mut state.resources, + attempt, + body_status, + body_panicked != 0, + )?; + unsafe { ptr::write(out_completion, completion) }; + Ok(()) + }) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_test_counts( + context: *const JettRuntimeContextV1, + out_counts: *mut NativeResourceCounts, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + context_call(context, out_counts, out_result, |authenticated| { + authenticated.with_state(|_, state| { + let counts = match &state.resource_state { + Some(resource) => resource.counts(&state.values, &state.resources), + None => NativeResourceCounts { + registry: state.resources.live_count() as u64, + ordinary_empty: u32::from(state.values.is_empty()), + ..NativeResourceCounts::default() + }, + }; + unsafe { ptr::write(out_counts, counts) }; + Ok(()) + }) + }) +} + +fn copy_regions( + context: *const JettRuntimeContextV1, + buffer: *mut T, + capacity: u64, + maximum: usize, + out_count: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> ResourceResult { + let capacity = checked_length(capacity, maximum)?; + let regions = [ + region(context, 1)?, + region(out_count, 1)?, + region(out_result, 1)?, + ]; + disjoint(®ions)?; + if capacity != 0 { + let buffer = region(buffer, capacity)?; + for other in regions { + disjoint(&[buffer, other])?; + } + } + Ok(capacity) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_test_events_copy( + context: *const JettRuntimeContextV1, + buffer: *mut NativeTestEvent, + capacity: u64, + out_count: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + let capacity = match copy_regions(context, buffer, capacity, MAX_EVENTS, out_count, out_result) + { + Ok(capacity) => capacity, + Err(_) => return JettRuntimeStatusV1::INVALID_ARGUMENT, + }; + result_call(out_result, || { + let authenticated = AuthenticatedResourceContext::acquire(context)?; + authenticated.with_state(|_, state| { + let copy = |events: &[NativeTestEvent]| { + unsafe { ptr::write(out_count, events.len() as u64) }; + if capacity == 0 { + return Ok(()); + } + if capacity < events.len() { + return Err(NativeResourceError::Capacity); + } + unsafe { ptr::copy_nonoverlapping(events.as_ptr(), buffer, events.len()) }; + Ok(()) + }; + match &state.resource_state { + Some(resource) => resource.observe_events(copy), + None => copy(&[]), + } + }) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_test_failure_copy( + context: *const JettRuntimeContextV1, + attempt: u64, + buffer: *mut u8, + capacity: u64, + out_length: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + let capacity = match copy_regions(context, buffer, capacity, 65_536, out_length, out_result) { + Ok(capacity) => capacity, + Err(_) => return JettRuntimeStatusV1::INVALID_ARGUMENT, + }; + result_call(out_result, || { + let authenticated = AuthenticatedResourceContext::acquire(context)?; + let attempt = ResourceHandleId::new(attempt)?; + authenticated.with_state(|_, state| { + let resource = state + .resource_state + .as_ref() + .ok_or(NativeResourceError::MissingInstallation)?; + let (resource, ordinary) = resource.completed_messages(attempt)?; + let message = if resource.is_empty() { + ordinary + } else { + resource + }; + unsafe { ptr::write(out_length, message.len() as u64) }; + if capacity == 0 { + return Ok(()); + } + if capacity < message.len() { + return Err(NativeResourceError::Capacity); + } + unsafe { ptr::copy_nonoverlapping(message.as_ptr(), buffer, message.len()) }; + Ok(()) + }) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_test_script_remaining( + context: *const JettRuntimeContextV1, + out_remaining: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + context_call(context, out_remaining, out_result, |authenticated| { + authenticated.with_state(|_, state| { + let resource = state + .resource_state + .as_ref() + .ok_or(NativeResourceError::MissingInstallation)?; + unsafe { ptr::write(out_remaining, resource.script_remaining() as u64) }; + Ok(()) + }) + }) +} diff --git a/crates/jett_runtime/src/native_abi/resource/test_archive_main.rs b/crates/jett_runtime/src/native_abi/resource/test_archive_main.rs new file mode 100644 index 00000000..887c723b --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/test_archive_main.rs @@ -0,0 +1,755 @@ +//! Linked only into the single matched private compiler-test runtime archive. +//! The parent module must gate this file on both test and the private archive cfg. +#![cfg(all(test, jett_resource_native_test_archive))] + +use crate::native_abi::{ + JETT_RUNTIME_ABI_VERSION_V1, JettRuntimeContextV1, JettRuntimeResultV1, JettRuntimeStatusV1, + jett_rt_v1_context_create, jett_rt_v1_context_destroy, +}; +use std::ffi::{c_char, c_int, c_void}; +use std::mem::{MaybeUninit, align_of, offset_of, size_of}; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::ptr; + +#[path = "test_archive_main/report.rs"] +mod report; +use report::{Attempt, Report}; + +const EXIT_CREATE: c_int = 70; +const EXIT_ENTRY: c_int = 71; +const EXIT_DESTROY: c_int = 72; +const EXIT_PANIC: c_int = 73; +const EXIT_INFRASTRUCTURE: c_int = 74; +const MAX_SCRIPT: usize = 1_048_576; + +#[repr(C)] +struct ObjectManifest { + layout: *const u8, + length: u64, + function: u32, + signature: u32, + scope: u32, + reserved: u32, +} +#[repr(C)] +struct ArchiveInfo { + runtime_abi: u32, + protocol: u32, + wire: u32, + pointer_bits: u32, + profile: u32, + reserved: [u32; 3], +} +#[repr(C)] +struct Completion { + attempt: u64, + body_status: u32, + cleanup_status: u32, + selected_kind: u32, + reserved: u32, +} +#[repr(C)] +struct Counts { + owners: u64, + loans: u64, + frames: u64, + registry_entries: u64, + owner_handles: u64, + loan_handles: u64, + frame_handles: u64, + provisional_returns: u64, + ordinary_empty: u32, + reserved: u32, +} +impl Counts { + fn clean(&self) -> bool { + self.reserved == 0 + && self.ordinary_empty == 1 + && [ + self.owners, + self.loans, + self.frames, + self.registry_entries, + self.owner_handles, + self.loan_handles, + self.frame_handles, + self.provisional_returns, + ] + .into_iter() + .all(|value| value == 0) + } +} +#[repr(C)] +#[derive(Clone, Copy)] +struct Event { + sequence: u64, + label: i64, + kind: u32, + reserved: u32, +} + +const _: () = { + assert!(size_of::() == 32 && align_of::() == 8); + assert!(offset_of!(ObjectManifest, function) == 16 && offset_of!(ObjectManifest, scope) == 24); + assert!(size_of::() == 32 && align_of::() == 4); + assert!(size_of::() == 24 && offset_of!(Completion, body_status) == 8); + assert!(size_of::() == 72 && offset_of!(Counts, ordinary_empty) == 64); + assert!(size_of::() == 24 && offset_of!(Event, kind) == 16); +}; + +unsafe extern "C" { + fn jett_aot_v1_entry(context: *mut c_void) -> u32; + fn jett_aot_resource_v1_manifest(out: *mut ObjectManifest) -> u32; + fn jett_rt_v1_resource_test_archive_info( + out: *mut ArchiveInfo, + result: *mut JettRuntimeResultV1, + ) -> JettRuntimeStatusV1; + fn jett_rt_v1_resource_test_install( + context: *const JettRuntimeContextV1, + manifest: *const ObjectManifest, + script: *const u8, + length: u64, + result: *mut JettRuntimeResultV1, + ) -> JettRuntimeStatusV1; + fn jett_rt_v1_resource_test_entry_begin( + context: *const JettRuntimeContextV1, + attempt: *mut u64, + result: *mut JettRuntimeResultV1, + ) -> JettRuntimeStatusV1; + fn jett_rt_v1_resource_test_entry_complete( + context: *const JettRuntimeContextV1, + attempt: u64, + body: u32, + panicked: u32, + completion: *mut Completion, + result: *mut JettRuntimeResultV1, + ) -> JettRuntimeStatusV1; + fn jett_rt_v1_resource_test_counts( + context: *const JettRuntimeContextV1, + counts: *mut Counts, + result: *mut JettRuntimeResultV1, + ) -> JettRuntimeStatusV1; + fn jett_rt_v1_resource_test_events_copy( + context: *const JettRuntimeContextV1, + events: *mut Event, + capacity: u64, + count: *mut u64, + result: *mut JettRuntimeResultV1, + ) -> JettRuntimeStatusV1; + fn jett_rt_v1_resource_test_failure_copy( + context: *const JettRuntimeContextV1, + attempt: u64, + buffer: *mut u8, + capacity: u64, + length: *mut u64, + result: *mut JettRuntimeResultV1, + ) -> JettRuntimeStatusV1; + fn jett_rt_v1_resource_test_script_remaining( + context: *const JettRuntimeContextV1, + remaining: *mut u64, + result: *mut JettRuntimeResultV1, + ) -> JettRuntimeStatusV1; +} + +fn contained(operation: impl FnOnce() -> T) -> Result { + catch_unwind(AssertUnwindSafe(operation)).map_err(crate::discard_panic_payload) +} +fn ok(status: JettRuntimeStatusV1) -> Result<(), ()> { + if status == JettRuntimeStatusV1::OK { + Ok(()) + } else { + Err(()) + } +} + +struct ActiveAttempt { + id: u64, + index: usize, + body_status: Option, + panicked: u32, + completion_attempted: bool, +} +struct Session { + context: Box>, + created: bool, + destroy_attempts: u32, + destroy_status: u32, + active: Option, +} +impl Session { + fn new() -> Self { + Self { + context: Box::new(MaybeUninit::uninit()), + created: false, + destroy_attempts: 0, + destroy_status: 0, + active: None, + } + } + fn pointer(&mut self) -> *mut JettRuntimeContextV1 { + self.context.as_mut_ptr() + } + fn create(&mut self) -> Result<(), ()> { + let mut result = MaybeUninit::uninit(); + // Distinct aligned outputs; the Box keeps the context stationary until destroy. + ok(unsafe { + jett_rt_v1_context_create( + JETT_RUNTIME_ABI_VERSION_V1, + self.pointer(), + result.as_mut_ptr(), + ) + })?; + self.created = true; + Ok(()) + } + fn destroy_once(&mut self) { + if !self.created || self.destroy_attempts != 0 { + return; + } + self.destroy_attempts = 1; + let mut result = MaybeUninit::uninit(); + self.destroy_status = match contained(|| unsafe { + jett_rt_v1_context_destroy(self.pointer(), result.as_mut_ptr()) + }) { + Ok(status) => status.code(), + Err(()) => JettRuntimeStatusV1::PANIC.code(), + }; + } +} +impl Drop for Session { + fn drop(&mut self) { + self.destroy_once(); + } +} + +fn script() -> Result<(Vec, usize), ()> { + let encoded = std::env::var("JETT_RESOURCE_NATIVE_TEST_SCRIPT_V1").map_err(|_| ())?; + if encoded.len() > MAX_SCRIPT * 2 || encoded.len() % 2 != 0 { + return Err(()); + } + let mut decoded = Vec::new(); + decoded + .try_reserve_exact(encoded.len() / 2) + .map_err(|_| ())?; + fn nibble(value: u8) -> Result { + match value { + b'0'..=b'9' => Ok(value - b'0'), + b'a'..=b'f' => Ok(value - b'a' + 10), + b'A'..=b'F' => Ok(value - b'A' + 10), + _ => Err(()), + } + } + for pair in encoded.as_bytes().chunks_exact(2) { + decoded.push((nibble(pair[0])? << 4) | nibble(pair[1])?); + } + if decoded.len() < 32 + || &decoded[..8] != b"JTRST001" + || decoded[8..12] != 1u32.to_le_bytes() + || decoded[12..16] != [0; 4] + || u64::from_le_bytes(decoded[16..24].try_into().map_err(|_| ())?) != decoded.len() as u64 + { + return Err(()); + } + let attempts = u32::from_le_bytes(decoded[24..28].try_into().map_err(|_| ())?) as usize; + let operations = u32::from_le_bytes(decoded[28..32].try_into().map_err(|_| ())?); + if !(1..=report::MAX_ATTEMPTS).contains(&attempts) || operations > 4096 { + return Err(()); + } + // The sole runtime decoder validates every closed operation before installation. + Ok((decoded, attempts)) +} + +fn install(session: &mut Session, script: &[u8]) -> Result<(), ()> { + let mut result = MaybeUninit::uninit(); + let mut info = MaybeUninit::uninit(); + ok(unsafe { jett_rt_v1_resource_test_archive_info(info.as_mut_ptr(), result.as_mut_ptr()) })?; + let info = unsafe { info.assume_init() }; + if info.runtime_abi != JETT_RUNTIME_ABI_VERSION_V1 + || info.protocol != 1 + || info.wire != crate::resource_custody::NATIVE_RESOURCE_LAYOUT_WIRE_VERSION + || info.pointer_bits != 64 + || info.profile != u32::from(!cfg!(debug_assertions)) + || info.reserved != [0; 3] + { + return Err(()); + } + let mut manifest = MaybeUninit::uninit(); + if unsafe { jett_aot_resource_v1_manifest(manifest.as_mut_ptr()) } != 0 { + return Err(()); + } + let manifest = unsafe { manifest.assume_init() }; + if manifest.reserved != 0 + || manifest.layout.is_null() + || manifest.length == 0 + || manifest.length > isize::MAX as u64 + { + return Err(()); + } + ok(unsafe { + jett_rt_v1_resource_test_install( + session.pointer(), + &manifest, + script.as_ptr(), + script.len() as u64, + result.as_mut_ptr(), + ) + }) +} + +type CopyMessage = unsafe extern "C" fn( + *const JettRuntimeContextV1, + *mut u8, + u64, + *mut u64, + *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1; +fn message(context: *const JettRuntimeContextV1, copy: CopyMessage) -> Result, ()> { + let mut result = MaybeUninit::uninit(); + let mut length = 0; + ok(unsafe { + copy( + context, + ptr::null_mut(), + 0, + &mut length, + result.as_mut_ptr(), + ) + })?; + let required = usize::try_from(length).map_err(|_| ())?; + if required > report::MAX_MESSAGE { + return Err(()); + } + let mut output = Vec::new(); + output.try_reserve_exact(required).map_err(|_| ())?; + output.resize(required, 0); + let mut copied = 0; + ok(unsafe { + copy( + context, + output.as_mut_ptr(), + length, + &mut copied, + result.as_mut_ptr(), + ) + })?; + if copied != length { + return Err(()); + } + Ok(output) +} +fn resource_message(context: *const JettRuntimeContextV1, attempt: u64) -> Result, ()> { + let mut result = MaybeUninit::uninit(); + let mut length = 0; + ok(unsafe { + jett_rt_v1_resource_test_failure_copy( + context, + attempt, + ptr::null_mut(), + 0, + &mut length, + result.as_mut_ptr(), + ) + })?; + let required = usize::try_from(length).map_err(|_| ())?; + if required > report::MAX_MESSAGE { + return Err(()); + } + let mut output = Vec::new(); + output.try_reserve_exact(required).map_err(|_| ())?; + output.resize(required, 0); + let mut copied = 0; + ok(unsafe { + jett_rt_v1_resource_test_failure_copy( + context, + attempt, + output.as_mut_ptr(), + length, + &mut copied, + result.as_mut_ptr(), + ) + })?; + if copied != length { + return Err(()); + } + Ok(output) +} +fn events(context: *const JettRuntimeContextV1) -> Result, ()> { + let mut result = MaybeUninit::uninit(); + let mut count = 0; + ok(unsafe { + jett_rt_v1_resource_test_events_copy( + context, + ptr::null_mut(), + 0, + &mut count, + result.as_mut_ptr(), + ) + })?; + let required = usize::try_from(count).map_err(|_| ())?; + if required > report::MAX_EVENTS { + return Err(()); + } + let mut output = Vec::new(); + output.try_reserve_exact(required).map_err(|_| ())?; + output.resize( + required, + Event { + sequence: 0, + label: 0, + kind: 0, + reserved: 0, + }, + ); + let mut copied = 0; + ok(unsafe { + jett_rt_v1_resource_test_events_copy( + context, + output.as_mut_ptr(), + count, + &mut copied, + result.as_mut_ptr(), + ) + })?; + if copied != count { + return Err(()); + } + let mut previous = 0; + for event in &output { + if event.sequence <= previous || event.reserved != 0 || !(1..=5).contains(&event.kind) { + return Err(()); + } + previous = event.sequence; + } + Ok(output) +} + +// Every query is independent. One refused copy must not suppress later +// pre-destroy observations or erase a prefix already collected. +fn counts(context: *const JettRuntimeContextV1) -> Result { + let mut result = MaybeUninit::uninit(); + let mut output = MaybeUninit::uninit(); + ok(unsafe { + jett_rt_v1_resource_test_counts(context, output.as_mut_ptr(), result.as_mut_ptr()) + })?; + Ok(unsafe { output.assume_init() }) +} +fn remaining(context: *const JettRuntimeContextV1) -> Result { + let mut result = MaybeUninit::uninit(); + let mut output = 0; + ok(unsafe { + jett_rt_v1_resource_test_script_remaining(context, &mut output, result.as_mut_ptr()) + })?; + Ok(output) +} +fn ordinary_status(context: *const JettRuntimeContextV1) -> Result { + let mut output = MaybeUninit::uninit(); + // This read returns the current first failure without taking or resetting it. + let status = unsafe { + crate::native_abi::values::jett_rt_v1_value_failure(context, output.as_mut_ptr()) + }; + let output = unsafe { output.assume_init() }; + if output.status != status { + return Err(()); + } + Ok(status.code()) +} +fn query(operation: impl FnOnce() -> Result) -> Result { + contained(operation)? +} +fn complete_active(session: &mut Session, report: &mut Report) { + let Some(active) = session.active.as_mut() else { + return; + }; + if active.completion_attempted { + return; + } + active.completion_attempted = true; + let id = active.id; + let index = active.index; + let body = active + .body_status + .unwrap_or(JettRuntimeStatusV1::PANIC.code()); + let panicked = if active.body_status.is_some() { + active.panicked + } else { + 1 + }; + // An orchestration abort before an observed entry result still requires + // cleanup, but cannot produce an accepted v1 body-status observation. + if active.body_status.is_none() { + report.refused = true; + } + let completion = query(|| { + let mut result = MaybeUninit::uninit(); + let mut output = MaybeUninit::uninit(); + ok(unsafe { + jett_rt_v1_resource_test_entry_complete( + session.pointer(), + id, + body, + panicked, + output.as_mut_ptr(), + result.as_mut_ptr(), + ) + })?; + Ok(unsafe { output.assume_init() }) + }); + match completion { + Ok(value) => { + // Preserve the completed header before any fallible later copy. + let valid = value.attempt == id + && value.body_status == body + && value.reserved == 0 + && value.selected_kind <= 3; + report.attempts[index].completion = Some(value); + if !valid { + report.refused = true; + } + } + Err(()) => report.refused = true, + } + // A refused completion is never retried. Destroy's safety cleanup remains + // the final fallback and its pre-destroy counts must expose any live state. + session.active = None; +} +fn observe_attempt(session: &mut Session, report: &mut Report, index: usize) { + let context = session.pointer(); + let id = report.attempts[index].id; + match query(|| counts(context)) { + Ok(value) => report.attempts[index].counts = Some(value), + Err(()) => report.refused = true, + } + match query(|| ordinary_status(context)) { + Ok(value) => report.attempts[index].ordinary_status = Some(value), + Err(()) => report.refused = true, + } + match query(|| { + message( + context, + crate::native_abi::values::jett_rt_v1_value_failure_copy, + ) + }) { + Ok(value) => report.attempts[index].ordinary_message = Some(value), + Err(()) => report.refused = true, + } + match query(|| resource_message(context, id)) { + Ok(value) => report.attempts[index].resource_message = Some(value), + Err(()) => report.refused = true, + } + match query(|| events(context)) { + Ok(value) => { + report.attempts[index].event_sequence_end = + Some(value.last().map_or(0, |event| event.sequence)); + report.events = Some(value); + } + Err(()) => report.refused = true, + } + match query(|| remaining(context)) { + Ok(value) => report.remaining = Some(value), + Err(()) => report.refused = true, + } +} +fn observe_final(session: &mut Session, report: &mut Report) { + if !session.created { + return; + } + if contained(|| complete_active(session, report)).is_err() { + report.refused = true; + } + if let Some(index) = report.attempts.len().checked_sub(1) { + if !report.attempts[index].observations_complete() + && contained(|| observe_attempt(session, report, index)).is_err() + { + report.refused = true; + } + } + let context = session.pointer(); + match query(|| counts(context)) { + Ok(value) => report.final_counts = Some(value), + Err(()) => report.refused = true, + } + match query(|| ordinary_status(context)) { + Ok(value) => report.final_ordinary_status = Some(value), + Err(()) => report.refused = true, + } + match query(|| { + message( + context, + crate::native_abi::values::jett_rt_v1_value_failure_copy, + ) + }) { + Ok(value) => report.final_ordinary_message = Some(value), + Err(()) => report.refused = true, + } + match query(|| events(context)) { + Ok(value) => report.events = Some(value), + Err(()) => report.refused = true, + } + match query(|| remaining(context)) { + Ok(value) => report.remaining = Some(value), + Err(()) => report.refused = true, + } +} +struct AttemptOutcome { + exit: c_int, + reenter: bool, +} +fn run_attempt(session: &mut Session, report: &mut Report) -> Result { + report.attempts.try_reserve(1).map_err(|_| ())?; + let mut result = MaybeUninit::uninit(); + let mut attempt = 0; + ok(unsafe { + jett_rt_v1_resource_test_entry_begin(session.pointer(), &mut attempt, result.as_mut_ptr()) + })?; + if attempt == 0 { + return Err(()); + } + let index = report.attempts.len(); + report.attempts.push(Attempt::pending(attempt)); + session.active = Some(ActiveAttempt { + id: attempt, + index, + body_status: None, + panicked: 0, + completion_attempted: false, + }); + let (body_status, panicked) = + match contained(|| unsafe { jett_aot_v1_entry(session.pointer().cast()) }) { + Ok(status) => (status, 0), + Err(()) => (JettRuntimeStatusV1::PANIC.code(), 1), + }; + report.attempts[index].observed_body_status = Some(body_status); + let active = session.active.as_mut().ok_or(())?; + active.body_status = Some(body_status); + active.panicked = panicked; + // Completion follows the entry immediately, including a caught entry panic. + complete_active(session, report); + observe_attempt(session, report, index); + if report.refused { + return Err(()); + } + let observed = &report.attempts[index]; + let completion = observed.completion.as_ref().ok_or(())?; + let counts = observed.counts.as_ref().ok_or(())?; + let ordinary = observed.ordinary_status.ok_or(())?; + if !counts.clean() { + return Err(()); + } + let exit = match completion.selected_kind { + 0 => 0, + 1 | 3 => EXIT_ENTRY, + 2 => EXIT_PANIC, + _ => return Err(()), + }; + // Errors remain immutable attempt observations. Only entry_begin can issue + // the next ordinary channel after predecessor, session poison and retirement + // checks. The launcher neither resets nor takes a failure. + let _ = ordinary; // Preserve the required known observation for this attempt. + Ok(AttemptOutcome { + exit, + reenter: true, + }) +} +fn run(session: &mut Session, report: &mut Report) -> c_int { + let (script, attempts) = match script() { + Ok(value) => value, + Err(()) => { + report.refused = true; + return EXIT_INFRASTRUCTURE; + } + }; + if session.create().is_err() { + report.refused = true; + return EXIT_CREATE; + } + if install(session, &script).is_err() { + report.refused = true; + return EXIT_INFRASTRUCTURE; + } + let mut exit = 0; + for _ in 0..attempts { + match run_attempt(session, report) { + Ok(outcome) => { + if exit == 0 { + exit = outcome.exit; + } + if !outcome.reenter { + break; + } + } + Err(()) => { + report.refused = true; + return EXIT_INFRASTRUCTURE; + } + } + } + // The final observation pass also runs for every earlier return or panic. + exit +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn main(_argc: c_int, _argv: *mut *mut c_char) -> c_int { + // The file itself and its parent require both private cfgs. Ordinary runtime + // and normal unit-test binaries retain Rust's existing panic hook. + let outer = contained(|| { + // Do not leave an earlier successful report after a refused current run. + match std::fs::remove_file("resource-native-report-v1.bin") { + Ok(()) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(_) => return EXIT_INFRASTRUCTURE, + } + std::panic::set_hook(Box::new(|_| {})); + let mut session = Session::new(); + let mut report = Report::default(); + let mut exit = match contained(|| run(&mut session, &mut report)) { + Ok(exit) => exit, + Err(()) => { + report.refused = true; + EXIT_INFRASTRUCTURE + } + }; + // Observations precede the sole teardown even after orchestration panic. + if contained(|| observe_final(&mut session, &mut report)).is_err() { + report.refused = true; + exit = EXIT_INFRASTRUCTURE; + } + if report.refused { + exit = EXIT_INFRASTRUCTURE; + } + if exit == 0 + && (report.remaining != Some(0) + || !report.final_counts.as_ref().is_some_and(Counts::clean)) + { + report.refused = true; + exit = EXIT_INFRASTRUCTURE; + } + session.destroy_once(); + report.created = session.created; + report.destroy_attempts = session.destroy_attempts; + report.destroy_status = session.destroy_status; + if session.created && session.destroy_status != 0 { + exit = EXIT_DESTROY; + } + // v1 cannot encode unknown counts/messages/sequence/remaining. Preserve + // the internal completed prefix, refuse the report, and exit nonzero. + let bytes = match report.encode() { + Ok(bytes) => bytes, + Err(()) => { + return if exit == EXIT_DESTROY { + exit + } else { + EXIT_INFRASTRUCTURE + }; + } + }; + if std::fs::write("resource-native-report-v1.bin", bytes).is_ok() { + return exit; + } + report.write_status = JettRuntimeStatusV1::IO_FAILURE.code(); + if let Ok(bytes) = report.encode() { + let _ = std::fs::write("resource-native-report-v1.bin", bytes); + } + EXIT_INFRASTRUCTURE + }); + outer.unwrap_or(EXIT_INFRASTRUCTURE) +} diff --git a/crates/jett_runtime/src/native_abi/resource/test_archive_main/report.rs b/crates/jett_runtime/src/native_abi/resource/test_archive_main/report.rs new file mode 100644 index 00000000..564cb64e --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/test_archive_main/report.rs @@ -0,0 +1,304 @@ +//! Canonical owned report transport for the private compiler-test main. +//! These records contain observations, never registry keys or custody tokens. +use super::{Completion, Counts, Event}; + +pub(super) const MAX_ATTEMPTS: usize = 64; +pub(super) const MAX_EVENTS: usize = 8192; +pub(super) const MAX_MESSAGE: usize = 65_536; +const MAX_REPORT: usize = 16 * 1024 * 1024; + +pub(super) struct Attempt { + pub(super) id: u64, + pub(super) observed_body_status: Option, + pub(super) completion: Option, + pub(super) counts: Option, + pub(super) ordinary_status: Option, + pub(super) ordinary_message: Option>, + pub(super) resource_message: Option>, + pub(super) event_sequence_end: Option, +} +impl Attempt { + pub(super) fn pending(id: u64) -> Self { + Self { + id, + observed_body_status: None, + completion: None, + counts: None, + ordinary_status: None, + ordinary_message: None, + resource_message: None, + event_sequence_end: None, + } + } + pub(super) fn observations_complete(&self) -> bool { + self.observed_body_status.is_some() + && self.completion.is_some() + && self.counts.is_some() + && self.ordinary_status.is_some() + && self.ordinary_message.is_some() + && self.resource_message.is_some() + && self.event_sequence_end.is_some() + } +} + +#[derive(Default)] +pub(super) struct Report { + pub(super) attempts: Vec, + pub(super) events: Option>, + pub(super) remaining: Option, + pub(super) final_counts: Option, + pub(super) final_ordinary_status: Option, + pub(super) final_ordinary_message: Option>, + pub(super) refused: bool, + pub(super) created: bool, + pub(super) destroy_attempts: u32, + pub(super) destroy_status: u32, + pub(super) write_status: u32, +} + +struct Encoder(Vec); +impl Encoder { + fn bytes(&mut self, value: &[u8]) -> Result<(), ()> { + let length = self.0.len().checked_add(value.len()).ok_or(())?; + if length > MAX_REPORT { + return Err(()); + } + self.0.try_reserve(value.len()).map_err(|_| ())?; + self.0.extend_from_slice(value); + Ok(()) + } + fn u32(&mut self, value: u32) -> Result<(), ()> { + self.bytes(&value.to_le_bytes()) + } + fn u64(&mut self, value: u64) -> Result<(), ()> { + self.bytes(&value.to_le_bytes()) + } + fn completion(&mut self, value: &Completion) -> Result<(), ()> { + if value.attempt == 0 || value.reserved != 0 || value.selected_kind > 3 { + return Err(()); + } + self.u64(value.attempt)?; + for field in [ + value.body_status, + value.cleanup_status, + value.selected_kind, + 0, + ] { + self.u32(field)?; + } + Ok(()) + } + fn counts(&mut self, value: &Counts) -> Result<(), ()> { + if value.reserved != 0 || value.ordinary_empty > 1 { + return Err(()); + } + for field in [ + value.owners, + value.loans, + value.frames, + value.registry_entries, + value.owner_handles, + value.loan_handles, + value.frame_handles, + value.provisional_returns, + ] { + self.u64(field)?; + } + self.u32(value.ordinary_empty)?; + self.u32(0) + } + fn attempt(&mut self, value: &Attempt) -> Result<(), ()> { + if !value.observations_complete() { + return Err(()); + } + let completion = value.completion.as_ref().ok_or(())?; + let counts = value.counts.as_ref().ok_or(())?; + let ordinary = value.ordinary_message.as_ref().ok_or(())?; + let resource = value.resource_message.as_ref().ok_or(())?; + let end = value.event_sequence_end.ok_or(())?; + if value.id != completion.attempt + || value.observed_body_status != Some(completion.body_status) + || ordinary.len() > MAX_MESSAGE + || resource.len() > MAX_MESSAGE + { + return Err(()); + } + self.completion(completion)?; + self.counts(counts)?; + self.u32(ordinary.len() as u32)?; + self.u32(resource.len() as u32)?; + self.u64(end)?; + self.bytes(ordinary)?; + self.bytes(resource) + } +} + +impl Report { + pub(super) fn encode(&self) -> Result, ()> { + if self.refused + || self.attempts.len() > MAX_ATTEMPTS + || self.events.as_ref().ok_or(())?.len() > MAX_EVENTS + || self.remaining.is_none() + || self.final_counts.is_none() + || self.final_ordinary_status.is_none() + || self.final_ordinary_message.is_none() + || self.destroy_attempts > 1 + || (self.created && self.destroy_attempts != 1) + || self + .final_counts + .as_ref() + .is_some_and(|counts| counts.reserved != 0 || counts.ordinary_empty > 1) + || (!self.created && self.destroy_attempts != 0) + { + return Err(()); + } + let events = self.events.as_ref().ok_or(())?; + let mut output = Encoder(Vec::new()); + output.bytes(b"JTRRP001")?; + output.u32(1)?; + output.u32(0)?; + output.u64(0)?; + output.u32(self.attempts.len() as u32)?; + output.u32(events.len() as u32)?; + output.u64(self.remaining.ok_or(())?)?; + let mut last_attempt = 0; + let mut last_end = 0; + for attempt in &self.attempts { + let completion = attempt.completion.as_ref().ok_or(())?; + let end = attempt.event_sequence_end.ok_or(())?; + if completion.attempt <= last_attempt || end < last_end { + return Err(()); + } + output.attempt(attempt)?; + last_attempt = completion.attempt; + last_end = end; + } + let mut last_sequence = 0; + for event in events { + if event.sequence <= last_sequence + || event.reserved != 0 + || !(1..=5).contains(&event.kind) + { + return Err(()); + } + output.u64(event.sequence)?; + output.u64(event.label as u64)?; + output.u32(event.kind)?; + output.u32(0)?; + last_sequence = event.sequence; + } + if last_end > last_sequence { + return Err(()); + } + for field in [ + self.destroy_status, + u32::from(self.created), + self.destroy_attempts, + self.write_status, + ] { + output.u32(field)?; + } + let length = output.0.len() as u64; + output.0[16..24].copy_from_slice(&length.to_le_bytes()); + Ok(output.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + fn counts() -> Counts { + Counts { + owners: 0, + loans: 0, + frames: 0, + registry_entries: 0, + owner_handles: 0, + loan_handles: 0, + frame_handles: 0, + provisional_returns: 0, + ordinary_empty: 1, + reserved: 0, + } + } + fn report() -> Report { + let mut attempt = Attempt::pending(41); + attempt.observed_body_status = Some(71); + attempt.completion = Some(Completion { + attempt: 41, + body_status: 71, + cleanup_status: 0, + selected_kind: 1, + reserved: 0, + }); + attempt.counts = Some(counts()); + attempt.ordinary_status = Some(0); + attempt.ordinary_message = Some(Vec::new()); + attempt.resource_message = Some(Vec::new()); + attempt.event_sequence_end = Some(0); + Report { + attempts: vec![attempt], + events: Some(Vec::new()), + remaining: Some(0), + final_counts: Some(counts()), + final_ordinary_status: Some(0), + final_ordinary_message: Some(Vec::new()), + refused: false, + created: true, + destroy_attempts: 1, + destroy_status: 0, + write_status: 0, + } + } + #[test] + fn private_report_v1_keeps_original_status_and_existing_binary_offsets() { + let bytes = report().encode().unwrap(); + assert_eq!(bytes.len(), 168); + assert_eq!(&bytes[..8], b"JTRRP001"); + assert_eq!(&bytes[8..12], &1u32.to_le_bytes()); + assert_eq!(&bytes[40..48], &41u64.to_le_bytes()); + assert_eq!(&bytes[48..52], &71u32.to_le_bytes()); + assert_eq!(&bytes[56..60], &1u32.to_le_bytes()); + } + #[test] + fn private_report_completed_prefix_survives_a_late_unknown_copy_without_encoding_success() { + let mut value = report(); + value.attempts[0].resource_message = None; + assert!(value.encode().is_err()); + let completed = value.attempts[0].completion.as_ref().unwrap(); + assert_eq!((completed.attempt, completed.body_status), (41, 71)); + assert!(value.attempts[0].counts.as_ref().unwrap().clean()); + assert_eq!(value.attempts[0].ordinary_message.as_deref(), Some(&[][..])); + } + #[test] + fn private_report_unknown_observation_differs_from_a_known_empty_carrier() { + for field in 0..6 { + let mut value = report(); + match field { + 0 => value.events = None, + 1 => value.remaining = None, + 2 => value.attempts[0].counts = None, + 3 => value.attempts[0].ordinary_status = None, + 4 => value.final_ordinary_message = None, + 5 => value.final_counts = None, + _ => unreachable!(), + } + assert!(value.encode().is_err(), "unknown observation {field}"); + } + assert!(report().encode().is_ok()); + } + #[test] + fn private_report_never_forges_original_body_or_exactly_once_teardown() { + let mut value = report(); + value.attempts[0].completion.as_mut().unwrap().body_status = 17; + assert!(value.encode().is_err()); + for attempts in [0, 2] { + let mut value = report(); + value.destroy_attempts = attempts; + assert!(value.encode().is_err()); + } + let mut value = report(); + value.refused = true; + assert!(value.encode().is_err()); + } +} diff --git a/crates/jett_runtime/src/native_abi/resource/tests.rs b/crates/jett_runtime/src/native_abi/resource/tests.rs new file mode 100644 index 00000000..12e1c807 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/tests.rs @@ -0,0 +1,988 @@ +//! Host protocol controls, not Source admission or linked native acceptance. +use super::*; +use std::mem::{align_of, offset_of, size_of}; + +fn words(bytes: &mut Vec, values: &[u32]) { + for value in values { + bytes.extend_from_slice(&value.to_le_bytes()); + } +} +fn layout() -> Vec { + layout_with_entry_network(true) +} +pub(super) fn layout_with_entry_network(network: bool) -> Vec { + // Independent canonical wire data is checked by the sole real registration issuer. + let mut bytes = b"JTRSC001".to_vec(); + words(&mut bytes, &[1, 0]); + bytes.extend_from_slice(&0u64.to_le_bytes()); + words(&mut bytes, &[1, 3, 4, 9, 2, 4, 17, 0]); + words(&mut bytes, &[0]); + for row in [[0, 0, 1, 0], [1, 0, 2, 1], [2, 0, 3, 2]] { + words(&mut bytes, &row); + } + // Factory, Borrow, Finalize and the containing entry-function signature. + words(&mut bytes, &[0, 2, 6, 0, 2, 1, 1]); + words(&mut bytes, &[1, 2, 7, 0, 2, 4, 2]); + words(&mut bytes, &[2, 1, 3, 4, 1]); + if network { + words(&mut bytes, &[3, 1, 3, 0, 2]); + } else { + words(&mut bytes, &[3, 0, 3]); + } + for row in [ + vec![0, 6], + vec![1, 1, 64, 1], + vec![2, 4], + vec![3, 5], + vec![4, 7, 0], + vec![5, 8, 4], + vec![6, 9, 4, 2], + vec![7, 9, 1, 2], + vec![8, 10, 0], + ] { + words(&mut bytes, &row); + } + // Exact containing-function signature for both Scope and Operation. + words(&mut bytes, &[0, 1, 0, 0, 0, 0, 3, 1, 0, 0]); + words(&mut bytes, &[1, 2, 0, 0, 0, 1, 3, 1, 1, 0]); + for row in [ + vec![0, 0, 4, 0], + vec![1, 1, 4, 0], + vec![2, 1, 6, 1, 2], + vec![3, 0, 5, 1, 1], + ] { + words(&mut bytes, &row); + } + let operations: [&[u32]; 17] = [ + &[1, 1, 0, 2], + &[10, 1, 2, 1], + &[2, 1, 1, 0], + &[3, 1, 0, 1], + &[6, 1, 1, 1, 3], + &[5, 1, 3], + &[7, 1, 2, 0], + &[8, 1, 1], + &[9, 1, 1, 2], + &[11, 1, 2], + &[12, 1, 0, 1], + &[13, 0], + &[14, 0], + &[15, 0, 0, 0], + &[9, 1, 1, 3], + &[10, 1, 3, 1], + &[2, 1, 0, 1], + ]; + for (ordinal, operation) in operations.into_iter().enumerate() { + words(&mut bytes, &[ordinal as u32, 0, 0, 0, 100 + ordinal as u32]); + words(&mut bytes, operation); + } + let length = bytes.len() as u64; + bytes[16..24].copy_from_slice(&length.to_le_bytes()); + bytes +} +pub(super) fn script(rows: &[(u32, i64, i64, &str)], attempts: u32) -> DecodedScript { + let mut bytes = b"JTRST001".to_vec(); + words(&mut bytes, &[1, 0]); + bytes.extend_from_slice(&0u64.to_le_bytes()); + words(&mut bytes, &[attempts, rows.len() as u32]); + for &(tag, label, result, error) in rows { + words(&mut bytes, &[tag]); + bytes.extend_from_slice(&label.to_le_bytes()); + match tag { + 2 | 5 => { + words(&mut bytes, &[error.len() as u32]); + bytes.extend_from_slice(error.as_bytes()); + } + 4 => bytes.extend_from_slice(&result.to_le_bytes()), + _ => {} + } + } + let length = bytes.len() as u64; + bytes[16..24].copy_from_slice(&length.to_le_bytes()); + DecodedScript::decode(&bytes).unwrap() +} +pub(super) fn entry() -> NativeEntry { + NativeEntry { + function: 0, + signature: 3, + scope: 0, + } +} +pub(super) fn context(operation: impl FnOnce(&AuthenticatedResourceContext) -> T) -> T { + let mut context = Box::new(JettRuntimeContextV1::retired()); + let mut result = JettRuntimeResultV1::ok(); + let status = unsafe { + jett_rt_v1_context_create(JETT_RUNTIME_ABI_VERSION_V1, &mut *context, &mut result) + }; + assert_eq!(status, JettRuntimeStatusV1::OK); + let authenticated = AuthenticatedResourceContext::acquire(&*context).unwrap(); + let result = operation(&authenticated); + drop(authenticated); // release the actual lease before retirement waits for it + let status = + unsafe { jett_rt_v1_context_destroy(&mut *context, &mut JettRuntimeResultV1::ok()) }; + assert_eq!(status, JettRuntimeStatusV1::OK); + result +} +fn install( + authenticated: &AuthenticatedResourceContext, + rows: &[(u32, i64, i64, &str)], + attempts: u32, +) { + install_scripted(authenticated, &layout(), entry(), script(rows, attempts)).unwrap(); +} +fn start( + authenticated: &AuthenticatedResourceContext, + purpose: ResourcePurpose, +) -> (ResourceHandleId, ResourceHandleId, ResourceHandleId, u64) { + authenticated + .with_resource(|state, ordinary, registry| { + let attempt = state.begin_entry(ordinary, registry, entry(), purpose)?; + let root = state.root_frame(attempt)?; + let network = if purpose == ResourcePurpose::Runtime { + state.entry_network(attempt, 0)? + } else { + 0 + }; + let operation = state.begin_operation_frame(ordinary, 1, root)?; + Ok((attempt, root, operation, network)) + }) + .unwrap() +} +fn construct( + authenticated: &AuthenticatedResourceContext, + frame: ResourceHandleId, + network: u64, + label: i64, +) -> ResourceHandleId { + authenticated + .with_resource(|state, ordinary, registry| { + let call = state.prepare_hook(ordinary, 0, frame, None)?; + let prepared = state.prepare_factory(ordinary, registry, call, network)?; + let result = state.factory(ordinary, registry, prepared, network, label)?; + assert_eq!(result.domain, RESOURCE_DOMAIN_OK); + let sum = ResourceHandleId::new(result.value)?; + state.sum_take(ordinary, registry, 1, frame, sum, frame) + }) + .unwrap() +} +fn complete( + authenticated: &AuthenticatedResourceContext, + attempt: ResourceHandleId, + body: u32, +) -> NativeResourceCompletion { + authenticated + .with_resource(|state, ordinary, registry| { + state.complete_entry(ordinary, registry, attempt, body, false) + }) + .unwrap() +} +fn events(authenticated: &AuthenticatedResourceContext) -> Vec<(i64, u32)> { + authenticated + .with_resource(|state, _, _| { + Ok(state.provider.observe_events(|events| { + events + .iter() + .map(|event| (event.label, event.kind)) + .collect() + })) + }) + .unwrap() +} +fn assert_empty(authenticated: &AuthenticatedResourceContext) { + authenticated + .with_resource(|state, ordinary, registry| { + assert_eq!( + state.counts(ordinary, registry), + NativeResourceCounts { + ordinary_empty: 1, + ..NativeResourceCounts::default() + } + ); + Ok(()) + }) + .unwrap(); +} + +#[test] +fn native_resource_state_production_default_and_pure_descriptor_installation_have_no_provider_grant() + { + context(|auth| { + auth.with_state(|_, state| { + assert!(state.resource_state.is_none()); + Ok(()) + }) + .unwrap(); + install_disabled(auth, &layout_with_entry_network(false), entry()).unwrap(); + auth.with_resource(|state, ordinary, registry| { + let attempt = + state.begin_entry(ordinary, registry, entry(), ResourcePurpose::Runtime)?; + let root = state.root_frame(attempt)?; + let frame = state.begin_operation_frame(ordinary, 1, root)?; + let descriptor = state.descriptor(ordinary, 12)?; + let call = state.prepare_hook(ordinary, 13, frame, Some(descriptor))?; + assert_eq!( + state.prepare_factory(ordinary, registry, call, 0), + Err(NativeResourceError::WrongGrant) + ); + assert!(state.network.is_none()); + assert!(!state.provider.enabled()); + state.complete_entry(ordinary, registry, attempt, 0, false)?; + Ok(()) + }) + .unwrap(); + assert_empty(auth); + }); +} + +#[test] +fn native_resource_state_real_core_construct_borrow_move_close_and_reentry() { + context(|auth| { + install( + auth, + &[(1, 501, 0, ""), (4, 501, 7, ""), (1, 502, 0, "")], + 2, + ); + let (attempt, root, frame, network) = start(auth, ResourcePurpose::Runtime); + let owner = construct(auth, frame, network, 501); + let owner = auth + .with_resource(|state, ordinary, registry| { + state.transfer(ordinary, registry, 2, frame, owner, root) + }) + .unwrap(); + let loan = auth + .with_resource(|state, ordinary, registry| { + state.borrow_begin(ordinary, registry, 3, frame, owner) + }) + .unwrap(); + let result = auth + .with_resource(|state, ordinary, registry| { + let call = state.prepare_hook(ordinary, 4, frame, None)?; + state.invoke_borrow(ordinary, registry, call, network, loan) + }) + .unwrap(); + auth.with_resource(|state, ordinary, registry| { + // A wrong family is rejected without borrowing or provider consumption. + assert_eq!( + state.transfer(ordinary, registry, 2, frame, loan, root), + Err(NativeResourceError::WrongFamily) + ); + ordinary + .drop_resource_ordinary_companion(result) + .map_err(ordinary_error)?; + state.borrow_end(5, frame, loan)?; + state.close_or_drop(registry, 6, frame, owner)?; + state.end_operation_frame(ordinary, registry, frame) + }) + .unwrap(); + assert_eq!(complete(auth, attempt, 0).selected_kind, 0); + assert_empty(auth); + let (attempt, _, frame, network) = start(auth, ResourcePurpose::Runtime); + let _owner = construct(auth, frame, network, 502); + assert_eq!(complete(auth, attempt, 0).selected_kind, 0); + assert_empty(auth); + assert_eq!( + events(auth), + vec![(501, 1), (501, 3), (501, 5), (502, 1), (502, 5)] + ); + }); +} + +#[test] +fn native_resource_state_installation_and_network_pair_are_atomic_and_context_exact() { + context(|auth| { + assert_ne!( + unsafe { + values::jett_rt_v1_grant_network( + auth.key.owner_address as *const JettRuntimeContextV1, + ) + }, + 0 + ); + assert_eq!( + install_scripted(auth, &layout(), entry(), script(&[], 1)), + Err(NativeResourceError::OrdinaryStorage( + JettRuntimeStatusV1::INVALID_ARGUMENT + )) + ); + auth.with_state(|_, state| { + assert!(state.resource_state.is_none()); + assert_eq!(state.resources.live_count(), 0); + Ok(()) + }) + .unwrap(); + }); + context(|first| { + context(|second| { + install(first, &[(1, 1, 0, "")], 1); + install(second, &[], 1); + let (_, _, frame, network) = start(first, ResourcePurpose::Runtime); + first + .with_resource(|state, ordinary, registry| { + let call = state.prepare_hook(ordinary, 0, frame, None)?; + assert_eq!( + state.prepare_factory(ordinary, registry, call, network + 1), + Err(NativeResourceError::WrongGrant) + ); + Ok(()) + }) + .unwrap(); + let foreign = first + .with_resource(|state, _, _| Ok(state.program_handle)) + .unwrap(); + second + .with_resource(|state, _, _| { + assert!(!state.handles.contains_key(&foreign)); + Ok(()) + }) + .unwrap(); + assert!(events(first).is_empty()); + // Destruction safety cleanup is not substituted for the explicit completion gate. + let attempt = first + .with_resource(|state, _, _| Ok(state.attempt.as_ref().unwrap().id)) + .unwrap(); + complete(first, attempt, 0); + assert_empty(first); + }) + }); +} + +#[test] +fn native_resource_state_factory_domain_error_is_data_and_ordinary_companion_is_retired() { + context(|auth| { + install(auth, &[(2, 4, 0, "domain failure")], 1); + let (attempt, _, frame, network) = start(auth, ResourcePurpose::Runtime); + auth.with_resource(|state, ordinary, registry| { + let call = state.prepare_hook(ordinary, 0, frame, None)?; + let prepared = state.prepare_factory(ordinary, registry, call, network)?; + let result = state.factory(ordinary, registry, prepared, network, 4)?; + assert_eq!(result.domain, RESOURCE_DOMAIN_ERROR); + let handle = ResourceHandleId::new(result.value)?; + assert_eq!(state.sum_tag(handle)?, 0); + assert_eq!(state.counts(ordinary, registry).owners, 0); + state.sum_drop(ordinary, registry, 9, frame, handle) + }) + .unwrap(); + assert_eq!(complete(auth, attempt, 0).selected_kind, 0); + assert_empty(auth); + assert_eq!(events(auth), vec![(4, 2)]); + }); +} + +#[test] +fn native_resource_state_active_loan_refuses_move_and_preserves_one_owner() { + context(|auth| { + install(auth, &[(1, 6, 0, "")], 1); + let (attempt, root, frame, network) = start(auth, ResourcePurpose::Runtime); + let owner = construct(auth, frame, network, 6); + auth.with_resource(|state, ordinary, registry| { + // Move into the caller scope before borrowing. + let owner = state.transfer(ordinary, registry, 2, frame, owner, root)?; + let loan = state.borrow_begin(ordinary, registry, 3, frame, owner)?; + assert_eq!( + state.transfer(ordinary, registry, 16, frame, owner, frame), + Err(NativeResourceError::Custody(CustodyError::ActiveBorrow)) + ); + assert_eq!( + state.close_or_drop(registry, 6, frame, owner), + Err(NativeResourceError::Cleanup( + ResourceCleanupFailure::Custody(CustodyError::ActiveBorrow) + )) + ); + assert_eq!(state.counts(ordinary, registry).owners, 1); + state.borrow_end(5, frame, loan)?; + state.close_or_drop(registry, 6, frame, owner) + }) + .unwrap(); + // The attempted cleanup failure remains latched independently of token retirement. + assert_eq!(complete(auth, attempt, 0).selected_kind, 3); + assert_empty(auth); + assert_eq!(events(auth), vec![(6, 1), (6, 5)]); + }); +} + +#[test] +fn native_resource_state_borrow_panic_records_before_pop_and_cleanup_wins() { + context(|auth| { + install(auth, &[(3, 7, 0, ""), (6, 7, 0, "")], 1); + let (attempt, root, frame, network) = start(auth, ResourcePurpose::Runtime); + let owner = construct(auth, frame, network, 7); + let owner = auth + .with_resource(|state, ordinary, registry| { + state.transfer(ordinary, registry, 2, frame, owner, root) + }) + .unwrap(); + let loan = auth + .with_resource(|state, ordinary, registry| { + state.borrow_begin(ordinary, registry, 3, frame, owner) + }) + .unwrap(); + let result = auth.with_resource(|state, ordinary, registry| { + let call = state.prepare_hook(ordinary, 4, frame, None)?; + state.invoke_borrow(ordinary, registry, call, network, loan) + }); + assert_eq!(result, Err(NativeResourceError::BodyFailed)); + let completion = complete(auth, attempt, 0); + assert_eq!(completion.body_status, 0); + assert_eq!(completion.cleanup_status, JettRuntimeStatusV1::PANIC.code()); + assert_eq!(completion.selected_kind, 3); + assert_empty(auth); + assert_eq!(events(auth), vec![(7, 1), (7, 3), (7, 5)]); + auth.with_resource(|state, _, _| { + assert_eq!(state.provider.remaining(), 0); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn native_resource_state_required_purpose_rejects_hook_without_provider_effect() { + for purpose in [ + ResourcePurpose::NamespaceConstant, + ResourcePurpose::ExplicitComptime, + ResourcePurpose::Verify, + ResourcePurpose::Property, + ] { + context(|auth| { + install(auth, &[(1, 9, 0, "")], 1); + let (attempt, _, frame, _) = start(auth, purpose); + auth.with_resource(|state, ordinary, _| { + assert_eq!( + state.entry_network(attempt, 0), + Err(NativeResourceError::WrongPurpose) + ); + let descriptor = state.descriptor(ordinary, 12)?; + assert!(matches!( + state.handles.get(&descriptor), + Some(NativeResourceEntry::Descriptor { .. }) + )); + assert_eq!( + state.prepare_hook(ordinary, 0, frame, None), + Err(NativeResourceError::WrongPurpose) + ); + Ok(()) + }) + .unwrap(); + complete(auth, attempt, 0); + assert_empty(auth); + assert!(events(auth).is_empty()); + }); + } +} + +#[test] +fn native_resource_state_sum_moves_do_not_clone_registry_payload() { + context(|auth| { + install(auth, &[(1, 11, 0, "")], 1); + let (attempt, root, frame, network) = start(auth, ResourcePurpose::Runtime); + let owner = construct(auth, frame, network, 11); + auth.with_resource(|state, ordinary, registry| { + let sum = state.sum_adopt(ordinary, registry, 14, frame, owner, root)?; + assert_eq!(state.sum_tag(sum)?, 1); + assert_eq!(state.counts(ordinary, registry).owners, 1); + assert!(!state.handles.contains_key(&owner)); + let owner = state.sum_take(ordinary, registry, 15, frame, sum, frame)?; + assert!(!state.handles.contains_key(&sum)); + state.close_or_drop(registry, 7, frame, owner) + }) + .unwrap(); + complete(auth, attempt, 0); + assert_empty(auth); + assert_eq!(events(auth), vec![(11, 1), (11, 5)]); + }); +} + +#[test] +fn native_resource_state_descriptor_and_prepared_handles_cannot_be_replayed() { + context(|auth| { + install(auth, &[(1, 12, 0, "")], 1); + let (attempt, _, frame, network) = start(auth, ResourcePurpose::Runtime); + auth.with_resource(|state, ordinary, registry| { + let descriptor = state.descriptor(ordinary, 12)?; + let call = state.prepare_hook(ordinary, 13, frame, Some(descriptor))?; + let prepared = state.prepare_factory(ordinary, registry, call, network)?; + state.factory(ordinary, registry, prepared, network, 12)?; + assert_eq!( + state.factory(ordinary, registry, prepared, network, 12), + Err(NativeResourceError::WrongFamily) + ); + assert_eq!( + state.prepare_hook(ordinary, 13, frame, Some(state.program_handle)), + Err(NativeResourceError::WrongFamily) + ); + Ok(()) + }) + .unwrap(); + complete(auth, attempt, 0); + assert_empty(auth); + assert_eq!(events(auth), vec![(12, 1), (12, 5)]); + }); +} + +#[test] +fn native_resource_state_ordinary_failure_is_attempt_owned_without_reset() { + context(|auth| { + install(auth, &[], 2); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + assert_ne!( + unsafe { + values::jett_rt_v1_assert_fail( + auth.key.owner_address as *const JettRuntimeContextV1, + ) + }, + 0 + ); + let completion = complete(auth, attempt, 0); + assert_eq!(completion.body_status, 0); + assert_eq!(completion.selected_kind, 1); + assert_empty(auth); + auth.with_resource(|state, _, _| { + assert_eq!(state.completed_attempt(attempt)?, completion); + assert_eq!(state.completed_messages(attempt)?.1, b"assertion failed"); + Ok(()) + }) + .unwrap(); + auth.with_state(|_, state| { + assert!(state.values.resource_failure().is_some()); + Ok(()) + }) + .unwrap(); + // The exact completed error remains immutable. A fresh constructor owns + // the next execution; no post-completion capture or reset is performed. + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + assert_eq!(complete(auth, attempt, 0).selected_kind, 0); + assert_empty(auth); + }); + context(|auth| { + install(auth, &[], 1); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + let pointer = auth.key.owner_address as *const JettRuntimeContextV1; + assert_ne!(unsafe { values::jett_rt_v1_assert_fail(pointer) }, 0); + let text = + unsafe { values::jett_rt_v1_failure_take_prefixed_text(pointer, ptr::null(), 0) }; + assert_ne!(text, 0); + auth.with_state(|_, state| { + state + .values + .drop_resource_ordinary_companion(text) + .map_err(ordinary_error) + }) + .unwrap(); + // A Source handler legally turned the ordinary error into data before completion. + let completion = complete(auth, attempt, 0); + assert_eq!(completion.body_status, 0); + assert_eq!(completion.cleanup_status, 0); + assert_eq!(completion.selected_kind, 0); + assert_empty(auth); + }); + context(|auth| { + install(auth, &[], 1); + let (attempt, _, frame, _) = start(auth, ResourcePurpose::Runtime); + let refusal = + auth.with_resource(|state, ordinary, _| state.prepare_hook(ordinary, 999, frame, None)); + assert_eq!(refusal, Err(NativeResourceError::WrongOperation)); + let pointer = auth.key.owner_address as *const JettRuntimeContextV1; + assert_ne!(unsafe { values::jett_rt_v1_assert_fail(pointer) }, 0); + let text = + unsafe { values::jett_rt_v1_failure_take_prefixed_text(pointer, ptr::null(), 0) }; + auth.with_state(|_, state| { + state + .values + .drop_resource_ordinary_companion(text) + .map_err(ordinary_error) + }) + .unwrap(); + // Clearing only the ordinary channel cannot erase the Resource-owned refusal. + let completion = complete(auth, attempt, 0); + assert_eq!(completion.body_status, 0); + assert_eq!(completion.selected_kind, 1); + assert_empty(auth); + }); +} + +#[test] +fn native_resource_state_reverse_drop_and_replacement_cleanup_are_real_once_only_core_events() { + context(|auth| { + install(auth, &[(1, 21, 0, ""), (3, 22, 0, "")], 1); + let (attempt, root, frame, network) = start(auth, ResourcePurpose::Runtime); + let first = construct(auth, frame, network, 21); + auth.with_resource(|state, ordinary, registry| { + state.transfer(ordinary, registry, 2, frame, first, root) + }) + .unwrap(); + construct(auth, frame, network, 22); + let completion = complete(auth, attempt, 0); + assert_eq!(completion.selected_kind, 3); + assert_empty(auth); + assert_eq!(events(auth), vec![(21, 1), (22, 1), (22, 5), (21, 5)]); + }); + for panic_on_old in [false, true] { + context(|auth| { + install( + auth, + &[ + (if panic_on_old { 3 } else { 1 }, 31, 0, ""), + (1, 32, 0, ""), + ], + 1, + ); + let (attempt, root, frame, network) = start(auth, ResourcePurpose::Runtime); + let first = construct(auth, frame, network, 31); + let first = auth + .with_resource(|state, ordinary, registry| { + state.transfer(ordinary, registry, 2, frame, first, root) + }) + .unwrap(); + let replacement = construct(auth, frame, network, 32); + let result = auth.with_resource(|state, ordinary, registry| { + state.replace(ordinary, registry, 10, frame, first, replacement) + }); + if panic_on_old { + assert_eq!( + result, + Err(NativeResourceError::Cleanup( + ResourceCleanupFailure::FinalizerPanic + )) + ); + } else { + assert!(result.is_ok()); + } + let completion = complete(auth, attempt, 0); + assert_eq!(completion.selected_kind, if panic_on_old { 3 } else { 0 }); + assert_empty(auth); + assert_eq!(events(auth), vec![(31, 1), (32, 1), (31, 5), (32, 5)]); + }); + } +} + +#[test] +fn native_resource_state_closed_script_and_completion_layout_refuse_malformed_inputs() { + assert_eq!(size_of::(), 24); + assert_eq!(align_of::(), 8); + assert_eq!(offset_of!(NativeResourceCompletion, selected_kind), 16); + assert_eq!(size_of::(), 72); + assert_eq!(size_of::(), 24); + assert!(DecodedScript::decode(b"JTRST001").is_err()); + context(|auth| { + install(auth, &[], 1); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + auth.with_resource(|state, ordinary, registry| { + assert_eq!( + state.complete_entry( + ordinary, + registry, + ResourceHandleId::new(attempt.raw() + 1)?, + 0, + false + ), + Err(NativeResourceError::InvalidEntry) + ); + Ok(()) + }) + .unwrap(); + complete(auth, attempt, 0); + auth.with_resource(|state, ordinary, registry| { + assert_eq!( + state.complete_entry(ordinary, registry, attempt, 0, false), + Err(NativeResourceError::InvalidEntry) + ); + Ok(()) + }) + .unwrap(); + assert_empty(auth); + }); +} + +#[test] +fn native_resource_state_completion_keeps_observed_nonzero_status_distinct_from_recorded_failures() +{ + for resource_failure in [false, true] { + context(|auth| { + install(auth, &[], 1); + let (attempt, _, frame, _) = start(auth, ResourcePurpose::Runtime); + if resource_failure { + assert_eq!( + auth.with_resource( + |state, ordinary, _| state.prepare_hook(ordinary, 999, frame, None) + ), + Err(NativeResourceError::WrongOperation) + ); + } else { + let pointer = auth.key.owner_address as *const JettRuntimeContextV1; + assert_ne!(unsafe { values::jett_rt_v1_assert_fail(pointer) }, 0); + } + let observed = JettRuntimeStatusV1::IO_FAILURE.code(); + assert_ne!( + observed, + NativeResourceError::WrongOperation.status().code() + ); + assert_ne!(observed, JettRuntimeStatusV1::INVALID_ARGUMENT.code()); + let completion = complete(auth, attempt, observed); + assert_eq!(completion.body_status, observed); + assert_eq!(completion.cleanup_status, 0); + assert_eq!(completion.selected_kind, 1); + assert_empty(auth); + auth.with_resource(|state, _, _| { + assert_eq!(state.completed_attempt(attempt)?, completion); + let (resource, ordinary) = state.completed_messages(attempt)?; + if resource_failure { + assert_eq!(resource, REFUSED); + assert!(ordinary.is_empty()); + } else { + assert!(resource.is_empty()); + assert_eq!(ordinary, b"assertion failed"); + } + Ok(()) + }) + .unwrap(); + }); + } +} + +#[test] +fn native_resource_state_completion_selects_host_panic_and_cleanup_without_rewriting_observed_status() + { + context(|auth| { + install(auth, &[], 1); + let (attempt, _, frame, _) = start(auth, ResourcePurpose::Runtime); + assert_eq!( + auth.with_resource(|state, ordinary, _| state.prepare_hook(ordinary, 999, frame, None)), + Err(NativeResourceError::WrongOperation) + ); + let completion = auth + .with_resource(|state, ordinary, registry| { + state.complete_entry(ordinary, registry, attempt, 0, true) + }) + .unwrap(); + assert_eq!(completion.body_status, 0); + assert_eq!(completion.cleanup_status, 0); + assert_eq!(completion.selected_kind, 2); + assert_empty(auth); + auth.with_resource(|state, _, _| { + // The earlier Resource refusal remains recorded independently of the host outcome. + assert_eq!(state.completed_messages(attempt)?.0, REFUSED); + Ok(()) + }) + .unwrap(); + }); + context(|auth| { + install(auth, &[(3, 41, 0, "")], 1); + let (attempt, _, frame, network) = start(auth, ResourcePurpose::Runtime); + construct(auth, frame, network, 41); + let observed = JettRuntimeStatusV1::IO_FAILURE.code(); + let completion = auth + .with_resource(|state, ordinary, registry| { + state.complete_entry(ordinary, registry, attempt, observed, true) + }) + .unwrap(); + assert_eq!(completion.body_status, observed); + assert_eq!(completion.cleanup_status, JettRuntimeStatusV1::PANIC.code()); + assert_eq!(completion.selected_kind, 3); + assert_empty(auth); + assert_eq!(events(auth), vec![(41, 1), (41, 5)]); + auth.with_resource(|state, _, _| { + assert_eq!(state.completed_messages(attempt)?.0, CLEANUP_FAILED); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn native_resource_state_borrow_domain_error_retires_ordinary_sum_and_string() { + context(|auth| { + install(auth, &[(1, 503, 0, ""), (5, 503, 0, "borrow failure")], 1); + let (attempt, root, frame, network) = start(auth, ResourcePurpose::Runtime); + let owner = construct(auth, frame, network, 503); + let owner = auth + .with_resource(|state, ordinary, registry| { + state.transfer(ordinary, registry, 2, frame, owner, root) + }) + .unwrap(); + let loan = auth + .with_resource(|state, ordinary, registry| { + state.borrow_begin(ordinary, registry, 3, frame, owner) + }) + .unwrap(); + let result = auth + .with_resource(|state, ordinary, registry| { + let call = state.prepare_hook(ordinary, 4, frame, None)?; + state.invoke_borrow(ordinary, registry, call, network, loan) + }) + .unwrap(); + let pointer = auth.key.owner_address as *const JettRuntimeContextV1; + assert_eq!(unsafe { values::jett_rt_v1_sum_tag(pointer, result) }, 0); + auth.with_resource(|state, ordinary, registry| { + assert!(ordinary.resource_failure().is_none()); + assert_eq!(state.counts(ordinary, registry).ordinary_empty, 0); + ordinary + .drop_resource_ordinary_companion(result) + .map_err(ordinary_error)?; + assert_eq!(state.counts(ordinary, registry).ordinary_empty, 1); + assert!(ordinary.drop_resource_ordinary_companion(result).is_err()); + state.borrow_end(5, frame, loan)?; + state.close_or_drop(registry, 6, frame, owner)?; + state.end_operation_frame(ordinary, registry, frame) + }) + .unwrap(); + assert_eq!(complete(auth, attempt, 0).selected_kind, 0); + assert_empty(auth); + assert_eq!(events(auth), vec![(503, 1), (503, 4), (503, 5)]); + }); +} + +#[path = "source_tests.rs"] +mod source_tests; + +#[test] +fn native_resource_replacement_preflight_keeps_both_owners_before_old_or_rhs_loan_refusal() { + for borrow_old in [false, true] { + context(|auth| { + install(auth, &[(1, 41, 0, ""), (1, 42, 0, "")], 1); + let (attempt, root, frame, network) = start(auth, ResourcePurpose::Runtime); + let original = construct(auth, frame, network, 41); + let old = auth + .with_resource(|state, ordinary, registry| { + state.transfer(ordinary, registry, 2, frame, original, root) + }) + .unwrap(); + let replacement = construct(auth, frame, network, 42); + auth.with_resource(|state, ordinary, registry| { + let NativeResourceEntry::Owner(borrowed) = state + .handles + .get(&if borrow_old { old } else { replacement }) + .unwrap() + else { + panic!("exact scripted owner"); + }; + let NativeResourceEntry::Frame(borrower) = state.handles.get(&frame).unwrap() + else { + panic!("exact operation frame"); + }; + let mut loan = + state + .custody + .begin_borrow(&borrowed.token, &borrower.token, registry)?; + let before = state.counts(ordinary, registry); + assert_eq!((before.owners, before.registry, before.loans), (2, 2, 1)); + let old_key = state + .custody + .validate_owned(&state.owner(old, 0)?.token, registry)?; + let rhs_key = state + .custody + .validate_owned(&state.owner(replacement, 1)?.token, registry)?; + assert_eq!( + state.replace(ordinary, registry, 10, frame, old, replacement), + Err(NativeResourceError::Custody(CustodyError::ActiveBorrow)) + ); + assert_eq!(state.counts(ordinary, registry), before); + assert_eq!( + state + .custody + .validate_owned(&state.owner(old, 0)?.token, registry)?, + old_key + ); + assert_eq!( + state + .custody + .validate_owned(&state.owner(replacement, 1)?.token, registry)?, + rhs_key + ); + assert_eq!( + state.provider.observe_events(|events| events + .iter() + .map(|event| (event.label, event.kind)) + .collect::>()), + vec![(41, 1), (42, 1)] + ); + state.custody.end_borrow(&mut loan)?; + let installed = state.replace(ordinary, registry, 10, frame, old, replacement)?; + assert!(!state.handles.contains_key(&old)); + assert!(!state.handles.contains_key(&replacement)); + assert_eq!( + state + .custody + .validate_owned(&state.owner(installed, 0)?.token, registry)?, + rhs_key + ); + assert_eq!( + ( + state.counts(ordinary, registry).owners, + registry.live_count() + ), + (1, 1) + ); + Ok(()) + }) + .unwrap(); + assert_eq!(events(auth), vec![(41, 1), (42, 1), (41, 5)]); + let completion = complete(auth, attempt, 0); + assert_eq!( + ( + completion.body_status, + completion.cleanup_status, + completion.selected_kind + ), + (0, 0, 0) + ); + assert_empty(auth); + assert_eq!(events(auth), vec![(41, 1), (42, 1), (41, 5), (42, 5)]); + }); + } +} + +#[test] +fn native_resource_replacement_preflight_stale_rhs_handle_has_no_finalizer_effect() { + context(|auth| { + install(auth, &[(1, 51, 0, ""), (1, 52, 0, "")], 1); + let (attempt, root, frame, network) = start(auth, ResourcePurpose::Runtime); + let stale = construct(auth, frame, network, 51); + let old = auth + .with_resource(|state, ordinary, registry| { + state.transfer(ordinary, registry, 2, frame, stale, root) + }) + .unwrap(); + let replacement = construct(auth, frame, network, 52); + auth.with_resource(|state, ordinary, registry| { + let before = state.counts(ordinary, registry); + assert_eq!((before.owners, before.registry), (2, 2)); + assert!(!state.handles.contains_key(&stale)); + assert_eq!( + state.replace(ordinary, registry, 10, frame, old, stale), + Err(NativeResourceError::InvalidHandle) + ); + assert_eq!(state.counts(ordinary, registry), before); + state + .custody + .validate_owned(&state.owner(old, 0)?.token, registry)?; + state + .custody + .validate_owned(&state.owner(replacement, 1)?.token, registry)?; + assert_eq!( + state.provider.observe_events(|events| events + .iter() + .map(|event| (event.label, event.kind)) + .collect::>()), + vec![(51, 1), (52, 1)] + ); + state.replace(ordinary, registry, 10, frame, old, replacement)?; + Ok(()) + }) + .unwrap(); + assert_eq!(events(auth), vec![(51, 1), (52, 1), (51, 5)]); + assert_eq!(complete(auth, attempt, 0).selected_kind, 0); + assert_empty(auth); + assert_eq!(events(auth), vec![(51, 1), (52, 1), (51, 5), (52, 5)]); + }); +} + +#[path = "borrowed_sum_tests.rs"] +mod borrowed_sum_tests; + +#[path = "ordinary_attempt_owner_tests.rs"] +mod ordinary_attempt_owners; +#[path = "ordinary_error_reentry_tests.rs"] +mod ordinary_error_reentry; + +#[path = "ordinary_reservation_failure_tests.rs"] +mod ordinary_reservation_failures; diff --git a/crates/jett_runtime/src/native_abi/values.rs b/crates/jett_runtime/src/native_abi/values.rs index 3b71fd5b..73a44441 100644 --- a/crates/jett_runtime/src/native_abi/values.rs +++ b/crates/jett_runtime/src/native_abi/values.rs @@ -20,6 +20,10 @@ mod debug_equal; mod debug_format; mod graphics_decode; pub mod interface_conversion; +mod ordinary_attempts; + +#[cfg(test)] +mod interface_argument_tests; pub type NativeHandle = u64; type Failure = (JettRuntimeStatusV1, &'static [u8]); @@ -36,6 +40,10 @@ const INVALID_STRUCT: Failure = ( JettRuntimeStatusV1::INVALID_ARGUMENT, b"invalid native struct handle or field", ); +const INVALID_INTERFACE_ARGUMENT: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"interface dispatch concrete argument owner mismatch", +); const INVALID_FUNCTION: Failure = ( JettRuntimeStatusV1::INVALID_ARGUMENT, b"invalid native function descriptor or label", @@ -44,6 +52,10 @@ const INVALID_ACTOR: Failure = ( JettRuntimeStatusV1::INVALID_ARGUMENT, b"invalid native actor handle or field", ); +const UNSUPPORTED_ACTOR_COMPARISON: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"unsupported binary operation on pending actor", +); const INVALID_CAPABILITY: Failure = ( JettRuntimeStatusV1::INVALID_ARGUMENT, b"invalid native capability authority or task", @@ -873,6 +885,12 @@ impl NativeBitfieldLayout { Ok(fields) } } +#[derive(Clone, Copy)] +struct NativeActorIdentity { + instance: NativeHandle, + ordinal: u64, +} + #[derive(Default)] pub(super) struct NativeValues { #[cfg(test)] @@ -886,7 +904,7 @@ pub(super) struct NativeValues { maps: HashMap, structs: HashMap, equalities: HashMap, - actors: HashMap, + actors: HashMap, capability_tasks: HashMap, opaque_capabilities: HashMap<&'static str, u64>, next_actor_ordinal: u64, @@ -903,6 +921,7 @@ pub(super) struct NativeValues { sums_destroyed: u64, bytes_created: u64, bytes_destroyed: u64, + resource_ordinary_channels: ordinary_attempts::ResourceOrdinaryChannels, failure: Option, dynamic_failure_message: Option>, // Diagnostic bytes only; this metadata retains no owned Jett values. @@ -1019,7 +1038,7 @@ impl NativeValues { return Ok(0); }; let suffix = if remaining == 1 { "sample" } else { "samples" }; - self.dynamic_failure_message = Some( + self.set_dynamic_failure_message( format!("{capability}: test provider has {remaining} unconsumed {suffix}").into_bytes(), ); Err(( @@ -1087,7 +1106,7 @@ impl NativeValues { let left = self.format_string(left)?; let right = self.format_string(right)?; let operation = if not_equal { "NotEq" } else { "Eq" }; - self.dynamic_failure_message = Some( + self.set_dynamic_failure_message( format!("unsupported binary operation: {left} {operation} {right}").into_bytes(), ); return Err(UNSUPPORTED_STRING_COMPARISON); @@ -1124,8 +1143,9 @@ impl NativeValues { let left = layout.format_value(self, left, layout.root)?; let right = layout.format_value(self, right, layout.root)?; let operation = if not_equal == 0 { "Eq" } else { "NotEq" }; - self.dynamic_failure_message = - Some(format!("unsupported binary operation: {left} {operation} {right}").into_bytes()); + self.set_dynamic_failure_message( + format!("unsupported binary operation: {left} {operation} {right}").into_bytes(), + ); Err(UNSUPPORTED_ENUM_COMPARISON) } fn same_string_value(&self, left: u64, right: u64) -> LeafResult { @@ -1143,8 +1163,9 @@ impl NativeValues { return Ok(0); } let value = self.format_string(id)?; - self.dynamic_failure_message = - Some(format!("Displayable.display returned {value} instead of string").into_bytes()); + self.set_dynamic_failure_message( + format!("Displayable.display returned {value} instead of string").into_bytes(), + ); Err(INVALID_DISPLAY_RESULT) } fn display_string(&mut self, id: u64) -> LeafResult { @@ -1231,8 +1252,9 @@ impl NativeValues { return Ok(0); } let pending = format_pending_value(&label, depth)?; - self.dynamic_failure_message = - Some(format!("expected function value, got {pending}").into_bytes()); + self.set_dynamic_failure_message( + format!("expected function value, got {pending}").into_bytes(), + ); Err(PENDING_FUNCTION_CALL) } fn debug_append(&mut self, builder: u64, label: &str, bits: u64, kind: u32) -> LeafResult { @@ -1271,6 +1293,15 @@ impl NativeValues { text.push_str(&value); Ok(0) } + /// Observe the selected ordinary payload without acquiring ownership. + /// Its exact type and stable backing belong to the checked MIR projection. + fn sum_payload_borrow(&self, value: u64, tag: u32) -> LeafResult { + let sum = self.sums.get(&value).ok_or(INVALID_SUM)?; + if tag > SUM_SUCCESS || sum.tag != tag || sum.pending_depth != 0 { + return Err(INVALID_SUM); + } + Ok(sum.bits) + } fn sum_handle_tag(&mut self, value: u64, layout: &[u8]) -> LeafResult { let sum = self.sums.get(&value).ok_or(INVALID_SUM)?; let tag = sum.tag; @@ -1280,7 +1311,7 @@ impl NativeValues { } let layout = NativeDebugLayout::parse(layout)?; let value = layout.format_value(self, value, layout.root)?; - self.dynamic_failure_message = Some( + self.set_dynamic_failure_message( format!("handle block requires a result or optional value, got {value}").into_bytes(), ); Err(( @@ -1441,7 +1472,7 @@ impl NativeValues { if let Some(message) = message { let mut full = prefix.to_vec(); full.extend_from_slice(message.as_bytes()); - self.dynamic_failure_message = Some(full); + self.set_dynamic_failure_message(full); return Err(INVALID_PENDING_HANDLE_CHECK); } Ok(0) @@ -1571,8 +1602,9 @@ impl NativeValues { let left = format_nothing(left)?; let right = format_nothing(right)?; let operation = if not_equal == 0 { "Eq" } else { "NotEq" }; - self.dynamic_failure_message = - Some(format!("unsupported binary operation: {left} {operation} {right}").into_bytes()); + self.set_dynamic_failure_message( + format!("unsupported binary operation: {left} {operation} {right}").into_bytes(), + ); Err(UNSUPPORTED_NOTHING_COMPARISON) } fn check_pending_scalar_binary( @@ -1591,7 +1623,7 @@ impl NativeValues { } let left = format_pending_value(&self.debug_value(left_bits, left_kind)?, left_depth)?; let right = format_pending_value(&self.debug_value(right_bits, right_kind)?, right_depth)?; - self.dynamic_failure_message = Some( + self.set_dynamic_failure_message( format!( "unsupported binary operation: {left} {} {right}", operation.label() @@ -1619,7 +1651,7 @@ impl NativeValues { 2 => format!("breakpoint condition must be bool, got {value}"), _ => return Err(INVALID_TRACE_LABEL), }; - self.dynamic_failure_message = Some(message.into_bytes()); + self.set_dynamic_failure_message(message.into_bytes()); Err(PENDING_BOOL_CONDITION) } fn byte_result(&mut self, decoded: Result, String>) -> LeafResult { @@ -1745,8 +1777,9 @@ impl NativeValues { return Err(INVALID_LIST); } if inner.pending_depth != 0 { - self.dynamic_failure_message = - Some(b"csv.__stringify expects list[list[string]]".to_vec()); + self.set_dynamic_failure_message( + b"csv.__stringify expects list[list[string]]".to_vec(), + ); return Err(INVALID_PENDING_HANDLE_CHECK); } let mut fields = Vec::new(); @@ -1786,8 +1819,9 @@ impl NativeValues { index: i64, fallback: Failure, ) -> LeafResult { - self.dynamic_failure_message = - Some(format!("{operation}: index {index} out of bounds").into_bytes()); + self.set_dynamic_failure_message( + format!("{operation}: index {index} out of bounds").into_bytes(), + ); Err(fallback) } fn check_list_arguments( @@ -1864,7 +1898,7 @@ impl NativeValues { if first_depth == 0 && second_depth == 0 && third_depth == 0 { return Ok(0); } - self.dynamic_failure_message = Some(message.to_vec()); + self.set_dynamic_failure_message(message.to_vec()); Err(( JettRuntimeStatusV1::INVALID_ARGUMENT, b"pending scalar intrinsic operand", @@ -1895,7 +1929,7 @@ impl NativeValues { if depth == 0 { return Ok(0); } - self.dynamic_failure_message = Some(message.to_vec()); + self.set_dynamic_failure_message(message.to_vec()); Err(( JettRuntimeStatusV1::INVALID_ARGUMENT, b"pending handle intrinsic operand", @@ -2826,7 +2860,7 @@ impl NativeValues { .ok_or(INVALID_HANDLE)?; if string.pending_depth != 0 { let value = format_pending_value(&string.text, string.pending_depth)?; - self.dynamic_failure_message = Some( + self.set_dynamic_failure_message( format!("string.__join requires a list of strings, found {value}").into_bytes(), ); return Err(INVALID_PENDING_HANDLE_CHECK); @@ -3156,39 +3190,101 @@ impl NativeValues { let Some(record) = self.structs.get(&id) else { return Err(INVALID_ACTOR); }; - if record.fields.iter().any(Option::is_none) || self.actors.contains_key(&id) { + if record.fields.iter().any(Option::is_none) + || record.pending_depth != 0 + || self.actors.contains_key(&id) + { return Err(INVALID_ACTOR); } let next = self.next_actor_ordinal.checked_add(1).ok_or(EXHAUSTED)?; - self.actors.insert(id, self.next_actor_ordinal); + self.actors.insert( + id, + NativeActorIdentity { + instance: id, + ordinal: self.next_actor_ordinal, + }, + ); self.next_actor_ordinal = next; Ok(id) } - fn run_actor(&mut self, id: u64) -> LeafResult { - if !self.actors.contains_key(&id) { + fn actor_identity_parts(&self, id: u64) -> LeafResult<(NativeActorIdentity, u64)> { + let identity = *self.actors.get(&id).ok_or(INVALID_ACTOR)?; + let record = self.structs.get(&id).ok_or(INVALID_ACTOR)?; + let base_identity = self.actors.get(&identity.instance).ok_or(INVALID_ACTOR)?; + let base = self.structs.get(&identity.instance).ok_or(INVALID_ACTOR)?; + if base_identity.instance != identity.instance + || base_identity.ordinal != identity.ordinal + || base.pending_depth != 0 + || base.fields.iter().any(Option::is_none) + || (id != identity.instance && (!record.fields.is_empty() || record.pending_depth == 0)) + { return Err(INVALID_ACTOR); } - let record = self.structs.get_mut(&id).ok_or(INVALID_ACTOR)?; - record.pending_depth = record.pending_depth.checked_add(1).ok_or(EXHAUSTED)?; - Ok(id) + Ok((identity, record.pending_depth)) } - fn join_actor(&mut self, id: u64) -> LeafResult { - if !self.actors.contains_key(&id) { + fn same_actor_identity(&self, left: u64, right: u64) -> LeafResult { + // Exact nominal actor types come from validated MIR/layout. IDs must + // still be registered in this context even when their bits are equal. + let (left_identity, left_depth) = self.actor_identity_parts(left)?; + let (right_identity, right_depth) = self.actor_identity_parts(right)?; + Ok(left_identity.instance == right_identity.instance && left_depth == right_depth) + } + fn equal_actor_identity(&mut self, left: u64, right: u64, not_equal: u32) -> LeafResult { + if not_equal > 1 { return Err(INVALID_ACTOR); } - let record = self.structs.get_mut(&id).ok_or(INVALID_ACTOR)?; - record.pending_depth = record.pending_depth.saturating_sub(1); - Ok(id) + let (left_identity, left_depth) = self.actor_identity_parts(left)?; + let (right_identity, right_depth) = self.actor_identity_parts(right)?; + if left_depth != 0 || right_depth != 0 { + let left = + format_pending_value(&format!("actor#{}", left_identity.ordinal), left_depth)?; + let right = + format_pending_value(&format!("actor#{}", right_identity.ordinal), right_depth)?; + let operation = if not_equal == 0 { "Eq" } else { "NotEq" }; + self.set_dynamic_failure_message( + format!("unsupported binary operation: {left} {operation} {right}").into_bytes(), + ); + return Err(UNSUPPORTED_ACTOR_COMPARISON); + } + Ok(u32::from( + (left_identity.instance == right_identity.instance) != (not_equal != 0), + )) + } + fn actor_pending_alias( + &mut self, + identity: NativeActorIdentity, + depth: u64, + ) -> LeafResult { + if depth == 0 { + return Ok(identity.instance); + } + self.actors.try_reserve(1).map_err(|_| EXHAUSTED)?; + let alias = self.new_struct(0)?; + self.structs + .get_mut(&alias) + .ok_or(INVALID_ACTOR)? + .pending_depth = depth; + self.actors.insert(alias, identity); + Ok(alias) + } + fn run_actor(&mut self, id: u64) -> LeafResult { + let (identity, depth) = self.actor_identity_parts(id)?; + self.actor_pending_alias(identity, depth.checked_add(1).ok_or(EXHAUSTED)?) + } + fn join_actor(&mut self, id: u64) -> LeafResult { + let (identity, depth) = self.actor_identity_parts(id)?; + self.actor_pending_alias(identity, depth.saturating_sub(1)) } fn check_actor_message(&mut self, id: u64) -> LeafResult { - let ordinal = *self.actors.get(&id).ok_or(INVALID_ACTOR)?; - let depth = self.structs.get(&id).ok_or(INVALID_ACTOR)?.pending_depth; + let (identity, depth) = self.actor_identity_parts(id)?; + let ordinal = identity.ordinal; if depth == 0 { return Ok(0); } let actor = format_pending_value(&format!("actor#{ordinal}"), depth)?; - self.dynamic_failure_message = - Some(format!("send/ask: expected actor value, got {actor}").into_bytes()); + self.set_dynamic_failure_message( + format!("send/ask: expected actor value, got {actor}").into_bytes(), + ); Err(PENDING_ACTOR_MESSAGE) } fn replace_actor_field( @@ -3816,7 +3912,7 @@ impl NativeValues { nodes: vec![NativeDebugNode::TypeConstruction], }; let value = layout.format_value(self, builder, 0)?; - self.dynamic_failure_message = Some( + self.set_dynamic_failure_message( format!("type.{operation}: first argument must be TypeConstruction, got {value}") .into_bytes(), ); @@ -4147,7 +4243,7 @@ impl NativeValues { } else { message.to_owned() }; - self.dynamic_failure_message = Some(message.into_bytes()); + self.set_dynamic_failure_message(message.into_bytes()); Err(INVALID_STRUCT) } fn enum_equal(&self, left: u64, right: u64, layout: &[u8]) -> LeafResult { @@ -4231,7 +4327,7 @@ impl NativeValues { } else { return Ok(index as u64); }; - self.dynamic_failure_message = Some(message.into_bytes()); + self.set_dynamic_failure_message(message.into_bytes()); Err(INVALID_TYPE_ARG_INDEX) } fn reflected_field_owner_label(&self, field: u64, mismatch: Failure) -> LeafResult { @@ -4259,7 +4355,7 @@ impl NativeValues { .pending_type_field_message(actual, metadata_layout, caller) .map_err(|_| mismatch)? { - self.dynamic_failure_message = Some(message.into_bytes()); + self.set_dynamic_failure_message(message.into_bytes()); return Err(mismatch); } let actual_owner = self.text(self.struct_field(actual, 1)?.bits)?; @@ -4281,7 +4377,7 @@ impl NativeValues { || expected_owner.to_owned(), |member| format!("{expected_owner}.{member}"), ); - self.dynamic_failure_message = Some( + self.set_dynamic_failure_message( format!( "{caller}: field metadata belongs to '{actual_label}', expected '{expected_label}'" ) @@ -4338,7 +4434,7 @@ impl NativeValues { 4 => ("type.machine_field_value", "machine"), _ => return Err(INVALID_REFLECTED_OWNER), }; - self.dynamic_failure_message = Some( + self.set_dynamic_failure_message( format!("{caller}: expected {expected} value for '{owner_name}', got {actual}") .into_bytes(), ); @@ -4370,7 +4466,7 @@ impl NativeValues { if !owner_type_matches || !owner_member_matches { let actual_owner = self.reflected_field_owner_label(actual, mismatch)?; let expected_owner = self.reflected_field_owner_label(expected, mismatch)?; - self.dynamic_failure_message = Some( + self.set_dynamic_failure_message( format!( "{caller}: field metadata belongs to '{actual_owner}', expected '{expected_owner}'" ) @@ -4404,7 +4500,7 @@ impl NativeValues { if compatible == 0 { let name = self.text(self.struct_field(expected, 3)?.bits)?; let field_type = self.text(self.struct_field(expected, 4)?.bits)?; - self.dynamic_failure_message = Some( + self.set_dynamic_failure_message( format!( "{caller}: field '{name}' has type '{field_type}', requested '{requested_type}'" ) @@ -4859,9 +4955,264 @@ fn native_lines(text: &str) -> Vec { parts } fn next_identity() -> LeafResult { + reserve_native_identities(1)?.take() +} + +// Resource machinery samples the ordinary channel; it never takes or resets it. +pub(super) struct ResourceOrdinaryFailure<'a> { + pub(super) status: JettRuntimeStatusV1, + pub(super) message: &'a [u8], + pub(super) prefix: Option<&'a [u8]>, +} + +pub(super) struct ReservedNativeIdentities { + next: u64, + end: u64, +} + +pub(super) fn reserve_native_identities(count: u64) -> LeafResult { static NEXT: AtomicU64 = AtomicU64::new(1); - NEXT.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| n.checked_add(1)) - .map_err(|_| EXHAUSTED) + if count == 0 { + return Err(EXHAUSTED); + } + let next = NEXT + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |next| { + if next == 0 { + None + } else { + next.checked_add(count) + } + }) + .map_err(|_| EXHAUSTED)?; + Ok(ReservedNativeIdentities { + next, + end: next + count, + }) +} + +impl ReservedNativeIdentities { + pub(super) fn take(&mut self) -> LeafResult { + if self.next == 0 || self.next >= self.end { + return Err(EXHAUSTED); + } + let value = self.next; + self.next += 1; + Ok(value) + } +} + +// A staged record cannot be created from an independently supplied Network bit. +pub(super) struct PreparedResourceNetwork { + authority: u64, +} + +impl NativeValues { + pub(super) fn resource_failure(&self) -> Option> { + self.current_failure() + .map(|(status, message)| ResourceOrdinaryFailure { + status, + message: self.current_dynamic_failure_message().unwrap_or(message), + prefix: self.current_property_case_context(), + }) + } + + pub(super) fn prepare_resource_network( + &mut self, + identities: &mut ReservedNativeIdentities, + ) -> LeafResult { + if self.opaque_capabilities.contains_key("Network") { + return Err(INVALID_HANDLE); + } + self.opaque_capabilities + .try_reserve(1) + .map_err(|_| EXHAUSTED)?; + Ok(PreparedResourceNetwork { + authority: identities.take()?, + }) + } + + pub(super) fn commit_resource_network(&mut self, prepared: PreparedResourceNetwork) -> u64 { + let authority = prepared.authority; + // The caller holds the same state mutex from preparation through publication. + self.opaque_capabilities.insert("Network", authority); + authority + } + + /// Check ordinary carriers at the exact installed schema. This cannot recover + /// Resource tokens: occupied shapes are refused and the Resource table is separate. + pub(super) fn validate_resource_ordinary( + &self, + value: u64, + layout: &crate::resource_custody::RegisteredNativeLayout, + shape: u32, + ) -> LeafResult<()> { + use crate::resource_custody::NativeShape; + let mut shape = shape; + let mut value = value; + loop { + match layout.shapes().get(shape as usize).ok_or(INVALID_HANDLE)? { + NativeShape::Integer { .. } | NativeShape::Float { .. } => return Ok(()), + NativeShape::Bool if value <= 1 => return Ok(()), + NativeShape::Nothing if value == 0 => return Ok(()), + NativeShape::String + if self + .strings + .get(&value) + .is_some_and(|s| s.references > 0 && s.pending_depth == 0) => + { + return Ok(()); + } + NativeShape::Network if self.validates_resource_network(value) => return Ok(()), + NativeShape::HookDescriptor { .. } => return Err(INVALID_HANDLE), + NativeShape::Optional { child } => { + let sum = self.sums.get(&value).ok_or(INVALID_HANDLE)?; + if sum.pending_depth != 0 || sum.payload_pending_depth != 0 || sum.tag > 1 { + return Err(INVALID_HANDLE); + } + if sum.tag == 0 { + return if !sum.owned && sum.bits == 0 { + Ok(()) + } else { + Err(INVALID_HANDLE) + }; + } + let payload = layout.shapes().get(*child as usize).ok_or(INVALID_HANDLE)?; + let owned = matches!( + payload, + NativeShape::String + | NativeShape::Optional { .. } + | NativeShape::Result { .. } + ); + if sum.owned != owned { + return Err(INVALID_HANDLE); + } + shape = *child; + value = sum.bits; + } + NativeShape::Result { ok, fail } => { + let sum = self.sums.get(&value).ok_or(INVALID_HANDLE)?; + if sum.pending_depth != 0 || sum.payload_pending_depth != 0 || sum.tag > 1 { + return Err(INVALID_HANDLE); + } + shape = if sum.tag == SUM_SUCCESS { *ok } else { *fail }; + let payload = layout.shapes().get(shape as usize).ok_or(INVALID_HANDLE)?; + let owned = matches!( + payload, + NativeShape::String + | NativeShape::Optional { .. } + | NativeShape::Result { .. } + ); + if sum.owned != owned { + return Err(INVALID_HANDLE); + } + value = sum.bits; + } + _ => return Err(INVALID_HANDLE), + } + } + } + + /// Copy only the exact ordinary String failure arm; never clone Resource data. + pub(super) fn clone_resource_string_companion( + &mut self, + layout: &crate::resource_custody::RegisteredNativeLayout, + shape: u32, + value: u64, + ) -> LeafResult { + if !matches!( + layout.shapes().get(shape as usize), + Some(crate::resource_custody::NativeShape::String) + ) { + return Err(INVALID_HANDLE); + } + self.validate_resource_ordinary(value, layout, shape)?; + self.clone_value(value) + } + + pub(super) fn drop_resource_typed_companion( + &mut self, + layout: &crate::resource_custody::RegisteredNativeLayout, + shape: u32, + value: u64, + ) -> LeafResult<()> { + use crate::resource_custody::NativeShape; + self.validate_resource_ordinary(value, layout, shape)?; + match layout.shapes().get(shape as usize).ok_or(INVALID_HANDLE)? { + NativeShape::String | NativeShape::Optional { .. } | NativeShape::Result { .. } => { + self.drop_value(value).map(|_| ()) + } + NativeShape::Integer { .. } + | NativeShape::Float { .. } + | NativeShape::Bool + | NativeShape::Nothing + | NativeShape::Network => Ok(()), + _ => Err(INVALID_HANDLE), + } + } + + pub(super) fn validates_resource_network(&self, authority: u64) -> bool { + authority != 0 && self.opaque_capabilities.get("Network") == Some(&authority) + } + + pub(super) fn prepare_resource_ordinary_output( + &mut self, + ) -> LeafResult { + #[cfg(test)] + self.allocation_checkpoint()?; + self.strings.try_reserve(1).map_err(|_| EXHAUSTED)?; + self.sums.try_reserve(1).map_err(|_| EXHAUSTED)?; + self.sums_created.checked_add(1).ok_or(EXHAUSTED)?; + reserve_native_identities(2) + } + + pub(super) fn publish_resource_error( + &mut self, + text: String, + identities: &mut ReservedNativeIdentities, + ) -> LeafResult { + let id = identities.take()?; + self.strings.insert( + id, + NativeString { + text, + references: 1, + pending_depth: 0, + }, + ); + Ok(id) + } + + pub(super) fn publish_resource_borrow_result( + &mut self, + result: Result, + identities: &mut ReservedNativeIdentities, + ) -> LeafResult { + let (tag, bits, owned) = match result { + Ok(value) => (SUM_SUCCESS, value as u64, false), + Err(text) => ( + SUM_FAILURE, + self.publish_resource_error(text, identities)?, + true, + ), + }; + let id = identities.take()?; + self.sums.insert( + id, + NativeSum { + tag, + bits, + owned, + pending_depth: 0, + payload_pending_depth: 0, + }, + ); + self.sums_created += 1; + Ok(id) + } + + pub(super) fn drop_resource_ordinary_companion(&mut self, handle: u64) -> LeafResult<()> { + self.drop_value(handle).map(|_| ()) + } } // Hold the existing context lease through the operation. Cleanup is permitted @@ -4904,7 +5255,7 @@ fn leaf( let Some(state) = state.as_mut() else { return T::failure_default(); }; - if state.values.failure.is_some() && !cleanup { + if state.values.operation_failure().is_some() && !cleanup { return T::failure_default(); } match catch_unwind(AssertUnwindSafe(|| operation(&mut state.values))) { @@ -4919,7 +5270,7 @@ fn leaf( Ok(Ok(_)) => unreachable!(), }; state.values.cleanup_failed |= cleanup; - state.values.failure.get_or_insert(error); + state.values.record_ordinary_failure(error); T::failure_default() } } @@ -5100,7 +5451,7 @@ macro_rules! leaves { pub fn result(self) -> AbiScalar { match self { $( Self::$variant => AbiScalar::$retabi, )* } } } $( - /// Typed leaf operation; borrowed inputs, owned handle results. + /// Typed leaf operation; result ownership follows its operation contract. /// # Safety /// Context must be readable, stationary and live for the call. /// Pointer/length inputs must describe one readable allocation. @@ -5173,6 +5524,13 @@ leaves! { s.builder_finish(builder, owner) }; FunctionAdapter, jett_rt_v1_function_adapter, false, (source: u64 => I64, code: u64 => I64), u64 => I64, |s| s.function_adapter(source, code); + InterfaceValidate, jett_rt_v1_interface_validate, false, (value: u64 => I64, layout: *const u8 => Pointer, length: u64 => I64), u32 => I32, + |s| { let length = usize::try_from(length).map_err(|_| INVALID_STRUCT)?; + if length > isize::MAX as usize || layout.is_null() { return Err(INVALID_STRUCT); } + let bytes = unsafe { std::slice::from_raw_parts(layout, length) }; + let conversion = interface_conversion::NativeInterfaceConversion::parse(bytes)?; + conversion.validate_owners(s, NativeField { bits: value, owned: true, pending_depth: 0 })?; + Ok(0) }; InterfaceConvert, jett_rt_v1_interface_convert, false, (value: u64 => I64, layout: *const u8 => Pointer, length: u64 => I64), u64 => I64, |s| { let length = usize::try_from(length).map_err(|_| INVALID_STRUCT)?; if length > isize::MAX as usize || layout.is_null() { return Err(INVALID_STRUCT); } @@ -5198,7 +5556,7 @@ leaves! { InterfacePendingDepth, jett_rt_v1_interface_pending_depth, false, (value: u64 => I64), u64 => I64, |s| s.structs.get(&value).map(|v| v.pending_depth).ok_or(INVALID_STRUCT); RuntimeFailMessage, jett_rt_v1_runtime_fail_message, false, (message: u64 => I64), u32 => I32, - |s| { s.dynamic_failure_message = Some(s.text(message)?.as_bytes().to_vec()); + |s| { s.set_dynamic_failure_message(s.text(message)?.as_bytes().to_vec()); Err((JettRuntimeStatusV1::INVALID_ARGUMENT, b"native runtime failure")) }; StructNew, jett_rt_v1_struct_new, false, (count: u64 => I64), u64 => I64, |s| s.new_struct(count); @@ -5226,6 +5584,8 @@ leaves! { |s| Ok(s.struct_field(value, index)?.pending_depth); ActorRegister, jett_rt_v1_actor_register, false, (value: u64 => I64), u64 => I64, |s| s.register_actor(value); + ActorIdentityEqual, jett_rt_v1_actor_identity_equal, false, (left: u64 => I64, right: u64 => I64, not_equal: u32 => I32), u32 => I32, + |s| s.equal_actor_identity(left, right, not_equal); ActorRun, jett_rt_v1_actor_run, false, (value: u64 => I64), u64 => I64, |s| s.run_actor(value); ActorTaskJoin, jett_rt_v1_actor_task_join, false, (value: u64 => I64), u64 => I64, @@ -5298,7 +5658,7 @@ leaves! { if length > isize::MAX as usize || (length != 0 && layout.is_null()) { return Err(mismatch); } let layout = if length == 0 { &[][..] } else { unsafe { std::slice::from_raw_parts(layout, length) } }; if let Some(message) = s.pending_type_field_message(metadata, layout, caller).map_err(|_| mismatch)? { - s.dynamic_failure_message = Some(message.into_bytes()); + s.set_dynamic_failure_message(message.into_bytes()); Err(mismatch) } else { Ok(0) @@ -5601,6 +5961,9 @@ leaves! { s.sum_handle_tag(value, layout) }; SumPayloadPendingDepth, jett_rt_v1_sum_payload_pending_depth, false, (value: u64 => I64), u64 => I64, |s| s.sums.get(&value).map(|v| v.payload_pending_depth).ok_or(INVALID_SUM); + // The selected payload remains borrowed from the unchanged owning sum. + SumPayloadBorrow, jett_rt_v1_sum_payload_borrow, false, (value: u64 => I64, tag: u32 => I32), u64 => I64, + |s| s.sum_payload_borrow(value, tag); SumTake, jett_rt_v1_sum_take, false, (value: u64 => I64, tag: u32 => I32), u64 => I64, |s| { if s.sums.get(&value).is_none_or(|v| v.tag != tag) { return Err(INVALID_SUM); } let sum = s.sums.remove(&value).ok_or(INVALID_SUM)?; @@ -5779,14 +6142,14 @@ leaves! { else { Ok(value.clamp(lower, upper)) } }; Status, jett_rt_v1_value_status, true, (), u32 => I32, - |s| Ok(s.failure.map_or(0, |e| e.0.code())); + |s| Ok(s.current_failure().map_or(0, |e| e.0.code())); FailureTakePrefixedText, jett_rt_v1_failure_take_prefixed_text, true, (prefix: *const u8 => Pointer, length: u64 => I64), u64 => I64, |s| { let length = usize::try_from(length).map_err(|_| INVALID_FAILURE_COPY)?; if length > isize::MAX as usize || (length != 0 && prefix.is_null()) { return Err(INVALID_FAILURE_COPY); } - if s.cleanup_failed { return Err(INVALID_FAILURE_COPY); } - let (_, static_message) = s.failure.ok_or(INVALID_FAILURE_COPY)?; + if s.cleanup_failed || !s.can_capture_refinement_failure() { return Err(INVALID_FAILURE_COPY); } + let (_, static_message) = s.current_failure().ok_or(INVALID_FAILURE_COPY)?; let prefix = if length == 0 { &[][..] } else { unsafe { std::slice::from_raw_parts(prefix, length) } }; - let message = s.dynamic_failure_message.as_deref().unwrap_or(static_message); + let message = s.current_dynamic_failure_message().unwrap_or(static_message); let prefix = std::str::from_utf8(prefix).map_err(|_| INVALID_FAILURE_COPY)?; let message = std::str::from_utf8(message).map_err(|_| INVALID_FAILURE_COPY)?; let mut text = String::new(); @@ -5794,8 +6157,7 @@ leaves! { text.push_str(prefix); text.push_str(message); let value = s.insert(text)?; - s.failure = None; - s.dynamic_failure_message = None; + s.finish_refinement_failure_capture(); Ok(value) }; RefinementPendingBoolText, jett_rt_v1_refinement_pending_bool_text, true, (bits: u64 => I64, depth: u64 => I64, name: *const u8 => Pointer, length: u64 => I64), u64 => I64, |s| { let length = usize::try_from(length).map_err(|_| INVALID_FAILURE_COPY)?; @@ -5806,9 +6168,9 @@ leaves! { let value = format_pending_value(value, depth)?; s.insert(format!("refinement constraint for '{name}' must return bool, got {value}")) }; PropertyCaseSet, jett_rt_v1_property_case_set, false, (message: u64 => I64), u32 => I32, - |s| { s.property_case_context = Some(s.text(message)?.as_bytes().to_vec()); Ok(0) }; + |s| { s.set_property_case_context(Some(s.text(message)?.as_bytes().to_vec())); Ok(0) }; PropertyCaseClear, jett_rt_v1_property_case_clear, false, (), u32 => I32, - |s| { s.property_case_context = None; Ok(0) }; + |s| { s.set_property_case_context(None); Ok(0) }; AssertFail, jett_rt_v1_assert_fail, false, (), u32 => I32, |_s| Err((JettRuntimeStatusV1::INVALID_ARGUMENT, b"assertion failed")); AssertFailMessage, jett_rt_v1_assert_fail_message, false, (message: u64 => I64), u32 => I32, @@ -5816,7 +6178,7 @@ leaves! { let mut owned = Vec::new(); owned.try_reserve_exact(text.len()).map_err(|_| EXHAUSTED)?; owned.extend_from_slice(text); - s.dynamic_failure_message = Some(owned); + s.set_dynamic_failure_message(owned); Err((JettRuntimeStatusV1::INVALID_ARGUMENT, b"assertion failed")) }; Retain, jett_rt_v1_string_retain, false, (value: u64 => I64), u64 => I64, |s| s.retain(value); @@ -6164,7 +6526,7 @@ pub unsafe extern "C" fn jett_rt_v1_value_failure( } }; let state = lock_unpoisoned(&lease.entry.state); - match state.as_ref().and_then(|s| s.values.failure) { + match state.as_ref().and_then(|s| s.values.current_failure()) { Some((status, message)) => JettRuntimeResultV1::failure(status, message), None => JettRuntimeResultV1::ok(), } @@ -6223,15 +6585,13 @@ pub unsafe extern "C" fn jett_rt_v1_value_failure_copy( }; let message = state .values - .dynamic_failure_message - .as_deref() - .or_else(|| state.values.failure.map(|(_, message)| message)) + .current_dynamic_failure_message() + .or_else(|| state.values.current_failure().map(|(_, message)| message)) .unwrap_or_default(); - let prefix = if state.values.failure.is_some() { + let prefix = if state.values.current_failure().is_some() { state .values - .property_case_context - .as_deref() + .current_property_case_context() .unwrap_or_default() } else { &[] @@ -6284,6 +6644,8 @@ mod tests { use super::*; use std::mem::MaybeUninit; + include!("values/actor_identity_tests.rs"); + #[test] fn float_remainder_leaf_preserves_ieee_values_without_runtime_failure() { let context = Context::new(); @@ -7638,7 +8000,7 @@ mod tests { #[test] fn display_result_check_preserves_strings_and_requires_every_pending_layer_to_join() { - for text in ["", "shown", "é🦀", "pending(shown)"] { + for text in ["", "shown", "é🦀", "pending(shown)"] { let mut values = NativeValues::default(); let ready = values.insert(text.into()).unwrap(); let alias = values.retain(ready).unwrap(); @@ -7730,7 +8092,7 @@ mod tests { #[test] fn display_result_check_leaf_preserves_ownership_status_and_first_failure() { - for text in ["", "shown", "é🦀"] { + for text in ["", "shown", "é🦀"] { for (selected, rendered) in [ (0, None), (1, Some(format!("pending({text})"))), @@ -8266,7 +8628,7 @@ mod tests { #[test] fn strings_release_immediately_and_retain_preserves_aliases() { let context = Context::new(); - let value = context.text("hé\0llo"); + let value = context.text("hé\0llo"); assert_ne!(value, 0); assert_eq!(context.count(), 1); unsafe { @@ -8431,16 +8793,16 @@ mod tests { if clear { assert_eq!(jett_rt_v1_property_case_clear(context.pointer()), 0); } - let message = context.text("backend 🧪"); + let message = context.text("backend 🧪"); assert_ne!( jett_rt_v1_assert_fail_message(context.pointer(), message), 0 ); assert_eq!(jett_rt_v1_string_release(context.pointer(), message), 0); let expected = if clear { - "backend 🧪" + "backend 🧪" } else { - "property 'target' trial 4: backend 🧪" + "property 'target' trial 4: backend 🧪" }; for _ in 0..2 { assert_eq!( @@ -8487,7 +8849,7 @@ mod tests { #[test] fn native_assert_failure_copies_dynamic_message_without_changing_v1_static_result() { let context = Context::new(); - let message = "expected 42, got 🧪"; + let message = "expected 42, got 🧪"; let handle = context.text(message); unsafe { assert_ne!(jett_rt_v1_assert_fail_message(context.pointer(), handle), 0); @@ -10198,7 +10560,7 @@ mod tests { let mut values = NativeValues::default(); let zebra = values.insert("zebra".into()).unwrap(); let apple = values.insert("apple".into()).unwrap(); - let eclair = values.insert("éclair".into()).unwrap(); + let eclair = values.insert("éclair".into()).unwrap(); let list = values.new_list(true).unwrap(); values.lists.get_mut(&list).unwrap().elements = vec![Some(zebra), Some(eclair), Some(apple)]; @@ -10710,3 +11072,9 @@ mod tests { #[cfg(test)] mod reflected_container_tests; + +#[cfg(test)] +mod resource_companion_tests; + +#[cfg(test)] +mod ordinary_borrowed_sum_tests; diff --git a/crates/jett_runtime/src/native_abi/values/actor_identity_tests.rs b/crates/jett_runtime/src/native_abi/values/actor_identity_tests.rs new file mode 100644 index 00000000..369ab152 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/actor_identity_tests.rs @@ -0,0 +1,219 @@ +mod actor_identity { + use super::*; + + fn actor(values: &mut NativeValues, state: u64) -> u64 { + let id = values.new_struct(1).unwrap(); + values.structs.get_mut(&id).unwrap().fields[0] = Some(NativeField { + bits: state, + owned: false, + pending_depth: 0, + }); + values.register_actor(id).unwrap() + } + + #[test] + fn native_actor_identity_aliases_equal_and_distinct_equal_state_actors_differ() { + let mut values = NativeValues::default(); + let first = actor(&mut values, 7); + let second = actor(&mut values, 7); + assert_ne!(first, second); + for (left, right, equal) in [(first, first, true), (first, second, false)] { + assert_eq!(values.same_actor_identity(left, right), Ok(equal)); + assert_eq!( + values.equal_actor_identity(left, right, 0), + Ok(u32::from(equal)) + ); + assert_eq!( + values.equal_actor_identity(left, right, 1), + Ok(u32::from(!equal)) + ); + } + values.replace_actor_field(first, 0, 88, false).unwrap(); + assert_eq!(values.equal_actor_identity(first, first, 0), Ok(1)); + assert_eq!(values.equal_actor_identity(first, second, 0), Ok(0)); + values.release_actors().unwrap(); + assert_eq!(values.structs_created, 2); + assert_eq!(values.structs_destroyed, 2); + assert!(values.is_empty()); + } + + #[test] + fn native_actor_identity_validates_current_context_even_for_equal_bits() { + let mut values = NativeValues::default(); + let mut other_context = NativeValues::default(); + let live = actor(&mut values, 7); + let foreign = actor(&mut other_context, 7); + let unregistered = values.new_struct(0).unwrap(); + assert_ne!(live, foreign); + for invalid in [0, foreign, unregistered] { + assert_eq!( + values.same_actor_identity(invalid, invalid), + Err(INVALID_ACTOR) + ); + assert_eq!( + values.equal_actor_identity(live, invalid, 0), + Err(INVALID_ACTOR) + ); + assert_eq!( + values.equal_actor_identity(invalid, live, 1), + Err(INVALID_ACTOR) + ); + } + assert_eq!( + values.equal_actor_identity(live, live, 2), + Err(INVALID_ACTOR) + ); + values.drop_value(unregistered).unwrap(); + values.release_actors().unwrap(); + assert_eq!(values.same_actor_identity(live, live), Err(INVALID_ACTOR)); + other_context.release_actors().unwrap(); + assert!(values.is_empty()); + assert!(other_context.is_empty()); + } + + #[test] + fn native_actor_identity_requires_a_complete_live_backing_record() { + let mut values = NativeValues::default(); + let live = actor(&mut values, 7); + let saved = values.structs.get_mut(&live).unwrap().fields[0].take(); + assert_eq!(values.same_actor_identity(live, live), Err(INVALID_ACTOR)); + values.structs.get_mut(&live).unwrap().fields[0] = saved; + values.release_actors().unwrap(); + assert!(values.is_empty()); + } + + #[test] + fn native_actor_identity_nested_layout_validates_ids_and_compares_pending_depth() { + let mut values = NativeValues::default(); + let first = actor(&mut values, 7); + let second = actor(&mut values, 7); + let layout = NativeDebugLayout { + root: 0, + nodes: vec![NativeDebugNode::Actor], + }; + assert_eq!(layout.equal_value(&values, first, first, 0), Ok(true)); + assert_eq!(layout.equal_value(&values, first, second, 0), Ok(false)); + assert_eq!(layout.equal_value(&values, 0, 0, 0), Err(INVALID_ACTOR)); + let pending = values.run_actor(first).unwrap(); + let also_pending = values.run_actor(first).unwrap(); + let twice = values.run_actor(pending).unwrap(); + assert_ne!(pending, also_pending); + assert_eq!(values.equal_actor_identity(first, first, 0), Ok(1)); + assert_eq!( + layout.equal_value(&values, pending, also_pending, 0), + Ok(true) + ); + assert_eq!(layout.equal_value(&values, first, pending, 0), Ok(false)); + assert_eq!(layout.equal_value(&values, pending, twice, 0), Ok(false)); + let once = values.join_actor(twice).unwrap(); + assert_eq!(layout.equal_value(&values, pending, once, 0), Ok(true)); + assert_eq!(values.join_actor(once), Ok(first)); + assert_eq!(values.structs[&first].fields[0].unwrap().bits, 7); + values.release_actors().unwrap(); + assert!(values.is_empty()); + for (operator, label) in [(0, "Eq"), (1, "NotEq")] { + // Each operation needs a fresh ordinary failure channel. The first + // failure is persistent within an owning context. + let mut values = NativeValues::default(); + let instance = actor(&mut values, 7); + let pending = values.run_actor(instance).unwrap(); + assert_eq!( + values.equal_actor_identity(pending, pending, operator), + Err(UNSUPPORTED_ACTOR_COMPARISON) + ); + assert_eq!( + values.dynamic_failure_message.as_deref(), + Some( + format!( + "unsupported binary operation: pending(actor#0) {label} pending(actor#0)" + ) + .as_bytes() + ) + ); + values.release_actors().unwrap(); + assert!(values.is_empty()); + } + } + + #[test] + fn native_actor_identity_leaf_preserves_context_failure_on_foreign_ids() { + let context = Context::new(); + let foreign_context = Context::new(); + unsafe { + let local = jett_rt_v1_struct_new(context.pointer(), 0); + let foreign = jett_rt_v1_struct_new(foreign_context.pointer(), 0); + assert_eq!(jett_rt_v1_actor_register(context.pointer(), local), local); + assert_eq!( + jett_rt_v1_actor_register(foreign_context.pointer(), foreign), + foreign + ); + assert_eq!( + jett_rt_v1_actor_identity_equal(context.pointer(), local, local, 0), + 1 + ); + assert_eq!(jett_rt_v1_value_status(context.pointer()), 0); + assert_eq!( + jett_rt_v1_actor_identity_equal(context.pointer(), foreign, foreign, 0), + JettRuntimeStatusV1::INVALID_CONTEXT.code() + ); + assert_eq!( + jett_rt_v1_value_status(context.pointer()), + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + assert_eq!(jett_rt_v1_value_status(foreign_context.pointer()), 0); + } + // The foreign handle error remains observable until retirement; clean + // context retirement releases the registered actor and returns OK. + context.destroy(JettRuntimeStatusV1::OK); + foreign_context.destroy(JettRuntimeStatusV1::OK); + } + + #[test] + fn native_actor_identity_pending_allocation_failure_preserves_ready_aliases() { + let mut values = NativeValues::default(); + let live = actor(&mut values, 7); + values.allocation_budget = Some(0); + assert_eq!(values.run_actor(live), Err(EXHAUSTED)); + assert_eq!(values.equal_actor_identity(live, live, 0), Ok(1)); + assert_eq!(values.structs_created, 1); + assert_eq!(values.actors.len(), 1); + values.allocation_budget = None; + let pending = values.run_actor(live).unwrap(); + assert_eq!( + values.actor_identity_parts(pending).unwrap().0.instance, + live + ); + assert_eq!(values.actor_identity_parts(pending).unwrap().0.ordinal, 0); + assert_eq!(values.next_actor_ordinal, 1); + values.release_actors().unwrap(); + assert_eq!(values.structs_created, values.structs_destroyed); + assert!(values.is_empty()); + } + + #[test] + fn native_actor_identity_pending_aliases_retire_owned_state_once() { + let mut values = NativeValues::default(); + let text = values.insert("one owned state payload".into()).unwrap(); + let instance = values.new_struct(1).unwrap(); + values.structs.get_mut(&instance).unwrap().fields[0] = Some(NativeField { + bits: text, + owned: true, + pending_depth: 0, + }); + values.register_actor(instance).unwrap(); + let pending = values.run_actor(instance).unwrap(); + let nested = values.run_actor(pending).unwrap(); + assert_eq!(values.join_actor(pending), Ok(instance)); + assert_eq!( + values.actor_identity_parts(nested).unwrap().0.instance, + instance + ); + assert_eq!(values.strings[&text].references, 1); + assert_eq!(values.structs_created, 3); + assert_eq!(values.structs[&pending].fields.len(), 0); + assert_eq!(values.structs[&nested].fields.len(), 0); + values.release_actors().unwrap(); + assert_eq!(values.structs_created, values.structs_destroyed); + assert!(values.is_empty()); + } +} diff --git a/crates/jett_runtime/src/native_abi/values/debug_equal.rs b/crates/jett_runtime/src/native_abi/values/debug_equal.rs index cc575596..5d1b699a 100644 --- a/crates/jett_runtime/src/native_abi/values/debug_equal.rs +++ b/crates/jett_runtime/src/native_abi/values/debug_equal.rs @@ -335,6 +335,7 @@ impl Comparison<'_> { _ => left_bits == right_bits, }, ), + NativeDebugNode::Actor => values.same_actor_identity(left_bits, right_bits), NativeDebugNode::Nothing => Ok(left_bits == right_bits), NativeDebugNode::Bytes => values.same_bytes_value(left_bits, right_bits), NativeDebugNode::Alias(base) => { @@ -481,7 +482,6 @@ impl Comparison<'_> { | NativeDebugNode::Redacted | NativeDebugNode::Interface | NativeDebugNode::Function - | NativeDebugNode::Actor | NativeDebugNode::TypeConstruction => Err(INVALID_STRUCT), } } diff --git a/crates/jett_runtime/src/native_abi/values/debug_format.rs b/crates/jett_runtime/src/native_abi/values/debug_format.rs index ee8f2272..d9c1ce62 100644 --- a/crates/jett_runtime/src/native_abi/values/debug_format.rs +++ b/crates/jett_runtime/src/native_abi/values/debug_format.rs @@ -166,15 +166,9 @@ impl Formatter<'_> { self.append(values.function_debug_label(bits)?)?; } NativeDebugNode::Actor => { - let ordinal = values.actors.get(&bits).ok_or(INVALID_ACTOR)?; - self.pending( - values - .structs - .get(&bits) - .ok_or(INVALID_ACTOR)? - .pending_depth, - )?; - self.append(&format!("actor#{ordinal}"))?; + let (identity, depth) = values.actor_identity_parts(bits)?; + self.pending(depth)?; + self.append(&format!("actor#{}", identity.ordinal))?; } NativeDebugNode::TypeConstruction => { let builder = values.builders.get(&bits).ok_or(INVALID_CONSTRUCTION)?; diff --git a/crates/jett_runtime/src/native_abi/values/interface_argument_tests.rs b/crates/jett_runtime/src/native_abi/values/interface_argument_tests.rs new file mode 100644 index 00000000..2bf0c0f1 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/interface_argument_tests.rs @@ -0,0 +1,146 @@ +use super::*; +use interface_conversion::NativeInterfaceConversion as C; + +fn boxed_scalar(values: &mut NativeValues, owner: u64, depth: u64) -> u64 { + // Identity metadata is independent of the redacted payload's display. + let mut layout = b"JD\x01".to_vec(); + layout.extend_from_slice(&1_u32.to_le_bytes()); + layout.extend_from_slice(&0_u32.to_le_bytes()); + layout.extend_from_slice(&1_u32.to_le_bytes()); + layout.push(NativeDebugTag::Redacted as u8); + values + .interface_box(owner, 7, false, depth, &layout) + .unwrap() +} + +fn unbox(owner: u64) -> C { + C::Unbox { + concrete: owner, + owned: false, + nothing: false, + } +} + +fn input(bits: u64) -> NativeField { + NativeField { + bits, + owned: true, + pending_depth: 0, + } +} + +#[test] +fn interface_owner_preflight_rejects_exact_wrong_owner_without_conversion() { + let mut values = NativeValues::default(); + let original = boxed_scalar(&mut values, 10, 0); + let structs = values.structs.len(); + let bytes = values.bytes.len(); + values.allocation_budget = Some(0); + assert_eq!( + unbox(9).validate_owners(&values, input(original)), + Err(INVALID_INTERFACE_ARGUMENT) + ); + assert_eq!(values.structs.len(), structs); + assert_eq!(values.bytes.len(), bytes); + assert_eq!(values.allocation_budget, Some(0)); + assert_eq!(values.struct_field(original, 0).unwrap().bits, 10); + values.allocation_budget = None; + values.drop_value(original).unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn interface_owner_preflight_matching_pending_payload_preserves_depth_and_ownership() { + let mut values = NativeValues::default(); + let original = boxed_scalar(&mut values, 9, 2); + values.allocation_budget = Some(0); + assert_eq!(unbox(9).validate_owners(&values, input(original)), Ok(())); + assert_eq!(values.owned_pending_depth(original), Ok(2)); + assert_eq!(values.allocation_budget, Some(0)); + values.allocation_budget = None; + values.drop_value(original).unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn interface_owner_preflight_nested_list_optional_catches_later_mismatch_without_clones() { + let mut values = NativeValues::default(); + let matching = boxed_scalar(&mut values, 9, 0); + let wrong = boxed_scalar(&mut values, 10, 0); + let first = values.sum(SUM_SUCCESS, matching, true).unwrap(); + let second = values.sum(SUM_SUCCESS, wrong, true).unwrap(); + let list = values.new_list(true).unwrap(); + values.lists.get_mut(&list).unwrap().elements = vec![Some(first), Some(second)]; + let conversion = C::List(Box::new(C::Optional(Box::new(unbox(9))))); + let inventory = ( + values.lists.len(), + values.sums.len(), + values.structs.len(), + values.bytes.len(), + ); + values.allocation_budget = Some(0); + assert_eq!( + conversion.validate_owners(&values, input(list)), + Err(INVALID_INTERFACE_ARGUMENT) + ); + assert_eq!( + ( + values.lists.len(), + values.sums.len(), + values.structs.len(), + values.bytes.len() + ), + inventory + ); + assert_eq!(values.allocation_budget, Some(0)); + assert_eq!(values.struct_field(matching, 0).unwrap().bits, 9); + assert_eq!(values.struct_field(wrong, 0).unwrap().bits, 10); + values.allocation_budget = None; + values.drop_value(list).unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn interface_owner_preflight_skips_absent_and_inactive_owner_payloads() { + let mut values = NativeValues::default(); + let absent = values.sum(SUM_FAILURE, 0, false).unwrap(); + let error = values.sum(SUM_FAILURE, 17, false).unwrap(); + assert_eq!( + C::Optional(Box::new(unbox(9))).validate_owners(&values, input(absent)), + Ok(()) + ); + assert_eq!( + C::Result(Box::new(unbox(9)), Box::new(C::Copy { owned: false })) + .validate_owners(&values, input(error)), + Ok(()) + ); + values.drop_value(absent).unwrap(); + values.drop_value(error).unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn interface_owner_preflight_unavailable_container_does_not_read_children() { + let mut values = NativeValues::default(); + let source = values.new_list(true).unwrap(); + values.lists.get_mut(&source).unwrap().pending_depth = 1; + values.lists.get_mut(&source).unwrap().elements = vec![Some(u64::MAX)]; + assert_eq!( + C::List(Box::new(unbox(9))).validate_owners(&values, input(source)), + Err(INVALID_INTERFACE_ARGUMENT) + ); + values.lists.get_mut(&source).unwrap().elements.clear(); + values.drop_value(source).unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn interface_owner_preflight_preserves_missing_handle_guard() { + let values = NativeValues::default(); + assert_eq!( + unbox(9).validate_owners(&values, input(u64::MAX)), + Err(INVALID_STRUCT) + ); + assert!(!C::Copy { owned: true }.requires_owner_validation()); + assert!(!C::FunctionAdapter { code: 123 }.requires_owner_validation()); +} diff --git a/crates/jett_runtime/src/native_abi/values/interface_conversion.rs b/crates/jett_runtime/src/native_abi/values/interface_conversion.rs index b58409cd..31ee2d77 100644 --- a/crates/jett_runtime/src/native_abi/values/interface_conversion.rs +++ b/crates/jett_runtime/src/native_abi/values/interface_conversion.rs @@ -29,6 +29,124 @@ pub enum NativeInterfaceConversion { } impl NativeInterfaceConversion { + pub fn requires_owner_validation(&self) -> bool { + match self { + Self::Unbox { .. } => true, + Self::List(inner) | Self::Optional(inner) | Self::Map(inner, _) => { + inner.requires_owner_validation() + } + Self::Result(ok, error) => { + ok.requires_owner_validation() || error.requires_owner_validation() + } + Self::Copy { .. } | Self::Box { .. } | Self::FunctionAdapter { .. } => false, + } + } + + pub(super) fn validate_owners( + &self, + values: &NativeValues, + input: NativeField, + ) -> LeafResult<()> { + if !self.requires_owner_validation() { + return Ok(()); + } + match self { + Self::Unbox { + concrete, owned, .. + } => { + if !input.owned { + return Err(INVALID_STRUCT); + } + // Type metadata is available without inspecting, cloning or + // joining the concrete payload, including a pending payload. + if values.struct_field(input.bits, 0)?.bits != *concrete { + return Err(INVALID_INTERFACE_ARGUMENT); + } + if values.struct_field(input.bits, 1)?.owned != *owned { + return Err(INVALID_STRUCT); + } + Ok(()) + } + Self::List(element) => { + let list = values.lists.get(&input.bits).ok_or(INVALID_LIST)?; + if input.pending_depth != 0 || list.pending_depth != 0 { + return Err(INVALID_INTERFACE_ARGUMENT); + } + for (index, value) in list.elements.iter().enumerate() { + if let Some(bits) = value { + element.validate_owners( + values, + NativeField { + bits: *bits, + owned: list.owned, + pending_depth: list + .element_pending_depths + .get(&index) + .copied() + .unwrap_or(0), + }, + )?; + } + } + Ok(()) + } + Self::Map(element, strings) => { + let map = values.maps.get(&input.bits).ok_or(INVALID_MAP)?; + if input.pending_depth != 0 || map.pending_depth != 0 { + return Err(INVALID_INTERFACE_ARGUMENT); + } + if map.key_strings != *strings && map.entries.iter().any(Option::is_some) { + return Err(INVALID_MAP); + } + for entry in map.entries.iter().flatten() { + if entry.key_taken { + return Err(INVALID_MAP); + } + element.validate_owners( + values, + NativeField { + bits: entry.value, + owned: map.value_owned, + pending_depth: entry.value_pending_depth, + }, + )?; + } + Ok(()) + } + Self::Optional(ok) => Self::validate_sum_owners(values, input, ok, None), + Self::Result(ok, error) => Self::validate_sum_owners(values, input, ok, Some(error)), + Self::Copy { .. } | Self::Box { .. } | Self::FunctionAdapter { .. } => Ok(()), + } + } + + fn validate_sum_owners( + values: &NativeValues, + input: NativeField, + ok: &Self, + error: Option<&Self>, + ) -> LeafResult<()> { + let sum = values.sums.get(&input.bits).ok_or(INVALID_SUM)?; + if input.pending_depth != 0 || sum.pending_depth != 0 { + return Err(INVALID_INTERFACE_ARGUMENT); + } + let conversion = if sum.tag == SUM_SUCCESS { + Some(ok) + } else { + error + }; + if let Some(conversion) = conversion { + conversion.validate_owners( + values, + NativeField { + bits: sum.bits, + owned: sum.owned, + pending_depth: sum.payload_pending_depth, + }, + )?; + } + Ok(()) + } + pub fn encode(&self) -> Vec { self.encode_with_function_offsets().0 } diff --git a/crates/jett_runtime/src/native_abi/values/ordinary_attempts.rs b/crates/jett_runtime/src/native_abi/values/ordinary_attempts.rs new file mode 100644 index 00000000..818ff6f0 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/ordinary_attempts.rs @@ -0,0 +1,290 @@ +//! Context-owned ordinary first-error channels for exact Resource entry lifetimes. +//! Old records are sealed observations; a fresh constructor selects a new record. +use super::*; +#[cfg(test)] +use crate::native_abi::resource::ordinary_reservation_faults::{ + ReservationFaults, ReservationSite, +}; +use crate::native_abi::resource::{ResourceOrdinaryCompletion, ResourceOrdinaryOwner}; + +#[derive(Default)] +struct AttemptFailureChannel { + failure: Option, + dynamic_failure_message: Option>, + property_case_context: Option>, +} + +#[derive(Clone, Copy)] +enum OrdinaryAttemptPhase { + Running, + Sealed(ResourceOrdinaryCompletion), +} + +struct OrdinaryAttemptChannel { + owner: ResourceOrdinaryOwner, + phase: OrdinaryAttemptPhase, + error: AttemptFailureChannel, +} + +#[derive(Default)] +pub(super) struct ResourceOrdinaryChannels { + records: Vec, + selected: Option, + #[cfg(test)] + reservation_faults: ReservationFaults, +} + +/// Borrows the exact values state through Resource frame publication. Its private +/// fields and sole constructor prevent a raw ID from selecting a failure channel. +pub(in crate::native_abi) struct PreparedResourceOrdinaryAttempt<'a> { + values: &'a mut NativeValues, + owner: ResourceOrdinaryOwner, +} + +impl PreparedResourceOrdinaryAttempt<'_> { + pub(in crate::native_abi) fn publish(self) { + let index = self.values.resource_ordinary_channels.records.len(); + self.values + .resource_ordinary_channels + .records + .push(OrdinaryAttemptChannel { + owner: self.owner, + phase: OrdinaryAttemptPhase::Running, + error: AttemptFailureChannel::default(), + }); + self.values.resource_ordinary_channels.selected = Some(index); + } +} + +impl ResourceOrdinaryChannels { + fn selected(&self) -> Option<&OrdinaryAttemptChannel> { + self.selected.and_then(|index| self.records.get(index)) + } + + fn running_mut(&mut self) -> Option<&mut AttemptFailureChannel> { + let record = self + .selected + .and_then(|index| self.records.get_mut(index))?; + match record.phase { + OrdinaryAttemptPhase::Running => Some(&mut record.error), + OrdinaryAttemptPhase::Sealed(_) => None, + } + } +} + +impl NativeValues { + pub(in crate::native_abi) fn session_failure(&self) -> Option { + self.failure + } + + // Between-entry operations belong to the legacy session channel. They never + // mutate a sealed observation, and any such error still blocks the next begin. + pub(super) fn operation_failure(&self) -> Option { + self.failure.or_else(|| { + let record = self.resource_ordinary_channels.selected()?; + match record.phase { + OrdinaryAttemptPhase::Running => record.error.failure, + OrdinaryAttemptPhase::Sealed(_) => None, + } + }) + } + + pub(super) fn current_failure(&self) -> Option { + self.failure.or_else(|| { + self.resource_ordinary_channels + .selected() + .and_then(|record| record.error.failure) + }) + } + + pub(super) fn current_dynamic_failure_message(&self) -> Option<&[u8]> { + if self.failure.is_some() { + self.dynamic_failure_message.as_deref() + } else if let Some(record) = self.resource_ordinary_channels.selected() { + record.error.dynamic_failure_message.as_deref() + } else { + self.dynamic_failure_message.as_deref() + } + } + + pub(super) fn current_property_case_context(&self) -> Option<&[u8]> { + if self.failure.is_some() { + self.property_case_context.as_deref() + } else if let Some(record) = self.resource_ordinary_channels.selected() { + record.error.property_case_context.as_deref() + } else { + self.property_case_context.as_deref() + } + } + + pub(super) fn set_dynamic_failure_message(&mut self, message: Vec) { + if let Some(channel) = self.resource_ordinary_channels.running_mut() { + if channel.failure.is_none() { + channel.dynamic_failure_message = Some(message); + } + } else if self.failure.is_none() { + self.dynamic_failure_message = Some(message); + } + } + + pub(super) fn set_property_case_context(&mut self, context: Option>) { + if let Some(channel) = self.resource_ordinary_channels.running_mut() { + if channel.failure.is_none() { + channel.property_case_context = context; + } + } else if self.failure.is_none() { + self.property_case_context = context; + } + } + + pub(super) fn record_ordinary_failure(&mut self, error: Failure) { + if let Some(channel) = self.resource_ordinary_channels.running_mut() { + channel.failure.get_or_insert(error); + } else { + self.failure.get_or_insert(error); + } + } + + pub(super) fn can_capture_refinement_failure(&self) -> bool { + !matches!( + self.resource_ordinary_channels + .selected() + .map(|record| &record.phase), + Some(OrdinaryAttemptPhase::Sealed(_)) + ) + } + + // This is the existing Source refinement conversion only. It cannot reach a + // sealed terminal attempt and is never called by the entry constructor. + pub(super) fn finish_refinement_failure_capture(&mut self) { + if let Some(channel) = self.resource_ordinary_channels.running_mut() { + channel.failure = None; + channel.dynamic_failure_message = None; + } else { + self.failure = None; + self.dynamic_failure_message = None; + } + } + + pub(in crate::native_abi) fn prepare_resource_ordinary_attempt( + &mut self, + owner: ResourceOrdinaryOwner, + predecessor: Option, + ) -> LeafResult> { + if self.failure.is_some() + || self.cleanup_failed + || !self.is_empty() + || !owner.within_attempt_budget() + || owner.ordinal() != self.resource_ordinary_channels.records.len() + { + return Err(INVALID_HANDLE); + } + match (self.resource_ordinary_channels.selected(), predecessor) { + (None, None) if self.resource_ordinary_channels.records.is_empty() => {} + (Some(record), Some(previous)) => match record.phase { + OrdinaryAttemptPhase::Sealed(sealed) + if sealed == previous + && sealed.diagnostic_ready() + && record.owner == sealed.owner() + && owner.continues_installation(record.owner) => {} + OrdinaryAttemptPhase::Running | OrdinaryAttemptPhase::Sealed(_) => { + return Err(INVALID_HANDLE); + } + }, + _ => return Err(INVALID_HANDLE), + } + #[cfg(test)] + { + let channels = &mut self.resource_ordinary_channels; + channels + .reservation_faults + .checkpoint( + ReservationSite::OrdinaryChannel, + channels.records.len() == channels.records.capacity(), + ) + .map_err(|_| EXHAUSTED)?; + } + self.resource_ordinary_channels + .records + .try_reserve(1) + .map_err(|_| EXHAUSTED)?; + Ok(PreparedResourceOrdinaryAttempt { + values: self, + owner, + }) + } + + pub(in crate::native_abi) fn validate_resource_ordinary_attempt( + &self, + owner: ResourceOrdinaryOwner, + ) -> LeafResult<()> { + match self.resource_ordinary_channels.selected() { + Some(record) + if record.owner == owner + && matches!(record.phase, OrdinaryAttemptPhase::Running) => + { + Ok(()) + } + _ => Err(INVALID_HANDLE), + } + } + + pub(in crate::native_abi) fn seal_resource_ordinary_attempt( + &mut self, + completion: ResourceOrdinaryCompletion, + ) -> LeafResult<()> { + self.validate_resource_ordinary_attempt(completion.owner())?; + if !completion.matches_owner() { + return Err(INVALID_HANDLE); + } + let record = self + .resource_ordinary_channels + .selected + .and_then(|index| self.resource_ordinary_channels.records.get_mut(index)) + .ok_or(INVALID_HANDLE)?; + record.phase = OrdinaryAttemptPhase::Sealed(completion); + Ok(()) + } +} + +#[cfg(test)] +impl NativeValues { + pub(in crate::native_abi) fn test_arm_resource_ordinary_reservation(&mut self) { + self.resource_ordinary_channels + .reservation_faults + .arm(ReservationSite::OrdinaryChannel); + } + + pub(in crate::native_abi) fn test_resource_ordinary_reservation_snapshot( + &self, + ) -> (usize, usize, bool) { + self.resource_ordinary_channels + .reservation_faults + .snapshot(ReservationSite::OrdinaryChannel) + } + + pub(in crate::native_abi) fn test_resource_ordinary_channel_capacity(&self) -> (usize, usize) { + let channels = &self.resource_ordinary_channels; + (channels.records.len(), channels.records.capacity()) + } + + pub(in crate::native_abi) fn test_resource_ordinary_channel_state( + &self, + ) -> ( + usize, + Option, + Option, + ) { + let channels = &self.resource_ordinary_channels; + let selected = channels.selected(); + let completion = selected.and_then(|record| match record.phase { + OrdinaryAttemptPhase::Running => None, + OrdinaryAttemptPhase::Sealed(completion) => Some(completion), + }); + ( + channels.records.len(), + selected.map(|record| record.owner), + completion, + ) + } +} diff --git a/crates/jett_runtime/src/native_abi/values/ordinary_borrowed_sum_tests.rs b/crates/jett_runtime/src/native_abi/values/ordinary_borrowed_sum_tests.rs new file mode 100644 index 00000000..685b1a22 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/ordinary_borrowed_sum_tests.rs @@ -0,0 +1,356 @@ +//! Nonconsuming ordinary sum reads keep the original shell and payload owners. +use super::*; +use std::mem::MaybeUninit; + +struct BorrowContext { + record: Box, + live: bool, +} + +impl BorrowContext { + fn new() -> Self { + let mut record = Box::new(JettRuntimeContextV1::retired()); + let mut result = MaybeUninit::uninit(); + assert_eq!( + unsafe { jett_rt_v1_context_create(1, &mut *record, result.as_mut_ptr()) }, + JettRuntimeStatusV1::OK + ); + Self { record, live: true } + } + + fn pointer(&self) -> *const JettRuntimeContextV1 { + &*self.record + } + + fn values(&self, inspect: impl FnOnce(&mut NativeValues) -> R) -> R { + let lease = acquire_context(context_key(self.pointer()).unwrap()).unwrap(); + let mut state = lock_unpoisoned(&lease.entry.state); + inspect(&mut state.as_mut().unwrap().values) + } + + fn retire(&mut self) { + assert!(self.live); + let mut result = MaybeUninit::uninit(); + assert_eq!( + unsafe { jett_rt_v1_context_destroy(&mut *self.record, result.as_mut_ptr()) }, + JettRuntimeStatusV1::OK + ); + self.live = false; + } +} + +impl Drop for BorrowContext { + fn drop(&mut self) { + if self.live { + self.retire(); + } + } +} + +fn scalar_sum(context: &BorrowContext, tag: u32, bits: u64, depth: u64) -> u64 { + let sum = unsafe { jett_rt_v1_sum_new_scalar_task(context.pointer(), tag, bits, depth) }; + assert_ne!(sum, 0); + sum +} + +fn drop_sum(context: &BorrowContext, sum: u64) { + assert_eq!(unsafe { jett_rt_v1_value_drop(context.pointer(), sum) }, 0); +} + +#[test] +fn ordinary_borrowed_sum_leaf_schema_matches_the_checked_native_call() { + assert_eq!( + NativeLeaf::SumPayloadBorrow.symbol(), + "jett_rt_v1_sum_payload_borrow" + ); + assert_eq!( + NativeLeaf::SumPayloadBorrow.parameters(), + &[AbiScalar::Pointer, AbiScalar::I64, AbiScalar::I32] + ); + assert_eq!(NativeLeaf::SumPayloadBorrow.result(), AbiScalar::I64); + let _: unsafe extern "C" fn(*const JettRuntimeContextV1, u64, u32) -> u64 = + jett_rt_v1_sum_payload_borrow; +} + +#[test] +fn ordinary_borrowed_sum_repeated_nested_reads_keep_identity_without_allocating() { + let context = BorrowContext::new(); + let pointer = context.pointer(); + let bytes = unsafe { jett_rt_v1_bytes_new(pointer) }; + let list = unsafe { jett_rt_v1_list_new(pointer, 1) }; + assert_eq!( + unsafe { jett_rt_v1_list_append(pointer, list, bytes) }, + list + ); + let inner = unsafe { jett_rt_v1_sum_new(pointer, SUM_SUCCESS, list, 1) }; + let outer = unsafe { jett_rt_v1_sum_new(pointer, SUM_SUCCESS, inner, 1) }; + context.values(|values| values.allocation_budget = Some(0)); + for _ in 0..3 { + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(pointer, outer, SUM_SUCCESS) }, + inner + ); + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(pointer, inner, SUM_SUCCESS) }, + list + ); + } + context.values(|values| { + assert_eq!(values.allocation_budget, Some(0)); + assert_eq!((values.sums_created, values.sums_destroyed), (2, 0)); + assert_eq!((values.lists_created, values.lists_destroyed), (1, 0)); + assert_eq!((values.bytes_created, values.bytes_destroyed), (1, 0)); + assert!(values.sums[&outer].owned && values.sums[&inner].owned); + assert_eq!(values.lists[&list].elements, vec![Some(bytes)]); + assert!(values.bytes.contains_key(&bytes)); + assert!(values.failure.is_none()); + assert!(values.dynamic_failure_message.is_none()); + }); + drop_sum(&context, outer); + context.values(|values| { + assert_eq!((values.sums_created, values.sums_destroyed), (2, 2)); + assert_eq!((values.lists_created, values.lists_destroyed), (1, 1)); + assert_eq!((values.bytes_created, values.bytes_destroyed), (1, 1)); + assert!(values.is_empty()); + assert!(!values.cleanup_failed); + }); +} + +#[test] +fn ordinary_borrowed_sum_failure_string_needs_an_explicit_owned_reference() { + let context = BorrowContext::new(); + let text = context.values(|values| { + let text = values.insert("failure payload".into()).unwrap(); + values.strings.get_mut(&text).unwrap().pending_depth = 3; + text + }); + let sum = unsafe { jett_rt_v1_sum_new(context.pointer(), SUM_FAILURE, text, 1) }; + context.values(|values| values.allocation_budget = Some(0)); + for _ in 0..3 { + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(context.pointer(), sum, SUM_FAILURE) }, + text + ); + } + context.values(|values| { + assert_eq!(values.strings[&text].references, 1); + assert_eq!(values.strings[&text].pending_depth, 3); + assert_eq!(values.sums[&sum].payload_pending_depth, 0); + assert_eq!(values.allocation_budget, Some(0)); + }); + let retained = unsafe { jett_rt_v1_string_retain(context.pointer(), text) }; + assert_eq!(retained, text); + context.values(|values| assert_eq!(values.strings[&text].references, 2)); + drop_sum(&context, sum); + context.values(|values| { + assert_eq!(values.strings[&text].references, 1); + assert_eq!(values.strings[&text].pending_depth, 3); + assert_eq!(values.text(text), Ok("failure payload")); + }); + assert_eq!( + unsafe { jett_rt_v1_string_release(context.pointer(), retained) }, + 0 + ); + context.values(|values| assert!(values.is_empty())); +} + +#[test] +fn ordinary_borrowed_sum_scalar_bits_and_pending_depth_are_not_flattened() { + for tag in [SUM_FAILURE, SUM_SUCCESS] { + for (bits, depth) in [(0, 0), (u64::MAX, 1), (7, 2), (0, u64::MAX)] { + let context = BorrowContext::new(); + let sum = scalar_sum(&context, tag, bits, depth); + context.values(|values| values.allocation_budget = Some(0)); + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(context.pointer(), sum, tag) }, + bits + ); + assert_eq!( + unsafe { jett_rt_v1_sum_payload_pending_depth(context.pointer(), sum) }, + depth + ); + assert_eq!(unsafe { jett_rt_v1_value_status(context.pointer()) }, 0); + context.values(|values| { + let shell = &values.sums[&sum]; + assert_eq!((shell.tag, shell.bits, shell.owned), (tag, bits, false)); + assert_eq!( + (shell.pending_depth, shell.payload_pending_depth), + (0, depth) + ); + assert_eq!((values.sums_created, values.sums_destroyed), (1, 0)); + assert_eq!(values.allocation_budget, Some(0)); + }); + drop_sum(&context, sum); + context.values(|values| assert!(values.is_empty())); + } + } +} + +#[test] +fn ordinary_borrowed_sum_ready_shell_keeps_pending_list_and_element_depth() { + let context = BorrowContext::new(); + let pointer = context.pointer(); + let list = unsafe { jett_rt_v1_list_new(pointer, 0) }; + assert_eq!( + unsafe { jett_rt_v1_list_append_scalar_task(pointer, list, 5, 2) }, + list + ); + context.values(|values| values.lists.get_mut(&list).unwrap().pending_depth = 3); + let sum = unsafe { jett_rt_v1_sum_new(pointer, SUM_SUCCESS, list, 1) }; + context.values(|values| values.allocation_budget = Some(0)); + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(pointer, sum, SUM_SUCCESS) }, + list + ); + assert_eq!( + unsafe { jett_rt_v1_list_element_pending_depth(pointer, list, 0) }, + 2 + ); + context.values(|values| { + assert_eq!(values.lists[&list].pending_depth, 3); + assert_eq!(values.lists[&list].element_pending_depths.get(&0), Some(&2)); + assert_eq!(values.sums[&sum].pending_depth, 0); + assert_eq!(values.sums[&sum].payload_pending_depth, 0); + assert_eq!(values.allocation_budget, Some(0)); + assert!(values.failure.is_none()); + }); + drop_sum(&context, sum); + context.values(|values| assert!(values.is_empty())); +} + +#[test] +fn ordinary_borrowed_sum_wrong_tags_and_outer_pending_keep_the_owned_shell() { + for (tag, selected, outer_depth) in [ + (SUM_SUCCESS, SUM_FAILURE, 0), + (SUM_FAILURE, SUM_SUCCESS, 0), + (SUM_SUCCESS, 2, 0), + (SUM_SUCCESS, u32::MAX, 0), + (SUM_SUCCESS, SUM_SUCCESS, 1), + (SUM_FAILURE, SUM_FAILURE, 2), + (SUM_SUCCESS, SUM_SUCCESS, u64::MAX), + ] { + let context = BorrowContext::new(); + let list = unsafe { jett_rt_v1_list_new(context.pointer(), 0) }; + let sum = unsafe { jett_rt_v1_sum_new(context.pointer(), tag, list, 1) }; + context.values(|values| { + let shell = values.sums.get_mut(&sum).unwrap(); + shell.pending_depth = outer_depth; + shell.payload_pending_depth = 7; + values.allocation_budget = Some(0); + }); + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(context.pointer(), sum, selected) }, + 0 + ); + assert_eq!( + unsafe { jett_rt_v1_value_status(context.pointer()) }, + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + context.values(|values| { + let shell = &values.sums[&sum]; + assert_eq!((shell.tag, shell.bits, shell.owned), (tag, list, true)); + assert_eq!( + (shell.pending_depth, shell.payload_pending_depth), + (outer_depth, 7) + ); + assert_eq!(values.failure, Some(INVALID_SUM)); + assert!(values.dynamic_failure_message.is_none()); + assert_eq!((values.sums_created, values.sums_destroyed), (1, 0)); + assert_eq!((values.lists_created, values.lists_destroyed), (1, 0)); + assert_eq!(values.allocation_budget, Some(0)); + }); + drop_sum(&context, sum); + context.values(|values| { + assert!(values.is_empty()); + assert!(!values.cleanup_failed); + }); + } +} + +#[test] +fn ordinary_borrowed_sum_foreign_stale_and_non_sum_ids_keep_other_owners() { + let foreign_context = BorrowContext::new(); + let foreign = scalar_sum(&foreign_context, SUM_SUCCESS, 41, 0); + for invalid_kind in 0..5 { + let context = BorrowContext::new(); + let list = unsafe { jett_rt_v1_list_new(context.pointer(), 0) }; + let sum = unsafe { jett_rt_v1_sum_new(context.pointer(), SUM_SUCCESS, list, 1) }; + let stale = scalar_sum(&context, SUM_SUCCESS, 43, 0); + drop_sum(&context, stale); + let invalid = match invalid_kind { + 0 => 0, + 1 => u64::MAX, + 2 => foreign, + 3 => list, + 4 => stale, + _ => unreachable!(), + }; + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(context.pointer(), invalid, SUM_SUCCESS) }, + 0 + ); + // This leaf is not a cleanup operation: terminal failure inhibits it. + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(context.pointer(), sum, SUM_SUCCESS) }, + 0 + ); + context.values(|values| { + assert_eq!(values.failure, Some(INVALID_SUM)); + assert_eq!(values.sums[&sum].bits, list); + assert!(values.sums[&sum].owned); + assert!(values.lists.contains_key(&list)); + assert_eq!((values.sums_created, values.sums_destroyed), (2, 1)); + assert_eq!((values.lists_created, values.lists_destroyed), (1, 0)); + }); + drop_sum(&context, sum); + context.values(|values| { + assert!(values.is_empty()); + assert!(!values.cleanup_failed); + }); + } + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(foreign_context.pointer(), foreign, SUM_SUCCESS) }, + 41 + ); + drop_sum(&foreign_context, foreign); + foreign_context.values(|values| assert!(values.is_empty())); +} + +#[test] +fn ordinary_borrowed_sum_null_copied_mismatched_and_retired_contexts_are_inert() { + let mut context = BorrowContext::new(); + let sum = scalar_sum(&context, SUM_SUCCESS, 47, 0); + let copied = JettRuntimeContextV1 { + token: context.record.token, + abi_version: context.record.abi_version, + state_tag: context.record.state_tag, + }; + let retired = JettRuntimeContextV1::retired(); + for pointer in [ptr::null(), &copied, &retired] { + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(pointer, sum, SUM_SUCCESS) }, + 0 + ); + } + context.record.abi_version = 2; + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(context.pointer(), sum, SUM_SUCCESS) }, + 0 + ); + context.record.abi_version = JETT_RUNTIME_ABI_VERSION_V1; + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(context.pointer(), sum, SUM_SUCCESS) }, + 47 + ); + context.values(|values| { + assert!(values.failure.is_none()); + assert_eq!((values.sums_created, values.sums_destroyed), (1, 0)); + }); + drop_sum(&context, sum); + context.values(|values| assert!(values.is_empty())); + context.retire(); + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(context.pointer(), sum, SUM_SUCCESS) }, + 0 + ); +} diff --git a/crates/jett_runtime/src/native_abi/values/resource_companion_tests.rs b/crates/jett_runtime/src/native_abi/values/resource_companion_tests.rs new file mode 100644 index 00000000..bee074ed --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/resource_companion_tests.rs @@ -0,0 +1,126 @@ +//! Malformed-host controls for the actual typed Resource failure-companion drop. +use super::*; +use crate::resource_custody::{NativeLayoutInstallation, RegisteredNativeLayout}; +use std::sync::Arc; + +fn layout() -> Arc { + let mut bytes = b"JTRSC001".to_vec(); + let words = |bytes: &mut Vec, values: &[u32]| { + for value in values { + bytes.extend_from_slice(&value.to_le_bytes()); + } + }; + words(&mut bytes, &[2, 0]); + bytes.extend_from_slice(&0u64.to_le_bytes()); + words(&mut bytes, &[0, 0, 1, 8, 1, 0, 0, 0]); + words(&mut bytes, &[0, 0, 0]); // pure Nothing signature + for row in [ + vec![0, 5], + vec![1, 1, 64, 1], + vec![2, 4], + vec![3, 8, 1], + vec![4, 8, 2], + vec![5, 8, 4], + vec![6, 9, 1, 4], + vec![7, 9, 4, 1], + ] { + words(&mut bytes, &row); + } + words(&mut bytes, &[0, 1, 0, 0, 0, 0, 0, 1, 0, 0]); + let length = bytes.len() as u64; + bytes[16..24].copy_from_slice(&length.to_le_bytes()); + let registry = ResourceRegistry::new(); + NativeLayoutInstallation::new(®istry) + .install(®istry, &bytes) + .unwrap() +} + +#[test] +fn resource_ordinary_companion_owned_integer_cannot_drop_an_unrelated_live_string() { + let layout = layout(); + let mut values = NativeValues::default(); + let unrelated = values.insert("unrelated owner".into()).unwrap(); + let malformed = values.sum(SUM_SUCCESS, unrelated, true).unwrap(); + assert!( + values + .validate_resource_ordinary(malformed, &layout, 3) + .is_err() + ); + assert!( + values + .drop_resource_typed_companion(&layout, 3, malformed) + .is_err() + ); + assert!(values.strings.contains_key(&unrelated)); + assert!(values.sums.contains_key(&malformed)); + values.sums.get_mut(&malformed).unwrap().owned = false; + values + .drop_resource_typed_companion(&layout, 3, malformed) + .unwrap(); + assert!(values.strings.contains_key(&unrelated)); + values.drop_value(unrelated).unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn resource_ordinary_companion_nested_failure_requires_each_immediate_owner_bit() { + let layout = layout(); + for missing_owner in 0..2 { + let mut values = NativeValues::default(); + let text = values.insert("failure payload".into()).unwrap(); + let inner = values.sum(SUM_SUCCESS, text, missing_owner != 0).unwrap(); + let outer = values.sum(SUM_FAILURE, inner, missing_owner != 1).unwrap(); + assert!( + values + .validate_resource_ordinary(outer, &layout, 6) + .is_err() + ); + assert!( + values + .drop_resource_typed_companion(&layout, 6, outer) + .is_err() + ); + assert!(values.strings.contains_key(&text)); + assert!(values.sums.contains_key(&inner) && values.sums.contains_key(&outer)); + values.sums.get_mut(&inner).unwrap().owned = true; + values.sums.get_mut(&outer).unwrap().owned = true; + values + .drop_resource_typed_companion(&layout, 6, outer) + .unwrap(); + assert!(values.is_empty()); + assert!( + values + .drop_resource_typed_companion(&layout, 6, outer) + .is_err() + ); + } +} + +#[test] +fn resource_ordinary_companion_absence_and_primitive_failure_do_not_own_payload_bits() { + let layout = layout(); + let mut values = NativeValues::default(); + let unrelated = values.insert("still live".into()).unwrap(); + let failed = values.sum(SUM_FAILURE, unrelated, true).unwrap(); + assert!( + values + .drop_resource_typed_companion(&layout, 7, failed) + .is_err() + ); + assert!(values.strings.contains_key(&unrelated)); + values.sums.get_mut(&failed).unwrap().owned = false; + values + .drop_resource_typed_companion(&layout, 7, failed) + .unwrap(); + assert!(values.strings.contains_key(&unrelated)); + let absent = values.sum(SUM_FAILURE, 0, false).unwrap(); + values + .drop_resource_typed_companion(&layout, 5, absent) + .unwrap(); + let inner = values.sum(SUM_SUCCESS, unrelated, true).unwrap(); + let nested = values.sum(SUM_SUCCESS, inner, true).unwrap(); + values + .drop_resource_typed_companion(&layout, 5, nested) + .unwrap(); + assert!(values.is_empty()); +} diff --git a/crates/jett_runtime/src/resource_custody.rs b/crates/jett_runtime/src/resource_custody.rs new file mode 100644 index 00000000..63b7fbcc --- /dev/null +++ b/crates/jett_runtime/src/resource_custody.rs @@ -0,0 +1,1025 @@ +//! Registry-backed ownership protocol, independent of compiler/interpreter data. +//! Native ABI transport remains a separate boundary from layout registration. + +use std::any::Any; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; + +use crate::{ + AuthorityProvenance, RegistryError, ResourceKey, ResourceRegistry, ResourceTypeId, + discard_panic_payload, +}; + +#[path = "resource_custody/carrier_holders.rs"] +mod carrier_holders; +use carrier_holders::CarrierHolderRoot; +pub(crate) use carrier_holders::{CarrierRootToken, RegisteredCarrierSlot}; + +static NEXT_CUSTODY: AtomicU64 = AtomicU64::new(1); + +#[path = "resource_custody/registration.rs"] +mod registration; +#[cfg(test)] +pub(crate) use registration::carrier_runtime_fixture_bytes; +pub(crate) use registration::{ + JettResourceCallResultV1, NATIVE_RESOURCE_LAYOUT_WIRE_VERSION, NativeAccess, + NativeCarrierChild, NativeCarrierChildSource, NativeCarrierDestination, NativeCarrierField, + NativeCarrierLayout, NativeCarrierLoanSource, NativeCarrierMember, NativeCarrierNode, + NativeCarrierObservation, NativeCarrierOperation, NativeCarrierOperationRecord, + NativeCarrierPath, NativeCarrierSelector, NativeCarrierSlot, NativeCarrierSource, NativeFormal, + NativeFrame, NativeFrameRole, NativeHook, NativeLayoutInstallation, NativeLoanSource, + NativeOperation, NativeOperationRecord, NativeParent, NativePayloadStep, NativePosition, + NativeRecipe, NativeShape, NativeSignature, NativeSite, NativeSlot, NativeSourceEffect, + NativeSourceFormal, NativeSourceInvocation, NativeSourceResult, NativeSourceSyntax, + NativeSourceValue, NativeSumLoanSource, RESOURCE_DOMAIN_ERROR, RESOURCE_DOMAIN_OK, + RegisteredNativeLayout, ResourceLayoutError, +}; + +#[derive(Debug)] +struct ProgramLayout { + context: u64, + kinds: Vec, + slots: Vec, +} + +/// Only a constructor-owned registration can create this immutable identity. +/// The private registration child is the sole production issuer; tests also +/// have a private issuer. Neither accepts a registry key as an owner. +#[derive(Debug, Clone)] +pub(crate) struct RegisteredResourceProgram(Arc); + +#[derive(Debug, Clone)] +pub(crate) struct RegisteredResourceKind { + program: Arc, + index: usize, +} + +#[derive(Debug, Clone)] +pub(crate) struct RegisteredOwnerSlot { + program: Arc, + index: usize, +} + +impl RegisteredResourceProgram { + pub(crate) fn kind(&self, index: usize) -> Result { + self.0.kinds.get(index).ok_or(CustodyError::WrongKind)?; + Ok(RegisteredResourceKind { + program: self.0.clone(), + index, + }) + } + + pub(crate) fn slot(&self, index: usize) -> Result { + self.0.slots.get(index).ok_or(CustodyError::UnknownSlot)?; + Ok(RegisteredOwnerSlot { + program: self.0.clone(), + index, + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ResourcePurpose { + Runtime, + NamespaceConstant, + ExplicitComptime, + Verify, + Property, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ResourceFrameKind { + Scope, + Operation, + Return, +} + +#[derive(Debug)] +pub(crate) struct ResourceFrameToken { + core: u64, + index: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum HolderAddress { + Slot(usize), + Carrier { root: usize, path: usize }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Holder { + frame: usize, + address: HolderAddress, +} + +#[derive(Debug)] +pub(crate) struct ResourceHolderToken { + core: u64, + holder: Holder, +} + +/// Neither Clone/Copy nor a finalizing Rust Drop. The ledger owns cleanup. +#[derive(Debug)] +pub(crate) struct OwnedResourceToken { + core: u64, + owner: usize, + holder_generation: u64, +} + +#[derive(Debug)] +pub(crate) struct BorrowedResourceToken { + core: u64, + loan: usize, +} + +/// Preflight is unoccupied metadata, not a provider grant or resource owner. +#[derive(Debug)] +pub(crate) struct PreparedResourceAcquisition { + core: u64, + holder: Holder, + kind: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CustodyError { + WrongContext, + WrongProgram, + WrongKind, + UnknownSlot, + InvalidFrame, + OutOfOrderFrame, + InvalidOwner, + StaleHolderGeneration, + InvalidLoan, + ActiveBorrow, + OccupiedHolder, + WrongPurpose, + CapacityExhausted, + UnownedRegistry, + Registry(RegistryError), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ResourceCleanupFailure { + Custody(CustodyError), + FinalizerPanic, +} + +#[derive(Debug, Default, PartialEq, Eq)] +pub(crate) struct ResourceCleanupOutcome { + first: Option, +} + +impl ResourceCleanupOutcome { + pub(crate) fn failure(&self) -> Option { + self.first + } + fn fail(&mut self, failure: ResourceCleanupFailure) { + self.first.get_or_insert(failure); + } + fn merge(&mut self, other: Self) { + if let Some(failure) = other.first { + self.fail(failure); + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum ResourceTransitionFailure { + Rejected(CustodyError), + Cleanup(ResourceCleanupFailure), +} + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum ResourceBodyOutcome { + Completed(Result), + HostPanic, +} + +impl ResourceBodyOutcome { + pub(crate) fn caught(outcome: std::thread::Result>) -> Self { + match outcome { + Ok(result) => Self::Completed(result), + Err(payload) => { + discard_panic_payload(payload); + Self::HostPanic + } + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum ResourceCompletionFailure { + Body(E), + BodyPanic, + Cleanup(ResourceCleanupFailure), +} + +pub(crate) fn complete_resource( + body: ResourceBodyOutcome, + cleanup: ResourceCleanupOutcome, +) -> Result> { + if let Some(failure) = cleanup.first { + return Err(ResourceCompletionFailure::Cleanup(failure)); + } + match body { + ResourceBodyOutcome::Completed(Ok(value)) => Ok(value), + ResourceBodyOutcome::Completed(Err(error)) => Err(ResourceCompletionFailure::Body(error)), + ResourceBodyOutcome::HostPanic => Err(ResourceCompletionFailure::BodyPanic), + } +} + +#[derive(Clone, Copy)] +struct Acquisition { + owner: usize, + holder: Holder, + generation: u64, +} + +struct Frame { + kind: ResourceFrameKind, + purpose: ResourcePurpose, + live: bool, + acquisitions: Vec, + loans: Vec, +} + +struct Owner { + kind: usize, + key: ResourceKey, + authority: AuthorityProvenance, + holder: Holder, + generation: u64, + live: bool, +} + +struct Loan { + owner: usize, + generation: u64, + frame: usize, + live: bool, +} + +/// Private test-only distinction between the two real begin-frame allocations. +#[cfg(test)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum FrameReservationSite { + Frames, + ActiveFrames, +} + +#[cfg(test)] +impl FrameReservationSite { + fn index(self) -> usize { + match self { + Self::Frames => 0, + Self::ActiveFrames => 1, + } + } +} + +pub(crate) struct ResourceCustody { + id: u64, + program: RegisteredResourceProgram, + frames: Vec, + active: Vec, + owners: Vec, + loans: Vec, + #[cfg(test)] + frame_reservation_fault: Option, + #[cfg(test)] + frame_reservation_observations: [(usize, usize); 2], + carrier_roots: Vec, +} + +impl ResourceCustody { + pub(crate) fn new( + program: RegisteredResourceProgram, + registry: &ResourceRegistry, + ) -> Result { + if program.0.context != registry.context_id { + return Err(CustodyError::WrongContext); + } + if registry.live_count() != 0 { + return Err(CustodyError::UnownedRegistry); + } + let id = NEXT_CUSTODY + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |next| { + next.checked_add(1) + }) + .map_err(|_| CustodyError::CapacityExhausted)?; + Ok(Self { + id, + program, + frames: Vec::new(), + active: Vec::new(), + owners: Vec::new(), + loans: Vec::new(), + #[cfg(test)] + frame_reservation_fault: None, + #[cfg(test)] + frame_reservation_observations: [(0, 0); 2], + carrier_roots: Vec::new(), + }) + } + + fn registry(&self, registry: &ResourceRegistry) -> Result<(), CustodyError> { + if self.program.0.context == registry.context_id { + Ok(()) + } else { + Err(CustodyError::WrongContext) + } + } + + fn frame(&self, index: usize) -> Result<&Frame, CustodyError> { + self.frames + .get(index) + .filter(|frame| frame.live) + .ok_or(CustodyError::InvalidFrame) + } + + fn frame_token(&self, token: &ResourceFrameToken) -> Result { + if token.core != self.id { + return Err(CustodyError::WrongProgram); + } + self.frame(token.index)?; + Ok(token.index) + } + + fn holder_token(&self, token: &ResourceHolderToken) -> Result { + if token.core != self.id { + return Err(CustodyError::WrongProgram); + } + self.frame(token.holder.frame)?; + self.holder_kind(token.holder)?; + Ok(token.holder) + } + + fn available(&self, holder: Holder) -> Result<(), CustodyError> { + if self + .owners + .iter() + .any(|owner| owner.live && owner.holder == holder) + { + return Err(CustodyError::OccupiedHolder); + } + Ok(()) + } + + fn runtime_frame(&self, frame: usize) -> Result<(), CustodyError> { + if self.frame(frame)?.purpose == ResourcePurpose::Runtime { + Ok(()) + } else { + Err(CustodyError::WrongPurpose) + } + } + + fn kind(&self, token: &RegisteredResourceKind) -> Result { + if !Arc::ptr_eq(&token.program, &self.program.0) { + return Err(CustodyError::WrongProgram); + } + self.program + .0 + .kinds + .get(token.index) + .ok_or(CustodyError::WrongKind)?; + Ok(token.index) + } + + #[cfg(test)] + pub(crate) fn test_arm_frame_reservation(&mut self, site: FrameReservationSite) { + assert!( + self.frame_reservation_fault.is_none(), + "one pending frame reservation fault" + ); + self.frame_reservation_fault = Some(site); + } + + #[cfg(test)] + fn test_frame_reservation_checkpoint( + &mut self, + site: FrameReservationSite, + growing: bool, + ) -> Result<(), CustodyError> { + if !growing { + return Ok(()); + } + let observation = &mut self.frame_reservation_observations[site.index()]; + observation.0 += 1; + if self.frame_reservation_fault == Some(site) { + self.frame_reservation_fault = None; + observation.1 += 1; + return Err(CustodyError::CapacityExhausted); + } + Ok(()) + } + + #[cfg(test)] + pub(crate) fn test_frame_reservation_snapshot( + &self, + site: FrameReservationSite, + ) -> (usize, usize, bool) { + let (growth, refusal) = self.frame_reservation_observations[site.index()]; + (growth, refusal, self.frame_reservation_fault == Some(site)) + } + + pub(crate) fn begin_frame( + &mut self, + kind: ResourceFrameKind, + purpose: ResourcePurpose, + ) -> Result { + if let Some(parent) = self.active.last() { + if self.frame(*parent)?.purpose != purpose { + return Err(CustodyError::WrongPurpose); + } + } else if self.live_owners() != 0 + || self.live_loans() != 0 + || self.live_carrier_roots() != 0 + { + return Err(CustodyError::InvalidFrame); + } + #[cfg(test)] + self.test_frame_reservation_checkpoint( + FrameReservationSite::Frames, + self.frames.len() == self.frames.capacity(), + )?; + self.frames + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted)?; + #[cfg(test)] + self.test_frame_reservation_checkpoint( + FrameReservationSite::ActiveFrames, + self.active.len() == self.active.capacity(), + )?; + self.active + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted)?; + let index = self.frames.len(); + self.frames.push(Frame { + kind, + purpose, + live: true, + acquisitions: Vec::new(), + loans: Vec::new(), + }); + self.active.push(index); + Ok(ResourceFrameToken { + core: self.id, + index, + }) + } + + pub(crate) fn holder( + &self, + frame: &ResourceFrameToken, + slot: &RegisteredOwnerSlot, + ) -> Result { + let frame = self.frame_token(frame)?; + if !Arc::ptr_eq(&slot.program, &self.program.0) { + return Err(CustodyError::WrongProgram); + } + self.program + .0 + .slots + .get(slot.index) + .ok_or(CustodyError::UnknownSlot)?; + Ok(ResourceHolderToken { + core: self.id, + holder: Holder { + frame, + address: HolderAddress::Slot(slot.index), + }, + }) + } + + fn acquisition_storage(&mut self, holder: Holder, kind: usize) -> Result<(), CustodyError> { + self.runtime_frame(holder.frame)?; + if self.holder_kind(holder)? != kind { + return Err(CustodyError::WrongKind); + } + self.available(holder)?; + self.owners + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted)?; + self.frames[holder.frame] + .acquisitions + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted)?; + Ok(()) + } + + pub(crate) fn prepare_acquisition( + &mut self, + kind: &RegisteredResourceKind, + destination: &ResourceHolderToken, + ) -> Result { + let kind = self.kind(kind)?; + let holder = self.holder_token(destination)?; + self.acquisition_storage(holder, kind)?; + Ok(PreparedResourceAcquisition { + core: self.id, + holder, + kind, + }) + } + + pub(crate) fn commit_acquisition( + &mut self, + registry: &mut ResourceRegistry, + prepared: PreparedResourceAcquisition, + authority: AuthorityProvenance, + payload: T, + finalizer: F, + ) -> Result + where + T: Any + Send, + F: FnOnce(T) + Send + 'static, + { + let checked = (|| { + self.registry(registry)?; + if prepared.core != self.id { + return Err(CustodyError::WrongProgram); + } + self.program + .0 + .kinds + .get(prepared.kind) + .ok_or(CustodyError::WrongKind)?; + self.frame(prepared.holder.frame)?; + let next_slot = registry + .free_slots + .last() + .copied() + .unwrap_or(registry.slots.len()); + u32::try_from(next_slot).map_err(|_| CustodyError::CapacityExhausted)?; + self.acquisition_storage(prepared.holder, prepared.kind) + })(); + if let Err(error) = checked { + let cleanup = catch_unwind(AssertUnwindSafe(|| finalizer(payload))); + return Err(match cleanup { + Ok(()) => ResourceTransitionFailure::Rejected(error), + Err(panic) => { + discard_panic_payload(panic); + ResourceTransitionFailure::Cleanup(ResourceCleanupFailure::FinalizerPanic) + } + }); + } + let registry_type = self.program.0.kinds[prepared.kind]; + let inserted = catch_unwind(AssertUnwindSafe(|| { + registry.insert(registry_type, payload, authority, finalizer) + })); + let key = match inserted { + Ok(Ok(key)) => key, + Ok(Err(error)) => { + return Err(ResourceTransitionFailure::Rejected(CustodyError::Registry( + error, + ))); + } + Err(panic) => { + discard_panic_payload(panic); + return Err(ResourceTransitionFailure::Cleanup( + ResourceCleanupFailure::FinalizerPanic, + )); + } + }; + // All ledger capacity is reserved before physical insertion. No fallible + // publication remains between insertion and the logged owning record. + let owner = self.owners.len(); + self.owners.push(Owner { + kind: prepared.kind, + key, + authority, + holder: prepared.holder, + generation: 1, + live: true, + }); + self.frames[prepared.holder.frame] + .acquisitions + .push(Acquisition { + owner, + holder: prepared.holder, + generation: 1, + }); + Ok(OwnedResourceToken { + core: self.id, + owner, + holder_generation: 1, + }) + } + + fn owner(&self, token: &OwnedResourceToken) -> Result<&Owner, CustodyError> { + if token.core != self.id { + return Err(CustodyError::WrongProgram); + } + let owner = self + .owners + .get(token.owner) + .filter(|owner| owner.live) + .ok_or(CustodyError::InvalidOwner)?; + if owner.generation != token.holder_generation { + return Err(CustodyError::StaleHolderGeneration); + } + self.frame(owner.holder.frame)?; + if self.holder_kind(owner.holder)? != owner.kind { + return Err(CustodyError::WrongKind); + } + Ok(owner) + } + + fn validate_registry_owner( + &self, + owner: &Owner, + registry: &ResourceRegistry, + ) -> Result { + self.registry(registry)?; + let registry_type = *self + .program + .0 + .kinds + .get(owner.kind) + .ok_or(CustodyError::WrongKind)?; + let index = registry + .validate_slot(owner.key, registry_type) + .map_err(CustodyError::Registry)?; + let entry = registry.slots[index] + .entry + .as_ref() + .ok_or(CustodyError::Registry(RegistryError::Retired))?; + if entry.authority != owner.authority { + return Err(CustodyError::Registry(RegistryError::AuthorityMismatch)); + } + Ok(owner.key) + } + + fn borrowed(&self, owner: usize) -> bool { + self.loans + .iter() + .any(|loan| loan.live && loan.owner == owner) + } + + pub(crate) fn validate_owned( + &self, + owner: &OwnedResourceToken, + registry: &ResourceRegistry, + ) -> Result { + self.validate_registry_owner(self.owner(owner)?, registry) + } + + fn checked_transfer( + &self, + token: &OwnedResourceToken, + destination: &ResourceHolderToken, + registry: &ResourceRegistry, + ) -> Result<(Holder, u64), CustodyError> { + let holder = self.holder_token(destination)?; + self.runtime_frame(holder.frame)?; + let owner = self.owner(token)?; + self.validate_registry_owner(owner, registry)?; + if self.borrowed(token.owner) { + return Err(CustodyError::ActiveBorrow); + } + if self.holder_kind(holder)? != owner.kind { + return Err(CustodyError::WrongKind); + } + self.available(holder)?; + let generation = owner + .generation + .checked_add(1) + .ok_or(CustodyError::CapacityExhausted)?; + Ok((holder, generation)) + } + + /// Authenticate the exact occupied destination and reserve the impending + /// transfer before replacement runs either owner's finalizer. No reusable + /// transfer permission is returned; the ordinary transfer rechecks it later. + pub(crate) fn preflight_replacement( + &mut self, + old: &OwnedResourceToken, + replacement: &OwnedResourceToken, + destination: &ResourceHolderToken, + registry: &ResourceRegistry, + ) -> Result<(), CustodyError> { + let holder = self.holder_token(destination)?; + self.runtime_frame(holder.frame)?; + let old_owner = self.owner(old)?; + self.validate_registry_owner(old_owner, registry)?; + if old_owner.holder != holder || old.owner == replacement.owner { + return Err(CustodyError::InvalidOwner); + } + let replacement_owner = self.owner(replacement)?; + self.runtime_frame(replacement_owner.holder.frame)?; + self.validate_registry_owner(replacement_owner, registry)?; + if self.borrowed(old.owner) || self.borrowed(replacement.owner) { + return Err(CustodyError::ActiveBorrow); + } + if self.holder_kind(holder)? != replacement_owner.kind { + return Err(CustodyError::WrongKind); + } + if self + .owners + .iter() + .enumerate() + .any(|(index, owner)| index != old.owner && owner.live && owner.holder == holder) + { + return Err(CustodyError::OccupiedHolder); + } + replacement_owner + .generation + .checked_add(1) + .ok_or(CustodyError::CapacityExhausted)?; + self.frames[holder.frame] + .acquisitions + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted)?; + Ok(()) + } + + /// A Source activation reserves its entire exact formal handoff before any + /// owner moves. References are existing live tokens and registered metadata; + /// this creates neither ownership nor a reusable transfer permission. + pub(crate) fn preflight_transfer_batch( + &mut self, + frame: &ResourceFrameToken, + transfers: &[(&OwnedResourceToken, &RegisteredOwnerSlot)], + registry: &ResourceRegistry, + ) -> Result<(), CustodyError> { + let frame_index = self.frame_token(frame)?; + let mut checked: Vec<(usize, Holder)> = Vec::new(); + checked + .try_reserve_exact(transfers.len()) + .map_err(|_| CustodyError::CapacityExhausted)?; + for &(token, slot) in transfers { + let destination = self.holder(frame, slot)?; + let (holder, _) = self.checked_transfer(token, &destination, registry)?; + if checked + .iter() + .any(|(owner, previous)| *owner == token.owner || *previous == holder) + { + return Err(CustodyError::OccupiedHolder); + } + checked.push((token.owner, holder)); + } + self.frames[frame_index] + .acquisitions + .try_reserve(transfers.len()) + .map_err(|_| CustodyError::CapacityExhausted)?; + Ok(()) + } + + pub(crate) fn transfer( + &mut self, + token: &mut OwnedResourceToken, + destination: &ResourceHolderToken, + registry: &ResourceRegistry, + ) -> Result<(), CustodyError> { + let (holder, generation) = self.checked_transfer(token, destination, registry)?; + self.frames[holder.frame] + .acquisitions + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted)?; + self.owners[token.owner].holder = holder; + self.owners[token.owner].generation = generation; + self.frames[holder.frame].acquisitions.push(Acquisition { + owner: token.owner, + holder, + generation, + }); + token.holder_generation = generation; + Ok(()) + } + + pub(crate) fn begin_borrow( + &mut self, + token: &OwnedResourceToken, + frame: &ResourceFrameToken, + registry: &ResourceRegistry, + ) -> Result { + let frame = self.frame_token(frame)?; + self.runtime_frame(frame)?; + let owner = self.owner(token)?; + self.validate_registry_owner(owner, registry)?; + let owner_position = self + .active + .iter() + .position(|value| *value == owner.holder.frame) + .ok_or(CustodyError::InvalidFrame)?; + let borrower_position = self + .active + .iter() + .position(|value| *value == frame) + .ok_or(CustodyError::InvalidFrame)?; + if borrower_position < owner_position { + return Err(CustodyError::InvalidLoan); + } + let generation = owner.generation; + self.loans + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted)?; + self.frames[frame] + .loans + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted)?; + let loan = self.loans.len(); + self.loans.push(Loan { + owner: token.owner, + generation, + frame, + live: true, + }); + self.frames[frame].loans.push(loan); + Ok(BorrowedResourceToken { + core: self.id, + loan, + }) + } + + fn loan(&self, token: &BorrowedResourceToken) -> Result<&Loan, CustodyError> { + if token.core != self.id { + return Err(CustodyError::WrongProgram); + } + let loan = self + .loans + .get(token.loan) + .filter(|loan| loan.live) + .ok_or(CustodyError::InvalidLoan)?; + self.frame(loan.frame)?; + let owner = self + .owners + .get(loan.owner) + .filter(|owner| owner.live && owner.generation == loan.generation) + .ok_or(CustodyError::InvalidLoan)?; + self.frame(owner.holder.frame)?; + Ok(loan) + } + + pub(crate) fn validate_borrowed( + &self, + token: &BorrowedResourceToken, + registry: &ResourceRegistry, + ) -> Result { + let loan = self.loan(token)?; + self.validate_registry_owner(&self.owners[loan.owner], registry) + } + + pub(crate) fn end_borrow( + &mut self, + token: &mut BorrowedResourceToken, + ) -> Result<(), CustodyError> { + self.loan(token)?; + self.loans[token.loan].live = false; + Ok(()) + } + + fn finalize( + &mut self, + owner: usize, + registry: &mut ResourceRegistry, + ) -> ResourceCleanupOutcome { + let mut outcome = ResourceCleanupOutcome::default(); + if self.borrowed(owner) { + outcome.fail(ResourceCleanupFailure::Custody(CustodyError::ActiveBorrow)); + return outcome; + } + let record = &self.owners[owner]; + if let Err(error) = self.validate_registry_owner(record, registry) { + // An externally retired/stale entry is already unavailable. Retire + // its obligation too, never invoke a different generation's finalizer. + if matches!( + error, + CustodyError::Registry( + RegistryError::UnknownSlot + | RegistryError::StaleGeneration + | RegistryError::Retired + ) + ) { + self.owners[owner].live = false; + } + outcome.fail(ResourceCleanupFailure::Custody(error)); + return outcome; + } + let key = record.key; + let registry_type = self.program.0.kinds[record.kind]; + self.owners[owner].live = false; + match catch_unwind(AssertUnwindSafe(|| registry.close(key, registry_type))) { + Ok(Ok(())) => {} + Ok(Err(error)) => outcome.fail(ResourceCleanupFailure::Custody( + CustodyError::Registry(error), + )), + Err(panic) => { + discard_panic_payload(panic); + outcome.fail(ResourceCleanupFailure::FinalizerPanic); + } + } + outcome + } + + pub(crate) fn close( + &mut self, + token: &mut OwnedResourceToken, + registry: &mut ResourceRegistry, + ) -> ResourceCleanupOutcome { + let mut outcome = ResourceCleanupOutcome::default(); + let checked = self + .registry(registry) + .and_then(|()| self.owner(token)) + .and_then(|owner| { + self.runtime_frame(owner.holder.frame)?; + self.validate_registry_owner(owner, registry).map(|_| ()) + }); + if let Err(error) = checked { + outcome.fail(ResourceCleanupFailure::Custody(error)); + return outcome; + } + self.finalize(token.owner, registry) + } + + fn end_index( + &mut self, + frame: usize, + registry: &mut ResourceRegistry, + ) -> ResourceCleanupOutcome { + let mut outcome = ResourceCleanupOutcome::default(); + let checked = self + .registry(registry) + .and_then(|()| self.frame(frame).map(|_| ())) + .and_then(|()| { + if self.active.last() == Some(&frame) { + Ok(()) + } else { + Err(CustodyError::OutOfOrderFrame) + } + }); + if let Err(error) = checked { + outcome.fail(ResourceCleanupFailure::Custody(error)); + return outcome; + } + for loan in &self.frames[frame].loans { + self.loans[*loan].live = false; + } + let mut acquisitions = std::mem::take(&mut self.frames[frame].acquisitions); + for acquisition in acquisitions.iter().rev() { + let owner = &self.owners[acquisition.owner]; + if !owner.live + || owner.holder != acquisition.holder + || owner.generation != acquisition.generation + { + continue; + } + outcome.merge(self.finalize(acquisition.owner, registry)); + } + acquisitions.retain(|acquisition| { + let owner = &self.owners[acquisition.owner]; + owner.live + && owner.holder == acquisition.holder + && owner.generation == acquisition.generation + }); + if acquisitions.is_empty() { + self.frames[frame].live = false; + self.active.pop(); + } else { + // A malformed longer-lived lease must not turn refusal into frame + // retirement. Retain its acquisitions for a later valid completion. + self.frames[frame].acquisitions = acquisitions; + } + outcome + } + + pub(crate) fn end_frame( + &mut self, + frame: &ResourceFrameToken, + registry: &mut ResourceRegistry, + ) -> ResourceCleanupOutcome { + match self.frame_token(frame) { + Ok(index) => self.end_index(index, registry), + Err(error) => ResourceCleanupOutcome { + first: Some(ResourceCleanupFailure::Custody(error)), + }, + } + } + + pub(crate) fn unwind_all(&mut self, registry: &mut ResourceRegistry) -> ResourceCleanupOutcome { + let mut outcome = ResourceCleanupOutcome::default(); + while let Some(frame) = self.active.last().copied() { + outcome.merge(self.end_index(frame, registry)); + if self.active.last() == Some(&frame) { + break; + } + } + outcome + } + + pub(crate) fn live_owners(&self) -> usize { + self.owners.iter().filter(|owner| owner.live).count() + } + pub(crate) fn live_loans(&self) -> usize { + self.loans.iter().filter(|loan| loan.live).count() + } + pub(crate) fn active_frames(&self) -> usize { + self.active.len() + } +} + +#[cfg(test)] +#[path = "resource_custody/tests.rs"] +mod tests; diff --git a/crates/jett_runtime/src/resource_custody/carrier_holder_tests.rs b/crates/jett_runtime/src/resource_custody/carrier_holder_tests.rs new file mode 100644 index 00000000..67b940c9 --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/carrier_holder_tests.rs @@ -0,0 +1,472 @@ +use super::*; +use crate::resource_custody::carrier_holders::{CarrierRootToken, RegisteredCarrierSlot}; + +// Deliberate constructor-owned test metadata; no public path/word issues a token. +fn carrier_slot( + program: &RegisteredResourceProgram, + index: usize, + wrong_second: bool, +) -> RegisteredCarrierSlot { + let field = |name: &[u8], node| NativeCarrierField { + name: name.to_vec(), + node, + }; + RegisteredCarrierSlot { + program: program.0.clone(), + graph: Arc::new(NativeCarrierLayout { + nodes: vec![ + NativeCarrierNode::Resource { kind: 0 }, + NativeCarrierNode::Ordinary { shape: 0 }, + NativeCarrierNode::Struct { + identity: b"app.OwnedPair".to_vec(), + arguments: vec![], + fields: vec![ + field(b"first", 0), + field(b"second", if wrong_second { 3 } else { 0 }), + field(b"marker", 1), + ], + }, + NativeCarrierNode::Resource { kind: 1 }, + ], + slots: (0..4) + .map(|_| NativeCarrierSlot { frame: 0, node: 2 }) + .collect(), + operations: vec![], + }), + index, + } +} + +fn begin_root( + core: &mut ResourceCustody, + program: &RegisteredResourceProgram, + frame: &ResourceFrameToken, + slot: usize, +) -> CarrierRootToken { + core.begin_carrier_root(frame, &carrier_slot(program, slot, false)) + .unwrap() +} + +#[test] +fn carrier_holders_interleave_scalar_carrier_scalar_and_release_original_slot() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(vec![])); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let _a = acquire( + &mut core, + &mut registry, + &program, + &frame, + 0, + 1, + &events, + false, + ); + let mut b = acquire( + &mut core, + &mut registry, + &program, + &frame, + 1, + 2, + &events, + false, + ); + let root = begin_root(&mut core, &program, &frame, 0); + core.transfer_into_carrier(&mut b, &root, &[NativeCarrierPath::Field(0)], ®istry) + .unwrap(); + core.complete_carrier_root(&root).unwrap(); + let _c = acquire( + &mut core, + &mut registry, + &program, + &frame, + 3, + 3, + &events, + false, + ); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [3, 2, 1]); + core.retire_carrier_metadata(&root).unwrap(); + clean(&core, ®istry); + assert_eq!(core.live_carrier_roots(), 0); + + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let mut first = acquire( + &mut core, + &mut registry, + &program, + &frame, + 1, + 4, + &events, + false, + ); + let root = begin_root(&mut core, &program, &frame, 0); + core.transfer_into_carrier(&mut first, &root, &[NativeCarrierPath::Field(0)], ®istry) + .unwrap(); + // The occupied tree remains live while its old scalar slot is reused. + let _replacement = acquire( + &mut core, + &mut registry, + &program, + &frame, + 1, + 5, + &events, + false, + ); + assert_eq!(core.live_owners(), 2); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [3, 2, 1, 5, 4]); + core.retire_carrier_metadata(&root).unwrap(); + clean(&core, ®istry); + assert_eq!(core.live_carrier_roots(), 0); +} + +#[test] +fn carrier_holders_caller_callee_move_is_exact_and_invalidates_old_generation() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(vec![])); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let caller = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let mut first = acquire( + &mut core, + &mut registry, + &program, + &caller, + 0, + 10, + &events, + false, + ); + let mut second = acquire( + &mut core, + &mut registry, + &program, + &caller, + 1, + 11, + &events, + false, + ); + let source = begin_root(&mut core, &program, &caller, 0); + // Constructor lexical order differs from declared field order. + core.transfer_into_carrier( + &mut second, + &source, + &[NativeCarrierPath::Field(1)], + ®istry, + ) + .unwrap(); + core.transfer_into_carrier( + &mut first, + &source, + &[NativeCarrierPath::Field(0)], + ®istry, + ) + .unwrap(); + core.complete_carrier_root(&source).unwrap(); + let stale = OwnedResourceToken { + core: first.core, + owner: first.owner, + holder_generation: first.holder_generation, + }; + let callee = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let destination = begin_root(&mut core, &program, &callee, 1); + core.move_carrier_inventory( + &source, + &destination, + &mut [ + ( + &mut second, + vec![NativeCarrierPath::Field(1)], + vec![NativeCarrierPath::Field(1)], + ), + ( + &mut first, + vec![NativeCarrierPath::Field(0)], + vec![NativeCarrierPath::Field(0)], + ), + ], + ®istry, + true, + ) + .unwrap(); + assert!(matches!( + core.validate_owned(&stale, ®istry), + Err(CustodyError::StaleHolderGeneration) + )); + assert!(matches!( + core.validate_carrier_ticket(&source, &[NativeCarrierPath::Field(0)], &first), + Err(CustodyError::StaleHolderGeneration) + )); + assert_eq!(core.end_frame(&callee, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [10, 11]); + core.retire_carrier_metadata(&destination).unwrap(); + assert_eq!(core.end_frame(&caller, &mut registry).failure(), None); + clean(&core, ®istry); + assert_eq!(core.live_carrier_roots(), 0); +} + +#[test] +fn carrier_holders_late_batch_wrong_kind_rolls_back_and_can_retry() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(vec![])); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let mut first = acquire( + &mut core, + &mut registry, + &program, + &frame, + 0, + 20, + &events, + false, + ); + let mut second = acquire( + &mut core, + &mut registry, + &program, + &frame, + 1, + 21, + &events, + false, + ); + let source = begin_root(&mut core, &program, &frame, 0); + core.transfer_into_carrier( + &mut first, + &source, + &[NativeCarrierPath::Field(0)], + ®istry, + ) + .unwrap(); + core.transfer_into_carrier( + &mut second, + &source, + &[NativeCarrierPath::Field(1)], + ®istry, + ) + .unwrap(); + core.complete_carrier_root(&source).unwrap(); + let before = (first.holder_generation, second.holder_generation); + let wrong = core + .begin_carrier_root(&frame, &carrier_slot(&program, 1, true)) + .unwrap(); + let result = core.move_carrier_inventory( + &source, + &wrong, + &mut [ + ( + &mut first, + vec![NativeCarrierPath::Field(0)], + vec![NativeCarrierPath::Field(0)], + ), + ( + &mut second, + vec![NativeCarrierPath::Field(1)], + vec![NativeCarrierPath::Field(1)], + ), + ], + ®istry, + true, + ); + assert_eq!(result, Err(CustodyError::WrongKind)); + assert_eq!(before, (first.holder_generation, second.holder_generation)); + core.validate_carrier_inventory( + &source, + &[ + (&first, vec![NativeCarrierPath::Field(0)]), + (&second, vec![NativeCarrierPath::Field(1)]), + ], + ) + .unwrap(); + assert!(events.lock().unwrap().is_empty()); + core.retire_carrier_metadata(&wrong).unwrap(); + let good = begin_root(&mut core, &program, &frame, 1); + core.move_carrier_inventory( + &source, + &good, + &mut [ + ( + &mut first, + vec![NativeCarrierPath::Field(0)], + vec![NativeCarrierPath::Field(0)], + ), + ( + &mut second, + vec![NativeCarrierPath::Field(1)], + vec![NativeCarrierPath::Field(1)], + ), + ], + ®istry, + true, + ) + .unwrap(); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [21, 20]); + core.retire_carrier_metadata(&good).unwrap(); + clean(&core, ®istry); + assert_eq!(core.live_carrier_roots(), 0); +} + +#[test] +fn carrier_holders_wrong_path_root_generation_foreign_and_borrow_refuse_without_effects() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(vec![])); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let mut owner = acquire( + &mut core, + &mut registry, + &program, + &frame, + 0, + 30, + &events, + false, + ); + let root = begin_root(&mut core, &program, &frame, 0); + assert_eq!( + core.transfer_into_carrier(&mut owner, &root, &[NativeCarrierPath::Field(2)], ®istry), + Err(CustodyError::WrongKind) + ); + let stale = CarrierRootToken { + core: root.core, + index: root.index, + generation: root.generation + 1, + }; + assert_eq!( + core.transfer_into_carrier( + &mut owner, + &stale, + &[NativeCarrierPath::Field(0)], + ®istry + ), + Err(CustodyError::StaleHolderGeneration) + ); + let foreign = CarrierRootToken { + core: root.core + 1, + index: root.index, + generation: root.generation, + }; + assert_eq!( + core.transfer_into_carrier( + &mut owner, + &foreign, + &[NativeCarrierPath::Field(0)], + ®istry + ), + Err(CustodyError::WrongProgram) + ); + let mut loan = core.begin_borrow(&owner, &frame, ®istry).unwrap(); + assert_eq!( + core.transfer_into_carrier(&mut owner, &root, &[NativeCarrierPath::Field(0)], ®istry), + Err(CustodyError::ActiveBorrow) + ); + core.end_borrow(&mut loan).unwrap(); + core.transfer_into_carrier(&mut owner, &root, &[NativeCarrierPath::Field(0)], ®istry) + .unwrap(); + let other = begin_root(&mut core, &program, &frame, 1); + assert_eq!( + core.validate_carrier_ticket(&other, &[NativeCarrierPath::Field(0)], &owner), + Err(CustodyError::InvalidOwner) + ); + assert_eq!( + core.validate_carrier_inventory(&root, &[]), + Err(CustodyError::InvalidOwner) + ); + assert_eq!( + core.validate_carrier_inventory( + &root, + &[ + (&owner, vec![NativeCarrierPath::Field(0)]), + (&owner, vec![NativeCarrierPath::Field(0)]) + ] + ), + Err(CustodyError::OccupiedHolder) + ); + assert!(events.lock().unwrap().is_empty()); + core.retire_carrier_metadata(&other).unwrap(); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); + core.retire_carrier_metadata(&root).unwrap(); + clean(&core, ®istry); + assert_eq!(core.live_carrier_roots(), 0); +} + +#[test] +fn carrier_holders_partial_operation_panic_retires_once_and_continues_unified_order() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(vec![])); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let parent = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let _parent = acquire( + &mut core, + &mut registry, + &program, + &parent, + 0, + 40, + &events, + false, + ); + let operation = core + .begin_frame(ResourceFrameKind::Operation, ResourcePurpose::Runtime) + .unwrap(); + let mut first = acquire( + &mut core, + &mut registry, + &program, + &operation, + 0, + 41, + &events, + false, + ); + let root = begin_root(&mut core, &program, &operation, 0); + core.transfer_into_carrier(&mut first, &root, &[NativeCarrierPath::Field(0)], ®istry) + .unwrap(); + let _later_scalar = acquire( + &mut core, + &mut registry, + &program, + &operation, + 1, + 42, + &events, + true, + ); + assert_eq!( + core.end_frame(&operation, &mut registry).failure(), + Some(ResourceCleanupFailure::FinalizerPanic) + ); + assert_eq!(*events.lock().unwrap(), [42, 41]); + core.retire_carrier_metadata(&root).unwrap(); + assert_eq!(core.end_frame(&parent, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [42, 41, 40]); + clean(&core, ®istry); + assert_eq!(core.live_carrier_roots(), 0); +} diff --git a/crates/jett_runtime/src/resource_custody/carrier_holders.rs b/crates/jett_runtime/src/resource_custody/carrier_holders.rs new file mode 100644 index 00000000..8e38ebe0 --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/carrier_holders.rs @@ -0,0 +1,447 @@ +//! Private typed carrier holders. Physical tree bits never issue ownership. +use super::*; + +/// Issued only by the validated NativeLayoutInstallation child. +#[derive(Debug, Clone)] +pub(crate) struct RegisteredCarrierSlot { + pub(super) program: Arc, + pub(super) graph: Arc, + pub(super) index: usize, +} + +/// Neither Clone/Copy nor a finalizing Rust Drop. A move consumes this identity. +#[derive(Debug)] +pub(crate) struct CarrierRootToken { + pub(super) core: u64, + pub(super) index: usize, + pub(super) generation: u64, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum CarrierPhase { + Building, + Ready, + Retired, +} + +struct CarrierHolderPath { + path: Vec, + kind: usize, +} + +pub(super) struct CarrierHolderRoot { + frame: usize, + slot: RegisteredCarrierSlot, + generation: u64, + phase: CarrierPhase, + paths: Vec, +} + +impl ResourceCustody { + fn carrier_root_record( + &self, + token: &CarrierRootToken, + ) -> Result<&CarrierHolderRoot, CustodyError> { + if token.core != self.id { + return Err(CustodyError::WrongProgram); + } + self.carrier_roots + .get(token.index) + .filter(|r| r.generation == token.generation && r.phase != CarrierPhase::Retired) + .ok_or(CustodyError::StaleHolderGeneration) + } + + pub(crate) fn begin_carrier_root( + &mut self, + frame: &ResourceFrameToken, + slot: &RegisteredCarrierSlot, + ) -> Result { + let frame = self.frame_token(frame)?; + self.runtime_frame(frame)?; + if !Arc::ptr_eq(&slot.program, &self.program.0) { + return Err(CustodyError::WrongProgram); + } + slot.graph + .slots + .get(slot.index) + .ok_or(CustodyError::UnknownSlot)?; + if self.carrier_roots.iter().any(|r| { + r.phase != CarrierPhase::Retired && r.frame == frame && r.slot.index == slot.index + }) { + return Err(CustodyError::OccupiedHolder); + } + self.carrier_roots + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted)?; + let index = self.carrier_roots.len(); + self.carrier_roots.push(CarrierHolderRoot { + frame, + slot: slot.clone(), + generation: 1, + phase: CarrierPhase::Building, + paths: Vec::new(), + }); + Ok(CarrierRootToken { + core: self.id, + index, + generation: 1, + }) + } + + fn carrier_holder( + &mut self, + token: &CarrierRootToken, + path: &[NativeCarrierPath], + ) -> Result { + let record = self.carrier_root_record(token)?; + self.runtime_frame(record.frame)?; + // A dynamic selection must be normalized by the exact reached runtime row. + if path.iter().any(|p| { + matches!( + p, + NativeCarrierPath::List(NativeCarrierSelector::Dynamic) + | NativeCarrierPath::MapKey(NativeCarrierSelector::Dynamic) + | NativeCarrierPath::MapValue(NativeCarrierSelector::Dynamic) + ) + }) { + return Err(CustodyError::UnknownSlot); + } + let node = record.slot.graph.slots[record.slot.index].node; + let leaf = record + .slot + .graph + .path_node(node, path) + .map_err(|_| CustodyError::UnknownSlot)?; + let NativeCarrierNode::Resource { kind } = record + .slot + .graph + .node(leaf) + .map_err(|_| CustodyError::UnknownSlot)? + else { + return Err(CustodyError::WrongKind); + }; + let kind = *kind as usize; + self.program + .0 + .kinds + .get(kind) + .ok_or(CustodyError::WrongKind)?; + let frame = record.frame; + let path_index = if let Some(i) = record.paths.iter().position(|p| p.path == path) { + if record.paths[i].kind != kind { + return Err(CustodyError::WrongKind); + } + i + } else { + let mut owned_path = Vec::new(); + owned_path + .try_reserve_exact(path.len()) + .map_err(|_| CustodyError::CapacityExhausted)?; + owned_path.extend_from_slice(path); + let record = &mut self.carrier_roots[token.index]; + record + .paths + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted)?; + let i = record.paths.len(); + record.paths.push(CarrierHolderPath { + path: owned_path, + kind, + }); + i + }; + Ok(ResourceHolderToken { + core: self.id, + holder: Holder { + frame, + address: HolderAddress::Carrier { + root: token.index, + path: path_index, + }, + }, + }) + } + + pub(crate) fn transfer_into_carrier( + &mut self, + owner: &mut OwnedResourceToken, + root: &CarrierRootToken, + path: &[NativeCarrierPath], + registry: &ResourceRegistry, + ) -> Result<(), CustodyError> { + if self.carrier_root_record(root)?.phase != CarrierPhase::Building { + return Err(CustodyError::InvalidOwner); + } + let destination = self.carrier_holder(root, path)?; + self.transfer(owner, &destination, registry) + } + + pub(crate) fn validate_carrier_ticket( + &self, + root: &CarrierRootToken, + path: &[NativeCarrierPath], + owner: &OwnedResourceToken, + ) -> Result<(), CustodyError> { + let record = self.carrier_root_record(root)?; + let owner = self.owner(owner)?; + let HolderAddress::Carrier { + root: index, + path: p, + } = owner.holder.address + else { + return Err(CustodyError::InvalidOwner); + }; + if index != root.index + || owner.holder.frame != record.frame + || record + .paths + .get(p) + .is_none_or(|r| r.path != path || r.kind != owner.kind) + { + return Err(CustodyError::InvalidOwner); + } + Ok(()) + } + + /// The complete physical tree and Core inventory must match in both directions. + pub(crate) fn validate_carrier_inventory( + &self, + root: &CarrierRootToken, + owners: &[(&OwnedResourceToken, Vec)], + ) -> Result<(), CustodyError> { + self.carrier_root_record(root)?; + for (i, (owner, path)) in owners.iter().enumerate() { + self.validate_carrier_ticket(root, path, owner)?; + if owners[..i] + .iter() + .any(|(previous, p)| previous.owner == owner.owner || p == path) + { + return Err(CustodyError::OccupiedHolder); + } + } + let live = self.owners.iter().filter(|o| o.live + && matches!(o.holder.address, HolderAddress::Carrier { root: r, .. } if r == root.index)).count(); + if live != owners.len() { + return Err(CustodyError::InvalidOwner); + } + Ok(()) + } + + pub(crate) fn complete_carrier_root( + &mut self, + root: &CarrierRootToken, + ) -> Result<(), CustodyError> { + if self.carrier_root_record(root)?.phase != CarrierPhase::Building { + return Err(CustodyError::InvalidOwner); + } + self.carrier_roots[root.index].phase = CarrierPhase::Ready; + Ok(()) + } + + pub(crate) fn validate_carrier_location( + &self, + root: &CarrierRootToken, + frame: &ResourceFrameToken, + slot: &RegisteredCarrierSlot, + building: bool, + ) -> Result<(), CustodyError> { + let frame = self.frame_token(frame)?; + let record = self.carrier_root_record(root)?; + if !Arc::ptr_eq(&slot.program, &self.program.0) + || record.slot.index != slot.index + || record.slot.graph.as_ref() != slot.graph.as_ref() + || record.frame != frame + || record.phase + != if building { + CarrierPhase::Building + } else { + CarrierPhase::Ready + } + { + return Err(CustodyError::InvalidOwner); + } + Ok(()) + } + + /// A reached tree is transferred in its lexical acquisition order. All checks + /// and capacity reservation precede the first owning-token mutation. + pub(crate) fn move_carrier_inventory( + &mut self, + source: &CarrierRootToken, + destination: &CarrierRootToken, + owners: &mut [( + &mut OwnedResourceToken, + Vec, + Vec, + )], + registry: &ResourceRegistry, + complete_destination: bool, + ) -> Result<(), CustodyError> { + if self.carrier_root_record(source)?.phase != CarrierPhase::Ready { + return Err(CustodyError::InvalidOwner); + } + let target = self.carrier_root_record(destination)?; + if target.phase != CarrierPhase::Building || source.index == destination.index { + return Err(CustodyError::OccupiedHolder); + } + let target_frame = target.frame; + let mut checked = Vec::new(); + checked + .try_reserve_exact(owners.len()) + .map_err(|_| CustodyError::CapacityExhausted)?; + for (token, from, to) in owners.iter() { + self.validate_carrier_ticket(source, from, token)?; + let holder = self.carrier_holder(destination, to)?; + let (holder, generation) = self.checked_transfer(token, &holder, registry)?; + if checked + .iter() + .any(|(previous, h, _)| *previous == token.owner || *h == holder) + { + return Err(CustodyError::OccupiedHolder); + } + checked.push((token.owner, holder, generation)); + } + let live = self.owners.iter().filter(|o| o.live + && matches!(o.holder.address, HolderAddress::Carrier { root: r, .. } if r == source.index)).count(); + if live != owners.len() { + return Err(CustodyError::InvalidOwner); + } + self.frames[target_frame] + .acquisitions + .try_reserve(owners.len()) + .map_err(|_| CustodyError::CapacityExhausted)?; + for ((token, _, _), (owner, holder, generation)) in owners.iter_mut().zip(checked) { + self.owners[owner].holder = holder; + self.owners[owner].generation = generation; + self.frames[target_frame].acquisitions.push(Acquisition { + owner, + holder, + generation, + }); + token.holder_generation = generation; + } + self.carrier_roots[source.index].phase = CarrierPhase::Retired; + if complete_destination { + self.carrier_roots[destination.index].phase = CarrierPhase::Ready; + } + Ok(()) + } + + pub(crate) fn retire_carrier_metadata( + &mut self, + root: &CarrierRootToken, + ) -> Result<(), CustodyError> { + self.carrier_root_record(root)?; + if self.owners.iter().any(|o| o.live + && matches!(o.holder.address, HolderAddress::Carrier { root: r, .. } if r == root.index)) { + return Err(CustodyError::InvalidOwner); + } + self.carrier_roots[root.index].phase = CarrierPhase::Retired; + Ok(()) + } + + pub(crate) fn preflight_carrier_retirement( + &self, + root: &CarrierRootToken, + owners: &[(&OwnedResourceToken, Vec)], + registry: &ResourceRegistry, + ) -> Result<(), CustodyError> { + self.validate_carrier_inventory(root, owners)?; + for (token, _) in owners { + self.validate_registry_owner(self.owner(token)?, registry)?; + if self.borrowed(token.owner) { + return Err(CustodyError::ActiveBorrow); + } + } + Ok(()) + } + + pub(crate) fn close_carrier_inventory( + &mut self, + root: &CarrierRootToken, + owners: &mut [(&mut OwnedResourceToken, Vec)], + registry: &mut ResourceRegistry, + ) -> ResourceCleanupOutcome { + // The caller preflights the exact inventory before removing its shell. + // Recheck all identity/count/borrow facts without allocating a receipt. + let mut outcome = ResourceCleanupOutcome::default(); + let checked = (|| { + self.carrier_root_record(root)?; + for (i, (token, path)) in owners.iter().enumerate() { + self.validate_carrier_ticket(root, path, token)?; + self.validate_registry_owner(self.owner(token)?, registry)?; + if self.borrowed(token.owner) { + return Err(CustodyError::ActiveBorrow); + } + if owners[..i] + .iter() + .any(|(p, old)| p.owner == token.owner || old == path) + { + return Err(CustodyError::OccupiedHolder); + } + } + let live = self + .owners + .iter() + .filter(|o| { + o.live + && matches!(o.holder.address, + HolderAddress::Carrier { root: r, .. } if r == root.index) + }) + .count(); + if live != owners.len() { + return Err(CustodyError::InvalidOwner); + } + Ok(()) + })(); + if let Err(error) = checked { + outcome.fail(ResourceCleanupFailure::Custody(error)); + return outcome; + } + for (token, _) in owners.iter_mut().rev() { + outcome.merge(self.close(token, registry)); + } + if let Err(error) = self.retire_carrier_metadata(root) { + outcome.fail(ResourceCleanupFailure::Custody(error)); + } + outcome + } + + pub(super) fn holder_kind(&self, holder: Holder) -> Result { + self.frame(holder.frame)?; + match holder.address { + HolderAddress::Slot(slot) => self + .program + .0 + .slots + .get(slot) + .copied() + .ok_or(CustodyError::UnknownSlot), + HolderAddress::Carrier { root, path } => { + let r = self + .carrier_roots + .get(root) + .filter(|r| r.phase != CarrierPhase::Retired && r.frame == holder.frame) + .ok_or(CustodyError::StaleHolderGeneration)?; + r.paths + .get(path) + .map(|p| p.kind) + .ok_or(CustodyError::UnknownSlot) + } + } + } + + pub(crate) fn live_carrier_roots(&self) -> usize { + self.carrier_roots + .iter() + .filter(|r| r.phase != CarrierPhase::Retired) + .count() + } + + pub(crate) fn carrier_retirement_completed(&self, root: &CarrierRootToken) -> bool { + root.core == self.id + && self.carrier_roots.get(root.index).is_some_and(|record| { + record.generation == root.generation && record.phase == CarrierPhase::Retired + }) + } +} diff --git a/crates/jett_runtime/src/resource_custody/registration.rs b/crates/jett_runtime/src/resource_custody/registration.rs new file mode 100644 index 00000000..38c76ef5 --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/registration.rs @@ -0,0 +1,281 @@ +//! Sole native core metadata issuer. Registration never installs a provider or owner. + +use super::{ + CustodyError, ProgramLayout, RegisteredCarrierSlot, RegisteredOwnerSlot, + RegisteredResourceKind, RegisteredResourceProgram, +}; +use crate::{ResourceRegistry, ResourceTypeId}; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; + +#[path = "registration/carriers.rs"] +mod carriers; +#[cfg(test)] +#[path = "registration/carriers_tests.rs"] +mod carriers_tests; +pub(crate) use carriers::*; +#[cfg(test)] +pub(crate) use carriers_tests::runtime_fixture_bytes as carrier_runtime_fixture_bytes; +#[path = "registration/schema.rs"] +mod schema; +#[path = "registration/source_validation.rs"] +mod source_validation; +#[path = "registration/validation.rs"] +mod validation; +#[path = "registration/wire.rs"] +mod wire; +pub(crate) use schema::{ + NativeAccess, NativeFormal, NativeFrame, NativeFrameRole, NativeHook, NativeLoanSource, + NativeOperation, NativeOperationRecord, NativeParent, NativePayloadStep, NativePosition, + NativeRecipe, NativeShape, NativeSignature, NativeSite, NativeSlot, NativeSourceEffect, + NativeSourceFormal, NativeSourceInvocation, NativeSourceResult, NativeSourceSyntax, + NativeSourceValue, NativeSumLoanSource, +}; + +pub(crate) const NATIVE_RESOURCE_LAYOUT_WIRE_VERSION: u32 = 3; + +static NEXT_NATIVE_KIND: AtomicU64 = AtomicU64::new(1); + +pub(crate) const RESOURCE_DOMAIN_OK: u32 = 0; +pub(crate) const RESOURCE_DOMAIN_ERROR: u32 = 1; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ResourceLayoutError { + Header, + Reserved, + Length, + Truncated, + Trailing, + WireLimit, + UnknownTag, + DenseOrdinal, + InvalidReference, + NonCanonical, + UnsupportedLayout, + RecipeMismatch, + PayloadPath, + ShapeMismatch, + FrameMismatch, + OperationMismatch, + CapacityExhausted, + AlreadyInstalled, + WrongContext, + RegistryBusy, + RegistryShutdown, + PhysicalStorage, +} +impl std::fmt::Display for ResourceLayoutError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "native Resource layout refused: {self:?}") + } +} +impl std::error::Error for ResourceLayoutError {} + +/// Fixed data record only. Its value is not a registry key or core token. +#[repr(C)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub(crate) struct JettResourceCallResultV1 { + pub(crate) domain: u32, + pub(crate) reserved: u32, + pub(crate) value: u64, +} + +#[derive(Debug)] +pub(crate) struct RegisteredNativeLayout { + core: RegisteredResourceProgram, + wire: schema::WireLayout, +} +impl RegisteredNativeLayout { + pub(crate) fn carrier_slot(&self, ordinal: u32) -> Result { + self.wire + .carriers + .slots + .get(ordinal as usize) + .ok_or(CustodyError::UnknownSlot)?; + Ok(RegisteredCarrierSlot { + program: self.core.0.clone(), + graph: Arc::new(self.wire.carriers.clone()), + index: ordinal as usize, + }) + } + pub(crate) fn carriers(&self) -> &NativeCarrierLayout { + &self.wire.carriers + } + pub(crate) fn carrier_source_node( + &self, + source: NativeCarrierSource, + ) -> Result { + carriers::source_node(&self.wire, source) + } + pub(crate) fn carrier_equivalent_sum(&self, node: u32, shape: u32) -> bool { + carriers::equivalent_sum(&self.wire, node, shape) + } + pub(crate) fn wire_version(&self) -> u32 { + self.wire.version + } + pub(crate) fn program(&self) -> &RegisteredResourceProgram { + &self.core + } + pub(crate) fn kind(&self, ordinal: u32) -> Result { + self.core.kind(ordinal as usize) + } + pub(crate) fn slot(&self, ordinal: u32) -> Result { + self.core.slot(ordinal as usize) + } + pub(crate) fn kind_id(&self, ordinal: u32) -> Result { + self.core + .0 + .kinds + .get(ordinal as usize) + .copied() + .ok_or(ResourceLayoutError::InvalidReference) + } + pub(crate) fn slot_kind(&self, ordinal: u32) -> Result { + self.core + .0 + .slots + .get(ordinal as usize) + .map(|kind| *kind as u32) + .ok_or(ResourceLayoutError::InvalidReference) + } + pub(crate) fn kinds_len(&self) -> usize { + self.core.0.kinds.len() + } + pub(crate) fn hooks(&self) -> &[NativeHook] { + &self.wire.hooks + } + pub(crate) fn signatures(&self) -> &[NativeSignature] { + &self.wire.signatures + } + pub(crate) fn shapes(&self) -> &[NativeShape] { + &self.wire.shapes + } + pub(crate) fn frames(&self) -> &[NativeFrame] { + &self.wire.frames + } + pub(crate) fn slots(&self) -> &[NativeSlot] { + &self.wire.slots + } + pub(crate) fn operations(&self) -> &[NativeOperationRecord] { + &self.wire.operations + } + + /// Before a provider effect; core and future opaque-token table preflights + /// remain independently mandatory. No payload/grant/preparation token is returned. + pub(crate) fn preflight_physical_acquisition( + &self, + registry: &mut ResourceRegistry, + ) -> Result<(), ResourceLayoutError> { + if registry.context_id != self.core.0.context { + return Err(ResourceLayoutError::WrongContext); + } + if registry.shutting_down { + return Err(ResourceLayoutError::RegistryShutdown); + } + if registry.next_creation_sequence == u64::MAX { + return Err(ResourceLayoutError::CapacityExhausted); + } + if let Some(&index) = registry.free_slots.last() { + checked_physical_slot(index)?; + let slot = registry + .slots + .get(index) + .ok_or(ResourceLayoutError::PhysicalStorage)?; + if slot.entry.is_some() || slot.generation == 0 { + return Err(ResourceLayoutError::PhysicalStorage); + } + if slot.generation == u64::MAX { + return Err(ResourceLayoutError::CapacityExhausted); + } + } else { + checked_physical_slot(registry.slots.len())?; + registry + .slots + .try_reserve(1) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + } + Ok(()) + } +} + +fn checked_physical_slot(index: usize) -> Result<(), ResourceLayoutError> { + u32::try_from(index) + .map(|_| ()) + .map_err(|_| ResourceLayoutError::CapacityExhausted) +} + +fn issued_kinds( + counter: &AtomicU64, + count: usize, +) -> Result, ResourceLayoutError> { + let mut kinds = wire::storage(count)?; + if count == 0 { + return Ok(kinds); + } + let count = u64::try_from(count).map_err(|_| ResourceLayoutError::CapacityExhausted)?; + let first = counter + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |next| { + if next == 0 { + return None; + } + next.checked_add(count) + .filter(|end| *end <= u64::from(u32::MAX) + 1) + }) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + for offset in 0..count { + kinds.push(ResourceTypeId::new((first + offset) as u32)); + } + Ok(kinds) +} + +/// One stationary runtime context owns this holder. Fields cannot be assembled +/// from independently supplied program/kind/slot metadata. +pub(crate) struct NativeLayoutInstallation { + context: u64, + installed: Option>, +} +impl NativeLayoutInstallation { + pub(crate) fn new(registry: &ResourceRegistry) -> Self { + Self { + context: registry.context_id, + installed: None, + } + } + pub(crate) fn installed(&self) -> Option<&Arc> { + self.installed.as_ref() + } + pub(crate) fn install( + &mut self, + registry: &ResourceRegistry, + bytes: &[u8], + ) -> Result, ResourceLayoutError> { + if registry.context_id != self.context { + return Err(ResourceLayoutError::WrongContext); + } + if self.installed.is_some() { + return Err(ResourceLayoutError::AlreadyInstalled); + } + if registry.shutting_down { + return Err(ResourceLayoutError::RegistryShutdown); + } + if registry.live_count != 0 { + return Err(ResourceLayoutError::RegistryBusy); + } + let wire = wire::decode(bytes)?; + let slots = validation::validate(&wire)?; + // All fallible table/projection allocation and checks precede identity issuance. + let kinds = issued_kinds(&NEXT_NATIVE_KIND, wire.kinds.len())?; + let core = RegisteredResourceProgram(Arc::new(ProgramLayout { + context: self.context, + kinds, + slots, + })); + let layout = Arc::new(RegisteredNativeLayout { core, wire }); + self.installed = Some(layout.clone()); + Ok(layout) + } +} + +#[cfg(test)] +#[path = "registration/tests.rs"] +mod tests; diff --git a/crates/jett_runtime/src/resource_custody/registration/carriers.rs b/crates/jett_runtime/src/resource_custody/registration/carriers.rs new file mode 100644 index 00000000..f5045697 --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/registration/carriers.rs @@ -0,0 +1,1156 @@ +//! V3 carrier consistency records. None of these records is Source authority. +use super::{ResourceLayoutError, schema::*}; +use std::collections::HashSet; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct NativeCarrierField { + pub(crate) name: Vec, + pub(crate) node: u32, +} +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct NativeCarrierMember { + pub(crate) name: Vec, + pub(crate) discriminant: i64, + pub(crate) fields: Vec, +} +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) enum NativeCarrierNode { + Ordinary { + shape: u32, + }, + Resource { + kind: u32, + }, + Optional { + child: u32, + }, + Result { + ok: u32, + fail: u32, + }, + List { + element: u32, + }, + Map { + key: u32, + value: u32, + }, + Struct { + identity: Vec, + arguments: Vec, + fields: Vec, + }, + Enum { + identity: Vec, + arguments: Vec, + variants: Vec, + }, + Machine { + identity: Vec, + states: Vec, + transitions: Vec<(u32, u32)>, + }, + MachineState { + machine: u32, + state: u32, + }, + Refinement { + identity: Vec, + base: u32, + predicate: Vec, + }, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum NativeCarrierSelector { + Static(u32), + Dynamic, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum NativeCarrierPath { + Field(u32), + EnumPayload { variant: u32, field: u32 }, + MachinePayload { state: u32, field: u32 }, + List(NativeCarrierSelector), + MapKey(NativeCarrierSelector), + MapValue(NativeCarrierSelector), + Some, + Ok, + Fail, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum NativeCarrierLoanSource { + ExistingBorrow { operation: u32 }, + IncomingViewFormal { scope: u32, parameter: u32 }, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum NativeCarrierSource { + Slot(u32), + Loan(NativeCarrierLoanSource), +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum NativeCarrierChildSource { + Ordinary { shape: u32 }, + Move { slot: u32 }, + LeafSum { slot: u32 }, + LeafOwner { slot: u32 }, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NativeCarrierChild { + pub(crate) index: u32, + pub(crate) node: u32, + pub(crate) source: NativeCarrierChildSource, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum NativeCarrierObservation { + Length, + Tag, + State, + Ordinary { shape: u32, clone: bool }, +} +impl NativeCarrierObservation { + /// Borrowed observation may copy scalar bits or clone one exact String. + /// Other ordinary owners require their own typed cloning transport. + pub(crate) fn ordinary_clone_policy(shapes: &[NativeShape], shape: u32) -> Option { + match shapes.get(shape as usize)? { + NativeShape::String => Some(true), + NativeShape::Integer { .. } + | NativeShape::Float { .. } + | NativeShape::Bool + | NativeShape::Nothing => Some(false), + _ => None, + } + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum NativeCarrierDestination { + Slot(u32), + Ordinary { shape: u32 }, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NativeCarrierSlot { + pub(crate) frame: u32, + pub(crate) node: u32, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum NativeCarrierOperation { + Construct { + frame: u32, + destination: u32, + selector: u32, + children: Vec, + }, + Transfer { + frame: u32, + source: u32, + destination: u32, + }, + Borrow { + frame: u32, + source: NativeCarrierSource, + path: Vec, + node: u32, + lease_frame: u32, + }, + EndBorrow { + frame: u32, + borrow: u32, + }, + Observe { + frame: u32, + source: NativeCarrierSource, + path: Vec, + observation: NativeCarrierObservation, + }, + Extract { + frame: u32, + source: u32, + path: Vec, + destination: NativeCarrierDestination, + }, + AdaptSum { + frame: u32, + source: NativeCarrierSource, + path: Vec, + sum_slot: u32, + lease_frame: u32, + borrowed: bool, + }, + Retire { + frame: u32, + source: u32, + }, + PublishReturn { + frame: u32, + source: u32, + }, + ResetIteration { + frame: u32, + source: u32, + carrier_binders: Vec, + leaf_sum_binders: Vec, + }, + QualifyMachine { + frame: u32, + source: u32, + destination: u32, + machine: u32, + state: u32, + }, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NativeCarrierOperationRecord { + pub(crate) site: NativeSite, + pub(crate) operation: NativeCarrierOperation, +} +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct NativeCarrierLayout { + pub(crate) nodes: Vec, + pub(crate) slots: Vec, + pub(crate) operations: Vec, +} +impl NativeCarrierOperation { + pub(crate) fn frame(&self) -> u32 { + match self { + Self::Construct { frame, .. } + | Self::Transfer { frame, .. } + | Self::Borrow { frame, .. } + | Self::EndBorrow { frame, .. } + | Self::Observe { frame, .. } + | Self::Extract { frame, .. } + | Self::AdaptSum { frame, .. } + | Self::Retire { frame, .. } + | Self::PublishReturn { frame, .. } + | Self::ResetIteration { frame, .. } + | Self::QualifyMachine { frame, .. } => *frame, + } + } +} +impl NativeCarrierLayout { + pub(crate) fn node(&self, node: u32) -> Result<&NativeCarrierNode, ResourceLayoutError> { + self.nodes + .get(node as usize) + .ok_or(ResourceLayoutError::InvalidReference) + } + pub(crate) fn selected_children( + &self, + node: u32, + selector: u32, + ) -> Result, ResourceLayoutError> { + let fields = |fields: &[NativeCarrierField]| fields.iter().map(|f| f.node).collect(); + Ok(match self.node(node)? { + NativeCarrierNode::Ordinary { .. } if selector == 0 => vec![node], + NativeCarrierNode::Optional { .. } if selector == 0 => vec![], + NativeCarrierNode::Optional { child } if selector == 1 => vec![*child], + NativeCarrierNode::Result { fail, .. } if selector == 0 => vec![*fail], + NativeCarrierNode::Result { ok, .. } if selector == 1 => vec![*ok], + NativeCarrierNode::Struct { fields: fs, .. } if selector == 0 => fields(fs), + NativeCarrierNode::Enum { variants, .. } => fields( + &variants + .get(selector as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .fields, + ), + NativeCarrierNode::Machine { states, .. } => fields( + &states + .get(selector as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .fields, + ), + NativeCarrierNode::MachineState { machine, state } if selector == *state => { + self.selected_children(*machine, selector)? + } + NativeCarrierNode::List { .. } | NativeCarrierNode::Map { .. } if selector == 0 => { + vec![] + } + _ => return Err(ResourceLayoutError::UnsupportedLayout), + }) + } + pub(crate) fn path_node( + &self, + mut node: u32, + path: &[NativeCarrierPath], + ) -> Result { + for step in path { + node = match (self.node(node)?, step) { + (NativeCarrierNode::Struct { fields, .. }, NativeCarrierPath::Field(i)) => { + fields + .get(*i as usize) + .ok_or(ResourceLayoutError::PayloadPath)? + .node + } + ( + NativeCarrierNode::Enum { variants, .. }, + NativeCarrierPath::EnumPayload { variant, field }, + ) => { + variants + .get(*variant as usize) + .and_then(|v| v.fields.get(*field as usize)) + .ok_or(ResourceLayoutError::PayloadPath)? + .node + } + ( + NativeCarrierNode::Machine { states, .. }, + NativeCarrierPath::MachinePayload { state, field }, + ) => { + states + .get(*state as usize) + .and_then(|s| s.fields.get(*field as usize)) + .ok_or(ResourceLayoutError::PayloadPath)? + .node + } + ( + NativeCarrierNode::MachineState { + machine, + state: expected, + }, + NativeCarrierPath::MachinePayload { state, field }, + ) if state == expected => self.path_node( + *machine, + &[NativeCarrierPath::MachinePayload { + state: *state, + field: *field, + }], + )?, + (NativeCarrierNode::List { element }, NativeCarrierPath::List(_)) => *element, + (NativeCarrierNode::Map { key, .. }, NativeCarrierPath::MapKey(_)) => *key, + (NativeCarrierNode::Map { value, .. }, NativeCarrierPath::MapValue(_)) => *value, + (NativeCarrierNode::Optional { child }, NativeCarrierPath::Some) => *child, + (NativeCarrierNode::Result { ok, .. }, NativeCarrierPath::Ok) => *ok, + (NativeCarrierNode::Result { fail, .. }, NativeCarrierPath::Fail) => *fail, + _ => return Err(ResourceLayoutError::PayloadPath), + }; + } + Ok(node) + } +} + +pub(super) fn validate(layout: &WireLayout) -> Result<(), ResourceLayoutError> { + let graph = &layout.carriers; + if layout.version != 3 && graph != &NativeCarrierLayout::default() { + return Err(ResourceLayoutError::UnsupportedLayout); + } + let mut unique = HashSet::new(); + let mut identities = HashSet::new(); + unique + .try_reserve(graph.nodes.len()) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + identities + .try_reserve(graph.nodes.len()) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + for (i, node) in graph.nodes.iter().enumerate() { + if !unique.insert(node) { + return Err(ResourceLayoutError::NonCanonical); + } + let child = |id: u32| { + if (id as usize) < i { + Ok(()) + } else { + Err(ResourceLayoutError::InvalidReference) + } + }; + let fields = |fs: &[NativeCarrierField]| { + let mut names = HashSet::new(); + names + .try_reserve(fs.len()) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + for f in fs { + if f.name.is_empty() + || std::str::from_utf8(&f.name).is_err() + || !names.insert(&f.name) + { + return Err(ResourceLayoutError::NonCanonical); + } + child(f.node)?; + } + Ok(()) + }; + let members = |ms: &[NativeCarrierMember]| { + let mut names = HashSet::new(); + names + .try_reserve(ms.len()) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + for m in ms { + if m.name.is_empty() + || std::str::from_utf8(&m.name).is_err() + || !names.insert(&m.name) + { + return Err(ResourceLayoutError::NonCanonical); + } + fields(&m.fields)?; + } + Ok(()) + }; + match node { + NativeCarrierNode::Ordinary { shape: shape_id } => { + let shape = layout + .shapes + .get(*shape_id as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + if super::source_validation::occupied(layout, *shape_id)? + || matches!(shape, NativeShape::HookDescriptor { .. }) + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeCarrierNode::Resource { kind } => { + layout + .kinds + .get(*kind as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + } + NativeCarrierNode::Optional { child: id } | NativeCarrierNode::List { element: id } => { + child(*id)? + } + NativeCarrierNode::Result { ok, fail } => { + child(*ok)?; + child(*fail)?; + } + NativeCarrierNode::Map { key, value } => { + child(*key)?; + child(*value)?; + if !map_key(layout, *key)? { + return Err(ResourceLayoutError::UnsupportedLayout); + } + } + NativeCarrierNode::Struct { + identity, + arguments, + fields: fs, + } => { + if identity.is_empty() || !identities.insert(identity) { + return Err(ResourceLayoutError::NonCanonical); + } + for a in arguments { + child(*a)?; + } + fields(fs)?; + } + NativeCarrierNode::Enum { + identity, + arguments, + variants, + } => { + if identity.is_empty() || !identities.insert(identity) { + return Err(ResourceLayoutError::NonCanonical); + } + for a in arguments { + child(*a)?; + } + members(variants)?; + } + NativeCarrierNode::Machine { + identity, + states, + transitions, + } => { + if identity.is_empty() || !identities.insert(identity) { + return Err(ResourceLayoutError::NonCanonical); + } + members(states)?; + let mut pairs = HashSet::new(); + for (a, b) in transitions { + if *a as usize >= states.len() + || *b as usize >= states.len() + || !pairs.insert((*a, *b)) + { + return Err(ResourceLayoutError::InvalidReference); + } + } + } + NativeCarrierNode::MachineState { machine, state } => { + child(*machine)?; + if !matches!(graph.node(*machine)?,NativeCarrierNode::Machine{states,..} if (*state as usize) { + child(*base)?; + if identity.is_empty() || predicate.is_empty() || !identities.insert(identity) { + return Err(ResourceLayoutError::NonCanonical); + } + } + } + } + for s in &graph.slots { + graph.node(s.node)?; + layout + .frames + .get(s.frame as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + } + for (i, row) in graph.operations.iter().enumerate() { + let frame = layout + .frames + .get(row.operation.frame() as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + if frame.site.function != row.site.function { + return Err(ResourceLayoutError::FrameMismatch); + } + let wrappers = layout + .operations + .iter() + .filter(|r| { + r.site == row.site + && matches!(r.operation,NativeOperation::Carrier{record} if record as usize==i) + }) + .count(); + if wrappers != 1 { + return Err(ResourceLayoutError::OperationMismatch); + } + validate_operation(layout, row)?; + } + Ok(()) +} + +pub(crate) fn source_node( + layout: &WireLayout, + source: NativeCarrierSource, +) -> Result { + match source { + NativeCarrierSource::Slot(slot) => layout + .carriers + .slots + .get(slot as usize) + .map(|s| s.node) + .ok_or(ResourceLayoutError::InvalidReference), + NativeCarrierSource::Loan(NativeCarrierLoanSource::ExistingBorrow { operation }) => { + let NativeOperation::Carrier { record } = layout + .operations + .get(operation as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .operation + else { + return Err(ResourceLayoutError::OperationMismatch); + }; + match layout + .carriers + .operations + .get(record as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .operation + { + NativeCarrierOperation::Borrow { node, .. } => Ok(node), + _ => Err(ResourceLayoutError::OperationMismatch), + } + } + NativeCarrierSource::Loan(NativeCarrierLoanSource::IncomingViewFormal { + scope, + parameter, + }) => { + let f = layout + .frames + .get(scope as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + let formal = layout + .signatures + .get(f.signature as usize) + .and_then(|s| s.parameters.get(parameter as usize)) + .ok_or(ResourceLayoutError::InvalidReference)?; + if f.role != NativeFrameRole::Scope || formal.access != NativeAccess::View { + return Err(ResourceLayoutError::OperationMismatch); + } + match layout.shapes.get(formal.shape as usize) { + Some(NativeShape::Carrier { node }) => Ok(*node), + _ => Err(ResourceLayoutError::ShapeMismatch), + } + } + } +} +fn validate_operation( + layout: &WireLayout, + row: &NativeCarrierOperationRecord, +) -> Result<(), ResourceLayoutError> { + let graph = &layout.carriers; + let slot = |id: u32| { + graph + .slots + .get(id as usize) + .ok_or(ResourceLayoutError::InvalidReference) + }; + let path = |source, path: &[NativeCarrierPath]| { + if path + .iter() + .filter(|s| { + matches!( + s, + NativeCarrierPath::List(NativeCarrierSelector::Dynamic) + | NativeCarrierPath::MapKey(NativeCarrierSelector::Dynamic) + | NativeCarrierPath::MapValue(NativeCarrierSelector::Dynamic) + ) + }) + .count() + > 1 + { + return Err(ResourceLayoutError::PayloadPath); + } + graph.path_node(source_node(layout, source)?, path) + }; + let source = |source: NativeCarrierSource| { + let function = match source { + NativeCarrierSource::Slot(id) => { + layout + .frames + .get(slot(id)?.frame as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .site + .function + } + NativeCarrierSource::Loan(NativeCarrierLoanSource::ExistingBorrow { operation }) => { + layout + .operations + .get(operation as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .site + .function + } + NativeCarrierSource::Loan(NativeCarrierLoanSource::IncomingViewFormal { + scope, + .. + }) => { + layout + .frames + .get(scope as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .site + .function + } + }; + if function != row.site.function { + return Err(ResourceLayoutError::FrameMismatch); + } + Ok(()) + }; + match &row.operation { + NativeCarrierOperation::Construct { children, .. } => { + for child in children { + match child.source { + NativeCarrierChildSource::Move { slot } => { + source(NativeCarrierSource::Slot(slot))? + } + NativeCarrierChildSource::LeafSum { slot } + | NativeCarrierChildSource::LeafOwner { slot } => { + let s = layout + .slots + .get(slot as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + if layout.frames[s.frame as usize].site.function != row.site.function { + return Err(ResourceLayoutError::FrameMismatch); + } + } + _ => {} + } + } + } + NativeCarrierOperation::Transfer { + source: id, + destination, + .. + } + | NativeCarrierOperation::QualifyMachine { + source: id, + destination, + .. + } => { + source(NativeCarrierSource::Slot(*id))?; + source(NativeCarrierSource::Slot(*destination))?; + } + NativeCarrierOperation::Borrow { source: id, .. } + | NativeCarrierOperation::Observe { source: id, .. } + | NativeCarrierOperation::AdaptSum { source: id, .. } => source(*id)?, + NativeCarrierOperation::EndBorrow { borrow, .. } => { + if layout + .operations + .get(*borrow as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .site + .function + != row.site.function + { + return Err(ResourceLayoutError::FrameMismatch); + } + } + NativeCarrierOperation::Extract { + source: id, + destination, + .. + } => { + source(NativeCarrierSource::Slot(*id))?; + if let NativeCarrierDestination::Slot(id) = destination { + source(NativeCarrierSource::Slot(*id))?; + } + } + NativeCarrierOperation::Retire { source: id, .. } + | NativeCarrierOperation::PublishReturn { source: id, .. } + | NativeCarrierOperation::ResetIteration { source: id, .. } => { + source(NativeCarrierSource::Slot(*id))? + } + } + match &row.operation { + NativeCarrierOperation::Construct { + frame, + destination, + selector, + children, + } => { + let target = slot(*destination)?; + if target.frame != *frame { + return Err(ResourceLayoutError::FrameMismatch); + } + let mut indices = HashSet::new(); + let expected = graph.selected_children(target.node, *selector)?; + for c in children { + graph.node(c.node)?; + if !indices.insert(c.index) { + return Err(ResourceLayoutError::NonCanonical); + } + match c.source { + NativeCarrierChildSource::Ordinary { shape } => { + if graph.node(c.node)? != &(NativeCarrierNode::Ordinary { shape }) { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeCarrierChildSource::Move { slot: id } => { + if slot(id)?.node != c.node { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeCarrierChildSource::LeafOwner { slot: id } => { + let s = layout + .slots + .get(id as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + let NativeCarrierNode::Resource { kind } = graph.node(c.node)? else { + return Err(ResourceLayoutError::ShapeMismatch); + }; + if layout.shapes.get(s.shape as usize) + != Some(&NativeShape::Resource { kind: *kind }) + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeCarrierChildSource::LeafSum { slot: id } => { + let s = layout + .slots + .get(id as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + if !equivalent_sum(layout, c.node, s.shape) { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + } + } + match graph.node(target.node)? { + NativeCarrierNode::List { element } => { + if children + .iter() + .enumerate() + .any(|(i, c)| c.index as usize != i || c.node != *element) + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeCarrierNode::Map { key, value } => { + if children.len() % 2 != 0 + || children.iter().enumerate().any(|(i, c)| { + c.index as usize != i + || c.node != if i % 2 == 0 { *key } else { *value } + }) + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + _ => { + if children.len() != expected.len() + || children + .iter() + .any(|c| expected.get(c.index as usize) != Some(&c.node)) + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + } + } + NativeCarrierOperation::Transfer { + source, + destination, + .. + } => { + if source == destination || slot(*source)?.node != slot(*destination)?.node { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeCarrierOperation::Borrow { + source, + path: p, + node, + lease_frame, + frame, + } => { + if path(*source, p)? != *node || lease_frame != frame { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeCarrierOperation::EndBorrow { borrow, .. } => { + let NativeOperation::Carrier { record } = layout + .operations + .get(*borrow as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .operation + else { + return Err(ResourceLayoutError::OperationMismatch); + }; + if !matches!( + graph.operations.get(record as usize).map(|r| &r.operation), + Some(NativeCarrierOperation::Borrow { .. }) + ) { + return Err(ResourceLayoutError::OperationMismatch); + } + } + NativeCarrierOperation::Observe { + source, + path: p, + observation, + .. + } => { + let n = graph.node(path(*source, p)?)?; + let ok = match observation { + NativeCarrierObservation::Length => matches!( + n, + NativeCarrierNode::List { .. } | NativeCarrierNode::Map { .. } + ), + NativeCarrierObservation::Tag => matches!( + n, + NativeCarrierNode::Optional { .. } + | NativeCarrierNode::Result { .. } + | NativeCarrierNode::Enum { .. } + ), + NativeCarrierObservation::State => matches!( + n, + NativeCarrierNode::Machine { .. } | NativeCarrierNode::MachineState { .. } + ), + NativeCarrierObservation::Ordinary { shape, clone } => { + if NativeCarrierObservation::ordinary_clone_policy(&layout.shapes, *shape) + != Some(*clone) + { + return Err(ResourceLayoutError::OperationMismatch); + } + n == &(NativeCarrierNode::Ordinary { shape: *shape }) + } + }; + if !ok { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeCarrierOperation::Extract { + source, + path: p, + destination, + .. + } => { + let node = path(NativeCarrierSource::Slot(*source), p)?; + match destination { + NativeCarrierDestination::Slot(id) => { + if slot(*id)?.node != node { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeCarrierDestination::Ordinary { shape } => { + if graph.node(node)? != &(NativeCarrierNode::Ordinary { shape: *shape }) { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + } + } + NativeCarrierOperation::AdaptSum { + source, + path: p, + sum_slot, + lease_frame, + frame, + borrowed, + } => { + let node = path(*source, p)?; + let sum = layout + .slots + .get(*sum_slot as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + if lease_frame != frame + || sum.frame != *frame + || (!borrowed && matches!(source, NativeCarrierSource::Loan(_))) + || !equivalent_sum(layout, node, sum.shape) + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeCarrierOperation::Retire { source, .. } => { + slot(*source)?; + } + NativeCarrierOperation::PublishReturn { source, frame } => { + let source = slot(*source)?; + let f = &layout.frames[*frame as usize]; + let r = &layout.frames[source.frame as usize]; + if f.role != NativeFrameRole::Scope + || r.role != NativeFrameRole::Return + || f.site.function != r.site.function + || f.signature != r.signature + || !f.parents.contains(&NativeParent::Frame(source.frame)) + || !matches!(layout.shapes.get(layout.signatures[f.signature as usize].result as usize),Some(NativeShape::Carrier{node}) if *node==source.node) + { + return Err(ResourceLayoutError::OperationMismatch); + } + } + NativeCarrierOperation::ResetIteration { + source, + carrier_binders, + leaf_sum_binders, + frame, + } => { + let source_slot = slot(*source)?; + if !matches!( + graph.node(source_slot.node)?, + NativeCarrierNode::List { .. } | NativeCarrierNode::Map { .. } + ) { + return Err(ResourceLayoutError::ShapeMismatch); + } + let mut unique = HashSet::new(); + for id in carrier_binders { + if *id == *source || !unique.insert(*id) || slot(*id)?.frame != *frame { + return Err(ResourceLayoutError::OperationMismatch); + } + } + let mut unique = HashSet::new(); + for id in leaf_sum_binders { + let leaf = layout + .slots + .get(*id as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + if !unique.insert(*id) + || leaf.frame != *frame + || !matches!( + layout.shapes.get(leaf.shape as usize), + Some(NativeShape::Optional { .. } | NativeShape::Result { .. }) + ) + { + return Err(ResourceLayoutError::OperationMismatch); + } + } + } + NativeCarrierOperation::QualifyMachine { + source, + destination, + machine, + state, + .. + } => { + if source == destination + || graph.node(slot(*source)?.node)? + != &(NativeCarrierNode::MachineState { + machine: *machine, + state: *state, + }) + || slot(*destination)?.node != *machine + || !matches!(graph.node(*machine)?,NativeCarrierNode::Machine{states,..} if (*state as usize) Result { + for _ in 0..=layout.carriers.nodes.len() { + match layout.carriers.node(node)? { + NativeCarrierNode::Refinement { base, .. } => node = *base, + NativeCarrierNode::Ordinary { shape } => { + return Ok(matches!( + layout.shapes.get(*shape as usize), + Some(NativeShape::Integer { .. } | NativeShape::String | NativeShape::Bool) + )); + } + _ => return Ok(false), + } + } + Err(ResourceLayoutError::InvalidReference) +} +pub(crate) fn equivalent_sum(layout: &WireLayout, node: u32, shape: u32) -> bool { + match ( + layout.carriers.node(node), + layout.shapes.get(shape as usize), + ) { + ( + Ok(NativeCarrierNode::Optional { child }), + Some(NativeShape::Optional { child: shape }), + ) => { + matches!((layout.carriers.node(*child),layout.shapes.get(*shape as usize)),(Ok(NativeCarrierNode::Resource{kind:a}),Some(NativeShape::Resource{kind:b})) if a==b) + } + ( + Ok(NativeCarrierNode::Result { ok, fail }), + Some(NativeShape::Result { + ok: sok, + fail: sfail, + }), + ) => { + matches!((layout.carriers.node(*ok),layout.shapes.get(*sok as usize),layout.carriers.node(*fail)),(Ok(NativeCarrierNode::Resource{kind:a}),Some(NativeShape::Resource{kind:b}),Ok(NativeCarrierNode::Ordinary{shape})) if a==b&&shape==sfail) + } + _ => false, + } +} + +pub(super) fn validate_formal( + layout: &WireLayout, + record: &NativeOperationRecord, + caller_frame: u32, + callee_scope: u32, + formal: &NativeSourceFormal, + caller_slots: &mut HashSet, + callee_slots: &mut HashSet, +) -> Result<(), ResourceLayoutError> { + if layout.version != 3 || formal.actual_shape != formal.callee_shape { + return Err(ResourceLayoutError::ShapeMismatch); + } + let Some(NativeShape::Carrier { node }) = layout.shapes.get(formal.actual_shape as usize) + else { + return Err(ResourceLayoutError::ShapeMismatch); + }; + match formal.value { + NativeSourceValue::CarrierOwned { + caller_argument_slot, + callee_parameter_slot, + } => { + let a = layout + .carriers + .slots + .get(caller_argument_slot as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + let b = layout + .carriers + .slots + .get(callee_parameter_slot as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + if formal.syntax != NativeSourceSyntax::Bare + || formal.effect != NativeSourceEffect::TransferOwned + || formal.access != NativeAccess::Owned + || a.node != *node + || b.node != *node + || a.frame != caller_frame + || b.frame != callee_scope + || !caller_slots.insert(caller_argument_slot) + || !callee_slots.insert(callee_parameter_slot) + { + return Err(ResourceLayoutError::OperationMismatch); + } + } + NativeSourceValue::CarrierView { source } => { + if formal.access != NativeAccess::View + || !matches!( + (formal.syntax, formal.effect), + ( + NativeSourceSyntax::Bare, + NativeSourceEffect::RelinquishOwned + ) | ( + NativeSourceSyntax::WrittenView, + NativeSourceEffect::RetainBorrow + ) + ) + || source_node(layout, NativeCarrierSource::Loan(source))? != *node + { + return Err(ResourceLayoutError::OperationMismatch); + } + let function = match source { + NativeCarrierLoanSource::ExistingBorrow { operation } => { + layout + .operations + .get(operation as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .site + .function + } + NativeCarrierLoanSource::IncomingViewFormal { scope, .. } => { + layout + .frames + .get(scope as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .site + .function + } + }; + if function != record.site.function { + return Err(ResourceLayoutError::FrameMismatch); + } + } + _ => return Err(ResourceLayoutError::OperationMismatch), + } + Ok(()) +} +pub(super) fn validate_result( + layout: &WireLayout, + record: &NativeOperationRecord, + caller_frame: u32, + callee: u32, + signature: &NativeSignature, + scope: &NativeFrame, + source: &NativeSourceInvocation, +) -> Result<(), ResourceLayoutError> { + let NativeSourceResult::Carrier { + shape, + caller_destination_frame, + caller_destination_slot, + callee_return_frame, + permitted_return_slots, + } = &source.result + else { + return Err(ResourceLayoutError::OperationMismatch); + }; + let Some(NativeShape::Carrier { node }) = layout.shapes.get(*shape as usize) else { + return Err(ResourceLayoutError::ShapeMismatch); + }; + let destination = layout + .carriers + .slots + .get(*caller_destination_slot as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + let return_frame = layout + .frames + .get(*callee_return_frame as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + let destination_frame = layout + .frames + .get(*caller_destination_frame as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + if layout.version != 3 + || *shape != signature.result + || source.callee_return != Some(*callee_return_frame) + || return_frame.role != NativeFrameRole::Return + || return_frame.site.function != callee + || return_frame.signature != scope.signature + || !return_frame + .parents + .contains(&NativeParent::Frame(caller_frame)) + || !scope + .parents + .contains(&NativeParent::Frame(*callee_return_frame)) + || destination.frame != *caller_destination_frame + || destination.node != *node + || destination_frame.site.function != record.site.function + || permitted_return_slots.is_empty() + || permitted_return_slots.windows(2).any(|p| p[0] >= p[1]) + { + return Err(ResourceLayoutError::OperationMismatch); + } + for id in permitted_return_slots { + let s = layout + .carriers + .slots + .get(*id as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + if s.frame != *callee_return_frame || s.node != *node { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + Ok(()) +} diff --git a/crates/jett_runtime/src/resource_custody/registration/carriers_tests.rs b/crates/jett_runtime/src/resource_custody/registration/carriers_tests.rs new file mode 100644 index 00000000..2c8dfb6b --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/registration/carriers_tests.rs @@ -0,0 +1,1995 @@ +//! Registration consistency tests only. Graph rows do not authorize live custody +//! or prove a refinement predicate; those remain compiler/runtime obligations. +use super::schema::WireLayout; +use super::wire::{decode, encode_records}; +use super::*; +use crate::ResourceRegistry; + +const INTEGER: u32 = 0; +const STRING: u32 = 1; +const NOTHING: u32 = 2; +const RESOURCE_SHAPE: u32 = 4; +const OPTIONAL_SHAPE: u32 = 5; +const RESULT_SHAPE: u32 = 6; +const WHOLE_SHAPE: u32 = 7; +const RESOURCE_NODE: u32 = 3; +const OPTIONAL_NODE: u32 = 4; +const RESULT_NODE: u32 = 5; +const LIST_NODE: u32 = 6; +const MAP_NODE: u32 = 7; +const RECORD_NODE: u32 = 8; +const ENUM_NODE: u32 = 9; +const MACHINE_NODE: u32 = 10; +const STATE_NODE: u32 = 11; +const REFINEMENT_NODE: u32 = 12; +const GENERIC_RESOURCE_NODE: u32 = 13; +const GENERIC_OPTIONAL_NODE: u32 = 14; +const WHOLE_NODE: u32 = 15; +const OTHER_RECORD_NODE: u32 = 16; + +fn site(index: u32) -> NativeSite { + NativeSite { + function: 0, + block: 0, + position: NativePosition::Statement(index), + } +} + +fn field(name: &str, node: u32) -> NativeCarrierField { + NativeCarrierField { + name: name.as_bytes().to_vec(), + node, + } +} + +fn member(name: &str, discriminant: i64, fields: Vec) -> NativeCarrierMember { + NativeCarrierMember { + name: name.as_bytes().to_vec(), + discriminant, + fields, + } +} + +fn fixture() -> WireLayout { + let record_fields = vec![field("marker", 0), field("channel", OPTIONAL_NODE)]; + WireLayout { + version: 3, + kinds: vec![0], + hooks: vec![], + signatures: vec![NativeSignature { + ordinal: 0, + parameters: vec![], + result: NOTHING, + }], + shapes: vec![ + NativeShape::Integer { + bits: 64, + signed: true, + }, + NativeShape::String, + NativeShape::Nothing, + NativeShape::Bool, + NativeShape::Resource { kind: 0 }, + NativeShape::Optional { + child: RESOURCE_SHAPE, + }, + NativeShape::Result { + ok: RESOURCE_SHAPE, + fail: STRING, + }, + NativeShape::Carrier { node: WHOLE_NODE }, + ], + frames: vec![ + NativeFrame { + ordinal: 0, + role: NativeFrameRole::Scope, + site: site(0), + signature: 0, + parents: vec![NativeParent::Root], + }, + NativeFrame { + ordinal: 1, + role: NativeFrameRole::Operation, + site: site(1), + signature: 0, + parents: vec![NativeParent::Frame(0)], + }, + ], + slots: vec![ + NativeSlot { + ordinal: 0, + frame: 1, + shape: OPTIONAL_SHAPE, + path: vec![NativePayloadStep::Some], + }, + NativeSlot { + ordinal: 1, + frame: 1, + shape: RESULT_SHAPE, + path: vec![NativePayloadStep::Ok], + }, + ], + operations: vec![], + carriers: NativeCarrierLayout { + nodes: vec![ + NativeCarrierNode::Ordinary { shape: INTEGER }, + NativeCarrierNode::Ordinary { shape: STRING }, + NativeCarrierNode::Ordinary { shape: 3 }, + NativeCarrierNode::Resource { kind: 0 }, + NativeCarrierNode::Optional { + child: RESOURCE_NODE, + }, + NativeCarrierNode::Result { + ok: RESOURCE_NODE, + fail: 1, + }, + NativeCarrierNode::List { + element: OPTIONAL_NODE, + }, + NativeCarrierNode::Map { + key: 0, + value: RESULT_NODE, + }, + NativeCarrierNode::Struct { + identity: b"transport.Record".to_vec(), + arguments: vec![], + fields: record_fields.clone(), + }, + NativeCarrierNode::Enum { + identity: b"transport.ChannelState".to_vec(), + arguments: vec![RESOURCE_NODE], + variants: vec![ + member("Closed", -7, vec![field("marker", 0)]), + member("Open", 41, vec![field("channel", RESOURCE_NODE)]), + ], + }, + NativeCarrierNode::Machine { + identity: b"transport.ChannelMachine".to_vec(), + states: vec![ + member("Empty", 11, vec![field("marker", 0)]), + member("Live", 22, vec![field("channel", RESOURCE_NODE)]), + ], + transitions: vec![(0, 1), (1, 0)], + }, + NativeCarrierNode::MachineState { + machine: MACHINE_NODE, + state: 0, + }, + NativeCarrierNode::Refinement { + identity: b"transport.PositiveMarker".to_vec(), + base: 0, + predicate: b"value > 0".to_vec(), + }, + NativeCarrierNode::Struct { + identity: b"transport.Cell[transport.Channel]".to_vec(), + arguments: vec![RESOURCE_NODE], + fields: vec![field("channel", OPTIONAL_NODE)], + }, + NativeCarrierNode::Struct { + identity: b"transport.Cell[optional[transport.Channel]]".to_vec(), + arguments: vec![OPTIONAL_NODE], + fields: vec![field("channel", OPTIONAL_NODE)], + }, + NativeCarrierNode::Struct { + identity: b"transport.Whole".to_vec(), + arguments: vec![], + fields: vec![ + field("record", RECORD_NODE), + field("entries", LIST_NODE), + field("lookup", MAP_NODE), + field("state", ENUM_NODE), + field("machine", MACHINE_NODE), + field("empty", STATE_NODE), + ], + }, + NativeCarrierNode::Struct { + identity: b"other.Record".to_vec(), + arguments: vec![], + fields: record_fields, + }, + ], + slots: vec![ + NativeCarrierSlot { + frame: 1, + node: OPTIONAL_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: RESULT_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: RECORD_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: OTHER_RECORD_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: GENERIC_RESOURCE_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: GENERIC_OPTIONAL_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: LIST_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: MAP_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: WHOLE_NODE, + }, + ], + operations: vec![], + }, + } +} + +#[test] +fn carrier_registration_leaf_owner_has_distinct_wire_tag_and_exact_resource_slot() { + let mut layout = fixture(); + let node = u32::try_from(layout.carriers.nodes.len()).unwrap(); + layout.carriers.nodes.push(NativeCarrierNode::Struct { + identity: b"app.OwnedEnvelope".to_vec(), + arguments: vec![], + fields: vec![field("token", RESOURCE_NODE), field("marker", 0)], + }); + let destination = u32::try_from(layout.carriers.slots.len()).unwrap(); + layout + .carriers + .slots + .push(NativeCarrierSlot { frame: 1, node }); + layout.slots.push(NativeSlot { + ordinal: 2, + frame: 1, + shape: RESOURCE_SHAPE, + path: vec![], + }); + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination, + selector: 0, + children: vec![ + NativeCarrierChild { + index: 0, + node: RESOURCE_NODE, + source: NativeCarrierChildSource::LeafOwner { slot: 2 }, + }, + ordinary_child(1, 0, INTEGER), + ], + }, + ); + assert_eq!( + parsed(&layout).unwrap(), + layout, + "real encoder/decoder must preserve distinct LeafOwner tag4" + ); + for mutation in 0..5 { + let mut forged = layout.clone(); + match mutation { + 0 => forged.slots[2].shape = OPTIONAL_SHAPE, + 1 => forged.slots[2].path = vec![NativePayloadStep::Some], + 2 => { + let NativeCarrierOperation::Construct { children, .. } = + &mut forged.carriers.operations.last_mut().unwrap().operation + else { + unreachable!() + }; + children[0].source = NativeCarrierChildSource::LeafSum { slot: 2 }; + } + 3 => { + let NativeCarrierOperation::Construct { children, .. } = + &mut forged.carriers.operations.last_mut().unwrap().operation + else { + unreachable!() + }; + children[0].node = OPTIONAL_NODE; + } + 4 => { + let NativeCarrierOperation::Construct { children, .. } = + &mut forged.carriers.operations.last_mut().unwrap().operation + else { + unreachable!() + }; + children[1].index = 0; + } + _ => unreachable!(), + } + assert!(parsed(&forged).is_err(), "LeafOwner mismatch {mutation}"); + } +} + +fn legacy(version: u32) -> WireLayout { + let mut layout = fixture(); + layout.version = version; + layout.carriers = NativeCarrierLayout::default(); + assert_eq!( + layout.shapes.pop(), + Some(NativeShape::Carrier { node: WHOLE_NODE }) + ); + layout +} + +fn parsed(layout: &WireLayout) -> Result { + let decoded = decode(&encode_records(layout))?; + validation::validate(&decoded)?; + Ok(decoded) +} + +fn operation(layout: &mut WireLayout, value: NativeCarrierOperation) -> u32 { + let ordinal = u32::try_from(layout.operations.len()).unwrap(); + let record = u32::try_from(layout.carriers.operations.len()).unwrap(); + let site = site(100 + ordinal); + layout.operations.push(NativeOperationRecord { + ordinal, + site, + operation: NativeOperation::Carrier { record }, + }); + layout + .carriers + .operations + .push(NativeCarrierOperationRecord { + site, + operation: value, + }); + ordinal +} + +fn ordinary_child(index: u32, node: u32, shape: u32) -> NativeCarrierChild { + NativeCarrierChild { + index, + node, + source: NativeCarrierChildSource::Ordinary { shape }, + } +} + +fn carrier_child(index: u32, node: u32, slot: u32) -> NativeCarrierChild { + NativeCarrierChild { + index, + node, + source: NativeCarrierChildSource::Move { slot }, + } +} + +fn suffix_offset(bytes: &[u8]) -> usize { + bytes + .windows(8) + .position(|part| part == b"JTCAR003") + .unwrap() +} + +fn write_word(bytes: &mut [u8], offset: usize, value: u32) { + bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); +} + +fn set_length(bytes: &mut [u8]) { + let length = u64::try_from(bytes.len()).unwrap(); + bytes[16..24].copy_from_slice(&length.to_le_bytes()); +} + +#[test] +fn carrier_registration_roundtrips_complete_unused_nominal_and_latent_metadata() { + let layout = fixture(); + assert_eq!(parsed(&layout).unwrap(), layout); + let registry = ResourceRegistry::new(); + let mut installation = NativeLayoutInstallation::new(®istry); + let mut bytes = encode_records(&layout); + let installed = installation.install(®istry, &bytes).unwrap(); + bytes.fill(0); + assert_eq!(installed.carriers(), &layout.carriers); + assert_eq!(installed.wire_version(), 3); + assert_eq!(installed.kinds_len(), 1); + assert_eq!(installed.slots().len(), 2); + assert_eq!(registry.live_count(), 0); + assert!(matches!( + installed.carriers().node(REFINEMENT_NODE).unwrap(), + NativeCarrierNode::Refinement { identity, base: 0, predicate } + if identity.as_slice() == b"transport.PositiveMarker" && predicate.as_slice() == b"value > 0" + )); + assert!(source_validation::occupied(&layout, WHOLE_SHAPE).unwrap()); +} + +#[test] +fn carrier_registration_keeps_legacy_wire_versions_and_refuses_the_v3_suffix() { + for version in [1, 2] { + let layout = legacy(version); + let bytes = encode_records(&layout); + assert_eq!(parsed(&layout).unwrap(), layout); + assert!(!bytes.windows(8).any(|part| part == b"JTCAR003")); + let mut trailing = bytes; + trailing.extend_from_slice(b"JTCAR003"); + trailing.extend_from_slice(&[0; 16]); + set_length(&mut trailing); + assert_eq!( + decode(&trailing).unwrap_err(), + ResourceLayoutError::Trailing + ); + } +} + +#[test] +fn carrier_registration_new_shape_and_operation_tags_require_wire_v3() { + for version in [1, 2] { + let mut shape = legacy(version); + shape.shapes.push(NativeShape::Carrier { node: 0 }); + assert_eq!( + decode(&encode_records(&shape)).unwrap_err(), + ResourceLayoutError::UnknownTag + ); + let mut op = legacy(version); + op.operations.push(NativeOperationRecord { + ordinal: 0, + site: site(100), + operation: NativeOperation::Carrier { record: 0 }, + }); + assert_eq!( + decode(&encode_records(&op)).unwrap_err(), + ResourceLayoutError::UnknownTag + ); + } +} + +#[test] +fn carrier_registration_new_source_roles_and_adapter_loan_require_wire_v3() { + for value in [ + NativeSourceValue::CarrierOwned { + caller_argument_slot: 0, + callee_parameter_slot: 0, + }, + NativeSourceValue::CarrierView { + source: NativeCarrierLoanSource::ExistingBorrow { operation: 0 }, + }, + ] { + let mut layout = legacy(2); + layout.operations.push(NativeOperationRecord { + ordinal: 0, + site: site(100), + operation: NativeOperation::InvokeSourceFunction { + frame: 1, + callee: 0, + signature: 0, + callee_scope: 0, + source: Some(NativeSourceInvocation { + callee_return: None, + evaluation_order: vec![0], + formals: vec![NativeSourceFormal { + parameter: 0, + source_index: 0, + actual_shape: INTEGER, + callee_shape: INTEGER, + syntax: NativeSourceSyntax::Bare, + effect: NativeSourceEffect::TransferOwned, + access: NativeAccess::Owned, + value, + }], + result: NativeSourceResult::Ordinary { shape: NOTHING }, + }), + }, + }); + assert_eq!( + decode(&encode_records(&layout)).unwrap_err(), + ResourceLayoutError::UnknownTag + ); + } + let mut result = legacy(2); + result.operations.push(NativeOperationRecord { + ordinal: 0, + site: site(100), + operation: NativeOperation::InvokeSourceFunction { + frame: 1, + callee: 0, + signature: 0, + callee_scope: 0, + source: Some(NativeSourceInvocation { + callee_return: Some(0), + evaluation_order: vec![], + formals: vec![], + result: NativeSourceResult::Carrier { + shape: INTEGER, + caller_destination_frame: 1, + caller_destination_slot: 0, + callee_return_frame: 0, + permitted_return_slots: vec![0], + }, + }), + }, + }); + assert_eq!( + decode(&encode_records(&result)).unwrap_err(), + ResourceLayoutError::UnknownTag + ); + let mut adapter = legacy(2); + adapter.operations.push(NativeOperationRecord { + ordinal: 0, + site: site(100), + operation: NativeOperation::ObserveSumView { + frame: 1, + source: NativeSumLoanSource::CarrierProjected { operation: 0 }, + }, + }); + assert_eq!( + decode(&encode_records(&adapter)).unwrap_err(), + ResourceLayoutError::UnknownTag + ); +} + +#[test] +fn carrier_registration_suffix_rejects_missing_magic_reserved_counts_tags_and_dense_rows() { + let bytes = encode_records(&fixture()); + let suffix = suffix_offset(&bytes); + for end in suffix..bytes.len() { + let mut prefix = bytes[..end].to_vec(); + set_length(&mut prefix); + assert!(decode(&prefix).is_err(), "carrier suffix prefix {end}"); + } + for (offset, value, expected) in [ + (suffix + 20, 1, ResourceLayoutError::Reserved), + (suffix + 8, 4097, ResourceLayoutError::WireLimit), + (suffix + 24, 1, ResourceLayoutError::DenseOrdinal), + (suffix + 28, 99, ResourceLayoutError::UnknownTag), + ] { + let mut altered = bytes.clone(); + write_word(&mut altered, offset, value); + assert_eq!(decode(&altered).unwrap_err(), expected); + } + let mut magic = bytes.clone(); + magic[suffix] = b'X'; + assert_eq!(decode(&magic).unwrap_err(), ResourceLayoutError::Header); + let mut trailing = bytes; + trailing.push(0); + set_length(&mut trailing); + assert_eq!( + decode(&trailing).unwrap_err(), + ResourceLayoutError::Trailing + ); + let mut oversized = fixture(); + let NativeCarrierNode::Struct { identity, .. } = &mut oversized.carriers.nodes[8] else { + unreachable!() + }; + *identity = vec![b'x'; 4097]; + assert_eq!( + decode(&encode_records(&oversized)).unwrap_err(), + ResourceLayoutError::WireLimit + ); +} + +#[test] +fn carrier_registration_validates_every_unused_graph_reference_and_ordinary_leaf() { + for case in 0..7 { + let mut layout = fixture(); + let expected = match case { + 0 => { + layout.carriers.nodes[4] = NativeCarrierNode::Optional { child: 4 }; + ResourceLayoutError::InvalidReference + } + 1 => { + layout.carriers.nodes[3] = NativeCarrierNode::Resource { kind: 1 }; + ResourceLayoutError::InvalidReference + } + 2 => { + layout.carriers.nodes[0] = NativeCarrierNode::Ordinary { + shape: RESOURCE_SHAPE, + }; + ResourceLayoutError::ShapeMismatch + } + 3 => { + layout.carriers.nodes[0] = NativeCarrierNode::Ordinary { + shape: OPTIONAL_SHAPE, + }; + ResourceLayoutError::ShapeMismatch + } + 4 => { + layout.carriers.nodes[0] = NativeCarrierNode::Ordinary { shape: WHOLE_SHAPE }; + ResourceLayoutError::ShapeMismatch + } + 5 => { + layout + .carriers + .nodes + .push(NativeCarrierNode::Resource { kind: 0 }); + ResourceLayoutError::NonCanonical + } + 6 => { + layout.carriers.slots[0].node = 99; + ResourceLayoutError::InvalidReference + } + _ => unreachable!(), + }; + assert_eq!( + parsed(&layout).unwrap_err(), + expected, + "unused graph case {case}" + ); + } +} + +#[test] +fn carrier_registration_rejects_duplicate_names_invalid_machine_state_and_unbound_refinement() { + for case in 0..7 { + let mut layout = fixture(); + match case { + 0 => { + let NativeCarrierNode::Struct { fields, .. } = &mut layout.carriers.nodes[8] else { + unreachable!() + }; + fields[1].name = fields[0].name.clone(); + } + 1 => { + let NativeCarrierNode::Enum { variants, .. } = &mut layout.carriers.nodes[9] else { + unreachable!() + }; + variants[1].name = variants[0].name.clone(); + } + 2 => { + let NativeCarrierNode::Machine { transitions, .. } = &mut layout.carriers.nodes[10] + else { + unreachable!() + }; + transitions.push((0, 1)); + } + 3 => { + layout.carriers.nodes[11] = NativeCarrierNode::MachineState { + machine: MACHINE_NODE, + state: 2, + } + } + 4 => { + layout.carriers.nodes[11] = NativeCarrierNode::MachineState { + machine: ENUM_NODE, + state: 0, + } + } + 5 => { + let NativeCarrierNode::Refinement { predicate, .. } = + &mut layout.carriers.nodes[12] + else { + unreachable!() + }; + predicate.clear(); + } + 6 => { + let NativeCarrierNode::Refinement { identity, .. } = &mut layout.carriers.nodes[12] + else { + unreachable!() + }; + *identity = b"transport.Record".to_vec(); + } + _ => unreachable!(), + } + assert!(parsed(&layout).is_err(), "nominal metadata case {case}"); + } +} + +#[test] +fn carrier_registration_latent_live_alternatives_do_not_reject_absent_constructor_metadata() { + let mut layout = fixture(); + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: 0, + selector: 0, + children: vec![], + }, + ); + assert_eq!(parsed(&layout).unwrap(), layout); + assert_eq!( + layout.carriers.selected_children(OPTIONAL_NODE, 0).unwrap(), + Vec::::new() + ); + assert_eq!( + layout.carriers.selected_children(OPTIONAL_NODE, 1).unwrap(), + vec![RESOURCE_NODE] + ); + assert_eq!( + layout.carriers.selected_children(ENUM_NODE, 0).unwrap(), + vec![0] + ); + assert_eq!( + layout.carriers.selected_children(ENUM_NODE, 1).unwrap(), + vec![RESOURCE_NODE] + ); + assert_eq!( + layout.carriers.selected_children(STATE_NODE, 0).unwrap(), + vec![0] + ); + assert!(layout.carriers.selected_children(STATE_NODE, 1).is_err()); +} + +#[test] +fn carrier_registration_constructor_lexical_order_preserves_explicit_storage_indices() { + let mut layout = fixture(); + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: 2, + selector: 0, + children: vec![ + carrier_child(1, OPTIONAL_NODE, 0), + ordinary_child(0, 0, INTEGER), + ], + }, + ); + assert_eq!(parsed(&layout).unwrap(), layout); + for case in 0..4 { + let mut invalid = layout.clone(); + let NativeCarrierOperation::Construct { children, .. } = + &mut invalid.carriers.operations[0].operation + else { + unreachable!() + }; + match case { + 0 => children[1].index = 1, + 1 => children[1].index = 2, + 2 => children[1] = ordinary_child(0, 1, STRING), + 3 => children[1].source = NativeCarrierChildSource::Ordinary { shape: STRING }, + _ => unreachable!(), + } + assert!(parsed(&invalid).is_err(), "constructor row case {case}"); + } +} + +#[test] +fn carrier_registration_collection_constructor_rejects_reordered_elements_and_map_halves() { + let mut list = fixture(); + operation( + &mut list, + NativeCarrierOperation::Construct { + frame: 1, + destination: 6, + selector: 0, + children: vec![carrier_child(0, OPTIONAL_NODE, 0)], + }, + ); + assert!(parsed(&list).is_ok()); + let NativeCarrierOperation::Construct { children, .. } = + &mut list.carriers.operations[0].operation + else { + unreachable!() + }; + children[0].index = 1; + assert!(parsed(&list).is_err()); + let mut map = fixture(); + operation( + &mut map, + NativeCarrierOperation::Construct { + frame: 1, + destination: 7, + selector: 0, + children: vec![ + ordinary_child(0, 0, INTEGER), + carrier_child(1, RESULT_NODE, 1), + ], + }, + ); + assert!(parsed(&map).is_ok()); + for case in 0..3 { + let mut invalid = map.clone(); + let NativeCarrierOperation::Construct { children, .. } = + &mut invalid.carriers.operations[0].operation + else { + unreachable!() + }; + match case { + 0 => { + children.pop(); + } + 1 => children.swap(0, 1), + 2 => children[0] = ordinary_child(0, 1, STRING), + _ => unreachable!(), + } + assert!(parsed(&invalid).is_err(), "map constructor case {case}"); + } +} + +#[test] +fn carrier_registration_nominal_and_generic_slots_cannot_transfer_by_matching_fields() { + for (source, destination) in [(2, 3), (4, 5), (0, 0)] { + let mut layout = fixture(); + operation( + &mut layout, + NativeCarrierOperation::Transfer { + frame: 1, + source, + destination, + }, + ); + assert!( + parsed(&layout).is_err(), + "carrier slot substitution {source} -> {destination}" + ); + } +} + +#[test] +fn carrier_registration_carrier_rows_require_one_exact_site_wrapper() { + let mut layout = fixture(); + operation( + &mut layout, + NativeCarrierOperation::Retire { + frame: 1, + source: 0, + }, + ); + assert!(parsed(&layout).is_ok()); + for case in 0..4 { + let mut invalid = layout.clone(); + match case { + 0 => invalid.operations.clear(), + 1 => invalid.operations[0].site.position = NativePosition::Statement(200), + 2 => { + let mut duplicate = invalid.operations[0].clone(); + duplicate.ordinal = 1; + invalid.operations.push(duplicate); + } + 3 => invalid.carriers.operations[0].site.function = 1, + _ => unreachable!(), + } + assert!( + parsed(&invalid).is_err(), + "carrier site wrapper case {case}" + ); + } +} + +#[test] +fn carrier_registration_borrow_and_projection_nodes_are_exact_and_bounded() { + let mut layout = fixture(); + let borrow = operation( + &mut layout, + NativeCarrierOperation::Borrow { + frame: 1, + source: NativeCarrierSource::Slot(2), + path: vec![NativeCarrierPath::Field(1)], + node: OPTIONAL_NODE, + lease_frame: 1, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Observe { + frame: 1, + source: NativeCarrierSource::Loan(NativeCarrierLoanSource::ExistingBorrow { + operation: borrow, + }), + path: vec![], + observation: NativeCarrierObservation::Tag, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::EndBorrow { frame: 1, borrow }, + ); + assert!(parsed(&layout).is_ok()); + for case in 0..4 { + let mut invalid = layout.clone(); + match case { + 0 => { + let NativeCarrierOperation::Borrow { node, .. } = + &mut invalid.carriers.operations[0].operation + else { + unreachable!() + }; + *node = RESOURCE_NODE; + } + 1 => { + let NativeCarrierOperation::Borrow { path, .. } = + &mut invalid.carriers.operations[0].operation + else { + unreachable!() + }; + *path = vec![NativeCarrierPath::Field(99)]; + } + 2 => { + let NativeCarrierOperation::Borrow { lease_frame, .. } = + &mut invalid.carriers.operations[0].operation + else { + unreachable!() + }; + *lease_frame = 0; + } + 3 => { + let NativeCarrierOperation::EndBorrow { borrow, .. } = + &mut invalid.carriers.operations[2].operation + else { + unreachable!() + }; + *borrow = 1; + } + _ => unreachable!(), + } + assert!(parsed(&invalid).is_err(), "carrier borrow row case {case}"); + } + let mut dynamic = fixture(); + operation( + &mut dynamic, + NativeCarrierOperation::Observe { + frame: 1, + source: NativeCarrierSource::Slot(6), + path: vec![NativeCarrierPath::List(NativeCarrierSelector::Dynamic)], + observation: NativeCarrierObservation::Tag, + }, + ); + assert!(parsed(&dynamic).is_ok()); + let NativeCarrierOperation::Observe { path, .. } = + &mut dynamic.carriers.operations[0].operation + else { + unreachable!() + }; + path.push(NativeCarrierPath::List(NativeCarrierSelector::Dynamic)); + assert_eq!( + parsed(&dynamic).unwrap_err(), + ResourceLayoutError::PayloadPath + ); +} + +#[test] +fn carrier_registration_ordinary_observations_do_not_reclassify_resource_nodes() { + let mut layout = fixture(); + operation( + &mut layout, + NativeCarrierOperation::Observe { + frame: 1, + source: NativeCarrierSource::Slot(2), + path: vec![NativeCarrierPath::Field(0)], + observation: NativeCarrierObservation::Ordinary { + shape: INTEGER, + clone: false, + }, + }, + ); + assert!(parsed(&layout).is_ok()); + let valid = layout.clone(); + let NativeCarrierOperation::Observe { path, .. } = &mut layout.carriers.operations[0].operation + else { + unreachable!() + }; + *path = vec![NativeCarrierPath::Field(1), NativeCarrierPath::Some]; + for (shape, clone, expected) in [ + ( + RESOURCE_SHAPE, + false, + ResourceLayoutError::OperationMismatch, + ), + (RESOURCE_SHAPE, true, ResourceLayoutError::OperationMismatch), + (INTEGER, false, ResourceLayoutError::ShapeMismatch), + (STRING, true, ResourceLayoutError::ShapeMismatch), + ] { + let NativeCarrierOperation::Observe { observation, .. } = + &mut layout.carriers.operations[0].operation + else { + unreachable!() + }; + *observation = NativeCarrierObservation::Ordinary { shape, clone }; + let bytes = encode_records(&layout); + assert_eq!( + decode(&bytes).unwrap(), + layout, + "forgery has a valid wire encoding" + ); + assert_eq!(parsed(&layout).unwrap_err(), expected); + let registry = ResourceRegistry::new(); + let mut installation = NativeLayoutInstallation::new(®istry); + assert_eq!( + installation.install(®istry, &bytes).unwrap_err(), + expected + ); + assert!(installation.installed().is_none()); + assert_eq!(registry.live_count(), 0); + let installed = installation + .install(®istry, &encode_records(&valid)) + .unwrap(); + assert_eq!( + installed.carriers().node(RESOURCE_NODE).unwrap(), + &NativeCarrierNode::Resource { kind: 0 } + ); + assert_eq!(registry.live_count(), 0); + } +} + +#[test] +fn carrier_registration_leaf_sum_adapter_requires_matching_kind_and_shape() { + let mut layout = fixture(); + operation( + &mut layout, + NativeCarrierOperation::AdaptSum { + frame: 1, + source: NativeCarrierSource::Slot(2), + path: vec![NativeCarrierPath::Field(1)], + sum_slot: 0, + lease_frame: 1, + borrowed: true, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: 2, + selector: 0, + children: vec![ + ordinary_child(0, 0, INTEGER), + NativeCarrierChild { + index: 1, + node: OPTIONAL_NODE, + source: NativeCarrierChildSource::LeafSum { slot: 0 }, + }, + ], + }, + ); + assert!(parsed(&layout).is_ok()); + for case in 0..3 { + let mut invalid = layout.clone(); + match case { + 0 => { + let NativeCarrierOperation::AdaptSum { sum_slot, .. } = + &mut invalid.carriers.operations[0].operation + else { + unreachable!() + }; + *sum_slot = 1; + } + 1 => { + let NativeCarrierOperation::Construct { children, .. } = + &mut invalid.carriers.operations[1].operation + else { + unreachable!() + }; + children[1].source = NativeCarrierChildSource::LeafSum { slot: 1 }; + } + 2 => { + invalid.kinds.push(1); + invalid.carriers.nodes[3] = NativeCarrierNode::Resource { kind: 1 }; + } + _ => unreachable!(), + } + assert!(parsed(&invalid).is_err(), "leaf sum adapter case {case}"); + } +} + +#[test] +fn carrier_registration_iteration_reset_names_only_distinct_binder_slots() { + let mut layout = fixture(); + operation( + &mut layout, + NativeCarrierOperation::ResetIteration { + frame: 1, + source: 6, + carrier_binders: vec![0], + leaf_sum_binders: vec![0], + }, + ); + assert_eq!(parsed(&layout).unwrap(), layout); + for case in 0..6 { + let mut invalid = layout.clone(); + match case { + 0 => { + let NativeCarrierOperation::ResetIteration { source, .. } = + &mut invalid.carriers.operations[0].operation + else { + unreachable!() + }; + *source = 2; + } + 1 => { + let NativeCarrierOperation::ResetIteration { + carrier_binders, .. + } = &mut invalid.carriers.operations[0].operation + else { + unreachable!() + }; + carrier_binders.push(0); + } + 2 => { + let NativeCarrierOperation::ResetIteration { + carrier_binders, .. + } = &mut invalid.carriers.operations[0].operation + else { + unreachable!() + }; + *carrier_binders = vec![6]; + } + 3 => { + let NativeCarrierOperation::ResetIteration { + leaf_sum_binders, .. + } = &mut invalid.carriers.operations[0].operation + else { + unreachable!() + }; + leaf_sum_binders.push(0); + } + 4 => invalid.carriers.slots[0].frame = 0, + 5 => invalid.slots[0].shape = NOTHING, + _ => unreachable!(), + } + assert!( + parsed(&invalid).is_err(), + "iteration reset binder case {case}" + ); + } +} + +#[test] +fn carrier_registration_publish_requires_matching_scope_return_and_result_node() { + let mut layout = fixture(); + layout.signatures[0].result = WHOLE_SHAPE; + layout.frames[0].parents.push(NativeParent::Frame(2)); + layout.frames.push(NativeFrame { + ordinal: 2, + role: NativeFrameRole::Return, + site: site(2), + signature: 0, + parents: vec![NativeParent::Root], + }); + layout.carriers.slots.push(NativeCarrierSlot { + frame: 2, + node: WHOLE_NODE, + }); + operation( + &mut layout, + NativeCarrierOperation::PublishReturn { + frame: 0, + source: 9, + }, + ); + assert_eq!(parsed(&layout).unwrap(), layout); + for case in 0..5 { + let mut invalid = layout.clone(); + match case { + 0 => invalid.frames[0].parents = vec![NativeParent::Root], + 1 => invalid.frames[2].role = NativeFrameRole::Operation, + 2 => invalid.carriers.slots[9].node = RECORD_NODE, + 3 => invalid.frames[2].site.function = 1, + 4 => invalid.signatures[0].result = NOTHING, + _ => unreachable!(), + } + assert!( + parsed(&invalid).is_err(), + "carrier publication linkage case {case}" + ); + } +} + +fn source_owned() -> WireLayout { + let mut layout = fixture(); + layout.signatures.push(NativeSignature { + ordinal: 1, + parameters: vec![NativeFormal { + shape: WHOLE_SHAPE, + access: NativeAccess::Owned, + }], + result: WHOLE_SHAPE, + }); + let callee_site = |index| NativeSite { + function: 1, + block: 0, + position: NativePosition::Statement(index), + }; + layout.frames.extend([ + NativeFrame { + ordinal: 2, + role: NativeFrameRole::Return, + site: callee_site(0), + signature: 1, + parents: vec![NativeParent::Frame(1)], + }, + NativeFrame { + ordinal: 3, + role: NativeFrameRole::Scope, + site: callee_site(1), + signature: 1, + parents: vec![NativeParent::Frame(2)], + }, + ]); + layout.carriers.slots.extend([ + NativeCarrierSlot { + frame: 0, + node: WHOLE_NODE, + }, + NativeCarrierSlot { + frame: 2, + node: WHOLE_NODE, + }, + NativeCarrierSlot { + frame: 3, + node: WHOLE_NODE, + }, + ]); + layout.operations.push(NativeOperationRecord { + ordinal: 0, + site: site(100), + operation: NativeOperation::InvokeSourceFunction { + frame: 1, + callee: 1, + signature: 1, + callee_scope: 3, + source: Some(NativeSourceInvocation { + callee_return: Some(2), + evaluation_order: vec![0], + formals: vec![NativeSourceFormal { + parameter: 0, + source_index: 0, + actual_shape: WHOLE_SHAPE, + callee_shape: WHOLE_SHAPE, + syntax: NativeSourceSyntax::Bare, + effect: NativeSourceEffect::TransferOwned, + access: NativeAccess::Owned, + value: NativeSourceValue::CarrierOwned { + caller_argument_slot: 8, + callee_parameter_slot: 11, + }, + }], + result: NativeSourceResult::Carrier { + shape: WHOLE_SHAPE, + caller_destination_frame: 0, + caller_destination_slot: 9, + callee_return_frame: 2, + permitted_return_slots: vec![10], + }, + }), + }, + }); + layout +} + +#[test] +fn carrier_registration_source_boundary_keeps_exact_owned_slots_and_return_permission() { + let layout = source_owned(); + assert_eq!(parsed(&layout).unwrap(), layout); + for case in 0..10 { + let mut invalid = layout.clone(); + let NativeOperation::InvokeSourceFunction { + source: Some(source), + .. + } = &mut invalid.operations[0].operation + else { + unreachable!() + }; + match case { + 0 => source.formals[0].value = NativeSourceValue::Ordinary, + 1 => source.formals[0].actual_shape = INTEGER, + 2 => { + source.formals[0].value = NativeSourceValue::CarrierOwned { + caller_argument_slot: 7, + callee_parameter_slot: 11, + } + } + 3 => { + source.formals[0].value = NativeSourceValue::CarrierOwned { + caller_argument_slot: 8, + callee_parameter_slot: 8, + } + } + 4 => source.formals[0].syntax = NativeSourceSyntax::WrittenView, + 5 => source.formals[0].effect = NativeSourceEffect::Copy, + 6 => source.result = NativeSourceResult::Ordinary { shape: WHOLE_SHAPE }, + 7 => { + let NativeSourceResult::Carrier { + permitted_return_slots, + .. + } = &mut source.result + else { + unreachable!() + }; + *permitted_return_slots = vec![9]; + } + 8 => { + let NativeSourceResult::Carrier { + permitted_return_slots, + .. + } = &mut source.result + else { + unreachable!() + }; + permitted_return_slots.clear(); + } + 9 => source.callee_return = Some(0), + _ => unreachable!(), + } + assert!( + parsed(&invalid).is_err(), + "carrier Source boundary case {case}" + ); + } +} + +#[test] +fn carrier_registration_source_view_uses_the_exact_existing_carrier_borrow() { + let mut layout = source_owned(); + let mut call = layout.operations.pop().unwrap(); + layout.signatures[1].parameters[0].access = NativeAccess::View; + let borrow = operation( + &mut layout, + NativeCarrierOperation::Borrow { + frame: 1, + source: NativeCarrierSource::Slot(8), + path: vec![], + node: WHOLE_NODE, + lease_frame: 1, + }, + ); + let NativeOperation::InvokeSourceFunction { + source: Some(source), + .. + } = &mut call.operation + else { + unreachable!() + }; + source.formals[0].syntax = NativeSourceSyntax::WrittenView; + source.formals[0].effect = NativeSourceEffect::RetainBorrow; + source.formals[0].access = NativeAccess::View; + source.formals[0].value = NativeSourceValue::CarrierView { + source: NativeCarrierLoanSource::ExistingBorrow { operation: borrow }, + }; + call.ordinal = 1; + call.site = site(101); + layout.operations.push(call); + assert_eq!(parsed(&layout).unwrap(), layout); + let NativeOperation::InvokeSourceFunction { + source: Some(source), + .. + } = &mut layout.operations[1].operation + else { + unreachable!() + }; + source.formals[0].value = NativeSourceValue::CarrierView { + source: NativeCarrierLoanSource::ExistingBorrow { operation: 1 }, + }; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::OperationMismatch + ); +} + +/// Exact slot indices for tests of the registered runtime fixture below. +pub(crate) const RUNTIME_CARRIER_FIXTURE_SLOTS: [(&str, u32); 9] = [ + ("optional", 0), + ("result", 1), + ("record", 2), + ("list", 6), + ("map", 7), + ("transferred_record", 9), + ("latent_resource", 10), + ("second_optional", 11), + ("iteration_binder", 12), +]; + +fn native_operation(layout: &mut WireLayout, function: u32, value: NativeOperation) -> u32 { + let ordinal = u32::try_from(layout.operations.len()).unwrap(); + layout.operations.push(NativeOperationRecord { + ordinal, + site: NativeSite { + function, + block: 0, + position: NativePosition::Statement(100 + ordinal), + }, + operation: value, + }); + ordinal +} + +/// Test bytes use the real registrar and decoder. This helper never constructs a +/// production registry identity, owner token, loan token, or Source proof. +pub(crate) fn runtime_fixture_bytes() -> Vec { + let mut layout = fixture(); + let slots = RUNTIME_CARRIER_FIXTURE_SLOTS; + let optional = slots[0].1; + let result = slots[1].1; + let record = slots[2].1; + let list = slots[3].1; + let map = slots[4].1; + let transferred_record = slots[5].1; + let latent_resource = slots[6].1; + let second_optional = slots[7].1; + let iteration_binder = slots[8].1; + layout.carriers.slots.extend([ + NativeCarrierSlot { + frame: 1, + node: RECORD_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: RESOURCE_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: OPTIONAL_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: OPTIONAL_NODE, + }, + ]); + for destination in [optional, second_optional, iteration_binder] { + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination, + selector: 0, + children: vec![], + }, + ); + } + // A selected live arm is consistent metadata, but runtime construction of a + // reached Resource node must refuse before acquiring an aggregate owner. + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: optional, + selector: 1, + children: vec![carrier_child(0, RESOURCE_NODE, latent_resource)], + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: record, + selector: 0, + children: vec![ + ordinary_child(0, 0, INTEGER), + carrier_child(1, OPTIONAL_NODE, optional), + ], + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: record, + selector: 0, + children: vec![ + ordinary_child(0, 0, INTEGER), + NativeCarrierChild { + index: 1, + node: OPTIONAL_NODE, + source: NativeCarrierChildSource::LeafSum { slot: 0 }, + }, + ], + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Transfer { + frame: 1, + source: record, + destination: transferred_record, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Transfer { + frame: 1, + source: transferred_record, + destination: record, + }, + ); + let full_borrow = operation( + &mut layout, + NativeCarrierOperation::Borrow { + frame: 1, + source: NativeCarrierSource::Slot(transferred_record), + path: vec![], + node: RECORD_NODE, + lease_frame: 1, + }, + ); + let child_borrow = operation( + &mut layout, + NativeCarrierOperation::Borrow { + frame: 1, + source: NativeCarrierSource::Loan(NativeCarrierLoanSource::ExistingBorrow { + operation: full_borrow, + }), + path: vec![NativeCarrierPath::Field(1)], + node: OPTIONAL_NODE, + lease_frame: 1, + }, + ); + let adapter = operation( + &mut layout, + NativeCarrierOperation::AdaptSum { + frame: 1, + source: NativeCarrierSource::Slot(transferred_record), + path: vec![NativeCarrierPath::Field(1)], + sum_slot: 0, + lease_frame: 1, + borrowed: true, + }, + ); + let nested_adapter = operation( + &mut layout, + NativeCarrierOperation::AdaptSum { + frame: 1, + source: NativeCarrierSource::Loan(NativeCarrierLoanSource::ExistingBorrow { + operation: child_borrow, + }), + path: vec![], + sum_slot: 0, + lease_frame: 1, + borrowed: true, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::EndBorrow { + frame: 1, + borrow: child_borrow, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::EndBorrow { + frame: 1, + borrow: full_borrow, + }, + ); + for operation in [adapter, nested_adapter] { + native_operation( + &mut layout, + 0, + NativeOperation::ObserveSumView { + frame: 1, + source: NativeSumLoanSource::CarrierProjected { operation }, + }, + ); + native_operation( + &mut layout, + 0, + NativeOperation::EndSumBorrow { + frame: 1, + borrow: operation, + }, + ); + } + operation( + &mut layout, + NativeCarrierOperation::AdaptSum { + frame: 1, + source: NativeCarrierSource::Slot(transferred_record), + path: vec![NativeCarrierPath::Field(1)], + sum_slot: 0, + lease_frame: 1, + borrowed: false, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Extract { + frame: 1, + source: transferred_record, + path: vec![NativeCarrierPath::Field(0)], + destination: NativeCarrierDestination::Ordinary { shape: INTEGER }, + }, + ); + for source in [record, transferred_record] { + operation( + &mut layout, + NativeCarrierOperation::Observe { + frame: 1, + source: NativeCarrierSource::Slot(source), + path: vec![NativeCarrierPath::Field(0)], + observation: NativeCarrierObservation::Ordinary { + shape: INTEGER, + clone: false, + }, + }, + ); + } + operation( + &mut layout, + NativeCarrierOperation::Observe { + frame: 1, + source: NativeCarrierSource::Loan(NativeCarrierLoanSource::ExistingBorrow { + operation: child_borrow, + }), + path: vec![], + observation: NativeCarrierObservation::Tag, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Observe { + frame: 1, + source: NativeCarrierSource::Slot(list), + path: vec![], + observation: NativeCarrierObservation::Length, + }, + ); + for children in [ + vec![], + vec![carrier_child(0, OPTIONAL_NODE, optional)], + vec![ + carrier_child(0, OPTIONAL_NODE, optional), + carrier_child(1, OPTIONAL_NODE, second_optional), + ], + ] { + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: list, + selector: 0, + children, + }, + ); + } + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: result, + selector: 0, + children: vec![ordinary_child(0, 1, STRING)], + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: map, + selector: 0, + children: vec![ + ordinary_child(0, 0, INTEGER), + carrier_child(1, RESULT_NODE, result), + ], + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Extract { + frame: 1, + source: list, + path: vec![NativeCarrierPath::List(NativeCarrierSelector::Dynamic)], + destination: NativeCarrierDestination::Slot(iteration_binder), + }, + ); + operation( + &mut layout, + NativeCarrierOperation::AdaptSum { + frame: 1, + source: NativeCarrierSource::Slot(iteration_binder), + path: vec![], + sum_slot: 0, + lease_frame: 1, + borrowed: false, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::ResetIteration { + frame: 1, + source: list, + carrier_binders: vec![iteration_binder], + leaf_sum_binders: vec![0], + }, + ); + for source in [ + optional, + result, + record, + transferred_record, + list, + map, + second_optional, + iteration_binder, + ] { + operation( + &mut layout, + NativeCarrierOperation::Retire { frame: 1, source }, + ); + } + native_operation( + &mut layout, + 0, + NativeOperation::CreateAbsentSum { + frame: 1, + destination_slot: 0, + }, + ); + let sum_borrow = native_operation( + &mut layout, + 0, + NativeOperation::BorrowSum { + frame: 1, + source_sum_slot: 0, + lease_frame: 1, + }, + ); + native_operation( + &mut layout, + 0, + NativeOperation::ObserveSumView { + frame: 1, + source: NativeSumLoanSource::ExistingBorrow { + operation: sum_borrow, + }, + }, + ); + native_operation( + &mut layout, + 0, + NativeOperation::EndSumBorrow { + frame: 1, + borrow: sum_borrow, + }, + ); + native_operation( + &mut layout, + 0, + NativeOperation::SumDrop { + frame: 1, + source: 0, + }, + ); + native_operation(&mut layout, 0, NativeOperation::Complete { frame: 1 }); + native_operation(&mut layout, 0, NativeOperation::Complete { frame: 0 }); + + layout.signatures.push(NativeSignature { + ordinal: 1, + parameters: vec![NativeFormal { + shape: OPTIONAL_SHAPE, + access: NativeAccess::View, + }], + result: NOTHING, + }); + layout.frames.push(NativeFrame { + ordinal: 2, + role: NativeFrameRole::Scope, + site: NativeSite { + function: 1, + block: 0, + position: NativePosition::Statement(0), + }, + signature: 1, + parents: vec![NativeParent::Frame(1)], + }); + native_operation( + &mut layout, + 0, + NativeOperation::InvokeSourceFunction { + frame: 1, + callee: 1, + signature: 1, + callee_scope: 2, + source: Some(NativeSourceInvocation { + callee_return: None, + evaluation_order: vec![0], + formals: vec![NativeSourceFormal { + parameter: 0, + source_index: 0, + actual_shape: OPTIONAL_SHAPE, + callee_shape: OPTIONAL_SHAPE, + syntax: NativeSourceSyntax::WrittenView, + effect: NativeSourceEffect::RetainBorrow, + access: NativeAccess::View, + value: NativeSourceValue::ResidentSumView { + source: NativeSumLoanSource::CarrierProjected { operation: adapter }, + }, + }], + result: NativeSourceResult::Ordinary { shape: NOTHING }, + }), + }, + ); + native_operation( + &mut layout, + 1, + NativeOperation::ObserveSumView { + frame: 2, + source: NativeSumLoanSource::IncomingViewFormal { + scope: 2, + parameter: 0, + }, + }, + ); + native_operation(&mut layout, 1, NativeOperation::Complete { frame: 2 }); + layout.carriers.slots.extend([ + NativeCarrierSlot { + frame: 1, + node: STATE_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: MACHINE_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: ENUM_NODE, + }, + ]); + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: 13, + selector: 0, + children: vec![ordinary_child(0, 0, INTEGER)], + }, + ); + operation( + &mut layout, + NativeCarrierOperation::QualifyMachine { + frame: 1, + source: 13, + destination: 14, + machine: MACHINE_NODE, + state: 0, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Observe { + frame: 1, + source: NativeCarrierSource::Slot(14), + path: vec![], + observation: NativeCarrierObservation::State, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: 15, + selector: 0, + children: vec![ordinary_child(0, 0, INTEGER)], + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Observe { + frame: 1, + source: NativeCarrierSource::Slot(15), + path: vec![], + observation: NativeCarrierObservation::Tag, + }, + ); + encode_records(&layout) +} + +#[test] +fn carrier_registration_runtime_fixture_is_a_closed_v3_layout() { + let decoded = decode(&runtime_fixture_bytes()).unwrap(); + assert_eq!(decoded.version, 3); + assert_eq!(decoded.signatures[0].parameters, vec![]); + assert_eq!(decoded.signatures[0].result, NOTHING); + assert_eq!(validation::validate(&decoded).unwrap(), vec![0, 0]); + let registry = ResourceRegistry::new(); + NativeLayoutInstallation::new(®istry) + .install(®istry, &runtime_fixture_bytes()) + .unwrap(); + assert_eq!(registry.live_count(), 0); +} + +fn ordinary_observation_fixture(shape: NativeShape, clone: bool) -> WireLayout { + let mut layout = fixture(); + let shape = if let Some(index) = layout.shapes.iter().position(|row| row == &shape) { + u32::try_from(index).unwrap() + } else { + let index = u32::try_from(layout.shapes.len()).unwrap(); + layout.shapes.push(shape); + index + }; + let ordinary = NativeCarrierNode::Ordinary { shape }; + let node = if let Some(index) = layout + .carriers + .nodes + .iter() + .position(|row| row == &ordinary) + { + u32::try_from(index).unwrap() + } else { + let index = u32::try_from(layout.carriers.nodes.len()).unwrap(); + layout.carriers.nodes.push(ordinary); + index + }; + let record = u32::try_from(layout.carriers.nodes.len()).unwrap(); + layout.carriers.nodes.push(NativeCarrierNode::Struct { + identity: b"transport.ObservedOrdinaryLeaf".to_vec(), + arguments: vec![], + fields: vec![field("value", node), field("channel", OPTIONAL_NODE)], + }); + let slot = u32::try_from(layout.carriers.slots.len()).unwrap(); + layout.carriers.slots.push(NativeCarrierSlot { + frame: 1, + node: record, + }); + operation( + &mut layout, + NativeCarrierOperation::Observe { + frame: 1, + source: NativeCarrierSource::Slot(slot), + path: vec![NativeCarrierPath::Field(0)], + observation: NativeCarrierObservation::Ordinary { shape, clone }, + }, + ); + layout +} + +#[test] +fn carrier_registration_scalar_observation_refuses_a_forged_clone_flag() { + for shape in [ + NativeShape::Integer { + bits: 8, + signed: true, + }, + NativeShape::Integer { + bits: 64, + signed: true, + }, + NativeShape::Integer { + bits: 64, + signed: false, + }, + NativeShape::Float { bits: 32 }, + NativeShape::Float { bits: 64 }, + NativeShape::Bool, + NativeShape::Nothing, + ] { + let valid = ordinary_observation_fixture(shape.clone(), false); + assert_eq!(parsed(&valid).unwrap(), valid, "scalar shape {shape:?}"); + let forged = ordinary_observation_fixture(shape.clone(), true); + assert_eq!( + decode(&encode_records(&forged)).unwrap(), + forged, + "clone flag is a valid wire flag for {shape:?}" + ); + assert_eq!( + parsed(&forged).unwrap_err(), + ResourceLayoutError::OperationMismatch, + "scalar observation must copy bits for {shape:?}" + ); + } +} + +#[test] +fn carrier_registration_string_observation_refuses_a_forged_borrowed_owner_flag() { + let valid = ordinary_observation_fixture(NativeShape::String, true); + assert_eq!(parsed(&valid).unwrap(), valid); + let forged = ordinary_observation_fixture(NativeShape::String, false); + assert_eq!(decode(&encode_records(&forged)).unwrap(), forged); + assert_eq!( + parsed(&forged).unwrap_err(), + ResourceLayoutError::OperationMismatch, + "string observation must create its independent ordinary owner" + ); +} + +#[test] +fn carrier_registration_owning_ordinary_sum_observation_refuses_both_clone_flags() { + for shape in [ + NativeShape::Optional { child: INTEGER }, + NativeShape::Result { + ok: INTEGER, + fail: STRING, + }, + ] { + let mut metadata = ordinary_observation_fixture(shape.clone(), false); + metadata.operations.clear(); + metadata.carriers.operations.clear(); + assert_eq!( + parsed(&metadata).unwrap(), + metadata, + "ordinary sum metadata itself is consistent for {shape:?}" + ); + for clone in [false, true] { + let forged = ordinary_observation_fixture(shape.clone(), clone); + assert_eq!(decode(&encode_records(&forged)).unwrap(), forged); + assert_eq!( + parsed(&forged).unwrap_err(), + ResourceLayoutError::OperationMismatch, + "owning sum observation remains pending for {shape:?}, clone={clone}" + ); + } + } +} diff --git a/crates/jett_runtime/src/resource_custody/registration/schema.rs b/crates/jett_runtime/src/resource_custody/registration/schema.rs new file mode 100644 index 00000000..2eb54377 --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/registration/schema.rs @@ -0,0 +1,404 @@ +//! Plain immutable wire records. None is a Source or custody authority. +use super::carriers::{NativeCarrierLayout, NativeCarrierLoanSource}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum NativeRecipe { + NetworkFactory, + NetworkBorrow, + Finalize, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum NativeAccess { + Owned, + View, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum NativeFrameRole { + Scope, + Operation, + Return, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum NativePayloadStep { + Some, + Ok, + Fail, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum NativePosition { + Statement(u32), + Terminator, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct NativeSite { + pub(super) function: u32, + pub(super) block: u32, + pub(super) position: NativePosition, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum NativeParent { + Root, + Frame(u32), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) enum NativeShape { + Integer { bits: u32, signed: bool }, + Float { bits: u32 }, + Bool, + String, + Nothing, + Network, + Resource { kind: u32 }, + Optional { child: u32 }, + Result { ok: u32, fail: u32 }, + HookDescriptor { hook: u32 }, + Carrier { node: u32 }, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct NativeFormal { + pub(super) shape: u32, + pub(super) access: NativeAccess, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NativeSignature { + pub(super) ordinal: u32, + pub(super) parameters: Vec, + pub(super) result: u32, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NativeHook { + pub(super) ordinal: u32, + pub(super) kind: u32, + pub(super) recipe: NativeRecipe, + pub(super) signature: u32, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NativeFrame { + pub(super) ordinal: u32, + pub(super) role: NativeFrameRole, + pub(super) site: NativeSite, + pub(super) signature: u32, + pub(super) parents: Vec, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NativeSlot { + pub(super) ordinal: u32, + pub(super) frame: u32, + pub(super) shape: u32, + pub(super) path: Vec, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum NativeLoanSource { + ExistingBorrow { operation: u32 }, + IncomingViewFormal { scope: u32, parameter: u32 }, + ProjectedSumPayload { operation: u32 }, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum NativeSumLoanSource { + CarrierProjected { operation: u32 }, + ExistingBorrow { operation: u32 }, + IncomingViewFormal { scope: u32, parameter: u32 }, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum NativeSourceSyntax { + Bare, + WrittenView, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum NativeSourceEffect { + Copy, + TransferOwned, + RelinquishOwned, + RetainBorrow, + ObserveData, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum NativeSourceValue { + CarrierOwned { + caller_argument_slot: u32, + callee_parameter_slot: u32, + }, + CarrierView { + source: NativeCarrierLoanSource, + }, + Ordinary, + Owned { + caller_argument_slot: u32, + callee_parameter_slot: u32, + }, + ResidentView { + source: NativeLoanSource, + }, + ResidentSumView { + source: NativeSumLoanSource, + }, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NativeSourceFormal { + pub(crate) parameter: u32, + pub(crate) source_index: u32, + pub(crate) actual_shape: u32, + pub(crate) callee_shape: u32, + pub(crate) syntax: NativeSourceSyntax, + pub(crate) effect: NativeSourceEffect, + pub(crate) access: NativeAccess, + pub(crate) value: NativeSourceValue, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum NativeSourceResult { + Carrier { + shape: u32, + caller_destination_frame: u32, + caller_destination_slot: u32, + callee_return_frame: u32, + permitted_return_slots: Vec, + }, + Ordinary { + shape: u32, + }, + Owned { + shape: u32, + caller_destination_frame: u32, + caller_destination_slot: u32, + callee_return_frame: u32, + permitted_return_slots: Vec, + }, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NativeSourceInvocation { + pub(crate) callee_return: Option, + pub(crate) evaluation_order: Vec, + pub(crate) formals: Vec, + pub(crate) result: NativeSourceResult, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum NativeOperation { + Carrier { + record: u32, + }, + Acquire { + frame: u32, + hook: u32, + destination: u32, + }, + Transfer { + frame: u32, + source: u32, + destination: u32, + }, + Borrow { + frame: u32, + source: u32, + lease_frame: u32, + }, + BoundedBorrowUse { + frame: u32, + source: NativeLoanSource, + callee_frame: u32, + }, + EndBorrow { + frame: u32, + borrow: u32, + }, + InvokeBorrow { + frame: u32, + hook: u32, + source: NativeLoanSource, + }, + Close { + frame: u32, + hook: u32, + source: u32, + }, + Drop { + frame: u32, + source: u32, + }, + SumAdopt { + frame: u32, + source: u32, + destination: u32, + }, + SumTake { + frame: u32, + source: u32, + destination: u32, + }, + SumDrop { + frame: u32, + source: u32, + }, + Replace { + frame: u32, + old: u32, + replacement: u32, + }, + Complete { + frame: u32, + }, + Descriptor { + hook: u32, + }, + InvokeDescriptor { + hook: u32, + signature: u32, + target: u32, + }, + InvokeSourceFunction { + frame: u32, + callee: u32, + signature: u32, + callee_scope: u32, + /// None is strict historical v1 metadata; it never enables Source activation. + source: Option, + }, + TakeFailureCompanion { + frame: u32, + source_sum_slot: u32, + failure_shape: u32, + }, + PublishReturn { + frame: u32, + source_return_slot: u32, + }, + CreateAbsentSum { + frame: u32, + destination_slot: u32, + }, + CreateFailureSum { + frame: u32, + destination_slot: u32, + failure_shape: u32, + }, + BorrowSum { + frame: u32, + source_sum_slot: u32, + lease_frame: u32, + }, + ObserveSumView { + frame: u32, + source: NativeSumLoanSource, + }, + ProjectSumView { + frame: u32, + source: NativeSumLoanSource, + path: NativePayloadStep, + lease_frame: u32, + }, + ReadFailureCompanion { + frame: u32, + source: NativeSumLoanSource, + failure_shape: u32, + }, + EndSumBorrow { + frame: u32, + borrow: u32, + }, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NativeOperationRecord { + pub(super) ordinal: u32, + pub(super) site: NativeSite, + pub(super) operation: NativeOperation, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct WireLayout { + pub(super) carriers: NativeCarrierLayout, + pub(super) version: u32, + pub(super) kinds: Vec, + pub(super) hooks: Vec, + pub(super) signatures: Vec, + pub(super) shapes: Vec, + pub(super) frames: Vec, + pub(super) slots: Vec, + pub(super) operations: Vec, +} + +impl NativeSite { + pub(crate) fn function(&self) -> u32 { + self.function + } + pub(crate) fn block(&self) -> u32 { + self.block + } + pub(crate) fn position(&self) -> NativePosition { + self.position + } +} +impl NativeFormal { + pub(crate) fn shape(&self) -> u32 { + self.shape + } + pub(crate) fn access(&self) -> NativeAccess { + self.access + } +} +impl NativeSignature { + pub(crate) fn ordinal(&self) -> u32 { + self.ordinal + } + pub(crate) fn parameters(&self) -> &[NativeFormal] { + &self.parameters + } + pub(crate) fn result(&self) -> u32 { + self.result + } +} +impl NativeHook { + pub(crate) fn ordinal(&self) -> u32 { + self.ordinal + } + pub(crate) fn kind(&self) -> u32 { + self.kind + } + pub(crate) fn recipe(&self) -> NativeRecipe { + self.recipe + } + pub(crate) fn signature(&self) -> u32 { + self.signature + } +} +impl NativeFrame { + pub(crate) fn ordinal(&self) -> u32 { + self.ordinal + } + pub(crate) fn role(&self) -> NativeFrameRole { + self.role + } + pub(crate) fn site(&self) -> NativeSite { + self.site + } + pub(crate) fn signature(&self) -> u32 { + self.signature + } + pub(crate) fn parents(&self) -> &[NativeParent] { + &self.parents + } +} +impl NativeSlot { + pub(crate) fn ordinal(&self) -> u32 { + self.ordinal + } + pub(crate) fn frame(&self) -> u32 { + self.frame + } + pub(crate) fn shape(&self) -> u32 { + self.shape + } + pub(crate) fn path(&self) -> &[NativePayloadStep] { + &self.path + } +} +impl NativeOperationRecord { + pub(crate) fn ordinal(&self) -> u32 { + self.ordinal + } + pub(crate) fn site(&self) -> NativeSite { + self.site + } + pub(crate) fn operation(&self) -> &NativeOperation { + &self.operation + } +} diff --git a/crates/jett_runtime/src/resource_custody/registration/source_validation.rs b/crates/jett_runtime/src/resource_custody/registration/source_validation.rs new file mode 100644 index 00000000..84fd878e --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/registration/source_validation.rs @@ -0,0 +1,518 @@ +//! Complete Source wire consistency. The compiler retains Source/CFG authority. +use super::{ResourceLayoutError, schema::*, validation::slot_kind}; +use std::collections::HashSet; + +fn get(values: &[T], id: u32) -> Result<&T, ResourceLayoutError> { + values + .get(id as usize) + .ok_or(ResourceLayoutError::InvalidReference) +} +pub(super) fn occupied(layout: &WireLayout, shape: u32) -> Result { + // Shape validation already established child-before-parent and Resource-free + // Result failure arms. Walk iteratively so bounded foreign nesting cannot + // turn table preflight into recursive stack growth. + let mut current = shape; + for _ in 0..=layout.shapes.len() { + match *get(&layout.shapes, current)? { + NativeShape::Resource { .. } | NativeShape::Carrier { .. } => return Ok(true), + NativeShape::Optional { child } => current = child, + NativeShape::Result { ok, .. } => current = ok, + _ => return Ok(false), + } + } + Err(ResourceLayoutError::InvalidReference) +} +fn frame<'a>( + layout: &'a WireLayout, + id: u32, + function: u32, + role: NativeFrameRole, +) -> Result<&'a NativeFrame, ResourceLayoutError> { + let result = get(&layout.frames, id)?; + if result.site.function != function || result.role != role { + return Err(ResourceLayoutError::FrameMismatch); + } + Ok(result) +} +fn source_kind( + layout: &WireLayout, + source: NativeLoanSource, + function: u32, +) -> Result { + match source { + NativeLoanSource::ProjectedSumPayload { operation } => { + if get(&layout.operations, operation)?.site.function != function { + return Err(ResourceLayoutError::FrameMismatch); + } + projected_kind(layout, operation) + } + NativeLoanSource::ExistingBorrow { operation } => { + let record = get(&layout.operations, operation)?; + if record.site.function != function { + return Err(ResourceLayoutError::FrameMismatch); + } + let NativeOperation::Borrow { source, .. } = record.operation else { + return Err(ResourceLayoutError::OperationMismatch); + }; + slot_kind(layout, get(&layout.slots, source)?) + } + NativeLoanSource::IncomingViewFormal { scope, parameter } => { + let scope = frame(layout, scope, function, NativeFrameRole::Scope)?; + let signature = get(&layout.signatures, scope.signature)?; + let formal = get(&signature.parameters, parameter)?; + if formal.access != NativeAccess::View { + return Err(ResourceLayoutError::OperationMismatch); + } + match *get(&layout.shapes, formal.shape)? { + NativeShape::Resource { kind } => Ok(kind), + _ => Err(ResourceLayoutError::ShapeMismatch), + } + } + } +} + +pub(super) fn sum_source_shape( + layout: &WireLayout, + source: NativeSumLoanSource, + function: u32, +) -> Result { + let shape = match source { + NativeSumLoanSource::CarrierProjected { operation } => { + let row = get(&layout.operations, operation)?; + if layout.version != 3 || row.site.function != function { + return Err(ResourceLayoutError::OperationMismatch); + } + let NativeOperation::Carrier { record } = row.operation else { + return Err(ResourceLayoutError::OperationMismatch); + }; + let super::carriers::NativeCarrierOperation::AdaptSum { + sum_slot, + borrowed: true, + .. + } = get(&layout.carriers.operations, record)?.operation + else { + return Err(ResourceLayoutError::OperationMismatch); + }; + get(&layout.slots, sum_slot)?.shape + } + NativeSumLoanSource::ExistingBorrow { operation } => { + let row = get(&layout.operations, operation)?; + if row.site.function != function { + return Err(ResourceLayoutError::FrameMismatch); + } + let NativeOperation::BorrowSum { + source_sum_slot, .. + } = row.operation + else { + return Err(ResourceLayoutError::OperationMismatch); + }; + get(&layout.slots, source_sum_slot)?.shape + } + NativeSumLoanSource::IncomingViewFormal { scope, parameter } => { + let scope = frame(layout, scope, function, NativeFrameRole::Scope)?; + let formal = get( + &get(&layout.signatures, scope.signature)?.parameters, + parameter, + )?; + if formal.access != NativeAccess::View { + return Err(ResourceLayoutError::OperationMismatch); + } + formal.shape + } + }; + conditional_kind(layout, shape)?; + Ok(shape) +} +pub(super) fn conditional_kind( + layout: &WireLayout, + shape: u32, +) -> Result<(u32, NativePayloadStep), ResourceLayoutError> { + let (child, path) = match *get(&layout.shapes, shape)? { + NativeShape::Optional { child } => (child, NativePayloadStep::Some), + NativeShape::Result { ok, .. } => (ok, NativePayloadStep::Ok), + _ => return Err(ResourceLayoutError::ShapeMismatch), + }; + let NativeShape::Resource { kind } = *get(&layout.shapes, child)? else { + return Err(ResourceLayoutError::ShapeMismatch); + }; + Ok((kind, path)) +} +pub(super) fn projected_kind( + layout: &WireLayout, + operation: u32, +) -> Result { + let row = get(&layout.operations, operation)?; + let NativeOperation::ProjectSumView { source, path, .. } = row.operation else { + return Err(ResourceLayoutError::OperationMismatch); + }; + let shape = sum_source_shape(layout, source, row.site.function)?; + let (kind, expected) = conditional_kind(layout, shape)?; + if path != expected { + return Err(ResourceLayoutError::PayloadPath); + } + Ok(kind) +} + +pub(super) fn invocation( + layout: &WireLayout, + record: &NativeOperationRecord, + caller_frame: u32, + callee: u32, + signature: u32, + callee_scope: u32, + source: &NativeSourceInvocation, +) -> Result<(), ResourceLayoutError> { + if !matches!(layout.version, 2 | 3) { + return Err(ResourceLayoutError::OperationMismatch); + } + frame( + layout, + caller_frame, + record.site.function, + NativeFrameRole::Operation, + )?; + let scope = frame(layout, callee_scope, callee, NativeFrameRole::Scope)?; + let signature_record = get(&layout.signatures, signature)?; + if scope.signature != signature + || source.formals.len() != signature_record.parameters.len() + || source.evaluation_order.len() != source.formals.len() + { + return Err(ResourceLayoutError::OperationMismatch); + } + let mut source_indices = super::wire::storage::(source.formals.len())?; + source_indices.resize(source.formals.len(), false); + let mut caller_slots = HashSet::new(); + let mut callee_slots = HashSet::new(); + caller_slots + .try_reserve(source.formals.len()) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + callee_slots + .try_reserve(source.formals.len()) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + for (index, formal) in source.formals.iter().enumerate() { + let target = &signature_record.parameters[index]; + let source_index = formal.source_index as usize; + if formal.parameter as usize != index + || source_index >= source.formals.len() + || source_indices[source_index] + || source.evaluation_order[source_index] != formal.parameter + || formal.callee_shape != target.shape + || formal.access != target.access + { + return Err(ResourceLayoutError::OperationMismatch); + } + source_indices[source_index] = true; + get(&layout.shapes, formal.actual_shape)?; + match formal.value { + NativeSourceValue::CarrierOwned { .. } | NativeSourceValue::CarrierView { .. } => { + super::carriers::validate_formal( + layout, + record, + caller_frame, + callee_scope, + formal, + &mut caller_slots, + &mut callee_slots, + )? + } + NativeSourceValue::Ordinary => { + if occupied(layout, formal.actual_shape)? || occupied(layout, formal.callee_shape)? + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeSourceValue::Owned { + caller_argument_slot, + callee_parameter_slot, + } => { + let caller = get(&layout.slots, caller_argument_slot)?; + let target = get(&layout.slots, callee_parameter_slot)?; + if formal.syntax != NativeSourceSyntax::Bare + || formal.effect != NativeSourceEffect::TransferOwned + || formal.access != NativeAccess::Owned + || formal.actual_shape != formal.callee_shape + || caller.frame != caller_frame + || target.frame != callee_scope + || caller.shape != formal.actual_shape + || target.shape != formal.callee_shape + || caller.path != target.path + || !caller_slots.insert(caller_argument_slot) + || !callee_slots.insert(callee_parameter_slot) + { + return Err(ResourceLayoutError::OperationMismatch); + } + if slot_kind(layout, caller)? != slot_kind(layout, target)? { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeSourceValue::ResidentSumView { source } => { + let tuple = matches!( + (formal.syntax, formal.effect), + ( + NativeSourceSyntax::Bare, + NativeSourceEffect::RelinquishOwned + ) | ( + NativeSourceSyntax::WrittenView, + NativeSourceEffect::RetainBorrow + ) + ); + if !tuple + || formal.access != NativeAccess::View + || formal.actual_shape != formal.callee_shape + || sum_source_shape(layout, source, record.site.function)? + != formal.callee_shape + { + return Err(ResourceLayoutError::OperationMismatch); + } + if formal.effect == NativeSourceEffect::RelinquishOwned { + let NativeSumLoanSource::ExistingBorrow { operation } = source else { + return Err(ResourceLayoutError::OperationMismatch); + }; + let NativeOperation::BorrowSum { + frame, + source_sum_slot, + lease_frame, + } = get(&layout.operations, operation)?.operation + else { + return Err(ResourceLayoutError::OperationMismatch); + }; + if frame != caller_frame + || lease_frame != caller_frame + || get(&layout.slots, source_sum_slot)?.frame != caller_frame + { + return Err(ResourceLayoutError::FrameMismatch); + } + } + } + NativeSourceValue::ResidentView { source } => { + let NativeShape::Resource { kind } = *get(&layout.shapes, formal.callee_shape)? + else { + return Err(ResourceLayoutError::ShapeMismatch); + }; + let tuple = matches!( + (formal.syntax, formal.effect), + ( + NativeSourceSyntax::Bare, + NativeSourceEffect::RelinquishOwned + ) | ( + NativeSourceSyntax::WrittenView, + NativeSourceEffect::RetainBorrow + ) + ); + if !tuple + || formal.access != NativeAccess::View + || formal.actual_shape != formal.callee_shape + || source_kind(layout, source, record.site.function)? != kind + { + return Err(ResourceLayoutError::OperationMismatch); + } + } + } + } + match &source.result { + NativeSourceResult::Carrier { .. } => super::carriers::validate_result( + layout, + record, + caller_frame, + callee, + signature_record, + scope, + source, + )?, + NativeSourceResult::Ordinary { shape } => { + if *shape != signature_record.result + || occupied(layout, *shape)? + || source.callee_return.is_some() + || !scope.parents.contains(&NativeParent::Frame(caller_frame)) + { + return Err(ResourceLayoutError::OperationMismatch); + } + } + NativeSourceResult::Owned { + shape, + caller_destination_frame, + caller_destination_slot, + callee_return_frame, + permitted_return_slots, + } => { + let return_frame = frame( + layout, + *callee_return_frame, + callee, + NativeFrameRole::Return, + )?; + let destination = get(&layout.slots, *caller_destination_slot)?; + let destination_frame = get(&layout.frames, *caller_destination_frame)?; + if *shape != signature_record.result + || !occupied(layout, *shape)? + || source.callee_return != Some(*callee_return_frame) + || return_frame.signature != signature + || !return_frame + .parents + .contains(&NativeParent::Frame(caller_frame)) + || !scope + .parents + .contains(&NativeParent::Frame(*callee_return_frame)) + || destination.frame != *caller_destination_frame + || destination.shape != *shape + || destination_frame.site.function != record.site.function + || permitted_return_slots.is_empty() + || permitted_return_slots + .windows(2) + .any(|pair| pair[0] >= pair[1]) + { + return Err(ResourceLayoutError::OperationMismatch); + } + for id in permitted_return_slots { + let slot = get(&layout.slots, *id)?; + if slot.frame != *callee_return_frame + || slot.shape != *shape + || slot.path != destination.path + || slot_kind(layout, slot)? != slot_kind(layout, destination)? + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + } + } + Ok(()) +} + +pub(super) fn operation( + layout: &WireLayout, + record: &NativeOperationRecord, +) -> Result<(), ResourceLayoutError> { + if !matches!(layout.version, 2 | 3) { + return Err(ResourceLayoutError::OperationMismatch); + } + let get_frame = |id| -> Result<&NativeFrame, ResourceLayoutError> { + let result = get(&layout.frames, id)?; + if result.site.function != record.site.function { + return Err(ResourceLayoutError::FrameMismatch); + } + Ok(result) + }; + match record.operation { + NativeOperation::TakeFailureCompanion { + frame, + source_sum_slot, + failure_shape, + } => { + get_frame(frame)?; + let slot = get(&layout.slots, source_sum_slot)?; + let NativeShape::Result { fail, .. } = *get(&layout.shapes, slot.shape)? else { + return Err(ResourceLayoutError::ShapeMismatch); + }; + if fail != failure_shape + || occupied(layout, fail)? + || slot.path != [NativePayloadStep::Ok] + || get(&layout.frames, slot.frame)?.site.function != record.site.function + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeOperation::PublishReturn { + frame, + source_return_slot, + } => { + let scope = get_frame(frame)?; + let slot = get(&layout.slots, source_return_slot)?; + let return_frame = get_frame(slot.frame)?; + if scope.role != NativeFrameRole::Scope + || return_frame.role != NativeFrameRole::Return + || scope.signature != return_frame.signature + || slot.shape != get(&layout.signatures, return_frame.signature)?.result + || !scope.parents.contains(&NativeParent::Frame(slot.frame)) + { + return Err(ResourceLayoutError::FrameMismatch); + } + } + NativeOperation::CreateAbsentSum { + frame, + destination_slot, + } => { + get_frame(frame)?; + let slot = get(&layout.slots, destination_slot)?; + if slot.frame != frame + || slot.path != [NativePayloadStep::Some] + || !matches!(get(&layout.shapes,slot.shape)?,NativeShape::Optional { child } + if matches!(get(&layout.shapes,*child)?,NativeShape::Resource { .. })) + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeOperation::CreateFailureSum { + frame, + destination_slot, + failure_shape, + } => { + get_frame(frame)?; + let slot = get(&layout.slots, destination_slot)?; + let NativeShape::Result { fail, .. } = *get(&layout.shapes, slot.shape)? else { + return Err(ResourceLayoutError::ShapeMismatch); + }; + if slot.frame != frame + || slot.path != [NativePayloadStep::Ok] + || fail != failure_shape + || occupied(layout, fail)? + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeOperation::BorrowSum { + frame, + source_sum_slot, + lease_frame, + } => { + get_frame(frame)?; + if lease_frame != frame { + return Err(ResourceLayoutError::FrameMismatch); + } + let slot = get(&layout.slots, source_sum_slot)?; + if get_frame(slot.frame)?.site.function != record.site.function + || slot.path != [conditional_kind(layout, slot.shape)?.1] + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeOperation::ObserveSumView { frame, source } => { + get_frame(frame)?; + sum_source_shape(layout, source, record.site.function)?; + } + NativeOperation::ProjectSumView { + frame, lease_frame, .. + } => { + get_frame(frame)?; + if lease_frame != frame { + return Err(ResourceLayoutError::FrameMismatch); + } + projected_kind(layout, record.ordinal)?; + } + NativeOperation::ReadFailureCompanion { + frame, + source, + failure_shape, + } => { + get_frame(frame)?; + let shape = sum_source_shape(layout, source, record.site.function)?; + let NativeShape::Result { fail, .. } = *get(&layout.shapes, shape)? else { + return Err(ResourceLayoutError::ShapeMismatch); + }; + if fail != failure_shape || !matches!(get(&layout.shapes, fail)?, NativeShape::String) { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeOperation::EndSumBorrow { frame, borrow } => { + get_frame(frame)?; + let row = get(&layout.operations, borrow)?; + if row.site.function != record.site.function + || !(matches!(row.operation, NativeOperation::BorrowSum { frame: f, .. } if f == frame) + || matches!(row.operation,NativeOperation::Carrier{record:id} if layout.version==3 && matches!(layout.carriers.operations.get(id as usize).map(|r|&r.operation),Some(super::carriers::NativeCarrierOperation::AdaptSum{frame:f,borrowed:true,..}) if *f==frame))) + { + return Err(ResourceLayoutError::OperationMismatch); + } + } + _ => return Err(ResourceLayoutError::OperationMismatch), + } + Ok(()) +} diff --git a/crates/jett_runtime/src/resource_custody/registration/tests.rs b/crates/jett_runtime/src/resource_custody/registration/tests.rs new file mode 100644 index 00000000..cf46a7f0 --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/registration/tests.rs @@ -0,0 +1,936 @@ +use super::schema::WireLayout; +use super::wire::{decode, encode_records}; +use super::*; +use crate::AuthorityProvenance; +use crate::resource_custody::{ + OwnedResourceToken, ResourceBodyOutcome, ResourceCleanupFailure, ResourceCompletionFailure, + ResourceCustody, ResourceFrameKind, ResourcePurpose, complete_resource, +}; +use std::mem::{align_of, offset_of, size_of}; +use std::sync::Mutex; + +fn site(function: u32, index: u32) -> NativeSite { + NativeSite { + function, + block: 0, + position: NativePosition::Statement(index), + } +} +fn canonical() -> WireLayout { + let view = |shape| NativeFormal { + shape, + access: NativeAccess::View, + }; + let owned = |shape| NativeFormal { + shape, + access: NativeAccess::Owned, + }; + let signature = |ordinal, parameters, result| NativeSignature { + ordinal, + parameters, + result, + }; + let frame = |ordinal, role, function, index, signature, parents| NativeFrame { + ordinal, + role, + site: site(function, index), + signature, + parents, + }; + let slot = |ordinal, frame, shape, path| NativeSlot { + ordinal, + frame, + shape, + path, + }; + use NativeOperation::*; + let loan = NativeLoanSource::ExistingBorrow { operation: 3 }; + let incoming = NativeLoanSource::IncomingViewFormal { + scope: 3, + parameter: 0, + }; + let operations = vec![ + Acquire { + frame: 1, + hook: 0, + destination: 2, + }, + SumTake { + frame: 1, + source: 2, + destination: 1, + }, + Transfer { + frame: 0, + source: 1, + destination: 0, + }, + Borrow { + frame: 1, + source: 0, + lease_frame: 1, + }, + InvokeBorrow { + frame: 1, + hook: 1, + source: loan, + }, + BoundedBorrowUse { + frame: 1, + source: loan, + callee_frame: 3, + }, + EndBorrow { + frame: 1, + borrow: 3, + }, + Close { + frame: 1, + hook: 2, + source: 0, + }, + Drop { + frame: 0, + source: 1, + }, + SumAdopt { + frame: 1, + source: 1, + destination: 2, + }, + SumDrop { + frame: 1, + source: 2, + }, + Replace { + frame: 0, + old: 0, + replacement: 1, + }, + Complete { frame: 0 }, + Descriptor { hook: 0 }, + InvokeDescriptor { + hook: 0, + signature: 0, + target: 0, + }, + InvokeSourceFunction { + frame: 1, + callee: 1, + signature: 4, + callee_scope: 3, + source: None, + }, + InvokeBorrow { + frame: 1, + hook: 1, + source: incoming, + }, + BoundedBorrowUse { + frame: 1, + source: incoming, + callee_frame: 3, + }, + ] + .into_iter() + .enumerate() + .map(|(ordinal, operation)| NativeOperationRecord { + ordinal: ordinal as u32, + site: site(0, 100 + ordinal as u32), + operation, + }) + .collect(); + WireLayout { + carriers: super::carriers::NativeCarrierLayout::default(), + version: 1, + kinds: vec![0, 1], + hooks: vec![ + NativeHook { + ordinal: 0, + kind: 0, + recipe: NativeRecipe::NetworkFactory, + signature: 0, + }, + NativeHook { + ordinal: 1, + kind: 0, + recipe: NativeRecipe::NetworkBorrow, + signature: 1, + }, + NativeHook { + ordinal: 2, + kind: 0, + recipe: NativeRecipe::Finalize, + signature: 2, + }, + NativeHook { + ordinal: 3, + kind: 1, + recipe: NativeRecipe::NetworkFactory, + signature: 5, + }, + ], + signatures: vec![ + signature(0, vec![view(0), owned(1)], 6), + signature(1, vec![view(0), view(4)], 7), + signature(2, vec![owned(4)], 3), + signature(3, vec![], 3), + signature(4, vec![view(4)], 3), + signature(5, vec![view(0), owned(1)], 13), + ], + shapes: vec![ + NativeShape::Network, + NativeShape::Integer { + bits: 64, + signed: true, + }, + NativeShape::String, + NativeShape::Nothing, + NativeShape::Resource { kind: 0 }, + NativeShape::Resource { kind: 1 }, + NativeShape::Result { ok: 4, fail: 2 }, + NativeShape::Result { ok: 1, fail: 2 }, + NativeShape::Optional { child: 4 }, + NativeShape::HookDescriptor { hook: 0 }, + NativeShape::Bool, + NativeShape::Float { bits: 64 }, + NativeShape::Integer { + bits: 8, + signed: false, + }, + NativeShape::Result { ok: 5, fail: 2 }, + ], + frames: vec![ + frame(0, NativeFrameRole::Scope, 0, 0, 3, vec![NativeParent::Root]), + frame( + 1, + NativeFrameRole::Operation, + 0, + 1, + 3, + vec![NativeParent::Frame(0)], + ), + frame( + 2, + NativeFrameRole::Return, + 0, + 2, + 3, + vec![NativeParent::Frame(0)], + ), + frame( + 3, + NativeFrameRole::Scope, + 1, + 0, + 4, + vec![NativeParent::Frame(1)], + ), + ], + slots: vec![ + slot(0, 0, 4, vec![]), + slot(1, 1, 4, vec![]), + slot(2, 1, 6, vec![NativePayloadStep::Ok]), + slot(3, 0, 8, vec![NativePayloadStep::Some]), + slot(4, 0, 5, vec![]), + ], + operations, + } +} +fn parsed(layout: &WireLayout) -> Result { + let decoded = decode(&encode_records(layout))?; + validation::validate(&decoded)?; + Ok(decoded) +} +fn installed(registry: &ResourceRegistry) -> Arc { + NativeLayoutInstallation::new(registry) + .install(registry, &encode_records(&canonical())) + .unwrap() +} + +fn source_v2() -> WireLayout { + let mut layout = canonical(); + layout.version = 2; + let NativeOperation::InvokeSourceFunction { source, .. } = &mut layout.operations[15].operation + else { + panic!("canonical Source row") + }; + *source = Some(NativeSourceInvocation { + callee_return: None, + evaluation_order: vec![0], + formals: vec![NativeSourceFormal { + parameter: 0, + source_index: 0, + actual_shape: 4, + callee_shape: 4, + syntax: NativeSourceSyntax::WrittenView, + effect: NativeSourceEffect::RetainBorrow, + access: NativeAccess::View, + value: NativeSourceValue::ResidentView { + source: NativeLoanSource::ExistingBorrow { operation: 3 }, + }, + }], + result: NativeSourceResult::Ordinary { shape: 3 }, + }); + layout +} + +#[test] +fn native_registration_source_v2_roundtrips_the_complete_boundary_without_upgrading_v1() { + let layout = source_v2(); + assert_eq!(parsed(&layout).unwrap(), layout); + let registry = ResourceRegistry::new(); + let mut installation = NativeLayoutInstallation::new(®istry); + let installed = installation + .install(®istry, &encode_records(&layout)) + .unwrap(); + assert_eq!(installed.wire_version(), 2); + assert_eq!(parsed(&canonical()).unwrap().version, 1); + let mut missing = layout.clone(); + let NativeOperation::InvokeSourceFunction { source, .. } = + &mut missing.operations[15].operation + else { + unreachable!() + }; + *source = None; + assert!(parsed(&missing).is_err()); + let mut wrong_version = layout; + wrong_version.version = 1; + assert!(parsed(&wrong_version).is_err()); +} + +#[test] +fn native_registration_source_v2_rejects_unused_permutation_formal_and_resident_substitutions() { + for case in 0..7 { + let mut layout = source_v2(); + let NativeOperation::InvokeSourceFunction { + source: Some(source), + .. + } = &mut layout.operations[15].operation + else { + unreachable!() + }; + match case { + 0 => source.evaluation_order[0] = 1, + 1 => source.formals[0].source_index = 1, + 2 => source.formals[0].syntax = NativeSourceSyntax::Bare, + 3 => source.formals[0].effect = NativeSourceEffect::TransferOwned, + 4 => source.formals[0].value = NativeSourceValue::Ordinary, + 5 => { + source.formals[0].value = NativeSourceValue::ResidentView { + source: NativeLoanSource::ExistingBorrow { operation: 0 }, + } + } + 6 => source.formals[0].actual_shape = 5, + _ => unreachable!(), + } + assert!( + parsed(&layout).is_err(), + "unused Source metadata case {case}" + ); + } +} + +#[test] +fn native_registration_source_v2_rejects_unbound_return_and_occupied_failure_companion_roles() { + for operation in [ + NativeOperation::PublishReturn { + frame: 3, + source_return_slot: 0, + }, + NativeOperation::TakeFailureCompanion { + frame: 1, + source_sum_slot: 2, + failure_shape: 4, + }, + NativeOperation::CreateFailureSum { + frame: 1, + destination_slot: 2, + failure_shape: 4, + }, + ] { + let mut layout = source_v2(); + let ordinal = layout.operations.len() as u32; + layout.operations.push(NativeOperationRecord { + ordinal, + site: site(1, 999), + operation, + }); + assert!(parsed(&layout).is_err()); + } +} + +#[test] +fn native_registration_roundtrips_closed_records_and_owns_input() { + let layout = canonical(); + assert_eq!(parsed(&layout).unwrap(), layout); + let mut registry = ResourceRegistry::new(); + let mut installation = NativeLayoutInstallation::new(®istry); + let mut bytes = encode_records(&layout); + let registered = installation.install(®istry, &bytes).unwrap(); + bytes.fill(0); + assert!(Arc::ptr_eq(installation.installed().unwrap(), ®istered)); + assert_eq!(registered.hooks()[0].recipe(), NativeRecipe::NetworkFactory); + assert_eq!(registered.shapes(), layout.shapes); + assert_eq!(registered.operations(), layout.operations); + assert_eq!( + (registered.kinds_len(), registered.slot_kind(2).unwrap()), + (2, 0) + ); + assert_ne!(registered.kind_id(0).unwrap(), ResourceTypeId::new(0)); + registered + .preflight_physical_acquisition(&mut registry) + .unwrap(); + assert_eq!(registry.live_count(), 0); + assert_eq!( + ( + size_of::(), + align_of::() + ), + (16, 8) + ); + assert_eq!( + ( + offset_of!(JettResourceCallResultV1, domain), + offset_of!(JettResourceCallResultV1, reserved), + offset_of!(JettResourceCallResultV1, value) + ), + (0, 4, 8) + ); + assert_eq!((RESOURCE_DOMAIN_OK, RESOURCE_DOMAIN_ERROR), (0, 1)); +} + +#[test] +fn native_registration_refuses_wire_headers_lengths_tags_and_ordinals() { + let bytes = encode_records(&canonical()); + for end in 0..bytes.len() { + assert!(decode(&bytes[..end]).is_err(), "prefix {end}"); + } + let mutate = |offset, value: u32| { + let mut altered = bytes.clone(); + altered[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + altered + }; + for unsupported_version in [0, NATIVE_RESOURCE_LAYOUT_WIRE_VERSION + 1, u32::MAX] { + assert_eq!( + decode(&mutate(8, unsupported_version)).unwrap_err(), + ResourceLayoutError::Header + ); + } + assert_eq!( + decode(&mutate(12, 1)).unwrap_err(), + ResourceLayoutError::Reserved + ); + assert_eq!( + decode(&mutate(52, 1)).unwrap_err(), + ResourceLayoutError::Reserved + ); + assert_eq!( + decode(&mutate(24, 4097)).unwrap_err(), + ResourceLayoutError::WireLimit + ); + assert_eq!( + decode(&mutate(56, 1)).unwrap_err(), + ResourceLayoutError::DenseOrdinal + ); + // Header56 + two kind rows8 + hook ordinal/kind8 = first recipe tag72. + assert_eq!( + decode(&mutate(72, 99)).unwrap_err(), + ResourceLayoutError::UnknownTag + ); + let mut trailing = bytes.clone(); + trailing.push(0); + let length = trailing.len() as u64; + trailing[16..24].copy_from_slice(&length.to_le_bytes()); + assert_eq!( + decode(&trailing).unwrap_err(), + ResourceLayoutError::Trailing + ); + assert_eq!( + decode(&vec![0; wire::MAX_BYTES + 1]).unwrap_err(), + ResourceLayoutError::WireLimit + ); +} + +#[test] +fn native_registration_validates_unused_rows_recipes_shapes_and_active_paths() { + let mut layout = canonical(); + layout.hooks[3].signature = 0; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::RecipeMismatch + ); + let mut layout = canonical(); + layout.shapes.push(NativeShape::String); + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::NonCanonical + ); + let mut layout = canonical(); + let mut duplicate = layout.signatures[0].clone(); + duplicate.ordinal = layout.signatures.len() as u32; + layout.signatures.push(duplicate); + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::NonCanonical + ); + let mut layout = canonical(); + layout.shapes[8] = NativeShape::Optional { child: 8 }; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::InvalidReference + ); + let mut layout = canonical(); + layout.shapes.push(NativeShape::Optional { child: 8 }); + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::UnsupportedLayout + ); + let mut layout = canonical(); + layout.shapes.push(NativeShape::Result { ok: 1, fail: 4 }); + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::UnsupportedLayout + ); + let mut layout = canonical(); + layout.slots[2].path = vec![NativePayloadStep::Fail]; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::PayloadPath + ); + let mut layout = canonical(); + layout.slots[4].shape = 99; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::InvalidReference + ); + // Equal tuples can describe distinct original hook declarations/storage sites. + let mut layout = canonical(); + let mut hook = layout.hooks[0].clone(); + hook.ordinal = 4; + layout.hooks.push(hook); + let mut slot = layout.slots[0].clone(); + slot.ordinal = 5; + layout.slots.push(slot); + assert!(parsed(&layout).is_ok()); +} + +#[test] +fn native_registration_checks_all_frame_operation_and_loan_source_relations() { + let mut layout = canonical(); + layout.frames[1].parents = vec![NativeParent::Frame(0), NativeParent::Frame(0)]; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::NonCanonical + ); + let mut layout = canonical(); + layout.frames[1].parents = vec![NativeParent::Frame(99)]; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::InvalidReference + ); + let mut layout = canonical(); + layout.operations[4].operation = NativeOperation::InvokeBorrow { + frame: 1, + hook: 0, + source: NativeLoanSource::ExistingBorrow { operation: 3 }, + }; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::OperationMismatch + ); + let mut layout = canonical(); + layout.operations[6].operation = NativeOperation::EndBorrow { + frame: 1, + borrow: 12, + }; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::OperationMismatch + ); + let mut layout = canonical(); + layout.operations[14].operation = NativeOperation::InvokeDescriptor { + hook: 0, + signature: 0, + target: 13, + }; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::OperationMismatch + ); + let mut layout = canonical(); + layout.operations[15].operation = NativeOperation::InvokeSourceFunction { + frame: 1, + callee: 0, + signature: 4, + callee_scope: 3, + source: None, + }; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::FrameMismatch + ); + let mut layout = canonical(); + layout.operations[16].operation = NativeOperation::InvokeBorrow { + frame: 1, + hook: 1, + source: NativeLoanSource::IncomingViewFormal { + scope: 0, + parameter: 0, + }, + }; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::InvalidReference + ); + let registry = ResourceRegistry::new(); + let registered = installed(®istry); + let core = ResourceCustody::new(registered.program().clone(), ®istry).unwrap(); + // Lawful metadata never mints a resident loan, active scope, owner or provider. + assert_eq!( + ( + core.live_owners(), + core.live_loans(), + core.active_frames(), + registry.live_count() + ), + (0, 0, 0, 0) + ); +} + +#[test] +fn native_registration_function_frame_signatures_cannot_depend_on_instruction_site() { + let layout = canonical(); + assert_ne!(layout.frames[0].site, layout.frames[1].site); + assert_ne!(layout.frames[0].site, layout.frames[2].site); + assert!(parsed(&layout).is_ok()); + for frame in [1, 2] { + let mut altered = layout.clone(); + altered.frames[frame].signature = 0; + assert_eq!( + parsed(&altered).unwrap_err(), + ResourceLayoutError::FrameMismatch + ); + } +} + +#[test] +fn native_registration_issuer_checks_context_program_kind_slot_and_exhaustion() { + let mut registry = ResourceRegistry::new(); + let foreign = ResourceRegistry::new(); + let bytes = encode_records(&canonical()); + let mut installation = NativeLayoutInstallation::new(®istry); + assert_eq!( + installation.install(&foreign, &bytes).unwrap_err(), + ResourceLayoutError::WrongContext + ); + let registered = installation.install(®istry, &bytes).unwrap(); + assert_eq!( + installation.install(®istry, &bytes).unwrap_err(), + ResourceLayoutError::AlreadyInstalled + ); + let other = installed(®istry); + assert_ne!(registered.kind_id(0).unwrap(), other.kind_id(0).unwrap()); + assert!(matches!( + ResourceCustody::new(registered.program().clone(), &foreign), + Err(CustodyError::WrongContext) + )); + let mut core = ResourceCustody::new(registered.program().clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let holder = core.holder(&frame, ®istered.slot(0).unwrap()).unwrap(); + assert_eq!( + core.prepare_acquisition(&other.kind(0).unwrap(), &holder) + .unwrap_err(), + CustodyError::WrongProgram + ); + assert_eq!( + core.prepare_acquisition(®istered.kind(1).unwrap(), &holder) + .unwrap_err(), + CustodyError::WrongKind + ); + assert!(matches!( + registered.slot(99), + Err(CustodyError::UnknownSlot) + )); + assert!(matches!(registered.kind(99), Err(CustodyError::WrongKind))); + let counter = AtomicU64::new(u64::from(u32::MAX)); + assert_eq!( + issued_kinds(&counter, 2).unwrap_err(), + ResourceLayoutError::CapacityExhausted + ); + assert_eq!( + issued_kinds(&counter, 1).unwrap(), + vec![ResourceTypeId::new(u32::MAX)] + ); + assert_eq!( + issued_kinds(&counter, 1).unwrap_err(), + ResourceLayoutError::CapacityExhausted + ); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); +} + +#[test] +fn native_registration_physical_preflight_refuses_before_provider_or_publication() { + let bytes = encode_records(&canonical()); + let mut busy = ResourceRegistry::new(); + let busy_key = busy + .insert( + ResourceTypeId::new(7), + (), + AuthorityProvenance::new(1, 1), + |_| {}, + ) + .unwrap(); + assert_eq!( + NativeLayoutInstallation::new(&busy) + .install(&busy, &bytes) + .unwrap_err(), + ResourceLayoutError::RegistryBusy + ); + busy.close(busy_key, ResourceTypeId::new(7)).unwrap(); + let mut registry = ResourceRegistry::new(); + let registered = installed(®istry); + registry.shutting_down = true; + assert_eq!( + NativeLayoutInstallation::new(®istry) + .install(®istry, &bytes) + .unwrap_err(), + ResourceLayoutError::RegistryShutdown + ); + assert_eq!( + registered.preflight_physical_acquisition(&mut registry), + Err(ResourceLayoutError::RegistryShutdown) + ); + registry.shutting_down = false; + registry.next_creation_sequence = u64::MAX; + assert_eq!( + registered.preflight_physical_acquisition(&mut registry), + Err(ResourceLayoutError::CapacityExhausted) + ); + registry.next_creation_sequence = 1; + registry.free_slots.push(0); + assert_eq!( + registered.preflight_physical_acquisition(&mut registry), + Err(ResourceLayoutError::PhysicalStorage) + ); + registry.free_slots.clear(); + registered + .preflight_physical_acquisition(&mut registry) + .unwrap(); + assert_eq!(registry.live_count(), 0); + registry.slots.push(crate::Slot { + generation: u64::MAX, + entry: None, + }); + registry.free_slots.push(0); + assert_eq!( + registered.preflight_physical_acquisition(&mut registry), + Err(ResourceLayoutError::CapacityExhausted) + ); + registry.slots[0].generation = 0; + assert_eq!( + registered.preflight_physical_acquisition(&mut registry), + Err(ResourceLayoutError::PhysicalStorage) + ); + registry.slots[0].generation = 1; + registered + .preflight_physical_acquisition(&mut registry) + .unwrap(); + if let Some(index) = (u32::MAX as usize).checked_add(1) { + assert_eq!( + checked_physical_slot(index), + Err(ResourceLayoutError::CapacityExhausted) + ); + } + let mut foreign = ResourceRegistry::new(); + assert_eq!( + registered.preflight_physical_acquisition(&mut foreign), + Err(ResourceLayoutError::WrongContext) + ); +} + +#[test] +fn native_registration_required_purposes_cannot_acquire_from_signature_metadata() { + for purpose in [ + ResourcePurpose::NamespaceConstant, + ResourcePurpose::ExplicitComptime, + ResourcePurpose::Verify, + ResourcePurpose::Property, + ] { + let mut registry = ResourceRegistry::new(); + let registered = installed(®istry); + let mut core = ResourceCustody::new(registered.program().clone(), ®istry).unwrap(); + let frame = core.begin_frame(ResourceFrameKind::Scope, purpose).unwrap(); + let holder = core.holder(&frame, ®istered.slot(0).unwrap()).unwrap(); + assert_eq!( + core.prepare_acquisition(®istered.kind(0).unwrap(), &holder) + .unwrap_err(), + CustodyError::WrongPurpose + ); + assert_eq!( + (core.live_owners(), core.live_loans(), registry.live_count()), + (0, 0, 0) + ); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); + } +} + +#[test] +fn native_registration_real_core_preserves_holder_generation_loans_and_once_cleanup() { + let mut registry = ResourceRegistry::new(); + let registered = installed(®istry); + let mut core = ResourceCustody::new(registered.program().clone(), ®istry).unwrap(); + let events = Arc::new(Mutex::new(Vec::::new())); + let root = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let holder = core.holder(&root, ®istered.slot(0).unwrap()).unwrap(); + registered + .preflight_physical_acquisition(&mut registry) + .unwrap(); + let prepared = core + .prepare_acquisition(®istered.kind(0).unwrap(), &holder) + .unwrap(); + let captured = events.clone(); + let mut owner = core + .commit_acquisition( + &mut registry, + prepared, + AuthorityProvenance::new(5, 7), + 501i64, + move |value| captured.lock().unwrap().push(value), + ) + .unwrap(); + let stale = OwnedResourceToken { + core: owner.core, + owner: owner.owner, + holder_generation: owner.holder_generation, + }; + let operation = core + .begin_frame(ResourceFrameKind::Operation, ResourcePurpose::Runtime) + .unwrap(); + let other_holder = core + .holder(&operation, ®istered.slot(1).unwrap()) + .unwrap(); + registered + .preflight_physical_acquisition(&mut registry) + .unwrap(); + let prepared = core + .prepare_acquisition(®istered.kind(0).unwrap(), &other_holder) + .unwrap(); + let captured = events.clone(); + let _other = core + .commit_acquisition( + &mut registry, + prepared, + AuthorityProvenance::new(5, 7), + 502i64, + move |value| captured.lock().unwrap().push(value), + ) + .unwrap(); + let destination = core + .holder(&operation, ®istered.slot(2).unwrap()) + .unwrap(); + let mut loan = core.begin_borrow(&owner, &operation, ®istry).unwrap(); + assert!(core.validate_borrowed(&loan, ®istry).is_ok()); + assert_eq!( + core.transfer(&mut owner, &destination, ®istry), + Err(CustodyError::ActiveBorrow) + ); + core.end_borrow(&mut loan).unwrap(); + core.transfer(&mut owner, &destination, ®istry).unwrap(); + assert_eq!( + core.validate_owned(&stale, ®istry), + Err(CustodyError::StaleHolderGeneration) + ); + assert_eq!(core.end_frame(&operation, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [501, 502]); + assert_eq!(core.end_frame(&root, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [501, 502]); + assert_eq!( + ( + core.live_owners(), + core.live_loans(), + core.active_frames(), + registry.live_count() + ), + (0, 0, 0, 0) + ); +} + +#[test] +fn native_registration_real_core_continues_cleanup_and_preserves_cleanup_precedence() { + let mut registry = ResourceRegistry::new(); + let registered = installed(®istry); + let mut core = ResourceCustody::new(registered.program().clone(), ®istry).unwrap(); + let events = Arc::new(Mutex::new(Vec::::new())); + let root = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let holder = core.holder(&root, ®istered.slot(0).unwrap()).unwrap(); + registered + .preflight_physical_acquisition(&mut registry) + .unwrap(); + let prepared = core + .prepare_acquisition(®istered.kind(0).unwrap(), &holder) + .unwrap(); + let captured = events.clone(); + let _first = core + .commit_acquisition( + &mut registry, + prepared, + AuthorityProvenance::new(5, 7), + 601i64, + move |label| captured.lock().unwrap().push(label), + ) + .unwrap(); + let operation = core + .begin_frame(ResourceFrameKind::Operation, ResourcePurpose::Runtime) + .unwrap(); + let holder = core + .holder(&operation, ®istered.slot(1).unwrap()) + .unwrap(); + registered + .preflight_physical_acquisition(&mut registry) + .unwrap(); + let prepared = core + .prepare_acquisition(®istered.kind(0).unwrap(), &holder) + .unwrap(); + let captured = events.clone(); + let _second = core + .commit_acquisition( + &mut registry, + prepared, + AuthorityProvenance::new(5, 7), + 602i64, + move |label| { + captured.lock().unwrap().push(label); + panic!("selected registered-layout finalizer failure"); + }, + ) + .unwrap(); + let cleanup = core.unwind_all(&mut registry); + assert_eq!( + complete_resource( + ResourceBodyOutcome::<(), &str>::Completed(Err("selected body failure")), + cleanup + ), + Err(ResourceCompletionFailure::Cleanup( + ResourceCleanupFailure::FinalizerPanic + )) + ); + assert_eq!(*events.lock().unwrap(), [602, 601]); + assert_eq!( + ( + core.live_owners(), + core.live_loans(), + core.active_frames(), + registry.live_count() + ), + (0, 0, 0, 0) + ); + assert_eq!(core.unwind_all(&mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [602, 601]); +} diff --git a/crates/jett_runtime/src/resource_custody/registration/validation.rs b/crates/jett_runtime/src/resource_custody/registration/validation.rs new file mode 100644 index 00000000..a94a8ae7 --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/registration/validation.rs @@ -0,0 +1,486 @@ +//! Layout consistency only. Original Source/CFG proof is the compiler's obligation. + +use super::ResourceLayoutError; +use super::schema::*; +use super::wire::storage; +use std::collections::{HashMap, HashSet}; + +fn get(values: &[T], ordinal: u32) -> Result<&T, ResourceLayoutError> { + values + .get(ordinal as usize) + .ok_or(ResourceLayoutError::InvalidReference) +} + +fn validate_shapes(layout: &WireLayout) -> Result<(), ResourceLayoutError> { + let mut unique = HashSet::new(); + unique + .try_reserve(layout.shapes.len()) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + let mut occupied = storage::(layout.shapes.len())?; + for (index, shape) in layout.shapes.iter().enumerate() { + if !unique.insert(shape) { + return Err(ResourceLayoutError::NonCanonical); + } + let contains_resource = match *shape { + NativeShape::Integer { bits, .. } if [8, 16, 32, 64].contains(&bits) => false, + NativeShape::Float { bits } if [32, 64].contains(&bits) => false, + NativeShape::Integer { .. } | NativeShape::Float { .. } => { + return Err(ResourceLayoutError::UnsupportedLayout); + } + NativeShape::Bool + | NativeShape::String + | NativeShape::Nothing + | NativeShape::Network => false, + NativeShape::Resource { kind } => { + get(&layout.kinds, kind)?; + true + } + NativeShape::Optional { child } => { + if child as usize >= index { + return Err(ResourceLayoutError::InvalidReference); + } + let has_resource = occupied[child as usize]; + if has_resource + && !matches!(layout.shapes[child as usize], NativeShape::Resource { .. }) + { + return Err(ResourceLayoutError::UnsupportedLayout); + } + has_resource + } + NativeShape::Result { ok, fail } => { + if ok as usize >= index || fail as usize >= index { + return Err(ResourceLayoutError::InvalidReference); + } + if occupied[fail as usize] + || (occupied[ok as usize] + && !matches!(layout.shapes[ok as usize], NativeShape::Resource { .. })) + { + return Err(ResourceLayoutError::UnsupportedLayout); + } + occupied[ok as usize] + } + NativeShape::Carrier { node } => { + if layout.version != 3 { + return Err(ResourceLayoutError::UnsupportedLayout); + } + layout.carriers.node(node)?; + true + } + NativeShape::HookDescriptor { hook } => { + get(&layout.hooks, hook)?; + false + } + }; + occupied.push(contains_resource); + } + Ok(()) +} + +fn validate_signatures(layout: &WireLayout) -> Result<(), ResourceLayoutError> { + let mut unique = HashSet::new(); + unique + .try_reserve(layout.signatures.len()) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + for signature in &layout.signatures { + get(&layout.shapes, signature.result)?; + for formal in &signature.parameters { + get(&layout.shapes, formal.shape)?; + } + if !unique.insert((signature.parameters.as_slice(), signature.result)) { + return Err(ResourceLayoutError::NonCanonical); + } + } + Ok(()) +} + +fn shape_is(layout: &WireLayout, ordinal: u32, expected: NativeShape) -> bool { + layout.shapes.get(ordinal as usize) == Some(&expected) +} + +fn validate_hooks(layout: &WireLayout) -> Result<(), ResourceLayoutError> { + for hook in &layout.hooks { + get(&layout.kinds, hook.kind)?; + let signature = get(&layout.signatures, hook.signature)?; + let formal = |index: usize, shape: NativeShape, access| { + signature.parameters.get(index).is_some_and(|parameter| { + parameter.access == access && shape_is(layout, parameter.shape, shape) + }) + }; + let resource = NativeShape::Resource { kind: hook.kind }; + let integer = NativeShape::Integer { + bits: 64, + signed: true, + }; + let returns_result = |ok: NativeShape| { + let NativeShape::Result { ok: value, fail } = layout.shapes[signature.result as usize] + else { + return false; + }; + shape_is(layout, value, ok) && shape_is(layout, fail, NativeShape::String) + }; + let matches = match hook.recipe { + NativeRecipe::NetworkFactory => { + signature.parameters.len() == 2 + && formal(0, NativeShape::Network, NativeAccess::View) + && formal(1, integer, NativeAccess::Owned) + && returns_result(resource) + } + NativeRecipe::NetworkBorrow => { + signature.parameters.len() == 2 + && formal(0, NativeShape::Network, NativeAccess::View) + && formal(1, resource, NativeAccess::View) + && returns_result(integer) + } + NativeRecipe::Finalize => { + signature.parameters.len() == 1 + && formal(0, resource, NativeAccess::Owned) + && shape_is(layout, signature.result, NativeShape::Nothing) + } + }; + if !matches { + return Err(ResourceLayoutError::RecipeMismatch); + } + } + Ok(()) +} + +fn validate_frames(layout: &WireLayout) -> Result<(), ResourceLayoutError> { + let mut signatures = HashMap::new(); + signatures + .try_reserve(layout.frames.len()) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + for frame in &layout.frames { + get(&layout.signatures, frame.signature)?; + if let Some(previous) = signatures.insert(frame.site.function, frame.signature) { + if previous != frame.signature { + return Err(ResourceLayoutError::FrameMismatch); + } + } + if frame.parents.windows(2).any(|pair| pair[0] >= pair[1]) { + return Err(ResourceLayoutError::NonCanonical); + } + for parent in &frame.parents { + if let NativeParent::Frame(index) = parent { + get(&layout.frames, *index)?; + } + } + } + Ok(()) +} + +pub(super) fn slot_kind( + layout: &WireLayout, + slot: &NativeSlot, +) -> Result { + let mut shape = get(&layout.shapes, slot.shape)?; + for step in &slot.path { + shape = match (shape, step) { + (NativeShape::Optional { child }, NativePayloadStep::Some) => { + get(&layout.shapes, *child)? + } + (NativeShape::Result { ok, .. }, NativePayloadStep::Ok) => get(&layout.shapes, *ok)?, + (NativeShape::Result { fail, .. }, NativePayloadStep::Fail) => { + get(&layout.shapes, *fail)? + } + _ => return Err(ResourceLayoutError::PayloadPath), + }; + } + if let NativeShape::Resource { kind } = shape { + Ok(*kind) + } else { + Err(ResourceLayoutError::PayloadPath) + } +} + +fn source_kind(layout: &WireLayout, source: NativeLoanSource) -> Result { + match source { + NativeLoanSource::ProjectedSumPayload { operation } => { + super::source_validation::projected_kind(layout, operation) + } + NativeLoanSource::ExistingBorrow { operation } => { + let record = get(&layout.operations, operation)?; + let NativeOperation::Borrow { source, .. } = record.operation else { + return Err(ResourceLayoutError::OperationMismatch); + }; + slot_kind(layout, get(&layout.slots, source)?) + } + NativeLoanSource::IncomingViewFormal { scope, parameter } => { + let frame = get(&layout.frames, scope)?; + if frame.role != NativeFrameRole::Scope { + return Err(ResourceLayoutError::FrameMismatch); + } + let signature = get(&layout.signatures, frame.signature)?; + let formal = get(&signature.parameters, parameter)?; + if formal.access != NativeAccess::View { + return Err(ResourceLayoutError::OperationMismatch); + } + match *get(&layout.shapes, formal.shape)? { + NativeShape::Resource { kind } => Ok(kind), + _ => Err(ResourceLayoutError::ShapeMismatch), + } + } + } +} + +fn validate_operation( + layout: &WireLayout, + record: &NativeOperationRecord, +) -> Result<(), ResourceLayoutError> { + let frame = |index| -> Result<&NativeFrame, ResourceLayoutError> { + let frame = get(&layout.frames, index)?; + if frame.site.function != record.site.function { + return Err(ResourceLayoutError::FrameMismatch); + } + Ok(frame) + }; + let slot = |index| get(&layout.slots, index); + let kind = |index| slot_kind(layout, slot(index)?); + let hook = |index, recipe, kind| -> Result<(), ResourceLayoutError> { + let hook = get(&layout.hooks, index)?; + if hook.recipe != recipe || hook.kind != kind { + return Err(ResourceLayoutError::OperationMismatch); + } + Ok(()) + }; + let same_slots = |a, b| -> Result<(), ResourceLayoutError> { + if a == b { + return Err(ResourceLayoutError::OperationMismatch); + } + let (a, b) = (slot(a)?, slot(b)?); + if a.shape != b.shape || a.path != b.path { + return Err(ResourceLayoutError::ShapeMismatch); + } + Ok(()) + }; + use NativeOperation::*; + if let Carrier { record: carrier } = record.operation { + let row = layout + .carriers + .operations + .get(carrier as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + if layout.version != 3 || row.site != record.site { + return Err(ResourceLayoutError::OperationMismatch); + } + return Ok(()); + } + if let InvokeBorrow { + source: NativeLoanSource::ProjectedSumPayload { operation }, + .. + } + | BoundedBorrowUse { + source: NativeLoanSource::ProjectedSumPayload { operation }, + .. + } = record.operation + { + if get(&layout.operations, operation)?.site.function != record.site.function { + return Err(ResourceLayoutError::FrameMismatch); + } + } + match record.operation { + Carrier { .. } => return Err(ResourceLayoutError::OperationMismatch), + Acquire { + frame: f, + hook: h, + destination, + } => { + frame(f)?; + hook(h, NativeRecipe::NetworkFactory, kind(destination)?)?; + } + Transfer { + frame: f, + source, + destination, + } => { + frame(f)?; + same_slots(source, destination)?; + } + Borrow { + frame: f, + source, + lease_frame, + } => { + frame(f)?; + kind(source)?; + get(&layout.frames, lease_frame)?; + } + BoundedBorrowUse { + frame: f, + source, + callee_frame, + } => { + frame(f)?; + let kind = source_kind(layout, source)?; + let callee = get(&layout.frames, callee_frame)?; + if callee.role != NativeFrameRole::Scope { + return Err(ResourceLayoutError::FrameMismatch); + } + let signature = get(&layout.signatures, callee.signature)?; + if !signature.parameters.iter().any(|p| { + p.access == NativeAccess::View + && shape_is(layout, p.shape, NativeShape::Resource { kind }) + }) { + return Err(ResourceLayoutError::OperationMismatch); + } + } + EndBorrow { frame: f, borrow } => { + frame(f)?; + if get(&layout.operations, borrow)?.site.function != record.site.function + || !matches!( + get(&layout.operations, borrow)?.operation, + Borrow { .. } | ProjectSumView { .. } + ) + { + return Err(ResourceLayoutError::OperationMismatch); + } + } + InvokeBorrow { + frame: f, + hook: h, + source, + } => { + frame(f)?; + hook(h, NativeRecipe::NetworkBorrow, source_kind(layout, source)?)?; + } + Close { + frame: f, + hook: h, + source, + } => { + frame(f)?; + hook(h, NativeRecipe::Finalize, kind(source)?)?; + } + Drop { frame: f, source } => { + frame(f)?; + kind(source)?; + } + SumAdopt { + frame: f, + source, + destination, + } => { + frame(f)?; + let (source_slot, destination_slot) = (slot(source)?, slot(destination)?); + if !source_slot.path.is_empty() + || destination_slot.path.len() != 1 + || kind(source)? != kind(destination)? + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + SumTake { + frame: f, + source, + destination, + } => { + frame(f)?; + let (source_slot, destination_slot) = (slot(source)?, slot(destination)?); + if source_slot.path.len() != 1 + || !destination_slot.path.is_empty() + || kind(source)? != kind(destination)? + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + SumDrop { frame: f, source } => { + frame(f)?; + if slot(source)?.path.len() != 1 { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + Replace { + frame: f, + old, + replacement, + } => { + frame(f)?; + same_slots(old, replacement)?; + } + Complete { frame: f } => { + frame(f)?; + } + Descriptor { hook } => { + get(&layout.hooks, hook)?; + } + InvokeDescriptor { + hook, + signature, + target, + } => { + let declared = get(&layout.hooks, hook)?; + if declared.signature != signature { + return Err(ResourceLayoutError::OperationMismatch); + } + let target = get(&layout.operations, target)?; + let (target_hook, recipe) = match target.operation { + Acquire { hook, .. } => (hook, NativeRecipe::NetworkFactory), + InvokeBorrow { hook, .. } => (hook, NativeRecipe::NetworkBorrow), + Close { hook, .. } => (hook, NativeRecipe::Finalize), + _ => return Err(ResourceLayoutError::OperationMismatch), + }; + if target_hook != hook || declared.recipe != recipe { + return Err(ResourceLayoutError::OperationMismatch); + } + } + InvokeSourceFunction { + frame: f, + callee, + signature, + callee_scope, + ref source, + } => { + frame(f)?; + if let Some(source) = source { + return super::source_validation::invocation( + layout, + record, + f, + callee, + signature, + callee_scope, + source, + ); + } + if layout.version != 1 { + return Err(ResourceLayoutError::OperationMismatch); + } + let scope = get(&layout.frames, callee_scope)?; + if scope.role != NativeFrameRole::Scope + || scope.site.function != callee + || scope.signature != signature + { + return Err(ResourceLayoutError::FrameMismatch); + } + } + BorrowSum { .. } + | ObserveSumView { .. } + | ProjectSumView { .. } + | ReadFailureCompanion { .. } + | EndSumBorrow { .. } + | TakeFailureCompanion { .. } + | PublishReturn { .. } + | CreateAbsentSum { .. } + | CreateFailureSum { .. } => { + return super::source_validation::operation(layout, record); + } + } + Ok(()) +} + +/// Returns the complete slot-kind projection before the issuer allocates identities. +pub(super) fn validate(layout: &WireLayout) -> Result, ResourceLayoutError> { + validate_shapes(layout)?; + validate_signatures(layout)?; + validate_hooks(layout)?; + validate_frames(layout)?; + let mut slot_kinds = storage(layout.slots.len())?; + for slot in &layout.slots { + get(&layout.frames, slot.frame)?; + slot_kinds.push(slot_kind(layout, slot)? as usize); + } + super::carriers::validate(layout)?; + for operation in &layout.operations { + validate_operation(layout, operation)?; + } + Ok(slot_kinds) +} diff --git a/crates/jett_runtime/src/resource_custody/registration/wire.rs b/crates/jett_runtime/src/resource_custody/registration/wire.rs new file mode 100644 index 00000000..76e5c5ee --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/registration/wire.rs @@ -0,0 +1,1074 @@ +//! Bounded owned decoding; wire bytes contain no Source or registry-key authority. + +use super::ResourceLayoutError; +use super::carriers::{NativeCarrierLayout, NativeCarrierLoanSource}; +use super::schema::*; +#[path = "wire/carriers.rs"] +mod carriers; + +pub(super) const MAX_BYTES: usize = 1024 * 1024; +pub(super) const MAX_ROWS: usize = 4096; +const HEADER_BYTES: usize = 56; +const MAGIC: &[u8; 8] = b"JTRSC001"; + +struct Reader<'a> { + bytes: &'a [u8], + position: usize, +} +impl<'a> Reader<'a> { + fn take(&mut self) -> Result<[u8; N], ResourceLayoutError> { + let end = self + .position + .checked_add(N) + .ok_or(ResourceLayoutError::WireLimit)?; + let value = self + .bytes + .get(self.position..end) + .ok_or(ResourceLayoutError::Truncated)?; + self.position = end; + Ok(value.try_into().expect("fixed checked slice")) + } + fn word(&mut self) -> Result { + Ok(u32::from_le_bytes(self.take()?)) + } + fn count(&mut self) -> Result { + let count = self.word()? as usize; + if count > MAX_ROWS { + return Err(ResourceLayoutError::WireLimit); + } + Ok(count) + } + fn ordinal(&mut self, index: usize) -> Result<(), ResourceLayoutError> { + if self.word()? as usize != index { + return Err(ResourceLayoutError::DenseOrdinal); + } + Ok(()) + } + fn site(&mut self) -> Result { + let function = self.word()?; + let block = self.word()?; + let tag = self.word()?; + let index = self.word()?; + let position = match (tag, index) { + (0, index) => NativePosition::Statement(index), + (1, 0) => NativePosition::Terminator, + (1, _) => return Err(ResourceLayoutError::Reserved), + _ => return Err(ResourceLayoutError::UnknownTag), + }; + Ok(NativeSite { + function, + block, + position, + }) + } + fn loan(&mut self, version: u32) -> Result { + Ok(match self.word()? { + 1 => NativeLoanSource::ExistingBorrow { + operation: self.word()?, + }, + 2 => NativeLoanSource::IncomingViewFormal { + scope: self.word()?, + parameter: self.word()?, + }, + 3 if matches!(version, 2 | 3) => NativeLoanSource::ProjectedSumPayload { + operation: self.word()?, + }, + _ => return Err(ResourceLayoutError::UnknownTag), + }) + } + fn sum_loan(&mut self, version: u32) -> Result { + Ok(match self.word()? { + 3 if version == 3 => NativeSumLoanSource::CarrierProjected { + operation: self.word()?, + }, + 1 => NativeSumLoanSource::ExistingBorrow { + operation: self.word()?, + }, + 2 => NativeSumLoanSource::IncomingViewFormal { + scope: self.word()?, + parameter: self.word()?, + }, + _ => return Err(ResourceLayoutError::UnknownTag), + }) + } + fn shape(&mut self, version: u32) -> Result { + Ok(match self.word()? { + 1 => { + let bits = self.word()?; + let signed = match self.word()? { + 0 => false, + 1 => true, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + NativeShape::Integer { bits, signed } + } + 2 => NativeShape::Float { bits: self.word()? }, + 3 => NativeShape::Bool, + 4 => NativeShape::String, + 5 => NativeShape::Nothing, + 6 => NativeShape::Network, + 7 => NativeShape::Resource { kind: self.word()? }, + 8 => NativeShape::Optional { + child: self.word()?, + }, + 9 => NativeShape::Result { + ok: self.word()?, + fail: self.word()?, + }, + 10 => NativeShape::HookDescriptor { hook: self.word()? }, + 11 if version == 3 => NativeShape::Carrier { node: self.word()? }, + _ => return Err(ResourceLayoutError::UnknownTag), + }) + } + fn operation(&mut self, version: u32) -> Result { + Ok(match self.word()? { + 26 if version == 3 => NativeOperation::Carrier { + record: self.word()?, + }, + 1 => NativeOperation::Acquire { + frame: self.word()?, + hook: self.word()?, + destination: self.word()?, + }, + 2 => NativeOperation::Transfer { + frame: self.word()?, + source: self.word()?, + destination: self.word()?, + }, + 3 => NativeOperation::Borrow { + frame: self.word()?, + source: self.word()?, + lease_frame: self.word()?, + }, + 4 => NativeOperation::BoundedBorrowUse { + frame: self.word()?, + source: self.loan(version)?, + callee_frame: self.word()?, + }, + 5 => NativeOperation::EndBorrow { + frame: self.word()?, + borrow: self.word()?, + }, + 6 => NativeOperation::InvokeBorrow { + frame: self.word()?, + hook: self.word()?, + source: self.loan(version)?, + }, + 7 => NativeOperation::Close { + frame: self.word()?, + hook: self.word()?, + source: self.word()?, + }, + 8 => NativeOperation::Drop { + frame: self.word()?, + source: self.word()?, + }, + 9 => NativeOperation::SumAdopt { + frame: self.word()?, + source: self.word()?, + destination: self.word()?, + }, + 10 => NativeOperation::SumTake { + frame: self.word()?, + source: self.word()?, + destination: self.word()?, + }, + 11 => NativeOperation::SumDrop { + frame: self.word()?, + source: self.word()?, + }, + 12 => NativeOperation::Replace { + frame: self.word()?, + old: self.word()?, + replacement: self.word()?, + }, + 13 => NativeOperation::Complete { + frame: self.word()?, + }, + 14 => NativeOperation::Descriptor { hook: self.word()? }, + 15 => NativeOperation::InvokeDescriptor { + hook: self.word()?, + signature: self.word()?, + target: self.word()?, + }, + 16 => NativeOperation::InvokeSourceFunction { + frame: self.word()?, + callee: self.word()?, + signature: self.word()?, + callee_scope: self.word()?, + source: if matches!(version, 2 | 3) { + Some(self.source_invocation(version)?) + } else { + None + }, + }, + 17 if matches!(version, 2 | 3) => NativeOperation::TakeFailureCompanion { + frame: self.word()?, + source_sum_slot: self.word()?, + failure_shape: self.word()?, + }, + 18 if matches!(version, 2 | 3) => NativeOperation::PublishReturn { + frame: self.word()?, + source_return_slot: self.word()?, + }, + 19 if matches!(version, 2 | 3) => NativeOperation::CreateAbsentSum { + frame: self.word()?, + destination_slot: self.word()?, + }, + 20 if matches!(version, 2 | 3) => NativeOperation::CreateFailureSum { + frame: self.word()?, + destination_slot: self.word()?, + failure_shape: self.word()?, + }, + 21 if matches!(version, 2 | 3) => NativeOperation::BorrowSum { + frame: self.word()?, + source_sum_slot: self.word()?, + lease_frame: self.word()?, + }, + 22 if matches!(version, 2 | 3) => NativeOperation::ObserveSumView { + frame: self.word()?, + source: self.sum_loan(version)?, + }, + 23 if matches!(version, 2 | 3) => NativeOperation::ProjectSumView { + frame: self.word()?, + source: self.sum_loan(version)?, + path: match self.word()? { + 1 => NativePayloadStep::Some, + 2 => NativePayloadStep::Ok, + _ => return Err(ResourceLayoutError::UnknownTag), + }, + lease_frame: self.word()?, + }, + 24 if matches!(version, 2 | 3) => NativeOperation::ReadFailureCompanion { + frame: self.word()?, + source: self.sum_loan(version)?, + failure_shape: self.word()?, + }, + 25 if matches!(version, 2 | 3) => NativeOperation::EndSumBorrow { + frame: self.word()?, + borrow: self.word()?, + }, + _ => return Err(ResourceLayoutError::UnknownTag), + }) + } + fn source_invocation( + &mut self, + version: u32, + ) -> Result { + let callee_return = match self.word()? { + 0 => None, + 1 => Some(self.word()?), + _ => return Err(ResourceLayoutError::UnknownTag), + }; + let count = self.count()?; + let mut evaluation_order = storage(count)?; + for _ in 0..count { + evaluation_order.push(self.word()?); + } + let count = self.count()?; + let mut formals = storage(count)?; + for _ in 0..count { + let parameter = self.word()?; + let source_index = self.word()?; + let actual_shape = self.word()?; + let callee_shape = self.word()?; + let syntax = match self.word()? { + 1 => NativeSourceSyntax::Bare, + 2 => NativeSourceSyntax::WrittenView, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + let effect = match self.word()? { + 1 => NativeSourceEffect::Copy, + 2 => NativeSourceEffect::TransferOwned, + 3 => NativeSourceEffect::RelinquishOwned, + 4 => NativeSourceEffect::RetainBorrow, + 5 => NativeSourceEffect::ObserveData, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + let access = match self.word()? { + 1 => NativeAccess::Owned, + 2 => NativeAccess::View, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + let value = match self.word()? { + 5 if version == 3 => NativeSourceValue::CarrierOwned { + caller_argument_slot: self.word()?, + callee_parameter_slot: self.word()?, + }, + 6 if version == 3 => NativeSourceValue::CarrierView { + source: carriers::loan(self)?, + }, + 1 => NativeSourceValue::Ordinary, + 2 => NativeSourceValue::Owned { + caller_argument_slot: self.word()?, + callee_parameter_slot: self.word()?, + }, + 3 => NativeSourceValue::ResidentView { + source: self.loan(2)?, + }, + 4 => NativeSourceValue::ResidentSumView { + source: self.sum_loan(version)?, + }, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + formals.push(NativeSourceFormal { + parameter, + source_index, + actual_shape, + callee_shape, + syntax, + effect, + access, + value, + }); + } + let result = match self.word()? { + 3 if version == 3 => { + let shape = self.word()?; + let caller_destination_frame = self.word()?; + let caller_destination_slot = self.word()?; + let callee_return_frame = self.word()?; + let permitted_return_slots = { + let n = self.count()?; + let mut v = storage(n)?; + for _ in 0..n { + v.push(self.word()?); + } + v + }; + NativeSourceResult::Carrier { + shape, + caller_destination_frame, + caller_destination_slot, + callee_return_frame, + permitted_return_slots, + } + } + 1 => NativeSourceResult::Ordinary { + shape: self.word()?, + }, + 2 => { + let shape = self.word()?; + let caller_destination_frame = self.word()?; + let caller_destination_slot = self.word()?; + let callee_return_frame = self.word()?; + let count = self.count()?; + let mut permitted_return_slots = storage(count)?; + for _ in 0..count { + permitted_return_slots.push(self.word()?); + } + NativeSourceResult::Owned { + shape, + caller_destination_frame, + caller_destination_slot, + callee_return_frame, + permitted_return_slots, + } + } + _ => return Err(ResourceLayoutError::UnknownTag), + }; + Ok(NativeSourceInvocation { + callee_return, + evaluation_order, + formals, + result, + }) + } +} + +pub(super) fn storage(count: usize) -> Result, ResourceLayoutError> { + let mut values = Vec::new(); + values + .try_reserve_exact(count) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + Ok(values) +} + +pub(super) fn decode(bytes: &[u8]) -> Result { + if bytes.len() > MAX_BYTES { + return Err(ResourceLayoutError::WireLimit); + } + if bytes.len() < HEADER_BYTES { + return Err(ResourceLayoutError::Truncated); + } + let mut input = Reader { bytes, position: 0 }; + if &input.take::<8>()? != MAGIC { + return Err(ResourceLayoutError::Header); + } + let version = input.word()?; + if ![1, 2, 3].contains(&version) { + return Err(ResourceLayoutError::Header); + } + if input.word()? != 0 { + return Err(ResourceLayoutError::Reserved); + } + if u64::from_le_bytes(input.take()?) != bytes.len() as u64 { + return Err(ResourceLayoutError::Length); + } + let counts = [ + input.count()?, + input.count()?, + input.count()?, + input.count()?, + input.count()?, + input.count()?, + input.count()?, + ]; + if input.word()? != 0 { + return Err(ResourceLayoutError::Reserved); + } + let mut kinds = storage(counts[0])?; + for i in 0..counts[0] { + input.ordinal(i)?; + kinds.push(i as u32); + } + let mut hooks = storage(counts[1])?; + for i in 0..counts[1] { + input.ordinal(i)?; + let kind = input.word()?; + let recipe = match input.word()? { + 1 => NativeRecipe::NetworkFactory, + 2 => NativeRecipe::NetworkBorrow, + 3 => NativeRecipe::Finalize, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + hooks.push(NativeHook { + ordinal: i as u32, + kind, + recipe, + signature: input.word()?, + }); + } + let mut signatures = storage(counts[2])?; + for i in 0..counts[2] { + input.ordinal(i)?; + let count = input.count()?; + let result = input.word()?; + let mut parameters = storage(count)?; + for _ in 0..count { + let shape = input.word()?; + let access = match input.word()? { + 1 => NativeAccess::Owned, + 2 => NativeAccess::View, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + parameters.push(NativeFormal { shape, access }); + } + signatures.push(NativeSignature { + ordinal: i as u32, + parameters, + result, + }); + } + let mut shapes = storage(counts[3])?; + for i in 0..counts[3] { + input.ordinal(i)?; + shapes.push(input.shape(version)?); + } + let mut frames = storage(counts[4])?; + for i in 0..counts[4] { + input.ordinal(i)?; + let role = match input.word()? { + 1 => NativeFrameRole::Scope, + 2 => NativeFrameRole::Operation, + 3 => NativeFrameRole::Return, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + let site = input.site()?; + let signature = input.word()?; + let count = input.count()?; + let mut parents = storage(count)?; + for _ in 0..count { + let tag = input.word()?; + let index = input.word()?; + parents.push(match (tag, index) { + (0, 0) => NativeParent::Root, + (0, _) => return Err(ResourceLayoutError::Reserved), + (1, index) => NativeParent::Frame(index), + _ => return Err(ResourceLayoutError::UnknownTag), + }); + } + frames.push(NativeFrame { + ordinal: i as u32, + role, + site, + signature, + parents, + }); + } + let mut slots = storage(counts[5])?; + for i in 0..counts[5] { + input.ordinal(i)?; + let frame = input.word()?; + let shape = input.word()?; + let count = input.count()?; + let mut path = storage(count)?; + for _ in 0..count { + path.push(match input.word()? { + 1 => NativePayloadStep::Some, + 2 => NativePayloadStep::Ok, + 3 => NativePayloadStep::Fail, + _ => return Err(ResourceLayoutError::UnknownTag), + }); + } + slots.push(NativeSlot { + ordinal: i as u32, + frame, + shape, + path, + }); + } + let mut operations = storage(counts[6])?; + for i in 0..counts[6] { + input.ordinal(i)?; + let site = input.site()?; + operations.push(NativeOperationRecord { + ordinal: i as u32, + site, + operation: input.operation(version)?, + }); + } + let carriers = if version == 3 { + carriers::decode(&mut input)? + } else { + NativeCarrierLayout::default() + }; + if input.position != bytes.len() { + return Err(ResourceLayoutError::Trailing); + } + Ok(WireLayout { + carriers, + version, + kinds, + hooks, + signatures, + shapes, + frames, + slots, + operations, + }) +} + +// Test encoder pins the same explicit tags/order. It intentionally admits mutated +// records so negative tests exercise production decoding/validation, not a second issuer. +#[cfg(test)] +pub(super) fn encode_records(layout: &WireLayout) -> Vec { + struct Writer(Vec); + impl Writer { + fn word(&mut self, value: u32) { + self.0.extend_from_slice(&value.to_le_bytes()); + } + fn site(&mut self, site: NativeSite) { + self.word(site.function); + self.word(site.block); + match site.position { + NativePosition::Statement(i) => { + self.word(0); + self.word(i); + } + NativePosition::Terminator => { + self.word(1); + self.word(0); + } + } + } + fn loan(&mut self, source: NativeLoanSource) { + match source { + NativeLoanSource::ProjectedSumPayload { operation } => { + self.word(3); + self.word(operation); + } + NativeLoanSource::ExistingBorrow { operation } => { + self.word(1); + self.word(operation); + } + NativeLoanSource::IncomingViewFormal { scope, parameter } => { + self.word(2); + self.word(scope); + self.word(parameter); + } + } + } + fn sum_loan(&mut self, source: NativeSumLoanSource) { + match source { + NativeSumLoanSource::CarrierProjected { operation } => { + self.word(3); + self.word(operation); + } + NativeSumLoanSource::ExistingBorrow { operation } => { + self.word(1); + self.word(operation); + } + NativeSumLoanSource::IncomingViewFormal { scope, parameter } => { + self.word(2); + self.word(scope); + self.word(parameter); + } + } + } + fn operation(&mut self, operation: &NativeOperation) { + use NativeOperation::*; + match *operation { + NativeOperation::Carrier { record } => { + self.word(26); + self.word(record); + } + Acquire { + frame, + hook, + destination, + } => { + self.word(1); + self.word(frame); + self.word(hook); + self.word(destination); + } + Transfer { + frame, + source, + destination, + } => { + self.word(2); + self.word(frame); + self.word(source); + self.word(destination); + } + Borrow { + frame, + source, + lease_frame, + } => { + self.word(3); + self.word(frame); + self.word(source); + self.word(lease_frame); + } + BoundedBorrowUse { + frame, + source, + callee_frame, + } => { + self.word(4); + self.word(frame); + self.loan(source); + self.word(callee_frame); + } + EndBorrow { frame, borrow } => { + self.word(5); + self.word(frame); + self.word(borrow); + } + InvokeBorrow { + frame, + hook, + source, + } => { + self.word(6); + self.word(frame); + self.word(hook); + self.loan(source); + } + Close { + frame, + hook, + source, + } => { + self.word(7); + self.word(frame); + self.word(hook); + self.word(source); + } + Drop { frame, source } => { + self.word(8); + self.word(frame); + self.word(source); + } + SumAdopt { + frame, + source, + destination, + } => { + self.word(9); + self.word(frame); + self.word(source); + self.word(destination); + } + SumTake { + frame, + source, + destination, + } => { + self.word(10); + self.word(frame); + self.word(source); + self.word(destination); + } + SumDrop { frame, source } => { + self.word(11); + self.word(frame); + self.word(source); + } + Replace { + frame, + old, + replacement, + } => { + self.word(12); + self.word(frame); + self.word(old); + self.word(replacement); + } + Complete { frame } => { + self.word(13); + self.word(frame); + } + Descriptor { hook } => { + self.word(14); + self.word(hook); + } + InvokeDescriptor { + hook, + signature, + target, + } => { + self.word(15); + self.word(hook); + self.word(signature); + self.word(target); + } + InvokeSourceFunction { + frame, + callee, + signature, + callee_scope, + ref source, + } => { + self.word(16); + self.word(frame); + self.word(callee); + self.word(signature); + self.word(callee_scope); + if let Some(source) = source { + self.source_invocation(source); + } + } + TakeFailureCompanion { + frame, + source_sum_slot, + failure_shape, + } => { + self.word(17); + self.word(frame); + self.word(source_sum_slot); + self.word(failure_shape); + } + PublishReturn { + frame, + source_return_slot, + } => { + self.word(18); + self.word(frame); + self.word(source_return_slot); + } + CreateAbsentSum { + frame, + destination_slot, + } => { + self.word(19); + self.word(frame); + self.word(destination_slot); + } + CreateFailureSum { + frame, + destination_slot, + failure_shape, + } => { + self.word(20); + self.word(frame); + self.word(destination_slot); + self.word(failure_shape); + } + BorrowSum { + frame, + source_sum_slot, + lease_frame, + } => { + self.word(21); + self.word(frame); + self.word(source_sum_slot); + self.word(lease_frame); + } + ObserveSumView { frame, source } => { + self.word(22); + self.word(frame); + self.sum_loan(source); + } + ProjectSumView { + frame, + source, + path, + lease_frame, + } => { + self.word(23); + self.word(frame); + self.sum_loan(source); + self.word(match path { + NativePayloadStep::Some => 1, + NativePayloadStep::Ok => 2, + NativePayloadStep::Fail => 3, + }); + self.word(lease_frame); + } + ReadFailureCompanion { + frame, + source, + failure_shape, + } => { + self.word(24); + self.word(frame); + self.sum_loan(source); + self.word(failure_shape); + } + EndSumBorrow { frame, borrow } => { + self.word(25); + self.word(frame); + self.word(borrow); + } + } + } + fn source_invocation(&mut self, source: &NativeSourceInvocation) { + match source.callee_return { + None => self.word(0), + Some(frame) => { + self.word(1); + self.word(frame); + } + } + self.word(source.evaluation_order.len() as u32); + for parameter in &source.evaluation_order { + self.word(*parameter); + } + self.word(source.formals.len() as u32); + for formal in &source.formals { + self.word(formal.parameter); + self.word(formal.source_index); + self.word(formal.actual_shape); + self.word(formal.callee_shape); + self.word(match formal.syntax { + NativeSourceSyntax::Bare => 1, + NativeSourceSyntax::WrittenView => 2, + }); + self.word(match formal.effect { + NativeSourceEffect::Copy => 1, + NativeSourceEffect::TransferOwned => 2, + NativeSourceEffect::RelinquishOwned => 3, + NativeSourceEffect::RetainBorrow => 4, + NativeSourceEffect::ObserveData => 5, + }); + self.word(match formal.access { + NativeAccess::Owned => 1, + NativeAccess::View => 2, + }); + match formal.value { + NativeSourceValue::CarrierOwned { + caller_argument_slot, + callee_parameter_slot, + } => { + self.word(5); + self.word(caller_argument_slot); + self.word(callee_parameter_slot); + } + NativeSourceValue::CarrierView { source } => { + self.word(6); + carriers::loan_out(&mut self.0, source); + } + NativeSourceValue::Ordinary => self.word(1), + NativeSourceValue::Owned { + caller_argument_slot, + callee_parameter_slot, + } => { + self.word(2); + self.word(caller_argument_slot); + self.word(callee_parameter_slot); + } + NativeSourceValue::ResidentView { source } => { + self.word(3); + self.loan(source); + } + NativeSourceValue::ResidentSumView { source } => { + self.word(4); + self.sum_loan(source); + } + } + } + match &source.result { + NativeSourceResult::Carrier { + shape, + caller_destination_frame, + caller_destination_slot, + callee_return_frame, + permitted_return_slots, + } => { + self.word(3); + self.word(*shape); + self.word(*caller_destination_frame); + self.word(*caller_destination_slot); + self.word(*callee_return_frame); + self.word(permitted_return_slots.len() as u32); + for slot in permitted_return_slots { + self.word(*slot); + } + } + NativeSourceResult::Ordinary { shape } => { + self.word(1); + self.word(*shape); + } + NativeSourceResult::Owned { + shape, + caller_destination_frame, + caller_destination_slot, + callee_return_frame, + permitted_return_slots, + } => { + self.word(2); + self.word(*shape); + self.word(*caller_destination_frame); + self.word(*caller_destination_slot); + self.word(*callee_return_frame); + self.word(permitted_return_slots.len() as u32); + for slot in permitted_return_slots { + self.word(*slot); + } + } + } + } + } + let mut out = Writer(Vec::new()); + out.0.extend_from_slice(MAGIC); + out.word(layout.version); + out.word(0); + out.0.extend_from_slice(&0u64.to_le_bytes()); + for count in [ + layout.kinds.len(), + layout.hooks.len(), + layout.signatures.len(), + layout.shapes.len(), + layout.frames.len(), + layout.slots.len(), + layout.operations.len(), + ] { + out.word(count as u32); + } + out.word(0); + for kind in &layout.kinds { + out.word(*kind); + } + for hook in &layout.hooks { + out.word(hook.ordinal); + out.word(hook.kind); + out.word(match hook.recipe { + NativeRecipe::NetworkFactory => 1, + NativeRecipe::NetworkBorrow => 2, + NativeRecipe::Finalize => 3, + }); + out.word(hook.signature); + } + for signature in &layout.signatures { + out.word(signature.ordinal); + out.word(signature.parameters.len() as u32); + out.word(signature.result); + for parameter in &signature.parameters { + out.word(parameter.shape); + out.word(match parameter.access { + NativeAccess::Owned => 1, + NativeAccess::View => 2, + }); + } + } + for (i, shape) in layout.shapes.iter().enumerate() { + out.word(i as u32); + match *shape { + NativeShape::Integer { bits, signed } => { + out.word(1); + out.word(bits); + out.word(u32::from(signed)); + } + NativeShape::Float { bits } => { + out.word(2); + out.word(bits); + } + NativeShape::Bool => out.word(3), + NativeShape::String => out.word(4), + NativeShape::Nothing => out.word(5), + NativeShape::Network => out.word(6), + NativeShape::Resource { kind } => { + out.word(7); + out.word(kind); + } + NativeShape::Optional { child } => { + out.word(8); + out.word(child); + } + NativeShape::Result { ok, fail } => { + out.word(9); + out.word(ok); + out.word(fail); + } + NativeShape::Carrier { node } => { + out.word(11); + out.word(node); + } + NativeShape::HookDescriptor { hook } => { + out.word(10); + out.word(hook); + } + } + } + for frame in &layout.frames { + out.word(frame.ordinal); + out.word(match frame.role { + NativeFrameRole::Scope => 1, + NativeFrameRole::Operation => 2, + NativeFrameRole::Return => 3, + }); + out.site(frame.site); + out.word(frame.signature); + out.word(frame.parents.len() as u32); + for parent in &frame.parents { + match *parent { + NativeParent::Root => { + out.word(0); + out.word(0); + } + NativeParent::Frame(index) => { + out.word(1); + out.word(index); + } + } + } + } + for slot in &layout.slots { + out.word(slot.ordinal); + out.word(slot.frame); + out.word(slot.shape); + out.word(slot.path.len() as u32); + for step in &slot.path { + out.word(match step { + NativePayloadStep::Some => 1, + NativePayloadStep::Ok => 2, + NativePayloadStep::Fail => 3, + }); + } + } + for operation in &layout.operations { + out.word(operation.ordinal); + out.site(operation.site); + out.operation(&operation.operation); + } + if layout.version == 3 { + out.0.extend_from_slice(&carriers::encode(&layout.carriers)); + } + let length = out.0.len() as u64; + out.0[16..24].copy_from_slice(&length.to_le_bytes()); + out.0 +} diff --git a/crates/jett_runtime/src/resource_custody/registration/wire/carriers.rs b/crates/jett_runtime/src/resource_custody/registration/wire/carriers.rs new file mode 100644 index 00000000..0fea1fe4 --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/registration/wire/carriers.rs @@ -0,0 +1,670 @@ +//! Explicit v3 suffix codec; old records are never reinterpreted. +use super::super::carriers::*; +use super::*; +const MAGIC: &[u8; 8] = b"JTCAR003"; +fn blob(r: &mut Reader<'_>) -> Result, ResourceLayoutError> { + let n = r.count()?; + let end = r + .position + .checked_add(n) + .ok_or(ResourceLayoutError::WireLimit)?; + let bytes = r + .bytes + .get(r.position..end) + .ok_or(ResourceLayoutError::Truncated)?; + let mut out = storage(n)?; + out.extend_from_slice(bytes); + r.position = end; + Ok(out) +} +fn ids(r: &mut Reader<'_>) -> Result, ResourceLayoutError> { + let n = r.count()?; + let mut v = storage(n)?; + for _ in 0..n { + v.push(r.word()?); + } + Ok(v) +} +fn fields(r: &mut Reader<'_>) -> Result, ResourceLayoutError> { + let n = r.count()?; + let mut v = storage(n)?; + for _ in 0..n { + v.push(NativeCarrierField { + name: blob(r)?, + node: r.word()?, + }); + } + Ok(v) +} +fn members(r: &mut Reader<'_>) -> Result, ResourceLayoutError> { + let n = r.count()?; + let mut v = storage(n)?; + for _ in 0..n { + v.push(NativeCarrierMember { + name: blob(r)?, + discriminant: i64::from_le_bytes(r.take()?), + fields: fields(r)?, + }); + } + Ok(v) +} +pub(super) fn loan(r: &mut Reader<'_>) -> Result { + Ok(match r.word()? { + 1 => NativeCarrierLoanSource::ExistingBorrow { + operation: r.word()?, + }, + 2 => NativeCarrierLoanSource::IncomingViewFormal { + scope: r.word()?, + parameter: r.word()?, + }, + _ => return Err(ResourceLayoutError::UnknownTag), + }) +} +fn source(r: &mut Reader<'_>) -> Result { + Ok(match r.word()? { + 1 => NativeCarrierSource::Slot(r.word()?), + 2 => NativeCarrierSource::Loan(loan(r)?), + _ => return Err(ResourceLayoutError::UnknownTag), + }) +} +fn selector(r: &mut Reader<'_>) -> Result { + Ok(match r.word()? { + 0 => NativeCarrierSelector::Static(r.word()?), + 1 => NativeCarrierSelector::Dynamic, + _ => return Err(ResourceLayoutError::UnknownTag), + }) +} +fn path(r: &mut Reader<'_>) -> Result, ResourceLayoutError> { + let n = r.count()?; + let mut v = storage(n)?; + for _ in 0..n { + v.push(match r.word()? { + 1 => NativeCarrierPath::Field(r.word()?), + 2 => NativeCarrierPath::EnumPayload { + variant: r.word()?, + field: r.word()?, + }, + 3 => NativeCarrierPath::MachinePayload { + state: r.word()?, + field: r.word()?, + }, + 4 => NativeCarrierPath::List(selector(r)?), + 5 => NativeCarrierPath::MapKey(selector(r)?), + 6 => NativeCarrierPath::MapValue(selector(r)?), + 7 => NativeCarrierPath::Some, + 8 => NativeCarrierPath::Ok, + 9 => NativeCarrierPath::Fail, + _ => return Err(ResourceLayoutError::UnknownTag), + }); + } + Ok(v) +} +fn flag(r: &mut Reader<'_>) -> Result { + match r.word()? { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(ResourceLayoutError::Reserved), + } +} +fn operation(r: &mut Reader<'_>) -> Result { + let tag = r.word()?; + let frame = r.word()?; + Ok(match tag { + 1 => { + let destination = r.word()?; + let selector = r.word()?; + let n = r.count()?; + let mut children = storage(n)?; + for _ in 0..n { + let index = r.word()?; + let node = r.word()?; + let source = match r.word()? { + 1 => NativeCarrierChildSource::Ordinary { shape: r.word()? }, + 2 => NativeCarrierChildSource::Move { slot: r.word()? }, + 3 => NativeCarrierChildSource::LeafSum { slot: r.word()? }, + 4 => NativeCarrierChildSource::LeafOwner { slot: r.word()? }, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + children.push(NativeCarrierChild { + index, + node, + source, + }); + } + NativeCarrierOperation::Construct { + frame, + destination, + selector, + children, + } + } + 2 => NativeCarrierOperation::Transfer { + frame, + source: r.word()?, + destination: r.word()?, + }, + 3 => NativeCarrierOperation::Borrow { + frame, + source: source(r)?, + path: path(r)?, + node: r.word()?, + lease_frame: r.word()?, + }, + 4 => NativeCarrierOperation::EndBorrow { + frame, + borrow: r.word()?, + }, + 5 => { + let source = source(r)?; + let path = path(r)?; + let observation = match r.word()? { + 1 => NativeCarrierObservation::Length, + 2 => NativeCarrierObservation::Tag, + 3 => NativeCarrierObservation::State, + 4 => NativeCarrierObservation::Ordinary { + shape: r.word()?, + clone: flag(r)?, + }, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + NativeCarrierOperation::Observe { + frame, + source, + path, + observation, + } + } + 6 => { + let source = r.word()?; + let path = path(r)?; + let destination = match r.word()? { + 1 => NativeCarrierDestination::Slot(r.word()?), + 2 => NativeCarrierDestination::Ordinary { shape: r.word()? }, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + NativeCarrierOperation::Extract { + frame, + source, + path, + destination, + } + } + 7 => NativeCarrierOperation::AdaptSum { + frame, + source: source(r)?, + path: path(r)?, + sum_slot: r.word()?, + lease_frame: r.word()?, + borrowed: flag(r)?, + }, + 8 => NativeCarrierOperation::Retire { + frame, + source: r.word()?, + }, + 9 => NativeCarrierOperation::PublishReturn { + frame, + source: r.word()?, + }, + 10 => NativeCarrierOperation::ResetIteration { + frame, + source: r.word()?, + carrier_binders: ids(r)?, + leaf_sum_binders: ids(r)?, + }, + 11 => NativeCarrierOperation::QualifyMachine { + frame, + source: r.word()?, + destination: r.word()?, + machine: r.word()?, + state: r.word()?, + }, + _ => return Err(ResourceLayoutError::UnknownTag), + }) +} +pub(super) fn decode(r: &mut Reader<'_>) -> Result { + if &r.take::<8>()? != MAGIC { + return Err(ResourceLayoutError::Header); + } + let ns = r.count()?; + let ss = r.count()?; + let os = r.count()?; + if r.word()? != 0 { + return Err(ResourceLayoutError::Reserved); + } + let mut nodes = storage(ns)?; + for i in 0..ns { + r.ordinal(i)?; + nodes.push(match r.word()? { + 1 => NativeCarrierNode::Ordinary { shape: r.word()? }, + 2 => NativeCarrierNode::Resource { kind: r.word()? }, + 3 => NativeCarrierNode::Optional { child: r.word()? }, + 4 => NativeCarrierNode::Result { + ok: r.word()?, + fail: r.word()?, + }, + 5 => NativeCarrierNode::List { element: r.word()? }, + 6 => NativeCarrierNode::Map { + key: r.word()?, + value: r.word()?, + }, + 7 => NativeCarrierNode::Struct { + identity: blob(r)?, + arguments: ids(r)?, + fields: fields(r)?, + }, + 8 => NativeCarrierNode::Enum { + identity: blob(r)?, + arguments: ids(r)?, + variants: members(r)?, + }, + 9 => { + let identity = blob(r)?; + let states = members(r)?; + let n = r.count()?; + let mut transitions = storage(n)?; + for _ in 0..n { + transitions.push((r.word()?, r.word()?)); + } + NativeCarrierNode::Machine { + identity, + states, + transitions, + } + } + 10 => NativeCarrierNode::MachineState { + machine: r.word()?, + state: r.word()?, + }, + 11 => NativeCarrierNode::Refinement { + identity: blob(r)?, + base: r.word()?, + predicate: blob(r)?, + }, + _ => return Err(ResourceLayoutError::UnknownTag), + }); + } + let mut slots = storage(ss)?; + for i in 0..ss { + r.ordinal(i)?; + slots.push(NativeCarrierSlot { + frame: r.word()?, + node: r.word()?, + }); + } + let mut operations = storage(os)?; + for i in 0..os { + r.ordinal(i)?; + operations.push(NativeCarrierOperationRecord { + site: r.site()?, + operation: operation(r)?, + }); + } + Ok(NativeCarrierLayout { + nodes, + slots, + operations, + }) +} +#[cfg(test)] +fn word(v: &mut Vec, n: u32) { + v.extend_from_slice(&n.to_le_bytes()); +} +#[cfg(test)] +fn blob_out(v: &mut Vec, b: &[u8]) { + word(v, b.len() as u32); + v.extend_from_slice(b); +} +#[cfg(test)] +fn ids_out(v: &mut Vec, ids: &[u32]) { + word(v, ids.len() as u32); + for n in ids { + word(v, *n); + } +} +#[cfg(test)] +fn fields_out(v: &mut Vec, fs: &[NativeCarrierField]) { + word(v, fs.len() as u32); + for f in fs { + blob_out(v, &f.name); + word(v, f.node); + } +} +#[cfg(test)] +fn members_out(v: &mut Vec, ms: &[NativeCarrierMember]) { + word(v, ms.len() as u32); + for m in ms { + blob_out(v, &m.name); + v.extend_from_slice(&m.discriminant.to_le_bytes()); + fields_out(v, &m.fields); + } +} +#[cfg(test)] +pub(super) fn loan_out(v: &mut Vec, s: NativeCarrierLoanSource) { + match s { + NativeCarrierLoanSource::ExistingBorrow { operation } => { + word(v, 1); + word(v, operation); + } + NativeCarrierLoanSource::IncomingViewFormal { scope, parameter } => { + word(v, 2); + word(v, scope); + word(v, parameter); + } + } +} +#[cfg(test)] +fn source_out(v: &mut Vec, s: NativeCarrierSource) { + match s { + NativeCarrierSource::Slot(slot) => { + word(v, 1); + word(v, slot); + } + NativeCarrierSource::Loan(s) => { + word(v, 2); + loan_out(v, s); + } + } +} +#[cfg(test)] +fn selector_out(v: &mut Vec, s: NativeCarrierSelector) { + match s { + NativeCarrierSelector::Static(i) => { + word(v, 0); + word(v, i); + } + NativeCarrierSelector::Dynamic => word(v, 1), + } +} +#[cfg(test)] +fn path_out(v: &mut Vec, ps: &[NativeCarrierPath]) { + word(v, ps.len() as u32); + for p in ps { + match *p { + NativeCarrierPath::Field(i) => { + word(v, 1); + word(v, i); + } + NativeCarrierPath::EnumPayload { variant, field } => { + word(v, 2); + word(v, variant); + word(v, field); + } + NativeCarrierPath::MachinePayload { state, field } => { + word(v, 3); + word(v, state); + word(v, field); + } + NativeCarrierPath::List(s) => { + word(v, 4); + selector_out(v, s); + } + NativeCarrierPath::MapKey(s) => { + word(v, 5); + selector_out(v, s); + } + NativeCarrierPath::MapValue(s) => { + word(v, 6); + selector_out(v, s); + } + NativeCarrierPath::Some => word(v, 7), + NativeCarrierPath::Ok => word(v, 8), + NativeCarrierPath::Fail => word(v, 9), + } + } +} +#[cfg(test)] +pub(super) fn encode(g: &NativeCarrierLayout) -> Vec { + let mut v = MAGIC.to_vec(); + for n in [g.nodes.len(), g.slots.len(), g.operations.len()] { + word(&mut v, n as u32); + } + word(&mut v, 0); + for (i, n) in g.nodes.iter().enumerate() { + word(&mut v, i as u32); + match n { + NativeCarrierNode::Ordinary { shape } => { + word(&mut v, 1); + word(&mut v, *shape); + } + NativeCarrierNode::Resource { kind } => { + word(&mut v, 2); + word(&mut v, *kind); + } + NativeCarrierNode::Optional { child } => { + word(&mut v, 3); + word(&mut v, *child); + } + NativeCarrierNode::Result { ok, fail } => { + word(&mut v, 4); + word(&mut v, *ok); + word(&mut v, *fail); + } + NativeCarrierNode::List { element } => { + word(&mut v, 5); + word(&mut v, *element); + } + NativeCarrierNode::Map { key, value } => { + word(&mut v, 6); + word(&mut v, *key); + word(&mut v, *value); + } + NativeCarrierNode::Struct { + identity, + arguments, + fields, + } => { + word(&mut v, 7); + blob_out(&mut v, identity); + ids_out(&mut v, arguments); + fields_out(&mut v, fields); + } + NativeCarrierNode::Enum { + identity, + arguments, + variants, + } => { + word(&mut v, 8); + blob_out(&mut v, identity); + ids_out(&mut v, arguments); + members_out(&mut v, variants); + } + NativeCarrierNode::Machine { + identity, + states, + transitions, + } => { + word(&mut v, 9); + blob_out(&mut v, identity); + members_out(&mut v, states); + word(&mut v, transitions.len() as u32); + for (a, b) in transitions { + word(&mut v, *a); + word(&mut v, *b); + } + } + NativeCarrierNode::MachineState { machine, state } => { + word(&mut v, 10); + word(&mut v, *machine); + word(&mut v, *state); + } + NativeCarrierNode::Refinement { + identity, + base, + predicate, + } => { + word(&mut v, 11); + blob_out(&mut v, identity); + word(&mut v, *base); + blob_out(&mut v, predicate); + } + } + } + for (i, s) in g.slots.iter().enumerate() { + word(&mut v, i as u32); + word(&mut v, s.frame); + word(&mut v, s.node); + } + for (i, row) in g.operations.iter().enumerate() { + word(&mut v, i as u32); + word(&mut v, row.site.function); + word(&mut v, row.site.block); + match row.site.position { + NativePosition::Statement(i) => { + word(&mut v, 0); + word(&mut v, i); + } + NativePosition::Terminator => { + word(&mut v, 1); + word(&mut v, 0); + } + } + let op = &row.operation; + let tag = match op { + NativeCarrierOperation::Construct { .. } => 1, + NativeCarrierOperation::Transfer { .. } => 2, + NativeCarrierOperation::Borrow { .. } => 3, + NativeCarrierOperation::EndBorrow { .. } => 4, + NativeCarrierOperation::Observe { .. } => 5, + NativeCarrierOperation::Extract { .. } => 6, + NativeCarrierOperation::AdaptSum { .. } => 7, + NativeCarrierOperation::Retire { .. } => 8, + NativeCarrierOperation::PublishReturn { .. } => 9, + NativeCarrierOperation::ResetIteration { .. } => 10, + NativeCarrierOperation::QualifyMachine { .. } => 11, + }; + word(&mut v, tag); + word(&mut v, op.frame()); + match op { + NativeCarrierOperation::Construct { + destination, + selector, + children, + .. + } => { + word(&mut v, *destination); + word(&mut v, *selector); + word(&mut v, children.len() as u32); + for c in children { + word(&mut v, c.index); + word(&mut v, c.node); + match c.source { + NativeCarrierChildSource::Ordinary { shape } => { + word(&mut v, 1); + word(&mut v, shape); + } + NativeCarrierChildSource::Move { slot } => { + word(&mut v, 2); + word(&mut v, slot); + } + NativeCarrierChildSource::LeafOwner { slot } => { + word(&mut v, 4); + word(&mut v, slot); + } + NativeCarrierChildSource::LeafSum { slot } => { + word(&mut v, 3); + word(&mut v, slot); + } + } + } + } + NativeCarrierOperation::Transfer { + source, + destination, + .. + } => { + word(&mut v, *source); + word(&mut v, *destination); + } + NativeCarrierOperation::Borrow { + source, + path, + node, + lease_frame, + .. + } => { + source_out(&mut v, *source); + path_out(&mut v, path); + word(&mut v, *node); + word(&mut v, *lease_frame); + } + NativeCarrierOperation::EndBorrow { borrow, .. } => word(&mut v, *borrow), + NativeCarrierOperation::Observe { + source, + path, + observation, + .. + } => { + source_out(&mut v, *source); + path_out(&mut v, path); + match observation { + NativeCarrierObservation::Length => word(&mut v, 1), + NativeCarrierObservation::Tag => word(&mut v, 2), + NativeCarrierObservation::State => word(&mut v, 3), + NativeCarrierObservation::Ordinary { shape, clone } => { + word(&mut v, 4); + word(&mut v, *shape); + word(&mut v, u32::from(*clone)); + } + } + } + NativeCarrierOperation::Extract { + source, + path, + destination, + .. + } => { + word(&mut v, *source); + path_out(&mut v, path); + match destination { + NativeCarrierDestination::Slot(s) => { + word(&mut v, 1); + word(&mut v, *s); + } + NativeCarrierDestination::Ordinary { shape } => { + word(&mut v, 2); + word(&mut v, *shape); + } + } + } + NativeCarrierOperation::AdaptSum { + source, + path, + sum_slot, + lease_frame, + borrowed, + .. + } => { + source_out(&mut v, *source); + path_out(&mut v, path); + word(&mut v, *sum_slot); + word(&mut v, *lease_frame); + word(&mut v, u32::from(*borrowed)); + } + NativeCarrierOperation::Retire { source, .. } + | NativeCarrierOperation::PublishReturn { source, .. } => word(&mut v, *source), + NativeCarrierOperation::ResetIteration { + source, + carrier_binders, + leaf_sum_binders, + .. + } => { + word(&mut v, *source); + ids_out(&mut v, carrier_binders); + ids_out(&mut v, leaf_sum_binders); + } + NativeCarrierOperation::QualifyMachine { + source, + destination, + machine, + state, + .. + } => { + word(&mut v, *source); + word(&mut v, *destination); + word(&mut v, *machine); + word(&mut v, *state); + } + } + } + v +} diff --git a/crates/jett_runtime/src/resource_custody/tests.rs b/crates/jett_runtime/src/resource_custody/tests.rs new file mode 100644 index 00000000..d5b6b6cd --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/tests.rs @@ -0,0 +1,830 @@ +use super::*; +use std::collections::HashSet; +use std::sync::Mutex; + +type Events = Arc>>; + +// Constructor-owned test registration only. No production issuer or provider. +fn registration( + registry: &ResourceRegistry, + kinds: Vec, + slots: Vec, +) -> Result { + let mut nominal = HashSet::new(); + if !kinds.iter().all(|kind| nominal.insert(*kind)) + || slots.iter().any(|kind| *kind >= kinds.len()) + { + return Err(CustodyError::WrongKind); + } + Ok(RegisteredResourceProgram(Arc::new(ProgramLayout { + context: registry.context_id, + kinds, + slots, + }))) +} + +fn test_program(registry: &ResourceRegistry) -> RegisteredResourceProgram { + registration( + registry, + vec![ResourceTypeId::new(100), ResourceTypeId::new(101)], + vec![0, 0, 1, 0], + ) + .unwrap() +} + +fn authority() -> AuthorityProvenance { + AuthorityProvenance::new(5, 7) +} + +fn destination( + core: &ResourceCustody, + program: &RegisteredResourceProgram, + frame: &ResourceFrameToken, + slot: usize, +) -> ResourceHolderToken { + core.holder(frame, &program.slot(slot).unwrap()).unwrap() +} + +fn acquire( + core: &mut ResourceCustody, + registry: &mut ResourceRegistry, + program: &RegisteredResourceProgram, + frame: &ResourceFrameToken, + slot: usize, + label: i64, + events: &Events, + panics: bool, +) -> OwnedResourceToken { + let holder = destination(core, program, frame, slot); + let kind = program.kind(program.0.slots[slot]).unwrap(); + let prepared = core.prepare_acquisition(&kind, &holder).unwrap(); + let events = events.clone(); + core.commit_acquisition(registry, prepared, authority(), label, move |value| { + events.lock().unwrap().push(value); + if panics { + panic!("selected custody finalizer"); + } + }) + .unwrap() +} + +fn clean(core: &ResourceCustody, registry: &ResourceRegistry) { + assert_eq!( + ( + core.live_owners(), + core.live_loans(), + core.active_frames(), + registry.live_count() + ), + (0, 0, 0, 0) + ); +} + +#[test] +fn custody_registration_joins_context_program_kind_and_slot_before_acquisition() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let foreign_registry = ResourceRegistry::new(); + assert!(matches!( + ResourceCustody::new(program.clone(), &foreign_registry), + Err(CustodyError::WrongContext) + )); + assert!(matches!( + registration(®istry, vec![ResourceTypeId::new(100); 2], vec![0]), + Err(CustodyError::WrongKind) + )); + assert!(matches!( + registration(®istry, vec![ResourceTypeId::new(100)], vec![1]), + Err(CustodyError::WrongKind) + )); + let other = registration(®istry, program.0.kinds.clone(), program.0.slots.clone()).unwrap(); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let holder = destination(&core, &program, &frame, 0); + assert!(matches!( + core.prepare_acquisition(&other.kind(0).unwrap(), &holder), + Err(CustodyError::WrongProgram) + )); + assert!(matches!( + core.holder(&frame, &other.slot(0).unwrap()), + Err(CustodyError::WrongProgram) + )); + assert!(matches!( + core.prepare_acquisition(&program.kind(1).unwrap(), &holder), + Err(CustodyError::WrongKind) + )); + assert!(matches!( + program.kind(usize::MAX), + Err(CustodyError::WrongKind) + )); + assert!(matches!( + program.slot(usize::MAX), + Err(CustodyError::UnknownSlot) + )); + assert_eq!((core.live_owners(), registry.live_count()), (0, 0)); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); + clean(&core, ®istry); +} + +#[test] +fn custody_registry_nominal_key_generation_and_provenance_remain_exact() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let mut owner = acquire( + &mut core, + &mut registry, + &program, + &frame, + 0, + 1, + &events, + false, + ); + let key = core.validate_owned(&owner, ®istry).unwrap(); + let other_registry = ResourceRegistry::new(); + assert_eq!( + core.validate_owned(&owner, &other_registry), + Err(CustodyError::WrongContext) + ); + core.owners[owner.owner].key.resource_type = ResourceTypeId::new(101); + assert_eq!( + core.validate_owned(&owner, ®istry), + Err(CustodyError::Registry(RegistryError::WrongType)) + ); + core.owners[owner.owner].key = key; + core.owners[owner.owner].authority = AuthorityProvenance::new(5, 99); + assert_eq!( + core.close(&mut owner, &mut registry).failure(), + Some(ResourceCleanupFailure::Custody(CustodyError::Registry( + RegistryError::AuthorityMismatch + ))) + ); + assert_eq!((core.live_owners(), registry.live_count()), (1, 1)); + assert!(events.lock().unwrap().is_empty()); + core.owners[owner.owner].authority = authority(); + core.owners[owner.owner].key.generation += 1; + assert_eq!( + core.validate_owned(&owner, ®istry), + Err(CustodyError::Registry(RegistryError::StaleGeneration)) + ); + core.owners[owner.owner].key = key; + assert_eq!(core.close(&mut owner, &mut registry).failure(), None); + assert_eq!( + core.close(&mut owner, &mut registry).failure(), + Some(ResourceCleanupFailure::Custody(CustodyError::InvalidOwner)) + ); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [1]); + clean(&core, ®istry); +} + +#[test] +fn custody_returned_holder_generation_controls_reverse_current_acquisition() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let outer = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let original = destination(&core, &program, &outer, 0); + let mut owner = acquire( + &mut core, + &mut registry, + &program, + &outer, + 0, + 1, + &events, + false, + ); + let stale = OwnedResourceToken { + core: owner.core, + owner: owner.owner, + holder_generation: owner.holder_generation, + }; + let _second = acquire( + &mut core, + &mut registry, + &program, + &outer, + 1, + 2, + &events, + false, + ); + let inner = core + .begin_frame(ResourceFrameKind::Return, ResourcePurpose::Runtime) + .unwrap(); + let returned = destination(&core, &program, &inner, 0); + core.transfer(&mut owner, &returned, ®istry).unwrap(); + assert_eq!( + core.validate_owned(&stale, ®istry), + Err(CustodyError::StaleHolderGeneration) + ); + core.transfer(&mut owner, &original, ®istry).unwrap(); + assert_eq!(core.end_frame(&inner, &mut registry).failure(), None); + assert!(events.lock().unwrap().is_empty()); + assert_eq!(core.end_frame(&outer, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [1, 2]); + assert_eq!( + core.validate_owned(&owner, ®istry), + Err(CustodyError::InvalidOwner) + ); + clean(&core, ®istry); +} + +#[test] +fn custody_live_lease_blocks_move_close_and_older_frame_borrow_before_effect() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let scope = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let operation = core + .begin_frame(ResourceFrameKind::Operation, ResourcePurpose::Runtime) + .unwrap(); + let mut owner = acquire( + &mut core, + &mut registry, + &program, + &operation, + 0, + 3, + &events, + false, + ); + assert!(matches!( + core.begin_borrow(&owner, &scope, ®istry), + Err(CustodyError::InvalidLoan) + )); + let mut loan = core.begin_borrow(&owner, &operation, ®istry).unwrap(); + let destination = destination(&core, &program, &scope, 0); + assert_eq!( + core.transfer(&mut owner, &destination, ®istry), + Err(CustodyError::ActiveBorrow) + ); + assert_eq!( + core.close(&mut owner, &mut registry).failure(), + Some(ResourceCleanupFailure::Custody(CustodyError::ActiveBorrow)) + ); + assert_eq!( + core.validate_borrowed(&loan, ®istry), + core.validate_owned(&owner, ®istry) + ); + assert!(events.lock().unwrap().is_empty()); + core.end_borrow(&mut loan).unwrap(); + assert_eq!(core.end_borrow(&mut loan), Err(CustodyError::InvalidLoan)); + core.transfer(&mut owner, &destination, ®istry).unwrap(); + assert_eq!( + core.validate_borrowed(&loan, ®istry), + Err(CustodyError::InvalidLoan) + ); + assert_eq!(core.end_frame(&operation, &mut registry).failure(), None); + assert_eq!(core.close(&mut owner, &mut registry).failure(), None); + assert_eq!(core.end_frame(&scope, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [3]); + clean(&core, ®istry); +} + +#[test] +fn custody_chronological_scope_operation_unwind_releases_callee_loan_first() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let caller = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let _caller_owner = acquire( + &mut core, + &mut registry, + &program, + &caller, + 0, + 1, + &events, + false, + ); + let operation = core + .begin_frame(ResourceFrameKind::Operation, ResourcePurpose::Runtime) + .unwrap(); + let operation_owner = acquire( + &mut core, + &mut registry, + &program, + &operation, + 0, + 2, + &events, + false, + ); + let callee = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let loan = core + .begin_borrow(&operation_owner, &callee, ®istry) + .unwrap(); + let _callee_owner = acquire( + &mut core, + &mut registry, + &program, + &callee, + 0, + 3, + &events, + true, + ); + assert_eq!( + core.end_frame(&operation, &mut registry).failure(), + Some(ResourceCleanupFailure::Custody( + CustodyError::OutOfOrderFrame + )) + ); + assert!(events.lock().unwrap().is_empty()); + let body = ResourceBodyOutcome::<(), &str>::HostPanic; + assert_eq!( + complete_resource(body, core.unwind_all(&mut registry)), + Err(ResourceCompletionFailure::Cleanup( + ResourceCleanupFailure::FinalizerPanic + )) + ); + assert_eq!(*events.lock().unwrap(), [3, 2, 1]); + assert_eq!( + core.validate_borrowed(&loan, ®istry), + Err(CustodyError::InvalidLoan) + ); + clean(&core, ®istry); +} + +#[test] +fn custody_required_purpose_cannot_be_overridden_by_nested_runtime_frame() { + for purpose in [ + ResourcePurpose::NamespaceConstant, + ResourcePurpose::ExplicitComptime, + ResourcePurpose::Verify, + ResourcePurpose::Property, + ] { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core.begin_frame(ResourceFrameKind::Scope, purpose).unwrap(); + let holder = destination(&core, &program, &frame, 0); + assert!(matches!( + core.prepare_acquisition(&program.kind(0).unwrap(), &holder), + Err(CustodyError::WrongPurpose) + )); + assert!(matches!( + core.begin_frame(ResourceFrameKind::Operation, ResourcePurpose::Runtime), + Err(CustodyError::WrongPurpose) + )); + assert_eq!( + ( + core.live_owners(), + registry.live_count(), + core.active_frames() + ), + (0, 0, 1) + ); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); + clean(&core, ®istry); + } +} + +#[test] +fn custody_failed_prepared_publication_cleans_only_the_unpublished_payload() { + for panics in [false, true] { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let holder = destination(&core, &program, &frame, 0); + let preparation = core + .prepare_acquisition(&program.kind(0).unwrap(), &holder) + .unwrap(); + let _original = acquire( + &mut core, + &mut registry, + &program, + &frame, + 0, + 1, + &events, + false, + ); + let observed = events.clone(); + let failure = core + .commit_acquisition( + &mut registry, + preparation, + authority(), + 99_i64, + move |payload| { + observed.lock().unwrap().push(payload); + if panics { + panic!("unpublished payload finalizer"); + } + }, + ) + .unwrap_err(); + assert_eq!( + failure, + if panics { + ResourceTransitionFailure::Cleanup(ResourceCleanupFailure::FinalizerPanic) + } else { + ResourceTransitionFailure::Rejected(CustodyError::OccupiedHolder) + } + ); + assert_eq!((core.live_owners(), registry.live_count()), (1, 1)); + assert_eq!(*events.lock().unwrap(), [99]); + assert_eq!(core.unwind_all(&mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [99, 1]); + clean(&core, ®istry); + } +} + +#[test] +fn custody_dropped_carrier_token_and_failed_caller_publication_do_not_lose_cleanup() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Operation, ResourcePurpose::Runtime) + .unwrap(); + let owner = acquire( + &mut core, + &mut registry, + &program, + &frame, + 0, + 7, + &events, + false, + ); + let physical = core.validate_owned(&owner, ®istry).unwrap(); + let physical_copy = physical; + assert_eq!(physical, physical_copy); + drop(owner); + assert!(events.lock().unwrap().is_empty()); + assert_eq!((core.live_owners(), registry.live_count()), (1, 1)); + assert!(matches!( + ResourceCustody::new(program.clone(), ®istry), + Err(CustodyError::UnownedRegistry) + )); + let body = ResourceBodyOutcome::<(), &str>::Completed(Err("caller publication failed")); + assert_eq!( + complete_resource(body, core.unwind_all(&mut registry)), + Err(ResourceCompletionFailure::Body("caller publication failed")) + ); + assert_eq!(*events.lock().unwrap(), [7]); + clean(&core, ®istry); +} + +#[test] +fn custody_repeated_entry_preserves_identity_and_rejects_stale_frames_and_tokens() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let id = core.id; + for label in 1..=3 { + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let owner = acquire( + &mut core, + &mut registry, + &program, + &frame, + 0, + label, + &events, + label == 2, + ); + let cleanup = core.unwind_all(&mut registry); + assert_eq!( + cleanup.failure(), + if label == 2 { + Some(ResourceCleanupFailure::FinalizerPanic) + } else { + None + } + ); + assert_eq!( + core.end_frame(&frame, &mut registry).failure(), + Some(ResourceCleanupFailure::Custody(CustodyError::InvalidFrame)) + ); + assert_eq!( + core.validate_owned(&owner, ®istry), + Err(CustodyError::InvalidOwner) + ); + assert_eq!(core.id, id); + clean(&core, ®istry); + } + assert_eq!(*events.lock().unwrap(), [1, 2, 3]); +} + +#[test] +fn custody_counter_exhaustion_refuses_transfer_without_reassigning_holder() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let mut owner = acquire( + &mut core, + &mut registry, + &program, + &frame, + 0, + 1, + &events, + false, + ); + core.owners[owner.owner].generation = u64::MAX; + owner.holder_generation = u64::MAX; + let before = core.owners[owner.owner].holder; + let log = core.frames[frame.index].acquisitions.len(); + let destination = destination(&core, &program, &frame, 1); + assert_eq!( + core.transfer(&mut owner, &destination, ®istry), + Err(CustodyError::CapacityExhausted) + ); + assert_eq!(core.owners[owner.owner].holder, before); + assert_eq!(core.frames[frame.index].acquisitions.len(), log); + assert!(events.lock().unwrap().is_empty()); + assert_eq!(core.close(&mut owner, &mut registry).failure(), None); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); + clean(&core, ®istry); +} + +#[test] +fn custody_registry_shutdown_and_postinsert_cleanup_failures_remain_separate() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let holder = destination(&core, &program, &frame, 0); + let prepared = core + .prepare_acquisition(&program.kind(0).unwrap(), &holder) + .unwrap(); + registry.shutdown(); + let observed = events.clone(); + assert_eq!( + core.commit_acquisition(&mut registry, prepared, authority(), 8_i64, move |value| { + observed.lock().unwrap().push(value) + }) + .unwrap_err(), + ResourceTransitionFailure::Rejected(CustodyError::Registry(RegistryError::ShuttingDown)) + ); + assert_eq!(*events.lock().unwrap(), [8]); + assert_eq!(core.unwind_all(&mut registry).failure(), None); + clean(&core, ®istry); + + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let first = acquire( + &mut core, + &mut registry, + &program, + &frame, + 0, + 1, + &events, + false, + ); + let _second = acquire( + &mut core, + &mut registry, + &program, + &frame, + 1, + 2, + &events, + false, + ); + let key = core.validate_owned(&first, ®istry).unwrap(); + registry.close(key, ResourceTypeId::new(100)).unwrap(); + let outcome = core.unwind_all(&mut registry); + assert!(matches!( + outcome.failure(), + Some(ResourceCleanupFailure::Custody(CustodyError::Registry( + RegistryError::StaleGeneration + ))) + )); + assert_eq!(*events.lock().unwrap(), [1, 2]); + clean(&core, ®istry); +} + +#[test] +fn custody_completion_has_cleanup_first_matrix_without_changing_domain_values() { + for cleanup in [ + None, + Some(ResourceCleanupFailure::Custody(CustodyError::InvalidFrame)), + Some(ResourceCleanupFailure::FinalizerPanic), + ] { + for body in [ + ResourceBodyOutcome::Completed(Ok(7_u8)), + ResourceBodyOutcome::Completed(Err("entry")), + ResourceBodyOutcome::HostPanic, + ] { + let expected = match cleanup { + Some(failure) => Err(ResourceCompletionFailure::Cleanup(failure)), + None => match &body { + ResourceBodyOutcome::Completed(Ok(value)) => Ok(*value), + ResourceBodyOutcome::Completed(Err(error)) => { + Err(ResourceCompletionFailure::Body(*error)) + } + ResourceBodyOutcome::HostPanic => Err(ResourceCompletionFailure::BodyPanic), + }, + }; + assert_eq!( + complete_resource(body, ResourceCleanupOutcome { first: cleanup }), + expected + ); + } + } + let domain = ResourceBodyOutcome::, &str>::Completed(Ok(Err("domain"))); + assert_eq!( + complete_resource(domain, ResourceCleanupOutcome::default()), + Ok(Err("domain")) + ); + let caught = ResourceBodyOutcome::<(), &str>::caught(catch_unwind(AssertUnwindSafe( + || -> Result<(), &str> { + panic!("body panic"); + }, + ))); + assert_eq!( + complete_resource(caught, ResourceCleanupOutcome::default()), + Err(ResourceCompletionFailure::BodyPanic) + ); + let mut first = ResourceCleanupOutcome { + first: Some(ResourceCleanupFailure::Custody(CustodyError::InvalidFrame)), + }; + first.merge(ResourceCleanupOutcome { + first: Some(ResourceCleanupFailure::FinalizerPanic), + }); + assert_eq!( + first.failure(), + Some(ResourceCleanupFailure::Custody(CustodyError::InvalidFrame)) + ); +} + +#[test] +fn custody_failed_provenance_cleanup_preserves_unretired_frame_and_continues_eligible_owners() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let first = acquire( + &mut core, + &mut registry, + &program, + &frame, + 0, + 1, + &events, + false, + ); + let _second = acquire( + &mut core, + &mut registry, + &program, + &frame, + 1, + 2, + &events, + false, + ); + core.owners[first.owner].authority = AuthorityProvenance::new(5, 99); + assert_eq!( + core.end_frame(&frame, &mut registry).failure(), + Some(ResourceCleanupFailure::Custody(CustodyError::Registry( + RegistryError::AuthorityMismatch + ))) + ); + assert_eq!(*events.lock().unwrap(), [2]); + assert_eq!( + ( + core.live_owners(), + registry.live_count(), + core.active_frames() + ), + (1, 1, 1) + ); + core.owners[first.owner].authority = authority(); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [2, 1]); + clean(&core, ®istry); +} + +#[path = "tests/source_batch.rs"] +mod source_batch; + +#[test] +fn custody_replacement_preflight_generation_capacity_refuses_before_old_finalization() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let scope = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let mut old = acquire( + &mut core, + &mut registry, + &program, + &scope, + 0, + 61, + &events, + false, + ); + let operation = core + .begin_frame(ResourceFrameKind::Operation, ResourcePurpose::Runtime) + .unwrap(); + let mut replacement = acquire( + &mut core, + &mut registry, + &program, + &operation, + 1, + 62, + &events, + false, + ); + let holder = destination(&core, &program, &scope, 0); + let old_key = core.validate_owned(&old, ®istry).unwrap(); + let rhs_key = core.validate_owned(&replacement, ®istry).unwrap(); + let before_holder = core.owners[replacement.owner].holder; + let before_generation = replacement.holder_generation; + let before_logs = ( + core.frames[scope.index].acquisitions.len(), + core.frames[operation.index].acquisitions.len(), + ); + // Deterministic private capacity fault, not an allocation/global-ID claim. + core.owners[replacement.owner].generation = u64::MAX; + replacement.holder_generation = u64::MAX; + assert_eq!(core.validate_owned(&replacement, ®istry), Ok(rhs_key)); + assert_eq!( + core.preflight_replacement(&old, &replacement, &holder, ®istry), + Err(CustodyError::CapacityExhausted) + ); + assert_eq!(core.validate_owned(&old, ®istry), Ok(old_key)); + assert_eq!(core.validate_owned(&replacement, ®istry), Ok(rhs_key)); + assert_eq!((core.live_owners(), registry.live_count()), (2, 2)); + assert_eq!(core.owners[replacement.owner].holder, before_holder); + assert_eq!(replacement.holder_generation, u64::MAX); + assert_eq!( + ( + core.frames[scope.index].acquisitions.len(), + core.frames[operation.index].acquisitions.len() + ), + before_logs + ); + assert!(events.lock().unwrap().is_empty()); + core.owners[replacement.owner].generation = before_generation; + replacement.holder_generation = before_generation; + core.preflight_replacement(&old, &replacement, &holder, ®istry) + .unwrap(); + assert!(events.lock().unwrap().is_empty()); + assert_eq!(core.close(&mut old, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [61]); + core.transfer(&mut replacement, &holder, ®istry).unwrap(); + assert_eq!(core.validate_owned(&replacement, ®istry), Ok(rhs_key)); + assert_eq!(replacement.holder_generation, before_generation + 1); + assert_eq!(core.end_frame(&operation, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [61]); + assert_eq!(core.end_frame(&scope, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [61, 62]); + clean(&core, ®istry); +} + +#[path = "carrier_holder_tests.rs"] +mod carrier_holder_tests; diff --git a/crates/jett_runtime/src/resource_custody/tests/source_batch.rs b/crates/jett_runtime/src/resource_custody/tests/source_batch.rs new file mode 100644 index 00000000..f4427278 --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/tests/source_batch.rs @@ -0,0 +1,118 @@ +use super::*; + +#[test] +fn custody_source_batch_late_active_borrow_refuses_without_moving_a_prefix() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let events = Arc::new(Mutex::new(Vec::new())); + let parent = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let mut first = acquire( + &mut core, + &mut registry, + &program, + &parent, + 0, + 501, + &events, + false, + ); + let mut second = acquire( + &mut core, + &mut registry, + &program, + &parent, + 1, + 502, + &events, + false, + ); + let mut loan = core.begin_borrow(&second, &parent, ®istry).unwrap(); + let callee = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let slot0 = program.slot(0).unwrap(); + let slot1 = program.slot(1).unwrap(); + assert_eq!( + core.preflight_transfer_batch(&callee, &[(&first, &slot0), (&second, &slot1)], ®istry), + Err(CustodyError::ActiveBorrow) + ); + assert_eq!( + ( + core.owners[first.owner].holder.frame, + core.owners[second.owner].holder.frame + ), + (parent.index, parent.index) + ); + assert!(core.frames[callee.index].acquisitions.is_empty()); + assert!(events.lock().unwrap().is_empty()); + core.end_borrow(&mut loan).unwrap(); + core.preflight_transfer_batch(&callee, &[(&first, &slot0), (&second, &slot1)], ®istry) + .unwrap(); + assert!(core.frames[callee.index].acquisitions.capacity() >= 2); + let holder0 = core.holder(&callee, &slot0).unwrap(); + let holder1 = core.holder(&callee, &slot1).unwrap(); + core.transfer(&mut first, &holder0, ®istry).unwrap(); + core.transfer(&mut second, &holder1, ®istry).unwrap(); + assert!(core.end_frame(&callee, &mut registry).failure().is_none()); + assert!(core.end_frame(&parent, &mut registry).failure().is_none()); + clean(&core, ®istry); + assert_eq!(*events.lock().unwrap(), vec![502, 501]); +} + +#[test] +fn custody_source_batch_late_generation_overflow_and_duplicate_owner_refuse_before_prefix() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let events = Arc::new(Mutex::new(Vec::new())); + let parent = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let first = acquire( + &mut core, + &mut registry, + &program, + &parent, + 0, + 501, + &events, + false, + ); + let mut second = acquire( + &mut core, + &mut registry, + &program, + &parent, + 1, + 502, + &events, + false, + ); + let callee = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let slot0 = program.slot(0).unwrap(); + let slot1 = program.slot(1).unwrap(); + assert_eq!( + core.preflight_transfer_batch(&callee, &[(&first, &slot0), (&first, &slot1)], ®istry), + Err(CustodyError::OccupiedHolder) + ); + core.owners[second.owner].generation = u64::MAX; + second.holder_generation = u64::MAX; + assert_eq!( + core.preflight_transfer_batch(&callee, &[(&first, &slot0), (&second, &slot1)], ®istry), + Err(CustodyError::CapacityExhausted) + ); + assert_eq!(core.owners[first.owner].holder.frame, parent.index); + assert_eq!(first.holder_generation, 1); + assert!(core.frames[callee.index].acquisitions.is_empty()); + assert!(events.lock().unwrap().is_empty()); + core.owners[second.owner].generation = 1; + second.holder_generation = 1; + assert!(core.unwind_all(&mut registry).failure().is_none()); + clean(&core, ®istry); + assert_eq!(*events.lock().unwrap(), vec![502, 501]); +} diff --git a/crates/jett_typecheck/src/checker.rs b/crates/jett_typecheck/src/checker.rs index 0a1bf97a..19de13d7 100644 --- a/crates/jett_typecheck/src/checker.rs +++ b/crates/jett_typecheck/src/checker.rs @@ -269,6 +269,8 @@ pub struct CheckedStructConstruction { /// The result of type checking. #[derive(Debug)] pub struct CheckResult { + // Original checked implementation identities for readonly conversion admission. + ordinary_interface_implementations: HashSet<(TypeId, TypeId)>, pub release: bool, /// Source bindings unavailable for observation after checked consumption. pub breakpoint_exclusions: HashMap>, @@ -331,6 +333,105 @@ pub struct CheckResult { pub reflection_metadata: Arc, } +impl CheckResult { + /// The ordinary checked conversion relation; this grants no new return proof. + pub fn ordinary_types_compatible(&self, expected: TypeId, got: TypeId) -> bool { + ordinary_types_compatible( + &self.interner, + &|pair| self.ordinary_interface_implementations.contains(pair), + expected, + got, + ) + } + + /// Contravariant input admission, retaining exact view and capability types. + /// Return strengthening is independently selected by the reflected plan. + pub fn ordinary_callable_parameters_compatible( + &self, + actual: TypeId, + requested: TypeId, + ) -> bool { + if actual.index() as usize >= self.interner.len() + || requested.index() as usize >= self.interner.len() + { + return false; + } + let ( + Type::Function { + params: source, + view_params: source_views, + .. + }, + Type::Function { + params: target, + view_params: target_views, + .. + }, + ) = ( + self.interner.resolve(actual), + self.interner.resolve(requested), + ) + else { + return false; + }; + source.len() == target.len() + && source_views == target_views + && source + .iter() + .zip(target) + .all(|(source, target)| self.ordinary_types_compatible(*source, *target)) + } +} + +fn ordinary_types_compatible( + types: &TypeInterner, + has_implementation: &impl Fn(&(TypeId, TypeId)) -> bool, + expected: TypeId, + got: TypeId, +) -> bool { + if expected.index() as usize >= types.len() || got.index() as usize >= types.len() { + return false; + } + if expected == got + || expected == TypeInterner::ERROR + || got == TypeInterner::ERROR + || got == TypeInterner::NEVER + { + return true; + } + let compatible = |a, b| ordinary_types_compatible(types, has_implementation, a, b); + match (types.resolve(expected), types.resolve(got)) { + (Type::Interface(_), Type::Interface(_)) => expected == got, + (Type::Interface(_), _) => has_implementation(&(expected, got)), + (Type::Secret(a), Type::Secret(b)) => compatible(*a, *b), + (Type::Secret(a), _) => compatible(*a, got), + (Type::List(a), Type::List(b)) + | (Type::Optional(a), Type::Optional(b)) + | (Type::Set(a), Type::Set(b)) => compatible(*a, *b), + (Type::Map(a, b), Type::Map(c, d)) | (Type::Result(a, b), Type::Result(c, d)) => { + compatible(*a, *c) && compatible(*b, *d) + } + ( + Type::Function { + params: a, + view_params: av, + return_type: ar, + }, + Type::Function { + params: b, + view_params: bv, + return_type: br, + }, + ) => { + a.len() == b.len() + && av == bv + && a.iter().zip(b).all(|(a, b)| compatible(*b, *a)) + && compatible(*ar, *br) + } + (Type::Machine(a), Type::MachineState { machine: b, .. }) => a == b, + _ => false, + } +} /// Mode-specific policy applied while type checking. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct CheckOptions { @@ -471,6 +572,7 @@ fn finish_check( .collect(); CheckResult { + ordinary_interface_implementations: checker.interface_impls.keys().copied().collect(), release: options.release, breakpoint_exclusions, diagnostics, @@ -3813,66 +3915,13 @@ impl<'a> TypeChecker<'a> { } fn types_compatible(&self, expected: TypeId, got: TypeId) -> bool { - // Bottom flows into a contextual type, but an inhabited value cannot - // flow into bottom. Otherwise a default, call or constructor can create - // a runtime payload in a checked `never` place. - if expected == got - || expected == TypeInterner::ERROR - || got == TypeInterner::ERROR - || got == TypeInterner::NEVER - { - return true; - } - - match (self.interner.resolve(expected), self.interner.resolve(got)) { - (Type::Interface(_), Type::Interface(_)) => expected == got, - (Type::Interface(_), _) => self.interface_impls.contains_key(&(expected, got)), - (Type::Secret(expected_inner), Type::Secret(got_inner)) => { - self.types_compatible(*expected_inner, *got_inner) - } - (Type::Secret(expected_inner), _) => self.types_compatible(*expected_inner, got), - (Type::List(expected_inner), Type::List(got_inner)) - | (Type::Optional(expected_inner), Type::Optional(got_inner)) => { - self.types_compatible(*expected_inner, *got_inner) - } - (Type::Set(expected_inner), Type::Set(got_inner)) => { - self.types_compatible(*expected_inner, *got_inner) - } - (Type::Map(expected_key, expected_val), Type::Map(got_key, got_val)) - | (Type::Result(expected_key, expected_val), Type::Result(got_key, got_val)) => { - self.types_compatible(*expected_key, *got_key) - && self.types_compatible(*expected_val, *got_val) - } - ( - Type::Function { - params: expected_params, - view_params: expected_views, - return_type: expected_return, - }, - Type::Function { - params: got_params, - view_params: got_views, - return_type: got_return, - }, - ) => { - expected_params.len() == got_params.len() - && expected_views == got_views - && expected_params - .iter() - .zip(got_params.iter()) - .all(|(expected, got)| self.types_compatible(*got, *expected)) - && self.types_compatible(*expected_return, *got_return) - } - // This compatibility is intentionally one-way. Crossing a bare - // machine boundary erases exact state; recovering it requires an - // explicit `at` guard rather than remembered caller provenance. - (Type::Machine(expected_machine), Type::MachineState { machine, .. }) => { - expected_machine == machine - } - _ => false, - } + ordinary_types_compatible( + &self.interner, + &|pair| self.interface_impls.contains_key(pair), + expected, + got, + ) } - /// Fill only absent inferred slots. Existing concrete widths and nominal /// owners must match; this is not a promotion or refinement coarsening. /// Function parameters reverse the direction because callable substitution @@ -10412,13 +10461,183 @@ impl<'a> TypeChecker<'a> { if !explicit_view && matches!(value, Expr::Clone(_, _) | Expr::FieldAccess(_, _, _)) { return CheckedBindingMode::Owned; } - let source = Self::assignment_root(value) + let source = self + .borrowed_handle_binding_root(&declaration.value, initializer_type) + .or_else(|| Self::assignment_root(value)) .and_then(|ident| self.ident_def_id(ident)) .map(CheckedViewSource::Binding) .unwrap_or(CheckedViewSource::Other); CheckedBindingMode::View { source } } + /// A borrowed sum payload retains the exact whole binding as its backing. + /// This is provenance only; HIR still checks the complete typed projection. + fn borrowed_handle_binding_root<'source>( + &self, + initializer: &'source Expr, + output: TypeId, + ) -> Option<&'source ast::Ident> { + let Expr::View(value, _) = Self::unparenthesized(initializer) else { + return None; + }; + let Expr::Handle(target, error, failure, _) = Self::unparenthesized(value) else { + return None; + }; + if Self::handle_body_has_default(failure) { + return None; + } + let target_ty = self.checked_expression_type(target.span())?; + match self.interner.resolve(target_ty) { + Type::Optional(payload) if *payload == output && error.is_none() => {} + Type::Result(payload, _) if *payload == output && error.is_some() => {} + _ => return None, + } + let Expr::View(backing, _) = Self::unparenthesized(target) else { + return None; + }; + let Expr::Ident(binding) = Self::unparenthesized(backing) else { + return None; + }; + let definition = self.ident_def_id(binding)?; + if !matches!( + self.resolve.scope_table.def(definition).kind, + DefKind::Variable | DefKind::Param + ) || self.checked_expression_type(backing.span()) != Some(target_ty) + { + return None; + } + Some(binding) + } + + /// Defaults in nested Handle bodies yield to that nested Handle, whereas + /// defaults anywhere in this lexical body can yield an alternate payload. + fn handle_body_has_default(body: &Block) -> bool { + body.stmts.iter().any(|statement| match statement { + Stmt::VarDecl(value) => Self::handle_expression_has_default(&value.value), + Stmt::Assign(value) => { + Self::handle_expression_has_default(&value.target) + || Self::handle_expression_has_default(&value.value) + } + Stmt::Return(value) => value + .value + .as_ref() + .is_some_and(Self::handle_expression_has_default), + Stmt::Respond(value) => Self::handle_expression_has_default(&value.value), + Stmt::ComptimeTypeBind(value) => { + Self::handle_expression_has_default(&value.value) + || Self::handle_body_has_default(&value.body) + } + Stmt::If(value) => { + Self::handle_expression_has_default(&value.condition) + || Self::handle_body_has_default(&value.then_block) + || value.else_ifs.iter().any(|(condition, body)| { + Self::handle_expression_has_default(condition) + || Self::handle_body_has_default(body) + }) + || value + .else_block + .as_ref() + .is_some_and(Self::handle_body_has_default) + } + Stmt::For(value) => { + Self::handle_expression_has_default(&value.iterable) + || Self::handle_body_has_default(&value.body) + } + Stmt::While(value) => { + Self::handle_expression_has_default(&value.condition) + || Self::handle_body_has_default(&value.body) + } + Stmt::Match(value) => { + Self::handle_expression_has_default(&value.expr) + || value + .arms + .iter() + .any(|arm| Self::handle_body_has_default(&arm.body)) + } + Stmt::Expr(value) => Self::handle_expression_has_default(&value.expr), + Stmt::Assert(value) => { + Self::handle_expression_has_default(&value.condition) + || value + .message + .as_ref() + .is_some_and(Self::handle_expression_has_default) + } + Stmt::Breakpoint(value) => value + .condition + .as_ref() + .is_some_and(Self::handle_expression_has_default), + Stmt::Use(_) | Stmt::Trace(_) | Stmt::Break(_) | Stmt::Continue(_) => false, + }) + } + + fn handle_expression_has_default(value: &Expr) -> bool { + match value { + Expr::Default(_, _) => true, + // Evaluating a nested Handle's target still belongs to this body; + // its failure body has its own nearest handler. + Expr::Handle(target, _, _, _) => Self::handle_expression_has_default(target), + Expr::Binary(left, _, right, _) => { + Self::handle_expression_has_default(left) + || Self::handle_expression_has_default(right) + } + Expr::Unary(_, value, _) + | Expr::FieldAccess(value, _, _) + | Expr::Paren(value, _) + | Expr::View(value, _) + | Expr::Comptime(value, _) + | Expr::Ok(value, _) + | Expr::Fail(value, _) + | Expr::Some(value, _) + | Expr::Declassify(value, _) + | Expr::Coarsen(value, _) + | Expr::At(value, _, _) + | Expr::Spawn(value, _) + | Expr::Send(value, _) + | Expr::Ask(value, _) + | Expr::Clone(value, _) + | Expr::Run(value, _) + | Expr::Join(value, _) + | Expr::Cancel(value, _) => Self::handle_expression_has_default(value), + Expr::Call(callee, args, _) | Expr::GenericCall(callee, _, args, _) => { + Self::handle_expression_has_default(callee) + || args + .iter() + .any(|arg| Self::handle_expression_has_default(&arg.value)) + } + Expr::ListConstruct(values, _) => { + values.iter().any(Self::handle_expression_has_default) + } + Expr::MapConstruct(entries, _) => entries.iter().any(|(key, value)| { + Self::handle_expression_has_default(key) + || Self::handle_expression_has_default(value) + }), + Expr::StringInterpolation(parts, _) => parts.iter().any(|part| match part { + ast::StringPart::Literal(_) => false, + ast::StringPart::Expr(value) => Self::handle_expression_has_default(value), + }), + Expr::Pipeline(value, steps, _) => { + Self::handle_expression_has_default(value) + || steps.iter().any(|step| { + Self::handle_expression_has_default(&step.function) + || step + .extra_args + .iter() + .any(|arg| Self::handle_expression_has_default(&arg.value)) + }) + } + Expr::IntLiteral(_, _) + | Expr::FloatLiteral(_, _) + | Expr::StringLiteral(_, _) + | Expr::BoolLiteral(_, _) + | Expr::Nothing(_) + | Expr::Ident(_) + | Expr::None(_) + | Expr::EnumVariant(_, _, _) + | Expr::InlineFn(_, _, _, _) + | Expr::Error(_) => false, + } + } + fn assignment_place_is_view(&self, target: &Expr) -> bool { match target { Expr::View(_, _) => true, @@ -17685,6 +17904,47 @@ function preserve(raw: string) returns string: (span, mode) } + #[test] + fn checked_binding_modes_borrowed_sum_projections_keep_exact_original_sources() { + let source = r#"function inspect(view optional_source: optional[list[int64]], view result_source: result[list[int64], string]) returns nothing: + list[int64] optional_alias = view((view optional_source) handle: + return nothing + ) + list[int64] result_alias = view((view result_source) handle error: + return nothing + ) + return nothing +"#; + let parsed = parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = check(&parsed.module, &resolved); + assert!( + resolved + .diagnostics + .iter() + .chain(&checked.diagnostics) + .all(|error| { error.severity != jett_diagnostics::Severity::Error }), + "{:?}: {:?}", + resolved.diagnostics, + checked.diagnostics + ); + for (alias, backing) in [ + ("optional_alias", "optional_source"), + ("result_alias", "result_source"), + ] { + let (_, mode) = binding_mode_named(&checked.binding_modes, source, alias); + let CheckedBindingMode::View { + source: CheckedViewSource::Binding(definition), + } = mode + else { + panic!("expected stable borrowed sum projection: {mode:?}"); + }; + assert_eq!(resolved.scope_table.def(definition).name, backing); + assert_eq!(resolved.scope_table.def(definition).kind, DefKind::Param); + } + } + #[test] fn checked_binding_modes_keep_immediate_sources_and_owned_copies() { let source = r#"struct Holder: diff --git a/crates/jett_typecheck/src/lib.rs b/crates/jett_typecheck/src/lib.rs index e605bde7..79eff6e1 100644 --- a/crates/jett_typecheck/src/lib.rs +++ b/crates/jett_typecheck/src/lib.rs @@ -6,9 +6,15 @@ pub mod checker; pub mod complexity; pub mod errors; pub mod ownership; +mod reflected_return; pub mod resource_hooks; mod resource_program; +pub use reflected_return::{ + CheckedReflectedCallablePlan, ProducedRefinementRequirement, ProducedRefinementRequirementPlan, + ProducedRefinementUnsupported, produced_refinement_requirement, +}; + pub use checker::{ CheckOptions, CheckResult, CheckedBindingMode, CheckedBodyFacts, CheckedCallArgumentOrder, CheckedComptimeTypeBinding, CheckedComptimeTypeSelection, CheckedGenericCall, diff --git a/crates/jett_typecheck/src/reflected_return.rs b/crates/jett_typecheck/src/reflected_return.rs new file mode 100644 index 00000000..5d6c7974 --- /dev/null +++ b/crates/jett_typecheck/src/reflected_return.rs @@ -0,0 +1,359 @@ +//! Type-only admission for a value produced by a reflected callable invocation. +//! No value, callback or predicate is inspected while deriving this plan. + +use std::collections::HashSet; + +use jett_types::{Type, TypeId, TypeInterner}; + +use crate::CheckResult; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProducedRefinementUnsupported { + Nested, + Secret, + CallableOrNominal, +} + +impl ProducedRefinementUnsupported { + pub fn message(self) -> &'static str { + match self { + Self::Nested => { + "reflected field refinement: native nested refinement validation is not implemented" + } + Self::Secret => { + "reflected field refinement: cannot establish new invariants beneath secret" + } + Self::CallableOrNominal => { + "reflected field refinement: unsupported callable or nominal refinement conversion" + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ProducedRefinementRequirement { + Exact, + Predicates(Vec), + Unsupported(ProducedRefinementUnsupported), + Refine { + base: Box, + predicates: Vec, + }, + List(Box), + Set(Box), + Map { + key: Box, + value: Box, + }, + Optional(Box), + Result { + ok: Box, + error: Box, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProducedRefinementRequirementPlan { + pub source_type: TypeId, + pub requested_type: TypeId, + pub requirement: ProducedRefinementRequirement, +} + +/// Constructor-only evidence from one checked type relation. Its caller must +/// additionally retain that exact immutable checked session. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CheckedReflectedCallablePlan { + source_type: TypeId, + requested_type: TypeId, + source_return: TypeId, + requested_return: TypeId, + return_requirement: ProducedRefinementRequirement, + source_signature: Type, + requested_signature: Type, +} + +impl CheckedReflectedCallablePlan { + pub fn source_type(&self) -> TypeId { + self.source_type + } + pub fn requested_type(&self) -> TypeId { + self.requested_type + } + pub fn source_return(&self) -> TypeId { + self.source_return + } + pub fn requested_return(&self) -> TypeId { + self.requested_return + } + pub fn return_requirement(&self) -> &ProducedRefinementRequirement { + &self.return_requirement + } + + pub fn validate(&self, checked: &CheckResult) -> bool { + checked + .reflected_callable_plan(self.source_type, self.requested_type) + .as_ref() + == Some(self) + } + + pub fn validate_types(&self, types: &TypeInterner) -> bool { + (self.source_type.index() as usize) < types.len() + && (self.requested_type.index() as usize) < types.len() + && types.resolve(self.source_type) == &self.source_signature + && types.resolve(self.requested_type) == &self.requested_signature + && produced_refinement_requirement(types, self.source_return, self.requested_return) + .as_ref() + == Some(&self.return_requirement) + } +} + +impl CheckResult { + pub fn reflected_callable_plan( + &self, + actual: TypeId, + requested: TypeId, + ) -> Option { + if !self.ordinary_callable_parameters_compatible(actual, requested) { + return None; + } + let Type::Function { + return_type: source_return, + .. + } = self.interner.resolve(actual) + else { + return None; + }; + let Type::Function { + return_type: requested_return, + .. + } = self.interner.resolve(requested) + else { + return None; + }; + let return_requirement = + produced_refinement_requirement(&self.interner, *source_return, *requested_return)?; + let node = ProducedRefinementRequirementPlan { + source_type: *source_return, + requested_type: *requested_return, + requirement: return_requirement.clone(), + }; + if !supported_node(self, &node) { + return None; + } + Some(CheckedReflectedCallablePlan { + source_type: actual, + requested_type: requested, + source_return: *source_return, + requested_return: *requested_return, + return_requirement, + source_signature: self.interner.resolve(actual).clone(), + requested_signature: self.interner.resolve(requested).clone(), + }) + } +} + +fn supported_node(checked: &CheckResult, node: &ProducedRefinementRequirementPlan) -> bool { + use ProducedRefinementRequirement as R; + let Some((source_base, source_chain)) = refinement_chain(&checked.interner, node.source_type) + else { + return false; + }; + let Some((requested_base, _)) = refinement_chain(&checked.interner, node.requested_type) else { + return false; + }; + match &node.requirement { + R::Exact => { + node.source_type == node.requested_type + || source_chain.contains(&node.requested_type) + || checked.ordinary_types_compatible(node.requested_type, node.source_type) + || checked.ordinary_types_compatible(node.requested_type, source_base) + } + R::Predicates(_) => checked.ordinary_types_compatible(requested_base, source_base), + R::Unsupported(_) => false, + R::Refine { base, .. } => supported_node(checked, base), + R::List(child) | R::Set(child) | R::Optional(child) => supported_node(checked, child), + R::Map { key, value } => supported_node(checked, key) && supported_node(checked, value), + R::Result { ok, error } => supported_node(checked, ok) && supported_node(checked, error), + } +} + +fn contains_refinement(types: &TypeInterner, ty: TypeId, seen: &mut HashSet) -> bool { + if ty.index() as usize >= types.len() { + return true; + } + if !seen.insert(ty) { + return false; + } + let children = match types.resolve(ty) { + Type::Refinement { .. } => return true, + Type::Secret(inner) | Type::List(inner) | Type::Set(inner) | Type::Optional(inner) => { + vec![*inner] + } + Type::Result(a, b) | Type::Map(a, b) => vec![*a, *b], + Type::Function { + params, + return_type, + .. + } => params + .iter() + .copied() + .chain(std::iter::once(*return_type)) + .collect(), + Type::Struct(_) + | Type::Enum(_) + | Type::Machine(_) + | Type::MachineState { .. } + | Type::Bitfield(_) + | Type::Interface(_) + | Type::Actor(_) => types.nominal_type_arguments(ty).to_vec(), + _ => Vec::new(), + }; + children + .into_iter() + .any(|child| contains_refinement(types, child, seen)) +} + +pub(crate) fn refinement_chain( + types: &TypeInterner, + mut ty: TypeId, +) -> Option<(TypeId, Vec)> { + let mut chain = Vec::new(); + for _ in 0..types.len() { + if ty.index() as usize >= types.len() { + return None; + } + match types.resolve(ty) { + Type::Refinement { base, .. } => { + chain.push(ty); + ty = *base; + } + _ => { + chain.reverse(); + return Some((ty, chain)); + } + } + } + None +} + +pub fn produced_refinement_requirement( + types: &TypeInterner, + actual: TypeId, + requested: TypeId, +) -> Option { + requirement_node(types, actual, requested, types.len()).map(|node| node.requirement) +} + +fn requirement_node( + types: &TypeInterner, + actual: TypeId, + requested: TypeId, + budget: usize, +) -> Option { + if budget == 0 + || actual.index() as usize >= types.len() + || requested.index() as usize >= types.len() + { + return None; + } + Some(ProducedRefinementRequirementPlan { + source_type: actual, + requested_type: requested, + requirement: requirement_action(types, actual, requested, budget - 1)?, + }) +} + +fn requirement_action( + types: &TypeInterner, + actual: TypeId, + requested: TypeId, + budget: usize, +) -> Option { + use ProducedRefinementRequirement as R; + if actual == requested || !contains_refinement(types, requested, &mut HashSet::new()) { + return Some(R::Exact); + } + let (actual_base, actual_chain) = refinement_chain(types, actual)?; + if actual_chain.contains(&requested) { + return Some(R::Exact); + } + if actual_base == requested + && !matches!( + types.resolve(requested), + Type::List(_) | Type::Set(_) | Type::Map(..) | Type::Optional(_) | Type::Result(..) + ) + { + return Some(R::Exact); + } + let (base, mut chain) = refinement_chain(types, requested)?; + if !chain.is_empty() { + if let Some(index) = chain.iter().rposition(|ty| actual_chain.contains(ty)) { + chain.drain(..=index); + return Some(R::Predicates(chain)); + } + let child = requirement_node(types, actual, base, budget)?; + return Some(match child.requirement { + R::Exact => R::Predicates(chain), + R::Unsupported(reason) => R::Unsupported(reason), + _ => R::Refine { + base: Box::new(child), + predicates: chain, + }, + }); + } + let child = |source, target| requirement_node(types, source, target, budget).map(Box::new); + Some( + match (types.resolve(actual_base), types.resolve(requested)) { + (Type::List(a), Type::List(b)) => R::List(child(*a, *b)?), + (Type::Set(a), Type::Set(b)) => R::Set(child(*a, *b)?), + (Type::Map(ak, av), Type::Map(bk, bv)) => R::Map { + key: child(*ak, *bk)?, + value: child(*av, *bv)?, + }, + (Type::Optional(a), Type::Optional(b)) => R::Optional(child(*a, *b)?), + (Type::Result(a, b), Type::Result(c, d)) => R::Result { + ok: child(*a, *c)?, + error: child(*b, *d)?, + }, + (_, Type::Secret(_)) => R::Unsupported(ProducedRefinementUnsupported::Secret), + _ => R::Unsupported(ProducedRefinementUnsupported::CallableOrNominal), + }, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn return_requirements_keep_exact_prefix_and_new_suffix_separate() { + let mut types = TypeInterner::new(); + let positive = types.intern(Type::Refinement { + name: "app.Positive".into(), + base: TypeInterner::INT64, + }); + let bounded = types.intern(Type::Refinement { + name: "app.Bounded".into(), + base: positive, + }); + assert_eq!( + produced_refinement_requirement(&types, TypeInterner::INT64, positive), + Some(ProducedRefinementRequirement::Predicates(vec![positive])) + ); + assert_eq!( + produced_refinement_requirement(&types, positive, bounded), + Some(ProducedRefinementRequirement::Predicates(vec![bounded])) + ); + assert_eq!( + produced_refinement_requirement(&types, bounded, positive), + Some(ProducedRefinementRequirement::Exact) + ); + let secret = types.intern(Type::Secret(positive)); + assert_eq!( + produced_refinement_requirement(&types, TypeInterner::INT64, secret), + Some(ProducedRefinementRequirement::Unsupported( + ProducedRefinementUnsupported::Secret + )) + ); + } +} diff --git a/crates/jett_types/src/interner.rs b/crates/jett_types/src/interner.rs index 253f26d6..9e37c59d 100644 --- a/crates/jett_types/src/interner.rs +++ b/crates/jett_types/src/interner.rs @@ -91,6 +91,22 @@ impl TypeInterner { } impl TypeInterner { + /// Copy all type metadata into an independent owned table with the same IDs. + /// This preserves lookup meanings, not checked Source or witness authority. + pub fn snapshot(&self) -> Self { + Self { + map: self.map.clone(), + types: self.types.clone(), + structs: self.structs.clone(), + bitfields: self.bitfields.clone(), + enums: self.enums.clone(), + interfaces: self.interfaces.clone(), + actors: self.actors.clone(), + machines: self.machines.clone(), + nominal_type_arguments: self.nominal_type_arguments.clone(), + } + } + /// Creates a new interner with all compiler-known leaf types pre-registered. pub fn new() -> Self { let builtins = vec![ @@ -207,6 +223,24 @@ impl TypeInterner { (0..self.types.len()).map(|index| TypeId(index as u32)) } + /// Whether this nominal header addresses an existing definition and state. + /// This is bounds validation only, never Source or ownership authority. + pub fn contains_nominal_definition(&self, ty: &Type) -> bool { + match ty { + Type::Struct(id) => (id.index() as usize) < self.structs.len(), + Type::Bitfield(id) => (id.index() as usize) < self.bitfields.len(), + Type::Enum(id) => (id.index() as usize) < self.enums.len(), + Type::Interface(id) => (id.index() as usize) < self.interfaces.len(), + Type::Actor(id) => (id.index() as usize) < self.actors.len(), + Type::Machine(id) => (id.index() as usize) < self.machines.len(), + Type::MachineState { machine, state } => self + .machines + .get(machine.index() as usize) + .is_some_and(|definition| (state.index() as usize) < definition.states.len()), + _ => true, + } + } + /// Register a new struct definition and return its [`StructId`]. pub fn add_struct(&mut self, def: StructDef) -> StructId { let id = StructId(self.structs.len() as u32); @@ -405,6 +439,133 @@ mod tests { use super::*; use crate::defs::{MachineDef, MachineStateDef, MachineTransitionDef, VariantDef}; + #[test] + fn snapshot_preserves_nominal_refinement_instantiation_lookups_and_is_independent() { + let mut original = TypeInterner::new(); + let positive = original.intern(Type::Refinement { + name: "models.Positive".into(), + base: TypeInterner::INT64, + }); + let record = original.add_struct(StructDef { + name: "models.Record[models.Positive]".into(), + fields: vec![("value".into(), positive)], + methods: Vec::new(), + }); + let record_type = original.intern(Type::Struct(record)); + original + .register_nominal_type_arguments(record_type, vec![positive]) + .unwrap(); + let bits = original.add_bitfield(BitfieldDef { + name: "models.Bits".into(), + network_order: true, + fields: vec![crate::defs::BitfieldFieldDef { + name: "flag".into(), + ty: TypeInterner::UINT8, + kind: crate::defs::BitfieldFieldKind::Bits { width: 8 }, + }], + }); + original.intern(Type::Bitfield(bits)); + let choice = original.add_enum(EnumDef { + name: "models.Choice".into(), + variants: vec![VariantDef { + name: "value".into(), + fields: vec![("record".into(), record_type)], + discriminant: 3, + }], + }); + original.intern(Type::Enum(choice)); + let interface = original.add_interface(InterfaceDef { + name: "models.Reader".into(), + methods: vec![crate::defs::FunctionSig { + name: "read".into(), + params: vec![("value".into(), positive, true)], + return_type: record_type, + is_pure: true, + }], + }); + original.intern(Type::Interface(interface)); + let actor = original.add_actor(ActorDef { + name: "models.Worker".into(), + capability_params: vec![("network".into(), TypeInterner::NETWORK)], + state_fields: vec![("record".into(), record_type)], + messages: vec![crate::defs::ActorMessageDef { + name: "read".into(), + params: vec![("value".into(), positive)], + responds: record_type, + }], + }); + original.intern(Type::Actor(actor)); + let machine = original.add_machine(MachineDef { + name: "models.Session".into(), + states: vec![MachineStateDef { + name: "ready".into(), + fields: vec![("value".into(), positive)], + }], + transitions: vec![MachineTransitionDef { + from: crate::defs::MachineStateId::new(0), + to: crate::defs::MachineStateId::new(0), + }], + }); + original.intern(Type::Machine(machine)); + original.intern(Type::MachineState { + machine, + state: crate::defs::MachineStateId::new(0), + }); + let callable = original.intern(Type::Function { + params: vec![record_type], + view_params: vec![true], + return_type: positive, + }); + + let mut snapshot = original.snapshot(); + assert_eq!(snapshot.len(), original.len()); + for ty in original.type_ids() { + assert_eq!(snapshot.resolve(ty), original.resolve(ty)); + assert_eq!(snapshot.intern(original.resolve(ty).clone()), ty); + assert_eq!( + snapshot.nominal_type_arguments(ty), + original.nominal_type_arguments(ty) + ); + assert!(snapshot.contains_nominal_definition(original.resolve(ty))); + } + assert_eq!( + snapshot.resolve_struct(record), + original.resolve_struct(record) + ); + assert_eq!( + snapshot.resolve_bitfield(bits), + original.resolve_bitfield(bits) + ); + assert_eq!(snapshot.resolve_enum(choice), original.resolve_enum(choice)); + assert_eq!( + snapshot.resolve_interface(interface), + original.resolve_interface(interface) + ); + assert_eq!(snapshot.resolve_actor(actor), original.resolve_actor(actor)); + assert_eq!( + snapshot.resolve_machine(machine), + original.resolve_machine(machine) + ); + assert_eq!(snapshot.type_name(callable), original.type_name(callable)); + assert_eq!(snapshot.nominal_type_arguments(record_type), &[positive]); + + let original_length = original.len(); + let added = snapshot.intern(Type::List(TypeInterner::BOOL)); + assert_eq!(added.index() as usize, original_length); + assert_eq!(original.len(), original_length); + let mut changed = snapshot.resolve_struct(record).clone(); + changed.fields[0].1 = TypeInterner::STRING; + snapshot.update_struct(record, changed); + assert_eq!(original.resolve_struct(record).fields[0].1, positive); + let mut changed = original.resolve_machine(machine).clone(); + changed.states[0].fields[0].1 = TypeInterner::INT64; + original.update_machine(machine, changed); + assert_eq!( + snapshot.resolve_machine(machine).states[0].fields[0].1, + positive + ); + } + // -- Primitive constant IDs ----------------------------------------------- #[test] diff --git a/docs/active/actor_identity_equality.md b/docs/active/actor_identity_equality.md new file mode 100644 index 00000000..443a1db0 --- /dev/null +++ b/docs/active/actor_identity_equality.md @@ -0,0 +1,38 @@ +# Native actor identity equality + +The reference evaluator and native backend implement live actor identity +comparison. `==` and `!=` require the same exact checked nominal actor type. +Cloning preserves instance identity, while separate spawns remain unequal even +when their state matches. `!=` negates the identity comparison. + +Both runtime IDs must belong to the current actor registry and have complete, +live backing records. Equality compares identity without reading actor state. +Enum payload traversal applies this rule through supported nested containers. +An explicit struct `Equatable.equals` method still controls struct comparison. + +Each pending handle records its own depth and refers to the base actor instance. +`run` creates an alias; `join` reaches the same ready instance. Wrapping one +handle leaves ready aliases unchanged. Nested enum comparison checks identity +and pending depth. Direct comparison of pending actors retains the existing +unsupported binary-operation error. + +## Verification + +Local MSVC static-CRT verification on 2026-10-12 passes the complete runtime +(301), interpreter (564), and code generator (118) unit suites, plus three actor +object and nominal-type rejection tests. The actor native suite runs nine +executables per runtime profile after deleting their Source: seven original +and supplemental debug-policy cases, and a production-output case with both +Jett optimization settings. Debug and Release runtime profiles both pass. + +The production case observes direct identity, nested list/optional enum +payloads, pending aliases and depths, state shared after joining, and explicit +struct equality that ignores its actor field. Runtime tests additionally cover +foreign and stale IDs, incomplete backing records, pending allocation failure, +and single state retirement. + +The six saved fixture Sources remain unchanged. These supplemental `tests/native` +controls leave the fixed `tests/native_parity.json` inventory at 207 fixtures +and 182 object obligations. Full predecessor, GNU, and installed-distribution +acceptance remain part of the native codegen completion work. Escaping comptime +actor graph import is tracked separately. diff --git a/docs/active/native_acceptance_audit.md b/docs/active/native_acceptance_audit.md index a2e4cb2e..b6bbe319 100644 --- a/docs/active/native_acceptance_audit.md +++ b/docs/active/native_acceptance_audit.md @@ -55,21 +55,14 @@ separate from parity for existing accepted behavior. The following semantic and implementation gaps prevent a full parity claim: -- [Actor equality](../open_design/actor_equality.md): actor identity versus rejecting comparisons. - [Actor handles escaping comptime](../open_design/comptime_actor_values.md): reject escaping handles versus materializing a defined actor graph. - [Erased interface equality](../open_design/interface_value_equality.md): explicit comparison versus a defined dynamic equality contract. -- [Concrete-owner arguments in erased calls](../open_design/interface_same_owner_arguments.md): runtime validation versus rejecting unsafe erased calls statically. - [Secrets hidden in debug print values](../open_design/debug_print_hidden_secrets.md): extend redaction to erased/builder payloads versus reject potentially secret values. - [Projected field assignment](../open_design/projected_field_assignment.md): implement safe updates to owned mutable locals versus reject projected writes and use construction/rebinding. The checker now rejects immutable, temporary, and known view roots before either execution backend; owned mutable roots remain admitted without an implemented update contract. -- [Global constant execution](../open_design/global_constant_execution.md): - both backends now receive shared immutable compile-time primitive values, - including transparent aliases, without startup initialization. Move-only and - nominal constant types still require a read, lifetime, and authority contract; - conservative E9001 rejection does not close that broader design obligation. - [List summation types](../open_design/list_sum_type_contract.md): numeric primitives and transparent aliases work in both paths, but the unconstrained source signature still admits refinements, secret wrappers, and nonnumeric @@ -79,16 +72,92 @@ The following semantic and implementation gaps prevent a full parity claim: current checks admit some refinement/base requests with matching carriers and secrecy. Preserving the actual owner on an admitted interface read does not select the broader requested-type contract or authorize arbitrary nominal casts. -- [Inferred uninhabited JSON parser slots](native_json_uninhabited_parse_slots.md): - public policy and actual reference execution admit empty/absent carriers and - return handled errors for occupied Never slots. The serializer repair is - accepted at `05494ed4`; the separate parser source-selector repair requires - its own full-stdlib, linked/compiler and release acceptance. + +[Exact concrete-owner preflight](native_erased_same_owner_arguments.md) now +implements the selected runtime contract in reference direct/named-callback +calls and generated native interface dispatch. It checks receiver-selected +checked identities before argument normalization or body entry, preserving +nominal types, generic arguments, primitive widths, ordinary view/owned modes +and earlier argument failures. Native validation observes existing values +without cloning or transferring ownership; existing unboxing guards remain. + +The restored integration's normal Debug and Release runtimes each pass thirteen +original Source-deleted cases and four production-output executions spanning +matching/mismatched owners and both Jett optimization settings: thirty-four +executions overall. All 2,331 workspace library tests pass, including six runtime +and eleven focused reference controls; twenty-seven broader interface tests pass. +Full coherent native conformance, GNU, installed and exact-revision CI acceptance +remain pending. Native Source coverage for refinement and pending combinations remains +incomplete. Checker substitution remains at whole interface parameters; +`list[Pairable]` retains its declared erased child contract without recursive +Self substitution. Erased-interface equality remains an independent unresolved +contract. These bounded gates leave the rough 87% estimate and fixed inventory +denominators unchanged. + +[Actor identity equality](actor_identity_equality.md) implements the selected +live-instance contract in the reference evaluator and native backend. Direct +comparisons require the same exact checked nominal actor type; recursive enum +payload comparison checks identity and per-handle pending depth without reading +state. Local MSVC static-CRT acceptance on 2026-10-12 passes nine Source-deleted +executables per Debug/Release runtime profile, including production output with +both Jett optimization settings. Escaping comptime actor graph import, full +predecessor, GNU, installed-distribution and coherent-revision acceptance remain +open. This bounded acceptance does not change the rough 87% planning estimate, +the fixed inventory denominators or the whole-language completion requirement. + +The [inferred uninhabited JSON parser repair](native_json_uninhabited_parse_slots.md) +is accepted at predecessor `08f9f7e7`: its 19 Source tests, frozen full workspace +and all four supported-host jobs passed. Each later revision still requires its +own coherent release gates. The [interface audit](native_interface_values.md) also retains the remaining facade, refinement-composition, and comptime combinations that need scrutiny. Passing its growing regression suite does not remove those audit obligations. +## Selected boundaries and unimplemented extensions + +Namespace constants have a selected current domain in `docs/design.md`: +implicitly copyable primitive values and transparent aliases are baked once by +the shared required evaluator; aggregates, nominal refinements, resources, +actors, function values and erased payloads report E9001. This is a frontend +boundary for both execution paths, not missing positive native support. +`native_global_constants_match_interpreter_without_source_in_both_profiles` +executes the admitted values and suites after Source removal, while driver +constant tests pin aggregate and hidden-pending rejection before publication. +Revalidate those gates at the final revision. The +[broader constant ownership note](../open_design/global_constant_execution.md) +records a possible extension and does not authorize runtime globals. + +Current task/actor execution is sequential: eager `run`, one-level `join`, +no-op `cancel`, and synchronous actor dispatch. Existing run/join/cancel, +actor/message ownership and scalar actor-comptime Source gates remain parity +obligations. `docs/architecture.md` explicitly marks asynchronous scheduling +and capability cancellation checkpoints unimplemented; the reference path also +rejects Resource task/message transport and has no corresponding production +provider APIs. Introducing those absent APIs or a scheduler is separate feature +work, not a prerequisite for parity with the existing interpreter. Implemented +portions of partial features, including occupied Resource custody, still require +the complete original-Source evidence specified by the accepted plan. + +The current private Resource checkpoint is 138 shared scenarios /276 native runs +in debug and release on MSVC, with Source absent at launch and zero obligations +before teardown. The nested-view packet passes all 276 independent reference runs +and all 276 native executions, including branch joins, scoped bodies, loops and +Break/Continue. The approximate feature estimate advances from 85% to 86%; it is +not measured code coverage or a changed inventory denominator. See the +[nested lifetime contract](native_resource_nested_sums.md). Archive provenance, +GNU private Source execution, and the final four supported-host workflow jobs +remain separate gates. + +The [ordinary borrowed-sum implementation](native_ordinary_borrowed_sum_codegen.md) +now passes 15 reference Sources and 30 Source-deleted native debug/release +executions, including later handled-argument failures and mutable copied +primitive/String destinations. The fresh runtime also re-passes all 138 Resource +scenarios /276 native executions. Full affected MIR/codegen/object/CFG, runtime +and driver-library checks pass; coherent full workspace and current platform +acceptance remain required. The current rough feature estimate is about 87%, +with the fixed inventory denominators unchanged and the full 100% objective open. + [Direct collection and sum equality](../completed/direct_collection_equality.md) is settled: both operators now report E0376 before comptime evaluation or native lowering, including wrapped and concretely instantiated types. Frontend fixture @@ -238,7 +307,8 @@ either argument, generic wrappers, captured public-value callbacks, wholly baked results, and comptime-created callbacks invoked at runtime. It verifies concrete owner dispatch through interface collections and exact output in debug/release after source removal. These probes strengthen the facade/comptime audit without -settling the remaining equality or erased-call policy decisions. +settling the separate erased-interface equality contract. Current concrete-owner +preflight acceptance is recorded above. The refined-sum audit fixes direct optional/result refinement boundaries to validate their whole declared base before considering extraction. Its linked @@ -1922,24 +1992,851 @@ MIR and native validation. Generated calls, temporary staging, viewed loops and handled defaults carry separate checked proofs. The [implementation contract](native_call_ownership_facts.md) records the scope. -The latest formatted compiler passes all 304 checker, 188 HIR, 181 MIR, 67 backend -and 63 object-emission tests, plus the MIR doctest. A stable Windows supplemental -native snapshot completed 472/479 test groups; seven failed. Subsequent focused -replays passed five of those failures: nested breakpoint conditions, primitive -and refined interface identity, contextual secrets in both profiles, and the -64-fixture scalar/owned-collection group. Explicit collection views preserve the -existing output oracles. Ten breakpoint-region and three intrinsic owning-staging -regression groups pass. The linked replays preceded the formatting-only pass; -the complete compiler-core gate follows it. No new complete native count is claimed. - -The builder replacement and reflected handled-view read tests remain unrepaired. -Full current workspace, complete native replay, fixed inventory and supported-host -acceptance remain required. Earlier 598-fixture frontend, 15 caller-group source -erasure and property-replay receipts retain their individual historical scope. +The earlier formatted caller-core checkpoint passed 304 checker, 188 HIR, +196 MIR, 70 backend and 63 object-emission tests, plus the MIR doctest. Focused +handled-producer and builder-generation checks passed 66 MIR acquisition groups, +seven generation groups, three native emission groups and both original native +regressions. All seven failures of the historical 472/479 run subsequently +passed their individual replays. + +The complete Windows native suite now passes **480/480** at exact committed +`a63f891016fac7dc50fffdde33be8b0ce45c655f`. Root terminal `06b7f8` and +`target/native480-after-owner-generation/receipt.json` record zero failures and +stable HEAD/tracked sources, ending `2026-10-04T14:20:12.987780+00:00` +(2320.39 seconds). Resource candidate changes are absent from that run. +This is complete native-corpus acceptance for that caller head, not whole-language +completion or new current full-workspace/platform acceptance. Fixed lowering +remains 182 fixtures; a separate earlier stable recovery scan passed all 182. +Current 182-object, workspace and supported-host receipts remain required. The accepted unchanged a9 baseline passed 92 workspace result groups, 464 native cases and all 182 object obligations. Workflow 37130142896 passed all four -Windows/Linux build and installed jobs. Those receipts do not validate this -candidate. The fixed inventory remains 207 with 182 object obligations and the -rough feature estimate stays 85%. Resource providers and cleanup, async and actor -lifetimes, and remaining capture/reflection shapes remain in the 100% objective. +Windows/Linux build and installed jobs. Those historical receipts do not transfer +to the Resource candidate. The fixed inventory remains 207 with 182 object +obligations and the rough feature estimate stays 85%. Async/actor lifetimes, +broader caller-generation/capture/reflection cases and Resource backend/provider +execution remain in the full 100% objective. + +## Resource reference lifecycle execution + +The private reference Resource implementation over a63 passes all **36 focused +tests** in `target/native-resource-after-checked-call-relay-focused.log` +(root terminal `923450`; zero failures, 418 filtered, 0.07 seconds). +The suite includes exact ordinary/mutual declaration identity, retained source +registration, real both-profile Source construct/borrow/move/close and reverse +cleanup, occupied returns, indirect close, later lexical actual failure, +ordinary closure return7, exact terminal list-kernel failure and mixed cleanup +failure precedence. Provider events and zero live custody/registry entries are +asserted before teardown. Closed descriptor/absence and required-worker purpose +controls do not invoke a runtime provider. The +[detailed reference scope](native_resource_hook_identity.md#reference-lifecycle-execution) +keeps runtime eligibility, custody and catalog authority separate. + +These are private reference tests with a compiler-test-only scripted provider, +not native Resource execution. Production catalogs/providers remain disabled; +no shipped Resource API or native hook/carrier/drop path is admitted. Whole +scoped/reflected/indirect/worker and aggregate ownership breadth, real providers, +async/actor cancellation and late-completion paths remain pending. Broad library, +whole-workspace and supported-host acceptance of the Resource candidate are +also pending. The 36 tests add no native inventory row and do not change the +480-case caller receipt, 207/182 fixed denominators or about-85% estimate. + +## Checked Resource reference transport follow-up + +The later scoped/required/generic, pipeline and mutable-assignment transport +passes **83/83** focused reference groups (`61a351`) in +`target/native-resource-concrete-intrinsic-pipeline-syntax-fixed-focused.log`. +Exact original region/body/step/statement proofs survive nested entry and restore +metadata/cursors. Concrete intrinsic metadata preserves generic substitutions +and alias names; copied source packets and foreign contexts remain refused. +The complete formatted gate passes **1,186/1,186** (501 Comptime, 87 driver, 598 frontend; `4ba85c`) in `target/native-resource-transport-formatted-property-fixed-broad.log`. Pipeline actuals retain lexical order, and assignment replacement retains the +previous owner until RHS succeeds and cleanup/publication complete. + +The prior 36/43 receipts remain historical. The foundation `7157f564` +[supported-host run](https://github.com/vycdev/jett/actions/runs/37217759157) passed +all four jobs and excludes this follow-up and custody core `92421063`. These reference +controls do not execute native Resource values. The 480-case native caller receipt, +fixed 207/182 inventory and approximate 85% estimate keep their separate scopes; +full providers, aggregates, capture/reflection/refinement breadth and +async/actor/cancellation native execution remain whole-goal requirements. + +## Checked Resource HIR/MIR handoff + +The original checked Resource manifest and exact Source hook calls now survive +HIR-to-MIR lowering, including unused hook validation and lexical argument +order. The five focused Source groups pass (`852dfc`). The formatted complete +phase gate passes **522/522**: 191 HIR, 198 MIR, 70 codegen and 63 object-emission +tests (`961b04`, `target/native-resource-manifest-metadata-fixed-phase.log`). +Earlier foreign nested-type rejection is pinned at manifest validation, with +the lower native type gate still tested independently. + +This is checked metadata preservation. Native Resource custody operations, +ownership/drop CFG, registration/ABI and linked Source execution remain pending. +These phase receipts do not extend the separate 480-case native caller receipt +or foundation-only platform acceptance. The fixed 207/182 inventory, approximate +85% feature estimate and whole-language 100% objective remain unchanged. + +## Native Resource registration protocol + +The bounded private runtime metadata issuer now validates every layout row and +issues context-bound nominal kind/slot metadata without creating a provider, +payload, grant, owner or loan. Exact guarded application and independent +byte/API review precede the initial **9/9** registration replay (`f753aa`). A +separate containing-function signature consistency correction adds an unused +Return-frame refusal control. The formatted full runtime passes **167/167**, +including ten registration and thirteen core custody controls (`56e7ac`, +`target/native-resource-registration-formatted-library.log`). + +These are runtime Rust protocol tests. The new metadata issuer has no connected +native Source ABI or provider, and the compiler still refuses Resource execution +without the dedicated ownership/drop plan. They do not enlarge the separate +480-case native caller acceptance, fixed 207/182 inventory or approximate 85% +estimate. Matching native Resource linked execution in both profiles, then full +providers, aggregates and concurrency breadth, remain required. + +## Source-authenticated ownership handoff + +The original checked HIR/type archive and constructor-emitted MIR witness now +support a separate `ResourceOwnershipPlan`. It records exact scopes, operation +and provisional-return frames, owner holders, incoming views, bounded loans, +transfers, occupied sum extraction and cleanup obligations. Original/current +Source call associations preserve bare owned Resource-to-view custody without +creating ordinary owning operand claims. Copied queries, changed call sites, +operands, Source certificates, evaluation order and nominal type meanings are +refused. Unnamed Resource-bearing intermediate expressions retain their witness. +Canonical block/local compaction retains exact current associations. + +All **12 focused ownership groups** pass (`155203`, +`target/native-resource-ownership-associated-focused.log`). The Source-derived +missing nominal-table regression passes (`fe7b0a`), returning the precise error +without a panic. The original nominal fixture retains both field-corruption +oracles after copying the complete original nominal tables and selecting its +actual Source Holder type. The malformed-type phase controls exposed a validation +ordering bug; valid type/invocation metadata is now required before custody +queries, preserving the exact manifest diagnostic and independent scalar refusals. + +The complete formatted affected gate passes **535/535** (192 HIR, 210 MIR, +70 codegen, 63 object-emission), plus 32 type-library tests and one MIR doctest +(`3c6a8a`, `target/native-resource-ownership-valid-metadata-phase.log`). Earlier +failed runs remain diagnostic evidence. This is initial compiler ownership +analysis: connected native frames, provider/ABI operations, operational cleanup, +handled actual normalization, complete aggregates/reflection and linked binaries +executed after source deletion remain required. The separate 480-case caller +receipt, fixed 207/182 inventory and approximate 85% estimate are unchanged. + +The broader driver/frontend gate also passes **693/693**: 87 driver library, +8 backend-lowering tests (including all 182 run-pass lowering obligations) and +598 frontend fixtures (`450ea4`, +`target/native-resource-ownership-driver-frontend.log`). The previous registration +commit's Windows CI job failed its CLI native-setup capture test with a main-thread +stack overflow; that separate platform regression is under investigation and is +not represented as current ownership or whole-workspace acceptance. + +## Native context adapter and custody operations + +The private runtime adapter connects the existing authenticated stationary +context lease, bounded layout issuer, registry and custody core. Exact current +attempt, frame, owner, loan, prepared hook and Resource sum records retain the +non-cloneable core tokens. Ordinary and Resource lookup IDs share one checked +monotonic allocator. Installation publishes a scripted provider and paired +ordinary Network grant transactionally after preflight; that provider is test +only. Production state starts absent with disabled providers and no new exported +Resource leaf. + +Real core operations now cover root entry, Operation frames, direct/descriptor +hooks, acquisition, moves, bounded borrowing, close/drop, occupied sums, staged +replacement and protected reverse cleanup. Completion preserves the observed +body status verbatim and separately selects cleanup, host panic and current +ordinary/Resource body failure. Ordinary FailureTake is neither invoked nor +reset by the adapter. Refused transfers retain the old owner; a cleanup fault +cannot publish success. Borrow domain Fail stays ordinary Result data. + +Initial compilation found three private API/field-name integration errors. The +first runnable focused check passed 13/14 and exposed string-only cleanup being +used for an ordinary borrow Result. The bridge now uses the existing recursive +native destructor, and an additional real-provider BorrowFail control checks +sum/string retirement, duplicate-drop refusal, clean completion and exact events. +All **15/15 focused groups** pass (`fd8577`, +`target/native-resource-state-adapter-companion-fixed-focused.log`). After +formatting, the full runtime package passes **182 unit + 13 integration tests** +(`96d995`, `target/native-resource-state-adapter-formatted-runtime.log`). The +earlier compile/failure logs remain diagnostic evidence. + +These are runtime host-protocol controls, not linked Source execution. Exact +Source child Scope/Return activation, resident incoming views, connected C leaves +and emission, operational compiler cleanup, the matched archive and linked +source-deleted binaries in both profiles remain required. The adapter-head Windows +CLI setup-capture stack overflow is recorded separately; the later helper +repair below supersedes that pending local regression. Broader +aggregate/reflection/refinement, provider and concurrency coverage remains part +of the full goal. Native caller 480/480, the fixed 207/182 inventory and the +approximate **85%** feature estimate retain their separate scopes. + +## Windows lowering stack repair + +The unchanged CLI native-setup capture test reproduced a main-thread stack +overflow at Resource ownership and adapter heads. The adapter-head Windows CI +also completed the native conformance suite with **480/480 passing** before the +workflow failed its separate CLI capture target (`b32d80`, workflow +`37244085637`). This is current adapter-head supported native execution evidence; +it still includes no native Resource lifecycle case. + +The repair extracts the existing heavy HIR statement/equality/inline-function +branches and MIR expression/per-actual branches into private helpers. Original +Source facts, Resource call capture, preflight, lexical order, shared call-view +scope, rollback, spans, diagnostics and context restoration remain at the same +operations. Source fixtures, assertions, snapshots, launcher order and stack +sizes are unchanged. Exact guarded application reconstructs both files and all +39 hunks (`3a8820`); the independent reconstruction record agrees. + +The original regression now passes **1/1** (`285533`), and the formatted full +CLI capture target passes **9/9** (`508636`). The formatted phase gate passes +**535/535** HIR/MIR/codegen/object checks plus 32 type tests and the MIR doctest +(`f8fe67`). The broader gate passes **693/693**: 87 driver library, 8 lowering +checks including all 182 run-pass fixtures, and 598 frontend fixtures (`647ede`, +`target/native-windows-stack-decomposition-driver-fixtures.log`). An initial +command used the nonexistent test target frontend; no driver test ran in that +attempt. The corrected fixture_suite command supplies the actual receipt. + +Actual Debug assembly measurement (`779571`, +`target/native-windows-stack-decomposition-frame-measurements.json`) records +MIR lower_value decreasing from 169,448 to 15,272 bytes and the ordered-call +loop from 73,736 to 4,504 bytes. Its separate per-actual helper uses 70,104 bytes. +HIR lower_expression decreases from 117,624 to 106,200 bytes and lower_statement +from 108,552 to 100,216 bytes. These are individual function-frame measurements, +not a whole-program stack limit or proof of every recursion shape. + +The local platform regression is repaired. Whole-head supported-host CI and +connected Resource Source ABI/emission/cleanup and linked lifecycle execution +remain required. The fixed 207/182 inventory and approximate 85% coverage +estimate remain unchanged; runtime/metadata checks do not supply the missing +Resource native execution. + +## Resource execution family and layout prerequisite + +Resource-free original direct Source callers now belong to the authenticated +Resource execution family. Fresh whole-program ownership plans project exact +formal facts, sum shell roles, Scope/Return distinctions and the actual +driver-selected entry into deterministic immutable v2 layout metadata. The +readonly object accessor emits no Source body or provider execution. + +Eight focused groups pass in both compiler profiles. The formatted compiler +gate passes **543/543** plus the MIR doctest (`fc3f02`); the final Return +projection passes all **74 codegen and 63 object checks** (`45ddfe`). The driver +gate passes **693/693** (`b9de51`). Corrections preserve the existing owned +runtime-capability entry rule and private constructor authority. See the +[implementation receipt](native_resource_hook_identity.md#resource-execution-family-and-emitted-layout). + +Native Resource Source execution remains unaccepted until dedicated body/ABI +emission and cleanup run in linked binaries with Source inputs removed. The +native **480/480** result remains the separate adapter-head Windows test receipt; +it contains no admitted Resource lifecycle case. The approximate **85%** feature +coverage estimate remains unchanged. Remaining aggregate, captured/reflected, +refinement, provider and concurrency cases stay within the full goal. + +## Native Source runtime transition gate + +At the Source runtime successor, all nine focused Source controls pass, as do +three v2 registration, two all-or-nothing transfer and fifteen retained adapter +controls. The repaired formatted runtime package passes 196 unit and thirteen +integration tests (`fd04d3`). One historical malformed-header assertion was +updated because v2 is now supported; unsupported versions remain refused. + +This supplies host/runtime and C-boundary evidence only. It does not add a +Source-native lifecycle fixture or close the Resource execution row. Generated +bodies, ordinary companion cleanup, matched archive and Source-deleted linked +runs remain pending; whole-language parity remains the goal and the broad +estimate stays about 85%. See the [runtime handoff](native_resource_hook_identity.md#native-source-runtime-and-custody-leaves). + +The final Source runtime successor additionally passes three causal ordinary +companion ownership regressions and five exact root-outcome controls. The final +formatted runtime gate is **204 unit plus thirteen integration tests** +(`716d31`). The malformed companion controls were observed failing before the +fix (`bf61fa`) and passing after it (`5af1b8`). Root-outcome evidence (`4f200a`) +preserves original body zero independently of cleanup failure and rejects +active/stale/foreign or wrong-purpose observations before effects. These remain +host/C protocol checks; generated Source-native lifecycle admission is pending. + +## Resource ordinary companion compiler gate + +The fresh-plan companion analysis passes three both-profile Source groups and +**546 HIR/MIR/codegen/object checks plus the MIR doctest** (`f48b08`). It preserves +ordinary initialization/liveness/alias/move/type/Source call checks and excludes +Resource endpoints from ordinary owned storage. Current expression identity +selects already validated roles; public ordinary Resource refusals remain. +Readonly acquisitions borrow the same fresh immutable program. This compiler +gate does not establish emitted Resource bodies or linked lifecycle execution; +the fixed inventory and approximate 85% feature estimate are unchanged. + +## Direct Resource body and matched archive checkpoint + +The direct Resource emitter's five original-Source object/ABI groups pass in +both compiler profiles. The formatted codegen/object/HIR/MIR gate passes +551 checks plus the MIR doctest; a detected ordinary CFG regression is repaired +by allocating Resource failure blocks and cleanup slots only in the selected +family. Ordinary runtime checking passes 217 tests. Four exact report encoder +controls and six actual driver decoder controls also pass. + +Both private Windows MSVC runtime profiles are compiled and inspected with +the actual static-CRT flags, native-library vector and discovered SDK/linker. +Each archive contains exactly one main, eight private test exports and 32 +Resource leaves. The production archive excludes private main/test exports. +The HOST C ABI controls pass with each measured archive, including malformed +input, foreign/stale context, repeated entry and output-capacity refusals. These +handwritten controls add no Source coverage. Their actual primary report shows +zero obligations before destroy and exact Constructed/Finalized events. + +The real reference gate and native gate each pass five original Source scenarios +under both profiles: connected ownership/borrow/return/close, factory failure, +later-actual ordinary failure, implicit drop and finalizer panic. Native binaries +run after both Project and synthetic Stdlib directories are deleted. The strict +report checks original body/cleanup outcomes, messages, events, exhausted script, +ordinary storage and every Resource obligation before exactly one destroy. +These are ten native executions in one conformance group, separate from the +earlier 480-case corpus. The first local Windows compiler-test slice is accepted; +production provider and supported-host distribution acceptance remain pending. + +The actual broader driver gate passes 693 tests, including all 182 lowering +fixtures. Indirect/aborted/Replace and broader Resource semantics remain within +the full goal. The fixed 207/182 inventory and rough 85% feature estimate retain +their separate scopes. The private measured library order begins bcrypt then +advapi32; the production canonical vector reverses those first two. The reviewed +private prerequisite retains actual measured ordering and changes no production +launcher validation or installed manifest. + +### Plain Resource replacement pre-effect validation + +At the runtime successor to cc06aa1f, `native-resource-replacement-preflight-focused.log` passes four replacement groups and `native-resource-replacement-preflight-runtime.log` passes 220 runtime checks. Exact loan, consumed-handle and private generation-exhaustion controls preserve both owners before any old finalizer, then verify valid once-only cleanup. No wire or C ABI change is made. This evidence does not establish Source-native mutable replacement, aggregate replacement, or current supported-host acceptance. The earlier 486 native regression result and ten Source runs remain evidence for cc06aa1f. Rebuilt matched archives and Source replacements are required next; overall tracking remains about 85%. + +### Source plain Resource replacement acceptance + +The formatted Source replacement successor to `2b9442b1` passes 558 compiler/object/HIR/MIR checks plus one MIR doctest, and all 606 frontend/backend-lowering fixture checks. `native-resource-replacement-final-reference.log` and `native-resource-replacement-final-native.log` each pass eleven shared original Source scenarios in both profiles. The native launch deletes Project and synthetic Stdlib Source, observes exact events/body/cleanup/messages/scripts, retires every Resource/ordinary obligation before one destroy, and preserves body 255/cleanup 255 for executed old-finalizer failure. Exact self-reseat emits no finalizer/transfer, while failed RHS evaluation leaves old available to its handler. The fixed 207 inventory/182 obligations and broad 85% estimate are unchanged. + +The new measured archive receipt `54a38248` preserves exact CRT/library/symbol provenance; both HOST C ABI controls pass. The broad driver-library attempt is 86 passed/1 failed due to CaptureLinkerOutput PermissionDenied during pre-spawn capture setup; the exact isolated property replay passes. The error alone does not identify capture creation versus second-handle opening. This is not a clean full driver-library result. The previous 486 native regression result remains specific to `cc06aa1f` until the complete suite is rerun on the new commit. Conditional/aggregate/projected replacement, other Resource execution and supported-host production distribution remain unproven. + + +### Exact native Resource replacement regression + +The clean pushed `cda86ac7` Source replacement commit passes the complete default +native gate: 486 tests, zero failures and one ignored Resource group, in 1355.37 +seconds. All 2099 recorded Rust/Jett/Cargo inputs remain exact after termination. +The ignored group was separately executed in all 22 native and 22 real-reference +scenario/profile runs. The earlier 86/87 driver-library result remains a recorded +capture setup failure; its isolated pass alone did not make that broad gate clean. + +### Retained native output capture + +The driver retains append-only capture files, gives children owned duplicate +handles, and reads bounded snapshots with explicit offsets. It no longer opens +the second file object through ReOpenFile. Private error context identifies +creation, duplication or snapshot while retaining the original I/O source. +The original PermissionDenied log proves a pre-spawn setup failure, but does +not distinguish creation from reopening or establish an external cause. + +After correcting the authored Windows test commands' quoting without changing +their stream/status/deadline oracles, all 12 capture checks and three command +checks pass. The unchanged property replay regression passes twice in isolation, +then the complete driver library passes all 92 tests (152.85 seconds). Existing +Source deletion, both optimize modes, 104 trials, trial-4 sentinel, counterexample +and original-stream requirements remain intact. Child polling/kill/reap and +deadline behavior are unchanged. This verification repair does not increase the +about-85% feature estimate. Full native and supported-host acceptance of the next +combined Source implementation remain separate gates. + +## Checked Source call staging and active-frame sums (2026-10-05) + +Direct Resource calls with handled actuals now lower to constructor-owned typed +Begin/Stage/Invoke/End regions. The sealed witness keeps the complete original +Source tuple and current graph; fresh CFG analysis proves lexical argument order, +arrived-prefix custody, exact endpoints and retirement before a handler Return +operand. Complete prepared Source and borrowed-formal metadata exists even when +Invoke is skipped. Public graph edits cannot recreate that authority. + +A genuine first-factory failure initially returned native protocol refusal after +ConstructionFailed, while the reference returned cleanly. The repair separates +Scope storage from active execution: a private exact-site projection supplies the +current frame for sum observations, transfers and extraction. Runtime top-frame, +slot ancestry and original Source checks remain strict. Both first-factory +success and failure now execute with their original outcomes. + +Eighteen shared Source cases pass in both reference and native profiles (36 +executions each). Native launch follows Source deletion; exact body, cleanup, +channel, messages, events, script consumption and zero obligations are observed +before teardown. Seven additions cover minimal and original-helper abandonment, +successful invocation, retained written views, first-factory failure and success, +and cleanup panic. The formatted compiler/object gate passes 573 checks plus the +MIR doctest; 606 frontend/lowering fixture checks, all four inventory gates +(including 182 host objects), and the complete 92-test driver library pass. +The final formatted Source replay also passes all 36 reference and 36 native +executions. The full current native regression and supported-host jobs remain +separate release gates. + +Whole-language coverage remains about 85%. Indirect/refinement/aggregate calls, +static region selection, broader exits and projections, production providers and +shared scheduling remain part of the full objective. These local Source results +do not establish complete native parity. + +### Return-prefix and completed failure propagation (2026-10-06) + +The shared original Source gate now passes 22 scenarios in both reference and +native profiles (44 executions each). Later handled argument failure retires +both temporary view owners in reverse lexical acquisition order. A handler +Return retires its earlier temporary owner before its original helper borrows +an independent retained owner. A default continues to one invocation; actual +cleanup panic prevents the Return helper and still cleans the retained owner. +All native programs launch after Source deletion and require exact channels, +messages, events, script consumption and zero obligations before one destroy. + +Two causal mismatches were repaired without changing those oracles. The reference +Return operand originally ran before pending-operation retirement; ordinary +callables then exposed an inherited caller-floor regression. Each callable now +has its own boundary. Native cleanup already retired both owners, but SourceStatus +converted the callee's recorded status 255 into generic refusal 1. A private +terminal completion record preserves original body and actual cleanup separately, +while exact caller/Source identity and retired Scope/Return guards remain strict. + +All 508 reference-library, 92 driver-library and 222 runtime checks pass. Six +Return-prefix controls cover floor/Scope/receipt/refusal/panic behavior; eleven +native Source controls include exact body 71, clean body 0 with real cleanup +panic, wrong activation/tuple and absent completion authority. Fresh measured +single-runtime archives in both profiles retain one main, eight private exports, +32 Resource leaves, static CRT, ABI 1 and layout wire 2. The source-deleted gate +passes against those archives; production providers remain disabled. + +The parent a30ca97e source tree has all four supported-host jobs green, with +full workspace and 486 default native tests on each host. Its local full run's +26 deadline failures subsequently passed unchanged deadlines in 25 plus one +replays; that does not replace a clean complete local run. Full successor +workspace/platform release acceptance remains distinct from the local gates. +The full 100% objective is active, with the coarse estimate unchanged at 85%. + + +### Required Resource values and exact ordinary helper bodies (2026-10-06) + +The latest local shared Source checkpoint is 61 scenarios and 122 executions per +backend in debug/release. It supersedes the earlier 22/32/46-scenario checkpoints +above. Required evaluation runs once through a provider-free checked cache before +runtime registration; the native path retains original Source provenance through +materialization, production value conversion, MIR validation, object emission, +linking and Source-deleted launch. Exact events, script consumption, failure +channels and zero obligations before teardown remain mandatory. + +All nine required-value MIR controls and 251 MIR library tests pass. The genuine +scalar-helper baseline accepted a changed required value and canonical pruning; +current body-only proof capture rejects both without changing that helper's +ordinary calling convention. Typed sum headers and normalized operation nodes +also participate in disconnected-custody detection. Exact guarded absence still +creates no Resource payload owner or finalizer at runtime. + +These results are local MSVC private-archive acceptance. GNU private execution, +full current workspace/default-native/platform gates and the remaining semantic +families remain separate requirements. Whole-language tracking remains about 85%; +the full 100% objective is active. + +The complete affected codegen suite passes 100 library and 63 object tests; +all 93 driver-library tests also pass. Final Rust/Jett formatting and whitespace +checks pass, and all 2,168 frozen source/config inputs revalidate unchanged. + +### Verified borrowed optional/result Resource payloads (2026-10-07) + +Sources 45..56 add twelve scenarios covering written/bare None/Some/Fail/Ok, +named-argument prefix abort, and handled borrow-domain failure. The unchanged +baseline passed all 73 shared scenarios / 146 reference executions but refused +Source45 at HIR stable-origin admission. The complete corpus now passes 146 +Source-deleted native executions against freshly measured Debug/Release MSVC +private runtime archives, with exact events and zero obligations before teardown. + +All 305 checker, 203 HIR, 254 MIR and 215 runtime library tests pass. Three focused +MIR groups and six runtime groups cover original-body custody, guarded projections, +conditional shell lifetimes, failure companions, argument aborts and forgery. +The full codegen suite passes 102 library and 63 object tests, including unchanged +prior invalid-MIR controls. Archives contain 37 Resource leaves, eight private +exports and one main; independent source/symbol/CRT receipts remain mandatory. + +Admission is limited to root-body Resource aliases. Nested/scoped aliases, +ordinary non-Resource borrowed sums and alternate Default payload lifetimes remain +pending. The rough full-language estimate stays about 85%, and the full 100% +native-codegen goal, GNU Resource gate and broader language/platform requirements +remain open. See [the integration contract](native_resource_borrowed_sums.md). + +All 521 reference-evaluator and 93 driver-library tests pass. The existing +Source33 required-value assertion now verifies its exact two original proofs, +including scalar 761 and the unique checked hook; other fixtures retain exact +one-value counts. This separately fixes the sole failure observed on both hosts +at 17d6c841. Rust and all twelve new Jett Source format checks pass, and the +2,188 frozen source/config inputs revalidate. The final MIR-only formatting change +is exactly one error-return line wrap and passes all 254 MIR tests again. + + +### Verified nested Resource lexical lifetimes (2026-10-07) + +Sources57..95 extend the original shared corpus to 138 scenarios. The Source-only +baseline passed all 276 reference executions but refused native Source57 at exact +constructor-scope admission. Private declaration/exit records and ordered native +loan retirement now pass all 276 Source-deleted MSVC executions in both profiles, +including owned backing reuse, sibling identities, borrow failures, Break and +Continue. All 2,234 frozen inputs revalidate; runtime bytes and the measured +37-leaf ABI1/wire2 archives remain unchanged. + +All 261 MIR, 103 codegen library, 63 object, 521 reference-evaluator and 93 driver +library tests pass. Six public object baselines and eighteen forged-exit refusals +bind codegen admission to exact private records. The rough feature estimate now +tracks about 86%, with unchanged fixed 207/182 inventory denominators. The whole +100% objective stays active. Ordinary borrowed list sums have a separate genuine +12-Source reference baseline and 24 native unsupported-handler refusals; their +implementation is not included here. Updated coherent-revision workspace/platform +and private GNU execution remain independent gates. + + +### Accepted ordinary borrowed-sum checkpoint (2026-10-08) + +The ordinary borrowed-sum implementation is accepted at clean committed HEAD +`2c9f9aa8f2cd481c1d99ce6340ddf63688b2657a`. Its coherent locked workspace passes +**3,446 tests across 92 result groups**, with **0 failed and 1 ignored**, +including **491 default native-conformance tests** and the **182-object inventory +gate**. All **3,637 tracked inputs remain unchanged** after execution, and the +worktree remains clean. The receipt is +`target/native-ordinary-2c9-workspace-evidence/after-evidence.json`, SHA-256 +`07fb0df1d059420c2705111a00da6d56b820e5e168eb37fc757c3fdadbe2dd1a`; its workspace +log SHA-256 is +`42657fc3b9ca894ee3a3a2d9b4997c1af044e15520f5b2031e6aeb01bf7b2e4f`. + +The exact-revision [CI run 37673804359](https://github.com/vycdev/jett/actions/runs/37673804359) +passes all four jobs: Windows MSVC and Linux GNU build/conformance, plus both +relocated installed-package jobs. This closes the coherent-workspace and +supported-host workflow requirements for this accepted ordinary-sum revision. +It does not establish the later expanded Resource pipeline test packet or a +complete language/distribution gate. + +The [ordinary borrowed-sum family](native_ordinary_borrowed_sum_codegen.md) +advances the broad planning estimate to **about 87%**. That judgment remains +separate from the fixed **207-fixture / 182-run-pass inventory**; neither +denominator changes. Other ordinary-carrier, Resource, reflection and semantic +obligations remain open. + +### Source-only Resource pipeline checkpoint (2026-10-08) + +The [pipeline packet](native_resource_pipelines.md) has measured **23 cases / 46 +Debug-and-Release attempts** through each distinct local gate: checked Source +reference execution, Cranelift object emission, and linked native execution with +Project and synthetic Stdlib Source files absent at launch. The reference focus +is included in the accepted **161-case / 322-profile-execution reference +corpus**, with **522 reference tests passing**. The reference observation tuple +proves exact events and zero outstanding owners/registry entries; it does not +measure script consumption. Native reports additionally require zero remaining +scripted inputs and zero obligations before teardown. + +`target/native-resource-pipeline-source-evidence/acceptance-v2.json` records all +**3,648 frozen repository inputs unchanged**; both matched local MSVC archives +and six nested receipts separately revalidate. Provider-panic retirement and cleanup-error precedence now +pass through genuine Source pipeline fixture 20: body/cleanup/channel and exit +are respectively **1/0/2, 73** and **1/255/3, 71**, with exact runtime-operation +and cleanup-error messages. These are tests of existing behavior, with no +language or ABI change. + +The preserved full **161-case / 322-execution linked native corpus passes** +(1256.05 seconds), with all frozen inputs and measured archives unchanged. +Its separate receipt is `full-acceptance-v2.json` in the same evidence directory. +Object emission is not execution, and these matched local MSVC runs do not prove +GNU or other platform acceptance of the expanded packet. Broader Resource +`For`/`Match`, reflected bodies and ordinary families remain separate work. This +checkpoint leaves the **207/182** inventory and **about 87%** estimate unchanged. + +### Expanded Resource For/Match transport (2026-10-08) + +The [control-flow successor](native_resource_control_flow.md) preserves custody +through consuming Resource-free list ForEach and ordinary Resource-free enum +Switch, without a language or ABI change. Constructor-owned reconstruction +authenticates the three temporary headers, source/cursor/length initialization, +extraction/increment prefix and original CFG; independent original/current +borrowed, lexical, call and descriptor proofs remain mandatory. Strict joins and +final raw ForEach refusal are unchanged. Historical v2 acceptance remains +22 cases / 44 Source-deleted MSVC executions, three focused MIR groups and +450 affected phase checks. + +V3's seven accepted Source files 01..05/07/08 cover consecutive and nested loops +as well as Fallthrough/Break/Continue and both unit-enum arms. All 30 cases / +60 focused Source-deleted MSVC executions pass in Debug/Release (204.35 seconds), +with exact outcomes/events/channels/messages, script exhaustion and zero ordinary/ +Resource custody before teardown. Reference zero-owner/registry observations do +not measure that full native tuple. The receipt +`target/native-resource-control-flow-source-evidence/focused-acceptance-v3.json`, +SHA-256 `7f41c389daa268942afe8a645085daac18d746f553f3be32e93d14f798fc30ea`, records +all 3,658 frozen inputs and both measured archives plus six nested receipts +unchanged at HEAD `50ad73a0693c502d1fa05720091b80a4ab623104`. Four focused MIR +groups pass; 89 Resource reference groups include all 191 cases / 382 profile +executions, and final object preflight separately emits 60 objects. + +The clean base `50ad` workspace independently passes 3,448 tests with zero +failures across 92 result groups. That is separate from the modified successor's +coherent-revision gate. Full 191/382 Source-deleted MSVC native execution now +passes in 1,288.83 seconds. `full-acceptance-v3.json` records actual exit zero, +all 3,658 frozen inputs, both measured archives, six nested receipts and the +executable unchanged; its SHA-256 is +`08bcf865bfed8a9b49f262a6c611336260cd0446ba8517139c71ab10627fb806`. +Successor workspace and exact-revision CI acceptance remain pending. Source06's +separate GNU reference repair passes authority, recursion and pure control-flow +tests plus 74 strict Resource entry attempts, including same-grant reentry after +failures; `after-v3.json` records all 3,662 inputs unchanged. Its native +HIR/MIR/codegen ordinal bridge remains pending, and no GNU native acceptance +follows from those reference results. +Erased carriers, borrowed/projected/Never-element or other iterators and broader +For/Match/reflected combinations remain compiler proof obligations under existing +Source semantics. The full goal stays active at the rough 87% planning estimate; +fixed 207/182 inventory denominators remain unchanged. + +### Checked reflected-field reference repair (2026-10-08) + +[Direct field selection](native_resource_reflected_fields.md) now passes all +541 comptime tests, including 19 added authority/recursion/control/metadata and +Resource tests. Strict original Source06/derived controls pass 74 entry attempts +in both checking profiles, including same-provider/grant reentry after every +selected failure. `after-v5.json` records all 3,664 inputs unchanged at the +complete crate gate. This checkpoint proves checked reference behavior; its +receipt alone establishes no native reflected Resource emission or Source-deleted +execution. Accepted native successor gates are recorded below; the full goal +remains active. + +### Checked Resource reflected-field native ordinal bridge (2026-10-08) + +The [direct-field bridge](native_resource_reflected_fields.md) now carries exact +nominal-struct TypeFields ordinal/body correspondence through checked Resource +HIR and MIR. An immutable original archive retains the checked program, original +For/binding and resolver identity, concrete owner and ordered full metadata, +checker-selected body and original lexical path. Every ordinal receives its own +original-field-index guard and one-arm TypeInfo dispatch, preserving distinct +bodies for fields with equal type and reflection while retaining native dispatch +uniqueness. Independent original/current seals reject changed metadata, guards, +targets, paths and public HIR before runtime effects. No runtime leaf, ABI or +wire-v2 row changes. + +Ordinary helpers inside the checked Resource program receive proof-only witnesses +and keep ordinary execution classification and sequence preparation. They gain +no Resource plan, hidden Scope parameter, provider permission or custody. The +ordinary `lower_program` entry retains its existing equal type/reflection +coalescing. Checking specializes type-level facts while field-index/name branches +use the current runtime binder; existing native fixtures already distinguish +same-type fields. Independent ordinary positive controls now pass 30 +Source-deleted native executions without a production change. Their bounded +parity establishes no ordinary coalescing defect or required production repair; +broader checked Resource reflection transport remains a separate obligation. + +The complete affected-phase run passes **1,202 checks**: **542 comptime, 203 HIR, +286 MIR, 107 Cranelift codegen, 63 object and one CFG check**, with zero failed or +ignored tests across ten result groups. Integrated controls include canonical view +iteration and an inferred empty list, exact ordinal guard/body validation and +full ownership rejection of redirected or stale selections. The receipt +`target/native-resource-reflected-field-native-bridge-evidence/after-v4.json` +records actual session **4116**, terminal **1335da**, exit **0**, and all **3,679 +frozen inputs unchanged** at parent HEAD +`f9fa84a6f05b2886a83e09fad4ae3b6dcb22c74a`. Its SHA-256 is +`a043e2b9b4b1b1e908f4037941c0e6ca5113309e08fe167979a046fbdea963ad` and it explicitly records `native_execution_accepted: false`. + +This earlier compiler phase gate is distinct from the native execution acceptance +recorded below. Generic/alias owners, bitfields, variants, machines, TypeInfo.args, +nested producers and broader reflected combinations retain their compiler proof +and Source-evidence obligations. This boundary adds no policy ban. The full goal +remains active at the rough **87%** estimate with fixed **207/182** denominators. + +#### Native execution acceptance + +Four focused Sources retain the original Source06 bytes and separately test +distinct field-index bodies, distinct field-name bodies and a proof-only ordinary +helper. They pass **16 Source-deleted native executions / 24 entries** in +Debug/Release: eight single-entry executables and eight clean two-entry +executables, with the same Session/provider/grant retained across both entries. +Each entry checks exact outcomes, ordered events and zero ordinary/Resource +obligations before teardown. The combined script is exhausted before one Session +destruction. +`target/native-resource-reflected-field-native-bridge-evidence/native-acceptance-v5.json` +records actual session **29429**, terminal **9cfc8d**, exit **0**; its SHA-256 is +`0b05489e3b97201ba0aee4642c2f157e81d5f9684e818f347eb403f775cda10a`. + +The unchanged predecessor corpus independently passes **191 cases / 382 +Source-deleted native executions** in Debug/Release in **1,273.64 seconds**, with +all strict reports accepted. +`target/native-resource-reflected-field-native-bridge-evidence/full-native-acceptance-v5.json` +records actual session **49641**, terminal **1d2132**, exit **0**; its SHA-256 is +`31d6b59bebc1e11c18b9dfb6102b5f5486f28dfd69d1439fce15498aa4050db9`. Both gates retain all +**3,686 frozen inputs** and **nine runtime files** unchanged, including the matched +measured MSVC archives and their build/CRT/native-static-libs receipts. The frozen +test executable SHA-256 remains +`76fe2a90106ee8ede00eb86758cefbaa4cefb42d6b818e90a06337b029aba39d`. +These receipts establish the bounded direct nominal-field native bridge and clean +reentry. Original Source06's separate exceptional acceptance is recorded below; +derived native control acceptance is recorded below; broader aggregate custody +remains a separate obligation. + +#### Independent ordinary reflection positive control + +Three Source families produce five forms covering nominal/generic and alias +fields, bitfields, variant/state fields, type arguments, nested fields and getter +pipelines. Main, verify and property suites in both checking profiles pass +**30 Source-deleted native executions**, matching reference stdout/empty debug +events and explicit-comptime stdout. +`target/native-ordinary-reflected-ordinal-control-evidence/after-v1.json` records +actual session **21527**, terminal **6befe6**, exit **0**, and all **3,703 frozen +inputs unchanged**; its SHA-256 is +`4a282285082fc411b4d8714a890b13c7146d81970533522469eb5848d4a0177e`. The receipt explicitly records +`production_changed: false` and no established ordinary equal-type coalescing +defect. These ordinary controls do not establish Resource transport for their +broader reflection families. The full goal remains active at the rough **87%** +estimate; fixed **207/182** inventory denominators remain unchanged. +#### Original reflected Source06 exceptional single-entry acceptance + +The eight original failure/cleanup scripts pass in both checking profiles: +**16 reference observations / 16 object attempts / 16 Source-deleted native +executions**, with unchanged exact 929-byte Source06. Strict completion channels, +messages, event order, script exhaustion and zero obligations before one teardown +all pass. The existing four READY cases and 191-case catalog are retained unchanged. +`target/native-reflected-exceptional-native-evidence/after-v1.json` records actual +session **2180**, terminal **aa0907**, three command exits **0**, and all **3,720 +frozen inputs / nine runtime files unchanged** at `2be038c7`; its SHA-256 is +`38662e59e4dfebfbbde4236ee0a8d6b2cc647b6a9554ea8afb9f2b1119bf3085`. +Original Source06 exceptional same-grant reentry is accepted separately below. +The bounded derived native control matrix is accepted separately below. +This single-entry evidence adds no aggregate transport acceptance and leaves the +full goal, rough **87%** estimate and fixed **207/182** inventory intact. + +#### Original reflected Source06 exceptional same-grant reentry acceptance + +All **nine original scripts followed by READY** pass in both profiles on the same +Session/provider/paired Network grant: **18 Source-deleted native executables / +36 entries**, plus **36 actual shared reference calls**. Both per-entry completion +tuples, exact diagnostic bytes, event boundaries and continuous ordered events are +retained. Each entry observes zero owners/loans/frames/registry entries, +owner/loan/frame handles and provisional returns, plus empty ordinary storage, +before teardown. The combined script is exhausted and the Session is destroyed +once. Exceptional first completions keep their original nonzero process exit +after clean READY, including cleanup-wins handled Return body 0 / cleanup 255 / +Cleanup channel / exit 71. + +The private launcher completes and copies real observations before continuing +after retired Resource cleanup failure with current ordinary status zero. The +existing next `entry_begin` independently enforces installed-entry/budget, +ordinary first-error/cleanup and active/live-custody checks. No failure latch is +cleared, no provider/grant is reinstalled and no reset/failure-taking API is added. +The immutable first failure remains observable after the clean second entry. + +Fresh MSVC Debug/Release archives were built in actual session **23542**, terminal +**e09148**, exit **0**, and independently measured at +`target/native-reflected-exceptional-reentry-archives/measurements-01/receipt.json`; +the compiled receipt pin is +`96721717326f7d6f96dd2bb98dc1d6e919f7119235e33e103e5ac89bd6f29d0a`. +Runtime ABI 1, Source wire 2 and script/report protocol 1 remain unchanged. + +`target/native-reflected-exceptional-reentry-evidence/after-v3.json` records actual +session **30992**, terminal **22c716**, both Rust harness exits **0**, and all +**3,722 frozen inputs / nine runtime files unchanged** in the tested candidate +based on parent `b5c55ac7`; its SHA-256 is +`f3dc34c6a13079c6e0be04716d37af9a69580a7b99b859a4ebd5301376e3aa8b`. +The corrected exact manifest filters each executed **one real test**. Earlier +session **52970**, terminal **e8c3b4**, selected zero tests; preserved `after-v2.json` +explicitly records zero executions and **nonacceptance**. Only the corrected real +gates supply this acceptance. + +The exact 929-byte original Source06 and prior single-entry/four READY/191-case +evidence remain unchanged. This is bounded local MSVC original-script reentry +acceptance; the bounded derived matrix and dedicated Return successor are +accepted below. Broader Default lifetimes, fresh-archive predecessor compatibility, +coherent-workspace/exact-revision CI gates and broader +aggregate Resource transport remain required. The full goal stays active at the +rough **87%** estimate and fixed **207/182** inventory denominators. + +#### Derived reflected control single-entry and same-grant native acceptance + +The exact existing Sources7/8/9 and historical ten scripts pass both profiles. +Actual control paths are **three handled Returns, two Break paths, two Continue +paths and three scalar Handle Default paths**. The failure handlers return before +the named Break/Continue statements. A separate eleventh Source9 READY baseline +was checked before use as that same Source's reentry tail; the original ten rows +and all three Source byte sequences are unchanged. + +Break preserves total17 and selected-body retirement; Continue preserves total34 +and skips the statement after its scoped-type body; scalar Default substitutes +17, resumes the selected body and preserves both field visits and total34. +The outer optional Resource owner is retained +until the existing close or handled Return. These are exact selected-body/scalar +Default lifetimes, not general Resource-valued Default or arbitrary lifetime +composition evidence. + +The accepted gates are **22 single-entry reference calls + 40 same-grant reference +calls = 62**, **22 object attempts**, **22 Source-deleted single-entry executables / +22 entries**, and **20 same-grant reentry executables / 40 entries**, totaling +**42 native executables / 62 entries**. Each historical first script uses its own +Source's READY on one retained Session/provider/paired Network grant. Exact +completion tuples/messages, continuous events and entry boundaries, zero all +eight Resource counts and empty ordinary storage before teardown, script +exhaustion and one clean destruction all pass. The three explicit-close cleanup +cases preserve body255 / cleanup255 / Cleanup channel / exit71 and exact +`native Resource cleanup failed`, including the immutable original completion +and first nonzero process exit after READY. Existing entry-begin checks remain +authoritative; no reset, latch clearing or provider/grant reinstallation occurs. + +`target/native-reflected-derived-control-native-evidence/after-v1.json` records +actual session **18249**, terminal **e1bcf6**, all five command exits **0**, and +**one real test selected by each filter**; its SHA-256 is +`f9cfe12b7a2013fd63ac0e73672a1c4fbee617e01a366e5747a1ab17a1e6da24`. +All **3,725 frozen inputs / nine runtime files** remain unchanged in the tested +candidate based on parent `f4c63a0f`. The measured MSVC Debug/Release archives use +the unchanged compiled receipt pin +`96721717326f7d6f96dd2bb98dc1d6e919f7119235e33e103e5ac89bd6f29d0a`. +Original Source06's nine-script matrix and earlier single-entry/four READY/ +ordinary-control/191-case evidence retain their original counts, receipts and +archive pins. This is local MSVC derived-control acceptance; GNU carrier/fresh-v3 +native execution is separate. The dedicated Return successor is recorded below. +Broader body/Default lifetimes, aggregate transport, fresh-archive predecessor, +coherent-workspace and exact-revision CI remain required. No Source policy ban is +added; the rough **87%** estimate and fixed **207/182** denominators stay unchanged. + +#### Dedicated reflected body and owned Resource Return acceptance + +The additive Sources10/11 and twelve shared rows pass in both profiles. Each +retains three equal-int64 fields and the statements following its conditional +Return and loop. Finite provider scripts/sentinels establish true successful +first/second-field Return and skipped later fields/statements, independently of +the original Sources7/8/9 ten-script matrix, which remains unchanged. + +Source10 returns nothing with scoped projection/outer owner retirement. Its two +implicit-cleanup cases retain **body0 / cleanup255 / Cleanup / exit71**, empty +ordinary message and exact `native Resource cleanup failed`. Source11 returns +owned survivor892 while outer891 retains the selected body's alias; successful +outer cleanup precedes publication and exact caller Borrow892(29)/close. The +successful events place F891 before B892. Its two cleanup cases suppress +publication/consumer borrowing, retire891 then provisional892, and retain root +**body255 / cleanup255 / Cleanup / exit71**. The helper Source follows a normal +Return path; the report directly observes the entry-root completion and exact +retirement/publication-suppression events, without reading a nested callee +completion. +Clean/handled-domain Return cases retain body0/cleanup0/clean/empty messages/exit0. + +Actual reference/object session **64018**, terminal **0b2a26**, passes three +one-test filters with exits0: **24 single reference calls + 48 same-grant calls = +72**, and **24 final objects**. Actual linked session **47358**, terminal +**dcfb82**, passes two one-test filters with exits0: **24 Source-deleted single +executables / 24 entries** and **24 same-grant reentry executables / 48 entries**, +for **48 native executables / 72 entries**. READY retains the same byte-identical +Source, Session/provider/paired Network grant. Increasing attempt IDs, immutable +first completion/diagnostics/first nonzero exit, continuous events and boundaries, +all eight zero Resource counts and empty ordinary storage before teardown, +combined-script exhaustion and one destruction pass. No reset or latch clearing +is introduced. + +`target/native-reflected-body-return-native-evidence/after-v1.json` records the +reference/object checkpoint. Linked acceptance is recorded in +`target/native-reflected-body-return-native-evidence/after-v2.json`, SHA-256 +`9250a5d37375cd66ba212e1cfcaa489ef0b55571dba0efa72dc59a6d306baa18`. +All **3,730 frozen inputs / nine measured v2 runtime files** remain unchanged at +tested parent `91105876e5dbbbe9bbaa1d19bc2408ec9a274255`, using the unchanged +measured MSVC Debug/Release receipt pin +`96721717326f7d6f96dd2bb98dc1d6e919f7119235e33e103e5ac89bd6f29d0a`. +This local MSVC gate gives no fresh-v3/GNU carrier or successor-workspace/CI +credit; predecessor receipts retain their original counts and archive pins. + +Owned operands still precede certified lexical retirement. A same-owner move +while its resident lease remains live is not covered by the distinct-survivor +success. That lifetime seam, broader Resource-valued Default/body compositions, +generic/nested reflection, aggregate live custody, fresh-archive predecessor +compatibility and coherent-workspace/exact-revision CI remain full-goal +obligations under unchanged Source semantics. The rough87% estimate and fixed +207/182 inventory denominators remain unchanged. diff --git a/docs/active/native_call_ownership_facts.md b/docs/active/native_call_ownership_facts.md index bdd3da51..681ea594 100644 --- a/docs/active/native_call_ownership_facts.md +++ b/docs/active/native_call_ownership_facts.md @@ -1,7 +1,10 @@ # Typed caller ownership: selected implementation contract -Status: integrated candidate in the isolated native-call-view-staging worktree, -based on `a9a6cd63db33fb29fb10f4b32607b3f82ab431bc`. An earlier formatted repair checkpoint +Status: caller repairs are committed at +`a63f891016fac7dc50fffdde33be8b0ce45c655f`. The complete Windows native suite +passes 480/480 with stable HEAD/tracked sources. Current full-workspace, +182-object inventory and supported-host acceptance remain separate obligations. +The integration began over `a9a6cd63db33fb29fb10f4b32607b3f82ab431bc`. An earlier formatted repair checkpoint passed all 304 checker, 188 HIR, 181 MIR, 67 backend and 63 object-emission tests, plus the MIR doctest. These counts describe that compiler-core gate. @@ -13,7 +16,8 @@ scalar/owned-collection group. The collection fixture corrections add explicit views to set/map observers and preserve their output oracles. All 64 sources also pass source checking. Ten breakpoint-context and three intrinsic owning-staging regression groups pass. These native replays preceded the formatting-only pass; -the compiler-core gate above follows it. A fresh complete native run is pending. +the compiler-core gate above follows it. The later complete 480-test receipt +below supersedes the historical 472/479 result. The bounded builder-generation and handled-producer repairs are now implemented. The actual focused gate passes all 66 MIR caller acquisition groups and all seven @@ -21,8 +25,12 @@ generation groups; three native emission groups and the original builder native regression also pass. A new converted handled-view linked fixture passes in both profiles after source deletion, observing exact payload lengths one and two. All seven failures from the historical 479-group run now pass individual replays. -A fresh complete run, full workspace, fixed inventory and supported-host acceptance -remain pending; these focused results are not a new complete-suite count. +The fresh complete Windows native suite now passes all 480 tests at exact a63, +with zero failures and stable HEAD/tracked sources, in +`target/native480-after-owner-generation/receipt.json` (root terminal `06b7f8`), +ending `2026-10-04T14:20:12.987780+00:00`. Full workspace, current fixed-inventory +and supported-host acceptance remain pending. The historical focused results +remain distinct from that new complete-suite count. Earlier integration receipts passed 598 frontend fixtures, 15 native caller groups with source deletion and exact debug/release streams, and both property replays @@ -32,7 +40,11 @@ exact-a9 baseline passed 464 native cases and all 182 object obligations; workfl receipts do not validate this candidate. This implements the existing Rule24 contract rather than a new source ownership -rule. No Resource carrier, provider, ticket runtime or finalizer acceptance follows. +rule. No Resource carrier, provider, ticket runtime or finalizer acceptance follows +from the caller/native receipt. A separate private reference Resource slice now +passes 36 focused tests, recorded in +[the resource-hook note](native_resource_hook_identity.md#reference-lifecycle-execution); +production providers and native Resource support remain disabled. The full 100% native-codegen objective and rough 85% feature estimate remain open. ## Existing authority and actual proof diff --git a/docs/active/native_codegen_parity_plan.md b/docs/active/native_codegen_parity_plan.md index 5bb0e60d..1804d762 100644 --- a/docs/active/native_codegen_parity_plan.md +++ b/docs/active/native_codegen_parity_plan.md @@ -320,6 +320,27 @@ nominal refinements. The remaining interface audit and cross-platform release gates are tracked in [native interface values](native_interface_values.md). Passing the existing fixture denominators alone does not close these gates. +[Exact concrete-owner preflight](native_erased_same_owner_arguments.md) now +validates erased direct/named-callback calls in the reference evaluator and +generated native dispatch before argument normalization or implementation body +entry. Matching owners preserve their view/owned modes; mismatches report +`interface dispatch concrete argument owner mismatch`. Retained nominal and +generic identities and primitive widths govern validation, with earlier argument +failure precedence and existing native unboxing guards preserved. + +The restored integration's normal Debug and Release runtimes each pass ten Owner +tests: thirteen original Source-deleted cases plus four production executions for +matching/mismatched owners with both Jett optimization settings, thirty-four +executions overall. All 2,331 workspace library tests pass, including six runtime +and eleven focused reference controls; twenty-seven broader interface tests pass. +Full coherent native, GNU, installed and exact-revision CI acceptance remain pending; +refinement and pending native Source combinations still need coverage. This +change preserves the checker's whole-parameter substitution: declared erased +children such as `list[Pairable]` retain their contract, and recursive Self +substitution is not introduced. Erased-interface equality remains unresolved. +These bounded gates do not change the rough 87% planning estimate, fixed +207-fixture/182-object inventory or whole-language completion requirement. + Explicit comptime values now retain each checked generic instantiation and lexical reflected type-binding chain, including nested callback bodies. The context fix and its differential regression coverage are recorded in @@ -347,12 +368,12 @@ below records implementation coverage; a row is complete only when its native object, linked execution, and differential behavior gates all pass. Typed lowering alone never changes an execution row to complete. -For planning, the broad native-language coverage estimate is **about 85%**. -Track it in five-percentage-point steps as the remaining surface gaps close. -This is a judgment based on the matrix, while the fixture counts below are -measured pass/denominator gates. -Later dated notes retain earlier 80% checkpoints as history; 85% is the -current estimate. +For planning, the broad native-language coverage estimate is **about 87%**. +Update it as concrete language families gain linked native coverage. This is a +judgment based on the matrix, while fixture counts remain measured +pass/denominator gates. Earlier 80%, 85% and 86% checkpoints below are history. +The latest increase follows the verified +[ordinary borrowed-sum family](native_ordinary_borrowed_sum_codegen.md). | Surface | Validated HIR/MIR | Cranelift object | Linked native behavior | | --- | --- | --- | --- | @@ -364,12 +385,12 @@ current estimate. | CSV | covered | checked parse, parse-with-header, and stringify leaves over owned lists and maps | strict quoting, CRLF, header values/errors, and nested allocation cleanup match interpreter | | Crypto | covered | private SHA-256, SHA-512, MD5, and HMAC-SHA-256 byte kernels | native differential fixture covers public text digests, binary HMAC, long keys, secret comparison, and explicit declassification | | Secret values | covered | transparent scalar and owned representations; redaction and string/bytes comparison | native differential fixture covers equal, unequal, length-mismatched and Unicode strings, bytes, redaction, and aggregate ownership | -| Results, optionals, and `handle` control flow | explicit CFG for statement-root, direct-call, indirect-call, supported intrinsic-argument, unary, cloneable binary, short-circuit boolean, collection, value-constructor, string-interpolation, machine-transition, task `run`/`join`, actor-spawn, actor-message, field-receiver, state-test, clone, coarsen, declassify, condition, match-scrutinee, for-iterable, assertion-condition, and breakpoint-condition handlers | genuine tags, owned payloads and selected extraction | nested sums, defaults, early returns, loop exits, terminal bypass, ordered call arguments and constructor fields, scalar, string, and enum evaluation order, short-circuit fallback skipping, handled field/state projections in `if`, `while`, and `match`, one-time for-iterable evaluation, handled debug conditions, ordered transition source/payload defaults, handled task values including pending `nothing`, actor source/argument order, and interpolated success/failure paths with ordered segment evaluation covered; other nested-expression and refinement handlers pending | +| Results, optionals, and `handle` control flow | explicit CFG for statement-root, direct-call, indirect-call, supported intrinsic-argument, unary, cloneable binary, short-circuit boolean, collection, value-constructor, string-interpolation, machine-transition, task `run`/`join`, actor-spawn, actor-message, field-receiver, state-test, clone, coarsen, declassify, condition, match-scrutinee, for-iterable, assertion-condition, and breakpoint-condition handlers | genuine tags, owned payloads, selected extraction and certified nonconsuming handled payload views | nested sums, defaults, early returns, loop exits, terminal bypass, ordered call arguments and constructor fields, scalar, string, and enum evaluation order, short-circuit fallback skipping, handled field/state projections in `if`, `while`, and `match`, one-time for-iterable evaluation, handled debug conditions, ordered transition source/payload defaults, handled task values including pending `nothing`, actor source/argument order, and interpolated success/failure paths with ordered segment evaluation covered; stable ordinary handled-view list payloads, copied primitive/String destinations, later argument failures and pending metadata now have 15-Source/30-native coverage; other carriers, outer Default lifetimes, nested-expression and refinement handlers remain pending | | Function values, closures, and indirect calls | covered; inline bodies extract to checked functions with explicit capture parameters and ownership modes | owned descriptors carry code addresses, copied capture environments, and source-derived debug labels; indirect calls pass the environment after the runtime context and borrow view parameters; parenthesized, returned, projected, inline, and pipeline callee expressions retain checked call order | named, capture-free, and captured callbacks passed, returned, copied through aggregates, and invoked through indirect calls, including view and capability parameters; callee handlers and terminal failures clean already evaluated owned arguments; explicit `comptime` materializes named, capture-free, and captured inline function values from closed pure expressions, including capability-accepting callbacks whose bodies execute only with runtime authority | | Compiler intrinsics and reflection | covered with checked operands, source-aware reflection metadata, and closed `IntrinsicId` identities | `type.name`, `type.kind`, `type.has_secret`, `type.kind_tag`, `type.primitive_tag`, recursively constructed `type.info`, checked `type.arg`, struct/bitfield, enum, and machine metadata lists and layouts, active enum variant and machine state metadata, reflected field values, and checked reflected-type dispatch covered; other aggregate reflection pending | direct and generic scalar reflection, nested `TypeInfo`, indexed type arguments, struct/bitfield, enum, and machine metadata, active enum and machine state selection, reflected field values, and alias-aware `comptime type` dispatch match the interpreter on positive cases; alias probes remain empty as required; mismatch diagnostics and other aggregate reflection pending | | Explicit `comptime` values | checked closed pure expressions and contextual expected types covered | evaluated scalar and supported composite values materialize as typed HIR; captured closure values gain typed caller-local bindings; original source bodies are not emitted | linked native/interpreter fixtures cover nested collections, sums, structs, bitfields, enums, machines, bytes, contextual `ok`/`fail`/`none`, named functions, capture-free inline functions, captured closures, and pending `nothing` values, including captured and list-contained values; runtime authority remains unsupported | | Capabilities and runtime resources | nominal checked types and static Resource copy/payload guards covered | entry grants for all eleven capability types; capability task handles retain authority and pending depth through `run`/`join`; provider effects remain limited to implemented families | Stdout output, Clock/Random sampling, and immutable Environment launch snapshots covered; native/interpreter fixtures compare plain and pending Stdout and Environment debug values, nested capability joins, Stdout cancellation, and a joined Stdout write; successful native runs reject unconsumed scripted Random, Clock, and Graphics inputs; other providers/resources pending | -| Actors and structured concurrency | covered | sequential `run`/`join`/`cancel` values and result propagation; pending depth for `nothing` persists through calls, captures, and aggregates, while owned strings, bytes, lists, sets, maps, optionals, results, structs, enums, bitfields, machines, function descriptors, and `TypeConstruction` builders retain pending depth through runtime handles; actor identities and capability authority retain depth through separate scalar handles; actor constructors, registration, handler dispatch, and state writeback covered; all fixed-width integer, floating-point, and boolean task carriers retain pending depth | native/interpreter differential probes cover successful and failed string tasks, nested pending `nothing` and owned values including optional and result branches, captured functions, builders, actor identities, and capabilities, bare-unit and plain Stdout join failure, current sequential cancellation behavior, pending-value debug output, string and enum direct equality errors, pending function-call, builder-use, and actor-message errors, and aggregate enum-payload equality, actor allocation and owned-state cleanup, message ordering, state mutation, and responses; asynchronous scheduling and cancellation checkpoints remain pending | +| Actors and structured concurrency | covered | sequential `run`/`join`/`cancel` values and result propagation; pending depth for `nothing` persists through calls, captures, and aggregates, while owned strings, bytes, lists, sets, maps, optionals, results, structs, enums, bitfields, machines, function descriptors, and `TypeConstruction` builders retain pending depth through runtime handles; actor identities and capability authority retain depth through separate scalar handles; actor constructors, registration, handler dispatch, and state writeback covered; all fixed-width integer, floating-point, and boolean task carriers retain pending depth | native/interpreter differential probes cover successful and failed string tasks, nested pending `nothing` and owned values including optional and result branches, captured functions, builders, actor identities, and capabilities, bare-unit and plain Stdout join failure, current sequential cancellation behavior, pending-value debug output, string and enum direct equality errors, pending function-call, builder-use, and actor-message errors, and aggregate enum-payload equality, direct ready-handle identity and recursive actor identity/pending-depth comparison, actor allocation and owned-state cleanup, message ordering, state mutation, and responses; asynchronous scheduling and cancellation checkpoints remain pending | | JSON and trusted stdlib hooks | covered | checked `JsonTree` calls use trusted raw stdlib functions; supported structs, machines, collections including sets of primitive-backed elements, and top-level refinements specialize the checked source serializer, including public omission of direct secret fields; supported top-level enums use dedicated checked source hooks; primitive parse calls use private source decoders, including bounded `int8`/`int16`/`int32`, `uint16`/`uint32`, and `float32` construction; concrete structs, bare and state-qualified machines, supported secret wrappers, top-level refinements over supported bases, lists, sets of primitive-backed hashable types, string-keyed maps, optionals, and results specialize the checked source parser; other JSON shapes remain pending | native/interpreter fixtures cover raw trees, primitive and structured serialization, enum unit and payload values, machine state envelopes and public secret omission, primitive and structured parsing, top-level refinement parsing and serialization, secret-bearing records and machines, exact validation, renamed fields, aliases, nested collections including lists/maps of enums and enum payload structs, result branches, errors, and owned cleanup; other concrete JSON types pending | | Trace, breakpoint, assert, and failure reporting | covered | primitive, capability, function, actor, `TypeConstruction`, and recursive list, set, map, optional, result, struct, bitfield, enum, and machine values trace; zero- and multi-binding breakpoints over those types; default and interpolated custom-message `assert` in test bodies; other special debug values pending | native debug lines match interpreter stderr for scalar and aggregate fixtures, including empty and recursive values, bitfield numeric/enum/payload fields, false conditions, Unicode strings, bytes, `nothing`, plain/pending capabilities, and an out-of-scope local; named, inline, captured, and comptime callbacks inside aggregates, with subsequent calls and failure cleanup; actor ordinals and partially filled struct, bitfield, enum, and machine builders match; custom assertion codegen, context-owned error text, launcher copy-out, and passing verify/property suites have separate tests; lexical breakpoint frames, nested-scope exit, and typed closure captures covered; recursive secret redaction covered, including inferred bindings, generic errors, and reflection failures | @@ -760,11 +781,18 @@ signed zero, and NaN. Payloads containing user structs now invoke exact checked frontend rejects missing methods and retains opaque method boundaries. See `docs/completed/enum_payload_struct_equality.md` for implementation and linked debug/release coverage. -Actor handles expose a separate equality mismatch: direct actor `==` is -accepted by the checker but fails in the interpreter, while recursive enum -equality compares actor IDs and native aggregate equality rejects the actor -payload. `docs/open_design/actor_equality.md` records the policy choices; -native actor-payload equality remains conservative until one rule is selected. +[Actor identity equality](actor_identity_equality.md) now implements live +instance identity in the reference evaluator and native backend. Exact checked +nominal actor types must match; clones preserve identity and separate spawns +remain unequal even with identical state. Recursive enum payload comparison +checks identity and per-handle pending depth without reading state, while direct +pending comparisons retain their unsupported binary-operation error. Local +MSVC static-CRT Debug/Release acceptance on 2026-10-12 passes nine Source-deleted +executables per runtime profile, including production output with both Jett +optimization settings. Escaping comptime actor graph import, full predecessor, +GNU, installed-distribution and coherent-revision acceptance remain open. This +closes the selected actor equality gate without changing the rough 87% estimate, +fixed 207-fixture/182-object inventory or whole-language completion requirement. Active `type.variant_value` selection matches native/interpreter output for payload and empty variants. Reflected `type.field_value` and `type.variant_field_value` now read checked struct, bitfield, and enum payload @@ -2300,8 +2328,310 @@ simultaneous cohorts and other owner kinds remain open. The [contract](native_call_ownership_facts.md) and [acceptance record](native_acceptance_audit.md#typed-caller-ownership-integration) -separate these focused results from pending full-workspace, fresh complete native -corpus, 182-object inventory and supported-host checks. The a9 predecessor is accepted locally +separate these focused results from the later complete Windows native receipt: +480/480 pass at exact committed `a63f891016fac7dc50fffdde33be8b0ce45c655f`, +with stable HEAD/tracked sources in +`target/native480-after-owner-generation/receipt.json`, ending +`2026-10-04T14:20:12.987780+00:00`. Resource changes are absent. Full-workspace, +current 182-object inventory and supported-host acceptance remain pending. The a9 predecessor is accepted locally and on all four supported-host jobs. The fixed 207 inventory, about 85% tracking -and 100% whole-language goal remain unchanged. Actual Resource lifecycle and -provider dispatch, followed by native ownership/cleanup, are the next work. +and 100% whole-language goal remain unchanged. A separate private reference Resource lifecycle now passes 36 focused tests, +including actual Source cleanup and required-worker controls. Production +provider dispatch and native ownership/cleanup remain required. + + +### Private reference Resource lifecycle + +The exact checked program now supports private reference hook descriptors, +runtime-purpose invocation gates, non-cloneable owning tickets and ordered +custody through Source calls, scopes, temporaries and occupied results. +Ordinary/mutual declarations rejoin their retained executable AST and exact +resolver metadata; registration clones cannot replace checked bodies. Caller +source order and formal permutation remain separate. Explicit close and reverse +cleanup retire owners before context teardown. Cleanup failure overrides entry +failure; clean cleanup preserves an ordinary entry error and source result.fail +remains data. The public production catalog and provider remain disabled. + +Root terminal `923450` passes all 36 Resource-focused reference tests in +`target/native-resource-after-checked-call-relay-focused.log`, including real +both-profile Source lifecycles, failed later actuals, indirect close, terminal +failure and required-worker/purpose controls. This is separate from the committed +caller native480 receipt and adds no native inventory case. Broad library, +full-workspace and supported-host acceptance of this Resource candidate remain +pending. See [the exact scope](native_resource_hook_identity.md#reference-lifecycle-execution). +Whole scoped/reflected/indirect/worker and aggregate ownership breadth, real +providers, native hook/carrier/drop integration and task/actor cancellation remain +required. The about-85% estimate, fixed 207/182 inventory and 100% goal are unchanged. + +### Source plain Resource replacement + +Native assignment now executes exact checked plain Resource replacement, self-reseat and vacant-after-close transfer. Six new shared scenarios extend the original five; all 11 pass in both real-reference profiles and as generated native programs after Source deletion. Runtime preflight preserves invalid inputs before finalization, and failure tests distinguish an ordinary RHS error from old-finalizer cleanup failure. The formatted compiler gate passes 558 checks plus the MIR doctest and606 frontend/lowering checks pass. One temporary capture failure in the broad driver-library run passes isolated replay; clean full-library, complete native regression and supported-host results remain separate gates. Conditional/aggregate/projected replacement and broader Resource/provider/async/reflection work remain required. The coarse estimate remains about 85%, with fixed 207 fixtures and 182 object obligations unchanged. + +### Handled Resource call progress (2026-10-05) + +The local native Resource gate now includes eighteen shared original Source +scenarios in both profiles (36 native and 36 reference executions). Direct Result +handlers inside call arguments preserve lexical evaluation, prepared formals, +acquired-prefix cleanup, retained views, clean skipped invocations and observed +cleanup failure. Nested first-factory success/failure also distinguish Scope +storage from the current execution frame. Every native case runs after Source +deletion and observes zero obligations before destroy. + +The formatted affected compiler/object gate passes 573 checks plus the MIR +doctest, with 606 frontend/lowering fixture checks, all four fixed inventory +gates (182 emitted host objects), and the complete 92-test driver library. +The final formatted Source replay passes all 36 native and 36 reference runs. +This advances the Resource row without closing its remaining indirect, aggregate, refinement and broader +control-flow obligations. The broad estimate remains about 85%; full current +native/workspace and supported-host release gates remain separate requirements. + +### Callable cleanup boundaries (2026-10-06) + +Three new original Source programs extend the shared lifecycle gate from 18 to +22 scenarios. Both reference and native profiles pass all 44 executions each; +native launch follows Source deletion and observes exact reports and zero +obligations before teardown. The additions cover reversed lexical/formal order +with two temporary view owners, a Return helper using an independent owner, +a continuing default, and cleanup panic suppressing the Return helper. + +The reference evaluator now retires the pending call prefix before evaluating +the original Return operand. Ordinary closure and named-callable bodies have +their own operation floor and no inherited Resource return destination. Native +Source joins retain authenticated terminal failure outcomes, keeping original +body status separate from cleanup instead of replacing status 255 with 1. + +The affected gates pass all 508 reference-library tests, all 92 driver-library +tests, and all 222 runtime tests (209 unit and 13 integration). Six private +Return-prefix controls and eleven focused native Source controls are included +in those totals. Fresh matched debug/release archives contain one main, eight +private exports and all 32 Resource leaves each, with the unchanged ABI/wire. + +Parent a30ca97e's exact source tree passed the complete workspace on GNU and +MSVC, including 486 default native tests per host and both source-absent installed +package jobs. The local full run had 26 deadline failures; all 26 subsequently +passed at the same deadlines, in a 25-case replay and an isolated inventory +replay. This is not a clean full local run. The successor's complete workspace +and supported-host checks remain separate release gates. Overall tracking stays +about 85%; indirect/aggregate/refinement Resource calls, broader projections and +reflection, production providers and shared scheduling remain in the full goal. + +### Named Resource function values, 2026-10-06 + +Exact named non-generic, capability-free callbacks now retain their original +Indirect Source authority through immutable root-scope locals and aliases. A +private original/current producer/use proof selects the retained body; native +code and empty-environment checks precede the existing hidden-Scope Source +bridge. Ordinary descriptor storage is admitted only for exact proved values, +with public signature refusals and raw hook descriptor handling unchanged. + +The shared Resource corpus increases from 22 to 28 scenarios. All 56 genuine reference +and 56 linked native executions pass in both profiles, including distinct +same-signature body selection and separate body/cleanup failure propagation. +The complete reference library passes 511 tests, MIR 233, native codegen 93 and +object emission 63. Runtime sources/archives are unchanged; native Source is +removed before launch and obligations retire before teardown. The complete +driver library passes 92 tests; ordinary callable regression groups pass +13 function, one captured-closure, one collection-callback and 16 caller-ownership +tests. Parent a5e33b1f passed the full workspace and 486 default native tests on +both GNU and MSVC, plus both Source-absent installed package jobs. Its tested +merge has the identical source tree. Fresh supported-host acceptance of this +named-callback successor remains a separate gate. +Namespace-member/returned/captured/scoped/generic/handled/pipeline forms, +aggregate/refinement Resource paths and production provider/concurrency work +remain required. The coarse 85% estimate, 207-fixture/182-object inventory and +whole-language 100% goal remain unchanged. + +### Namespace-qualified Resource callbacks, 2026-10-06 + +The interpreter now joins a qualified function value to its exact full-span +resolved declaration and retained checked header/signature. Instance fields, +method adapters, generic functions and compiler hooks cannot mint named-body +proof. Original expression identity, alias chain, body attempt and indirect +argument tuple remain authenticated. + +Two new Source programs and four shared oracles grow the Resource corpus from +28 to 32 scenarios. All 64 reference and 64 Source-absent linked native executions +pass across debug and release, including alias close, handled factory failure +and cleanup panic. Seven callback-proof tests and the complete 515-test reference +library pass; the complete driver library passes 92 tests. Fresh supported-host +acceptance remains a separate gate. Runtime sources and matched archives are +unchanged. + +Returned Resource hook callbacks are the next executed baseline. MIR currently +loses exact hook identity across a factory return, and native invocation still +refuses the descriptor route. Existing runtime descriptor validation and frame +staging provide a compatible ABI path; exact return provenance, opaque carrier +transport and emitted descriptor invocation rows remain to implement. Dynamic, +captured/scoped/generic/handled/pipeline/required callbacks, aggregates, providers +and concurrency remain required. Overall tracking remains about 85%. + + +### Returned compiler-hook callbacks, 2026-10-06 + +Exact returned Factory, Borrow and Close descriptors now pass all 46 shared +Resource scenarios in both reference and Source-absent linked native profiles: +92 executions each, including stored/alias/relay/unused metadata, immediate +callees, observable argument-before-callee order and factory/actual/callee/ +finalizer failure cleanup. Immutable return provenance and independent current +body/entry/header proofs survive canonical remaps. Copied/resealed pruning +regressions are refused before evidence can be erased. Returned metadata is +installation-owned; only exact proved occurrences bypass ordinary Function +storage. Existing wire-v2/runtime leaves and matched archives remain unchanged. + +Nine focused MIR groups and 242 MIR library tests pass. Seven focused codegen +groups, all 100 codegen library tests and 63 object tests pass. Authenticated +Resource roots now seed typed reachability before walking ordinary dependencies; +pure helpers retain their normal ABI. All 92 driver-library tests pass; fresh +supported-host gates remain pending. See +[the contract and remaining forms](native_returned_resource_hooks.md). +Required/comptime materialization, returned named callbacks, dynamic/captured/ +scoped/generic/handled/pipeline forms, nested returns, aggregates and production +providers/concurrency remain required. The coarse estimate remains about 85%, +the fixed inventory remains 207 fixtures/182 objects, and the whole-language +100% goal is active. + +### Required Resource descriptors: candidate extension + +The shared original Source corpus has 60 candidate scenarios (46 previously +accepted plus fourteen new cases across eleven Source files). The reference +oracle now runs the real provider-free required worker, installs its same-program +checked cache before runtime registration, and refuses missing or failed required +evaluation. All 120 debug/release reference executions pass. The initial native +baseline fails on the first new comptime descriptor with pending nested Resource +evaluation requiring exact Source-order staging. + +Implementation in progress uses opaque private checked required proofs and an +authenticated materialized HIR view that preserves original Source. New candidate +cases cover Factory/Borrow/Close, factory and cleanup failure, aliases, relays, +unused storage, immediate invocation, absence, pure conditional selection and +generic/scoped selectors. They do not count as accepted native execution until +the same cases pass both profiles with source absent, exact event/status oracles +and zero obligations before teardown. The whole-language estimate remains about +85%, and the full completion obligations remain active. + + +### Verified required Resource values and ordinary helper seals (2026-10-06) + +The shared original Source corpus now contains 61 scenarios. All 122 reference +and 122 source-absent native executions pass in both profiles against the +unchanged matched MSVC compiler-test archives. Required capture and materialization +use the actual driver seams; acceptance also includes production HIR value +conversion and general MIR validation before native object emission. + +Opaque private-cache proofs retain the exact checked program, original owner, +required region, specialization and scoped selection. The original archive is +immutable. Every required row seals its current body, including ordinary runtime +helpers that retain their normal ABI and receive no Resource execution authority. +Exact guarded sum arms are both planned; runtime absence still skips extraction +and finalization. Disconnected Resource headers and normalized operation nodes +remain custody obligations. + +Nine focused MIR controls, 251 MIR library tests and the driver foreign-context +control pass. Full native coverage remains active: qualified/complex required +transport, broader callbacks/aggregates/views/reflection, production providers, +concurrency, GNU private-archive execution and final coherent workspace/platform +acceptance are still required. The rough tracking estimate remains about 85%. + +The complete affected codegen suite passes 100 library and 63 object tests; +all 93 driver-library tests also pass. Final Rust/Jett formatting and whitespace +checks pass, and all 2,168 frozen source/config inputs revalidate unchanged. + +### Verified borrowed optional/result Resource payloads (2026-10-07) + +Sources 45..56 add twelve scenarios covering written/bare None/Some/Fail/Ok, +named-argument prefix abort, and handled borrow-domain failure. The unchanged +baseline passed all 73 shared scenarios / 146 reference executions but refused +Source45 at HIR stable-origin admission. The complete corpus now passes 146 +Source-deleted native executions against freshly measured Debug/Release MSVC +private runtime archives, with exact events and zero obligations before teardown. + +All 305 checker, 203 HIR, 254 MIR and 215 runtime library tests pass. Three focused +MIR groups and six runtime groups cover original-body custody, guarded projections, +conditional shell lifetimes, failure companions, argument aborts and forgery. +The full codegen suite passes 102 library and 63 object tests, including unchanged +prior invalid-MIR controls. Archives contain 37 Resource leaves, eight private +exports and one main; independent source/symbol/CRT receipts remain mandatory. + +Admission is limited to root-body Resource aliases. Nested/scoped aliases, +ordinary non-Resource borrowed sums and alternate Default payload lifetimes remain +pending. The rough full-language estimate stays about 85%, and the full 100% +native-codegen goal, GNU Resource gate and broader language/platform requirements +remain open. See [the integration contract](native_resource_borrowed_sums.md). + +All 521 reference-evaluator and 93 driver-library tests pass. The existing +Source33 required-value assertion now verifies its exact two original proofs, +including scalar 761 and the unique checked hook; other fixtures retain exact +one-value counts. This separately fixes the sole failure observed on both hosts +at 17d6c841. Rust and all twelve new Jett Source format checks pass, and the +2,188 frozen source/config inputs revalidate. The final MIR-only formatting change +is exactly one error-return line wrap and passes all 254 MIR tests again. + +### Selected lazy reflected callable return validation + +A checked reflected callable request may establish supported new return +refinements through an owned lazy adapter. Exact owner, member, field ordinal, +pending-selector and declared-schema checks precede construction. The adapter +retains the original stored signature, source body, captures, debug identity +and pending state; constructing or reading it invokes neither callback nor +predicate. Requested parameter, view and capability contracts must satisfy the +existing ordinary callable adapter relation. A descriptor or requested type +annotation grants no proof, effect authority or nominal conversion. + +Each invocation evaluates its actuals once, invokes the retained source once +under the original signature, and preserves the source's ordinary validation. +Only after the source result exists does the supported typed produced-refinement +conversion establish the unproved requested return suffix. Failed validation +uses the existing terminal refinement-boundary error and publishes no trusted +target result. Exact established prefixes are reused. The adapter never joins +implicitly or inspects pending payload; a pending result requiring a new check +reaches the existing pending-inspection refusal before predicates or consumers, +while exact already-proved pending transport remains unchanged. + +On 2026-10-12 the managed integration passes all eighteen original Sources, +including the byte-exact wider/exact probes, named/captured callbacks, lazy reads, +repeated calls, invalid results, selector precedence, optional results and +Nothing/refined-unit pending transport. Their thirty-six Source-deleted native +executions pass against normal MSVC Debug/Release Main archives, with the +original release=false Source/debug policy and unoptimized objects preserved. +Three additive canonical Stdout controls pass six objects and twelve executions +across both Jett optimization settings and both runtime profiles: lazy repeated +calls preserve the source descriptor; invalid returns suppress the consumer; +wrong selector ownership refuses before callback body output. The four Driver +gates compare exact stdout, stderr and exit status. The ordinary public launcher +does not expose context-retirement counters. + +The current fourteen focused reference controls preserve every original thirteen +test body and add a factory/primitive-call origin-restoration control. Original +closure body and typed capture/context witnesses are constructed at the trusted +capture site. Admission and invocation compare against retained witnesses; +mutating the body, checked context, signature, ordinal, Source owner or predicate +cannot mint proof. Ordinary required constants/refinements and already-proved +ancestor transport retain their existing path. + +Capture-origin preparation now skips an unused checked-root projection only when +the exhaustive named-body scan finds no possible inline capture origin. Inline +setup stays inside its function-value arm; InlineFn, Comptime, Error and +ComptimeTypeBind remain conservative positive shapes. Reflected authentication +and pending capture snapshots remain unchanged. One isolated, unchanged native +reflection control takes 295.06 seconds before and 64.86 seconds after this +change; this measures that gate's wall time rather than general native throughput. + +After this performance change, all 2,359 workspace library tests across 22 result +groups pass, together with the four callable Driver gates (48 native executions), +27 broader interface, 10 erased-owner and nine actor native regression tests. +Fresh current compiler/source/binary/dep-info guards also accept all sixteen +private Resource gates: 754 Source-deleted executions and 900 entry completions. +Only the unchanged, independently measured v4 Debug/Release ABI1/wire3 static-CRT +runtime archives are reused, with compiled receipt pin +`bc2ad54743380441f074d8d5d212048ecf440669817c0cb69ce2fd0810ba1c95`. +The fresh post-change gate and complete source guard supply current acceptance; +old v4 execution records do not. Each Rust gate checks its existing exact +completion, script/event, message and cleanup oracles before one teardown. + +This is bounded local MSVC integration acceptance. Full coherent-workspace and +native conformance gates, exact-revision supported-host CI, GNU and installed +distribution remain pending. New nominal/secret proof establishment, protected +secret observation, broader generic/reflected forms and authenticated generated +verify/property-suite transitions remain full-goal obligations. No Source +exclusion or broad language-completion claim follows: the rough 87% estimate, +fixed 207-fixture/182-object inventory and full 100% goal remain unchanged. diff --git a/docs/active/native_erased_same_owner_arguments.md b/docs/active/native_erased_same_owner_arguments.md new file mode 100644 index 00000000..2269be00 --- /dev/null +++ b/docs/active/native_erased_same_owner_arguments.md @@ -0,0 +1,72 @@ +# Exact concrete arguments for erased interface dispatch + +Erased interface calls validate additional concrete argument requirements against +the receiver-selected implementation before conversion or method body entry. +Different nominal types remain different even when their fields have identical +layouts. The selected runtime contract was recorded before implementation on +2026-10-08; this does not define equality operators on erased interfaces. + +The original wrong-owner Source05 now reports +`interface dispatch concrete argument owner mismatch` before its Left body. +Original matching-owner Source12 retains `left:one:two`. The fifteen original +parity Sources remain unchanged; the two required fixtures restored with this +slice retain their exact recovered bytes. + +## Implementation + +Reference direct and named-callback calls preflight already evaluated arguments +before installing callee scopes, selecting body metadata or normalizing values. +The selected declaration namespace and type substitutions preserve nominal +refinements, full generic arguments and primitive widths. + +Native dispatch borrows already evaluated formal locals and checks every required +finite conversion descriptor before conversion. The additive `InterfaceValidate` +leaf checks retained exact TypeIds and occupied container/sum arms without cloning, +changing native value storage or transferring custody. Descriptor parsing can +allocate Rust `Vec`/`Box` storage. Existing unboxing and malformed-handle checks +remain mandatory; validation failure follows the existing cleanup path. + +Pending receivers preserve canonical undefined-dispatch failure. Direct pending +identity can be checked without joining or reading fields; unavailable containers +provide no child-owner facts. Earlier argument failures keep their message and +output order. Matching view and owning argument modes remain unchanged. + +## Current integration verification + +On 2026-10-12 the restored Actor/Resource/Owner integration passes all 2,331 +workspace library tests, including eleven focused reference and six native runtime +preflight controls. Broader native interface regressions pass. Rust formatting and +whitespace checks pass. + +Normal MSVC static-CRT Debug and Release launcher archives each pass ten native +Owner tests. The original nine tests execute thirteen Source-deleted cases per +runtime profile with unoptimized Jett objects. An additional production `Stdout` +test executes matching and mismatched owners with both Jett optimization settings: +four more executions per profile, seventeen per profile and thirty-four overall. +Its release-policy builds preserve successful production output or the exact +owner-mismatch error and exit without requiring a debug-output exception. + +All original Source output, body-entry witness, evaluation-order, pending failure +precedence, expected statuses and cleanup assertions remain operation-specific. +The tests compile before removing the Source and launch fresh native processes. +Normal launcher builds use current runtime sources, including the additive leaf. + +## Remaining scope + +The checker substitutes the selected owner when an entire interface parameter is +the interface type. It does not recursively substitute Self inside +`list[Pairable]`; that declared erased-child contract remains unchanged. Nested +source coverage uses a concrete collection owner such as `list[optional[Left]]`. +Helper-level recursive checks do not establish unsupported Source signatures. + +Refinement and pending-value preflight controls are reference/runtime tests; +the current native Source suite does not establish their full linked coverage. +The optimized production control covers the original matching/mismatched pair, +not every nested/generic witness. GNU, installed-distribution and complete +coherent-revision native parity remain open. This checkpoint does not establish +normal Main Resource cleanup counters, a public Resource provider or full-goal +completion. The rough 87% estimate and fixed inventory remain unchanged. + +Historical isolated 3,640-test and twenty-six-execution records describe earlier +candidate work. Their target receipts were discarded during disk cleanup; those +records are not acceptance evidence for this integration. diff --git a/docs/active/native_named_resource_callbacks.md b/docs/active/native_named_resource_callbacks.md new file mode 100644 index 00000000..51e9120b --- /dev/null +++ b/docs/active/native_named_resource_callbacks.md @@ -0,0 +1,79 @@ +# Named Resource callbacks + +## Selected implementation contract + +The next native parity milestone is an immutable function value initialized from +one original named, non-generic, capability-free function. Passing an owned +Resource to that value is existing Jett syntax. This work changes backend support; +it introduces no source-language restriction or new callable syntax. + +The original checked invocation stays indirect. A separate private proof selects +the exact retained declaration from the original callable initializer and its +current use. Signature equality, a public function name or an edited MIR pointer +cannot select another body. Immutable alias chains preserve exact provenance; +ambiguous joins, reassignment and escapes require their own proved transport. + +Reference execution must use the retained checked body and existing argument +custody envelopes, Return destination and operation floor. Native execution must +validate the selected code pointer and empty environment before entering the +existing Resource Scope bridge. Ordinary callable invocation omits that hidden +Scope and cannot be used for a Resource family target. Refusal must clean any +already acquired argument prefix without entering or publishing the wrong body. + +## Original Source baseline, 2026-10-06 + +The shared `18_indirect_named_close.jett` parses, resolves and checks. Its named +`close_selected` owns `resource_probe.TestHandle`, calls the checked close wrapper, +and is bound to immutable `dispose` before construction of token 701. + +The genuine reference gate reaches the first new scenario and fails with +`Resource source binding has no live cleanup custody`. The native gate reaches it +and fails MIR validation with `indirect custody call lacks an exact descriptor +producer`. These are executed failures, not accepted behavior. Baseline logs and +Source snapshots are retained in +`target/native-resource-indirect-named-source-integration/`. + +## Required acceptance + +The shared oracles add successful named close, handled construction failure, +ordinary target-body failure, close finalizer failure, and combined body/cleanup +failure. Original body status must survive cleanup failure, while cleanup +diagnostics take precedence. +A same-signature `wrong_target` performs an ordinary terminal failure; a separate +legal Source selects it and must execute that failure while retiring the Resource. +Private target-swap controls must refuse substituting that body into the checked +close program. This distinguishes exact body selection from matching signatures. + +Both profiles must pass genuine reference and linked native execution, including +native execution after deleting the project and synthetic stdlib Source. Provider +events, body versus cleanup status, diagnostics, zero remaining obligations and +exactly-once teardown remain shared acceptance requirements. Existing 22 scenarios +and ordinary callable regressions stay required. Runtime ABI changes are not +planned; any runtime change requires newly built, measured matching archives. + +## First reference gate + +The new three private named-body tests pass, and the complete reference library +passes 511 tests. The shared gate executes all 28 scenarios in both profiles, +including exact target selection, immutable aliases and combined body/cleanup +failure: 56 reference executions with zero live provider obligations before +teardown. The corrected complete MIR suite passes 233 tests; native codegen passes 93 +unit tests and 63 object-emission tests. The linked native gate passes all 28 +scenarios in both profiles (56 executions), with Source absent and zero live +obligations before teardown. Existing hook descriptor storage remains unchanged; +the earlier broader companion predicate was rejected by its regression test. +Runtime sources and both measured archives still match receipt `bf0c7805`. +The complete driver library passes 92 tests. Ordinary callable regression groups +pass 13 function tests, one captured-closure test, one collection-callback test +and 16 caller-ownership tests. Fresh supported-host acceptance of this successor +remains a separate gate. + +## Remaining full-goal work + +Qualified namespace members are covered by the subsequent +[namespace callback proof](native_namespace_resource_callbacks.md). Returned +compiler hook descriptors, dynamic and captured callables, scoped and generic +bodies, reflected/pipeline/handled invocation and occupied aggregate +Resource paths remain required. Reference fixture07 returns a compiler hook +descriptor and does not prove named Source callback support. This milestone does +not count those separate invocation forms as covered. diff --git a/docs/active/native_namespace_resource_callbacks.md b/docs/active/native_namespace_resource_callbacks.md new file mode 100644 index 00000000..5dc58f1e --- /dev/null +++ b/docs/active/native_namespace_resource_callbacks.md @@ -0,0 +1,39 @@ +# Namespace-qualified Resource callbacks + +A namespace-qualified function value must select its exact resolved declaration +and retained checked body, just like an unqualified named function. The original +call remains indirect. Namespace members resolve at the whole expression span; +field spelling or a matching function signature cannot grant that authority. +Instance fields, method adapters and compiler hooks need their own transport and +must not enter this named-body path merely because they use dotted syntax. + +The genuine shared Source programs initialize an immutable callback from +`resource_probe.close`, then call it directly or through an immutable alias. +The shared corpus grows from 28 to 32 scenarios: successful close, handled factory +failure, finalizer failure and alias close. Both profiles require exact events, +separate body/cleanup reports, Source-absent native launch and zero obligations +before teardown. The baseline reference gate fails on the first qualified +callback with `resource value lost its checked payload custody`. The actual +native gate passes all 64 executions. A corrected test filter selects the one +shared Resource group; an earlier zero-test invocation is excluded. Runtime +sources and measured archives are unchanged. + +This closes one existing callable provenance seam. Returned hook descriptors, +dynamic/captured/scoped/generic/handled/pipeline callbacks, aggregate Resource +ownership, production providers and scheduling remain in the full goal. + +## Verified reference and native execution + +Reference proof now admits the original qualified producer only through its +full-span resolved Function definition, unique retained non-generic header and +exact checked Function signature. The original expression occurrence, body +attempt, immutable alias chain and indirect invocation packet remain checked. +Member spelling and runtime function names cannot supply body authority. + +All seven named-callable proof tests and all 515 reference-library tests pass, +including all 64 shared Resource executions. Genuine instance-field, method-value, +generic and compiler-hook controls cannot mint named-body proof. Copied, foreign, +same-signature target, signature and producer-as-invocation substitutions fail. +The final linked native gate passes all 64 executions with Source absent and zero +obligations before teardown. The complete driver library also passes 92 tests. +Fresh supported-host acceptance remains a separate gate. The broad native-language estimate stays about 85%. diff --git a/docs/active/native_ordinary_borrowed_sum_codegen.md b/docs/active/native_ordinary_borrowed_sum_codegen.md new file mode 100644 index 00000000..5e5cd875 --- /dev/null +++ b/docs/active/native_ordinary_borrowed_sum_codegen.md @@ -0,0 +1,110 @@ +# Native ordinary borrowed optional/result payloads + +The native-codegen goal remains active. This change implements the existing +ordinary handled-view family; it does not establish whole-language parity. +The [Source-only baseline](native_ordinary_borrowed_sums.md) retains its original +12 reference passes and 24 native unsupported-handler refusals as history. + +## Contract and implementation + +The canonical initializer is an explicit outer `view` of a Handle targeting an +explicit `view` of one stable immutable sum local or View parameter. The absent +continuation ends with Return. A Some/Ok move-only payload keeps its exact checked +type and backing dependency without a clone, sum transfer, or owning cleanup +slot. Forwarded, nested and loop aliases preserve that dependency. Explicit +`clone` still acquires independent ownership. + +Implicitly copyable final destinations keep ordinary copy semantics. Primitive +values are copied; String owns its retained copy. A copied destination may be +mutable and rebound. After initialization, generated projection intermediates +leave the borrow inventory, allowing a later ordinary transfer or owning unwrap +of the original sum. This does not expire a persistent move-only alias or permit +changing its backing owner. A Result Fail String companion likewise owns one +retained copy for its handler. + +MIR seals the original declaration, current headers, backing identity, tag +observation, selecting branch, success/failure projections and exact final +initializer. Canonical remapping authenticates both the prior and resulting +graphs; exact no-op transitions preserve proof identity. Missing, copied, +foreign, resealed or disconnected witnesses cannot authorize payload views. +Native admission validates this proof before consuming sum lowering. + +The nonconsuming `jett_rt_v1_sum_payload_borrow` leaf returns the selected payload +bits without allocation, clone, retain, adoption or mutation. Generated source +and success payload locals remain nonowning. Only the checked copy destination +acquires ordinary ownership. Scalar pending depth and opaque container metadata +are preserved. Outer pending sums fail before extraction or continuation; +ready sums retain later payload-observer readiness checks. + +This bounded proof grants no Resource custody. Alternate outer Default payload +lifetimes, mutable backing origins, temporary-backed aliases, general loan expiry +and payload identity hidden by erased interfaces, callables, actors or capability +carriers remain separate work. Nested scalar Handle defaults retain their +existing semantics. + +## Measured acceptance + +The coherent candidate based on `c453297d` passes: + +| Gate | Result | +| --- | --- | +| Original frozen list-payload packet | 12 checked reference Sources; 24 Source-deleted debug/release native executions | +| Separate supplemental packet | 3 checked reference Sources; 6 Source-deleted debug/release native executions | +| MIR and native codegen | 276 MIR, 107 codegen, 63 object and 1 CFG checks | +| Runtime | 223 checks, including 8 nonconsuming payload controls | +| Driver library | 93 checks | +| Resource regression with fresh matched runtime archives | 138 scenarios; 276 Source-deleted debug/release native executions | + +The final combined Source gate passes all four Rust groups in 79.12 seconds. +It compares complete status/stdout/stderr and requires empty debug streams. +The Sources cover Some/None and Ok/Fail, stable owned/View-parameter roots, +forwarding, nested aliases, real loops, clones, named argument-prefix Return and +terminal failure, outer pending sums and pending payloads. Supplemental cases +add a later argument Handle Default failure after a staged alias, plus mutable +copied integer/String bindings followed by original owning unwrap. They preserve +the original Sources, runner and baseline note; their parent registration is +an additive change, separately recorded from the frozen baseline artifact. + +The final compiler/runtime/Source snapshot contains 2257 inputs, independently +revalidated after execution. Its manifest SHA256 is +`20c4c6b5ea49b94f16ed8f68b6db4aa913909227cbfce8571e403d78077e8a94`. +Evidence is retained under `target/native-ordinary-borrowed-sum-candidate-evidence/` +in `acceptance-v7.json`, `driver-all-packets-v7.log`, `mir-codegen-full-v5.log` +and `fresh-runtime-resource-acceptance.json`. + +Fresh Resource archive receipt SHA256 is +`29dd83c7f3874d7fe4b4fd3a5784786a5b31ee45337e9f40f133da9f33316d42`. +The receipt is pinned at test compilation; runtime environment chooses only its +path. The 138-scenario gate passes in 761.14 seconds with Source absent at launch, +exact reference events/status/messages and zero obligations before teardown. +Both archives, all nested receipts, the frozen test executable and current +compiler/runtime/Source inputs are rehashed after the run. Subsequent changes to +compiled inputs are limited to supplemental tests, their formatting, and a +codegen test symbol lookup; documentation changes do not affect that gate. + +Reproduce the public gates from the repository: + +```text +cargo test --locked -p jett_mir -p jett_codegen_cranelift +cargo test --locked -p jett_runtime +cargo test --locked -p jett_driver --lib +cargo test --locked -p jett_driver --test native_conformance ordinary_borrowed_sums -- --nocapture --test-threads=1 +``` + +The private Resource gate additionally needs the independently measured archive +receipt and its compile-time pin. See [archive measurement](native_resource_gnu_gate.md) +and the [Resource runner](../../crates/jett_driver/tests/native_conformance/resource_execution.rs). + +## Remaining release gates + +The estimate advances from about 86% to about 87% as this concrete family gains +linked native coverage. It is a planning estimate, not measured code coverage; +the fixed 207-fixture/182-object inventory denominators do not change. +The clean committed `2c9f9aa8` workspace passes 3,446 tests across 92 result +groups, with every one of 3,637 tracked inputs unchanged. Its [exact-revision +workflow](https://github.com/vycdev/jett/actions/runs/37673804359) passes all four +supported-host jobs, including private GNU Source execution. These gates are +accepted for this compiler/runtime revision. Later Source packets retain their +own complete-corpus and platform requirements. The complete 100% objective and +all other semantic, ordinary-carrier, Resource and reflection obligations remain +open in the [acceptance audit](native_acceptance_audit.md). diff --git a/docs/active/native_ordinary_borrowed_sums.md b/docs/active/native_ordinary_borrowed_sums.md new file mode 100644 index 00000000..63925bfc --- /dev/null +++ b/docs/active/native_ordinary_borrowed_sums.md @@ -0,0 +1,133 @@ +# Ordinary borrowed optional/result payloads + +## Source-only baseline + +This packet isolates the existing public `optional[list[int64]]` and +`result[list[int64], string]` language family. It changes no compiler, runtime, +stdlib, language policy, or production representation. The baseline production +revision is `35bef5ea63cc8c28eb0c45edcf15fcd827a8f3b9`, in the separate +`codex/ordinary-borrowed-sums` worktree. + +The canonical payload alias is an explicit outer View of a Handle whose target +is an explicit View of one stable immutable sum local or View parameter: + +```jett +list[int64] alias = view((view source) handle: + return nothing +) +``` + +A Result uses `handle error:`. The checked alias retains the exact backing +binding. The occupied arm exposes a nonowning payload; the absent arm ends with +Return and creates no payload alias. A nested expression-level Handle can have +its own scalar Default. This packet does not select an alternate outer Default +payload lifetime, owner mutation, owner consumption after alias creation, or +loan expiry. + +## Frozen packet scope + +The 12 independent Sources are in +`crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/`. + +| Source | Concrete behavior | Reference outcome | +|---|---|---| +| `01_optional_branches.jett` | Some/None across written and bare owned actuals to a View sum formal; repeated occupied aliases; nested scalar Handle Default inside the None continuation | success, exact branch markers, lengths and sums | +| `02_result_branches.jett` | Ok/Fail across written and bare owned actuals; repeated occupied aliases and exact string failure payload | success, exact branch markers, lengths and sums | +| `03_owned_scoped_forwarded.jett` | Immutable owned sum roots, forwarded/nested/loop aliases, explicit independent clones and repeated backing reads; scalar/list generic instances | success, exact sums and generic reachability markers | +| `04_named_prefix_return.jett` | Both kinds; original alias View actual evaluated before a later handler Return, with lexical named order differing from formal order; retained caller roots read again | success, exact argument-order and return markers; consuming body absent | +| `05_optional_terminal_prefix.jett` | Optional alias View actual staged before a later terminal argument failure; other owners retained | exact terminal failure after the occupied alias prefix | +| `06_result_terminal_prefix.jett` | Result counterpart of Source05 | exact terminal failure after the occupied alias prefix | +| `07_outer_pending_some.jett` | Twice-pending Some sum | Handle fails before handler or alias observation | +| `08_outer_pending_none.jett` | Twice-pending None sum | same boundary, no failure continuation | +| `09_outer_pending_ok.jett` | Twice-pending Ok sum | same boundary | +| `10_outer_pending_fail.jett` | Twice-pending Fail sum with string companion | same boundary | +| `11_pending_optional_payload.jett` | Ready Some containing a twice-pending list; alias forwarding succeeds | payload observer fails after `afteralias` | +| `12_pending_result_payload.jett` | Ready Ok counterpart of Source11 | same boundary | + +Sources01/02 pass owned sum bindings bare only at their last use. An existing +borrowed payload alias must be passed with explicit `view`; an initial draft's +bare alias call was correctly rejected by E0401 and repaired before freezing. +Source03's generic helper deliberately returns a reachability marker: its +generic rows establish alias initializer admission, not observation of generic +payload contents. Concrete list payloads are observed separately by length and +numeric sum. Identical Source outputs alone do not prove absence of an implicit +clone; implementation acceptance must also preserve the checked nonowning origin +and ownership facts. + +## Actual reference evidence + +The focused reference test is +`ordinary_borrowed_sums_reference_packet_matches_exact_source_outcomes`, in +`crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums.rs`. +Every Source first passes `build_file_with_options`, then executes through +`run_file_capture_outcome`. The final formatted packet passed all 12 cases: +four success outcomes and eight exact runtime failures. The runner checks the +whole stdout, exact terminal message, and empty runtime/frontend debug streams. + +Sources05/06 end with +`runtime error: list.__remove_at: index -1 out of bounds`. Sources07-10 preserve +the exact nested pending Some/None/Ok/Fail rendering in the existing Handle +error. Sources11/12 end with +`runtime error: list.__sum: argument must be a list`, after `afteralias\n`. +The Rust packet lists every exact stdout/error expectation. + +All 12 Sources were formatted with the built Jett CLI, then passed +`jett format --check`. The Rust module was formatted with +`rustfmt --edition 2024 --config skip_children=true`. + +## Native baseline and acceptance gate + +The focused native test is +`ordinary_borrowed_sums_native_packet_matches_reference_without_source`. +For each genuine Source, it repeats the checked reference run, attempts native +object emission with both `BuildOptions { release: false }` and +`BuildOptions { release: true }`, and records every refusal. Object preflight +reaches the actual native frontend/MIR/verifier before building a runtime +archive. A refused object is a failing parity result, not an expected passing +assertion or an ignored test. + +If emission succeeds, the same gate builds a profile-matched linked executable, +deletes its temporary Source, asserts Source absence, executes the bounded +binary, and compares its status, stdout and stderr to the reference. Temporary +Sources are deleted even when object emission refuses. Baseline refusals produce +no binary, so Source-independent binary execution and matching remain unproven +until this gate passes. + +The final native baseline reached all 24 profile attempts against unchanged +production bytes: all 12 Sources refused in both profiles. Every actual refusal +reports +`NativeBuildError::Codegen` containing +`CodegenError::UnsupportedMir { construct: "failure handler", ... }`. +The native command exited 101 with 24 refusals after 34.44 seconds. No native +object or binary was produced and no native binary was executed. The gate remains +failing; no MIR/codegen/runtime fix is included here. + +## Evidence storage and reproduction + +Only this Cargo target was used: + +```text +C:/Users/Vycto/Documents/GitHub/jett/target/native-ordinary-borrowed-sum-baseline-build +``` + +Its `baseline-evidence/` directory contains the raw final reference/native +process logs, exact copied Source/Rust/documentation bytes, and SHA-256 manifests. +The production manifest covers all crate `src/` files, crate manifests/build +scripts, stdlib files, and root Cargo manifest/lockfile: 332 files. Its initial +SHA-256 is +`842a746b3a70128be7c4de351e2e97a8b07154ea013426d250e859f31e88ee9d`. +The final production manifest matches it byte-for-byte. + +From the isolated worktree, with `CARGO_TARGET_DIR` set to the path above: + +```text +cargo test -q -p jett_driver --test native_conformance ordinary_borrowed_sums_reference_packet_matches_exact_source_outcomes -- --nocapture +cargo test -q -p jett_driver --test native_conformance ordinary_borrowed_sums_native_packet_matches_reference_without_source -- --nocapture +``` + +An initial sandboxed run could compile the driver test but could not scan this +worktree's stdlib because directory access was denied. The final authorized +worktree runs used the same dedicated target with expanded filesystem access; +that environment failure is not language or native parity evidence. Existing +compiler warnings are retained in the logs. No existing primary-worktree paths, +commits, pushes, PRs, or native coverage denominators were changed. diff --git a/docs/active/native_prepared_reference_driver.md b/docs/active/native_prepared_reference_driver.md index d66baf85..2d0a82b0 100644 --- a/docs/active/native_prepared_reference_driver.md +++ b/docs/active/native_prepared_reference_driver.md @@ -1,7 +1,8 @@ # Retained checked-program reference execution -Status: implemented isolated follow-up over -`94bb6b2e021181ec283489c9a02f29a09497fbc7`. The contract was selected before +Status: implemented retained reference driver boundary. The original isolated +follow-up was over `94bb6b2e021181ec283489c9a02f29a09497fbc7`. +The contract was selected before tracked Rust changes. All 46 focused Resource groups and four new driver groups pass with unchanged source hashes. The broader 544 driver/HIR/resolver/typechecker tests, 598 frontend fixtures and eight backend-lowering regressions also pass. @@ -62,10 +63,13 @@ No test needs a live Resource or native archive. The focused gates below are executed; new full-workspace/platform acceptance remains pending. Driver build_source and backend lowering are outside this bounded preparation -repair. Their existing paths are not claimed unified. Required-evaluation -runtime-only Resource invocation eligibility remains a separate prerequisite. -The whole-language 100% objective, about-85% estimate, fixed 207 inventory/182 -object obligations and 464 supplemental cases remain unchanged. +repair. Their existing paths are not claimed unified. Runtime-only Resource +invocation eligibility was separate at this checkpoint; the later private +reference implementation and its remaining breadth are recorded below. +At this earlier driver checkpoint the whole-language 100% objective, about-85% +estimate, fixed 207 inventory/182 object obligations and 464 supplemental cases +were unchanged. The later caller native receipt passes 480 cases, separately +from the Resource reference tests. ## Verification and remaining boundaries @@ -96,9 +100,12 @@ historical `616b51b9` platform result, are centralized in [the acceptance audit](native_acceptance_audit.md#retained-reference-preparation). This follow-up adds no native inventory case and changes no coverage metric. -Runtime-only required-evaluation effects, program-bound Resource callable -identity, caller ownership retention, Resource carriers/providers, ordered -cleanup and native lifecycle remain prerequisites. The sealed snapshot and +The later [reference Resource lifecycle](native_resource_hook_identity.md#reference-lifecycle-execution) +implements program-bound callable identity, runtime-only hook eligibility, +checked caller effects, carriers and ordered cleanup under a compiler-test-only +scripted provider. Its 36 focused tests are separate from this driver receipt. +Broader scoped/reflected/indirect/worker coverage, production providers and +native lifecycle remain prerequisites. The sealed snapshot and private reference bridge alone grant none of that authority. build_source and backend lowering keep their existing independent paths. A formal View mode is not proof of the caller's retained owner; no new generic borrowing policy is diff --git a/docs/active/native_required_resource_hooks.md b/docs/active/native_required_resource_hooks.md new file mode 100644 index 00000000..622a0429 --- /dev/null +++ b/docs/active/native_required_resource_hooks.md @@ -0,0 +1,68 @@ +# Required Resource hook materialization + +This change connects explicitly required, closed pure comptime evaluation to +native Resource hook values. It does not change function eligibility: every +semantically pure function remains eligible, including project and stdlib +functions, generic and scoped selections, arbitrary pure control flow, aliases +and relays. A runtime capability or provider is never available to that worker. + +The shared reference oracle must evaluate required expressions using the same +CheckedResourceProgram identity before registering runtime code or installing a +provider. A failed evaluation or absent exact cache entry is an error; runtime +evaluation of the comptime body is not an acceptable oracle. + +The existing public span/context cache is compatibility materialization data, +not Resource authority. An opaque proof is minted only from the private +program-bound checked required cache. It retains the original required-region +key, owner, outer expression occurrence and complete concrete specialization and +scoped selection. The same checked program and manifest must authenticate the +selected hook. A public mirror insertion, foreign program with identical Source, +copied or resealed expression, changed context, stale hook or missing evaluation +cannot mint that proof. + +HIR consumes that opaque proof through an acyclic dependency on jett_comptime. +This precise compiler-internal coupling avoids a publicly forgeable tuple of +program, span and DefId. HIR preserves its immutable original ResourceSourceArchive +and records an authenticated materialized view of the original Comptime +occurrence. It must not overwrite the original Source, reseal a replacement as +original, or pretend required evaluation was a runtime call. MIR verifies that +mapping at the exact current occurrence and through canonical CFG/local remaps. + +The current native value transport supports exact primitive literals, absent +optional Resource sums, and exact compiler hook descriptors. Qualified and +complex required values remain transport work; this does not restrict which +semantically pure functions may be evaluated. Required scalar rows also seal +ordinary runtime helpers without granting those helpers Resource frames or a +Resource calling convention. + +Runtime Resource operations and ordinary descriptor values are preserved while +the baker walks the program. Only required values are materialized. Runtime +reachability must not force compile-time-only pure selector bodies through a +narrow returned-hook allowlist. Such a restriction would contradict existing +pure-function eligibility rather than implement native coverage. + +The shared corpus grows from 46 to 61 scenarios using twelve new original +Source files. All 122 real-reference and 122 source-absent native runs pass in +debug and release against the unchanged matched MSVC archives. They cover all +three recipes, factory/domain/cleanup failure, alias, relay, unused storage, +immediate invocation, absent Resource sum, pure branch selection, generic/scoped +pure selection, and an ordinary runtime helper whose own required scalar value +keeps its normal calling convention. Reports require exact event order, outcome +channels, complete script consumption and zero custody before teardown. + +Nine focused MIR controls and all 251 MIR library tests pass. Genuine pre-fix +controls exposed acceptance of a changed required scalar helper and omission of +disconnected Resource extraction headers; independent body sealing and typed +header detection now reject both. Exact sum-tag branches plan both syntactic +arms while the unchanged runtime branch selects absence or occupancy. Ordinary +non-Resource sums do not gain custody classification. + +The complete codegen suite passes 100 library and 63 object-emission tests, and +all 93 driver-library tests pass. Rust/Jett formatting and diff checks pass; +all 2,168 frozen source/config inputs remain exact. The complete current +workspace, default native regression and supported-host jobs remain release gates. + +Full native coverage remains active. Returned named/dynamic/captured callbacks, +broader Resource aggregates and views, production providers, concurrency, GNU +private execution, and coherent final workspace/distribution acceptance remain +separate outstanding requirements. diff --git a/docs/active/native_resource_absent_generated_bindings.md b/docs/active/native_resource_absent_generated_bindings.md new file mode 100644 index 00000000..b921d152 --- /dev/null +++ b/docs/active/native_resource_absent_generated_bindings.md @@ -0,0 +1,139 @@ +# Checked absent For and Match bindings + +The reference repair passes all 554 comptime tests, including the original +Source17 replay and nine focused tests with 60 Source entry observations. +Coherent-revision workspace checks remain separate; these reference results +establish no native aggregate acceptance. + +The reference path preserves existing checked Source semantics for generated +list elements, map values and enum payloads whose types can contain Resource but +whose selected values are recursively inactive. It adds no Source syntax or +language permission and does not transport occupied aggregate custody. + +## Original control and binding proof + +The proof joins the exact original For or Match statement to its current checked +attempt and active body. It retains binder object identity and ordinal, resolver +definition, declaration span, concrete TypeId, immutable binding mode and exact +expression facts. Match additionally retains its selected arm, variant and +payload cardinality, respecting concrete/scoped static selection and the first +matching Source arm. Equal spans, copied controls/binders, another checked +program or a changed body cannot substitute for that correspondence. + +Preparation validates the complete selected parent. Every generated child is +checked against its exact retained type by the existing recursive absence-shape +validator before publication. Nested occupied ordinary fields, keys, elements, +variant/state payloads and nominal/concrete headers remain checked; physical live +Resource carriers and grants remain refused. A nominal refinement tag cannot +replace its ordinary predicate proof. + +Resource-bearing children enter the existing declaration binding slot as ordinary +evaluated values with empty custody. No owner ticket, provider grant or aggregate +custody path is minted. The strict missing-slot identifier guard remains intact. +View For loops authenticate the original control and use the existing transport +to retain their parent binding; move-only elements/values keep their checked view +mode while implicitly copyable map keys keep their ordinary owned mode. +Generated scopes retire on ordinary completion and selected control/error exits. +Unrelated live owners retain their existing tickets and normal cleanup; admission +does not require global owner or registry counts to be zero. + +## Retained predecessor evidence + +The unchanged Source17 baseline admits all fourteen wrappers in both checking +profiles. At parent `f9fa84a6f05b2886a83e09fad4ae3b6dcb22c74a`, 22 of its 28 +reference attempts match their exact oracles. Six attempts in `02_absent_tokens`, +`04_absent_map` and `10_absent_choice` fail at ReferenceCall with +`Resource source binding has no live cleanup custody`. This is a failed baseline, +not an accepted generated-binding implementation. The retained receipt is +`target/native-resource-absent-aggregate-baseline-evidence/after-v1-wrapper.json`. + +The independent original gate passes 120 constructor calls across twelve +constructors, five execution purposes and both profiles, plus the selected +required/verify/property controls. All 3,696 frozen baseline inputs remain +unchanged. Its receipt is +`target/native-resource-absent-aggregate-baseline-evidence/after-v1-all.json`. +The original 3,600-byte Source17 prefix remains exact, SHA-256 +`c8e74848356a5b3d09ab6659318b1e1292718d34443f7f37222bafd6f3eed0ff`. +Existing constructor/refinement/required-value/verify/property bodies and exact +ordinary Source13 error oracles are preserved. + +## Successor reference acceptance + +The integrated candidate passes **nine focused tests**, zero failed, covering +exact original-control/binder/ordinal/body/type facts, recursive parent/child +shape rejection, occupied/grant rejection and existing empty-custody publication. +Its **60 Source entry observations** comprise **56 unchanged original-wrapper +entries** (fourteen Sources, both profiles, repeated entry) and **four genuine +view/Continue/Break/parent-reuse entries**. Exact success and ordinary Source13 +error oracles, empty provider/debug observations and full entry restoration pass. +The unrelated live-owner control keeps the owner live across generated-binding +publication and retires it exactly once through its existing outer cleanup. + +`target/native-resource-absent-generated-binding-repair-evidence/after-v3.json` +records actual session **23693**, terminal **5bc44f**, exit **0**, at committed +parent `c14189d9d95b09a7613651ccf3c2dade2c308994`. Its SHA-256 is +`b4d8582032012cb05999eab2c5527fe4bc881253e339872be1205ce73cfc12d8`. +The matching before-input receipt is `inputs-v3.json` in the same directory, +SHA-256 `f1c18f376491a429d6de6e24270ee957b1b0851db016d171a306ab087ee3b59d`. +All **3,709 frozen inputs** remain unchanged through the focused gate. The +fourteen original wrappers and the exact original Source17 prefix remain byte +preserved. The receipt explicitly records `native_execution_accepted: false`. + +The complete `jett_comptime` gate independently passes **554 tests**, zero failed +or ignored, at the same committed parent. Its separate original wrapper replay +records **28 attempts / 28 matched / zero failed**. The original **120 constructor +calls** pass across five execution purposes and both profiles. Namespace constant +17, required primitive 34, original verify blocks and **100 property iterations +per profile** also pass. These totals remain distinct from the 60 focused entry +observations, which run again inside the complete crate gate. +`target/native-resource-absent-generated-binding-repair-evidence/full-comptime-after-v3.json` +records actual session **22882**, terminal **e0c10b**, exit **0**; its SHA-256 is +`73f109efba6c69cf72f92abb9d8bd5cd5f1eeda4318b61f57ea1b0bf86eee883`. All **3,709 frozen inputs** +remain unchanged, the original Source17 prefix stays exact, and +`native_execution_accepted` remains false. + +| Gate | Actual outcome | +|---|---| +| Original-control/binder/shape and custody checks | Accepted in the nine-test focused gate | +| Unchanged fourteen wrappers in both profiles, repeated entry | 56 exact entry observations accepted | +| Genuine view/Continue/Break/parent-reuse control | Four exact entry observations accepted | +| Unrelated live-owner retention and exact cleanup | Accepted focused control | +| Separate original 28-wrapper baseline replay | 28 matched / zero failed in the full crate gate | +| Original 120 constructor calls and required/verify/property controls | Accepted full crate replay | +| Full comptime crate | 554 passed / zero failed / zero ignored | +| Coherent-revision workspace checks | Separate gate still pending | + +The earlier failed Rust/source-policy diagnostics remain retained as predecessor +evidence. Original Sources and exact oracles remain unchanged; failed compilation +or execution attempts are not counted as successful reference gates. + +## Independent native obligation + +All 28 baseline native attempts pass MIR lowering and structural validation, then +fail at public ObjectEmission with `CodegenError::InvalidMir`: +`pending ResourceOwnershipPlan: occupied Resource layout needs its dedicated aggregate or qualification transport`. +No object or linked native execution is accepted by that baseline or by this +reference repair. The original aggregate returns and refined contract remain +required compiler transport work; removing exports or pruning those declarations +would not satisfy the gate. Occupied aggregate custody and broader For/Match +Resource paths remain required. The full native-codegen goal remains active at +the rough 87% planning estimate with unchanged inventory denominators. + +## Final formatted fixture + +Canonical formatting removed five appended blank lines while keeping the original +3,600-byte Source17 prefix exact. The focused nine tests / 60 entry observations +pass again on those final bytes. All 3,709 inputs remain unchanged during that +gate; production files remain exact from the complete 554-test replay above. +The receipt is +`target/native-resource-absent-generated-binding-repair-evidence/after-v4.json` +(SHA-256 `630e5f3c47037efadbdab9a3deec49b7560a1ca8c2b5150a43aa10d049e19d72`), actual session 29722, terminal `bfe7a5`, exit zero. + +## Integration with native reflection bridge + +The combined candidate retains both reflected-field positive controls and the +aggregate baseline module. Its full comptime gate passes 556 tests, zero failed +or ignored, with all 3,720 inputs unchanged during execution. The sole merge +conflict appended both original test-module registrations. The receipt is +`target/native-resource-absent-reference-pr-integration-evidence/after-v1.json` +(SHA-256 `e29fb085aeded7a534ea01a603bc974b468bcf3a952bcc4618270b60c2831854`), actual session 72978, terminal `24c775`, exit zero. diff --git a/docs/active/native_resource_aggregate_carriers.md b/docs/active/native_resource_aggregate_carriers.md new file mode 100644 index 00000000..3333d9f9 --- /dev/null +++ b/docs/active/native_resource_aggregate_carriers.md @@ -0,0 +1,245 @@ +# Typed Resource aggregate transport + +Status: typed absence transport, current compiler proof checks, the v20 whole- +workspace gate and all eleven v21 MSVC-v3 native regression gates pass. This +work preserves the existing Source +language and full native parity goal, with no aggregate, refinement, machine or +generic Source exclusion. + +The whole original absent aggregate Source contains twelve exported constructors, +record/generic/enum/machine/qualified declarations, an unused refined view contract, +required controls, verify and property100. Fourteen wrappers preserve its exact +prefix. The historical pre-carrier baseline passed MIR lowering and public MIR +validation but failed ObjectEmission in both profiles because the single-leaf +ownership/layout model could not represent those declarations. The bounded +object/native checkpoints below supersede that historical failure. + +The selected replacement is a private typed carrier plan with a complete original +type graph and opaque runtime roots. Constructor-issued roots retain nominal owner, +concrete generic arguments, ordered fields/payloads, selected variant/state, +qualification and checked refinement proof boundary. Root generation and typed +paths identify each projection; equality of physical layout or type spelling is +not authority. Full original HIR/type/Source and current MIR/CFG witnesses are +validated before the plan is issued. Existing single-leaf custody remains separate. + +Zero live leaves is the first connected implementation. Empty collections, each +`none` shell, result failure data and inactive enum/machine payloads preserve their +complete selected data. A type's potential Resource paths are not removed by +actual absence. Latent occupied view formals and refinement metadata remain in +their original interfaces. Unimplemented live tree operations refuse before +effects while the same root/path interface awaits individual leaf custody support. +Copied physical Resource or grant handles never receive that authority. + +Wire v3 adds typed graph, carrier slot/constructor/projection and Source carrier +roles. Strict accepted v1/v2 decoding and all old tag meanings remain. Fixed-width +status/opaque-u64 C ABI v1 records remain selected; actual new leaf symbol/layout +checks are required. Ordinary ownership fields cannot contain Resource-table sum +or owner handles. Typed observations route exact carrier lengths, keys, tags, +fields and sequence extraction through dedicated leaves. + +An explicit borrowed-sum adapter retains carrier parent/path/generation and lease +while presenting selected optional/result children to the existing sum-loan +protocol. Owned extraction transfers once. Original call lexical staging, abandoned +prefix retirement before Return operands, Default joins, body/cleanup separation, +provisional Return publication and one-shot retirement remain unchanged. + +Bounded whole-Source17 object/native gates and current fresh-v3 regression pass +as recorded below, including the unchanged 191-case and reflected Return gates. +Broader lifetime/purpose acceptance remains required. + +Legitimate occupied trees, multiple kinds and dynamic paths, aggregate replacement, +machine transitions, refinement construction, recursive nominals, other reflected +families and derived or aggregate exceptional native reentry remain full-goal +follow-ons. Original Source06 single entries and original-then-READY same-grant +reentry were accepted at f4c63a0f and now pass fresh matched-v3 regression at +v21. Earlier wire-2 and bounded v15 absence receipts remain separate historical +evidence; current post-repair regression is established by v21 below. Existing checker/reference semantics determine live +replacement and destruction order; record any unresolved tradeoff before +implementing new behavior in Rust. + +## Bounded object/native acceptance and current compiler checkpoint + +All fourteen whole-Source17 wrappers emitted **28/28** genuine host objects in +both profiles at v14, with zero refusals. The full original 3,600-byte Source +prefix, twelve exports, unused refined contract, required controls, verify and +property100 remain intact and in scope. This main-wrapper gate does not establish +every original purpose or live aggregate value. + +target/native-resource-carrier-compile-evidence/after-v14.json (SHA-256 +43c3a3f1fc2fa6d1e4c6cbd7cc7e0bad4f0d560dce0685e6a8fcb2b411aee2dc) +records actual session 94028, terminal f7b2aa, exit zero. + +The separate v15 gate passes **28 Source-deleted single-entry executables / +28 entries**, including Source13's exact ordinary bounds error, and **26 clean +same-grant reentry executables / 52 entries**: **54 native executables / +80 entries**. Exact completion/message/event reports, all eight zero Resource +custody counts and empty ordinary storage before one teardown, event boundaries +and script exhaustion pass. Source13's repeated ordinary-error call on the same +grant remains pending; thirteen clean Sources in both profiles do not replace +that lifetime obligation. + +target/native-resource-carrier-compile-evidence/after-v15.json (SHA-256 +b20c4d9427b11676e7207e5691171c64a861666e0f728b8aa24dba631123154b) +records actual session 93295, terminal b1a472, exit zero. Both v14/v15 retain +all **3,752 frozen inputs / nine runtime files unchanged** at parent +91e3706b770124cbddca952e5fe6b558abc170be. The measured v15 runtime receipt pin is +74a811660ee1772e0374e04579dd51234e8ff38191b942335bedaab5b4e4b6cd. + +The current post-refinement-repair compiler gate is separate from v15 native +acceptance. At v19, **17 focused entry controls (13 MIR + four codegen)** pass, +including four new raw/refinement archive controls. Full package gates pass +**314 MIR checks (313 library + one doctest)** and **181 Cranelift checks +(118 library + 63 object)**, with zero failures or ignored tests. + +target/native-resource-carrier-compile-evidence/after-v19.json (SHA-256 +b549b995be46093f29dbf45314b47f1eea3b09c9bfc20cebf068e477bfaa8f8b) +records actual session 65346, terminal 26ed19, exit zero and all **3,757 frozen +inputs / nine runtime files unchanged** at the same parent. Earlier v3 runtime +gates passed **275 checks at v6** and **44 focused carrier checks at v7**. These +are historical revision-specific results, not current whole-runtime or workspace +success. + +## Current workspace and wider-language observations + +The earlier v20 whole-workspace gate passes **3,582 checks, zero failures and +13 ignored tests**, across **66 real selected groups / 92 total result groups**. +Actual session **75431**, terminal **1f1c8b**, exits zero for +`cargo test --offline -q --workspace --no-fail-fast`. +target/native-resource-carrier-compile-evidence/after-v20.json (SHA-256 +fb59285ab91f6d15ce57ab59ecf7cc5c00187cdf4c323de04b5bbafe4a78e7d2) +records all **3,757 source inputs / nine runtime files unchanged** at parent +91e3706b770124cbddca952e5fe6b558abc170be. The inputs-v20 manifest SHA-256 is +58dd6f590a1bf2fd63f04d0ec0abdd15aa71452e9e00b1ff36b1feb29ab3612a; +the workspace log SHA-256 is +f610439bbbcb334d88de90c8b8ebf4178f1015be564cc7caf63bba2b5d6ed8da. +This is measured workspace success, not fresh ignored-native acceptance. + +All **11 fresh-v3 native gates now pass**, each selected as a real one-test gate. +Actual session **41872**, terminal **3c7b60**, exits zero with **576 Source-deleted +native executions / 672 entry calls**, across Debug and Release. This includes +the unchanged **191-case / 382-execution corpus drawn from 113 distinct Sources**, +carrier controls, reflected-body controls and owned-Return regression. The v20 +workspace's ignored-test count is historical; v21 independently executes the +eleven native gates rather than treating ignored tests as acceptance. + +target/native-resource-carrier-compile-evidence/after-v21.json (SHA-256 +38f67805c93e66e273d29de84c1cfc3351d70c0235bf656ea661244474582fa2) +records all **3,757 source inputs / nine runtime files, HEAD and inventory +unchanged** at parent 91e3706b770124cbddca952e5fe6b558abc170be. The frozen native +binary SHA-256 remains +110ba3b7bf23e63677ac7afd13a83fc2fa98aeccebc553e8df4f294fb5bc872f. +Matched MSVC v3 archive provenance passes before and after execution with receipt +pin 74a811660ee1772e0374e04579dd51234e8ff38191b942335bedaab5b4e4b6cd. +The successful runner report SHA-256 is +699f2364769ccdd489c7322832f2410690129d8f687006239e3dc6f0b6e82ae6. +This gate establishes no GNU runtime-archive acceptance. + +Owned-Return cleanup failure retains caller/entry-root **body255 / cleanup255**; +the observations establish retirement and root completion, not a separate nested +callee completion. The accepted historical Return semantics and exact Source +prefixes remain intact. Earlier prospective fresh-v3, full-191/382 and reflected- +Return gate statements in retained historical sections are superseded by v21; +broader body/Default, generic/reflected lifetimes and occupied custody remain open. + +At v21, Source13's repeated ordinary error remained pending; the v24 +same-grant gate below closes that bounded obligation. Occupied resources, wider +whole-language parity and the successor's exact-revision CI remain pending. +The rough 87% estimate and fixed 207/182 inventory do not advance. + +The separate 15-case wider-language probe is an **observation baseline, not +acceptance**. Actual session **77834**, terminal **ee797f**, leaves compiler +inputs unchanged and records eight emitted objects, six object refusals, one +earlier backend failure and three reference failures. Frontend/driver build +success does not establish linked parity. Its authoritative receipt is +target/native-language-parity-probe-observations-v1/root-after-evidence.json +(SHA-256 434cba150528d36e018d89b9dbe3acb05adf0309ac6a5aeb47a8ea1b906b4c3d). + +## Raw refinement proof boundary + +Pure refinement predicates lowered through raw/default HIR without an +authenticated checked Resource archive stay ordinary through canonical +preparation. Declaration kind or spelling alone supplies no Resource witness or +entry Scope. Raw bodies with actual Resource custody retain the independent +mandatory capture/refusal path. + +Authenticated predicate proof requirements derive the original declaration kind +from the immutable archived function. Removing a witness and changing the current +predicate kind cannot escape that requirement. Constructor seals reject missing +or foreign proof, modified predicate body and changed header. The repair preserves +ordinary raw refinement behavior while retaining authenticated original authority. + +## Remaining acceptance + +The earlier whole-workspace gate passes at v20, and all eleven matched- +MSVC-v3 native gates pass at v21, including full **191-case / 382-execution**, +carrier, reflected-body and owned-Return regression. These remain independent +historical receipts; current ordinary-error regression and workspace acceptance +are recorded at v24 below. Earlier accepted GNU Source-prefix and Return +documentation and wire-v2 receipts retain their independent historical scope. +The successor's exact-revision CI remains pending. No GNU runtime-archive +acceptance is inferred from these local MSVC results. + +Occupied aggregate transport/replacement, broader Resource-valued Default +and generic/nested reflected lifetime compositions remain full-goal obligations. Whole Source/export/refinement and verify/property100 +purposes remain required. No refusal counts as successful Source execution, no +Source policy exclusion is introduced, and the full native parity goal stays +active. The rough 87% estimate and fixed 207/182 inventory do not advance. + +## Accepted ordinary-error successor checkpoint (v24) + +The changed ordinary runtime passes its measured **307-check** suite with fresh +matched MSVC `measurements-02` archives. The accepted twelve-gate native scope +covers **604 Source-deleted executables / 728 entries**: the new all-fourteen +same-grant gate contributes **28/56** from session **62286**, including both +Source13 Bounds errors and original body1/cleanup0/Body completions; session +**76385**, terminal **d30da4**, passes the eleven predecessor gates with +**576/672**. The latter includes full **191-case/382-execution** regression from +**113 distinct Sources**, carrier, reflected body, owned Return and original +Source06. No separate nested callee completion is inferred from Return root +observations. + +The fresh whole workspace passes **3,614 checks, zero failures and 14 ignored +tests**, across **66 nonzero / 92 total groups**, at actual session **75810**, +terminal **0f5a7b**. Independent verification at **58261 / 75fabb** authenticates +all **3,763 source inputs / nine fresh and nine historical runtime artifacts**, +HEAD/inventory, both native binaries and fresh02 provenance/log hashes unchanged. +The authoritative receipt is +`target/native-resource-carrier-compile-evidence/after-v24-native12-workspace.json` +(SHA-256 +`4dabdff125cefac5891d6b026cb0b67ec14448db67915755116f014698ea6aa2`). +The v20/v21 receipts above retain their accepted historical scope; current +ordinary-error regression is established independently by v24. Earlier +Source13-pending statements describe their original checkpoint and are now +superseded for this bounded same-grant obligation. See the +[ordinary terminal-error note](native_resource_ordinary_error_reentry.md) for +the exact archive, binary and per-attempt messages/retirement evidence. + +Local MSVC results provide no GNU runtime-archive or full platform acceptance. +The separate normal-main linked baseline is complete: sixteen observations, +twelve exit0 and four exit71, from eight exact opaque objects reused with +Debug/Release runtime artifact profiles; all fifteen raw-probe Sources remain +preserved. This is baseline collection without a desired Source oracle, new +feature acceptance or ordinary public-Main context-retirement observations. +Broader ordinary parity and fixes remain open. Occupied custody/replacement, broader Default and +generic/nested reflected lifetimes, wider language parity and the successor's +exact-revision CI remain open. The rough **87%** estimate, fixed **207/182** +inventories and full 100% goal do not change. + +## Occupied named struct checkpoint + +Fresh local MSVC static-CRT acceptance on 2026-10-12 passes the three valid +occupied-struct entry Sources on both measured runtime profiles. The two gates +cover partial prefix retirement, reversed named arguments, cleanup failure and +same-context reentry: 44 native executions/66 entries. The original twelve +predecessor gates pass 604/728, and additional pipeline/control-flow subset +gates pass 106/106 against the same archives. All sixteen Resource gates pass +754 executions/900 entries overall. All 2,314 workspace library tests and the +six-object gate also pass. + +The discarded historical raw-artifact packet is not evidence for this integration. +Current Rust gates check decoded reports and exact Source oracles; Source and +compiled-artifact guards pass. Broader occupied collection, recursive/generic, +projection/replacement, reflected and captured lifetime compositions, GNU and +installed-distribution acceptance remain required. The rough 87% estimate and +fixed 207/182 inventories stay unchanged. See the +[occupied struct note](native_resource_occupied_structs.md) for scope and limits. diff --git a/docs/active/native_resource_borrowed_sums.md b/docs/active/native_resource_borrowed_sums.md new file mode 100644 index 00000000..79b6d1b8 --- /dev/null +++ b/docs/active/native_resource_borrowed_sums.md @@ -0,0 +1,100 @@ +# Native borrowed Resource sums + +The full native-codegen parity goal remains active. This note records the verified local +implementation family after required value materialization at 17d6c841. +It implements the existing view rules; it adds no ownership or lifetime syntax. + +## Source and evidence + +The shared conformance corpus contains 73 scenarios. Sources 45 through 56 add +written and bare View actuals for None, Some, Fail and Ok; two named-argument +aborts after an earlier borrowed sum; and two handled borrow-domain failures. +All 146 reference executions passed in both profiles before production changes. +The original native baseline first refuses Source45 at HIR lowering, File0 +186..236: a handled view is not yet recognized as a stable local origin. +The expanded corpus now passes all 146 Source-deleted native executions against +freshly measured Debug/Release MSVC runtime archives. Exact events, consumed +scripts, failure channels and zero ordinary/Resource obligations before teardown +remain mandatory. This supersedes the earlier 61-scenario / 122-run checkpoint. + +## Existing source contract + +An immutable typed declaration whose initializer explicitly views the handled +success payload may borrow from an explicitly viewed optional/result binding. +Its exact Optional or Result kind, success payload type and original binding +identity remain checked. The failure branch in this family exits the function. +An alternate Default, including one in a conditional before a final Return, +needs its own backing/lifetime proof and cannot inherit the original sum origin. +Defaults belonging to nested handlers are distinct from the outer continuation. +Temporary producers cannot acquire stable alias identity by annotation alone. + +Written View actuals keep the caller's sum and sole occupied Resource owner. +A bare owned actual into a View formal transfers the entire sum into owning +argument storage; the callee only borrows that holder. Caller source argument +order governs evaluation even when named formal order differs. A later argument +failure skips the callee, ends all prefix leases, and drops each acquired holder +once. None and Fail carry no occupied Resource token or finalizer. + +## Proof across compiler phases + +Checked binding metadata retains the exact concrete body's resolved backing +DefId. HIR verifies the complete typed handled-view initializer against that +immutable local chain, without changing general assignment-root recognition. +The private checked Source archive supplies authority; public metadata or a +matching span/type never grants Resource execution permission. + +MIR retains the original function, declaration, handled expression and target, +sum and endpoint types, failure block, source/alias headers, and concrete scope. +Canonical lowering introduces nonowning sum and payload headers authenticated +against that original occurrence. A fresh SumTag and its exact Branch select +Some/Ok before projection. Borrowed extraction never performs an owning SumTake +or manufactures an owner slot. Altered tag sources, arms, headers, current bodies, +foreign archives and truly disconnected extractions must fail independent proof, +including before canonical pruning or remapping. + +## Native execution + +Conditional sum views need a distinct runtime representation from direct +Resource loans. Occupied payload views are bounded child loans; absence creates +no occupied loan. Fail's ordinary string is observed/copied without consuming the +caller shell. All handles remain tied to the exact attempt, frame, registered +nominal shape and payload path. The original shell cannot move, be replaced, be +taken or be destroyed while any shell/payload view is live, including absent +shells that have no Resource token to enforce that restriction. + +Children retire before their parent shell view; source call retirement precedes +argument-holder cleanup. Registration rows must authenticate conditional shape, +loan source, selected path, operation and frame rather than infer permission +from a handle tag. Any runtime schema or implementation change requires fresh +Debug/Release single-runtime archives, symbol/source measurements and a pinned +receipt before the complete Source corpus may count as native acceptance. + +## Verification and remaining work + +The complete reference suite passes 521 tests, including all 146 current Source +executions; all 93 driver-library tests pass. A separate test correction verifies +Source33's exact scalar and hook proofs and fixes the previous commit's sole +Ubuntu/Windows CI failure. Rust and all twelve new Source formatting checks pass. +All 2,188 compiler/source/config inputs revalidate; the final MIR formatting change +is an exact error-return line wrap, followed by another complete 254-test pass. + +All 305 checker, 203 HIR, 254 MIR and 215 runtime library tests pass. Three +focused MIR groups verify actual Source profiles, altered headers/guards/bodies, +foreign or missing proofs, disconnected extraction and exact loan ordering. Six +runtime groups cover conditional shell custody, failure companions, child loans, +argument-prefix aborts, forged registration and C output preflight. All 102 +codegen library and 63 object tests pass, including unchanged prior forgery +controls. Fresh custody validation precedes local-view queries that consume its +proof, preserving forged-graph rejection as invalid MIR. + +Both freshly measured archives contain 37 Resource leaves, eight private test +exports and one main, with static CRT and the actual ordered native library list. +Their independent receipt is pinned in the native harness. Neither this private +MSVC gate nor earlier platform CI proves current full workspace/GNU acceptance. +Broader aggregates, qualified values, callbacks, reflection, production providers, +asynchronous execution and complete platform acceptance remain in the full plan. + +The first constructor inventory is limited to root-body Resource aliases. +Nested/scoped Resource alias declarations, ordinary non-Resource borrowed sums, +and alternate Default payload lifetimes remain separate unverified native paths. +Checker/HIR acceptance alone does not establish native execution for these paths. diff --git a/docs/active/native_resource_checked_program.md b/docs/active/native_resource_checked_program.md index 29048ce8..33764da5 100644 --- a/docs/active/native_resource_checked_program.md +++ b/docs/active/native_resource_checked_program.md @@ -1,8 +1,9 @@ # Immutable checked resource program handoff -Status: implemented compiler handoff prerequisite over `b7180bd9`. All 43 -focused Resource groups and the resolver/typechecker/HIR suites pass. Full -workspace and supported-host validation of this change remain pending. +Status: implemented compiler handoff prerequisite. Its original isolated step +over `b7180bd9` passed 43 focused Resource groups and the resolver/typechecker/HIR +suites. The later private reference lifecycle now passes 36 focused tests, as +recorded below; broader acceptance of that Resource candidate remains pending. The accepted authority is the opaque runtime resource contract in `docs/completed/opaque_runtime_resource_contract.md`. @@ -95,12 +96,14 @@ The snapshot proves parsing, resolution, checking and hook shape only. It does not bypass explicit comptime or verification failures, retain baked runtime authority or authenticate a foreign raw DefId. The separate reference bridge repairs post-preparation rereading without granting live Resource authority. -Program-bound callable descriptors, reached-hook runtime-context eligibility, -checked caller ownership facts, one-owner ledgers and native drop elaboration -remain required. Current name/source-position-based first-view retention and -formal-mode lowering disagree on some call shapes; no universal bare-argument -cleanup policy is selected here. The next characterization must establish those -source outcomes before a live Resource route is admitted. +Typed caller ownership now carries the existing Rule24 effects separately from +callee access. The later private reference lifecycle adds program-bound hook +descriptors, reached-hook purpose checks and one-owner custody ledgers; its +36-test acceptance is recorded in +[the resource-hook note](native_resource_hook_identity.md#reference-lifecycle-execution). +The successful snapshot alone grants none of those execution rights. Broader +scoped/reflected/indirect/worker cases, production providers and native drop +elaboration remain required. ## Retained reference integration diff --git a/docs/active/native_resource_control_flow.md b/docs/active/native_resource_control_flow.md new file mode 100644 index 00000000..2f7a5843 --- /dev/null +++ b/docs/active/native_resource_control_flow.md @@ -0,0 +1,155 @@ +# Native Resource control-flow transport + +The native-codegen goal remains active. This successor preserves existing +Resource custody through consuming Resource-free list iteration and ordinary +Resource-free enum selection. It adds no Source syntax, ownership rule, provider +authority or runtime ABI. The evidence below is bounded local MSVC acceptance; +the complete successor native corpus passes; coherent-revision gates remain pending. + +## Genuine Source baseline and expansion + +The original five Sources01..05 place the established owned Optional/Result +backing, written handled payload view and Resource observation inside genuine +`For` and `Match` bodies. Sources01..03 cover two-iteration Fallthrough, Break +and Continue; Sources04..05 select each arm of an ordinary exhaustive unit enum. +Each backing is reused by an owning extraction and explicit close after the +lexical boundary. Their 22 script rows also cover construction failure, +first/later borrow failure and explicit-close cleanup panic. No While or +direct-call substitute establishes these Source forms. + +With unchanged production code, all 22 cases passed 44 real-reference executions +in Debug/Release, while all 44 final native codegen attempts refused emission. +That independent historical baseline is recorded in +`target/native-resource-control-flow-source-evidence/baseline-after-v1.json`. +The first v2 repair then passed all 22 cases / 44 Source-deleted MSVC executions; +Sources07/08 were still unverified drafts at that checkpoint. + +V3 includes those consecutive and nested-loop Sources07/08 and eight additional +rows. The accepted focused packet now contains seven Source files and 30 cases / +60 profile executions. It preserves the accepted 161-case / 322-execution +predecessor as an exact Case-row prefix and retains its Support bytes. The +complete successor contains 191 cases; its full 382 reference executions have +passed, and its full 382 Source-deleted MSVC native executions now pass. + +Reference observations check exact outcomes/events and zero owners/registry +entries before context teardown. That observation tuple does not measure script +exhaustion or every native custody counter, and reference success does not +establish linked native success. + +## Constructor-owned transport + +Fresh Resource CFG analysis admits only a structurally proven Resource-free +consuming `list` operand with one exact ordinary element binder, or an ordinary +Resource-free enum discriminant with distinct checked variant payload binders +and the original exhaustive edges. Resource operations in their bodies retain +their independent custody plan; the iterable, binder and discriminant do not +create a Resource owner or loan. + +Sequence preparation validates the complete before-witness, then reconstructs +the finite canonical edit itself. It appends exactly three temporary headers in +cursor, length, source order; initializes source, cursor zero and length at the +unique original preheader; prefixes the body with consuming element extraction +then cursor increment; and replaces `ForEach` with `cursor < length` using its +original body and exit edges. Exact types, spans, header metadata, statement +order and the entire resulting graph must match. Added blocks, redirected edges +or an otherwise convincing edit transcript cannot grant authority. + +Only after that equality check may the two-statement body prefix advance current +borrowed-projection, lexical-exit and normalized-call sites. Independent +original/current association seals and descriptor-body checks remain required. +The archival declaration, initializer, backing and Source facts stay distinct +from current sites. Consecutive or nested normalization must validate each +finite transition; a preceding successful edit grants no general rewrite +authority. Duplicated, missing, moved, retargeted or independently resealed +projections/exits fail, including before canonical pruning. Preparation without +another edit preserves every witness. + +Existing strict joins are unchanged: operation stacks, arrived prefixes, +retirement obligations and leases must agree, and a moved/uninitialized versus +live owner still requires its own conditional cleanup proof. Certified lexical +exits end child payload loans before parent shell loans and clear only their +nonowning aliases, preserving the backing for its later owning use. Final native +codegen still refuses a raw, unprepared `ForEach`. + +## Distinct acceptance gates + +The historical v2 receipt, +`target/native-resource-control-flow-source-evidence/acceptance-v2.json`, records +22 cases / 44 Source-deleted MSVC executions in Debug/Release (228.02 seconds), +three focused MIR groups and 450 affected phase checks: 107 codegen library, +63 object, 279 MIR library and one CFG test. Its compiled receipt-pin forgery +control passes. All 3,656 frozen inputs, its recorded base HEAD, both measured +archives and six nested receipts revalidate unchanged. Those phase totals remain +historical v2 evidence, rather than a newly measured v3 phase gate. + +The v3 receipt, +`target/native-resource-control-flow-source-evidence/focused-acceptance-v3.json`, +records 30 cases / 60 Source-deleted MSVC executions in both profiles +(204.35 seconds). Native reports require exact outcomes, channels, messages and +events, exact script exhaustion, and zero ordinary and Resource custody +obligations before teardown. These are separate from reference observations. +All 3,658 frozen inputs and both measured archives plus six nested receipts +revalidate unchanged at recorded HEAD +`50ad73a0693c502d1fa05720091b80a4ab623104`. The receipt SHA-256 is +`7f41c389daa268942afe8a645085daac18d746f553f3be32e93d14f798fc30ea`; +its executable SHA-256 is +`10922730f3f84fa314635f90f8e88ab984aa4212c68b8c0141630a53510367d5`. +The recorded HEAD alone does not establish a later committed candidate gate. + +Four focused MIR groups pass. The separate reference gate passes 89 Resource +groups, including the complete 191-case / 382-profile-execution reference corpus +and the 30-case / 60-execution control-flow focus. Final object preflight +separately emits all 60 control-flow objects. These results are recorded in +`reference-after-v3.json` and `object-after-v3.json` in the same evidence +directory; object emission alone does not prove linked execution. + +The separate full v3 receipt, +`target/native-resource-control-flow-source-evidence/full-acceptance-v3.json`, +records all 191 cases / 382 Source-deleted MSVC executions in both profiles +(1,288.83 seconds), with the same strict native report assertions. Actual session +17187 completed with exit zero. All 3,658 frozen inputs, the recorded HEAD, both +runtime archives, six nested receipts and the frozen executable revalidate +unchanged. The full receipt SHA-256 is +`08bcf865bfed8a9b49f262a6c611336260cd0446ba8517139c71ab10627fb806`. +This full corpus includes the preserved predecessor and the new seven-Source +control-flow packet; it does not include reflected Source06. +The clean base revision `50ad73a0693c502d1fa05720091b80a4ab623104` independently +passes 3,448 workspace tests across 92 result groups, with zero failures and two +ignored tests. Its 3,649 tracked inputs remain unchanged and its worktree stays +clean, as recorded in +`target/native-pipeline-50ad-workspace-evidence/after-evidence.json`. That accepted +base gate does not establish the modified successor's coherent-workspace or +exact-revision CI acceptance. + +## Remaining obligations + +The structural no-Resource proof does not cover erased interface, function, +actor, capability or `TypeConstruction` carriers. Borrowed/projected iteration, +`Never` elements and other iterator forms need their exact compiler transport +proofs. These are outstanding implementation obligations for existing Source +semantics, not new language-policy rejections. The measured Match Sources use +unit variants; payload-variant compositions need independent Source evidence. +Broader owning declarations, alternate Default lifetimes, aggregate custody, +reflected bodies and other For/Match combinations remain in the full goal. + +Source06's historical metadata-selected reflected-field baseline genuinely +reports `resource execution has no exact checked body` (`MissingCheckedBody`), +after construction and before the intended observations; cleanup still retires +its owner. Its diagnostic probe deliberately fails and does not provide an +acceptance oracle. The separate reference repair in the GNU worktree passes the original 11 +proof tests, recursive runtime and required-evaluation tests, and pure selected +Return/Break/Continue/Default controls. Its strict Resource tests also pass all +74 entry attempts across both profiles: nine original Source06 script controls, +same-provider/grant reentry after each, and ten explicit control-flow adaptations. +Exact outcomes/events, full entry-context restoration, zero owners/registry and +empty debug observations are checked before interpreter teardown. These results +are recorded in `target/native-resource-reflected-field-reference-evidence/after-v2.json` +and `after-v3.json`; all 3,662 inputs of the latter gate remain unchanged. +The native HIR/MIR/codegen ordinal bridge remains pending. This reference result +establishes no GNU native acceptance and does not add Source06 to the seven-Source +control-flow packet. + +The complete 191-case / 382-execution Source-deleted MSVC native successor passes. The successor's coherent workspace and +exact-revision CI remain pending, and no GNU or other host acceptance follows +from the focused MSVC receipt. The coarse planning estimate stays about 87%, +with the fixed 207-fixture / 182-run-pass inventory unchanged. diff --git a/docs/active/native_resource_generated_startup.md b/docs/active/native_resource_generated_startup.md new file mode 100644 index 00000000..2d5cb80a --- /dev/null +++ b/docs/active/native_resource_generated_startup.md @@ -0,0 +1,194 @@ +# Generated Resource object startup prerequisite + +This restored bounded implementation connects the selected Resource ownership +contract to the normal launcher. It changes no public Source policy and supplies +no production I/O provider. Authoring was based on published GNU f4/v25. The +current Actor/Resource/Owner/reuse/Callback compiler passes local MSVC focused +controls, the existing private Resource gates and the complete bounded 80-row +startup matrix. Full-workspace/native conformance, exact-revision CI, GNU, +installed distribution and public-provider acceptance remain pending. Earlier +isolated results below are historical handoff claims; they do not validate the +current restored compiler or its archives. + +The normal `jett_aot_v1_entry(context) -> u32` ABI is preserved. A Resource object +calls its own immutable `jett_aot_resource_v1_manifest` accessor into a distinct +32-byte stack record, then calls `jett_rt_v1_resource_program_begin`. Ordinary +objects do not reference either operation. The shared launcher never imports an +optional manifest symbol. + +Begin validates every known input/output/context region before any effect. Under +the same authenticated stationary-context lease, the existing installation +issuer decodes the checked layout, validates the exact entry and retains an owned +byte-for-byte copy before provider/grant publication. Generated ownership fixes +purpose to Runtime. Different wire bytes/entries and active reentry refuse; exact +reentry after completed retirement uses the existing new-attempt constructor. +No error channel is reset, overwritten or transferred to legacy session state. + +The normal provider remains Disabled. Its entry installation receives no paired +Network grant, and actual Network/resource use refuses. This is startup plumbing, +not a real creation/use/drop or production I/O acceptance receipt. The exact +Network-only/Nothing entry boundary is retained for this prerequisite; supporting +mixed ordinary capabilities such as Stdout is still a required subsequent step. + +After successful begin, every generated body/refusal return goes through normal +completion once. The core retains the original body status, first ordinary and +Resource diagnostics, first cleanup status and independent selected kind. A +separate private terminal-status field covers recorded first errors even when the +original body status is zero, without changing the existing completion record +ABI. Cleanup retains priority. Report-allocation failure retains a tombstone and +blocks new attempts; failed retirement remains observable and is never retried as +though it had succeeded. + +The normal launcher reads the retained Resource outcome and copies its selected +diagnostic while the context is alive. Ordinary contexts keep the previous value +failure path. After a recoverable entry panic, the launcher completes only a +Generated-owned Running attempt, before its existing single context destruction. +The returned selected kind distinguishes host panic from cleanup failure even +when both statuses are 255. An existing Completing attempt is refused, never +restarted. Ordinary contexts and externally managed private attempts are no-ops. + +Only `cfg(all(test, jett_resource_native_test_archive))` admits an exact already +Running Runtime attempt owned by the scripted private installer. Begin returns a +zero borrowed-attempt sentinel solely for this generated-wrapper compatibility +branch; the private runner remains responsible for completion and retained +telemetry. Production cannot adopt externally owned custody. Explicit comptime, +verify and property attempts cannot pass this Runtime-only handoff. + +The initial focused controls cover exact reentry/history, active/wrong-context +refusal, swapped layouts, Disabled/no-Network installation, output/context +overlap, reserved metadata, original ordinary first-error copying, Resource first +error/cleanup priority, recoverable panic retirement and non-runtime refusal. +These are host controls, not linked normal-archive acceptance. Required validation +also includes byte-for-byte predecessor private receipts, a normal Resource object +against ordinary debug/release launcher archives, ordinary non-Resource objects, +original body/cleanup outcomes and copied messages, source deletion and context +retirement observations. Those initial host controls supply no native execution or percentage credit. +Current linked observations are recorded below. + +The standalone archive contract retains 37 ordinary leaves, 13 carrier leaves +and eight private hooks, and adds exactly five normal startup exports from the +unconditional canonical `program_entry` child module. Each startup declaration +must remain an unmangled unsafe C function with its exact context, manifest, +attempt, status and output-pointer signature. The standalone Resource inventory +is 55 production symbols plus eight private symbols. Later graph or scalar +composition requires its own exact inventory and fresh authenticated receipts. +The startup source guard runs before tool discovery/output and again after the +source snapshot; receipt authentication checks the same source contract. Python +controls exercise missing/duplicate/foreign/conditional/mangled declarations, +every argument and return type, relocated names, exact text symbol definitions +and mutation between source guards. Restoration preserves the current MSVC +measurement and receipt checks while adding the recovered startup controls. +The current restoration passed all 68 Python test methods; the actual log is +`target/native/startup-python-controls.log`. The saved v2 note reported 30 Python controls passing on Windows Python +3.12 and two initial sandbox permission errors. Those historical observations +supply no current Rust runtime, native or distribution acceptance. + +The saved v2 note reported eight normal Runtime `program_entry` controls and +11 existing launcher lifecycle controls passing with direct stable Cargo/rustc, +offline/locked, explicit MSVC/static CRT, incremental zero, DEBUG zero and two +jobs. Its reported 3766-file source and 60-file Runtime guards describe that +historical isolated tree. Current restored-source controls are recorded below. +The 11 launcher controls cover existing lifecycle paths; they do not +establish linked generated Resource object acceptance. The separate ninth +Runtime handoff control is admitted only by +`cfg(all(test, jett_resource_native_test_archive))`. + +The recovered v3 successor extracts the unchanged entry instruction construction +into a private typed builder used by normal object definition and focused +controls. The controls verify the actual generated IR, startup output separation, +context forwarding, exact ABI, manifest/begin success and refusal edges, the +single completion gate and exact terminal status, private zero-sentinel branch, +and real object relocations owned solely by the generated C entry. Both codegen +optimization modes also require an ordinary object with the same entry and no +Resource startup/manifest reference. These are compiler host controls; linked +normal/debug/release distribution, source deletion and real providers remain +unaccepted. + +The saved v3 note reported four focused codegen controls passing under the same +serial bounded MSVC configuration, including both Source release policies and +both object codegen optimization selections. It also reported the entry +instruction-construction body matching v2 after newline normalization (8630 +characters, SHA-256 +`83e5961d37f88d963374f58588276e998b2f88738d7c0ecea9228756a8f65a68`). +Those historical comparisons and passes do not authenticate current codegen. +The fresh restored-source results below are separate. + +Current restoration passed all 68 Python archive-helper controls, eight normal +Runtime startup controls, four codegen IR/object controls and the 11 existing +launcher lifecycle controls. The workspace library check passed 2,371 tests +across 22 result groups, with zero failures and zero ignored tests. These results +are recorded in `target/native/startup-python-controls.log`, +`startup-runtime-controls.log`, `startup-codegen-controls.log`, +`startup-launcher-controls.log` and `startup-workspace-libraries.log`. Four fresh +callable Driver gates also pass against the changed normal launcher: 48 native +executions, recorded in `target/native/startup-callback-regressions.log`. This is +a compatibility result, with no controlled performance comparison. Host controls +and library tests are distinct from linked native execution. + +Fresh Debug/Release private archives use the current MSVC/static-CRT compiler +inputs, retain exactly 55 Resource exports plus eight private hooks and one main, +and are bound by `target/native/resource-msvc-v6-startup/receipt.json` SHA-256 +`f2675af94be6a5ff32e10157eaadf605ae3374ce9417a0a97f02286e69f42721`. +All 16 existing Resource gates pass against that compiled receipt pin: 754 +executions and 900 entries, counted from the actual v6 logs by +`native-codegen-recovery-2026-10-09/resource-gate-reuse/v6-gate-counts.json`. +This revalidates the existing private scripted-provider scope after startup; +it supplies no real-provider acceptance. + +Generated `program_unwind` selects the context's current Running attempt and +carries no invocation or thread token. Hosts must exclusively own entry and +finish for that context. A caller rejected for concurrent or recursive entry +must not unwind the owner's attempt. The unsafe call requires entry control to +have returned. The built-in launcher creates a fresh context for one entry; +these controls establish no concurrent-host ABI coverage. + +The recovered preparation packet described an 80-execution plan and performed +no builds or native runs. Its current local MSVC successor has now accepted every +one of the 80 exact case IDs: 48 normal controls and 32 private controls, 40 per +Runtime archive profile. All Source checking is Debug (`source_check_release=false`). +Resource objects cover both Jett optimization selections; the four ordinary +controls retain their supported unoptimized selection. The runner and all guards +pass; actual native exits are 60 zero and 20 expected 71 refusal/failure outcomes. +All native terminal receipts are reaped without timeout, ephemeral Sources and +case executables are removed, and the 902 compiler/Source inputs plus protected +artifacts match their afterguards. + +The 32 private controls contain exactly 40 decoded entries. Twenty-eight +unchanged-object controls account for 32 entries; four separately classified +derived borrowed-probe controls account for eight. The two unique derived +objects preserve code/data bytes, relocations, section and symbol metadata, +changing only the entry symbol name. Each derived execution retains a 48-byte +probe containing two rows of six zero u32 fields. Normal manual C controls are +counted as executions, without inventing a Source-entry total from API calls. + +The full result is +`native-codegen-recovery-2026-10-09/startup-linked-current-runner-v1/full80-v6-observations-2/after.json`, +SHA-256 `403d51214936e6c297aafe741a95fc584c825726910421d36481658e43459f1b`. +This closes the local bounded startup80 gate. The earlier private-six result is +a subset and is not added to the 80 executions. Normal Runtime-only and launcher +archives retain their measured compilation, source, COFF and symbol observations. +Their actual links use the exact ordered 14-token static-CRT library vector +measured from fresh private v6, including repeated `kernel32.lib` and +`/defaultlib:libcmt`; no separate normal compiler-emitted +`--print=native-static-libs` note was measured. This narrower vector provenance +is retained explicitly. Full coherent-workspace/native conformance, exact CI, +GNU, installed distribution, public providers and actor-graph import remain open. + +Actual object/manifest observations resolve the earlier wire3 assumption. The +76 linked Resource-object observations contain wire2 layouts; 36 runtime C +manifest checks observe wire2 at execution. The four ordinary controls contain +no Resource manifest. `actual-manifest-wires.json` in the current full80 packet +records the exact observed wire bytes. A separate occupied Source33 object +contains wire3, but that supplemental object-only observation receives no linked +startup execution credit. The Runtime's current wire constant remains 3 with its +existing legacy wire1/wire2 acceptance; these observations require no schema +change or new provider authority. + +The full production objective still requires the public driver/catalog envelope, +closed typed TCP hooks, source-owned APIs, protected Endpoint construction, +Network restriction records, actual stream/listener payloads, readiness/deadline +and cancellation behavior, task/actor/message custody, supported-host loopback +and installed/relocated distribution. The socket behavior contract is selected; +Endpoint public representation and malformed dynamic `Network.allow` failure +policy need focused notes before implementation. Disabled cannot satisfy any of +those production provider requirements. diff --git a/docs/active/native_resource_gnu_gate.md b/docs/active/native_resource_gnu_gate.md new file mode 100644 index 00000000..10fec567 --- /dev/null +++ b/docs/active/native_resource_gnu_gate.md @@ -0,0 +1,82 @@ +# Native Resource GNU acceptance gate + +This change prepares a private GNU Resource acceptance gate. Full GNU Rust +archive builds and Source execution remain unverified until the new CI steps pass. +The existing production package and Source-absent installed jobs remain required. + +## Trust contract + +`tools/measure_resource_native_archives.py prepare` builds fresh Debug and Release +archives on the exact `x86_64-unknown-linux-gnu` host. Cargo receives +`--cfg test --cfg jett_resource_native_test_archive` only after `cargo rustc --` +for the selected runtime package. Private archive target directories, the gate's +compiler target directory, and production package artifacts stay separate. + +The helper measures unchanged runtime/Cargo inputs, compiler and linker identity, +actual ordered `native-static-libs` arguments including duplicates, complete +symbol inventories, Cargo fingerprints, and ELF observations. Each archive must +contain the exact 37 Resource leaves, eight private test exports, and one +`main`. A separate C probe observes the dynamic GNU CRT and `-no-pie` linker +contract; that probe does not establish Resource Source execution. + +Rustup proxy invocation paths are preserved while executable bytes are measured. +Compiler overrides and wrappers are rejected, Cargo uses the measured compiler, +and actual verbose commands must match its identity. Cargo output is forced +uncolored through `CARGO_TERM_COLOR=never` and `--color never`, so command and +library observations are parsed without discarding unexpected escape bytes. +Raw fingerprint bytes and all provenance records are retained for review. + +The separate `run` command receives the independently measured receipt digest +and embeds it in the compiled test binary. Runtime environment supplies only +the receipt path; it cannot replace the expected digest. The existing local MSVC +receipt pin remains the fallback for compilation without a CI pin. The harness +rehashes runtime/archive and measured linker bytes before each link, and the +helper revalidates archives, nested provenance, and Source/compiler inputs before +recording acceptance. + +The ignored lifecycle test executes the complete shared Source corpus in both +profiles. Every execution deletes its primary and synthetic stdlib Source before +launch, then checks exact events, messages, statuses, consumed script, and zero +obligations before teardown. CI retains these measurements and execution evidence +separately from production packages. + +## Invocation + +On the supported GNU host, use a fresh output directory and a fresh gate target: + +```text +python tools/measure_resource_native_archives.py prepare --output target/native-resource-gnu +python tools/measure_resource_native_archives.py run --receipt RECEIPT_PATH --expected-sha256 MEASURED_DIGEST --target-dir target/native-resource-gnu-gate +``` + +Preparation reports its receipt path and digest. CI obtains them through the +preparation step's outputs; execution must not derive its own expected pin from +an arbitrary runtime receipt. + +## Actual verification + +At parent `d1ffd08c`, the revised harness passes all 73 accepted Source scenarios +in Debug and Release on MSVC: **146 Source-deleted executions**, in 533.06 seconds +(`bbaf50`, `target/gnu-private-resource-msvc-accepted-source-regression.log` in +the main workspace). The runtime-environment forged-pin refusal test also passes. +The final helper-only color change does not alter this Rust harness. + +Fifteen focused helper groups pass on Windows and the existing Ubuntu WSL host. +Python syntax, Rust formatting, and diff whitespace checks pass. Real GNU +`cc`/`ar`/`nm` controls accept the exact stub symbol inventory, reject duplicate +definitions, and observe dynamic CRT linking through `readelf`; their evidence is +retained under `target/gnu-private-resource-host-tools-real/` in the main workspace. +Those C controls are not Rust Resource archive or Source acceptance. + +A later narrow typing repair gives JSON list/dict reads precise readonly types +while preserving their exact runtime predicates and diagnostics, and replaces one +untyped test mock with a typed function. Strict Pyright 1.1.405 in Python 3.12 +mode independently selects both helper/test files and reports zero errors, +warnings, or information messages (`3b711f`); all fifteen helper groups also pass +after the repair (`0696a3`). The installed benchmark checker requires an explicit +typeshed path and relative include paths to select those two files. + +No accessible GNU Rust toolchain was available for local archive verification. +**Local full GNU Rust Source execution remains unverified.** Broader language, +provider, concurrency, and coherent-revision platform requirements remain in the +native parity plan. diff --git a/docs/active/native_resource_hook_identity.md b/docs/active/native_resource_hook_identity.md index ea7099da..01b88302 100644 --- a/docs/active/native_resource_hook_identity.md +++ b/docs/active/native_resource_hook_identity.md @@ -1,12 +1,15 @@ # Checked private opaque resource hook identities -Status: the isolated identity implementation passes all 14 focused tests. -The broader compiler, object, driver, frontend and lowering gates also pass. -The subsequent clean `616b51b9` full workspace and all four supported-host -build/installed jobs also pass. The [acceptance audit](native_acceptance_audit.md#resource-static-ownership-boundaries) -keeps local and platform evidence separate. The production catalog is empty; -no Resource value or provider operation executes in this change. Whole-language native coverage remains about 85%, with the fixed -207-fixture inventory and 100% objective unchanged. +Status: the identity prerequisite is implemented. Its original focused receipt +passes all 14 tests, and that earlier compiler/object/driver/frontend/lowering +checkpoint passes. The subsequent clean `616b51b9` workspace and all four +supported-host jobs also pass. These are historical identity/static-guard +receipts, not broader acceptance of the later reference lifecycle. +The private reference follow-up now passes 83 Resource-focused tests; its latest +scope is recorded below. The production catalog stays empty. Whole-language native +coverage remains about 85%, with the fixed 207-fixture inventory and 100% +objective unchanged. The [acceptance audit](native_acceptance_audit.md) +keeps local, reference and platform evidence separate. ## Selected boundary before Rust @@ -130,11 +133,12 @@ selected field/getter copies, known-view payload acquisition and root-value observation gaps. They do not establish source execution or settle the remaining nested printing and borrow provenance boundaries. -The checked map is not a provider installation. The reference interpreter still -needs a real owned Resource carrier, a deterministic fake provider and an -explicit runtime installation retaining the exact checked source/declaration -snapshot. Re-reading a same-named function and attaching earlier DefIds by -name/span is not a valid bridge. +The checked map is not a provider installation. The private reference +interpreter now has the carrier, ordered custody ledger and internal scripted +provider described in [the reference record](#reference-lifecycle-execution). +Execution retains the same successful checked source/declaration snapshot; +re-reading a same-named function and attaching earlier DefIds by name/span +remains invalid. Production catalogs and provider installation stay disabled. One owned value carries one cleanup obligation; a copied internal key or ordinary Rust value clone cannot create another owner. The ordered acquisition @@ -165,3 +169,575 @@ ordinary source failures stay inspectable. This closes the successful-source proof gap without installing a provider or changing the Close signature, source purity rule or Resource value representation. Driver retention and reached-hook runtime eligibility remain separate prerequisites. + + +## Reference lifecycle execution + +The reference implementation over committed caller head `a63f8910` retains an +Arc of the successful CheckedResourceProgram. Physical Resource carriers and +hook descriptors keep that program identity and exact nominal type/hook facts. +The non-cloneable holder ticket is the owner; cloning an internal carrier or +ordinary Value cannot duplicate custody. Caller packets select source-order +argument effects before formal permutation, so View access alone cannot retain +an owner passed bare. Ordinary checked calls relay evaluated ownership envelopes +through entry and return. Generic body selection remains exact; broader scoped +and reflected cursor/capture cases are not claimed complete. + +Executable registration rejoins the unique retained Item::Function and exact +DefInfo. Ordinary declarations have no name-span resolution entry; their actual +DefInfo span supplies the declaration join. Actual mutual bodies additionally +rejoin the authoritative resolution and the retained forward declaration. +Namespace/header checks stay exact, and the retained checked body executes even +if a host registration clone's body changes. A raw name or matching signature +cannot install a hook or replace executable Source authority. + +The ledger tracks source scopes, operation temporaries, partial arguments, +owned returns/storage and occupied result payloads. Views carry checked live +backing without ownership. Explicit close retires the owner once. Reverse-order +cleanup on normal/early return, handled/terminal failure and failed later actuals +is observed before context teardown. The later-named-argument witness preserves +source order `[token, marker, net]` and formal permutation `[1, 0, 2]`: exactly +one Borrow501 proves the first actual ran, and a second would expose erroneous +callee entry after the marker failed. + +Cleanup continues through older owners after a finalizer panic. Cleanup failure +overrides entry failure; successful cleanup preserves an ordinary entry error. +Ordinary source result.fail remains a protocol value. The terminal Source +fixture reaches the actual checked synthetic-Stdlib list kernel and pins +`list.__remove_at: index -1 out of bounds`. The ordinary closure fixture returns +Int64(7) to its Rust caller while the Resource/grant remain unreferenced by the +closure. Both controls assert cleanup/provider observations before teardown. + +Descriptor creation does not execute a provider. Reached Construct, Borrow and +Close hooks require ReferenceRuntime purpose and exact checked target/signature, +caller facts, nominal type, context and authority provenance. Close remains +semantically pure at the source signature boundary; required evaluation cannot +invoke it. Closed descriptors and absent values remain usable without a provider +where already checked. Property evaluation restores its prior purpose on ordinary +success, failure/shrinking and unsupported sample-pool returns. + +Actual root receipt `923450` passes all **36 Resource-focused reference tests** +(36 passed, zero failed, 418 filtered; 0.07 seconds) in +`target/native-resource-after-checked-call-relay-focused.log`. Seven declaration +identity/registration groups, real both-profile Source lifecycles, terminal and +mixed-failure cleanup, and required-worker/purpose controls are included. +Earlier API, parser and declaration-identity failures remain preserved in their +original logs; they are not passing receipts. Root receipt `14ee9c` also passes +all **454 comptime library tests and 87 driver library tests** (541 total) in +`target/native-resource-reference-libraries.log`. The complete frontend fixture +suite then passes **598/598** in +`target/native-resource-reference-fixture-suite.log`. These checks cover the same +unformatted Resource reference candidate, before later pipeline/assignment, +absence-shape and scoped-entry changes. Full-workspace and supported-host +acceptance of this Resource candidate remain pending. + +The required reference work still includes exact scoped/reflected/captured and +worker body contexts, empty Resource-containing aggregate validation, pipeline +input and argument custody, mutable binding replacement, repeated checked-entry +restoration, and occupied aggregate transport/iteration. The existing 36-test +receipt does not cover these gaps. Native providers and MIR cleanup remain +required after reference lifecycle parity. + +The typed absence follow-up repairs exact unoccupied list/map, nominal +struct/generic-struct, enum and machine layouts. It checks every occupied +ordinary field, element, key, variant and state against the checked schema; +absence never grants an owner or provider right. An enum variant constructor +is joined through its exact resolved owner, checked callable signature and +variant field tuple before any actual evaluation. Actuals are evaluated once +in lexical order and only then permuted into fields. Occupied Resource enum +and other aggregate custody transport remains required. + +The expanded focused replay passes **43/43** (`e78a71`), including five new +aggregate/required-worker groups and two both-profile pipeline/assignment +frontend-prerequisite groups. The latter two are source-checking evidence, +not pipeline or assignment runtime acceptance. Current broader receipt +`fa9a70` passes **461 comptime library tests, 87 driver library tests and +598 frontend fixtures** (1146 total) in +`target/native-resource-after-absence-enum-broad.log`. Rust formatting and all +22 Jett fixture format checks pass. The original prerequisite E0378 failure +and enum dispatch failure are retained in their logs, superseded by these +actual passing results. + +Ordinary refinement fields inside an otherwise Resource-containing absent +aggregate still need exact predicate validation; a nominal type tag alone is +not proof. Constrained aliases whose base contains Resource retain the existing +E0367 rejection. The stronger original-body/scoped/reusable-entry layer remains +an unaccepted intermediate until collected required-expression workers and +remaining contexts retain their positive controls. Pipeline/assignment source +validity does not implement custody transport, and production/native providers +and async/actor lifetimes remain required. + +The separate committed caller suite passes **480/480 native tests** at exact +`a63f891016fac7dc50fffdde33be8b0ce45c655f`; see +`target/native480-after-owner-generation/receipt.json`, ending +`2026-10-04T14:20:12.987780+00:00` with stable HEAD/tracked sources. That corpus +contains no Resource candidate and grants no native Resource/provider acceptance. +Its exact supported-host workflow +[37206485724](https://github.com/vycdev/jett/actions/runs/37206485724) passes both +Windows/Linux locked workspace builds and both installed relocation jobs, +ending `2026-10-04T14:51:19Z`. The installed jobs have no source checkout; +`target/native-platform-a63f8910.json` retains the authoritative four-job receipt. +That result validates the caller commit and does not transfer to this Resource +reference candidate. +The 36 reference tests add no native inventory case. The fixed inventory stays +207 fixtures/182 object obligations and the broad feature estimate stays about +85%, with the whole-language 100% objective unchanged. + +Production hook catalogs remain empty; no Resource declaration/provider is +installed in the stdlib. Scripted provider installation is compiler-test-only. +Whole scoped/reflected/indirect/worker breadth, other aggregate ownership paths, +real providers, task/actor cancellation and late completion remain required. +At that reference checkpoint, HIR hook identity/operation lowering, Resource-linear +MIR transfer/drop proofs and native carriers/providers were still missing. Nested ordinary printing, +hidden Secret, known absence and omitted rendering policy is unchanged. + +## Native custody core protocol + +The private native runtime core is implemented independently of reference AST +transport. Program/kind/owner-slot references keep one constructor-owned +registration identity. Non-cloneable owner and loan tokens rejoin the registry's +context, nominal kind, key generation and authority provenance. Transfers +advance holder generations without minting an owner from a copied carrier. +Chronological scope/operation/return frames retire loans before reverse owner +cleanup. A protected finalizer panic does not stop cleanup of later owners. + +Resource completion retains cleanup-first precedence in a separate channel; +ordinary native first-error behavior is unchanged. The controls cover failed +publication cleanup, active-loan move refusal, stale/foreign/wrong-generation +tokens, repeated entries, counter exhaustion, cleanup continuation and every +entry/cleanup outcome pair. They assert owner, loan, frame and registry counts +before context teardown. Dropping a copied physical carrier or token is not +source-scope cleanup. + +Root's actual focused receipt `7b0e8f` passes **13/13** protocol tests in +`target/native-resource-runtime-custody-core-first-focused.log`. After runtime +formatting, `9e9957` passes **157/157** complete runtime library tests in +`target/native-resource-runtime-custody-core-formatted-library.log`. +At the92421063 checkpoint, registration was test-only. Its later private +metadata implementation is recorded below. Native ABI, Resource CFG ownership/ +drop plans, linked source-deleted lifecycle execution and real providers +remain required. This protocol core adds no native inventory +case; the broad feature estimate remains **85%** and the whole-language objective +remains 100%. + +## Checked required, scoped, pipeline and assignment transport + +The expanded reference transport preserves the immutable checked program and +original function, generic/scoped body, namespace initializer, explicit comptime, +verify or property region. Entry selection and metadata installation are +transactional; normal return, failure and re-entry restore the previous body +cursor and complete concrete type/reflection facts. A cloned AST or another +program/context cannot replace the original source occurrence. + +Pipelines stage actuals in source order using original step identities before +formal permutation. Mutable assignment keeps its declaration slot after a move +or Close, evaluates RHS while the previous owner remains observable, then retires +and publishes owners exactly once. Reverse cleanup still completes after an +eligible finalizer panic. Generic intrinsic arguments use the exact selected +body's concrete type IDs and complete reflection records, preserving alias names; +no ambient metadata or unresolved source type name supplies that proof. + +Root receipt `61a351` passes **83/83** focused tests in +`target/native-resource-concrete-intrinsic-pipeline-syntax-fixed-focused.log`. +This includes all 12 pipeline and seven assignment groups, the preserved generic +Source27 oracle and four new both-profile metadata/corruption groups. The two +test helper API corrections and canonical `into` fixture spelling preserve all +expected values and refusal oracles. The complete formatted reference/driver/frontend gate also passes all **1,186 tests** (501 Comptime, 87 driver, 598 frontend; `4ba85c`) in `target/native-resource-transport-formatted-property-fixed-broad.log`. Rust formatting and all eight new Jett +fixture format checks pass (`658c63`). Earlier failed gates remain in target logs. + +The foundation `7157f564` supported-host run +[37217759157](https://github.com/vycdev/jett/actions/runs/37217759157) completed all +four Windows/Linux workspace, package and relocated installed-runtime jobs. +That receipt excludes these later transport changes and custody core `92421063`. +Ordinary refinement children, complete capture/reflection/worker contexts, +occupied aggregates and real providers remain required. This is reference +execution; it adds no native inventory case or native Resource admission. + +## Checked HIR/MIR Resource manifest handoff + +The checked lowering entry accepts the original immutable +`Arc`. Its private manifest retains every nominal +Resource declaration and trusted hook, including unused entries, and validates +exact declaration origin, type identity, closed recipe and complete signature. +Program-bound kind and hook references cannot be minted by raw lowering or a +matching name/signature. HIR descriptor and invocation nodes preserve the exact +original Source call facts, operands and lexical argument order; MIR retains the +same manifest through its visitors and remaps. + +This is a metadata handoff. Resource actuals that require unsupported eager +staging fail transactionally. Ordinary acquisitions, copy/move/drop plans, +native constants and emission retain explicit pending Resource refusals. +Descriptor retention does not issue runtime storage, a provider or ownership. + +The focused original Source gate passes all five groups (`852dfc`). After +formatting and the reviewed diagnostic successor, the complete phase gate passes +**522/522**: 191 HIR, 198 MIR, 70 codegen and 63 object-emission tests +(`961b04`, `target/native-resource-manifest-metadata-fixed-phase.log`). Foreign +nested type IDs are now rejected by recursive manifest validation before the +ordinary native type gate; the controls pin that exact diagnostic and separately +retain lower-level malformed-type refusal coverage. All other Source fixtures, +corruption inputs and expected values are unchanged. + +Resource CFG ownership/drop plans, native registration/ABI, real providers and +linked source-deleted Resource execution remain required. These checks add no +native execution case and do not increase the approximate 85% coverage estimate. + +## Bounded native registration and nominal issuance + +The private runtime registration path now owns and validates the complete +`JTRSC001` layout blob. A 56-byte header, explicit little-endian tags, bounded +section sizes and dense ordinals reject malformed/truncated/trailing data. +Every unused hook, canonical signature/shape, occupied path, frame and operation +reference is checked before the sole production-capable issuer creates fresh +nonzero nominal kinds and the complete slot-kind projection. One context-bound +installation holder rejects repeated installation, foreign context, shutdown +and nonempty registry state. Metadata agreement cannot create Source authority, +a provider grant, a resident loan or ownership from carrier bits. + +Scope, Operation and Return signatures all describe the containing Source +function. Signature agreement is checked across that function's frames even at +different instruction sites; hook and callee signatures remain separate. The +new refusal control also corrupts an unused Return frame. Physical acquisition +preflight checks slot bounds, generation/creation exhaustion and storage +capacity before a later provider runs. The core preparation and future opaque +token-table preflights remain separate mandatory checks. + +The exact six-file/eight-hunk packet and eleven inputs passed Root guarded +application (`58a435`) after independent byte/API review. The initial nine +registration groups pass (`f753aa`). After the selected frame-signature +strengthening and formatting, the complete runtime passes **167/167** +(`56e7ac`, `target/native-resource-registration-formatted-library.log`), including +ten registration and the existing thirteen custody protocol controls. + +The context/ABI adapter, typed grants, provider installation, compiler ownership +and cleanup CFG, occupied sums and linked source-deleted execution are still +required. These are runtime metadata/protocol tests; they add no native Source +execution case and keep the approximate coverage estimate at 85%. Full aggregate, +capture/reflection/refinement, real-provider and async/actor/cancellation breadth +remain part of the 100% objective. + +## Source-authenticated ownership handoff + +The original checked HIR/type archive and constructor-emitted MIR witness now +support a separate `ResourceOwnershipPlan`. It records exact scopes, operation +and provisional-return frames, owner holders, incoming views, bounded loans, +transfers, occupied sum extraction and cleanup obligations. Original/current +Source call associations preserve bare owned Resource-to-view custody without +creating ordinary owning operand claims. Copied queries, changed call sites, +operands, Source certificates, evaluation order and nominal type meanings are +refused. Unnamed Resource-bearing intermediate expressions retain their witness. +Canonical block/local compaction retains exact current associations. + +All **12 focused ownership groups** pass (`155203`, +`target/native-resource-ownership-associated-focused.log`). The Source-derived +missing nominal-table regression passes (`fe7b0a`), returning the precise error +without a panic. The original nominal fixture retains both field-corruption +oracles after copying the complete original nominal tables and selecting its +actual Source Holder type. The malformed-type phase controls exposed a validation +ordering bug; valid type/invocation metadata is now required before custody +queries, preserving the exact manifest diagnostic and independent scalar refusals. + +The complete formatted affected gate passes **535/535** (192 HIR, 210 MIR, +70 codegen, 63 object-emission), plus 32 type-library tests and one MIR doctest +(`3c6a8a`, `target/native-resource-ownership-valid-metadata-phase.log`). Earlier +failed runs remain diagnostic evidence. This is initial compiler ownership +analysis: connected native frames, provider/ABI operations, operational cleanup, +handled actual normalization, complete aggregates/reflection and linked binaries +executed after source deletion remain required. The separate 480-case caller +receipt, fixed 207/182 inventory and approximate 85% estimate are unchanged. + +The broader driver/frontend gate also passes **693/693**: 87 driver library, +8 backend-lowering tests (including all 182 run-pass lowering obligations) and +598 frontend fixtures (`450ea4`, +`target/native-resource-ownership-driver-frontend.log`). The previous registration +commit's Windows CI job failed its CLI native-setup capture test with a main-thread +stack overflow; that separate platform regression is under investigation and is +not represented as current ownership or whole-workspace acceptance. + +## Native context adapter and custody operations + +The private runtime adapter connects the existing authenticated stationary +context lease, bounded layout issuer, registry and custody core. Exact current +attempt, frame, owner, loan, prepared hook and Resource sum records retain the +non-cloneable core tokens. Ordinary and Resource lookup IDs share one checked +monotonic allocator. Installation publishes a scripted provider and paired +ordinary Network grant transactionally after preflight; that provider is test +only. Production state starts absent with disabled providers and no new exported +Resource leaf. + +Real core operations now cover root entry, Operation frames, direct/descriptor +hooks, acquisition, moves, bounded borrowing, close/drop, occupied sums, staged +replacement and protected reverse cleanup. Completion preserves the observed +body status verbatim and separately selects cleanup, host panic and current +ordinary/Resource body failure. Ordinary FailureTake is neither invoked nor +reset by the adapter. Refused transfers retain the old owner; a cleanup fault +cannot publish success. Borrow domain Fail stays ordinary Result data. + +Initial compilation found three private API/field-name integration errors. The +first runnable focused check passed 13/14 and exposed string-only cleanup being +used for an ordinary borrow Result. The bridge now uses the existing recursive +native destructor, and an additional real-provider BorrowFail control checks +sum/string retirement, duplicate-drop refusal, clean completion and exact events. +All **15/15 focused groups** pass (`fd8577`, +`target/native-resource-state-adapter-companion-fixed-focused.log`). After +formatting, the full runtime package passes **182 unit + 13 integration tests** +(`96d995`, `target/native-resource-state-adapter-formatted-runtime.log`). The +earlier compile/failure logs remain diagnostic evidence. + +These are runtime host-protocol controls, not linked Source execution. Exact +Source child Scope/Return activation, resident incoming views, connected C leaves +and emission, operational compiler cleanup, the matched archive and linked +source-deleted binaries in both profiles remain required. The adapter-head Windows +CLI setup-capture stack overflow is recorded separately; the later helper +repair below supersedes that pending local regression. Broader +aggregate/reflection/refinement, provider and concurrency coverage remains part +of the full goal. Native caller 480/480, the fixed 207/182 inventory and the +approximate **85%** feature estimate retain their separate scopes. + +## Windows lowering stack repair + +The unchanged CLI native-setup capture test reproduced a main-thread stack +overflow at Resource ownership and adapter heads. The adapter-head Windows CI +also completed the native conformance suite with **480/480 passing** before the +workflow failed its separate CLI capture target (`b32d80`, workflow +`37244085637`). This is current adapter-head supported native execution evidence; +it still includes no native Resource lifecycle case. + +The repair extracts the existing heavy HIR statement/equality/inline-function +branches and MIR expression/per-actual branches into private helpers. Original +Source facts, Resource call capture, preflight, lexical order, shared call-view +scope, rollback, spans, diagnostics and context restoration remain at the same +operations. Source fixtures, assertions, snapshots, launcher order and stack +sizes are unchanged. Exact guarded application reconstructs both files and all +39 hunks (`3a8820`); the independent reconstruction record agrees. + +The original regression now passes **1/1** (`285533`), and the formatted full +CLI capture target passes **9/9** (`508636`). The formatted phase gate passes +**535/535** HIR/MIR/codegen/object checks plus 32 type tests and the MIR doctest +(`f8fe67`). The broader gate passes **693/693**: 87 driver library, 8 lowering +checks including all 182 run-pass fixtures, and 598 frontend fixtures (`647ede`, +`target/native-windows-stack-decomposition-driver-fixtures.log`). An initial +command used the nonexistent test target frontend; no driver test ran in that +attempt. The corrected fixture_suite command supplies the actual receipt. + +Actual Debug assembly measurement (`779571`, +`target/native-windows-stack-decomposition-frame-measurements.json`) records +MIR lower_value decreasing from 169,448 to 15,272 bytes and the ordered-call +loop from 73,736 to 4,504 bytes. Its separate per-actual helper uses 70,104 bytes. +HIR lower_expression decreases from 117,624 to 106,200 bytes and lower_statement +from 108,552 to 100,216 bytes. These are individual function-frame measurements, +not a whole-program stack limit or proof of every recursion shape. + +The local platform regression is repaired. Whole-head supported-host CI and +connected Resource Source ABI/emission/cleanup and linked lifecycle execution +remain required. The fixed 207/182 inventory and approximate 85% coverage +estimate remain unchanged; runtime/metadata checks do not supply the missing +Resource native execution. + +## Resource execution family and emitted layout + +The compiler now captures the Resource execution family only after validating +the complete original checked HIR archive. Resource-free direct Source caller +predecessors receive sealed constructor witnesses and fresh Scope/invocation +plans. Edited current graphs cannot remove or recapture those witnesses. +Exact formal projections retain actual and parameter types, access, spelling, +effects and lexical evaluation order. Absent Optional and failed Result shells +receive exact holders; an ordinary failure companion is extracted once. + +One fresh whole-program plan now projects deterministic bounded v2 layout bytes +and the exact driver-selected entry tuple. Every manifest hook remains checked, +including unused descriptors. The immutable object accessor emits metadata only. +Return transfer executes under the callee root Scope while retaining its private +Return destination; publication uses the same completed Scope and resolves the +active caller destination. These are distinct from the original recorded Return +storage and site facts. + +The original producer packet reconstructs 11 paths and 33 hunks (`4319e3`). +Actual compilation exposed a missing FunctionId ordering implementation and a +private-module import; the fixes use stable dense IDs and supported readonly MIR +reexports. New fixtures were corrected to the existing owned-capability entry +rule after E0503. No language or checker rule was relaxed. Two narrow writer +successors preserve Return storage while projecting its active Scope (`25bf9c`, +`f36175`). Independent source/API review found no additional concrete blocker. + +All eight focused groups pass in both compiler profiles (`1cba25`, `242535`). +The formatted compiler/object gate passes **543/543** plus the MIR doctest +(`fc3f02`). After the final Return projection, all **74 codegen and 63 object +checks** pass again (`45ddfe`). The driver gate passes **693/693**: 87 library, +8 lowering and 598 frontend fixture checks (`b9de51`). These are Source, compiler +and metadata checks. Dedicated body/ABI emission, ordinary companion cleanup, +matched runtime archive and Source-deleted linked lifecycle runs remain required. + +## Native Source runtime and custody leaves + +The runtime validates complete v2 Source invocation rows and implements the +32 exact Resource custody leaves. Formal permutation, lexical evaluation order, +owned parameter slots, resident parent loans, callee Scope/Return roles and the +original caller result destination are checked before execution. Version 1 +remains historical metadata and cannot activate the new Source transitions. + +Source entry preflights all owner generations, borrows, destination holders and +capacity before transferring any owner. Resident views keep one parent loan. +Return publication joins the completed callee Scope, current provisional Return +activation and exact caller destination. Scope/Return cleanup failure cancels +publication and outranks the separately retained observed body status. None and +Fail shells move whole; ordinary Fail companions use ordinary recursive drop. +C output alignment, overflow and known overlaps are refused before effects. +Production state and providers remain disabled. + +The guarded runtime packet applied 16 paths and 61 hunks (`4788d3`). Its first +nine Source controls passed (`ea7d1f`). The formatted focused gate passed three +v2 registration, two batch-transfer and all fifteen previous adapter controls +(`29d4b6`). The first full suite exposed an obsolete test that expected version 2 +to be an invalid header. The correction tests unsupported versions 0, the next +version and u32::MAX; no decoder refusal was weakened. The repaired formatted +runtime suite passes **196 unit and 13 integration tests** (`fd04d3`). + +These checks execute real private runtime transitions and C-boundary controls, +not generated Jett Source bodies. Dedicated native body emission, ordinary +companion cleanup, a matched archive and Source-deleted linked lifecycle runs +remain required. Real providers, occupied aggregates, captured/reflected +contexts and concurrency retain their full-goal obligations. The approximate +85% estimate and fixed inventory are unchanged. + +Independent byte review found eight unrelated Unicode test literals changed by +encoding conversion in the authored packet; Root restored the original HEAD +bytes exactly and reran the runtime suite (`e45fe3`). Independent semantic review +also found that ordinary nested sums could carry the wrong physical payload +ownership bit. Three causal controls failed before the repair (`bf61fa`), then +passed with exact immediate Optional/Result ownership validation (`5af1b8`), +preserving malformed carriers and unrelated live strings on refusal. + +A selected pre-Rust root-outcome seam preserves the original body status after +exact root Scope retirement. Its readonly C lookup joins the same installed +entry tuple and current Runtime attempt, refusing active, stale, child, foreign +or worker-purpose roots without latching a body fault. Output refusal precedes +state effects. Body zero stays zero when cleanup panics; host completion retains +its independent cleanup-first channel. The successor applied five paths and +eleven hunks (`f54e7e`). One authored cleanup control incorrectly left its owner +in an Operation holder; the actual failure (`5763d2`) led to moving it through +real Source Return publication into the root holder. All five controls then +passed (`4f200a`). The final formatted runtime suite passes **204 unit and 13 +integration tests** (`716d31`). Source-native execution remains pending. + +The final reference Resource regression filter also passes **67/67** (`fd2bdb`). +Independent current source/API reviews confirm the malformed ordinary-sum fix +and exact readonly root-outcome boundary. Their finite scope does not establish +multi-owned Source actual or recursive activation coverage or native execution. + +## Fresh-plan ordinary companion analysis + +ResourceCompanionPlan borrows the fresh custody plan, exact immutable current +function and types. It retains ordinary storage/type/child, initialization, +liveness, alias, loan and move analysis and complete original Source acquisition +validation. Resource carriers do not enter ordinary owned storage. Readonly +ordinary storage and CallerAcquisitions retain the same current program lifetime; +public ordinary Copy/Move/Caller gates keep their Resource-pending refusals. +Exact expression occurrence roles are captured by the fresh CFG validator; +identical cloned expressions do not select operations. + +The guarded prerequisite applied seven paths and 24 hunks (`7bca1d`). Actual +compilation exposed use of the public caller admission gate inside the private +companion path (`12afac`). The repair uses the same complete internal Source +validator only in the already authenticated companion context, retaining the +ordinary public gate. The constructor also stores its readonly Source acquisition +result for dedicated body emission. All three both-profile Source groups pass +(`bbbf1d`). The formatted gate passes **546 checks**: 74 codegen, 63 object, +192 HIR and 217 MIR tests, plus the MIR doctest (`f48b08`). Independent current +source/API review confirms the finite ordinary-analysis boundary. Dedicated +body emission and linked native Resource lifecycle execution remain pending; +whole-language coverage remains the about-85% estimate. + +## Direct body and matched archive checkpoint + +Direct original-Source body emission now compiles the selected Scope ABI, +lexical Source/Hook actual staging, resident-view transport, typed sums, +ordinary failure companions and provisional Return publication. Five focused +groups check both compiler profiles. The full formatted compiler gate passes +551 checks plus the MIR doctest after restricting Resource cleanup storage and +blocks to the selected family. Exact indirect descriptors, eagerly aborted +Resource acquisition CFGs and mutable Replace rows remain explicit successor +work; they are part of the full goal. + +The actual double-cfg debug and release runtime archives compile on Windows +MSVC with coherent `+crt-static` dependency flags. Complete symbol inspection +finds exactly one main, eight private observation/host exports and all 32 +production Resource leaves in each. A separate production archive has no +private main or test exports; the ordinary runtime suite passes 217 checks. +The measured vectors retain their original order and duplication, including +`/defaultlib:libcmt`. These are local Rust 1.93.1 checks; supported-host CI uses +its separately pinned toolchain and remains a distinct gate. + +Four encoder controls run against exact current record declarations and the +real report module; six driver decoder controls pass. They preserve unknown +versus empty observations and original body/cleanup channels. Actual HOST C ABI +negative controls pass with both measured profiles; these handwritten controls +remain separate from Source conformance. The real reference and native gates +each pass five shared original Source scenarios under both profiles, covering +connected lifecycle, factory failure, partial actual failure, implicit drop and +finalizer panic. Native execution runs after deleting Project and synthetic +Stdlib Source directories and independently verifies outcomes, exact events, +script exhaustion and zero Resource/ordinary obligations before one destroy. +This accepts the first local compiler-test slice. It does not remove provider, +aggregate, reflection, refinement, task, actor, concurrency or supported-host +distribution obligations. The broader driver gate also passes 693 checks. + +## Replacement refusal before finalization + +The native plain-owner replacement protocol now validates both owners and reserves generation/acquisition capacity before old cleanup. New causal controls cover a loan on either owner, a consumed stale RHS handle and deterministic RHS generation exhaustion; each refusal preserves live owners and finalizer events, followed by a valid replacement or transfer. The unchanged success/finalizer-panic baseline also passes. The formatted focused gate passes four tests, and the full runtime gate passes 220 checks (207 unit and 13 integration). These are runtime/HOST controls. Source Replace producer/writer/emitter and aggregate replacement remain required; the 85% estimate is unchanged. Earlier matched archives and ten Source-native executions apply to cc06aa1f, before this runtime change. Fresh private archives and Source acceptance must be measured separately. + +## Source mutable replacement checkpoint + +The original Source replacement gate failed at the missing evaluated-RHS layout field before this compiler change. MIR now seals that field, the writer emits exact existing tag 12, and native assignment handles live replacement, exact self-reseat and vacant-after-close transfer separately. Seven new both-profile compiler groups cover endpoints, Source/header mutation refusals and object routes. Actual compilation required one missing test-only ExpressionKind import; canonical formatting changed no Source behavior. The final compiler/object/HIR/MIR gate passes 558 checks plus the MIR doctest, and 606 frontend/lowering fixture checks pass. All eleven shared original Source cases now pass both real reference and Source-deleted native execution under debug and release (22 executions each). Six added cases cover live replacement/use, self-reseat, close/rebind, handled RHS failure borrowing old, ordinary RHS failure and old-finalizer panic cleaning both owners. + +The fresh runtime archives have measured receipt `54a38248`; both HOST C ABI profiles pass. The complete driver-library run passes 86/87, with one pre-spawn temporary capture PermissionDenied; that exact property replay passes in isolation. This is recorded separately from Source acceptance, and no clean complete driver-library or current full-native regression pass is inferred. Whole-language coverage remains about 85%; indirect/handled/aggregate/worker/provider/concurrency and supported-host obligations remain required. + +## Constructor-owned handled Source calls (2026-10-05) + +MIR now preserves handled direct calls through typed Begin/Stage/Invoke/End +regions with private original/current witnesses. Fresh flow joins the exact +operation stack, arrived lexical prefix, loans and owners. A handler Return ends +its acquired prefix before lowering its original operand; clean abandonment +creates no body error and performs no target invocation. Prepared formals and +loan parameters are available without scanning a future reached Invoke. + +Native frame storage retains the dynamic operation and prepared call separately. +Staging evaluates once and registers exact Source actuals; invocation uses arrived +endpoints and checks Source status before publishing its output. Retirement uses +the fresh prefix plan. Local sum storage can belong to an ancestor Scope while +its instructions execute under the active Operation; a private validated current +site projection supplies that execution frame. Wire-v2 and runtime checks stay +unchanged. + +All eighteen shared Sources pass both real reference and Source-deleted native +profiles, with exact outcomes/events and zero obligations before destroy (36 +executions each). The formatted affected gate passes 573 compiler/object checks +and the MIR doctest, plus 606 frontend/lowering fixture checks, all four fixed +inventory gates (182 host objects), and the complete 92-test driver library. +The final formatted Source replay also passes all 36 reference and 36 native +executions. Broader Resource transport and the final full-goal release gates +remain required. + +## Callable boundaries and failed native joins (2026-10-06) + +The shared Source lifecycle gate now has 22 scenarios, all passing reference and +source-deleted native execution in debug/release (44 each). Three new programs +exercise reversed temporary-owner ordering, an independent Return helper and a +continuing default. A fourth scenario reuses the helper program with real cleanup +panic and proves the helper is not evaluated while both owners are retired. + +Reference Return cancels only newer operations within the current callable's +floor. Ordinary closure/named fallback bodies have no Resource publication +destination. Exact one-shot cancellation receipts cannot authorize normal +success or output adoption. Native SourceStatus validates a terminal completion +record tied to the caller operation, immutable Source row and physically retired +callee Scope/Return. Nonzero original body 71 stays 71; body 0 plus actual cleanup +panic remains original 0 internally and carries cleanup 255 to its caller. + +The full affected suites pass 508 reference-library, 92 driver-library and 222 +runtime checks. Fresh measured debug/release archives and all 44 native runs pass +with unchanged ABI 1, wire 2, one main, eight private exports and 32 Resource +leaves per archive. Exact parent a30ca97e workspace/default-native and all four +supported-host distribution jobs pass; successor release gates remain separate. +These results advance lifecycle coverage while the broader 85% estimate and +full language/provider/concurrency objective remain unchanged. diff --git a/docs/active/native_resource_nested_sums.md b/docs/active/native_resource_nested_sums.md new file mode 100644 index 00000000..d3b7a67c --- /dev/null +++ b/docs/active/native_resource_nested_sums.md @@ -0,0 +1,111 @@ +# Nested borrowed Resource sums + +The native-codegen goal remains active, at an approximate 86% feature +checkpoint after the native execution gate. This successor implements established lexical view lifetimes; it +must not infer ownership authority from a matching expression, span or type. +No new source spelling or runtime Resource ABI is proposed. + +## Genuine initial baseline + +The accepted predecessor is d1ffd08c433023f65aa18f227b0d344e559f728c: +73 shared scenarios, with 146 real-reference and 146 Source-deleted MSVC native +executions in debug and release. The original sources and complete production +inputs were frozen before the next probe. + +Source57 declares an explicitly viewed handled Optional Resource payload inside +an `if enabled` body, then returns the borrowed observation. With unchanged +production code, all 74 shared scenarios pass 148 real-reference executions. +Native lowering rejects the new original source in MIR, at File0 14..387: +`pending ResourceOwnershipPlan: nested Resource declarations need exact constructor Scope records`. +That genuine rejection establishes the declaration-inventory gap; it is not new +native acceptance. The terminating branch alone cannot establish joining-branch +or loop cleanup. + +## Planned original Source packet + +Sources58 through95 exercise written/bare actuals, Optional Some/None and Result +Ok/Fail, and aliases inside joining `if`, scoped type bodies and two-iteration +`while` bodies. Additional controls retain an owned backing sum in the same +function and unwrap/close it after the lexical boundary, distinguish same-named +aliases backed by separate locals in sibling blocks, and handle an inner borrow +failure without changing the outer payload handler. Owned loop controls also +exercise Break and Continue before using the original backing afterward. + +Every occupied source has a separate construction-failure script against the +same bytes. Later-construction failures preserve exact prior finalization order. +The frozen packet contains 138 scenarios / 276 executions per backend. All +276 genuine reference executions pass with unchanged production code. The +expanded native baseline again refuses Source57 at the same MIR span and +constructor-scope diagnostic. All 2,227 source/config inputs revalidate after +both actual runs, and the accepted 73 Case rows remain exact. Source authoring +or reference success does not increase accepted native coverage. + +## Required private proof + +Use one exhaustive original-body declaration inventory over lexical blocks and +handler failure bodies, without entering a separate inline callable body. Each +borrowed alias key retains the original function/archive, declaration LocalId, +immutable local headers, complete initializer, exact backing local and lexical +path. Match the complete expected key set bijectively; an equal row count or an +expression/span lookup is insufficient. Canonical remaps may update current +headers and sites only after validating the independent original/current body +seal. They cannot rewrite the original occurrence or manufacture authority. + +Only exact authenticated borrowed alias declarations may cross the current +nested-declaration refusal. Nested owning declarations and unrelated view locals +retain their own conservative admission rules. Child payload loans end before +parent shell loans, and generated alias/source/output state is retired at the +actual lexical exit before a branch join or loop backedge. Runtime reissue at +the same static row must receive fresh dynamic loan handles. + +Authenticate Fallthrough, Return, Break and Continue independently. A Return +operand that observes its alias must be evaluated before that alias retires; +abandoned argument-prefix cleanup retains its existing separate ordering. +For/Match/reflected bodies and declarations after a terminal Return remain +full-goal obligations unless actual original Source and constructor exit proofs +cover them. A recursive visitor alone cannot claim execution support or erase an +unemitted occurrence as permission. + +## Acceptance + +Freeze the full Source-only packet and production hashes, pass every real +reference execution, then implement the constructor/MIR/codegen handoff. Require +negative controls for sibling/backing swaps, duplicate/missing keys, altered +headers/initializers/guards/exit membership, foreign archives, copied or +independently resealed/pruned bodies, and disconnected extraction. Run all shared +sources in both native profiles with exact scripts, outcomes and event ordering, +Source absent at launch, and zero ordinary/Resource obligations before teardown. + +The existing runtime can end and reissue a loan at the same static operation; +logical lexical retirement alone requires no ABI/schema change. Runtime changes +would instead require fresh independently measured archives and a new pinned +receipt. Inactive loan history grows across iterations even when live obligations +are zero; this existing storage behavior is separate from lexical correctness. +GNU private execution and current coherent workspace/package/installed gates +remain independent acceptance requirements. + + +## Verified native execution + +The frozen candidate passes all 138 shared scenarios in Debug and Release: +276 Source-deleted MSVC native executions in 968.37 seconds, with exact scripts, +channels, messages and finalization events, and zero ordinary/Resource obligations +before teardown. All 2,234 compiler/Source/config/gate inputs revalidate after the +actual run; the original packet and runtime bytes remain unchanged. The initial +276 reference executions remain the independent expected-outcome baseline. + +All 261 MIR tests pass. New controls cover original declaration identity, sibling +backings, guards, copied/missing/altered exits and exact owning-backing reuse after +retirement. Two earlier disconnected-custody mutations remain intact and now fail +at the stronger graph seal before the final CFG scan. Six genuine public object +baselines and eighteen copied/retargeted/unwitnessed graph controls pass across +both profiles; no forged graph emits an object. All 103 codegen library, 63 object, +521 reference-evaluator and 93 driver-library tests pass. Workspace formatting +and the frozen Source packet's Jett formatting also pass. + +This closes the tested If/scoped/While/Break/Continue family. For/Match/reflection, +unemitted declarations, ordinary data views and alternate Default lifetimes stay +open. GNU private execution uses the separate measured gate. The four supported +host jobs passed at predecessor d1ffd08c; updated coherent-revision workspace and +platform acceptance is still required. The approximate 86% feature estimate is a +planning measure, not measured code coverage or whole-language parity. diff --git a/docs/active/native_resource_occupied_structs.md b/docs/active/native_resource_occupied_structs.md new file mode 100644 index 00000000..229bb2ce --- /dev/null +++ b/docs/active/native_resource_occupied_structs.md @@ -0,0 +1,97 @@ +# Occupied Resource struct constructors + +Checked named construction preserves written field evaluation order and the +original nominal schema. Begin/Stage/Commit/End carry a private constructor +witness through MIR and codegen. Original/current body and operation checks +reject forged public nodes before pruning. Slot and carrier migration keep one +cleanup obligation per owned Resource. Normal and abandoned-prefix cleanup +retire owners before finalizers, in reverse acquisition order. + +Sources33..35 cover ordinary scope drop, reversed named argument order, and +abandoned construction after a Resource has staged. Sources30..32 preserve the +historical invalid view-capability entry and its E0503 refusal. The shared support +Sources and original Source17 remain unchanged. + +## Resource predecessor verification + +At Resource commit `c141de22`, the integration includes the published actor +identity change. Local +MSVC static-CRT checks on 2026-10-12 pass all 308 runtime, 577 interpreter, 317 +MIR, and 120 code generator unit tests. The three-Source object gate emits all +six objects with both codegen optimization settings. Focused tests validate +constructor provenance, lexical staging, copied-node and forged-header refusal, +exact owner migration, stale generations, atomic late refusal, and cleanup that +continues after finalizer panic. + +Fresh linked acceptance passes on 2026-10-12 against independently measured +private Debug/Release MSVC static-CRT archives with both cfgs and ABI1/wire3. +The occupied retirement gate passes 22 executions/22 entries; same-grant reentry +passes 22/44. The original twelve predecessor gates pass 604/728 against the +same archives. Additional pipeline/control-flow subset gates pass 106/106, +so all sixteen Resource gates pass 754 native executions/900 entry completions. Each +gate removes its project and synthetic stdlib Sources before execution and checks +exact script events, failure precedence, statuses, messages, process exits, eight +zero custody counts and empty ordinary storage before one teardown. + +The MSVC measurement helper passes all 60 Python controls. It measures actual +compiler/linker commands, ordered native libraries, complete COFF members and +exact runtime exports. Cargo reports a freshly compiled gate; its dep-info +contains the independently supplied receipt pin exactly once. Runtime receives +only the receipt path. Source, archive, compiled gate and dep-info guards pass +after execution. The local predecessor collector was repaired to recognize its +two actual summary formats, then validated the completed logs without rerunning +the native tests. Accounting distinguishes the original twelve-gate corpus from +the subset and occupied gates. All 2,314 workspace library tests pass. This +evidence does not include an independent second raw-report +decoder or retain thousands of per-case executable copies. + +## Combined revision and executable reuse + +Fresh verification on 2026-10-12 includes Actor, Resource and Owner commit +`7ff46cc7` plus the executable-reuse harness change. All sixteen private MSVC +gates pass again: 754 Source-deleted executions and 900 entry completions. +The newly measured Debug/Release ABI1/wire3 static-CRT runtimes include the +additive Owner validation leaf. Receipt SHA-256 is +`bc2ad54743380441f074d8d5d212048ecf440669817c0cb69ce2fd0810ba1c95`. +Fresh gate compilation, source/archive/tool/binary/dep-info guards and the +runtime-environment pin refusal control pass. The combined implementation +passes all 2,331 workspace library tests; this is not full native conformance. + +Consecutive script cases with identical project bytes reuse one completed +program within the exact runtime profile. Every case still runs in a fresh +process and working directory, after Source removal and binary-hash validation, +with its own script, report and unchanged oracle. Compiler-session proofs and +runtime state are never cached. The 191-case gate retains 382 executions while +reducing emitted/linked programs from 382 to 246. Occupied retirement retains +22 executions while reducing builds from 22 to 6. Reentry gates are unchanged. + +An additional evidence-enabled occupied run passes all 22 cases. Independent +file checks verify each executable, object, original script, report, exit and +empty capture by size and SHA-256. Compact records retain report/script bytes +and six distinct executable hashes; temporary per-case executable copies were +pruned after verification. These checks validate report framing, not a second +full event decoder. Source absence is asserted by the passing guarded harness. +All wider language, provider and platform obligations below remain open. + +## Historical checkpoint + +The isolated v16 checkpoint recorded 22 executables/22 entries for occupied +scope retirement and 22/44 for same-grant reentry. Together with its twelve +predecessor gates, it recorded 648 executables/794 entries on private MSVC +archives. Those historical target receipts were discarded during disk cleanup; +they do not establish acceptance of the restored integration. + +## Remaining scope + +The current eleven script cases do not observe completed fields, stage owning +strings or nested occupied carriers, or exercise occupied constructor allocation +refusal. Pair factory failures happen before construction; abandoned-prefix +coverage uses a later ordinary marker handler return. These are remaining +native completion obligations, along with projection, extraction, replacement, +reflection, recursive/generic aggregates, collections, captured lifetimes, +broader Default compositions, and GNU/installed-distribution acceptance. + +Private scripted execution does not establish public Driver admission or a +normal-runtime Resource provider. Backend refusals remain incomplete coverage. +The fixed inventory is 207 fixtures/182 object obligations; overall native +coverage is not complete. diff --git a/docs/active/native_resource_ordinary_entry.md b/docs/active/native_resource_ordinary_entry.md new file mode 100644 index 00000000..86d0cd02 --- /dev/null +++ b/docs/active/native_resource_ordinary_entry.md @@ -0,0 +1,347 @@ +# Ordinary entry in a Resource object + +The ordinary-entry bridge has bounded Source17 object/native acceptance and a +current compiler proof checkpoint. It changes entry metadata and wrapper emission, +preserving Jett source semantics and ordinary function calling conventions. +The earlier v20 whole-workspace gate and all eleven v21 MSVC-v3 native +regression gates pass; exact-revision CI remains pending. + +## Historical failure and selected repair + +Root's v7 gate compiled the candidate and passed all fourteen MIR controls and +all forty-four runtime controls. The first thirteen original Source17 wrappers +emit actual objects before wrapper14 fails. The reported error is `callee has +no fresh execution-family signature`. Root's v7 terminal is `6b7069`, session +15179; its unchanged-3742-input receipt is +`d57fca527e65a7008a8fce54a1c0293cdc984a90bddd01b2c1485b2a4bedead6`. + +`emit_for_triple` selects the Resource emitter when the checked manifest is +nonempty. `EmittedResourceLayout::from_program` validates the selected current +FunctionId and its original Source header, populates execution-family rows, and +then calls `rows.function_signature(entry)` before checking +`plan.function(entry)`. Thus the reported error is the selected entry lookup. +`source_row` first checks `plan.function(target)` and would report a different +error for a missing Source callee. + +For wrapper14, `original_source(entry)` succeeded. This entry has an initially +captured proof-only witness. Its required values and original/current graph are +authenticated, but it does not need runtime Resource custody. +`has_execution_records` and `validate_resource_ownership` correctly keep it out +of `plan.functions()`. `absent_required_controls` likewise remains ordinary. + +## Decision + +Keep the existing execution-family classification. Introduce a separate private +entry Scope record for the exact driver-selected ordinary entry. The native +program wrapper owns this Scope and its completion. The Jett main and ordinary +helpers keep their ordinary context/environment ABI. + +Appending the ordinary main to `plan.functions()` is unsuitable: signature and +translation currently use presence in that collection to add a third Scope +parameter and select `ResourceEmission`. Falling back to the ordinary object +emitter is also unsuitable: it omits the installed private archive entry, +authenticated root tuple, and retirement contract. + +## Authenticated MIR API + +A readonly `ResourceEntryScopePlan` with private fields and no public +constructor. It exposes the exact selected `FunctionId`, complete identity and +header, one root `ResourceFrame`, and one `ResourceOperation` whose role is +`Complete`. This frame and completion describe wrapper execution. They do not +claim that a Source branch or Source Return executed a Resource operation. + +The entry-aware API is: + +```rust +pub fn validate_resource_ownership_for_entry<'p>( + program: &'p Program, + types: &'p TypeInterner, + entry: FunctionId, +) -> Result, Vec>; + +impl ResourceOwnershipPlan<'_> { + pub fn entry_scope(&self) -> Option<&ResourceEntryScopePlan>; +} +``` + +The existing validator and `function`/`functions` getters retain their meaning. +The entry-aware validator first runs complete current call and witness +validation. If the selected entry is already an execution-family function, it +uses the existing family plan without adding a bridge. Otherwise it mints only +the separate entry record under all of these conditions: + +- The selected ID denotes the exact current function in the dense table. +- The selected function has its own initially captured Source witness. Another + function's archive is not sufficient authority to reconstruct its MIR body. +- Its original/materialized archive, manifest, interner, identity, complete + current graph and local/parameter headers pass the existing private checks. +- It is not required-only and has no runtime execution-family classification. +- The exact original/current native root header is closed, capture-free, + Network-only (including zero parameters), and returns Nothing, matching the + currently supported Resource entry header. + +The record borrows the immutable validated Program through the plan lifetime. +It adds no owner slot, loan, carrier slot, callable proof, Source invocation, +descriptor authority, provisional return or execution-family member. The +original required-value proof remains responsible for authenticating the +materialized values and body through canonical preparation. + +At initial authenticated checked lowering, capture a proof-only witness for +every capture-free, Nothing-returning, Network-only function (including zero +parameters) in an authentic Resource Source archive. This uses identity and +header, not a `main` name. Require `source.manifest().is_some()` and preserve the +complete original/current archive join. An independent full-body graph seal is +mandatory for this proof-only class, including genuinely pure bodies with no +required-value row. Missing-witness validation derives eligibility from the +exact immutable execution-archive function ID and header. A public header edit +cannot remove the original function from this mandatory-witness class. + +This metadata condition does not alter `runtime_custody_needed`, which remains +the execution-family classifier. Raw HIR/default archive and ordinary +non-Resource paths remain unchanged. Public mutable MIR is never sealed +retrospectively. The entry-aware constructor selects only the exact passed ID +from these authenticated candidates, and still rejects required-only selection. + +## Codegen projection and wrapper + +`resource_execution::emit` validates the original graph with the entry-aware API +before preparation. `EmittedResourceLayout::from_program` repeats that fresh +entry-aware proof after canonical preparation. It serializes a bridge signature, +root Scope frame and Complete row separately from execution-family rows. The +root has only the Root parent. All ordinals are produced from the private typed +record, not from source names or a public map override. + +`plan.function(main)` and `plan.function(absent_required_controls)` remain None. +Both declarations and body translations therefore keep the ordinary two hidden +parameters and ordinary ownership/caller proofs. Entry metadata presence in the +layout's Source-family signature, frame and operation maps exclude bridge rows. +`source_row` retains its mandatory family-plan check. A legitimate ordinary call +to main elsewhere continues to use the ordinary ABI. + +The bridge-aware wrapper performs this exact sequence: + +1. Obtain the installed entry Scope through the existing EntryScope leaf. +2. Validate the compiled function/signature/Scope tuple with ScopeValidate; + ordinary main has no Resource prologue to perform this validation for it. +3. Obtain Network arguments through the existing EntryNetwork checks. +4. Call ordinary main with context, zero environment and ordinary parameters; + do not append the Scope argument or select ResourceEmission for its body. +5. Read the existing NativeLeaf::Status after the body. Do not take, reset or + replace the original body status. +6. Attempt the exact bridge Complete row through ScopeComplete, passing that + original status. ScopeComplete is the existing cleanup leaf. Its selected + error can differ from a nonzero original body status, so do not return that + selected error prematurely before observing the retired root outcome. +7. Read EntryOutcome with the compiled tuple and return its exact original body + status. If retirement or tuple validation failed, propagate the authentic + refusal; do not invent a success or a replacement body status. + +Existing Resource-family entries continue through their current three-parameter +body and completion path. No C ABI, layout wire version, runtime leaf, report +format or provider behavior changes are needed. + +Reachability must explicitly include the selected bridge entry as a native root +without making it an execution-family member. Required-only exclusion and all +ordinary transitive dependency/type checks remain intact. Keep all fourteen +wrappers, twelve original exports, refinements and shared Source fixtures. + +## Required controls + +- Actual original wrapper14 emits and runs in both profiles, with the correct + required primitive values, body status and zero-before-destroy obligations. +- All first thirteen original wrappers and the complete export/refinement + inventories continue to pass; do not substitute simplified Sources. +- Typed plan and emitted signature controls prove main and the ordinary helper + remain ordinary, while only the selected wrapper has the bridge Scope and + Complete leaf calls. +- A genuine checked pure entry with no Resource operation, comptime expression + or required-value row receives its own initial proof-only witness and bridge. + An equivalent raw/default-archive HIR path receives no Resource entry proof. +- A genuine ordinary internal call to the selected main remains valid with its + ordinary ABI; bridge metadata must not give it Source-family authority. +- Missing, foreign or copied/resealed entry witness/body/header substitutions + fail the current private proof checks, before canonical preparation can erase + the mutation. +- A required-only selected ID, wrong current ID, changed original Network/Nothing + header, capture or stale canonical graph refuses bridge construction. +- An ordinary body failure preserves its original status and message/channel, + still attempts exact root retirement, and completes with no live obligations. +- Missing installation, wrong installed tuple and an invalid completion ordinal + refuse without manufacturing Source activation or status. + +Root owns production integration, remaining execution gates and final receipts. +The measured bounded checkpoints and current proof repair are recorded below. + +The initial independent body seal uses existing certified canonical transports. +Consuming ordinary list iteration is covered by a genuine positive control. +Borrowed-view iteration and other unsupported composed transports remain +pending unless their own exact constructor transition is implemented and proved; +this bridge adds no general reseal of a changed body. + +## Bounded object/native acceptance and current compiler checkpoint + +All fourteen whole-Source17 wrappers emitted **28/28** genuine host objects in +both profiles at v14, with zero refusals. The full original 3,600-byte Source +prefix, twelve exports, unused refined contract, required controls, verify and +property100 remain intact and in scope. This main-wrapper gate does not establish +every original purpose or live aggregate value. + +target/native-resource-carrier-compile-evidence/after-v14.json (SHA-256 +43c3a3f1fc2fa6d1e4c6cbd7cc7e0bad4f0d560dce0685e6a8fcb2b411aee2dc) +records actual session 94028, terminal f7b2aa, exit zero. + +The separate v15 gate passes **28 Source-deleted single-entry executables / +28 entries**, including Source13's exact ordinary bounds error, and **26 clean +same-grant reentry executables / 52 entries**: **54 native executables / +80 entries**. Exact completion/message/event reports, all eight zero Resource +custody counts and empty ordinary storage before one teardown, event boundaries +and script exhaustion pass. Source13's repeated ordinary-error call on the same +grant remains pending; thirteen clean Sources in both profiles do not replace +that lifetime obligation. + +target/native-resource-carrier-compile-evidence/after-v15.json (SHA-256 +b20c4d9427b11676e7207e5691171c64a861666e0f728b8aa24dba631123154b) +records actual session 93295, terminal b1a472, exit zero. Both v14/v15 retain +all **3,752 frozen inputs / nine runtime files unchanged** at parent +91e3706b770124cbddca952e5fe6b558abc170be. The measured v15 runtime receipt pin is +74a811660ee1772e0374e04579dd51234e8ff38191b942335bedaab5b4e4b6cd. + +The current post-refinement-repair compiler gate is separate from v15 native +acceptance. At v19, **17 focused entry controls (13 MIR + four codegen)** pass, +including four new raw/refinement archive controls. Full package gates pass +**314 MIR checks (313 library + one doctest)** and **181 Cranelift checks +(118 library + 63 object)**, with zero failures or ignored tests. + +target/native-resource-carrier-compile-evidence/after-v19.json (SHA-256 +b549b995be46093f29dbf45314b47f1eea3b09c9bfc20cebf068e477bfaa8f8b) +records actual session 65346, terminal 26ed19, exit zero and all **3,757 frozen +inputs / nine runtime files unchanged** at the same parent. Earlier v3 runtime +gates passed **275 checks at v6** and **44 focused carrier checks at v7**. These +are historical revision-specific results, not current whole-runtime or workspace +success. + +## Current workspace and wider-language observations + +The earlier v20 whole-workspace gate passes **3,582 checks, zero failures and +13 ignored tests**, across **66 real selected groups / 92 total result groups**. +Actual session **75431**, terminal **1f1c8b**, exits zero for +`cargo test --offline -q --workspace --no-fail-fast`. +target/native-resource-carrier-compile-evidence/after-v20.json (SHA-256 +fb59285ab91f6d15ce57ab59ecf7cc5c00187cdf4c323de04b5bbafe4a78e7d2) +records all **3,757 source inputs / nine runtime files unchanged** at parent +91e3706b770124cbddca952e5fe6b558abc170be. The inputs-v20 manifest SHA-256 is +58dd6f590a1bf2fd63f04d0ec0abdd15aa71452e9e00b1ff36b1feb29ab3612a; +the workspace log SHA-256 is +f610439bbbcb334d88de90c8b8ebf4178f1015be564cc7caf63bba2b5d6ed8da. +This is measured workspace success, not fresh ignored-native acceptance. + +All **11 fresh-v3 native gates now pass**, each selected as a real one-test gate. +Actual session **41872**, terminal **3c7b60**, exits zero with **576 Source-deleted +native executions / 672 entry calls**, across Debug and Release. This includes +the unchanged **191-case / 382-execution corpus drawn from 113 distinct Sources**, +carrier controls, reflected-body controls and owned-Return regression. The v20 +workspace's ignored-test count is historical; v21 independently executes the +eleven native gates rather than treating ignored tests as acceptance. + +target/native-resource-carrier-compile-evidence/after-v21.json (SHA-256 +38f67805c93e66e273d29de84c1cfc3351d70c0235bf656ea661244474582fa2) +records all **3,757 source inputs / nine runtime files, HEAD and inventory +unchanged** at parent 91e3706b770124cbddca952e5fe6b558abc170be. The frozen native +binary SHA-256 remains +110ba3b7bf23e63677ac7afd13a83fc2fa98aeccebc553e8df4f294fb5bc872f. +Matched MSVC v3 archive provenance passes before and after execution with receipt +pin 74a811660ee1772e0374e04579dd51234e8ff38191b942335bedaab5b4e4b6cd. +The successful runner report SHA-256 is +699f2364769ccdd489c7322832f2410690129d8f687006239e3dc6f0b6e82ae6. +This gate establishes no GNU runtime-archive acceptance. + +Owned-Return cleanup failure retains caller/entry-root **body255 / cleanup255**; +the observations establish retirement and root completion, not a separate nested +callee completion. The accepted historical Return semantics and exact Source +prefixes remain intact. Earlier prospective fresh-v3, full-191/382 and reflected- +Return gate statements in retained historical sections are superseded by v21; +broader body/Default, generic/reflected lifetimes and occupied custody remain open. + +At v21, Source13's repeated ordinary error remained pending; the v24 +same-grant gate below closes that bounded obligation. Occupied resources, wider +whole-language parity and the successor's exact-revision CI remain pending. +The rough 87% estimate and fixed 207/182 inventory do not advance. + +The separate 15-case wider-language probe is an **observation baseline, not +acceptance**. Actual session **77834**, terminal **ee797f**, leaves compiler +inputs unchanged and records eight emitted objects, six object refusals, one +earlier backend failure and three reference failures. Frontend/driver build +success does not establish linked parity. Its authoritative receipt is +target/native-language-parity-probe-observations-v1/root-after-evidence.json +(SHA-256 434cba150528d36e018d89b9dbe3acb05adf0309ac6a5aeb47a8ea1b906b4c3d). + +## Raw refinement proof boundary + +Pure refinement predicates lowered through raw/default HIR without an +authenticated checked Resource archive stay ordinary through canonical +preparation. Declaration kind or spelling alone supplies no Resource witness or +entry Scope. Raw bodies with actual Resource custody retain the independent +mandatory capture/refusal path. + +Authenticated predicate proof requirements derive the original declaration kind +from the immutable archived function. Removing a witness and changing the current +predicate kind cannot escape that requirement. Constructor seals reject missing +or foreign proof, modified predicate body and changed header. The repair preserves +ordinary raw refinement behavior while retaining authenticated original authority. + +## Remaining acceptance + +The earlier whole-workspace gate passes at v20, and all eleven matched- +MSVC-v3 native gates pass at v21, including full **191-case / 382-execution**, +carrier, reflected-body and owned-Return regression. These remain independent +historical receipts; current ordinary-error regression and workspace acceptance +are recorded at v24 below. Earlier accepted GNU Source-prefix and Return +documentation and wire-v2 receipts retain their independent historical scope. +The successor's exact-revision CI remains pending. No GNU runtime-archive +acceptance is inferred from these local MSVC results. + +Occupied aggregate transport/replacement, broader Resource-valued Default +and generic/nested reflected lifetime compositions remain full-goal obligations. Whole Source/export/refinement and verify/property100 +purposes remain required. No refusal counts as successful Source execution, no +Source policy exclusion is introduced, and the full native parity goal stays +active. The rough 87% estimate and fixed 207/182 inventory do not advance. + +## Accepted ordinary-error successor checkpoint (v24) + +The changed ordinary runtime passes its measured **307-check** suite with fresh +matched MSVC `measurements-02` archives. The accepted twelve-gate native scope +covers **604 Source-deleted executables / 728 entries**: the new all-fourteen +same-grant gate contributes **28/56** from session **62286**, including both +Source13 Bounds errors and original body1/cleanup0/Body completions; session +**76385**, terminal **d30da4**, passes the eleven predecessor gates with +**576/672**. The latter includes full **191-case/382-execution** regression from +**113 distinct Sources**, carrier, reflected body, owned Return and original +Source06. No separate nested callee completion is inferred from Return root +observations. + +The fresh whole workspace passes **3,614 checks, zero failures and 14 ignored +tests**, across **66 nonzero / 92 total groups**, at actual session **75810**, +terminal **0f5a7b**. Independent verification at **58261 / 75fabb** authenticates +all **3,763 source inputs / nine fresh and nine historical runtime artifacts**, +HEAD/inventory, both native binaries and fresh02 provenance/log hashes unchanged. +The authoritative receipt is +`target/native-resource-carrier-compile-evidence/after-v24-native12-workspace.json` +(SHA-256 +`4dabdff125cefac5891d6b026cb0b67ec14448db67915755116f014698ea6aa2`). +The v20/v21 receipts above retain their accepted historical scope; current +ordinary-error regression is established independently by v24. Earlier +Source13-pending statements describe their original checkpoint and are now +superseded for this bounded same-grant obligation. See the +[ordinary terminal-error note](native_resource_ordinary_error_reentry.md) for +the exact archive, binary and per-attempt messages/retirement evidence. + +Local MSVC results provide no GNU runtime-archive or full platform acceptance. +The separate normal-main linked baseline is complete: sixteen observations, +twelve exit0 and four exit71, from eight exact opaque objects reused with +Debug/Release runtime artifact profiles; all fifteen raw-probe Sources remain +preserved. This is baseline collection without a desired Source oracle, new +feature acceptance or ordinary public-Main context-retirement observations. +Broader ordinary parity and fixes remain open. Occupied custody/replacement, broader Default and +generic/nested reflected lifetimes, wider language parity and the successor's +exact-revision CI remain open. The rough **87%** estimate, fixed **207/182** +inventories and full 100% goal do not change. diff --git a/docs/active/native_resource_ordinary_error_reentry.md b/docs/active/native_resource_ordinary_error_reentry.md new file mode 100644 index 00000000..1b94cbe9 --- /dev/null +++ b/docs/active/native_resource_ordinary_error_reentry.md @@ -0,0 +1,181 @@ +# Ordinary terminal-error lifetime for checked Resource entries + +Status: the bounded ordinary-error same-grant obligation passes in both +native checking profiles. The accepted twelve-gate scope covers **604 +Source-deleted executables / 728 entries**, and the fresh whole workspace passes +**3,614 checks / zero failures / 14 ignored tests**. Source13 retains both +original Bounds outcomes without error reset. Broader full-language acceptance +remains required; the rough feature estimate stays **87%**. + +The full 100% native-codegen goal includes existing repeated-entry semantics. +Original Source17's fourteen unchanged wrappers are executed twice in each +checking profile by `absent_generated_unchanged_source17_wrappers_repeat_exact_outcomes_and_restore_entry`: +56 reference entries on one interpreter/provider/grant per Source/profile. Source13 +returns `list.__remove_at: index -1 out of bounds` twice, with empty effects, +zero custody/registry, restored entry context and empty debug events. The full +original Source17 prefix, constructor returns, namespace baked value, explicit +required controls, verify and property100 remain present. + +Historical v10 wire-v3 Source13 single-entry execution passes in Debug and +Release with exact tuple `(1, 0, Body, exit 71)`, signed bounds ordinary bytes, +`native Resource protocol refused` Resource message, no events, all eight Resource +counts and ordinary storage zero before successful destruction. Root session +22590 / terminal b71964 receipt +`target/native-resource-carrier-compile-evidence/after-v10.json` SHA-256 +`a4d03928a4992b8049c0bfe7dc1712de9ca632765db18af4cea63d961695c305` +retains 3,745 inputs and nine archive files unchanged. At that historical +checkpoint the whole-family strict gates exit101 on the separate ordinary-entry +bridge case; this receipt is not whole-family or repeated-error acceptance. + +## Selected implementation + +Use context-owned immutable attempt records for terminal ordinary status, +dynamic message and diagnostic prefix, issued by the exact Resource entry +constructor. Nested calls share the running channel. Legacy session failure and +persistent ordinary cleanup poison remain sticky; historical Resource cleanup +failure keeps its separate accepted reentry behavior. Exact completion/retirement +seals the channel, retains original body and cleanup observations and creates no +error reset. Only an authenticated next constructor can select a fresh channel +after exact predecessor/ordinal, installation, budget and all custody/storage +checks. A report refusal/tombstone or partial cleanup cannot authorize reentry. + +Existing refinement capture remains within a valid active evaluation. It cannot +take a sealed terminal error or provide reentry authority. The private launcher +requests the declared next entry after complete observation; runtime guards decide +admission, and the first nonzero process exit and every completion survive. +No Source syntax, provider API, runtime-global rule or new recovery construct is +introduced. ABI1 and current layout/report protocols need no new export for this +internal lifetime, subject to genuine verification. + +## Current measured runtime and archive checkpoint + +The ordinary-error successor is now present in the managed private worktree. +Actual session **3890**, terminal **56ec08**, exits zero with **307 runtime +checks / zero failures** and genuine matching Debug/Release MSVC archives in +`measurements-02`. The measured receipt is +`target/native-resource-carrier-compile-evidence/after-v23-measurements02.json` +(SHA-256 a429ef699cfe5804fce2755f53fdb9869c4dc7d7a393ce00ec18fb7fc0db91d3). It records **3,763 source inputs**, nine new runtime +artifacts and nine historical artifacts unchanged; `measurements-01` remains +unchanged. Capture pin +`6dea1dd7a2b26e366a8b76d80fe0f403f8e5694baab41870659f6f54f1cce707` +and Verify pin +`535b996c1b697694514cbfb92c81094cc4c8a9fc11b630a7c5877c477b11e033` +authenticate fresh archive receipt +`9c6ada6414341f3d2755e6e896f1b50d42a4db133363d0c066057665551171ef`. + +The native harness build separately passes at actual session **93714**, terminal +**3a2dc1**, with binary SHA-256 +`9a6a84c4ef0643cb4a2760c29d0b532ffde43d934cead0056e72ae39381d4301`. +The v24 freeze SHA-256 is +`1eb489a0eed0f294dc7111609e019995b5f82c09c7be46745792d51aae68b578`: +3,763 source inputs, nine fresh runtime artifacts and nine historical artifacts. +Neither runtime tests nor binary construction alone establishes Source execution. + +## Accepted all-fourteen gate and predecessor regression + +The new +`native_resource_absent_aggregate_all_original_sources_reenter_with_same_provider_grant` +gate passes in actual session **62286**: **28 Source-deleted native executables / 56 entry calls**, across +Debug and Release. Each unchanged original Source17 wrapper runs twice on one +retained Session/provider/grant. Its completed process record is +`target/native-resource-ordinary-error-native-v24/run-01/absent_aggregate_all14_error_reentry.process.json` +(SHA-256 e69c14092fac1d1f64d44b453acab559ed962e234da2498778741e2862084c0a); stdout/stderr SHA-256 are +`4e8e36bfd4ff70e92064961a992771fffdd19dcc1b2cb08c1a4ad5cb0a9d3a84` +and `862e8121741ea9ee81462e498e7686fb07e93d2278ed7fba20d1ae20be70290a`. + +Source13 retains **both** original `list.__remove_at: index -1 out of bounds` +ordinary messages and `native Resource protocol refused` Resource messages. +Both attempts retain **body1 / cleanup0 / Body**, with first process exit **71**, +monotonic attempt identities, zero eight Resource custody counts, empty ordinary +storage before destruction, no events, exhausted script and one successful +Session destruction. The unchanged full Source prefix, constructor returns, +namespace baked value, explicit required controls, verify and property100 remain +in scope. This records an actual gate result, not merely the earlier clean +thirteen-row gate or a build. + +The accepted combined **twelve-gate** scope is **604 Source-deleted +executables / 728 entries**. It joins this authenticated new **28/56** gate with +eleven freshly rerun predecessor gates: actual session **76385**, terminal +**d30da4**, exits zero with **576 executables / 672 entries**. The latter includes +the unchanged **191-case / 382-execution corpus from 113 distinct Sources**, +reflected-body, owned-Return, carrier and original Source06 regression. The first +gate is reused only as its authenticated 28/56 result; all 604/728 executions are +not attributed to the resumed session. + +The fresh whole workspace independently passes **3,614 checks, zero failures +and 14 ignored tests**, across **66 nonzero / 92 total result groups**. Actual +session **75810**, terminal **0f5a7b**, exits zero. These are measured counts, +not projections from v20 or credit for ignored tests. The twelve native gates +were separately selected and actually executed. + +Independent Root verification, actual session **58261**, terminal **75fabb**, +exits zero and authenticates all **3,763 source inputs / nine fresh and nine +historical runtime artifacts**, HEAD/inventory, both native binaries, fresh02 +provenance and process/log hashes unchanged. The authoritative receipt is +`target/native-resource-carrier-compile-evidence/after-v24-native12-workspace.json` +(SHA-256 +`4dabdff125cefac5891d6b026cb0b67ec14448db67915755116f014698ea6aa2`). +Its native runner success SHA-256 is +`5737084cdeea8bc8fb5be172ab0788051c19800344f89d093b5142c98c13decb`; +workspace stdout/stderr SHA-256 are +`101f3e11d9726dba3b3ada6f8c7b4472d32e245c694e18dc8c2ee2a3eaea98ed` +and `7825efe2c018844c19f4842027808208f236b7750314f5beb85f6d4acc247cea`. +The first gate's independent source/artifact authentication is retained in +`after-v24-disk-recovery.json` (SHA-256 +`77387a2d98b1e4363dd19abf534cb00020a8c8f562c67842af47b8a17b95d4c7`). +The measured successor is at parent +`202cf02066b9e84c8994d88a184879454d5c11e3`; this does not claim exact-revision +CI for the not-yet-published runtime changes. + +Broader ordinary normal-main archives separately prepare successfully at actual +session **26531**, terminal **421094**, with receipt SHA-256 +`781bb358a5965b87b0b75fa591352f50f69b638453fbddcd9d0d5ecee558c371`. +The full normal-main linked baseline is now collected at actual session +**44746**, terminal **a66d7d**, exit **0**: **16 observations**, comprising +**12 exit0 / four exit71**, from **eight exact opaque objects** reused with +Debug/Release runtime artifact profiles. All **15 original raw-probe Sources** +remain preserved, and isolated Sources were deleted before linking. Source +checking used release=false and codegen optimization=false; the two artifact +profiles do not establish separate Source-checking or codegen profiles. + +Independent evidence is recorded in +`target/native-resource-carrier-compile-evidence/after-v24-ordinary-linked-baseline.json` +(SHA-256 +`c22d671cad677ae97d68c82034d97437e65d0538d12640d7a1dd47efb6c71e84`), +with raw linked report SHA-256 +`131bf7f52e1a8c7c58d24041db7e62637ee0021ded140c11f39e0e79d6a6f18a`. +Original Source, archive, object, executable and log hashes authenticate the +collected baseline. Ordinary public Main artifacts expose process exits and +diagnostics without context-retirement observations. This collects actual raw +behavior without a desired Source oracle or new language feature acceptance; +broader ordinary parity and fixes remain open. Historical emission hashes still +do not authenticate the fourteen changed current Source paths. + +Earlier v20 workspace and v21 eleven-gate receipts keep their historical scope. +The v24 receipt above separately establishes regression acceptance for the +changed ordinary runtime. +Owned Return's caller/entry-root body255/cleanup255 does not prove an independent +nested callee completion. Local matched MSVC archives confer no GNU runtime- +archive or complete platform credit. Occupied aggregate custody/replacement, +broader Resource-valued Default, generic/nested reflected lifetimes, wider +language parity and the successor's exact-revision CI remain open. The rough **87%** feature +estimate, fixed **207/182** inventories and full 100% goal stay unchanged. + +## Deterministic reservation refusals + +The managed runtime successor includes private `cfg(test)` one-shot +faults immediately before five native-protocol fallible growth reservations: entry +active-frame storage, immutable completion history, ordinary attempt channels, +and Resource/ordinary diagnostic copies. Two additional core frame/active-stack +reservations prove that a failure after ordinary preparation publishes no channel +or custody. Non-growing reservations do not consume a fault; ordinary-channel +proof refusals precede its allocation hook. Resource frame/history preflight stays +in its existing earlier order. Diagnostic faults occur after bounded-length +validation and real retirement; report refusal seals a retained tombstone whose +exact first ordinary failure and completion cannot authorize another entry. +Sealing/history publication itself remains covered by storage reserved before +entry publication. These deterministic controls belong to the measured +307-check runtime suite; +they do not independently establish Source execution or allocator behavior +outside the selected hooks. Fresh measurements-02 is separate from unchanged +historical measurements-01. diff --git a/docs/active/native_resource_ownership_boundaries.md b/docs/active/native_resource_ownership_boundaries.md index 35589d7b..092907c2 100644 --- a/docs/active/native_resource_ownership_boundaries.md +++ b/docs/active/native_resource_ownership_boundaries.md @@ -122,6 +122,8 @@ need audit. Nested ordinary Resource printing requires an observation boundary for occupied data, hidden Secret, known absence and omitted values; this slice does not select that policy. Trace/breakpoint type-and-ownership-availability summaries are distinct from ordinary printing and expose no registry key, slot, -generation or provider state. Opaque layouts and source/native live Resource -values remain unsupported. Actor/message transfer, pending work, cancellation, +generation or provider state. The later private [reference lifecycle](native_resource_hook_identity.md#reference-lifecycle-execution) +now has carrier/custody and scripted-provider Source tests; those 36 passing +reference groups are separate from the static checking evidence above. Production +providers, opaque native layouts and native Resource values remain unsupported. Actor/message transfer, pending work, cancellation, late completion and real provider authority remain independent lifecycle work. diff --git a/docs/active/native_resource_pipelines.md b/docs/active/native_resource_pipelines.md new file mode 100644 index 00000000..11e39086 --- /dev/null +++ b/docs/active/native_resource_pipelines.md @@ -0,0 +1,49 @@ +# Source-only native Resource pipeline evidence + +This records checked Source pipeline coverage for existing Resource behavior. + +The expanded pipeline packet has measured local acceptance for **23 cases in +Debug and Release**. Its frozen-input receipt, +`target/native-resource-pipeline-source-evidence/acceptance-v2.json`, records +worktree HEAD `5d01097e5330d5aaa894c5fc92c786378f0e105a` and all **3,648 inputs +unchanged**; both measured archives and six nested receipts separately revalidate. +The input +snapshot identifies this expanded test packet; the recorded HEAD alone does not +establish acceptance of a later committed revision. Its measured +archive-receipt SHA-256 is +`29dd83c7f3874d7fe4b4fd3a5784786a5b31ee45337e9f40f133da9f33316d42`. + +| Gate | Recorded evidence | Limit | +| --- | --- | --- | +| Checked Source reference execution | 46 pipeline attempts, included in the full 161-case / 322-profile-execution reference corpus; 522 reference tests pass. Exact handwritten outcomes and event order; zero outstanding owners and zero registry entries before teardown. | The reference observation tuple does not measure unconsumed script entries. Full reference acceptance does not prove full linked native acceptance. | +| Cranelift object preflight | 46 emitted objects through the checked Source/HIR/MIR path | Object emission does not establish linked execution. | +| Linked local native execution | 46 Debug/Release executions with Project and synthetic Stdlib Source files deleted before launch; exact status, messages and events; zero obligations before teardown and no remaining scripted inputs | Uses matched local Windows MSVC archives; does not establish GNU or other platform acceptance. | + +The packet covers construction/move/close pipelines, written-view +retention, bare-owner view operations, named-argument abort ordering, domain +failure stopping the next step, and an indirect close descriptor. Its failure +rows check construction and borrow errors plus finalizer panic and retirement +before teardown. These are genuine checked Source programs; the scripted +provider selects outcomes for their runtime operations. + +The two test-only additions now have measured Source reference and local native +acceptance: + +| Accepted pipeline row | Measured existing behavior | +| --- | --- | +| `pipeline_bare_owner_provider_panic` | Provider panic retires the owner; native body/cleanup/channel `1/0/2`, exit `73`, message `runtime operation panicked`. | +| `pipeline_bare_owner_provider_panic_cleanup_wins` | Finalizer panic takes precedence over the provider panic; native body/cleanup/channel `1/255/3`, exit `71`, message `native Resource cleanup failed`. | + +The earlier 21-case / 42-execution receipt remains historical evidence. The +preserved complete gate passes **161 Resource cases / 322 Source-deleted native +executions** in both profiles (1256.05 seconds). Its receipt is +`target/native-resource-pipeline-source-evidence/full-acceptance-v2.json`; all +3,648 frozen inputs, both measured archives and six nested receipts revalidate +unchanged. No GNU or other platform acceptance of the expanded test packet is +claimed here. + +Broader Resource `For`/`Match`, reflected bodies and ordinary borrowed-data +families remain separate obligations. This supplemental packet leaves the fixed +`tests/native_parity.json` inventory at **207 fixtures / 182 run-pass lowering +obligations**, and leaves the broad planning estimate at **about 87%**. It does +not close the complete native-language or distribution gate. diff --git a/docs/active/native_resource_reflected_fields.md b/docs/active/native_resource_reflected_fields.md new file mode 100644 index 00000000..a131b112 --- /dev/null +++ b/docs/active/native_resource_reflected_fields.md @@ -0,0 +1,371 @@ +# Checked reflected-field execution and native ordinal transport + +The checked reference interpreter now enters the exact original body selected +by a direct `for field in type.fields[T]()` loop and its +`comptime type Element = field.type_info` binding. This repairs existing checked +Source execution; it adds no language syntax, provider or runtime capability. +The checked Resource lowering path now retains a direct nominal-struct field +ordinal bridge through HIR and MIR. Four focused Source cases pass 16 linked +native executions and 24 entry attempts; the unchanged 191-case corpus also +passes all 382 Source-deleted Debug/Release executions. Original Source06 also +passes 18 exceptional same-grant executables / 36 entry attempts. Derived +Sources7/8/9 separately pass 42 native executables / 62 entries. Dedicated +Sources10/11 pass 48 native executables / 72 entries, recorded separately below. + +## Retained selection proof + +A private prepared loop retains its exact checked program, original For/body, +parent attempt, resolver binder definition, concrete owner and ordered field +snapshot. Each item carries that proof and its ordinal. Before entering the +selected scope, execution validates the actual item against the retained full +field metadata, rejoins the original binding and lexical path, and selects the +checker-owned `ReflectedIteration(ordinal)` body with its exact TypeId and +source-visible reflection. Names, equal canonical types and lookalike metadata +cannot identify an executable body. Preparation does not change the cursor. + +The checked `type.fields` intrinsic reads retained owner metadata. An ambient +reflection table cannot supply the proof or redirect the selected body. Equal +`int64` fields retain distinct ordinals; an alias field additionally retains +its source-visible name and kind. Struct, bitfield and concrete generic owners +use the same boundary. Existing shape probes for primitives, enums and opaque +resources return empty field metadata without enabling a provider. + +Named and recursive calls isolate their active item selectors and restore the +caller's selectors and aliases. Scope entry retires on ordinary completion, +Return, Break, Continue, Handle Default and unwinding. Entry cleanup restores +its original full context even when a provider or finalizer panics; the existing +cleanup-precedence rule remains intact. Closed pure required evaluation uses +the same checked selection without receiving a runtime provider or grant. + +## Genuine acceptance + +The reference checkpoint passed all 541 `jett_comptime` tests. The candidate's 3,664 frozen inputs and +recorded parent HEAD remain unchanged at that gate, as recorded in +`target/native-resource-reflected-field-reference-evidence/after-v5.json` +(receipt SHA-256 +`5c3104bee1dd58acb19c861f17dd61c39aacbd879ad59856ea80dd3b507b7966`). +This includes 19 added tests for original-node/parent/program proof boundaries, +corrupt ordinals and metadata, distinct equal-type fields, aliases, genuine +same-body recursion, required evaluation, selected control signals, and +heterogeneous/bitfield/concrete-generic metadata. + +The strict Resource runner executes unchanged Source06 plus three explicit +control-flow adaptations in both checking profiles. Its 74 entry attempts +cover nine original scripts, same-provider/grant reentry after every script, +and ten derived script controls. Exact provider/finalizer panic text and +cleanup-wins outcomes, ordered events, zero owners/registry, empty debug events +and full entry-context restoration are checked before interpreter teardown. +The original Source06 bytes remain exact; derived Break, Continue and primitive +Result Default bodies are named separately. Reference observations do not +independently count every native loan or expose remaining script length. + +Reproduce the crate gate with: + +```text +cargo test --offline --locked -p jett_comptime +``` + +## Native compiler ordinal bridge + +The new native producer is the direct `for field in type.fields[T]()` loop +over a nominal struct at the root of an exact non-generic, noncapturing function +in an authenticated `CheckedResourceProgram`, with a direct +`comptime type Element = field.type_info` binding in that loop. It +retains every checker-selected `ReflectedIteration(ordinal)` body, including +fields with equal canonical types and equal source-visible type reflection. +The ordinary `lower_program` entry retains its existing equal-type/reflection +coalescing. Checking specializes type, name, kind and primitive facts, while +`field.index` and `field.name` branches inspect the current runtime field binder. +Existing native `type_construction_enum` and `comptime_reflected_callbacks` +fixtures already distinguish same-type fields. A separate ordinary positive +control passes 30 Source-deleted native executions without a production change. +This covers five Source forms across nominal/generic fields, aliases, bitfields, +variant/state fields, type arguments, nested fields and getter pipelines. It +establishes parity for those controls; no ordinary coalescing defect is established. + +The immutable `ResourceSourceArchive` captures the exact original checked +program, declaration, For and binding objects, resolver identities, concrete +owner TypeId and source-aware owner reflection, ordered complete field table, +checked body facts, HIR field headers and original lexical paths. Only the +successful checked constructor can create this archive membership. Public HIR +changes, source spans, field names, an equal TypeId or lookalike metadata cannot +replace the original selection proof. + +MIR derives an ordinal guard from that archive: the original immutable field +binder's `TypeField.index` must equal the selected ordinal. Each guard leads to +a one-arm `ReflectedTypeDispatch` which checks the source-aware TypeInfo identity +before entering that ordinal's exact body. Two `int64` fields can therefore +select different field-index- or field-name-dependent bodies. Each native +TypeInfo dispatch retains the existing unique-identity rule. Metadata stays +ordinary storage and grants no executable authority by itself. + +Each arm carries its original lexical path. Nested handler lookup remains +inside that arm's archived subtree, preventing equal source spans in distinct +specializations from redirecting a Scope. Existing borrowed-projection and +lexical-retirement seals retain distinct local/site membership, child-before- +parent loan endings and Return cleanup rules. Private MIR records seal each +guard, dispatch, body target, refusing target and current field header against +both the original archive and the constructor graph. Canonical sequence edits, +block deletion and local remaps transport only current sites and headers; +original Source paths stay fixed. Reordered equal-type ordinals, redirected +guards or body targets, changed metadata, stale paths and edited original HIR +fail validation before provider or runtime effects. + +An ordinary helper inside the same checked Resource program may retain its own +exact field-loop proof. Its MIR witness supplies only that proof: runtime +classification stays ordinary, with no Resource execution plan, hidden Scope +parameter, provider permission or custody authority. Existing ordinary sequence +preparation still handles its map, string, view and uninhabited loops through +the private finite `SequenceEdit` validator. It preserves function identities +and original headers, forbids added custody, and refreshes only the current +graph and independent body seal. Resource execution functions retain their +separate reconstructed consuming-list transition. + +No Source rule, runtime leaf, ABI or wire-v2 record changes in this bridge. + +## Affected compiler phase gate + +The integrated candidate passes 1,202 checks: 542 comptime, 203 HIR, 286 MIR, +107 Cranelift codegen, 63 object and one CFG check. The ten result groups report +zero failed or ignored tests. Focused controls cover equal-type ordinal/body +selection, field-index/name-dependent bodies, altered original HIR and metadata, +reordered guards, redirected dispatch/body targets, stale Scope paths, proof-only +helpers, and their retained ordinary sequence preparation. The canonical view +control is `for observed in view values:`; the uninhabited control uses the +inferred empty `for impossible in list():` form. + +`target/native-resource-reflected-field-native-bridge-evidence/after-v4.json` +records actual session 4116, terminal `1335da`, exit zero and all 3,679 frozen +inputs unchanged at parent HEAD `f9fa84a6f05b2886a83e09fad4ae3b6dcb22c74a`. +The receipt explicitly records `native_execution_accepted: false`. These +phase checks do not prove linked native execution, matched runtime archives, +Source absence at launch, script exhaustion or zero native obligations. + +## Native execution acceptance + +The four focused cases are the byte-preserved original Source06, distinct +field-index bodies, distinct field-name bodies and a proof-only ordinary helper. +Both checking profiles pass eight single-entry executables and eight clean +two-entry executables: **16 Source-deleted native executions / 24 entry attempts**. +The clean two-entry cases retain the same Session, provider and grant. Each entry +has the exact outcome, ordered events and zero ordinary/Resource obligations +before teardown. The combined script is exhausted before one Session destruction. +The focused receipt is +`target/native-resource-reflected-field-native-bridge-evidence/native-acceptance-v5.json` +(SHA-256 `0b05489e3b97201ba0aee4642c2f157e81d5f9684e818f347eb403f775cda10a`), actual session 29429, +terminal `9cfc8d`, exit zero. + +The unchanged predecessor corpus separately passes **191 cases / 382 +Source-deleted native executions** in Debug/Release in 1,273.64 seconds. All +strict reports pass. The full receipt is +`target/native-resource-reflected-field-native-bridge-evidence/full-native-acceptance-v5.json` +(SHA-256 `31d6b59bebc1e11c18b9dfb6102b5f5486f28dfd69d1439fce15498aa4050db9`), actual session 49641, +terminal `1d2132`, exit zero. Both execution gates keep all **3,686 frozen inputs** +and **nine runtime files** unchanged, including the matched measured MSVC +Debug/Release archives and their build/CRT/native-static-libs receipts. The frozen +test executable SHA-256 is +`76fe2a90106ee8ede00eb86758cefbaa4cefb42d6b818e90a06337b029aba39d`. +The phase receipt above remains a distinct earlier compiler gate. + +## Ordinary reflection positive control + +The independent ordinary control passes three Source families in five forms, +each with main, verify and property suite executables in both checking profiles: +**30 Source-deleted native executions**. Reference stdout and empty debug-event +oracles, explicit-comptime stdout and native suite outcomes all agree. Its receipt +`target/native-ordinary-reflected-ordinal-control-evidence/after-v1.json` +(SHA-256 `4a282285082fc411b4d8714a890b13c7146d81970533522469eb5848d4a0177e`) records actual session 21527, +terminal `6befe6`, exit zero and all 3,703 frozen inputs unchanged. It explicitly +records `production_changed: false` and no established ordinary equal-type +coalescing defect. These ordinary controls do not establish Resource transport +for those broader reflection families. + +## Original reflected failure single entries + +The byte-identical 929-byte Source06 now passes all eight original exceptional +scripts in both checking profiles: 16 reference observations, 16 emitted objects +and **16 Source-deleted native executions**. The cases cover handled construction +and borrow failures, provider panic, explicit close cleanup failure and both +provider/domain failure with cleanup failure. Exact completion channels, messages, +ordered events, script exhaustion, zero ordinary/Resource obligations before +teardown and one Session destruction agree with the strict native oracles. + +`target/native-reflected-exceptional-native-evidence/after-v1.json` +(SHA-256 `38662e59e4dfebfbbde4236ee0a8d6b2cc647b6a9554ea8afb9f2b1119bf3085`) +records actual session 2180, terminal `aa0907`, all three commands exiting zero, +and all 3,720 frozen inputs plus nine measured runtime files unchanged at +`2be038c77f084395e8638beaaf2617fe86b49730`. Only additive test leaves are involved; +compiler/runtime code, four READY controls and the 191-case catalog are unchanged. +These single entries do not establish exceptional same-grant reentry. + +## Original reflected exceptional same-grant reentry + +All nine original Source06 scripts now execute followed by exact READY on the +same Session, provider and paired Network grant in both checking profiles: +**18 Source-deleted native executables / 36 entry attempts**. The shared checked +reference matrix separately passes 36 real calls. This includes all three +cleanup-failure cases as well as the original handled Return and provider-panic +paths. The exact 929-byte Source06 and original scripts remain unchanged. + +The private launcher first completes the actual attempt and copies its original +body status, cleanup status, selected channel, exact ordinary/Resource message +bytes, event boundary and live counts. Every entry has zero owners, loans, frames, +registry entries, owner/loan/frame handles and provisional returns, plus empty +ordinary value storage, before teardown. After these checks, completed Resource +cleanup failure permits the declared next entry when current ordinary status is +zero. Existing `entry_begin` independently validates the installed entry, attempt +budget, ordinary first-error/cleanup state and absence of active or residual custody. +It creates fresh attempt/root identities on the retained Session/provider/grant. +No failure latch is cleared and no reset or failure-taking API is invoked. + +Each original completion and diagnostic remains immutable in the report. Events +retain one continuous sequence with exact per-entry boundaries; the combined +script is exhausted and the Session is destroyed once. The first nonzero process +exit remains 71 or 73 after clean READY. Handled-domain Return with failed cleanup +retains original body 0, cleanup 255, Cleanup channel and exit 71. These are exact +observed exceptional outcomes; the second entry does not replace the first record. + +Fresh independently measured MSVC Debug/Release archives were built in actual +session 23542, terminal `e09148`, exit zero. Their receipt is +`target/native-reflected-exceptional-reentry-archives/measurements-01/receipt.json` +with compiled pin +`96721717326f7d6f96dd2bb98dc1d6e919f7119235e33e103e5ac89bd6f29d0a`. +The private launcher continuation changed; runtime ABI 1, Source wire 2 and +script/report protocol 1 remain unchanged. The archive build alone is separate +from the subsequent actual Source execution. + +`target/native-reflected-exceptional-reentry-evidence/after-v3.json` +(SHA-256 `f3dc34c6a13079c6e0be04716d37af9a69580a7b99b859a4ebd5301376e3aa8b`) +records actual session 30992, terminal `22c716`, both Rust harnesses exiting zero, +and all **3,722 frozen inputs / nine runtime files unchanged** in the tested +candidate based on parent `b5c55ac7ed9ec559fbae6616de74fff507bff042`. The exact +manifest filters each ran one real test. Earlier session 52970 selected zero +tests; preserved `after-v2.json` +explicitly records zero executions and nonacceptance. The earlier single-entry, +four READY and 191/382 receipts remain separate evidence with their original +archive pins. This gate establishes the original exceptional same-grant matrix; +fresh-archive predecessor, coherent-workspace and exact-revision CI gates remain +separate obligations. + +## Derived reflected control native acceptance + +The unchanged derived Sources7/8/9 now pass their historical ten-script matrix +in both checking profiles. The executed signals are **three handled Returns, +two Break paths, two Continue paths and three scalar Handle Default paths**. +The first-borrow failure in Source7 and first/later-borrow failures in Source8 +return from their handlers before Break or Continue; they are counted as Return. +A separate eleventh Source9 READY baseline was added and verified, without +changing Source bytes or the original ten scripts. + +Break preserves the total17 guard and retires the selected scoped-type body +before the consuming close. Continue preserves both field visits and total34, +and skips the statement after its scoped-type body. Scalar Default substitutes +17 for the failed primitive borrow, resumes the selected body and preserves both +field visits and total34. These exact controls exercise selected body retirement +with the outer optional owner retained until its existing close or handled Return. +They do not +establish arbitrary Resource-valued Default or other body/Default lifetime +compositions. + +The five actual gates pass **22 single-entry reference calls, 40 same-grant +reference calls and 22 emitted objects**, followed by **22 Source-deleted +single-entry executables / 22 entries** and **20 same-grant reentry executables / +40 entries**: **42 native executables / 62 entries** and **62 reference calls**. +Every historical first script is followed by its own unchanged Source's READY +on the same Session/provider/paired Network grant; the separately verified +Source9 READY supplies its two-borrow tail. Exact completion tuples, diagnostic +bytes, continuous events and per-entry boundaries, zero ordinary/all eight +Resource obligations before teardown, combined-script exhaustion and one clean +Session destruction all pass. The three explicit-close cleanup cases retain +body 255 / cleanup 255 / Cleanup channel / exit 71 and exact +`native Resource cleanup failed`; their original completion and first nonzero +process exit remain unchanged after clean READY. Existing `entry_begin` checks +remain authoritative; no reset, failure-latch clearing or provider/grant +reinstallation is introduced. + +`target/native-reflected-derived-control-native-evidence/after-v1.json` +(SHA-256 `f9cfe12b7a2013fd63ac0e73672a1c4fbee617e01a366e5747a1ab17a1e6da24`) +records actual session **18249**, terminal **e1bcf6**, all five commands exiting +zero with **one real test each**, and all **3,725 frozen inputs / nine runtime +files unchanged** in the tested candidate based on parent `f4c63a0f`. It uses +the unchanged measured MSVC Debug/Release receipt pin +`96721717326f7d6f96dd2bb98dc1d6e919f7119235e33e103e5ac89bd6f29d0a`. +The earlier original Source06 nine-script, four READY, ordinary-control and +191/382 receipts remain separate evidence. No GNU carrier/fresh-v3 native +execution acceptance follows from this local MSVC gate. + +## Dedicated reflected body and owned Resource Return acceptance + +Sources10/11 are separately named additive controls; the original Sources7/8/9, +historical ten scripts and scalar-Default READY baseline remain byte-identical. +Twelve new shared cases execute genuine first- and second-field successful +Returns, handled first/later borrow failures and four cleanup combinations in +both checking profiles. Each Source retains three equal-int64 reflected fields, +its statements after the Return condition and its statements after the loop. +Finite provider scripts and terminal sentinels verify that Return skips those +statements and all later fields. + +Source10 returns `nothing` from the selected scoped-type body while an outer +optional Resource owner remains live. Its exact borrowed projection ends and the +outer owner retires during Scope completion. Both cleanup cases retain original +body **0**, cleanup **255**, Cleanup channel, empty ordinary message, exact +`native Resource cleanup failed` and first process exit **71**. This differs from +the historical derived explicit-close cases' body255/cleanup255. + +Source11's helper returns an actual distinct, unborrowed Resource owner892 while +aliasing outer owner891. Successful body retirement and outer cleanup precede +publication; the caller's exact Borrow892(29) and consuming close establish the +returned owner's identity and custody. Its ordered events place Finalized891 +before caller Borrowed892. Both helper cleanup cases suppress publication and +caller borrowing, retire891 then the provisional892, and retain root body **255**, +cleanup **255**, Cleanup channel and exit **71**. The helper Source follows a +normal Return path before cleanup failure propagates. The directly observed +tuple belongs to the entry root; the report does not separately read a nested +callee completion. Clean success and +handled failures remain body0/cleanup0 with empty messages and exit0. + +Actual session **64018**, terminal **0b2a26**, passes two one-test reference groups +and one final object group: **24 single-entry reference calls + 48 same-grant +reference calls = 72**, and **24 objects**. Actual session **47358**, terminal +**dcfb82**, passes both one-test linked groups: **24 Source-deleted single-entry +executables / 24 entries** plus **24 same-grant reentry executables / 48 entries**, +for **48 native executables / 72 entries**. Every first script is followed by its +same byte-identical Source's READY on the same Session/provider/paired Network +grant. Increasing nonzero attempt identities, immutable first completion/messages +and first nonzero exit, continuous events/entry boundaries, all eight zero native +Resource counts and empty ordinary storage before teardown, combined-script +exhaustion and one clean destruction all pass. No reset, latch clearing or grant +replacement is introduced. + +`target/native-reflected-body-return-native-evidence/after-v1.json` records the +reference/object gate. The linked receipt is +`target/native-reflected-body-return-native-evidence/after-v2.json` +(SHA-256 `9250a5d37375cd66ba212e1cfcaa489ef0b55571dba0efa72dc59a6d306baa18`). +Both gates retain all **3,730 frozen inputs / nine measured v2 runtime files** +unchanged in the tested candidate based on parent +`91105876e5dbbbe9bbaa1d19bc2408ec9a274255`. The unchanged measured MSVC +Debug/Release receipt pin is +`96721717326f7d6f96dd2bb98dc1d6e919f7119235e33e103e5ac89bd6f29d0a`. +Earlier original/derived/191-case receipts retain their counts and archive pins. +This is bounded local MSVC Return acceptance; it adds no fresh-v3, GNU carrier, +coherent-workspace or exact-revision CI acceptance. + +## Remaining native and reflection work + +The producer breadth is deliberately limited to the exact direct nominal-struct +case in the checked Resource program. Generic and alias owners, bitfield field +families, reflected variants, machines, `TypeInfo.args`, nested reflected +producers and broader function compositions remain compiler implementation and +Source-evidence obligations in the checked Resource path. The ordinary controls +above establish their own bounded parity and leave ordinary `lower_program` +unchanged. Original Source06 exceptional same-grant reentry is accepted above. +The derived historical control matrix and dedicated body/owned Resource Return +controls are accepted above. Same-owner Return while its resident lease remains +live, broader body/Default lifetimes and broad aggregate Resource custody remain +separate obligations. Return operands still precede certified lexical retirement; +the accepted owned-return control uses a distinct unborrowed survivor. This +boundary adds no +compiler-policy ban on existing Source semantics. + +The full native-codegen goal remains active at the rough 87% planning estimate. +The fixed 207-fixture / 182-run-pass inventory denominators do not change. diff --git a/docs/active/native_returned_resource_hooks.md b/docs/active/native_returned_resource_hooks.md new file mode 100644 index 00000000..ad839187 --- /dev/null +++ b/docs/active/native_returned_resource_hooks.md @@ -0,0 +1,62 @@ +# Returned Resource hook descriptors + +Native lowering now supports exact returned compiler-hook descriptors for the +Factory, Borrow and Close recipes. Immutable bindings, aliases, root Return +relays, storage without invocation and immediately invoked returned callees +retain the original checked hook. These are installation-owned metadata, with +no Resource owner, finalizer or ordinary code/environment Function allocation. +A matching public function type never selects a hook or grants provider authority. + +MIR captures the original return and producer family from the retained +ResourceSourceArchive, then rechecks exact headers, original/current Source +calls, aliases and Return occurrences against the current CFG. An independent +body/entry/header snapshot protects callee effects and selected actual order. +Canonical block/local transforms authenticate that snapshot before consuming +remaps; missing retained targets fail without erasing evidence. Fresh CFG facts +invalidate changed locals and joins. Readonly descriptor getters select exact +current occurrences; copied expressions do not receive that authority. + +A preceding ordinary branch with no nested Return can preserve one exact root +Return. Nested or conditional descriptor returns, mutable/dynamic producers, +captures, generic/scoped bodies and descriptor escapes remain pending. The +current rule does not infer an identity from the callable signature. + +Native wire shape 10 records the exact returned hook. Wire tag 15 joins the +original indirect invocation to its hook, signature and separate physical +Acquire, Borrow or Close target at the exact operation frame. The emitter opens +the operation, evaluates and stages original actuals, evaluates the opaque +callee once last, and only then prepares the descriptor invocation. Existing +runtime installation, hook, signature, target and frame checks remain intact. +Raw returned metadata is adopted only after SourceStatus authenticates completed +Scope retirement. Exact opaque occurrences bypass ordinary Function clone and +ownership storage; ordinary named callbacks retain their separate lifetime. + +Authenticated Resource function roots seed the existing typed reachability +traversal before dependencies are walked. An ordinary pure helper reached from +an otherwise unused descriptor accessor is declared and verified normally; it +retains the ordinary context/environment ABI and receives no hidden Scope or +carrier exemption. + +The initial six-case baseline passed 76 reference executions across 38 scenarios +but failed native MIR with `indirect custody call lacks an exact descriptor +producer`. The expanded corpus has 46 scenarios, with all 92 reference and 92 +Source-absent linked native executions passing across debug and release. Stored +and immediate callbacks cover all three recipes, aliases, relays, unused +metadata, factory failure, argument-before-callee effects, failed later actual, +failed callee and finalizer failure. Every execution checks zero obligations +before context teardown. + +A review regression reproduced a copied/resealed branch followed by canonical +pruning being accepted before the repair. Nine focused MIR groups and all 242 +MIR library tests now pass, including block/local pruning and missing-target +controls. Seven focused codegen groups, all 100 codegen library tests and 63 +object tests and all 92 driver-library tests pass. Runtime sources and the +matched private archives remain unchanged. + +This is local MSVC private-archive acceptance. Required/comptime materialization, +returned named callbacks, dynamic/captured/scoped/generic/handled/pipeline forms, +nested return transport, broader aggregates, production providers and +concurrency remain required for the full native-codegen goal. GNU private-archive +Source execution and final-revision workspace/distribution gates remain separate +obligations. The coarse whole-language estimate remains about 85%; this corpus +is execution evidence rather than measured whole-language coverage. diff --git a/docs/active/native_value_abi.md b/docs/active/native_value_abi.md index 8d78e892..5ed2d4c5 100644 --- a/docs/active/native_value_abi.md +++ b/docs/active/native_value_abi.md @@ -318,12 +318,18 @@ number of non-overlapping matches and zero for an empty needle. The scanner rejects byte matches that begin or end inside a grapheme, matching the interpreter's string contract. -A context-local first terminal failure (fixed status plus static message) is -separate from Jett result.fail. Fallible leaf and compiled calls are followed -by a failure edge, which releases live temporaries/locals before returning a -zero placeholder. Callers must inspect failure before using that placeholder. -The entry wrapper returns failure status and the launcher renders the message -before context destruction. Cleanup does not overwrite the first failure. +The first terminal failure of the current execution lifetime is separate from +Jett result.fail. Ordinary one-entry contexts keep their legacy session channel. +Independently authenticated Resource entries retain separate attempt-owned +status, dynamic message and diagnostic-prefix records. Nested compiled calls +share the active record; exact completion seals it. A new entry constructor may +select a fresh record only after predecessor, session-poison and complete +custody/storage checks, preserving every old observation without resetting it. +Fallible leaf and compiled calls are followed by a failure edge, which releases +live temporaries/locals before returning a zero placeholder. Callers must inspect +failure before using that placeholder. The entry wrapper returns failure status +and the launcher renders the message before context destruction. Cleanup does +not overwrite the first failure. Context destruction checks that all owning values have been released; it must not silently hide compiler leaks by acting as a program-long value arena. diff --git a/docs/architecture.md b/docs/architecture.md index 4a8129ff..3ef412c4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -828,9 +828,36 @@ Copy-owned alias expressions acquire retained string or cloned descriptor temporaries before entering containers; borrowed argument lowering reads their existing handles. Plain string view expressions retain ordinary copy behavior. +Ordinary handled optional/result projections authenticate the explicit outer +View, inner View of an exact stable immutable sum local or View parameter, +concrete endpoint type and terminal Return continuation. HIR retains the +checked binding identity. MIR seals the original declaration, current local +headers, tag observation, selecting branch, success projection, absent path +and final initializer before pruning or ownership planning. Generated sum and +success-payload locals remain nonowning; a move-only final alias has the same +backing dependency and no cleanup slot. + +The checked final ownership mode distinguishes a borrowed payload from an +implicitly copyable primitive or String destination. The copy destination may +be mutable, and String receives an owning retained copy. After it is +initialized, constructor-only projection intermediates leave the borrow +inventory, permitting a later ordinary shell transfer or unwrap. The Result +Fail companion likewise owns one String copy. A nonconsuming runtime payload +read preserves the shell and selected payload identity; pending scalar depth +and opaque container metadata remain intact. Outer pending sums fail at Handle +observation before either extraction or failure continuation. + +This bounded ordinary proof does not grant Resource custody or prove the +payload identity hidden by erased interfaces, callables, actors or capability +carriers. Alternate outer Default payload lifetimes, mutable backing origins, +temporary-backed aliases and general loan expiry remain outside it. See the +[ordinary borrowed-sum implementation](active/native_ordinary_borrowed_sum_codegen.md). + Native ownership planning conservatively refuses owner consumption or rebinding -after a source local alias may have been created on a reachable path. It does -not select a source lifetime-expiry rule. Mutable source alias chains, +after a persistent move-only source alias may have been created on a reachable +path. Final copied primitive/String bindings retain the ordinary ownership +behavior described above. This does not select a source lifetime-expiry rule. +Mutable source alias chains, temporary-backed source aliases, projected mutation, allocating conversions, flow-narrowed bare-machine origins, and field paths without the checked declared-owner proof remain explicit native limitations. Owner transitions @@ -1437,8 +1464,10 @@ returns retain ordinary assignment checks before any native arm is removed. Contextual sum handling keeps the producer's original local or call signature. Inhabited nested containers receive an explicit conversion before extraction; empty collections are rebuilt with the target ownership flags, and nested -function values retain their checked adapters. Handler snapshots clone the +function values retain their checked adapters. Owning Handle snapshots clone the underlying local before conversion so owned and borrowed sources stay readable. +A certified ordinary borrowed-payload initializer instead preserves the exact +backing and nonowning success projection described above. Inferred generic reflection peels transparent aliases only at each argument's root and preserves nested alias witnesses. Repeated compatible witnesses fill only missing `never` slots; concrete alias identity follows declaration parameter @@ -2329,9 +2358,121 @@ finalizers, and rejected insertions release their consumed provider payloads. Suppressed panic payloads are dropped under unwind protection; if their destructors panic too, only that secondary payload is deliberately forgotten to bound cleanup while retaining the first failure for propagation. -The checked identity substrate does not yet install interpreter trusted-hook -dispatch, live owned Resource values or source-level scope/drop integration. -Those remain later stages of the resource contract. +The private reference interpreter now carries Resource and hook-descriptor +values bound to an Arc of the successful checked program. The physical carrier +may be copied internally without creating ownership; the separate non-cloneable +holder ticket records custody and acquisition order. Checked argument packets +supply source order, formal permutation and caller effects independently of +callee View/Owned access. Nested checked calls keep those envelopes through +ordinary function entry/return rather than unwrapping a Resource into an +ordinary Value argument. + +Generated absent For/Match binders use a private original-control proof keyed to +the current checked attempt and active body. It retains each original binder, +ordinal, resolver definition, concrete TypeId and immutable ownership mode; Match +also retains the exact selected arm and its payload cardinality. Complete proof +revalidation and the existing recursive absence-shape validator precede binding +publication. Resource-bearing children enter the existing declaration slot as +ordinary evaluated values with empty custody; the strict missing-slot identifier +guard remains intact. Original view loops borrow their retained parent binding +through the existing transport, and generated scope retirement leaves unrelated +live owner tickets intact. No Resource owner/grant, runtime leaf, ABI or native +aggregate permission is added. The full comptime replay passes 554 tests, +including the original 28-wrapper/120-constructor controls and 60 focused entry +observations. Native aggregate emission remains a separate gate. See the +[generated-binding contract](active/native_resource_absent_generated_bindings.md). + +An executable source function rejoins a unique retained AST declaration and its +exact resolver definition. Ordinary declarations use their actual DefInfo span; +mutual definitions additionally rejoin the actual-body resolution and retained +forward declaration. Namespace and header checks remain exact. Runtime +registration clones identify the original checked body and cannot replace it. +Generic selection remains an independent concrete-body proof. + +The ordered ledger covers source scopes, operation temporaries, partial actuals, +owned storage/returns and occupied result payloads. Explicit close and reverse +scope/failure cleanup retire each owning ticket once; views keep a live checked +backing and never gain a ticket. Cleanup completes before context teardown. +Protected cleanup continues after a finalizer panic. A cleanup failure takes +precedence over entry failure; clean cleanup preserves the ordinary entry error, +and source result.fail remains data. The implemented synchronous reference +slice does not prove task/actor cancellation or all aggregate operations. + +Hook descriptors grant no invocation authority. A reached exact hook must have +ReferenceRuntime purpose, matching program/type/signature/caller facts and +runtime capability provenance. Explicit comptime, constants, verify and property +workers cannot execute it; purpose is restored after property evaluation. +The production catalog remains empty, its installed provider is Disabled, and +scripted provider installation is compiler-test-only. All 83 Resource-focused +reference tests pass, including both-profile Source lifecycles, exact absent +aggregate layouts, enum-constructor dispatch and required-worker controls. +Direct scoped/generic/required regions retain original body identity and complete +concrete intrinsic metadata. Pipelines preserve original steps and lexical actual +order; mutable assignment retains declaration slots and replaces owners only +after RHS evaluation. Repeated entry restores metadata and body cursors. +Ordinary refinement predicates, occupied aggregate custody, broader captured/ +reflected/worker coverage and production providers remain pending. Checked HIR +lowering now retains an Arc-bound Resource manifest, exact hook descriptors and +original Source invocation packets; MIR preserves that same manifest and lexical +argument order. Whole-program validation checks unused hooks, nominal types and +complete signatures. Raw lowering has an empty manifest and cannot authorize a +Resource layout. Ordinary staging, acquisition, copy, move and native emission +refuse Resource operations until the separate CFG ownership/drop plan and ABI +exist. Five focused Source handoff groups and all 522 HIR/MIR/codegen/object +emission tests pass; this does not establish native Resource execution. See +[the current reference record](active/native_resource_hook_identity.md#reference-lifecycle-execution). + +A separate private `jett_runtime::resource_custody` core now keeps program/kind/ +owner-slot identity, non-cloneable owner and loan tokens, and scope/operation/ +return frames. Moves advance holder generations; copied physical carriers +cannot adopt an owner. Protected reverse cleanup continues after a finalizer +panic and records Resource cleanup failures separately from ordinary native +first-error state. A private native registration module now decodes bounded +owned little-endian layout tables and validates every row, including unused +hooks, complete signatures, occupied paths and operation references, before +issuing fresh nominal kinds. Its single installation holder binds the registry +context and rejects second installation, shutdown and live registry entries. +Frame signature consistency follows the containing function independently of +instruction site. A separate physical acquisition preflight checks/reserves +storage before a future provider effect; core and opaque-table preflights remain +mandatory. Registration itself creates no owner, loan, payload or provider. +The private native context adapter now uses the existing stationary context +lease and registry for exact installed layout, attempt, frame, owner, loan, +prepared hook and sum records. Ordinary and Resource lookup IDs share one checked +monotonic allocator; lookup bits alone cannot supply custody. Moves consume the +old carrier only after successful core transfer, and refused transfers restore +it. Test-only provider installation stages its paired Network publication after +fallible preflight. Production state remains absent and providers disabled. + +Completion performs protected reverse cleanup before retaining outcome messages +and counts. The caller's observed body status remains unchanged; ordinary first +error, Resource body faults, host panic and cleanup failures determine the +separate selected outcome. A legal ordinary FailureTake cannot erase a Resource +fault. Ordinary borrow Result carriers and their error strings use the existing +recursive native destruction path. Complete v2 Source rows retain lexical/formal +permutations, exact owned argument holders, resident parent loans, callee Scope +and provisional Return frames, and the original caller result destination. +Source entry validates every owned transfer and reserves the whole acquisition +log before moving a prefix. Return publication requires the completed Scope and +current Return activation; failed Scope or Return cleanup cancels publication +and takes priority over body failure. Absent and failed sum shells move as whole +carriers without recovering a Resource owner. Historical v1 metadata cannot +activate these Source transitions. + +All 32 exact Resource leaves use the existing authenticated context; entry +getters return its actual root Scope and paired Network carrier. Output checks +precede effects and reject known context/output overlaps. The formatted runtime +package passes 204 unit and 13 integration tests, including nine Source controls, +three v2 registration controls, two batch-transfer controls and all fifteen +previous adapter controls. The exact root-outcome getter reads the body status +saved only after root Scope retirement in the same Runtime attempt; it neither +resets errors nor substitutes cleanup status. Five controls pin the original +status, active/stale/foreign refusal and body zero with cleanup panic. Ordinary +failure-companion validation checks every selected nested sum payload's ownership +bit before drop. These are runtime host/C-boundary checks. Dedicated +compiler body emission and ordinary companion cleanup, the matched archive, +linked Source lifecycle execution and production providers remain pending. See +[the Source runtime record](active/native_resource_hook_identity.md#native-source-runtime-and-custody-leaves). Move dataflow transfers one cleanup obligation; views never own cleanup. Scope exit, return, handled failure, cancellation, dropped actor messages, and runtime @@ -3628,6 +3769,13 @@ in the interpreter. Every attempt has a fresh process/context and a deadline. Only exits 0 and 71 count as completed attempts; other exits, compilation/link errors, timeouts, and non-reproducible failures abort the runner. Initial suite stdout/stderr remain separate from replay output and the shrunk counterexample. +Native link and execution capture retains parent-owned append-only temporary +files and gives each child an owned duplicate handle. Each readback uses explicit +offsets bounded by one sampled file length; append-only writes preserve earlier +bytes when Windows positioned reads move the shared cursor. Capture errors retain +their stream, creation/duplication/snapshot stage and original I/O source. The +existing polling deadline and child kill/reap protocol do not wait for EOF from +descendants that inherited output handles. The CLI validates explicit build targets before loading source, accepting only the current supported host. Native `build` invokes the driver object and link stages and publishes an executable atomically. `build --check` retains frontend-only validation. The @@ -4027,3 +4175,811 @@ The native move planner borrows only explicitly viewed Join operands for that copy operation and ends the temporary loan before returning the owning result. Ordinary Join operands retain their consumption checks, including view-parameter rejection without an explicit `view`. + +### Source-authenticated Resource ownership analysis + +Successful checked HIR lowering captures an immutable archive of the complete +original functions, equality map and type meanings, including nominal definitions +and generic arguments. MIR authenticates it before constructor capture. A private +per-function witness records the initially emitted graph and exact original/current +Source call associations. Full Source operands, target, access, lexical context and +evaluation order remain independently checked. Canonical block/local compaction +remaps current records; other unsupported rewrites roll back transactionally. +Nominal bounds are checked before archive getters, and malformed type metadata +is rejected before dependent custody queries. + +`ResourceOwnershipPlan` separately analyzes scope/operation/provisional-return +frames, exact local/expression/argument holders, incoming views, bounded loans, +moves, occupied Optional/Result arms and cleanup obligations. A relinquished +Resource actual has no ordinary caller owning-operand claim. The dynamic runtime +acquisition log must determine reverse cleanup order; local-number order is not +cleanup order. The plan's entry block is distinct from the driver-selected program +entry function. The initial plain/Optional/Result analysis does not yet implement +the execution ABI. A fresh plan now supplies the emitted native layout described +below. Direct handled actuals use the constructor-owned call regions described +below; complete aggregate/scoped/reflected custody and concurrency remain required. + +The formatted affected gate passes 535 HIR/MIR/codegen/object-emission tests, +32 type-library tests and the MIR doctest. Twelve focused ownership groups and +the missing-nominal-table regression pass. These are compiler checks, not linked +native Resource execution. See the [ownership handoff record](active/native_resource_hook_identity.md#source-authenticated-ownership-handoff). + +### Bounded recursive lowering frames on Windows + +Heavy HIR statement, equality and inline-function branches and MIR expression +and per-actual branches now execute in private non-inlined helpers. Their +existing Source checks, context restoration, lexical argument order, whole-call +preflight and rollback remain unchanged. Resource call capture remains at the +original expression-lowering entry. This reduces unrelated Debug branch locals +in recursive callers without changing Source limits, worker stacks or launcher +ordering. The original Windows CLI capture regression and all nine capture +checks pass; compiler/object and driver/fixture gates pass. Measured MIR +lower_value frames decrease from 169,448 to 15,272 bytes. See the +[repair receipt](active/native_resource_hook_identity.md#windows-lowering-stack-repair) +for individual frame measurements and exact verification scopes. Native +Resource Source execution remains pending its connected ABI and cleanup. + +### Resource execution family and object layout + +After authenticating the entire original checked HIR archive, MIR derives a +private execution family from Resource functions and their direct Source caller +predecessors. Resource-free wrappers receive their own sealed constructor +witnesses and fresh Scope and invocation proofs. Current graph edits cannot +recompute or expand that authority. Direct calls with handled actuals use typed +Begin/Stage/Invoke/End statements. Their private witness retains the original +call, lexical/formal permutation, generated endpoint headers, complete current +graph and exact completion sites. Fresh CFG analysis tracks the active operation +stack and arrived argument prefix across handler edges. Abandonment retires that +prefix before the original Return operand; normal completion follows exactly one +reached invocation. + +The reference interpreter records each callable body's operation floor and +Return destination separately from its lexical Scopes. Checked Source functions +retain the exact caller destination; ordinary closure and named-callable bodies +have no Resource publication destination. Return retires only newer Operations, +innermost first, while interleaved Scope owners and loans remain live. Private +one-shot receipts bind a canceled Operation to its exact parent and permit +completion only while propagating Return or error, never ordinary successful +output adoption. Cleanup failure drains the eligible prefix before preventing +the original Return operand. + +Native Source status checks validate a terminal completion record tied to the +exact caller Operation, immutable Source tuple and retired callee Scope/Return +frames. That record keeps the original body status separate from cleanup +failure. An aborted call propagates its recorded failure without permitting +output publication; incomplete or unrelated calls cannot acquire a completion +record. Clean output still requires a running attempt with no latched failure. + +Each native operation-frame slot stores its dynamic frame and prepared Source +call in sixteen bytes. Begin prepares the complete Source tuple without actual +effects. Stage evaluates once, activates its exact transfer or loan, and records +one lexical actual. Invoke consumes arrived endpoints and checks Source status +before publishing its result. End clears prepared storage and retires only the +proved prefix. Prepared borrowed-formal metadata remains complete even when no +invocation is reached, using the existing wire-v2 rows and runtime ABI. + +The fresh custody plan also records the exact incoming execution frame at every +reachable current statement and terminator. Sum tag reads, temporary transfers +and selected extraction use that frame; their Local storage may remain in the +ancestor Scope. This projection has no span, type or holder fallback, and public +graph edits still fail the original/current witness checks. + +The code generator projects one fresh whole-program ownership plan into bounded, +deterministic v2 layout bytes. Rows preserve exact containing-function signatures, +parameter access and evaluation order, caller result destinations, provisional +Return storage, absent sums and ordinary failure companions. Return publication +uses the callee's completed root Scope while retaining the distinct Return slot. +The immutable object accessor exposes that layout and the actual driver-selected +entry function, signature and root Scope. It emits no Source body or provider. +Dedicated body calls to the runtime ABI, a matched archive and linked lifecycle +execution remain required. + +ResourceCompanionPlan borrows one fresh ResourceOwnershipPlan and selects only +its exact immutable current function and types. Its private context retains the +complete Source caller validation and ordinary type/child, initialization, +liveness, alias, loan and move checks. Ordinary ownership storage excludes +Resource carriers; their custody remains in the independent CFG plan. The plan +exposes readonly ordinary storage and complete Source acquisition facts with the +same immutable program lifetime. Public ordinary CopyValuePlan, MoveValuePlan +and caller admission keep their Resource-pending refusals. The CFG validator +associates roles with exact current expression occurrences; clones or same-span, +same-type expressions cannot select those roles. This is a compiler prerequisite, +not native body or runtime authority. Three focused both-profile groups and all +546 HIR/MIR/codegen/object checks plus the MIR doctest pass. + +### Direct Resource emission and private matched archives + +The direct Resource emitter validates the original checked graph before native +preparation, derives a fresh immutable custody/layout plan and retains the full +ordinary companion/acquisition analyses. Only functions in that plan receive +the internal Scope argument. Ordinary functions retain their previous CFG and +storage: Resource failure blocks and Return cleanup slots are allocated only +for selected Resource functions. The C entry reads the authenticated original +root body outcome after retirement; Scope cleanup has its separate channel. + +One double-cfg `jett_runtime` staticlib owns the compiler-test main, private +installation and read-only observations. It carries the actual wire-v2 codec +and 32 Resource leaves. The matching receipt records actual profile, source, +archive, native-library order, CRT fingerprints and complete symbol/import +inspection. The driver report decoder preserves original outcome fields and +pre-destruction counts; missing observations refuse acceptance. Ordinary +runtime/test builds exclude the private modules. Actual HOST controls and five +shared reference/native Source scenarios pass in both private archive profiles; +the native programs run after deleting their Project and synthetic Stdlib files. +Exact outcomes/events and zero obligations are observed before teardown. The +private link uses its actual measured library ordering; the reviewed bcrypt/ +advapi32 prefix difference does not alter production launcher validation. +This is a local compiler-test distribution seam, with production providers, +supported-host distribution and broader Resource execution still pending. + +The selected normal-startup prerequisite is generated-object owned: the Resource +C wrapper obtains its own immutable manifest, authenticates and installs the +exact bytes/entry in the normal runtime, begins a Runtime attempt, and completes +that attempt on body/refusal returns. Ordinary objects keep their entry ABI and +the shared launcher has no optional manifest dependency. The normal launcher +observes retained outcomes/messages and completes a Generated-owned Running +attempt after a recoverable entry panic before its single context destruction. +The double-cfg scripted private runner retains ownership of its existing +attempts. Disabled installation supplies no Network authority or production I/O +credit. See [the generated startup prerequisite](active/native_resource_generated_startup.md) +for the exact handoff, first-error channels and current bounded validation. The +restored source passes 68 Python helper controls, eight Runtime startup controls, +four codegen controls, the 11 existing launcher lifecycle controls and 2,371 +workspace library tests. Fresh callable regression gates retain 48 executions +against the changed normal launcher. The 16 fresh private Resource gates retain +754 executions and 900 entries. The complete local startup80 gate passes all +48 normal and 32 private controls across Runtime Debug/Release; the private +controls retain 40 decoded entries, including four derived borrowed probes +classified separately. Linked manifests are wire2; occupied Source33 wire3 is +object-only in this startup packet. Normal links use the ordered library vector +measured from private v6, without a separately measured normal compiler note. +Hosts must exclusively own entry and finish for the context; generated unwind +has no invocation or thread token, and a rejected concurrent or recursive caller +must not unwind its owner. The built-in launcher uses a fresh context for one +entry. Full workspace/native conformance, exact CI, GNU, installed distribution, +actor-graph import, public catalog, typed TCP transport, mixed entry capabilities +and real providers remain required. No concurrent-host ABI or provider acceptance +is implied. + +### Resource replacement preflight + +The native replacement leaf preflights its exact old and evaluated replacement owners against the registered destination before removing handles or invoking finalizers. Core custody checks both current generations and runtime frames, exclusive destination occupancy, absent loans, nominal kind and reserved transfer capacity. The existing transfer rechecks custody after old cleanup; no reusable transfer permission is exposed. Cleanup failure still retires the independently staged replacement and keeps its original failure priority. Four focused replacement tests and all 220 runtime checks pass. The Source projection is described below; aggregate replacement remains required. + +### Source Resource replacement emission + +The fresh MIR custody constructor seals the evaluated RHS slot into Replace, alongside its original mutable destination and old occupancy. The layout projects these exact distinct plain-owner slots to existing tag 12; equal nominal shape and the containing Scope are checked independently. Assignment emits the composite replacement once and publishes its new handle only after Status0, without replaying descriptive Drop/Transfer rows. Constructor-authenticated Local-to-same-Local reseating has no runtime row; vacant assignment uses its exact Transfer. Public Source/witness, ordinary companion and current activation checks stay mandatory. Eleven original Source scenarios pass the real reference and generated native programs in both profiles, with Source deleted before native launch and all native obligations retired before destroy. Conditional/sum, aggregate and projected replacement remain separate transport work. + +### Named Resource function values + +Checked named-indirect body selection retains the original Indirect invocation +and exact argument packet. A private proof joins the original callee occurrence, +immutable local initializer/alias chain, signature and retained declaration under +the current checked program/body attempt. Namespace-qualified producers require +the whole expression span to resolve to that Function definition and exact +retained header/signature; instance fields and method adapters carry no such +namespace authority. The reached function value and runtime +registration must identify that declaration; matching a signature or supplying a +public function name cannot select a replacement body. Execution uses the +retained Source body and the existing Scope, argument envelopes and Return floor. + +MIR captures the original root-scope immutable producer and aliases, authenticates +current headers/initializers after local remaps, and derives target facts through +the current CFG. An independent current-use check refuses escapes even if a +private current-body witness is copied and resealed. Exact current producer, +value and reached invocation occurrences receive readonly projections. Ordinary +companion storage admits only those proven descriptor expressions and locals; +public type-only Resource signature refusals and compiler-hook descriptor +handling remain unchanged. + +The native callee is evaluated once after the original actuals. Callable checks, +exact selected code-address equality and environment-zero validation precede +SourceEnter. Refusal reaches ordinary failure cleanup and retires the acquired +prefix. Accepted calls use the selected Resource family's context/environment/ +Scope/actual ABI, followed by SourceStatus; an ordinary indirect call cannot +supply that Scope. The descriptor has its ordinary owner lifetime and does not +create another Resource owner or finalizer. Runtime ABI and wire-v2 are unchanged. + +Thirty-two shared Source scenarios pass in both reference and linked native +profiles, including exact same-signature target selection, immutable aliases, +construction failure, terminal body failure, finalizer failure and combined +body/cleanup failure. Native Source is absent at launch and obligations are zero +before teardown. This is local MSVC private-archive acceptance; returned hook, +captured/scoped/generic and handled/pipeline callback forms and +GNU private-archive Source execution remain separate required work. + + +### Returned Resource hook descriptor transport + +Exact hook Return summaries are captured from the retained checked Source archive. +Current producer/alias/Return occurrences, headers and CFG state are joined to +that archive, with an independent body/entry/header snapshot checked before +canonical remaps. Readonly getters select only exact current opaque metadata +occurrences. A preceding ordinary branch can preserve a single root Return; +nested returns, captures and dynamic alternatives do not inherit that proof. + +Returned hook shape 10 and indirect invocation tag 15 select the exact hook and +signature plus a separate Acquire, Borrow or Close row at the same sealed frame. +Actual staging precedes the once-only callee evaluation and HookPrepare. Source +results are read only after SourceStatus completes Scope retirement. Exact opaque +metadata avoids ordinary Function cloning/ownership storage; it creates no +Resource owner or finalizer. Existing runtime wire-v2 and custody leaves are +unchanged. Authenticated Resource function roots seed typed dependency traversal, +so ordinary helpers receive normal declarations, verification and their existing +context/environment ABI. + +All 46 shared Source scenarios pass in both reference and Source-absent linked +native profiles (92 executions each), with zero obligations before teardown. +Nine focused MIR groups and 242 MIR library tests, seven focused codegen groups, +100 codegen library tests, 63 object tests and all 92 driver-library tests pass. +This is local MSVC private-archive acceptance. Broader implemented callback, +aggregate and reflection combinations, GNU private-archive Source execution and +coherent-revision release gates remain in current parity scope. Production +provider and asynchronous scheduler integration are unimplemented extension work. +See the +[returned descriptor note](active/native_returned_resource_hooks.md). + +Required Resource materialization uses an opaque value proof produced from +jett_comptime's private program-bound checked cache. HIR consumes that proof +through an acyclic compiler-internal dependency, derives the admitted replacement +itself and records an authenticated execution view while retaining the immutable +original ResourceSourceArchive. MIR checks the original-to-materialized occurrence, +function identity, specialization and same-manifest hook relation through CFG +transforms. Public mirrored cache data or a fresh snapshot of mutated HIR cannot +supply this authority. + +The driver exposes checked required evaluation and baking seams using one +CheckedResourceProgram. Runtime Resource operations are preserved during baking; +only explicit required values are replaced. Compile-time-only helper classification +must be authenticated from the original checked call graph and exported/runtime +roots. It cannot be inferred from a callable signature or used to erase original +body, header or type integrity checks. + + +Required-value rows also seal ordinary helpers' current MIR bodies. Proof capture +is distinct from runtime custody classification: it cannot add Resource frames, +executable Resource roots or hidden Resource ABI parameters to an ordinary helper. +Both syntactic arms of an exact sum-tag guard are planned; the emitted runtime +branch and extraction checks retain actual absence/occupancy. Typed sum headers +and normalized operation nodes remain visible to disconnected-custody checks. +The current shared checkpoint passes 61 scenarios in both reference and Source- +absent native profiles (122 runs each) against the unchanged matched MSVC archives. + +### Borrowed Resource sum integration + +The next Resource family retains the checked concrete body's exact handled-view +binding origin through HIR and a private MIR projection region. Generated sum and +payload locals are nonowning. Fresh typed tag edges distinguish Some/Ok projections +from None/Fail observations; ordinary consuming SumTake remains a separate role. +Wire-v2 registration adds conditional source formals and explicit sum borrow, +observe, project, failure-read and end operations. Five selected status/out C leaves +extend the Resource leaf inventory from 32 to 37 without changing public record ABI. +Absent shells receive live-loan guards despite having no occupied Resource token. + +The expanded 138-scenario corpus passes 276 reference and 276 Source-deleted +native runs in Debug/Release against matched measured MSVC private runtime +archives. Original declaration keys include exact lexical paths; current headers, +backings, guards and constructor graphs retain independent seals. Certified +Fallthrough/Return/Break/Continue records end child loans before parent loans and +clear nonowning aliases before joins/backedges. Return operands run first; lexical +retirement does not complete RootScope or consume its backing. Recursive declaration +inventory alone does not admit For/Match/reflected bodies or unemitted declarations. +Alternate outer Default lifetimes remain pending. Ordinary borrowed data sums +follow their [separate implementation](active/native_ordinary_borrowed_sum_codegen.md); +this borrowed-sum slice does not itself extend Resource For/Match/reflected-body +admission. See +[the borrowed-sum contract](active/native_resource_borrowed_sums.md) and +[the verified nested contract](active/native_resource_nested_sums.md). + +A separate [Resource control-flow proof](active/native_resource_control_flow.md) +authenticates consuming Resource-free list ForEach and ordinary Resource-free enum +Switch. Sequence preparation reconstructs the exact three temporary headers, +source/cursor/length initialization, get/increment prefix and original CFG before +transporting current borrowed, lexical, call and descriptor proofs. Strict joins +and final raw ForEach refusal remain unchanged. The focused local MSVC packet +passes 30 cases / 60 Source-deleted executions across seven Sources, including +consecutive and nested loops. Full 191/382 reference and Source-deleted MSVC native +execution pass. At committed control-flow revision +`f4fa59da6656b1424e7689b78f7035975b14cc14`, the coherent workspace passes 3,454 +tests with zero failures and three ignored tests across 92 result groups; all +3,659 frozen inputs remain unchanged. The receipt is +`target/native-control-flow-f4fa-workspace-evidence/after-evidence.json`. +[Exact-revision CI run 37744039455](https://github.com/vycdev/jett/actions/runs/37744039455) +also completes all four Windows/Linux compiler and installed-toolchain jobs +successfully, recorded in the adjacent `ci-final.json`. These accepted gates +belong to that control-flow revision. The latest reflected-field bridge still +requires its own coherent-workspace and exact-revision CI gates. Excluded erased +carriers and other iterator forms need separate compiler transport proofs under existing +Source semantics. Source06's separate reference repair passes strict Resource execution, including +exceptional exits and same-grant reentry. A separate direct-field HIR/MIR ordinal +bridge now retains the exact checked scopes and passes its bounded native gate, +including clean same-grant reentry. Original Source06 exceptional native +single-entry and same-grant reentry acceptance, plus the bounded derived control +matrix, are recorded below. Broader lifetime combinations remain obligations. + +The [checked reflected-field reference bridge](active/native_resource_reflected_fields.md) +retains the original For/body, parent attempt, resolver binder and ordered owner +metadata. Each field ordinal selects its exact checker-owned scope and full +reflection; runtime metadata alone grants no body authority. Calls isolate and +restore item selectors, and normal/control/failure exits restore the entry frame. +Retained `type.fields` facts also support pure required evaluation without a +provider. Direct nominal-field native acceptance is recorded below; broader +checked Resource reflected loops remain pending. + +### Checked Resource reflected-field ordinal transport + +`ResourceSourceArchive` now captures constructor-only correspondence for direct +nominal-struct `TypeFields` loops in the root body of exact non-generic, +noncapturing functions in its `CheckedResourceProgram`. HIR preserves +every checked `ReflectedIteration(ordinal)` expansion instead of coalescing equal +type/reflection arms in this path. The archive retains exact original For and +binding objects, unique resolver declarations, owner TypeId and full source-aware +reflection, ordered field snapshots, checked body facts, HIR headers and original +loop/dispatch/arm Scope paths. Its opaque proof cannot be created from public HIR +or caller-supplied metadata. The ordinary `lower_program` entry lacks this archive +and retains its current coalescing behavior. Checking specializes type-level +reflection facts while field-index/name branches inspect the current runtime +binder. Independent ordinary positive controls pass 30 Source-deleted native +main/verify/property executions across five forms without a production change. +Those bounded controls establish no ordinary coalescing defect and do not prove +broader checked Resource reflection transport. + +MIR consumes the exact archived dispatch occurrence. For each ordinal it derives +the original immutable `TypeField` binder's index-equality guard and a one-arm +`ReflectedTypeDispatch` with the full source-aware TypeInfo identity. Equal-TypeId +fields consequently retain separate guards and body targets while the existing +native dispatch uniqueness checks remain intact. Private original/current seals +revalidate guard order, dispatches, body and refusing targets, field headers, +archive membership and original lexical paths. Canonical sequence normalization, +pruning and local remaps update only current sites and headers. Arm-specific +subtree lookup preserves the exact nested handler paths; independent borrowed +projection and lexical retirement validation keep local membership and custody +joins unchanged. + +An ordinary function inside this checked Resource program can receive a proof-only +MIR witness for its own archived loop. Execution-family classification remains +ordinary: the witness adds no Resource plan, runtime Scope, provider permission or +custody. Its ordinary map, string, view and uninhabited sequence lowering uses the +existing finite `SequenceEdit` validator, retaining original headers and function +identity and refreshing only the current graph and independent body seal. +Resource execution functions retain their separate consuming-list reconstruction. +No runtime leaf, ABI or wire-v2 row is added. + +The integrated affected-phase gate passes 1,202 checks with all 3,679 frozen inputs +unchanged. The subsequent focused native gate passes 16 Source-deleted executables +and 24 entries across four cases, including clean same-provider/grant reentry. +The unchanged predecessor corpus separately passes all 191 cases / 382 +Source-deleted Debug/Release executions. Both native gates retain all 3,686 frozen +inputs, nine matched MSVC runtime files and the frozen test executable unchanged. +Exact receipts and the independent 30-execution ordinary positive control are in +the [field-loop note](active/native_resource_reflected_fields.md). +Generic/alias owners, bitfields, variants, machines, `TypeInfo.args` and nested +producers still require checked Resource compiler transport proofs and Source +evidence. The original Source06 additionally passes all eight exceptional scripts +through 16 reference observations, 16 objects and 16 Source-deleted native single +entries in both checking profiles, with exact completion and pre-teardown retirement. + +The original nine-script matrix also passes 18 Source-deleted same-grant native +executables / 36 entries in Debug/Release: every original script followed by READY +on one retained Session/provider/paired Network grant. The private launcher +completes and observes the first attempt before requesting the next existing +`entry_begin`. All eight Resource counts and ordinary value storage are zero +before teardown; the runtime independently validates current ordinary +first-error/cleanup state, installed entry, attempt budget and absence of active +or residual custody. Completed Resource cleanup failure is retained historical +evidence and permits the declared next entry after these checks. No error reset +or failure-taking API is introduced. Both immutable completion/diagnostic records, +continuous ordered events and original first nonzero process exit remain exact; +the combined script is exhausted before one Session destruction. + +Fresh measured MSVC Debug/Release archives use compiled receipt pin +`96721717326f7d6f96dd2bb98dc1d6e919f7119235e33e103e5ac89bd6f29d0a`. +Actual reference/native session 30992, terminal `22c716`, passes both Rust +harnesses with exit zero and retains all 3,722 inputs and nine runtime files +unchanged in the tested candidate based on parent `b5c55ac7`, recorded in +`target/native-reflected-exceptional-reentry-evidence/after-v3.json` +(SHA-256 `f3dc34c6a13079c6e0be04716d37af9a69580a7b99b859a4ebd5301376e3aa8b`). +The earlier zero-test filter attempt remains explicitly unaccepted in `after-v2.json`; +each corrected manifest filter executed one real test. Runtime ABI 1, Source wire 2 +and private script/report protocol 1 remain unchanged. This is local MSVC original +Source06 acceptance; the separate derived control acceptance follows. + +The unchanged derived Sources7/8/9 now additionally pass the historical ten-script +matrix: three handled Returns, two Break paths, two Continue paths and three +scalar Handle Default paths. Borrow-failure handlers in Sources7/8 return before +Break/Continue; the executed signal, rather than the fixture family name, defines +these counts. A separate Source9 READY baseline was verified without changing +Source bytes. Break keeps total17 and retires its selected scoped-type body; +Continue keeps total34 and skips the statement after that body; scalar Default +supplies primitive17, resumes the selected body and preserves both field visits +and total34. The outer optional owner is retained until the existing consuming +close or handled Return. + +Actual session 18249, terminal `e1bcf6`, passes all five one-test gates with exit +zero: 22 single-entry reference calls, 40 same-grant reference calls, 22 objects, +22 Source-deleted single-entry executables / 22 entries and 20 same-grant reentry +executables / 40 entries, for **42 native executables / 62 entries**. Each original +script is followed by its own Source's READY on the same Session/provider/paired +Network grant. Strict completion/diagnostic/event records, all eight zero +Resource counts and empty ordinary storage before teardown, combined-script +exhaustion and one destruction remain exact. Explicit-close cleanup failures +retain body255 / cleanup255 / Cleanup channel / first exit71 after READY; existing +entry-begin checks and immutable first observations are unchanged, with no reset. +`target/native-reflected-derived-control-native-evidence/after-v1.json` +(SHA-256 `f9cfe12b7a2013fd63ac0e73672a1c4fbee617e01a366e5747a1ab17a1e6da24`) +retains all 3,725 frozen inputs and nine measured v2 runtime files unchanged in +the tested candidate based on parent `f4c63a0f`, using the MSVC receipt pin above. +Prior original Source06 and 191/382 receipts remain separate. This gate adds no +GNU carrier/fresh-v3 native execution acceptance. The dedicated Return successor +is recorded below; broader lifetimes and coherent-revision gates remain separate. + +Dedicated additive Sources10/11 now pass twelve shared cases for genuine first- +and second-field successful Return, handled first/later borrow failure and cleanup +combinations. Three equal-int64 fields and terminal sentinels verify skipped +subsequent fields/statements. Source10's implicit Return retires its scoped loan +and outer optional owner; cleanup failure preserves original body0/cleanup255. +Source11 returns a distinct owned Resource892 while retiring aliased outer891. +Successful outer cleanup gates publication and exact caller Borrow892(29)/close; +helper cleanup failure suppresses caller use, retires891 then provisional892 and +propagates root body255/cleanup255. The helper Source follows a normal Return +path; the report observes the entry-root completion and retirement events, not a +separate nested callee completion. Cleanup channel, exact diagnostic and first +exit71 remain immutable after same-Source READY. + +Actual sessions 64018 (`0b2a26`) and 47358 (`dcfb82`) pass all five one-test gates: +72 reference calls, 24 objects, 24 Source-deleted single executables/24 entries +and 24 same-grant reentry executables/48 entries, for **48 native executables / +72 entries**. All eight native custody counts and ordinary storage retire before +one destruction; continuous ordered events, per-entry boundaries, script +exhaustion and both exact completions remain preserved on the same +Session/provider/paired Network grant. No reset or failure-latch clearing occurs. +`target/native-reflected-body-return-native-evidence/after-v2.json` +(SHA-256 `9250a5d37375cd66ba212e1cfcaa489ef0b55571dba0efa72dc59a6d306baa18`) +retains all 3,730 frozen inputs and nine measured v2 runtime files unchanged at +tested parent `91105876e5dbbbe9bbaa1d19bc2408ec9a274255`, using the unchanged +MSVC receipt pin above. Sources7/8/9 and their historical ten scripts remain exact. + +The Return operand continues to precede certified lexical retirement; the +successful owned-return case uses a survivor without the outer owner's resident +lease. Same-owner live-lease Return, broader body/Default lifetimes, generic/nested +reflected producers, aggregate custody, fresh-v3 native evidence and coherent- +workspace/exact-revision CI gates remain separate full-goal obligations. The +rough 87% estimate and fixed 207/182 inventory denominators remain unchanged. + + +### Typed Resource aggregate carrier transport + +A separate private carrier plan captures the complete authenticated original type +graph and exact constructor/projection/current-CFG associations. Its slots reuse +Scope/Operation/Return frames while preserving individual leaf custody separately. +Runtime roots bind installed shape, activation, slot and generation; typed paths +and bounded leases validate live projections. Dedicated v3 graph/slot/Source roles +retain strict accepted v1/v2 decoding. An explicit parent-bound sum-loan adapter +bridges selected optional/result children without consuming borrowed backing. +Ordinary ownership fields cannot contain Resource-table handles. Bounded v14/v15 +object/native absence acceptance and current post-repair fresh MSVC-v3 +regression pass as recorded below; broader lifetime/purpose acceptance remains +required. See the +[typed aggregate plan](active/native_resource_aggregate_carriers.md). + +### Authenticated ordinary native entry Scope + +The entry-aware MIR validator authenticates the complete current/original graph +before canonical preparation and returns a separate readonly entry Scope plan. +It adds no execution-family function, owner, loan or Source-call authority. Initial +checked Resource-archive lowering captures proof-only metadata and an independent +body seal for closed Network-only/Nothing candidates, including genuinely pure +entries. Missing, foreign, transplanted or resealed mutable bodies refuse; raw +HIR/default archive paths cannot mint this proof. + +Codegen serializes the selected wrapper root and Complete row separately from +Source-family signature/frame maps and includes the exact entry as a reachability +root. The wrapper validates the installed tuple, obtains the existing Network +grant, calls the ordinary two-hidden-parameter body, reads its retained Status, +attempts exact Complete without replacing the original outcome, and observes +EntryOutcome. Existing Resource-family bodies keep their calling convention. +Runtime ABI, layout wire and provider APIs remain unchanged by this entry bridge. +Current compiler proof and bounded earlier native checkpoints are recorded below +and in [the ordinary entry plan](active/native_resource_ordinary_entry.md). + +### Bounded object/native acceptance and current compiler checkpoint + +All fourteen whole-Source17 wrappers emitted **28/28** genuine host objects in +both profiles at v14, with zero refusals. The full original 3,600-byte Source +prefix, twelve exports, unused refined contract, required controls, verify and +property100 remain intact and in scope. This main-wrapper gate does not establish +every original purpose or live aggregate value. + +target/native-resource-carrier-compile-evidence/after-v14.json (SHA-256 +43c3a3f1fc2fa6d1e4c6cbd7cc7e0bad4f0d560dce0685e6a8fcb2b411aee2dc) +records actual session 94028, terminal f7b2aa, exit zero. + +The separate v15 gate passes **28 Source-deleted single-entry executables / +28 entries**, including Source13's exact ordinary bounds error, and **26 clean +same-grant reentry executables / 52 entries**: **54 native executables / +80 entries**. Exact completion/message/event reports, all eight zero Resource +custody counts and empty ordinary storage before one teardown, event boundaries +and script exhaustion pass. Source13's repeated ordinary-error call on the same +grant remains pending; thirteen clean Sources in both profiles do not replace +that lifetime obligation. + +target/native-resource-carrier-compile-evidence/after-v15.json (SHA-256 +b20c4d9427b11676e7207e5691171c64a861666e0f728b8aa24dba631123154b) +records actual session 93295, terminal b1a472, exit zero. Both v14/v15 retain +all **3,752 frozen inputs / nine runtime files unchanged** at parent +91e3706b770124cbddca952e5fe6b558abc170be. The measured v15 runtime receipt pin is +74a811660ee1772e0374e04579dd51234e8ff38191b942335bedaab5b4e4b6cd. + +The current post-refinement-repair compiler gate is separate from v15 native +acceptance. At v19, **17 focused entry controls (13 MIR + four codegen)** pass, +including four new raw/refinement archive controls. Full package gates pass +**314 MIR checks (313 library + one doctest)** and **181 Cranelift checks +(118 library + 63 object)**, with zero failures or ignored tests. + +target/native-resource-carrier-compile-evidence/after-v19.json (SHA-256 +b549b995be46093f29dbf45314b47f1eea3b09c9bfc20cebf068e477bfaa8f8b) +records actual session 65346, terminal 26ed19, exit zero and all **3,757 frozen +inputs / nine runtime files unchanged** at the same parent. Earlier v3 runtime +gates passed **275 checks at v6** and **44 focused carrier checks at v7**. These +are historical revision-specific results, not current whole-runtime or workspace +success. + +### Current workspace and wider-language observations + +The current v20 whole-workspace gate passes **3,582 checks, zero failures and +13 ignored tests**, across **66 real selected groups / 92 total result groups**. +Actual session **75431**, terminal **1f1c8b**, exits zero for +`cargo test --offline -q --workspace --no-fail-fast`. +target/native-resource-carrier-compile-evidence/after-v20.json (SHA-256 +fb59285ab91f6d15ce57ab59ecf7cc5c00187cdf4c323de04b5bbafe4a78e7d2) +records all **3,757 source inputs / nine runtime files unchanged** at parent +91e3706b770124cbddca952e5fe6b558abc170be. The inputs-v20 manifest SHA-256 is +58dd6f590a1bf2fd63f04d0ec0abdd15aa71452e9e00b1ff36b1feb29ab3612a; +the workspace log SHA-256 is +f610439bbbcb334d88de90c8b8ebf4178f1015be564cc7caf63bba2b5d6ed8da. +This is measured workspace success, not fresh ignored-native acceptance. + +All **11 fresh-v3 native gates now pass**, each selected as a real one-test gate. +Actual session **41872**, terminal **3c7b60**, exits zero with **576 Source-deleted +native executions / 672 entry calls**, across Debug and Release. This includes +the unchanged **191-case / 382-execution corpus drawn from 113 distinct Sources**, +carrier controls, reflected-body controls and owned-Return regression. The v20 +workspace's ignored-test count is historical; v21 independently executes the +eleven native gates rather than treating ignored tests as acceptance. + +target/native-resource-carrier-compile-evidence/after-v21.json (SHA-256 +38f67805c93e66e273d29de84c1cfc3351d70c0235bf656ea661244474582fa2) +records all **3,757 source inputs / nine runtime files, HEAD and inventory +unchanged** at parent 91e3706b770124cbddca952e5fe6b558abc170be. The frozen native +binary SHA-256 remains +110ba3b7bf23e63677ac7afd13a83fc2fa98aeccebc553e8df4f294fb5bc872f. +Matched MSVC v3 archive provenance passes before and after execution with receipt +pin 74a811660ee1772e0374e04579dd51234e8ff38191b942335bedaab5b4e4b6cd. +The successful runner report SHA-256 is +699f2364769ccdd489c7322832f2410690129d8f687006239e3dc6f0b6e82ae6. +This gate establishes no GNU runtime-archive acceptance. + +Owned-Return cleanup failure retains caller/entry-root **body255 / cleanup255**; +the observations establish retirement and root completion, not a separate nested +callee completion. The accepted historical Return semantics and exact Source +prefixes remain intact. Earlier prospective fresh-v3, full-191/382 and reflected- +Return gate statements in retained historical sections are superseded by v21; +broader body/Default, generic/reflected lifetimes and occupied custody remain open. + +Source13's repeated ordinary error, occupied resources, wider whole-language +parity and exact-revision CI remain pending. The rough 87% estimate and fixed +207/182 inventory do not advance. + +The separate 15-case wider-language probe is an **observation baseline, not +acceptance**. Actual session **77834**, terminal **ee797f**, leaves compiler +inputs unchanged and records eight emitted objects, six object refusals, one +earlier backend failure and three reference failures. Frontend/driver build +success does not establish linked parity. Its authoritative receipt is +target/native-language-parity-probe-observations-v1/root-after-evidence.json +(SHA-256 434cba150528d36e018d89b9dbe3acb05adf0309ac6a5aeb47a8ea1b906b4c3d). + +### Raw refinement proof boundary + +Pure refinement predicates lowered through raw/default HIR without an +authenticated checked Resource archive stay ordinary through canonical +preparation. Declaration kind or spelling alone supplies no Resource witness or +entry Scope. Raw bodies with actual Resource custody retain the independent +mandatory capture/refusal path. + +Authenticated predicate proof requirements derive the original declaration kind +from the immutable archived function. Removing a witness and changing the current +predicate kind cannot escape that requirement. Constructor seals reject missing +or foreign proof, modified predicate body and changed header. The repair preserves +ordinary raw refinement behavior while retaining authenticated original authority. + +### Remaining acceptance + +The current whole-workspace gate passes at v20, and all eleven fresh matched- +MSVC-v3 native gates pass at v21, including full **191-case / 382-execution**, +carrier, reflected-body and owned-Return regression. Exact-revision CI remains +pending. Earlier accepted GNU Source-prefix and Return documentation and wire-v2 +receipts retain their independent historical scope; only the measured v21 gate +supplies current MSVC-v3 regression credit. No GNU runtime-archive acceptance is +inferred from these local MSVC results. + +Source13 repeated-error lifetime, occupied aggregate transport/replacement, +broader Resource-valued Default and generic/nested reflected lifetime compositions +remain full-goal obligations. Whole Source/export/refinement and verify/property100 +purposes remain required. No refusal counts as successful Source execution, no +Source policy exclusion is introduced, and the full native parity goal stays +active. The rough 87% estimate and fixed 207/182 inventory do not advance. + + +### Ordinary terminal-error lifetime for repeated checked entries + +The selected Resource entry lifetime keeps ordinary first-error state in a +context-owned private record issued with the exact installed entry, purpose, +RootScope and monotonic attempt identity. Nested ordinary/Source calls share +that record. Exact entry completion retires Resource custody and verifies the +ordinary owner/counter predicate before sealing status, dynamic diagnostic and +prefix. Existing completed Resource diagnostics and original body status remain +independent from selected cleanup. A failed report leaves a non-reenterable +tombstone; a failed retirement leaves the Completing attempt observable. + +The next constructor authenticates the exact sealed predecessor, fresh ordinal, +session failure/ordinary cleanup poison, installed layout and all original +custody/storage guards before publishing another record under the same context +lease. No reset/take-error API authorizes this transition. Existing checked +refinement recovery applies only within its active evaluation and cannot consume +a sealed terminal record. Historical Resource cleanup failure retains the +accepted original Source06 behavior; persistent ordinary cleanup poison remains +fatal even after empty tables. The private launcher requests declared entries, +retains the first nonzero process exit and every completion, and destroys one +Session. Legacy ordinary context failure behavior remains unchanged. + +The managed ordinary runtime passes 307 measured checks with fresh matched +MSVC archives in measurements-02. The new fourteen-Source same-grant gate passes +in Debug/Release: 28 Source-deleted executables / 56 entries, including both +original control13 Bounds messages, body1/cleanup0/Body and retirement before +one destruction. The accepted combined twelve-gate scope is 604 executables / +728 entries: the authenticated first 28/56 plus eleven freshly rerun predecessor +gates at 576/672. The fresh whole workspace passes 3,614 checks, zero failures +and 14 ignored tests across 66 nonzero / 92 total groups. Independent v24 receipt +verification preserves all 3,763 Source inputs, nine fresh/nine historical runtime +artifacts, HEAD/inventory, both native binaries and fresh02 provenance. +See the [ordinary error lifetime note](active/native_resource_ordinary_error_reentry.md) +for exact sessions and hashes. Earlier Source13-pending statements describe +historical checkpoints superseded by this bounded acceptance. No reset, +independent nested callee observation, GNU archive credit or feature-denominator +change is introduced. The separate normal-main linked baseline is complete: +16 observations (12 exit0 / four exit71) from eight exact opaque objects reused +with Debug/Release runtime artifact profiles, with fifteen raw-probe Sources +preserved. It supplies no desired Source oracle, ordinary public-Main context- +retirement observations or feature credit. Occupied custody, broader lifetimes, +wider ordinary parity and fixes, and the successor's exact CI remain open; the +rough estimate stays 87%. + +### Native actor identity comparison + +Validated MIR retains exact nominal actor operand TypeIds and the checked +aggregate layout. The direct comparison leaf validates both opaque IDs in the +current context actor registry and requires complete live backing records. +The recursive Actor layout node compares base instance identity and pending +depth. Actor state stays behind its existing message interface. + +Pending handles use separate zero-field records with the base instance's debug +ordinal. `run` and `join` retain shared state without changing other aliases' +depths. Context retirement releases the base state once. The reference evaluator +uses the same live identity rule for direct and recursive comparison. + +The local actor suite passes on Debug and Release runtime archives after Source +deletion, including production output with Jett optimization enabled. See the +[actor identity verification note](active/actor_identity_equality.md) for scope +and remaining native codegen acceptance work. + +### Checked occupied Resource struct constructors + +MIR retains authenticated Begin/Stage/Commit/End constructor operations and +their exact original HIR fields, written permutation, scope and companion +completion roles. Original/current body checks run before pruning; public +node edits cannot mint the retained constructor proof. The carrier plan and +codegen validator join each operation to the same checked family. + +Runtime ownership transfers transactionally between slot and carrier holders. +A unified acquisition ledger retires ownership before invoking finalizers, +including partial constructor cleanup and same-context reentry. Fresh local +MSVC Debug/Release ABI1/wire3/static-CRT acceptance on 2026-10-12 passes 754 +Source-deleted native executions / 900 entries across all sixteen Resource gates: +the original twelve predecessor gates, two occupied gates and two additional +pipeline/control-flow subset gates. Executed Rust gates decode each completion and check +the exact Source event, message, status and exit oracles, eight zero custody counts +and empty ordinary storage before one destruction. Compiler and Source inputs, +measured archives, compiled gate and dep-info guards pass after execution. The +current integration also passes all 2,314 workspace library tests, including +308 runtime, 577 interpreter, 317 MIR and 120 code generator unit tests, plus +six occupied-struct object emissions. Private +double-cfg archive measurement is separate from ordinary runtime unit execution. +Owning strings/nested occupied carriers, broader aggregate families, public Driver +provider admission, GNU and installed-distribution acceptance remain pending. See [the occupied struct checkpoint](active/native_resource_occupied_structs.md). + +### Selected erased-interface concrete argument validation + +The dispatch boundary must bind a selected concrete method to the receiver's +retained checked implementation owner, then validate every concrete argument +requirement before concrete normalization and body entry. Runtime values retain +nominal and generic identities through erasure; identical field layouts cannot +substitute for the checked type. This is validation only and does not authorize +cloning, bypass pending failures, or remove native unboxing/handle guards. +Generated dispatch preflights already evaluated formals with `InterfaceValidate` +before normalization. Its exact retained TypeIds include nominal refinements, +generic arguments and widths. The reference path uses the selected declaration +namespace before installing callee scopes. The leaf grants no ownership and does +not allocate native value storage; descriptor parsing may allocate Rust storage. +Local MSVC Debug/Release Source-deleted gates pass, including optimized production +matching/mismatched cases. See +[exact concrete argument verification](active/native_erased_same_owner_arguments.md) +for scope, helper-level limits and remaining platform acceptance. + +### Selected lazy reflected callable return validation + +A checked reflected callable request may establish supported new return +refinements through an owned lazy adapter. Exact owner, member, field ordinal, +pending-selector and declared-schema checks precede construction. The adapter +retains the original stored signature, source body, captures, debug identity +and pending state; constructing or reading it invokes neither callback nor +predicate. Requested parameter, view and capability contracts must satisfy the +existing ordinary callable adapter relation. A descriptor or requested type +annotation grants no proof, effect authority or nominal conversion. + +Each invocation evaluates its actuals once, invokes the retained source once +under the original signature, and preserves the source's ordinary validation. +Only after the source result exists does the supported typed produced-refinement +conversion establish the unproved requested return suffix. Failed validation +uses the existing terminal refinement-boundary error and publishes no trusted +target result. Exact established prefixes are reused. The adapter never joins +implicitly or inspects pending payload; a pending result requiring a new check +reaches the existing pending-inspection refusal before predicates or consumers, +while exact already-proved pending transport remains unchanged. + +On 2026-10-12 the managed integration passes all eighteen original Sources, +including the byte-exact wider/exact probes, named/captured callbacks, lazy reads, +repeated calls, invalid results, selector precedence, optional results and +Nothing/refined-unit pending transport. Their thirty-six Source-deleted native +executions pass against normal MSVC Debug/Release Main archives, with the +original release=false Source/debug policy and unoptimized objects preserved. +Three additive canonical Stdout controls pass six objects and twelve executions +across both Jett optimization settings and both runtime profiles: lazy repeated +calls preserve the source descriptor; invalid returns suppress the consumer; +wrong selector ownership refuses before callback body output. The four Driver +gates compare exact stdout, stderr and exit status. The ordinary public launcher +does not expose context-retirement counters. + +The current fourteen focused reference controls preserve every original thirteen +test body and add a factory/primitive-call origin-restoration control. Original +closure body and typed capture/context witnesses are constructed at the trusted +capture site. Admission and invocation compare against retained witnesses; +mutating the body, checked context, signature, ordinal, Source owner or predicate +cannot mint proof. Ordinary required constants/refinements and already-proved +ancestor transport retain their existing path. + +Capture-origin preparation now skips an unused checked-root projection only when +the exhaustive named-body scan finds no possible inline capture origin. Inline +setup stays inside its function-value arm; InlineFn, Comptime, Error and +ComptimeTypeBind remain conservative positive shapes. Reflected authentication +and pending capture snapshots remain unchanged. One isolated, unchanged native +reflection control takes 295.06 seconds before and 64.86 seconds after this +change; this measures that gate's wall time rather than general native throughput. + +After this performance change, all 2,359 workspace library tests across 22 result +groups pass, together with the four callable Driver gates (48 native executions), +27 broader interface, 10 erased-owner and nine actor native regression tests. +Fresh current compiler/source/binary/dep-info guards also accept all sixteen +private Resource gates: 754 Source-deleted executions and 900 entry completions. +Only the unchanged, independently measured v4 Debug/Release ABI1/wire3 static-CRT +runtime archives are reused, with compiled receipt pin +`bc2ad54743380441f074d8d5d212048ecf440669817c0cb69ce2fd0810ba1c95`. +The fresh post-change gate and complete source guard supply current acceptance; +old v4 execution records do not. Each Rust gate checks its existing exact +completion, script/event, message and cleanup oracles before one teardown. + +This is bounded local MSVC integration acceptance. Full coherent-workspace and +native conformance gates, exact-revision supported-host CI, GNU and installed +distribution remain pending. New nominal/secret proof establishment, protected +secret observation, broader generic/reflected forms and authenticated generated +verify/property-suite transitions remain full-goal obligations. No Source +exclusion or broad language-completion claim follows: the rough 87% estimate, +fixed 207-fixture/182-object inventory and full 100% goal remain unchanged. diff --git a/docs/design.md b/docs/design.md index 140c411e..5b86c5bf 100644 --- a/docs/design.md +++ b/docs/design.md @@ -2214,11 +2214,29 @@ Private trusted resource operations bind to the exact associated resource and Function declaration identities, loader-owned stdlib origin, and complete checked parameter ownership modes and result type. Ordinary privacy and declaration order still apply; a matching name or type cannot grant authority. -The compiler's identity substrate has an empty production catalog and introduces -no public provider API or resource value. Its checked boundary and remaining -lifecycle work are recorded in the +The production catalog remains empty and no public provider API is exposed. +The private reference interpreter can execute the exact checked compiler-test +hooks with an internal scripted provider. This does not enable a shipped +Resource API or native Resource execution. Known absence is admitted only +through the exact checked aggregate shape; it grants no cleanup obligation or +provider authority. Ordinary refinement predicates remain required even when +another field is an absent Resource container. The checked boundary and current +reference evidence are recorded in the [active resource-hook note](active/native_resource_hook_identity.md). +Generated For element/map-value and Match payload bindings can retain a checked +inactive Resource-bearing shape without acquiring cleanup ownership. Reference +execution authenticates the original control, selected binder/ordinal, active +body and exact type/mode facts, then validates the complete selected parent and +child before installing an empty-custody binding. View iteration retains its +original parent binding; unrelated live owners keep their existing tickets. +This supplies no occupied aggregate custody, provider authority, refinement +predicate shortcut or native aggregate execution. All 554 comptime tests pass, +including the unchanged Source17 replay and 60 focused entry observations. +The bounded contract and +acceptance record are in the +[generated-binding note](active/native_resource_absent_generated_bindings.md). + The compiler can retain one immutable checked resource program by owning the parsed source and deriving its resolution and checking internally. Parse, resolution and checking errors prevent that handoff, including errors unrelated @@ -2249,6 +2267,85 @@ printed or reflected as a value. Nested printing remains a separate boundary, including hidden secrets, known absence and omitted values. See the [implemented ownership boundaries](active/native_resource_ownership_boundaries.md). +Reference Resource execution retains the same successful checked program, +exact declaration and concrete body facts through argument evaluation. Caller +ownership follows Rule24 independently of callee access. An opaque physical +carrier is not an owner: a separate non-cloneable ticket transfers the one +cleanup obligation through scopes, temporaries, calls and occupied results. +The implemented reference tests cover reverse cleanup, explicit close, owned +returns and failure paths before context teardown. Descriptor creation is data; +a reached hook requires runtime purpose and matching provider authority. +Required comptime, verify and property execution do not acquire that authority. +Direct scoped bodies, concrete generic type metadata, pipelines and mutable +assignment retain their exact original checked body, step or statement facts. +Repeated entry restores its metadata and body cursor before another invocation. +Broader captured/reflected/worker contexts, ordinary refinement predicates, +occupied aggregate custody and native drop elaboration remain required; the +bounded tests do not establish those complete domains. + +Checked Resource lowering now preserves the original immutable program's +manifest, nominal kinds and exact hook signatures through HIR and MIR. Typed +descriptor and invocation nodes retain original caller facts and lexical actual +order. Raw lowering cannot create those identities, and all manifest entries, +including unused hooks, are validated. A separate Source-authenticated ownership analysis now records scope, operation +and provisional-return frames, exact owner slots, loans, transfers, occupied sum +arms and cleanup obligations. It retains the original checked HIR and complete +original type meanings; edited public HIR cannot recreate that authority. An +original bare owned Resource actual passed to a view formal belongs to an owning +argument holder and a bounded loan, rather than an ordinary acquisition record. +This analysis is compiler metadata. Native execution still requires the emitted +layout, connected custody ABI and cleanup on every control-flow edge. +The compiler now derives the native execution family from that authenticated +original program, including resource-free direct callers of Resource functions. +It projects the fresh ownership plan into immutable layout bytes and the exact +driver-selected entry tuple. These projections provide consistency metadata; +they do not grant runtime authority or enable native Resource body execution. +A separate ordinary companion plan borrows that fresh custody proof and its +exact current function. It retains complete Source acquisitions, ordinary type, +initialization, liveness, alias and move checks while excluding Resource carriers +from ordinary ownership storage. Exact current expression identity selects +already validated custody roles; identical cloned expressions cannot select them. +Public ordinary move/copy/caller planning still refuses Resource authority. + +Native lowering also retains exact returned compiler-hook descriptors through +immutable bindings, aliases and root Return relays. Immediately invoked returned +callees execute once after the original actuals. Installation-owned hook +metadata carries no Resource cleanup obligation and is distinct from the +ordinary named Function object. Its exact original/current producer, body, +header and Return proof selects the hook; a public callable type or matching +signature cannot do so. Canonical remaps authenticate an independent body proof +before pruning, and runtime invocation still validates installation, hook, +signature and exact physical operation frame. Pure helper dependencies retain +their ordinary ABI. The local native/reference corpus passes all 46 scenarios +in both profiles; dynamic, captured, scoped/generic, required/comptime and +returned named callbacks remain separate work. See the +[returned hook implementation](active/native_returned_resource_hooks.md). + +The private native runtime custody core separately tracks non-cloneable owned +and borrowed tokens, checked frame destinations and reverse acquisition +cleanup. Its protocol tests cover generation/provenance refusal and +cleanup after failure or panic. A private bounded registration path validates +all layout rows before issuing context-bound nominal kinds and slot metadata. +Registration installs no payload, provider, grant or owning token. A separate +private context adapter now connects that registration to the custody core with +exact frame, owner, loan, sum and prepared-operation records. Scripted providers +and a paired Network grant are available only to compiler tests; production +state remains absent and providers disabled. Completion cleans obligations +before recording the outcome, preserving observed body status and separate +ordinary, Resource body and cleanup errors. Ordinary Result companions use the +existing recursive destruction path. The private runtime now validates complete +v2 Source-call metadata and exports exact custody leaves for child Scope entry, +owned actual transfer, resident views, provisional Return publication and absent +or failed sum carriers. It checks every owned actual and reserves the callee +acquisition log before any transfer. Return cancellation cleanup takes priority +over body failure. The generated entry can read the original body status from +its exact retired root Scope while the same Runtime attempt remains active; +this read does not reset errors or replace the separate cleanup outcome. +Ordinary optional/result companions validate each selected payload's physical +ownership before recursive destruction. These runtime transitions still require dedicated compiler +body emission, ordinary companion cleanup, a matched runtime archive and linked +Source execution; production providers remain disabled. + Moving a resource transfers its single cleanup obligation. A `view` borrows it without taking cleanup responsibility. An explicit close consumes the owner; otherwise the compiler invokes the same trusted finalizer exactly once when the @@ -5937,6 +6034,36 @@ box; its runtime implementation may copy the payload. These bounded paths make no zero-runtime-copy claim; see the [call-view implementation](active/native_scoped_call_view_staging.md). +An explicitly viewed handled optional/result payload follows the same +borrowing rule when it retains one stable immutable sum local or view +parameter. The canonical initializer has an explicit outer `view` around a +Handle whose target is an explicit `view` of that binding. A Some/Ok move-only +payload is a read-only, nonowning alias with the exact checked payload type; +it is not an implicit clone or transfer out of the sum. Forwarded aliases +retain the same backing dependency. An explicit `clone` acquires independent +ownership. + +A final implicitly copyable value keeps its ordinary copy semantics instead +of becoming a persistent borrow. Primitive values are copied; a `string` +destination owns its retained copy. A copied destination may be declared +`mutable` and rebound. Once that destination is initialized, the generated +projection views no longer prevent a later ordinary owned transfer or unwrap +of the original sum. This does not end a live move-only payload alias or +permit changing its backing owner. + +The bounded native ordinary-sum proof requires a terminal Return on the +None/Fail continuation. In `result[T, string]`, the Fail companion owns one +String copy for that handler. Outer pending sums fail before extraction or +continuation execution; ready sums retain their payload's pending metadata for +later observers. Nested expression-level Handles can retain their own scalar +Default, but an alternate outer Default payload needs its own backing-lifetime +contract. Mutable backing origins, temporary-backed aliases, loan expiry and +erased Resource-bearing payload identity are not established by this proof. +The [ordinary borrowed-sum implementation](active/native_ordinary_borrowed_sum_codegen.md) +is separate from the +[Resource borrowed-sum contract](active/native_resource_borrowed_sums.md) and +[verified nested Resource contract](active/native_resource_nested_sums.md). + **Rule 2: A view cannot be sent to another thread.** ``` @@ -8194,8 +8321,10 @@ retaining their checked type and span. Supported namespace constant reads use the same materializer and have no runtime initializer. Ordinary pure calls outside required evaluation remain runtime calls. The backend rejects unresolved `Comptime` and namespace `Constant` markers instead of emitting their source -computation. Move-only namespace constant ownership remains an explicit parity -obligation. A native regression executes baked `math.factorial(5)` after removing +computation. Other namespace constant types report E9001 under the current +supported domain; a separate extension would require a read and ownership +contract. Current parity validates admitted baked values and shared frontend +rejections. A native regression executes baked `math.factorial(5)` after removing its source file. Native verify and property suites honor the selected debug or release build @@ -8212,10 +8341,15 @@ String handles have explicit retain/release ownership and are destroyed at last release. MIR definite-initialization and liveness facts drive local and temporary cleanup through branches, loops, overwrites, returns, and terminal failures. -Terminal runtime failure is a context-local first error, not `result.fail` data. -Compiled calls test that channel before using a return value; failure edges release -frame owners and propagate it to the launcher. Context destruction checks for leaked -native owners, and cleanup failure overrides entry failure. UTF-8 string kernels +Terminal runtime failure is the first error of the current execution lifetime, +not `result.fail` data. Ordinary entries retain the context-local channel. Each +independently declared checked Resource entry owns a privately authenticated +attempt channel; nested calls share it. Exact completion seals its status and +original diagnostic. Only the entry constructor may select a fresh attempt after +retirement, while all prior completion/error records and session poison remain. +Compiled calls test the current channel before using a return value; failure edges +release frame owners and propagate it to the launcher. Context destruction checks +for leaked native owners, and cleanup failure overrides entry failure. UTF-8 string kernels use the same extended-grapheme segmentation dependency as the interpreter. This initial handle representation is not the proposed inline/SSO optimization. @@ -8396,3 +8530,293 @@ the set's existing equality semantics. Generic task joins preserve the instantiated error type in handler observations, including when one source body is called with both secret and public errors. + +The native backend now emits direct checked Resource calls with an internal +Scope parameter, exact lexical acquisition, provisional Return publication and +ordinary companion cleanup. Ordinary callable signatures and Source rules stay +the same. Direct Result handlers inside call arguments use constructor-owned +Begin/Stage/Invoke/End regions. Arguments stage in lexical source order, and a +handler Return retires only the acquired prefix before evaluating its original +return operand. A skipped invocation preserves the handled Source outcome; +cleanup failure retains its own observed failure channel. Temporary sum storage +and the active execution frame are checked separately. +The reference evaluator records a separate operation floor for each callable +body. A Return retires only operations opened within that body, preserving +interleaved Scope owners and loans. Ordinary callable bodies cannot inherit +a Resource return destination from their caller. Cleanup failure prevents +evaluation of the original Return operand. +Native call joins propagate the callee's completed failure status without +replacing it with a protocol refusal. The callee's original body outcome +stays separate from failure during cleanup. +Native function values initialized from exact named, non-generic, +capability-free declarations now support Resource calls through root-scope +immutable locals and immutable aliases, including namespace-qualified +functions selected by their full-expression resolver identity. Their original +invocation remains +indirect. The backend verifies the selected code address and empty environment, +then supplies the selected body's internal Scope through the checked Source +bridge. Descriptor storage remains ordinary; the Resource argument retains its +independent custody and cleanup. Exact returned compiler-hook descriptors and +required pure comptime hook materialization now retain their checked authority +through aliases, root Return relays and immediate invocation. Their independent +original-body proofs survive canonical pruning and reject altered or missing +occurrences. Captured/scoped/generic runtime callables, handled indirect calls, +nested descriptor returns and conditional/aggregate replacement still require +separate implementations and Source evidence. +Compiler-test Resource providers are confined to a single runtime archive built +with both `test` and `jett_resource_native_test_archive`; production providers +remain disabled. Archive compilation and object emission alone do not establish +linked Source execution. The shared original Source corpus now contains 138 +scenarios passing in both MSVC native profiles against their real reference +outcomes: 276 native runs with Source files absent at launch and zero live +obligations before teardown. Nested handled-view aliases now support terminating +and joining branches, scoped type bodies, and While/Break/Continue boundaries. +Exact declaration identity and certified child-before-parent lexical loan ends +preserve backing ownership; Return operands run before retirement. Broader +For/Match forms, reflected bodies, alternate Default lifetimes and native Resource +semantics remain separate work. Ordinary borrowed data sums have their +[separate implementation](active/native_ordinary_borrowed_sum_codegen.md). +This local test slice keeps production Resource providers disabled. See the +[nested borrowed-sum contract](active/native_resource_nested_sums.md). + +The separate [Resource control-flow transport](active/native_resource_control_flow.md) +now preserves checked custody through consuming Resource-free list iteration and +ordinary Resource-free enum selection. Exact constructor-owned normalization and +independent original/current proofs keep backing ownership and strict joins intact. +Its seven-Source, 30-case / 60-execution local MSVC Source-deleted gate passes, +including consecutive and nested loops. The full 191-case / 382-execution MSVC native successor also passes. +Its coherent-workspace and exact-revision CI gates remain pending. Erased carriers, other iterator forms +and reflected bodies retain their compiler proof obligations under existing Source +semantics; the separate Source06 reference repair establishes no native admission. + +Native plain Resource replacement validates both exact live owners, active runtime holders, loans, nominal kinds, generation capacity and acquisition storage before retiring the old value. An invalid replacement leaves both owners intact. The native compiler consumes this check for exact plain-owner replacement. + +Native plain Resource assignment now retains the evaluated RHS while the old owner remains live. Successful replacement retires old and installs RHS once; failed RHS evaluation preserves old for its handler or scope cleanup. Exact self-assignment moves and reseats the same local without a runtime transfer or finalizer. Assignment after close transfers into the vacant local. Conditional, aggregate and projected native replacement remain pending. + +Native required Resource values retain their compile-time authority separately +from the public span/context cache. Only a successful required evaluation in the +same checked program can select its exact hook. Materialization preserves the +original comptime expression and its full concrete generic and scoped context. +Pure branch selection, aliases and relays do not introduce a separate eligibility +rule; no runtime capability or provider enters the required worker. + +Checked reference execution now preserves exact field ordinals and original +scope bodies in direct `type.fields` loops, including equal-type fields, aliases, +recursive calls and Resource cleanup through early exits. Closed pure required +evaluation uses the same boundary without runtime authority. + +The checked Resource native lowering path now retains distinct field ordinals +for direct nominal-struct `type.fields` loops and their direct `field.type_info` +type bindings. An immutable original Source archive joins each ordinal to its +exact checker-selected body and lexical Scope path. MIR selects that body with +the original field binder's index guard followed by a one-arm TypeInfo dispatch, +so equal-type fields keep distinct bodies without weakening TypeInfo identity +checks. Runtime metadata alone cannot select an executable body. + +Ordinary helpers in the same checked Resource program may retain this proof +without gaining Resource custody, provider authority or a hidden Scope ABI. +Their ordinary map, string, view and uninhabited sequence preparation stays +available. This bridge adds no Source rule, runtime leaf, ABI or wire-v2 change. +The affected compiler phase gate passes 1,202 checks. Four focused cases pass +16 Source-deleted native executions / 24 entries, including clean same-grant +reentry; the unchanged 191-case corpus passes all 382 Debug/Release executions. +All 3,686 frozen inputs and nine matched runtime files remain unchanged. +Ordinary `lower_program` coalescing stays unchanged: independent positive controls +pass 30 Source-deleted main/verify/property executions without a production change, +and establish no ordinary coalescing defect. Generic and alias owners, bitfields, +variants, machines, type arguments and nested producers remain implementation +obligations in the checked Resource path. Original Source06 additionally passes +16 exceptional Source-deleted single entries and 18 same-grant native executables / +36 entries in both checking profiles. Its original completions and first nonzero +process exits remain exact after READY on the retained Session/provider/grant. +Explicit derived Return/Break/Continue/Default native controls and broader aggregate +custody remain separate acceptance obligations under existing Source semantics. See the +[reflected-field execution contract](active/native_resource_reflected_fields.md). + + +Typed Resource aggregate transport is being implemented through a separate +constructor-owned carrier plan. Actual empty collections and inactive payloads +retain their complete nominal, generic, variant/state and qualification identity; +absence cannot erase a type's potential Resource paths or prove a refinement. +Dedicated carrier observations and a parent-bound borrowed-sum adapter preserve +existing Source ownership and cleanup. Wire v3 retains strict v1/v2 compatibility. +Bounded typed absence transport and its v21 local MSVC regression have actual +native acceptance; the ordinary-error successor's twelve-gate regression also +passes at v24. Live aggregate custody and replacement remain follow-ons. See the +[typed aggregate plan](active/native_resource_aggregate_carriers.md). + +### Native ordinary entries in Resource programs + +A program may declare Resource APIs while its selected entry and ordinary helpers +perform no Resource custody operations. Native compilation preserves their +ordinary calling convention. The generated program wrapper owns a separately +validated cleanup Scope for the exact selected closed, capture-free, +Network-only/Nothing entry, including an entry with no parameters. Initial checked +lowering retains its own original Source provenance and full-body graph proof; +mutable MIR cannot manufacture this authority afterward. This native metadata +changes neither Source execution-family membership nor purity/comptime rules. +See [the ordinary entry plan](active/native_resource_ordinary_entry.md) for the +bounded implementation and pending acceptance gates. + +The selected normal startup prerequisite makes the generated Resource object +install its exact immutable layout and own the Runtime begin/complete lifecycle. +It preserves the ordinary entry ABI, retained first-error/cleanup outcomes and +Runtime-only authority. The normal provider remains Disabled and grants no +Network authority; actual TCP/OS providers, public driver/catalog wiring, mixed +entry capabilities and shipped-host execution remain required. The separate +[generated startup note](active/native_resource_generated_startup.md) records the +internal ABI choice, private-runner compatibility and current bounded validation. +Current local MSVC controls pass all 68 Python helper, eight Runtime startup, +four codegen and 11 existing launcher lifecycle controls, plus 2,371 workspace +library tests. Four fresh callable Driver gates retain 48 executions against the +changed normal launcher. The 16 fresh private Resource gates retain 754 executions +and 900 entries. The complete local startup80 gate accepts 48 normal and 32 +private executions; the private reports retain 40 entries, with four derived +borrowed probes classified separately. Actual linked Resource manifests are +wire2; supplemental occupied Source33 wire3 is object-only. Normal links use the +private-v6 measured ordered static-CRT vector; a separate normal compiler-emitted +vector was not measured. Hosts must exclusively own entry and finish for a +context; generated unwind has no invocation or thread token, and a rejected +concurrent or recursive caller must not unwind its owner. The built-in launcher +creates a fresh context for one entry. Full workspace/native conformance, exact +CI, GNU, installed distribution, public providers and actor-graph import remain +open. No concurrent-host ABI or real-provider acceptance is established. + +The selected ordinary-error reentry repair keeps sealed attempt observations +separate from legacy session failures and persistent ordinary cleanup poison. +It introduces no error reset, test recovery API or recovery inside a failed Source +function. The managed runtime and fresh matched MSVC archives pass their measured +checkpoint. The full fourteen-Source same-grant gate passes in Debug/Release, +retaining both control13 signed Bounds errors, body1/cleanup0/Body and exact +retirement before destruction. Its authenticated 28 executables/56 entries and +eleven fresh predecessor gates at 576/672 establish a combined twelve-gate +604-executable/728-entry scope. The fresh whole workspace passes 3,614 checks, +zero failures and 14 ignored tests; independent v24 input/artifact verification +passes. These are bounded local MSVC results. The separate normal-main linked +baseline is complete: sixteen observations, twelve exit0 and four exit71, from +eight exact opaque objects reused with Debug/Release runtime artifact profiles; +all fifteen raw-probe Sources remain preserved. Baseline collection supplies no +desired Source oracle, ordinary public-Main context-retirement observations or +feature credit. Occupied aggregates, broader lifetimes, wider ordinary parity +and fixes, and exact successor CI remain obligations. See the +[ordinary terminal-error lifetime note](active/native_resource_ordinary_error_reentry.md). + +### Actor handle identity equality + +Actor `==` and `!=` compare live instance identity for the same checked nominal +actor type in its owning runtime context. An explicit clone aliases the same +actor; distinct spawns compare unequal even when state is identical. `!=` +negates the same comparison. Enum payload traversal applies this rule through +supported nested containers. Explicit user-struct `Equatable.equals` remains +its own exact two-view boundary. + +The actor registry separates base instance identity from pending handle depth. +`run` creates a registered pending alias that shares state, and `join` reaches +the same base instance at ready depth. Ready cloned aliases remain ready when +another handle is wrapped. Recursive actor payload equality compares instance +identity and per-handle pending depth. Direct pending comparisons retain their +unsupported binary-operation error. Context retirement releases actor state +once. See [the implementation and verification note](active/actor_identity_equality.md). + +### Occupied Resource struct constructor checkpoint + +The checked Resource path now stages named struct fields in their written +evaluation order, then commits their owned tokens to one constructor carrier. +Abandoning a partially initialized constructor retires its reached fields; +normal scope exit retires committed fields in reverse acquisition order. +Cleanup failure remains visible, and a later entry uses the existing sealed +completion transition after the prior entry has retired its custody. + +Fresh local MSVC static-CRT Debug/Release acceptance on 2026-10-12 covers the +three valid entry Sources for scope drop, reversed named arguments and abandoned +prefix: 44 Source-deleted native executions / 66 entries. The original twelve-gate +predecessor corpus passes 604 executions / 728 entries, and additional pipeline +and control-flow subset gates pass 106 / 106 against the same independently +measured private archives. All sixteen gates pass 754 executions / 900 entries. Recursive and generic aggregates, collections, +projection, extraction, replacement, reflection and captured aggregate lifetimes +remain full native-codegen obligations. This checkpoint adds no Source policy +exclusion. See the [occupied struct note](active/native_resource_occupied_structs.md). + +### Selected erased-interface concrete argument contract + +An erased call remains admissible when its interface method requires additional +arguments of the receiver's concrete implementation type. Dynamic dispatch must +validate those arguments' retained exact checked concrete identities before +converting arguments or entering the concrete method. Structurally identical +nominal types are not interchangeable. Validation preserves pending failure +precedence, ordinary view semantics and argument cleanup, and supplies no clone +or ownership authority. The reference evaluator and native backend implement this +contract. Local MSVC Debug/Release gates pass, including optimized production +matching/mismatched calls after Source deletion. See +[exact concrete argument verification](active/native_erased_same_owner_arguments.md) +for measured scope and remaining work; interface equality is separate. + +### Selected lazy reflected callable return validation + +A checked reflected callable request may establish supported new return +refinements through an owned lazy adapter. Exact owner, member, field ordinal, +pending-selector and declared-schema checks precede construction. The adapter +retains the original stored signature, source body, captures, debug identity +and pending state; constructing or reading it invokes neither callback nor +predicate. Requested parameter, view and capability contracts must satisfy the +existing ordinary callable adapter relation. A descriptor or requested type +annotation grants no proof, effect authority or nominal conversion. + +Each invocation evaluates its actuals once, invokes the retained source once +under the original signature, and preserves the source's ordinary validation. +Only after the source result exists does the supported typed produced-refinement +conversion establish the unproved requested return suffix. Failed validation +uses the existing terminal refinement-boundary error and publishes no trusted +target result. Exact established prefixes are reused. The adapter never joins +implicitly or inspects pending payload; a pending result requiring a new check +reaches the existing pending-inspection refusal before predicates or consumers, +while exact already-proved pending transport remains unchanged. + +On 2026-10-12 the managed integration passes all eighteen original Sources, +including the byte-exact wider/exact probes, named/captured callbacks, lazy reads, +repeated calls, invalid results, selector precedence, optional results and +Nothing/refined-unit pending transport. Their thirty-six Source-deleted native +executions pass against normal MSVC Debug/Release Main archives, with the +original release=false Source/debug policy and unoptimized objects preserved. +Three additive canonical Stdout controls pass six objects and twelve executions +across both Jett optimization settings and both runtime profiles: lazy repeated +calls preserve the source descriptor; invalid returns suppress the consumer; +wrong selector ownership refuses before callback body output. The four Driver +gates compare exact stdout, stderr and exit status. The ordinary public launcher +does not expose context-retirement counters. + +The current fourteen focused reference controls preserve every original thirteen +test body and add a factory/primitive-call origin-restoration control. Original +closure body and typed capture/context witnesses are constructed at the trusted +capture site. Admission and invocation compare against retained witnesses; +mutating the body, checked context, signature, ordinal, Source owner or predicate +cannot mint proof. Ordinary required constants/refinements and already-proved +ancestor transport retain their existing path. + +Capture-origin preparation now skips an unused checked-root projection only when +the exhaustive named-body scan finds no possible inline capture origin. Inline +setup stays inside its function-value arm; InlineFn, Comptime, Error and +ComptimeTypeBind remain conservative positive shapes. Reflected authentication +and pending capture snapshots remain unchanged. One isolated, unchanged native +reflection control takes 295.06 seconds before and 64.86 seconds after this +change; this measures that gate's wall time rather than general native throughput. + +After this performance change, all 2,359 workspace library tests across 22 result +groups pass, together with the four callable Driver gates (48 native executions), +27 broader interface, 10 erased-owner and nine actor native regression tests. +Fresh current compiler/source/binary/dep-info guards also accept all sixteen +private Resource gates: 754 Source-deleted executions and 900 entry completions. +Only the unchanged, independently measured v4 Debug/Release ABI1/wire3 static-CRT +runtime archives are reused, with compiled receipt pin +`bc2ad54743380441f074d8d5d212048ecf440669817c0cb69ce2fd0810ba1c95`. +The fresh post-change gate and complete source guard supply current acceptance; +old v4 execution records do not. Each Rust gate checks its existing exact +completion, script/event, message and cleanup oracles before one teardown. + +This is bounded local MSVC integration acceptance. Full coherent-workspace and +native conformance gates, exact-revision supported-host CI, GNU and installed +distribution remain pending. New nominal/secret proof establishment, protected +secret observation, broader generic/reflected forms and authenticated generated +verify/property-suite transitions remain full-goal obligations. No Source +exclusion or broad language-completion claim follows: the rough 87% estimate, +fixed 207-fixture/182-object inventory and full 100% goal remain unchanged. diff --git a/docs/open_design/actor_equality.md b/docs/open_design/actor_equality.md index e4459f71..2001c433 100644 --- a/docs/open_design/actor_equality.md +++ b/docs/open_design/actor_equality.md @@ -1,10 +1,14 @@ # Actor handle equality -The checker currently accepts `first == first` for actor handles, but the -interpreter rejects it at runtime as an unsupported binary operation. An enum -whose payload is an actor handle is also accepted; the interpreter's recursive -enum equality compares the actor IDs, while native aggregate enum equality -rejects that payload type. These paths need one language rule before native +Decision status: live instance identity implemented in the reference evaluator and native backend; local Debug/Release native acceptance passes. See [the active selected contract](../active/actor_identity_equality.md). The observations and alternatives below record the prior unresolved state. + +## Historical divergence and alternatives + +Before this implementation, the checker accepted `first == first` for actor handles, but the +interpreter rejected it at runtime as an unsupported binary operation. An enum +whose payload is an actor handle was also accepted; the interpreter's recursive +enum equality compared the actor IDs, while native aggregate enum equality +rejected that payload type. These paths needed one language rule before native codegen can claim parity. Two coherent choices are: @@ -20,3 +24,7 @@ Two coherent choices are: The decision should preserve user-defined struct equality's separate exact `Equatable.equals` rule. Differential tests should cover the same cloned actor handle, distinct spawned actors, nested enum and list payloads, and `!=`. + +## Selected contract + +Actor equality now selects live instance identity. Exact checked nominal actor types must match; runtime IDs must belong to the owning context. Cloning preserves identity, distinct spawns remain unequal even with equal state, and != negates the same comparison. State is never structurally compared. See [the selected implementation note](../active/actor_identity_equality.md). Escaping comptime actor graph import remains a separate contract. diff --git a/docs/open_design/global_constant_execution.md b/docs/open_design/global_constant_execution.md index a3c10744..d06bfa49 100644 --- a/docs/open_design/global_constant_execution.md +++ b/docs/open_design/global_constant_execution.md @@ -62,6 +62,8 @@ not a decision to narrow the language's constant contract permanently. Local values and supported explicit `comptime` results outside namespace declarations keep their existing behavior. -Full native acceptance still requires closing this ownership policy and checking -the final revision against the independent inventory, workspace, and distribution -gates. The primitive handoff alone does not establish complete native parity. +Extending namespace constants beyond the current supported domain requires +selecting this ownership policy first. Current native acceptance validates the +admitted baked values and conservative frontend rejections, together with the +final revision's independent inventory, workspace and distribution gates. The +primitive handoff alone does not establish complete native parity. diff --git a/docs/open_design/interface_same_owner_arguments.md b/docs/open_design/interface_same_owner_arguments.md index dfdd2531..1e68cbc1 100644 --- a/docs/open_design/interface_same_owner_arguments.md +++ b/docs/open_design/interface_same_owner_arguments.md @@ -18,7 +18,7 @@ prints `left:one:two`, despite the second argument's nominal type. Native code checks the concrete owner when unboxing and fails with the internal diagnostic `invalid native struct handle or field`. -The remaining language decision is whether to: +Before selection, the language tradeoff was whether to: - Permit erased calls and validate every concrete argument requirement at runtime, reporting a typed dispatch error before entering the implementation; @@ -31,3 +31,33 @@ remove native unboxing checks, or add structural compatibility. The chosen rule must cover view and owned parameters, nested occurrences of the owner type, pending arguments, matching owners, and consistent cleanup/error precedence. This is independent of the decision about equality operators on interfaces. + +## Selected runtime contract: exact concrete arguments + +The native-codegen goal selects runtime validation rather than banning an erased +call whose static interface signature contains another occurrence of the owner. +Selection was recorded before implementation on 2026-10-08. Source accepted by +the checker remains accepted; an erased interface is not a proof that two values +have the same nominal implementation owner. + +Resolve the receiver's exact checked concrete type and selected method first. +Before converting any argument to that concrete signature or entering the method +body, validate each concrete argument requirement against the argument's retained +checked concrete identity. Different nominal structs remain different even when +their fields have identical names and representations. This also applies to +checked generic arguments, refinements, primitive widths, and nested occurrences +of the owner in parameter types. Matching owners retain ordinary view semantics; +validation grants neither cloning nor ownership transfer. + +A mismatch fails dispatch before the implementation body. It follows existing +runtime failure propagation and argument cleanup, with no user-defined body +execution. Pending receiver/argument failures retain their existing evaluation +and error precedence; validation must not inspect an unavailable payload or +replace an earlier failure. Native unboxing and handle checks remain mandatory. +This decision does not define equality operators on erased interfaces. + +The reference/native implementation and local MSVC Debug/Release gates are +recorded in [exact concrete argument verification](../active/native_erased_same_owner_arguments.md). +That bounded acceptance includes optimized production matching/mismatched calls; +full refinement/pending Source, GNU, installed and coherent native parity remain +open. No recursive Self substitution or interface equality rule is introduced. \ No newline at end of file diff --git a/docs/open_design/reflected_field_request_compatibility.md b/docs/open_design/reflected_field_request_compatibility.md index 888f8627..e4c76e70 100644 --- a/docs/open_design/reflected_field_request_compatibility.md +++ b/docs/open_design/reflected_field_request_compatibility.md @@ -86,14 +86,62 @@ establishment without rendering the value, while retaining exact qualified reads. Keep the hidden-observer policy in [debug_print_hidden_secrets.md](debug_print_hidden_secrets.md) open. -Changed callable results need a selected adapter contract; inspecting a -descriptor cannot prove its future result. Changed nominal generic instances -need a selected conversion contract; walking builtin wrappers does not validate -arbitrary nominal fields. Refuse proof establishment for those requests when -they introduce refinements. These remain explicit feature gaps, rather than -newly supported casts or a claim that reflected reads are fully covered. +Changed callable results now have the selected lazy return-validation adapter +contract below. A descriptor alone still cannot prove a future result. Changed +nominal generic instances need a separate selected conversion contract; walking +builtin wrappers does not validate arbitrary nominal fields. Their unsupported +proof establishment remains an explicit feature gap, not a claim of complete +reflected read coverage. The same observer concern applies to a successful summation fallback whose selected type is one or more secret qualifiers around a refinement. Refusing that unsupported proof does not define the still-open summation domain, add container-valued summation, or change existing arithmetic and shape errors. + +### Selected lazy reflected callable return validation + +A checked reflected callable request may establish supported new return +refinements through an owned lazy adapter. Exact owner, member, field ordinal, +pending-selector and declared-schema checks precede construction. The adapter +retains the original stored signature, source body, captures, debug identity +and pending state; constructing or reading it invokes neither callback nor +predicate. Requested parameter, view and capability contracts must satisfy the +existing ordinary callable adapter relation. A descriptor or requested type +annotation grants no proof, effect authority or nominal conversion. + +Each invocation evaluates its actuals once, invokes the retained source once +under the original signature, and preserves the source's ordinary validation. +Only after the source result exists does the supported typed produced-refinement +conversion establish the unproved requested return suffix. Failed validation +uses the existing terminal refinement-boundary error and publishes no trusted +target result. Exact established prefixes are reused. The adapter never joins +implicitly or inspects pending payload; a pending result requiring a new check +reaches the existing pending-inspection refusal before predicates or consumers, +while exact already-proved pending transport remains unchanged. + +The selected contract now has an isolated checked/reference/HIR/MIR/native +prototype. Eighteen genuine Sources include the two byte-exact original wider +and exact callback probes, named and captured callbacks, lazy reads, repeated +calls, invalid results, selector precedence, recursive optional results and +ready, pending, exact and ancestor Nothing/refined-unit returns. All eighteen +reference entries and host objects pass; all thirty-six Source-deleted linked +executions pass against freshly built normal MSVC Debug/Release Main archives. +Source checking uses release=false independently of runtime artifact profile. +The native assertions compare exact stdout, stderr and exit status. The public +ordinary launcher does not expose context-retirement counters. + +Current isolated authentication gates also pass thirteen reference, five HIR +and six MIR tests. Original closure body and typed capture/context witnesses +are constructed at the trusted capture site. Admission and invocation compare +against those retained witnesses; mutating a same-span body, checked context, +signature, field ordinal, Source owner or predicate cannot mint a replacement +proof. Ordinary required constants and refinements retain their original +checking and materialization path; ancestor return transport uses the already +proved type relation without a new pending inspection or predicate. + +This is target-only acceptance of the exact saved candidate. Managed +integration, broad workspace and successor CI checks remain pending. New +nominal/secret proof establishment, protected secret observation, generic and +other reflected forms, and authenticated generated verify/property-suite +transitions remain full-goal obligations. No Source exclusion, fixture +denominator change or broad language-completion claim follows from this gate. diff --git a/tests/native/actor_direct_identity.jett b/tests/native/actor_direct_identity.jett new file mode 100644 index 00000000..1c7f4cae --- /dev/null +++ b/tests/native/actor_direct_identity.jett @@ -0,0 +1,18 @@ +namespace app +actor Counter: + mutable int64 count = 1 + receive add(amount: int64): + count = count + amount + receive read responds int64: + respond count +type CounterAlias = Counter +function same(view first: Counter, view second: Counter) returns bool: + return first == second +function main() returns nothing: + Counter worker = spawn Counter() + CounterAlias alias = clone worker + Counter distinct = spawn Counter() + println(worker == alias, worker != alias, worker == distinct, worker != distinct) + send worker.add(9) + println(same(view worker, view alias), same(view worker, view distinct)) + return nothing diff --git a/tests/native/actor_enum_equality.jett b/tests/native/actor_enum_equality.jett new file mode 100644 index 00000000..dc67dcb5 --- /dev/null +++ b/tests/native/actor_enum_equality.jett @@ -0,0 +1,13 @@ +namespace app +actor Counter: + mutable int64 count = 1 + receive read responds int64: + respond count +enum Holder: + held(worker: Counter) +function main() returns nothing: + Counter worker = spawn Counter() + Holder first = Holder.held(clone worker) + Holder second = Holder.held(clone worker) + println(first == second, first != second) + return nothing diff --git a/tests/native/actor_nested_identity.jett b/tests/native/actor_nested_identity.jett new file mode 100644 index 00000000..697a1036 --- /dev/null +++ b/tests/native/actor_nested_identity.jett @@ -0,0 +1,36 @@ +namespace app +actor Counter: + mutable int64 count = 1 + receive read responds int64: + respond count +enum Leaf: + held(worker: Counter) + vacant +enum Tree: + leaf(worker: Counter) + children(values: list[Tree]) +enum Bundle: + grouped(values: list[Leaf], worker: optional[Counter]) +struct Keyed: + key: int64 + worker: Counter +implement Equatable for Keyed: + function equals(view self: Keyed, view other: Keyed) returns bool: + return self.key == other.key +enum Custom: + keyed(value: Keyed) +function main() returns nothing: + Counter worker = spawn Counter() + Counter other = spawn Counter() + Bundle first = Bundle.grouped(list(Leaf.held(clone worker)), some(clone worker)) + Bundle alias = clone first + Bundle different = Bundle.grouped(list(Leaf.held(clone other)), some(clone other)) + println(first == alias, first != alias, first == different, first != different) + Tree original = Tree.children(list(Tree.leaf(clone worker))) + Tree repeated = Tree.children(list(Tree.leaf(clone worker))) + Tree distinct = Tree.children(list(Tree.leaf(clone other))) + println(original == repeated, original != distinct) + Custom left = Custom.keyed(Keyed(key: 7, worker: clone worker)) + Custom right = Custom.keyed(Keyed(key: 7, worker: clone other)) + println(left == right, left != right) + return nothing diff --git a/tests/native/actor_optimized_identity.jett b/tests/native/actor_optimized_identity.jett new file mode 100644 index 00000000..f13d1383 --- /dev/null +++ b/tests/native/actor_optimized_identity.jett @@ -0,0 +1,46 @@ +namespace app +actor Counter: + mutable int64 count = 1 + receive add(amount: int64): + count = count + amount + receive read responds int64: + respond count +enum Holder: + held(worker: Counter) +enum Bundle: + grouped(values: list[Holder], worker: optional[Counter]) +struct Keyed: + key: int64 + worker: Counter +implement Equatable for Keyed: + function equals(view self: Keyed, view other: Keyed) returns bool: + return self.key == other.key +enum Custom: + keyed(value: Keyed) +function main(stdout: Stdout) returns nothing: + Counter worker = spawn Counter() + Counter alias = clone worker + Counter other = spawn Counter() + Stdout.write(view stdout, "{worker == alias} {worker != alias} {worker == other} {worker != other}\n") + Bundle first = Bundle.grouped(list(Holder.held(clone worker)), some(clone worker)) + Bundle repeated = clone first + Bundle different = Bundle.grouped(list(Holder.held(clone other)), some(clone other)) + Stdout.write(view stdout, "{first == repeated} {first != repeated} {first == different} {first != different}\n") + Counter pending = run clone worker + Counter also_pending = run clone worker + Counter twice = run clone pending + Holder pending_first = Holder.held(clone pending) + Holder pending_repeated = Holder.held(clone also_pending) + Holder deeper = Holder.held(clone twice) + Holder ready = Holder.held(clone worker) + Stdout.write(view stdout, "{pending_first == pending_repeated} {pending_first != pending_repeated} {pending_first == deeper} {pending_first != deeper} {pending_first == ready} {pending_first != ready}\n") + Counter once = join twice handle error: + return Stdout.write(view stdout, "unexpected first join: {error}\n") + Counter joined = join once handle error: + return Stdout.write(view stdout, "unexpected second join: {error}\n") + send joined.add(9) + Stdout.write(view stdout, "{worker == joined} {worker != joined} {ask worker.read} {ask joined.read}\n") + Custom left = Custom.keyed(Keyed(key: 7, worker: clone worker)) + Custom right = Custom.keyed(Keyed(key: 7, worker: clone other)) + Stdout.write(view stdout, "{left == right} {left != right}\n") + return nothing diff --git a/tests/native/actor_pending_alias_baseline.jett b/tests/native/actor_pending_alias_baseline.jett new file mode 100644 index 00000000..be8f5d2d --- /dev/null +++ b/tests/native/actor_pending_alias_baseline.jett @@ -0,0 +1,13 @@ +namespace app +actor Counter: + mutable int64 count = 1 + receive read responds int64: + respond count +function main() returns nothing: + Counter worker = spawn Counter() + Counter pending = run clone worker + println(view worker, view pending) + Counter ready = join pending handle error: + return println(error) + println(view worker, view ready) + return nothing diff --git a/tests/native/actor_pending_alias_identity.jett b/tests/native/actor_pending_alias_identity.jett new file mode 100644 index 00000000..55ed9fb2 --- /dev/null +++ b/tests/native/actor_pending_alias_identity.jett @@ -0,0 +1,28 @@ +namespace app +actor Counter: + mutable int64 count = 1 + receive add(amount: int64): + count = count + amount + receive read responds int64: + respond count +enum Holder: + held(worker: Counter) +function main() returns nothing: + Counter worker = spawn Counter() + Counter alias = clone worker + Counter pending = run clone worker + Counter also_pending = run clone worker + Counter twice = run clone pending + println(worker == alias, worker != alias) + Holder first = Holder.held(clone pending) + Holder repeated = Holder.held(clone also_pending) + Holder deeper = Holder.held(clone twice) + Holder ready = Holder.held(clone worker) + println(first == repeated, first != repeated, first == deeper, first != deeper, first == ready, first != ready) + Counter once = join twice handle error: + return println(error) + Counter joined = join once handle error: + return println(error) + send joined.add(9) + println(worker == joined, worker != joined, ask worker.read, ask joined.read) + return nothing diff --git a/tests/native/actor_pending_identity.jett b/tests/native/actor_pending_identity.jett new file mode 100644 index 00000000..0efb3926 --- /dev/null +++ b/tests/native/actor_pending_identity.jett @@ -0,0 +1,10 @@ +namespace app +actor Counter: + mutable int64 count = 1 + receive read responds int64: + respond count +function main() returns nothing: + Counter worker = spawn Counter() + Counter pending = run clone worker + println(pending == pending) + return nothing diff --git a/tests/native/erased_same_owner/05_erased_other_owner.jett b/tests/native/erased_same_owner/05_erased_other_owner.jett new file mode 100644 index 00000000..e6cad1bf --- /dev/null +++ b/tests/native/erased_same_owner/05_erased_other_owner.jett @@ -0,0 +1,18 @@ +namespace app +interface Pairable: + function combine(view self: Pairable, view other: Pairable) returns string +struct Left: + name: string +struct Right: + name: string +implement Pairable for Left: + function combine(view self: Left, view other: Left) returns string: + return "left:{self.name}:{other.name}" +implement Pairable for Right: + function combine(view self: Right, view other: Right) returns string: + return "right:{self.name}:{other.name}" +function main() returns nothing: + Pairable first = Left(name: "one") + Pairable second = Right(name: "two") + println(Pairable.combine(view first, view second)) + return nothing diff --git a/tests/native/erased_same_owner/12_erased_matching_owner.jett b/tests/native/erased_same_owner/12_erased_matching_owner.jett new file mode 100644 index 00000000..4d461004 --- /dev/null +++ b/tests/native/erased_same_owner/12_erased_matching_owner.jett @@ -0,0 +1,13 @@ +namespace app +interface Pairable: + function combine(view self: Pairable, view other: Pairable) returns string +struct Left: + name: string +implement Pairable for Left: + function combine(view self: Left, view other: Left) returns string: + return "left:{self.name}:{other.name}" +function main() returns nothing: + Pairable first = Left(name: "one") + Pairable second = Left(name: "two") + println(Pairable.combine(view first, view second)) + return nothing diff --git a/tests/native/reflected_iteration_controls/arguments_nested.jett b/tests/native/reflected_iteration_controls/arguments_nested.jett new file mode 100644 index 00000000..1c286a60 --- /dev/null +++ b/tests/native/reflected_iteration_controls/arguments_nested.jett @@ -0,0 +1,55 @@ +namespace app +struct Inner: + first: int64 + second: int64 +struct Outer: + left: Inner + right: Inner +function first[T]() returns string: + return "first:{type.name[T]()}" +function second[T]() returns string: + return "second:{type.name[T]()}" +function is_first(index: int64) returns bool: + return index == 0 +function nested_label[T](name: string, index: int64) returns string: + if is_first(index): + return "{name}:{first[T]()}" + return "{name}:{second[T]()}" +function arguments(high: bool) returns string: + mutable list[string] parts = list.new[string]() + mutable int64 index = 0 + if high: + index = 1 + for arg in type.info[map[int64, int64]]().args: + comptime type Arg = arg: + if is_first(index): + parts = list.append[string](parts, first[Arg]()) + else: + parts = list.append[string](parts, second[Arg]()) + index = index + 1 + return string.join(parts, ";") +function nested_fields() returns string: + mutable list[string] parts = list.new[string]() + for outer in type.fields[Outer](): + comptime type Field = outer.type_info: + for inner in type.fields[Field](): + comptime type Item = inner.type_info: + string name = "{outer.name}.{inner.name}" + parts = list.append[string](parts, nested_label[Item](name, inner.index)) + return string.join(parts, ";") +function report(high: bool) returns string: + return "{arguments(high)}|{nested_fields()}" +function expected(high: bool) returns string: + string nested = "left.first:first:int64;left.second:second:int64;right.first:first:int64;right.second:second:int64" + if high: + return "second:int64;second:int64|{nested}" + return "first:int64;second:int64|{nested}" +function main(stdout: Stdout) returns nothing: + string baked = comptime report(false) + Stdout.write(view stdout, "{report(false)}\n{baked}\n") +verify ordinary_reflection_arguments_nested: + assert report(false) == expected(false) + assert report(true) == expected(true) +property ordinary_reflection_arguments_nested_trials: + given high: bool + assert report(high) == expected(high) diff --git a/tests/native/reflected_iteration_controls/arguments_nested.stdout b/tests/native/reflected_iteration_controls/arguments_nested.stdout new file mode 100644 index 00000000..16dd4c56 --- /dev/null +++ b/tests/native/reflected_iteration_controls/arguments_nested.stdout @@ -0,0 +1,2 @@ +first:int64;second:int64|left.first:first:int64;left.second:second:int64;right.first:first:int64;right.second:second:int64 +first:int64;second:int64|left.first:first:int64;left.second:second:int64;right.first:first:int64;right.second:second:int64 diff --git a/tests/native/reflected_iteration_controls/members.jett b/tests/native/reflected_iteration_controls/members.jett new file mode 100644 index 00000000..df0d827a --- /dev/null +++ b/tests/native/reflected_iteration_controls/members.jett @@ -0,0 +1,85 @@ +namespace app +bitfield Header: + left: 4 bits + right: 4 bits +enum Event: + empty + first(left: int64, right: int64) + second(left: int64, right: int64) +machine Session: + states: + empty + first(left: int64, right: int64) + second(left: int64, right: int64) + transitions: + empty to first + first to second +function first(value: int64) returns int64: + return value + 10 +function second(value: int64) returns int64: + return value + 100 +function is_right(view field: TypeField) returns bool: + return field.name == "right" +function bitfield_report(high: bool) returns string: + mutable int64 left = 2 + mutable int64 right = 3 + if high: + left = 5 + right = 7 + Header source = Header(left: left, right: right) handle error: + return error + mutable list[string] parts = list.new[string]() + for field in type.fields[Header](): + comptime type Field = field.type_info: + Field value = type.field_value[Header, Field](view source, view field) + if field.index == 0: + parts = list.append[string](parts, "{field.name}:{first(value)}") + else: + parts = list.append[string](parts, "{field.name}:{second(value)}") + return string.join(parts, ";") +function variant_report(seed: int64) returns string: + mutable list[string] parts = list.new[string]() + for variant in type.variants[Event](): + string member = variant.name + mutable int64 total = 0 + for field in variant.fields: + comptime type Field = field.type_info: + Field value = seed + if field.index == 0: + total = total + first(value) + else: + total = total + second(value) + parts = list.append[string](parts, "{member}:{total}") + return string.join(parts, ";") +function machine_report(seed: int64) returns string: + mutable list[string] parts = list.new[string]() + for state in type.machine_states[Session](): + string member = state.name + mutable int64 total = 0 + for field in state.fields: + comptime type Field = field.type_info: + Field value = seed + if is_right(view field): + total = total + second(value) + else: + total = total + first(value) + parts = list.append[string](parts, "{member}:{total}") + return string.join(parts, ";") +function report(high: bool) returns string: + mutable int64 seed = 17 + if high: + seed = 23 + return "{bitfield_report(high)}|{variant_report(seed)}|{machine_report(seed)}" +function expected(high: bool) returns string: + if high: + return "left:15;right:107|empty:0;first:156;second:156|empty:0;first:156;second:156" + return "left:12;right:103|empty:0;first:144;second:144|empty:0;first:144;second:144" +function main(stdout: Stdout) returns nothing: + string baked = comptime report(false) + Stdout.write(view stdout, "{report(false)}\n{baked}\n") +verify ordinary_reflection_members: + assert report(false) == expected(false) + assert report(true) == expected(true) +property ordinary_reflection_member_trials: + given high: bool + assert report(high) == expected(high) diff --git a/tests/native/reflected_iteration_controls/members.stdout b/tests/native/reflected_iteration_controls/members.stdout new file mode 100644 index 00000000..c86a8be1 --- /dev/null +++ b/tests/native/reflected_iteration_controls/members.stdout @@ -0,0 +1,2 @@ +left:12;right:103|empty:0;first:144;second:144|empty:0;first:144;second:144 +left:12;right:103|empty:0;first:144;second:144|empty:0;first:144;second:144 diff --git a/tests/native/reflected_iteration_controls/nominal.jett b/tests/native/reflected_iteration_controls/nominal.jett new file mode 100644 index 00000000..719d8545 --- /dev/null +++ b/tests/native/reflected_iteration_controls/nominal.jett @@ -0,0 +1,81 @@ +namespace app +struct Record: + left: int64 + right: int64 +struct PairOf[T]: + left: T + right: T +type Slot = int64 +struct AliasPair: + left: Slot + right: Slot +function first(value: int64) returns int64: + return value + 10 +function second(value: int64) returns int64: + return value + 100 +function is_right(view field: TypeField) returns bool: + return field.name == "right" +function typed_label[T](name: string) returns string: + return "{name}:{type.name[T]()}" +function by_index(view source: Record) returns string: + mutable list[string] parts = list.new[string]() + for field in type.fields[Record](): + comptime type Field = field.type_info: + Field value = type.field_value[Record, Field](view source, view field) + if field.index == 0: + parts = list.append[string](parts, "{field.name}:{first(value)}") + else: + parts = list.append[string](parts, "{field.name}:{second(value)}") + return string.join(parts, ";") +function by_name(view source: Record) returns string: + mutable list[string] parts = list.new[string]() + for field in type.fields[Record](): + comptime type Field = field.type_info: + Field value = type.field_value[Record, Field](view source, view field) + if is_right(view field): + parts = list.append[string](parts, "{field.name}:{first(value)}") + else: + parts = list.append[string](parts, "{field.name}:{second(value)}") + return string.join(parts, ";") +function generic_by_index[T](view source: T) returns string: + mutable list[string] parts = list.new[string]() + for field in type.fields[T](): + comptime type Field = field.type_info: + Field value = type.field_value[T, Field](view source, view field) + if field.index == 0: + parts = list.append[string](parts, "{field.name}:{first(value)}") + else: + parts = list.append[string](parts, "{field.name}:{second(value)}") + return string.join(parts, ";") +function alias_labels() returns string: + mutable list[string] parts = list.new[string]() + for field in type.fields[AliasPair](): + comptime type Field = field.type_info: + parts = list.append[string](parts, typed_label[Field](field.name)) + return string.join(parts, ";") +function report(high: bool) returns string: + mutable int64 left = 2 + mutable int64 right = 3 + if high: + left = 5 + right = 7 + Record source = Record(left: left, right: right) + PairOf[int64] generic_source = PairOf[int64](left: left, right: right) + string indexed = by_index(view source) + string named = by_name(view source) + string generic_record = generic_by_index[Record](view source) + string generic_owner = generic_by_index[PairOf[int64]](view generic_source) + return "{indexed}|{named}|{generic_record}|{generic_owner}|{alias_labels()}" +function expected(high: bool) returns string: + if high: + return "left:15;right:107|left:105;right:17|left:15;right:107|left:15;right:107|left:app.Slot;right:app.Slot" + return "left:12;right:103|left:102;right:13|left:12;right:103|left:12;right:103|left:app.Slot;right:app.Slot" +function main(stdout: Stdout) returns nothing: + string baked = comptime report(false) + Stdout.write(view stdout, "{report(false)}\n{baked}\n") +verify ordinary_reflection_nominal: + assert report(false) == expected(false) + assert report(true) == expected(true) +property ordinary_reflection_nominal_trials: + given high: bool + assert report(high) == expected(high) diff --git a/tests/native/reflected_iteration_controls/nominal.stdout b/tests/native/reflected_iteration_controls/nominal.stdout new file mode 100644 index 00000000..5aab8ad5 --- /dev/null +++ b/tests/native/reflected_iteration_controls/nominal.stdout @@ -0,0 +1,2 @@ +left:12;right:103|left:102;right:13|left:12;right:103|left:12;right:103|left:app.Slot;right:app.Slot +left:12;right:103|left:102;right:13|left:12;right:103|left:12;right:103|left:app.Slot;right:app.Slot diff --git a/tools/measure_resource_native_archives.py b/tools/measure_resource_native_archives.py new file mode 100644 index 00000000..14cdb434 --- /dev/null +++ b/tools/measure_resource_native_archives.py @@ -0,0 +1,1236 @@ +#!/usr/bin/env python3 +"""Measure fresh private Resource archives, then run their pinned Source gate. + +These archives are compiler-test artifacts, never production launcher packages. +Preparation and gate execution are separate commands and separate processes. +""" +from __future__ import annotations + +import argparse +from collections import Counter +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +import hashlib +import json +import os +from pathlib import Path +import re +import shlex +import shutil +import subprocess +from typing import TypeAlias, TypeGuard + + +ROOT = Path(__file__).resolve().parents[1] +TARGET = "x86_64-unknown-linux-gnu" +MSVC_TARGET = "x86_64-pc-windows-msvc" +MSVC_RUSTFLAGS = ("-C", "target-feature=+crt-static", "-C", "debuginfo=0") +MSVC_FLAGS_ENV = "CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS" +# Raw-dylib import members observed in the supported MSVC standard runtime. +# New DLLs require an independent observation before extending this closed set. +MSVC_IMPORT_DLLS = frozenset({ + "advapi32.dll", "api-ms-win-core-handle-l1-1-0.dll", "api-ms-win-core-synch-l1-2-0.dll", + "bcp47mrm.dll", "bcryptprimitives.dll", "elscore.dll", "gdi32.dll", "icu.dll", + "icuin.dll", "icuuc.dll", "kernel32.dll", "normaliz.dll", "ntdll.dll", + "oleaut32.dll", "user32.dll", "usp10.dll", +}) +OCCUPIED_STRUCT_TESTS = ( + "resource_execution::occupied_struct_harness::" + "native_resource_occupied_struct_scripts_retire_before_teardown", + "resource_execution::occupied_struct_harness::" + "native_resource_occupied_struct_scripts_reenter_with_same_provider_grant", +) +CURRENT_LAYOUT_WIRE = 3 +CARRIER_EXPORTS = frozenset( + "jett_rt_v1_resource_carrier_" + suffix for suffix in ( + "construct_begin", "child", "commit", "transfer", "qualify", "borrow", "observe", + "extract", "adapt", "end", "retire", "reset_iteration", "publish", + ) +) +CFGS = ["test", "jett_resource_native_test_archive"] +STARTUP_SIGNATURES: dict[str, tuple[str, ...]] = { + "jett_rt_v1_resource_program_begin": ( + "*const JettRuntimeContextV1", "*const ObjectManifest", "*mut u64", + "*mut JettRuntimeResultV1", + ), + "jett_rt_v1_resource_program_complete": ( + "*const JettRuntimeContextV1", "u64", "u32", + ), + "jett_rt_v1_resource_program_unwind": ( + "*const JettRuntimeContextV1", "u32", "u32", "*mut u32", + ), + "jett_rt_v1_resource_program_failure": ( + "*const JettRuntimeContextV1", "*mut JettRuntimeResultV1", + ), + "jett_rt_v1_resource_program_failure_copy": ( + "*const JettRuntimeContextV1", "*mut u8", "u64", "*mut u64", + "*mut JettRuntimeResultV1", + ), +} +STARTUP_EXPORTS = frozenset(STARTUP_SIGNATURES) +PIN_ENV = "JETT_RESOURCE_NATIVE_TEST_ARCHIVE_EXPECTED_SHA256_V1" +RECEIPT_ENV = "JETT_RESOURCE_NATIVE_TEST_ARCHIVE_RECEIPT_V1" +TEST = ("resource_execution::" + "native_resource_original_source_lifecycle_matches_reference_and_retires_before_teardown") +Json: TypeAlias = str | int | bool | None | list["Json"] | dict[str, "Json"] + + +class MeasurementError(RuntimeError): + """A build or independent observation cannot support an acceptance receipt.""" + + +@dataclass(frozen=True) +class Tools: + cargo: Path + rustc: Path + nm: Path + readelf: Path + cc: Path + + +@dataclass(frozen=True) +class MsvcTools: + cargo: Path + rustc: Path + nm: Path + readobj: Path + linker: Path + + +def windows_arguments(command: str) -> list[str]: + """Decode the Windows CRT argv quoting used by Cargo's verbose command log.""" + arguments: list[str] = [] + index = 0 + while index < len(command): + while index < len(command) and command[index] in " \t": + index += 1 + if index == len(command): + break + value: list[str] = [] + quoted = False + while index < len(command) and (quoted or command[index] not in " \t"): + slashes = 0 + while index < len(command) and command[index] == "\\": + slashes += 1 + index += 1 + if index < len(command) and command[index] == '"': + value.extend("\\" * (slashes // 2)) + if slashes % 2: + value.append('"') + elif quoted and index + 1 < len(command) and command[index + 1] == '"': + value.append('"') + index += 1 + else: + quoted = not quoted + index += 1 + else: + value.extend("\\" * slashes) + if index < len(command) and (quoted or command[index] not in " \t"): + value.append(command[index]) + index += 1 + require(not quoted, "unterminated Windows command quoting") + arguments.append("".join(value)) + return arguments + + +def option_values(arguments: Sequence[str], option: str) -> list[str]: + values: list[str] = [] + for index, argument in enumerate(arguments): + if argument == option: + require(index + 1 < len(arguments), f"missing actual argument for {option}") + values.append(arguments[index + 1]) + elif argument.startswith(option + "="): + values.append(argument[len(option) + 1:]) + elif option == "-C" and argument.startswith("-C") and len(argument) > 2: + values.append(argument[2:]) + return values + + +def msvc_compiler_commands(text: str, compiler: Path) -> list[list[str]]: + commands: list[list[str]] = [] + for line in text.splitlines(): + line = line.strip() + if not line.startswith("Running ") or "--crate-name" not in line: + continue + command = line.removeprefix("Running ") + require(command.startswith("`") and command.endswith("`"), + "actual Windows Cargo command delimiters are missing") + arguments = windows_arguments(command[1:-1]) + require(bool(arguments) and Path(arguments[0]).absolute() == compiler.absolute(), + "Cargo used an unmeasured Windows compiler or compiler wrapper") + commands.append(arguments) + require(bool(commands), "actual Windows compiler invocation is missing") + return commands + + +def checked_msvc_flags(arguments: Sequence[str]) -> None: + codes = option_values(arguments, "-C") + features = [value for value in codes if value.startswith("target-feature=")] + debug = [value for value in codes if value.startswith("debuginfo=")] + require(features == ["target-feature=+crt-static"] and bool(debug) + and all(value == "debuginfo=0" for value in debug), + "actual MSVC runtime command must use static CRT and zero debug information") + require(not any(value.startswith("incremental=") for value in codes), + "incremental private archive compilation is forbidden") + + +def msvc_selected_cfgs(text: str, compiler: Path) -> None: + commands = msvc_compiler_commands(text, compiler) + selected = [arguments for arguments in commands + if any(value in CFGS for value in option_values(arguments, "--cfg"))] + require(len(selected) == 1, "private cfgs must occur only in one selected Windows runtime command") + arguments = selected[0] + require(option_values(arguments, "--crate-name") == ["jett_runtime"], + "private cfg escaped selected runtime package") + cfgs = option_values(arguments, "--cfg") + require([value for value in cfgs if value in CFGS] == CFGS, + "both private cfgs must occur exactly once in the selected runtime command") + require("staticlib" in option_values(arguments, "--crate-type") and "--test" not in arguments, + "private runtime must be a staticlib without the Rust test harness") + require(option_values(arguments, "--target") == [MSVC_TARGET], + "selected runtime command has the wrong MSVC target") + require(option_values(arguments, "--print") == ["native-static-libs"], + "selected runtime must actually print its native library vector") + checked_msvc_flags(arguments) + + +def msvc_library_name(argument: str) -> str: + # Decode only the two observed safe linker forms; retain the raw argv elsewhere. + if argument.lower().startswith("/defaultlib:"): + name = argument[len("/defaultlib:"):] + require(re.fullmatch(r"[A-Za-z0-9_][A-Za-z0-9_.+-]*\.lib|[A-Za-z0-9_][A-Za-z0-9_+-]*", + name, re.IGNORECASE) is not None, + f"unsafe MSVC default library argument: {argument}") + return name if name.lower().endswith(".lib") else name + ".lib" + require(re.fullmatch(r"[A-Za-z0-9_][A-Za-z0-9_.+-]*\.lib", argument) is not None, + f"unexpected MSVC native library argument: {argument}") + return argument + + +def checked_msvc_library(argument: str) -> None: + lower = msvc_library_name(argument).lower() + require("jett_runtime" not in lower and "jett_native_launcher" not in lower, + "a second Rust runtime archive is forbidden") + require(lower not in {"msvcrt.lib", "msvcrtd.lib", "ucrt.lib", "ucrtd.lib", + "vcruntime.lib", "vcruntimed.lib", "libcmtd.lib", + "libucrtd.lib", "libvcruntimed.lib"}, + f"dynamic or debug CRT library observed: {argument}") + + +def checked_msvc_libraries(arguments: Sequence[str]) -> None: + require(bool(arguments), "native-static-libs vector must not be empty") + for argument in arguments: + checked_msvc_library(argument) + require("libcmt.lib" in {msvc_library_name(value).lower() for value in arguments}, + "MSVC static CRT libcmt.lib was not observed") + + +def msvc_native_libraries(text: str) -> list[str]: + lines = text.splitlines() + indices = [index for index, line in enumerate(lines) if "note: native-static-libs:" in line] + require(len(indices) == 1, "exactly one actual native-static-libs observation is required") + index = indices[0] + parts = [lines[index].split("note: native-static-libs:", 1)[1]] + for line in lines[index + 1:]: + if re.match(r"\s+Finished\s", line): + break + if not line.strip() or not line[0].isspace(): + break + # A wrapped vector may contain /defaultlib tokens, never response files or paths. + continuation = windows_arguments(line.strip()) + require(bool(continuation), "empty native library continuation") + for argument in continuation: + checked_msvc_library(argument) + parts.append(line.strip()) + arguments = windows_arguments(" ".join(parts)) + checked_msvc_libraries(arguments) + return arguments + + +def checked_msvc_environment(environment: Mapping[str, str], linker: Path) -> None: + require(bool(environment.get("VCINSTALLDIR")), "MSVC measurement requires an inherited Developer Shell") + require(environment.get("VSCMD_ARG_TGT_ARCH") == "x64", "Developer Shell target architecture must be x64") + require(bool(environment.get("LIB")), "Developer Shell SDK LIB environment is missing") + require(bool(environment.get("PATH")), "Developer Shell PATH environment is missing") + first = shutil.which("link.exe", path=environment.get("PATH")) + require(first is not None and Path(first).absolute() == linker.absolute(), + "measured linker must be the first link.exe in the inherited Developer Shell PATH") + override = environment.get("CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER") + require(not override or Path(override).absolute() == linker.absolute(), + "target linker override differs from measured link.exe") + + +def msvc_build_environment() -> dict[str, str]: + environment = os.environ.copy() + for name in ("RUSTFLAGS", "CARGO_ENCODED_RUSTFLAGS", MSVC_FLAGS_ENV): + require(not environment.get(name, "").strip(), f"MSVC measurement requires empty incoming {name}") + for name in ("RUSTC", "CARGO_BUILD_RUSTC", "RUSTC_WRAPPER", "RUSTC_WORKSPACE_WRAPPER", + "CARGO_BUILD_RUSTC_WRAPPER", "CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER"): + require(not environment.get(name), f"unmeasured compiler wrapper: {name}") + environment.pop(PIN_ENV, None) + environment.pop(RECEIPT_ENV, None) + environment["CARGO_TERM_COLOR"] = "never" + environment[MSVC_FLAGS_ENV] = " ".join(MSVC_RUSTFLAGS) + environment["CARGO_INCREMENTAL"] = "0" + environment["CARGO_PROFILE_DEV_DEBUG"] = "0" + environment["CARGO_PROFILE_RELEASE_DEBUG"] = "0" + return environment + + +def msvc_fingerprint_records(directory: Path) -> list[Json]: + paths = sorted(directory.glob("jett_runtime-*/lib-jett_runtime.json")) + require(bool(paths), "runtime Cargo fingerprint is missing") + records: list[Json] = [] + for path in paths: + value = read_object(path) + require(value.get("rustflags") == list(MSVC_RUSTFLAGS), + "MSVC target fingerprint must contain the exact static CRT and zero-debug flags") + records.append({**artifact(path), "raw_json_utf8": path.read_text(encoding="utf-8")}) + return records + + +def checked_coff_member(text: str) -> list[str]: + files = re.findall(r"^File: (.+)$", text, re.MULTILINE) + formats = re.findall(r"^Format: (.+)$", text, re.MULTILINE) + require(len(files) == len(formats) == 1, "each COFF member needs one file and target format observation") + architectures = re.findall(r"^Arch: (.+)$", text, re.MULTILINE) + sizes = re.findall(r"^AddressSize: (.+)$", text, re.MULTILINE) + machines = re.findall(r"^\s+Machine: (.+)$", text, re.MULTILINE) + directives = re.findall(r"^Directive\(s\):[ \t]*(.*)$", text, re.MULTILINE) + if formats == ["COFF-x86-64"]: + require(architectures == ["x86_64"] and sizes == ["64bit"] + and machines == ["IMAGE_FILE_MACHINE_AMD64 (0x8664)"], + "every ordinary private archive member must have complete COFF AMD64 headers") + return directives + require(formats == ["COFF-import-file-x86-64"], "unknown or foreign private archive member format") + require(files[0].lower() in MSVC_IMPORT_DLLS, + f"private archive import DLL is outside the supported Windows system set: {files[0]}") + require(not architectures and not sizes and not machines and not directives, + "COFF import members must use their observed import-file metadata") + kinds = re.findall(r"^Type: (.+)$", text, re.MULTILINE) + names = re.findall(r"^Name type: (.+)$", text, re.MULTILINE) + symbols = re.findall(r"^Symbol: (.+)$", text, re.MULTILINE) + require(kinds == ["code"] + and names == ["name"] and bool(symbols), + "complete supported AMD64 import-file metadata is required") + return [] + + +def checked_coff_archive(text: str) -> None: + text = "\n".join(text.splitlines()) + blocks = re.split(r"(?=^File: )", text, flags=re.MULTILINE) + require(len(blocks) > 1 and not blocks[0].strip(), "complete COFF member observations are required") + directives: list[str] = [] + ordinary = 0 + for block in blocks[1:]: + directives.extend(checked_coff_member(block)) + ordinary += len(re.findall(r"^Format: COFF-x86-64$", block, re.MULTILINE)) + require(ordinary > 0, "private runtime must contain ordinary AMD64 COFF definitions") + # Rust staticlibs may expose no embedded directives; retain that absence. + libraries: list[str] = [] + for line in directives: + for argument in windows_arguments(line): + lower = argument.lower() + require(not lower.startswith(("/nodefaultlib", "-nodefaultlib")), + "COFF directives suppress measured CRT default libraries") + if lower.startswith(("/defaultlib:", "-defaultlib:")): + library = argument.split(":", 1)[1] + libraries.append(library if library.lower().endswith(".lib") else library + ".lib") + if "runtimelibrary=" in lower: + require(lower.split("runtimelibrary=", 1)[1] == "mt_staticrelease", + "COFF RuntimeLibrary directive must select the release static CRT") + for library in libraries: + checked_msvc_library(library) + +def require(condition: bool, message: str) -> None: + if not condition: + raise MeasurementError(message) + + +def digest(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def artifact(path: Path) -> dict[str, Json]: + require(path.is_file(), f"missing measured file: {path}") + return {"path": str(path.resolve()), "sha256": digest(path)} + + +def write_json(path: Path, value: Json) -> None: + with path.open("x", encoding="utf-8", newline="\n") as output: + output.write(json.dumps(value, indent=2, sort_keys=True) + "\n") + + +def json_array(value: object) -> TypeGuard[Sequence[object]]: + return isinstance(value, list) + + +def json_object(value: object) -> TypeGuard[Mapping[object, object]]: + return isinstance(value, dict) + + +def normalize_json(value: object) -> Json: + if value is None or isinstance(value, (str, bool, int)): + return value + if json_array(value): + return [normalize_json(item) for item in value] + if json_object(value): + output: dict[str, Json] = {} + for key, item in value.items(): + require(isinstance(key, str), "JSON object keys must be strings") + if isinstance(key, str): + output[key] = normalize_json(item) + return output + raise MeasurementError("unexpected JSON value in measured record") + + +def read_object(path: Path) -> dict[str, Json]: + raw: object = json.loads(path.read_text(encoding="utf-8")) + value = normalize_json(raw) + require(isinstance(value, dict), f"expected JSON object: {path}") + if isinstance(value, dict): + return value + raise MeasurementError(f"expected JSON object: {path}") + + +def executable(value: str) -> Path: + found = shutil.which(value) + require(found is not None, f"required executable is unavailable: {value}") + if found is None: + raise MeasurementError(f"required executable is unavailable: {value}") + # Rustup proxy dispatch depends on argv[0]; preserve cargo/rustc symlinks. + path = Path(found).absolute() + require(path.is_file(), f"executable must be a regular file: {path}") + return path + + +def logged(argv: list[str], directory: Path, environment: dict[str, str], + log: Path, timeout: int = 3600) -> str: + with log.open("xb") as output: + result = subprocess.run(argv, cwd=directory, env=environment, stdin=subprocess.DEVNULL, + stdout=output, stderr=subprocess.STDOUT, timeout=timeout) + text = log.read_text(encoding="utf-8", errors="replace") + require(result.returncode == 0, f"command exited {result.returncode}; see {log}") + return text + + +def sources(repo: Path, complete: bool = False) -> dict[str, Json]: + paths = {repo / "Cargo.toml", repo / "Cargo.lock", + repo / "crates/jett_runtime/Cargo.toml", + repo / "tools/measure_resource_native_archives.py"} + paths.update((repo / "crates/jett_runtime").rglob("*.rs")) + paths.update((repo / ".cargo").rglob("*.toml")) + if complete: + paths.add(repo / ".github/workflows/native.yml") + for suffix in ("*.rs", "*.jett", "Cargo.toml"): + paths.update((repo / "crates").rglob(suffix)) + paths.update((repo / "stdlib").rglob("*.jett")) + return {str(path.relative_to(repo)): digest(path) for path in sorted(paths)} + + +def build_environment() -> dict[str, str]: + environment = os.environ.copy() + for name in ("RUSTFLAGS", "CARGO_ENCODED_RUSTFLAGS", + "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS"): + require(not environment.get(name, "").strip(), + f"GNU dynamic-CRT measurement requires empty {name}") + for name in ("RUSTC", "CARGO_BUILD_RUSTC", "RUSTC_WRAPPER", "RUSTC_WORKSPACE_WRAPPER", + "CARGO_BUILD_RUSTC_WRAPPER", "CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER"): + require(not environment.get(name), f"unmeasured compiler wrapper: {name}") + # The selected Cargo command alone supplies the two private cfgs. + environment.pop(PIN_ENV, None) + environment.pop(RECEIPT_ENV, None) + # Cargo command identity and rustc's native-library note are parsed as text. + # Override user/config color preferences instead of discarding escape bytes. + environment["CARGO_TERM_COLOR"] = "never" + return environment + + +def native_libraries(text: str) -> list[str]: + lines = text.splitlines() + indices = [index for index, line in enumerate(lines) if "note: native-static-libs:" in line] + require(len(indices) == 1, "exactly one actual native-static-libs observation is required") + index = indices[0] + parts = [lines[index].split("note: native-static-libs:", 1)[1]] + for line in lines[index + 1:]: + if not re.fullmatch(r"\s+-\S.*", line): + break + parts.append(line.strip()) + arguments = shlex.split(" ".join(parts)) + require(bool(arguments), "native-static-libs vector must not be empty") + for argument in arguments: + lower = argument.lower() + require("jett_runtime" not in lower and "jett_native_launcher" not in lower, + "a second Rust runtime archive is forbidden") + require(argument.startswith("-l") and len(argument) > 2, + f"unexpected GNU native library argument: {argument}") + require("-lc" in arguments and "-lgcc_s" in arguments, + "GNU dynamic CRT libraries were not observed") + return arguments + + +def selected_cfgs(text: str, compiler: Path | None = None) -> None: + commands = [line.strip() for line in text.splitlines() if line.strip().startswith("Running ")] + private = [line for line in commands if "jett_resource_native_test_archive" in line] + require(len(private) == 1, "private cfg must occur in exactly one executed rustc command") + command = private[0] + require("--crate-name jett_runtime " in command, "private cfg escaped selected runtime package") + require("--cfg test " in command and "--cfg jett_resource_native_test_archive" in command, + "both selected-package cfgs must occur in the actual rustc command") + require("--crate-type staticlib" in command, "private runtime staticlib was not built") + if compiler is not None: + checked_compiler_commands(text, compiler) + + +def checked_compiler_commands(text: str, compiler: Path) -> None: + commands = [line.strip() for line in text.splitlines() + if line.strip().startswith("Running ") and "--crate-name " in line] + require(bool(commands), "actual compiler invocation is missing") + for command in commands: + arguments = shlex.split(command.removeprefix("Running ").strip("`")) + require(bool(arguments) and Path(arguments[0]).absolute() == compiler.absolute(), + "Cargo used an unmeasured compiler or compiler wrapper") + + +def startup_exports(directory: Path) -> set[str]: + owner, child = directory.with_suffix(".rs"), directory / "program_entry.rs" + require(owner.is_file() and child.is_file(), "Resource startup module sources are required") + owner_text = owner.read_text(encoding="utf-8") + module_pattern = r'(?m)^[ \t]*((?:#\[[^\n]*\][ \t]*\n[ \t]*)*)mod[ \t]+program_entry[ \t]*;' + modules = re.findall(module_pattern, owner_text) + require(len(re.findall(r"\bmod\s+program_entry\b", owner_text)) == 1 and modules == [""], + "Resource startup must have one unconditional canonical child module") + text = child.read_text(encoding="utf-8") + require("#![" not in text, "Resource startup child cannot carry conditional module attributes") + pattern = (r'(?m)^[ \t]*((?:#\[[^\n]*\][ \t]*\n[ \t]*)*)' + r'pub\s+unsafe\s+extern\s+"C"\s+fn\s+(jett_rt_v1_resource_[a-z_]+)' + r'\s*\(([^)]*)\)\s*->\s*([A-Za-z_][A-Za-z0-9_]*)\s*\{') + declarations = re.findall(pattern, text) + names = [name for _, name, _, _ in declarations] + require(len(names) == len(set(names)) == 5 and set(names) == STARTUP_EXPORTS, + "five exact Resource startup exports are required") + for attributes, name, parameters, result in declarations: + require(attributes.strip() == "#[unsafe(no_mangle)]", + f"Resource startup export must be unconditional and unmangled: {name}") + types: list[str] = [] + for parameter in parameters.split(","): + if not parameter.strip(): + continue + parts = parameter.split(":", 1) + require(len(parts) == 2, f"invalid Resource startup parameter: {name}") + types.append(" ".join(parts[1].split())) + require(tuple(types) == STARTUP_SIGNATURES[name] and result == "JettRuntimeStatusV1", + f"Resource startup C signature differs: {name}") + return set(names) + + +def export_names(repo: Path) -> tuple[set[str], set[str]]: + directory = repo / "crates/jett_runtime/src/native_abi/resource" + pattern = r'pub unsafe extern "C" fn (jett_rt_v1_resource_[a-z_]+)\(' + leaves = re.findall(pattern, (directory / "leaves.rs").read_text(encoding="utf-8")) + carriers: list[str] = re.findall(pattern, (directory / "leaves/carriers.rs").read_text(encoding="utf-8")) + private = re.findall(pattern, (directory / "test_archive_api.rs").read_text(encoding="utf-8")) + require(len(leaves) == len(set(leaves)) == 37, "frozen Resource leaf contract must contain 37 names") + require(not set(leaves) & CARRIER_EXPORTS, "carrier exports must occur only in their declared child module") + require(len(carriers) == len(set(carriers)) == 13 and set(carriers) == set(CARRIER_EXPORTS), + "frozen carrier leaf contract must contain the exact 13 names") + require(len(private) == len(set(private)) == 8, "frozen private test contract must contain eight names") + require(all(name.startswith("jett_rt_v1_resource_test_") for name in private), + "unexpected private export name") + require(len(set(leaves) | set(carriers)) == 50, "current Resource contract must contain 50 names") + startup = startup_exports(directory) + require(not startup & (set(leaves) | set(carriers) | set(private)), + "Resource startup exports belong only to their canonical child module") + production = set(leaves) | set(carriers) | startup + require(len(production) == 55, "fifty-five production Resource exports are required") + return production, set(private) + + +def checked_layout_wire(repo: Path) -> None: + registration = repo / "crates/jett_runtime/src/resource_custody/registration.rs" + versions = re.findall(r"pub\(crate\) const NATIVE_RESOURCE_LAYOUT_WIRE_VERSION: u32 = ([0-9]+);", + registration.read_text(encoding="utf-8")) + require(versions == [str(CURRENT_LAYOUT_WIRE)], + "fresh carrier archives require the exact current runtime wire version 3") + + +def checked_measurement_source(repo: Path) -> None: + retained = repo / "tools/measure_resource_native_archives.py" + require(retained.is_file() and digest(retained) == digest(Path(__file__)), + "executing measurement helper differs from the measured repository helper") + + +def check_symbols(text: str, leaves: set[str], private: set[str]) -> None: + definitions: Counter[str] = Counter() + production: set[str] = set() + test_exports: set[str] = set() + for line in text.splitlines(): + fields = line.split() + if len(fields) < 3 or len(fields[1]) != 1: + continue + name, kind = fields[:2] + if name == "main" or name.startswith("jett_rt_v1_resource_"): + require(kind == "T", f"expected one global text definition for {name}, got {kind}") + definitions[name] += 1 + if name.startswith("jett_rt_v1_resource_test_"): + test_exports.add(name) + elif name != "main": + production.add(name) + require(production == leaves, "Resource leaf symbol inventory differs from frozen source") + require(test_exports == private, "private test symbol inventory differs from frozen source") + for name in leaves | private | {"main"}: + require(definitions[name] == 1, f"expected exactly one definition of {name}") + + +def fingerprint_records(directory: Path) -> list[Json]: + paths = sorted(directory.glob("jett_runtime-*/lib-jett_runtime.json")) + require(bool(paths), "runtime Cargo fingerprint is missing") + records: list[Json] = [] + for path in paths: + value = read_object(path) + require(value.get("rustflags") == [], "GNU target fingerprint has unexpected rustflags") + records.append({**artifact(path), "raw_json_utf8": path.read_text(encoding="utf-8")}) + return records + + +def toolchain(tools: Tools, repo: Path, output: Path, environment: dict[str, str]) -> dict[str, Json]: + observed: dict[str, Json] = {} + for name, path, arguments in ( + ("rustc", tools.rustc, ["-vV"]), ("cargo", tools.cargo, ["-vV"]), + ("nm", tools.nm, ["--version"]), ("readelf", tools.readelf, ["--version"]), + ("cc", tools.cc, ["--version"]), + ): + log = output / f"{name}-version.log" + text = logged([str(path), *arguments], repo, environment, log) + observed[name] = {"invocation_path": str(path), "executable": artifact(path), + "version_observation": artifact(log)} + if name == "rustc": + require(f"host: {TARGET}" in text.splitlines(), "private archives must be built on their GNU host") + target_log = output / "cc-target.log" + target = logged([str(tools.cc), "-dumpmachine"], repo, environment, target_log).strip() + require(re.fullmatch(r"x86_64(?:-[a-z0-9_]+)*-linux-gnu", target) is not None, + f"unexpected C linker target: {target}") + observed["cc_target"] = artifact(target_log) + return observed + + +def dynamic_crt(tools: Tools, repo: Path, output: Path, profile: str, + arguments: list[str], environment: dict[str, str]) -> dict[str, Json]: + source = output / f"{profile}-crt-probe.c" + source.write_text("int main(void) { return 0; }\n", encoding="utf-8") + binary = output / f"{profile}-crt-probe" + command = [str(tools.cc), "-no-pie", "-o", str(binary), str(source), *arguments] + link_log = output / f"{profile}-crt-probe-link.log" + logged(command, repo, environment, link_log, timeout=60) + inspection = output / f"{profile}-crt-probe-elf.log" + text = logged([str(tools.readelf), "--file-header", "--program-headers", "--dynamic", str(binary)], + repo, environment, inspection, timeout=60) + require(re.search(r"Type:\s+EXEC\b", text) is not None, "GNU probe must be an ELF executable") + require("INTERP" in text and "(NEEDED)" in text and "libc.so" in text, + "dynamic GNU CRT interpreter/libc dependency was not observed") + return {"source": artifact(source), "binary": artifact(binary), "link_command": list(command), + "link_observation": artifact(link_log), "elf_observation": artifact(inspection), + "scope": "Measured C driver and native-library vector; not Resource Source execution"} + + +def measure_profile(tools: Tools, repo: Path, output: Path, profile: str, + environment: dict[str, str], inputs: dict[str, Json], + versions: dict[str, Json], leaves: set[str], private: set[str]) -> dict[str, Json]: + target_dir = output / f"{profile}-build" + command = [str(tools.cargo), "rustc", "--locked", "--color", "never", "--verbose", "-p", "jett_runtime", "--lib", + "--target", TARGET, "--target-dir", str(target_dir)] + if profile == "release": + command.append("--release") + command.extend(["--", "--cfg", "test", "--cfg", "jett_resource_native_test_archive", + "--print=native-static-libs"]) + build_log = output / f"{profile}-build.log" + text = logged(command, repo, environment, build_log) + require(sources(repo) == inputs, "runtime inputs changed during private archive build") + selected_cfgs(text, tools.rustc) + arguments = native_libraries(text) + directory = target_dir / TARGET / profile + archive = directory / "libjett_runtime.a" + require(archive.is_file() and archive.stat().st_size <= 256 * 1024 * 1024, + "private archive is missing or exceeds the harness limit") + archive_hash = digest(archive) + symbols = output / f"{profile}-symbols.log" + text = logged([str(tools.nm), "--defined-only", "--extern-only", "--format=posix", str(archive)], + repo, environment, symbols) + check_symbols(text, leaves, private) + elf = output / f"{profile}-archive-elf.log" + text = logged([str(tools.readelf), "--file-header", "--symbols", str(archive)], repo, environment, elf) + require("ELF64" in text and "X86-64" in text, "GNU archive target inspection failed") + fingerprints = fingerprint_records(directory / ".fingerprint") + probe = dynamic_crt(tools, repo, output, profile, arguments, environment) + require(sources(repo) == inputs, "runtime inputs changed during archive measurement") + require(digest(archive) == archive_hash, "runtime archive changed during independent measurement") + build = output / f"{profile}-build.json" + write_json(build, {"profile": profile, "target": TARGET, "toolchain": versions, + "runtime_sources": inputs, "workspace_lock": inputs["Cargo.lock"], + "cfgs_selected_package_only": list(CFGS), "actual_build_command": list(command), + "cargo_build_log": artifact(build_log), "archive": artifact(archive), + "measurement_script": artifact(Path(__file__))}) + libraries = output / f"{profile}-native-static-libs.json" + write_json(libraries, {"actual_print_log": artifact(build_log), "arguments": list(arguments), + "order_and_duplication_preserved": True}) + crt = output / f"{profile}-crt.json" + write_json(crt, {"actual_target_scoped_rustflags": [], "cargo_fingerprints": fingerprints, + "native_static_libs": artifact(libraries), "complete_symbol_inventory": artifact(symbols), + "complete_elf_archive_inventory": artifact(elf), "dynamic_crt_probe": probe, + "linker_path": str(tools.cc), "linker_executable": artifact(tools.cc), "mode": "dynamic"}) + return {"profile": profile, "target": TARGET, "archive_path": str(archive), + "archive_sha256": archive_hash, "runtime_abi": 1, "layout_wire": CURRENT_LAYOUT_WIRE, + "private_cfgs": list(CFGS), "crt_mode": "dynamic", "linker_path": str(tools.cc), + "native_library_args": list(arguments), "build_receipt": artifact(build), + "native_static_libs_receipt": artifact(libraries), "crt_receipt": artifact(crt)} + + +def msvc_linker_version(linker: Path, repo: Path, environment: dict[str, str], + log: Path) -> tuple[str, int]: + # MSVC's help command uses 1100 for valid help; build commands remain strict. + with log.open("xb") as output: + result = subprocess.run([str(linker), "/?"], cwd=repo, env=environment, + stdin=subprocess.DEVNULL, stdout=output, + stderr=subprocess.STDOUT, timeout=60) + text = log.read_text(encoding="utf-8", errors="replace") + require(result.returncode in (0, 1100), + f"linker version probe exited {result.returncode}; see {log}") + require(re.search(r"^Microsoft \(R\) Incremental Linker Version [0-9]+(?:\.[0-9]+){3}\s*$", + text, re.MULTILINE) is not None + and re.search(r"^\s*usage: LINK \[options\] \[files\] \[@commandfile\]\s*$", + text, re.MULTILINE) is not None + and re.search(r"^\s*options:\s*$", text, re.MULTILINE) is not None, + "linker version probe must contain the Microsoft version and complete help header") + require(re.search(r"(?:fatal\s+error|error\s+LNK[0-9]+)", text, re.IGNORECASE) is None, + "linker version probe contains an actual linker error") + return text, result.returncode + + +def msvc_toolchain(tools: MsvcTools, repo: Path, output: Path, + environment: dict[str, str]) -> dict[str, Json]: + observed: dict[str, Json] = {} + for name, path, arguments in ( + ("rustc", tools.rustc, ["-vV"]), ("cargo", tools.cargo, ["-vV"]), + ("nm", tools.nm, ["--version"]), ("readobj", tools.readobj, ["--version"]), + ("linker", tools.linker, ["/?"]), + ): + log = output / f"{name}-version.log" + if name == "linker": + text, returncode = msvc_linker_version(path, repo, environment, log) + else: + text = logged([str(path), *arguments], repo, environment, log) + returncode = 0 + observed[name] = {"invocation_path": str(path), "executable": artifact(path), + "version_observation": artifact(log), "returncode": returncode} + if name == "rustc": + require(f"host: {MSVC_TARGET}" in text.splitlines(), + "private MSVC archives must be built on their Windows MSVC host") + elif name in ("nm", "readobj"): + require("LLVM" in text, f"MSVC {name} observations require the LLVM tool") + elif name == "linker": + require("Microsoft" in text and "Linker" in text, + "measured link.exe must be the Microsoft linker") + return observed + + +def checked_msvc_linker_commands(text: str, compiler: Path, linker: Path) -> None: + for arguments in msvc_compiler_commands(text, compiler): + if option_values(arguments, "--target") != [MSVC_TARGET]: + continue + linkers = [value.removeprefix("linker=") for value in option_values(arguments, "-C") + if value.startswith("linker=")] + require(linkers == [str(linker)], "actual Windows compiler target linker differs from measured link.exe") + + +def measure_msvc_profile(tools: MsvcTools, repo: Path, output: Path, profile: str, + environment: dict[str, str], inputs: dict[str, Json], + versions: dict[str, Json], leaves: set[str], private: set[str]) -> dict[str, Json]: + target_dir = output / f"{profile}-build" + require(not target_dir.exists(), "each private MSVC profile requires a fresh build directory") + require(sources(repo) == inputs, "runtime inputs changed before private archive build") + command = [str(tools.cargo), "rustc", "--offline", "--locked", "--color", "never", "--verbose", + "-p", "jett_runtime", "--lib", "--target", MSVC_TARGET, "--target-dir", str(target_dir)] + if profile == "release": + command.append("--release") + command.extend(["--", "--cfg", "test", "--cfg", "jett_resource_native_test_archive", + "--print=native-static-libs"]) + build_log = output / f"{profile}-build.log" + text = logged(command, repo, environment, build_log) + require(sources(repo) == inputs, "runtime inputs changed during private archive build") + msvc_selected_cfgs(text, tools.rustc) + checked_msvc_linker_commands(text, tools.rustc, tools.linker) + arguments = msvc_native_libraries(text) + directory = target_dir / MSVC_TARGET / profile + archive = directory / "jett_runtime.lib" + require(archive.is_file() and archive.stat().st_size <= 256 * 1024 * 1024, + "private archive is missing or exceeds the harness limit") + archive_hash = digest(archive) + symbols = output / f"{profile}-symbols.log" + text = logged([str(tools.nm), "--defined-only", "--extern-only", "--format=posix", str(archive)], + repo, environment, symbols) + check_symbols(text, leaves, private) + coff = output / f"{profile}-archive-coff.log" + text = logged([str(tools.readobj), "--file-headers", "--coff-directives", str(archive)], + repo, environment, coff) + checked_coff_archive(text) + directive_count = len(re.findall(r"^Directive\(s\):", text, re.MULTILINE)) + fingerprints = msvc_fingerprint_records(directory / ".fingerprint") + require(sources(repo) == inputs, "runtime inputs changed during archive measurement") + require(digest(archive) == archive_hash, "runtime archive changed during independent measurement") + checked_msvc_environment(environment, tools.linker) + build = output / f"{profile}-build.json" + write_json(build, {"profile": profile, "target": MSVC_TARGET, "toolchain": versions, + "runtime_sources": inputs, "workspace_lock": inputs["Cargo.lock"], + "cfgs_selected_package_only": list(CFGS), "actual_build_command": list(command), + "cargo_build_log": artifact(build_log), "archive": artifact(archive), + "build_environment": {name: environment[name] for name in ( + MSVC_FLAGS_ENV, "CARGO_INCREMENTAL", "CARGO_PROFILE_DEV_DEBUG", + "CARGO_PROFILE_RELEASE_DEBUG")}, + "measurement_script": artifact(Path(__file__))}) + libraries = output / f"{profile}-native-static-libs.json" + write_json(libraries, {"actual_print_log": artifact(build_log), "arguments": list(arguments), + "order_and_duplication_preserved": True}) + crt = output / f"{profile}-crt.json" + write_json(crt, {"actual_target_scoped_rustflags": list(MSVC_RUSTFLAGS), + "cargo_fingerprints": fingerprints, "native_static_libs": artifact(libraries), + "complete_symbol_inventory": artifact(symbols), + "complete_coff_archive_inventory": artifact(coff), + "coff_directive_record_count": directive_count, + "linker_path": str(tools.linker), "linker_executable": artifact(tools.linker), + "sdk_LIB": environment["LIB"], + "developer_shell": {name: environment[name] for name in ("VCINSTALLDIR", "VSCMD_ARG_TGT_ARCH")}, + "mode": "static"}) + return {"profile": profile, "target": MSVC_TARGET, "archive_path": str(archive), + "archive_sha256": archive_hash, "runtime_abi": 1, "layout_wire": CURRENT_LAYOUT_WIRE, + "private_cfgs": list(CFGS), "crt_mode": "static", "linker_path": str(tools.linker), + "native_library_args": list(arguments), "build_receipt": artifact(build), + "native_static_libs_receipt": artifact(libraries), "crt_receipt": artifact(crt)} + + +def prepare_msvc(repo: Path, output: Path, nm: str = "llvm-nm", readobj: str = "llvm-readobj", + linker: str = "link.exe") -> tuple[Path, str]: + require(not output.exists(), "preserve immutable measurements: choose a fresh output directory") + checked_layout_wire(repo) + leaves, private = export_names(repo) + checked_measurement_source(repo) + environment = msvc_build_environment() + tools = MsvcTools(executable("cargo"), executable("rustc"), executable(nm), + executable(readobj), executable(linker)) + checked_msvc_environment(environment, tools.linker) + # Pin both compiler and linker; Developer Shell LIB remains byte-for-byte inherited. + environment["RUSTC"] = str(tools.rustc) + environment["RUSTC_WRAPPER"] = "" + environment["RUSTC_WORKSPACE_WRAPPER"] = "" + environment["CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER"] = str(tools.linker) + inputs = sources(repo) + checked_layout_wire(repo) + require(export_names(repo) == (leaves, private), "Resource export sources changed before measurement") + checked_measurement_source(repo) + require(sources(repo) == inputs, "runtime inputs changed while establishing archive guards") + output.mkdir(parents=True) + versions = msvc_toolchain(tools, repo, output, environment) + archives: list[Json] = [] + for profile in ("debug", "release"): + archives.append(measure_msvc_profile(tools, repo, output, profile, environment, + inputs, versions, leaves, private)) + checked_measurement_source(repo) + require(sources(repo) == inputs, "runtime inputs changed across matched archive profiles") + receipt = output / "receipt.json" + write_json(receipt, {"version": 1, "scope": "Measured compiler-test MSVC static CRT archives; not Source execution", + "archives": archives}) + return receipt, digest(receipt) + +def prepare(repo: Path, output: Path, cc: str, nm: str, readelf: str, *, + target: str = TARGET, readobj: str = "llvm-readobj", linker: str = "link.exe") -> tuple[Path, str]: + require(target in (TARGET, MSVC_TARGET), "unsupported archive measurement target") + if target == MSVC_TARGET: + return prepare_msvc(repo, output, nm, readobj, linker) + require(not output.exists(), "preserve immutable measurements: choose a fresh output directory") + checked_layout_wire(repo) + leaves, private = export_names(repo) + checked_measurement_source(repo) + environment = build_environment() + tools = Tools(executable("cargo"), executable("rustc"), executable(nm), executable(readelf), executable(cc)) + # Pin Cargo to the measured rustup proxy and disable config-provided wrappers. + environment["RUSTC"] = str(tools.rustc) + environment["RUSTC_WRAPPER"] = "" + environment["RUSTC_WORKSPACE_WRAPPER"] = "" + inputs = sources(repo) + checked_layout_wire(repo) + require(export_names(repo) == (leaves, private), "Resource export sources changed before measurement") + checked_measurement_source(repo) + require(sources(repo) == inputs, "runtime inputs changed while establishing archive guards") + output.mkdir(parents=True) + versions = toolchain(tools, repo, output, environment) + archives: list[Json] = [] + for profile in ("debug", "release"): + archives.append(measure_profile(tools, repo, output, profile, environment, inputs, + versions, leaves, private)) + checked_measurement_source(repo) + require(sources(repo) == inputs, "runtime inputs changed across matched archive profiles") + receipt = output / "receipt.json" + write_json(receipt, {"version": 1, "scope": "Measured compiler-test GNU archives; not Source execution", + "archives": archives}) + return receipt, digest(receipt) + + +def measured_artifact(record: Json) -> Path: + require(isinstance(record, dict), "measured artifact must be an object") + if not isinstance(record, dict): + raise MeasurementError("measured artifact must be an object") + path, expected = record.get("path"), record.get("sha256") + require(isinstance(path, str) and isinstance(expected, str), "artifact path/hash are required") + if not isinstance(path, str) or not isinstance(expected, str): + raise MeasurementError("artifact path/hash are required") + file = Path(path) + require(file.is_absolute() and file.is_file() and file.stat().st_size <= 256 * 1024 * 1024 + and digest(file) == expected, + f"measured artifact changed: {file}") + return file + + +def verify_embedded_artifacts(value: Json) -> None: + if isinstance(value, dict): + if "path" in value and "sha256" in value: + measured_artifact(value) + raw = value.get("raw_json_utf8") + if isinstance(raw, str): + require(hashlib.sha256(raw.encode("utf-8")).hexdigest() == value["sha256"], + "retained Cargo fingerprint bytes differ from their observation") + for item in value.values(): + verify_embedded_artifacts(item) + elif isinstance(value, list): + for item in value: + verify_embedded_artifacts(item) + + +def checked_msvc_receipt(row: dict[str, Json], build: dict[str, Json], + libraries: dict[str, Json], crt: dict[str, Json], repo: Path) -> None: + require(build.get("target") == MSVC_TARGET and build.get("profile") == row.get("profile"), + "MSVC build provenance target/profile differs from archive row") + require(build.get("archive") == {"path": row.get("archive_path"), "sha256": row.get("archive_sha256")}, + "MSVC build provenance must describe the exact linked runtime archive") + require(build.get("cfgs_selected_package_only") == CFGS, + "MSVC build provenance must preserve the selected-package private cfgs") + require(crt.get("actual_target_scoped_rustflags") == list(MSVC_RUSTFLAGS), + "MSVC CRT receipt must preserve the exact target flags") + require(build.get("build_environment") == { + MSVC_FLAGS_ENV: " ".join(MSVC_RUSTFLAGS), "CARGO_INCREMENTAL": "0", + "CARGO_PROFILE_DEV_DEBUG": "0", "CARGO_PROFILE_RELEASE_DEBUG": "0"}, + "MSVC build must disable incremental compilation and debug information") + path = row.get("linker_path") + require(isinstance(path, str), "measured MSVC linker path is missing") + if not isinstance(path, str): + raise MeasurementError("measured MSVC linker path is missing") + checked_msvc_environment(os.environ, Path(path)) + require(crt.get("sdk_LIB") == os.environ.get("LIB"), + "SDK LIB differs from the inherited measured Developer Shell environment") + require(crt.get("developer_shell") == { + name: os.environ.get(name) for name in ("VCINSTALLDIR", "VSCMD_ARG_TGT_ARCH")}, + "Developer Shell identity differs from measured environment") + toolchain_value = build.get("toolchain") + require(isinstance(toolchain_value, dict), "MSVC measured toolchain is missing") + if not isinstance(toolchain_value, dict): + raise MeasurementError("MSVC measured toolchain is missing") + compiler_record = toolchain_value.get("rustc") + require(isinstance(compiler_record, dict), "MSVC measured compiler is missing") + if not isinstance(compiler_record, dict): + raise MeasurementError("MSVC measured compiler is missing") + compiler_path = compiler_record.get("invocation_path") + require(isinstance(compiler_path, str), "MSVC compiler invocation path is missing") + if not isinstance(compiler_path, str): + raise MeasurementError("MSVC compiler invocation path is missing") + compiler = measured_artifact(compiler_record.get("executable")) + require(Path(compiler_path).absolute() == compiler.absolute(), "measured compiler invocation identity differs") + build_log = measured_artifact(build.get("cargo_build_log")) + text = build_log.read_text(encoding="utf-8") + msvc_selected_cfgs(text, Path(compiler_path)) + checked_msvc_linker_commands(text, Path(compiler_path), Path(path)) + require(libraries.get("actual_print_log") == build.get("cargo_build_log"), + "native library observation must be from the measured runtime build") + observed_libraries = msvc_native_libraries(measured_artifact( + libraries.get("actual_print_log")).read_text(encoding="utf-8")) + require(libraries.get("arguments") == observed_libraries + and row.get("native_library_args") == observed_libraries, + "MSVC native libraries differ from the actual ordered rustc observation") + require(crt.get("native_static_libs") == row.get("native_static_libs_receipt"), + "CRT provenance refers to a different native library measurement") + fingerprints = crt.get("cargo_fingerprints") + require(isinstance(fingerprints, list) and bool(fingerprints), "MSVC Cargo fingerprint provenance is missing") + if not isinstance(fingerprints, list): + raise MeasurementError("MSVC Cargo fingerprint provenance is missing") + for record in fingerprints: + fingerprint = read_object(measured_artifact(record)) + require(fingerprint.get("rustflags") == list(MSVC_RUSTFLAGS), + "MSVC fingerprint contradicts the static CRT flags") + coff = measured_artifact(crt.get("complete_coff_archive_inventory")) + coff_text = coff.read_text(encoding="utf-8") + checked_coff_archive(coff_text) + directive_count = crt.get("coff_directive_record_count") + require(isinstance(directive_count, int) and not isinstance(directive_count, bool) + and directive_count == len(re.findall(r"^Directive\(s\):", coff_text, re.MULTILINE)), + "CRT receipt directive count differs from the complete COFF observation") + symbols = measured_artifact(crt.get("complete_symbol_inventory")) + leaves, private = export_names(repo) + check_symbols(symbols.read_text(encoding="utf-8"), leaves, private) + +def receipt_sources(receipt: Path, expected: str, repo: Path, *, target: str = TARGET) -> None: + require(target in (TARGET, MSVC_TARGET), "unsupported archive receipt target") + mode = "static" if target == MSVC_TARGET else "dynamic" + require(re.fullmatch(r"[0-9a-f]{64}", expected) is not None, "expected SHA-256 must be canonical") + require(receipt.is_file() and receipt.stat().st_size <= 1024 * 1024, + "receipt exceeds the test harness limit") + require(digest(receipt) == expected, "receipt differs from the independently supplied compile-time pin") + checked_layout_wire(repo) + export_names(repo) + checked_measurement_source(repo) + record = read_object(receipt) + require(record.get("version") == 1, "unsupported archive receipt version") + archives = record.get("archives") + require(isinstance(archives, list) and len(archives) == 2, "two measured runtime profiles are required") + if not isinstance(archives, list): + raise MeasurementError("archive rows are required") + profiles: set[str] = set() + for row in archives: + require(isinstance(row, dict), "archive row must be an object") + if not isinstance(row, dict): + raise MeasurementError("archive row must be an object") + profile = row.get("profile") + require(isinstance(profile, str) and profile in ("debug", "release"), "unknown runtime profile") + if isinstance(profile, str): + profiles.add(profile) + require(row.get("target") == target and row.get("runtime_abi") == 1 + and row.get("layout_wire") == CURRENT_LAYOUT_WIRE and row.get("private_cfgs") == CFGS + and row.get("crt_mode") == mode, "archive contract differs from measured gate") + measured_artifact({"path": row.get("archive_path"), "sha256": row.get("archive_sha256")}) + for name in ("build_receipt", "native_static_libs_receipt", "crt_receipt"): + provenance = read_object(measured_artifact(row.get(name))) + verify_embedded_artifacts(provenance) + build = read_object(measured_artifact(row.get("build_receipt"))) + require(build.get("runtime_sources") == sources(repo), "measured runtime source differs from current tree") + require(digest(measured_artifact(build.get("measurement_script"))) == digest(Path(__file__)), + "measured helper differs from the executing helper") + libraries = read_object(measured_artifact(row.get("native_static_libs_receipt"))) + require(row.get("native_library_args") == libraries.get("arguments"), "native library vector changed") + crt = read_object(measured_artifact(row.get("crt_receipt"))) + require(crt.get("mode") == mode and crt.get("linker_path") == row.get("linker_path"), + "linker/CRT identity differs from receipt") + linker = measured_artifact(crt.get("linker_executable")) + path = row.get("linker_path") + require(isinstance(path, str), "measured linker path is missing") + if isinstance(path, str): + require(Path(path).resolve(strict=True) == linker.resolve(strict=True), "measured linker changed") + if target == MSVC_TARGET: + checked_msvc_receipt(row, build, libraries, crt, repo) + require(profiles == {"debug", "release"}, "measured profiles must be unique") + + +def compiled_test(messages: str) -> Path: + executables: set[Path] = set() + for line in messages.splitlines(): + value = normalize_json(json.loads(line)) + if not isinstance(value, dict) or value.get("reason") != "compiler-artifact": + continue + target = value.get("target") + executable_path = value.get("executable") + if isinstance(target, dict) and target.get("name") == "native_conformance" and isinstance(executable_path, str): + executables.add(Path(executable_path)) + require(len(executables) == 1, "one compiled native_conformance executable must be selected") + executable_path = next(iter(executables)) + require(executable_path.is_absolute() and executable_path.is_file(), "compiled gate executable is missing") + return executable_path + + +def checked_msvc_gate_artifact(messages: str, binary: Path, expected: str) -> Path: + matches: list[dict[str, Json]] = [] + for line in messages.splitlines(): + value = normalize_json(json.loads(line)) + if not isinstance(value, dict) or value.get("reason") != "compiler-artifact": + continue + target = value.get("target") + if isinstance(target, dict) and target.get("name") == "native_conformance": + matches.append(value) + require(len(matches) == 1 and matches[0].get("fresh") is False + and matches[0].get("executable") == str(binary), + "MSVC gate artifact must be freshly compiled with the independently supplied pin") + dep_info = binary.with_suffix(".d") + require(dep_info.is_file(), "fresh MSVC gate compiler dep-info is missing") + pins = re.findall(r"^# env-dep:JETT_RESOURCE_NATIVE_TEST_ARCHIVE_EXPECTED_SHA256_V1=(.*)$", + dep_info.read_text(encoding="utf-8"), re.MULTILINE) + require(pins == [expected], "MSVC gate dep-info does not prove the exact compile-time receipt pin") + return dep_info + + +def run_msvc_gate(repo: Path, receipt: Path, expected: str, target_dir: Path, *, + suite: str = "original", reuse_target_dir: bool = False) -> None: + require(suite in ("original", "occupied-structs"), "unsupported Resource gate suite") + require(reuse_target_dir or not target_dir.exists(), + "existing MSVC gate targets require explicit --reuse-target-dir") + receipt_sources(receipt, expected, repo, target=MSVC_TARGET) + environment = msvc_build_environment() + compiler = executable("rustc") + linker = executable("link.exe") + checked_msvc_environment(environment, linker) + environment["RUSTC"] = str(compiler) + environment["RUSTC_WRAPPER"] = "" + environment["RUSTC_WORKSPACE_WRAPPER"] = "" + environment["CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER"] = str(linker) + environment["CARGO_PROFILE_TEST_DEBUG"] = "0" + inputs = sources(repo, complete=True) + output = receipt.parent + compiler_log = output / "gate-rustc-version.log" + version = logged([str(compiler), "-vV"], repo, environment, compiler_log) + require(f"host: {MSVC_TARGET}" in version.splitlines(), "Source gate must compile on its measured MSVC host") + environment[PIN_ENV] = expected + command = [str(executable("cargo")), "test", "--offline", "--locked", "--color", "never", "-p", "jett_driver", + "--test", "native_conformance", "--target", MSVC_TARGET, "--target-dir", str(target_dir), + "--verbose", "--no-run", "--message-format=json"] + messages = output / "gate-compile-messages.jsonl" + errors = output / "gate-compile-stderr.log" + with messages.open("xb") as stdout, errors.open("xb") as stderr: + result = subprocess.run(command, cwd=repo, env=environment, stdin=subprocess.DEVNULL, + stdout=stdout, stderr=stderr, timeout=3600) + require(result.returncode == 0, f"gate compilation failed; see {errors}") + actual_commands = msvc_compiler_commands(errors.read_text(encoding="utf-8"), compiler) + gates = [arguments for arguments in actual_commands + if option_values(arguments, "--crate-name") == ["native_conformance"]] + require(len(gates) == 1 and "--test" in gates[0] + and option_values(gates[0], "--target") == [MSVC_TARGET], + "MSVC gate must have an actual native_conformance test compiler invocation") + checked_msvc_flags(gates[0]) + checked_msvc_linker_commands(errors.read_text(encoding="utf-8"), compiler, linker) + message_text = messages.read_text(encoding="utf-8") + binary = compiled_test(message_text) + dep_info = checked_msvc_gate_artifact(message_text, binary, expected) + binary_hash, dep_info_hash = digest(binary), digest(dep_info) + require(sources(repo, complete=True) == inputs, "compiler/Source inputs changed during gate compilation") + receipt_sources(receipt, expected, repo, target=MSVC_TARGET) + environment.pop(PIN_ENV) + environment[RECEIPT_ENV] = str(receipt) + tests = OCCUPIED_STRUCT_TESTS if suite == "occupied-structs" else (TEST,) + observations: list[Json] = [] + counts: dict[str, Json] = {} + for index, test in enumerate(tests, start=1): + log = output / f"gate-execution-{index}.log" + text = logged([str(binary), test, "--ignored", "--exact", "--test-threads=1", "--nocapture"], + repo, environment, log, timeout=3600) + require(re.search(r"^running 1 test$", text, re.MULTILINE) is not None + and "1 passed; 0 failed; 0 ignored;" in text, + f"exactly one ignored Resource test must execute and pass: {test}") + require(digest(binary) == binary_hash and digest(dep_info) == dep_info_hash, + "compiled MSVC gate or pin dep-info changed during execution") + observations.append({"test": test, "execution": artifact(log)}) + if suite == "original": + summary = re.findall(r"Resource native acceptance: (\d+) cases; (\d+) Source-deleted executions; profiles=debug,release", text) + require(len(summary) == 1, "complete Source execution summary is missing") + count, executions = (int(value) for value in summary[0]) + require(count > 0 and executions == 2 * count, "both archive profiles must execute the complete corpus") + counts = {"cases": count, "source_deleted_executions": executions} + require(sources(repo, complete=True) == inputs, "compiler/Source inputs changed during gate execution") + receipt_sources(receipt, expected, repo, target=MSVC_TARGET) + write_json(output / "gate-acceptance.json", { + "scope": ("Occupied struct retirement and clean reentry Source gates on private MSVC archives" + if suite == "occupied-structs" else "Complete shared Source corpus on private MSVC archives"), + "target": MSVC_TARGET, "suite": suite, "profiles": ["debug", "release"], **counts, + "compile_time_receipt_sha256": expected, "receipt": artifact(receipt), + "compiler_and_source_inputs": inputs, "compiler_executable": artifact(compiler), + "compiler_version": artifact(compiler_log), "compile_command": list(command), + "compile_messages": artifact(messages), "compile_stderr": artifact(errors), + "compiled_gate": artifact(binary), "compiler_dep_info": artifact(dep_info), + "gate_executions": observations, "runtime_expected_pin_environment": False}) + +def run_gate(repo: Path, receipt: Path, expected: str, target_dir: Path, *, target: str = TARGET, + suite: str = "original", reuse_target_dir: bool = False) -> None: + require(target in (TARGET, MSVC_TARGET), "unsupported Resource gate target") + if target == MSVC_TARGET: + run_msvc_gate(repo, receipt, expected, target_dir, suite=suite, reuse_target_dir=reuse_target_dir) + return + require(suite == "original" and not reuse_target_dir, "GNU gate requires the original suite and a fresh target") + require(not target_dir.exists(), "compile the receipt pin in a fresh gate target directory") + receipt_sources(receipt, expected, repo) + environment = build_environment() + compiler = executable("rustc") + environment["RUSTC"] = str(compiler) + environment["RUSTC_WRAPPER"] = "" + environment["RUSTC_WORKSPACE_WRAPPER"] = "" + inputs = sources(repo, complete=True) + output = receipt.parent + compiler_log = output / "gate-rustc-version.log" + version = logged([str(compiler), "-vV"], repo, environment, compiler_log) + require(f"host: {TARGET}" in version.splitlines(), "Source gate must compile on its measured GNU host") + environment[PIN_ENV] = expected + command = [str(executable("cargo")), "test", "--locked", "--color", "never", "-p", "jett_driver", "--test", "native_conformance", + "--target", TARGET, "--target-dir", str(target_dir), "--verbose", "--no-run", "--message-format=json"] + messages = output / "gate-compile-messages.jsonl" + errors = output / "gate-compile-stderr.log" + with messages.open("xb") as stdout, errors.open("xb") as stderr: + result = subprocess.run(command, cwd=repo, env=environment, stdin=subprocess.DEVNULL, + stdout=stdout, stderr=stderr, timeout=3600) + require(result.returncode == 0, f"gate compilation failed; see {errors}") + checked_compiler_commands(errors.read_text(encoding="utf-8"), compiler) + binary = compiled_test(messages.read_text(encoding="utf-8")) + require(sources(repo, complete=True) == inputs, "compiler/Source inputs changed during gate compilation") + receipt_sources(receipt, expected, repo) + environment.pop(PIN_ENV) + environment[RECEIPT_ENV] = str(receipt) + gate_log = output / "gate-execution.log" + text = logged([str(binary), TEST, "--ignored", "--exact", "--test-threads=1", "--nocapture"], + repo, environment, gate_log, timeout=3600) + require("1 passed; 0 failed; 0 ignored;" in text, "the complete ignored Resource gate did not execute") + summary = re.findall(r"Resource native acceptance: (\d+) cases; (\d+) Source-deleted executions; profiles=debug,release", text) + require(len(summary) == 1, "complete Source execution summary is missing") + count, executions = (int(value) for value in summary[0]) + require(count > 0 and executions == 2 * count, "both archive profiles must execute the complete corpus") + require(sources(repo, complete=True) == inputs, "compiler/Source inputs changed during gate execution") + receipt_sources(receipt, expected, repo) + write_json(output / "gate-acceptance.json", {"scope": "Complete shared Source corpus on private GNU archives", + "target": TARGET, "cases": count, "source_deleted_executions": executions, + "profiles": ["debug", "release"], "compile_time_receipt_sha256": expected, + "receipt": artifact(receipt), "compiler_and_source_inputs": inputs, + "compiler_executable": artifact(compiler), "compiler_version": artifact(compiler_log), + "compile_command": list(command), "compile_messages": artifact(messages), + "compile_stderr": artifact(errors), "compiled_gate": artifact(binary), + "gate_execution": artifact(gate_log), "runtime_expected_pin_environment": False}) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest="command", required=True) + preparation = commands.add_parser("prepare", help="build and independently measure both private archives") + preparation.add_argument("--repo", type=Path, default=ROOT) + preparation.add_argument("--output", type=Path, required=True) + preparation.add_argument("--cc", default=os.environ.get("JETT_NATIVE_CC", "cc")) + preparation.add_argument("--target", choices=(TARGET, MSVC_TARGET), default=TARGET) + preparation.add_argument("--nm") + preparation.add_argument("--readobj", default="llvm-readobj") + preparation.add_argument("--linker", default="link.exe") + preparation.add_argument("--readelf", default="readelf") + preparation.add_argument("--github-output", type=Path) + gate = commands.add_parser("run", help="compile the independently supplied receipt pin and run Source gate") + gate.add_argument("--repo", type=Path, default=ROOT) + gate.add_argument("--receipt", type=Path, required=True) + gate.add_argument("--expected-sha256", required=True) + gate.add_argument("--target-dir", type=Path, required=True) + gate.add_argument("--target", choices=(TARGET, MSVC_TARGET), default=TARGET) + gate.add_argument("--suite", choices=("original", "occupied-structs"), default="original") + gate.add_argument("--reuse-target-dir", action="store_true", + help="MSVC only: reuse a gate target while requiring fresh compilation and dep-info pin evidence") + arguments = parser.parse_args() + if arguments.command == "prepare": + nm = arguments.nm or ("llvm-nm" if arguments.target == MSVC_TARGET else "nm") + receipt, expected = prepare(arguments.repo.resolve(), arguments.output.resolve(), arguments.cc, + nm, arguments.readelf, target=arguments.target, + readobj=arguments.readobj, linker=arguments.linker) + if arguments.github_output is not None: + require("\n" not in str(receipt) and "\r" not in str(receipt), "receipt path cannot contain a newline") + with arguments.github_output.open("a", encoding="utf-8", newline="\n") as output: + output.write(f"receipt={receipt}\nsha256={expected}\n") + print(json.dumps({"receipt": str(receipt), "sha256": expected, "native_resource_source_execution": False})) + else: + run_gate(arguments.repo.resolve(), arguments.receipt.resolve(), arguments.expected_sha256, + arguments.target_dir.resolve(), target=arguments.target, suite=arguments.suite, + reuse_target_dir=arguments.reuse_target_dir) + + +if __name__ == "__main__": + main() diff --git a/tools/tests/test_measure_resource_native_archives.py b/tools/tests/test_measure_resource_native_archives.py new file mode 100644 index 00000000..69e6f43d --- /dev/null +++ b/tools/tests/test_measure_resource_native_archives.py @@ -0,0 +1,1438 @@ +"""Evidence controls for private GNU/MSVC archives and compiled pin handoff.""" +from __future__ import annotations + +import json +import os +from pathlib import Path +import subprocess +import tempfile +from typing import BinaryIO +import unittest +from unittest.mock import patch + +from tools import measure_resource_native_archives as measure + + +def copy_resource_export_fixture(root: Path) -> tuple[Path, Path]: + resource = root / "crates/jett_runtime/src/native_abi/resource" + (resource / "leaves").mkdir(parents=True) + for relative in ("leaves.rs", "test_archive_api.rs", "program_entry.rs"): + original = measure.ROOT / "crates/jett_runtime/src/native_abi/resource" / relative + (resource / relative).write_bytes(original.read_bytes()) + owner = resource.with_suffix(".rs") + owner.write_bytes((measure.ROOT / "crates/jett_runtime/src/native_abi/resource.rs").read_bytes()) + carriers = resource / "leaves/carriers.rs" + carriers.write_text("\n".join(f'pub unsafe extern "C" fn {name}() {{}}' + for name in sorted(measure.CARRIER_EXPORTS)), encoding="utf-8") + return owner, resource / "program_entry.rs" + + +class NativeLibraryTests(unittest.TestCase): + def test_actual_order_and_duplicate_arguments_are_retained(self) -> None: + text = "note: native-static-libs: -lgcc_s -lc -lm -lc\n -lpthread\nFinished build\n" + self.assertEqual(measure.native_libraries(text), + ["-lgcc_s", "-lc", "-lm", "-lc", "-lpthread"]) + + def test_missing_multiple_or_unsafe_observations_are_refused(self) -> None: + values = [ + "", "note: native-static-libs: -lc\n", + "note: native-static-libs: -lgcc_s -lc\nnote: native-static-libs: -lc\n", + "note: native-static-libs: -lgcc_s -lc -ljett_runtime\n", + "note: native-static-libs: -lgcc_s -lc -ljett_native_launcher\n", + "note: native-static-libs: -lgcc_s -lc -static\n", + ] + for value in values: + with self.subTest(value=value), self.assertRaises(measure.MeasurementError): + measure.native_libraries(value) + + def test_private_cfgs_belong_to_one_selected_runtime_command(self) -> None: + runtime = ("Running `rustc --crate-name jett_runtime --crate-type rlib " + "--crate-type staticlib --cfg test --cfg jett_resource_native_test_archive " + "--print=native-static-libs`") + measure.selected_cfgs(runtime) + values = [ + runtime.replace("jett_runtime ", "dependency "), + runtime + "\n" + runtime, + runtime.replace("--cfg test ", ""), + runtime.replace("--crate-type staticlib ", ""), + ] + for value in values: + with self.subTest(value=value), self.assertRaises(measure.MeasurementError): + measure.selected_cfgs(value) + + def test_executed_compiler_identity_rejects_wrappers_and_other_compilers(self) -> None: + compiler = Path("/measured/rustc").absolute() + command = f"Running `{str(compiler)!r} --crate-name jett_runtime --crate-type staticlib`" + measure.checked_compiler_commands(command, compiler) + for invalid in ("", "Running `\"/foreign/rustc\" --crate-name jett_runtime`", + f"Running `wrapper {str(compiler)!r} --crate-name jett_runtime`"): + with self.subTest(command=invalid), self.assertRaises(measure.MeasurementError): + measure.checked_compiler_commands(invalid, compiler) + + def test_rustup_proxy_invocation_keeps_its_original_name(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + proxy = root / "cargo" + proxy.write_bytes(b"proxy") + with patch.object(measure.shutil, "which", return_value=str(proxy)), \ + patch.object(Path, "resolve", return_value=root / "rustup") as resolve: + self.assertEqual(measure.executable("cargo"), proxy) + resolve.assert_not_called() + + def test_cargo_color_preferences_cannot_change_measured_output(self) -> None: + with patch.dict(os.environ, {"CARGO_TERM_COLOR": "always"}, clear=True): + self.assertEqual(measure.build_environment()["CARGO_TERM_COLOR"], "never") + + def test_global_cfgs_and_wrappers_are_refused(self) -> None: + for name, value in [ + ("RUSTFLAGS", "--cfg test"), + ("CARGO_ENCODED_RUSTFLAGS", "--cfg\x1ftest"), + ("CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS", "-Ctarget-feature=+crt-static"), + ("RUSTC_WRAPPER", "unmeasured-rustc"), + ("RUSTC", "unmeasured-rustc"), + ("CARGO_BUILD_RUSTC", "unmeasured-rustc"), + ]: + with self.subTest(name=name), patch.dict(os.environ, {name: value}, clear=True): + with self.assertRaises(measure.MeasurementError): + measure.build_environment() + with patch.dict(os.environ, {measure.PIN_ENV: "old", measure.RECEIPT_ENV: "old"}, clear=True): + environment = measure.build_environment() + self.assertNotIn(measure.PIN_ENV, environment) + self.assertNotIn(measure.RECEIPT_ENV, environment) + + +class SymbolAndFingerprintTests(unittest.TestCase): + def setUp(self) -> None: + self.leaves, self.private = measure.export_names(measure.ROOT) + self.symbols = "archive[member.o]:\n" + "\n".join( + f"{name} T 0 10" for name in sorted(self.leaves | self.private | {"main"})) + + def test_complete_current_source_export_contract(self) -> None: + self.assertEqual((len(self.leaves), len(self.private)), (55, 8)) + self.assertTrue(measure.CARRIER_EXPORTS <= self.leaves) + self.assertTrue(measure.STARTUP_EXPORTS <= self.leaves) + self.assertEqual(len(self.leaves - measure.STARTUP_EXPORTS), 50) + measure.check_symbols(self.symbols, self.leaves, self.private) + + def test_each_startup_symbol_must_have_one_text_definition(self) -> None: + for name in sorted(measure.STARTUP_EXPORTS): + line = f"{name} T 0 10" + for invalid in (self.symbols.replace(line, ""), self.symbols + "\n" + line, + self.symbols.replace(line, f"{name} D 0 10")): + with self.subTest(name=name, symbols=invalid), self.assertRaises(measure.MeasurementError): + measure.check_symbols(invalid, self.leaves, self.private) + + def test_missing_duplicate_foreign_or_nontext_exports_are_refused(self) -> None: + leaf = sorted(self.leaves)[0] + private = sorted(self.private)[0] + values = [ + self.symbols.replace(f"{leaf} T 0 10", ""), + self.symbols + "\nmain T 0 10", + self.symbols + f"\n{private} T 0 10", + self.symbols + "\njett_rt_v1_resource_foreign T 0 10", + self.symbols + "\njett_rt_v1_resource_test_foreign T 0 10", + self.symbols.replace(f"{leaf} T", f"{leaf} W"), + ] + for value in values: + with self.subTest(value=value), self.assertRaises(measure.MeasurementError): + measure.check_symbols(value, self.leaves, self.private) + + def test_fingerprints_require_exact_gnu_flags_and_existing_inventory(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + with self.assertRaises(measure.MeasurementError): + measure.fingerprint_records(root) + directory = root / "jett_runtime-hash" + directory.mkdir() + record = directory / "lib-jett_runtime.json" + record.write_text('{"rustflags":[]}', encoding="utf-8") + self.assertEqual(len(measure.fingerprint_records(root)), 1) + record.write_text('{"rustflags":["--cfg","test"]}', encoding="utf-8") + with self.assertRaises(measure.MeasurementError): + measure.fingerprint_records(root) + + +class ReceiptAndGateTests(unittest.TestCase): + def fixture(self, root: Path, inputs: dict[str, measure.Json]) -> Path: + copy_resource_export_fixture(root) + helper = root / "tools/measure_resource_native_archives.py" + helper.parent.mkdir(parents=True) + helper.write_bytes(Path(measure.__file__).read_bytes()) + registration = root / "crates/jett_runtime/src/resource_custody/registration.rs" + registration.parent.mkdir(parents=True) + registration.write_text("pub(crate) const NATIVE_RESOURCE_LAYOUT_WIRE_VERSION: u32 = 3;\n", + encoding="utf-8") + linker = root / "cc" + linker.write_bytes(b"measured C linker") + log = root / "observation.log" + log.write_bytes(b"independent observation") + archives: list[measure.Json] = [] + for profile in ("debug", "release"): + archive = root / f"{profile}-runtime.a" + archive.write_bytes(profile.encode()) + fingerprint = root / f"{profile}-fingerprint.json" + fingerprint.write_text('{"rustflags":[]}', encoding="utf-8") + build = root / f"{profile}-build.json" + measure.write_json(build, {"runtime_sources": inputs, "archive": measure.artifact(archive), + "measurement_script": measure.artifact(Path(measure.__file__))}) + libraries = root / f"{profile}-libraries.json" + measure.write_json(libraries, {"arguments": ["-lgcc_s", "-lc"], + "actual_print_log": measure.artifact(log)}) + crt = root / f"{profile}-crt.json" + measure.write_json(crt, {"mode": "dynamic", "linker_path": str(linker), + "linker_executable": measure.artifact(linker), "cargo_fingerprints": [ + {**measure.artifact(fingerprint), "raw_json_utf8": fingerprint.read_text(encoding="utf-8")}], + "native_static_libs": measure.artifact(libraries)}) + archives.append({"profile": profile, "target": measure.TARGET, "runtime_abi": 1, + "layout_wire": measure.CURRENT_LAYOUT_WIRE, "private_cfgs": list(measure.CFGS), "crt_mode": "dynamic", + "archive_path": str(archive), "archive_sha256": measure.digest(archive), + "linker_path": str(linker), "native_library_args": ["-lgcc_s", "-lc"], + "build_receipt": measure.artifact(build), "native_static_libs_receipt": measure.artifact(libraries), + "crt_receipt": measure.artifact(crt)}) + receipt = root / "receipt.json" + measure.write_json(receipt, {"version": 1, "archives": archives}) + return receipt + + def test_stale_sources_changed_provenance_and_untrusted_pin_are_refused(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + inputs: dict[str, measure.Json] = {"Cargo.lock": "frozen"} + receipt = self.fixture(root, inputs) + pin = measure.digest(receipt) + with patch.object(measure, "sources", return_value=inputs): + measure.receipt_sources(receipt, pin, root) + for invalid in ("", "0" * 64, pin.upper()): + with self.subTest(pin=invalid), self.assertRaises(measure.MeasurementError): + measure.receipt_sources(receipt, invalid, root) + with patch.object(measure, "sources", return_value={"Cargo.lock": "changed"}): + with self.assertRaises(measure.MeasurementError): + measure.receipt_sources(receipt, pin, root) + (root / "debug-build.json").write_text("{}", encoding="utf-8") + with patch.object(measure, "sources", return_value=inputs): + with self.assertRaises(measure.MeasurementError): + measure.receipt_sources(receipt, pin, root) + + def test_archive_linker_and_nested_fingerprint_mutation_are_refused(self) -> None: + for name in ("debug-runtime.a", "cc", "debug-fingerprint.json", "debug-libraries.json"): + with self.subTest(name=name), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + inputs: dict[str, measure.Json] = {"Cargo.lock": "frozen"} + receipt = self.fixture(root, inputs) + expected = measure.digest(receipt) + (root / name).write_bytes(b"changed after independent measurement") + with patch.object(measure, "sources", return_value=inputs): + with self.assertRaises(measure.MeasurementError): + measure.receipt_sources(receipt, expected, root) + + def test_existing_output_is_never_rebuilt_or_reauthenticated(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + with self.assertRaises(measure.MeasurementError): + measure.prepare(root, root, "unused-cc", "unused-nm", "unused-readelf") + + def test_compiler_messages_must_select_one_actual_executable(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + first = root / "first" + second = root / "second" + first.write_bytes(b"first") + second.write_bytes(b"second") + def message(path: Path) -> str: + return json.dumps({"reason": "compiler-artifact", + "target": {"name": "native_conformance"}, "executable": str(path)}) + self.assertEqual(measure.compiled_test(message(first)), first) + for invalid in ("", message(root / "absent"), message(first) + "\n" + message(second)): + with self.subTest(value=invalid), self.assertRaises(measure.MeasurementError): + measure.compiled_test(invalid) + + def test_pin_is_compiled_then_removed_for_complete_source_gate(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + inputs: dict[str, measure.Json] = {"Cargo.lock": "frozen"} + receipt = self.fixture(root, inputs) + expected = measure.digest(receipt) + binary = root / "compiled-gate" + binary.write_bytes(b"compiled test") + compiler = root / "rustc" + compiler.write_bytes(b"measured compiler proxy") + compile_environment: list[dict[str, str]] = [] + runtime_environment: list[dict[str, str]] = [] + def compile_test(argv: list[str], *, cwd: Path, env: dict[str, str], stdin: int, + stdout: BinaryIO, stderr: BinaryIO, timeout: int) -> subprocess.CompletedProcess[bytes]: + self.assertIn("--no-run", argv) + self.assertIn("--target", argv) + self.assertEqual(argv[argv.index("--color") + 1], "never") + self.assertEqual(env["CARGO_TERM_COLOR"], "never") + self.assertNotIn("--cfg", argv) + self.assertEqual(cwd, root) + self.assertEqual(stdin, subprocess.DEVNULL) + compile_environment.append(dict(env)) + stderr.write(f"Running `{str(compiler)!r} --crate-name jett_driver`\n".encode()) + stdout.write((json.dumps({"reason": "compiler-artifact", + "target": {"name": "native_conformance"}, "executable": str(binary)}) + "\n").encode()) + return subprocess.CompletedProcess(argv, 0) + def execute_test(argv: list[str], directory: Path, environment: dict[str, str], + log: Path, timeout: int = 3600) -> str: + if argv == [str(compiler), "-vV"]: + self.assertNotIn(measure.PIN_ENV, environment) + text = f"host: {measure.TARGET}\n" + log.write_text(text, encoding="utf-8") + return text + self.assertEqual(argv[0], str(binary)) + self.assertEqual(argv[1], measure.TEST) + self.assertIn("--ignored", argv) + self.assertIn("--exact", argv) + self.assertEqual(directory, root) + runtime_environment.append(dict(environment)) + text = ("Resource native acceptance: 73 cases; 146 Source-deleted executions; " + "profiles=debug,release\ntest result: ok. 1 passed; 0 failed; 0 ignored;\n") + log.write_text(text, encoding="utf-8") + return text + def executable_for_test(name: str) -> Path: + return root / name + with patch.object(measure, "sources", return_value=inputs), \ + patch.object(measure, "executable", side_effect=executable_for_test), \ + patch.object(measure.subprocess, "run", side_effect=compile_test), \ + patch.object(measure, "logged", side_effect=execute_test), \ + patch.dict(os.environ, {}, clear=True): + measure.run_gate(root, receipt, expected, root / "fresh-gate-target") + self.assertEqual(compile_environment[0][measure.PIN_ENV], expected) + self.assertEqual(compile_environment[0]["RUSTC"], str(compiler)) + self.assertNotIn(measure.RECEIPT_ENV, compile_environment[0]) + self.assertNotIn(measure.PIN_ENV, runtime_environment[0]) + self.assertEqual(runtime_environment[0][measure.RECEIPT_ENV], str(receipt)) + accepted = measure.read_object(root / "gate-acceptance.json") + self.assertEqual(accepted["source_deleted_executions"], 146) + self.assertEqual(accepted["runtime_expected_pin_environment"], False) + + def test_old_future_or_mixed_receipt_wire_cannot_authenticate_current_sources(self) -> None: + for wires in ((2, 2), (4, 4), (3, 2), (1, 3)): + with self.subTest(wires=wires), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + inputs: dict[str, measure.Json] = {"Cargo.lock": "frozen"} + receipt = self.fixture(root, inputs) + record = measure.read_object(receipt) + rows = record["archives"] + self.assertIsInstance(rows, list) + if not isinstance(rows, list): + self.fail("fixture archive rows must be a list") + for row, wire in zip(rows, wires, strict=True): + self.assertIsInstance(row, dict) + if not isinstance(row, dict): + self.fail("fixture archive row must be an object") + row["layout_wire"] = wire + receipt.write_text(json.dumps(record), encoding="utf-8") + with patch.object(measure, "sources", return_value=inputs), \ + self.assertRaises(measure.MeasurementError): + measure.receipt_sources(receipt, measure.digest(receipt), root) + + +class CarrierContractTests(unittest.TestCase): + def fixture(self, root: Path) -> tuple[Path, Path]: + helper = root / "tools/measure_resource_native_archives.py" + helper.parent.mkdir(parents=True) + helper.write_bytes(Path(measure.__file__).read_bytes()) + copy_resource_export_fixture(root) + resource = root / "crates/jett_runtime/src/native_abi/resource" + carriers = resource / "leaves/carriers.rs" + registration = root / "crates/jett_runtime/src/resource_custody/registration.rs" + registration.parent.mkdir(parents=True) + registration.write_text("pub(crate) const NATIVE_RESOURCE_LAYOUT_WIRE_VERSION: u32 = 3;\n", + encoding="utf-8") + return registration, carriers + + def test_exact_current_wire_and_carrier_source_contract(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + self.fixture(root) + measure.checked_layout_wire(root) + leaves, private = measure.export_names(root) + self.assertEqual((len(leaves), len(private)), (55, 8)) + self.assertTrue(measure.CARRIER_EXPORTS <= leaves) + self.assertEqual(leaves & measure.STARTUP_EXPORTS, measure.STARTUP_EXPORTS) + + def test_old_future_duplicate_or_missing_runtime_wire_is_refused(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + registration, _ = self.fixture(root) + original = registration.read_text(encoding="utf-8") + for invalid in (original.replace("= 3;", "= 2;"), original.replace("= 3;", "= 4;"), + original + original, ""): + registration.write_text(invalid, encoding="utf-8") + with self.subTest(invalid=invalid), self.assertRaises(measure.MeasurementError): + measure.checked_layout_wire(root) + + def test_missing_duplicate_and_foreign_carrier_names_are_refused(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, carriers = self.fixture(root) + original = carriers.read_text(encoding="utf-8") + first, *rest = original.splitlines() + for invalid in ("\n".join(rest), original + "\n" + first, + original.replace("carrier_adapt(", "carrier_foreign(")): + carriers.write_text(invalid, encoding="utf-8") + with self.subTest(invalid=invalid), self.assertRaises(measure.MeasurementError): + measure.export_names(root) + carriers.write_text(original, encoding="utf-8") + legacy = carriers.parent.parent / "leaves.rs" + original_legacy = legacy.read_text(encoding="utf-8") + first_name = sorted(measure.export_names(root)[0] - measure.CARRIER_EXPORTS - measure.STARTUP_EXPORTS)[0] + legacy.write_text(original_legacy.replace(first_name + "(", + "jett_rt_v1_resource_carrier_adapt("), encoding="utf-8") + with self.assertRaises(measure.MeasurementError): + measure.export_names(root) + + def test_stale_wire_or_symbols_refused_before_tools_and_output(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + registration, carriers = self.fixture(root) + output = root / "fresh-measurement" + original = registration.read_text(encoding="utf-8") + registration.write_text(original.replace("= 3;", "= 2;"), encoding="utf-8") + with patch.object(measure, "executable") as executable, \ + self.assertRaises(measure.MeasurementError): + measure.prepare(root, output, "unused-cc", "unused-nm", "unused-readelf") + executable.assert_not_called() + self.assertFalse(output.exists()) + registration.write_text(original, encoding="utf-8") + carriers.write_text("", encoding="utf-8") + with patch.object(measure, "executable") as executable, \ + self.assertRaises(measure.MeasurementError): + measure.prepare(root, output, "unused-cc", "unused-nm", "unused-readelf") + executable.assert_not_called() + self.assertFalse(output.exists()) + + def test_foreign_executing_helper_is_refused(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + self.fixture(root) + measure.checked_measurement_source(root) + (root / "tools/measure_resource_native_archives.py").write_bytes(b"foreign helper") + with self.assertRaises(measure.MeasurementError): + measure.checked_measurement_source(root) + + def test_guards_are_bound_to_the_source_snapshot_before_output_or_build(self) -> None: + for changed in ("wire", "exports", "startup_module", "startup_signature", "startup_exports"): + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + registration, carriers = self.fixture(root) + output = root / "fresh-measurement" + def snapshot_mutation(repo: Path, complete: bool = False) -> dict[str, measure.Json]: + self.assertEqual(repo, root) + self.assertFalse(complete) + if changed == "wire": + registration.write_text("pub(crate) const NATIVE_RESOURCE_LAYOUT_WIRE_VERSION: u32 = 2;\n", + encoding="utf-8") + elif changed == "exports": + carriers.write_text("", encoding="utf-8") + elif changed == "startup_module": + owner = carriers.parent.parent.with_suffix(".rs") + owner.write_text(owner.read_text(encoding="utf-8").replace( + "mod program_entry;", "#[cfg(test)]\nmod program_entry;"), encoding="utf-8") + else: + child = carriers.parent.parent / "program_entry.rs" + text = child.read_text(encoding="utf-8") + if changed == "startup_signature": + text = text.replace("out_attempt: *mut u64,", "out_attempt: *mut u32,") + else: + text = text.replace("jett_rt_v1_resource_program_begin(", + "jett_rt_v1_resource_program_foreign(") + child.write_text(text, encoding="utf-8") + return {} + with patch.object(measure, "sources", side_effect=snapshot_mutation), \ + patch.object(measure, "executable", return_value=root / "unused-tool"), \ + patch.object(measure, "build_environment", return_value={}), \ + patch.object(measure, "toolchain") as toolchain, \ + self.assertRaises(measure.MeasurementError): + measure.prepare(root, output, "unused-cc", "unused-nm", "unused-readelf") + toolchain.assert_not_called() + self.assertFalse(output.exists()) + + +class StartupContractTests(unittest.TestCase): + def test_module_missing_duplicate_conditional_or_redirected_is_refused(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + owner, _ = copy_resource_export_fixture(root) + original = owner.read_text(encoding="utf-8") + for replacement in ("", "mod program_entry;\nmod program_entry;", + "#[cfg(test)]\nmod program_entry;", + '#[path = "other.rs"]\nmod program_entry;', + "pub mod program_entry;"): + owner.write_text(original.replace("mod program_entry;", replacement), encoding="utf-8") + with self.subTest(module=replacement), self.assertRaises(measure.MeasurementError): + measure.export_names(root) + + def test_missing_child_and_inner_conditional_module_are_refused(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, child = copy_resource_export_fixture(root) + original = child.read_text(encoding="utf-8") + child.write_text("#![cfg(test)]\n" + original, encoding="utf-8") + with self.assertRaises(measure.MeasurementError): + measure.export_names(root) + child.unlink() + with self.assertRaises(measure.MeasurementError): + measure.export_names(root) + + def test_missing_duplicate_foreign_conditional_and_mangled_exports_are_refused(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, child = copy_resource_export_fixture(root) + original = child.read_text(encoding="utf-8") + for name in sorted(measure.STARTUP_EXPORTS): + prefix = f'pub unsafe extern "C" fn {name}(' + start = original.index(prefix) + end = original.index("{", start) + header = original[start:end] + for invalid in ( + original.replace(prefix, prefix.removeprefix("pub "), 1), + original + '\n#[unsafe(no_mangle)]\n' + header + '{}\n', + original.replace(name + "(", "jett_rt_v1_resource_program_foreign(", 1), + original.replace(prefix, "#[cfg(test)]\n" + prefix, 1), + original[:start].removesuffix("#[unsafe(no_mangle)]\n") + original[start:], + original.replace(prefix, prefix.replace('extern "C"', 'extern "Rust"'), 1), + ): + child.write_text(invalid, encoding="utf-8") + with self.subTest(name=name, source=invalid), self.assertRaises(measure.MeasurementError): + measure.export_names(root) + + def test_every_startup_argument_and_return_type_is_exact(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, child = copy_resource_export_fixture(root) + original = child.read_text(encoding="utf-8") + for name in sorted(measure.STARTUP_EXPORTS): + start = original.index(f'pub unsafe extern "C" fn {name}(') + end = original.index("{", start) + header = original[start:end] + lines = header.splitlines(keepends=True) + for index, line in enumerate(lines): + if ": " not in line and "-> JettRuntimeStatusV1" not in line: + continue + changed = list(lines) + if ": " in line: + parameter = line.split(": ", 1)[0] + changed[index] = parameter + ": usize,\n" + else: + changed[index] = line.replace("JettRuntimeStatusV1", "u32") + child.write_text(original[:start] + "".join(changed) + original[end:], encoding="utf-8") + with self.subTest(name=name, parameter=index), self.assertRaises(measure.MeasurementError): + measure.export_names(root) + + def test_startup_cannot_be_relocated_into_old_leaf_inventory(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, child = copy_resource_export_fixture(root) + leaves = child.parent / "leaves.rs" + original = leaves.read_text(encoding="utf-8") + names = sorted(measure.export_names(root)[0] - measure.CARRIER_EXPORTS - measure.STARTUP_EXPORTS) + leaves.write_text(original.replace(names[0] + "(", + "jett_rt_v1_resource_program_begin(", 1), encoding="utf-8") + with self.assertRaises(measure.MeasurementError): + measure.export_names(root) + + def test_invalid_startup_is_refused_before_tool_discovery_and_output(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, carriers = CarrierContractTests().fixture(root) + startup = carriers.parent.parent / "program_entry.rs" + startup.write_text(startup.read_text(encoding="utf-8").replace( + "out_attempt: *mut u64,", "out_attempt: *mut u32,"), encoding="utf-8") + output = root / "fresh-measurement" + with patch.object(measure, "executable") as executable, \ + self.assertRaises(measure.MeasurementError): + measure.prepare(root, output, "unused-cc", "unused-nm", "unused-readelf") + executable.assert_not_called() + self.assertFalse(output.exists()) + + def test_receipt_authentication_checks_startup_contract(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + inputs: dict[str, measure.Json] = {"Cargo.lock": "frozen"} + receipt = ReceiptAndGateTests().fixture(root, inputs) + child = root / "crates/jett_runtime/src/native_abi/resource/program_entry.rs" + child.write_text(child.read_text(encoding="utf-8").replace( + "out_attempt: *mut u64,", "out_attempt: *mut u32,"), encoding="utf-8") + with patch.object(measure, "sources", return_value=inputs), \ + self.assertRaises(measure.MeasurementError): + measure.receipt_sources(receipt, measure.digest(receipt), root) + + +class MsvcObservationTests(unittest.TestCase): + flags = ["-C", "target-feature=+crt-static", "-C", "debuginfo=0"] + libraries = ["kernel32.lib", "advapi32.lib", "ntdll.lib", "userenv.lib", + "ws2_32.lib", "dbghelp.lib", "bcrypt.lib", "advapi32.lib", + "libvcruntime.lib", "libucrt.lib", "libcmt.lib"] + + raw_libraries = ["bcrypt.lib", "advapi32.lib", "gdi32.lib", "kernel32.lib", "msimg32.lib", + "opengl32.lib", "user32.lib", "winspool.lib", "kernel32.lib", "ntdll.lib", + "userenv.lib", "ws2_32.lib", "dbghelp.lib", "/defaultlib:libcmt"] + @staticmethod + def runtime_command(compiler: Path) -> str: + return (f'Running `"{compiler}" --crate-name jett_runtime --crate-type rlib ' + '--crate-type staticlib --target x86_64-pc-windows-msvc ' + '--cfg test --cfg jett_resource_native_test_archive ' + '-C target-feature=+crt-static -C debuginfo=0 --print=native-static-libs`') + + @staticmethod + def coff_observation() -> str: + # llvm-readobj prints one complete target record for every archive member. + return ('File: runtime.lib(runtime.o)\nFormat: COFF-x86-64\nArch: x86_64\n' + 'AddressSize: 64bit\nImageFileHeader {\n Machine: IMAGE_FILE_MACHINE_AMD64 (0x8664)\n}\n' + 'Directive(s): /DEFAULTLIB:libcmt /DEFAULTLIB:libvcruntime /DEFAULTLIB:libucrt\n' + 'File: runtime.lib(compiler_builtins.o)\nFormat: COFF-x86-64\nArch: x86_64\n' + 'AddressSize: 64bit\nImageFileHeader {\n Machine: IMAGE_FILE_MACHINE_AMD64 (0x8664)\n}\n') + + @staticmethod + def rust_archive_without_directives() -> str: + ordinary = MsvcObservationTests.coff_observation().replace( + "Directive(s): /DEFAULTLIB:libcmt /DEFAULTLIB:libvcruntime /DEFAULTLIB:libucrt\n", "") + imported = ("File: kernel32.dll\nFormat: COFF-import-file-x86-64\nType: code\n" + "Name type: name\nExport name: CloseHandle\n" + "Symbol: __imp_CloseHandle\nSymbol: CloseHandle\n") + return ordinary + imported + def test_windows_argv_preserves_paths_and_quoted_cfg_values(self) -> None: + command = (r'"C:\Program Files\Rust\rustc.exe" --cfg "feature=\"native-tests\"" ' + r'-L "C:\target with spaces\deps" --cfg test') + self.assertEqual(measure.windows_arguments(command), [ + r"C:\Program Files\Rust\rustc.exe", "--cfg", 'feature="native-tests"', + "-L", r"C:\target with spaces\deps", "--cfg", "test"]) + with self.assertRaises(measure.MeasurementError): + measure.windows_arguments(r'"C:\Program Files\Rust\rustc.exe') + + def test_msvc_library_observation_keeps_order_and_duplicates(self) -> None: + text = ("note: native-static-libs: kernel32.lib advapi32.lib ntdll.lib userenv.lib\n" + " ws2_32.lib dbghelp.lib bcrypt.lib advapi32.lib\n" + " libvcruntime.lib libucrt.lib libcmt.lib\nFinished build\n") + self.assertEqual(measure.msvc_native_libraries(text), self.libraries) + status = text.replace("Finished build", " Finished `dev` profile [optimized] target(s) in 8.3s") + self.assertEqual(measure.msvc_native_libraries(status), self.libraries) + self.assertEqual(self.libraries.count("advapi32.lib"), 2) + + def test_msvc_library_observation_refuses_foreign_crt_or_linker_options(self) -> None: + good = "note: native-static-libs: " + " ".join(self.libraries) + "\n" + invalid = ["", good + good, good.replace("libcmt.lib", "msvcrt.lib"), good.replace("libcmt.lib", ""), + good.replace("libucrt.lib", "ucrt.lib"), good + " /NODEFAULTLIB:libcmt\n", + good + " jett_runtime.lib\n", good + " jett_native_launcher.lib\n", + good.replace("libcmt.lib", "libcmt.a"), good + " @response.rsp\n"] + for text in invalid: + with self.subTest(text=text), self.assertRaises(measure.MeasurementError): + measure.msvc_native_libraries(text) + + def test_selected_windows_compiler_owns_both_private_cfgs(self) -> None: + with tempfile.TemporaryDirectory(prefix="compiler identity ") as temporary: + compiler = Path(temporary) / "measured rustc.exe" + command = self.runtime_command(compiler) + dependency = (f'Running `"{compiler}" --crate-name dependency --crate-type rlib ' + '--target x86_64-pc-windows-msvc -C target-feature=+crt-static ' + '-C debuginfo=0 --cfg "feature=\\\"jett_resource_native_test_archive\\\""`') + measure.msvc_selected_cfgs(dependency + "\n" + command, compiler) + invalid = [ + command + "\n" + command, + command.replace("--crate-name jett_runtime", "--crate-name dependency"), + command.replace("--cfg test ", ""), + command.replace("--cfg test ", "--cfg test --cfg test "), + command.replace("--crate-type staticlib ", ""), + command.replace("--print=native-static-libs", "--test --print=native-static-libs"), + command.replace("target-feature=+crt-static", "target-feature=-crt-static"), + command.replace("debuginfo=0", "debuginfo=2"), + command.replace(str(compiler), str(compiler.parent / "foreign rustc.exe")), + command.replace('Running `"', 'Running `wrapper.exe "', 1), + command + "\n" + dependency.replace('--cfg "feature=\\\"jett_resource_native_test_archive\\\""', + '--cfg jett_resource_native_test_archive'), + ] + for text in invalid: + with self.subTest(text=text), self.assertRaises(measure.MeasurementError): + measure.msvc_selected_cfgs(text, compiler) + + def test_static_crt_fingerprint_requires_observed_flags_not_an_asserted_mode(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + with self.assertRaises(measure.MeasurementError): + measure.msvc_fingerprint_records(root) + directory = root / "jett_runtime-hash" + directory.mkdir() + fingerprint = directory / "lib-jett_runtime.json" + fingerprint.write_text(json.dumps({"rustflags": self.flags}), encoding="utf-8") + records = measure.msvc_fingerprint_records(root) + self.assertEqual(len(records), 1) + self.assertEqual(records[0], {**measure.artifact(fingerprint), + "raw_json_utf8": fingerprint.read_text(encoding="utf-8")}) + for flags in ([], self.flags[:-2], ["--cfg", "test", *self.flags], + ["-C", "target-feature=-crt-static", "-C", "debuginfo=0"], + ["-C", "target-feature=+crt-static", "-C", "debuginfo=2"]): + fingerprint.write_text(json.dumps({"rustflags": flags}), encoding="utf-8") + with self.subTest(flags=flags), self.assertRaises(measure.MeasurementError): + measure.msvc_fingerprint_records(root) + + def test_every_coff_member_has_the_measured_target_and_static_crt_directives(self) -> None: + good = self.coff_observation() + measure.checked_coff_archive(good) + measure.checked_coff_archive(good + 'Directive(s): /FAILIFMISMATCH:"RuntimeLibrary=MT_StaticRelease"\n') + second = good.index("File: runtime.lib(compiler_builtins.o)") + invalid = [ + "", good[:second] + good[second:].replace("COFF-x86-64", "COFF-ARM64"), + good[:second] + good[second:].replace("x86_64", "aarch64"), + good[:second] + good[second:].replace("64bit", "32bit"), + good[:second] + good[second:].replace("IMAGE_FILE_MACHINE_AMD64 (0x8664)", + "IMAGE_FILE_MACHINE_I386 (0x14C)"), + good[:second] + good[second:].replace(" Machine: IMAGE_FILE_MACHINE_AMD64 (0x8664)\n", ""), + good + "Directive(s): /DEFAULTLIB:msvcrt\n", + good + 'Directive(s): /DEFAULTLIB:"MSVCRTD.LIB"\n', + good + "Directive(s): /DEFAULTLIB:ucrt\n", + good + "Directive(s): /NODEFAULTLIB:libcmt\n", + good + 'Directive(s): /FAILIFMISMATCH:"RuntimeLibrary=MD_DynamicRelease"\n', + good + 'Directive(s): /FAILIFMISMATCH:"RuntimeLibrary=MTd_StaticDebug"\n', + ] + for text in invalid: + with self.subTest(text=text), self.assertRaises(measure.MeasurementError): + measure.checked_coff_archive(text) + + def test_msvc_environment_keeps_sdk_lib_verbatim_and_blocks_global_cfg_injection(self) -> None: + sdk = r"C:\VS Libraries\x64;C:\SDK\ucrt\x64;C:\SDK\um\x64;" + with patch.dict(os.environ, {"LIB": sdk, "CARGO_TERM_COLOR": "always"}, clear=True): + environment = measure.msvc_build_environment() + self.assertEqual(environment["LIB"], sdk) + self.assertEqual(environment["CARGO_INCREMENTAL"], "0") + self.assertEqual(environment["CARGO_TERM_COLOR"], "never") + self.assertEqual(environment["CARGO_PROFILE_DEV_DEBUG"], "0") + self.assertEqual(environment["CARGO_PROFILE_RELEASE_DEBUG"], "0") + for name, value in (("RUSTFLAGS", "--cfg test"), + ("CARGO_ENCODED_RUSTFLAGS", "--cfg\x1ftest"), + ("CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS", "-Ctarget-feature=-crt-static"), + ("RUSTC_WRAPPER", "wrapper.exe"), ("RUSTC", "foreign-rustc.exe")): + with self.subTest(name=name), patch.dict(os.environ, {"LIB": sdk, name: value}, clear=True): + with self.assertRaises(measure.MeasurementError): + measure.msvc_build_environment() + + def test_coff_system_import_members_need_their_own_target_and_dll_evidence(self) -> None: + # LLVM import members omit the ordinary Arch/AddressSize/FileHeader rows. + imported = ("File: kernel32.dll\nFormat: COFF-import-file-x86-64\nType: code\n" + "Name type: name\nExport name: CloseHandle\n" + "Symbol: __imp_CloseHandle\nSymbol: CloseHandle\n") + ordinary = self.coff_observation() + measure.checked_coff_archive(ordinary + imported) + measured_dlls = ("advapi32.dll", "api-ms-win-core-handle-l1-1-0.dll", + "api-ms-win-core-synch-l1-2-0.dll", "bcp47mrm.dll", "bcryptprimitives.dll", + "elscore.dll", "gdi32.dll", "icu.dll", "icuin.dll", "icuuc.dll", "kernel32.dll", + "normaliz.dll", "ntdll.dll", "oleaut32.dll", "user32.dll", "usp10.dll") + measure.checked_coff_archive(ordinary + "".join(imported.replace("kernel32.dll", name) + for name in measured_dlls)) + invalid = ( + imported.replace("COFF-import-file-x86-64", "COFF-import-file-i386"), + imported.replace("COFF-import-file-x86-64", "COFF-import-file-arm64"), + imported.replace("COFF-import-file-x86-64", "unknown-import-format"), + imported.replace("Type: code", "Type: data"), + imported.replace("Name type: name", "Name type: ordinal"), + imported.replace("kernel32.dll", "ucrtbase.dll"), + imported.replace("kernel32.dll", "vcruntime140.dll"), + imported.replace("kernel32.dll", "msvcrt.dll"), + imported.replace("kernel32.dll", "foreign-runtime.dll"), + ) + for value in invalid: + with self.subTest(imported=value), self.assertRaises(measure.MeasurementError): + measure.checked_coff_archive(ordinary + value) + + def test_mixed_rust_archive_with_no_directive_records_has_valid_coff_evidence(self) -> None: + text = self.rust_archive_without_directives() + self.assertNotIn("Directive(s):", text) + measure.checked_coff_archive(text) + + def test_observed_directives_need_no_embedded_crt_library_but_must_not_conflict(self) -> None: + text = self.rust_archive_without_directives() + accepted = ("/DEFAULTLIB:kernel32", '/FAILIFMISMATCH:"_ITERATOR_DEBUG_LEVEL=0"', + '/FAILIFMISMATCH:"RuntimeLibrary=MT_StaticRelease"', "/DEFAULTLIB:libvcruntime") + rejected = ("/DEFAULTLIB:msvcrt", "/DEFAULTLIB:ucrt", "/DEFAULTLIB:libcmtd", + "/NODEFAULTLIB:libcmt", '/FAILIFMISMATCH:"RuntimeLibrary=MD_DynamicRelease"') + for directive in accepted: + with self.subTest(directive=directive): + observed = text.replace("File: kernel32.dll", f"Directive(s): {directive}\nFile: kernel32.dll") + self.assertNotIn("/DEFAULTLIB:libcmt", observed) + measure.checked_coff_archive(observed) + for directive in rejected: + with self.subTest(directive=directive), self.assertRaises(measure.MeasurementError): + measure.checked_coff_archive(text.replace("File: kernel32.dll", + f"Directive(s): {directive}\nFile: kernel32.dll")) + + def test_actual_native_library_vector_preserves_duplicate_and_raw_crt_flag(self) -> None: + text = ("note: native-static-libs: bcrypt.lib advapi32.lib gdi32.lib kernel32.lib " + "msimg32.lib opengl32.lib user32.lib winspool.lib kernel32.lib ntdll.lib " + "userenv.lib ws2_32.lib dbghelp.lib /defaultlib:libcmt\n") + self.assertEqual(measure.msvc_native_libraries(text), self.raw_libraries) + self.assertEqual(self.raw_libraries.count("kernel32.lib"), 2) + self.assertEqual(self.raw_libraries[-1], "/defaultlib:libcmt") + + def test_wrapped_safe_defaultlib_flag_is_validated_without_rewriting_it(self) -> None: + for flag in ("/defaultlib:libcmt", "/DEFAULTLIB:libcmt.lib", "/DefaultLib:LiBcMt.LiB"): + with self.subTest(flag=flag): + text = ("note: native-static-libs: " + " ".join(self.raw_libraries[:-1]) + + "\n " + flag + "\n Finished `dev` profile [optimized] target(s) in 8.3s\n") + self.assertEqual(measure.msvc_native_libraries(text), [*self.raw_libraries[:-1], flag]) + + def test_defaultlib_tokens_cannot_hide_dynamic_crt_paths_or_unknown_switches(self) -> None: + invalid = ("/defaultlib:msvcrt", "/DEFAULTLIB:UCRT.LIB", "/defaultlib:libcmtd", + "/defaultlib:vcruntime", "/defaultlib:libcmt.a", "/defaultlib:libcmt.lib.exe", + r"/defaultlib:C:\SDK\libcmt.lib", "/defaultlib:../libcmt.lib", r"/LIBPATH:C:\SDK", + "@native.rsp", "/WHOLEARCHIVE:libcmt.lib", "-defaultlib:libcmt", "/defaultlib:") + for flag in invalid: + for separator in (" ", "\n "): + with self.subTest(flag=flag, wrapped=separator != " "), self.assertRaises(measure.MeasurementError): + measure.msvc_native_libraries("note: native-static-libs: " + + " ".join(self.raw_libraries[:-1]) + separator + flag + "\n") + with self.assertRaises(measure.MeasurementError): + measure.msvc_native_libraries("note: native-static-libs: " + " ".join(self.raw_libraries[:-1]) + + "\n /defaultlib:libcmt @native.rsp\n") + +class MsvcReceiptTests(unittest.TestCase): + @staticmethod + def rewrite(path: Path, value: measure.Json) -> None: + path.write_text(json.dumps(value), encoding="utf-8") + + @staticmethod + def rows(receipt: Path) -> list[dict[str, measure.Json]]: + value = measure.read_object(receipt).get("archives") + if not isinstance(value, list) or any(not isinstance(row, dict) for row in value): + raise AssertionError("fixture must retain archive objects") + return [row for row in value if isinstance(row, dict)] + + def fixture(self, root: Path) -> tuple[Path, dict[str, str]]: + # These are synthetic measurements, never copies of built Cargo artifacts. + helper = root / "tools/measure_resource_native_archives.py" + helper.parent.mkdir(parents=True) + helper.write_bytes(Path(measure.__file__).read_bytes()) + runtime = root / "crates/jett_runtime" + resource = runtime / "src/native_abi/resource" + copy_resource_export_fixture(root) + leaves, private = measure.export_names(measure.ROOT) + for path, names in ( + (resource / "leaves.rs", leaves - measure.CARRIER_EXPORTS - measure.STARTUP_EXPORTS), + (resource / "leaves/carriers.rs", measure.CARRIER_EXPORTS), + (resource / "test_archive_api.rs", private), + ): + path.write_text("\n".join(f'pub unsafe extern "C" fn {name}() {{}}' for name in sorted(names)), + encoding="utf-8") + registration = runtime / "src/resource_custody/registration.rs" + registration.parent.mkdir(parents=True) + registration.write_text("pub(crate) const NATIVE_RESOURCE_LAYOUT_WIRE_VERSION: u32 = 3;\n", + encoding="utf-8") + for path in (root / "Cargo.lock", root / "Cargo.toml", runtime / "Cargo.toml"): + path.write_bytes(b"synthetic retained source fixture") + compiler = root / "tool chain/rustc.exe" + compiler.parent.mkdir() + compiler.write_bytes(b"measured compiler fixture") + linker = root / "tool chain/link.exe" + linker.write_bytes(b"measured linker fixture") + environment = {"LIB": str(root / "SDK Libraries/x64") + ";" + str(root / "VS Libraries/x64") + ";", + "VCINSTALLDIR": str(root / "VS Libraries"), "VSCMD_ARG_TGT_ARCH": "x64", + "PATH": str(linker.parent)} + inputs = measure.sources(root) + archives: list[measure.Json] = [] + for profile in ("debug", "release"): + archive = root / f"{profile}-runtime.lib" + archive.write_bytes(("synthetic " + profile + " archive fixture").encode()) + fingerprint = root / f"{profile}-fingerprint.json" + self.rewrite(fingerprint, {"rustflags": MsvcObservationTests.flags}) + build_log = root / f"{profile}-build.log" + build_log.write_text(MsvcObservationTests.runtime_command(compiler).replace("--print=native-static-libs", + f'-C "linker={linker}" --print=native-static-libs') + "\n" + + "note: native-static-libs: " + " ".join(MsvcObservationTests.libraries) + "\n", + encoding="utf-8") + symbols = root / f"{profile}-symbols.log" + symbols.write_text("runtime.lib(runtime.o):\n" + "\n".join( + f"{name} T 0 10" for name in sorted(leaves | private | {"main"})), encoding="utf-8") + coff = root / f"{profile}-coff.log" + coff.write_text(MsvcObservationTests.coff_observation(), encoding="utf-8") + build = root / f"{profile}-build.json" + measure.write_json(build, {"profile": profile, "target": "x86_64-pc-windows-msvc", + "runtime_sources": inputs, "archive": measure.artifact(archive), + "cfgs_selected_package_only": ["test", "jett_resource_native_test_archive"], + "measurement_script": measure.artifact(Path(measure.__file__)), + "cargo_build_log": measure.artifact(build_log), "build_environment": { + "CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS": "-C target-feature=+crt-static -C debuginfo=0", + "CARGO_INCREMENTAL": "0", "CARGO_PROFILE_DEV_DEBUG": "0", "CARGO_PROFILE_RELEASE_DEBUG": "0"}, + "toolchain": { + "rustc": {"invocation_path": str(compiler), "executable": measure.artifact(compiler)}}}) + libraries = root / f"{profile}-libraries.json" + measure.write_json(libraries, {"arguments": MsvcObservationTests.libraries, + "actual_print_log": measure.artifact(build_log), "order_and_duplication_preserved": True}) + crt = root / f"{profile}-crt.json" + measure.write_json(crt, {"mode": "static", "linker_path": str(linker), + "linker_executable": measure.artifact(linker), "sdk_LIB": environment["LIB"], + "developer_shell": {"VCINSTALLDIR": environment["VCINSTALLDIR"], "VSCMD_ARG_TGT_ARCH": "x64"}, + "actual_target_scoped_rustflags": MsvcObservationTests.flags, + "cargo_fingerprints": [{**measure.artifact(fingerprint), + "raw_json_utf8": fingerprint.read_text(encoding="utf-8")}], + "complete_coff_archive_inventory": measure.artifact(coff), "coff_directive_record_count": 1, + "complete_symbol_inventory": measure.artifact(symbols), + "native_static_libs": measure.artifact(libraries)}) + archives.append({"profile": profile, "target": "x86_64-pc-windows-msvc", "runtime_abi": 1, + "layout_wire": 3, "private_cfgs": ["test", "jett_resource_native_test_archive"], + "crt_mode": "static", "archive_path": str(archive), "archive_sha256": measure.digest(archive), + "linker_path": str(linker), "native_library_args": MsvcObservationTests.libraries, + "build_receipt": measure.artifact(build), "native_static_libs_receipt": measure.artifact(libraries), + "crt_receipt": measure.artifact(crt)}) + receipt = root / "receipt.json" + measure.write_json(receipt, {"version": 1, "archives": archives}) + return receipt, environment + + def refresh_artifacts(self, value: measure.Json) -> None: + if isinstance(value, list): + for item in value: + self.refresh_artifacts(item) + elif isinstance(value, dict): + for item in value.values(): + self.refresh_artifacts(item) + path = value.get("path") + if isinstance(path, str) and isinstance(value.get("sha256"), str): + file = Path(path) + if file.suffix == ".json": + record = measure.read_object(file) + self.refresh_artifacts(record) + self.rewrite(file, record) + value["sha256"] = measure.digest(file) + if "raw_json_utf8" in value: + value["raw_json_utf8"] = file.read_text(encoding="utf-8") + + def reauthenticate(self, receipt: Path) -> str: + record = measure.read_object(receipt) + self.refresh_artifacts(record) + self.rewrite(receipt, record) + return measure.digest(receipt) + + def verify(self, receipt: Path, root: Path, environment: dict[str, str], expected: str | None = None) -> None: + with patch.dict(os.environ, environment, clear=True), \ + patch.object(measure.shutil, "which", return_value=str(root / "tool chain/link.exe")): + measure.receipt_sources(receipt, expected or measure.digest(receipt), root, + target="x86_64-pc-windows-msvc") + + def test_complete_msvc_receipt_retains_current_exports_and_sdk_evidence(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.fixture(root) + self.verify(receipt, root, environment) + self.assertEqual([row["profile"] for row in self.rows(receipt)], ["debug", "release"]) + self.assertEqual(self.rows(receipt)[0]["native_library_args"], MsvcObservationTests.libraries) + leaves, private = measure.export_names(root) + self.assertEqual((len(leaves - measure.CARRIER_EXPORTS - measure.STARTUP_EXPORTS), + len(measure.CARRIER_EXPORTS), len(leaves & measure.STARTUP_EXPORTS), len(private)), + (37, 13, 5, 8)) + + def test_receipt_pin_and_actual_runtime_or_helper_source_changes_are_refused(self) -> None: + for changed in ("Cargo.lock", "tools/measure_resource_native_archives.py", + "crates/jett_runtime/src/native_abi/resource/leaves.rs"): + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.fixture(root) + expected = measure.digest(receipt) + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment, "0" * 64) + (root / changed).write_bytes(b"changed current source after measurement") + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment, expected) + + def test_sdk_lib_and_developer_shell_cannot_change_after_measurement(self) -> None: + for name, replacement in (("LIB", "same directories;different exact vector"), + ("VSCMD_ARG_TGT_ARCH", "x86"), ("VCINSTALLDIR", "foreign installation")): + with self.subTest(name=name), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.fixture(root) + environment[name] = environment[name].replace(";", ";;", 1) if name == "LIB" else replacement + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment) + + def test_rehashed_forged_observations_do_not_prove_static_crt_or_compiler_identity(self) -> None: + corruptions = { + "fingerprint": ("debug-fingerprint.json", '{"rustflags":[]}'), + "coff-crt": ("debug-coff.log", MsvcObservationTests.coff_observation() + "Directive(s): /DEFAULTLIB:msvcrt\n"), + "coff-member": ("debug-coff.log", MsvcObservationTests.coff_observation().replace("COFF-x86-64", "COFF-ARM64", 1)), + "symbols": ("debug-symbols.log", "main T 0 10\nmain T 0 10\n"), + } + for name, (filename, text) in corruptions.items(): + with self.subTest(name=name), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.fixture(root) + (root / filename).write_text(text, encoding="utf-8") + if filename == "debug-coff.log": + crt_path = root / "debug-crt.json" + crt = measure.read_object(crt_path) + crt["coff_directive_record_count"] = text.count("Directive(s):") + self.rewrite(crt_path, crt) + expected = self.reauthenticate(receipt) + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment, expected) + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.fixture(root) + build_log = root / "debug-build.log" + build_log.write_text(build_log.read_text(encoding="utf-8").replace('Running `"', 'Running `wrapper.exe "'), + encoding="utf-8") + expected = self.reauthenticate(receipt) + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment, expected) + + def test_rehashed_receipt_cannot_change_order_duplicates_or_profile_contract(self) -> None: + changes: tuple[tuple[str, measure.Json], ...] = ( + ("native_library_args", list(reversed(MsvcObservationTests.libraries))), + ("native_library_args", list(dict.fromkeys(MsvcObservationTests.libraries))), + ("profile", "release"), ("target", "x86_64-unknown-linux-gnu"), ("runtime_abi", 2), + ("layout_wire", 2), ("private_cfgs", ["test"]), ("crt_mode", "dynamic")) + for field, value in changes: + with self.subTest(field=field, value=value), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.fixture(root) + record = measure.read_object(receipt) + archives = record.get("archives") + if not isinstance(archives, list) or not isinstance(archives[0], dict): + self.fail("fixture must retain archive objects") + archives[0][field] = value + self.rewrite(receipt, record) + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment) + + def test_changed_nested_archive_linker_or_observation_cannot_keep_original_pin(self) -> None: + for name in ("debug-runtime.lib", "tool chain/link.exe", "debug-build.log", "debug-fingerprint.json"): + with self.subTest(name=name), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.fixture(root) + expected = measure.digest(receipt) + (root / name).write_bytes(b"changed independent artifact") + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment, expected) + + def test_inherited_path_must_select_the_same_linker_without_an_override(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, environment = self.fixture(root) + linker = root / "tool chain/link.exe" + with patch.object(measure.shutil, "which", return_value=str(linker)) as which: + measure.checked_msvc_environment(environment, linker) + which.assert_called_once_with("link.exe", path=environment["PATH"]) + with patch.object(measure.shutil, "which", return_value=str(root / "foreign-link.exe")), \ + self.assertRaises(measure.MeasurementError): + measure.checked_msvc_environment(environment, linker) + environment["CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER"] = str(root / "foreign-link.exe") + with patch.object(measure.shutil, "which", return_value=str(linker)), \ + self.assertRaises(measure.MeasurementError): + measure.checked_msvc_environment(environment, linker) + + def test_existing_output_and_stale_source_contract_stop_before_tools(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + self.fixture(root) + with patch.object(measure, "executable") as executable, self.assertRaises(measure.MeasurementError): + measure.prepare_msvc(root, root) + executable.assert_not_called() + registration = root / "crates/jett_runtime/src/resource_custody/registration.rs" + registration.write_text("pub(crate) const NATIVE_RESOURCE_LAYOUT_WIRE_VERSION: u32 = 2;\n", + encoding="utf-8") + output = root / "fresh-output" + with patch.object(measure, "executable") as executable, self.assertRaises(measure.MeasurementError): + measure.prepare_msvc(root, output) + executable.assert_not_called() + self.assertFalse(output.exists()) + + def test_sources_changed_by_build_stop_before_archive_observation(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, environment = self.fixture(root) + inputs = measure.sources(root) + leaves, private = measure.export_names(root) + directory = root / "measurement" + directory.mkdir() + tools = measure.MsvcTools(root / "tool chain/cargo.exe", root / "tool chain/rustc.exe", + root / "tool chain/llvm-nm.exe", root / "tool chain/llvm-readobj.exe", + root / "tool chain/link.exe") + def changed_build(argv: list[str], repo: Path, env: dict[str, str], + log: Path, timeout: int = 3600) -> str: + self.assertEqual(argv[1], "rustc") + self.assertEqual(repo, root) + self.assertEqual(env["LIB"], environment["LIB"]) + text = MsvcObservationTests.runtime_command(tools.rustc) + log.write_text(text, encoding="utf-8") + (root / "Cargo.lock").write_bytes(b"changed during the selected runtime build") + return text + with patch.object(measure, "logged", side_effect=changed_build) as logged, \ + self.assertRaises(measure.MeasurementError): + measure.measure_msvc_profile(tools, root, directory, "debug", environment, + inputs, {}, leaves, private) + self.assertEqual(logged.call_count, 1) + self.assertFalse((directory / "debug-symbols.log").exists()) + self.assertFalse((directory / "receipt.json").exists()) + + def test_rehashed_build_cannot_redirect_archive_or_assert_different_private_cfgs(self) -> None: + for change in ("archive", "cfgs"): + with self.subTest(change=change), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.fixture(root) + build_path = root / "debug-build.json" + build = measure.read_object(build_path) + if change == "archive": + build["archive"] = measure.artifact(root / "release-runtime.lib") + else: + build["cfgs_selected_package_only"] = ["test", "test", "jett_resource_native_test_archive"] + self.rewrite(build_path, build) + expected = self.reauthenticate(receipt) + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment, expected) + + def test_rehashed_library_metadata_cannot_override_the_observed_vector(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.fixture(root) + changed = list(dict.fromkeys(MsvcObservationTests.libraries)) + record = measure.read_object(receipt) + archives = record.get("archives") + if not isinstance(archives, list) or not isinstance(archives[0], dict): + self.fail("fixture must retain archive objects") + archives[0]["native_library_args"] = changed + self.rewrite(receipt, record) + library_path = root / "debug-libraries.json" + libraries = measure.read_object(library_path) + libraries["arguments"] = changed + self.rewrite(library_path, libraries) + expected = self.reauthenticate(receipt) + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment, expected) + + def no_directive_fixture(self, root: Path) -> tuple[Path, dict[str, str]]: + receipt, environment = self.fixture(root) + for profile in ("debug", "release"): + (root / f"{profile}-coff.log").write_text(MsvcObservationTests.rust_archive_without_directives(), + encoding="utf-8") + crt_path = root / f"{profile}-crt.json" + crt = measure.read_object(crt_path) + crt["coff_directive_record_count"] = 0 + self.rewrite(crt_path, crt) + self.reauthenticate(receipt) + return receipt, environment + + def test_receipt_can_record_zero_directives_with_independent_static_crt_proof(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.no_directive_fixture(root) + self.verify(receipt, root, environment) + for profile in ("debug", "release"): + self.assertEqual(measure.read_object(root / f"{profile}-crt.json")["coff_directive_record_count"], 0) + self.assertNotIn("Directive(s):", (root / f"{profile}-coff.log").read_text(encoding="utf-8")) + self.assertIn("libcmt.lib", MsvcObservationTests.libraries) + + def test_rehashed_receipt_cannot_forge_missing_or_noninteger_directive_counts(self) -> None: + for count in (1, -1, "0", False, None): + with self.subTest(count=count), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.no_directive_fixture(root) + crt_path = root / "debug-crt.json" + crt = measure.read_object(crt_path) + if count is None: + del crt["coff_directive_record_count"] + else: + crt["coff_directive_record_count"] = count + self.rewrite(crt_path, crt) + expected = self.reauthenticate(receipt) + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment, expected) + + def raw_defaultlib_fixture(self, root: Path) -> tuple[Path, dict[str, str]]: + receipt, environment = self.fixture(root) + record = measure.read_object(receipt) + archives = record.get("archives") + if not isinstance(archives, list): + self.fail("fixture must retain archive rows") + for row in archives: + if not isinstance(row, dict): + self.fail("fixture must retain archive objects") + row["native_library_args"] = MsvcObservationTests.raw_libraries + self.rewrite(receipt, record) + for profile in ("debug", "release"): + build_log = root / f"{profile}-build.log" + build_log.write_text(build_log.read_text(encoding="utf-8").replace( + " ".join(MsvcObservationTests.libraries), " ".join(MsvcObservationTests.raw_libraries)), + encoding="utf-8") + library_path = root / f"{profile}-libraries.json" + libraries = measure.read_object(library_path) + libraries["arguments"] = MsvcObservationTests.raw_libraries + self.rewrite(library_path, libraries) + self.reauthenticate(receipt) + return receipt, environment + + def test_receipt_accepts_raw_defaultlib_token_and_retains_the_original_vector(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.raw_defaultlib_fixture(root) + self.verify(receipt, root, environment) + for row in self.rows(receipt): + self.assertEqual(row["native_library_args"], MsvcObservationTests.raw_libraries) + + def test_rehashed_receipt_cannot_replace_observed_crt_flag_with_an_equivalent_library(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.raw_defaultlib_fixture(root) + normalized = [*MsvcObservationTests.raw_libraries[:-1], "libcmt.lib"] + record = measure.read_object(receipt) + archives = record.get("archives") + if not isinstance(archives, list) or not isinstance(archives[0], dict): + self.fail("fixture must retain archive objects") + archives[0]["native_library_args"] = normalized + self.rewrite(receipt, record) + libraries_path = root / "debug-libraries.json" + libraries = measure.read_object(libraries_path) + libraries["arguments"] = normalized + self.rewrite(libraries_path, libraries) + expected = self.reauthenticate(receipt) + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment, expected) + +class MsvcGateTests(unittest.TestCase): + occupied_tests = ( + "resource_execution::occupied_struct_harness::native_resource_occupied_struct_scripts_retire_before_teardown", + "resource_execution::occupied_struct_harness::native_resource_occupied_struct_scripts_reenter_with_same_provider_grant", + ) + + def run_fixture(self, root: Path, *, fresh: bool = False, mutate_gate: str | None = None, + execution: str = "running 1 test\ntest result: ok. 1 passed; 0 failed; 0 ignored;\n") -> tuple[ + dict[str, measure.Json], list[dict[str, str]], list[dict[str, str]]]: + receipt, environment = MsvcReceiptTests().fixture(root) + workflow = root / ".github/workflows/native.yml" + workflow.parent.mkdir(parents=True) + workflow.write_bytes(b"synthetic complete Source fixture") + expected = measure.digest(receipt) + compiler = root / "tool chain/rustc.exe" + linker = root / "tool chain/link.exe" + binary = root / "compiled gate.exe" + target = root / "existing gate target" + target.mkdir() + compile_environments: list[dict[str, str]] = [] + runtime_environments: list[dict[str, str]] = [] + def compile_gate(argv: list[str], *, cwd: Path, env: dict[str, str], stdin: int, + stdout: BinaryIO, stderr: BinaryIO, timeout: int) -> subprocess.CompletedProcess[bytes]: + self.assertEqual(cwd, root) + self.assertEqual(stdin, subprocess.DEVNULL) + self.assertIn("--no-run", argv) + self.assertIn("--offline", argv) + self.assertEqual(argv[argv.index("--target") + 1], "x86_64-pc-windows-msvc") + self.assertEqual(argv[argv.index("--target-dir") + 1], str(target)) + self.assertNotIn("--cfg", argv) + compile_environments.append(dict(env)) + binary.write_bytes(b"synthetic freshly compiled gate") + binary.with_suffix(".d").write_text( + f"# env-dep:JETT_RESOURCE_NATIVE_TEST_ARCHIVE_EXPECTED_SHA256_V1={env[measure.PIN_ENV]}\n", + encoding="utf-8") + stderr.write((f'Running `"{compiler}" --crate-name native_conformance --test ' + '--target x86_64-pc-windows-msvc --emit=dep-info,link ' + f'-C target-feature=+crt-static -C debuginfo=0 -C "linker={linker}"`\n').encode()) + stdout.write((json.dumps({"reason": "compiler-artifact", "fresh": fresh, + "target": {"name": "native_conformance"}, "executable": str(binary)}) + "\n").encode()) + return subprocess.CompletedProcess(argv, 0) + def execute_gate(argv: list[str], repo: Path, env: dict[str, str], + log: Path, timeout: int = 3600) -> str: + self.assertEqual(repo, root) + if argv == [str(compiler), "-vV"]: + self.assertNotIn(measure.PIN_ENV, env) + text = "host: x86_64-pc-windows-msvc\n" + else: + self.assertEqual(argv, [str(binary), self.occupied_tests[len(runtime_environments)], + "--ignored", "--exact", "--test-threads=1", "--nocapture"]) + runtime_environments.append(dict(env)) + if mutate_gate == "binary": + binary.write_bytes(b"changed gate during execution") + elif mutate_gate == "dep-info": + binary.with_suffix(".d").write_bytes(b"changed compiled pin evidence during execution") + text = execution + log.write_text(text, encoding="utf-8") + return text + def tool(name: str) -> Path: + return root / "tool chain" / (name if name.endswith(".exe") else name + ".exe") + with patch.dict(os.environ, environment, clear=True), \ + patch.object(measure.shutil, "which", return_value=str(linker)), \ + patch.object(measure, "executable", side_effect=tool), \ + patch.object(measure.subprocess, "run", side_effect=compile_gate), \ + patch.object(measure, "logged", side_effect=execute_gate): + measure.run_gate(root, receipt, expected, target, target="x86_64-pc-windows-msvc", + suite="occupied-structs", reuse_target_dir=True) + return measure.read_object(root / "gate-acceptance.json"), compile_environments, runtime_environments + + def test_occupied_gates_compile_the_pin_and_execute_each_exact_test_once(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + accepted, compiled, executed = self.run_fixture(root) + self.assertEqual(len(compiled), 1) + self.assertEqual(len(executed), 2) + self.assertEqual(compiled[0][measure.PIN_ENV], accepted["compile_time_receipt_sha256"]) + self.assertNotIn(measure.RECEIPT_ENV, compiled[0]) + for environment in executed: + self.assertNotIn(measure.PIN_ENV, environment) + self.assertEqual(environment[measure.RECEIPT_ENV], str(root / "receipt.json")) + self.assertEqual(accepted["suite"], "occupied-structs") + observations = accepted["gate_executions"] + if not isinstance(observations, list): + self.fail("accepted executions must be an observed list") + self.assertEqual([row.get("test") for row in observations if isinstance(row, dict)], + list(self.occupied_tests)) + self.assertNotIn("cases", accepted) + self.assertNotIn("source_deleted_executions", accepted) + self.assertEqual(accepted["runtime_expected_pin_environment"], False) + + def test_reused_target_cannot_accept_cached_compilation_or_empty_test_selection(self) -> None: + for fresh, execution in ((True, "running 1 test\ntest result: ok. 1 passed; 0 failed; 0 ignored;\n"), + (False, "running 0 tests\ntest result: ok. 0 passed; 0 failed; 0 ignored;\n"), + (False, "running 2 tests\ntest result: ok. 1 passed; 0 failed; 0 ignored;\n")): + with self.subTest(fresh=fresh, execution=execution), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + with self.assertRaises(measure.MeasurementError): + self.run_fixture(root, fresh=fresh, execution=execution) + self.assertFalse((root / "gate-acceptance.json").exists()) + + def test_compiler_dep_info_requires_one_exact_pin_and_fresh_artifact(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + binary = Path(temporary) / "native_conformance.exe" + binary.write_bytes(b"synthetic test binary") + expected = "a" * 64 + observation: dict[str, measure.Json] = {"reason": "compiler-artifact", "fresh": False, + "target": {"name": "native_conformance"}, "executable": str(binary)} + message = json.dumps(observation) + dep_info = binary.with_suffix(".d") + exact_pin = f"# env-dep:JETT_RESOURCE_NATIVE_TEST_ARCHIVE_EXPECTED_SHA256_V1={expected}\n" + with self.assertRaises(measure.MeasurementError): + measure.checked_msvc_gate_artifact(message, binary, expected) + dep_info.write_text(exact_pin, encoding="utf-8") + self.assertEqual(measure.checked_msvc_gate_artifact(message, binary, expected), dep_info) + for invalid in ("", exact_pin + exact_pin, exact_pin.replace(expected, "b" * 64)): + dep_info.write_text(invalid, encoding="utf-8") + with self.subTest(dep_info=invalid), self.assertRaises(measure.MeasurementError): + measure.checked_msvc_gate_artifact(message, binary, expected) + dep_info.write_text(exact_pin, encoding="utf-8") + cached = {**observation, "fresh": True} + missing_fresh = {key: value for key, value in observation.items() if key != "fresh"} + foreign = {**observation, "executable": str(binary.parent / "foreign.exe")} + for invalid in (json.dumps(cached), json.dumps(missing_fresh), json.dumps(foreign), message + "\n" + message): + with self.subTest(message=invalid), self.assertRaises(measure.MeasurementError): + measure.checked_msvc_gate_artifact(invalid, binary, expected) + + def test_unknown_suite_and_implicit_target_reuse_stop_before_compilation(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + target = root / "existing" + target.mkdir() + with patch.object(measure, "receipt_sources") as receipt_sources, \ + patch.object(measure.subprocess, "run") as compile_gate: + for suite, reuse in (("foreign", True), ("occupied-structs", False)): + with self.subTest(suite=suite, reuse=reuse), self.assertRaises(measure.MeasurementError): + measure.run_gate(root, root / "absent-receipt.json", "a" * 64, target, + target="x86_64-pc-windows-msvc", suite=suite, reuse_target_dir=reuse) + receipt_sources.assert_not_called() + compile_gate.assert_not_called() + + def test_gate_binary_and_compiled_pin_evidence_cannot_change_during_execution(self) -> None: + for changed in ("binary", "dep-info"): + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + with self.assertRaises(measure.MeasurementError): + self.run_fixture(root, mutate_gate=changed) + self.assertFalse((root / "gate-acceptance.json").exists()) + +class MsvcLinkerVersionTests(unittest.TestCase): + help_text = ("Microsoft (R) Incremental Linker Version 14.44.35213.0\n" + "Copyright (C) Microsoft Corporation. All rights reserved.\n\n" + "usage: LINK [options] [files] [@commandfile]\n\n" + " options:\n\n" + " /ALIGN:#\n /DEBUG\n") + + def observe(self, root: Path, text: str, returncode: int, *, generic: bool = False) -> tuple[str, int]: + linker = root / "measured link.exe" + linker.write_bytes(b"synthetic linker executable") + log = root / "linker-version.log" + environment = {"LIB": r"C:\SDK Libraries\x64;", "PATH": str(root)} + def version_probe(argv: list[str], *, cwd: Path, env: dict[str, str], stdin: int, + stdout: BinaryIO, stderr: int, timeout: int) -> subprocess.CompletedProcess[bytes]: + self.assertEqual(argv, [str(linker), "/?"]) + self.assertEqual(cwd, root) + self.assertEqual(env, environment) + self.assertEqual(stdin, subprocess.DEVNULL) + self.assertEqual(stderr, subprocess.STDOUT) + stdout.write(text.encode("utf-8")) + return subprocess.CompletedProcess(argv, returncode) + with patch.object(measure.subprocess, "run", side_effect=version_probe) as run: + if generic: + result = measure.logged([str(linker), "/?"], root, environment, log, timeout=60) + observed = (result, returncode) + else: + observed = measure.msvc_linker_version(linker, root, environment, log) + run.assert_called_once() + self.assertEqual(log.read_text(encoding="utf-8"), text) + return observed + + def test_microsoft_help_exit_1100_and_zero_retain_the_actual_version_observation(self) -> None: + for returncode in (0, 1100): + with self.subTest(returncode=returncode), tempfile.TemporaryDirectory() as temporary: + self.assertEqual(self.observe(Path(temporary), self.help_text, returncode), + (self.help_text, returncode)) + + def test_error_exit_1100_cannot_authenticate_even_with_a_valid_help_prefix(self) -> None: + for diagnostic in ("LINK : fatal error LNK1158: cannot run 'rc.exe'\n", + "LINK : error LNK2001: unresolved external symbol main\n"): + for prefix in ("", self.help_text): + with self.subTest(diagnostic=diagnostic, help_prefix=bool(prefix)), \ + tempfile.TemporaryDirectory() as temporary, self.assertRaises(measure.MeasurementError): + self.observe(Path(temporary), prefix + diagnostic, 1100) + + def test_unsupported_status_or_incomplete_or_foreign_help_is_refused(self) -> None: + invalid = ((self.help_text, 1), (self.help_text, 2), + ("Microsoft (R) Incremental Linker Version 14.44.35213.0\n", 1100), + (self.help_text.replace("usage: LINK [options] [files] [@commandfile]\n", ""), 1100), + (self.help_text.replace(" options:\n", ""), 1100), + (self.help_text.replace("Microsoft (R) Incremental Linker Version 14.44.35213.0", + "LLVM Linker Version 21.0.0"), 1100)) + for text, returncode in invalid: + with self.subTest(text=text, returncode=returncode), tempfile.TemporaryDirectory() as temporary, \ + self.assertRaises(measure.MeasurementError): + self.observe(Path(temporary), text, returncode) + + def test_generic_logged_still_rejects_valid_help_with_nonzero_exit_status(self) -> None: + with tempfile.TemporaryDirectory() as temporary, self.assertRaises(measure.MeasurementError): + self.observe(Path(temporary), self.help_text, 1100, generic=True) + + def test_toolchain_receipt_records_the_actual_linker_help_status_and_hashed_log(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + directory = root / "observations" + directory.mkdir() + tools = measure.MsvcTools(root / "cargo.exe", root / "rustc.exe", root / "llvm-nm.exe", + root / "llvm-readobj.exe", root / "link.exe") + for file in (tools.cargo, tools.rustc, tools.nm, tools.readobj, tools.linker): + file.write_bytes(b"synthetic measured tool") + expected: dict[str, tuple[str, str, int]] = { + str(tools.cargo): ("-vV", "cargo 1.92.0\n", 0), + str(tools.rustc): ("-vV", "rustc 1.92.0\nhost: x86_64-pc-windows-msvc\n", 0), + str(tools.nm): ("--version", "LLVM version 21.0.0\n", 0), + str(tools.readobj): ("--version", "LLVM version 21.0.0\n", 0), + str(tools.linker): ("/?", self.help_text, 1100), + } + environment = {"LIB": r"C:\SDK\x64;", "PATH": str(root)} + def observe(argv: list[str], *, cwd: Path, env: dict[str, str], stdin: int, + stdout: BinaryIO, stderr: int, timeout: int) -> subprocess.CompletedProcess[bytes]: + argument, text, returncode = expected[argv[0]] + self.assertEqual(argv, [argv[0], argument]) + self.assertEqual(cwd, root) + self.assertEqual(env, environment) + self.assertEqual(stdin, subprocess.DEVNULL) + self.assertEqual(stderr, subprocess.STDOUT) + stdout.write(text.encode("utf-8")) + return subprocess.CompletedProcess(argv, returncode) + with patch.object(measure.subprocess, "run", side_effect=observe) as run: + recorded = measure.msvc_toolchain(tools, root, directory, environment) + self.assertEqual(run.call_count, 5) + linker = recorded["linker"] + if not isinstance(linker, dict): + self.fail("toolchain linker observation must be an object") + self.assertEqual(linker["returncode"], 1100) + self.assertEqual(linker["version_observation"], measure.artifact(directory / "linker-version.log")) + self.assertEqual(linker["executable"], measure.artifact(tools.linker)) + for name in ("rustc", "cargo", "nm", "readobj"): + observed = recorded[name] + if not isinstance(observed, dict): + self.fail("toolchain version observation must be an object") + self.assertEqual(observed["returncode"], 0) + +if __name__ == "__main__": + unittest.main()