diff --git a/crates/jett_codegen_cranelift/src/emit.rs b/crates/jett_codegen_cranelift/src/emit.rs index 08923145..2829f41a 100644 --- a/crates/jett_codegen_cranelift/src/emit.rs +++ b/crates/jett_codegen_cranelift/src/emit.rs @@ -229,7 +229,7 @@ fn emit_for_triple( 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); + jett_mir::prepare_native_generated_functions(&mut prepared, types); let program = &prepared; let verified = verify_program(program, types)?; if let Some(entry) = entry @@ -770,6 +770,8 @@ 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) .map_err(|message| contract_error(symbol, function.span, message))?; let local_slots = function @@ -796,6 +798,58 @@ fn translate_function( )) }) .collect::>(); + let mut generation_slots = Vec::new(); + let mut generation_ids = std::collections::BTreeSet::new(); + let mut escrow_ids = std::collections::BTreeSet::new(); + for slot in ownership.generation_storage().slots() { + let root = function.local(slot.root()).ok_or_else(|| { + contract_error( + symbol, + function.span, + "call owner generation root is absent", + ) + })?; + let root_slot = local_slots + .get(slot.root().index() as usize) + .copied() + .flatten() + .ok_or_else(|| { + contract_error( + symbol, + function.span, + "call owner generation has no owning root storage", + ) + })?; + if !generation_ids.insert(slot.generation().index()) + || !escrow_ids.insert(slot.escrow().index()) + || root.ty != slot.ty() + || root.ty.index() as usize >= types.len() + || !matches!(types.resolve(root.ty), Type::TypeConstruction) + || root.view_source.is_some() + { + return Err(contract_error( + symbol, + function.span, + "call owner generation storage differs from its exact plan", + )); + } + generation_slots.push(NativeCallGenerationStorage { + generation: slot.generation(), + root: slot.root(), + ty: slot.ty(), + root_slot, + owner_slot: builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 8, + 3, + )), + retired_slot: builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 8, + 3, + )), + }); + } let failure_block = builder.create_block(); let mut variables = Vec::with_capacity(function.locals.len()); @@ -853,6 +907,10 @@ fn translate_function( for slot in local_slots.iter().flatten().chain(&temporary_slots) { builder.ins().stack_store(zero, *slot, 0); } + for storage in &generation_slots { + builder.ins().stack_store(zero, storage.owner_slot, 0); + builder.ins().stack_store(zero, storage.retired_slot, 0); + } } let mut translator = Translator { @@ -869,12 +927,15 @@ fn translate_function( equality_methods: &program.equality_methods, symbol, local_types: &function.locals, + caller_acquisitions: &caller_acquisitions, + taking_binding: None, test_ownership: matches!( function.identity.declaration.kind, jett_hir::DeclarationKind::Verify | jett_hir::DeclarationKind::Property ), local_slots: &local_slots, temporary_slots: &temporary_slots, + generation_slots: &generation_slots, next_temporary: 0, failure_block, }; @@ -1054,12 +1115,15 @@ fn translate_function( equality_methods: &program.equality_methods, symbol, local_types: &function.locals, + caller_acquisitions: &caller_acquisitions, + taking_binding: None, test_ownership: matches!( function.identity.declaration.kind, jett_hir::DeclarationKind::Verify | jett_hir::DeclarationKind::Property ), local_slots: &local_slots, temporary_slots: &temporary_slots, + generation_slots: &generation_slots, next_temporary: temporary_slots.len(), failure_block, }; @@ -1086,6 +1150,18 @@ enum LoweredValue { Owned(Value, ir::StackSlot), } +/// Internal owning storage from one freshly validated Function plan. Neither +/// this handle slot nor its retired state can be addressed as a Jett Local. +#[derive(Clone, Copy)] +struct NativeCallGenerationStorage { + generation: jett_mir::CallOwnerGenerationId, + root: jett_mir::LocalId, + ty: TypeId, + root_slot: ir::StackSlot, + owner_slot: ir::StackSlot, + retired_slot: ir::StackSlot, +} + struct Translator<'a, 'builder> { equality_methods: &'a std::collections::HashMap, builder: &'a mut FunctionBuilder<'builder>, @@ -1100,9 +1176,12 @@ struct Translator<'a, 'builder> { types: &'a TypeInterner, symbol: &'a str, local_types: &'a [jett_mir::Local], + caller_acquisitions: &'a jett_mir::CallerAcquisitions<'a>, + taking_binding: Option, test_ownership: bool, local_slots: &'a [Option], temporary_slots: &'a [ir::StackSlot], + generation_slots: &'a [NativeCallGenerationStorage], next_temporary: usize, failure_block: ir::Block, } @@ -1325,11 +1404,36 @@ impl Translator<'_, '_> { let value = if borrowed { self.argument(value, true)? } else { - self.expression(value)? + let source = self + .caller_acquisitions + .owner_initializer(*local) + .and_then(|source| source.binding); + let previous = std::mem::replace(&mut self.taking_binding, source); + let result = self.expression(value); + self.taking_binding = previous; + result? }; self.define_local(*local, value, statement.span) } StatementKind::EndCallView { .. } => Ok(()), + StatementKind::OpenCallOwnerGeneration { generation, root } => { + let storage = + self.call_generation_storage(*generation, Some(*root), statement.span)?; + self.clear_slot(storage.owner_slot); + self.clear_slot(storage.retired_slot); + Ok(()) + } + StatementKind::ReplaceCallOwnerGeneration { + generation, + root, + rhs_owner, + } => self.replace_call_generation(*generation, *root, *rhs_owner, statement.span), + StatementKind::CloseCallOwnerGeneration { generation } => { + let storage = self.call_generation_storage(*generation, None, statement.span)?; + self.drop_slot(storage.owner_slot)?; + self.clear_slot(storage.retired_slot); + Ok(()) + } StatementKind::CheckRefinement { local, call, @@ -1784,6 +1888,17 @@ impl Translator<'_, '_> { } fn expression(&mut self, expression: &Expression) -> Result { + let source = self.caller_acquisitions.argument_binding(expression); + let previous = self.taking_binding; + if source.is_some() { + self.taking_binding = source; + } + let result = self.expression_inner(expression); + self.taking_binding = previous; + result + } + + fn expression_inner(&mut self, expression: &Expression) -> Result { let kind = scalar_kind(self.types, expression.ty, "native expression")?; match &expression.kind { ExpressionKind::Int(value) => { @@ -1831,6 +1946,14 @@ impl Translator<'_, '_> { if let Some(slot) = self.local_slots[local.index() as usize] { let v = self.builder.ins().stack_load(ir::types::I64, slot, 0); self.expect_machine_state(local.index() as usize, expression.ty, v)?; + if self.taking_binding == Some(*local) { + self.clear_slot(slot); + return if is_linear(self.types, expression.ty) { + self.own_linear(v) + } else { + self.own(v) + }; + } if is_linear(self.types, expression.ty) { if self.test_ownership { return self.clone_linear( @@ -2124,6 +2247,7 @@ impl Translator<'_, '_> { function, args, evaluation_order, + .. } => self.call(*function, args, evaluation_order, expression), ExpressionKind::FunctionAdapter { value, function } => { let lowered = self.expression(value)?; @@ -2231,6 +2355,7 @@ impl Translator<'_, '_> { callee, args, evaluation_order, + .. } => self.indirect_call(callee, args, evaluation_order, expression), ExpressionKind::StructConstruct { fields, @@ -3676,7 +3801,7 @@ function root() returns int64: .expect("original descriptor metadata"); 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); + jett_mir::prepare_native_generated_functions(&mut program, &types); let verified = verify_program(&program, &types).expect("prepared native program"); let mut module = test_object_module(); let declarations = declare_reachable_functions(&mut module, &program, &types, &verified) @@ -3922,6 +4047,7 @@ function root() returns int64: .iter() .any(|param| { param.local == factory.locals[detached].id }) ); + let mut invalid_metadata = None; if corruption == 2 { factory.blocks[0].statements.push(jett_mir::Statement { kind: StatementKind::Trace(factory.locals[detached].id), @@ -3946,19 +4072,35 @@ function root() returns int64: invalid = foreign.intern(jett_types::Type::List(invalid)); } assert!(invalid.index() as usize >= types.len()); + invalid_metadata = Some(invalid); if corruption == 0 { factory.locals[detached].ty = invalid; } else { factory.locals[detached].debug_ty = invalid; } } - assert!( - matches!( - emit_host_object(&program, &types), - Err(CodegenError::UnsupportedType { .. }) - ), - "enclosing slot corruption {corruption}" - ); + let result = emit_host_object(&program, &types); + if let Some(invalid) = invalid_metadata { + assert!( + matches!(result, Err(CodegenError::InvalidMir(errors)) + if errors.len() == 1 && errors[0].message + == "call ownership local metadata is outside its function or interner"), + "enclosing metadata corruption {corruption}" + ); + assert!(matches!( + crate::verify::scalar_kind(&types, invalid, "enclosing original metadata"), + Err(CodegenError::UnsupportedType { type_name, context }) + if type_name == format!("", invalid.index()) + && context == "enclosing original metadata" + )); + } else { + // These live Trace/Let controls still demand a native Never + // carrier, independently of detached-slot compaction. + assert!( + matches!(result, Err(CodegenError::UnsupportedType { .. })), + "enclosing live slot corruption {corruption}" + ); + } } } @@ -3967,6 +4109,29 @@ function root() returns int64: for corruption in 0..6 { let (mut program, types) = lower_source(UNINHABITED_DESCRIPTOR_SOURCE); let function_count = program.functions.len(); + let source_call = program + .functions + .iter() + .flat_map(|function| &function.blocks) + .find_map(|block| match &block.terminator.kind { + TerminatorKind::Return(Some(value)) + if matches!(value.kind, ExpressionKind::Call { .. }) => + { + Some(value.kind.clone()) + } + _ => block + .statements + .iter() + .find_map(|statement| match &statement.kind { + StatementKind::Let { value, .. } + if matches!(value.kind, ExpressionKind::Call { .. }) => + { + Some(value.kind.clone()) + } + _ => None, + }), + }) + .expect("fixture retains a checked source call packet"); let descriptor = program .functions .iter_mut() @@ -4018,11 +4183,11 @@ function root() returns int64: }) else { panic!("inline value return"); }; - value.kind = ExpressionKind::Call { - function: FunctionId::new(function_count as u32 + 1), - args: Vec::new(), - evaluation_order: Vec::new(), + value.kind = source_call; + let ExpressionKind::Call { function, .. } = &mut value.kind else { + panic!("source call fixture"); }; + *function = FunctionId::new(function_count as u32 + 1); } 5 => { let mut foreign = TypeInterner::new(); @@ -4065,6 +4230,25 @@ function root() returns int64: assert!(crate::verify::descriptor_only_function(target)); assert!(matches!( emit_host_object(&program, &types), + Err(CodegenError::InvalidMir(errors)) + if errors.len() == 1 && errors[0].message + == "source function ownership signature differs from the exact HIR declaration" + )); + let root = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "root") + .expect("original checked caller"); + let expression = root + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("original callable return expression"); + assert!(matches!( + crate::verify::verify_callable_expression_for_test(&program, &types, root, expression), Err(CodegenError::InvalidMirContract { message, .. }) if message == "callable direct edge requires an uninhabited parameter" )); @@ -4112,6 +4296,25 @@ function root() returns int64: } assert!(matches!( emit_host_object(&program, &types), + Err(CodegenError::InvalidMir(errors)) + if errors.len() == 1 && errors[0].message + == "indirect call ownership signature differs from its actual callee" + )); + let root = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "root") + .expect("original checked caller"); + let expression = root + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("original callable return expression"); + assert!(matches!( + crate::verify::verify_callable_expression_for_test(&program, &types, root, expression), Err(CodegenError::InvalidMirContract { message, .. }) if message == "callable indirect edge requires an uninhabited parameter" )); @@ -4301,7 +4504,7 @@ function root() returns nothing: .expect("valid original metadata in absent sum arms"); 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); + jett_mir::prepare_native_generated_functions(&mut program, &types); let verified = verify_program(&program, &types).expect("prepared inhabited sum arms"); assert!( verified @@ -4700,6 +4903,214 @@ function root() returns int64: )); } + fn secret_gate_returned_call(function: &mut Function) -> &mut Expression { + let entry = function.entry.index() as usize; + let TerminatorKind::Return(Some(value)) = &mut function.blocks[entry].terminator.kind + else { + panic!("source returned invocation"); + }; + assert!(matches!( + value.kind, + ExpressionKind::Call { .. } | ExpressionKind::IndirectCall { .. } + )); + value + } + + fn assert_checked_native_call_gate(program: &Program, types: &TypeInterner, expected: &str) { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "caller") + .expect("source caller"); + let expression = function + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("source return expression"); + assert!( + matches!( + crate::verify::verify_callable_expression_for_test(program, types, function, expression), + Err(CodegenError::InvalidMirContract { message, .. }) if message == expected + ), + "native expression must retain its precise gate: {expected}" + ); + } + + #[test] + fn native_secret_call_result_gate_remains_independent_of_source_certificates() { + for (callee, expected) in [ + ( + "callback", + "direct call result type does not match its signature", + ), + ("factory()", "indirect call result type mismatch"), + ] { + let source = format!( + "function callback(value: int64) returns int64:\n return value\n\ + function factory() returns function(int64) returns int64:\n return callback\n\ + function caller(value: secret[int64]) returns secret[int64]:\n return {callee}(value)\n" + ); + let (mut program, types) = lower_source(&source); + let caller = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "caller") + .unwrap(); + caller.return_type = TypeInterner::INT64; + secret_gate_returned_call(caller).ty = TypeInterner::INT64; + assert_checked_native_call_gate(&program, &types, expected); + } + } + + #[test] + fn native_secret_call_payload_gate_preserves_exact_primitive_and_nominal_types() { + for (callee, expected) in [ + ( + "callback", + "direct call argument type does not match its parameter", + ), + ("factory()", "indirect call argument type mismatch"), + ] { + for parameter in ["int64", "Positive"] { + let source = format!( + "type Positive = int64 where value > 0\n\ + function callback(value: {parameter}) returns {parameter}:\n return value\n\ + function factory() returns function({parameter}) returns {parameter}:\n return callback\n\ + function caller(value: secret[{parameter}]) returns secret[{parameter}]:\n return {callee}(value)\n" + ); + let (mut program, mut types) = lower_source(&source); + let payload = if parameter == "Positive" { + TypeInterner::INT64 + } else { + TypeInterner::INT32 + }; + let hidden = types.intern(jett_types::Type::Secret(payload)); + let caller = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "caller") + .unwrap(); + let expression = secret_gate_returned_call(caller); + let args = match &mut expression.kind { + ExpressionKind::Call { args, .. } + | ExpressionKind::IndirectCall { args, .. } => args, + _ => unreachable!(), + }; + args[0].ty = hidden; + args[0].kind = ExpressionKind::Int(7); + assert_checked_native_call_gate(&program, &types, expected); + } + } + } + + #[test] + fn native_secret_call_impurity_gate_remains_independent_of_source_certificates() { + for (callee, expected) in [ + ( + "callback", + "direct call argument type does not match its parameter", + ), + ("factory()", "indirect call argument type mismatch"), + ] { + let source = format!( + "function callback(view stdout: Stdout, value: int64) returns int64:\n return value\n\ + function factory() returns function(view Stdout, int64) returns int64:\n return callback\n\ + function caller(view stdout: Stdout, value: int64) returns int64:\n return {callee}(view stdout, value)\n" + ); + let (mut program, mut types) = lower_source(&source); + let hidden = types.intern(jett_types::Type::Secret(TypeInterner::INT64)); + let caller = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "caller") + .unwrap(); + let expression = secret_gate_returned_call(caller); + let args = match &mut expression.kind { + ExpressionKind::Call { args, .. } | ExpressionKind::IndirectCall { args, .. } => { + args + } + _ => unreachable!(), + }; + args[1].ty = hidden; + args[1].kind = ExpressionKind::Int(7); + assert_checked_native_call_gate(&program, &types, expected); + } + } + + #[test] + fn native_reflected_metadata_expression_gates_preserve_arity_and_exact_types() { + let source = "enum Event:\n empty = 17\n content(text: string, values: list[optional[int64]])\n\ + machine Session:\n states:\n empty\n content(text: string, values: list[optional[int64]])\n transitions:\n empty to content\n\ + function variant_pipe(view source: Event) returns TypeVariant:\n TypeVariant selected = source into view type.variant_value[Event]()\n return selected\n\ + function state_pipe(view source: Session) returns TypeMachineState:\n TypeMachineState selected = source into view type.machine_state_value[Session]()\n return selected\n\ + function narrowed_pipe(view source: Session at content) returns TypeMachineState:\n TypeMachineState selected = source into view type.machine_state_value[Session at content]()\n return selected\n\ + function map_pipe(index: int64) returns TypeInfo:\n TypeInfo selected = index into type.arg[map[string, list[optional[int64]]]]()\n return selected\n"; + for (name, expected) in [ + ( + "variant_pipe", + "invalid checked type.variant_value operands", + ), + ( + "state_pipe", + "invalid checked type.machine_state_value operands", + ), + ( + "narrowed_pipe", + "invalid checked type.machine_state_value operands", + ), + ("map_pipe", "invalid checked type.arg operands"), + ] { + for remove in [true, false] { + let (program, types) = lower_source(source); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .expect("source reflected observer"); + let mut expression = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + StatementKind::Let { value, .. } + if matches!(value.kind, ExpressionKind::Intrinsic { .. }) => + { + Some(value.clone()) + } + _ => None, + }) + .expect("exact checked observer initializer"); + let ExpressionKind::Intrinsic { + args, + evaluation_order, + .. + } = &mut expression.kind + else { + unreachable!() + }; + assert!(args.len() > 1); + if remove { + args.pop(); + evaluation_order.pop(); + } else { + let metadata = args.last_mut().unwrap(); + metadata.kind = ExpressionKind::Bool(false); + metadata.ty = TypeInterner::BOOL; + } + assert!( + matches!( + crate::verify::verify_callable_expression_for_test(&program, &types, function, &expression), + Err(CodegenError::InvalidMirContract { message, .. }) if message == expected + ), + "{name}/remove={remove}: native metadata expression gate remains exact" + ); + } + } + } + #[test] fn custom_assertion_message_in_verify_body_emits_native_entry() { let file = FileId::new(0); @@ -5469,13 +5880,14 @@ function escaped(items: list[int64]) returns list[int64]: fn consuming_sequence_cannot_take_from_a_borrowed_parameter() { let (mut program, types) = lower_source( r#" -function visit(items: list[int64]) returns nothing: +function visit(view items: list[int64]) returns nothing: for item in view items: break "#, ); jett_mir::prepare_native_sequences(&mut program, &types); - program.functions[0].params[0].mode = jett_mir::ParamMode::View; + emit_host_object(&program, &types) + .expect("checked borrowed iteration is valid before corruption"); for block in &mut program.functions[0].blocks { for statement in &mut block.statements { if let StatementKind::SequenceGet { consume, .. } = &mut statement.kind { @@ -5484,11 +5896,13 @@ function visit(items: list[int64]) returns nothing: } } let error = emit_host_object(&program, &types).expect_err("cannot take a borrowed element"); - assert!( - error - .to_string() - .contains("cannot take element from borrowed"), - "{error}" + let CodegenError::InvalidMir(errors) = error else { + panic!("prepared borrowed extraction must fail before emission: {error:?}"); + }; + assert_eq!(errors.len(), 1, "{errors:?}"); + assert_eq!( + errors[0].message, + "call ownership viewed extraction changed source/cursor/target/role or consumption" ); } #[test] @@ -5532,4 +5946,171 @@ function visit(items: list[int64]) returns int64: } emit_host_object(&program, &types).expect("late-numbered unique loop preheader"); } + + const CALL_OWNER_GENERATION_SOURCE: &str = r#"namespace app +struct User: + name: string +struct Alternate: + code: int64 +function inspect_builder(view builder: TypeConstruction, extra: int64) returns int64: + return extra +function maybe(flag: bool) returns optional[int64]: + if flag: + return some(11) + return none +function exercise(flag: bool) returns int64: + mutable TypeConstruction switchable = type.construct_start[User]() + int64 selected = inspect_builder(view switchable, maybe(flag) handle: + switchable = type.construct_start[Alternate]() + switchable = type.construct_start[User]() + default 7 + ) + return selected +"#; + + fn emit_call_owner_generation(source: &str) -> (Program, TypeInterner) { + let (program, types) = lower_source(source); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "exercise") + .expect("actual source exercise function"); + let ownership = MoveValuePlan::analyze(&program, function, &types) + .expect("fresh generation ownership proof"); + let slots = ownership.generation_storage().slots(); + assert_eq!(slots.len(), 1, "one source captured generation"); + let root = function.local(slots[0].root()).expect("generation root"); + assert!(matches!(types.resolve(root.ty), Type::TypeConstruction)); + assert_eq!(slots[0].ty(), root.ty); + assert!(root.mutable && root.view_source.is_none()); + let verified = verify_program(&program, &types).expect("full typed generation program"); + let mut module = test_object_module(); + let declarations = declare_reachable_functions(&mut module, &program, &types, &verified) + .expect("native declarations"); + let declaration = declarations.get(function.id).expect("exercise declaration"); + let mut context = module.make_context(); + context.func.signature = declaration.signature.clone(); + translate_function( + &mut module, + &declarations, + &program, + function, + &types, + &declaration.symbol, + &mut context, + ) + .expect("generation native translation"); + // This function handles a produced sum, so there is no independent + // stable-sum snapshot to obscure a call-capture clone regression. + assert!( + module.get_name(NativeLeaf::StructClone.symbol()).is_none(), + "generation capture must stack-move its owner without cloning" + ); + let object = emit_host_object(&program, &types).expect("generation object"); + assert!(!object.bytes.is_empty()); + (program, types) + } + + #[test] + fn native_call_owner_generation_emits_repeated_replacement_without_capture_clone() { + let (program, _types) = emit_call_owner_generation(CALL_OWNER_GENERATION_SOURCE); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "exercise") + .unwrap(); + let replacements = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| { + matches!( + statement.kind, + StatementKind::ReplaceCallOwnerGeneration { .. } + ) + }) + .count(); + assert_eq!( + replacements, 2, + "both independent replacements share one escrow" + ); + } + + #[test] + fn native_call_owner_generation_emits_conditional_and_early_abort_cleanup() { + let conditional = CALL_OWNER_GENERATION_SOURCE + .replace(" switchable = type.construct_start[User]()\n", ""); + emit_call_owner_generation(&conditional); + let abort = conditional.replace(" default 7\n", " return 13\n"); + emit_call_owner_generation(&abort); + } + + #[test] + fn native_call_owner_generation_rejects_changed_storage_protocol_before_emission() { + let (program, types) = emit_call_owner_generation(CALL_OWNER_GENERATION_SOURCE); + let index = program + .functions + .iter() + .position(|function| function.identity.declaration.name == "exercise") + .unwrap(); + for corruption in 0..4 { + let mut changed = program.clone(); + let function = &mut changed.functions[index]; + let mut applied = false; + for block in &mut function.blocks { + if applied { + break; + } + let position = block + .statements + .iter() + .position(|statement| match corruption { + 0 => matches!( + statement.kind, + StatementKind::OpenCallOwnerGeneration { .. } + ), + 1 => matches!( + statement.kind, + StatementKind::ReplaceCallOwnerGeneration { .. } + ), + 2 | 3 => matches!( + statement.kind, + StatementKind::CloseCallOwnerGeneration { .. } + ), + _ => unreachable!(), + }); + let Some(position) = position else { + continue; + }; + match corruption { + 0 | 2 => { + block.statements.remove(position); + } + 1 => { + let StatementKind::ReplaceCallOwnerGeneration { + root, rhs_owner, .. + } = &mut block.statements[position].kind + else { + unreachable!(); + }; + *rhs_owner = *root; + } + 3 => { + let duplicate = block.statements[position].clone(); + block.statements.insert(position + 1, duplicate); + } + _ => unreachable!(), + } + applied = true; + } + assert!( + applied, + "corruption {corruption} found its canonical operation" + ); + crate::verify::verify_descriptor_bodies(&changed, &types) + .expect_err("original metadata gate must retain generation integrity"); + emit_host_object(&changed, &types) + .expect_err("changed generation storage protocol must fail before emission"); + } + } } diff --git a/crates/jett_codegen_cranelift/src/emit/values.rs b/crates/jett_codegen_cranelift/src/emit/values.rs index c0baf055..7754d8a2 100644 --- a/crates/jett_codegen_cranelift/src/emit/values.rs +++ b/crates/jett_codegen_cranelift/src/emit/values.rs @@ -1365,6 +1365,113 @@ impl Translator<'_, '_> { self.builder.ins().stack_store(zero, slot, 0); Ok(()) } + pub(super) fn call_generation_storage( + &self, + generation: jett_mir::CallOwnerGenerationId, + root: Option, + span: Span, + ) -> Result { + let mut matches = self + .generation_slots + .iter() + .filter(|storage| storage.generation == generation); + let storage = matches.next().copied().ok_or_else(|| { + contract_error( + self.symbol, + span, + "call owner generation has no validated native storage", + ) + })?; + if matches.next().is_some() || root.is_some_and(|root| root != storage.root) { + return Err(contract_error( + self.symbol, + span, + "call owner generation changes its exact root or storage", + )); + } + Ok(storage) + } + pub(super) fn replace_call_generation( + &mut self, + generation: jett_mir::CallOwnerGenerationId, + root: jett_mir::LocalId, + rhs_owner: jett_mir::LocalId, + span: Span, + ) -> Result<(), CodegenError> { + let storage = self.call_generation_storage(generation, Some(root), span)?; + let rhs = self + .local_types + .get(rhs_owner.index() as usize) + .ok_or_else(|| { + contract_error( + self.symbol, + span, + "call owner generation RHS local is absent", + ) + })?; + let rhs_slot = self + .local_slots + .get(rhs_owner.index() as usize) + .copied() + .flatten() + .ok_or_else(|| { + contract_error( + self.symbol, + span, + "call owner generation RHS has no owning storage", + ) + })?; + if rhs.id != rhs_owner + || rhs.ty != storage.ty + || rhs.view_source.is_some() + || rhs_owner == root + || rhs_slot == storage.root_slot + { + return Err(contract_error( + self.symbol, + span, + "call owner generation RHS is not its independent exact owner", + )); + } + // RHS acquisition already completed at its unique checked owning Let. + // Its slot is transferred exactly once; no source expression runs here. + let next = self.builder.ins().stack_load(ir::types::I64, rhs_slot, 0); + self.clear_slot(rhs_slot); + let retired = self + .builder + .ins() + .stack_load(ir::types::I64, storage.retired_slot, 0); + let has_retired = self.builder.ins().icmp_imm(IntCC::NotEqual, retired, 0); + let attached = self.builder.create_block(); + let replaced = self.builder.create_block(); + let installed = self.builder.create_block(); + self.builder + .ins() + .brif(has_retired, replaced, &[], attached, &[]); + + self.builder.switch_to_block(attached); + let captured = self + .builder + .ins() + .stack_load(ir::types::I64, storage.root_slot, 0); + self.clear_slot(storage.root_slot); + self.builder + .ins() + .stack_store(captured, storage.owner_slot, 0); + let one = self.builder.ins().iconst(ir::types::I64, 1); + self.builder.ins().stack_store(one, storage.retired_slot, 0); + self.builder.ins().jump(installed, &[]); + + self.builder.switch_to_block(replaced); + // The old captured owner stays in escrow. Only the independent current + // generation is dropped before its ordinary replacement is installed. + self.drop_slot(storage.root_slot)?; + self.builder.ins().jump(installed, &[]); + + self.builder.switch_to_block(installed); + self.builder.ins().stack_store(next, storage.root_slot, 0); + Ok(()) + } pub(super) fn drop_temporaries(&mut self) -> Result<(), CodegenError> { for &slot in &self.temporary_slots[..self.next_temporary] { self.drop_slot(slot)?; @@ -1384,7 +1491,13 @@ impl Translator<'_, '_> { } pub(super) fn drop_all(&mut self) -> Result<(), CodegenError> { self.drop_temporaries()?; - self.drop_dead_locals(&BTreeSet::new()) + self.drop_dead_locals(&BTreeSet::new())?; + for index in 0..self.generation_slots.len() { + let storage = self.generation_slots[index]; + self.drop_slot(storage.owner_slot)?; + self.clear_slot(storage.retired_slot); + } + Ok(()) } pub(super) fn static_data(&mut self, bytes: &[u8]) -> Result<(Value, Value), CodegenError> { self.static_data_with_functions(bytes, &[]) diff --git a/crates/jett_codegen_cranelift/src/reachability.rs b/crates/jett_codegen_cranelift/src/reachability.rs index ff130651..212f24e2 100644 --- a/crates/jett_codegen_cranelift/src/reachability.rs +++ b/crates/jett_codegen_cranelift/src/reachability.rs @@ -178,7 +178,12 @@ fn collect_function_references(function: &Function, references: &mut References< collect_expression_references(condition, references); } } - StatementKind::EndCallView { .. } + // Generation operations use current Local operands only; + // their immutable Source archives are not callable roots. + StatementKind::OpenCallOwnerGeneration { .. } + | StatementKind::ReplaceCallOwnerGeneration { .. } + | StatementKind::CloseCallOwnerGeneration { .. } + | StatementKind::EndCallView { .. } | StatementKind::ReflectedContainerReady { .. } | StatementKind::IterationBorrow { .. } | StatementKind::SequenceLength { .. } @@ -351,6 +356,7 @@ fn collect_expression_references(expression: &Expression, references: &mut Refer field_validation: _, args, evaluation_order: _, + .. } => { for argument in args { collect_expression_references(argument, references); @@ -651,7 +657,7 @@ function root() returns int64: #[test] fn retained_calls_and_references_still_reach_uninhabited_specializations() { - for source in [ + for (case, source) in [ r#"namespace app function identity(value: int64) returns int64: return value @@ -664,7 +670,10 @@ function identity(value: int64) returns int64: function root() returns function(int64) returns int64: return identity "#, - ] { + ] + .into_iter() + .enumerate() + { let (mut program, types) = lower_source_with_types(source); let callee = program .functions @@ -678,11 +687,42 @@ function root() returns function(int64) returns int64: callee.locals[parameter].debug_ty = jett_types::TypeInterner::NEVER; assert!(uninhabited_specialization(callee)); assert_eq!(reachable_names(&program), ["identity", "root"]); - // A fabricated surviving use must fail ordinary verification; - // suppressing implicit roots must never supply a `never` carrier. + // Reachability keeps both identities, but an earlier source + // packet rejection is independent of the native callable gate. + let result = crate::emit_host_object(&program, &types); + if case == 0 { + assert!(matches!(result, Err(CodegenError::InvalidMir(errors)) + if errors.len() == 1 && errors[0].message + == "source function ownership signature differs from the exact HIR declaration")); + } else { + // Preserve the original reference-only refusal until its + // actual validation path is characterized separately. + assert!(matches!( + result, + Err(CodegenError::InvalidMirContract { .. }) + )); + } + let root = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "root") + .expect("original checked root"); + let expression = root + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("retained original call or reference"); + let expected = if case == 0 { + "callable direct edge requires an uninhabited parameter" + } else { + "function value signature does not match target" + }; assert!(matches!( - crate::emit_host_object(&program, &types), - Err(CodegenError::InvalidMirContract { .. }) + crate::verify::verify_callable_expression_for_test(&program, &types, root, expression), + Err(CodegenError::InvalidMirContract { message, .. }) if message == expected )); } } diff --git a/crates/jett_codegen_cranelift/src/verify.rs b/crates/jett_codegen_cranelift/src/verify.rs index 302123f8..b68bd9fd 100644 --- a/crates/jett_codegen_cranelift/src/verify.rs +++ b/crates/jett_codegen_cranelift/src/verify.rs @@ -195,7 +195,7 @@ pub(crate) fn verify_descriptor_bodies( program: &Program, types: &TypeInterner, ) -> Result<(), CodegenError> { - jett_mir::validate(program).map_err(CodegenError::InvalidMir)?; + jett_mir::validate_call_ownership(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 @@ -302,11 +302,35 @@ impl VerifiedProgram { } } +/// Unit-only access to the existing native expression integrity gate. +/// Outer emission independently checks the original caller ownership packet; +/// this helper supplies no preparation, callable frame or emission authority. +#[cfg(test)] +pub(crate) fn verify_callable_expression_for_test( + program: &Program, + types: &TypeInterner, + function: &Function, + expression: &Expression, +) -> Result<(), CodegenError> { + let verified = VerifiedProgram { + functions: Vec::new(), + by_mir_index: vec![None; program.functions.len()], + }; + let verifier = Verifier { + program, + types, + verified: &verified, + phase: VerifierPhase::CallableNative, + metadata_functions: RefCell::new(HashSet::new()), + }; + verifier.expression(function, expression) +} + pub(crate) fn verify_program( program: &Program, types: &TypeInterner, ) -> Result { - jett_mir::validate(program).map_err(CodegenError::InvalidMir)?; + jett_mir::validate_call_ownership(program, types).map_err(CodegenError::InvalidMir)?; // Reachability may omit lifted bodies, but malformed source metadata must // not disappear with an unused descriptor. @@ -1020,6 +1044,55 @@ impl Verifier<'_> { })?; Ok(()) } + StatementKind::OpenCallOwnerGeneration { root, .. } + | StatementKind::ReplaceCallOwnerGeneration { root, .. } => { + let owner = function.local(*root).ok_or_else(|| { + self.contract_error( + function, + statement.span, + "call owner generation root is absent from the local table", + ) + })?; + if owner.ty.index() as usize >= self.types.len() + || !matches!(self.types.resolve(owner.ty), Type::TypeConstruction) + || !owner.mutable + || function.is_view_local(*root) + || function.parameter_for_local(*root).is_some() + { + return Err(self.contract_error( + function, + statement.span, + "call owner generation root is not its exact mutable builder owner", + )); + } + self.value_kind(function, owner.ty, "call owner generation root")?; + if let StatementKind::ReplaceCallOwnerGeneration { rhs_owner, .. } = &statement.kind + { + let rhs = function.local(*rhs_owner).ok_or_else(|| { + self.contract_error( + function, + statement.span, + "call owner generation RHS is absent from the local table", + ) + })?; + if rhs_owner == root + || rhs.ty != owner.ty + || function.is_view_local(*rhs_owner) + || function.parameter_for_local(*rhs_owner).is_some() + { + return Err(self.contract_error( + function, + statement.span, + "call owner generation RHS is not its independent owning builder", + )); + } + self.value_kind(function, rhs.ty, "call owner generation RHS")?; + } + // Exact sites, private Source joins and Open/Replace/Close CFG + // authority were checked by the original all-body MIR gate. + Ok(()) + } + StatementKind::CloseCallOwnerGeneration { .. } => Ok(()), StatementKind::CheckRefinement { local, call, .. } => { let local = function.local(*local).ok_or_else(|| { self.contract_error( diff --git a/crates/jett_codegen_cranelift/tests/object_emission.rs b/crates/jett_codegen_cranelift/tests/object_emission.rs index f7327372..d4865d1d 100644 --- a/crates/jett_codegen_cranelift/tests/object_emission.rs +++ b/crates/jett_codegen_cranelift/tests/object_emission.rs @@ -347,7 +347,15 @@ function main(raw: int64) returns Higher: #[test] fn native_return_dynamic_failure_keeps_its_message_producer_reachable() { let (mut program, types) = lower_source( - "namespace app\nfunction message() returns string:\n return \"dynamic error\"\nfunction main() returns nothing:\n return nothing\n", + r#"namespace app +function message() returns string: + return "dynamic error" +function unreferenced_message() returns string: + return "unused error" +function main() returns nothing: + message() + return nothing +"#, ); let message = program .functions @@ -357,34 +365,55 @@ fn native_return_dynamic_failure_keeps_its_message_producer_reachable() { message.identity.declaration.origin = SourceOrigin::Stdlib; let message_id = message.id; let message_symbol = symbol_name(&message.identity, &types).unwrap(); + let unreferenced = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "unreferenced_message") + .unwrap(); + unreferenced.identity.declaration.origin = SourceOrigin::Stdlib; + let unreferenced_symbol = symbol_name(&unreferenced.identity, &types).unwrap(); let main = program .functions .iter_mut() .find(|function| function.identity.declaration.name == "main") .unwrap(); - let span = main.span; - main.blocks[main.entry.index() as usize] + let statement = main.blocks[main.entry.index() as usize] .statements - .push(jett_mir::Statement { - kind: jett_mir::StatementKind::Evaluate(jett_hir::Expression { - kind: jett_hir::ExpressionKind::RuntimeFailureMessage(Box::new( - jett_hir::Expression { - kind: jett_hir::ExpressionKind::Call { - function: message_id, - args: Vec::new(), - evaluation_order: Vec::new(), - }, - ty: TypeInterner::STRING, - span, - }, - )), - ty: TypeInterner::NOTHING, - span, - }), + .iter_mut() + .find(|statement| { + matches!(&statement.kind, + jett_mir::StatementKind::Evaluate(jett_hir::Expression { + kind: jett_hir::ExpressionKind::Call { function, .. }, .. + }) if *function == message_id) + }) + .expect("checked message call statement"); + let jett_mir::StatementKind::Evaluate(value) = &mut statement.kind else { + unreachable!("selected Evaluate statement"); + }; + assert!(matches!(&value.kind, + jett_hir::ExpressionKind::Call { + function, + ownership: jett_hir::CallOwnership::Source(_), + .. + } if *function == message_id)); + let span = value.span; + // Reparent the checked call without changing its source certificate. + let message_call = std::mem::replace( + value, + jett_hir::Expression { + kind: jett_hir::ExpressionKind::Nothing, + ty: TypeInterner::NOTHING, span, - }); + }, + ); + *value = jett_hir::Expression { + kind: jett_hir::ExpressionKind::RuntimeFailureMessage(Box::new(message_call)), + ty: TypeInterner::NOTHING, + span, + }; let artifact = emit_host_object(&program, &types).expect("dynamic message producer"); assert!(artifact.symbols.contains(&message_symbol)); + assert!(!artifact.symbols.contains(&unreferenced_symbol)); } #[test] @@ -886,8 +915,8 @@ fn rejects_secret_lifted_calls_with_untainted_mir_results() { let error = emit_host_object(&program, &types) .expect_err("secret lifting must retain the checked result taint"); assert!( - matches!(error, CodegenError::InvalidMirContract { ref message, .. } - if message.contains("call result type")), + matches!(error, CodegenError::InvalidMir(ref errors) + if errors.iter().any(|error| error.message == "call ownership result differs from its checked source certificate")), "{callee}: {error:?}" ); } @@ -1030,8 +1059,10 @@ fn rejects_secret_lifting_with_mismatched_argument_types() { let error = emit_host_object(&program, &types) .expect_err("secret lifting must preserve the expected payload type"); assert!( - matches!(error, CodegenError::InvalidMirContract { ref message, .. } - if message.contains("call argument type")), + matches!(error, CodegenError::InvalidMir(ref errors) + if errors.iter().any(|error| error.message == if callee == "callback" { + "call ownership converted operand differs from its physical parameter" + } else { "call ownership indirect converted operand differs from its physical parameter" })), "{callee}({parameter}): {error:?}" ); } @@ -1066,8 +1097,10 @@ fn rejects_secret_lifting_into_impure_call_parameters() { let error = emit_host_object(&program, &types) .expect_err("impure calls cannot accept secret-lifted arguments"); assert!( - matches!(error, CodegenError::InvalidMirContract { ref message, .. } - if message.contains("call argument type")), + matches!(error, CodegenError::InvalidMir(ref errors) + if errors.iter().any(|error| error.message == if callee == "callback" { + "call ownership converted operand differs from its physical parameter" + } else { "call ownership indirect converted operand differs from its physical parameter" })), "{callee}: {error:?}" ); } @@ -2646,7 +2679,10 @@ fn native_reflected_value_pipelines_reject_missing_and_wrong_typed_metadata() { emit_host_object(&program, &types) })); assert!( - matches!(rejection, Ok(Err(CodegenError::InvalidMirContract { .. }))), + matches!(rejection, Ok(Err(CodegenError::InvalidMir(ref errors))) + if errors.iter().any(|error| error.message == if remove { + "call ownership operand count is invalid" + } else { "generated operand has no checked actual/conversion type" })), "{name}, remove={remove}: malformed metadata must be rejected without panicking: {rejection:?}" ); } diff --git a/crates/jett_driver/src/lib.rs b/crates/jett_driver/src/lib.rs index 66ac93bd..d173cf31 100644 --- a/crates/jett_driver/src/lib.rs +++ b/crates/jett_driver/src/lib.rs @@ -3021,8 +3021,13 @@ 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) - .map_err(|error| error.with_debug_observations(&debug_observations))? + append_native_verify_suite( + &mut hir, + &parse_result.module, + entry_file, + &check_result.interner, + ) + .map_err(|error| error.with_debug_observations(&debug_observations))? } else { None }; @@ -3565,6 +3570,7 @@ fn append_native_verify_suite( hir: &mut jett_hir::Program, module: &Module, entry_file: FileId, + types: &jett_types::TypeInterner, ) -> Result, BackendLoweringError> { let mut verifies = Vec::new(); let mut suite_namespace = None; @@ -3602,19 +3608,34 @@ fn append_native_verify_suite( let id = jett_hir::FunctionId::new(hir.functions.len() as u32); let statements = verifies .into_iter() - .map(|(function, span)| jett_hir::Statement { - kind: jett_hir::StatementKind::Expression(jett_hir::Expression { - kind: jett_hir::ExpressionKind::Call { - function, - args: Vec::new(), - evaluation_order: Vec::new(), - }, - ty: jett_types::TypeInterner::NOTHING, + .map(|(function, span)| { + let ownership = jett_hir::CallOwnership::generated( + jett_hir::GeneratedOperation::NativeSuite { function }, + &[], + &[], + &[], + jett_types::TypeInterner::NOTHING, + &[], + types, + ) + .map_err(|message| { + BackendLoweringError::Hir(vec![jett_hir::LowerError { span, message }]) + })?; + Ok(jett_hir::Statement { + kind: jett_hir::StatementKind::Expression(jett_hir::Expression { + kind: jett_hir::ExpressionKind::Call { + function, + args: Vec::new(), + evaluation_order: Vec::new(), + ownership, + }, + ty: jett_types::TypeInterner::NOTHING, + span, + }), span, - }), - span, + }) }) - .collect(); + .collect::, BackendLoweringError>>()?; hir.functions.push(jett_hir::Function { id, debug_kind: jett_hir::FunctionDebugKind::named( diff --git a/crates/jett_driver/src/native_builder_constants.rs b/crates/jett_driver/src/native_builder_constants.rs index 24ee8e04..d39a3b3d 100644 --- a/crates/jett_driver/src/native_builder_constants.rs +++ b/crates/jett_driver/src/native_builder_constants.rs @@ -8,7 +8,9 @@ use jett_hir::{ }; use jett_types::{ReflectionFieldInfo, ReflectionTypeInfo, Type, TypeId, TypeInterner}; -use crate::native_property_cases::{ValueContext, value_expression}; +use crate::native_property_cases::{ + ValueContext, evaluated_intrinsic_expression, value_expression, +}; pub(super) fn builder_expression( value: &Value, @@ -109,7 +111,7 @@ pub(super) fn builder_expression( context, )?); } - let mut current = intrinsic_expression( + let mut current = evaluated_intrinsic_expression( intrinsic, vec![owner], arguments, @@ -120,7 +122,8 @@ pub(super) fn builder_expression( TypeInterner::TYPE_CONSTRUCTION }, span, - ); + types, + )?; if member.is_some() { current = unwrap_builder(current, span); } @@ -163,14 +166,15 @@ pub(super) fn builder_expression( )?; let payload = value_expression(payload, payload_type, span, context)?; current = unwrap_builder( - intrinsic_expression( + evaluated_intrinsic_expression( IntrinsicId::TypeConstructPut, vec![owner, payload_type], vec![info.clone(), payload_info], vec![current, metadata, payload], builder_result_type(types)?, span, - ), + types, + )?, span, ); } @@ -221,29 +225,6 @@ fn builder_result_type(types: &TypeInterner) -> Result { .ok_or("checked builder result type is absent".into()) } -fn intrinsic_expression( - intrinsic: IntrinsicId, - type_arguments: Vec, - reflection_arguments: Vec, - args: Vec, - ty: TypeId, - span: Span, -) -> Expression { - Expression { - kind: ExpressionKind::Intrinsic { - intrinsic, - type_arguments, - reflection_arguments, - refinement_predicates: Vec::new(), - field_validation: None, - evaluation_order: (0..args.len()).collect(), - args, - }, - ty, - span, - } -} - fn unwrap_builder(target: Expression, span: Span) -> Expression { Expression { kind: ExpressionKind::Handle { diff --git a/crates/jett_driver/src/native_property_cases.rs b/crates/jett_driver/src/native_property_cases.rs index 9b816c10..62f57908 100644 --- a/crates/jett_driver/src/native_property_cases.rs +++ b/crates/jett_driver/src/native_property_cases.rs @@ -10,7 +10,7 @@ use jett_hir::{ StateId, Statement, StatementKind, VariantId, }; use jett_parser::ast::{Item, Module}; -use jett_types::{ReflectionMetadata, Type, TypeId, TypeInterner}; +use jett_types::{ReflectionMetadata, ReflectionTypeInfo, Type, TypeId, TypeInterner}; use std::collections::HashSet; pub(super) fn append_property_suite( @@ -243,11 +243,38 @@ fn append_trial( .map(|(value, param)| value_expression(value, param.ty, span, &mut context)) .collect::, _>>() .map_err(|message| error(span, message))?; + let evaluation_order = (0..args.len()).collect::>(); + let parameter_types = function + .params + .iter() + .map(|parameter| parameter.ty) + .collect::>(); + let access = function + .params + .iter() + .map(|parameter| match parameter.mode { + ParamMode::Owned => jett_typecheck::CheckedCalleeAccess::Owned, + ParamMode::View => jett_typecheck::CheckedCalleeAccess::View, + }) + .collect::>(); + let ownership = jett_hir::CallOwnership::generated( + jett_hir::GeneratedOperation::NativeSuite { + function: function.id, + }, + &args, + ¶meter_types, + &access, + TypeInterner::NOTHING, + &evaluation_order, + types, + ) + .map_err(|message| error(span, message))?; statements.push(Statement { kind: StatementKind::Expression(Expression { kind: ExpressionKind::Call { function: function.id, - evaluation_order: (0..args.len()).collect(), + evaluation_order, + ownership, args, }, ty: TypeInterner::NOTHING, @@ -964,58 +991,89 @@ fn function_value_expression( } } -fn set_expression( - values: &[Value], - element: TypeId, +pub(super) fn evaluated_intrinsic_expression( + intrinsic: IntrinsicId, + type_arguments: Vec, + reflection_arguments: Vec, + args: Vec, ty: TypeId, span: Span, - context: &mut ValueContext<'_>, + types: &TypeInterner, ) -> Result { - let mut current = Expression { + let parameter_types = args.iter().map(|argument| argument.ty).collect::>(); + let access = (0..args.len()) + .map(|index| { + jett_typecheck::intrinsic_operand_access(intrinsic, index, args.len()) + .ok_or_else(|| "evaluated intrinsic operand has no checked access role".to_string()) + }) + .collect::, _>>()?; + let evaluation_order = (0..args.len()).collect::>(); + let ownership = jett_hir::CallOwnership::generated( + jett_hir::GeneratedOperation::EvaluatedValue { intrinsic }, + &args, + ¶meter_types, + &access, + ty, + &evaluation_order, + types, + )?; + Ok(Expression { kind: ExpressionKind::Intrinsic { - intrinsic: IntrinsicId::SetNew, - type_arguments: vec![element], - reflection_arguments: Vec::new(), + intrinsic, + type_arguments, + reflection_arguments, refinement_predicates: Vec::new(), field_validation: None, - args: Vec::new(), - evaluation_order: Vec::new(), + args, + evaluation_order, + ownership, }, ty, span, - }; + }) +} + +fn set_expression( + values: &[Value], + element: TypeId, + ty: TypeId, + span: Span, + context: &mut ValueContext<'_>, +) -> Result { + let mut current = evaluated_intrinsic_expression( + IntrinsicId::SetNew, + vec![element], + Vec::new(), + Vec::new(), + ty, + span, + context.types, + )?; for value in values { - current = Expression { - kind: ExpressionKind::Intrinsic { - intrinsic: IntrinsicId::SetAdd, - type_arguments: vec![element], - reflection_arguments: Vec::new(), - refinement_predicates: Vec::new(), - field_validation: None, - args: vec![current, value_expression(value, element, span, context)?], - evaluation_order: vec![0, 1], - }, + let element_value = value_expression(value, element, span, context)?; + current = evaluated_intrinsic_expression( + IntrinsicId::SetAdd, + vec![element], + Vec::new(), + vec![current, element_value], ty, span, - }; + context.types, + )?; } Ok(current) } fn bytes_expression(bytes: &[u8], span: Span, types: &TypeInterner) -> Result { - let empty = Expression { - kind: ExpressionKind::Intrinsic { - intrinsic: IntrinsicId::BytesNew, - type_arguments: Vec::new(), - reflection_arguments: Vec::new(), - refinement_predicates: Vec::new(), - field_validation: None, - args: Vec::new(), - evaluation_order: Vec::new(), - }, - ty: TypeInterner::BYTES, + let empty = evaluated_intrinsic_expression( + IntrinsicId::BytesNew, + Vec::new(), + Vec::new(), + Vec::new(), + TypeInterner::BYTES, span, - }; + types, + )?; if bytes.is_empty() { return Ok(empty); } @@ -1028,23 +1086,19 @@ fn bytes_expression(bytes: &[u8], span: Span, types: &TypeInterner) -> Result { + let output = jett_driver::run_file_capture_outcome(&source) + .unwrap_or_else(|error| panic!("{name}: checked reference fixture: {error:?}")); + assert_eq!(output.stdout, stdout, "{name}"); + (output, 0, String::new()) + } + ExpectedOutcome::RuntimeFailure { stdout, message } => { + let failure = jett_driver::run_file_capture_outcome(&source) + .expect_err("expected terminal caller-ownership fixture failure"); + assert_eq!(failure.output.stdout, stdout, "{name}"); + assert_eq!(failure.message, message, "{name}"); + (failure.output, 71, format!("{message}\n")) + } + }; + assert!(reference.debug_events.is_empty(), "{name}: {reference:?}"); + assert!( + reference.frontend_debug_observations.is_empty(), + "{name}: {reference:?}" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("{name}_{release}.exe")); + let artifact = jett_driver::native::build_host_executable_with_options( + &source, + launcher_for_options(release), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: native build: {error}")); + assert!( + artifact.debug_observations.is_empty(), + "{name}: {artifact:?}" + ); + binaries.push((binary, release)); + } + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(status), + "{name}, release={release}: {actual:?}" + ); + assert_eq!( + actual.stdout, + reference.stdout.as_bytes(), + "{name}, release={release}" + ); + assert_eq!( + actual.stderr, + terminal_stderr.as_bytes(), + "{name}, release={release}: {actual:?}" + ); + } +} + +#[test] +fn native_call_ownership_direct_bare_view_argument_relinquishes_the_owner() { + run_case( + "01_direct_last_use", + include_str!("call_ownership/01_direct_last_use.jett"), + ExpectedOutcome::Success("direct:2\n"), + ); +} + +#[test] +fn native_call_ownership_indirect_bare_view_argument_relinquishes_the_owner() { + run_case( + "02_indirect_last_use", + include_str!("call_ownership/02_indirect_last_use.jett"), + ExpectedOutcome::Success("indirect:2\n"), + ); +} + +#[test] +fn native_call_ownership_named_view_arguments_follow_the_checked_permutation() { + run_case( + "03_named_last_use", + include_str!("call_ownership/03_named_last_use.jett"), + ExpectedOutcome::Success("named:5\n"), + ); +} + +#[test] +fn native_call_ownership_pipeline_bare_view_argument_relinquishes_the_owner() { + run_case( + "04_pipeline_last_use", + include_str!("call_ownership/04_pipeline_last_use.jett"), + ExpectedOutcome::Success("pipeline:2\n"), + ); +} + +#[test] +fn native_call_ownership_written_views_and_explicit_clone_preserve_the_owner() { + run_case( + "05_explicit_retention", + include_str!("call_ownership/05_explicit_retention.jett"), + ExpectedOutcome::Success("retained:2:2:2:2:2:2\n"), + ); +} + +#[test] +fn native_call_ownership_source_first_owned_parameter_is_transferred() { + run_case( + "06_source_first_owned_transfer", + include_str!("call_ownership/06_source_first_owned_transfer.jett"), + ExpectedOutcome::Success("transfer:5:5:2\n"), + ); +} + +#[test] +fn native_call_ownership_relinquished_owner_survives_a_later_default_handler() { + run_case( + "07_later_handler_default", + include_str!("call_ownership/07_later_handler_default.jett"), + ExpectedOutcome::Success("before:default\nlater:default\nanswer:5\n"), + ); +} + +#[test] +fn native_call_ownership_aborted_call_cleans_owner_before_independent_owned_return() { + run_case( + "08_early_return_cleanup", + include_str!("call_ownership/08_early_return_cleanup.jett"), + ExpectedOutcome::Success("before:return\nlater:return\nreturned:3\n"), + ); +} + +#[test] +fn native_call_ownership_later_failure_cleans_staged_and_live_owners() { + run_case( + "09_terminal_failure_cleanup", + include_str!("call_ownership/09_terminal_failure_cleanup.jett"), + ExpectedOutcome::RuntimeFailure { + stdout: "before:cleanup\nfailure:cleanup\n", + message: "runtime error: list.__remove_at: index -1 out of bounds", + }, + ); +} + +#[test] +fn native_call_ownership_copyable_scalars_and_noncopyable_representations_are_distinct() { + run_case( + "10_scalar_refinement_function", + include_str!("call_ownership/10_scalar_refinement_function.jett"), + ExpectedOutcome::Success("typed:7:7:7:4\n"), + ); +} + +#[test] +fn native_call_ownership_named_owned_producers_are_evaluated_once_in_source_order() { + run_case( + "11_named_producer_order", + include_str!("call_ownership/11_named_producer_order.jett"), + ExpectedOutcome::Success("source:right\nsource:left\nordered:5\n"), + ); +} + +#[test] +fn native_call_ownership_consumed_binding_rereads_fail_before_native_publication() { + let cases = [ + ( + "negative_direct_reread", + include_str!("call_ownership/negative_direct_reread.jett"), + ), + ( + "negative_parenthesized_reread", + include_str!("call_ownership/negative_parenthesized_reread.jett"), + ), + ( + "negative_indirect_reread", + include_str!("call_ownership/negative_indirect_reread.jett"), + ), + ( + "negative_named_owned_reread", + include_str!("call_ownership/negative_named_owned_reread.jett"), + ), + ( + "negative_pipeline_reread", + include_str!("call_ownership/negative_pipeline_reread.jett"), + ), + ( + "negative_refinement_reread", + include_str!("call_ownership/negative_refinement_reread.jett"), + ), + ( + "negative_function_reread", + include_str!("call_ownership/negative_function_reread.jett"), + ), + ]; + for (name, source_text) in cases { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, source_text).unwrap(); + let launcher = if cfg!(windows) { + jett_driver::native::NativeLauncherBundle::windows_msvc_static_v1( + directory.path().join("unused.lib"), + ) + } else { + jett_driver::native::NativeLauncherBundle::linux_gnu_v1( + directory.path().join("unused.a"), + ) + }; + let output = directory.path().join("preserved.exe"); + let sentinel = b"existing caller ownership publication"; + fs::write(&output, sentinel).unwrap(); + assert!(!launcher.archive_path.exists()); + for release in [false, true] { + let error = jett_driver::native::build_host_executable_with_options( + &source, + &launcher, + &output, + jett_driver::BuildOptions { release }, + ) + .expect_err("consumed binding must fail before runtime archive lookup"); + let build = error.build_result().expect("typed frontend failure"); + assert!(build.has_errors, "{name}, release={release}"); + let errors = build + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert_eq!(errors.len(), 1, "{name}, release={release}: {errors:?}"); + assert_eq!(errors[0].code.code(), 400, "{name}, release={release}"); + assert!(error.debug_observations().is_empty()); + assert!(build.debug_observations.is_empty()); + assert!(!launcher.archive_path.exists()); + assert_eq!(fs::read(&output).unwrap(), sentinel); + } + } +} + +#[test] +fn native_call_ownership_present_success_keeps_its_payload_after_pruning() { + run_case( + "12_inferred_present_success", + include_str!("call_ownership/12_inferred_present_success.jett"), + ExpectedOutcome::Success("before:success\nafter:success\ncount:2\n"), + ); +} + +#[test] +fn native_call_ownership_inhabited_error_control_keeps_the_original_branch() { + run_case( + "13_inhabited_error_control", + include_str!("call_ownership/13_inhabited_error_control.jett"), + ExpectedOutcome::Success("before:success\nafter:success\ncount:2\n"), + ); +} + +#[test] +fn native_call_ownership_present_success_still_rejects_pending_before_extraction() { + run_case( + "14_pending_present_success", + include_str!("call_ownership/14_pending_present_success.jett"), + ExpectedOutcome::RuntimeFailure { + stdout: "before:success\n", + message: "runtime error: handle block requires a result or optional value, got pending(pending(ok(list(1, 2))))", + }, + ); +} + +#[test] +fn native_call_ownership_converted_handled_view_reads_its_input_and_owns_the_box() { + run_case( + "15_converted_handled_view", + include_str!("call_ownership/15_converted_handled_view.jett"), + ExpectedOutcome::Success( + "source\ndefault:source\nlater\ndefault:later\nabsent:item:1:3\nsource\nlater\npresent:item:2:11\n", + ), + ); +} diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/01_direct_last_use.jett b/crates/jett_driver/tests/native_conformance/call_ownership/01_direct_last_use.jett new file mode 100644 index 00000000..b3882295 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/01_direct_last_use.jett @@ -0,0 +1,10 @@ +namespace call_probe +export function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + list[int64] values = list(1, 2) + int64 answer = call_probe.count(values) + Stdout.write(view stdout, "direct:{answer}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/02_indirect_last_use.jett b/crates/jett_driver/tests/native_conformance/call_ownership/02_indirect_last_use.jett new file mode 100644 index 00000000..18b5244b --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/02_indirect_last_use.jett @@ -0,0 +1,11 @@ +namespace call_probe +export function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + list[int64] values = list(1, 2) + function(view list[int64]) returns int64 callback = call_probe.count + int64 answer = callback(values) + Stdout.write(view stdout, "indirect:{answer}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/03_named_last_use.jett b/crates/jett_driver/tests/native_conformance/call_ownership/03_named_last_use.jett new file mode 100644 index 00000000..43458d97 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/03_named_last_use.jett @@ -0,0 +1,11 @@ +namespace call_probe +export function pair(view left: list[int64], view right: list[int64]) returns int64: + return list.length[int64](view left) + list.length[int64](view right) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + list[int64] left = list(1, 2) + list[int64] right = list(3, 4, 5) + int64 answer = call_probe.pair(right: right, left: left) + Stdout.write(view stdout, "named:{answer}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/04_pipeline_last_use.jett b/crates/jett_driver/tests/native_conformance/call_ownership/04_pipeline_last_use.jett new file mode 100644 index 00000000..01a4ccd7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/04_pipeline_last_use.jett @@ -0,0 +1,10 @@ +namespace call_probe +export function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + list[int64] values = list(1, 2) + int64 answer = values into call_probe.count() + Stdout.write(view stdout, "pipeline:{answer}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/05_explicit_retention.jett b/crates/jett_driver/tests/native_conformance/call_ownership/05_explicit_retention.jett new file mode 100644 index 00000000..aba23424 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/05_explicit_retention.jett @@ -0,0 +1,17 @@ +namespace call_probe +export function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + use call_probe as p + list[int64] values = list(1, 2) + function(view list[int64]) returns int64 callback = call_probe.count + int64 direct = call_probe.count(view values) + int64 aliased = p.count(view values) + int64 indirect = callback(view values) + int64 piped = values into view p.count() + int64 copied = call_probe.count(clone values) + int64 after = call_probe.count(view values) + Stdout.write(view stdout, "retained:{direct}:{aliased}:{indirect}:{piped}:{copied}:{after}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/06_source_first_owned_transfer.jett b/crates/jett_driver/tests/native_conformance/call_ownership/06_source_first_owned_transfer.jett new file mode 100644 index 00000000..2f003e5a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/06_source_first_owned_transfer.jett @@ -0,0 +1,12 @@ +namespace call_probe +export function mix(view left: list[int64], right: list[int64]) returns int64: + return list.length[int64](view left) + list.length[int64](right) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + list[int64] left = list(1, 2) + list[int64] right = list(3, 4, 5) + int64 answer = call_probe.mix(right: right, left: view left) + int64 retained = call_probe.mix(right: list(6, 7, 8), left: view left) + Stdout.write(view stdout, "transfer:{answer}:{retained}:{list.length[int64](view left)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/07_later_handler_default.jett b/crates/jett_driver/tests/native_conformance/call_ownership/07_later_handler_default.jett new file mode 100644 index 00000000..52159e37 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/07_later_handler_default.jett @@ -0,0 +1,15 @@ +namespace app +function with_extra(view values: list[int64], extra: int64) returns int64: + return list.length[int64](view values) + extra +function fallback(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "later:default\n") + return 3 +function main(stdout: Stdout) returns nothing: + list[int64] values = list(1, 2) + optional[int64] absent = none + Stdout.write(view stdout, "before:default\n") + int64 answer = with_extra(values, absent handle: + default fallback(view stdout) + ) + Stdout.write(view stdout, "answer:{answer}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/08_early_return_cleanup.jett b/crates/jett_driver/tests/native_conformance/call_ownership/08_early_return_cleanup.jett new file mode 100644 index 00000000..0fb21f2a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/08_early_return_cleanup.jett @@ -0,0 +1,14 @@ +namespace app +function with_extra(view values: list[int64], extra: int64) returns int64: + return list.length[int64](view values) + extra +function choose(values: list[int64], backup: list[int64], incoming: optional[int64], view stdout: Stdout) returns list[int64]: + Stdout.write(view stdout, "before:return\n") + int64 answer = with_extra(values, incoming handle: + Stdout.write(view stdout, "later:return\n") + return backup + ) + return list(answer) +function main(stdout: Stdout) returns nothing: + list[int64] selected = choose(list(1, 2), list(3, 4, 5), none, view stdout) + Stdout.write(view stdout, "returned:{list.length[int64](selected)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/09_terminal_failure_cleanup.jett b/crates/jett_driver/tests/native_conformance/call_ownership/09_terminal_failure_cleanup.jett new file mode 100644 index 00000000..9d4609dc --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/09_terminal_failure_cleanup.jett @@ -0,0 +1,18 @@ +namespace app +function with_extra(view values: list[int64], extra: int64) returns int64: + return list.length[int64](view values) + extra +function failing(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "failure:cleanup\n") + list[string] broken = list.remove_at[string](list("owned", "other"), -1) + return list.length[string](broken) +function main(stdout: Stdout) returns nothing: + bytes payload = bytes.from_string("cleanup") + list[string] retained = list("owner", "kept") + list[int64] values = list(1, 2) + optional[int64] absent = none + Stdout.write(view stdout, "before:cleanup\n") + int64 answer = with_extra(values, absent handle: + default failing(view stdout) + ) + Stdout.write(view stdout, "after:{answer}:{bytes.to_hex(view payload)}:{list.length[string](view retained)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/10_scalar_refinement_function.jett b/crates/jett_driver/tests/native_conformance/call_ownership/10_scalar_refinement_function.jett new file mode 100644 index 00000000..0d481fce --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/10_scalar_refinement_function.jett @@ -0,0 +1,21 @@ +namespace app +type Positive = int64 where value > 0 +function scalar(view value: int64) returns int64: + return value +function refined(view value: Positive) returns int64: + return coarsen clone value +function increment(value: int64) returns int64: + return value + 1 +function apply(view callback: function(int64) returns int64, value: int64) returns int64: + return callback(value) +function report(number: int64) returns string: + Positive established = number handle error: + return error + int64 nominal = refined(established) + function(int64) returns int64 callback = increment + int64 invoked = apply(callback, 3) + int64 copied = scalar(number) + return "{copied}:{number}:{nominal}:{invoked}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "typed:{report(7)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/11_named_producer_order.jett b/crates/jett_driver/tests/native_conformance/call_ownership/11_named_producer_order.jett new file mode 100644 index 00000000..c834df12 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/11_named_producer_order.jett @@ -0,0 +1,13 @@ +namespace app +function pair(view left: list[int64], view right: list[int64]) returns int64: + return list.length[int64](view left) + list.length[int64](view right) +function make_right(view stdout: Stdout) returns list[int64]: + Stdout.write(view stdout, "source:right\n") + return list(3, 4, 5) +function make_left(view stdout: Stdout) returns list[int64]: + Stdout.write(view stdout, "source:left\n") + return list(1, 2) +function main(stdout: Stdout) returns nothing: + int64 answer = pair(right: make_right(view stdout), left: make_left(view stdout)) + Stdout.write(view stdout, "ordered:{answer}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/12_inferred_present_success.jett b/crates/jett_driver/tests/native_conformance/call_ownership/12_inferred_present_success.jett new file mode 100644 index 00000000..450f1c43 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/12_inferred_present_success.jett @@ -0,0 +1,15 @@ +namespace app +function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +function measure[E](outcome: result[list[int64], E], view stdout: Stdout) returns int64: + Stdout.write(view stdout, "before:success\n") + int64 answer = count(outcome handle error: + Stdout.write(view stdout, "failure:success\n") + return 0 + ) + Stdout.write(view stdout, "after:success\n") + return answer +function main(stdout: Stdout) returns nothing: + int64 answer = measure(ok(list(1, 2)), view stdout) + Stdout.write(view stdout, "count:{answer}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/13_inhabited_error_control.jett b/crates/jett_driver/tests/native_conformance/call_ownership/13_inhabited_error_control.jett new file mode 100644 index 00000000..61f08d32 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/13_inhabited_error_control.jett @@ -0,0 +1,16 @@ +namespace app +function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +function measure[E](outcome: result[list[int64], E], view stdout: Stdout) returns int64: + Stdout.write(view stdout, "before:success\n") + int64 answer = count(outcome handle error: + Stdout.write(view stdout, "failure:success\n") + return 0 + ) + Stdout.write(view stdout, "after:success\n") + return answer +function main(stdout: Stdout) returns nothing: + result[list[int64], string] outcome = ok(list(1, 2)) + int64 answer = measure(outcome, view stdout) + Stdout.write(view stdout, "count:{answer}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/14_pending_present_success.jett b/crates/jett_driver/tests/native_conformance/call_ownership/14_pending_present_success.jett new file mode 100644 index 00000000..12f8d854 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/14_pending_present_success.jett @@ -0,0 +1,15 @@ +namespace app +function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +function measure[E](outcome: result[list[int64], E], view stdout: Stdout) returns int64: + Stdout.write(view stdout, "before:success\n") + int64 answer = count(outcome handle error: + Stdout.write(view stdout, "failure:success\n") + return 0 + ) + Stdout.write(view stdout, "after:success\n") + return answer +function main(stdout: Stdout) returns nothing: + int64 answer = measure(run run ok(list(1, 2)), view stdout) + Stdout.write(view stdout, "count:{answer}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/15_converted_handled_view.jett b/crates/jett_driver/tests/native_conformance/call_ownership/15_converted_handled_view.jett new file mode 100644 index 00000000..ac71ab24 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/15_converted_handled_view.jett @@ -0,0 +1,34 @@ +namespace app +interface Named: + function name(view self: Named) returns string +struct Item: + values: list[int64] +implement Named for Item: + function name(view self: Item) returns string: + return "item:{list.length[int64](view self.values)}" +function read(view value: Named, extra: int64) returns string: + return "{Named.name(view value)}:{extra}" +function make(view stdout: Stdout, present: bool) returns optional[Item]: + Stdout.write(view stdout, "source\n") + if present: + return some(Item(values: list(2, 3))) + return none +function later(view stdout: Stdout, present: bool) returns optional[int64]: + Stdout.write(view stdout, "later\n") + if present: + return some(11) + return none +function exercise(view stdout: Stdout, present: bool) returns string: + return read(view(make(view stdout, present) handle: + Stdout.write(view stdout, "default:source\n") + default Item(values: list(7)) + ), later(view stdout, present) handle: + Stdout.write(view stdout, "default:later\n") + default 3 + ) +function main(stdout: Stdout) returns nothing: + string absent = exercise(view stdout, false) + Stdout.write(view stdout, "absent:{absent}\n") + string present = exercise(view stdout, true) + Stdout.write(view stdout, "present:{present}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/negative_direct_reread.jett b/crates/jett_driver/tests/native_conformance/call_ownership/negative_direct_reread.jett new file mode 100644 index 00000000..3f2a4631 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/negative_direct_reread.jett @@ -0,0 +1,11 @@ +namespace call_probe +export function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + list[int64] values = list(1, 2) + int64 before = call_probe.count(values) + int64 after = call_probe.count(view values) + Stdout.write(view stdout, "qualified:{before}:{after}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/negative_function_reread.jett b/crates/jett_driver/tests/native_conformance/call_ownership/negative_function_reread.jett new file mode 100644 index 00000000..4cbd5d98 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/negative_function_reread.jett @@ -0,0 +1,11 @@ +namespace app +function increment(value: int64) returns int64: + return value + 1 +function apply(view callback: function(int64) returns int64, value: int64) returns int64: + return callback(value) +function main(stdout: Stdout) returns nothing: + function(int64) returns int64 callback = increment + int64 before = apply(callback, 3) + int64 after = apply(view callback, 5) + Stdout.write(view stdout, "{before}:{after}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/negative_indirect_reread.jett b/crates/jett_driver/tests/native_conformance/call_ownership/negative_indirect_reread.jett new file mode 100644 index 00000000..28cb6ff4 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/negative_indirect_reread.jett @@ -0,0 +1,12 @@ +namespace call_probe +export function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + list[int64] values = list(1, 2) + function(view list[int64]) returns int64 callback = call_probe.count + int64 before = callback(values) + int64 after = call_probe.count(view values) + Stdout.write(view stdout, "indirect:{before}:{after}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/negative_named_owned_reread.jett b/crates/jett_driver/tests/native_conformance/call_ownership/negative_named_owned_reread.jett new file mode 100644 index 00000000..c35a36f2 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/negative_named_owned_reread.jett @@ -0,0 +1,14 @@ +namespace call_probe +export function mix(view left: list[int64], right: list[int64]) returns int64: + return list.length[int64](view left) + list.length[int64](view right) +export function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + list[int64] left = list(1) + list[int64] right = list(2, 3) + int64 before = call_probe.mix(right: right, left: left) + int64 after = call_probe.count(view right) + Stdout.write(view stdout, "named-owned:{before}:{after}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/negative_parenthesized_reread.jett b/crates/jett_driver/tests/native_conformance/call_ownership/negative_parenthesized_reread.jett new file mode 100644 index 00000000..5b287af7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/negative_parenthesized_reread.jett @@ -0,0 +1,11 @@ +namespace call_probe +export function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + list[int64] values = list(1, 2) + int64 before = (call_probe.count)(values) + int64 after = call_probe.count(view values) + Stdout.write(view stdout, "parenthesized:{before}:{after}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/negative_pipeline_reread.jett b/crates/jett_driver/tests/native_conformance/call_ownership/negative_pipeline_reread.jett new file mode 100644 index 00000000..f7f1fdd3 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/negative_pipeline_reread.jett @@ -0,0 +1,11 @@ +namespace call_probe +export function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + list[int64] values = list(1, 2) + int64 before = values into call_probe.count() + int64 after = call_probe.count(view values) + Stdout.write(view stdout, "pipeline-qualified:{before}:{after}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/negative_refinement_reread.jett b/crates/jett_driver/tests/native_conformance/call_ownership/negative_refinement_reread.jett new file mode 100644 index 00000000..bb992045 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/negative_refinement_reread.jett @@ -0,0 +1,13 @@ +namespace app +type Positive = int64 where value > 0 +function refined(view value: Positive) returns int64: + return coarsen clone value +function report(number: int64) returns string: + Positive established = number handle error: + return error + int64 before = refined(established) + int64 after = refined(view established) + return "{before}:{after}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{report(7)}\n") + return nothing diff --git a/crates/jett_driver/tests/view_escapes.rs b/crates/jett_driver/tests/view_escapes.rs index 35eb55fe..296b9099 100644 --- a/crates/jett_driver/tests/view_escapes.rs +++ b/crates/jett_driver/tests/view_escapes.rs @@ -54,11 +54,11 @@ fn rejected_cases() -> Vec<(&'static str, String)> { ), ( "verify_alias", - "verify invalid_check:\n list[int64] values = list(7)\n list[int64] borrowed = view values\n int64 observed = consume(borrowed)\n assert observed == 1\n", + "function invalid(view values: list[int64]) returns int64:\n list[int64] borrowed = view values\n return consume(borrowed)\nverify invalid_check:\n list[int64] values = list(7)\n int64 observed = invalid(view values)\n assert observed == 1\n", ), ( "property_alias", - "property invalid_trials:\n given input: int64\n list[int64] values = list(input)\n list[int64] borrowed = view values\n int64 observed = consume(borrowed)\n assert observed == 1\n", + "function invalid(view values: list[int64]) returns int64:\n list[int64] borrowed = view values\n return consume(borrowed)\nproperty invalid_trials:\n given input: int64\n list[int64] values = list(input)\n int64 observed = invalid(view values)\n assert observed == 1\n", ), ] .into_iter() @@ -99,6 +99,52 @@ fn borrowed_values_cannot_escape_before_program_or_suite_execution() { } } +#[test] +fn lexical_verify_and_property_observations_preserve_borrowed_aliases() { + let cases = [ + ( + "verify_alias", + "verify observed_check:\n list[int64] values = list(7)\n list[int64] borrowed = view values\n int64 observed = consume(borrowed)\n assert observed == 1\n assert list.length(view borrowed) == 1\n assert list.length(view values) == 1\n", + "observed_check", + false, + ), + ( + "property_alias", + "property observed_trials:\n given input: int64\n list[int64] values = list(input)\n list[int64] borrowed = view values\n int64 observed = consume(borrowed)\n assert observed == 1\n assert list.length(view borrowed) == 1\n assert list.length(view values) == 1\n", + "observed_trials", + true, + ), + ]; + for (name, body, block_name, is_property) in cases { + let source = format!("{DECLARATIONS}{body}"); + let built = build_source(&source, name); + assert!(!built.has_errors, "{name}: {:?}", built.diagnostics); + assert!(built.debug_observations.is_empty(), "{name}"); + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("main.jett"); + fs::write(&path, source).unwrap(); + let outcome = test_file(&path).expect("lexical observation suites should execute"); + assert_eq!( + (outcome.total, outcome.passed, outcome.failed), + (1, 1, 0), + "{name}" + ); + assert!(outcome.debug_observations.is_empty(), "{name}"); + assert_eq!(outcome.blocks.len(), 1, "{name}"); + let block = &outcome.blocks[0]; + assert_eq!(block.name, block_name, "{name}"); + assert!(block.passed, "{name}: {:?}", block.error); + assert!(block.error.is_none(), "{name}"); + assert_eq!(block.is_property, is_property, "{name}"); + assert_eq!( + block.iterations, + if is_property { Some(100) } else { None }, + "{name}" + ); + assert!(block.debug_events.is_empty(), "{name}"); + } +} + #[cfg(all( target_arch = "x86_64", any( diff --git a/crates/jett_hir/src/call_ownership.rs b/crates/jett_hir/src/call_ownership.rs new file mode 100644 index 00000000..8cadc4a4 --- /dev/null +++ b/crates/jett_hir/src/call_ownership.rs @@ -0,0 +1,5257 @@ +//! Caller authority retained separately from physical invocation operands. +use jett_common::Span; +use jett_typecheck::{ + CheckedCalleeAccess, CheckedCallerEffect, CheckedCallerSyntax, CheckedInvocationShape, + CheckedOwnershipContext, +}; +use jett_types::{Type, TypeId, TypeInterner}; + +use crate::{Expression, ExpressionKind, FunctionId, FunctionIdentity, IntrinsicId, LocalId}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CallerViewSource { + Local(LocalId), + Other, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CallerBindingMode { + Owned, + View { source: CallerViewSource }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CallerBindingFact { + pub local: LocalId, + pub declaration_span: Span, + pub ty: TypeId, + pub mode: CallerBindingMode, + pub mutable: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CallerOrigin { + Binding(CallerBindingFact), + BorrowedProjection { source: CallerViewSource }, + OwnedFieldCopy { parent: CallerViewSource }, + OwnedExpression, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ArgumentOwnership { + pub source_span: Span, + pub source_index: usize, + pub parameter_index: usize, + pub actual_type: TypeId, + pub parameter_type: TypeId, + pub syntax: CheckedCallerSyntax, + pub origin: CallerOrigin, + pub callee_access: CheckedCalleeAccess, + /// Actual implementation access; a validated helper bridge may differ. + pub physical_access: CheckedCalleeAccess, + pub effect: CheckedCallerEffect, + pub staging: ArgumentStaging, + /// Immutable source authority. MIR may stage an operand, never rewrite this witness. + witness: SourceArgumentWitness, + retained_snapshot: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceArgumentWitness { + source_span: Span, + source_index: usize, + parameter_index: usize, + actual_type: TypeId, + occurrence_type: TypeId, + parameter_type: TypeId, + syntax: CheckedCallerSyntax, + origin: CallerOrigin, + callee_access: CheckedCalleeAccess, + effect: CheckedCallerEffect, + context: CheckedOwnershipContext, + observation: Option, + handled_result: Option, + projection_root: Option, +} + +/// Original checked flow occurrence, separate from the owner's stored type. +/// Only the Source lowerer can mint this; it grants no alias or loan authority. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct SourceProjectionRoot { + local: LocalId, + span: Span, + occurrence_type: TypeId, + storage_type: TypeId, +} + +fn retained_snapshot_origin(origin: &CallerOrigin) -> bool { + matches!( + origin, + CallerOrigin::Binding(_) + | CallerOrigin::BorrowedProjection { + source: CallerViewSource::Local(_) + } + ) +} + +/// Checked ordinary-data endpoint permission, independent of caller consumption. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct RetainedSnapshotProof { + actual_type: TypeId, + occurrence_type: TypeId, + /// Exact initial HIR header; pipeline View and raw source spans differ. + physical_span: Span, +} + +/// Exact original Handle backing; it grants no CFG acquisition authority. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct SourceHandledResult { + ty: TypeId, + span: Span, +} + +/// Checked capture ownership, separate from a function's parameter/result types. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ObservationProof { + actual_type: TypeId, + captures: Vec<(LocalId, TypeId)>, + descriptor_checked: bool, +} + +impl ArgumentOwnership { + pub fn source_witness(&self) -> &SourceArgumentWitness { + &self.witness + } + pub fn observation_proof(&self) -> Option<&ObservationProof> { + self.witness.observation.as_ref() + } + pub fn retained_snapshot_type(&self) -> Option { + self.retained_snapshot.map(|proof| proof.occurrence_type) + } + pub fn retained_snapshot_span(&self) -> Option { + self.retained_snapshot.map(|proof| proof.physical_span) + } +} + +impl SourceArgumentWitness { + pub fn projection_root(&self) -> Option<(LocalId, Span, TypeId, TypeId)> { + self.projection_root.map(|root| { + ( + root.local, + root.span, + root.occurrence_type, + root.storage_type, + ) + }) + } + pub fn occurrence_type(&self) -> TypeId { + self.occurrence_type + } + pub fn syntax(&self) -> CheckedCallerSyntax { + self.syntax + } + pub fn context(&self) -> CheckedOwnershipContext { + self.context + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ArgumentStaging { + Original, + Transferred { owner: LocalId }, + Copied { value: LocalId }, + Borrowed { loan: LocalId }, + Relinquished { owner: LocalId, loan: LocalId }, + RetainedSnapshot { owner: LocalId, loan: LocalId }, + Observed { owner: LocalId }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CallTarget { + Function(FunctionId), + /// Proven source facade identity; its implementation body need not survive pruning. + Declaration { + identity: FunctionIdentity, + signature_type: TypeId, + }, + Interface { + function: FunctionId, + interface_type: TypeId, + method_index: usize, + }, + Indirect { + signature_type: TypeId, + }, + Intrinsic(IntrinsicId), +} + +/// An implementation helper does not change the original caller disposition. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CallBridge { + Direct, + TrustedIntrinsic { + intrinsic: IntrinsicId, + }, + JsonSource { + intrinsic: IntrinsicId, + function: FunctionId, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceCallOwnership { + pub target: CallTarget, + pub shape: CheckedInvocationShape, + pub context: CheckedOwnershipContext, + /// Formal order; source_index retains original lexical order. + pub arguments: Vec, + pub bridge: CallBridge, + /// HIR-added metadata tail. It is never counted as a source actual. + pub generated_operands: Vec, + certificate: SourceCallCertificate, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct SourceCallCertificate { + result_type: TypeId, + generated_operands: Vec, + target: CallTarget, + shape: CheckedInvocationShape, + context: CheckedOwnershipContext, + bridge: CallBridge, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GeneratedAcquisition { + Copy, + OwnedExpression, + Borrow { source: CallerViewSource }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GeneratedArgumentOwnership { + pub actual_type: TypeId, + pub parameter_type: TypeId, + pub callee_access: CheckedCalleeAccess, + pub acquisition: GeneratedAcquisition, + pub staging: GeneratedArgumentStaging, + staging_certificate: GeneratedArgumentStaging, + witness: GeneratedArgumentWitness, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GeneratedArgumentStaging { + Existing { loan: Option }, + OwnedProducer { owner: LocalId, loan: LocalId }, + OrdinarySnapshot { owner: LocalId, loan: LocalId }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GeneratedSnapshotProof { + actual_type: TypeId, +} + +impl GeneratedSnapshotProof { + pub fn actual_type(&self) -> TypeId { + self.actual_type + } +} + +/// Private original node identity. The exact discriminant is recorded even for +/// ordinary data leaves; a typed leaf is never interchangeable with a Clone. +#[derive(Debug, Clone, PartialEq, Eq)] +struct GeneratedProducerShape { + kind: std::mem::Discriminant, + ty: TypeId, + span: Span, + identity: GeneratedProducerIdentity, + backing: Vec, + ordinary_handle_source_snapshot: bool, + ordinary_local_snapshot: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum GeneratedProducerIdentity { + Data, + Transparent(GeneratedTransparentShape), + Local(LocalId), + Constant(Span), + Function(FunctionId), + Closure { + function: FunctionId, + captures: Vec, + }, + Call { + function: FunctionId, + operands: Vec<(TypeId, Span)>, + order: Vec, + }, + Intrinsic { + intrinsic: IntrinsicId, + type_arguments: Vec, + operands: Vec<(TypeId, Span)>, + order: Vec, + }, + Indirect { + operands: Vec<(TypeId, Span)>, + order: Vec, + }, + Struct { + owner: TypeId, + order: Vec, + validates_refinements: bool, + }, + Bitfield { + owner: TypeId, + order: Vec, + validates_widths: bool, + }, + Enum { + owner: TypeId, + variant: crate::VariantId, + order: Vec, + }, + Machine { + owner: TypeId, + state: crate::StateId, + }, + Transition { + owner: TypeId, + state: crate::StateId, + }, + Field { + owner: TypeId, + field: crate::FieldId, + }, + InterfaceCoerce(Vec), + FunctionAdapter(FunctionId), + Handle { + kind: crate::HandleKind, + error_local: Option, + failure_span: Span, + }, + Inline { + params: Vec, + view_params: Vec, + local_floor: u32, + body_span: Span, + }, + ActorSpawn { + constructor: Option, + actor_type: String, + order: Vec, + }, + ActorMessage { + handler: FunctionId, + message: String, + order: Vec, + }, + State(crate::StateId), +} + +/// Created only at the canonical extractor's checked Inline-to-descriptor join. +#[derive(Debug, Clone)] +pub(crate) struct GeneratedInlineExtraction { + original: GeneratedProducerShape, + replacement: GeneratedProducerShape, +} + +pub(crate) fn generated_inline_extraction( + original: &Expression, + replacement: &Expression, +) -> Option { + if !matches!(original.kind, ExpressionKind::InlineFunction { .. }) + || !matches!( + replacement.kind, + ExpressionKind::FunctionRef(_) | ExpressionKind::ClosureRef { .. } + ) + || original.ty != replacement.ty + || original.span != replacement.span + { + return None; + } + Some(GeneratedInlineExtraction { + original: GeneratedProducerShape::from_expression(original), + replacement: GeneratedProducerShape::from_expression(replacement), + }) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum GeneratedTransparentShape { + View, + Coarsen, + Declassify, + RefinementValidated, +} + +/// Immutable original node, usable only after the consumer authenticates a +/// canonical owning storage definition. It cannot mint or replace a witness. +#[derive(Debug, Clone, Copy)] +pub struct GeneratedOriginalShape<'a> { + shape: &'a GeneratedProducerShape, +} + +impl<'a> GeneratedOriginalShape<'a> { + pub fn ty(&self) -> TypeId { + self.shape.ty + } + pub fn span(&self) -> Span { + self.shape.span + } + pub fn handle(&self) -> Option> { + matches!( + self.shape.identity, + GeneratedProducerIdentity::Handle { .. } + ) + .then_some(GeneratedHandleShape { shape: self.shape }) + } + pub fn validate_reconstructed( + &self, + value: &Expression, + mut validate_storage: impl FnMut(&Expression, GeneratedOriginalShape<'a>) -> Result<(), String>, + ) -> Result<(), String> { + self.shape.validate_lowered(value, &mut validate_storage) + } + pub fn validate_existing_local_snapshot( + &self, + value: &Expression, + mut validate_storage: impl FnMut(&Expression, GeneratedOriginalShape<'a>) -> Result<(), String>, + ) -> Result<(), String> { + if !self.shape.ordinary_local_snapshot { + return Err( + "call ownership generated node has no sealed existing Local snapshot permission" + .into(), + ); + } + let restored = self + .shape + .restore_local_snapshot(value) + .ok_or("call ownership generated node is not its exact existing Local snapshot")?; + self.shape + .validate_lowered(&restored, &mut validate_storage) + } +} + +/// Read-only original Handle node supplied only during independently +/// authenticated MIR reconstruction. It cannot mint or replace a certificate. +#[derive(Debug, Clone, Copy)] +pub struct GeneratedHandleShape<'a> { + shape: &'a GeneratedProducerShape, +} + +impl<'a> GeneratedHandleShape<'a> { + pub fn kind(&self) -> &'a crate::HandleKind { + let GeneratedProducerIdentity::Handle { kind, .. } = &self.shape.identity else { + unreachable!("private Handle shape"); + }; + kind + } + pub fn result_type(&self) -> TypeId { + self.shape.ty + } + pub fn source_type(&self) -> TypeId { + self.shape.backing[0].ty + } + pub fn span(&self) -> Span { + self.shape.span + } + pub fn source_span(&self) -> Span { + self.shape.backing[0].span + } + pub fn error_local(&self) -> Option { + let GeneratedProducerIdentity::Handle { error_local, .. } = &self.shape.identity else { + unreachable!("private Handle shape"); + }; + *error_local + } + pub fn failure_span(&self) -> Span { + let GeneratedProducerIdentity::Handle { failure_span, .. } = &self.shape.identity else { + unreachable!("private Handle shape"); + }; + *failure_span + } + pub fn validate_source_shape(&self, value: &Expression) -> Result<(), String> { + let mut reject = |_: &Expression, _: GeneratedOriginalShape<'a>| { + Err("call ownership generated Handle source requires its original tree".into()) + }; + self.validate_lowered_source_shape(value, &mut reject) + } + pub fn validate_lowered_source_shape( + &self, + value: &Expression, + mut validate_handle: impl FnMut(&Expression, GeneratedOriginalShape<'a>) -> Result<(), String>, + ) -> Result<(), String> { + let original = &self.shape.backing[0]; + match original.validate_lowered(value, &mut validate_handle) { + Ok(()) => Ok(()), + Err(error) => { + if !self.shape.ordinary_handle_source_snapshot { + return Err(error); + } + let Some(restored) = original.restore_local_snapshot(value) else { + return Err(error); + }; + original.validate_lowered(&restored, &mut validate_handle) + } + } + } +} + +fn generated_backing_values(value: &Expression) -> Vec<&Expression> { + use ExpressionKind as E; + match &value.kind { + E::IndirectCall { callee, .. } => vec![callee], + E::Field { base, .. } => vec![base], + E::Handle { target, .. } => vec![target], + E::StructConstruct { fields, .. } | E::BitfieldConstruct { fields, .. } => { + fields.iter().collect() + } + E::EnumConstruct { payloads, .. } | E::MachineConstruct { payloads, .. } => { + payloads.iter().collect() + } + E::MachineTransition { + source, payloads, .. + } => std::iter::once(source.as_ref()).chain(payloads).collect(), + E::Binary { left, right, .. } => vec![left, right], + E::Unary { value, .. } + | E::View(value) + | E::Clone(value) + | E::Coarsen(value) + | E::Declassify(value) + | E::RefinementValidated(value) + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::InterfaceType(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::RuntimeFailureMessage(value) + | E::Comptime { value, .. } + | E::InterfaceCoerce { value, .. } + | E::FunctionAdapter { value, .. } + | E::StateIs { value, .. } => vec![value], + E::ListConstruct { elements } => elements.iter().collect(), + E::MapConstruct { entries } => entries + .iter() + .flat_map(|entry| [&entry.key, &entry.value]) + .collect(), + E::StringInterpolation(parts) => parts + .iter() + .filter_map(|part| match part { + crate::StringSegment::Value(value) => Some(value), + _ => None, + }) + .collect(), + E::ActorSpawn { args, .. } => args.iter().collect(), + E::ActorMessage { actor, args, .. } => { + std::iter::once(actor.as_ref()).chain(args).collect() + } + E::Int(_) + | E::Float(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::Local(_) + | E::Constant { .. } + | E::FunctionRef(_) + | E::ClosureRef { .. } + | E::Call { .. } + | E::Intrinsic { .. } + | E::OptionalNone + | E::RuntimeFailure(_) + | E::PropertyCaseContext(_) + | E::InlineFunction { .. } => Vec::new(), + } +} + +impl GeneratedProducerShape { + fn from_expression(value: &Expression) -> Self { + use ExpressionKind as E; + let occurrence = + |values: &[Expression]| values.iter().map(|value| (value.ty, value.span)).collect(); + let mut backing = Vec::new(); + let mut add = |value: &Expression| backing.push(Self::from_expression(value)); + let identity = match &value.kind { + E::Local(local) => GeneratedProducerIdentity::Local(*local), + E::Constant { declaration } => GeneratedProducerIdentity::Constant(*declaration), + E::FunctionRef(function) => GeneratedProducerIdentity::Function(*function), + E::ClosureRef { function, captures } => GeneratedProducerIdentity::Closure { + function: *function, + captures: captures.clone(), + }, + // Invocation arguments have their own independently sealed packets. + // Retain their exact typed occurrences, not rewritten storage trees. + E::Call { + function, + args, + evaluation_order, + .. + } => GeneratedProducerIdentity::Call { + function: *function, + operands: occurrence(args), + order: evaluation_order.clone(), + }, + E::Intrinsic { + intrinsic, + type_arguments, + args, + evaluation_order, + .. + } => GeneratedProducerIdentity::Intrinsic { + intrinsic: *intrinsic, + type_arguments: type_arguments.clone(), + operands: occurrence(args), + order: evaluation_order.clone(), + }, + E::IndirectCall { + callee, + args, + evaluation_order, + .. + } => { + add(callee); + GeneratedProducerIdentity::Indirect { + operands: occurrence(args), + order: evaluation_order.clone(), + } + } + E::StructConstruct { + struct_type, + fields, + evaluation_order, + validates_refinements, + .. + } => { + for value in fields { + add(value); + } + GeneratedProducerIdentity::Struct { + owner: *struct_type, + order: evaluation_order.clone(), + validates_refinements: *validates_refinements, + } + } + E::BitfieldConstruct { + bitfield_type, + fields, + evaluation_order, + validates_widths, + } => { + for value in fields { + add(value); + } + GeneratedProducerIdentity::Bitfield { + owner: *bitfield_type, + order: evaluation_order.clone(), + validates_widths: *validates_widths, + } + } + E::EnumConstruct { + enum_type, + variant, + payloads, + evaluation_order, + } => { + for value in payloads { + add(value); + } + GeneratedProducerIdentity::Enum { + owner: *enum_type, + variant: *variant, + order: evaluation_order.clone(), + } + } + E::MachineConstruct { + state_type, + state, + payloads, + } => { + for value in payloads { + add(value); + } + GeneratedProducerIdentity::Machine { + owner: *state_type, + state: *state, + } + } + E::MachineTransition { + source, + state_type, + target, + payloads, + } => { + add(source); + for value in payloads { + add(value); + } + GeneratedProducerIdentity::Transition { + owner: *state_type, + state: *target, + } + } + E::Field { + base, + owner_type, + field, + } => { + add(base); + GeneratedProducerIdentity::Field { + owner: *owner_type, + field: *field, + } + } + E::InterfaceCoerce { value, adapters } => { + add(value); + GeneratedProducerIdentity::InterfaceCoerce(adapters.clone()) + } + E::FunctionAdapter { value, function } => { + add(value); + GeneratedProducerIdentity::FunctionAdapter(*function) + } + E::Handle { + target, + kind, + error_local, + failure, + } => { + add(target); + GeneratedProducerIdentity::Handle { + kind: kind.clone(), + error_local: *error_local, + failure_span: failure.span, + } + } + E::InlineFunction { + params, + view_params, + local_floor, + body, + .. + } => GeneratedProducerIdentity::Inline { + params: params.clone(), + view_params: view_params.clone(), + local_floor: *local_floor, + body_span: body.span, + }, + E::ActorSpawn { + actor_type, + args, + evaluation_order, + constructor, + } => { + for value in args { + add(value); + } + GeneratedProducerIdentity::ActorSpawn { + constructor: *constructor, + actor_type: actor_type.clone(), + order: evaluation_order.clone(), + } + } + E::ActorMessage { + actor, + message, + handler, + args, + evaluation_order, + .. + } => { + add(actor); + for value in args { + add(value); + } + GeneratedProducerIdentity::ActorMessage { + handler: *handler, + message: message.clone(), + order: evaluation_order.clone(), + } + } + E::StateIs { value, state } => { + add(value); + GeneratedProducerIdentity::State(*state) + } + E::Binary { left, right, .. } => { + add(left); + add(right); + GeneratedProducerIdentity::Data + } + E::View(value) => { + add(value); + GeneratedProducerIdentity::Transparent(GeneratedTransparentShape::View) + } + E::Coarsen(value) => { + add(value); + GeneratedProducerIdentity::Transparent(GeneratedTransparentShape::Coarsen) + } + E::Declassify(value) => { + add(value); + GeneratedProducerIdentity::Transparent(GeneratedTransparentShape::Declassify) + } + E::RefinementValidated(value) => { + add(value); + GeneratedProducerIdentity::Transparent( + GeneratedTransparentShape::RefinementValidated, + ) + } + E::Unary { value, .. } + | E::Clone(value) + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::InterfaceType(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::RuntimeFailureMessage(value) + | E::Comptime { value, .. } => { + add(value); + GeneratedProducerIdentity::Data + } + E::ListConstruct { elements } => { + for value in elements { + add(value); + } + GeneratedProducerIdentity::Data + } + E::MapConstruct { entries } => { + for entry in entries { + add(&entry.key); + add(&entry.value); + } + GeneratedProducerIdentity::Data + } + E::StringInterpolation(parts) => { + for part in parts { + if let crate::StringSegment::Value(value) = part { + add(value); + } + } + GeneratedProducerIdentity::Data + } + E::Int(_) + | E::Float(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::OptionalNone + | E::RuntimeFailure(_) + | E::PropertyCaseContext(_) => GeneratedProducerIdentity::Data, + }; + Self { + kind: std::mem::discriminant(&value.kind), + ty: value.ty, + span: value.span, + identity, + backing, + ordinary_handle_source_snapshot: false, + ordinary_local_snapshot: false, + } + } + + fn apply_inline_extractions(&mut self, records: &[GeneratedInlineExtraction]) { + if matches!(self.identity, GeneratedProducerIdentity::Inline { .. }) + && let Some(record) = records.iter().find(|record| record.original == *self) + { + *self = record.replacement.clone(); + return; + } + for value in &mut self.backing { + value.apply_inline_extractions(records); + } + } + + fn certify_handle_sources(&mut self, types: &TypeInterner) { + self.ordinary_local_snapshot = + self.local_snapshot_backing() && observation_data_type(types, self.ty); + if matches!(self.identity, GeneratedProducerIdentity::Handle { .. }) { + let target = &self.backing[0]; + self.ordinary_handle_source_snapshot = + target.local_snapshot_backing() && observation_data_type(types, target.ty); + } + for value in &mut self.backing { + value.certify_handle_sources(types); + } + } + + fn local_snapshot_backing(&self) -> bool { + match self.identity { + GeneratedProducerIdentity::Local(_) => true, + GeneratedProducerIdentity::InterfaceCoerce(_) => { + self.backing[0].local_snapshot_backing() + } + _ => false, + } + } + + fn restore_local_snapshot(&self, value: &Expression) -> Option { + match (&self.identity, &value.kind) { + (GeneratedProducerIdentity::Local(_), ExpressionKind::Clone(inner)) + if value.ty == self.ty + && value.span == self.span + && inner.ty == self.ty + && inner.span == self.span => + { + Some(inner.as_ref().clone()) + } + ( + GeneratedProducerIdentity::InterfaceCoerce(expected), + ExpressionKind::InterfaceCoerce { + value: inner, + adapters, + }, + ) if adapters == expected && value.ty == self.ty && value.span == self.span => { + let mut restored = value.clone(); + let ExpressionKind::InterfaceCoerce { value, .. } = &mut restored.kind else { + unreachable!() + }; + *value = Box::new(self.backing[0].restore_local_snapshot(inner)?); + Some(restored) + } + _ => None, + } + } + + fn validate_lowered<'a>( + &'a self, + value: &Expression, + validate_handle: &mut impl FnMut(&Expression, GeneratedOriginalShape<'a>) -> Result<(), String>, + ) -> Result<(), String> { + let actual = Self::from_expression(value); + if self.kind != actual.kind + || self.ty != actual.ty + || self.span != actual.span + || self.identity != actual.identity + || self.backing.len() != actual.backing.len() + { + if matches!(value.kind, ExpressionKind::Local(_)) + && value.ty == self.ty + && value.span == self.span + { + return validate_handle(value, GeneratedOriginalShape { shape: self }); + } + return Err( + "call ownership generated operand changes its sealed original producer shape" + .into(), + ); + } + for (expected, value) in self.backing.iter().zip(generated_backing_values(value)) { + expected.validate_lowered(value, validate_handle)?; + } + Ok(()) + } + + fn metadata_types(&self, visit: &mut impl FnMut(TypeId)) { + visit(self.ty); + match &self.identity { + GeneratedProducerIdentity::Call { operands, .. } + | GeneratedProducerIdentity::Indirect { operands, .. } + | GeneratedProducerIdentity::Intrinsic { operands, .. } => { + for &(ty, _) in operands { + visit(ty); + } + } + _ => {} + } + match &self.identity { + GeneratedProducerIdentity::Intrinsic { type_arguments, .. } => { + for &ty in type_arguments { + visit(ty); + } + } + GeneratedProducerIdentity::Struct { owner, .. } + | GeneratedProducerIdentity::Bitfield { owner, .. } + | GeneratedProducerIdentity::Enum { owner, .. } + | GeneratedProducerIdentity::Machine { owner, .. } + | GeneratedProducerIdentity::Transition { owner, .. } + | GeneratedProducerIdentity::Field { owner, .. } => visit(*owner), + GeneratedProducerIdentity::InterfaceCoerce(adapters) => { + for adapter in adapters { + visit(adapter.source); + visit(adapter.target); + } + } + GeneratedProducerIdentity::Handle { + kind: + crate::HandleKind::Refinement { + refined_type, + predicates, + }, + .. + } => { + visit(*refined_type); + for predicate in predicates { + visit(predicate.refined_type); + visit(predicate.base_type); + visit(predicate.input_type); + } + } + _ => {} + } + for value in &self.backing { + value.metadata_types(visit); + } + } + + fn metadata_locals(&self, visit: &mut impl FnMut(LocalId)) { + match &self.identity { + GeneratedProducerIdentity::Local(local) => visit(*local), + GeneratedProducerIdentity::Closure { captures, .. } => { + for &local in captures { + visit(local); + } + } + GeneratedProducerIdentity::Handle { + error_local: Some(local), + .. + } => visit(*local), + GeneratedProducerIdentity::Inline { + params, + view_params, + .. + } => { + for &local in params.iter().chain(view_params) { + visit(local); + } + } + _ => {} + } + for value in &self.backing { + value.metadata_locals(visit); + } + } + + fn remap_locals( + &mut self, + map: &mut impl FnMut(LocalId) -> Result, + ) -> Result<(), E> { + match &mut self.identity { + GeneratedProducerIdentity::Local(local) => *local = map(*local)?, + GeneratedProducerIdentity::Closure { captures, .. } => { + for local in captures { + *local = map(*local)?; + } + } + GeneratedProducerIdentity::Handle { + error_local: Some(local), + .. + } => *local = map(*local)?, + GeneratedProducerIdentity::Inline { + params, + view_params, + .. + } => { + for local in params.iter_mut().chain(view_params) { + *local = map(*local)?; + } + } + _ => {} + } + for value in &mut self.backing { + value.remap_locals(map)?; + } + Ok(()) + } + + fn metadata_functions(&self, visit: &mut impl FnMut(FunctionId)) { + match &self.identity { + GeneratedProducerIdentity::Function(function) + | GeneratedProducerIdentity::FunctionAdapter(function) + | GeneratedProducerIdentity::Closure { function, .. } + | GeneratedProducerIdentity::Call { function, .. } + | GeneratedProducerIdentity::ActorMessage { + handler: function, .. + } => visit(*function), + GeneratedProducerIdentity::InterfaceCoerce(adapters) => { + for adapter in adapters { + visit(adapter.function); + } + } + GeneratedProducerIdentity::ActorSpawn { + constructor: Some(function), + .. + } => visit(*function), + GeneratedProducerIdentity::Handle { + kind: crate::HandleKind::Refinement { predicates, .. }, + .. + } => { + for predicate in predicates { + visit(predicate.function); + } + } + _ => {} + } + for value in &self.backing { + value.metadata_functions(visit); + } + } + + fn remap_functions( + &mut self, + map: &mut impl FnMut(FunctionId) -> Result, + ) -> Result<(), E> { + match &mut self.identity { + GeneratedProducerIdentity::Function(function) + | GeneratedProducerIdentity::FunctionAdapter(function) + | GeneratedProducerIdentity::Closure { function, .. } + | GeneratedProducerIdentity::Call { function, .. } + | GeneratedProducerIdentity::ActorMessage { + handler: function, .. + } => *function = map(*function)?, + GeneratedProducerIdentity::InterfaceCoerce(adapters) => { + for adapter in adapters { + adapter.function = map(adapter.function)?; + } + } + GeneratedProducerIdentity::ActorSpawn { + constructor: Some(function), + .. + } => *function = map(*function)?, + GeneratedProducerIdentity::Handle { + kind: crate::HandleKind::Refinement { predicates, .. }, + .. + } => { + for predicate in predicates { + predicate.function = map(predicate.function)?; + } + } + _ => {} + } + for value in &mut self.backing { + value.remap_functions(map)?; + } + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GeneratedArgumentWitness { + actual_type: TypeId, + parameter_type: TypeId, + callee_access: CheckedCalleeAccess, + acquisition: GeneratedAcquisition, + origin: CallerViewSource, + source_span: Span, + written_view: bool, + owned_producer: bool, + producer_shape: GeneratedProducerShape, + ordinary_snapshot: Option, +} + +impl GeneratedArgumentWitness { + pub fn actual_type(&self) -> TypeId { + self.actual_type + } + pub fn parameter_type(&self) -> TypeId { + self.parameter_type + } + pub fn callee_access(&self) -> CheckedCalleeAccess { + self.callee_access + } + pub fn acquisition(&self) -> &GeneratedAcquisition { + &self.acquisition + } + pub fn origin(&self) -> CallerViewSource { + self.origin + } + pub fn source_span(&self) -> Span { + self.source_span + } + pub fn written_view(&self) -> bool { + self.written_view + } + pub fn owned_producer(&self) -> bool { + self.owned_producer + } + /// Compare an authenticated restored operand with the immutable original + /// operation/backing shape; matching a type or producer flag is insufficient. + pub fn validate_producer_shape(&self, value: &Expression) -> Result<(), String> { + self.producer_shape.validate_lowered(value, &mut |_, _| { + Err("call ownership original generated operand cannot be a lowered storage slot".into()) + }) + } + /// A storage substitution needs an independently authenticated unique + /// owning Let; original Handles additionally require their exact CFG proof. + pub fn validate_lowered_producer_shape<'a>( + &'a self, + value: &Expression, + mut validate_handle: impl FnMut(&Expression, GeneratedOriginalShape<'a>) -> Result<(), String>, + ) -> Result<(), String> { + self.producer_shape + .validate_lowered(value, &mut validate_handle) + } + pub fn ordinary_snapshot_proof(&self) -> Option<&GeneratedSnapshotProof> { + self.ordinary_snapshot.as_ref() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct GeneratedCallCertificate { + operation: GeneratedOperation, + result_type: TypeId, + evaluation_order: Vec, + arguments: Vec, +} + +fn generated_owned_producer(mut value: &Expression) -> bool { + loop { + match &value.kind { + ExpressionKind::View(inner) + | ExpressionKind::Coarsen(inner) + | ExpressionKind::Declassify(inner) + | ExpressionKind::RefinementValidated(inner) => value = inner, + ExpressionKind::Call { .. } + | ExpressionKind::IndirectCall { .. } + | ExpressionKind::Intrinsic { .. } + | ExpressionKind::StructConstruct { .. } + | ExpressionKind::BitfieldConstruct { .. } + | ExpressionKind::MachineConstruct { .. } + | ExpressionKind::MachineTransition { .. } + | ExpressionKind::Clone(_) + | ExpressionKind::Run(_) + | ExpressionKind::Join(_) => return true, + _ => return false, + } + } +} + +fn generated_witness( + value: &Expression, + parameter_type: TypeId, + callee_access: CheckedCalleeAccess, + acquisition: GeneratedAcquisition, + types: &TypeInterner, +) -> GeneratedArgumentWitness { + let mut producer_shape = GeneratedProducerShape::from_expression(value); + producer_shape.certify_handle_sources(types); + GeneratedArgumentWitness { + actual_type: value.ty, + parameter_type, + callee_access, + acquisition, + origin: immediate_source(value), + source_span: value.span, + written_view: original_view(value), + owned_producer: generated_owned_producer(value), + producer_shape, + ordinary_snapshot: observation_data_type(types, value.ty).then_some( + GeneratedSnapshotProof { + actual_type: value.ty, + }, + ), + } +} + +fn generated_tuple_matches(argument: &GeneratedArgumentOwnership) -> bool { + let witness = &argument.witness; + argument.actual_type == witness.actual_type + && argument.parameter_type == witness.parameter_type + && argument.callee_access == witness.callee_access + && argument.acquisition == witness.acquisition +} + +fn generated_operation_function(operation: &GeneratedOperation) -> Option { + match operation { + GeneratedOperation::Display { method } | GeneratedOperation::Equality { method } => { + Some(*method) + } + GeneratedOperation::InterfaceDispatch { function, .. } + | GeneratedOperation::NativeSuite { function } + | GeneratedOperation::RefinementPredicate { function } => Some(*function), + _ => None, + } +} + +fn remap_generated_operation( + operation: &mut GeneratedOperation, + map: &mut impl FnMut(FunctionId) -> Result, +) -> Result<(), E> { + match operation { + GeneratedOperation::Display { method } | GeneratedOperation::Equality { method } => { + *method = map(*method)? + } + GeneratedOperation::InterfaceDispatch { function, .. } + | GeneratedOperation::NativeSuite { function } + | GeneratedOperation::RefinementPredicate { function } => *function = map(*function)?, + _ => {} + } + Ok(()) +} + +fn remap_generated_operation_locals( + operation: &mut GeneratedOperation, + map: &mut impl FnMut(LocalId) -> Result, +) -> Result<(), E> { + if let GeneratedOperation::FunctionAdapter { callee, .. } = operation { + *callee = map(*callee)?; + } + Ok(()) +} + +fn validate_generated_stage( + argument: &GeneratedArgumentOwnership, + argument_type: TypeId, + local: &impl Fn(LocalId) -> Option, + initial_hir: bool, +) -> Result<(), String> { + if !generated_tuple_matches(argument) { + return Err("generated ownership differs from its sealed original acquisition".into()); + } + if argument.staging != argument.staging_certificate { + return Err("generated stage differs from its sealed storage association".into()); + } + if initial_hir && argument.staging != (GeneratedArgumentStaging::Existing { loan: None }) { + return Err("initial HIR generated ownership cannot carry MIR staging authority".into()); + } + let (owner, loan) = match argument.staging { + GeneratedArgumentStaging::Existing { loan: None } => return Ok(()), + GeneratedArgumentStaging::Existing { loan: Some(loan) } => (None, loan), + GeneratedArgumentStaging::OwnedProducer { owner, loan } => { + if !argument.witness.owned_producer { + return Err("generated stage has no sealed owning producer proof".into()); + } + (Some(owner), loan) + } + GeneratedArgumentStaging::OrdinarySnapshot { owner, loan } => { + if argument + .witness + .ordinary_snapshot + .as_ref() + .map(|proof| proof.actual_type) + != Some(argument.actual_type) + { + return Err("generated stage has no sealed ordinary snapshot proof".into()); + } + (Some(owner), loan) + } + }; + if argument.callee_access != CheckedCalleeAccess::View + || !matches!( + argument.acquisition, + GeneratedAcquisition::Borrow { .. } | GeneratedAcquisition::Copy + ) + { + return Err("generated loan stage has no original physically borrowed acquisition".into()); + } + if require_local(loan, local)?.ty != argument_type || argument_type != argument.actual_type { + return Err("generated loan stage changed its sealed operand type".into()); + } + if let Some(owner) = owner { + let owner = require_local(owner, local)?; + if owner.ty != argument_type + || owner.view_source.is_some() + || owner.is_view_parameter + || owner.view_iteration.is_some() + { + return Err("generated backing stage cannot acquire a borrowed slot".into()); + } + } + Ok(()) +} + +fn generated_stage_local_ids(staging: GeneratedArgumentStaging, visit: &mut impl FnMut(LocalId)) { + match staging { + GeneratedArgumentStaging::Existing { loan: Some(loan) } => visit(loan), + GeneratedArgumentStaging::OwnedProducer { owner, loan } + | GeneratedArgumentStaging::OrdinarySnapshot { owner, loan } => { + visit(owner); + visit(loan); + } + GeneratedArgumentStaging::Existing { loan: None } => {} + } +} + +fn remap_generated_staging( + staging: &mut GeneratedArgumentStaging, + map: &mut impl FnMut(LocalId) -> Result, +) -> Result<(), E> { + match staging { + GeneratedArgumentStaging::Existing { loan: Some(loan) } => *loan = map(*loan)?, + GeneratedArgumentStaging::OwnedProducer { owner, loan } + | GeneratedArgumentStaging::OrdinarySnapshot { owner, loan } => { + *owner = map(*owner)?; + *loan = map(*loan)?; + } + GeneratedArgumentStaging::Existing { loan: None } => {} + } + Ok(()) +} + +/// Each operation is validated against its own existing compiler contract. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GeneratedOperation { + Display { + method: FunctionId, + }, + Equality { + method: FunctionId, + }, + InterfaceDispatch { + function: FunctionId, + interface_type: TypeId, + method_index: usize, + }, + FunctionAdapter { + callee: LocalId, + source_type: TypeId, + target_type: TypeId, + }, + JsonBridge { + intrinsic: IntrinsicId, + }, + NativeSuite { + function: FunctionId, + }, + RefinementPredicate { + function: FunctionId, + }, + FailureFormatting, + PropertyContext, + ReflectionMetadata { + intrinsic: IntrinsicId, + }, + EvaluatedValue { + intrinsic: IntrinsicId, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GeneratedCallOwnership { + pub operation: GeneratedOperation, + pub arguments: Vec, + certificate: GeneratedCallCertificate, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CallOwnership { + Source(SourceCallOwnership), + Generated(GeneratedCallOwnership), +} + +impl CallOwnership { + pub fn metadata_types(&self, mut visit: impl FnMut(TypeId)) { + match self { + Self::Source(source) => { + visit(source.certificate.result_type); + match &source.shape { + CheckedInvocationShape::Function { signature_type } => visit(*signature_type), + CheckedInvocationShape::Intrinsic { + result_type, + operands, + .. + } => { + visit(*result_type); + for role in operands { + visit(role.ty()); + } + } + } + match &source.target { + CallTarget::Declaration { + identity, + signature_type, + } => { + visit(*signature_type); + for &ty in &identity.type_arguments { + visit(ty); + } + for binding in &identity.scoped_type_bindings { + visit(binding.ty); + } + } + CallTarget::Interface { interface_type, .. } => visit(*interface_type), + CallTarget::Indirect { signature_type } => visit(*signature_type), + _ => {} + } + for argument in &source.arguments { + visit(argument.actual_type); + visit(argument.witness.occurrence_type); + if let Some(result) = argument.witness.handled_result { + visit(result.ty); + } + if let Some(root) = argument.witness.projection_root { + visit(root.occurrence_type); + visit(root.storage_type); + } + if let Some(proof) = argument.retained_snapshot { + visit(proof.actual_type); + visit(proof.occurrence_type); + } + visit(argument.parameter_type); + if let CallerOrigin::Binding(fact) = &argument.origin { + visit(fact.ty); + } + if let Some(proof) = argument.observation_proof() { + visit(proof.actual_type); + for &(_, ty) in &proof.captures { + visit(ty); + } + } + } + for argument in &source.generated_operands { + visit(argument.actual_type); + visit(argument.parameter_type); + argument.witness.producer_shape.metadata_types(&mut visit); + } + } + Self::Generated(generated) => { + visit(generated.certificate.result_type); + match generated.operation { + GeneratedOperation::InterfaceDispatch { interface_type, .. } => { + visit(interface_type) + } + GeneratedOperation::FunctionAdapter { + source_type, + target_type, + .. + } => { + visit(source_type); + visit(target_type); + } + _ => {} + } + for argument in &generated.arguments { + visit(argument.actual_type); + visit(argument.parameter_type); + argument.witness.producer_shape.metadata_types(&mut visit); + } + } + } + } + pub fn source_parameter_effect(&self, index: usize) -> Option { + match self { + Self::Source(source) => source.arguments.get(index).map(|arg| arg.effect), + Self::Generated(_) => None, + } + } + pub fn source_parameter_access(&self, index: usize) -> Option { + match self { + Self::Source(source) => source.arguments.get(index).map(|arg| arg.callee_access), + Self::Generated(_) => None, + } + } + pub fn source_parameter_syntax(&self, index: usize) -> Option { + match self { + Self::Source(source) => source.arguments.get(index).map(|arg| arg.syntax), + Self::Generated(_) => None, + } + } + pub fn parameter_count(&self) -> usize { + match self { + Self::Source(source) => source.arguments.len() + source.generated_operands.len(), + Self::Generated(generated) => generated.arguments.len(), + } + } + + pub fn parameter_physical_access(&self, index: usize) -> Option { + match self { + Self::Source(source) => source + .arguments + .get(index) + .map(|arg| arg.physical_access) + .or_else(|| { + index.checked_sub(source.arguments.len()).and_then(|tail| { + source + .generated_operands + .get(tail) + .map(|arg| arg.callee_access) + }) + }), + Self::Generated(generated) => { + generated.arguments.get(index).map(|arg| arg.callee_access) + } + } + } + + pub fn stage_parameter( + &mut self, + index: usize, + owner: Option, + loan: LocalId, + ) -> Result<(), String> { + let Self::Source(source) = self else { + return Err("generated operand cannot acquire source staging authority".into()); + }; + let argument = source + .arguments + .get_mut(index) + .ok_or_else(|| "source staging parameter is out of bounds".to_string())?; + if argument.staging != ArgumentStaging::Original + || argument.physical_access != CheckedCalleeAccess::View + { + return Err("source staging requires an original physically borrowed operand".into()); + } + argument.staging = match owner { + Some(owner) + if argument.effect == CheckedCallerEffect::RelinquishOwned + || (argument.effect == CheckedCallerEffect::TransferOwned + && source.bridge != CallBridge::Direct) => + { + ArgumentStaging::Relinquished { owner, loan } + } + None if matches!( + argument.effect, + CheckedCallerEffect::RetainBorrow | CheckedCallerEffect::ObserveData + ) => + { + ArgumentStaging::Borrowed { loan } + } + _ => { + return Err( + "source staging does not match the checked caller effect and bridge".into(), + ); + } + }; + Ok(()) + } + + pub fn stage_retained_snapshot( + &mut self, + index: usize, + owner: LocalId, + loan: LocalId, + ) -> Result<(), String> { + let Self::Source(source) = self else { + return Err( + "generated operand cannot acquire retained source snapshot authority".into(), + ); + }; + let argument = source + .arguments + .get_mut(index) + .ok_or("retained snapshot parameter is out of bounds")?; + if argument.staging != ArgumentStaging::Original + || argument.effect != CheckedCallerEffect::RetainBorrow + || argument.physical_access != CheckedCalleeAccess::View + || argument.retained_snapshot.is_none() + || owner == loan + { + return Err("retained snapshot requires an original checked ordinary-data view".into()); + } + argument.staging = ArgumentStaging::RetainedSnapshot { owner, loan }; + Ok(()) + } + + pub fn stage_observation(&mut self, index: usize, owner: LocalId) -> Result<(), String> { + let Self::Source(source) = self else { + return Err("generated operand cannot acquire source observation authority".into()); + }; + let argument = source + .arguments + .get_mut(index) + .ok_or_else(|| "source observation parameter is out of bounds".to_string())?; + if argument.staging != ArgumentStaging::Original + || argument.effect != CheckedCallerEffect::ObserveData + || argument.physical_access != CheckedCalleeAccess::Owned + { + return Err( + "observation staging requires original ObserveData at a physical owned boundary" + .into(), + ); + } + argument.staging = ArgumentStaging::Observed { owner }; + Ok(()) + } + + pub fn stage_acquisition(&mut self, index: usize, local: LocalId) -> Result<(), String> { + let Self::Source(source) = self else { + return Err("generated operand cannot acquire source staging authority".into()); + }; + let argument = source + .arguments + .get_mut(index) + .ok_or("source acquisition parameter is out of bounds")?; + if argument.staging != ArgumentStaging::Original { + return Err("source acquisition is already staged".into()); + } + argument.staging = match (argument.effect, argument.physical_access) { + (CheckedCallerEffect::TransferOwned, CheckedCalleeAccess::Owned) => { + ArgumentStaging::Transferred { owner: local } + } + (CheckedCallerEffect::Copy, _) => ArgumentStaging::Copied { value: local }, + _ => { + return Err( + "source acquisition does not match checked copy or physical ownership".into(), + ); + } + }; + Ok(()) + } + + pub fn stage_generated_parameter( + &mut self, + index: usize, + staging: GeneratedArgumentStaging, + ) -> Result<(), String> { + let Self::Generated(generated) = self else { + return Err("source packet cannot acquire generated staging authority".into()); + }; + if generated.operation != generated.certificate.operation { + return Err("generated stage operation differs from its certificate".into()); + } + if generated.arguments.len() != generated.certificate.arguments.len() { + return Err("generated stage argument count differs from its certificate".into()); + } + let witness = generated + .certificate + .arguments + .get(index) + .ok_or("generated stage certificate parameter is out of bounds")?; + let argument = generated + .arguments + .get_mut(index) + .ok_or("generated stage parameter is out of bounds")?; + if !generated_tuple_matches(argument) + || argument.witness != *witness + || argument.staging != argument.staging_certificate + || argument.staging != (GeneratedArgumentStaging::Existing { loan: None }) + { + return Err( + "generated stage cannot rewrite its original acquisition certificate".into(), + ); + } + if argument.callee_access != CheckedCalleeAccess::View + || !matches!( + argument.acquisition, + GeneratedAcquisition::Borrow { .. } | GeneratedAcquisition::Copy + ) + { + return Err("generated stage requires a physically borrowed original operand".into()); + } + match staging { + GeneratedArgumentStaging::Existing { loan: Some(_) } => {} + GeneratedArgumentStaging::OwnedProducer { .. } if argument.witness.owned_producer => {} + GeneratedArgumentStaging::OrdinarySnapshot { .. } + if argument.witness.ordinary_snapshot.is_some() => {} + _ => return Err("generated stage has no sealed original backing permission".into()), + } + argument.staging = staging; + argument.staging_certificate = staging; + Ok(()) + } + + /// Structural metadata only: this must not feed execution liveness or loans. + pub fn metadata_local_ids(&self, mut visit: impl FnMut(LocalId)) { + match self { + Self::Source(source) => { + for argument in &source.arguments { + if let Some(root) = argument.witness.projection_root { + visit(root.local); + } + if let Some(proof) = argument.observation_proof() { + for &(capture, _) in &proof.captures { + visit(capture); + } + } + match argument.staging { + ArgumentStaging::Original => {} + ArgumentStaging::Transferred { owner } => visit(owner), + ArgumentStaging::Copied { value } => visit(value), + ArgumentStaging::Borrowed { loan } => visit(loan), + ArgumentStaging::Relinquished { owner, loan } + | ArgumentStaging::RetainedSnapshot { owner, loan } => { + visit(owner); + visit(loan); + } + ArgumentStaging::Observed { owner } => visit(owner), + } + match argument.origin { + CallerOrigin::Binding(fact) => { + visit(fact.local); + if let CallerBindingMode::View { + source: CallerViewSource::Local(id), + } = fact.mode + { + visit(id); + } + } + CallerOrigin::BorrowedProjection { + source: CallerViewSource::Local(id), + } + | CallerOrigin::OwnedFieldCopy { + parent: CallerViewSource::Local(id), + } => visit(id), + _ => {} + } + } + for operand in &source.generated_operands { + operand.metadata_local_ids(&mut visit); + } + } + Self::Generated(generated) => { + if let GeneratedOperation::FunctionAdapter { callee, .. } = generated.operation { + visit(callee); + } + for operand in &generated.arguments { + operand.metadata_local_ids(&mut visit); + } + } + } + } + + pub fn remap_metadata_locals( + &mut self, + mut map: impl FnMut(LocalId) -> Result, + ) -> Result<(), E> { + match self { + Self::Source(source) => { + for argument in &mut source.arguments { + if let Some(root) = &mut argument.witness.projection_root { + root.local = map(root.local)?; + } + if let Some(proof) = &mut argument.witness.observation { + for (capture, _) in &mut proof.captures { + *capture = map(*capture)?; + } + } + remap_origin(&mut argument.witness.origin, &mut map)?; + match &mut argument.staging { + ArgumentStaging::Original => {} + ArgumentStaging::Transferred { owner } => *owner = map(*owner)?, + ArgumentStaging::Copied { value } => *value = map(*value)?, + ArgumentStaging::Borrowed { loan } => *loan = map(*loan)?, + ArgumentStaging::Relinquished { owner, loan } + | ArgumentStaging::RetainedSnapshot { owner, loan } => { + *owner = map(*owner)?; + *loan = map(*loan)?; + } + ArgumentStaging::Observed { owner } => *owner = map(*owner)?, + } + match &mut argument.origin { + CallerOrigin::Binding(fact) => { + fact.local = map(fact.local)?; + if let CallerBindingMode::View { source } = &mut fact.mode { + remap_source(source, &mut map)?; + } + } + CallerOrigin::BorrowedProjection { source } + | CallerOrigin::OwnedFieldCopy { parent: source } => { + remap_source(source, &mut map)? + } + CallerOrigin::OwnedExpression => {} + } + } + for operand in &mut source.generated_operands { + operand.remap_metadata_locals(&mut map)?; + } + for operand in &mut source.certificate.generated_operands { + operand.remap_metadata_locals(&mut map)?; + } + } + Self::Generated(generated) => { + for operand in &mut generated.arguments { + operand.remap_metadata_locals(&mut map)?; + } + remap_generated_operation_locals(&mut generated.operation, &mut map)?; + remap_generated_operation_locals(&mut generated.certificate.operation, &mut map)?; + for witness in &mut generated.certificate.arguments { + if let GeneratedAcquisition::Borrow { source } = &mut witness.acquisition { + remap_source(source, &mut map)?; + } + remap_source(&mut witness.origin, &mut map)?; + witness.producer_shape.remap_locals(&mut map)?; + } + } + } + Ok(()) + } + + /// Structural identities only; declaration provenance is not an executable root. + pub fn metadata_function_ids(&self, mut visit: impl FnMut(FunctionId)) { + match self { + Self::Source(source) => { + match source.target { + CallTarget::Function(function) | CallTarget::Interface { function, .. } => { + visit(function) + } + _ => {} + } + if let CallBridge::JsonSource { function, .. } = source.bridge { + visit(function); + } + for argument in &source.generated_operands { + argument + .witness + .producer_shape + .metadata_functions(&mut visit); + } + } + Self::Generated(generated) => { + if let Some(function) = generated_operation_function(&generated.operation) { + visit(function); + } + for argument in &generated.arguments { + argument + .witness + .producer_shape + .metadata_functions(&mut visit); + } + } + } + } + + pub fn remap_metadata_functions( + &mut self, + mut map: impl FnMut(FunctionId) -> Result, + ) -> Result<(), E> { + match self { + Self::Source(source) => { + match &mut source.target { + CallTarget::Function(function) | CallTarget::Interface { function, .. } => { + *function = map(*function)? + } + _ => {} + } + if let CallBridge::JsonSource { function, .. } = &mut source.bridge { + *function = map(*function)?; + } + match &mut source.certificate.target { + CallTarget::Function(function) | CallTarget::Interface { function, .. } => { + *function = map(*function)? + } + _ => {} + } + if let CallBridge::JsonSource { function, .. } = &mut source.certificate.bridge { + *function = map(*function)?; + } + for argument in &mut source.generated_operands { + argument.witness.producer_shape.remap_functions(&mut map)?; + } + for argument in &mut source.certificate.generated_operands { + argument.witness.producer_shape.remap_functions(&mut map)?; + } + } + Self::Generated(generated) => { + remap_generated_operation(&mut generated.operation, &mut map)?; + remap_generated_operation(&mut generated.certificate.operation, &mut map)?; + for argument in &mut generated.arguments { + argument.witness.producer_shape.remap_functions(&mut map)?; + } + for witness in &mut generated.certificate.arguments { + witness.producer_shape.remap_functions(&mut map)?; + } + } + } + Ok(()) + } + + pub(crate) fn apply_generated_inline_extractions( + &mut self, + records: &[GeneratedInlineExtraction], + ) { + match self { + Self::Source(source) => { + for argument in &mut source.generated_operands { + argument + .witness + .producer_shape + .apply_inline_extractions(records); + } + for argument in &mut source.certificate.generated_operands { + argument + .witness + .producer_shape + .apply_inline_extractions(records); + } + } + Self::Generated(generated) => { + for argument in &mut generated.arguments { + argument + .witness + .producer_shape + .apply_inline_extractions(records); + } + for witness in &mut generated.certificate.arguments { + witness.producer_shape.apply_inline_extractions(records); + } + } + } + } + + pub fn generated( + operation: GeneratedOperation, + args: &[Expression], + parameter_types: &[TypeId], + access: &[CheckedCalleeAccess], + result_type: TypeId, + evaluation_order: &[usize], + types: &TypeInterner, + ) -> Result { + if !valid_type(types, result_type) { + return Err("generated result type is outside its interner".into()); + } + if evaluation_order.len() != args.len() { + return Err("generated evaluation order has the wrong operand count".into()); + } + let mut seen = vec![false; args.len()]; + for &index in evaluation_order { + let slot = seen + .get_mut(index) + .ok_or("generated evaluation order is out of bounds")?; + if *slot { + return Err("generated evaluation order repeats an operand".into()); + } + *slot = true; + } + if args.len() != parameter_types.len() || args.len() != access.len() { + return Err("generated ownership parameter count is invalid".into()); + } + let mut arguments = Vec::with_capacity(args.len()); + for ((argument, ¶meter_type), &callee_access) in + args.iter().zip(parameter_types).zip(access) + { + if !valid_type(types, argument.ty) || !valid_type(types, parameter_type) { + return Err("generated ownership type is outside its interner".into()); + } + let acquisition = + if jett_typecheck::ownership::is_implicitly_copyable(types, argument.ty) { + GeneratedAcquisition::Copy + } else if callee_access == CheckedCalleeAccess::View { + GeneratedAcquisition::Borrow { + source: immediate_source(argument), + } + } else { + GeneratedAcquisition::OwnedExpression + }; + let witness = generated_witness( + argument, + parameter_type, + callee_access, + acquisition.clone(), + types, + ); + arguments.push(GeneratedArgumentOwnership { + actual_type: argument.ty, + parameter_type, + callee_access, + acquisition, + staging: GeneratedArgumentStaging::Existing { loan: None }, + staging_certificate: GeneratedArgumentStaging::Existing { loan: None }, + witness, + }); + } + let certificate = GeneratedCallCertificate { + operation: operation.clone(), + result_type, + evaluation_order: evaluation_order.to_vec(), + arguments: arguments + .iter() + .map(|argument| argument.witness.clone()) + .collect(), + }; + Ok(Self::Generated(GeneratedCallOwnership { + operation, + arguments, + certificate, + })) + } +} + +fn valid_type(types: &TypeInterner, ty: TypeId) -> bool { + (ty.index() as usize) < types.len() +} + +pub(crate) fn immediate_source(mut value: &Expression) -> CallerViewSource { + loop { + match &value.kind { + ExpressionKind::Local(local) => return CallerViewSource::Local(*local), + ExpressionKind::Field { base, .. } + | ExpressionKind::View(base) + | ExpressionKind::Coarsen(base) + | ExpressionKind::Declassify(base) + | ExpressionKind::RefinementValidated(base) => value = base, + ExpressionKind::InterfaceCoerce { + value: inner, + adapters, + } if adapters.is_empty() => value = inner, + _ => return CallerViewSource::Other, + } + } +} + +fn remap_source( + source: &mut CallerViewSource, + map: &mut impl FnMut(LocalId) -> Result, +) -> Result<(), E> { + if let CallerViewSource::Local(id) = source { + *id = map(*id)?; + } + Ok(()) +} + +fn remap_origin( + origin: &mut CallerOrigin, + map: &mut impl FnMut(LocalId) -> Result, +) -> Result<(), E> { + match origin { + CallerOrigin::Binding(fact) => { + fact.local = map(fact.local)?; + if let CallerBindingMode::View { source } = &mut fact.mode { + remap_source(source, map)?; + } + } + CallerOrigin::BorrowedProjection { source } + | CallerOrigin::OwnedFieldCopy { parent: source } => remap_source(source, map)?, + CallerOrigin::OwnedExpression => {} + } + Ok(()) +} + +impl GeneratedArgumentOwnership { + pub fn original_witness(&self) -> &GeneratedArgumentWitness { + &self.witness + } + fn metadata_local_ids(&self, visit: &mut impl FnMut(LocalId)) { + if let GeneratedAcquisition::Borrow { + source: CallerViewSource::Local(id), + } = self.acquisition + { + visit(id); + } + if let CallerViewSource::Local(id) = self.witness.origin { + visit(id); + } + self.witness.producer_shape.metadata_locals(visit); + generated_stage_local_ids(self.staging, visit); + generated_stage_local_ids(self.staging_certificate, visit); + } + fn remap_metadata_locals( + &mut self, + map: &mut impl FnMut(LocalId) -> Result, + ) -> Result<(), E> { + if let GeneratedAcquisition::Borrow { source } = &mut self.acquisition { + remap_source(source, map)?; + } + if let GeneratedAcquisition::Borrow { source } = &mut self.witness.acquisition { + remap_source(source, map)?; + } + remap_source(&mut self.witness.origin, map)?; + self.witness.producer_shape.remap_locals(map)?; + remap_generated_staging(&mut self.staging, map)?; + remap_generated_staging(&mut self.staging_certificate, map)?; + Ok(()) + } +} + +/// Primitive local facts usable by HIR and MIR without sharing their bodies. +#[derive(Debug, Clone, Copy)] +pub struct OwnershipLocalInfo { + pub ty: TypeId, + pub mutable: bool, + pub span: Span, + pub view_source: Option, + pub is_view_parameter: bool, + /// Present only while an exact original/prepared viewed loop proves this binding. + pub view_iteration: Option, +} + +/// Validate structural authority before considering the current operand tree. +/// The caller separately proves its typed conversion/staging/target relation. +pub fn validate_operand_ownership( + ownership: &CallOwnership, + argument_types: &[TypeId], + evaluation_order: &[usize], + types: &TypeInterner, + local: impl Fn(LocalId) -> Option, + initial_hir: bool, +) -> Result<(), String> { + if ownership.parameter_count() != argument_types.len() + || evaluation_order.len() != argument_types.len() + { + return Err("call ownership operand count is invalid".into()); + } + let mut seen = vec![false; argument_types.len()]; + for ¶meter in evaluation_order { + let Some(slot) = seen.get_mut(parameter) else { + return Err("call ownership evaluation permutation is out of bounds".into()); + }; + if *slot { + return Err("call ownership evaluation permutation repeats an operand".into()); + } + *slot = true; + } + for &ty in argument_types { + if !valid_type(types, ty) { + return Err("call ownership operand type is outside its interner".into()); + } + } + let mut valid_metadata = true; + ownership.metadata_types(|ty| valid_metadata &= valid_type(types, ty)); + if !valid_metadata { + return Err("call ownership metadata type is outside its interner".into()); + } + match ownership { + CallOwnership::Source(source) => { + if source.target != source.certificate.target + || source.shape != source.certificate.shape + || source.context != source.certificate.context + || source.bridge != source.certificate.bridge + || source.generated_operands != source.certificate.generated_operands + { + return Err( + "call ownership invocation differs from its checked source certificate".into(), + ); + } + let source_count = source.arguments.len(); + let mut source_seen = vec![false; source_count]; + match &source.shape { + CheckedInvocationShape::Function { signature_type } => { + if !valid_type(types, *signature_type) { + return Err("call ownership signature type is outside its interner".into()); + } + let Type::Function { + params, + view_params, + .. + } = types.resolve(*signature_type) + else { + return Err("call ownership signature is not a function".into()); + }; + if params.len() != source_count || view_params.len() != source_count { + return Err("call ownership signature arity is invalid".into()); + } + for (index, argument) in source.arguments.iter().enumerate() { + let access = if view_params[index] { + CheckedCalleeAccess::View + } else { + CheckedCalleeAccess::Owned + }; + if argument.parameter_type != params[index] + || argument.callee_access != access + { + return Err( + "call ownership signature parameter type or access is invalid" + .into(), + ); + } + } + } + CheckedInvocationShape::Intrinsic { + intrinsic, + result_type, + operands, + } => { + if !valid_type(types, *result_type) || operands.len() != source_count { + return Err("call ownership intrinsic source shape is invalid".into()); + } + for (index, (argument, role)) in + source.arguments.iter().zip(operands).enumerate() + { + if !valid_type(types, role.ty()) || role.ty() != argument.parameter_type { + return Err("call ownership intrinsic operand type is invalid".into()); + } + let permitted = jett_typecheck::intrinsic_operand_access( + *intrinsic, + index, + source_count, + ) + .ok_or_else(|| { + "call ownership intrinsic source operand has no closed role".to_string() + })?; + let print = matches!( + role, + jett_typecheck::CheckedIntrinsicOperandRole::PrintArgument { .. } + ); + if print { + if !matches!(intrinsic, IntrinsicId::Print | IntrinsicId::Println) { + return Err( + "call ownership print role belongs to another intrinsic".into(), + ); + } + let expected = if argument.syntax == CheckedCallerSyntax::WrittenView { + CheckedCalleeAccess::View + } else { + CheckedCalleeAccess::Owned + }; + if argument.callee_access != expected { + return Err("call ownership print access is invalid".into()); + } + } else if permitted != argument.callee_access { + return Err( + "call ownership intrinsic access differs from its closed role" + .into(), + ); + } + if !print { + let copyable = + jett_typecheck::ownership::is_implicitly_copyable(types, role.ty()); + let valid = match role { + jett_typecheck::CheckedIntrinsicOperandRole::Copy { .. } => { + copyable + } + jett_typecheck::CheckedIntrinsicOperandRole::Owned { .. } => { + !copyable && permitted == CheckedCalleeAccess::Owned + } + jett_typecheck::CheckedIntrinsicOperandRole::View { .. } => { + !copyable && permitted == CheckedCalleeAccess::View + } + jett_typecheck::CheckedIntrinsicOperandRole::PrintArgument { + .. + } => false, + }; + if !valid { + return Err("call ownership intrinsic role does not match its checked type and access".into()); + } + } + } + } + } + for (parameter_index, argument) in source.arguments.iter().enumerate() { + let witness = &argument.witness; + if argument.source_span != witness.source_span + || argument.source_index != witness.source_index + || argument.parameter_index != witness.parameter_index + || argument.actual_type != witness.actual_type + || argument.parameter_type != witness.parameter_type + || argument.syntax != witness.syntax + || argument.origin != witness.origin + || argument.callee_access != witness.callee_access + || argument.effect != witness.effect + || source.context != witness.context + { + return Err("call ownership disagrees with its original source witness".into()); + } + if argument.parameter_index != parameter_index + || argument.source_index >= source_count + || source_seen[argument.source_index] + { + return Err("call ownership source/formal permutation is invalid".into()); + } + source_seen[argument.source_index] = true; + if evaluation_order.get(argument.source_index) != Some(¶meter_index) { + return Err("call ownership lexical evaluation order disagrees with its source permutation".into()); + } + if !valid_type(types, argument.actual_type) + || !valid_type(types, witness.occurrence_type) + || witness + .handled_result + .is_some_and(|result| !valid_type(types, result.ty)) + || !valid_type(types, argument.parameter_type) + { + return Err("call ownership source type is outside its interner".into()); + } + if witness.occurrence_type != argument.actual_type + && !(has_outer_secret(types, argument.parameter_type) + && secret_taint_matches( + types, + witness.occurrence_type, + argument.actual_type, + )) + { + return Err("call ownership sealed occurrence has no exact expected secret qualification".into()); + } + validate_origin(&argument.origin, &local, types)?; + validate_effect(argument, source.context, types)?; + if argument.effect == CheckedCallerEffect::ObserveData { + let proof = argument + .observation_proof() + .ok_or("observing call has no checked data acquisition proof")?; + if proof.actual_type != argument.actual_type + || !(observation_data_type(types, proof.actual_type) + || (proof.descriptor_checked + && matches!( + types.resolve(proof.actual_type), + Type::Function { .. } + ))) + { + return Err( + "observing call acquisition contains unsupported authority".into() + ); + } + for &(capture, ty) in &proof.captures { + let value = require_local(capture, &local)?; + if value.ty != ty + || value.view_source.is_some() + || value.is_view_parameter + || value.view_iteration.is_some() + || !observation_data_type(types, ty) + { + return Err( + "observing descriptor capture has no owning ordinary-data proof" + .into(), + ); + } + } + } + if let Some(proof) = argument.retained_snapshot { + if argument.effect != CheckedCallerEffect::RetainBorrow + || !retained_snapshot_origin(&argument.origin) + || proof.actual_type != argument.actual_type + || proof.occurrence_type != witness.occurrence_type + || !observation_data_type(types, proof.actual_type) + || !observation_data_type(types, proof.occurrence_type) + { + return Err( + "retained snapshot lost its checked ordinary-data endpoint proof" + .into(), + ); + } + } + if let Some(root) = witness.projection_root { + let stored = require_local(root.local, &local)?; + if stored.ty != root.storage_type + || !flow_projection_root_matches( + types, + root.occurrence_type, + root.storage_type, + ) + || !matches!(argument.origin, + CallerOrigin::BorrowedProjection { source: CallerViewSource::Local(id) } + | CallerOrigin::OwnedFieldCopy { parent: CallerViewSource::Local(id) } + if id == root.local) + || root.span.file != argument.source_span.file + || root.span.start < argument.source_span.start + || root.span.end > argument.source_span.end + || root.span.start >= root.span.end + { + return Err( + "call ownership flow projection lost its exact checked root".into() + ); + } + } + if initial_hir && argument.staging != ArgumentStaging::Original { + return Err( + "initial HIR call ownership cannot carry MIR staging authority".into(), + ); + } + match argument.staging { + ArgumentStaging::Original => {} + ArgumentStaging::Transferred { owner } => { + let owner = require_local(owner, &local)?; + if argument.effect != CheckedCallerEffect::TransferOwned + || argument.physical_access != CheckedCalleeAccess::Owned + || owner.ty != argument_types[parameter_index] + || owner.view_source.is_some() + || owner.is_view_parameter + || owner.view_iteration.is_some() + { + return Err( + "transferred call acquisition disagrees with checked ownership" + .into(), + ); + } + } + ArgumentStaging::Copied { value } => { + if argument.effect != CheckedCallerEffect::Copy + || require_local(value, &local)?.ty != argument_types[parameter_index] + { + return Err( + "copied call staging disagrees with checked copy or operand type" + .into(), + ); + } + } + ArgumentStaging::Borrowed { loan } => { + if argument.physical_access != CheckedCalleeAccess::View + || !matches!( + argument.effect, + CheckedCallerEffect::RetainBorrow + | CheckedCallerEffect::ObserveData + ) + { + return Err( + "borrowed call staging disagrees with the caller effect".into() + ); + } + if require_local(loan, &local)?.ty != argument_types[parameter_index] { + return Err("borrowed call staging has the wrong operand type".into()); + } + } + ArgumentStaging::Relinquished { owner, loan } => { + if argument.physical_access != CheckedCalleeAccess::View + || !(argument.effect == CheckedCallerEffect::RelinquishOwned + || (argument.effect == CheckedCallerEffect::TransferOwned + && source.bridge != CallBridge::Direct)) + { + return Err( + "owning call staging disagrees with the caller effect or bridge" + .into(), + ); + } + let owner = require_local(owner, &local)?; + if owner.ty != argument_types[parameter_index] + || owner.view_source.is_some() + || owner.is_view_parameter + || owner.view_iteration.is_some() + { + return Err("relinquished call owner cannot be a borrowed slot".into()); + } + if require_local(loan, &local)?.ty != argument_types[parameter_index] { + return Err("relinquished call loan has the wrong operand type".into()); + } + } + ArgumentStaging::RetainedSnapshot { owner, loan } => { + if argument.effect != CheckedCallerEffect::RetainBorrow + || argument.physical_access != CheckedCalleeAccess::View + || argument.retained_snapshot_type() + != Some(argument_types[parameter_index]) + || owner == loan + { + return Err("retained snapshot disagrees with checked endpoint or caller retention".into()); + } + let owner = require_local(owner, &local)?; + if owner.ty != argument_types[parameter_index] + || owner.view_source.is_some() + || owner.is_view_parameter + || owner.view_iteration.is_some() + || require_local(loan, &local)?.ty != owner.ty + { + return Err( + "retained snapshot requires an owning endpoint and exact loan type" + .into(), + ); + } + } + ArgumentStaging::Observed { owner } => { + if argument.effect != CheckedCallerEffect::ObserveData + || argument.physical_access != CheckedCalleeAccess::Owned + { + return Err("observation owner disagrees with the caller effect".into()); + } + let owner = require_local(owner, &local)?; + if owner.ty != argument_types[parameter_index] + || owner.view_source.is_some() + || owner.is_view_parameter + || owner.view_iteration.is_some() + { + return Err("observation snapshot cannot be a borrowed slot or change the operand type".into()); + } + } + } + if source.bridge == CallBridge::Direct + && argument.physical_access != argument.callee_access + { + return Err("direct source call changed its physical parameter access".into()); + } + } + for (tail, argument) in source.generated_operands.iter().enumerate() { + if !metadata_root_matches(source, argument.actual_type, types) + || argument.actual_type != argument.parameter_type + || argument.callee_access != CheckedCalleeAccess::Owned + || argument.acquisition != GeneratedAcquisition::OwnedExpression + { + return Err( + "call ownership metadata tail has no closed compiler-owned schema".into(), + ); + } + if evaluation_order.get(source_count + tail) != Some(&(source_count + tail)) { + return Err( + "generated metadata must follow all original source operands".into(), + ); + } + validate_generated_operand(argument, &local, types)?; + } + } + CallOwnership::Generated(generated) => { + if generated.operation != generated.certificate.operation + || evaluation_order != generated.certificate.evaluation_order + || generated.arguments.len() != generated.certificate.arguments.len() + { + return Err( + "generated invocation differs from its original operation/order certificate" + .into(), + ); + } + for (index, argument) in generated.arguments.iter().enumerate() { + if argument.witness != generated.certificate.arguments[index] { + return Err( + "generated argument witness differs from its invocation certificate".into(), + ); + } + validate_generated_operand(argument, &local, types)?; + validate_generated_stage(argument, argument_types[index], &local, initial_hir)?; + } + if let GeneratedOperation::FunctionAdapter { + callee, + source_type, + .. + } = generated.operation + { + if require_local(callee, &local)?.ty != source_type { + return Err("generated callable adapter capture differs from its sealed source signature".into()); + } + } + } + } + Ok(()) +} + +fn require_local( + id: LocalId, + local: &impl Fn(LocalId) -> Option, +) -> Result { + local(id).ok_or_else(|| "call ownership origin is outside its function".into()) +} + +fn validate_source( + source: CallerViewSource, + local: &impl Fn(LocalId) -> Option, +) -> Result<(), String> { + if let CallerViewSource::Local(id) = source { + require_local(id, local)?; + } + Ok(()) +} + +fn validate_origin( + origin: &CallerOrigin, + local: &impl Fn(LocalId) -> Option, + types: &TypeInterner, +) -> Result<(), String> { + match origin { + CallerOrigin::Binding(fact) => { + let value = require_local(fact.local, local)?; + if !valid_type(types, fact.ty) + || value.ty != fact.ty + || value.mutable != fact.mutable + || value.span.file != fact.declaration_span.file + || fact.declaration_span.start < value.span.start + || fact.declaration_span.end > value.span.end + { + return Err( + "call ownership binding metadata differs from its declared local".into(), + ); + } + match fact.mode { + CallerBindingMode::Owned + if !jett_typecheck::ownership::is_implicitly_copyable(types, fact.ty) + && (value.view_source.is_some() + || value.is_view_parameter + || value.view_iteration.is_some()) => + { + return Err("call ownership falsely claims an owned borrowed binding".into()); + } + CallerBindingMode::View { source } => { + validate_source(source, local)?; + let expected = match source { + CallerViewSource::Local(id) => Some(id), + CallerViewSource::Other => None, + }; + let iteration = value + .view_iteration + .is_some_and(|proof| proof.matches_binding(fact.ty, fact.declaration_span)); + if value.view_source != expected + || (expected.is_none() && !value.is_view_parameter && !iteration) + { + return Err( + "call ownership borrowed binding lost its immediate source".into() + ); + } + } + CallerBindingMode::Owned => {} + } + } + CallerOrigin::BorrowedProjection { source } + | CallerOrigin::OwnedFieldCopy { parent: source } => validate_source(*source, local)?, + CallerOrigin::OwnedExpression => {} + } + Ok(()) +} + +fn validate_effect( + argument: &ArgumentOwnership, + context: CheckedOwnershipContext, + types: &TypeInterner, +) -> Result<(), String> { + if argument.syntax == CheckedCallerSyntax::WrittenView + && argument.callee_access == CheckedCalleeAccess::Owned + { + return Err("call ownership written view cannot satisfy a source-owned parameter".into()); + } + let copyable = jett_typecheck::ownership::is_implicitly_copyable(types, argument.actual_type); + if argument.effect == CheckedCallerEffect::ObserveData { + if context == CheckedOwnershipContext::Ordinary { + return Err("ordinary call has no observation retention authority".into()); + } + return Ok(()); // Snapshot/capture acquisition is proved independently by its consumer. + } + let expected = if copyable { + CheckedCallerEffect::Copy + } else if argument.syntax == CheckedCallerSyntax::WrittenView { + CheckedCallerEffect::RetainBorrow + } else { + if matches!( + argument.origin, + CallerOrigin::Binding(CallerBindingFact { + mode: CallerBindingMode::View { .. }, + .. + }) | CallerOrigin::BorrowedProjection { .. } + ) { + return Err("bare move-only borrowed call input cannot acquire ownership".into()); + } + match argument.callee_access { + CheckedCalleeAccess::Owned => CheckedCallerEffect::TransferOwned, + CheckedCalleeAccess::View => CheckedCallerEffect::RelinquishOwned, + } + }; + if argument.effect != expected { + return Err("call ownership effect does not follow its checked tuple".into()); + } + Ok(()) +} + +fn validate_generated_operand( + argument: &GeneratedArgumentOwnership, + local: &impl Fn(LocalId) -> Option, + types: &TypeInterner, +) -> Result<(), String> { + if !valid_type(types, argument.actual_type) || !valid_type(types, argument.parameter_type) { + return Err("generated call ownership type is outside its interner".into()); + } + match argument.acquisition { + GeneratedAcquisition::Copy + if !jett_typecheck::ownership::is_implicitly_copyable(types, argument.actual_type) => + { + return Err("generated operand cannot copy a move-only value".into()); + } + GeneratedAcquisition::Borrow { source } => { + if argument.callee_access != CheckedCalleeAccess::View { + return Err("generated borrow cannot supply physical ownership".into()); + } + validate_source(source, local)?; + } + GeneratedAcquisition::OwnedExpression + if argument.callee_access != CheckedCalleeAccess::Owned => + { + return Err("generated owner has no physical ownership destination".into()); + } + _ => {} + } + Ok(()) +} + +/// Ordinary snapshot domain. Function capture authority is proved from the +/// descriptor's checked construction separately, never from its signature. +pub fn observation_data_type(types: &TypeInterner, ty: TypeId) -> bool { + fn visit( + types: &TypeInterner, + ty: TypeId, + seen: &mut std::collections::HashSet, + ) -> bool { + if !valid_type(types, ty) { + return false; + } + if !seen.insert(ty) { + return true; + } + if !types + .nominal_type_arguments(ty) + .iter() + .all(|&argument| visit(types, argument, seen)) + { + return false; + } + match types.resolve(ty) { + Type::Resource(_) + | Type::Capability(_) + | Type::Actor(_) + | Type::Interface(_) + | Type::Function { .. } + | Type::Error + | Type::TypeConstruction => false, + 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() + .all(|(_, field)| visit(types, *field, seen)), + Type::Bitfield(id) => types + .resolve_bitfield(*id) + .fields + .iter() + .all(|field| visit(types, field.ty, seen)), + Type::Enum(id) => types.resolve_enum(*id).variants.iter().all(|variant| { + variant + .fields + .iter() + .all(|(_, field)| visit(types, *field, seen)) + }), + Type::Machine(id) => types.resolve_machine(*id).states.iter().all(|state| { + state + .fields + .iter() + .all(|(_, field)| visit(types, *field, seen)) + }), + Type::MachineState { machine, state } => types + .resolve_machine(*machine) + .state(*state) + .is_some_and(|state| { + state + .fields + .iter() + .all(|(_, field)| visit(types, *field, 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, + } + } + visit(types, ty, &mut std::collections::HashSet::new()) +} + +impl crate::BodyLowerer<'_, '_> { + pub(crate) fn generated_ownership( + &mut self, + operation: GeneratedOperation, + args: &[Expression], + parameter_types: &[TypeId], + access: &[CheckedCalleeAccess], + result_type: TypeId, + evaluation_order: &[usize], + span: Span, + ) -> Option { + match CallOwnership::generated( + operation, + args, + parameter_types, + access, + result_type, + evaluation_order, + &self.parent.check.interner, + ) { + Ok(ownership) => Some(ownership), + Err(message) => { + self.parent.error(span, message); + None + } + } + } + pub(crate) fn finish_call_ownership( + &mut self, + mut ownership: CallOwnership, + function: Option, + intrinsic: Option, + args: &[Expression], + span: Span, + ) -> Option { + let result = self.join_physical_ownership(&mut ownership, function, intrinsic, args); + if let Err(message) = result { + self.parent.error(span, message); + return None; + } + Some(ownership) + } + + fn join_physical_ownership( + &self, + ownership: &mut CallOwnership, + function: Option, + intrinsic: Option, + args: &[Expression], + ) -> Result<(), String> { + let CallOwnership::Source(source) = ownership else { + return Err("source handoff requires a source packet".into()); + }; + let source_count = source.arguments.len(); + if source_count > args.len() { + return Err("source handoff removed an original operand".into()); + } + if let Some(function) = function { + let (params, access) = self.ownership_function_parameters(function)?; + if params.len() != args.len() { + return Err("source handoff physical function arity changed".into()); + } + let direct = matches!(source.target, CallTarget::Function(target) | CallTarget::Interface { function: target, .. } if target == function); + if !direct { + let CallTarget::Intrinsic(intrinsic) = source.target else { + return Err( + "source call cannot redirect to an unrelated implementation function" + .into(), + ); + }; + if !json_intrinsic(intrinsic) || !self.json_ownership_helper(function, intrinsic) { + return Err("source JSON handoff has no closed trusted helper identity".into()); + } + source.bridge = CallBridge::JsonSource { + intrinsic, + function, + }; + } + for (index, argument) in source.arguments.iter_mut().enumerate() { + argument.physical_access = access[index]; + } + } else if let Some(intrinsic) = intrinsic { + match source.target.clone() { + CallTarget::Intrinsic(original) if original == intrinsic => {} + CallTarget::Function(function) => { + let key = self + .function_ids + .iter() + .find_map(|(key, &id)| (id == function).then_some(key)) + .ok_or("intrinsic facade has no exact source declaration key")?; + let identity = self.ownership_declaration(key)?; + let CheckedInvocationShape::Function { signature_type } = source.shape else { + return Err("intrinsic facade has no checked signature".into()); + }; + if !trusted_intrinsic_declaration(&identity, intrinsic) { + return Err("intrinsic handoff is not a closed compiler facade".into()); + } + source.target = CallTarget::Declaration { + identity, + signature_type, + }; + source.bridge = CallBridge::TrustedIntrinsic { intrinsic }; + } + CallTarget::Declaration { ref identity, .. } + if trusted_intrinsic_declaration(identity, intrinsic) => + { + source.bridge = CallBridge::TrustedIntrinsic { intrinsic }; + } + _ => return Err("source intrinsic handoff changed its checked target".into()), + } + for (index, argument) in source.arguments.iter_mut().enumerate() { + argument.physical_access = + if matches!(intrinsic, IntrinsicId::Print | IntrinsicId::Println) { + argument.callee_access + } else { + jett_typecheck::intrinsic_operand_access(intrinsic, index, source_count) + .ok_or("source intrinsic physical operand has no closed access")? + }; + } + } + for argument in &args[source_count..] { + validate_compiler_metadata_operand(argument, source, &self.parent.check.interner)?; + source.generated_operands.push(GeneratedArgumentOwnership { + actual_type: argument.ty, + parameter_type: argument.ty, + callee_access: CheckedCalleeAccess::Owned, + acquisition: GeneratedAcquisition::OwnedExpression, + staging: GeneratedArgumentStaging::Existing { loan: None }, + staging_certificate: GeneratedArgumentStaging::Existing { loan: None }, + witness: generated_witness( + argument, + argument.ty, + CheckedCalleeAccess::Owned, + GeneratedAcquisition::OwnedExpression, + &self.parent.check.interner, + ), + }); + } + source.certificate.generated_operands = source.generated_operands.clone(); + source.certificate.target = source.target.clone(); + source.certificate.bridge = source.bridge.clone(); + Ok(()) + } + + pub(crate) fn ownership_function_parameters( + &self, + function: FunctionId, + ) -> Result<(Vec, Vec), String> { + let key = self + .function_ids + .iter() + .find_map(|(key, &id)| (id == function).then_some(key)) + .ok_or("physical source helper has no exact HIR key")?; + let (params, views) = match key { + crate::FunctionKey::Definition { + definition, + concrete_args, + specialization, + } if !concrete_args.is_empty() => { + let instance = self + .parent + .check + .generic_function_instantiations + .iter() + .find(|instance| { + instance.definition == *definition + && instance.concrete_args == *concrete_args + && instance.specialization == *specialization + }) + .ok_or("physical source helper has no exact checked generic body")?; + let source = self + .parent + .module + .items + .iter() + .find_map(|item| match item { + jett_parser::ast::Item::Function(source) + if self.parent.definition_at( + source.name.span, + jett_resolve::DefKind::Function, + ) == Some(*definition) => + { + Some(source) + } + _ => None, + }) + .ok_or("physical helper has no source parameter modes")?; + ( + instance.parameter_types.clone(), + source.params.iter().map(|param| param.view).collect(), + ) + } + crate::FunctionKey::Definition { definition, .. } => { + let ty = *self + .parent + .check + .definition_types + .get(definition) + .ok_or("physical source helper has no checked function type")?; + let Type::Function { + params, + view_params, + .. + } = self.parent.check.interner.resolve(ty) + else { + return Err("physical source helper type is not a function".into()); + }; + (params.clone(), view_params.clone()) + } + crate::FunctionKey::Method { source_span } => { + let method = self + .parent + .check + .method_definitions + .iter() + .find(|method| method.source_span == *source_span) + .ok_or("physical method has no checked declaration")?; + let source = self + .parent + .method_at(*source_span) + .ok_or("physical method has no source parameter modes")?; + ( + method.parameter_types.clone(), + source.params.iter().map(|param| param.view).collect(), + ) + } + crate::FunctionKey::Interface { owner, method } => { + let Type::Interface(id) = self.parent.check.interner.resolve(*owner) else { + return Err("physical dispatcher is not an interface".into()); + }; + let signature = self + .parent + .check + .interner + .resolve_interface(*id) + .methods + .get(*method) + .ok_or("physical interface slot is out of bounds")?; + ( + signature.params.iter().map(|(_, ty, _)| *ty).collect(), + signature.params.iter().map(|(_, _, view)| *view).collect(), + ) + } + }; + Ok(( + params, + views + .into_iter() + .map(|view| { + if view { + CheckedCalleeAccess::View + } else { + CheckedCalleeAccess::Owned + } + }) + .collect(), + )) + } + + fn json_ownership_helper(&self, function: FunctionId, intrinsic: IntrinsicId) -> bool { + let Some(key) = self + .function_ids + .iter() + .find_map(|(key, &id)| (id == function).then_some(key)) + else { + return false; + }; + self.ownership_declaration(key) + .is_ok_and(|identity| json_helper_declaration(&identity, intrinsic)) + } + + pub(crate) fn source_ownership( + &mut self, + span: Span, + args: &[Expression], + evaluation_order: &[usize], + ) -> Option { + let result = self.build_source_ownership(span, args, evaluation_order); + match result { + Ok(value) => Some(value), + Err(message) => { + self.parent.error(span, message); + None + } + } + } + + fn build_source_ownership( + &self, + span: Span, + args: &[Expression], + evaluation_order: &[usize], + ) -> Result { + let checked = self + .call_ownership + .get(&span) + .ok_or("source invocation has no checked caller ownership")?; + if checked.context != self.ownership_context { + return Err("source invocation ownership context is not its lexical context".into()); + } + if checked.arguments.len() != args.len() || evaluation_order.len() != args.len() { + return Err("checked caller ownership source operand count changed".into()); + } + let target = self.checked_ownership_target(&checked.target, &checked.shape)?; + let mut arguments = Vec::with_capacity(args.len()); + for parameter_index in 0..args.len() { + let fact = checked + .arguments + .iter() + .find(|fact| fact.parameter_index == parameter_index) + .ok_or("checked caller ownership has no exact formal occurrence")?; + if checked.arguments.get(fact.source_index) != Some(fact) + || evaluation_order.get(fact.source_index) != Some(¶meter_index) + { + return Err("checked caller ownership source/formal join changed".into()); + } + if self.source_expression_types.get(&fact.source_span) != Some(&fact.actual_type) { + return Err( + "call ownership raw source occurrence differs from its exact checked body" + .into(), + ); + } + // A pipeline input and its field/callable target can share a span. + // The dedicated ledger retains input authority without overwriting + // either the target expression or the prior raw source output. + let occurrence_type = + if fact.source_index == 0 && self.pipeline_step_call_types.contains_key(&span) { + *self + .pipeline_step_input_types + .get(&span) + .ok_or("pipeline ownership input has no exact checked physical type")? + } else { + *self + .expression_types + .get(&fact.source_span) + .ok_or("call ownership occurrence has no exact checked physical type")? + }; + if occurrence_type != fact.actual_type + && !(has_outer_secret(&self.parent.check.interner, fact.parameter_type) + && secret_taint_matches( + &self.parent.check.interner, + occurrence_type, + fact.actual_type, + )) + { + return Err( + "call ownership occurrence has no checked outer-secret qualification".into(), + ); + } + let origin = self.checked_caller_origin(&fact.origin)?; + let observation = if fact.effect == CheckedCallerEffect::ObserveData { + Some(self.observation_proof(&args[parameter_index], fact.actual_type)?) + } else { + None + }; + let witness = SourceArgumentWitness { + source_span: fact.source_span, + source_index: fact.source_index, + parameter_index, + actual_type: fact.actual_type, + occurrence_type, + parameter_type: fact.parameter_type, + syntax: fact.syntax, + origin: origin.clone(), + callee_access: fact.callee_access, + effect: fact.effect, + context: checked.context, + observation, + handled_result: None, + projection_root: self.checked_flow_projection_root(fact.source_span, &origin)?, + }; + let mut argument = ArgumentOwnership { + source_span: fact.source_span, + source_index: fact.source_index, + parameter_index, + actual_type: fact.actual_type, + parameter_type: fact.parameter_type, + syntax: fact.syntax, + origin: origin.clone(), + callee_access: fact.callee_access, + physical_access: fact.callee_access, + effect: fact.effect, + staging: ArgumentStaging::Original, + witness, + retained_snapshot: (fact.effect == CheckedCallerEffect::RetainBorrow + && retained_snapshot_origin(&origin) + && observation_data_type(&self.parent.check.interner, fact.actual_type) + && observation_data_type(&self.parent.check.interner, occurrence_type)) + .then_some(RetainedSnapshotProof { + actual_type: fact.actual_type, + occurrence_type, + physical_span: args[parameter_index].span, + }), + }; + if argument.origin == CallerOrigin::OwnedExpression { + let original = source_operand( + &args[parameter_index], + &argument, + &CallBridge::Direct, + &self.parent.check.interner, + )?; + let backing = source_view_backing(original); + if matches!(backing.kind, ExpressionKind::Handle { .. }) { + argument.witness.handled_result = Some(SourceHandledResult { + ty: backing.ty, + span: backing.span, + }); + } + } + arguments.push(argument); + } + let shape = checked.shape.clone(); + let context = checked.context; + let result_type = self + .pipeline_step_call_types + .get(&span) + .or_else(|| self.expression_types.get(&span)) + .copied() + .ok_or("source invocation has no exact checked result type")?; + let ownership = CallOwnership::Source(SourceCallOwnership { + target: target.clone(), + shape: shape.clone(), + context, + arguments, + bridge: CallBridge::Direct, + generated_operands: Vec::new(), + certificate: SourceCallCertificate { + target, + shape, + context, + bridge: CallBridge::Direct, + result_type, + generated_operands: Vec::new(), + }, + }); + validate_operand_ownership( + &ownership, + &args.iter().map(|arg| arg.ty).collect::>(), + evaluation_order, + &self.parent.check.interner, + |id| self.ownership_local_info(id), + true, + )?; + Ok(ownership) + } + + fn checked_flow_projection_root( + &self, + source_span: Span, + origin: &CallerOrigin, + ) -> Result, String> { + let local = match origin { + CallerOrigin::BorrowedProjection { + source: CallerViewSource::Local(id), + } + | CallerOrigin::OwnedFieldCopy { + parent: CallerViewSource::Local(id), + } => *id, + _ => return Ok(None), + }; + let stored = self + .locals + .get(local.index() as usize) + .filter(|stored| stored.id == local) + .ok_or("call ownership projection root has no exact stored local")?; + if !valid_type(&self.parent.check.interner, stored.ty) { + return Err("call ownership projection root has invalid stored metadata".into()); + } + if !matches!( + self.parent.check.interner.resolve(stored.ty), + Type::Machine(_) + ) { + return Ok(None); + } + let root = self + .source_projection_roots + .get(&source_span) + .ok_or("call ownership projection has no original source AST root")?; + let occurrence_type = *self + .source_expression_types + .get(&root.span) + .ok_or("call ownership projection root has no exact checked body type")?; + if !flow_projection_root_matches(&self.parent.check.interner, occurrence_type, stored.ty) { + return Ok(None); + } + let definition = self + .parent + .resolve + .resolutions + .get(&root.span) + .ok_or("call ownership projection root has no exact source definition")?; + if self.local_ids.get(definition) != Some(&local) + || self.expression_types.get(&root.span) != Some(&occurrence_type) + || root.span.file != source_span.file + || root.span.start < source_span.start + || root.span.end > source_span.end + || root.span.start >= root.span.end + { + return Err( + "call ownership flow projection differs from its original source root".into(), + ); + } + Ok(Some(SourceProjectionRoot { + local, + span: root.span, + occurrence_type, + storage_type: stored.ty, + })) + } + + fn checked_ownership_target( + &self, + target: &jett_typecheck::CheckedInvocationTarget, + shape: &CheckedInvocationShape, + ) -> Result { + use jett_typecheck::CheckedInvocationTarget as Target; + let key = match target { + Target::Resolved(definition) => crate::FunctionKey::Definition { + definition: *definition, + concrete_args: Vec::new(), + specialization: jett_typecheck::CheckedGenericSpecialization::default(), + }, + Target::Generic(generic) => crate::FunctionKey::Definition { + definition: generic.definition, + concrete_args: generic.concrete_args.clone(), + specialization: generic.specialization.clone(), + }, + Target::Method(method) => crate::FunctionKey::Method { + source_span: method.source_span, + }, + Target::Interface(interface) => { + let key = crate::FunctionKey::Interface { + owner: interface.interface_type, + method: interface.method_index, + }; + let function = *self + .function_ids + .get(&key) + .ok_or("source interface ownership target has no HIR dispatcher")?; + return Ok(CallTarget::Interface { + function, + interface_type: interface.interface_type, + method_index: interface.method_index, + }); + } + Target::Indirect(signature_type) => { + return Ok(CallTarget::Indirect { + signature_type: *signature_type, + }); + } + Target::Intrinsic(intrinsic) => return Ok(CallTarget::Intrinsic(*intrinsic)), + }; + if let Some(&function) = self.function_ids.get(&key) { + return Ok(CallTarget::Function(function)); + } + let CheckedInvocationShape::Function { signature_type } = shape else { + return Err("source declaration ownership target has no function signature".into()); + }; + let identity = self.ownership_declaration(&key)?; + if identity.declaration.origin != jett_common::SourceOrigin::Stdlib { + return Err("source ownership declaration has no executable HIR target".into()); + } + Ok(CallTarget::Declaration { + identity, + signature_type: *signature_type, + }) + } + + fn ownership_declaration(&self, key: &crate::FunctionKey) -> Result { + let crate::FunctionKey::Definition { + definition, + concrete_args, + specialization, + } = key + else { + return Err("ownership declaration is not an ordinary concrete source function".into()); + }; + let info = self + .parent + .resolve + .scope_table + .definitions + .get(definition.index() as usize) + .filter(|info| info.id == *definition) + .ok_or("ownership declaration definition is outside its resolution session")?; + if info.kind != jett_resolve::DefKind::Function { + return Err("ownership declaration is not a function definition".into()); + } + let origin = self + .parent + .origins + .get(&info.span.file) + .cloned() + .ok_or("ownership declaration has no compiler source origin")?; + Ok(FunctionIdentity { + declaration: crate::DeclarationId { + origin, + namespace: info.namespace.clone().unwrap_or_default(), + name: info + .name + .rsplit('.') + .next() + .unwrap_or(&info.name) + .to_string(), + kind: crate::DeclarationKind::Function, + }, + scoped_type_bindings: Vec::new(), + type_arguments: concrete_args.clone(), + specialization: specialization.clone(), + }) + } + + fn checked_view_source( + &self, + source: jett_typecheck::CheckedViewSource, + ) -> Result { + match source { + jett_typecheck::CheckedViewSource::Other => Ok(CallerViewSource::Other), + jett_typecheck::CheckedViewSource::Binding(definition) => self + .local_ids + .get(&definition) + .copied() + .map(CallerViewSource::Local) + .ok_or_else(|| "caller origin has no local in its exact checked body".into()), + } + } + + fn ownership_local_info(&self, id: LocalId) -> Option { + let local = self + .locals + .get(id.index() as usize) + .filter(|local| local.id == id)?; + let is_view_parameter = self.view_parameter_locals.contains(&id); + Some(OwnershipLocalInfo { + ty: local.ty, + mutable: local.mutable, + span: local.span, + view_source: local.view_source, + is_view_parameter, + view_iteration: self.view_iteration_bindings.get(&id).copied(), + }) + } + + pub(crate) fn check_pipeline_actuals( + &mut self, + span: Span, + piped: &Expression, + written_view: bool, + extras: &[jett_parser::ast::CallArg], + ) -> Option<()> { + let valid = self.call_ownership.get(&span).is_some_and(|checked| { + checked.arguments.len() == extras.len() + 1 + && checked.arguments.first().is_some_and(|first| { + self.source_expression_types.get(&first.source_span) == Some(&first.actual_type) + && self.pipeline_step_input_types.get(&span) == Some(&piped.ty) + && first.syntax + == if written_view || original_view(piped) { + CheckedCallerSyntax::WrittenView + } else { + CheckedCallerSyntax::Bare + } + }) + && checked.arguments[1..] + .iter() + .zip(extras) + .all(|(fact, extra)| { + fact.source_span == extra.value.span() + && fact.syntax == source_syntax(&extra.value) + && self.source_expression_types.get(&extra.value.span()) + == Some(&fact.actual_type) + }) + }); + if !valid { + self.parent.error( + span, + "pipeline ownership differs from its original virtual source occurrences", + ); + return None; + } + Some(()) + } + + fn checked_caller_origin( + &self, + origin: &jett_typecheck::CheckedCallerOrigin, + ) -> Result { + use jett_typecheck::CheckedCallerOrigin as Origin; + match origin { + Origin::Binding(fact) => { + if self.binding_facts.get(&fact.declaration_span) != Some(fact) { + return Err("caller binding fact is not owned by its exact checked body".into()); + } + let local = *self + .local_ids + .get(&fact.definition) + .ok_or("caller binding has no exact HIR local")?; + let mode = match fact.mode { + jett_typecheck::CheckedBindingMode::Owned => CallerBindingMode::Owned, + jett_typecheck::CheckedBindingMode::View { source } => { + CallerBindingMode::View { + source: self.checked_view_source(source)?, + } + } + }; + Ok(CallerOrigin::Binding(CallerBindingFact { + local, + declaration_span: fact.declaration_span, + ty: fact.ty, + mode, + mutable: fact.mutable, + })) + } + Origin::BorrowedProjection { source } => Ok(CallerOrigin::BorrowedProjection { + source: self.checked_view_source(*source)?, + }), + Origin::OwnedFieldCopy { parent } => Ok(CallerOrigin::OwnedFieldCopy { + parent: self.checked_view_source(*parent)?, + }), + Origin::OwnedExpression => Ok(CallerOrigin::OwnedExpression), + } + } + + pub(crate) fn check_source_actuals( + &mut self, + span: Span, + actuals: &[jett_parser::ast::CallArg], + ) -> Option<()> { + let valid = self.call_ownership.get(&span).is_some_and(|checked| { + checked.arguments.len() == actuals.len() + && checked.arguments.iter().zip(actuals).all(|(fact, actual)| { + fact.source_span == actual.value.span() + && fact.syntax == source_syntax(&actual.value) + && self.source_expression_types.get(&actual.value.span()) + == Some(&fact.actual_type) + }) + }); + if !valid { + self.parent.error( + span, + "caller ownership differs from its original source argument syntax or type", + ); + return None; + } + Some(()) + } + + fn observation_proof( + &self, + expression: &Expression, + actual_type: TypeId, + ) -> Result { + let mut value = expression; + while let ExpressionKind::View(inner) = &value.kind { + value = inner; + } + let types = &self.parent.check.interner; + if observation_data_type(types, actual_type) { + return Ok(ObservationProof { + actual_type, + captures: Vec::new(), + descriptor_checked: false, + }); + } + if !valid_type(types, actual_type) + || !matches!(types.resolve(actual_type), Type::Function { .. }) + { + return Err( + "observing caller input has no supported ordinary-data snapshot proof".into(), + ); + } + let captures = match &value.kind { + ExpressionKind::FunctionRef(_) => Vec::new(), + ExpressionKind::InlineFunction { + local_floor, body, .. + } => self + .locals + .iter() + .filter(|local| { + local.id.index() < *local_floor + && crate::inline_functions::body_uses_local(body, local.id) + }) + .map(|local| (local.id, local.ty)) + .collect(), + _ => { + return Err( + "observing function descriptor has no checked capture construction proof" + .into(), + ); + } + }; + for &(id, ty) in &captures { + let local = self + .ownership_local_info(id) + .ok_or("observing descriptor capture is outside its function")?; + if local.view_source.is_some() + || local.is_view_parameter + || local.view_iteration.is_some() + || !observation_data_type(types, ty) + { + return Err( + "observing descriptor capture cannot acquire authority or a borrowed owner" + .into(), + ); + } + } + Ok(ObservationProof { + actual_type, + captures, + descriptor_checked: true, + }) + } +} + +fn source_syntax(mut expression: &jett_parser::ast::Expr) -> CheckedCallerSyntax { + loop { + match expression { + jett_parser::ast::Expr::Paren(inner, _) + | jett_parser::ast::Expr::Coarsen(inner, _) + | jett_parser::ast::Expr::Declassify(inner, _) => expression = inner, + jett_parser::ast::Expr::View(_, _) => return CheckedCallerSyntax::WrittenView, + _ => return CheckedCallerSyntax::Bare, + } + } +} + +/// Syntactic place provenance only, recorded while lowering the original AST. +pub(super) fn source_projection_ast_root( + mut expression: &jett_parser::ast::Expr, +) -> Option<&jett_parser::ast::Ident> { + use jett_parser::ast::Expr; + let mut projected = false; + loop { + expression = match expression { + Expr::View(inner, _) + | Expr::Paren(inner, _) + | Expr::Coarsen(inner, _) + | Expr::Declassify(inner, _) => inner, + Expr::FieldAccess(base, _, _) => { + projected = true; + base + } + Expr::Ident(root) if projected => return Some(root), + _ => return None, + }; + } +} + +fn original_view(mut expression: &Expression) -> bool { + loop { + match &expression.kind { + ExpressionKind::Coarsen(inner) | ExpressionKind::Declassify(inner) => { + expression = inner + } + ExpressionKind::View(_) => return true, + _ => return false, + } + } +} + +fn json_intrinsic(intrinsic: IntrinsicId) -> bool { + matches!( + intrinsic, + IntrinsicId::JsonParse + | IntrinsicId::JsonParseExact + | IntrinsicId::JsonSerialize + | IntrinsicId::JsonSerializePublic + ) +} + +fn trusted_intrinsic_declaration(identity: &FunctionIdentity, intrinsic: IntrinsicId) -> bool { + if identity.declaration.origin != jett_common::SourceOrigin::Stdlib + || identity.declaration.kind != crate::DeclarationKind::Function + { + return false; + } + match ( + identity.declaration.namespace.as_str(), + identity.declaration.name.as_str(), + intrinsic, + ) { + ("math", "average", IntrinsicId::MathAverage) + | ("math", "median", IntrinsicId::MathMedian) => true, + ("json", "serialize", IntrinsicId::JsonSerialize) + | ("json", "serialize_public", IntrinsicId::JsonSerializePublic) + | ("json", "parse", IntrinsicId::JsonParse) + | ("json", "parse_exact", IntrinsicId::JsonParseExact) => true, + _ => false, + } +} + +fn json_helper_declaration(identity: &FunctionIdentity, intrinsic: IntrinsicId) -> bool { + if identity.declaration.origin != jett_common::SourceOrigin::Stdlib + || identity.declaration.namespace != "json" + || identity.declaration.kind != crate::DeclarationKind::Function + { + return false; + } + match intrinsic { + IntrinsicId::JsonSerialize | IntrinsicId::JsonSerializePublic => matches!( + identity.declaration.name.as_str(), + "serialize" + | "serialize_public" + | "serialize_raw" + | "json_serialize_native_enum" + | "json_serialize_native_bitfield" + | "json_serialize_native_bytes" + | "json_serialize_native_nothing" + ), + IntrinsicId::JsonParse => matches!( + identity.declaration.name.as_str(), + "parse" + | "parse_raw" + | "json_parse_native_enum" + | "json_parse_native_bitfield" + | "json_parse_native_secret_tree" + | "json_parse_native_string" + | "json_parse_native_bool" + | "json_parse_native_int64" + | "json_parse_native_uint8" + | "json_parse_native_uint64" + | "json_parse_native_float64" + | "json_parse_native_bytes" + | "json_parse_native_nothing" + ), + IntrinsicId::JsonParseExact => matches!( + identity.declaration.name.as_str(), + "parse_exact" + | "parse_raw" + | "json_parse_exact_native_enum" + | "json_parse_exact_native_bitfield" + | "json_parse_native_secret_tree" + | "json_parse_native_string" + | "json_parse_native_bool" + | "json_parse_native_int64" + | "json_parse_native_uint8" + | "json_parse_native_uint64" + | "json_parse_native_float64" + | "json_parse_native_bytes" + | "json_parse_native_nothing" + ), + _ => false, + } +} + +/// Full typed HIR gate, run on every retained or unused invocation before MIR. +pub fn validate_program_call_ownership( + program: &crate::Program, + types: &TypeInterner, +) -> Result<(), Vec> { + crate::validate_backend_types(program, types) +} + +pub fn validate_hir_invocation( + functions: &[crate::Function], + locals: &[OwnershipLocalInfo], + expression: &Expression, + types: &TypeInterner, + context: CheckedOwnershipContext, +) -> Result<(), String> { + let (ownership, args, order) = match &expression.kind { + ExpressionKind::Call { + ownership, + args, + evaluation_order, + .. + } + | ExpressionKind::Intrinsic { + ownership, + args, + evaluation_order, + .. + } + | ExpressionKind::IndirectCall { + ownership, + args, + evaluation_order, + .. + } => (ownership, args, evaluation_order), + _ => return Ok(()), + }; + validate_operand_ownership( + ownership, + &args.iter().map(|arg| arg.ty).collect::>(), + order, + types, + |id| locals.get(id.index() as usize).copied(), + true, + )?; + validate_invocation_target(functions, expression, types, context)?; + match ownership { + CallOwnership::Source(source) => { + for (index, argument) in source.arguments.iter().enumerate() { + if argument + .retained_snapshot_span() + .is_some_and(|span| span != args[index].span) + { + return Err( + "call ownership retained snapshot physical occurrence changed".into(), + ); + } + validate_source_operand(&args[index], argument, &source.bridge, locals, types)?; + } + for (tail, argument) in source.generated_operands.iter().enumerate() { + validate_generated_operand_tree( + &args[source.arguments.len() + tail], + argument, + locals, + )?; + validate_compiler_metadata_operand( + &args[source.arguments.len() + tail], + source, + types, + )?; + } + } + CallOwnership::Generated(generated) => { + for (value, argument) in args.iter().zip(&generated.arguments) { + validate_generated_operand_tree(value, argument, locals)?; + } + } + } + Ok(()) +} + +/// Validate invocation identity without granting authority to a rewritten operand. +/// MIR separately proves each generated result, owning Let and BeginCallView. +pub fn validate_invocation_target( + functions: &[crate::Function], + expression: &Expression, + types: &TypeInterner, + context: CheckedOwnershipContext, +) -> Result<(), String> { + let (ownership, args, _order) = match &expression.kind { + ExpressionKind::Call { + ownership, + args, + evaluation_order, + .. + } + | ExpressionKind::Intrinsic { + ownership, + args, + evaluation_order, + .. + } + | ExpressionKind::IndirectCall { + ownership, + args, + evaluation_order, + .. + } => (ownership, args, evaluation_order), + _ => return Ok(()), + }; + if ownership.parameter_count() != args.len() { + return Err("call ownership target operand count is invalid".into()); + } + let mut valid_metadata = true; + ownership.metadata_types(|ty| valid_metadata &= valid_type(types, ty)); + if !valid_metadata { + return Err("call ownership target metadata type is outside its interner".into()); + } + if let CallOwnership::Source(source) = ownership { + if source.target != source.certificate.target + || source.shape != source.certificate.shape + || source.context != source.certificate.context + || source.bridge != source.certificate.bridge + || source.generated_operands != source.certificate.generated_operands + { + return Err( + "call ownership invocation differs from its checked source certificate".into(), + ); + } + } + if args.iter().any(|argument| !valid_type(types, argument.ty)) { + return Err("call ownership physical operand type is outside its interner".into()); + } + let target = match &expression.kind { + ExpressionKind::Call { function, .. } => Some(function_lookup(functions, *function)?), + _ => None, + }; + match ownership { + CallOwnership::Source(source) => { + if expression.ty != source.certificate.result_type { + return Err( + "call ownership result differs from its checked source certificate".into(), + ); + } + if source.context != context { + return Err("call ownership source context is not its lexical owner".into()); + } + match (&source.target, &source.bridge, &expression.kind) { + ( + CallTarget::Function(source_target), + CallBridge::Direct, + ExpressionKind::Call { function, .. }, + ) if source_target == function => { + validate_function_shape( + &source.shape, + target.ok_or("source call has no physical function")?, + types, + )?; + } + ( + CallTarget::Interface { + function: source_target, + interface_type, + method_index, + }, + CallBridge::Direct, + ExpressionKind::Call { function, .. }, + ) if source_target == function => { + let target = + target.ok_or("source interface call has no physical dispatcher")?; + validate_function_shape(&source.shape, target, types)?; + let Type::Interface(id) = types.resolve(*interface_type) else { + return Err("source interface ownership owner is not an interface".into()); + }; + let method = types + .resolve_interface(*id) + .methods + .get(*method_index) + .ok_or("source interface ownership slot is out of bounds")?; + if target.source_definition.is_some() + || target.params.len() != method.params.len() + || target + .params + .iter() + .zip(&method.params) + .any(|(param, (_, ty, view))| { + param.ty != *ty || (param.mode == crate::ParamMode::View) != *view + }) + || target.return_type != method.return_type + { + return Err( + "source interface ownership dispatcher differs from the checked slot" + .into(), + ); + } + } + ( + CallTarget::Indirect { signature_type }, + CallBridge::Direct, + ExpressionKind::IndirectCall { callee, .. }, + ) => { + if callee.ty != *signature_type + || source.shape + != (CheckedInvocationShape::Function { + signature_type: *signature_type, + }) + { + return Err( + "indirect call ownership signature differs from its actual callee" + .into(), + ); + } + } + ( + CallTarget::Intrinsic(source_id), + CallBridge::Direct, + ExpressionKind::Intrinsic { intrinsic, .. }, + ) if source_id == intrinsic => { + if !matches!(source.shape, CheckedInvocationShape::Intrinsic { intrinsic: id, .. } if id == *intrinsic) + { + return Err("source intrinsic ownership shape has another operation".into()); + } + } + ( + CallTarget::Declaration { + identity, + signature_type, + }, + CallBridge::TrustedIntrinsic { intrinsic: bridge }, + ExpressionKind::Intrinsic { intrinsic, .. }, + ) if bridge == intrinsic + && trusted_intrinsic_declaration(identity, *intrinsic) + && source.shape + == (CheckedInvocationShape::Function { + signature_type: *signature_type, + }) => {} + ( + CallTarget::Intrinsic(source_id), + CallBridge::JsonSource { + intrinsic: bridge, + function: helper, + }, + ExpressionKind::Call { function, .. }, + ) if source_id == bridge && helper == function && json_intrinsic(*bridge) => { + let target = target.ok_or("source JSON ownership helper is absent")?; + if !json_helper_declaration(&target.identity, *bridge) + || target.capture_count != 0 + { + return Err( + "source JSON ownership helper has no trusted closed identity".into(), + ); + } + let CheckedInvocationShape::Intrinsic { result_type, .. } = source.shape else { + return Err("source JSON helper has no original intrinsic shape".into()); + }; + if target.return_type != result_type { + return Err("source JSON helper changed the checked result type".into()); + } + } + _ => { + return Err( + "call ownership physical target has no exact checked source join".into(), + ); + } + } + for (index, argument) in source.arguments.iter().enumerate() { + if let Some(target) = target { + let parameter = target + .params + .get(index) + .ok_or("ownership physical function parameter is missing")?; + if argument.physical_access != access(parameter.mode) { + return Err("call ownership physical function access changed".into()); + } + if !source_parameter_matches(argument, args[index].ty, parameter.ty, types) { + return Err( + "call ownership converted operand differs from its physical parameter" + .into(), + ); + } + } else if let ExpressionKind::IndirectCall { callee, .. } = &expression.kind { + let Type::Function { params, .. } = types.resolve(callee.ty) else { + return Err("call ownership indirect callee is not a function".into()); + }; + let parameter_type = params + .get(index) + .copied() + .ok_or("call ownership indirect parameter is missing")?; + if !source_parameter_matches(argument, args[index].ty, parameter_type, types) { + return Err("call ownership indirect converted operand differs from its physical parameter".into()); + } + } + } + } + CallOwnership::Generated(generated) => { + if generated.operation != generated.certificate.operation + || _order != &generated.certificate.evaluation_order + || expression.ty != generated.certificate.result_type + { + return Err( + "generated invocation differs from its sealed operation/result/order".into(), + ); + } + if target.is_some_and(|target| target.params.len() != args.len()) { + return Err( + "generated invocation differs from its exact physical target arity".into(), + ); + } + match (&generated.operation, &expression.kind) { + (GeneratedOperation::Display { method }, ExpressionKind::Call { function, .. }) + if method == function => + { + let target = target.ok_or("display ownership method is absent")?; + if target.params.len() != 1 + || target.params[0].mode != crate::ParamMode::View + || target.return_type != TypeInterner::STRING + || target.identity.declaration.kind != crate::DeclarationKind::Method + { + return Err( + "generated display ownership has an invalid method shape".into() + ); + } + } + ( + GeneratedOperation::Equality { method }, + ExpressionKind::Call { function, .. }, + ) if method == function => { + let target = target.ok_or("equality ownership method is absent")?; + if target.params.len() != 2 + || target.params[0].ty != target.params[1].ty + || target + .params + .iter() + .any(|param| param.mode != crate::ParamMode::View) + || target.return_type != TypeInterner::BOOL + || target.identity.declaration.kind != crate::DeclarationKind::Method + { + return Err( + "generated equality ownership has an invalid method shape".into() + ); + } + } + ( + GeneratedOperation::InterfaceDispatch { + function: expected, + interface_type, + method_index, + }, + ExpressionKind::Call { function, .. }, + ) if expected == function => { + if !valid_type(types, *interface_type) { + return Err("generated dispatch owner is outside its interner".into()); + } + let Type::Interface(id) = types.resolve(*interface_type) else { + return Err("generated dispatch owner is not an interface".into()); + }; + let slot = types + .resolve_interface(*id) + .methods + .get(*method_index) + .ok_or("generated dispatch slot is out of bounds")?; + let target = target.ok_or("generated dispatch implementation is absent")?; + if target.identity.declaration.kind != crate::DeclarationKind::Method + || slot.params.len() != target.params.len() + || target.return_type != slot.return_type + || slot + .params + .iter() + .zip(&target.params) + .any(|((_, _, view), param)| { + *view != (param.mode == crate::ParamMode::View) + }) + { + return Err( + "generated dispatch implementation differs from its interface slot" + .into(), + ); + } + } + ( + GeneratedOperation::FunctionAdapter { + callee: expected, + source_type, + target_type, + }, + ExpressionKind::IndirectCall { callee, .. }, + ) => { + if !matches!(callee.kind, ExpressionKind::Local(local) if local == *expected) + || callee.ty != *source_type + || !valid_type(types, *source_type) + || !valid_type(types, *target_type) + || !matches!(types.resolve(*target_type), Type::Function { .. }) + { + return Err("generated callable adapter ownership types are invalid".into()); + } + } + ( + GeneratedOperation::RefinementPredicate { + function: predicate, + }, + ExpressionKind::Call { function, .. }, + ) if predicate == function => { + let target = target.ok_or("generated refinement predicate is absent")?; + if target.params.len() != 1 + || target.params[0].mode != crate::ParamMode::Owned + || target.capture_count != 0 + || target.return_type != TypeInterner::BOOL + || target.identity.declaration.kind + != crate::DeclarationKind::RefinementPredicate + { + return Err( + "generated refinement ownership has an invalid predicate shape".into(), + ); + } + } + ( + GeneratedOperation::NativeSuite { function: expected }, + ExpressionKind::Call { function, .. }, + ) if expected == function => { + let target = target.ok_or("generated native suite target is absent")?; + if !matches!( + target.identity.declaration.kind, + crate::DeclarationKind::Verify | crate::DeclarationKind::Property + ) || target.return_type != TypeInterner::NOTHING + || target.capture_count != 0 + { + return Err( + "generated native suite has no exact checked verify/property target" + .into(), + ); + } + } + ( + GeneratedOperation::EvaluatedValue { + intrinsic: expected, + }, + ExpressionKind::Intrinsic { + intrinsic, + type_arguments, + .. + }, + ) if expected == intrinsic => { + validate_evaluated_value_shape( + *intrinsic, + type_arguments, + args, + expression.ty, + &generated.arguments, + types, + )?; + } + ( + GeneratedOperation::ReflectionMetadata { + intrinsic: expected, + }, + ExpressionKind::Intrinsic { + intrinsic, + type_arguments, + args, + .. + }, + ) if *expected == IntrinsicId::TypeName + && expected == intrinsic + && args.is_empty() + && type_arguments.len() == 1 + && valid_type(types, type_arguments[0]) + && expression.ty == TypeInterner::STRING => {} + _ => { + return Err( + "generated call ownership has no closed operation/target contract".into(), + ); + } + } + for (index, argument) in generated.arguments.iter().enumerate() { + if let Some(target) = target { + let parameter = target + .params + .get(index) + .ok_or("generated physical function parameter is missing")?; + if argument.parameter_type != parameter.ty + || argument.callee_access != access(parameter.mode) + { + return Err( + "generated ownership differs from its physical function signature" + .into(), + ); + } + if args[index].ty != parameter.ty + && !(matches!( + generated.operation, + GeneratedOperation::Display { .. } + | GeneratedOperation::Equality { .. } + ) && has_outer_secret(types, argument.actual_type) + && secret_taint_matches(types, args[index].ty, parameter.ty)) + { + return Err( + "generated converted operand differs from its physical parameter" + .into(), + ); + } + } else if let ExpressionKind::IndirectCall { callee, .. } = &expression.kind { + let Type::Function { + params, + view_params, + .. + } = types.resolve(callee.ty) + else { + return Err("generated indirect ownership callee is not a function".into()); + }; + if params.get(index) != Some(&argument.parameter_type) + || params.get(index) != Some(&args[index].ty) + || view_params.get(index).copied().map(|view| { + if view { + CheckedCalleeAccess::View + } else { + CheckedCalleeAccess::Owned + } + }) != Some(argument.callee_access) + { + return Err( + "generated indirect ownership differs from its descriptor signature" + .into(), + ); + } + } + } + } + } + let declared_result = if let Some(target) = target { + Some(target.return_type) + } else if let ExpressionKind::IndirectCall { callee, .. } = &expression.kind { + match types.resolve(callee.ty) { + Type::Function { return_type, .. } => Some(*return_type), + _ => None, + } + } else { + None + }; + if let Some(declared) = declared_result { + let taint = match ownership { + CallOwnership::Source(source) => expression.ty == source.certificate.result_type, + CallOwnership::Generated(generated) => { + matches!( + generated.operation, + GeneratedOperation::Display { .. } | GeneratedOperation::Equality { .. } + ) && generated + .arguments + .iter() + .any(|argument| has_outer_secret(types, argument.actual_type)) + } + }; + if expression.ty != declared + && !(taint && secret_taint_matches(types, expression.ty, declared)) + { + return Err( + "call ownership physical result has no exact checked return/secret join".into(), + ); + } + } + Ok(()) +} + +fn access(mode: crate::ParamMode) -> CheckedCalleeAccess { + if mode == crate::ParamMode::View { + CheckedCalleeAccess::View + } else { + CheckedCalleeAccess::Owned + } +} + +fn function_lookup( + functions: &[crate::Function], + id: FunctionId, +) -> Result<&crate::Function, String> { + functions + .get(id.index() as usize) + .filter(|function| function.id == id) + .ok_or_else(|| "ownership physical function is outside its program".into()) +} + +fn validate_function_shape( + shape: &CheckedInvocationShape, + target: &crate::Function, + types: &TypeInterner, +) -> Result<(), String> { + let CheckedInvocationShape::Function { signature_type } = shape else { + return Err("source function ownership has an intrinsic shape".into()); + }; + let Type::Function { + params, + view_params, + return_type, + } = types.resolve(*signature_type) + else { + return Err("source function ownership signature is invalid".into()); + }; + if target.capture_count != 0 + || params.len() != target.params.len() + || *return_type != target.return_type + || target + .params + .iter() + .zip(params) + .zip(view_params) + .any(|((parameter, ty), view)| { + parameter.ty != *ty || (parameter.mode == crate::ParamMode::View) != *view + }) + { + return Err( + "source function ownership signature differs from the exact HIR declaration".into(), + ); + } + Ok(()) +} + +fn source_operand<'a>( + expression: &'a Expression, + argument: &ArgumentOwnership, + bridge: &CallBridge, + types: &TypeInterner, +) -> Result<&'a Expression, String> { + if expression.ty != argument.actual_type + && matches!( + bridge, + CallBridge::JsonSource { + intrinsic: IntrinsicId::JsonSerialize | IntrinsicId::JsonSerializePublic, + .. + } + ) + && jett_typecheck::ownership::is_implicitly_copyable(types, argument.actual_type) + { + return json_scalar_original(expression, argument, types); + } + let mut value = expression; + while value.span != argument.source_span { + value = match &value.kind { + ExpressionKind::View(inner) => inner, + _ => return Err("call ownership actual lost its original typed occurrence".into()), + }; + } + while value.ty != argument.actual_type { + if value.ty == argument.witness.occurrence_type + && has_outer_secret(types, argument.parameter_type) + && secret_taint_matches(types, value.ty, argument.actual_type) + { + break; + } + value = match &value.kind { + ExpressionKind::InterfaceCoerce { value, .. } + | ExpressionKind::FunctionAdapter { value, .. } => value, + _ => { + return Err("call ownership current operand has no typed conversion from the checked actual".into()); + } + }; + } + if value.span != argument.source_span { + return Err("call ownership actual lost its original typed occurrence".into()); + } + Ok(value) +} + +fn json_scalar_original<'a>( + expression: &'a Expression, + argument: &ArgumentOwnership, + types: &TypeInterner, +) -> Result<&'a Expression, String> { + let ExpressionKind::View(tree) = &expression.kind else { + return Err("source JSON scalar bridge requires a borrowed raw tree".into()); + }; + let ExpressionKind::EnumConstruct { + enum_type, + variant, + payloads, + evaluation_order, + } = &tree.kind + else { + return Err("source JSON scalar bridge lost its raw tree construction".into()); + }; + let Type::Enum(id) = types.resolve(*enum_type) else { + return Err("source JSON scalar bridge tree is not an enum".into()); + }; + let definition = types.resolve_enum(*id); + let variant = definition + .variants + .get(variant.index() as usize) + .ok_or("source JSON scalar bridge variant is invalid")?; + if definition.name != "json.JsonTree" + || tree.ty != *enum_type + || expression.ty != *enum_type + || payloads.len() != 1 + || evaluation_order != &[0] + || payloads[0].ty != TypeInterner::STRING + { + return Err("source JSON scalar bridge raw shape is invalid".into()); + } + let original = match (&payloads[0].kind, types.resolve(argument.actual_type)) { + (ExpressionKind::Clone(original), Type::String) if variant.name == "string_value" => { + original.as_ref() + } + ( + ExpressionKind::StringInterpolation(parts), + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64, + ) if variant.name == "number_value" => { + let [crate::StringSegment::Value(original)] = parts.as_slice() else { + return Err( + "source JSON numeric bridge must evaluate exactly one source value".into(), + ); + }; + original + } + _ => return Err("source JSON scalar bridge does not match the original primitive".into()), + }; + if original.ty != argument.actual_type || original.span != argument.source_span { + return Err("source JSON scalar bridge lost its exact original occurrence".into()); + } + Ok(original) +} + +/// Prove the original checked expression, its finite conversion and source place. +/// This accepts an original expression, never a lowered result slot by itself. +pub fn validate_source_operand<'a>( + expression: &'a Expression, + argument: &ArgumentOwnership, + bridge: &CallBridge, + locals: &[OwnershipLocalInfo], + types: &TypeInterner, +) -> Result<&'a Expression, String> { + let original = source_operand(expression, argument, bridge, types)?; + if let Some(backing) = validate_source_handled_operand(expression, argument, bridge, types)? + && !matches!(backing.kind, ExpressionKind::Handle { .. }) + { + return Err("call ownership original handled occurrence is no longer a Handle".into()); + } + validate_operand_origin(original, argument, locals, types)?; + Ok(original) +} + +fn source_view_backing(mut value: &Expression) -> &Expression { + while let ExpressionKind::View(inner) = &value.kind { + value = inner; + } + value +} + +/// Rejoin a privately retained Handle occurrence through its original finite +/// conversion. MIR must independently prove a lowered Local's complete CFG; +/// this occurrence check never grants an owner or manufactures a Binding. +pub fn validate_source_handled_operand<'a>( + expression: &'a Expression, + argument: &ArgumentOwnership, + bridge: &CallBridge, + types: &TypeInterner, +) -> Result, String> { + let Some(result) = argument.witness.handled_result else { + return Ok(None); + }; + if argument.origin != CallerOrigin::OwnedExpression { + return Err("call ownership handled occurrence is not an original source producer".into()); + } + let original = source_operand(expression, argument, bridge, types)?; + let backing = source_view_backing(original); + if backing.ty != result.ty || backing.span != result.span { + return Err( + "call ownership handled backing differs from its original typed occurrence".into(), + ); + } + Ok(Some(backing)) +} + +fn validate_operand_origin( + value: &Expression, + argument: &ArgumentOwnership, + locals: &[OwnershipLocalInfo], + types: &TypeInterner, +) -> Result<(), String> { + // This is only the exact checked SOURCE projection proof. The runtime tree + // and any nested call's sealed promoted result certificate stay unchanged. + let raw_occurrence = if value.ty != argument.actual_type + && value.ty == argument.witness.occurrence_type + && has_outer_secret(types, argument.parameter_type) + && secret_taint_matches(types, value.ty, argument.actual_type) + { + let mut raw = value.clone(); + raw.ty = argument.actual_type; + let mut place = &mut raw; + while let ExpressionKind::View(inner) = &mut place.kind { + if inner.ty == argument.witness.occurrence_type { + inner.ty = argument.actual_type; + } + place = inner; + } + Some(raw) + } else { + None + }; + let value = raw_occurrence.as_ref().unwrap_or(value); + let mut leaf = value; + loop { + leaf = match &leaf.kind { + ExpressionKind::View(inner) + | ExpressionKind::Coarsen(inner) + | ExpressionKind::Declassify(inner) + | ExpressionKind::RefinementValidated(inner) + | ExpressionKind::InterfaceCoerce { value: inner, .. } + | ExpressionKind::FunctionAdapter { value: inner, .. } => inner, + _ => break, + }; + } + match &argument.origin { + CallerOrigin::Binding(fact) => { + let checked_state_narrowing = value.ty == leaf.ty + && leaf.ty == argument.actual_type + && leaf.ty == argument.witness.occurrence_type + && machine_state_parent_matches(types, leaf.ty, fact.ty); + if !matches!(leaf.kind, ExpressionKind::Local(local) if local == fact.local) + || (leaf.ty != fact.ty && !checked_state_narrowing) + { + return Err( + "call ownership binding does not match its actual typed operand".into(), + ); + } + } + CallerOrigin::BorrowedProjection { source } + | CallerOrigin::OwnedFieldCopy { parent: source } => { + if !matches!(leaf.kind, ExpressionKind::Field { .. }) + || immediate_source(leaf) != *source + { + return Err( + "call ownership projection does not match its immediate checked source".into(), + ); + } + if let CallerViewSource::Local(id) = source { + let parent = locals + .get(id.index() as usize) + .ok_or("call projection source is outside its function")?; + if let Some(root) = argument.witness.projection_root { + validate_flow_projection_operand(value, *id, parent.ty, root, types)?; + } else { + crate::validate_local_view_initializer(value, *id, parent.ty, value.ty, types) + .map_err(str::to_string)?; + } + } else { + let mut field = leaf; + while let ExpressionKind::Field { base, .. } = &field.kind { + crate::local_views::validate_field_projection(field, types) + .map_err(str::to_string)?; + field = base; + } + } + } + CallerOrigin::OwnedExpression => { + if matches!( + leaf.kind, + ExpressionKind::Local(_) | ExpressionKind::Field { .. } + ) { + return Err("call ownership cannot manufacture a producer acquisition from a binding or field".into()); + } + } + } + Ok(()) +} + +fn validate_flow_projection_operand( + value: &Expression, + source: LocalId, + storage_type: TypeId, + root: SourceProjectionRoot, + types: &TypeInterner, +) -> Result<(), String> { + if root.local != source + || root.storage_type != storage_type + || !flow_projection_root_matches(types, root.occurrence_type, storage_type) + { + return Err("call ownership flow projection changed its declared root storage".into()); + } + // Every existing field/wrapper/endpoint check remains strict. Only this + // privately certified source occurrence supplies its exact flowed root type. + crate::validate_local_view_initializer(value, source, root.occurrence_type, value.ty, types) + .map_err(str::to_string)?; + let mut terminal = value; + loop { + terminal = match &terminal.kind { + ExpressionKind::Field { base, .. } => base, + ExpressionKind::View(inner) + | ExpressionKind::Coarsen(inner) + | ExpressionKind::Declassify(inner) => inner, + ExpressionKind::InterfaceCoerce { value, adapters } if adapters.is_empty() => value, + ExpressionKind::Local(id) + if *id == root.local + && terminal.span == root.span + && terminal.ty == root.occurrence_type => + { + return Ok(()); + } + _ => { + return Err( + "call ownership flow projection changed its exact source occurrence".into(), + ); + } + }; + } +} + +fn flow_projection_root_matches(types: &TypeInterner, occurrence: TypeId, storage: TypeId) -> bool { + if !valid_type(types, occurrence) || !valid_type(types, storage) { + return false; + } + let (Type::MachineState { machine, state }, Type::Machine(parent)) = + (types.resolve(occurrence), types.resolve(storage)) + else { + return false; + }; + machine == parent && types.resolve_machine(*machine).state(*state).is_some() +} + +pub fn validate_generated_operand_tree( + value: &Expression, + argument: &GeneratedArgumentOwnership, + locals: &[OwnershipLocalInfo], +) -> Result<(), String> { + if !generated_tuple_matches(argument) { + return Err("generated operand differs from its sealed acquisition witness".into()); + } + let mut original = value; + // Physical View wrapping is separate from the original generated operand. + if !argument.witness.written_view + && argument.callee_access == CheckedCalleeAccess::View + && let ExpressionKind::View(inner) = &original.kind + && original.span == inner.span + && original.ty == inner.ty + { + original = inner; + } + // A canonical conversion keeps the physical occurrence header, but may + // allocate an adapter around the sealed raw parameter. Rejoin its backing + // only after the existing exact-actual-type conversion walk below. + if original.span != argument.witness.source_span { + return Err( + "generated operand differs from its original typed occurrence or backing".into(), + ); + } + while original.ty != argument.actual_type { + original = match &original.kind { + ExpressionKind::InterfaceCoerce { value, .. } + | ExpressionKind::FunctionAdapter { value, .. } => value, + _ => return Err("generated operand has no checked actual/conversion type".into()), + }; + } + if original.span != argument.witness.source_span + || original_view(original) != argument.witness.written_view + || immediate_source(original) != argument.witness.origin + { + return Err( + "generated operand differs from its original typed occurrence or backing".into(), + ); + } + argument.witness.validate_producer_shape(original)?; + match argument.acquisition { + GeneratedAcquisition::Borrow { source } if immediate_source(original) != source => { + return Err("generated borrow origin differs from its operand".into()); + } + GeneratedAcquisition::OwnedExpression => { + if original_view(original) { + return Err("generated owner cannot acquire an explicit borrowed operand".into()); + } + if let CallerViewSource::Local(id) = immediate_source(original) { + let local = locals + .get(id.index() as usize) + .ok_or("generated operand source is outside its function")?; + if local.view_source.is_some() + || local.is_view_parameter + || local.view_iteration.is_some() + { + return Err("generated owner cannot acquire a borrowed binding".into()); + } + } + } + _ => {} + } + Ok(()) +} + +fn metadata_operation(source: &SourceCallOwnership) -> Option { + match (&source.target, &source.bridge) { + (CallTarget::Intrinsic(id), CallBridge::Direct) + if matches!( + id, + IntrinsicId::TypeFieldValue + | IntrinsicId::TypeVariantFieldValue + | IntrinsicId::TypeMachineFieldValue + | IntrinsicId::TypeVariantValue + | IntrinsicId::TypeMachineStateValue + | IntrinsicId::TypeArg + ) => + { + Some(*id) + } + _ => None, + } +} + +fn canonical_metadata_enum(types: &TypeInterner, ty: TypeId) -> bool { + if !valid_type(types, ty) { + return false; + } + let Type::Enum(id) = types.resolve(ty) else { + return false; + }; + let definition = types.resolve_enum(*id); + let names: &[&str] = match definition.name.as_str() { + "TypeKind" => &[ + "primitive_type", + "alias_type", + "refinement_type", + "struct_type", + "bitfield_type", + "enum_type", + "list_type", + "set_type", + "map_type", + "optional_type", + "result_type", + "secret_type", + "function_type", + "machine_type", + "machine_state_type", + "unknown_type", + "resource_type", + ], + "TypePrimitive" => &[ + "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", + "type_construction_type", + "unknown_type", + ], + _ => return false, + }; + definition.variants.len() == names.len() + && definition + .variants + .iter() + .zip(names) + .enumerate() + .all(|(index, (variant, name))| { + variant.name == *name + && variant.fields.is_empty() + && variant.discriminant == index as i64 + }) +} + +fn canonical_metadata_type(types: &TypeInterner, ty: TypeId) -> bool { + fn visit( + types: &TypeInterner, + ty: TypeId, + seen: &mut std::collections::HashSet, + ) -> bool { + if !valid_type(types, ty) || !types.nominal_type_arguments(ty).is_empty() { + return false; + } + if !seen.insert(ty) { + return true; + } + match types.resolve(ty) { + Type::Int64 | Type::String | Type::Bool => true, + Type::Optional(inner) | Type::List(inner) => visit(types, *inner, seen), + Type::Enum(_) => canonical_metadata_enum(types, ty), + Type::Struct(id) => { + let definition = types.resolve_struct(*id); + if !definition.methods.is_empty() { + return false; + } + let fields = &definition.fields; + let names: &[&str] = match definition.name.as_str() { + "TypeInfo" => &[ + "type_name", + "kind", + "kind_tag", + "primitive_tag", + "has_secret", + "args", + ], + "TypeField" => &[ + "index", + "owner_type", + "owner_member", + "name", + "type_name", + "kind", + "kind_tag", + "serialize_name", + "has_secret", + "type_info", + ], + "TypeVariant" => &[ + "index", + "owner_type", + "name", + "discriminant", + "has_secret", + "fields", + ], + "TypeMachineState" => &["index", "owner_type", "name", "has_secret", "fields"], + _ => return false, + }; + if !fields + .iter() + .map(|(name, _)| name.as_str()) + .eq(names.iter().copied()) + { + return false; + } + let field = |index: usize| fields[index].1; + let named = |candidate: TypeId, name: &str| { + valid_type(types, candidate) + && match types.resolve(candidate) { + Type::Struct(id) => types.resolve_struct(*id).name == name, + Type::Enum(id) => types.resolve_enum(*id).name == name, + _ => false, + } + }; + let optional = |candidate: TypeId, payload: TypeId| { + valid_type(types, candidate) + && matches!(types.resolve(candidate), Type::Optional(inner) if *inner == payload) + }; + let list_named = |candidate: TypeId, name: &str| { + valid_type(types, candidate) + && matches!(types.resolve(candidate), Type::List(inner) if named(*inner, name)) + }; + let shape = match definition.name.as_str() { + "TypeInfo" => { + field(0) == TypeInterner::STRING + && field(1) == TypeInterner::STRING + && named(field(2), "TypeKind") + && valid_type(types, field(3)) + && matches!(types.resolve(field(3)), Type::Optional(inner) if named(*inner, "TypePrimitive")) + && field(4) == TypeInterner::BOOL + && valid_type(types, field(5)) + && matches!(types.resolve(field(5)), Type::List(inner) if *inner == ty) + } + "TypeField" => { + field(0) == TypeInterner::INT64 + && [1, 3, 4, 5, 7] + .into_iter() + .all(|index| field(index) == TypeInterner::STRING) + && optional(field(2), TypeInterner::STRING) + && named(field(6), "TypeKind") + && field(8) == TypeInterner::BOOL + && named(field(9), "TypeInfo") + } + "TypeVariant" => { + field(0) == TypeInterner::INT64 + && field(1) == TypeInterner::STRING + && field(2) == TypeInterner::STRING + && field(3) == TypeInterner::INT64 + && field(4) == TypeInterner::BOOL + && list_named(field(5), "TypeField") + } + "TypeMachineState" => { + field(0) == TypeInterner::INT64 + && field(1) == TypeInterner::STRING + && field(2) == TypeInterner::STRING + && field(3) == TypeInterner::BOOL + && list_named(field(4), "TypeField") + } + _ => false, + }; + shape && fields.iter().all(|(_, child)| visit(types, *child, seen)) + } + _ => false, + } + } + visit(types, ty, &mut std::collections::HashSet::new()) +} + +fn metadata_root_matches(source: &SourceCallOwnership, ty: TypeId, types: &TypeInterner) -> bool { + let Some(operation) = metadata_operation(source) else { + return false; + }; + if !canonical_metadata_type(types, ty) { + return false; + } + let Type::Struct(id) = types.resolve(ty) else { + return false; + }; + let expected = match operation { + IntrinsicId::TypeFieldValue + | IntrinsicId::TypeVariantFieldValue + | IntrinsicId::TypeMachineFieldValue => "TypeField", + IntrinsicId::TypeVariantValue => "TypeVariant", + IntrinsicId::TypeMachineStateValue => "TypeMachineState", + IntrinsicId::TypeArg => "TypeInfo", + _ => return false, + }; + types.resolve_struct(*id).name == expected + && match (&source.shape, operation) { + ( + CheckedInvocationShape::Intrinsic { operands, .. }, + IntrinsicId::TypeFieldValue + | IntrinsicId::TypeVariantFieldValue + | IntrinsicId::TypeMachineFieldValue, + ) => operands.get(1).is_some_and(|role| role.ty() == ty), + (CheckedInvocationShape::Intrinsic { result_type, .. }, _) => *result_type == ty, + _ => false, + } +} + +/// Compiler metadata is a closed constant construction, never a borrowed or +/// cloned source payload. MIR must reconstruct a lowered producer before use. +pub fn validate_compiler_metadata_operand( + value: &Expression, + source: &SourceCallOwnership, + types: &TypeInterner, +) -> Result<(), String> { + fn constant(value: &Expression, types: &TypeInterner) -> bool { + if !canonical_metadata_type(types, value.ty) { + return false; + } + match (&value.kind, types.resolve(value.ty)) { + (ExpressionKind::Int(_), Type::Int64) + | (ExpressionKind::String(_), Type::String) + | (ExpressionKind::Bool(_), Type::Bool) => true, + (ExpressionKind::OptionalNone, Type::Optional(_)) => true, + (ExpressionKind::OptionalSome(value), Type::Optional(inner)) => { + value.ty == *inner && constant(value, types) + } + (ExpressionKind::ListConstruct { elements }, Type::List(inner)) => elements + .iter() + .all(|value| value.ty == *inner && constant(value, types)), + ( + ExpressionKind::EnumConstruct { + enum_type, + variant, + payloads, + evaluation_order, + }, + Type::Enum(id), + ) => { + *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.fields.is_empty()) + } + ( + ExpressionKind::StructConstruct { + struct_type, + fields, + evaluation_order, + validates_refinements, + refinement_predicates, + }, + Type::Struct(id), + ) => { + let definition = types.resolve_struct(*id); + *struct_type == value.ty + && !*validates_refinements + && refinement_predicates.is_empty() + && evaluation_order.iter().copied().eq(0..fields.len()) + && fields.len() == definition.fields.len() + && fields + .iter() + .zip(&definition.fields) + .all(|(value, (_, ty))| value.ty == *ty && constant(value, types)) + } + _ => false, + } + } + if metadata_root_matches(source, value.ty, types) && constant(value, types) { + Ok(()) + } else { + Err( + "call ownership compiler metadata tail has no canonical owned construction proof" + .into(), + ) + } +} + +fn secret_taint_matches(types: &TypeInterner, mut actual: TypeId, declared: TypeId) -> bool { + if !valid_type(types, actual) || !valid_type(types, declared) { + return false; + } + for _ in 0..types.len() { + if actual == declared { + return true; + } + let Type::Secret(inner) = types.resolve(actual) else { + return false; + }; + actual = *inner; + } + false +} + +fn has_outer_secret(types: &TypeInterner, ty: TypeId) -> bool { + valid_type(types, ty) && matches!(types.resolve(ty), Type::Secret(_)) +} + +/// Locate only the first Secret boundary behind nominal refinement bases. +/// This is metadata for an exact checked actual, not refinement coarsening. +fn nominal_secret_payload(types: &TypeInterner, mut actual: TypeId) -> Option { + if !valid_type(types, actual) || !matches!(types.resolve(actual), Type::Refinement { .. }) { + return None; + } + for _ in 0..types.len() { + if !valid_type(types, actual) { + return None; + } + match types.resolve(actual) { + Type::Refinement { base, .. } => actual = *base, + Type::Secret(inner) => return valid_type(types, *inner).then_some(*inner), + _ => return None, + } + } + None +} + +// The checker admits only this one-way nominal state erasure. No state is +// recovered or substituted, and the checked actual occurrence stays exact. +fn machine_state_parent_matches(types: &TypeInterner, actual: TypeId, declared: TypeId) -> bool { + if !valid_type(types, actual) || !valid_type(types, declared) { + return false; + } + matches!( + (types.resolve(actual), types.resolve(declared)), + (Type::MachineState { machine, .. }, Type::Machine(parent)) if machine == parent + ) +} + +fn source_parameter_matches( + argument: &ArgumentOwnership, + physical: TypeId, + declared: TypeId, + types: &TypeInterner, +) -> bool { + physical == declared + || (physical == argument.actual_type + && physical == argument.witness.occurrence_type + && declared == argument.parameter_type + && machine_state_parent_matches(types, physical, declared)) + || (has_outer_secret(types, argument.actual_type) + && secret_taint_matches(types, physical, declared)) + || (physical == argument.actual_type + && physical == argument.witness.occurrence_type + && declared == argument.parameter_type + && nominal_secret_payload(types, physical) == Some(declared)) + || (physical == argument.actual_type + && physical == argument.witness.occurrence_type + && declared == argument.parameter_type + && has_outer_secret(types, declared) + && secret_taint_matches(types, declared, physical)) +} + +fn evaluated_key_type(types: &TypeInterner, mut ty: TypeId) -> bool { + let mut seen = std::collections::HashSet::new(); + while valid_type(types, ty) && seen.insert(ty) { + match types.resolve(ty) { + Type::Refinement { base, .. } => ty = *base, + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::String + | Type::Bool => return true, + _ => return false, + } + } + false +} + +fn validate_evaluated_value_shape( + intrinsic: IntrinsicId, + type_arguments: &[TypeId], + args: &[Expression], + result: TypeId, + arguments: &[GeneratedArgumentOwnership], + types: &TypeInterner, +) -> Result<(), String> { + if type_arguments.iter().any(|&ty| !valid_type(types, ty)) || !valid_type(types, result) { + return Err("generated evaluated value has an unknown type argument or result".into()); + } + let construction_result = || { + matches!(types.resolve(result), Type::Result(ok, error) + if *ok == TypeInterner::TYPE_CONSTRUCTION && *error == TypeInterner::STRING) + }; + let metadata = |ty: TypeId, name: &str| { + valid_type(types, ty) + && canonical_metadata_type(types, ty) + && matches!(types.resolve(ty), Type::Struct(id) if types.resolve_struct(*id).name == name) + }; + let construction_owner = |ty: TypeId| { + matches!( + types.resolve(ty), + Type::Struct(_) + | Type::Bitfield(_) + | Type::Enum(_) + | Type::Machine(_) + | Type::MachineState { .. } + ) + }; + let shape = match intrinsic { + IntrinsicId::SetNew => { + type_arguments.len() == 1 + && args.is_empty() + && evaluated_key_type(types, type_arguments[0]) + && matches!(types.resolve(result), Type::Set(inner) if *inner == type_arguments[0]) + } + IntrinsicId::SetAdd => { + type_arguments.len() == 1 + && args.len() == 2 + && evaluated_key_type(types, type_arguments[0]) + && args[0].ty == result + && args[1].ty == type_arguments[0] + && matches!(types.resolve(result), Type::Set(inner) if *inner == type_arguments[0]) + } + IntrinsicId::BytesNew => { + type_arguments.is_empty() && args.is_empty() && result == TypeInterner::BYTES + } + IntrinsicId::BytesFromHex => { + type_arguments.is_empty() + && args.len() == 1 + && args[0].ty == TypeInterner::STRING + && matches!(types.resolve(result), Type::Result(ok, error) if *ok == TypeInterner::BYTES && *error == TypeInterner::STRING) + } + IntrinsicId::TypeConstructStart => { + type_arguments.len() == 1 + && args.is_empty() + && construction_owner(type_arguments[0]) + && result == TypeInterner::TYPE_CONSTRUCTION + } + IntrinsicId::TypeConstructVariantStart => { + type_arguments.len() == 1 + && args.len() == 1 + && matches!(types.resolve(type_arguments[0]), Type::Enum(_)) + && metadata(args[0].ty, "TypeVariant") + && construction_result() + } + IntrinsicId::TypeConstructMachineStart => { + type_arguments.len() == 1 + && args.len() == 1 + && matches!( + types.resolve(type_arguments[0]), + Type::Machine(_) | Type::MachineState { .. } + ) + && metadata(args[0].ty, "TypeMachineState") + && construction_result() + } + IntrinsicId::TypeConstructPut => { + type_arguments.len() == 2 + && args.len() == 3 + && construction_owner(type_arguments[0]) + && args[0].ty == TypeInterner::TYPE_CONSTRUCTION + && metadata(args[1].ty, "TypeField") + && args[2].ty == type_arguments[1] + && construction_result() + } + _ => false, + }; + if !shape { + return Err("generated evaluated value has no closed intrinsic/type/result shape".into()); + } + for (index, argument) in arguments.iter().enumerate() { + let access = jett_typecheck::intrinsic_operand_access(intrinsic, index, args.len()) + .ok_or("generated evaluated value operand has no closed access")?; + if argument.callee_access != access || argument.parameter_type != args[index].ty { + return Err( + "generated evaluated value ownership differs from its closed physical operand" + .into(), + ); + } + } + Ok(()) +} diff --git a/crates/jett_hir/src/call_ownership_tests.rs b/crates/jett_hir/src/call_ownership_tests.rs new file mode 100644 index 00000000..46ac0a78 --- /dev/null +++ b/crates/jett_hir/src/call_ownership_tests.rs @@ -0,0 +1,3366 @@ +//! Source-derived caller-packet corruption controls. Not executed in this draft. +use super::*; +use jett_diagnostics::Severity; + +const NAMED: &str = r#"function observe(view first: list[int64], view second: list[int64]) returns int64: + return 7 +function alternate(view first: list[int64], view second: list[int64]) returns int64: + return 9 +function exercise(left: list[int64], right: list[int64], unrelated: list[string]) returns int64: + return observe(second: right, first: left) +"#; + +fn checked_source(source: &str) -> (Program, CheckResult) { + checked_source_with_suites(source, false) +} + +fn checked_source_with_suites(source: &str, include_suites: bool) -> (Program, CheckResult) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "parse: {:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "resolve: {:?}", + resolved.diagnostics, + ); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "check: {:?}", + checked.diagnostics, + ); + let origins = HashMap::from([(file, SourceOrigin::Project)]); + let lowered = if include_suites { + lower_with_test_bodies(&parsed.module, &resolved, &checked, &origins) + } else { + lower(&parsed.module, &resolved, &checked, &origins) + }; + let program = lowered.expect("source lowers"); + validate_program_call_ownership(&program, &checked.interner) + .expect("original packet validates"); + (program, checked) +} + +fn exercise_index(program: &Program) -> usize { + program + .functions + .iter() + .position(|function| function.identity.declaration.name == "exercise") + .expect("source exercise") +} + +fn returned_call(function: &Function) -> &Expression { + function + .body + .statements + .iter() + .find_map(|statement| match &statement.kind { + StatementKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("returned source call") +} + +fn returned_call_mut(function: &mut Function) -> &mut Expression { + function + .body + .statements + .iter_mut() + .find_map(|statement| match &mut statement.kind { + StatementKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("returned source call") +} + +fn source_packet_mut(expression: &mut Expression) -> &mut SourceCallOwnership { + let packet = match &mut expression.kind { + ExpressionKind::Call { ownership, .. } + | ExpressionKind::IndirectCall { ownership, .. } + | ExpressionKind::Intrinsic { ownership, .. } => ownership, + _ => panic!("source invocation"), + }; + let CallOwnership::Source(source) = packet else { + panic!("checked source packet"); + }; + source +} + +fn local_id(function: &Function, name: &str) -> LocalId { + function + .locals + .iter() + .find(|local| local.name == name) + .expect("source local") + .id +} + +fn rejects(program: &Program, checked: &CheckResult, case: &str) { + let errors = validate_program_call_ownership(program, &checked.interner) + .expect_err("corrupted checked invocation must be refused"); + assert!( + errors.iter().any(|error| error.message.contains("call ownership") + || error.message.contains("caller") || error.message.contains("invocation") + || (case == "source-backed tail" && matches!(error.message.as_str(), + "generated owner cannot acquire a borrowed binding" + | "generated operand differs from its original typed occurrence or backing"))), + "{case}: {errors:?}", + ); +} + +#[test] +fn caller_packets_preserve_named_order_and_borrowed_source_authority() { + let (program, checked) = checked_source(NAMED); + let function = &program.functions[exercise_index(&program)]; + let ExpressionKind::Call { + args, + evaluation_order, + ownership, + .. + } = &returned_call(function).kind + else { + panic!("direct named invocation"); + }; + let CallOwnership::Source(source) = ownership else { + panic!("source packet"); + }; + assert_eq!(evaluation_order, &[1, 0]); + assert_eq!( + source + .arguments + .iter() + .map(|argument| argument.source_index) + .collect::>(), + [1, 0] + ); + assert!(source.arguments.iter().all(|argument| { + argument.effect == jett_typecheck::CheckedCallerEffect::RelinquishOwned + && argument.syntax == jett_typecheck::CheckedCallerSyntax::Bare + && argument.staging == ArgumentStaging::Original + })); + assert!( + matches!(args[0].kind, ExpressionKind::Local(local) if local == local_id(function, "left")) + ); + assert!( + matches!(args[1].kind, ExpressionKind::Local(local) if local == local_id(function, "right")) + ); + assert!(matches!( + checked.interner.resolve(source.arguments[0].actual_type), + Type::List(_) + )); + + let source = "function observe(view value: list[int64]) returns int64:\n return 7\nfunction exercise(view value: list[int64]) returns int64:\n return observe(view value)\n"; + let (program, _) = checked_source(source); + let expression = returned_call(&program.functions[exercise_index(&program)]); + let ExpressionKind::Call { + ownership: CallOwnership::Source(source), + .. + } = &expression.kind + else { + panic!("retained source invocation"); + }; + let argument = &source.arguments[0]; + assert_eq!( + argument.effect, + jett_typecheck::CheckedCallerEffect::RetainBorrow + ); + assert!(matches!( + argument.origin, + CallerOrigin::Binding(CallerBindingFact { + mode: CallerBindingMode::View { + source: CallerViewSource::Other + }, + .. + }) + )); +} + +#[test] +fn caller_packets_reject_count_order_and_source_fact_corruption() { + for case in [ + "count", + "order", + "source index", + "effect", + "syntax", + "context", + "origin", + ] { + let (mut program, checked) = checked_source(NAMED); + let index = exercise_index(&program); + let expression = returned_call_mut(&mut program.functions[index]); + if case == "order" { + let ExpressionKind::Call { + evaluation_order, .. + } = &mut expression.kind + else { + unreachable!(); + }; + *evaluation_order = vec![0, 1]; + } else { + let packet = source_packet_mut(expression); + match case { + "count" => { + packet.arguments.pop(); + } + "source index" => packet.arguments[0].source_index = 0, + "effect" => { + packet.arguments[0].effect = jett_typecheck::CheckedCallerEffect::RetainBorrow + } + "syntax" => { + packet.arguments[0].syntax = jett_typecheck::CheckedCallerSyntax::WrittenView + } + "context" => packet.context = jett_typecheck::CheckedOwnershipContext::Verify, + "origin" => packet.arguments[0].origin = CallerOrigin::OwnedExpression, + _ => unreachable!(), + } + } + rejects(&program, &checked, case); + } +} + +#[test] +fn caller_packets_bind_the_executed_target_and_complete_signature() { + for case in ["physical target", "packet target", "signature"] { + let (mut program, mut checked) = checked_source(NAMED); + let alternate = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "alternate") + .unwrap() + .id; + let index = exercise_index(&program); + let expression = returned_call_mut(&mut program.functions[index]); + if case == "physical target" { + let ExpressionKind::Call { function, .. } = &mut expression.kind else { + unreachable!(); + }; + *function = alternate; + } else { + let packet = source_packet_mut(expression); + if case == "packet target" { + packet.target = CallTarget::Function(alternate); + } else { + let ty = packet.arguments[0].parameter_type; + packet.shape = jett_typecheck::CheckedInvocationShape::Function { + signature_type: checked.interner.intern(Type::Function { + params: vec![ty, ty], + view_params: vec![true, true], + return_type: TypeInterner::BOOL, + }), + }; + } + } + rejects(&program, &checked, case); + } +} + +#[test] +fn caller_packets_require_the_original_operand_and_exact_local_metadata() { + for case in [ + "different same-type owner", + "different type", + "local metadata", + "foreign local", + ] { + let (mut program, checked) = checked_source(NAMED); + let index = exercise_index(&program); + let function = &mut program.functions[index]; + let left = local_id(function, "left"); + let right = local_id(function, "right"); + let unrelated = local_id(function, "unrelated"); + if case == "local metadata" { + let replacement = function.locals[unrelated.index() as usize].ty; + function.locals[left.index() as usize].ty = replacement; + } else { + let expression = returned_call_mut(function); + let ExpressionKind::Call { args, .. } = &mut expression.kind else { + unreachable!(); + }; + args[0].kind = ExpressionKind::Local(match case { + "different same-type owner" => right, + "different type" => unrelated, + "foreign local" => LocalId::new(u32::MAX), + _ => unreachable!(), + }); + } + rejects(&program, &checked, case); + } +} + +#[test] +fn caller_packets_cannot_import_mir_loan_staging_into_original_hir() { + let (mut program, checked) = checked_source(NAMED); + let index = exercise_index(&program); + let owner = local_id(&program.functions[index], "left"); + let loan = local_id(&program.functions[index], "right"); + let expression = returned_call_mut(&mut program.functions[index]); + let ExpressionKind::Call { ownership, .. } = &mut expression.kind else { + unreachable!(); + }; + ownership + .stage_parameter(0, Some(owner), loan) + .expect("typed MIR stage mutation"); + rejects(&program, &checked, "MIR stage in original HIR"); +} + +#[test] +fn caller_packets_preserve_safe_secret_promotion_but_reject_forged_operands() { + let source = "function observe(view value: secret[list[int64]]) returns int64:\n return 7\nfunction exercise(value: list[int64]) returns int64:\n return observe(value)\n"; + let (mut program, checked) = checked_source(source); + let index = exercise_index(&program); + let expression = returned_call_mut(&mut program.functions[index]); + let ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + .. + } = &mut expression.kind + else { + panic!("source promoted operand"); + }; + assert_ne!( + packet.arguments[0].actual_type, + packet.arguments[0].parameter_type + ); + assert!(matches!( + checked.interner.resolve(packet.arguments[0].parameter_type), + Type::Secret(_) + )); + args[0].ty = TypeInterner::INT64; + rejects(&program, &checked, "invented promoted operand type"); +} + +const REFLECTED_METADATA_SOURCE: &str = r#"namespace app +struct Record: + value: int64 +function exercise(view value: Record, view field: TypeField) returns int64: + return type.field_value[Record, int64](view value, view field) +"#; + +#[test] +fn caller_packets_preserve_canonical_owned_metadata_and_reject_tail_corruption() { + let (program, checked) = checked_source(REFLECTED_METADATA_SOURCE); + let expression = returned_call(&program.functions[exercise_index(&program)]); + let ExpressionKind::Intrinsic { + args, + ownership: CallOwnership::Source(source), + .. + } = &expression.kind + else { + panic!("reflected source intrinsic"); + }; + assert_eq!(source.arguments.len(), 2); + assert_eq!(source.generated_operands.len(), 1); + assert_eq!(args.len(), 3); + assert_eq!( + source.generated_operands[0].acquisition, + GeneratedAcquisition::OwnedExpression + ); + assert!(!jett_typecheck::ownership::is_implicitly_copyable( + &checked.interner, + args[2].ty + )); + validate_compiler_metadata_operand(&args[2], source, &checked.interner) + .expect("canonical compiler metadata"); + + for case in [ + "tail acquisition", + "source-backed tail", + "metadata schema", + "tail evaluation order", + ] { + let (mut program, mut checked) = checked_source(REFLECTED_METADATA_SOURCE); + let index = exercise_index(&program); + let field = local_id(&program.functions[index], "field"); + let expression = returned_call_mut(&mut program.functions[index]); + let ExpressionKind::Intrinsic { + args, + ownership: CallOwnership::Source(source), + evaluation_order, + .. + } = &mut expression.kind + else { + panic!("reflected source intrinsic"); + }; + match case { + "tail acquisition" => { + source.generated_operands[0].acquisition = GeneratedAcquisition::Copy + } + "source-backed tail" => args[2].kind = ExpressionKind::Local(field), + "metadata schema" => { + let Type::Struct(id) = *checked.interner.resolve(args[2].ty) else { + panic!("metadata struct"); + }; + let mut definition = checked.interner.resolve_struct(id).clone(); + definition.fields[0].1 = TypeInterner::STRING; + checked.interner.update_struct(id, definition); + } + "tail evaluation order" => *evaluation_order = vec![2, 0, 1], + _ => unreachable!(), + } + rejects(&program, &checked, case); + } +} + +#[test] +fn caller_packets_seal_checked_results_and_exact_physical_operands() { + for case in ["source result", "converted operand"] { + let (mut program, checked) = checked_source(NAMED); + let index = exercise_index(&program); + let expression = returned_call_mut(&mut program.functions[index]); + if case == "source result" { + expression.ty = TypeInterner::BOOL; + } else { + let ExpressionKind::Call { args, .. } = &mut expression.kind else { + panic!("source call"); + }; + args[0].ty = TypeInterner::STRING; + } + rejects(&program, &checked, case); + } + + let source = "function exercise(callback: function(view list[int64]) returns int64, value: list[int64]) returns int64:\n return callback(value)\n"; + let (mut program, checked) = checked_source(source); + let index = exercise_index(&program); + let expression = returned_call_mut(&mut program.functions[index]); + let ExpressionKind::IndirectCall { args, .. } = &mut expression.kind else { + panic!("source indirect call"); + }; + args[0].ty = TypeInterner::STRING; + rejects(&program, &checked, "indirect converted operand"); +} + +#[test] +fn caller_packets_preserve_checked_contextual_secret_producer_results() { + let source = "function produce() returns list[int64]:\n return list(1)\nfunction exercise() returns secret[list[int64]]:\n return produce()\n"; + let (mut program, checked) = checked_source(source); + let index = exercise_index(&program); + let expression = returned_call_mut(&mut program.functions[index]); + assert!( + matches!(checked.interner.resolve(expression.ty), Type::Secret(inner) + if matches!(checked.interner.resolve(*inner), Type::List(_))) + ); + expression.ty = TypeInterner::STRING; + rejects(&program, &checked, "forged contextual producer result"); +} + +#[test] +fn caller_packets_separate_raw_and_qualified_source_occurrences() { + let sources = [ + "function observe(view value: secret[list[int64]]) returns int64:\n return 7\nfunction exercise(value: list[int64]) returns int64:\n return observe(value)\n", + "struct Packet:\n items: list[int64]\nfunction observe(view value: secret[list[int64]]) returns int64:\n return 7\nfunction exercise(value: Packet) returns int64:\n return observe(value.items)\n", + "struct Packet:\n items: list[int64]\nfunction observe(view value: secret[list[int64]]) returns int64:\n return 7\nfunction exercise(value: Packet) returns int64:\n return observe(view value.items)\n", + "function produce() returns list[int64]:\n return list(1)\nfunction observe(view value: secret[list[int64]]) returns int64:\n return 7\nfunction exercise() returns int64:\n return observe(produce())\n", + "function produce() returns list[int64]:\n return list(1)\nfunction observe(view value: secret[list[int64]]) returns int64:\n return 7\nfunction exercise() returns int64:\n return observe(view produce())\n", + "function observe(view value: secret[list[int64]]) returns int64:\n return 7\nfunction exercise(value: list[int64]) returns int64:\n return value into observe()\n", + "function observe(view value: secret[list[int64]]) returns int64:\n return 7\nfunction exercise(value: list[int64]) returns int64:\n return observe(view value)\n", + ]; + for source in sources { + let (mut program, checked) = checked_source(source); + let index = exercise_index(&program); + let expression = returned_call_mut(&mut program.functions[index]); + let ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + .. + } = &mut expression.kind + else { + panic!("qualified source call"); + }; + let argument = &packet.arguments[0]; + assert!(matches!( + checked.interner.resolve(argument.actual_type), + Type::List(_) + )); + assert!( + matches!(checked.interner.resolve(argument.parameter_type), Type::Secret(inner) + if *inner == argument.actual_type) + ); + assert_eq!( + checked.source_type_map.get(&argument.source_span), + Some(&argument.actual_type) + ); + assert!( + argument.source_witness().occurrence_type() == argument.actual_type + || matches!(checked.interner.resolve(argument.source_witness().occurrence_type()), Type::Secret(inner) + if *inner == argument.actual_type) + ); + args[0].ty = TypeInterner::STRING; + rejects(&program, &checked, "forged qualified occurrence"); + } +} + +#[test] +fn caller_packets_keep_pipeline_input_separate_from_field_callee_occurrences() { + let sources = [ + "function increment(value: int64) returns int64:\n return value + 1\nstruct Holder:\n callback: function(int64) returns int64\nfunction exercise() returns int64:\n Holder holder = Holder(callback: increment)\n return 4 into holder.callback\n", + "function increment(value: int64) returns int64:\n return value + 1\nstruct Holder:\n callback: function(int64) returns int64\nfunction exercise() returns int64:\n Holder holder = Holder(callback: increment)\n return 4 into holder.callback into holder.callback\n", + "function recover(value: int64) returns result[int64, string]:\n return ok(value)\nfunction increment(value: int64) returns int64:\n return value + 1\nstruct Holder:\n callback: function(int64) returns int64\nfunction exercise() returns int64:\n Holder holder = Holder(callback: increment)\n return 4\n into recover() handle error:\n default 0\n into holder.callback\n", + ]; + for source in sources { + let (mut program, checked) = checked_source(source); + assert!(!checked.pipeline_step_input_types.is_empty()); + assert!( + checked + .pipeline_step_input_types + .values() + .all(|ty| *ty == TypeInterner::INT64) + ); + assert!( + checked.pipeline_step_input_types.keys().any(|span| { + checked.type_map.get(span).is_some_and(|ty| { + matches!(checked.interner.resolve(*ty), Type::Function { .. }) + }) + }), + "field target shares a step span with its virtual input" + ); + let index = exercise_index(&program); + let expression = returned_call_mut(&mut program.functions[index]); + let ExpressionKind::IndirectCall { + args, + ownership: CallOwnership::Source(packet), + .. + } = &mut expression.kind + else { + panic!("field pipeline remains an indirect invocation"); + }; + assert_eq!(packet.arguments[0].actual_type, TypeInterner::INT64); + assert_eq!( + packet.arguments[0].source_witness().occurrence_type(), + TypeInterner::INT64 + ); + args[0].ty = TypeInterner::STRING; + rejects(&program, &checked, "forged pipeline input operand"); + } +} + +#[test] +fn generated_native_suites_seal_target_result_and_zero_argument_order() { + let source = "function ordinary() returns nothing:\n return nothing\nverify accepted:\n assert true\n"; + let (program, checked) = checked_source_with_suites(source, true); + let suite = program + .functions + .iter() + .find(|function| function.identity.declaration.kind == DeclarationKind::Verify) + .expect("checked verify"); + let ordinary = program + .functions + .iter() + .find(|function| function.identity.declaration.kind == DeclarationKind::Function) + .expect("ordinary function"); + let ownership = CallOwnership::generated( + GeneratedOperation::NativeSuite { function: suite.id }, + &[], + &[], + &[], + TypeInterner::NOTHING, + &[], + &checked.interner, + ) + .expect("compiler suite packet"); + let original = Expression { + kind: ExpressionKind::Call { + ownership, + function: suite.id, + args: vec![], + evaluation_order: vec![], + }, + ty: TypeInterner::NOTHING, + span: suite.span, + }; + validate_hir_invocation( + &program.functions, + &[], + &original, + &checked.interner, + jett_typecheck::CheckedOwnershipContext::Ordinary, + ) + .expect("exact checked suite"); + for case in ["target", "operation", "result"] { + let mut changed = original.clone(); + let ExpressionKind::Call { + ownership: CallOwnership::Generated(packet), + function, + .. + } = &mut changed.kind + else { + unreachable!() + }; + match case { + "target" => *function = ordinary.id, + "operation" => { + packet.operation = GeneratedOperation::NativeSuite { + function: ordinary.id, + } + } + "result" => changed.ty = TypeInterner::BOOL, + _ => unreachable!(), + } + assert!( + validate_hir_invocation( + &program.functions, + &[], + &changed, + &checked.interner, + jett_typecheck::CheckedOwnershipContext::Ordinary + ) + .is_err(), + "{case}" + ); + } + let mut ordinary_call = original.clone(); + let ExpressionKind::Call { + ownership, + function, + .. + } = &mut ordinary_call.kind + else { + unreachable!() + }; + *function = ordinary.id; + *ownership = CallOwnership::generated( + GeneratedOperation::NativeSuite { + function: ordinary.id, + }, + &[], + &[], + &[], + TypeInterner::NOTHING, + &[], + &checked.interner, + ) + .unwrap(); + assert!( + validate_hir_invocation( + &program.functions, + &[], + &ordinary_call, + &checked.interner, + jett_typecheck::CheckedOwnershipContext::Ordinary + ) + .is_err() + ); +} + +#[test] +fn generated_evaluated_values_validate_closed_intrinsic_shapes() { + let mut types = TypeInterner::new(); + let span = Span::new(FileId::new(0), 0, 1); + let set = types.intern(Type::Set(TypeInterner::INT64)); + let bytes_result = types.intern(Type::Result(TypeInterner::BYTES, TypeInterner::STRING)); + let make = |intrinsic, + type_arguments, + args: Vec, + result, + access: Vec| { + let order = (0..args.len()).collect::>(); + let parameters = args.iter().map(|value| value.ty).collect::>(); + let ownership = CallOwnership::generated( + GeneratedOperation::EvaluatedValue { intrinsic }, + &args, + ¶meters, + &access, + result, + &order, + &types, + ) + .unwrap(); + Expression { + kind: ExpressionKind::Intrinsic { + ownership, + intrinsic, + type_arguments, + reflection_arguments: vec![], + refinement_predicates: vec![], + field_validation: None, + args, + evaluation_order: order, + }, + ty: result, + span, + } + }; + let empty_set = make( + IntrinsicId::SetNew, + vec![TypeInterner::INT64], + vec![], + set, + vec![], + ); + let inserted = make( + IntrinsicId::SetAdd, + vec![TypeInterner::INT64], + vec![ + empty_set.clone(), + Expression { + kind: ExpressionKind::Int(7), + ty: TypeInterner::INT64, + span, + }, + ], + set, + vec![jett_typecheck::CheckedCalleeAccess::Owned; 2], + ); + let empty_bytes = make( + IntrinsicId::BytesNew, + vec![], + vec![], + TypeInterner::BYTES, + vec![], + ); + let parsed_bytes = make( + IntrinsicId::BytesFromHex, + vec![], + vec![Expression { + kind: ExpressionKind::String("00".into()), + ty: TypeInterner::STRING, + span, + }], + bytes_result, + vec![jett_typecheck::CheckedCalleeAccess::Owned], + ); + for original in [empty_set, inserted, empty_bytes, parsed_bytes] { + validate_hir_invocation( + &[], + &[], + &original, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary, + ) + .expect("closed evaluated value"); + let mut changed = original.clone(); + let ExpressionKind::Intrinsic { type_arguments, .. } = &mut changed.kind else { + unreachable!() + }; + type_arguments.push(TypeInterner::STRING); + assert!( + validate_hir_invocation( + &[], + &[], + &changed, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary + ) + .is_err() + ); + } +} + +#[test] +fn generated_stages_keep_private_acquisition_and_snapshot_authority() { + let mut types = TypeInterner::new(); + let list = types.intern(Type::List(TypeInterner::INT64)); + let span = Span::new(FileId::new(0), 0, 1); + let borrowed = Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(LocalId::new(0)), + ty: list, + span, + })), + ty: list, + span, + }; + let mut packet = CallOwnership::generated( + GeneratedOperation::Display { + method: FunctionId::new(0), + }, + &[borrowed], + &[list], + &[jett_typecheck::CheckedCalleeAccess::View], + TypeInterner::STRING, + &[0], + &types, + ) + .unwrap(); + assert!( + packet + .stage_generated_parameter( + 0, + GeneratedArgumentStaging::OwnedProducer { + owner: LocalId::new(1), + loan: LocalId::new(2) + } + ) + .is_err() + ); + packet + .stage_generated_parameter( + 0, + GeneratedArgumentStaging::OrdinarySnapshot { + owner: LocalId::new(1), + loan: LocalId::new(2), + }, + ) + .expect("ordinary data snapshot"); + let local = |id: LocalId| { + Some(OwnershipLocalInfo { + ty: list, + mutable: false, + span, + view_source: (id == LocalId::new(2)).then_some(LocalId::new(1)), + is_view_parameter: false, + view_iteration: None, + }) + }; + validate_operand_ownership(&packet, &[list], &[0], &types, local, false) + .expect("typed staged shape"); + assert!(validate_operand_ownership(&packet, &[list], &[0], &types, local, true).is_err()); + assert!( + packet + .stage_generated_parameter( + 0, + GeneratedArgumentStaging::Existing { + loan: Some(LocalId::new(2)) + } + ) + .is_err(), + "a second transition cannot replace a sealed storage association" + ); + let CallOwnership::Generated(generated) = &mut packet else { + unreachable!() + }; + generated.arguments[0].staging = GeneratedArgumentStaging::OwnedProducer { + owner: LocalId::new(1), + loan: LocalId::new(2), + }; + assert!( + validate_operand_ownership(&packet, &[list], &[0], &types, local, false).is_err(), + "public stage cannot relabel a snapshot as a producer" + ); + let mut appended = packet.clone(); + let CallOwnership::Generated(generated) = &mut appended else { + unreachable!() + }; + generated.arguments.push(generated.arguments[0].clone()); + assert!( + appended + .stage_generated_parameter( + 1, + GeneratedArgumentStaging::Existing { + loan: Some(LocalId::new(2)) + } + ) + .is_err(), + "public extra argument is bounded before certificate lookup" + ); + let signature = types.intern(Type::Function { + params: vec![], + view_params: vec![], + return_type: TypeInterner::NOTHING, + }); + let descriptor = Expression { + kind: ExpressionKind::Local(LocalId::new(0)), + ty: signature, + span, + }; + let mut descriptor_packet = CallOwnership::generated( + GeneratedOperation::FunctionAdapter { + callee: LocalId::new(0), + source_type: signature, + target_type: signature, + }, + &[descriptor], + &[signature], + &[jett_typecheck::CheckedCalleeAccess::View], + TypeInterner::NOTHING, + &[0], + &types, + ) + .unwrap(); + assert!( + descriptor_packet + .stage_generated_parameter( + 0, + GeneratedArgumentStaging::OrdinarySnapshot { + owner: LocalId::new(1), + loan: LocalId::new(2) + } + ) + .is_err(), + "signature cannot prove captures" + ); +} + +fn generated_metadata_constant(types: &TypeInterner, ty: TypeId, span: Span) -> Expression { + let kind = match types.resolve(ty) { + Type::Int64 => ExpressionKind::Int(0), + Type::String => ExpressionKind::String(String::new()), + Type::Bool => ExpressionKind::Bool(false), + Type::Optional(_) => ExpressionKind::OptionalNone, + Type::List(_) => ExpressionKind::ListConstruct { elements: vec![] }, + Type::Enum(_) => ExpressionKind::EnumConstruct { + enum_type: ty, + variant: VariantId::new(0), + payloads: vec![], + evaluation_order: vec![], + }, + Type::Struct(id) => { + let fields = types + .resolve_struct(*id) + .fields + .iter() + .map(|(_, ty)| generated_metadata_constant(types, *ty, span)) + .collect::>(); + ExpressionKind::StructConstruct { + struct_type: ty, + evaluation_order: (0..fields.len()).collect(), + fields, + validates_refinements: false, + refinement_predicates: vec![], + } + } + other => panic!("unexpected compiler metadata field: {other:?}"), + }; + Expression { kind, ty, span } +} + +#[test] +fn generated_evaluated_builders_keep_canonical_metadata_and_payload_shapes() { + let source = "struct Record:\n value: int64\nenum Choice:\n chosen(value: int64)\nmachine Session:\n states:\n ready(value: int64)\n empty\n transitions:\n ready to empty\nfunction exercise() returns nothing:\n return nothing\n"; + let (_, checked) = checked_source(source); + let mut types = checked.interner; + let named = |types: &TypeInterner, name: &str| { + types + .type_ids() + .find(|&ty| types.type_name(ty) == name) + .expect("checked named type") + }; + let record = named(&types, "Record"); + let choice = named(&types, "Choice"); + let session = named(&types, "Session"); + let field = named(&types, "TypeField"); + let variant = named(&types, "TypeVariant"); + let state = named(&types, "TypeMachineState"); + let result = types.intern(Type::Result( + TypeInterner::TYPE_CONSTRUCTION, + TypeInterner::STRING, + )); + let span = Span::new(FileId::new(0), 0, 1); + let make = |intrinsic, type_arguments, args: Vec, result| { + let parameters = args.iter().map(|value| value.ty).collect::>(); + let order = (0..args.len()).collect::>(); + let access = (0..args.len()) + .map(|index| { + jett_typecheck::intrinsic_operand_access(intrinsic, index, args.len()).unwrap() + }) + .collect::>(); + let ownership = CallOwnership::generated( + GeneratedOperation::EvaluatedValue { intrinsic }, + &args, + ¶meters, + &access, + result, + &order, + &types, + ) + .unwrap(); + Expression { + kind: ExpressionKind::Intrinsic { + ownership, + intrinsic, + type_arguments, + reflection_arguments: vec![], + refinement_predicates: vec![], + field_validation: None, + args, + evaluation_order: order, + }, + ty: result, + span, + } + }; + let started = make( + IntrinsicId::TypeConstructStart, + vec![record], + vec![], + TypeInterner::TYPE_CONSTRUCTION, + ); + let variant_started = make( + IntrinsicId::TypeConstructVariantStart, + vec![choice], + vec![generated_metadata_constant(&types, variant, span)], + result, + ); + let machine_started = make( + IntrinsicId::TypeConstructMachineStart, + vec![session], + vec![generated_metadata_constant(&types, state, span)], + result, + ); + let put = make( + IntrinsicId::TypeConstructPut, + vec![record, TypeInterner::INT64], + vec![ + started.clone(), + generated_metadata_constant(&types, field, span), + Expression { + kind: ExpressionKind::Int(7), + ty: TypeInterner::INT64, + span, + }, + ], + result, + ); + for original in [started, variant_started, machine_started, put] { + validate_hir_invocation( + &[], + &[], + &original, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary, + ) + .expect("closed evaluated builder"); + let mut changed = original.clone(); + let ExpressionKind::Intrinsic { type_arguments, .. } = &mut changed.kind else { + unreachable!() + }; + type_arguments.clear(); + assert!( + validate_hir_invocation( + &[], + &[], + &changed, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary + ) + .is_err() + ); + } + let malformed = make( + IntrinsicId::TypeConstructVariantStart, + vec![choice], + vec![generated_metadata_constant(&types, field, span)], + result, + ); + assert!( + validate_hir_invocation( + &[], + &[], + &malformed, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary + ) + .is_err(), + "field metadata cannot stand in for selected variant metadata" + ); + let malformed_owner = make( + IntrinsicId::TypeConstructStart, + vec![TypeInterner::INT64], + vec![], + TypeInterner::TYPE_CONSTRUCTION, + ); + assert!( + validate_hir_invocation( + &[], + &[], + &malformed_owner, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary + ) + .is_err(), + "a primitive cannot be substituted for the checked construction owner" + ); +} + +#[test] +fn generated_zero_argument_adapters_bind_and_remap_the_actual_capture() { + let mut types = TypeInterner::new(); + let signature = types.intern(Type::Function { + params: vec![], + view_params: vec![], + return_type: TypeInterner::INT64, + }); + let span = Span::new(FileId::new(0), 0, 1); + let mut ownership = CallOwnership::generated( + GeneratedOperation::FunctionAdapter { + callee: LocalId::new(1), + source_type: signature, + target_type: signature, + }, + &[], + &[], + &[], + TypeInterner::INT64, + &[], + &types, + ) + .unwrap(); + let local = |id: LocalId| { + Some(OwnershipLocalInfo { + ty: if id == LocalId::new(1) { + signature + } else { + TypeInterner::INT64 + }, + mutable: false, + span, + view_source: None, + is_view_parameter: false, + view_iteration: None, + }) + }; + validate_operand_ownership(&ownership, &[], &[], &types, local, true) + .expect("capture type joined even without arguments"); + assert!(validate_operand_ownership(&ownership, &[], &[], &types, |_| None, true).is_err()); + assert!( + validate_operand_ownership( + &ownership, + &[], + &[], + &types, + |id| { + let mut fact = local(id).unwrap(); + fact.ty = TypeInterner::INT64; + Some(fact) + }, + true + ) + .is_err() + ); + let mut original = Expression { + kind: ExpressionKind::IndirectCall { + ownership: ownership.clone(), + callee: Box::new(Expression { + kind: ExpressionKind::Local(LocalId::new(1)), + ty: signature, + span, + }), + args: vec![], + evaluation_order: vec![], + }, + ty: TypeInterner::INT64, + span, + }; + validate_invocation_target( + &[], + &original, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary, + ) + .expect("exact captured target"); + let ExpressionKind::IndirectCall { callee, .. } = &mut original.kind else { + unreachable!() + }; + callee.kind = ExpressionKind::Local(LocalId::new(0)); + assert!( + validate_invocation_target( + &[], + &original, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary + ) + .is_err() + ); + let mut ids = vec![]; + ownership.metadata_local_ids(|id| ids.push(id)); + assert!(ids.contains(&LocalId::new(1))); + ownership + .remap_metadata_locals(|id| Ok::<_, String>(LocalId::new(id.index() + 2))) + .unwrap(); + validate_operand_ownership( + &ownership, + &[], + &[], + &types, + |id| { + (id == LocalId::new(3)).then_some(OwnershipLocalInfo { + ty: signature, + mutable: false, + span, + view_source: None, + is_view_parameter: false, + view_iteration: None, + }) + }, + true, + ) + .expect("public and private target identities remap together"); + let remapped = Expression { + kind: ExpressionKind::IndirectCall { + ownership, + callee: Box::new(Expression { + kind: ExpressionKind::Local(LocalId::new(3)), + ty: signature, + span, + }), + args: vec![], + evaluation_order: vec![], + }, + ty: TypeInterner::INT64, + span, + }; + validate_invocation_target( + &[], + &remapped, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary, + ) + .unwrap(); +} + +#[test] +fn generated_metadata_return_comes_from_the_exact_intrinsic_schema() { + let types = TypeInterner::new(); + let span = Span::new(FileId::new(0), 0, 1); + let make = |intrinsic, type_arguments, result| { + let ownership = CallOwnership::generated( + GeneratedOperation::ReflectionMetadata { intrinsic }, + &[], + &[], + &[], + result, + &[], + &types, + ) + .unwrap(); + Expression { + kind: ExpressionKind::Intrinsic { + ownership, + intrinsic, + type_arguments, + reflection_arguments: vec![], + refinement_predicates: vec![], + field_validation: None, + args: vec![], + evaluation_order: vec![], + }, + ty: result, + span, + } + }; + let original = make( + IntrinsicId::TypeName, + vec![TypeInterner::INT64], + TypeInterner::STRING, + ); + validate_hir_invocation( + &[], + &[], + &original, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary, + ) + .expect("exact generated TypeName"); + for changed in [ + make( + IntrinsicId::TypeName, + vec![TypeInterner::INT64], + TypeInterner::BOOL, + ), + make(IntrinsicId::TypeName, vec![], TypeInterner::STRING), + make( + IntrinsicId::TypeName, + vec![TypeInterner::INT64, TypeInterner::STRING], + TypeInterner::STRING, + ), + make( + IntrinsicId::TypeHasSecret, + vec![TypeInterner::INT64], + TypeInterner::BOOL, + ), + ] { + assert!( + validate_hir_invocation( + &[], + &[], + &changed, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary + ) + .is_err(), + "a freshly minted certificate cannot manufacture a metadata return or operation schema" + ); + } +} + +#[test] +fn generated_owned_operand_cannot_acquire_a_logical_viewed_iteration_binding() { + let mut types = TypeInterner::new(); + let item = types.intern(Type::List(TypeInterner::INT64)); + let iterable = types.intern(Type::List(item)); + let span = Span::new(FileId::new(0), 0, 10); + let value = Expression { + kind: ExpressionKind::Local(LocalId::new(0)), + ty: item, + span, + }; + let packet = CallOwnership::generated( + GeneratedOperation::NativeSuite { + function: FunctionId::new(0), + }, + &[value.clone()], + &[item], + &[jett_typecheck::CheckedCalleeAccess::Owned], + TypeInterner::NOTHING, + &[0], + &types, + ) + .unwrap(); + let CallOwnership::Generated(generated) = &packet else { + unreachable!() + }; + assert_eq!( + generated.arguments[0].acquisition, + GeneratedAcquisition::OwnedExpression, + "the canonical owned formal must exercise the generated-owner gate" + ); + let proof = crate::checked_view_iteration_binding( + &types, + span, + iterable, + item, + crate::IterationPart::Element, + ) + .unwrap(); + let local = OwnershipLocalInfo { + ty: item, + mutable: false, + span, + view_source: None, + is_view_parameter: false, + view_iteration: Some(proof), + }; + let error = validate_generated_operand_tree(&value, &generated.arguments[0], &[local]) + .expect_err("generated acquisition cannot relabel logical source view"); + assert_eq!(error, "generated owner cannot acquire a borrowed binding"); +} + +const GENERATED_PRODUCER_SOURCE: &str = r#"interface Displayable: + function display(view self: Displayable) returns string +namespace app +struct Item: + value: int64 +implement Displayable for Item: + function display(view self: Item) returns string: + return "shown" +function make() returns Item: + return Item(value: 7) +function alternate() returns Item: + return Item(value: 9) +"#; + +fn displayed_call_mut(function: &mut Function) -> &mut Expression { + let ExpressionKind::StringInterpolation(parts) = &mut returned_call_mut(function).kind else { + panic!("source interpolation"); + }; + let call = parts + .iter_mut() + .find_map(|part| match part { + StringSegment::Value(Expression { + kind: ExpressionKind::DisplayResult(call), + .. + }) => Some(call.as_mut()), + _ => None, + }) + .expect("exact generated display invocation"); + assert!(matches!( + call.kind, + ExpressionKind::Call { + ownership: CallOwnership::Generated(_), + .. + } + )); + call +} + +fn generated_original_argument( + call: &mut Expression, +) -> (&mut Expression, &GeneratedArgumentOwnership) { + let ExpressionKind::Call { + args, + ownership: CallOwnership::Generated(packet), + .. + } = &mut call.kind + else { + panic!("generated invocation"); + }; + (&mut args[0], &packet.arguments[0]) +} + +fn generated_endpoint_mut(value: &mut Expression) -> &mut Expression { + match value { + Expression { + kind: ExpressionKind::View(inner), + .. + } => inner, + other => other, + } +} + +#[test] +fn generated_producer_shapes_keep_original_clone_call_and_constructor_distinct() { + for endpoint in ["make()", "Item(value: 7)", "clone make()"] { + let source = format!( + "{GENERATED_PRODUCER_SOURCE}function exercise() returns string:\n return \"{{{endpoint}}}\"\n" + ); + let (program, checked) = checked_source(&source); + let index = exercise_index(&program); + let mut changed = program.clone(); + let (value, argument) = + generated_original_argument(displayed_call_mut(&mut changed.functions[index])); + assert!(argument.original_witness().owned_producer()); + let endpoint = generated_endpoint_mut(value); + let original = endpoint.clone(); + endpoint.kind = ExpressionKind::Clone(Box::new(original)); + rejects(&changed, &checked, "added generated endpoint clone"); + } + let source = format!( + "{GENERATED_PRODUCER_SOURCE}function exercise() returns string:\n return \"{{make()}}\"\n" + ); + let (mut program, checked) = checked_source(&source); + let alternate = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "alternate") + .unwrap() + .id; + let index = exercise_index(&program); + let (value, _) = generated_original_argument(displayed_call_mut(&mut program.functions[index])); + let ExpressionKind::Call { function, .. } = &mut generated_endpoint_mut(value).kind else { + panic!("original producer call"); + }; + *function = alternate; + rejects( + &program, + &checked, + "same-result generated target substitution", + ); +} + +#[test] +fn generated_producer_backing_follows_local_and_function_metadata_remaps() { + let mut types = TypeInterner::new(); + let list = types.intern(Type::List(TypeInterner::INT64)); + let signature = types.intern(Type::Function { + params: vec![], + view_params: vec![], + return_type: list, + }); + let span = Span::new(FileId::new(0), 0, 1); + let source = Expression { + kind: ExpressionKind::IndirectCall { + callee: Box::new(Expression { + kind: ExpressionKind::Local(LocalId::new(1)), + ty: signature, + span, + }), + args: vec![], + evaluation_order: vec![], + ownership: CallOwnership::generated( + GeneratedOperation::FunctionAdapter { + callee: LocalId::new(1), + source_type: signature, + target_type: signature, + }, + &[], + &[], + &[], + list, + &[], + &types, + ) + .unwrap(), + }, + ty: list, + span, + }; + let mut packet = CallOwnership::generated( + GeneratedOperation::Display { + method: FunctionId::new(2), + }, + &[source.clone()], + &[list], + &[jett_typecheck::CheckedCalleeAccess::View], + TypeInterner::STRING, + &[0], + &types, + ) + .unwrap(); + let CallOwnership::Generated(generated) = &packet else { + unreachable!() + }; + generated.arguments[0] + .original_witness() + .validate_producer_shape(&source) + .unwrap(); + let mut substituted = source.clone(); + let ExpressionKind::IndirectCall { callee, .. } = &mut substituted.kind else { + unreachable!() + }; + callee.kind = ExpressionKind::Local(LocalId::new(0)); + assert!( + generated.arguments[0] + .original_witness() + .validate_producer_shape(&substituted) + .is_err() + ); + let mut locals = vec![]; + packet.metadata_local_ids(|id| locals.push(id)); + assert!(locals.contains(&LocalId::new(1))); + packet + .remap_metadata_locals(|id| Ok::<_, String>(LocalId::new(id.index() + 3))) + .unwrap(); + packet + .remap_metadata_functions(|id| Ok::<_, String>(FunctionId::new(id.index() + 5))) + .unwrap(); + let mut functions = vec![]; + packet.metadata_function_ids(|id| functions.push(id)); + assert!(functions.contains(&FunctionId::new(7))); + let mut remapped = source; + let ExpressionKind::IndirectCall { callee, .. } = &mut remapped.kind else { + unreachable!() + }; + callee.kind = ExpressionKind::Local(LocalId::new(4)); + let CallOwnership::Generated(generated) = &packet else { + unreachable!() + }; + generated.arguments[0] + .original_witness() + .validate_producer_shape(&remapped) + .unwrap(); + validate_operand_ownership(&packet, &[list], &[0], &types, |_| None, true).unwrap(); +} + +#[test] +fn generated_constructor_shapes_keep_authenticated_inline_descriptors() { + let source = r#"interface Displayable: + function display(view self: Displayable) returns string +namespace app +struct Item: + callback: function(int64) returns int64 +implement Displayable for Item: + function display(view self: Item) returns string: + return "shown" +function exercise(offset: int64) returns string: + return "{Item(callback: function(value: int64) returns int64: return value + offset)}" +"#; + let (mut program, checked) = checked_source(source); + let index = exercise_index(&program); + let (value, _) = generated_original_argument(displayed_call_mut(&mut program.functions[index])); + let ExpressionKind::StructConstruct { fields, .. } = &mut generated_endpoint_mut(value).kind + else { + panic!("owned constructor"); + }; + let ExpressionKind::ClosureRef { captures, .. } = &mut fields[0].kind else { + panic!("canonical captured inline descriptor"); + }; + assert_eq!(captures.len(), 1); + captures.clear(); + rejects(&program, &checked, "descriptor backing substitution"); +} + +#[test] +fn generated_handle_shape_restoration_keeps_source_snapshot_and_cfg_authority_separate() { + let source = format!( + "{GENERATED_PRODUCER_SOURCE}function exercise(incoming: optional[int64]) returns string:\n return \"{{Item(value: incoming handle: default 7)}}\"\n" + ); + let (mut program, _checked) = checked_source(&source); + let index = exercise_index(&program); + let (value, argument) = + generated_original_argument(displayed_call_mut(&mut program.functions[index])); + let original = value.clone(); + let ExpressionKind::StructConstruct { fields, .. } = &mut generated_endpoint_mut(value).kind + else { + panic!("owned constructor"); + }; + let ExpressionKind::Handle { target, .. } = &fields[0].kind else { + panic!("original handled field"); + }; + let original_target = target.as_ref().clone(); + let handle_span = fields[0].span; + fields[0].kind = ExpressionKind::Local(LocalId::new(100)); + assert!( + argument + .original_witness() + .validate_producer_shape(value) + .is_err(), + "original HIR cannot carry lowered result authority" + ); + let mut reached = 0; + argument + .original_witness() + .validate_lowered_producer_shape(value, |produced, original| { + let handle = original.handle().expect("exact original Handle node"); + assert_eq!(handle.kind(), &HandleKind::Optional); + assert_eq!(produced.ty, handle.result_type()); + assert_eq!(produced.span, handle_span); + handle.validate_source_shape(&original_target)?; + let snapshot = Expression { + kind: ExpressionKind::Clone(Box::new(original_target.clone())), + ty: original_target.ty, + span: original_target.span, + }; + handle.validate_source_shape(&snapshot)?; + let double = Expression { + kind: ExpressionKind::Clone(Box::new(snapshot)), + ty: original_target.ty, + span: original_target.span, + }; + assert!(handle.validate_source_shape(&double).is_err()); + reached += 1; + Ok(()) + }) + .expect("callback only authenticates this original Handle node"); + assert_eq!(reached, 1); + let mut foreign = original.clone(); + let ExpressionKind::StructConstruct { fields, .. } = + &mut generated_endpoint_mut(&mut foreign).kind + else { + unreachable!() + }; + fields[0].kind = ExpressionKind::Int(7); + assert!( + argument + .original_witness() + .validate_lowered_producer_shape(&foreign, |_, _| { + panic!("a non-Local replacement never receives Handle authority") + }) + .is_err() + ); +} + +#[test] +fn generated_storage_callbacks_keep_exact_non_handle_nodes_and_copy_permissions() { + let mut types = TypeInterner::new(); + let span = Span::new(FileId::new(0), 0, 1); + let literal = Expression { + kind: ExpressionKind::Int(7), + ty: TypeInterner::INT64, + span, + }; + let packet = CallOwnership::generated( + GeneratedOperation::Display { + method: FunctionId::new(0), + }, + &[literal.clone()], + &[literal.ty], + &[jett_typecheck::CheckedCalleeAccess::View], + TypeInterner::STRING, + &[0], + &types, + ) + .unwrap(); + let CallOwnership::Generated(packet) = packet else { + unreachable!() + }; + let witness = packet.arguments[0].original_witness(); + let storage = Expression { + kind: ExpressionKind::Local(LocalId::new(100)), + ..literal.clone() + }; + assert!(witness.validate_producer_shape(&storage).is_err()); + witness + .validate_lowered_producer_shape(&storage, |value, original| { + assert_eq!((value.ty, value.span), (original.ty(), original.span())); + assert!(original.handle().is_none()); + original + .validate_reconstructed(&literal, |_, _| Err("unexpected nested storage".into()))?; + let inserted_clone = Expression { + kind: ExpressionKind::Clone(Box::new(literal.clone())), + ..literal.clone() + }; + assert!( + original + .validate_reconstructed(&inserted_clone, |_, _| Err( + "unexpected nested storage".into() + )) + .is_err() + ); + assert!( + original + .validate_existing_local_snapshot(&inserted_clone, |_, _| Err( + "unexpected nested storage".into() + )) + .is_err() + ); + Ok(()) + }) + .expect("read-only callback recurses against the same original primitive node"); + + let list = types.intern(Type::List(TypeInterner::INT64)); + let descriptor = types.intern(Type::Function { + params: vec![], + view_params: vec![], + return_type: TypeInterner::INT64, + }); + for (ty, ordinary_copy) in [(list, true), (descriptor, false)] { + let original = Expression { + kind: ExpressionKind::Local(LocalId::new(1)), + ty, + span, + }; + let packet = CallOwnership::generated( + GeneratedOperation::Display { + method: FunctionId::new(0), + }, + &[original.clone()], + &[ty], + &[jett_typecheck::CheckedCalleeAccess::View], + TypeInterner::STRING, + &[0], + &types, + ) + .unwrap(); + let CallOwnership::Generated(packet) = packet else { + unreachable!() + }; + let witness = packet.arguments[0].original_witness(); + let storage = Expression { + kind: ExpressionKind::Local(LocalId::new(100)), + ty, + span, + }; + let snapshot = Expression { + kind: ExpressionKind::Clone(Box::new(original.clone())), + ty, + span, + }; + witness + .validate_lowered_producer_shape(&storage, |_, node| { + assert!( + node.validate_reconstructed(&snapshot, |_, _| Err( + "unexpected nested storage".into() + )) + .is_err() + ); + assert_eq!( + node.validate_existing_local_snapshot(&snapshot, |_, _| Err( + "unexpected nested storage".into() + )) + .is_ok(), + ordinary_copy + ); + let double = Expression { + kind: ExpressionKind::Clone(Box::new(snapshot.clone())), + ty, + span, + }; + assert!( + node.validate_existing_local_snapshot(&double, |_, _| Err( + "unexpected nested storage".into() + )) + .is_err() + ); + let wrong_backing = Expression { + kind: ExpressionKind::Clone(Box::new(storage.clone())), + ty, + span, + }; + assert!( + node.validate_existing_local_snapshot(&wrong_backing, |_, _| Err( + "foreign storage cannot replace original backing".into() + )) + .is_err() + ); + Ok(()) + }) + .expect("storage restoration does not infer descriptor copy authority"); + } +} + +#[test] +fn caller_packets_preserve_exact_nominal_secret_argument_joins() { + for callee in ["callback", "factory()"] { + for (parameter, argument, result) in [ + ("int64", "Classified", "secret[int64]"), + ("int64", "DeepClassified", "secret[int64]"), + ("Classified", "secret[Classified]", "Classified"), + ("DeepClassified", "secret[DeepClassified]", "DeepClassified"), + ] { + let source = format!( + "type Classified = secret[int64] where true\n\ + type DeepClassified = Classified where true\n\ + type Alternative = secret[int64] where true\n\ + type PublicValue = int64 where true\n\ + function callback(value: {parameter}) returns {parameter}:\n return value\n\ + function factory() returns function({parameter}) returns {parameter}:\n return callback\n\ + function exercise(value: {argument}, unrelated: Alternative, public_value: PublicValue) returns {result}:\n return {callee}(value)\n" + ); + let (program, checked) = checked_source(&source); + let exercise = &program.functions[exercise_index(&program)]; + let expression = returned_call(exercise); + let (args, source) = match &expression.kind { + ExpressionKind::Call { + args, + ownership: CallOwnership::Source(source), + .. + } + | ExpressionKind::IndirectCall { + args, + ownership: CallOwnership::Source(source), + .. + } => (args, source), + _ => panic!("checked source call"), + }; + let argument = &source.arguments[0]; + assert_eq!( + argument.actual_type, + argument.source_witness().occurrence_type() + ); + let CallerOrigin::Binding(binding) = &argument.origin else { + panic!("exact nominal source binding"); + }; + assert_eq!(binding.ty, argument.actual_type); + match checked.interner.resolve(argument.actual_type) { + Type::Refinement { .. } => assert_eq!(args[0].ty, argument.actual_type), + Type::Secret(inner) => assert_eq!(*inner, argument.parameter_type), + _ => panic!("nominal-secret or original explicit-secret control"), + } + assert_eq!(source.arguments.len(), 1); + } + } +} + +#[test] +fn caller_packets_reject_replaced_nominal_secret_operands_and_untainted_results() { + for callee in ["callback", "factory()"] { + let source = format!( + "type Classified = secret[int64] where true\n\ + type Alternative = secret[int64] where true\n\ + type PublicValue = int64 where true\n\ + function callback(value: int64) returns int64:\n return value\n\ + function factory() returns function(int64) returns int64:\n return callback\n\ + function exercise(value: Classified, unrelated: Alternative, public_value: PublicValue) returns secret[int64]:\n return {callee}(value)\n" + ); + let (program, checked) = checked_source(&source); + let index = exercise_index(&program); + for name in ["unrelated", "public_value"] { + let mut corrupted = program.clone(); + let replacement = corrupted.functions[index] + .locals + .iter() + .find(|local| local.name == name) + .expect("independent nominal control") + .ty; + let expression = returned_call_mut(&mut corrupted.functions[index]); + let args = match &mut expression.kind { + ExpressionKind::Call { args, .. } | ExpressionKind::IndirectCall { args, .. } => { + args + } + _ => panic!("checked source call"), + }; + assert_ne!(replacement, args[0].ty); + args[0].ty = replacement; + let expression = returned_call(&corrupted.functions[index]); + let error = validate_invocation_target( + &corrupted.functions, + expression, + &checked.interner, + jett_typecheck::CheckedOwnershipContext::Ordinary, + ) + .expect_err("another nominal type cannot replace the immutable actual"); + assert!( + error.contains("converted operand differs from its physical parameter"), + "{callee}/{name}: {error}" + ); + rejects(&corrupted, &checked, "replaced nominal Secret operand"); + } + let mut corrupted = program.clone(); + returned_call_mut(&mut corrupted.functions[index]).ty = TypeInterner::INT64; + let error = validate_invocation_target( + &corrupted.functions, + returned_call(&corrupted.functions[index]), + &checked.interner, + jett_typecheck::CheckedOwnershipContext::Ordinary, + ) + .expect_err("Secret lifting retains its checked result"); + assert_eq!( + error, + "call ownership result differs from its checked source certificate" + ); + rejects(&corrupted, &checked, "untainted nominal Secret result"); + } +} + +#[test] +fn caller_packets_accept_exact_qualified_descriptors_but_not_value_substitutions() { + let source = r#"namespace app +struct Holder: + callback: function(int64) returns int64 +function identity(value: int64) returns int64: + return value +function inspect(view callback: function(int64) returns int64, marker: int64) returns int64: + return marker +function exercise(holder: Holder, stored: function(int64) returns int64) returns int64: + return inspect(marker: 1, callback: app.identity) +"#; + for replacement in ["local", "field"] { + let (mut program, checked) = checked_source(source); + let index = exercise_index(&program); + let holder = local_id(&program.functions[index], "holder"); + let stored = local_id(&program.functions[index], "stored"); + let holder_type = program.functions[index].locals[holder.index() as usize].ty; + let expression = returned_call_mut(&mut program.functions[index]); + let ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + evaluation_order, + .. + } = &mut expression.kind + else { + panic!("qualified descriptor argument"); + }; + assert_eq!(evaluation_order, &[1, 0]); + assert_eq!(packet.arguments[0].origin, CallerOrigin::OwnedExpression); + assert!(matches!(args[0].kind, ExpressionKind::FunctionRef(_))); + args[0].kind = if replacement == "local" { + ExpressionKind::Local(stored) + } else { + ExpressionKind::Field { + base: Box::new(Expression { + kind: ExpressionKind::Local(holder), + ty: holder_type, + span: args[0].span, + }), + owner_type: holder_type, + field: FieldId::new(0), + } + }; + let errors = validate_program_call_ownership(&program, &checked.interner) + .expect_err("a descriptor producer cannot be replaced by a value origin"); + assert!(errors.iter().any(|error| error.message == + "call ownership cannot manufacture a producer acquisition from a binding or field"), + "{replacement}: {errors:?}"); + } +} + +#[test] +fn caller_packets_keep_method_producers_and_real_callback_field_projections_distinct() { + let source = r#"namespace models +export struct Point: + value: int64 + function amount(view self: Point) returns int64: + return self.value +namespace app +struct Holder: + callback: function(view models.Point) returns int64 +function inspect(view callback: function(view models.Point) returns int64) returns int64: + return 7 +function method_call() returns int64: + use models as m + return inspect(view m.Point.amount) +function exercise(view holder: Holder) returns int64: + return inspect(view holder.callback) +"#; + let (program, checked) = checked_source(source); + let method = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "method_call") + .expect("method source caller"); + let ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + .. + } = &returned_call(method).kind + else { + panic!("method descriptor call"); + }; + assert_eq!(packet.arguments[0].origin, CallerOrigin::OwnedExpression); + let ExpressionKind::View(inner) = &args[0].kind else { + panic!("written descriptor view"); + }; + assert!(matches!(inner.kind, ExpressionKind::FunctionRef(_))); + let field = &program.functions[exercise_index(&program)]; + let ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + .. + } = &returned_call(field).kind + else { + panic!("borrowed value field call"); + }; + assert!( + matches!(packet.arguments[0].origin, CallerOrigin::BorrowedProjection { + source: CallerViewSource::Local(local) + } if local == local_id(field, "holder")) + ); + let ExpressionKind::View(inner) = &args[0].kind else { + panic!("written field view"); + }; + assert!(matches!(inner.kind, ExpressionKind::Field { .. })); + validate_program_call_ownership(&program, &checked.interner) + .expect("both exact origins validate"); +} + +#[test] +fn caller_packets_generic_mutual_source_calls_have_exact_syntax_type_and_target_proofs() { + let source = r#"namespace sample +mutual: + function leaf[T](view value: T) returns int64 + function alternate[T](value: T) returns int64 + function forward[T](view value: T) returns int64 +function leaf[T](view value: T) returns int64: + return 7 +function alternate[T](value: T) returns int64: + return 9 +function forward[T](view value: T) returns int64: + return leaf[T](view value) +function exercise(values: list[int64]) returns int64: + return forward[list[int64]](view values) +function instantiate_alternate(values: list[int64]) returns int64: + return alternate[list[int64]](values) +"#; + let (program, checked) = checked_source(source); + let forward = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "forward") + .expect("concrete mutual forwarding"); + let ExpressionKind::Call { + ownership: CallOwnership::Source(packet), + .. + } = &returned_call(forward).kind + else { + panic!("source forward call"); + }; + assert_eq!( + packet.arguments[0].syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert_eq!( + packet.arguments[0].effect, + jett_typecheck::CheckedCallerEffect::RetainBorrow + ); + assert_eq!(packet.arguments[0].callee_access, CheckedCalleeAccess::View); + validate_program_call_ownership(&program, &checked.interner).expect("every exact packet"); + for mutation in ["syntax", "type", "target"] { + let mut corrupted = program.clone(); + let alternate = corrupted + .functions + .iter() + .find(|function| function.identity.declaration.name == "alternate") + .expect("owned alternative") + .id; + let index = corrupted + .functions + .iter() + .position(|function| function.identity.declaration.name == "forward") + .unwrap(); + let expression = returned_call_mut(&mut corrupted.functions[index]); + let ExpressionKind::Call { + function, + ownership: CallOwnership::Source(packet), + .. + } = &mut expression.kind + else { + panic!("real mutual packet"); + }; + match mutation { + "syntax" => packet.arguments[0].syntax = jett_typecheck::CheckedCallerSyntax::Bare, + "type" => packet.arguments[0].actual_type = TypeInterner::INT64, + "target" => *function = alternate, + _ => unreachable!(), + } + let errors = validate_program_call_ownership(&corrupted, &checked.interner) + .expect_err("exact original mutual proof cannot be changed"); + assert!(!errors.is_empty(), "{mutation}"); + } +} + +const MACHINE_STATE_SOURCE_JOINS: &str = r#"machine Session: + states: + guest + logged_in(user_id: string) + transitions: + guest to logged_in +machine OtherSession: + states: + guest + logged_in(user_id: string) + transitions: + guest to logged_in +function precise(session: Session at logged_in) returns string: + return session.user_id +function guest_only(session: Session at guest) returns string: + return "guest" +function other_only(view session: OtherSession at logged_in) returns int64: + return 2 +function wide(view session: Session) returns int64: + return 7 +function owned_wide(session: Session) returns int64: + return 9 +function guarded(session: Session) returns string: + if session at logged_in: + return precise(session) + return "guest" +function exercise_direct(view session: Session at logged_in) returns int64: + return wide(view session) +function exercise_indirect(view session: Session at logged_in, callback: function(view Session) returns int64) returns int64: + return callback(view session) +"#; + +fn machine_state_caller_index(program: &Program, name: &str) -> usize { + program + .functions + .iter() + .position(|function| function.identity.declaration.name == name) + .expect("exact source function") +} + +fn guarded_machine_call(function: &Function) -> &Expression { + let StatementKind::If { then_block, .. } = &function.body.statements[0].kind else { + panic!("checked source state guard"); + }; + let StatementKind::Return(Some(expression)) = &then_block.statements[0].kind else { + panic!("call inside exact state guard"); + }; + expression +} + +#[test] +fn caller_packets_machine_guard_keeps_exact_occurrence_and_declared_binding() { + let (program, checked) = checked_source(MACHINE_STATE_SOURCE_JOINS); + let caller = &program.functions[machine_state_caller_index(&program, "guarded")]; + let ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + .. + } = &guarded_machine_call(caller).kind + else { + panic!("checked narrowed call"); + }; + let argument = &packet.arguments[0]; + let CallerOrigin::Binding(fact) = &argument.origin else { + panic!("original binding"); + }; + let Type::Machine(parent) = checked.interner.resolve(fact.ty) else { + panic!("declaration remains bare machine"); + }; + let Type::MachineState { machine, .. } = checked.interner.resolve(argument.actual_type) else { + panic!("checked occurrence retains exact state"); + }; + assert_eq!(parent, machine); + assert_ne!(fact.ty, argument.actual_type); + assert_eq!(caller.locals[fact.local.index() as usize].ty, fact.ty); + assert_eq!(args[0].ty, argument.actual_type); + assert_eq!( + argument.source_witness().occurrence_type(), + argument.actual_type + ); + assert!(matches!(args[0].kind, ExpressionKind::Local(local) if local == fact.local)); + assert_eq!(argument.syntax, jett_typecheck::CheckedCallerSyntax::Bare); + assert_eq!( + argument.effect, + jett_typecheck::CheckedCallerEffect::TransferOwned + ); + assert_eq!(argument.callee_access, CheckedCalleeAccess::Owned); + validate_program_call_ownership(&program, &checked.interner).expect("exact state proof"); +} + +#[test] +fn caller_packets_machine_state_views_erase_only_to_exact_parent_parameters() { + let (program, checked) = checked_source(MACHINE_STATE_SOURCE_JOINS); + for name in ["exercise_direct", "exercise_indirect"] { + let caller = &program.functions[machine_state_caller_index(&program, name)]; + let (args, packet) = match &returned_call(caller).kind { + ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + .. + } + | ExpressionKind::IndirectCall { + args, + ownership: CallOwnership::Source(packet), + .. + } => (args, packet), + _ => panic!("checked direct or indirect source call"), + }; + let argument = &packet.arguments[0]; + let Type::MachineState { machine, .. } = checked.interner.resolve(argument.actual_type) + else { + panic!("physical argument remains exact state"); + }; + let Type::Machine(parent) = checked.interner.resolve(argument.parameter_type) else { + panic!("parameter is bare parent machine"); + }; + assert_eq!(machine, parent); + assert_ne!(argument.actual_type, argument.parameter_type); + assert_eq!(args[0].ty, argument.actual_type); + assert_eq!( + argument.source_witness().occurrence_type(), + argument.actual_type + ); + assert!(matches!(args[0].kind, ExpressionKind::View(_))); + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert_eq!( + argument.effect, + jett_typecheck::CheckedCallerEffect::RetainBorrow + ); + assert_eq!(argument.callee_access, CheckedCalleeAccess::View); + } + validate_program_call_ownership(&program, &checked.interner) + .expect("one-way exact parent joins"); +} + +#[test] +fn caller_packets_machine_state_joins_preserve_private_type_syntax_and_mode_proofs() { + let (program, checked) = checked_source(MACHINE_STATE_SOURCE_JOINS); + let direct = machine_state_caller_index(&program, "exercise_direct"); + let guest = program.functions[machine_state_caller_index(&program, "guest_only")].params[0].ty; + let other = program.functions[machine_state_caller_index(&program, "other_only")].params[0].ty; + let owned = program.functions[machine_state_caller_index(&program, "owned_wide")].id; + for mutation in ["wrong state", "wrong machine", "syntax", "owned target"] { + let mut corrupted = program.clone(); + let ExpressionKind::Call { + function, + args, + ownership: CallOwnership::Source(packet), + .. + } = &mut returned_call_mut(&mut corrupted.functions[direct]).kind + else { + panic!("real source View packet"); + }; + match mutation { + "wrong state" | "wrong machine" => { + let replacement = if mutation == "wrong state" { + guest + } else { + other + }; + assert_ne!(args[0].ty, replacement); + args[0].ty = replacement; + let ExpressionKind::View(inner) = &mut args[0].kind else { + panic!("written View"); + }; + inner.ty = replacement; + } + "syntax" => packet.arguments[0].syntax = jett_typecheck::CheckedCallerSyntax::Bare, + "owned target" => *function = owned, + _ => unreachable!(), + } + let errors = validate_program_call_ownership(&corrupted, &checked.interner) + .expect_err("private exact state/source/mode witness cannot be changed"); + assert!(!errors.is_empty(), "{mutation}"); + } + let mut corrupted = program.clone(); + let guarded = machine_state_caller_index(&corrupted, "guarded"); + let StatementKind::If { then_block, .. } = + &mut corrupted.functions[guarded].body.statements[0].kind + else { + panic!("original guard"); + }; + let StatementKind::Return(Some(expression)) = &mut then_block.statements[0].kind else { + panic!("guarded call"); + }; + let packet = source_packet_mut(expression); + let CallerOrigin::Binding(fact) = &mut packet.arguments[0].origin else { + panic!("binding fact"); + }; + fact.ty = other; + assert!( + validate_program_call_ownership(&corrupted, &checked.interner).is_err(), + "the declared binding fact stays exact" + ); +} + +#[test] +fn caller_packets_machine_state_join_does_not_admit_wrong_nominal_or_state_sources() { + for source in [ + MACHINE_STATE_SOURCE_JOINS.replace( + "return wide(view session)", + "return other_only(view session)", + ), + MACHINE_STATE_SOURCE_JOINS.replace("return precise(session)", "return guest_only(session)"), + ] { + let parsed = jett_parser::parse(&source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "{:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == Severity::Error + && diagnostic.code.code() == 304), + "wrong machine/state remains a checked argument error: {:?}", + checked.diagnostics + ); + } +} + +const CONVERTED_SOURCE_HANDLE: &str = r#"namespace app +interface Show: + function show(view self: Show) returns string +implement Show for uint64: + function show(view self: uint64) returns string: + return "uint64" +function read(view value: Show) returns int64: + return 7 +function exercise(incoming: optional[uint64], spare: uint64) returns int64: + uint64 fallback = 0 + return read(view(incoming handle: + default fallback + )) +"#; + +#[test] +fn caller_packets_converted_handle_keeps_inner_occurrence_separate_from_written_view() { + let (program, checked) = checked_source(CONVERTED_SOURCE_HANDLE); + let function = &program.functions[exercise_index(&program)]; + let ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + .. + } = &returned_call(function).kind + else { + panic!("source read"); + }; + let argument = &packet.arguments[0]; + let backing = call_ownership::validate_source_handled_operand( + &args[0], + argument, + &packet.bridge, + &checked.interner, + ) + .expect("exact finite conversion") + .expect("original Handle"); + assert!(matches!(backing.kind, ExpressionKind::Handle { .. })); + assert_eq!(backing.ty, TypeInterner::UINT64); + assert_eq!(argument.actual_type, TypeInterner::UINT64); + assert!(matches!( + checked.interner.resolve(argument.parameter_type), + Type::Interface(_) + )); + assert_ne!(backing.span, argument.source_span); + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert_eq!(argument.origin, CallerOrigin::OwnedExpression); +} + +#[test] +fn caller_packets_converted_handle_rejects_lost_inner_occurrence_and_original_node() { + let (program, checked) = checked_source(CONVERTED_SOURCE_HANDLE); + for mutation in ["span", "type", "literal replacement"] { + let mut corrupted = program.clone(); + let function = &mut corrupted.functions[exercise_index(&program)]; + let ExpressionKind::Call { args, .. } = &mut returned_call_mut(function).kind else { + panic!("source read"); + }; + let mut backing = &mut args[0]; + while matches!( + backing.kind, + ExpressionKind::View(_) + | ExpressionKind::InterfaceCoerce { .. } + | ExpressionKind::FunctionAdapter { .. } + ) { + backing = match &mut backing.kind { + ExpressionKind::View(inner) + | ExpressionKind::InterfaceCoerce { value: inner, .. } + | ExpressionKind::FunctionAdapter { value: inner, .. } => inner, + _ => unreachable!("transparent conversion checked before borrowing"), + }; + } + assert!(matches!(backing.kind, ExpressionKind::Handle { .. })); + match mutation { + "span" => backing.span.start += 1, + "type" => backing.ty = TypeInterner::INT64, + "literal replacement" => backing.kind = ExpressionKind::Int(0), + _ => unreachable!(), + } + rejects(&corrupted, &checked, mutation); + } +} + +const QUALIFIED_UNIT_ENUM_SOURCE: &str = r#"namespace models +export enum Choice: + empty + full(value: int64) +namespace app +struct Holder: + choice: models.Choice +function inspect(view choice: models.Choice, marker: int64) returns int64: + return marker +function exercise(holder: Holder, stored: models.Choice) returns int64: + use models as m + return inspect(marker: 1, choice: m.Choice.empty) +"#; + +#[test] +fn caller_packets_qualified_unit_enums_are_producers_not_same_typed_value_origins() { + let (program, checked) = checked_source(QUALIFIED_UNIT_ENUM_SOURCE); + let index = exercise_index(&program); + let ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + evaluation_order, + .. + } = &returned_call(&program.functions[index]).kind + else { + panic!("qualified enum Source invocation"); + }; + assert_eq!(evaluation_order, &[1, 0]); + assert_eq!(packet.arguments[0].origin, CallerOrigin::OwnedExpression); + assert_eq!( + packet.arguments[0].effect, + jett_typecheck::CheckedCallerEffect::RelinquishOwned + ); + let ExpressionKind::EnumConstruct { + enum_type, + variant, + payloads, + evaluation_order, + } = &args[0].kind + else { + panic!("exact static unit construction"); + }; + assert_eq!(*enum_type, packet.arguments[0].actual_type); + assert_eq!(args[0].ty, *enum_type); + assert!(payloads.is_empty() && evaluation_order.is_empty()); + let Type::Enum(id) = checked.interner.resolve(*enum_type) else { + panic!("nominal Enum identity"); + }; + let declaration = checked.interner.resolve_enum(*id); + assert_eq!(declaration.name, "models.Choice"); + assert_eq!(declaration.variants[variant.index() as usize].name, "empty"); + assert!( + declaration.variants[variant.index() as usize] + .fields + .is_empty() + ); + for replacement in ["local", "field", "public projection"] { + let mut corrupted = program.clone(); + let holder = local_id(&corrupted.functions[index], "holder"); + let stored = local_id(&corrupted.functions[index], "stored"); + let holder_type = corrupted.functions[index].locals[holder.index() as usize].ty; + let expression = returned_call_mut(&mut corrupted.functions[index]); + let ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + .. + } = &mut expression.kind + else { + panic!("checked enum invocation"); + }; + match replacement { + "local" => args[0].kind = ExpressionKind::Local(stored), + "field" => { + args[0].kind = ExpressionKind::Field { + base: Box::new(Expression { + kind: ExpressionKind::Local(holder), + ty: holder_type, + span: args[0].span, + }), + owner_type: holder_type, + field: FieldId::new(0), + } + } + "public projection" => { + packet.arguments[0].origin = CallerOrigin::OwnedFieldCopy { + parent: CallerViewSource::Other, + } + } + _ => unreachable!(), + } + let expected = if replacement == "public projection" { + "call ownership disagrees with its original source witness" + } else { + "call ownership cannot manufacture a producer acquisition from a binding or field" + }; + let errors = validate_program_call_ownership(&corrupted, &checked.interner) + .expect_err("static producer authority cannot become a real value origin"); + assert!( + errors.iter().any(|error| error.message == expected), + "{replacement}: {errors:?}" + ); + } +} + +#[test] +fn caller_packets_enum_valued_bindings_fields_and_temporary_fields_keep_exact_roots() { + let source = r#"namespace models +export enum Choice: + empty + full(value: int64) +namespace app +struct Holder: + choice: models.Choice +function inspect(view choice: models.Choice, marker: int64) returns int64: + return marker +function from_field(view holder: Holder) returns int64: + return inspect(holder.choice, 1) +function from_view(view holder: Holder) returns int64: + return inspect(view holder.choice, 2) +function exercise(view stored: models.Choice) returns int64: + return inspect(view stored, 3) +function from_temporary() returns int64: + use models + return inspect(Holder(choice: models.Choice.empty).choice, 4) +"#; + let (program, checked) = checked_source(source); + for name in ["from_field", "from_view", "exercise", "from_temporary"] { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .expect("checked runtime origin control"); + let ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + .. + } = &returned_call(function).kind + else { + panic!("ordinary enum call"); + }; + let argument = &packet.arguments[0]; + assert!(matches!( + checked.interner.resolve(argument.actual_type), + Type::Enum(_) + )); + let mut value = &args[0]; + if let ExpressionKind::View(inner) = &value.kind { + value = inner; + } + match name { + "from_field" => { + assert_eq!( + argument.origin, + CallerOrigin::OwnedFieldCopy { + parent: CallerViewSource::Local(local_id(function, "holder")), + } + ); + assert!(matches!(value.kind, ExpressionKind::Field { .. })); + } + "from_view" => { + assert_eq!( + argument.origin, + CallerOrigin::BorrowedProjection { + source: CallerViewSource::Local(local_id(function, "holder")), + } + ); + assert!(matches!(value.kind, ExpressionKind::Field { .. })); + } + "exercise" => { + assert!(matches!(argument.origin, CallerOrigin::Binding(fact) + if fact.local == local_id(function, "stored"))); + assert!(matches!(value.kind, ExpressionKind::Local(_))); + } + "from_temporary" => { + assert_eq!( + argument.origin, + CallerOrigin::OwnedFieldCopy { + parent: CallerViewSource::Other, + } + ); + let ExpressionKind::Field { base, .. } = &value.kind else { + panic!("a real temporary field is not a static enum producer"); + }; + assert!(matches!(base.kind, ExpressionKind::StructConstruct { .. })); + } + _ => unreachable!(), + } + } + validate_program_call_ownership(&program, &checked.interner) + .expect("enum TypeId alone does not change source roots or copying rules"); +} + +#[test] +fn caller_packets_retained_pipeline_seals_physical_header_separately_from_raw_source() { + let source = r#"namespace app +function read(view values: list[int64], extra: int64) returns int64: + return extra +function exercise(view values: list[int64]) returns int64: + return values into view read(2) +"#; + let (program, checked) = checked_source(source); + let index = exercise_index(&program); + let ExpressionKind::Call { + ownership: CallOwnership::Source(packet), + args, + .. + } = &returned_call(&program.functions[index]).kind + else { + panic!("source pipeline"); + }; + let argument = &packet.arguments[0]; + assert_eq!(argument.retained_snapshot_span(), Some(args[0].span)); + assert_ne!(args[0].span, argument.source_span); + assert_eq!(argument.source_span, { + let ExpressionKind::View(raw) = &args[0].kind else { + panic!("written pipeline View"); + }; + raw.span + }); + assert_eq!( + argument.effect, + jett_typecheck::CheckedCallerEffect::RetainBorrow + ); + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert_eq!(argument.staging, ArgumentStaging::Original); + + let mut changed = program.clone(); + let ExpressionKind::Call { args, .. } = + &mut returned_call_mut(&mut changed.functions[index]).kind + else { + panic!("original pipeline"); + }; + args[0].span.start += 1; + let errors = validate_program_call_ownership(&changed, &checked.interner) + .expect_err("a new physical header cannot replace the private initial occurrence"); + assert!( + errors + .iter() + .any(|error| error.message + == "call ownership retained snapshot physical occurrence changed"), + "{errors:?}" + ); +} + +const GENERATED_CONVERTED_CALLABLE_SOURCE: &str = r#"namespace app +interface Named: + function name(view self: Named) returns string +struct User: + label: string +implement Named for User: + function name(view self: User) returns string: + return "user:{self.label}" +function show(view item: Named) returns string: + return Named.name(view item) +function invoke_reader(reader: function(view User) returns string) returns string: + User input = User(label: "higher-order") + return reader(view input) +function exercise() returns string: + function(function(view Named) returns string) returns string higher = invoke_reader + return higher(show) +"#; +const GENERATED_CONVERTED_CONTAINER_SOURCE: &str = r#"namespace app +interface Named: + function name(view self: Named) returns string +struct User: + label: string +implement Named for User: + function name(view self: User) returns string: + return "user:{self.label}" +function make_reader(prefix: string) returns function(view Named) returns string: + return function(view item: Named) returns string: return "{prefix}:{Named.name(view item)}" +function first_reader(items: list[function(view User) returns string]) returns string: + User input = User(label: "higher-container") + for callback in items: + return callback(view input) + return "empty" +function exercise() returns string: + function(list[function(view Named) returns string]) returns string adapted = first_reader + return adapted(list(make_reader("higher"))) +"#; +const GENERATED_CONVERTED_TWO_READERS: &str = r#"namespace app +interface Named: + function name(view self: Named) returns string +struct User: + label: string +implement Named for User: + function name(view self: User) returns string: + return "user:{self.label}" +function show(view item: Named) returns string: + return Named.name(view item) +function invoke_reader(first: function(view User) returns string, second: function(view User) returns string) returns string: + User input = User(label: "higher-order") + return "{first(view input)}:{second(view input)}" +function exercise() returns string: + function(function(view Named) returns string, function(view Named) returns string) returns string higher = invoke_reader + return higher(show, show) +"#; + +fn checked_generated_conversion_source(source: &str, release: bool) -> (Program, CheckResult) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "parse: {:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "resolve: {:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check_with_options( + &parsed.module, + &resolved, + jett_typecheck::CheckOptions { release }, + ); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "check: {:?}", + checked.diagnostics + ); + let program = lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .expect("canonical generated adapter keeps its sealed raw actual"); + validate_program_call_ownership(&program, &checked.interner) + .expect("complete generated ownership"); + (program, checked) +} + +fn generated_converted_adapter_index(program: &Program, container: bool) -> usize { + program + .functions + .iter() + .position(|function| { + function.body.statements.iter().any(|statement| { + let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::IndirectCall { + ownership: CallOwnership::Generated(packet), + args, + .. + }, + .. + })) = &statement.kind + else { + return false; + }; + matches!(packet.operation, GeneratedOperation::FunctionAdapter { .. }) + && args.iter().zip(&packet.arguments).any(|(value, argument)| { + value.ty != argument.actual_type + && match &value.kind { + ExpressionKind::InterfaceCoerce { adapters, .. } => { + container && !adapters.is_empty() + } + ExpressionKind::FunctionAdapter { .. } => !container, + _ => false, + } + }) + }) + }) + .expect("source-derived generated conversion body") +} + +fn generated_converted_argument(function: &Function) -> (&Expression, &GeneratedArgumentOwnership) { + let ExpressionKind::IndirectCall { + ownership: CallOwnership::Generated(packet), + args, + .. + } = &returned_call(function).kind + else { + panic!("generated indirect adapter call"); + }; + args.iter() + .zip(&packet.arguments) + .find(|(value, argument)| value.ty != argument.actual_type) + .expect("original actual differs from converted physical formal") +} + +fn generated_converted_endpoint(mut value: &Expression, actual: TypeId) -> &Expression { + while value.ty != actual { + value = match &value.kind { + ExpressionKind::InterfaceCoerce { value, .. } + | ExpressionKind::FunctionAdapter { value, .. } => value, + _ => panic!("canonical finite conversion"), + }; + } + value +} + +fn generated_converted_endpoint_mut(mut value: &mut Expression, actual: TypeId) -> &mut Expression { + while value.ty != actual { + value = match &mut value.kind { + ExpressionKind::InterfaceCoerce { value, .. } + | ExpressionKind::FunctionAdapter { value, .. } => value, + _ => panic!("canonical finite conversion"), + }; + } + value +} + +fn generated_conversion_locals(function: &Function) -> Vec { + function + .locals + .iter() + .map(|local| OwnershipLocalInfo { + ty: local.ty, + mutable: local.mutable, + span: local.span, + view_source: local.view_source, + is_view_parameter: function + .params + .iter() + .any(|param| param.local == local.id && param.mode == ParamMode::View), + view_iteration: None, + }) + .collect() +} + +#[test] +fn generated_converted_callable_rejoins_original_target_parameter_without_place_peeling() { + for release in [false, true] { + let (mut program, checked) = + checked_generated_conversion_source(GENERATED_CONVERTED_CALLABLE_SOURCE, release); + let index = generated_converted_adapter_index(&program, false); + let function = &program.functions[index]; + let (value, argument) = generated_converted_argument(function); + assert!(matches!(value.kind, ExpressionKind::FunctionAdapter { .. })); + assert_eq!( + call_ownership::immediate_source(value), + CallerViewSource::Other, + "a callable adapter remains outside general place provenance" + ); + let raw = generated_converted_endpoint(value, argument.actual_type); + let ExpressionKind::Local(local) = raw.kind else { + panic!("sealed raw target parameter"); + }; + assert_eq!( + argument.original_witness().origin(), + CallerViewSource::Local(local) + ); + assert_eq!(raw.span, argument.original_witness().source_span()); + assert_eq!(value.span, raw.span); + assert_eq!(raw.ty, function.locals[local.index() as usize].ty); + assert_eq!(argument.acquisition, GeneratedAcquisition::OwnedExpression); + validate_generated_operand_tree(value, argument, &generated_conversion_locals(function)) + .expect("strict raw backing after canonical callable conversion"); + let count = program.functions.len(); + complete_value_conversions(&mut program, &checked.interner) + .expect("existing adapter completion is repeatable"); + assert_eq!(program.functions.len(), count, "no new duplicate adapters"); + } +} + +#[test] +fn generated_converted_container_rejoins_raw_local_without_erasing_callback_table() { + for release in [false, true] { + let (program, checked) = + checked_generated_conversion_source(GENERATED_CONVERTED_CONTAINER_SOURCE, release); + let function = &program.functions[generated_converted_adapter_index(&program, true)]; + let (value, argument) = generated_converted_argument(function); + let ExpressionKind::InterfaceCoerce { adapters, .. } = &value.kind else { + panic!("container conversion"); + }; + assert!(!adapters.is_empty()); + assert_eq!( + call_ownership::immediate_source(value), + CallerViewSource::Other + ); + let raw = generated_converted_endpoint(value, argument.actual_type); + let ExpressionKind::Local(local) = raw.kind else { + panic!("raw list-of-callables target parameter"); + }; + assert_eq!( + argument.original_witness().origin(), + CallerViewSource::Local(local) + ); + assert_eq!(argument.acquisition, GeneratedAcquisition::OwnedExpression); + for adapter in adapters { + let target = &program.functions[adapter.function.index() as usize]; + assert_eq!(target.id, adapter.function); + assert_eq!(target.capture_count, 1); + assert_eq!(target.params[0].ty, adapter.source); + let Type::Function { + params, + view_params, + return_type, + } = checked.interner.resolve(adapter.target) + else { + panic!("checked callback target signature"); + }; + assert_eq!( + target.params[1..] + .iter() + .map(|param| param.ty) + .collect::>(), + *params + ); + assert_eq!( + target.params[1..] + .iter() + .map(|param| param.mode == ParamMode::View) + .collect::>(), + *view_params + ); + assert_eq!(target.return_type, *return_type); + } + validate_generated_operand_tree(value, argument, &generated_conversion_locals(function)) + .expect("raw origin does not replace independent callback table validation"); + } +} + +#[test] +fn generated_converted_raw_occurrence_refuses_same_typed_backing_and_arbitrary_wrappers() { + let (program, checked) = + checked_generated_conversion_source(GENERATED_CONVERTED_TWO_READERS, false); + let index = generated_converted_adapter_index(&program, false); + let function = &program.functions[index]; + let (value, argument) = generated_converted_argument(function); + let raw = generated_converted_endpoint(value, argument.actual_type); + let ExpressionKind::Local(local) = raw.kind else { + panic!("raw target parameter"); + }; + let spare = function + .params + .iter() + .find(|param| param.local != local && param.ty == raw.ty) + .expect("independent same-typed source parameter") + .local; + for mutation in [ + "physical span", + "raw span", + "foreign local", + "same type clone", + "same type adapter", + "nonconversion", + ] { + let mut changed = program.clone(); + let function = &mut changed.functions[index]; + let ExpressionKind::IndirectCall { + args, + ownership: CallOwnership::Generated(packet), + .. + } = &mut returned_call_mut(function).kind + else { + panic!("generated converted call"); + }; + let actual = packet.arguments[0].actual_type; + if mutation == "physical span" { + args[0].span.start += 1; + } else if mutation == "nonconversion" { + let original = args[0].clone(); + args[0].kind = ExpressionKind::Clone(Box::new(original)); + } else { + let raw = generated_converted_endpoint_mut(&mut args[0], actual); + match mutation { + "raw span" => raw.span.start += 1, + "foreign local" => raw.kind = ExpressionKind::Local(spare), + "same type clone" => raw.kind = ExpressionKind::Clone(Box::new(raw.clone())), + "same type adapter" => { + raw.kind = ExpressionKind::FunctionAdapter { + value: Box::new(raw.clone()), + function: program.functions[index].id, + } + } + _ => unreachable!(), + } + } + let errors = validate_program_call_ownership(&changed, &checked.interner).expect_err( + "conversion cannot replace original raw occurrence/backing or mint same-type peel", + ); + assert!( + errors.iter().any(|error| matches!( + error.message.as_str(), + "generated operand differs from its original typed occurrence or backing" + | "generated operand has no checked actual/conversion type" + )), + "{mutation}: {errors:?}" + ); + } +} + +#[test] +fn generated_converted_raw_owner_keeps_borrowed_slot_gate_and_metadata_remap_identity() { + let (program, _) = + checked_generated_conversion_source(GENERATED_CONVERTED_CALLABLE_SOURCE, false); + let function = &program.functions[generated_converted_adapter_index(&program, false)]; + let (value, argument) = generated_converted_argument(function); + let raw = generated_converted_endpoint(value, argument.actual_type); + let ExpressionKind::Local(local) = raw.kind else { + panic!("raw original Local"); + }; + let mut locals = generated_conversion_locals(function); + locals[local.index() as usize].is_view_parameter = true; + assert_eq!( + validate_generated_operand_tree(value, argument, &locals).unwrap_err(), + "generated owner cannot acquire a borrowed binding" + ); + + let ExpressionKind::IndirectCall { ownership, .. } = &returned_call(function).kind else { + panic!("adapter call"); + }; + let mut remapped = ownership.clone(); + remapped + .remap_metadata_locals(|id| Ok::<_, ()>(LocalId::new(id.index() + 3))) + .expect("structural remap"); + let CallOwnership::Generated(packet) = remapped else { + panic!("same generated packet"); + }; + let argument = &packet.arguments[0]; + let mapped = LocalId::new(local.index() + 3); + assert_eq!( + argument.original_witness().origin(), + CallerViewSource::Local(mapped) + ); + let mut raw = raw.clone(); + raw.kind = ExpressionKind::Local(mapped); + argument + .original_witness() + .validate_producer_shape(&raw) + .expect("raw shape remaps with the origin"); + let mut stale = raw; + stale.kind = ExpressionKind::Local(local); + assert!( + argument + .original_witness() + .validate_producer_shape(&stale) + .is_err(), + "old numeric Local cannot replace remapped original backing" + ); +} + +const FLOW_PROJECTED_MACHINE: &str = r#"namespace app +interface Named: + function name(view self: Named) returns string +struct Item: + label: string +implement Named for Item: + function name(view self: Item) returns string: + return self.label +machine Session: + states: + active(item: Named) + cached(item: Named) + empty +machine Other: + states: + active(item: Named) +function exercise(source: Session, other: Session, foreign: Other) returns string: + if source at active: + return Named.name(view source.item) + return "empty" +machine TaskState: + states: + ready(value: nothing) + cached(value: nothing) + empty +function pending_depth(value: nothing) returns int64: + return 7 +function copied(state: TaskState) returns int64: + if state at ready: + return pending_depth(state.value) + return -1 +"#; + +fn flow_projected_call(function: &Function) -> &Expression { + let StatementKind::If { then_block, .. } = &function.body.statements[0].kind else { + panic!("source state guard"); + }; + let StatementKind::Return(Some(value)) = &then_block.statements[0].kind else { + panic!("guarded source invocation"); + }; + value +} + +fn flow_projected_call_mut(function: &mut Function) -> &mut Expression { + let StatementKind::If { then_block, .. } = &mut function.body.statements[0].kind else { + panic!("source state guard"); + }; + let StatementKind::Return(Some(value)) = &mut then_block.statements[0].kind else { + panic!("guarded source invocation"); + }; + value +} + +fn flow_projected_field_mut(expression: &mut Expression) -> &mut Expression { + let ExpressionKind::Call { args, .. } = &mut expression.kind else { + panic!("source invocation"); + }; + let value = &mut args[0]; + fn field(value: &mut Expression) -> &mut Expression { + if matches!(value.kind, ExpressionKind::View(_)) { + let ExpressionKind::View(inner) = &mut value.kind else { + unreachable!() + }; + return field(inner); + } + assert!(matches!(value.kind, ExpressionKind::Field { .. })); + value + } + field(value) +} + +#[test] +fn caller_packets_flow_projected_machine_calls_preserve_view_and_nothing_copy() { + use jett_typecheck::CheckedCallerEffect; + let (program, checked) = checked_source(FLOW_PROJECTED_MACHINE); + for (name, effect) in [ + ("exercise", CheckedCallerEffect::RetainBorrow), + ("copied", CheckedCallerEffect::Copy), + ] { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + let ExpressionKind::Call { + ownership: CallOwnership::Source(packet), + args, + .. + } = &flow_projected_call(function).kind + else { + panic!("original source packet"); + }; + let argument = &packet.arguments[0]; + let (local, span, occurrence, storage) = argument + .source_witness() + .projection_root() + .expect("exact checked flow root"); + assert_eq!(argument.effect, effect); + assert!(matches!( + checked.interner.resolve(storage), + Type::Machine(_) + )); + assert!(matches!( + checked.interner.resolve(occurrence), + Type::MachineState { .. } + )); + assert_eq!(function.locals[local.index() as usize].ty, storage); + assert!( + function + .locals + .iter() + .all(|local| local.view_source.is_none()) + ); + let value = if let ExpressionKind::View(inner) = &args[0].kind { + inner + } else { + &args[0] + }; + let ExpressionKind::Field { + base, owner_type, .. + } = &value.kind + else { + panic!("source field"); + }; + assert_eq!(*owner_type, occurrence); + assert_eq!(base.span, span); + assert!(matches!(base.kind, ExpressionKind::Local(id) if id == local)); + assert_eq!(base.ty, occurrence); + assert!( + validate_local_view_initializer( + &args[0], + local, + storage, + args[0].ty, + &checked.interner + ) + .is_err(), + "Source certificate cannot weaken the persistent alias helper" + ); + if name == "copied" { + assert_eq!(argument.actual_type, TypeInterner::NOTHING); + } + } +} + +#[test] +fn caller_packets_flow_projected_machine_rejects_sibling_state_and_root_corruption() { + let (program, mut checked) = checked_source(FLOW_PROJECTED_MACHINE); + let index = exercise_index(&program); + let function = &program.functions[index]; + let source = local_id(function, "source"); + let other = local_id(function, "other"); + let foreign = local_id(function, "foreign"); + let Type::Machine(machine) = *checked + .interner + .resolve(function.locals[source.index() as usize].ty) + else { + panic!("bare stored owner"); + }; + let Type::Machine(foreign_machine) = *checked + .interner + .resolve(function.locals[foreign.index() as usize].ty) + else { + panic!("foreign nominal owner"); + }; + let cached = checked + .interner + .resolve_machine(machine) + .state_id("cached") + .unwrap(); + let foreign_active = checked + .interner + .resolve_machine(foreign_machine) + .state_id("active") + .unwrap(); + let cached_type = checked.interner.intern(Type::MachineState { + machine, + state: cached, + }); + let foreign_type = checked.interner.intern(Type::MachineState { + machine: foreign_machine, + state: foreign_active, + }); + let stale = checked.interner.intern(Type::MachineState { + machine, + state: jett_types::MachineStateId::new(u32::MAX), + }); + for mutation in [ + "sibling", + "foreign", + "stale", + "root local", + "root span", + "field", + "endpoint", + "public origin", + ] { + let mut changed = program.clone(); + let invocation = flow_projected_call_mut(&mut changed.functions[index]); + if mutation == "public origin" { + source_packet_mut(invocation).arguments[0].origin = CallerOrigin::BorrowedProjection { + source: CallerViewSource::Local(other), + }; + } else { + let field = flow_projected_field_mut(invocation); + if mutation == "endpoint" { + field.ty = TypeInterner::BOOL; + } else { + let ExpressionKind::Field { + base, + owner_type, + field, + .. + } = &mut field.kind + else { + panic!("exact field"); + }; + match mutation { + "sibling" | "foreign" | "stale" => { + let ty = match mutation { + "sibling" => cached_type, + "foreign" => foreign_type, + _ => stale, + }; + *owner_type = ty; + base.ty = ty; + } + "root local" => base.kind = ExpressionKind::Local(other), + "root span" => base.span.start += 1, + "field" => *field = FieldId::new(1), + _ => unreachable!(), + } + } + } + assert!( + validate_program_call_ownership(&changed, &checked.interner).is_err(), + "{mutation}" + ); + } +} + +#[test] +fn caller_packets_flow_projection_metadata_visits_and_remaps_private_root() { + let (program, checked) = checked_source(FLOW_PROJECTED_MACHINE); + let function = &program.functions[exercise_index(&program)]; + let ExpressionKind::Call { ownership, .. } = &flow_projected_call(function).kind else { + panic!("source call"); + }; + let CallOwnership::Source(packet) = ownership else { + panic!("source packet"); + }; + let original = packet.arguments[0] + .source_witness() + .projection_root() + .unwrap(); + let mut visited = Vec::new(); + ownership.metadata_types(|ty| visited.push(ty)); + assert!(visited.contains(&original.2) && visited.contains(&original.3)); + let mut locals = Vec::new(); + ownership.metadata_local_ids(|local| locals.push(local)); + assert!(locals.contains(&original.0)); + let mut remapped = ownership.clone(); + remapped + .remap_metadata_locals(|local| Ok::<_, ()>(LocalId::new(local.index() + 1))) + .unwrap(); + let CallOwnership::Source(packet) = remapped else { + panic!("remapped packet"); + }; + let rewritten = packet.arguments[0] + .source_witness() + .projection_root() + .unwrap(); + assert_eq!( + rewritten, + ( + LocalId::new(original.0.index() + 1), + original.1, + original.2, + original.3 + ) + ); + assert!( + matches!(packet.arguments[0].origin, CallerOrigin::BorrowedProjection { + source: CallerViewSource::Local(local) } if local == rewritten.0) + ); + validate_program_call_ownership(&program, &checked.interner).unwrap(); +} + +const ORDINARY_PROJECTED_ROOTS: &str = r#"namespace app +interface Named: + function name(view self: Named) returns string +struct Holder: + item: Named +machine Session: + states: + active(item: Named) + empty +function ordinary(view source: Holder) returns string: + return Named.name(view source.item) +function qualified(view source: Session at active) returns string: + return Named.name(view source.item) +"#; + +#[test] +fn caller_packets_ordinary_and_declared_state_projection_roots_keep_existing_helper() { + let (program, checked) = checked_source(ORDINARY_PROJECTED_ROOTS); + for name in ["ordinary", "qualified"] { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + let ExpressionKind::Call { + ownership: CallOwnership::Source(packet), + args, + .. + } = &returned_call(function).kind + else { + panic!("original source packet"); + }; + let argument = &packet.arguments[0]; + assert_eq!(argument.source_witness().projection_root(), None); + assert_eq!( + argument.effect, + jett_typecheck::CheckedCallerEffect::RetainBorrow + ); + let CallerOrigin::BorrowedProjection { + source: CallerViewSource::Local(local), + } = argument.origin + else { + panic!("exact original source root"); + }; + let stored = &function.locals[local.index() as usize]; + assert!(matches!( + checked.interner.resolve(stored.ty), + Type::Struct(_) | Type::MachineState { .. } + )); + validate_local_view_initializer(&args[0], local, stored.ty, args[0].ty, &checked.interner) + .expect("ordinary stored root keeps the original stable-backing proof"); + } +} diff --git a/crates/jett_hir/src/inline_functions.rs b/crates/jett_hir/src/inline_functions.rs index 3d74f1fb..ec7cfc5d 100644 --- a/crates/jett_hir/src/inline_functions.rs +++ b/crates/jett_hir/src/inline_functions.rs @@ -14,6 +14,7 @@ pub(super) fn extract_inline_functions(functions: &mut Vec, types: &Ty next_id: functions.len() as u32, pending: Vec::new(), detached_aliases: HashSet::new(), + generated_shape_rewrites: Vec::new(), }; let mut index = 0; while index < functions.len() { @@ -23,6 +24,7 @@ pub(super) fn extract_inline_functions(functions: &mut Vec, types: &Ty locals: &function.locals, }; extractor.detached_aliases.clear(); + extractor.generated_shape_rewrites.clear(); extractor.block(&mut function.body, &parent); // Only bodies actually transferred into extracted functions can leave // stale origins here. Arbitrary orphan metadata must remain an error. @@ -46,6 +48,7 @@ struct Extractor<'a> { next_id: u32, pending: Vec, detached_aliases: HashSet, + generated_shape_rewrites: Vec, } impl Extractor<'_> { @@ -182,6 +185,7 @@ impl Extractor<'_> { let Some(mut capture_parameters) = capture_parameters else { return; }; + let original_shape = expression.clone(); let capture_count = capture_parameters.len(); capture_parameters.extend(parameters); let id = FunctionId(self.next_id); @@ -247,6 +251,11 @@ impl Extractor<'_> { captures, } }; + if let Some(record) = + crate::call_ownership::generated_inline_extraction(&original_shape, expression) + { + self.generated_shape_rewrites.push(record); + } return; } match &mut expression.kind { @@ -275,18 +284,33 @@ impl Extractor<'_> { | ExpressionKind::Field { base: value, .. } | ExpressionKind::View(value) | ExpressionKind::Clone(value) => self.expression(value, parent), - ExpressionKind::Call { args, .. } - | ExpressionKind::Intrinsic { args, .. } - | ExpressionKind::ActorSpawn { args, .. } => { + ExpressionKind::Call { + args, ownership, .. + } + | ExpressionKind::Intrinsic { + args, ownership, .. + } => { + for argument in args { + self.expression(argument, parent); + } + ownership.apply_generated_inline_extractions(&self.generated_shape_rewrites); + } + ExpressionKind::ActorSpawn { args, .. } => { for argument in args { self.expression(argument, parent); } } - ExpressionKind::IndirectCall { callee, args, .. } => { + ExpressionKind::IndirectCall { + callee, + args, + ownership, + .. + } => { self.expression(callee, parent); for argument in args { self.expression(argument, parent); } + ownership.apply_generated_inline_extractions(&self.generated_shape_rewrites); } ExpressionKind::StructConstruct { fields, .. } | ExpressionKind::BitfieldConstruct { fields, .. } => { @@ -355,6 +379,10 @@ impl Extractor<'_> { } } +pub(crate) fn body_uses_local(block: &Block, target: LocalId) -> bool { + block_uses_local(block, target.index()) +} + fn block_uses_local(block: &Block, target: u32) -> bool { block_mentions_local(block, target, false) } @@ -435,6 +463,18 @@ fn block_mentions_local(block: &Block, target: u32, include_bindings: bool) -> b } fn expression_mentions_local(expression: &Expression, target: u32, include_bindings: bool) -> bool { + if include_bindings { + if let ExpressionKind::Call { ownership, .. } + | ExpressionKind::IndirectCall { ownership, .. } + | ExpressionKind::Intrinsic { ownership, .. } = &expression.kind + { + let mut mentioned = false; + ownership.metadata_local_ids(|local| mentioned |= local.index() == target); + if mentioned { + return true; + } + } + } match &expression.kind { ExpressionKind::Local(local) => local.index() == target, ExpressionKind::Binary { left, right, .. } => { diff --git a/crates/jett_hir/src/interface_values.rs b/crates/jett_hir/src/interface_values.rs index 3b84b315..234ac54e 100644 --- a/crates/jett_hir/src/interface_values.rs +++ b/crates/jett_hir/src/interface_values.rs @@ -165,8 +165,38 @@ impl Lowerer<'_> { value }) .collect::>(); + let access = params + .iter() + .map(|param| { + if param.mode == ParamMode::View { + jett_typecheck::CheckedCalleeAccess::View + } else { + jett_typecheck::CheckedCalleeAccess::Owned + } + }) + .collect::>(); + let ownership = match crate::CallOwnership::generated( + crate::GeneratedOperation::InterfaceDispatch { + function: target, + interface_type: owner, + method_index, + }, + &args, + &implementation.parameter_types, + &access, + implementation.return_type, + &(0..args.len()).collect::>(), + &self.check.interner, + ) { + Ok(ownership) => ownership, + Err(message) => { + self.error(span, message); + continue; + } + }; let value = Expression { kind: ExpressionKind::Call { + ownership, function: target, evaluation_order: (0..args.len()).collect(), args, @@ -412,7 +442,8 @@ impl Adapters { } let Type::Function { return_type: source_return, - .. + params: source_params, + view_params: source_views, } = types.resolve(source) else { return None; @@ -470,10 +501,35 @@ impl Adapters { span, }) .collect::>(); + let access = source_views + .iter() + .map(|view| { + if *view { + jett_typecheck::CheckedCalleeAccess::View + } else { + jett_typecheck::CheckedCalleeAccess::Owned + } + }) + .collect::>(); + let ownership = crate::CallOwnership::generated( + crate::GeneratedOperation::FunctionAdapter { + callee: params[0].local, + source_type: source, + target_type: target, + }, + &args, + source_params, + &access, + *source_return, + &(0..args.len()).collect::>(), + types, + ) + .ok()?; let value = Expression { kind: ExpressionKind::IndirectCall { + ownership, callee: Box::new(Expression { - kind: ExpressionKind::Local(LocalId(0)), + kind: ExpressionKind::Local(params[0].local), ty: source, span, }), diff --git a/crates/jett_hir/src/iteration_bindings.rs b/crates/jett_hir/src/iteration_bindings.rs new file mode 100644 index 00000000..8ee00eb3 --- /dev/null +++ b/crates/jett_hir/src/iteration_bindings.rs @@ -0,0 +1,74 @@ +//! Exact source-level backing for viewed iteration bindings. +//! These are logical views, separate from ABI parameters and backend storage. +use jett_common::Span; +use jett_types::{Type, TypeId, TypeInterner}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IterationPart { + Element, + Key, + Value, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ViewIterationBinding { + loop_span: Span, + iterable_type: TypeId, + binder_type: TypeId, + part: IterationPart, +} + +impl ViewIterationBinding { + pub fn loop_span(self) -> Span { + self.loop_span + } + pub fn iterable_type(self) -> TypeId { + self.iterable_type + } + pub fn binder_type(self) -> TypeId { + self.binder_type + } + pub fn part(self) -> IterationPart { + self.part + } + pub(crate) fn matches_binding(self, ty: TypeId, declaration: Span) -> bool { + self.binder_type == ty + && declaration.file == self.loop_span.file + && declaration.start >= self.loop_span.start + && declaration.end <= self.loop_span.end + } +} + +/// Prove one endpoint of the actual checked loop. The caller separately proves +/// its exact binder LocalId, original For/ForEach association and body scope. +pub fn checked_view_iteration_binding( + types: &TypeInterner, + loop_span: Span, + iterable_type: TypeId, + binder_type: TypeId, + part: IterationPart, +) -> Result { + if iterable_type == TypeInterner::ERROR + || binder_type == TypeInterner::ERROR + || iterable_type.index() as usize >= types.len() + || binder_type.index() as usize >= types.len() + { + return Err("viewed iteration references an invalid endpoint type".into()); + } + let endpoint = match (types.resolve(iterable_type), part) { + (Type::List(element) | Type::Set(element), IterationPart::Element) => *element, + (Type::String, IterationPart::Element) => TypeInterner::STRING, + (Type::Map(key, _), IterationPart::Key) => *key, + (Type::Map(_, value), IterationPart::Value) => *value, + _ => return Err("viewed iteration binding role differs from its exact iterable".into()), + }; + if endpoint != binder_type { + return Err("viewed iteration binding type differs from its exact endpoint".into()); + } + Ok(ViewIterationBinding { + loop_span, + iterable_type, + binder_type, + part, + }) +} diff --git a/crates/jett_hir/src/lib.rs b/crates/jett_hir/src/lib.rs index 04c1b1af..bfcbe594 100644 --- a/crates/jett_hir/src/lib.rs +++ b/crates/jett_hir/src/lib.rs @@ -11,7 +11,8 @@ pub use jett_intrinsics::IntrinsicId; use jett_parser::ast::{self, Expr, Item, Module, Stmt}; use jett_resolve::{DefId, DefKind, ResolveResult}; use jett_typecheck::{ - CheckResult, CheckedBindingMode, CheckedBodyFacts, CheckedCallArgumentOrder, + CheckResult, CheckedBindingFact, CheckedBindingMode, CheckedBodyFacts, + CheckedCallArgumentOrder, CheckedCallOwnership, CheckedCalleeAccess, CheckedComptimeTypeBinding, CheckedComptimeTypeSelection, CheckedGenericCall, CheckedGenericFunctionInstantiation, CheckedGenericSpecialization, CheckedInterfaceCall, CheckedMethodCall, CheckedMethodDefinition, CheckedMethodValue, CheckedStaticSelection, @@ -23,6 +24,11 @@ use jett_types::{ ReflectionTypeInfo, ReflectionVariantInfo, Type, TypeId, TypeInterner, }; +mod call_ownership; +mod iteration_bindings; +pub use iteration_bindings::{IterationPart, ViewIterationBinding, checked_view_iteration_binding}; +#[cfg(test)] +mod call_ownership_tests; mod inline_functions; mod interface_values; #[cfg(test)] @@ -31,6 +37,17 @@ mod local_views; mod reflected_fields; mod type_validation; +pub use call_ownership::{ + ArgumentOwnership, ArgumentStaging, CallBridge, CallOwnership, CallTarget, CallerBindingFact, + CallerBindingMode, CallerOrigin, CallerViewSource, GeneratedAcquisition, + GeneratedArgumentOwnership, GeneratedArgumentStaging, GeneratedArgumentWitness, + GeneratedCallOwnership, GeneratedHandleShape, GeneratedOperation, GeneratedOriginalShape, + GeneratedSnapshotProof, ObservationProof, OwnershipLocalInfo, SourceArgumentWitness, + SourceCallOwnership, observation_data_type, validate_compiler_metadata_operand, + validate_generated_operand_tree, validate_hir_invocation, validate_invocation_target, + validate_operand_ownership, validate_program_call_ownership, validate_source_handled_operand, + validate_source_operand, +}; pub use interface_values::complete_value_conversions; pub use local_views::{ is_borrowed_local, local_view_root, validate_local_view_initializer, validate_local_views, @@ -357,6 +374,7 @@ pub enum ExpressionKind { args: Vec, /// Parameter indexes in lexical source evaluation order. evaluation_order: Vec, + ownership: CallOwnership, }, Intrinsic { intrinsic: IntrinsicId, @@ -375,11 +393,13 @@ pub enum ExpressionKind { field_validation: Option, args: Vec, evaluation_order: Vec, + ownership: CallOwnership, }, IndirectCall { callee: Box, args: Vec, evaluation_order: Vec, + ownership: CallOwnership, }, StructConstruct { struct_type: TypeId, @@ -878,6 +898,7 @@ impl Validator<'_> { function, args, evaluation_order, + .. } => { if function.index() as usize >= self.program.functions.len() { self.error(expression.span, "call references an unknown HIR function"); @@ -938,6 +959,7 @@ impl Validator<'_> { callee, args, evaluation_order, + .. } => { self.expression(callee); self.check_evaluation_order(evaluation_order, args.len(), expression.span); @@ -1613,8 +1635,11 @@ impl<'a> Lowerer<'a> { self, &function_ids, self.check.type_map.clone(), + self.check.source_type_map.clone(), self.check.debug_type_names.clone(), self.check.binding_modes.clone(), + self.check.call_ownership.clone(), + self.check.binding_facts.clone(), self.check.generic_calls.clone(), self.check.intrinsic_ids.clone(), self.check.intrinsic_type_arguments.clone(), @@ -1625,9 +1650,20 @@ impl<'a> Lowerer<'a> { self.check.method_values.clone(), self.check.struct_constructions.clone(), self.check.pipeline_step_call_types.clone(), + self.check.pipeline_step_input_types.clone(), HashMap::new(), facts_in_span(&self.check.comptime_type_bindings, span), ); + body_lowerer.ownership_context = match kind { + DeclarationKind::Verify => jett_typecheck::CheckedOwnershipContext::Verify, + DeclarationKind::Property => jett_typecheck::CheckedOwnershipContext::Property, + _ => { + body_lowerer + .parent + .error(span, "test ownership context has no test declaration"); + return None; + } + }; let mut params = Vec::with_capacity(givens.len()); for given in givens { let Some(definition) = body_lowerer @@ -1699,8 +1735,11 @@ impl<'a> Lowerer<'a> { parameter_types, return_type, expression_types, + source_expression_types, debug_type_names, binding_modes, + call_ownership, + binding_facts, generic_calls, intrinsic_ids, intrinsic_type_arguments, @@ -1711,6 +1750,7 @@ impl<'a> Lowerer<'a> { method_values, struct_constructions, pipeline_step_call_types, + pipeline_step_input_types, static_selections, comptime_type_bindings, concrete_args, @@ -1720,8 +1760,11 @@ impl<'a> Lowerer<'a> { instantiation.parameter_types.clone(), instantiation.return_type, instantiation.type_map.clone(), + instantiation.source_type_map.clone(), instantiation.debug_type_names.clone(), instantiation.binding_modes.clone(), + instantiation.call_ownership.clone(), + instantiation.binding_facts.clone(), instantiation.generic_calls.clone(), instantiation.intrinsic_ids.clone(), instantiation.intrinsic_type_arguments.clone(), @@ -1732,6 +1775,7 @@ impl<'a> Lowerer<'a> { instantiation.method_values.clone(), instantiation.struct_constructions.clone(), instantiation.pipeline_step_call_types.clone(), + instantiation.pipeline_step_input_types.clone(), instantiation.static_selections.clone(), instantiation.comptime_type_bindings.clone(), instantiation.concrete_args.clone(), @@ -1742,8 +1786,11 @@ impl<'a> Lowerer<'a> { method.parameter_types.clone(), method.return_type, self.check.type_map.clone(), + self.check.source_type_map.clone(), self.check.debug_type_names.clone(), self.check.binding_modes.clone(), + self.check.call_ownership.clone(), + self.check.binding_facts.clone(), self.check.generic_calls.clone(), self.check.intrinsic_ids.clone(), self.check.intrinsic_type_arguments.clone(), @@ -1754,6 +1801,7 @@ impl<'a> Lowerer<'a> { self.check.method_values.clone(), self.check.struct_constructions.clone(), self.check.pipeline_step_call_types.clone(), + self.check.pipeline_step_input_types.clone(), HashMap::new(), facts_in_span(&self.check.comptime_type_bindings, source.function.span), Vec::new(), @@ -1792,8 +1840,11 @@ impl<'a> Lowerer<'a> { parameter_types, return_type, self.check.type_map.clone(), + self.check.source_type_map.clone(), self.check.debug_type_names.clone(), self.check.binding_modes.clone(), + self.check.call_ownership.clone(), + self.check.binding_facts.clone(), self.check.generic_calls.clone(), self.check.intrinsic_ids.clone(), self.check.intrinsic_type_arguments.clone(), @@ -1804,6 +1855,7 @@ impl<'a> Lowerer<'a> { self.check.method_values.clone(), self.check.struct_constructions.clone(), self.check.pipeline_step_call_types.clone(), + self.check.pipeline_step_input_types.clone(), HashMap::new(), facts_in_span(&self.check.comptime_type_bindings, source.function.span), Vec::new(), @@ -1823,8 +1875,11 @@ impl<'a> Lowerer<'a> { self, &function_ids, expression_types, + source_expression_types, debug_type_names, binding_modes, + call_ownership, + binding_facts, generic_calls, intrinsic_ids, intrinsic_type_arguments, @@ -1835,6 +1890,7 @@ impl<'a> Lowerer<'a> { method_values, struct_constructions, pipeline_step_call_types, + pipeline_step_input_types, static_selections, comptime_type_bindings, ); @@ -1870,6 +1926,12 @@ impl<'a> Lowerer<'a> { span: param.span, }); } + body_lowerer.view_parameter_locals.extend( + params + .iter() + .filter(|parameter| parameter.mode == ParamMode::View) + .map(|parameter| parameter.local), + ); let body = body_lowerer.lower_block(&source.function.body); body_lowerer.reject_unconsumed_static_selections(); body_lowerer.reject_unconsumed_comptime_type_bindings(); @@ -1983,8 +2045,11 @@ impl<'a> Lowerer<'a> { self, &function_ids, self.check.type_map.clone(), + self.check.source_type_map.clone(), self.check.debug_type_names.clone(), self.check.binding_modes.clone(), + self.check.call_ownership.clone(), + self.check.binding_facts.clone(), self.check.generic_calls.clone(), self.check.intrinsic_ids.clone(), self.check.intrinsic_type_arguments.clone(), @@ -1995,6 +2060,7 @@ impl<'a> Lowerer<'a> { self.check.method_values.clone(), self.check.struct_constructions.clone(), self.check.pipeline_step_call_types.clone(), + self.check.pipeline_step_input_types.clone(), HashMap::new(), constructor_bindings, ); @@ -2198,8 +2264,11 @@ impl<'a> Lowerer<'a> { let function_ids = self.function_ids.clone(); let expression_types = self.check.type_map.clone(); + let source_expression_types = self.check.source_type_map.clone(); let debug_type_names = self.check.debug_type_names.clone(); let binding_modes = self.check.binding_modes.clone(); + let call_ownership = self.check.call_ownership.clone(); + let binding_facts = self.check.binding_facts.clone(); let generic_calls = self.check.generic_calls.clone(); let intrinsic_ids = self.check.intrinsic_ids.clone(); let intrinsic_type_arguments = self.check.intrinsic_type_arguments.clone(); @@ -2210,6 +2279,7 @@ impl<'a> Lowerer<'a> { let method_values = self.check.method_values.clone(); let struct_constructions = self.check.struct_constructions.clone(); let pipeline_step_call_types = self.check.pipeline_step_call_types.clone(); + let pipeline_step_input_types = self.check.pipeline_step_input_types.clone(); let static_selections = HashMap::new(); let comptime_type_bindings = facts_in_span(&self.check.comptime_type_bindings, source.handler.span); @@ -2217,8 +2287,11 @@ impl<'a> Lowerer<'a> { self, &function_ids, expression_types, + source_expression_types, debug_type_names, binding_modes, + call_ownership, + binding_facts, generic_calls, intrinsic_ids, intrinsic_type_arguments, @@ -2229,6 +2302,7 @@ impl<'a> Lowerer<'a> { method_values, struct_constructions, pipeline_step_call_types, + pipeline_step_input_types, static_selections, comptime_type_bindings, ); @@ -2339,6 +2413,12 @@ impl<'a> Lowerer<'a> { span: param.span, }); } + body_lowerer.view_parameter_locals.extend( + params + .iter() + .filter(|parameter| parameter.mode == ParamMode::View) + .map(|parameter| parameter.local), + ); let body = body_lowerer.lower_block(&source.handler.body); body_lowerer.reject_unconsumed_static_selections(); body_lowerer.reject_unconsumed_comptime_type_bindings(); @@ -2398,8 +2478,11 @@ impl<'a> Lowerer<'a> { self, &function_ids, self.check.type_map.clone(), + self.check.source_type_map.clone(), self.check.debug_type_names.clone(), self.check.binding_modes.clone(), + self.check.call_ownership.clone(), + self.check.binding_facts.clone(), self.check.generic_calls.clone(), self.check.intrinsic_ids.clone(), self.check.intrinsic_type_arguments.clone(), @@ -2410,6 +2493,7 @@ impl<'a> Lowerer<'a> { self.check.method_values.clone(), self.check.struct_constructions.clone(), self.check.pipeline_step_call_types.clone(), + self.check.pipeline_step_input_types.clone(), HashMap::new(), facts_in_span(&self.check.comptime_type_bindings, source.alias.span), ); @@ -2492,8 +2576,12 @@ struct BodyLowerer<'lowerer, 'program> { parent: &'lowerer mut Lowerer<'program>, function_ids: &'lowerer HashMap, expression_types: HashMap, + source_expression_types: HashMap, + source_projection_roots: HashMap, debug_type_names: HashMap, binding_modes: HashMap, + call_ownership: HashMap, + binding_facts: HashMap, generic_calls: HashMap, intrinsic_ids: HashMap, intrinsic_type_arguments: HashMap>, @@ -2504,6 +2592,7 @@ struct BodyLowerer<'lowerer, 'program> { method_values: HashMap, struct_constructions: HashMap, pipeline_step_call_types: HashMap, + pipeline_step_input_types: HashMap, static_selections: HashMap, comptime_type_bindings: HashMap>, consumed_static_selections: HashSet, @@ -2513,6 +2602,9 @@ struct BodyLowerer<'lowerer, 'program> { visible_bindings: Vec>, scoped_type_bindings: Vec, return_type: Option, + ownership_context: jett_typecheck::CheckedOwnershipContext, + view_parameter_locals: HashSet, + view_iteration_bindings: HashMap, } impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { @@ -2520,8 +2612,11 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { parent: &'lowerer mut Lowerer<'program>, function_ids: &'lowerer HashMap, expression_types: HashMap, + source_expression_types: HashMap, debug_type_names: HashMap, binding_modes: HashMap, + call_ownership: HashMap, + binding_facts: HashMap, generic_calls: HashMap, intrinsic_ids: HashMap, intrinsic_type_arguments: HashMap>, @@ -2532,6 +2627,7 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { method_values: HashMap, struct_constructions: HashMap, pipeline_step_call_types: HashMap, + pipeline_step_input_types: HashMap, static_selections: HashMap, comptime_type_bindings: HashMap>, ) -> Self { @@ -2539,8 +2635,12 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { parent, function_ids, expression_types, + source_expression_types, + source_projection_roots: HashMap::new(), debug_type_names, binding_modes, + call_ownership, + binding_facts, generic_calls, intrinsic_ids, intrinsic_type_arguments, @@ -2551,6 +2651,7 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { method_values, struct_constructions, pipeline_step_call_types, + pipeline_step_input_types, static_selections, comptime_type_bindings, consumed_static_selections: HashSet::new(), @@ -2560,6 +2661,9 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { visible_bindings: vec![HashMap::new()], scoped_type_bindings: Vec::new(), return_type: None, + ownership_context: jett_typecheck::CheckedOwnershipContext::Ordinary, + view_parameter_locals: HashSet::new(), + view_iteration_bindings: HashMap::new(), } } @@ -2864,6 +2968,55 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { 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 @@ -2883,6 +3036,7 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { } else { self.lower_block(&loop_stmt.body) }; + self.view_iteration_bindings = saved_iteration_bindings; self.visible_bindings.pop(); ( StatementKind::For { @@ -2958,10 +3112,16 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { (StatementKind::Trace(local), trace.span) } Stmt::Breakpoint(point) => { + let enclosing_context = std::mem::replace( + &mut self.ownership_context, + jett_typecheck::CheckedOwnershipContext::BreakpointExpression, + ); let condition = match &point.condition { - Some(value) => Some(self.lower_expression(value)?), - None => None, + Some(value) => self.lower_expression(value).map(Some), + None => Some(None), }; + self.ownership_context = enclosing_context; + let condition = condition?; let mut visible = std::collections::BTreeMap::new(); for scope in &self.visible_bindings { visible.extend(scope.iter().map(|(name, local)| (name, *local))); @@ -3180,8 +3340,11 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { ) -> Block { let CheckedBodyFacts { type_map, + source_type_map, debug_type_names, binding_modes, + call_ownership, + binding_facts, generic_calls, intrinsic_ids, intrinsic_type_arguments, @@ -3192,14 +3355,19 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { method_values, struct_constructions, pipeline_step_call_types, + pipeline_step_input_types, static_selections, comptime_type_bindings, } = facts; let saved_expression_types = std::mem::replace(&mut self.expression_types, type_map); + let saved_source_expression_types = + std::mem::replace(&mut self.source_expression_types, source_type_map); let saved_debug_type_names = std::mem::replace(&mut self.debug_type_names, debug_type_names); let saved_binding_modes = std::mem::replace(&mut self.binding_modes, binding_modes); + let saved_call_ownership = std::mem::replace(&mut self.call_ownership, call_ownership); + let saved_binding_facts = std::mem::replace(&mut self.binding_facts, binding_facts); let saved_generic_calls = std::mem::replace(&mut self.generic_calls, generic_calls); let saved_intrinsic_ids = std::mem::replace(&mut self.intrinsic_ids, intrinsic_ids); let saved_intrinsic_type_arguments = @@ -3217,6 +3385,10 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { std::mem::replace(&mut self.struct_constructions, struct_constructions); let saved_pipeline_step_call_types = std::mem::replace(&mut self.pipeline_step_call_types, pipeline_step_call_types); + let saved_pipeline_step_input_types = std::mem::replace( + &mut self.pipeline_step_input_types, + pipeline_step_input_types, + ); let saved_static_selections = std::mem::replace(&mut self.static_selections, static_selections); let saved_comptime_type_bindings = @@ -3231,8 +3403,11 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { self.local_ids = saved_local_ids; self.expression_types = saved_expression_types; + self.source_expression_types = saved_source_expression_types; self.debug_type_names = saved_debug_type_names; self.binding_modes = saved_binding_modes; + self.call_ownership = saved_call_ownership; + self.binding_facts = saved_binding_facts; self.generic_calls = saved_generic_calls; self.intrinsic_ids = saved_intrinsic_ids; self.intrinsic_type_arguments = saved_intrinsic_type_arguments; @@ -3243,6 +3418,7 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { self.method_values = saved_method_values; self.struct_constructions = saved_struct_constructions; self.pipeline_step_call_types = saved_pipeline_step_call_types; + self.pipeline_step_input_types = saved_pipeline_step_input_types; self.static_selections = saved_static_selections; self.comptime_type_bindings = saved_comptime_type_bindings; self.consumed_static_selections = saved_consumed_static; @@ -3346,11 +3522,26 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { ); return None; }; + let (parameter_types, _) = self + .ownership_function_parameters(function) + .map_err(|message| self.parent.error(expression.span(), message)) + .ok()?; + let args = vec![value]; + let ownership = self.generated_ownership( + GeneratedOperation::Display { method: function }, + &args, + ¶meter_types, + &[CheckedCalleeAccess::View], + TypeInterner::STRING, + &[0], + expression.span(), + )?; Some(Expression { kind: ExpressionKind::DisplayResult(Box::new(Expression { kind: ExpressionKind::Call { + ownership, function, - args: vec![value], + args, evaluation_order: vec![0], }, ty: TypeInterner::STRING, @@ -3364,6 +3555,9 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { fn lower_expression(&mut self, expression: &Expr) -> Option { let span = expression.span(); let ty = self.expression_type(expression)?; + if let Some(root) = call_ownership::source_projection_ast_root(expression) { + self.source_projection_roots.insert(span, root.clone()); + } let kind = match expression { Expr::IntLiteral(value, _) => ExpressionKind::Int(*value), Expr::FloatLiteral(value, _) => ExpressionKind::Float(*value), @@ -3399,10 +3593,25 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { 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: vec![self.lower_expression(left)?, self.lower_expression(right)?], + args, evaluation_order: vec![0, 1], }, ty, @@ -3609,7 +3818,20 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { } } 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 { @@ -3981,9 +4203,18 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { method: call.method_index, }; let function = self.function_ids.get(&key).copied()?; + self.check_source_actuals(call_span, args)?; let (args, evaluation_order) = self.lower_arguments_in_parameter_order(args, call_span)?; + let ownership = self.source_ownership(call_span, &args, &evaluation_order)?; return Some(ExpressionKind::Call { + ownership: self.finish_call_ownership( + ownership, + Some(function), + None, + &args, + call_span, + )?, function, args, evaluation_order, @@ -4078,6 +4309,8 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { let (mut lowered_args, mut evaluation_order) = 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)?; 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 @@ -4087,14 +4320,29 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { && self.is_checked_function_value(callee) { return Some(ExpressionKind::IndirectCall { + ownership: self.finish_call_ownership( + ownership, + None, + None, + &lowered_args, + call_span, + )?, callee: Box::new(self.lower_expression(callee)?), args: lowered_args, evaluation_order, }); } if source_call { + let function = self.resolve_user_call_target(callee, call_span)?; Some(ExpressionKind::Call { - function: self.resolve_user_call_target(callee, call_span)?, + ownership: self.finish_call_ownership( + ownership, + Some(function), + None, + &lowered_args, + call_span, + )?, + function, args: lowered_args, evaluation_order, }) @@ -4125,6 +4373,13 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { return None; }; return Some(ExpressionKind::Call { + ownership: self.finish_call_ownership( + ownership, + Some(function), + None, + &lowered_args, + call_span, + )?, function, args: lowered_args, evaluation_order, @@ -4146,6 +4401,13 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { self.trusted_stdlib_generic_function("json", name, call_span) { return Some(ExpressionKind::Call { + ownership: self.finish_call_ownership( + ownership, + Some(function), + None, + &lowered_args, + call_span, + )?, function, args: lowered_args, evaluation_order, @@ -4170,6 +4432,13 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { self.trusted_stdlib_generic_function("json", name, call_span) { return Some(ExpressionKind::Call { + ownership: self.finish_call_ownership( + ownership, + Some(function), + None, + &lowered_args, + call_span, + )?, function, args: lowered_args, evaluation_order, @@ -4190,6 +4459,13 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { return None; }; return Some(ExpressionKind::Call { + ownership: self.finish_call_ownership( + ownership, + Some(function), + None, + &lowered_args, + call_span, + )?, function, args: lowered_args, evaluation_order, @@ -4208,6 +4484,13 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { return None; }; return Some(ExpressionKind::Call { + ownership: self.finish_call_ownership( + ownership, + Some(function), + None, + &lowered_args, + call_span, + )?, function, args: lowered_args, evaluation_order, @@ -4231,6 +4514,13 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { return None; }; return Some(ExpressionKind::Call { + ownership: self.finish_call_ownership( + ownership, + Some(function), + None, + &lowered_args, + call_span, + )?, function, args: lowered_args, evaluation_order, @@ -4244,6 +4534,7 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { && let Some(kind) = self.lower_primitive_json_serialization( type_arguments[0], &lowered_args[0], + &ownership, call_span, ) { @@ -4416,6 +4707,13 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { let field_validation = self.reflected_read_validation(intrinsic, &type_arguments, call_span)?; Some(ExpressionKind::Intrinsic { + ownership: self.finish_call_ownership( + ownership, + None, + Some(intrinsic), + &lowered_args, + call_span, + )?, intrinsic, type_arguments, reflection_arguments, @@ -5681,12 +5979,22 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { &mut self, ty: TypeId, value: &Expression, + ownership: &CallOwnership, span: Span, ) -> Option { if matches!(self.parent.check.interner.resolve(ty), Type::Nothing) { + let function = self.trusted_stdlib_function("json", "json_serialize_native_nothing")?; + let args = vec![value.clone()]; return Some(ExpressionKind::Call { - function: self.trusted_stdlib_function("json", "json_serialize_native_nothing")?, - args: vec![value.clone()], + ownership: self.finish_call_ownership( + ownership.clone(), + Some(function), + None, + &args, + span, + )?, + function, + args, evaluation_order: vec![0], }); } @@ -5754,13 +6062,21 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { ty: tree_type, span, }; + let args = vec![Expression { + kind: ExpressionKind::View(Box::new(tree)), + ty: tree_type, + span, + }]; Some(ExpressionKind::Call { - function, - args: vec![Expression { - kind: ExpressionKind::View(Box::new(tree)), - ty: tree_type, + ownership: self.finish_call_ownership( + ownership.clone(), + Some(function), + None, + &args, span, - }], + )?, + function, + args, evaluation_order: vec![0], }) } @@ -6153,7 +6469,7 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { let mut current = self.lower_expression(initial)?; for (index, step) in steps.iter().enumerate() { let output_type = if let Some(next_step) = steps.get(index + 1) { - let Some(ty) = self.expression_types.get(&next_step.span).copied() else { + let Some(ty) = self.pipeline_step_input_types.get(&next_step.span).copied() else { self.parent .error(next_step.span, "pipeline step has no checked input type"); return None; @@ -6190,6 +6506,9 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { output_type: TypeId, ) -> Option { let (callee, extra_args, piped_as_view) = Self::pipeline_step_call_parts(step); + if self.enum_constructor_variant(callee, output_type).is_none() { + self.check_pipeline_actuals(step.span, &piped, piped_as_view, extra_args)?; + } let piped = if piped_as_view { Expression { ty: piped.ty, @@ -6213,18 +6532,30 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { && !self.intrinsic_ids.contains_key(&step.span) && self.is_checked_function_value(callee) { + let ownership = self.source_ownership(step.span, &args, &evaluation_order)?; ExpressionKind::IndirectCall { + ownership: self.finish_call_ownership(ownership, None, None, &args, step.span)?, callee: Box::new(self.lower_expression(callee)?), args, evaluation_order, } } else if source_call { + let function = self.resolve_user_call_target(callee, step.span)?; + let ownership = self.source_ownership(step.span, &args, &evaluation_order)?; ExpressionKind::Call { - function: self.resolve_user_call_target(callee, step.span)?, + ownership: self.finish_call_ownership( + ownership, + Some(function), + None, + &args, + step.span, + )?, + function, args, evaluation_order, } } else { + let ownership = self.source_ownership(step.span, &args, &evaluation_order)?; let has_explicit_type_arguments = Self::has_explicit_type_arguments(&step.function); let intrinsic = self.checked_intrinsic_id(step.span)?; let type_arguments = @@ -6256,6 +6587,13 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { let field_validation = self.reflected_read_validation(intrinsic, &type_arguments, step.span)?; ExpressionKind::Intrinsic { + ownership: self.finish_call_ownership( + ownership, + None, + Some(intrinsic), + &args, + step.span, + )?, intrinsic, type_arguments, reflection_arguments, @@ -7772,6 +8110,7 @@ function main() returns int64: function, args, evaluation_order, + .. }, .. })) = &main.body.statements[1].kind @@ -7824,6 +8163,7 @@ function main() returns int64: function, args, evaluation_order, + .. }, .. })) = &main.body.statements[1].kind @@ -8575,6 +8915,18 @@ function callbacks() returns secret[list[Callback]]: for kind in [ ExpressionKind::Local(LocalId::new(0)), ExpressionKind::Call { + ownership: CallOwnership::generated( + GeneratedOperation::NativeSuite { + function: FunctionId::new(0), + }, + &[], + &[], + &[], + TypeInterner::NOTHING, + &[], + &types, + ) + .unwrap(), function: FunctionId::new(0), args: vec![], evaluation_order: vec![], @@ -10233,6 +10585,18 @@ function main() returns nothing: ( active, ExpressionKind::Call { + ownership: CallOwnership::generated( + GeneratedOperation::NativeSuite { + function: FunctionId::new(0), + }, + &[], + &[], + &[], + TypeInterner::NOTHING, + &[], + &types, + ) + .unwrap(), function: FunctionId::new(0), args: vec![], evaluation_order: vec![], @@ -10778,6 +11142,7 @@ function direct() returns int64: callee, args, evaluation_order, + .. } = &returned_value(name).kind else { panic!("expected indirect call in {name}"); @@ -10833,6 +11198,7 @@ function main() returns int64: callee, args, evaluation_order, + .. }, .. })) = &program.functions[1].body.statements[2].kind @@ -11102,6 +11468,7 @@ function main() returns int64: function, args, evaluation_order, + .. } = &value.kind else { panic!("expected lowered generic pipeline step"); @@ -11118,6 +11485,7 @@ function main() returns int64: function, args, evaluation_order, + .. }, .. })) = &main.body.statements[2].kind @@ -11130,6 +11498,7 @@ function main() returns int64: function, args, evaluation_order, + .. } = &args[0].kind else { panic!("expected concrete method pipeline step"); @@ -11160,6 +11529,7 @@ function main() returns int64: callee, args, evaluation_order, + .. }, .. })) = &program.functions[function_index].body.statements[statement_index].kind @@ -11216,6 +11586,7 @@ function grouped_without_arguments() returns int64: callee, args, evaluation_order, + .. }, .. })) = &function @@ -11652,4 +12023,195 @@ function precise() returns Session at empty: ); } } + + const VIEWED_ITERATION_CALL_SOURCE: &str = r#"namespace app +struct Item: + code: int64 +function inspect(view item: Item) returns int64: + return item.code +function text_size(view text: string) returns int64: + return 1 +function list_items(view items: list[Item]) returns int64: + mutable int64 total = 0 + for item in view items: + total = total + inspect(view item) + return total +function map_items(view items: map[string, Item]) returns int64: + mutable int64 total = 0 + for key, item in view items: + total = total + text_size(view key) + inspect(view item) + return total +function strings(view text: string) returns int64: + mutable int64 total = 0 + for item in view text: + total = total + text_size(view item) + return total +function set_items(view items: set[string]) returns int64: + mutable int64 total = 0 + for item in view items: + total = total + text_size(view item) + return total +function owned_items(items: list[Item]) returns int64: + mutable int64 total = 0 + for item in items: + total = total + inspect(item) + return total +"#; + + #[test] + fn viewed_loop_caller_origins_use_typed_iteration_backing_not_abi_parameters() { + let (program, checked) = lower_source_with_check(VIEWED_ITERATION_CALL_SOURCE, false); + validate_backend_types(&program, &checked.interner).expect("exact viewed iteration source"); + for name in ["list_items", "map_items", "strings", "set_items"] { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + let StatementKind::For { + key, + value, + by_view, + .. + } = &function.body.statements[1].kind + else { + panic!("source For") + }; + assert!(*by_view); + assert!(!function.params.iter().any(|param| param.local == *key)); + assert!( + !function + .params + .iter() + .any(|param| Some(param.local) == *value) + ); + assert!(function.locals[key.index() as usize].view_source.is_none()); + } + let owned = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "owned_items") + .unwrap(); + assert!(matches!( + owned.body.statements[1].kind, + StatementKind::For { by_view: false, .. } + )); + } + + #[test] + fn viewed_loop_proof_stays_in_exact_body_and_rejects_forged_binders() { + let (program, checked) = lower_source_with_check(VIEWED_ITERATION_CALL_SOURCE, false); + let index = program + .functions + .iter() + .position(|function| function.identity.declaration.name == "list_items") + .unwrap(); + let original_loop = program.functions[index].body.statements[1].clone(); + for case in 0..6 { + let mut changed = program.clone(); + let function = &mut changed.functions[index]; + let StatementKind::For { + key, by_view, body, .. + } = &mut function.body.statements[1].kind + else { + unreachable!() + }; + match case { + 0 => *by_view = false, + 1 => { + let escaped = body.statements[0].clone(); + function.body.statements.insert(2, escaped); + } + 2 => *key = function.params[0].local, + 3 => function.locals[key.index() as usize].ty = TypeInterner::STRING, + 4 => { + let escaped = body.statements[0].clone(); + function.body.statements[1] = escaped; + } + 5 => function.body.statements.insert(2, original_loop.clone()), + _ => unreachable!(), + } + let errors = validate_backend_types(&changed, &checked.interner) + .expect_err("forged viewed loop backing"); + assert!( + errors.iter().any(|error| error + .message + .contains("borrowed binding lost its immediate source") + || error.message.contains("viewed iteration binder") + || error.message.contains("viewed iteration binding type") + || error + .message + .contains("iteration binding is declared more than once")), + "{case}: {errors:?}" + ); + } + assert!(matches!( + original_loop.kind, + StatementKind::For { by_view: true, .. } + )); + } + + #[test] + fn nested_viewed_iteration_preserves_each_immediate_binder_and_forwarded_alias() { + let (program, checked) = lower_source_with_check( + r#"namespace app +struct Item: + code: int64 +function inspect(view item: Item) returns int64: + return item.code +function nested(view groups: list[list[Item]]) returns int64: + mutable int64 total = 0 + for items in view groups: + for item in view items: + Item alias = view item + total = total + inspect(view alias) + return total +"#, + false, + ); + validate_backend_types(&program, &checked.interner) + .expect("nested and forwarded view binding"); + let mut changed = program.clone(); + let function = changed + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "nested") + .unwrap(); + let StatementKind::For { + key: outer, body, .. + } = &mut function.body.statements[1].kind + else { + unreachable!() + }; + let StatementKind::For { key: inner, .. } = &mut body.statements[0].kind else { + unreachable!() + }; + *inner = *outer; + let errors = validate_backend_types(&changed, &checked.interner) + .expect_err("nested loop cannot reuse its parent binder"); + assert!( + errors + .iter() + .any(|error| error.message.contains("fresh immutable local")), + "{errors:?}" + ); + } + + #[test] + fn copyable_viewed_loop_binding_can_be_captured_as_an_ordinary_copy() { + let (program, checked) = lower_source_with_check( + r#"namespace app +function copy_capture(view items: list[int64]) returns int64: + mutable int64 total = 0 + for item in view items: + function() returns int64 callback = function() returns int64: + return item + total = total + callback() + return total +"#, + false, + ); + validate_backend_types(&program, &checked.interner) + .expect("copyable viewed iteration capture"); + } } diff --git a/crates/jett_hir/src/local_views.rs b/crates/jett_hir/src/local_views.rs index 8f027692..239ae1da 100644 --- a/crates/jett_hir/src/local_views.rs +++ b/crates/jett_hir/src/local_views.rs @@ -193,6 +193,73 @@ fn declared_field_is_secret(types: &TypeInterner, mut ty: TypeId) -> Option Result<(), &'static str> { + let ExpressionKind::Field { + base, + owner_type, + field, + } = &value.kind + else { + return Err("borrowed projection metadata does not describe a field"); + }; + if owner_type.index() as usize >= types.len() || base.ty.index() as usize >= types.len() { + return Err("native borrowed projection has an invalid field owner"); + } + let secret_owner = matches!( + types.resolve(base.ty), + Type::Secret(inner) + if *inner == *owner_type + && matches!( + types.resolve(*inner), + Type::Struct(_) | Type::Bitfield(_) | Type::MachineState { .. } + ) + ); + if base.ty != *owner_type && !secret_owner { + return Err("native borrowed projection has an invalid field owner"); + } + let field_type = match types.resolve(*owner_type) { + Type::Struct(struct_id) => types + .resolve_struct(*struct_id) + .fields + .get(field.index() as usize) + .map(|(_, ty)| *ty), + Type::Bitfield(bitfield_id) => types + .resolve_bitfield(*bitfield_id) + .fields + .get(field.index() as usize) + .map(|field| field.ty), + Type::MachineState { machine, state } => { + let Some(state) = types.resolve_machine(*machine).state(*state) else { + return Err("native borrowed projection has an invalid machine state"); + }; + state.fields.get(field.index() as usize).map(|(_, ty)| *ty) + } + _ => { + return Err( + "native borrowed projection requires ordinary struct, bitfield or state-qualified machine fields", + ); + } + }; + let Some(field_type) = field_type else { + return Err("native borrowed projection has an invalid field index"); + }; + let Some(already_secret) = declared_field_is_secret(types, field_type) else { + return Err("native borrowed projection has an invalid field endpoint type"); + }; + let valid_result = if secret_owner && field_type != TypeInterner::NOTHING && !already_secret { + matches!(types.resolve(value.ty), Type::Secret(inner) if *inner == field_type) + } else { + value.ty == field_type + }; + if !valid_result { + return Err("native borrowed projection has an invalid field endpoint type"); + } + Ok(()) +} + pub fn validate_local_view_initializer( value: &Expression, source: LocalId, @@ -214,68 +281,8 @@ pub fn validate_local_view_initializer( { return Ok(()); } - ExpressionKind::Field { - base, - owner_type, - field, - } => { - if owner_type.index() as usize >= types.len() - || base.ty.index() as usize >= types.len() - { - return Err("native borrowed projection has an invalid field owner"); - } - let secret_owner = matches!( - types.resolve(base.ty), - Type::Secret(inner) - if *inner == *owner_type - && matches!( - types.resolve(*inner), - Type::Struct(_) | Type::Bitfield(_) | Type::MachineState { .. } - ) - ); - if base.ty != *owner_type && !secret_owner { - return Err("native borrowed projection has an invalid field owner"); - } - let field_type = match types.resolve(*owner_type) { - Type::Struct(struct_id) => types - .resolve_struct(*struct_id) - .fields - .get(field.index() as usize) - .map(|(_, ty)| *ty), - Type::Bitfield(bitfield_id) => types - .resolve_bitfield(*bitfield_id) - .fields - .get(field.index() as usize) - .map(|field| field.ty), - Type::MachineState { machine, state } => { - let Some(state) = types.resolve_machine(*machine).state(*state) else { - return Err("native borrowed projection has an invalid machine state"); - }; - state.fields.get(field.index() as usize).map(|(_, ty)| *ty) - } - _ => { - return Err( - "native borrowed projection requires ordinary struct, bitfield or state-qualified machine fields", - ); - } - }; - let Some(field_type) = field_type else { - return Err("native borrowed projection has an invalid field index"); - }; - let Some(already_secret) = declared_field_is_secret(types, field_type) else { - return Err("native borrowed projection has an invalid field endpoint type"); - }; - let valid_result = if secret_owner - && field_type != TypeInterner::NOTHING - && !already_secret - { - matches!(types.resolve(value.ty), Type::Secret(inner) if *inner == field_type) - } else { - value.ty == field_type - }; - if !valid_result { - return Err("native borrowed projection has an invalid field endpoint type"); - } + ExpressionKind::Field { base, .. } => { + validate_field_projection(value, types)?; value = base; } ExpressionKind::View(inner) => { diff --git a/crates/jett_hir/src/reflected_fields.rs b/crates/jett_hir/src/reflected_fields.rs index e2725616..e60d39ef 100644 --- a/crates/jett_hir/src/reflected_fields.rs +++ b/crates/jett_hir/src/reflected_fields.rs @@ -2052,6 +2052,7 @@ function pipeline_operands(view value: Record, view field: TypeField) returns in function, args, evaluation_order, + .. } = &returned_read(&program, "pipeline_operands").kind else { panic!("next source call"); @@ -2397,6 +2398,7 @@ function arg_order(index: int64) returns string: function, args, evaluation_order, + .. } = &returned_read(&program, name).kind else { panic!("following call"); 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 cb8d3eb0..779e48ee 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 @@ -1,5 +1,6 @@ --- source: crates/jett_hir/src/lib.rs +assertion_line: 7203 expression: program --- Program { @@ -316,6 +317,147 @@ Program { 1, 0, ], + ownership: Source( + SourceCallOwnership { + target: Function( + FunctionId( + 0, + ), + ), + shape: Function { + signature_type: TypeId( + 47, + ), + }, + context: Ordinary, + arguments: [ + ArgumentOwnership { + source_span: Span { + file: FileId( + 0, + ), + start: 162, + end: 163, + }, + source_index: 1, + parameter_index: 0, + actual_type: TypeId( + 3, + ), + parameter_type: TypeId( + 3, + ), + syntax: Bare, + origin: OwnedExpression, + callee_access: Owned, + physical_access: Owned, + effect: Copy, + staging: Original, + witness: SourceArgumentWitness { + source_span: Span { + file: FileId( + 0, + ), + start: 162, + end: 163, + }, + source_index: 1, + parameter_index: 0, + actual_type: TypeId( + 3, + ), + occurrence_type: TypeId( + 3, + ), + parameter_type: TypeId( + 3, + ), + syntax: Bare, + origin: OwnedExpression, + callee_access: Owned, + effect: Copy, + context: Ordinary, + observation: None, + handled_result: None, + projection_root: None, + }, + retained_snapshot: None, + }, + ArgumentOwnership { + source_span: Span { + file: FileId( + 0, + ), + start: 153, + end: 154, + }, + source_index: 0, + parameter_index: 1, + actual_type: TypeId( + 3, + ), + parameter_type: TypeId( + 3, + ), + syntax: Bare, + origin: OwnedExpression, + callee_access: Owned, + physical_access: Owned, + effect: Copy, + staging: Original, + witness: SourceArgumentWitness { + source_span: Span { + file: FileId( + 0, + ), + start: 153, + end: 154, + }, + source_index: 0, + parameter_index: 1, + actual_type: TypeId( + 3, + ), + occurrence_type: TypeId( + 3, + ), + parameter_type: TypeId( + 3, + ), + syntax: Bare, + origin: OwnedExpression, + callee_access: Owned, + effect: Copy, + context: Ordinary, + observation: None, + handled_result: None, + projection_root: None, + }, + retained_snapshot: None, + }, + ], + bridge: Direct, + generated_operands: [], + certificate: SourceCallCertificate { + result_type: TypeId( + 3, + ), + generated_operands: [], + target: Function( + FunctionId( + 0, + ), + ), + shape: Function { + signature_type: TypeId( + 47, + ), + }, + context: Ordinary, + bridge: Direct, + }, + }, + ), }, ty: TypeId( 3, 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 a03aedbe..57b08091 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 @@ -1,5 +1,6 @@ --- source: crates/jett_hir/src/lib.rs +assertion_line: 7225 expression: program --- Program { @@ -309,6 +310,95 @@ Program { evaluation_order: [ 0, ], + ownership: Source( + SourceCallOwnership { + target: Function( + FunctionId( + 0, + ), + ), + shape: Function { + signature_type: TypeId( + 49, + ), + }, + context: Ordinary, + arguments: [ + ArgumentOwnership { + source_span: Span { + file: FileId( + 0, + ), + start: 208, + end: 211, + }, + source_index: 0, + parameter_index: 0, + actual_type: TypeId( + 10, + ), + parameter_type: TypeId( + 10, + ), + syntax: Bare, + origin: OwnedExpression, + callee_access: Owned, + physical_access: Owned, + effect: Copy, + staging: Original, + witness: SourceArgumentWitness { + source_span: Span { + file: FileId( + 0, + ), + start: 208, + end: 211, + }, + source_index: 0, + parameter_index: 0, + actual_type: TypeId( + 10, + ), + occurrence_type: TypeId( + 10, + ), + parameter_type: TypeId( + 10, + ), + syntax: Bare, + origin: OwnedExpression, + callee_access: Owned, + effect: Copy, + context: Ordinary, + observation: None, + handled_result: None, + projection_root: None, + }, + retained_snapshot: None, + }, + ], + bridge: Direct, + generated_operands: [], + certificate: SourceCallCertificate { + result_type: TypeId( + 48, + ), + generated_operands: [], + target: Function( + FunctionId( + 0, + ), + ), + shape: Function { + signature_type: TypeId( + 49, + ), + }, + context: Ordinary, + bridge: Direct, + }, + }, + ), }, ty: TypeId( 48, diff --git a/crates/jett_hir/src/type_validation.rs b/crates/jett_hir/src/type_validation.rs index 017773ec..46cda8be 100644 --- a/crates/jett_hir/src/type_validation.rs +++ b/crates/jett_hir/src/type_validation.rs @@ -23,12 +23,16 @@ pub fn validate_backend_types( ) -> Result<(), Vec> { let mut validator = BackendTypeValidator { interner, + functions: &program.functions, + ownership_locals: Vec::new(), + ownership_context: jett_typecheck::CheckedOwnershipContext::Ordinary, visited_types: HashSet::new(), visited_definitions: HashSet::new(), local_views: Vec::new(), local_types: Vec::new(), local_view_initializers: HashSet::new(), borrowed_parameters: HashSet::new(), + iteration_binders: HashSet::new(), errors: Vec::new(), }; for function in &program.functions { @@ -83,12 +87,16 @@ enum DefinitionKey { struct BackendTypeValidator<'a> { interner: &'a TypeInterner, + functions: &'a [Function], + ownership_locals: Vec, + ownership_context: jett_typecheck::CheckedOwnershipContext, visited_types: HashSet, visited_definitions: HashSet, local_views: Vec>, local_types: Vec, local_view_initializers: HashSet, borrowed_parameters: HashSet, + iteration_binders: HashSet, errors: Vec, } @@ -101,6 +109,33 @@ impl BackendTypeValidator<'_> { } fn function(&mut self, function: &Function) { + self.iteration_binders.clear(); + self.ownership_locals = function + .locals + .iter() + .map(|local| crate::OwnershipLocalInfo { + ty: local.ty, + mutable: local.mutable, + span: local.span, + view_source: local.view_source, + is_view_parameter: function + .params + .iter() + .any(|param| param.local == local.id && param.mode == crate::ParamMode::View), + view_iteration: None, + }) + .collect(); + self.ownership_context = if function.debug_kind == crate::FunctionDebugKind::Inline { + jett_typecheck::CheckedOwnershipContext::Ordinary + } else { + match function.identity.declaration.kind { + crate::DeclarationKind::Verify => jett_typecheck::CheckedOwnershipContext::Verify, + crate::DeclarationKind::Property => { + jett_typecheck::CheckedOwnershipContext::Property + } + _ => jett_typecheck::CheckedOwnershipContext::Ordinary, + } + }; self.local_views = function .locals .iter() @@ -218,9 +253,59 @@ impl BackendTypeValidator<'_> { self.expression(condition, function_name); self.block(body, function_name); } - StatementKind::For { iterable, body, .. } => { + StatementKind::For { + key, + value, + by_view, + iterable, + body, + } => { self.expression(iterable, function_name); + let saved_locals = self.ownership_locals.clone(); + for id in [*key].into_iter().chain(*value) { + if !self.iteration_binders.insert(id) { + self.error( + statement.span, + "iteration binding is declared more than once in its exact function", + ); + } + } + if *by_view { + let part = if (iterable.ty.index() as usize) < self.interner.len() + && matches!(self.interner.resolve(iterable.ty), Type::Map(..)) + { + crate::IterationPart::Key + } else { + crate::IterationPart::Element + }; + let bindings = [(*key, part)] + .into_iter() + .chain(value.map(|value| (value, crate::IterationPart::Value))); + for (id, part) in bindings { + let result = self.ownership_locals.get(id.index() as usize).copied().ok_or_else(|| "viewed iteration binder is outside its function".to_string()).and_then(|local| { + if local.mutable || local.view_source.is_some() || local.is_view_parameter || local.view_iteration.is_some() + || local.span.file != statement.span.file || local.span.start < statement.span.start || local.span.end > statement.span.end { + return Err("viewed iteration binder is not a fresh immutable local in its exact For".into()); + } + crate::checked_view_iteration_binding(self.interner, statement.span, iterable.ty, local.ty, part) + }); + match result { + Ok(proof) + if !jett_typecheck::ownership::is_implicitly_copyable( + self.interner, + proof.binder_type(), + ) => + { + self.ownership_locals[id.index() as usize].view_iteration = + Some(proof) + } + Ok(_) => {} + Err(message) => self.error(statement.span, message), + } + } + } self.block(body, function_name); + self.ownership_locals = saved_locals; } StatementKind::Match { scrutinee, arms } => { self.expression(scrutinee, function_name); @@ -235,9 +320,14 @@ impl BackendTypeValidator<'_> { } } StatementKind::Breakpoint { condition, .. } => { + let context = std::mem::replace( + &mut self.ownership_context, + jett_typecheck::CheckedOwnershipContext::BreakpointExpression, + ); if let Some(condition) = condition { self.expression(condition, function_name); } + self.ownership_context = context; } StatementKind::Scope(block) => self.block(block, function_name), StatementKind::ReflectedTypeDispatch { type_info, arms } => { @@ -259,6 +349,26 @@ impl BackendTypeValidator<'_> { } fn expression(&mut self, expression: &Expression, function_name: &str) { + if let ExpressionKind::Call { ownership, .. } + | ExpressionKind::IndirectCall { ownership, .. } + | ExpressionKind::Intrinsic { ownership, .. } = &expression.kind + { + let mut metadata_types = Vec::new(); + ownership.metadata_types(|ty| metadata_types.push(ty)); + for ty in metadata_types { + self.type_id(ty, expression.span, "call ownership metadata type".into()); + } + // Source and generated authority is checked before dead bodies/locals can be pruned. + if let Err(message) = crate::call_ownership::validate_hir_invocation( + self.functions, + &self.ownership_locals, + expression, + self.interner, + self.ownership_context, + ) { + self.error(expression.span, message); + } + } self.type_id( expression.ty, expression.span, @@ -515,6 +625,8 @@ impl BackendTypeValidator<'_> { ExpressionKind::StateIs { value, .. } => self.expression(value, function_name), ExpressionKind::InlineFunction { scoped_type_bindings, + params, + view_params, body, .. } => { @@ -528,7 +640,25 @@ impl BackendTypeValidator<'_> { ), ); } + let saved_locals = self.ownership_locals.clone(); + for local in &mut self.ownership_locals { + local.view_iteration = None; + local.is_view_parameter = false; + } + for parameter in params { + if let Some(local) = self.ownership_locals.get_mut(parameter.index() as usize) { + local.is_view_parameter = view_params.contains(parameter); + } + } + let context = std::mem::replace( + &mut self.ownership_context, + jett_typecheck::CheckedOwnershipContext::Ordinary, + ); + let enclosing_iteration_binders = std::mem::take(&mut self.iteration_binders); self.block(body, function_name); + self.iteration_binders = enclosing_iteration_binders; + self.ownership_context = context; + self.ownership_locals = saved_locals; } ExpressionKind::ActorSpawn { args, .. } => { for argument in args { @@ -554,9 +684,13 @@ impl BackendTypeValidator<'_> { ExpressionKind::ClosureRef { captures, .. } => { if captures.iter().any(|id| { (self - .local_views + .ownership_locals .get(id.index() as usize) - .is_some_and(Option::is_some) + .is_some_and(|local| local.view_iteration.is_some()) + || self + .local_views + .get(id.index() as usize) + .is_some_and(Option::is_some) || self.borrowed_parameters.contains(id)) && self.local_types.get(id.index() as usize).is_some_and(|ty| { (ty.index() as usize) < self.interner.len() @@ -1193,6 +1327,18 @@ mod tests { let statement = Statement { kind: StatementKind::Expression(Expression { kind: ExpressionKind::Intrinsic { + ownership: crate::CallOwnership::generated( + crate::GeneratedOperation::ReflectionMetadata { + intrinsic: IntrinsicId::TypeName, + }, + &[], + &[], + &[], + TypeInterner::STRING, + &[], + &interner, + ) + .unwrap(), intrinsic: IntrinsicId::TypeName, type_arguments: vec![nested_error], reflection_arguments: Vec::new(), diff --git a/crates/jett_mir/Cargo.toml b/crates/jett_mir/Cargo.toml index 22a6e530..a99ebd84 100644 --- a/crates/jett_mir/Cargo.toml +++ b/crates/jett_mir/Cargo.toml @@ -8,10 +8,10 @@ license.workspace = true [dependencies] jett_common.workspace = true jett_hir.workspace = true +jett_typecheck.workspace = true jett_types.workspace = true [dev-dependencies] jett_diagnostics.workspace = true jett_parser.workspace = true jett_resolve.workspace = true -jett_typecheck.workspace = true diff --git a/crates/jett_mir/src/breakpoint_regions.rs b/crates/jett_mir/src/breakpoint_regions.rs new file mode 100644 index 00000000..c4a69f77 --- /dev/null +++ b/crates/jett_mir/src/breakpoint_regions.rs @@ -0,0 +1,1268 @@ +//! Constructor-owned lexical context for extracted breakpoint conditions. +//! These snapshots are metadata, never execution roots or caller authority. +use super::*; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub(super) struct Site { + block: BlockId, + index: usize, +} + +#[derive(Debug, Clone, PartialEq)] +struct ArchivedCondition(hir::Statement); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct Edge { + source: BlockId, + arm: usize, + target: BlockId, +} + +#[derive(Debug, Clone, PartialEq)] +enum Endpoint { + Live(Site), + // Minted only by the exact typed Never-side selection below. The old + // endpoint is archival after compaction; active selected edges are remapped. + CanonicallyAborted { + selections: Vec, + loops: Vec, + }, +} + +#[derive(Debug, Clone, PartialEq)] +struct SelectedEdge { + block: BlockId, + source: LocalId, + tag: LocalId, + source_type: TypeId, + selected: BlockId, + span: Span, +} + +#[derive(Debug, Clone, PartialEq)] +struct SelectedLoop { + block: BlockId, + preheader: BlockId, + source: SequenceSource, + source_type: TypeId, + length: LocalId, + selected: BlockId, + span: Span, +} + +#[derive(Debug, Clone, PartialEq)] +pub(super) struct BreakpointRegion { + id: usize, + parent: Option, + owner: FunctionId, + identity: FunctionIdentity, + debug_kind: hir::FunctionDebugKind, + entry: BlockId, + origin: ArchivedCondition, + start: Site, + statements: BTreeMap, + terminators: BTreeMap, + blocks: BTreeSet, + edges: Vec, + endpoint: Endpoint, + selections: Vec, + loops: Vec, + abort_allowed: bool, +} + +#[derive(Clone, Default)] +pub(super) struct Capture { + drafts: Vec, + stack: Vec, +} + +#[derive(Clone)] +struct Draft { + parent: Option, + origin: ArchivedCondition, + start: Site, + statements: BTreeMap, + terminators: BTreeMap, + blocks: BTreeSet, + endpoint: Option, +} + +impl Capture { + pub(super) fn begin(&mut self, original: &hir::Statement, block: BlockId, index: usize) { + let id = self.drafts.len(); + self.drafts.push(Draft { + parent: self.stack.last().copied(), + origin: ArchivedCondition(original.clone()), + start: Site { block, index }, + statements: BTreeMap::new(), + terminators: BTreeMap::new(), + blocks: BTreeSet::from([block]), + endpoint: None, + }); + self.stack.push(id); + } + + pub(super) fn statement(&mut self, block: BlockId, index: usize, value: &Statement) { + if let Some(id) = self.stack.last().copied() { + let draft = &mut self.drafts[id]; + draft.blocks.insert(block); + draft + .statements + .insert(Site { block, index }, value.clone()); + } + } + + pub(super) fn terminator(&mut self, block: BlockId, value: &Terminator) { + if let Some(id) = self.stack.last().copied() { + let draft = &mut self.drafts[id]; + draft.blocks.insert(block); + draft.terminators.insert(block, value.clone()); + } + } + + pub(super) fn block(&mut self, block: BlockId) { + if let Some(id) = self.stack.last().copied() { + self.drafts[id].blocks.insert(block); + } + } + + pub(super) fn end(&mut self, block: BlockId, index: usize) { + if let Some(id) = self.stack.pop() { + self.drafts[id].endpoint = Some(Site { block, index }); + } + } + + pub(super) fn seal(self, function: &Function) -> Result, String> { + if !self.stack.is_empty() { + return Err("breakpoint constructor has an unfinished condition capture".into()); + } + let mut records = Vec::new(); + for (id, draft) in self.drafts.into_iter().enumerate() { + let endpoint = draft + .endpoint + .ok_or("breakpoint constructor lacks an exact final endpoint")?; + records.push(BreakpointRegion { + id, + parent: draft.parent, + owner: function.id, + identity: function.identity.clone(), + debug_kind: function.debug_kind.clone(), + entry: function.entry, + origin: draft.origin, + start: draft.start, + statements: draft.statements, + terminators: draft.terminators, + blocks: draft.blocks, + edges: Vec::new(), + endpoint: Endpoint::Live(endpoint), + selections: Vec::new(), + loops: Vec::new(), + abort_allowed: false, + }); + } + for id in (0..records.len()).rev() { + if let Some(parent) = records[id].parent { + let child_blocks = records[id].blocks.clone(); + records[parent].blocks.extend(child_blocks); + } + } + let all = edges(&function.blocks); + for record in &mut records { + record.edges = touching(&all, &record.blocks); + } + Ok(records) + } +} + +pub(super) struct VerifiedSites { + statements: BTreeSet, + terminators: BTreeSet, +} + +impl VerifiedSites { + pub(super) fn statement(&self, block: BlockId, index: usize) -> bool { + self.statements.contains(&Site { block, index }) + } + pub(super) fn terminator(&self, block: BlockId) -> bool { + self.terminators.contains(&block) + } +} + +pub(super) fn validate(function: &Function, types: &TypeInterner) -> Result { + let mut verified = VerifiedSites { + statements: BTreeSet::new(), + terminators: BTreeSet::new(), + }; + if function.breakpoint_regions.is_empty() { + if function.blocks.iter().any(|block| { + block.statements.iter().any(|statement| { + matches!( + statement.kind, + StatementKind::Breakpoint { + condition: Some(_), + .. + } + ) + }) + }) { + return Err("breakpoint condition has no exact constructor-owned endpoint".into()); + } + return Ok(verified); + } + let mut ids = BTreeSet::new(); + let mut endpoints = BTreeSet::new(); + let all = edges(&function.blocks); + for record in &function.breakpoint_regions { + if !ids.insert(record.id) + || record.owner != function.id + || record.identity != function.identity + || record.debug_kind != function.debug_kind + || record.entry != function.entry + { + return Err("breakpoint extraction has duplicate or foreign function ownership".into()); + } + if let Some(parent) = record.parent + && (parent >= record.id || !function.breakpoint_regions.iter().any(|r| r.id == parent)) + { + return Err("breakpoint extraction has an orphan lexical parent".into()); + } + let hir::StatementKind::Breakpoint { + condition: Some(condition), + .. + } = &record.origin.0.kind + else { + return Err("breakpoint extraction lacks its archived checked condition".into()); + }; + if condition.ty != TypeInterner::BOOL + || record.blocks.is_empty() + || !record.blocks.contains(&record.start.block) + || function + .blocks + .get(record.start.block.index() as usize) + .is_none_or(|block| record.start.index > block.statements.len()) + || touching(&all, &record.blocks) != record.edges + { + return Err( + "breakpoint extraction changes its condition or exact CFG boundaries".into(), + ); + } + for (&site, expected) in &record.statements { + let actual = function + .blocks + .get(site.block.index() as usize) + .filter(|block| block.id == site.block) + .and_then(|block| block.statements.get(site.index)); + if !record.blocks.contains(&site.block) + || !verified.statements.insert(site) + || actual.is_none_or(|actual| !statement_equal(actual, expected)) + { + return Err( + "breakpoint extraction changes or duplicates an exact statement occurrence" + .into(), + ); + } + } + for (&block, expected) in &record.terminators { + let actual = function + .blocks + .get(block.index() as usize) + .filter(|value| value.id == block); + if !record.blocks.contains(&block) + || !verified.terminators.insert(block) + || actual.is_none_or(|value| !terminator_equal(&value.terminator, expected)) + { + return Err( + "breakpoint extraction changes or duplicates an exact terminator occurrence" + .into(), + ); + } + } + match &record.endpoint { + Endpoint::Live(site) => { + if !endpoints.insert(*site) { + return Err("breakpoint extraction duplicates its endpoint".into()); + } + if !matches!(record.statements.get(site).map(|s| &s.kind), Some(StatementKind::Breakpoint { condition: Some(value), .. }) if value.ty == TypeInterner::BOOL) + { + return Err("breakpoint extraction loses its exact final endpoint".into()); + } + } + Endpoint::CanonicallyAborted { selections, loops } => { + if !record.abort_allowed + || (selections.is_empty() && loops.is_empty()) + || selections != &record.selections + || loops != &record.loops + { + return Err( + "breakpoint extraction has no canonical aborted-edge evidence".into(), + ); + } + } + } + for selected in &record.selections { + validate_selection(function, selected, types)?; + } + for selected in &record.loops { + validate_loop_selection(function, selected, types)?; + } + } + for block in &function.blocks { + for (index, statement) in block.statements.iter().enumerate() { + if matches!( + statement.kind, + StatementKind::Breakpoint { + condition: Some(_), + .. + } + ) && !endpoints.contains(&Site { + block: block.id, + index, + }) { + return Err("breakpoint condition has no exact constructor-owned endpoint".into()); + } + } + } + Ok(verified) +} + +fn edges(blocks: &[BasicBlock]) -> Vec { + let mut result = Vec::new(); + for block in blocks { + let mut targets = Vec::new(); + match &block.terminator.kind { + TerminatorKind::Goto(target) => targets.push(*target), + TerminatorKind::Branch { + then_block, + else_block, + .. + } => targets.extend([*then_block, *else_block]), + TerminatorKind::Switch { + variants, + otherwise, + .. + } => { + targets.extend(variants.iter().map(|(_, target, _)| *target)); + targets.extend(*otherwise); + } + TerminatorKind::ForEach { body, exit, .. } => targets.extend([*body, *exit]), + TerminatorKind::ReflectedTypeDispatch { + arms, otherwise, .. + } => { + targets.extend(arms.iter().map(|arm| arm.target)); + targets.push(*otherwise); + } + TerminatorKind::Return(_) + | TerminatorKind::Respond(_) + | TerminatorKind::Unreachable => {} + } + result.extend(targets.into_iter().enumerate().map(|(arm, target)| Edge { + source: block.id, + arm, + target, + })); + } + result +} + +fn touching(all: &[Edge], blocks: &BTreeSet) -> Vec { + all.iter() + .copied() + .filter(|edge| blocks.contains(&edge.source) || blocks.contains(&edge.target)) + .collect() +} + +// Only the canonical sequence pass constructs this finite edit description. +// Before is already authenticated; additions come from that exact ForEach, +// never from current call context, source spans or a post-mutation search. +#[derive(Clone)] +pub(super) struct SequenceEdit { + pub header: BlockId, + pub before: Terminator, + pub after: Terminator, + pub append: Vec<(BlockId, Vec)>, + pub prefix: Option<(BlockId, Vec)>, + pub redirects: Vec<(BlockId, Terminator, Terminator)>, + pub blocks: Vec, +} + +pub(super) fn sequence_transition( + function: &mut Function, + before: &Function, + edit: SequenceEdit, + types: &TypeInterner, +) -> Result<(), String> { + validate(before, types)?; + let Some(header) = before + .blocks + .get(edit.header.index() as usize) + .filter(|block| block.id == edit.header) + else { + return Err("breakpoint sequence transition has no exact header".into()); + }; + let existing = |id: BlockId| { + before + .blocks + .get(id.index() as usize) + .is_some_and(|block| block.id == id) + }; + if edit.append.iter().any(|(block, _)| !existing(*block)) + || edit + .prefix + .as_ref() + .is_some_and(|(block, _)| !existing(*block)) + || edit.redirects.iter().any(|(block, _, _)| !existing(*block)) + || edit + .blocks + .iter() + .enumerate() + .any(|(index, block)| block.id.index() as usize != before.blocks.len() + index) + { + return Err("breakpoint sequence edit has a foreign or nondense occurrence".into()); + } + if !terminator_equal(&header.terminator, &edit.before) + || !matches!(edit.before.kind, TerminatorKind::ForEach { .. }) + { + return Err("breakpoint sequence transition differs from its original ForEach".into()); + } + let mut expected = before.clone(); + expected.blocks[edit.header.index() as usize].terminator = edit.after.clone(); + for (block, values) in &edit.append { + expected.blocks[block.index() as usize] + .statements + .extend(values.clone()); + } + if let Some((block, prefix)) = &edit.prefix { + let statements = &mut expected.blocks[block.index() as usize].statements; + let mut values = prefix.clone(); + values.append(statements); + *statements = values; + } + for (block, old, new) in &edit.redirects { + if !terminator_equal(&expected.blocks[block.index() as usize].terminator, old) { + return Err("breakpoint sequence redirect has a foreign before-edge".into()); + } + expected.blocks[block.index() as usize].terminator = new.clone(); + } + expected.blocks.extend(edit.blocks.clone()); + if !blocks_equal(&expected.blocks, &function.blocks) { + return Err("breakpoint sequence transition has changes outside its canonical edit".into()); + } + let owners = before + .breakpoint_regions + .iter() + .filter(|record| record.terminators.contains_key(&edit.header)) + .map(|record| record.id) + .collect::>(); + let mut records = before.breakpoint_regions.clone(); + for record in &mut records { + if let Some((block, prefix)) = &edit.prefix { + shift(record, *block, 0, prefix.len()); + } + if record.terminators.contains_key(&edit.header) { + record.terminators.insert(edit.header, edit.after.clone()); + } + for (block, _, new) in &edit.redirects { + if record.terminators.contains_key(block) { + record.terminators.insert(*block, new.clone()); + } + } + if owners.contains(&record.id) { + if let ( + TerminatorKind::ForEach { iterable, exit, .. }, + TerminatorKind::Goto(selected), + ) = (&edit.before.kind, &edit.after.kind) + { + let Some((preheader, source, length)) = + edit.append.iter().find_map(|(block, values)| { + values.iter().find_map(|statement| { + if let StatementKind::SequenceLength { source, target } = + &statement.kind + { + Some((*block, source.clone(), *target)) + } else { + None + } + }) + }) + else { + return Err( + "breakpoint empty sequence lacks canonical length validation".into(), + ); + }; + if selected != exit || !empty_sequence(types, iterable.ty) { + return Err("breakpoint sequence selected an inhabited body".into()); + } + record.loops.push(SelectedLoop { + block: edit.header, + preheader, + source, + source_type: iterable.ty, + length, + selected: *selected, + span: edit.after.span, + }); + } + for (block, values) in &edit.append { + let start = before.blocks[block.index() as usize].statements.len(); + for (index, value) in values.iter().enumerate() { + record.statements.insert( + Site { + block: *block, + index: start + index, + }, + value.clone(), + ); + } + record.blocks.insert(*block); + } + if let Some((block, values)) = &edit.prefix { + for (index, value) in values.iter().enumerate() { + record.statements.insert( + Site { + block: *block, + index, + }, + value.clone(), + ); + } + record.blocks.insert(*block); + } + for block in &edit.blocks { + record.blocks.insert(block.id); + for (index, value) in block.statements.iter().enumerate() { + record.statements.insert( + Site { + block: block.id, + index, + }, + value.clone(), + ); + } + record + .terminators + .insert(block.id, block.terminator.clone()); + } + } + } + // Ancestors include child CFG boundaries without claiming child sites. + for id in (0..records.len()).rev() { + if let Some(parent) = records[id].parent { + let blocks = records[id].blocks.clone(); + let parent_record = records + .iter_mut() + .find(|record| record.id == parent) + .ok_or("breakpoint preparation loses a parent")?; + parent_record.blocks.extend(blocks); + } + } + for record in &mut records { + record.edges = touching(&edges(&expected.blocks), &record.blocks); + if let Endpoint::Live(endpoint) = &record.endpoint { + record.abort_allowed |= reachable(&before.blocks, before.entry, endpoint.block) + && !reachable(&expected.blocks, expected.entry, endpoint.block) + && (!record.selections.is_empty() || !record.loops.is_empty()); + } + } + function.breakpoint_regions = records; + Ok(()) +} + +fn shift(record: &mut BreakpointRegion, block: BlockId, start: usize, count: usize) { + record.statements = std::mem::take(&mut record.statements) + .into_iter() + .map(|(mut site, statement)| { + if site.block == block && site.index >= start { + site.index += count; + } + (site, statement) + }) + .collect(); + if record.start.block == block && record.start.index >= start { + record.start.index += count; + } + if let Endpoint::Live(site) = &mut record.endpoint + && site.block == block + && site.index >= start + { + site.index += count; + } +} + +pub(super) fn sum_transition( + function: &mut Function, + before: &Function, + types: &TypeInterner, +) -> Result<(), String> { + validate(before, types)?; + let mut expected = before.clone(); + let mut changed = Vec::new(); + for block in &mut expected.blocks { + let Some(Statement { + kind: StatementKind::SumTag { source, target }, + .. + }) = block.statements.last() + else { + continue; + }; + let TerminatorKind::Branch { + condition, + then_block, + else_block, + } = &block.terminator.kind + else { + continue; + }; + let Some(source_local) = before.local(*source) else { + continue; + }; + if condition.ty != TypeInterner::BOOL + || !matches!(condition.kind, hir::ExpressionKind::Local(local) if local == *target) + || before + .local(*target) + .is_none_or(|local| local.ty != TypeInterner::BOOL) + || source_local.ty.index() as usize >= types.len() + { + continue; + } + let selected = match types.resolve(source_local.ty) { + jett_types::Type::Optional(inner) if *inner == TypeInterner::NEVER => *else_block, + jett_types::Type::Result(ok, error) + if *ok == TypeInterner::NEVER + && *error != TypeInterner::NEVER + && (error.index() as usize) < types.len() => + { + *else_block + } + jett_types::Type::Result(ok, error) + if *ok != TypeInterner::NEVER + && *error == TypeInterner::NEVER + && (ok.index() as usize) < types.len() => + { + *then_block + } + _ => continue, + }; + changed.push(SelectedEdge { + block: block.id, + source: *source, + tag: *target, + source_type: source_local.ty, + selected, + span: block.terminator.span, + }); + block.terminator.kind = TerminatorKind::Goto(selected); + } + if !blocks_equal(&expected.blocks, &function.blocks) { + return Err("breakpoint sum transition changes more than an exact Never-side edge".into()); + } + for record in &mut function.breakpoint_regions { + for selected in &changed { + if record.blocks.contains(&selected.block) { + if record.terminators.contains_key(&selected.block) { + record.terminators.insert( + selected.block, + expected.blocks[selected.block.index() as usize] + .terminator + .clone(), + ); + } + record.selections.push(selected.clone()); + } + } + record.edges = touching(&edges(&expected.blocks), &record.blocks); + if let Endpoint::Live(endpoint) = &record.endpoint { + record.abort_allowed |= reachable(&before.blocks, before.entry, endpoint.block) + && !reachable(&expected.blocks, expected.entry, endpoint.block) + && !record.selections.is_empty(); + } + } + Ok(()) +} + +fn validate_selection( + function: &Function, + selected: &SelectedEdge, + types: &TypeInterner, +) -> Result<(), String> { + let Some(block) = function + .blocks + .get(selected.block.index() as usize) + .filter(|block| block.id == selected.block) + else { + return Err("breakpoint selected edge has no retained block".into()); + }; + let exact_never = (selected.source_type.index() as usize) < types.len() + && match types.resolve(selected.source_type) { + jett_types::Type::Optional(inner) => *inner == TypeInterner::NEVER, + jett_types::Type::Result(ok, error) => { + (*ok == TypeInterner::NEVER) != (*error == TypeInterner::NEVER) + } + _ => false, + }; + if !exact_never + || function + .local(selected.source) + .is_none_or(|local| local.ty != selected.source_type) + || function + .local(selected.tag) + .is_none_or(|local| local.ty != TypeInterner::BOOL) + || !matches!(block.terminator.kind, TerminatorKind::Goto(target) if target == selected.selected) + || block.terminator.span != selected.span + || !matches!(block.statements.last().map(|s| &s.kind), Some(StatementKind::SumTag { source, target }) if *source == selected.source && *target == selected.tag) + { + return Err("breakpoint selected edge loses its exact runtime sum/tag proof".into()); + } + Ok(()) +} + +pub(super) fn remap_blocks(function: &mut Function, map: &[Option]) -> Result<(), String> { + let get = |id: BlockId| { + map.get(id.index() as usize) + .copied() + .flatten() + .ok_or_else(|| "breakpoint surviving CFG reference has no block remap".to_string()) + }; + let old = std::mem::take(&mut function.breakpoint_regions); + let mut records = Vec::new(); + for mut record in old.clone() { + let surviving = old.iter().any(|child| { + (child.id == record.id || descendant(child, record.id, &old)) + && (child.statements.keys().any(|site| get(site.block).is_ok()) + || child.terminators.keys().any(|block| get(*block).is_ok())) + }); + if !surviving { + continue; + } + if let Endpoint::Live(site) = record.endpoint.clone() { + if get(site.block).is_err() { + if !record.abort_allowed + || (record.selections.is_empty() && record.loops.is_empty()) + { + return Err( + "breakpoint endpoint removal lacks canonical selected-edge proof".into(), + ); + } + record.endpoint = Endpoint::CanonicallyAborted { + selections: record.selections.clone(), + loops: record.loops.clone(), + }; + } + } + record.entry = get(record.entry)?; + // The entry prefix can itself be unreachable only when the whole region + // is removed. A surviving fragment never invents a replacement entry. + record.start.block = get(record.start.block)?; + record.statements = record + .statements + .into_iter() + .filter_map(|(mut site, value)| { + let block = get(site.block).ok()?; + site.block = block; + Some((site, value)) + }) + .collect(); + let mut terms = BTreeMap::new(); + for (block, mut value) in record.terminators { + let Ok(block) = get(block) else { + continue; + }; + let missing = std::cell::Cell::new(false); + sequences::prune::block_targets(&mut value.kind, &|id| match get(*id) { + Ok(new) => *id = new, + Err(_) => missing.set(true), + }); + if missing.get() { + return Err("breakpoint retained terminator targets a removed block".into()); + } + terms.insert(block, value); + } + record.terminators = terms; + record.blocks = record + .blocks + .into_iter() + .filter_map(|id| get(id).ok()) + .collect(); + record.edges = record + .edges + .into_iter() + .filter_map(|mut edge| { + edge.source = get(edge.source).ok()?; + edge.target = get(edge.target).ok()?; + Some(edge) + }) + .collect(); + record.selections.retain_mut(|selected| { + let (Ok(block), Ok(target)) = (get(selected.block), get(selected.selected)) else { + return false; + }; + selected.block = block; + selected.selected = target; + true + }); + record.loops.retain_mut(|selected| { + let (Ok(block), Ok(preheader), Ok(target)) = ( + get(selected.block), + get(selected.preheader), + get(selected.selected), + ) else { + return false; + }; + selected.block = block; + selected.preheader = preheader; + selected.selected = target; + true + }); + match &mut record.endpoint { + Endpoint::Live(site) => site.block = get(site.block)?, + Endpoint::CanonicallyAborted { selections, loops } => { + *selections = record.selections.clone(); + *loops = record.loops.clone(); + } + } + records.push(record); + } + function.breakpoint_regions = records; + Ok(()) +} + +pub(super) fn remap_locals(function: &mut Function, map: &[Option]) -> Result<(), String> { + for record in &mut function.breakpoint_regions { + for value in record.statements.values_mut() { + let mut block = BasicBlock { + id: BlockId(0), + statements: vec![value.clone()], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: value.span, + }, + }; + remap_snapshot(&mut block, map)?; + *value = block.statements.remove(0); + } + for value in record.terminators.values_mut() { + let mut block = BasicBlock { + id: BlockId(0), + statements: Vec::new(), + terminator: value.clone(), + }; + remap_snapshot(&mut block, map)?; + *value = block.terminator; + } + for selected in &mut record.selections { + selected.source = map + .get(selected.source.index() as usize) + .copied() + .flatten() + .ok_or("breakpoint selected source has no local remap")?; + selected.tag = map + .get(selected.tag.index() as usize) + .copied() + .flatten() + .ok_or("breakpoint selected tag has no local remap")?; + } + for selected in &mut record.loops { + selected.length = map + .get(selected.length.index() as usize) + .copied() + .flatten() + .ok_or("breakpoint sequence length has no local remap")?; + let root = match &mut selected.source { + SequenceSource::Local(root) | SequenceSource::Projected { owner: root, .. } => root, + }; + *root = map + .get(root.index() as usize) + .copied() + .flatten() + .ok_or("breakpoint sequence source has no local remap")?; + } + if let Endpoint::CanonicallyAborted { selections, loops } = &mut record.endpoint { + *selections = record.selections.clone(); + *loops = record.loops.clone(); + } + } + Ok(()) +} + +fn descendant(child: &BreakpointRegion, ancestor: usize, records: &[BreakpointRegion]) -> bool { + let mut parent = child.parent; + for _ in 0..records.len() { + let Some(id) = parent else { + return false; + }; + if id == ancestor { + return true; + } + parent = records + .iter() + .find(|record| record.id == id) + .and_then(|record| record.parent); + } + false +} + +fn reachable(blocks: &[BasicBlock], entry: BlockId, target: BlockId) -> bool { + let all = edges(blocks); + let mut seen = BTreeSet::new(); + let mut pending = vec![entry]; + while let Some(block) = pending.pop() { + if !seen.insert(block) { + continue; + } + if block == target { + return true; + } + pending.extend( + all.iter() + .filter(|edge| edge.source == block) + .map(|edge| edge.target), + ); + } + false +} + +fn empty_sequence(types: &TypeInterner, ty: TypeId) -> bool { + if ty.index() as usize >= types.len() { + return false; + } + match types.resolve(ty) { + jett_types::Type::List(element) | jett_types::Type::Set(element) => { + *element == TypeInterner::NEVER + } + jett_types::Type::Map(key, value) => { + *key == TypeInterner::NEVER || *value == TypeInterner::NEVER + } + _ => false, + } +} + +fn validate_loop_selection( + function: &Function, + selected: &SelectedLoop, + types: &TypeInterner, +) -> Result<(), String> { + if !empty_sequence(types, selected.source_type) + || selected.source.ty(function) != Some(selected.source_type) + || function.local(selected.length).is_none_or(|local| local.ty != TypeInterner::INT64) + || !function.blocks.get(selected.preheader.index() as usize).is_some_and(|block| block.statements.iter().any(|statement| matches!(&statement.kind, StatementKind::SequenceLength { source, target } if source == &selected.source && *target == selected.length))) + || !function.blocks.get(selected.block.index() as usize).is_some_and(|block| block.terminator.span == selected.span && matches!(block.terminator.kind, TerminatorKind::Goto(target) if target == selected.selected)) { + return Err("breakpoint empty sequence loses its exact validation or selected edge".into()); + } + Ok(()) +} + +pub(super) fn remap_snapshot( + block: &mut BasicBlock, + map: &[Option], +) -> Result<(), String> { + let mut missing = false; + sequences::prune::block_locals( + block, + &mut |id| { + if let Some(new) = map.get(id.index() as usize).copied().flatten() { + *id = new; + } else { + missing = true; + } + }, + &mut |floor| { + if (*floor as usize) <= map.len() { + *floor = map[..*floor as usize] + .iter() + .filter(|id| id.is_some()) + .count() as u32; + } + }, + ); + if missing { + Err("breakpoint surviving snapshot has no local remap".into()) + } else { + Ok(()) + } +} + +// Typed normalization changes only f64 payloads for equality. Their exact bits +// are compared separately, so NaN and signed zero never weaken a snapshot. +pub(super) fn statement_equal(left: &Statement, right: &Statement) -> bool { + let span = left.span; + let left = BasicBlock { + id: BlockId(0), + statements: vec![left.clone()], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span, + }, + }; + let right = BasicBlock { + id: BlockId(0), + statements: vec![right.clone()], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span, + }, + }; + block_equal(&left, &right) +} +pub(super) fn terminator_equal(left: &Terminator, right: &Terminator) -> bool { + block_equal( + &BasicBlock { + id: BlockId(0), + statements: Vec::new(), + terminator: left.clone(), + }, + &BasicBlock { + id: BlockId(0), + statements: Vec::new(), + terminator: right.clone(), + }, + ) +} +pub(super) fn blocks_equal(left: &[BasicBlock], right: &[BasicBlock]) -> bool { + left.len() == right.len() + && left + .iter() + .zip(right) + .all(|(left, right)| block_equal(left, right)) +} +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()); + float_block(&mut left, &mut left_bits); + float_block(&mut right, &mut right_bits); + left == right && left_bits == right_bits +} + +fn float_block(block: &mut BasicBlock, bits: &mut Vec) { + for statement in &mut block.statements { + match &mut statement.kind { + StatementKind::Let { value, .. } + | StatementKind::BeginCallView { value, .. } + | StatementKind::CheckRefinement { call: value, .. } + | StatementKind::Evaluate(value) + | StatementKind::HandleDefault(value) => float_expression(value, bits), + StatementKind::Assign { target, value } => { + float_expression(target, bits); + float_expression(value, bits); + } + StatementKind::Assert { condition, message } => { + float_expression(condition, bits); + if let Some(value) = message { + float_expression(value, bits); + } + } + StatementKind::Breakpoint { condition, .. } => { + if let Some(value) = condition { + float_expression(value, bits); + } + } + StatementKind::OpenCallOwnerGeneration { .. } + | StatementKind::ReplaceCallOwnerGeneration { .. } + | StatementKind::CloseCallOwnerGeneration { .. } + | StatementKind::EndCallView { .. } + | StatementKind::ReflectedContainerReady { .. } + | StatementKind::SequenceLength { .. } + | StatementKind::SequenceGet { .. } + | StatementKind::IterationBorrow { .. } + | StatementKind::SumTag { .. } + | StatementKind::SumTake { .. } + | StatementKind::Trace(_) => {} + } + } + match &mut block.terminator.kind { + TerminatorKind::Return(value) => { + if let Some(value) = value { + float_expression(value, bits); + } + } + TerminatorKind::Respond(value) + | TerminatorKind::Branch { + condition: value, .. + } + | TerminatorKind::Switch { + scrutinee: value, .. + } + | TerminatorKind::ForEach { + iterable: value, .. + } + | TerminatorKind::ReflectedTypeDispatch { + type_info: value, .. + } => float_expression(value, bits), + TerminatorKind::Goto(_) | TerminatorKind::Unreachable => {} + } +} + +fn float_expression(value: &mut Expression, bits: &mut Vec) { + use hir::ExpressionKind as E; + match &mut value.kind { + E::Float(value) => { + bits.push(value.to_bits()); + *value = 0.0; + } + E::Binary { left, right, .. } => { + float_expression(left, bits); + float_expression(right, bits); + } + E::Unary { value, .. } + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::Declassify(value) + | E::Coarsen(value) + | E::RefinementValidated(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::RuntimeFailureMessage(value) + | E::InterfaceCoerce { value, .. } + | E::FunctionAdapter { value, .. } + | E::InterfaceType(value) + | E::Comptime { value, .. } + | E::StateIs { value, .. } + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::Field { base: value, .. } + | E::View(value) + | E::Clone(value) => float_expression(value, bits), + E::Call { args, .. } + | E::Intrinsic { args, .. } + | E::ActorSpawn { args, .. } + | E::StructConstruct { fields: args, .. } + | E::BitfieldConstruct { fields: args, .. } + | E::MachineConstruct { payloads: args, .. } + | E::EnumConstruct { payloads: args, .. } + | E::ListConstruct { elements: args } => { + for arg in args { + float_expression(arg, bits); + } + } + E::IndirectCall { callee, args, .. } + | E::ActorMessage { + actor: callee, + args, + .. + } + | E::MachineTransition { + source: callee, + payloads: args, + .. + } => { + float_expression(callee, bits); + for arg in args { + float_expression(arg, bits); + } + } + E::MapConstruct { entries } => { + for entry in entries { + float_expression(&mut entry.key, bits); + float_expression(&mut entry.value, bits); + } + } + E::Handle { + target, failure, .. + } => { + float_expression(target, bits); + float_hir_block(failure, bits); + } + E::StringInterpolation(parts) => { + for part in parts { + if let hir::StringSegment::Value(value) = part { + float_expression(value, bits); + } + } + } + E::InlineFunction { body, .. } => float_hir_block(body, bits), + E::Int(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::Local(_) + | E::Constant { .. } + | E::FunctionRef(_) + | E::ClosureRef { .. } + | E::OptionalNone + | E::RuntimeFailure(_) + | E::PropertyCaseContext(_) => {} + } +} + +fn float_hir_block(block: &mut hir::Block, bits: &mut Vec) { + use hir::StatementKind as H; + for statement in &mut block.statements { + match &mut statement.kind { + H::Let { value, .. } + | H::HandleDefault(value) + | H::Expression(value) + | H::Respond(value) => float_expression(value, bits), + H::Assign { target, value } => { + float_expression(target, bits); + float_expression(value, bits); + } + H::Return(value) => { + if let Some(value) = value { + float_expression(value, bits); + } + } + H::If { + condition, + then_block, + else_block, + } => { + float_expression(condition, bits); + float_hir_block(then_block, bits); + if let Some(block) = else_block { + float_hir_block(block, bits); + } + } + H::While { condition, body } => { + float_expression(condition, bits); + float_hir_block(body, bits); + } + H::For { iterable, body, .. } => { + float_expression(iterable, bits); + float_hir_block(body, bits); + } + H::Match { scrutinee, arms } => { + float_expression(scrutinee, bits); + for arm in arms { + float_hir_block(&mut arm.body, bits); + } + } + H::Assert { condition, message } => { + float_expression(condition, bits); + if let Some(message) = message { + float_expression(message, bits); + } + } + H::Breakpoint { condition, .. } => { + if let Some(condition) = condition { + float_expression(condition, bits); + } + } + H::Scope(block) => float_hir_block(block, bits), + H::ReflectedTypeDispatch { type_info, arms } => { + float_expression(type_info, bits); + for arm in arms { + float_hir_block(&mut arm.body, bits); + } + } + H::Trace(_) | H::Break | H::Continue => {} + } + } +} + +#[cfg(test)] +mod tests; + +// Read-only bit-exact expression comparison, sharing the constructor snapshot rule. +pub(super) fn expression_equal(left: &Expression, right: &Expression) -> bool { + statement_equal( + &Statement { + kind: StatementKind::Evaluate(left.clone()), + span: left.span, + }, + &Statement { + kind: StatementKind::Evaluate(right.clone()), + span: right.span, + }, + ) +} diff --git a/crates/jett_mir/src/breakpoint_regions/tests.rs b/crates/jett_mir/src/breakpoint_regions/tests.rs new file mode 100644 index 00000000..509a662d --- /dev/null +++ b/crates/jett_mir/src/breakpoint_regions/tests.rs @@ -0,0 +1,485 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; +use std::collections::HashMap; + +const HANDLED: &str = r#"namespace app +function maybe_flag(valid: bool) returns result[bool, string]: + if valid: + return ok(true) + return fail("missing") +function exercise() returns bool: + bool before = true + breakpoint(maybe_flag(true) handle error: + default false + ) + breakpoint(maybe_flag(false) handle error: + default false + ) + return before +"#; + +fn source_program(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); + assert!( + resolved + .diagnostics + .iter() + .all(|d| d.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(|d| d.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let high = hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .expect("exact checked HIR"); + ( + crate::lower(&high, &checked.interner).expect("exact source MIR"), + checked.interner, + ) +} + +fn exercise(program: &Program) -> &Function { + program + .functions + .iter() + .find(|f| f.identity.declaration.name == "exercise") + .expect("exercise") +} + +fn exercise_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|f| f.identity.declaration.name == "exercise") + .expect("exercise") +} + +fn rejects(program: &Program, types: &TypeInterner) { + let errors = crate::validate_call_ownership(program, types) + .expect_err("corrupted extraction is refused"); + assert!( + errors + .iter() + .any(|e| e.message.contains("breakpoint") || e.message.contains("source context")), + "{errors:?}" + ); +} + +#[test] +fn breakpoint_extraction_rejoins_real_nested_handler_conditions_and_release_elision() { + let (mut program, types) = source_program(HANDLED, false); + let function = exercise(&program); + assert_eq!(function.breakpoint_regions.len(), 2); + let sites = validate(function, &types).expect("original exact sites"); + let first = &function.blocks[function.entry.index() as usize].statements[0]; + assert!(matches!(first.kind, StatementKind::Let { .. })); + assert!( + !sites.statement(function.entry, 0), + "ordinary shared prefix remains ordinary" + ); + assert!( + !sites.terminator(function.blocks.last().expect("return block").id), + "ordinary return remains ordinary" + ); + for pass in [ + crate::prepare_native_sequences, + crate::prepare_native_uninhabited_sums, + crate::prepare_native_generated_functions, + ] { + pass(&mut program, &types); + crate::validate_call_ownership(&program, &types) + .expect("every canonical boundary preserves exact sites"); + } + let (released, types) = source_program(HANDLED, true); + assert!(exercise(&released).breakpoint_regions.is_empty()); + assert!(exercise(&released).blocks.iter().all(|b| { + b.statements + .iter() + .all(|s| !matches!(s.kind, StatementKind::Breakpoint { .. })) + })); + crate::validate_call_ownership(&released, &types) + .expect("release never evaluates debug condition"); +} + +#[test] +fn breakpoint_extraction_does_not_authorize_copied_moved_or_new_marker_calls() { + let (program, types) = source_program(HANDLED, false); + let function = exercise(&program); + let (site, original) = function.breakpoint_regions[0].statements.iter().find(|(_, statement)| matches!(&statement.kind, StatementKind::Let { value, .. } if matches!(value.kind, hir::ExpressionKind::Call { .. }))).map(|(site, value)| (*site, value.clone())).expect("extracted real call"); + for mutation in ["copy", "move", "marker", "endpoint", "condition"] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + match mutation { + "copy" => { + let id = BlockId(function.blocks.len() as u32); + function.blocks.push(BasicBlock { + id, + statements: vec![original.clone()], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: original.span, + }, + }); + } + "move" => { + function.blocks[site.block.index() as usize] + .statements + .remove(site.index); + } + "marker" => { + let Endpoint::Live(endpoint) = &function.breakpoint_regions[0].endpoint else { + panic!("live endpoint"); + }; + let value = function.blocks[endpoint.block.index() as usize].statements + [endpoint.index] + .clone(); + let id = BlockId(function.blocks.len() as u32); + function.blocks.push(BasicBlock { + id, + statements: vec![value], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: original.span, + }, + }); + } + "endpoint" | "condition" => { + let Endpoint::Live(endpoint) = &function.breakpoint_regions[0].endpoint else { + panic!("live endpoint"); + }; + let endpoint = *endpoint; + let target = &mut function.blocks[endpoint.block.index() as usize].statements + [endpoint.index]; + if mutation == "endpoint" { + target.kind = StatementKind::Evaluate(Expression { + kind: hir::ExpressionKind::Bool(true), + ty: TypeInterner::BOOL, + span: target.span, + }); + } else { + let StatementKind::Breakpoint { condition, .. } = &mut target.kind else { + panic!("endpoint"); + }; + condition.as_mut().expect("condition").kind = hir::ExpressionKind::Bool(true); + } + } + _ => unreachable!(), + } + rejects(&changed, &types); + } +} + +#[test] +fn breakpoint_extraction_rejects_orphan_duplicate_foreign_and_alternate_entry_records() { + let (program, types) = source_program(HANDLED, false); + for mutation in ["duplicate", "orphan", "foreign", "index", "edge"] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + match mutation { + "duplicate" => function + .breakpoint_regions + .push(function.breakpoint_regions[0].clone()), + "orphan" => function.breakpoint_regions[0].parent = Some(usize::MAX), + "foreign" => function.breakpoint_regions[0].owner = program.functions[0].id, + "index" => { + let record = &mut function.breakpoint_regions[0]; + let (site, value) = record + .statements + .first_key_value() + .map(|(site, value)| (*site, value.clone())) + .expect("site"); + record.statements.remove(&site); + record.statements.insert( + Site { + index: site.index + 1, + ..site + }, + value, + ); + } + "edge" => { + let id = BlockId(function.blocks.len() as u32); + function.blocks.push(BasicBlock { + id, + statements: Vec::new(), + terminator: Terminator { + kind: TerminatorKind::Goto(function.breakpoint_regions[0].start.block), + span: function.span, + }, + }); + } + _ => unreachable!(), + } + rejects(&changed, &types); + } +} + +const NESTED: &str = r#"namespace app +function inspect(view values: list[int64]) returns bool: + return true +function maybe_flag(valid: bool) returns result[bool, string]: + if valid: + return ok(true) + return fail("missing") +function exercise(values: list[int64]) returns bool: + breakpoint(false or (maybe_flag(false) handle error: + for item in view values: + breakpoint(inspect(view values)) + default true + )) + return inspect(view values) +"#; + +#[test] +fn breakpoint_extraction_nested_lazy_loop_sites_survive_sequence_prefixes_edges_and_remaps() { + let (mut program, types) = source_program(NESTED, false); + let function = exercise(&program); + assert_eq!(function.breakpoint_regions.len(), 2); + assert_eq!( + function.breakpoint_regions[1].parent, + Some(function.breakpoint_regions[0].id) + ); + let parent_sites = function.breakpoint_regions[0] + .statements + .keys() + .copied() + .collect::>(); + assert!( + function.breakpoint_regions[1] + .statements + .keys() + .all(|site| !parent_sites.contains(site)) + ); + crate::prepare_native_sequences(&mut program, &types); + crate::validate_call_ownership(&program, &types) + .expect("exact sequence prefixes and split edges"); + assert!( + exercise(&program) + .blocks + .iter() + .all(|b| !matches!(b.terminator.kind, TerminatorKind::ForEach { .. })) + ); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::prepare_native_generated_functions(&mut program, &types); + crate::validate_call_ownership(&program, &types).expect("dense prepared nested sites"); + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + let prefix = function + .blocks + .iter_mut() + .flat_map(|b| &mut b.statements) + .find(|s| matches!(s.kind, StatementKind::SequenceGet { .. })) + .expect("canonical getter"); + prefix.span.start += 1; + rejects(&changed, &types); +} + +#[test] +fn breakpoint_extraction_exact_never_side_selection_retains_aborted_evaluation_context() { + let source = r#"namespace app +function accept[T](value: T) returns bool: + return true +function exercise[T](outcome: optional[T]) returns bool: + breakpoint(accept(outcome handle: + return false + )) + return true +function main() returns nothing: + bool observed = exercise(none) + trace observed + return nothing +"#; + let (mut program, types) = source_program(source, false); + crate::prepare_native_uninhabited_sums(&mut program, &types); + let function = exercise(&program); + assert_eq!(function.breakpoint_regions.len(), 1); + assert!(matches!( + function.breakpoint_regions[0].endpoint, + Endpoint::CanonicallyAborted { .. } + )); + assert!(!function.blocks.iter().any(|b| { + b.statements + .iter() + .any(|s| matches!(s.kind, StatementKind::Breakpoint { .. })) + })); + crate::validate_call_ownership(&program, &types) + .expect("absent call executes before the exact canonical abort"); + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + function.breakpoint_regions[0].selections[0].selected = function.entry; + rejects(&changed, &types); + let mut changed = program.clone(); + exercise_mut(&mut changed).breakpoint_regions[0].abort_allowed = false; + rejects(&changed, &types); +} + +#[test] +fn breakpoint_extraction_observation_stays_lexical_across_generic_and_inline_bodies() { + let source = r#"namespace app +function consume[T](value: T) returns bool: + return true +function exercise(values: list[int64]) returns bool: + function(list[int64]) returns bool callback = function(item: list[int64]) returns bool: return consume(item) + breakpoint(consume(values) and callback(clone values)) + return consume(values) +"#; + let (program, types) = source_program(source, false); + crate::validate_call_ownership(&program, &types) + .expect("ordinary callee bodies do not inherit observation"); + assert!( + program + .functions + .iter() + .filter(|f| f.debug_kind == hir::FunctionDebugKind::Inline) + .all(|f| f.breakpoint_regions.is_empty()) + ); + let function = exercise(&program); + let sites = validate(function, &types).expect("lexical observed call sites"); + assert!(!sites.terminator(function.blocks.last().expect("return").id)); + let ordinary = source.replace( + " breakpoint(consume(values) and callback(clone values))", + " bool used = consume(values)", + ); + let parsed = jett_parser::parse(&ordinary, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked.diagnostics.iter().any(|d| d.code.code() == 400), + "{:?}", + checked.diagnostics + ); +} + +#[test] +fn breakpoint_extraction_float_snapshot_equality_is_bitwise_including_nan_and_signed_zero() { + let span = Span::new(FileId::new(0), 1, 2); + let statement = |bits| Statement { + kind: StatementKind::Evaluate(Expression { + kind: hir::ExpressionKind::Float(f64::from_bits(bits)), + ty: TypeInterner::FLOAT64, + span, + }), + span, + }; + let nan = statement(0x7ff8_0000_0000_0001); + assert!(statement_equal(&nan, &nan)); + assert!(!statement_equal(&nan, &statement(0x7ff8_0000_0000_0002))); + assert!(!statement_equal(&statement(0), &statement(1_u64 << 63))); +} + +#[test] +fn breakpoint_extraction_invalid_original_never_disappears_in_a_preparation() { + let (program, types) = source_program(HANDLED, false); + for pass in [ + crate::prepare_native_sequences, + crate::prepare_native_uninhabited_sums, + crate::prepare_native_generated_functions, + ] { + let mut changed = program.clone(); + let record = &mut exercise_mut(&mut changed).breakpoint_regions[0]; + record.edges.clear(); + let before = changed.clone(); + pass(&mut changed, &types); + assert_eq!( + changed, before, + "failed original proof keeps all runtime and private records" + ); + rejects(&changed, &types); + } +} + +#[test] +fn breakpoint_extraction_present_success_selection_keeps_the_live_endpoint_and_tag() { + let source = r#"namespace app +function exercise[T, E](outcome: result[T, E]) returns bool: + breakpoint(outcome handle error: + return false + ) + return true +function main() returns nothing: + bool observed = exercise(ok(true)) + trace observed + return nothing +"#; + let (mut program, types) = source_program(source, false); + crate::prepare_native_uninhabited_sums(&mut program, &types); + let function = exercise(&program); + assert!(matches!( + function.breakpoint_regions[0].endpoint, + Endpoint::Live(_) + )); + assert_eq!(function.breakpoint_regions[0].selections.len(), 1); + assert!(function.blocks.iter().any(|block| { + block + .statements + .iter() + .any(|statement| matches!(statement.kind, StatementKind::SumTag { .. })) + })); + crate::validate_call_ownership(&program, &types) + .expect("selected true edge retains runtime tag and exact live endpoint"); + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + let selecting = function.breakpoint_regions[0].selections[0].block; + function.blocks[selecting.index() as usize] + .terminator + .span + .start += 1; + rejects(&changed, &types); +} + +#[test] +fn breakpoint_extraction_ordinary_loop_prefixes_shift_sites_without_gaining_observation() { + let source = r#"namespace app +function maybe_flag(valid: bool) returns result[bool, string]: + if valid: + return ok(true) + return fail("missing") +function exercise(view values: list[int64]) returns bool: + for item in view values: + breakpoint(maybe_flag(true) handle error: + default false + ) + return true +"#; + let (mut program, types) = source_program(source, false); + crate::prepare_native_sequences(&mut program, &types); + let function = exercise(&program); + let sites = + validate(function, &types).expect("ordinary loop boundary and dense breakpoint sites"); + let mut getters = 0; + for block in &function.blocks { + for (index, statement) in block.statements.iter().enumerate() { + if matches!(statement.kind, StatementKind::SequenceGet { .. }) { + getters += 1; + assert!( + !sites.statement(block.id, index), + "outside ForEach prefix stays ordinary" + ); + } + } + } + assert_eq!(getters, 1); + crate::prepare_native_generated_functions(&mut program, &types); + crate::validate_call_ownership(&program, &types) + .expect("shared body prefix index remaps are exact"); +} diff --git a/crates/jett_mir/src/call_owner_generations.rs b/crates/jett_mir/src/call_owner_generations.rs new file mode 100644 index 00000000..6859c7e8 --- /dev/null +++ b/crates/jett_mir/src/call_owner_generations.rs @@ -0,0 +1,1339 @@ +//! Unique ownership of a captured mutable TypeConstruction generation. +//! Escrows are internal native storage, never Jett value locals or Source roots. +use super::*; +use jett_typecheck::{CheckedCalleeAccess, CheckedCallerEffect, CheckedCallerSyntax}; +use std::collections::{BTreeMap, BTreeSet, HashSet, VecDeque}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct CallOwnerGenerationId(usize); +impl CallOwnerGenerationId { + pub fn index(self) -> usize { + self.0 + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct CallOwnerEscrowId(usize); +impl CallOwnerEscrowId { + pub fn index(self) -> usize { + self.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CallGenerationSlot { + generation: CallOwnerGenerationId, + escrow: CallOwnerEscrowId, + root: LocalId, + ty: TypeId, +} +impl CallGenerationSlot { + pub fn generation(&self) -> CallOwnerGenerationId { + self.generation + } + pub fn escrow(&self) -> CallOwnerEscrowId { + self.escrow + } + pub fn root(&self) -> LocalId { + self.root + } + pub fn ty(&self) -> TypeId { + self.ty + } +} + +#[derive(Debug, Clone)] +pub struct CallGenerationStoragePlan { + slots: Vec, + loans: BTreeMap>, +} +impl CallGenerationStoragePlan { + pub(crate) fn empty() -> Self { + Self { + slots: Vec::new(), + loans: BTreeMap::new(), + } + } + pub fn slots(&self) -> &[CallGenerationSlot] { + &self.slots + } + pub fn slot(&self, generation: CallOwnerGenerationId) -> Option<&CallGenerationSlot> { + self.slots + .get(generation.index()) + .filter(|slot| slot.generation == generation) + } + pub(crate) fn covers_loan(&self, generation: CallOwnerGenerationId, loan: LocalId) -> bool { + self.loans + .get(&generation) + .is_some_and(|loans| loans.contains(&loan)) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct Site { + block: BlockId, + statement: usize, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Edge { + source: BlockId, + arm: usize, + target: BlockId, +} +#[derive(Debug, Clone, PartialEq)] +struct CaptureSite { + loan: LocalId, + parameter: usize, + original_argument: hir::ArgumentOwnership, + begin: Site, +} +#[derive(Debug, Clone, PartialEq)] +struct ReplacementSite { + original: hir::Statement, + rhs: LocalId, + acquired: Site, + replacement: Site, +} + +/// The original invocation/assignment archive is immutable in original IDs. +/// Only live root/loan/RHS/sites and exact current snapshots are remapped. +#[derive(Debug, Clone, PartialEq)] +pub(super) struct CallOwnerGeneration { + id: CallOwnerGenerationId, + slot: CallOwnerEscrowId, + owner: FunctionId, + identity: FunctionIdentity, + entry: BlockId, + original_call: Expression, + original_root: hir::CallerBindingFact, + // Exact original HIR local header. Its full declaration span differs + // from the immutable Source fact's identifier span and never remaps. + original_root_header: Local, + current_source: hir::SourceCallOwnership, + consumer: Option, + root: LocalId, + captures: Vec, + replacements: Vec, + start: Site, + statements: BTreeMap, + terminators: BTreeMap, + blocks: BTreeSet, + edges: Vec, +} + +#[derive(Clone)] +struct Scope { + original: Option, + generations: Vec, +} +#[derive(Clone)] +struct Draft { + original_call: Expression, + original_root: hir::CallerBindingFact, + // Exact original HIR local header. Its full declaration span differs + // from the immutable Source fact's identifier span and never remaps. + original_root_header: Local, + root: LocalId, + captures: Vec, + replacements: Vec, + start: Site, + statements: BTreeMap, + terminators: BTreeMap, + blocks: BTreeSet, +} +#[derive(Clone, Default)] +pub(super) struct Capture { + scopes: Vec, + drafts: Vec, + recording: BTreeSet, +} +impl Capture { + pub(super) fn begin_scope(&mut self, original: &Expression, types: &TypeInterner) { + let selected = source_packet(original).is_some_and(|source| source.arguments.iter().any(|argument| + argument.syntax == CheckedCallerSyntax::WrittenView + && argument.effect == CheckedCallerEffect::RetainBorrow + && argument.callee_access == CheckedCalleeAccess::View + && argument.physical_access == CheckedCalleeAccess::View + && matches!(&argument.origin, hir::CallerOrigin::Binding(fact) if fact.mode == hir::CallerBindingMode::Owned && fact.mutable && (fact.ty.index() as usize) < types.len() && matches!(types.resolve(fact.ty), jett_types::Type::TypeConstruction)))); + self.scopes.push(Scope { + original: selected.then(|| original.clone()), + generations: Vec::new(), + }); + } + pub(super) fn end_scope(&mut self) -> Vec { + self.scopes + .pop() + .map(|scope| scope.generations) + .unwrap_or_default() + } + pub(super) fn abandoned(&self, depth: usize) -> Vec { + self.scopes + .iter() + .skip(depth) + .rev() + .flat_map(|scope| scope.generations.iter().rev().copied()) + .collect() + } + pub(super) fn stop(&mut self, id: CallOwnerGenerationId) { + self.recording.remove(&id); + } + pub(super) fn suspend(&mut self) -> Vec<(Option, Vec)> { + std::mem::take(&mut self.scopes) + .into_iter() + .map(|scope| (scope.original, scope.generations)) + .collect() + } + pub(super) fn restore( + &mut self, + scopes: Vec<(Option, Vec)>, + ) { + self.scopes = scopes + .into_iter() + .map(|(original, generations)| Scope { + original, + generations, + }) + .collect(); + } + pub(super) fn protected(&self, root: LocalId) -> Option { + self.scopes + .iter() + .rev() + .flat_map(|scope| scope.generations.iter().rev()) + .find(|id| self.drafts[id.index()].root == root) + .copied() + } + pub(super) fn candidate( + &mut self, + parameter: usize, + root: LocalId, + projection: &Expression, + block: BlockId, + statement: usize, + locals: &[Local], + types: &TypeInterner, + ) -> Result, String> { + let Some(scope) = self.scopes.last() else { + return Ok(None); + }; + let Some(original) = &scope.original else { + return Ok(None); + }; + let Some(source) = source_packet(original) else { + return Ok(None); + }; + let Some(argument) = source.arguments.get(parameter) else { + return Ok(None); + }; + let hir::CallerOrigin::Binding(fact) = &argument.origin else { + return Ok(None); + }; + let fact = *fact; + if argument.syntax != CheckedCallerSyntax::WrittenView + || argument.effect != CheckedCallerEffect::RetainBorrow + || argument.callee_access != CheckedCalleeAccess::View + || argument.physical_access != CheckedCalleeAccess::View + || fact.mode != hir::CallerBindingMode::Owned + || !fact.mutable + || fact.local != root + || fact.ty.index() as usize >= types.len() + || !matches!(types.resolve(fact.ty), jett_types::Type::TypeConstruction) + { + return Ok(None); + } + let Some(local) = locals + .get(root.index() as usize) + .filter(|local| local.id == root) + else { + return Err("call owner generation source root is outside its function".into()); + }; + let E::View(raw) = &projection.kind else { + return Err("call owner generation needs its original explicit view".into()); + }; + if !matches!(raw.kind, E::Local(id) if id == root) + || local.ty != fact.ty + || local.debug_ty != fact.ty + || !local.mutable + || local.view_source.is_some() + || local.span.file != fact.declaration_span.file + || fact.declaration_span.start < local.span.start + || fact.declaration_span.end > local.span.end + || argument.actual_type != fact.ty + || argument.parameter_type != fact.ty + || argument.source_witness().occurrence_type() != fact.ty + || raw.ty != fact.ty + || projection.ty != fact.ty + || source_operands(original) + .and_then(|args| args.get(parameter)) + .is_none_or(|original| { + !crate::breakpoint_regions::expression_equal(original, projection) + }) + { + return Err("call owner generation changes its exact plain source binding".into()); + } + if let Some(id) = self.protected(root) { + if !self + .scopes + .last() + .is_some_and(|scope| scope.generations.contains(&id)) + || !self.drafts[id.index()].replacements.is_empty() + { + return Err( + "call owner generation needs a separate overlapping cohort proof".into(), + ); + } + return Ok(Some((id, false))); + } + let id = CallOwnerGenerationId(self.drafts.len()); + let draft = Draft { + original_call: original.clone(), + original_root: fact, + original_root_header: local.clone(), + root, + captures: Vec::new(), + replacements: Vec::new(), + start: Site { block, statement }, + statements: BTreeMap::new(), + terminators: BTreeMap::new(), + blocks: BTreeSet::from([block]), + }; + self.drafts.push(draft); + self.scopes + .last_mut() + .ok_or("call owner generation has no current scope")? + .generations + .push(id); + self.recording.insert(id); + Ok(Some((id, true))) + } + pub(super) fn capture_loan( + &mut self, + id: CallOwnerGenerationId, + loan: LocalId, + parameter: usize, + block: BlockId, + statement: usize, + ) -> Result<(), String> { + let draft = self + .drafts + .get_mut(id.index()) + .ok_or("call owner generation capture is absent")?; + let argument = source_packet(&draft.original_call) + .and_then(|source| source.arguments.get(parameter)) + .ok_or("call owner generation original source argument is absent")? + .clone(); + if draft + .captures + .iter() + .any(|capture| capture.loan == loan || capture.parameter == parameter) + { + return Err("call owner generation duplicates a capture".into()); + } + draft.captures.push(CaptureSite { + loan, + parameter, + original_argument: argument, + begin: Site { block, statement }, + }); + Ok(()) + } + pub(super) fn replacement( + &mut self, + id: CallOwnerGenerationId, + original: &hir::Statement, + rhs: LocalId, + acquired: (BlockId, usize), + replacement: (BlockId, usize), + ) -> Result<(), String> { + let draft = self + .drafts + .get_mut(id.index()) + .ok_or("call owner generation replacement is absent")?; + let hir::StatementKind::Assign { target, value } = &original.kind else { + return Err("call owner generation replacement is not its source Assign".into()); + }; + if !matches!(target.kind, E::Local(root) if root == draft.root) + || target.ty != draft.original_root.ty + || value.ty != target.ty + || rhs == draft.root + { + return Err( + "call owner generation replacement changes its root or acquired type".into(), + ); + } + draft.replacements.push(ReplacementSite { + original: original.clone(), + rhs, + acquired: Site { + block: acquired.0, + statement: acquired.1, + }, + replacement: Site { + block: replacement.0, + statement: replacement.1, + }, + }); + Ok(()) + } + pub(super) fn statement(&mut self, block: BlockId, statement: usize, value: &Statement) { + for id in &self.recording { + let draft = &mut self.drafts[id.index()]; + draft.blocks.insert(block); + draft + .statements + .insert(Site { block, statement }, value.clone()); + } + } + pub(super) fn terminator(&mut self, block: BlockId, value: &Terminator) { + for id in &self.recording { + let draft = &mut self.drafts[id.index()]; + draft.blocks.insert(block); + draft.terminators.insert(block, value.clone()); + } + } + pub(super) fn block(&mut self, block: BlockId) { + for id in &self.recording { + self.drafts[id.index()].blocks.insert(block); + } + } + pub(super) fn seal(self, function: &Function) -> Result, String> { + if !self.scopes.is_empty() || !self.recording.is_empty() { + return Err("call owner generation constructor left an open source scope".into()); + } + let edges = all_edges(function); + let records = self.drafts.into_iter().enumerate().map(|(index, draft)| CallOwnerGeneration { + id: CallOwnerGenerationId(index), slot: CallOwnerEscrowId(index), owner: function.id, identity: function.identity.clone(), entry: function.entry, + current_source: draft.statements.values().find_map(|statement| { + statement_source(statement).filter(|source| draft.captures.iter().all(|capture| + source.arguments.get(capture.parameter).is_some_and(|argument| + matches!(argument.staging, hir::ArgumentStaging::Borrowed { loan } if loan == capture.loan)))) + .cloned() + }).expect("constructor-selected consuming Source invocation"), + consumer: draft.statements.iter().find_map(|(site, statement)| { + statement_source(statement).filter(|source| draft.captures.iter().all(|capture| + source.arguments.get(capture.parameter).is_some_and(|argument| + matches!(argument.staging, hir::ArgumentStaging::Borrowed { loan } if loan == capture.loan)))) + .map(|_| *site) + }), + original_call: draft.original_call, original_root: draft.original_root, + original_root_header: draft.original_root_header, root: draft.root, + captures: draft.captures, replacements: draft.replacements, start: draft.start, + statements: draft.statements, terminators: draft.terminators, + edges: touching(&edges, &draft.blocks), blocks: draft.blocks, + }).collect::>(); + if records.iter().any(|record| record.consumer.is_none()) { + return Err("call owner generation has no constructor-owned Source consumer".into()); + } + Ok(records) + } +} + +fn statement_source(statement: &Statement) -> Option<&hir::SourceCallOwnership> { + match &statement.kind { + StatementKind::Let { value, .. } | StatementKind::Evaluate(value) => source_packet(value), + _ => None, + } +} + +pub(super) fn has_records(function: &Function) -> bool { + !function.call_owner_generations.is_empty() +} + +fn source_operands(value: &Expression) -> Option<&[Expression]> { + match &value.kind { + E::Call { args, .. } | E::IndirectCall { args, .. } | E::Intrinsic { args, .. } => { + Some(args) + } + _ => None, + } +} + +fn source_packet(value: &Expression) -> Option<&hir::SourceCallOwnership> { + match &value.kind { + E::Call { + ownership: hir::CallOwnership::Source(source), + .. + } + | E::IndirectCall { + ownership: hir::CallOwnership::Source(source), + .. + } + | E::Intrinsic { + ownership: hir::CallOwnership::Source(source), + .. + } => Some(source), + _ => None, + } +} +use hir::ExpressionKind as E; +fn all_edges(function: &Function) -> Vec { + let mut result = Vec::new(); + for block in &function.blocks { + let targets = match &block.terminator.kind { + TerminatorKind::Goto(target) => vec![*target], + TerminatorKind::Branch { + then_block, + else_block, + .. + } => vec![*then_block, *else_block], + TerminatorKind::Switch { + variants, + otherwise, + .. + } => variants + .iter() + .map(|(_, target, _)| *target) + .chain(*otherwise) + .collect(), + TerminatorKind::ForEach { body, exit, .. } => vec![*body, *exit], + TerminatorKind::ReflectedTypeDispatch { + arms, otherwise, .. + } => arms + .iter() + .map(|arm| arm.target) + .chain(std::iter::once(*otherwise)) + .collect(), + TerminatorKind::Return(_) + | TerminatorKind::Respond(_) + | TerminatorKind::Unreachable => Vec::new(), + }; + result.extend(targets.into_iter().enumerate().map(|(arm, target)| Edge { + source: block.id, + arm, + target, + })); + } + result +} +fn touching(edges: &[Edge], blocks: &BTreeSet) -> Vec { + edges + .iter() + .copied() + .filter(|edge| blocks.contains(&edge.source) || blocks.contains(&edge.target)) + .collect() +} +fn operation(statement: &StatementKind) -> Option { + match statement { + StatementKind::OpenCallOwnerGeneration { generation, .. } + | StatementKind::ReplaceCallOwnerGeneration { generation, .. } + | StatementKind::CloseCallOwnerGeneration { generation } => Some(*generation), + _ => None, + } +} + +pub(super) fn validate( + function: &Function, + types: &TypeInterner, +) -> Result { + let records = &function.call_owner_generations; + if records.is_empty() { + return if function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| operation(&statement.kind).is_some()) + { + Err("call owner generation operation has no constructor-owned record".into()) + } else { + Ok(CallGenerationStoragePlan::empty()) + }; + } + let mut plan = CallGenerationStoragePlan::empty(); + let mut occurrences = BTreeMap::new(); + let edges = all_edges(function); + for (index, record) in records.iter().enumerate() { + if record.id.index() != index + || record.slot.index() != index + || record.owner != function.id + || record.identity != function.identity + || record.entry != function.entry + { + return Err("call owner generation has duplicate or foreign ownership".into()); + } + let root = function + .local(record.root) + .ok_or("call owner generation root is absent")?; + let header = &record.original_root_header; + if header.id != record.original_root.local + || header.ty != record.original_root.ty + || header.debug_ty != header.ty + || !header.mutable + || header.view_source.is_some() + || header.span.file != record.original_root.declaration_span.file + || record.original_root.declaration_span.start < header.span.start + || record.original_root.declaration_span.end > header.span.end + { + return Err( + "call owner generation changes its sealed original source or local header".into(), + ); + } + if root.name != header.name + || root.ty != header.ty + || root.debug_ty != header.debug_ty + || root.debug_type_name != header.debug_type_name + || root.mutable != header.mutable + || root.view_source != header.view_source + || root.span != header.span + { + return Err("call owner generation changes its sealed local declaration header".into()); + } + if root.ty != record.original_root.ty + || root.debug_ty != root.ty + || !root.mutable + || root.view_source.is_some() + || function.parameter_for_local(root.id).is_some() + || root.ty.index() as usize >= types.len() + || !matches!(types.resolve(root.ty), jett_types::Type::TypeConstruction) + || record.original_root.mode != hir::CallerBindingMode::Owned + || !record.original_root.mutable + || record.captures.is_empty() + || !record.blocks.contains(&record.start.block) + || touching(&edges, &record.blocks) != record.edges + { + return Err( + "call owner generation loses its owning mutable plain source root or CFG".into(), + ); + } + for (&site, snapshot) in &record.statements { + let actual = function + .blocks + .get(site.block.index() as usize) + .filter(|block| block.id == site.block) + .and_then(|block| block.statements.get(site.statement)); + if !record.blocks.contains(&site.block) + || actual.is_none_or(|actual| { + !crate::breakpoint_regions::statement_equal(actual, snapshot) + }) + { + return Err( + "call owner generation changes an exact constructor-owned statement".into(), + ); + } + if operation(&snapshot.kind) == Some(record.id) + && occurrences.insert(site, record.id).is_some() + { + return Err("call owner generation duplicates its operation occurrence".into()); + } + } + for (&block, snapshot) in &record.terminators { + if function + .blocks + .get(block.index() as usize) + .filter(|value| value.id == block) + .is_none_or(|value| { + !crate::breakpoint_regions::terminator_equal(&value.terminator, snapshot) + }) + { + return Err( + "call owner generation changes an exact constructor-owned terminator".into(), + ); + } + } + if !matches!(record.statements.get(&record.start).map(|s| &s.kind), Some(StatementKind::OpenCallOwnerGeneration { generation, root }) if *generation == record.id && *root == record.root) + { + return Err("call owner generation loses its unique original Open".into()); + } + let mut loans = HashSet::new(); + for capture in &record.captures { + let loan = function + .local(capture.loan) + .ok_or("call owner generation loan is absent")?; + if !loans.insert(capture.loan) + || loan.ty != root.ty + || loan.view_source != Some(root.id) + || function.parameter_for_local(loan.id).is_some() + { + return Err( + "call owner generation duplicates or changes its nonowning capture".into(), + ); + } + let Some(Statement { + kind: StatementKind::BeginCallView { local, value }, + .. + }) = record.statements.get(&capture.begin) + else { + return Err("call owner generation capture loses its exact Begin".into()); + }; + let E::View(raw) = &value.kind else { + return Err( + "call owner generation capture needs its original explicit view".into(), + ); + }; + let argument = &capture.original_argument; + if source_packet(&record.original_call) + .and_then(|source| source.arguments.get(capture.parameter)) + != Some(argument) + || !matches!(&argument.origin, hir::CallerOrigin::Binding(fact) if *fact == record.original_root) + { + return Err( + "call owner generation loses its immutable original source witness".into(), + ); + } + if *local != loan.id + || raw.ty != root.ty + || value.ty != root.ty + || !matches!(raw.kind, E::Local(id) if id == root.id) + || source_operands(&record.original_call) + .and_then(|args| args.get(capture.parameter)) + .is_none_or(|original| original.span != value.span) + || argument.actual_type != root.ty + || argument.parameter_type != root.ty + || argument.source_witness().occurrence_type() != root.ty + || argument.syntax != CheckedCallerSyntax::WrittenView + || argument.effect != CheckedCallerEffect::RetainBorrow + || argument.callee_access != CheckedCalleeAccess::View + || argument.physical_access != CheckedCalleeAccess::View + { + return Err( + "call owner generation capture changes its complete source contract".into(), + ); + } + } + for replacement in &record.replacements { + let rhs = function + .local(replacement.rhs) + .ok_or("call owner generation acquired RHS is absent")?; + if rhs.id == root.id + || rhs.ty != root.ty + || rhs.view_source.is_some() + || function.parameter_for_local(rhs.id).is_some() + || !matches!(record.statements.get(&replacement.acquired).map(|s| &s.kind), Some(StatementKind::Let { local, value }) if *local == rhs.id && value.ty == root.ty) + || !matches!(record.statements.get(&replacement.replacement).map(|s| &s.kind), Some(StatementKind::ReplaceCallOwnerGeneration { generation, root: actual, rhs_owner }) if *generation == record.id && *actual == root.id && *rhs_owner == rhs.id) + { + return Err( + "call owner generation replacement loses its prior independent owning RHS" + .into(), + ); + } + dominates(function, replacement.acquired, replacement.replacement)?; + if function.blocks.iter().flat_map(|block| &block.statements).filter(|statement| matches!(statement.kind, StatementKind::Let { local, .. } | StatementKind::BeginCallView { local, .. } if local == rhs.id)).count() != 1 { + return Err("call owner generation RHS does not have a unique acquisition".into()); + } + } + if let Some(consumer) = record.consumer { + let source = record + .statements + .get(&consumer) + .and_then(statement_source) + .ok_or("call owner generation loses its exact Source consumer")?; + for capture in &record.captures { + let argument = source + .arguments + .get(capture.parameter) + .ok_or("call owner generation consumer loses its formal")?; + if !matches!(argument.staging, hir::ArgumentStaging::Borrowed { loan } if loan == capture.loan) + { + return Err("call owner generation consumer changes its nonowning loan".into()); + } + dominates(function, capture.begin, consumer)?; + } + if *source != record.current_source { + return Err( + "call owner generation consumer changes its original closed Source witness" + .into(), + ); + } + } + plan.slots.push(CallGenerationSlot { + generation: record.id, + escrow: record.slot, + root: root.id, + ty: root.ty, + }); + plan.loans.insert(record.id, loans); + } + for block in &function.blocks { + for (statement, value) in block.statements.iter().enumerate() { + if let Some(generation) = operation(&value.kind) { + if occurrences.get(&Site { + block: block.id, + statement, + }) != Some(&generation) + { + return Err( + "call owner generation operation has no exact constructor-owned occurrence" + .into(), + ); + } + } + } + } + if !records.is_empty() { + validate_protocol(function, &plan)?; + } + Ok(plan) +} + +fn dominates(function: &Function, acquired: Site, used: Site) -> Result<(), String> { + let cfg = ControlFlowGraph::analyze(function).map_err(|errors| format!("{errors:?}"))?; + let mut queue = vec![used]; + let mut seen = BTreeSet::new(); + let mut reached = false; + while let Some(site) = queue.pop() { + if site.block == acquired.block && acquired.statement < site.statement { + reached = true; + continue; + } + if !seen.insert(site.block) { + continue; + } + if site.block == function.entry || cfg.predecessors(site.block).is_empty() { + return Err("call owner generation replacement precedes RHS acquisition".into()); + } + for &block in cfg.predecessors(site.block) { + queue.push(Site { + block, + statement: function.blocks[block.index() as usize].statements.len(), + }); + } + } + if !reached { + return Err("call owner generation RHS has an uninitialized cycle".into()); + } + Ok(()) +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum StorageState { + Closed, + Attached, + Retired, + MaybeRetired, +} +#[derive(Clone, PartialEq, Eq)] +struct ProtocolState { + storage: StorageState, + loans: HashSet, +} +impl ProtocolState { + fn closed() -> Self { + Self { + storage: StorageState::Closed, + loans: HashSet::new(), + } + } +} +fn merge(left: &ProtocolState, right: &ProtocolState) -> Result { + let storage = match (left.storage, right.storage) { + (StorageState::Closed, StorageState::Closed) => StorageState::Closed, + (StorageState::Closed, _) | (_, StorageState::Closed) => { + return Err("call owner generation has a path bypassing its active scope".into()); + } + (left, right) if left == right => left, + _ => StorageState::MaybeRetired, + }; + if left.loans != right.loans { + return Err("call owner generation joins different active capture cohorts".into()); + } + Ok(ProtocolState { + storage, + loans: left.loans.clone(), + }) +} +fn validate_protocol(function: &Function, plan: &CallGenerationStoragePlan) -> Result<(), String> { + let cfg = ControlFlowGraph::analyze(function).map_err(|errors| format!("{errors:?}"))?; + let initial = vec![ProtocolState::closed(); plan.slots.len()]; + let mut outgoing: Vec>> = vec![None; function.blocks.len()]; + let mut queue = VecDeque::from([function.entry]); + let mut pending = BTreeSet::from([function.entry]); + while let Some(block) = queue.pop_front() { + pending.remove(&block); + let mut incoming: Option> = + (block == function.entry).then(|| initial.clone()); + for predecessor in cfg.predecessors(block) { + if let Some(states) = &outgoing[predecessor.index() as usize] { + if let Some(current) = &mut incoming { + for (current, state) in current.iter_mut().zip(states) { + *current = merge(current, state)?; + } + } else { + incoming = Some(states.clone()); + } + } + } + let Some(mut states) = incoming else { + continue; + }; + transfer(function, block, plan, &mut states)?; + if outgoing[block.index() as usize].as_ref() != Some(&states) { + outgoing[block.index() as usize] = Some(states); + for &successor in cfg.successors(block) { + if pending.insert(successor) { + queue.push_back(successor); + } + } + } + } + // Disconnected blocks remain an integrity obligation, never a permission. + for block in &function.blocks { + if outgoing[block.id.index() as usize].is_none() { + let mut states = initial.clone(); + transfer(function, block.id, plan, &mut states)?; + } + } + Ok(()) +} +fn transfer( + function: &Function, + block: BlockId, + plan: &CallGenerationStoragePlan, + states: &mut [ProtocolState], +) -> Result<(), String> { + for statement in &function.blocks[block.index() as usize].statements { + match &statement.kind { + StatementKind::OpenCallOwnerGeneration { generation, root } => { + let slot = plan + .slot(*generation) + .ok_or("call owner generation Open has no storage")?; + let state = &mut states[generation.index()]; + if *root != slot.root + || state.storage != StorageState::Closed + || !state.loans.is_empty() + { + return Err("call owner generation opens an active or foreign escrow".into()); + } + state.storage = StorageState::Attached; + } + StatementKind::ReplaceCallOwnerGeneration { + generation, + root, + rhs_owner, + } => { + let slot = plan + .slot(*generation) + .ok_or("call owner generation Replace has no storage")?; + let state = &mut states[generation.index()]; + if *root != slot.root + || *rhs_owner == *root + || state.storage == StorageState::Closed + || state.loans.is_empty() + { + return Err( + "call owner generation replacement has no captured active owner".into(), + ); + } + state.storage = StorageState::Retired; + } + StatementKind::CloseCallOwnerGeneration { generation } => { + let state = states + .get_mut(generation.index()) + .ok_or("call owner generation Close has no storage")?; + if state.storage == StorageState::Closed || !state.loans.is_empty() { + return Err( + "call owner generation closes an active loan or already closed escrow" + .into(), + ); + } + state.storage = StorageState::Closed; + } + StatementKind::BeginCallView { local, .. } => { + for (id, loans) in &plan.loans { + if loans.contains(local) { + let state = &mut states[id.index()]; + if state.storage != StorageState::Attached || !state.loans.insert(*local) { + return Err( + "call owner generation recaptures a retired or active cohort" + .into(), + ); + } + } + } + } + StatementKind::EndCallView { local } => { + for (id, loans) in &plan.loans { + if loans.contains(local) && !states[id.index()].loans.remove(local) { + return Err("call owner generation End has no active capture".into()); + } + } + } + StatementKind::Assign { target, .. } => { + if let E::Local(root) = target.kind { + if plan.slots.iter().any(|slot| { + slot.root == root + && states[slot.generation.index()].storage != StorageState::Closed + }) { + return Err( + "call owner generation protected root still uses ordinary Assign" + .into(), + ); + } + } + } + _ => {} + } + } + if matches!( + function.blocks[block.index() as usize].terminator.kind, + TerminatorKind::Return(_) | TerminatorKind::Respond(_) + ) && states + .iter() + .any(|state| state.storage != StorageState::Closed || !state.loans.is_empty()) + { + return Err("call owner generation exits before exact loan and escrow cleanup".into()); + } + Ok(()) +} + +// Only finite canonical preparation calls these transitions. Original archives +// never change and are never marked as runtime or liveness roots. +fn shift(record: &mut CallOwnerGeneration, block: BlockId, amount: usize) { + let shift_site = |site: &mut Site| { + if site.block == block { + site.statement += amount; + } + }; + shift_site(&mut record.start); + if let Some(consumer) = &mut record.consumer { + shift_site(consumer); + } + for capture in &mut record.captures { + shift_site(&mut capture.begin); + } + for replacement in &mut record.replacements { + shift_site(&mut replacement.acquired); + shift_site(&mut replacement.replacement); + } + record.statements = std::mem::take(&mut record.statements) + .into_iter() + .map(|(mut site, value)| { + shift_site(&mut site); + (site, value) + }) + .collect(); +} +pub(super) fn sequence_transition( + function: &mut Function, + before: &Function, + edit: &crate::breakpoint_regions::SequenceEdit, + types: &TypeInterner, +) -> Result<(), String> { + if !has_records(before) { + return Ok(()); + } + validate(before, types)?; + // Reuse the identical complete finite before/after edit validation. This + // shadow cannot mint a generation or change the actual breakpoint records. + let mut shadow = function.clone(); + crate::breakpoint_regions::sequence_transition(&mut shadow, before, edit.clone(), types)?; + let mut records = before.call_owner_generations.clone(); + for record in &mut records { + if let Some((block, prefix)) = &edit.prefix { + shift(record, *block, prefix.len()); + } + let owns_header = record.terminators.contains_key(&edit.header); + if owns_header { + record.terminators.insert(edit.header, edit.after.clone()); + } + for (block, _, new) in &edit.redirects { + if record.terminators.contains_key(block) { + record.terminators.insert(*block, new.clone()); + } + } + if owns_header { + for (block, values) in &edit.append { + record.blocks.insert(*block); + let offset = before.blocks[block.index() as usize].statements.len(); + let offset = offset + + edit + .prefix + .as_ref() + .filter(|(id, _)| id == block) + .map_or(0, |(_, values)| values.len()); + for (index, value) in values.iter().enumerate() { + record.statements.insert( + Site { + block: *block, + statement: offset + index, + }, + value.clone(), + ); + } + } + if let Some((block, values)) = &edit.prefix { + record.blocks.insert(*block); + for (index, value) in values.iter().enumerate() { + record.statements.insert( + Site { + block: *block, + statement: index, + }, + value.clone(), + ); + } + } + for block in &edit.blocks { + record.blocks.insert(block.id); + record + .terminators + .insert(block.id, block.terminator.clone()); + for (index, value) in block.statements.iter().enumerate() { + record.statements.insert( + Site { + block: block.id, + statement: index, + }, + value.clone(), + ); + } + } + } + record.edges = touching(&all_edges(function), &record.blocks); + } + function.call_owner_generations = records; + Ok(()) +} +pub(super) fn sum_transition( + function: &mut Function, + before: &Function, + types: &TypeInterner, +) -> Result<(), String> { + if !has_records(before) { + return Ok(()); + } + validate(before, types)?; + let mut shadow = function.clone(); + crate::breakpoint_regions::sum_transition(&mut shadow, before, types)?; + for record in &mut function.call_owner_generations { + for (block, snapshot) in &mut record.terminators { + let actual = &function.blocks[block.index() as usize].terminator; + if !crate::breakpoint_regions::terminator_equal(snapshot, actual) { + *snapshot = actual.clone(); + } + } + record.edges = touching(&all_edges_from_blocks(&function.blocks), &record.blocks); + } + Ok(()) +} +fn all_edges_from_blocks(blocks: &[BasicBlock]) -> Vec { + let mut edges = Vec::new(); + for block in blocks { + let targets = match &block.terminator.kind { + TerminatorKind::Goto(target) => vec![*target], + TerminatorKind::Branch { + then_block, + else_block, + .. + } => vec![*then_block, *else_block], + TerminatorKind::Switch { + variants, + otherwise, + .. + } => variants + .iter() + .map(|(_, target, _)| *target) + .chain(*otherwise) + .collect(), + TerminatorKind::ForEach { body, exit, .. } => vec![*body, *exit], + TerminatorKind::ReflectedTypeDispatch { + arms, otherwise, .. + } => arms + .iter() + .map(|arm| arm.target) + .chain(std::iter::once(*otherwise)) + .collect(), + TerminatorKind::Return(_) + | TerminatorKind::Respond(_) + | TerminatorKind::Unreachable => Vec::new(), + }; + edges.extend(targets.into_iter().enumerate().map(|(arm, target)| Edge { + source: block.id, + arm, + target, + })); + } + edges +} +fn remap_statement(value: &mut Statement, map: &[Option]) -> Result<(), String> { + let mut block = BasicBlock { + id: BlockId(0), + statements: vec![value.clone()], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: value.span, + }, + }; + crate::breakpoint_regions::remap_snapshot(&mut block, map)?; + *value = block.statements.remove(0); + Ok(()) +} +pub(super) fn remap_locals(function: &mut Function, map: &[Option]) -> Result<(), String> { + let get = |id: LocalId| { + map.get(id.index() as usize) + .copied() + .flatten() + .ok_or("generation surviving operand has no local remap") + }; + for record in &mut function.call_owner_generations { + record.root = get(record.root)?; + for capture in &mut record.captures { + capture.loan = get(capture.loan)?; + } + for replacement in &mut record.replacements { + replacement.rhs = get(replacement.rhs)?; + } + for value in record.statements.values_mut() { + remap_statement(value, map)?; + } + for value in record.terminators.values_mut() { + let mut block = BasicBlock { + id: BlockId(0), + statements: Vec::new(), + terminator: value.clone(), + }; + crate::breakpoint_regions::remap_snapshot(&mut block, map)?; + *value = block.terminator; + } + if record.consumer.is_some() { + let mut ownership = hir::CallOwnership::Source(record.current_source.clone()); + ownership.remap_metadata_locals(|id| get(id).map_err(str::to_owned))?; + let hir::CallOwnership::Source(source) = ownership else { + unreachable!() + }; + record.current_source = source; + } + } + Ok(()) +} +pub(super) fn remap_blocks( + function: &mut Function, + before: &Function, + map: &[Option], +) -> Result<(), String> { + if !has_records(before) { + return Ok(()); + } + // Exactly the reachable compaction, never a caller-supplied subset/range. + let cfg = ControlFlowGraph::analyze(before).map_err(|errors| format!("{errors:?}"))?; + let reachable = cfg + .reverse_postorder() + .iter() + .copied() + .collect::>(); + if map.len() != before.blocks.len() + || before + .blocks + .iter() + .any(|block| map[block.id.index() as usize].is_some() != reachable.contains(&block.id)) + { + return Err("generation block remap is not exact reachable compaction".into()); + } + let get = |id: BlockId| { + map.get(id.index() as usize) + .copied() + .flatten() + .ok_or("generation surviving site has no block remap") + }; + let mut expected = before + .blocks + .iter() + .filter(|block| reachable.contains(&block.id)) + .cloned() + .collect::>(); + for block in &mut expected { + block.id = get(block.id)?; + crate::sequences::prune::block_targets(&mut block.terminator.kind, &|target| { + // Reachability includes every actual target, so this cannot be absent. + *target = map[target.index() as usize].expect("reachable edge target remains"); + }); + } + if !crate::breakpoint_regions::blocks_equal(&expected, &function.blocks) + || function.entry != get(before.entry)? + { + return Err("generation compaction changes surviving original blocks".into()); + } + let mut records = Vec::new(); + let mut ids = vec![None; before.call_owner_generations.len()]; + for mut record in before.call_owner_generations.clone() { + if get(record.start.block).is_err() { + if record.statements.keys().any(|site| get(site.block).is_ok()) { + return Err("generation surviving operations lose their Open".into()); + } + continue; + } + let new_id = CallOwnerGenerationId(records.len()); + ids[record.id.index()] = Some(new_id); + record.id = new_id; + record.slot = CallOwnerEscrowId(new_id.index()); + record.entry = get(record.entry)?; + record.start.block = get(record.start.block)?; + record.consumer = record.consumer.and_then(|mut site| { + site.block = get(site.block).ok()?; + Some(site) + }); + record.captures.retain_mut(|capture| { + if let Ok(block) = get(capture.begin.block) { + capture.begin.block = block; + true + } else { + false + } + }); + record.replacements.retain_mut(|replacement| { + if let (Ok(acquired), Ok(replaced)) = ( + get(replacement.acquired.block), + get(replacement.replacement.block), + ) { + replacement.acquired.block = acquired; + replacement.replacement.block = replaced; + true + } else { + false + } + }); + record.statements = record + .statements + .into_iter() + .filter_map(|(mut site, value)| { + site.block = get(site.block).ok()?; + Some((site, value)) + }) + .collect(); + record.terminators = record + .terminators + .into_iter() + .filter_map(|(block, mut value)| { + let block = get(block).ok()?; + crate::sequences::prune::block_targets(&mut value.kind, &|target| { + *target = map[target.index() as usize].expect("reachable snapshot edge") + }); + Some((block, value)) + }) + .collect(); + record.blocks = record + .blocks + .into_iter() + .filter_map(|block| get(block).ok()) + .collect(); + records.push(record); + } + let remap_operation = |statement: &mut Statement| -> Result<(), String> { + match &mut statement.kind { + StatementKind::OpenCallOwnerGeneration { generation, .. } + | StatementKind::ReplaceCallOwnerGeneration { generation, .. } + | StatementKind::CloseCallOwnerGeneration { generation } => { + *generation = ids + .get(generation.index()) + .copied() + .flatten() + .ok_or("generation operation survives its removed record")?; + } + _ => {} + } + Ok(()) + }; + for block in &mut function.blocks { + for statement in &mut block.statements { + remap_operation(statement)?; + } + } + for record in &mut records { + for value in record.statements.values_mut() { + remap_operation(value)?; + } + record.edges = touching(&all_edges(function), &record.blocks); + } + function.call_owner_generations = records; + Ok(()) +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_mir/src/call_owner_generations/tests.rs b/crates/jett_mir/src/call_owner_generations/tests.rs new file mode 100644 index 00000000..974a5aad --- /dev/null +++ b/crates/jett_mir/src/call_owner_generations/tests.rs @@ -0,0 +1,484 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; +use std::collections::HashMap; + +const CONDITIONAL: &str = r#"namespace app +struct User: + name: string +struct Alternate: + code: int64 +function inspect_builder(view builder: TypeConstruction, extra: int64) returns int64: + trace builder + return extra +function exercise(incoming: optional[int64]) returns int64: + mutable TypeConstruction switchable = type.construct_start[User]() + int64 selected = inspect_builder(view switchable, incoming handle: + trace switchable + switchable = type.construct_start[Alternate]() + trace switchable + default 7 + ) + trace switchable + return selected +function main() returns nothing: + int64 absent = exercise(none) + trace absent + int64 present = exercise(some(11)) + trace present + return nothing +"#; + +const ABORT: &str = r#"namespace app +struct User: + name: string +struct Alternate: + code: int64 +function inspect_builder(view builder: TypeConstruction, extra: int64) returns int64: + trace builder + return extra +function exercise(incoming: optional[int64]) returns int64: + mutable TypeConstruction switchable = type.construct_start[User]() + int64 selected = inspect_builder(view switchable, incoming handle: + switchable = type.construct_start[Alternate]() + trace switchable + return 13 + ) + trace switchable + return selected +function main() returns nothing: + int64 aborted = exercise(none) + trace aborted + return nothing +"#; + +fn source_program(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); + assert!( + resolved + .diagnostics + .iter() + .all(|d| d.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(|d| d.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let high = hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .expect("checked Source HIR"); + let program = crate::lower(&high, &checked.interner).expect("constructor-owned generation MIR"); + crate::validate_call_ownership(&program, &checked.interner) + .expect("all original Source and generation proofs"); + (program, checked.interner) +} +fn exercise(program: &Program) -> &Function { + program + .functions + .iter() + .find(|f| f.identity.declaration.name == "exercise") + .expect("exercise") +} +fn exercise_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|f| f.identity.declaration.name == "exercise") + .expect("exercise") +} +fn prepared(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); + crate::validate_call_ownership(program, types) + .expect("fresh prepared Source and generation proofs"); +} +fn rejects(program: &Program, types: &TypeInterner, expected: &str) { + let message = validate(exercise(program), types) + .expect_err("corrupted generation must fail independently"); + assert!(message.contains(expected), "{message}"); + assert!( + crate::validate_call_ownership(program, types).is_err(), + "outer complete gate also refuses" + ); +} + +#[test] +fn generation_conditional_replacement_preserves_current_reads_and_captured_owner() { + for release in [false, true] { + let (mut program, types) = source_program(CONDITIONAL, release); + let function = exercise(&program); + assert_eq!(function.call_owner_generations.len(), 1); + let record = &function.call_owner_generations[0]; + assert_eq!(record.captures.len(), 1); + assert_eq!(record.replacements.len(), 1); + let loan = record.captures[0].loan; + let root = record.root; + // The actual capture is exactly a View(Local), never a new owner clone. + let capture = &function.blocks[record.captures[0].begin.block.index() as usize].statements + [record.captures[0].begin.statement]; + assert!( + matches!(&capture.kind, StatementKind::BeginCallView { value, .. } if matches!(&value.kind, E::View(raw) if matches!(raw.kind, E::Local(local) if local == root))) + ); + let plan = crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("scoped old owner survives replacement without cloning"); + let slot = plan.generation_storage().slots()[0]; + assert_eq!(slot.root(), root); + assert_eq!(slot.generation().index(), 0); + assert_eq!(slot.escrow().index(), 0); + assert!( + plan.generation_storage() + .covers_loan(slot.generation(), loan) + ); + prepared(&mut program, &types); + crate::move_values::MoveValuePlan::analyze(&program, exercise(&program), &types) + .expect("fresh prepared generation and liveness"); + } +} + +#[test] +fn generation_abort_ends_loans_before_cleanup_and_never_requires_callee_entry() { + for release in [false, true] { + let (mut program, types) = source_program(ABORT, release); + let function = exercise(&program); + let record = &function.call_owner_generations[0]; + let abort = function.blocks.iter().find(|block| matches!(&block.terminator.kind, TerminatorKind::Return(Some(value)) if matches!(value.kind, E::Int(13)))).expect("source handler return13"); + let close = abort.statements.iter().position(|s| matches!(s.kind, StatementKind::CloseCallOwnerGeneration { generation } if generation == record.id)).expect("abort closes escrow"); + assert!( + close > 0 + && matches!(abort.statements[close - 1].kind, StatementKind::EndCallView { local } if local == record.captures[0].loan) + ); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("abort cleanup has no retained owning capture"); + prepared(&mut program, &types); + crate::move_values::MoveValuePlan::analyze(&program, exercise(&program), &types) + .expect("prepared abort cleanup"); + } +} + +#[test] +fn generation_repeated_replacements_keep_one_captured_escrow() { + let source = CONDITIONAL.replace(" trace switchable\n default 7", " trace switchable\n switchable = type.construct_start[User]()\n trace switchable\n default 7"); + for release in [false, true] { + let (program, types) = source_program(&source, release); + let function = exercise(&program); + assert_eq!(function.call_owner_generations.len(), 1); + assert_eq!(function.call_owner_generations[0].replacements.len(), 2); + let plan = crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("two replacements preserve the first captured generation"); + assert_eq!(plan.generation_storage().slots().len(), 1); + } +} + +#[test] +fn generation_operations_need_exact_private_sites_identity_and_independent_rhs() { + let (program, types) = source_program(CONDITIONAL, false); + for mutation in 0..6 { + let mut altered = program.clone(); + let function = exercise_mut(&mut altered); + let record = function.call_owner_generations[0].clone(); + match mutation { + 0 => { + function.blocks[record.start.block.index() as usize] + .statements + .remove(record.start.statement); + } + 1 => { + let value = function.blocks[record.start.block.index() as usize].statements + [record.start.statement] + .clone(); + function.blocks[record.start.block.index() as usize] + .statements + .push(value); + } + 2 => { + let site = record.replacements[0].replacement; + let StatementKind::ReplaceCallOwnerGeneration { rhs_owner, .. } = + &mut function.blocks[site.block.index() as usize].statements[site.statement] + .kind + else { + panic!("Replace") + }; + *rhs_owner = record.root; + } + 3 => { + let site = record.replacements[0].acquired; + let StatementKind::Let { value, .. } = + &mut function.blocks[site.block.index() as usize].statements[site.statement] + .kind + else { + panic!("acquired Let") + }; + value.kind = E::Local(record.captures[0].loan); + } + 4 => { + let last = function + .blocks + .iter_mut() + .find(|block| { + block.statements.iter().any(|s| { + matches!(s.kind, StatementKind::CloseCallOwnerGeneration { .. }) + }) + }) + .expect("close block"); + let value = last + .statements + .iter() + .find(|s| matches!(s.kind, StatementKind::CloseCallOwnerGeneration { .. })) + .unwrap() + .clone(); + last.statements.push(value); + } + 5 => { + function.call_owner_generations[0].owner = program + .functions + .iter() + .find(|f| f.id != function.id) + .unwrap() + .id; + } + _ => unreachable!(), + } + rejects( + &altered, + &types, + if mutation == 5 { + "foreign" + } else { + "constructor-owned" + }, + ); + } +} + +#[test] +fn generation_disconnected_copy_and_alternate_scope_entry_are_not_authority() { + let (program, types) = source_program(CONDITIONAL, false); + let mut copy = program.clone(); + let function = exercise_mut(&mut copy); + let record = function.call_owner_generations[0].clone(); + let close = function + .blocks + .iter() + .flat_map(|b| &b.statements) + .find(|s| matches!(s.kind, StatementKind::CloseCallOwnerGeneration { .. })) + .unwrap() + .clone(); + let block = BlockId(function.blocks.len() as u32); + function.blocks.push(BasicBlock { + id: block, + statements: vec![close], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: function.span, + }, + }); + rejects(©, &types, "constructor-owned"); + + let mut entry = program.clone(); + let function = exercise_mut(&mut entry); + let block = BlockId(function.blocks.len() as u32); + function.blocks.push(BasicBlock { + id: block, + statements: Vec::new(), + terminator: Terminator { + kind: TerminatorKind::Goto(record.replacements[0].replacement.block), + span: function.span, + }, + }); + rejects(&entry, &types, "CFG"); +} + +#[test] +fn generation_canonical_loop_and_dense_compaction_preserve_archives_without_roots() { + let source = CONDITIONAL.replace(" trace switchable\n default 7", " trace switchable\n for item in view values:\n trace switchable\n default 7").replace(" int64 selected =", " list[int64] values = list(1, 2)\n int64 selected ="); + for release in [false, true] { + let (mut program, types) = source_program(&source, release); + let original = exercise(&program).call_owner_generations[0].clone(); + let function = exercise_mut(&mut program); + let extra = LocalId::new(function.locals.len() as u32); + function.locals.push(Local { + id: extra, + name: "unused_generation_control".to_string(), + debug_type_name: None, + ty: TypeInterner::INT64, + debug_ty: TypeInterner::INT64, + mutable: false, + span: function.span, + view_source: None, + }); + assert!( + function + .local(extra) + .is_some_and(|local| local.name == "unused_generation_control") + ); + let block = BlockId(function.blocks.len() as u32); + function.blocks.push(BasicBlock { + id: block, + statements: Vec::new(), + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: function.span, + }, + }); + crate::validate_call_ownership(&program, &types) + .expect("unused metadata creates no execution authority"); + prepared(&mut program, &types); + let function = exercise(&program); + let record = &function.call_owner_generations[0]; + assert!( + crate::breakpoint_regions::expression_equal( + &original.original_call, + &record.original_call + ), + "original call archive is immutable" + ); + assert_eq!( + record.original_root, original.original_root, + "Source binding archive is immutable" + ); + assert_eq!( + record.original_root_header, original.original_root_header, + "full local header archive is immutable" + ); + assert_eq!( + record + .replacements + .iter() + .map(|replacement| &replacement.original) + .collect::>(), + original + .replacements + .iter() + .map(|replacement| &replacement.original) + .collect::>(), + "original source assignment archives are immutable" + ); + // Sequence lowering adds live cursor/length locals, so total count + // cannot prove whether this particular unused seed was removed. + assert!( + function + .locals + .iter() + .all(|local| local.name != "unused_generation_control") + ); + assert!( + function + .locals + .iter() + .enumerate() + .all(|(index, local)| local.id == LocalId::new(index as u32)), + "all surviving current local IDs remain dense" + ); + assert!( + function + .blocks + .iter() + .all(|b| !matches!(b.terminator.kind, TerminatorKind::ForEach { .. })) + ); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("canonical sequence addition and compaction rejoin every generation site"); + } +} + +#[test] +fn generation_source_name_and_full_local_headers_remain_independent_and_exact() { + for release in [false, true] { + let (mut program, types) = source_program(CONDITIONAL, release); + let function = exercise(&program); + let record = &function.call_owner_generations[0]; + let source_header = record.original_root; + let local_header = record.original_root_header.clone(); + assert_eq!(function.local(record.root), Some(&local_header)); + assert_eq!(source_header.local, local_header.id); + assert_eq!(source_header.declaration_span.file, local_header.span.file); + assert!(local_header.span.start < source_header.declaration_span.start); + assert!(source_header.declaration_span.end < local_header.span.end); + assert_eq!( + &CONDITIONAL[source_header.declaration_span.start as usize + ..source_header.declaration_span.end as usize], + "switchable" + ); + assert!( + CONDITIONAL[local_header.span.start as usize..local_header.span.end as usize] + .starts_with("mutable TypeConstruction switchable =") + ); + validate(function, &types) + .expect("each original checked header has its own exact coordinate role"); + prepared(&mut program, &types); + let record = &exercise(&program).call_owner_generations[0]; + assert_eq!( + record.original_root, source_header, + "Source identifier fact stays original" + ); + assert_eq!( + record.original_root_header, local_header, + "whole declaration archive never remaps" + ); + crate::move_values::MoveValuePlan::analyze(&program, exercise(&program), &types) + .expect("fresh prepared storage retains both independent headers"); + + for mutation in 0..6 { + let mut altered = program.clone(); + let function = exercise_mut(&mut altered); + let root = function.call_owner_generations[0].root; + match mutation { + 0 => { + // This span still contains the original name; containment + // alone must not replace exact sealed-header validation. + function.locals[root.index() as usize].span.start += 1; + } + 1 => function.locals[root.index() as usize] + .name + .push_str("_foreign"), + 2 => { + function.locals[root.index() as usize].debug_type_name = + Some("ForeignHeader".into()) + } + 3 => { + function.call_owner_generations[0] + .original_root + .declaration_span + .start += 1 + } + 4 => { + let header = &mut function.call_owner_generations[0].original_root_header; + header.id = LocalId::new(header.id.index() + 1); + } + 5 => { + function.call_owner_generations[0] + .original_root_header + .span + .start += 1 + } + _ => unreachable!(), + } + rejects( + &altered, + &types, + match mutation { + 3 => "immutable original source witness", + 4 => "sealed original source or local header", + _ => "sealed local declaration header", + }, + ); + } + } +} diff --git a/crates/jett_mir/src/call_ownership.rs b/crates/jett_mir/src/call_ownership.rs new file mode 100644 index 00000000..18adc816 --- /dev/null +++ b/crates/jett_mir/src/call_ownership.rs @@ -0,0 +1,3189 @@ +//! Typed source acquisitions, separate from physical invocation operands. +//! Every original block is checked before reachability or local compaction. +use std::cell::RefCell; +use std::collections::{BTreeMap, BTreeSet}; + +use jett_common::Span; +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}; + +#[cfg(test)] +mod tests; + +/// A validated source transfer. `binding` is the original caller's binding, +/// not the generated physical slot or a backing parent of a field copy. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceAcquisition { + pub binding: Option, + pub effect: CheckedCallerEffect, + pub actual_type: TypeId, + pub storage_type: TypeId, + pub source_span: Span, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ArgumentAcquisition { + pub parameter_index: usize, + pub source_index: usize, + pub source: SourceAcquisition, +} + +/// References identify already-validated occurrences in this immutable MIR. +/// Pointer equality is only a cache lookup, never ownership authority. +#[derive(Debug)] +pub struct CallerAcquisitions<'a> { + owners: BTreeMap, + calls: Vec<(&'a Expression, Vec)>, + operands: Vec<(&'a Expression, LocalId)>, + // Exact input reads authenticated by a complete converted Source Handle. + handled_conversions: Vec<(&'a Expression, &'a Expression)>, + pub(super) absent_successes: Vec, + pub(super) present_successes: Vec, + generation_storage: crate::CallGenerationStoragePlan, +} + +impl Default for CallerAcquisitions<'_> { + fn default() -> Self { + Self { + owners: BTreeMap::new(), + calls: Vec::new(), + operands: Vec::new(), + handled_conversions: Vec::new(), + absent_successes: Vec::new(), + present_successes: Vec::new(), + generation_storage: crate::CallGenerationStoragePlan::empty(), + } + } +} +impl CallerAcquisitions<'_> { + pub(crate) fn generation_storage(&self) -> &crate::CallGenerationStoragePlan { + &self.generation_storage + } + pub fn owner_initializer(&self, owner: LocalId) -> Option<&SourceAcquisition> { + self.owners.get(&owner.index()) + } + + pub fn arguments(&self, expression: &Expression) -> Option<&[ArgumentAcquisition]> { + self.calls.iter().find_map(|(call, arguments)| { + std::ptr::eq(*call, expression).then_some(arguments.as_slice()) + }) + } + + pub fn owner_initializers(&self) -> impl Iterator { + self.owners + .iter() + .map(|(&local, source)| (LocalId::new(local), source)) + } + + /// Readonly occurrence lookup after full Source/CFG/Begin validation. + /// This does not make an unassociated View initializer owning or copyable. + pub(crate) fn handled_conversion_input(&self, conversion: &Expression) -> Option<&Expression> { + self.handled_conversions + .iter() + .find_map(|(proved, input)| std::ptr::eq(*proved, conversion).then_some(*input)) + } + + pub fn argument_binding(&self, expression: &Expression) -> Option { + self.operands + .iter() + .find_map(|(operand, binding)| std::ptr::eq(*operand, expression).then_some(*binding)) + } +} + +/// Internal original proof; only canonical sum preparation turns it into a +/// retained certificate. It is not part of the public caller-acquisition API. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct AbsentSuccessPlan { + pub(super) function: hir::FunctionId, + pub(super) identity: hir::FunctionIdentity, + pub(super) source: LocalId, + pub(super) tag: LocalId, + pub(super) output: LocalId, + pub(super) source_type: TypeId, + pub(super) output_type: TypeId, + pub(super) span: Span, + pub(super) selecting: BlockId, + pub(super) failure: BlockId, +} + +/// Only a complete original success-edge proof can be minted by canonical +/// Result(T,Never) preparation. No public packet or Goto creates this authority. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct PresentSuccessPlan { + pub(super) function: hir::FunctionId, + pub(super) identity: hir::FunctionIdentity, + pub(super) source: LocalId, + pub(super) tag: LocalId, + pub(super) output: LocalId, + pub(super) source_type: TypeId, + pub(super) output_type: TypeId, + pub(super) span: Span, + pub(super) selecting: BlockId, + pub(super) success: BlockId, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Site { + block: BlockId, + statement: usize, +} + +#[derive(Clone, Copy)] +enum Definition<'a> { + Let(&'a Expression), + Begin(&'a Expression), + SumTake { source: LocalId, success: bool }, + Other, +} + +#[derive(Clone, Copy)] +struct Defined<'a> { + site: Site, + span: Span, + kind: Definition<'a>, +} + +fn restore_converted_observation_snapshot( + initializer: &Expression, + argument: &hir::ArgumentOwnership, + types: &TypeInterner, +) -> Result { + if !matches!(initializer.kind, E::InterfaceCoerce { .. }) + || !hir::observation_data_type(types, argument.actual_type) + || !crate::handlers::can_snapshot_view(types, argument.actual_type) + { + return Err( + "call ownership observation requires its exact ordinary raw snapshot initializer" + .into(), + ); + } + fn restore( + value: &Expression, + argument: &hir::ArgumentOwnership, + ) -> Result { + match &value.kind { + E::InterfaceCoerce { + value: raw, + adapters, + } => Ok(Expression { + kind: E::InterfaceCoerce { + value: Box::new(restore(raw, argument)?), + adapters: adapters.clone(), + }, + ty: value.ty, + span: value.span, + }), + E::Clone(raw) + if value.ty == argument.actual_type + && raw.ty == argument.actual_type + && value.span == argument.source_span + && raw.span == argument.source_span => + { + Ok(raw.as_ref().clone()) + } + _ => Err("call ownership observation lost its exact raw snapshot occurrence".into()), + } + } + restore(initializer, argument) +} + +/// The integration gate runs this for every original function, including +/// descriptor-only bodies and disconnected blocks. HIR signature projections +/// retain exact IDs/identities/modes; no source names supply target authority. +pub fn validate_function<'a>( + program: &Program, + function: &'a Function, + types: &TypeInterner, +) -> Result, String> { + ProgramValidation::new(program).validate_function(function, types) +} + +/// One immutable signature projection for one ownership validation pass. +/// The program borrow prevents reuse across mutation or canonical preparation. +pub(crate) struct ProgramValidation<'p> { + _program: &'p Program, + signatures: Vec, +} + +impl<'p> ProgramValidation<'p> { + pub(crate) fn new(program: &'p Program) -> Self { + let signatures = program + .functions + .iter() + .map(|function| hir::Function { + id: function.id, + identity: function.identity.clone(), + debug_kind: function.debug_kind.clone(), + // MIR has already replaced resolver-session IDs with declaration identity. + // The source certificate and exact dispatcher shape remain authoritative. + source_definition: None, + params: function.params.clone(), + capture_count: function.capture_count, + return_type: function.return_type, + locals: function.locals.clone(), + body: hir::Block { + statements: Vec::new(), + span: function.span, + }, + span: function.span, + }) + .collect::>(); + Self { + _program: program, + signatures, + } + } + + pub(crate) fn validate_function<'a>( + &self, + function: &'a Function, + types: &TypeInterner, + ) -> Result, String> { + validate_function_with_signatures(function, types, &self.signatures) + } +} + +fn validate_function_with_signatures<'a>( + function: &'a Function, + types: &TypeInterner, + signatures: &[hir::Function], +) -> 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 context = if function.debug_kind == hir::FunctionDebugKind::Inline { + CheckedOwnershipContext::Ordinary + } else { + match function.identity.declaration.kind { + hir::DeclarationKind::Verify => CheckedOwnershipContext::Verify, + hir::DeclarationKind::Property => CheckedOwnershipContext::Property, + _ => CheckedOwnershipContext::Ordinary, + } + }; + for block in &function.blocks { + for (statement, value) in block.statements.iter().enumerate() { + let site = Site { + block: block.id, + statement, + }; + let context = if breakpoint_sites.statement(block.id, statement) { + CheckedOwnershipContext::BreakpointExpression + } else { + context + }; + validator.statement(&value.kind, site, context)?; + } + let site = Site { + block: block.id, + statement: block.statements.len(), + }; + let context = if breakpoint_sites.terminator(block.id) { + CheckedOwnershipContext::BreakpointExpression + } else { + context + }; + validator.terminator(&block.terminator.kind, site, context)?; + } + validator.validate_prepared_absences()?; + validator.validate_prepared_presences()?; + validator.acquisitions.absent_successes = validator.absent_successes.into_inner(); + validator.acquisitions.present_successes = validator.present_successes.into_inner(); + validator.acquisitions.generation_storage = + crate::call_owner_generations::validate(function, types)?; + Ok(validator.acquisitions) +} + +struct Validator<'a, 't, 's> { + function: &'a Function, + types: &'t TypeInterner, + signatures: &'s [hir::Function], + locals: Vec, + definitions: BTreeMap>>, + predecessors: Vec>, + stages: BTreeMap, + claimed_slots: BTreeSet, + acquisitions: CallerAcquisitions<'a>, + absent_successes: RefCell>, + present_successes: RefCell>, + iteration_scopes: Vec, + iteration_scope_enabled: bool, +} + +impl<'a, 't, 's> Validator<'a, 't, 's> { + fn new( + function: &'a Function, + types: &'t TypeInterner, + signatures: &'s [hir::Function], + ) -> Result { + let iteration_scopes = crate::iteration_views::scopes(function, types)?; + let locals = function + .locals + .iter() + .map(|local| hir::OwnershipLocalInfo { + ty: local.ty, + mutable: local.mutable, + span: local.span, + view_source: local.view_source, + view_iteration: None, + is_view_parameter: function + .parameter_for_local(local.id) + .is_some_and(|parameter| parameter.mode == hir::ParamMode::View), + }) + .collect::>(); + let stages = crate::call_views::validate(function, types)?; + let mut definitions = BTreeMap::>>::new(); + let mut predecessors = vec![Vec::new(); function.blocks.len()]; + let mut define = |local: LocalId, site, span, kind| { + definitions + .entry(local.index()) + .or_default() + .push(Defined { site, span, kind }); + }; + for block in &function.blocks { + if block.id.index() as usize >= function.blocks.len() + || function.blocks[block.id.index() as usize].id != block.id + { + return Err("call ownership block IDs are not dense".into()); + } + for (index, statement) in block.statements.iter().enumerate() { + let site = Site { + block: block.id, + statement: index, + }; + match &statement.kind { + S::Let { local, value } => { + define(*local, site, statement.span, Definition::Let(value)) + } + S::BeginCallView { local, value } => { + define(*local, site, statement.span, Definition::Begin(value)) + } + S::SumTake { + source, + target, + success, + } => define( + *target, + site, + statement.span, + Definition::SumTake { + source: *source, + success: *success, + }, + ), + S::CheckRefinement { local, .. } + | S::SequenceLength { target: local, .. } + | S::SequenceGet { target: local, .. } + | S::SumTag { target: local, .. } => { + define(*local, site, statement.span, Definition::Other) + } + S::ReplaceCallOwnerGeneration { root, .. } => { + define(*root, site, statement.span, Definition::Other) + } + S::Assign { target, .. } => { + if let Some(local) = address_root(target) { + define(local, site, statement.span, Definition::Other); + } + } + _ => {} + } + } + let site = Site { + block: block.id, + statement: block.statements.len(), + }; + match &block.terminator.kind { + T::Switch { variants, .. } => { + for (_, target, bindings) in variants { + for &local in bindings { + define( + local, + Site { + block: *target, + statement: 0, + }, + block.terminator.span, + Definition::Other, + ); + } + } + } + T::ForEach { + key, value, body, .. + } => { + define( + *key, + Site { + block: *body, + statement: 0, + }, + block.terminator.span, + Definition::Other, + ); + if let Some(value) = value { + define( + *value, + Site { + block: *body, + statement: 0, + }, + block.terminator.span, + Definition::Other, + ); + } + } + _ => { + let _ = site; + } + } + for target in successors(&block.terminator.kind) { + let incoming = predecessors + .get_mut(target.index() as usize) + .ok_or("call ownership CFG target is outside its function")?; + incoming.push(block.id); + } + } + for (id, local) in function.locals.iter().enumerate() { + if local.id.index() as usize != id + || !valid_type(types, local.ty) + || !valid_type(types, local.debug_ty) + { + return Err( + "call ownership local metadata is outside its function or interner".into(), + ); + } + } + for &local in definitions.keys() { + if function.local(LocalId::new(local)).is_none() { + return Err("call ownership definition is outside its local table".into()); + } + } + Ok(Self { + function, + types, + signatures, + locals, + definitions, + predecessors, + stages, + claimed_slots: BTreeSet::new(), + acquisitions: CallerAcquisitions::default(), + absent_successes: RefCell::new(Vec::new()), + present_successes: RefCell::new(Vec::new()), + iteration_scopes, + iteration_scope_enabled: true, + }) + } + + fn invocation( + &mut self, + expression: &'a Expression, + site: Site, + context: CheckedOwnershipContext, + ) -> Result<(), String> { + let (ownership, args, order) = match &expression.kind { + E::Call { + ownership, + args, + evaluation_order, + .. + } + | E::Intrinsic { + ownership, + args, + evaluation_order, + .. + } + | E::IndirectCall { + ownership, + args, + evaluation_order, + .. + } => (ownership, args, evaluation_order), + _ => return Ok(()), + }; + if self.iteration_scope_enabled { + let ensure = |source: hir::CallerViewSource| { + if let hir::CallerViewSource::Local(local) = source { + crate::iteration_views::ensure_scope( + self.function, + &self.iteration_scopes, + local, + site.block, + )?; + } + Ok::<_, String>(()) + }; + match ownership { + hir::CallOwnership::Source(source) => { + for argument in &source.arguments { + match argument.origin { + hir::CallerOrigin::Binding(ref fact) => { + ensure(hir::CallerViewSource::Local(fact.local))? + } + hir::CallerOrigin::BorrowedProjection { source } + | hir::CallerOrigin::OwnedFieldCopy { parent: source } => { + ensure(source)? + } + hir::CallerOrigin::OwnedExpression => {} + } + } + for argument in &source.generated_operands { + ensure(argument.original_witness().origin())?; + } + } + hir::CallOwnership::Generated(generated) => { + for argument in &generated.arguments { + ensure(argument.original_witness().origin())?; + } + } + } + } + let local_info = self.locals_at(site); + hir::validate_operand_ownership( + ownership, + &args.iter().map(|value| value.ty).collect::>(), + order, + self.types, + |local| local_info.get(local.index() as usize).copied(), + false, + )?; + hir::validate_invocation_target(self.signatures, expression, self.types, context)?; + match ownership { + hir::CallOwnership::Source(source) => { + let mut original = Vec::new(); + for (index, argument) in source.arguments.iter().enumerate() { + let value = &args[index]; + let acquisition = self.source_acquisition(argument, value.ty); + match argument.staging { + hir::ArgumentStaging::Original => { + if argument.effect == CheckedCallerEffect::RelinquishOwned + || (argument.effect == CheckedCallerEffect::ObserveData + && argument.physical_access + == jett_typecheck::CheckedCalleeAccess::Owned) + { + 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 let Some(binding) = acquisition.binding { + self.acquisitions.operands.push((value, binding)); + } + original.push(ArgumentAcquisition { + parameter_index: index, + source_index: argument.source_index, + source: acquisition, + }); + } + } + hir::ArgumentStaging::Transferred { owner } => { + self.slot_operand(value, owner, false)?; + let (initializer, defined) = self.owning_let(owner, value.ty, site)?; + self.original(initializer, argument, &source.bridge, defined.site)?; + self.claim_owner(owner, acquisition)?; + if !jett_typecheck::ownership::is_implicitly_copyable( + self.types, + argument.actual_type, + ) { + self.acquisitions.operands.push((value, owner)); + } + } + hir::ArgumentStaging::Copied { value: copied } => { + self.slot_operand(value, copied, matches!(value.kind, E::View(_)))?; + let (initializer, defined) = + self.storage_let(copied, value.ty, site, true)?; + if let Err(error) = + self.original(initializer, argument, &source.bridge, defined.site) + { + let E::Clone(inner) = &initializer.kind else { + return Err(error); + }; + if inner.ty != initializer.ty + || !jett_typecheck::ownership::is_implicitly_copyable( + self.types, + argument.actual_type, + ) + { + return Err(error); + } + self.original(inner, argument, &source.bridge, defined.site)?; + } + self.claim_slot(copied)?; + } + hir::ArgumentStaging::Observed { owner } => { + self.slot_operand(value, owner, false)?; + let (initializer, defined) = self.owning_let(owner, value.ty, site)?; + if argument.observation_proof().is_none() { + return Err("call ownership observation lost its checked data/capture proof".into()); + } + if let E::Clone(observed) = &initializer.kind { + if observed.ty != initializer.ty + || !crate::handlers::can_snapshot_view( + self.types, + initializer.ty, + ) + { + return Err("call ownership observation cannot snapshot an erased or unsupported value".into()); + } + self.original(observed, argument, &source.bridge, defined.site)?; + } else { + let restored = restore_converted_observation_snapshot( + initializer, + argument, + self.types, + )?; + self.original(&restored, argument, &source.bridge, defined.site)?; + } + self.claim_slot(owner)?; + } + hir::ArgumentStaging::Relinquished { owner, loan } => { + self.slot_operand(value, loan, true)?; + let (projection, begun) = self.loan(loan, value.ty, site)?; + self.slot_operand(projection, owner, true)?; + if self + .function + .local(loan) + .and_then(|local| local.view_source) + != Some(owner) + { + return Err( + "call ownership loan does not borrow the acquired owner".into(), + ); + } + let (initializer, defined) = + self.owning_let(owner, value.ty, begun.site)?; + // The scoped loan proves physical View access separately. + // Rejoin the complete owning initializer without inventing + // a wrapper around its original typed conversion spine. + self.original(initializer, argument, &source.bridge, defined.site)?; + self.claim_slot(loan)?; + self.claim_owner(owner, acquisition)?; + } + hir::ArgumentStaging::RetainedSnapshot { owner, loan } => { + self.slot_operand(value, loan, true)?; + let (projection, begun) = self.loan(loan, value.ty, site)?; + self.slot_operand(projection, owner, true)?; + if self + .function + .local(loan) + .and_then(|local| local.view_source) + != Some(owner) + { + return Err( + "retained snapshot loan does not borrow its exact owner".into(), + ); + } + let (initializer, defined) = + self.owning_let(owner, value.ty, begun.site)?; + let E::Clone(endpoint) = &initializer.kind else { + return Err( + "retained snapshot requires a full-endpoint Clone initializer" + .into(), + ); + }; + let physical_span = argument + .retained_snapshot_span() + .ok_or("retained snapshot has no checked physical occurrence")?; + // The initial physical View may have a pipeline-step span. + // Its child still rejoins the unchanged exact raw Source span. + let E::View(physical_loan) = &value.kind else { + unreachable!("slot_operand proved View"); + }; + let E::View(physical_owner) = &projection.kind else { + unreachable!("slot_operand proved View"); + }; + if argument.retained_snapshot_type() != Some(initializer.ty) + || endpoint.ty != initializer.ty + || initializer.span != physical_span + || endpoint.span != physical_span + || value.span != physical_span + || physical_loan.span != physical_span + || projection.span != physical_span + || physical_owner.span != physical_span + || !crate::handlers::can_snapshot_view(self.types, initializer.ty) + { + return Err("retained snapshot changes its checked endpoint type or occurrence".into()); + } + self.original(endpoint, argument, &source.bridge, defined.site)?; + self.retained_snapshot_order(source, argument, defined.site)?; + self.claim_slot(owner)?; + self.claim_slot(loan)?; + } + hir::ArgumentStaging::Borrowed { loan } => { + self.slot_operand(value, loan, true)?; + let (projection, begun) = self.loan(loan, value.ty, site)?; + let restored = self + .restore_temporary_projection(projection, argument, begun.site)?; + if let Err(error) = + self.original(&restored, argument, &source.bridge, begun.site) + { + if !self.original_handled_view_initializer( + value, + projection, + argument, + &source.bridge, + begun.site, + )? { + return Err(error); + } + } else if hir::validate_source_handled_operand( + &restored, + argument, + &source.bridge, + self.types, + )? + .is_some() + { + self.handled_view_storage_headers( + value, projection, argument, begun.site, + )?; + } + self.claim_slot(loan)?; + } + } + } + self.acquisitions.calls.push((expression, original)); + for (tail, argument) in source.generated_operands.iter().enumerate() { + let value = &args[source.arguments.len() + tail]; + let (metadata, at) = match value.kind { + E::Local(local) => { + let (initializer, defined) = self.owning_let(local, value.ty, site)?; + self.claim_slot(local)?; + (initializer, defined.site) + } + _ => (value, site), + }; + self.generated_original(metadata, argument, at)?; + hir::validate_compiler_metadata_operand(metadata, source, self.types)?; + } + } + hir::CallOwnership::Generated(generated) => { + for (value, argument) in args.iter().zip(&generated.arguments) { + self.generated_operand(value, argument, site)?; + } + } + } + Ok(()) + } + + fn retained_snapshot_order( + &self, + source: &hir::SourceCallOwnership, + argument: &hir::ArgumentOwnership, + defined: Site, + ) -> Result<(), String> { + for later in &source.arguments { + if later.source_index <= argument.source_index { + continue; + } + let within = |span: Span| { + span.file == later.source_span.file + && span.start < span.end + && later.source_span.start <= span.start + && span.end <= later.source_span.end + }; + for block in &self.function.blocks { + for (statement, value) in block.statements.iter().enumerate() { + if within(value.span) { + self.dominates( + defined, + Site { + block: block.id, + statement, + }, + )?; + } + } + if within(block.terminator.span) { + self.dominates( + defined, + Site { + block: block.id, + statement: block.statements.len(), + }, + )?; + } + } + } + Ok(()) + } + + fn locals_at(&self, site: Site) -> Vec { + let mut locals = self.locals.clone(); + if self.iteration_scope_enabled { + crate::iteration_views::apply( + &self.iteration_scopes, + site.block, + &mut locals, + self.types, + ); + } + locals + } + + fn source_acquisition( + &self, + argument: &hir::ArgumentOwnership, + storage_type: TypeId, + ) -> SourceAcquisition { + let binding = match &argument.origin { + hir::CallerOrigin::Binding(fact) + if matches!(fact.mode, hir::CallerBindingMode::Owned) + && !jett_typecheck::ownership::is_implicitly_copyable(self.types, fact.ty) => + { + Some(fact.local) + } + _ => None, + }; + SourceAcquisition { + binding, + effect: argument.effect, + actual_type: argument.actual_type, + storage_type, + source_span: argument.source_span, + } + } + + fn claim_slot(&mut self, local: LocalId) -> Result<(), String> { + if !self.claimed_slots.insert(local.index()) { + return Err( + "call ownership staging slot belongs to more than one source acquisition".into(), + ); + } + Ok(()) + } + + fn claim_owner(&mut self, owner: LocalId, source: SourceAcquisition) -> Result<(), String> { + self.claim_slot(owner)?; + if source.binding == Some(owner) { + return Err( + "call ownership acquired owner cannot replace its original binding in place".into(), + ); + } + self.acquisitions.owners.insert(owner.index(), source); + Ok(()) + } + + fn slot_operand( + &self, + expression: &Expression, + local: LocalId, + borrowed: bool, + ) -> Result<(), String> { + let value = if borrowed { + let E::View(value) = &expression.kind else { + return Err("call ownership staged loan requires physical View(Local)".into()); + }; + if value.ty != expression.ty { + return Err("call ownership staged view changes its slot type".into()); + } + value.as_ref() + } else { + expression + }; + if !matches!(value.kind, E::Local(id) if id == local) + || self + .function + .local(local) + .is_none_or(|metadata| metadata.ty != value.ty) + { + return Err( + "call ownership staged operand does not identify its exact storage slot".into(), + ); + } + Ok(()) + } + + fn owning_let( + &self, + local: LocalId, + ty: TypeId, + used: Site, + ) -> Result<(&'a Expression, Defined<'a>), String> { + self.storage_let(local, ty, used, false) + } + + fn storage_let( + &self, + local: LocalId, + ty: TypeId, + used: Site, + copy_view: bool, + ) -> Result<(&'a Expression, Defined<'a>), String> { + let metadata = self + .function + .local(local) + .ok_or("call ownership owner slot is absent")?; + if metadata.ty != ty + || metadata.debug_ty != ty + || metadata.view_source.is_some() + || self.function.parameter_for_local(local).is_some() + { + return Err("call ownership acquisition requires an exact new owning slot".into()); + } + let [defined] = self + .definitions + .get(&local.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err("call ownership acquired slot requires one unique initializer".into()); + }; + let Definition::Let(value) = defined.kind else { + return Err("call ownership acquired slot is not an owning Let".into()); + }; + if value.ty != ty + || metadata.span != value.span + || (!copy_view && matches!(value.kind, E::View(_))) + { + return Err( + "call ownership owner initializer changes type, occurrence or ownership".into(), + ); + } + self.dominates(defined.site, used)?; + Ok((value, *defined)) + } + + fn loan( + &self, + loan: LocalId, + ty: TypeId, + used: Site, + ) -> Result<(&'a Expression, Defined<'a>), String> { + if !self.stages.contains_key(&(loan.index() as usize)) { + return Err("call ownership staged borrow has no typed Begin/End authority".into()); + } + let [defined] = self + .definitions + .get(&loan.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err("call ownership loan requires one unique Begin".into()); + }; + let Definition::Begin(value) = defined.kind else { + return Err("call ownership loan initializer is not BeginCallView".into()); + }; + if value.ty != ty { + return Err("call ownership loan initializer changes its endpoint type".into()); + } + self.dominates(defined.site, used)?; + Ok((value, *defined)) + } + + fn original( + &self, + value: &Expression, + argument: &hir::ArgumentOwnership, + bridge: &hir::CallBridge, + site: Site, + ) -> Result<(), String> { + let local_info = self.locals_at(site); + match hir::validate_source_operand(value, argument, bridge, &local_info, self.types) { + Ok(_) => Ok(()), + Err(error) => { + // A CFG result is not a source binding and never gets a fabricated + // Binding witness. Only a closed original producer may use this path. + if argument.origin != hir::CallerOrigin::OwnedExpression { + return Err(error); + } + let original = match hir::validate_source_handled_operand( + value, argument, bridge, self.types, + )? { + Some(backing) => backing, + None => { + let mut original = value; + while let E::View(inner) = &original.kind { + original = inner; + } + let occurrence_type = argument.source_witness().occurrence_type(); + if original.ty != occurrence_type || original.span != argument.source_span { + return Err(error); + } + original + } + }; + let E::Local(local) = original.kind else { + return Err(error); + }; + // The sealed physical occurrence may carry an expected Secret + // layer. Raw actual_type still controls caller acquisition. + self.produced_result(local, original.ty, original.span, site) + } + } + } + + fn produced_result( + &self, + local: LocalId, + ty: TypeId, + span: Span, + used: Site, + ) -> Result<(), String> { + let metadata = self + .function + .local(local) + .ok_or("call ownership produced result is absent")?; + if metadata.ty != ty + || metadata.debug_ty != ty + || metadata.span != span + || metadata.view_source.is_some() + || self.function.parameter_for_local(local).is_some() + { + return Err("call ownership producer result has no exact owned metadata".into()); + } + let definitions = self + .definitions + .get(&local.index()) + .ok_or("call ownership producer result has no definitions")?; + let sum_anchors = definitions + .iter() + .filter_map(|defined| match defined.kind { + Definition::SumTake { + source, + success: true, + } if defined.span == span => Some(source), + _ => None, + }) + .collect::>(); + let prepared = self + .function + .prepared_absent_successes + .iter() + .find(|record| record.plan().output == local); + if let Some(record) = prepared { + if record.plan().span != span || record.plan().output_type != ty { + return Err( + "call ownership prepared absence differs from its exact output occurrence" + .into(), + ); + } + self.prepared_absence(record.plan())?; + } + let default_source = match sum_anchors.as_slice() { + [source] => Some(*source), + [] => prepared.map(|record| record.plan().source), + _ => None, + }; + let present = self + .function + .prepared_present_successes + .iter() + .find(|record| record.plan().output == local); + if let Some(record) = present { + if record.plan().span != span || record.plan().output_type != ty { + return Err( + "call ownership prepared success differs from its exact output occurrence" + .into(), + ); + } + self.prepared_presence(record.plan())?; + } + let mut absent_plan = None; + let mut present_plan = None; + let mut producer = prepared.is_some(); + for defined in definitions { + match defined.kind { + Definition::SumTake { + source, + success: true, + } => { + if defined.span != span { + return Err("call ownership producer result has definitions from another source occurrence".into()); + } + let source_ty = self + .function + .local(source) + .ok_or("call ownership sum source is absent")? + .ty; + let payload = match self.types.resolve(source_ty) { + Type::Optional(inner) | Type::Result(inner, _) => *inner, + _ => { + return Err( + "call ownership produced result is not a typed sum payload".into(), + ); + } + }; + if payload == TypeInterner::NEVER { + absent_plan = Some(self.original_absence( + source, + local, + source_ty, + ty, + span, + defined.site.block, + )?); + } else if payload != ty + && !matches!(self.types.resolve(ty), Type::Secret(inner) if *inner == payload) + { + return Err( + "call ownership produced result differs from its sum payload".into(), + ); + } + self.producer_source(source, source_ty, span, defined.site)?; + self.sum_success_edge(source, defined.site.block)?; + if present.is_none() && self.present_source_payload(source_ty).is_some() { + present_plan = Some(self.original_presence( + source, + local, + source_ty, + ty, + span, + defined.site.block, + )?); + } + producer = true; + } + Definition::Let(value) if value.ty == ty && !matches!(value.kind, E::View(_)) => { + if let Some(source) = default_source { + if !contains(span, defined.span) || !contains(defined.span, value.span) { + return Err("call ownership handled default is outside its exact source occurrence".into()); + } + self.sum_failure_region(source, defined.site.block, span)?; + if self.handled_default_is_borrowed(value, defined.site) { + return Err( + "call ownership handled default cannot acquire a borrowed owner" + .into(), + ); + } + } else if defined.span != span { + return Err("call ownership producer result has definitions from another source occurrence".into()); + } + if let E::RefinementValidated(inner) = &value.kind { + let E::Local(source) = inner.kind else { + return Err( + "call ownership validated result has no owned candidate".into() + ); + }; + self.producer_source(source, inner.ty, span, defined.site)?; + if !matches!(self.types.resolve(ty), Type::Refinement { .. }) { + return Err( + "call ownership validated result has no nominal refinement target" + .into(), + ); + } + producer = true; + } else if !matches!(value.kind, E::Local(_) | E::Field { .. }) { + // Typed default/constructor definitions do not take a + // borrowed owner. The normal value/CFG verifier still + // checks their complete body and initialization edges. + producer = true; + } else if let E::Local(source) = value.kind { + if self.function.is_view_local(source) { + return Err( + "call ownership producer default cannot acquire a borrowed local" + .into(), + ); + } + } + } + _ => { + return Err( + "call ownership producer result contains an unsupported definition".into(), + ); + } + } + } + if !producer { + return Err( + "call ownership result is only a renamed binding, not a proved producer".into(), + ); + } + self.definitions_reach(definitions, used)?; + if let Some(plan) = absent_plan { + let mut plans = self.absent_successes.borrow_mut(); + if let Some(existing) = plans.iter().find(|existing| existing.output == plan.output) { + if existing != &plan { + return Err( + "call ownership absent output has contradictory original proofs".into(), + ); + } + } else { + plans.push(plan); + } + } + if let Some(plan) = present_plan { + let mut plans = self.present_successes.borrow_mut(); + if let Some(existing) = plans.iter().find(|existing| existing.output == plan.output) { + if existing != &plan { + return Err( + "call ownership present output has contradictory original proofs".into(), + ); + } + } else { + plans.push(plan); + } + } + Ok(()) + } + + fn restore_temporary_projection( + &self, + projection: &Expression, + argument: &hir::ArgumentOwnership, + used: Site, + ) -> Result { + let temporary = matches!( + argument.origin, + hir::CallerOrigin::BorrowedProjection { + source: hir::CallerViewSource::Other + } + ) || (argument.origin == hir::CallerOrigin::OwnedExpression + && argument.source_witness().syntax() + == jett_typecheck::CheckedCallerSyntax::WrittenView); + if !temporary { + return Ok(projection.clone()); + } + let root = address_root(projection) + .ok_or("call ownership temporary projection has no materialized root")?; + let ty = self + .function + .local(root) + .ok_or("call ownership temporary projection root is absent")? + .ty; + let (initializer, _) = self.owning_let(root, ty, used)?; + crate::call_views::replace_borrowed_root(projection, initializer.clone()) + .ok_or_else(|| "call ownership temporary projection lost its exact root type".into()) + } + + /// Rejoin only the exact converted original Handle after its owning storage + /// and Begin have been proved. The added physical View is not a source cast. + fn original_handled_view_initializer( + &mut self, + value: &Expression, + projection: &Expression, + argument: &hir::ArgumentOwnership, + bridge: &hir::CallBridge, + used: Site, + ) -> Result { + use jett_typecheck::{CheckedCalleeAccess as Access, CheckedCallerSyntax as Syntax}; + if argument.origin != hir::CallerOrigin::OwnedExpression + || argument.syntax != Syntax::WrittenView + || argument.effect != CheckedCallerEffect::RetainBorrow + || argument.physical_access != Access::View + { + return Ok(false); + } + let E::View(_physical_loan) = &value.kind else { + return Ok(false); + }; + let E::View(physical_owner) = &projection.kind else { + return Ok(false); + }; + let E::Local(owner) = physical_owner.kind else { + return Ok(false); + }; + let (initializer, defined) = self.owning_let(owner, projection.ty, used)?; + if !matches!( + initializer.kind, + E::InterfaceCoerce { .. } | E::FunctionAdapter { .. } + ) { + return Ok(false); + } + let Some(backing) = + hir::validate_source_handled_operand(initializer, argument, bridge, self.types)? + else { + return Ok(false); + }; + if !matches!(backing.kind, E::Local(_)) { + return Ok(false); + } + self.handled_view_storage_headers(value, projection, argument, used)?; + self.original(initializer, argument, bridge, defined.site)?; + // The existing boxing operation reads its concrete payload borrowed and + // returns an independent owned box. Preserve the original written View; + // only this exact authenticated conversion/input pair models that read. + if let E::InterfaceCoerce { value: input, .. } = &initializer.kind + && matches!(self.types.resolve(initializer.ty), Type::Interface(_)) + && !crate::move_values::is_erased_interface(self.types, input.ty) + && matches!(&input.kind, E::View(raw) if std::ptr::eq(raw.as_ref(), backing)) + { + self.acquisitions + .handled_conversions + .push((initializer, input.as_ref())); + } + Ok(true) + } + + fn handled_view_storage_headers( + &self, + value: &Expression, + projection: &Expression, + argument: &hir::ArgumentOwnership, + used: Site, + ) -> Result<(), String> { + use jett_typecheck::{CheckedCalleeAccess as Access, CheckedCallerSyntax as Syntax}; + if argument.origin != hir::CallerOrigin::OwnedExpression + || argument.syntax != Syntax::WrittenView + || argument.effect != CheckedCallerEffect::RetainBorrow + || argument.physical_access != Access::View + { + return Ok(()); + } + let E::View(physical_loan) = &value.kind else { + return Err("call ownership handled view lost its physical loan".into()); + }; + let E::View(physical_owner) = &projection.kind else { + return Err("call ownership handled view lost its physical owner".into()); + }; + let E::Local(owner) = physical_owner.kind else { + return Err("call ownership handled view has no materialized owner".into()); + }; + let (initializer, _) = self.owning_let(owner, projection.ty, used)?; + if value.span != argument.source_span + || physical_loan.span != argument.source_span + || projection.span != argument.source_span + || physical_owner.span != initializer.span + { + return Err("call ownership handled view changes its exact physical occurrence".into()); + } + Ok(()) + } + + fn producer_source( + &self, + source: LocalId, + ty: TypeId, + span: Span, + used: Site, + ) -> Result<(), String> { + let metadata = self + .function + .local(source) + .ok_or("call ownership producer source is absent")?; + if metadata.ty != ty + || metadata.debug_ty != ty + || metadata.span != span + || metadata.view_source.is_some() + || self.function.parameter_for_local(source).is_some() + { + return Err("call ownership producer source has no exact new owned slot".into()); + } + let [defined] = self + .definitions + .get(&source.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err("call ownership producer source requires one owning initializer".into()); + }; + let Definition::Let(value) = defined.kind else { + return Err("call ownership producer source is not an owning Let".into()); + }; + if value.ty != ty + || defined.span != span + || !contains(span, value.span) + || matches!(value.kind, E::View(_)) + { + return Err( + "call ownership producer source changes its type, source occurrence or ownership" + .into(), + ); + } + self.dominates(defined.site, used) + } + + fn sum_success_edge(&self, source: LocalId, success: BlockId) -> Result<(), String> { + let incoming = self + .predecessors + .get(success.index() as usize) + .ok_or("call ownership sum success block is absent")?; + if incoming.is_empty() { + return Err("call ownership sum payload has no selecting branch".into()); + } + for &previous in incoming { + let block = self + .function + .blocks + .get(previous.index() as usize) + .ok_or("call ownership sum selecting block is absent")?; + if let Some(record) = self + .function + .prepared_present_successes + .iter() + .find(|record| { + let plan = record.plan(); + plan.source == source && plan.success == success && plan.selecting == previous + }) + { + self.prepared_presence(record.plan())?; + continue; + } + let T::Branch { + condition, + then_block, + else_block, + } = &block.terminator.kind + else { + return Err("call ownership sum payload is not selected by its tag".into()); + }; + let E::Local(tag) = condition.kind else { + return Err("call ownership sum selector is not a typed tag local".into()); + }; + if *then_block != success + || *else_block == success + || condition.ty != TypeInterner::BOOL + { + return Err( + "call ownership sum success edge does not preserve tag ordering".into(), + ); + } + let [defined] = self + .definitions + .get(&tag.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err("call ownership sum selector requires a unique tag definition".into()); + }; + let statement = self + .function + .blocks + .get(defined.site.block.index() as usize) + .and_then(|block| block.statements.get(defined.site.statement)) + .ok_or("call ownership sum tag definition is absent")?; + if !matches!(statement.kind, S::SumTag { source: owner, target } if owner == source && target == tag) + { + return Err("call ownership sum selector belongs to another owner".into()); + } + self.dominates( + defined.site, + Site { + block: previous, + statement: block.statements.len(), + }, + )?; + } + Ok(()) + } + + fn present_source_payload(&self, ty: TypeId) -> Option { + if !valid_type(self.types, ty) { + return None; + } + match self.types.resolve(ty) { + Type::Result(ok, error) + if *ok != TypeInterner::NEVER + && *ok != TypeInterner::ERROR + && *error == TypeInterner::NEVER + && valid_type(self.types, *ok) => + { + Some(*ok) + } + _ => None, + } + } + + fn original_presence( + &self, + source: LocalId, + output: LocalId, + source_type: TypeId, + output_type: TypeId, + span: Span, + success: BlockId, + ) -> Result { + if self.present_source_payload(source_type).is_none() { + return Err( + "call ownership present success requires an exact Result(T,Never) source".into(), + ); + } + let definitions = self + .definitions + .get(&output.index()) + .ok_or("call ownership present output is undefined")?; + if definitions + .iter() + .filter(|defined| { + matches!(defined.kind, + Definition::SumTake { source: owner, success: true } if owner == source) + && defined.span == span + && defined.site.block == success + }) + .count() + != 1 + { + return Err("call ownership present output requires one exact success anchor".into()); + } + let [selecting] = self + .predecessors + .get(success.index() as usize) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err( + "call ownership present success requires one original selecting branch".into(), + ); + }; + let block = &self.function.blocks[selecting.index() as usize]; + let T::Branch { + condition, + then_block, + else_block, + } = &block.terminator.kind + else { + return Err("call ownership present success has no original branch".into()); + }; + let E::Local(tag) = condition.kind else { + return Err("call ownership present selector is not a tag local".into()); + }; + if *then_block != success + || *else_block == success + || condition.ty != TypeInterner::BOOL + || condition.span != span + || block.terminator.span != span + { + return Err("call ownership present selector differs from its exact Handle".into()); + } + let plan = PresentSuccessPlan { + function: self.function.id, + identity: self.function.identity.clone(), + source, + tag, + output, + source_type, + output_type, + span, + selecting: *selecting, + success, + }; + self.presence_tag(&plan)?; + Ok(plan) + } + + fn presence_tag(&self, plan: &PresentSuccessPlan) -> Result<(), String> { + let payload = self + .present_source_payload(plan.source_type) + .ok_or("call ownership prepared success has no exact Result(T,Never) type")?; + if plan.function != self.function.id + || plan.identity != self.function.identity + || !valid_type(self.types, plan.output_type) + || (plan.output_type != payload + && !matches!(self.types.resolve(plan.output_type), Type::Secret(inner) if *inner == payload)) + { + return Err( + "call ownership prepared success has foreign function or payload type authority" + .into(), + ); + } + let source = self + .function + .local(plan.source) + .ok_or("call ownership present source is outside its function")?; + let output = self + .function + .local(plan.output) + .ok_or("call ownership present output is outside its function")?; + let tag = self + .function + .local(plan.tag) + .ok_or("call ownership present tag is outside its function")?; + if source.ty != plan.source_type + || source.debug_ty != plan.source_type + || output.ty != plan.output_type + || output.debug_ty != plan.output_type + || output.span != plan.span + || output.view_source.is_some() + || self.function.parameter_for_local(plan.output).is_some() + || tag.ty != TypeInterner::BOOL + || tag.debug_ty != TypeInterner::BOOL + || tag.span != plan.span + || tag.view_source.is_some() + || self.function.parameter_for_local(plan.tag).is_some() + { + return Err( + "call ownership present certificate changed its exact local metadata".into(), + ); + } + let block = self + .function + .blocks + .get(plan.selecting.index() as usize) + .ok_or("call ownership present selecting block is outside its function")?; + if self + .function + .blocks + .get(plan.success.index() as usize) + .is_none() + || plan.selecting == plan.success + || block.terminator.span != plan.span + { + return Err("call ownership present success edge is outside its exact Handle".into()); + } + let Some(statement) = block.statements.last() else { + return Err("call ownership present edge lost its runtime tag".into()); + }; + if statement.span != plan.span + || !matches!(statement.kind, + S::SumTag { source, target } if source == plan.source && target == plan.tag) + { + return Err("call ownership present edge lost its exact runtime SumTag".into()); + } + let [definition] = self + .definitions + .get(&plan.tag.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err("call ownership present edge has an ambiguous tag definition".into()); + }; + if definition.site + != (Site { + block: plan.selecting, + statement: block.statements.len() - 1, + }) + { + return Err("call ownership present tag belongs to another selecting block".into()); + } + self.producer_source(plan.source, plan.source_type, plan.span, definition.site) + } + + fn prepared_presence(&self, plan: &PresentSuccessPlan) -> Result<(), String> { + self.presence_tag(plan)?; + if !matches!(self.function.blocks[plan.selecting.index() as usize].terminator.kind, + T::Goto(target) if target == plan.success) + { + return Err( + "call ownership prepared success changed its canonical selected edge".into(), + ); + } + let [selecting] = self + .predecessors + .get(plan.success.index() as usize) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err("call ownership prepared success has a foreign incoming edge".into()); + }; + if *selecting != plan.selecting { + return Err("call ownership prepared success lost its selecting predecessor".into()); + } + let [defined] = self + .definitions + .get(&plan.output.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err( + "call ownership prepared success output requires one retained extraction".into(), + ); + }; + if defined.span != plan.span + || defined.site.block != plan.success + || !matches!(defined.kind, Definition::SumTake { source, success: true } if source == plan.source) + { + return Err("call ownership prepared success lost its exact successful SumTake".into()); + } + self.producer_source(plan.source, plan.source_type, plan.span, defined.site) + } + + fn validate_prepared_presences(&self) -> Result<(), String> { + let mut outputs = BTreeSet::new(); + for record in &self.function.prepared_present_successes { + if !outputs.insert(record.plan().output.index()) { + return Err( + "call ownership prepared success has duplicate output authority".into(), + ); + } + self.prepared_presence(record.plan())?; + } + Ok(()) + } + + fn absent_source_type(&self, ty: TypeId) -> bool { + if !valid_type(self.types, ty) { + return false; + } + match self.types.resolve(ty) { + Type::Optional(inner) => *inner == TypeInterner::NEVER, + Type::Result(ok, error) => { + *ok == TypeInterner::NEVER + && *error != TypeInterner::NEVER + && valid_type(self.types, *error) + } + _ => false, + } + } + + fn original_absence( + &self, + source: LocalId, + output: LocalId, + source_type: TypeId, + output_type: TypeId, + span: Span, + success: BlockId, + ) -> Result { + if !self.absent_source_type(source_type) { + return Err( + "call ownership absent success requires an exact uninhabited sum source".into(), + ); + } + let definitions = self + .definitions + .get(&output.index()) + .ok_or("call ownership absent output is undefined")?; + if definitions + .iter() + .filter(|defined| { + matches!(defined.kind, + Definition::SumTake { source: owner, success: true } if owner == source) + && defined.span == span + }) + .count() + != 1 + { + return Err("call ownership absent output requires one exact success anchor".into()); + } + let [selecting] = self + .predecessors + .get(success.index() as usize) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err("call ownership absent success requires one selecting branch".into()); + }; + let block = &self.function.blocks[selecting.index() as usize]; + let T::Branch { + condition, + then_block, + else_block, + } = &block.terminator.kind + else { + return Err("call ownership absent success has no original branch".into()); + }; + let E::Local(tag) = condition.kind else { + return Err("call ownership absent selector is not a tag local".into()); + }; + if *then_block != success + || *else_block == success + || condition.ty != TypeInterner::BOOL + || condition.span != span + || block.terminator.span != span + { + return Err("call ownership absent selector differs from its exact Handle".into()); + } + let plan = AbsentSuccessPlan { + function: self.function.id, + identity: self.function.identity.clone(), + source, + tag, + output, + source_type, + output_type, + span, + selecting: *selecting, + failure: *else_block, + }; + self.absence_tag(&plan)?; + Ok(plan) + } + + fn absence_tag(&self, plan: &AbsentSuccessPlan) -> Result<(), String> { + if plan.function != self.function.id + || plan.identity != self.function.identity + || !self.absent_source_type(plan.source_type) + || !valid_type(self.types, plan.output_type) + { + return Err( + "call ownership prepared absence has foreign function or type authority".into(), + ); + } + let source = self + .function + .local(plan.source) + .ok_or("call ownership absent source is outside its function")?; + let output = self + .function + .local(plan.output) + .ok_or("call ownership absent output is outside its function")?; + let tag = self + .function + .local(plan.tag) + .ok_or("call ownership absent tag is outside its function")?; + if source.ty != plan.source_type + || source.debug_ty != plan.source_type + || output.ty != plan.output_type + || output.debug_ty != plan.output_type + || output.span != plan.span + || output.view_source.is_some() + || self.function.parameter_for_local(plan.output).is_some() + || tag.ty != TypeInterner::BOOL + || tag.debug_ty != TypeInterner::BOOL + || tag.span != plan.span + { + return Err( + "call ownership absent certificate changed its exact local metadata".into(), + ); + } + let block = self + .function + .blocks + .get(plan.selecting.index() as usize) + .ok_or("call ownership absent selecting block is outside its function")?; + if self + .function + .blocks + .get(plan.failure.index() as usize) + .is_none() + || plan.selecting == plan.failure + || block.terminator.span != plan.span + { + return Err("call ownership absent failure edge is outside its exact Handle".into()); + } + let Some(statement) = block.statements.last() else { + return Err("call ownership absent edge lost its runtime tag".into()); + }; + if statement.span != plan.span + || !matches!(statement.kind, + S::SumTag { source, target } if source == plan.source && target == plan.tag) + { + return Err("call ownership absent edge lost its exact runtime SumTag".into()); + } + let [definition] = self + .definitions + .get(&plan.tag.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err("call ownership absent edge has an ambiguous tag definition".into()); + }; + if definition.site + != (Site { + block: plan.selecting, + statement: block.statements.len() - 1, + }) + { + return Err("call ownership absent tag belongs to another selecting block".into()); + } + self.producer_source(plan.source, plan.source_type, plan.span, definition.site) + } + + fn prepared_absence(&self, plan: &AbsentSuccessPlan) -> Result<(), String> { + self.absence_tag(plan)?; + if !matches!(self.function.blocks[plan.selecting.index() as usize].terminator.kind, + T::Goto(target) if target == plan.failure) + { + return Err( + "call ownership prepared absence changed its canonical selected edge".into(), + ); + } + let definitions = self + .definitions + .get(&plan.output.index()) + .ok_or("call ownership prepared absent output has no defaults")?; + for defined in definitions { + let Definition::Let(value) = defined.kind else { + return Err( + "call ownership prepared absent output has a foreign definition".into(), + ); + }; + if value.ty != plan.output_type + || matches!(value.kind, E::View(_)) + || !contains(plan.span, defined.span) + || !contains(defined.span, value.span) + { + return Err( + "call ownership prepared absent default changes type or occurrence".into(), + ); + } + if let E::Local(local) = value.kind { + if self.function.is_view_local(local) { + return Err( + "call ownership prepared absent default acquires a borrowed owner".into(), + ); + } + } + self.sum_failure_region(plan.source, defined.site.block, plan.span)?; + } + Ok(()) + } + + fn validate_prepared_absences(&self) -> Result<(), String> { + let mut outputs = BTreeSet::new(); + for record in &self.function.prepared_absent_successes { + if !outputs.insert(record.plan().output.index()) { + return Err( + "call ownership prepared absence has duplicate output authority".into(), + ); + } + self.prepared_absence(record.plan())?; + } + Ok(()) + } + + /// A default definition belongs to the failed arm of this exact Handle, + /// not merely to a source span somewhere inside the same function. + fn sum_failure_region( + &self, + source: LocalId, + start: BlockId, + span: Span, + ) -> Result<(), String> { + let mut queue = vec![start]; + let mut seen = BTreeSet::new(); + let mut anchored = false; + while let Some(current) = queue.pop() { + if !seen.insert(current.index()) { + continue; + } + let incoming = self + .predecessors + .get(current.index() as usize) + .ok_or("call ownership handled default block is absent")?; + if current == self.function.entry || incoming.is_empty() { + return Err( + "call ownership handled default has no exact failure-edge anchor".into(), + ); + } + for &previous in incoming { + let block = self + .function + .blocks + .get(previous.index() as usize) + .ok_or("call ownership handled default predecessor is absent")?; + let mut selected = false; + if let Some(record) = + self.function + .prepared_absent_successes + .iter() + .find(|record| { + let plan = record.plan(); + plan.source == source + && plan.span == span + && plan.selecting == previous + && plan.failure == current + }) + { + self.absence_tag(record.plan())?; + if !matches!(block.terminator.kind, T::Goto(target) if target == current) { + return Err( + "call ownership prepared default changed its authenticated false edge" + .into(), + ); + } + selected = true; + anchored = true; + } + if let T::Branch { + condition, + then_block, + else_block, + } = &block.terminator.kind + { + if let E::Local(tag) = condition.kind { + if let [defined] = self + .definitions + .get(&tag.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + { + let statement = self + .function + .blocks + .get(defined.site.block.index() as usize) + .and_then(|block| block.statements.get(defined.site.statement)); + if statement.is_some_and(|statement| matches!(statement.kind, + S::SumTag { source: owner, target } if owner == source && target == tag)) { + if condition.ty != TypeInterner::BOOL + || self.function.local(tag).is_none_or(|local| local.ty != TypeInterner::BOOL) + || block.terminator.span != span || defined.span != span + || *else_block != current || *then_block == current { + return Err("call ownership handled default is not selected by its exact false-tag edge".into()); + } + self.dominates(defined.site, Site { block: previous, statement: block.statements.len() })?; + selected = true; + anchored = true; + } + } + } + } + if !selected { + queue.push(previous); + } + } + } + if !anchored { + return Err( + "call ownership handled default cycle has no exact failure-edge anchor".into(), + ); + } + Ok(()) + } + + fn generated_operand( + &mut self, + value: &Expression, + argument: &hir::GeneratedArgumentOwnership, + used: Site, + ) -> Result<(), String> { + let local_info = self.locals_at(used); + match argument.staging { + hir::GeneratedArgumentStaging::Existing { loan: None } => { + if hir::validate_generated_operand_tree(value, argument, &local_info).is_ok() { + return Ok(()); + } + // A physical owned operand can have been materialized by the + // ordinary ordered-value path. Its unique Let must restore the + // sealed original tree, including primitive Copy acquisitions. + if argument.callee_access != jett_typecheck::CheckedCalleeAccess::Owned { + return Err( + "generated borrowed operand lost its explicit storage association".into(), + ); + } + let E::Local(local) = value.kind else { + return self.generated_original(value, argument, used); + }; + self.slot_operand(value, local, false)?; + let (initializer, defined) = self.owning_let(local, value.ty, used)?; + if let Err(error) = self.generated_original(initializer, argument, defined.site) { + let E::Clone(original) = &initializer.kind else { + return Err(error); + }; + if argument.acquisition != hir::GeneratedAcquisition::Copy + || original.ty != initializer.ty + || !jett_typecheck::ownership::is_implicitly_copyable( + self.types, + argument.actual_type, + ) + { + return Err(error); + } + self.generated_original(original, argument, defined.site)?; + } + self.claim_slot(local) + } + hir::GeneratedArgumentStaging::Existing { loan: Some(loan) } => { + self.slot_operand(value, loan, true)?; + let (projection, begun) = self.loan(loan, value.ty, used)?; + self.generated_original(projection, argument, begun.site)?; + self.claim_slot(loan) + } + hir::GeneratedArgumentStaging::OwnedProducer { owner, loan } + | hir::GeneratedArgumentStaging::OrdinarySnapshot { owner, loan } => { + self.slot_operand(value, loan, true)?; + let (projection, begun) = self.loan(loan, value.ty, used)?; + self.slot_operand(projection, owner, true)?; + if self + .function + .local(loan) + .and_then(|local| local.view_source) + != Some(owner) + { + return Err("generated loan does not borrow its exact acquired owner".into()); + } + let (initializer, defined) = self.owning_let(owner, value.ty, begun.site)?; + match argument.staging { + hir::GeneratedArgumentStaging::OwnedProducer { .. } => { + if !argument.original_witness().owned_producer() + || matches!(initializer.kind, E::View(_)) + { + return Err("generated owner lost its sealed full producer".into()); + } + let restored = crate::call_views::replace_borrowed_root( + projection, + initializer.clone(), + ) + .ok_or("generated owner changed its original endpoint type")?; + self.generated_original(&restored, argument, defined.site)?; + } + hir::GeneratedArgumentStaging::OrdinarySnapshot { .. } => { + if argument + .original_witness() + .ordinary_snapshot_proof() + .map(|proof| proof.actual_type()) + != Some(argument.actual_type) + { + return Err( + "generated snapshot lost its closed ordinary-data proof".into() + ); + } + let E::Clone(observed) = &initializer.kind else { + return Err( + "generated snapshot requires its exact explicit Clone initializer" + .into(), + ); + }; + if observed.ty != initializer.ty || initializer.ty != argument.actual_type { + return Err( + "generated snapshot changed its sealed endpoint type".into() + ); + } + self.generated_original(observed, argument, defined.site)?; + } + _ => unreachable!("matched generated owner stage"), + } + self.claim_slot(owner)?; + self.claim_slot(loan) + } + } + } + + fn generated_original( + &self, + value: &Expression, + argument: &hir::GeneratedArgumentOwnership, + used: Site, + ) -> Result<(), String> { + let restored = self.restore_generated_internal_copy(value, argument); + let value = restored.as_ref().unwrap_or(value); + let local_info = self.locals_at(used); + let error = match hir::validate_generated_operand_tree(value, argument, &local_info) { + Ok(()) => return Ok(()), + Err(error) => error, + }; + let witness = argument.original_witness(); + if witness.origin() != hir::CallerViewSource::Other + || value.ty != witness.actual_type() + || value.span != witness.source_span() + { + return Err(error); + } + let mut original = value; + if !witness.written_view() + && let E::View(inner) = &original.kind + && original.ty == inner.ty + && original.span == inner.span + { + original = inner; + } + // A private original node can be restored through unique owning storage; + // only its sealed Handle variant authorizes the selected CFG result proof. + // Every reconstructed root/target/wrapper/backing remains exact, including + // original explicit Clone versus an introduced Clone. + witness.validate_lowered_producer_shape(original, |value, shape| { + self.generated_storage(value, shape, used) + }) + } + + fn generated_storage( + &self, + value: &Expression, + shape: hir::GeneratedOriginalShape<'_>, + used: Site, + ) -> Result<(), String> { + self.generated_storage_inner(value, shape, used, &mut BTreeSet::new()) + } + + fn generated_storage_inner( + &self, + value: &Expression, + shape: hir::GeneratedOriginalShape<'_>, + used: Site, + aliases: &mut BTreeSet, + ) -> Result<(), String> { + if value.ty != shape.ty() || value.span != shape.span() { + return Err( + "call ownership generated storage changes its sealed original occurrence".into(), + ); + } + let E::Local(local) = value.kind else { + return Err("call ownership generated storage has no exact materialized slot".into()); + }; + if !aliases.insert(local.index()) { + return Err("call ownership generated storage contains a cycle".into()); + } + let result = (|| { + if let Some(handle) = shape.handle() { + return self.generated_handled_result(value, handle, shape, used, aliases); + } + let (initializer, defined) = self.owning_let(local, shape.ty(), used)?; + match shape.validate_reconstructed(initializer, |value, nested| { + self.generated_storage_inner(value, nested, defined.site, aliases) + }) { + Ok(()) => Ok(()), + Err(error) => shape + .validate_existing_local_snapshot(initializer, |value, nested| { + self.generated_storage_inner(value, nested, defined.site, aliases) + }) + .map_err(|_| error), + } + })(); + aliases.remove(&local.index()); + result + } + + fn generated_handled_result( + &self, + value: &Expression, + shape: hir::GeneratedHandleShape<'_>, + original_node: hir::GeneratedOriginalShape<'_>, + used: Site, + aliases: &mut BTreeSet, + ) -> Result<(), String> { + if matches!(shape.kind(), hir::HandleKind::Refinement { .. }) { + return self.generated_refinement_result(value, shape, original_node, used, aliases); + } + if !matches!( + shape.kind(), + hir::HandleKind::Optional | hir::HandleKind::Result + ) || value.ty != shape.result_type() + || value.span != shape.span() + { + return Err( + "call ownership generated Handle has no exact selected sum occurrence".into(), + ); + } + let E::Local(local) = value.kind else { + return Err( + "call ownership generated Handle is not an authenticated CFG result".into(), + ); + }; + let definitions = self + .definitions + .get(&local.index()) + .ok_or("call ownership generated Handle result is undefined")?; + let prepared = self + .function + .prepared_absent_successes + .iter() + .find(|record| record.plan().output == local); + if prepared.is_none() + && let [defined] = definitions.as_slice() + && let Definition::Let(initializer) = defined.kind + && matches!(initializer.kind, E::Local(_)) + { + // Ordered constructor fields can add a unique owning slot around + // an already lowered result. Rejoin only the same exact occurrence; + // no binding, View, Clone or arbitrary producer becomes a Handle. + let (initializer, defined) = self.owning_let(local, shape.result_type(), used)?; + if initializer.span != shape.span() { + return Err( + "call ownership generated Handle storage changes its occurrence".into(), + ); + } + return self.generated_storage_inner(initializer, original_node, defined.site, aliases); + } + self.produced_result(local, shape.result_type(), shape.span(), used)?; + let sources = definitions + .iter() + .filter_map(|defined| match defined.kind { + Definition::SumTake { + source, + success: true, + } if defined.span == shape.span() => Some(source), + _ => None, + }) + .collect::>(); + let source = match (sources.as_slice(), prepared) { + ([source], None) => *source, + ([], Some(record)) => record.plan().source, + _ => return Err("call ownership generated Handle lost its exact source anchor".into()), + }; + let source_type = self + .function + .local(source) + .ok_or("call ownership generated Handle source is absent")? + .ty; + if source_type != shape.source_type() + || !valid_type(self.types, source_type) + || !matches!( + (shape.kind(), self.types.resolve(source_type)), + (hir::HandleKind::Optional, Type::Optional(_)) + | (hir::HandleKind::Result, Type::Result(_, _)) + ) + { + return Err( + "call ownership generated Handle source differs from its sealed sum type".into(), + ); + } + self.producer_source(source, source_type, shape.span(), used)?; + let [defined] = self + .definitions + .get(&source.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err( + "call ownership generated Handle requires one exact source initializer".into(), + ); + }; + let Definition::Let(initializer) = defined.kind else { + return Err( + "call ownership generated Handle source is not its owning initializer".into(), + ); + }; + shape.validate_lowered_source_shape(initializer, |value, nested| { + self.generated_storage_inner(value, nested, defined.site, aliases) + }) + } + + fn generated_refinement_result( + &self, + value: &Expression, + shape: hir::GeneratedHandleShape<'_>, + original_node: hir::GeneratedOriginalShape<'_>, + used: Site, + aliases: &mut BTreeSet, + ) -> Result<(), String> { + let hir::HandleKind::Refinement { + refined_type, + predicates, + } = shape.kind() + else { + return Err("call ownership generated refinement lost its immutable kind".into()); + }; + if value.ty != shape.result_type() + || value.span != shape.span() + || *refined_type != shape.result_type() + || predicates.is_empty() + || predicates + .last() + .is_none_or(|predicate| predicate.refined_type != *refined_type) + || !valid_type(self.types, *refined_type) + || !matches!(self.types.resolve(*refined_type), Type::Refinement { .. }) + { + return Err( + "call ownership generated refinement differs from its exact nominal occurrence" + .into(), + ); + } + let E::Local(output) = value.kind else { + return Err( + "call ownership generated refinement has no authenticated output slot".into(), + ); + }; + let definitions = self + .definitions + .get(&output.index()) + .ok_or("call ownership generated refinement output is undefined")?; + if let [defined] = definitions.as_slice() + && let Definition::Let(initializer) = defined.kind + && matches!(initializer.kind, E::Local(_)) + { + let (initializer, defined) = self.owning_let(output, value.ty, used)?; + if initializer.span != shape.span() { + return Err( + "call ownership generated refinement storage changes its occurrence".into(), + ); + } + return self.generated_storage_inner(initializer, original_node, defined.site, aliases); + } + let metadata = self + .function + .local(output) + .ok_or("call ownership generated refinement output is absent")?; + if metadata.ty != value.ty + || metadata.debug_ty != value.ty + || metadata.span != shape.span() + || metadata.view_source.is_some() + || self.function.parameter_for_local(output).is_some() + { + return Err( + "call ownership generated refinement output has no exact owning metadata".into(), + ); + } + let successes = definitions + .iter() + .filter_map(|defined| match defined.kind { + Definition::Let(Expression { + kind: E::RefinementValidated(inner), + ty, + span, + }) if *ty == value.ty && *span == shape.span() && defined.span == shape.span() => { + Some((*defined, inner.as_ref())) + } + _ => None, + }) + .collect::>(); + let [(success, candidate)] = successes.as_slice() else { + return Err( + "call ownership generated refinement requires one exact validated output".into(), + ); + }; + let E::Local(source) = candidate.kind else { + return Err( + "call ownership generated refinement validation has no exact candidate".into(), + ); + }; + if candidate.ty != shape.source_type() || candidate.span != shape.span() { + return Err( + "call ownership generated refinement candidate differs from its immutable source" + .into(), + ); + } + self.producer_source(source, candidate.ty, shape.span(), success.site)?; + let success_block = &self.function.blocks[success.site.block.index() as usize]; + if success.site.statement != 0 + || success_block.statements.len() != 1 + || success_block.terminator.span != shape.span() + || !matches!(success_block.terminator.kind, T::Goto(_)) + { + return Err( + "call ownership generated refinement changed its canonical success block".into(), + ); + } + let mut next = success.site.block; + let mut rejected = BTreeSet::new(); + let mut selecting = BTreeSet::new(); + let mut failed = None; + for predicate in predicates.iter().rev() { + let (previous, refusal, failure) = + self.refinement_selection(next, source, shape, predicate)?; + if !selecting.insert(previous.index()) + || !rejected.insert(refusal.index()) + || failed.is_some_and(|expected| expected != failure) + { + return Err("call ownership generated refinement changed its ordered checks or shared failure".into()); + } + failed = Some(failure); + next = previous; + } + let failed = failed.ok_or("call ownership generated refinement has no selected failure")?; + let failure_block = self + .function + .blocks + .get(failed.index() as usize) + .ok_or("call ownership generated refinement failure block is absent")?; + let incoming = self + .predecessors + .get(failed.index() as usize) + .ok_or("call ownership generated refinement failure predecessors are absent")?; + if incoming + .iter() + .map(|block| block.index()) + .collect::>() + != rejected + || incoming.len() != rejected.len() + || failed == success.site.block + || failure_block + .statements + .iter() + .any(|statement| !contains(shape.failure_span(), statement.span)) + || (!contains(shape.failure_span(), failure_block.terminator.span) + && failure_block.terminator.span != shape.span()) + { + return Err( + "call ownership generated refinement failure has a foreign edge or occurrence" + .into(), + ); + } + for defined in definitions { + if defined.site == success.site { + continue; + } + let Definition::Let(default) = defined.kind else { + return Err( + "call ownership generated refinement output has a foreign definition".into(), + ); + }; + if default.ty != value.ty + || !contains(shape.failure_span(), defined.span) + || !contains(defined.span, default.span) + || self.handled_default_is_borrowed(default, defined.site) + { + return Err("call ownership generated refinement default lost its exact owned failure occurrence".into()); + } + self.refinement_failure_region(failed, defined.site.block)?; + } + self.definitions_reach(definitions, used)?; + let [defined] = self + .definitions + .get(&source.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err( + "call ownership generated refinement requires one owning source initializer".into(), + ); + }; + let Definition::Let(initializer) = defined.kind else { + return Err( + "call ownership generated refinement source is not its owning initializer".into(), + ); + }; + shape.validate_lowered_source_shape(initializer, |value, nested| { + self.generated_storage_inner(value, nested, defined.site, aliases) + }) + } + + fn refinement_selection( + &self, + success: BlockId, + source: LocalId, + shape: hir::GeneratedHandleShape<'_>, + predicate: &hir::RefinementPredicate, + ) -> Result<(BlockId, BlockId, BlockId), String> { + let [previous] = self + .predecessors + .get(success.index() as usize) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err( + "call ownership generated refinement success has a bypass or foreign predecessor" + .into(), + ); + }; + let block = &self.function.blocks[previous.index() as usize]; + let T::Branch { + condition, + then_block, + else_block, + } = &block.terminator.kind + else { + return Err( + "call ownership generated refinement requires its original predicate Branch".into(), + ); + }; + if *then_block != success + || *else_block == success + || block.terminator.span != shape.span() + || condition.ty != TypeInterner::BOOL + || condition.span != shape.span() + { + return Err("call ownership generated refinement changed its selecting edge".into()); + } + let E::Local(passed) = condition.kind else { + return Err( + "call ownership generated refinement requires its exact passed local".into(), + ); + }; + let (test, defined) = self.owning_let( + passed, + TypeInterner::BOOL, + Site { + block: *previous, + statement: block.statements.len(), + }, + )?; + if defined.site.block != *previous + || defined.site.statement + 1 != block.statements.len() + || defined.site.statement == 0 + { + return Err( + "call ownership generated refinement passed test is not after its exact check" + .into(), + ); + } + let S::CheckRefinement { + local: error, + call, + type_name, + } = &block.statements[defined.site.statement - 1].kind + else { + return Err( + "call ownership generated refinement lost its exact predicate check".into(), + ); + }; + let checked_site = Site { + block: *previous, + statement: defined.site.statement - 1, + }; + let checked = &block.statements[checked_site.statement]; + let [error_definition] = self + .definitions + .get(&error.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err( + "call ownership generated refinement error has a foreign definition".into(), + ); + }; + let error_metadata = self + .function + .local(*error) + .ok_or("call ownership generated refinement error is absent")?; + if error_definition.site != checked_site + || error_definition.span != shape.span() + || error_metadata.ty != TypeInterner::STRING + || error_metadata.debug_ty != TypeInterner::STRING + || error_metadata.span != shape.span() + || error_metadata.view_source.is_some() + || self.function.parameter_for_local(*error).is_some() + || checked.span != shape.span() + || type_name != &predicate.type_name + || call.ty != TypeInterner::BOOL + || call.span != shape.span() + { + return Err( + "call ownership generated refinement changed its exact predicate occurrence".into(), + ); + } + let E::Call { + function, + args, + evaluation_order, + ownership: hir::CallOwnership::Generated(packet), + } = &call.kind + else { + return Err( + "call ownership generated refinement check has no exact generated target".into(), + ); + }; + if *function != predicate.function + || args.len() != 1 + || evaluation_order != &[0] + || packet.operation + != (hir::GeneratedOperation::RefinementPredicate { + function: predicate.function, + }) + { + return Err( + "call ownership generated refinement check differs from its immutable predicate" + .into(), + ); + } + let span = shape.span(); + let mut input = Expression { + kind: E::Clone(Box::new(Expression { + kind: E::Local(source), + ty: shape.source_type(), + span, + })), + ty: shape.source_type(), + span, + }; + if input.ty != predicate.base_type && input.ty != predicate.input_type { + input = Expression { + kind: E::Coarsen(Box::new(input)), + ty: predicate.base_type, + span, + }; + } + if input.ty != predicate.input_type { + input = Expression { + kind: E::Declassify(Box::new(input)), + ty: predicate.input_type, + span, + }; + } + if args[0] != input + || test.ty != TypeInterner::BOOL + || test.span != span + || defined.span != span + { + return Err( + "call ownership generated refinement changed its exact candidate input".into(), + ); + } + let expected = Expression { + kind: E::Binary { + left: Box::new(Expression { + kind: E::View(Box::new(Expression { + kind: E::Local(*error), + ty: TypeInterner::STRING, + span, + })), + ty: TypeInterner::STRING, + span, + }), + op: hir::BinaryOp::Equal, + right: Box::new(Expression { + kind: E::String(String::new()), + ty: TypeInterner::STRING, + span, + }), + }, + ty: TypeInterner::BOOL, + span, + }; + if *test != expected { + return Err( + "call ownership generated refinement changed its exact empty-error test".into(), + ); + } + self.producer_source(source, shape.source_type(), span, checked_site)?; + let refusal = &self.function.blocks[else_block.index() as usize]; + let T::Goto(failed) = refusal.terminator.kind else { + return Err( + "call ownership generated refinement rejection does not enter its failure body" + .into(), + ); + }; + if refusal.terminator.span != span + || self.predecessors[else_block.index() as usize].as_slice() != [*previous] + { + return Err("call ownership generated refinement rejection has a foreign edge".into()); + } + if let Some(local) = shape.error_local() { + let metadata = self + .function + .local(local) + .ok_or("call ownership generated refinement error binding is absent")?; + if metadata.ty != TypeInterner::STRING + || metadata.debug_ty != TypeInterner::STRING + || metadata.view_source.is_some() + || self.function.parameter_for_local(local).is_some() + { + return Err( + "call ownership generated refinement error binding has invalid owning metadata" + .into(), + ); + } + } + match (shape.error_local(), refusal.statements.as_slice()) { + (None, []) => {} + (Some(local), [statement]) + if statement.span == span + && matches!(&statement.kind, S::Let { local: target, value } if *target == local + && value.ty == TypeInterner::STRING && value.span == span + && matches!(value.kind, E::Local(source) if source == *error)) => {} + _ => { + return Err( + "call ownership generated refinement lost its exact error binding".into(), + ); + } + } + Ok((*previous, *else_block, failed)) + } + + fn handled_default_is_borrowed(&self, value: &Expression, site: Site) -> bool { + let endpoint = value.ty; + let mut backing = value; + loop { + backing = match &backing.kind { + E::Coarsen(inner) | E::Declassify(inner) => inner, + E::View(_) | E::RefinementValidated(_) => return true, + E::Local(local) => { + if jett_typecheck::ownership::is_implicitly_copyable(self.types, endpoint) { + return false; + } + return self.locals_at(site).get(local.index() as usize).is_none_or( + |metadata| { + metadata.view_source.is_some() + || metadata.is_view_parameter + || metadata.view_iteration.is_some() + }, + ); + } + _ => return false, + }; + } + } + + fn refinement_failure_region(&self, failure: BlockId, start: BlockId) -> Result<(), String> { + let mut queue = vec![start]; + let mut seen = BTreeSet::new(); + let mut anchored = false; + while let Some(current) = queue.pop() { + if current == failure { + anchored = true; + continue; + } + if !seen.insert(current.index()) { + continue; + } + let incoming = self + .predecessors + .get(current.index() as usize) + .ok_or("call ownership generated refinement default block is absent")?; + if current == self.function.entry || incoming.is_empty() { + return Err( + "call ownership generated refinement default has a failure-region bypass" + .into(), + ); + } + queue.extend(incoming.iter().copied()); + } + if !anchored { + return Err( + "call ownership generated refinement default has no exact failure anchor".into(), + ); + } + Ok(()) + } + + fn restore_generated_internal_copy( + &self, + value: &Expression, + argument: &hir::GeneratedArgumentOwnership, + ) -> Option { + let hir::CallerViewSource::Local(source) = argument.original_witness().origin() else { + return None; + }; + argument.original_witness().ordinary_snapshot_proof()?; + // Existing Coarsen/Declassify lowering can snapshot a source Local. + // Restore only that exact transparent path for metadata validation; + // the emitted Clone and qualified runtime tree remain unchanged. + fn restore(value: &mut Expression, source: LocalId) -> bool { + let replacement = match &value.kind { + E::Clone(inner) + if value.ty == inner.ty + && value.span == inner.span + && matches!(inner.kind, E::Local(local) if local == source) => + { + Some(inner.as_ref().clone()) + } + _ => None, + }; + if let Some(replacement) = replacement { + *value = replacement; + return true; + } + match &mut value.kind { + E::View(inner) | E::Coarsen(inner) | E::Declassify(inner) => restore(inner, source), + _ => false, + } + } + let mut original = value.clone(); + if !restore(&mut original, source) { + return None; + } + let root = self.function.local(source)?; + let proof = Expression { + kind: E::View(Box::new(original.clone())), + ty: original.ty, + span: original.span, + }; + hir::validate_local_view_initializer(&proof, source, root.ty, original.ty, self.types) + .ok()?; + Some(original) + } + fn dominates(&self, definition: Site, used: Site) -> Result<(), String> { + self.definitions_reach( + &[Defined { + site: definition, + span: self.function.span, + kind: Definition::Other, + }], + used, + ) + } + + /// Check every backward path, retaining disconnected-block obligations. + /// A cycle with no defining predecessor cannot manufacture initialization. + fn definitions_reach(&self, definitions: &[Defined<'_>], used: Site) -> Result<(), String> { + let mut queue = vec![used]; + let mut seen = BTreeSet::new(); + let mut reached = false; + while let Some(site) = queue.pop() { + if definitions.iter().any(|definition| { + definition.site.block == site.block && definition.site.statement < site.statement + }) { + reached = true; + continue; + } + if !seen.insert(site.block.index()) { + continue; + } + let incoming = self + .predecessors + .get(site.block.index() as usize) + .ok_or("call ownership use block is absent")?; + if site.block == self.function.entry || incoming.is_empty() { + return Err( + "call ownership storage use is not preceded by its initializer on every path" + .into(), + ); + } + for &previous in incoming { + let block = self + .function + .blocks + .get(previous.index() as usize) + .ok_or("call ownership predecessor is absent")?; + queue.push(Site { + block: previous, + statement: block.statements.len(), + }); + } + } + if !reached { + return Err("call ownership storage cycle has no initialized producer".into()); + } + Ok(()) + } + + fn statement( + &mut self, + statement: &'a S, + site: Site, + context: CheckedOwnershipContext, + ) -> Result<(), String> { + match statement { + S::Let { value, .. } + | S::BeginCallView { value, .. } + | S::Evaluate(value) + | S::HandleDefault(value) => self.expression(value, site, context)?, + S::CheckRefinement { call, .. } => self.expression(call, site, context)?, + S::Assign { target, value } => { + self.expression(target, site, context)?; + self.expression(value, site, context)?; + } + S::Assert { condition, message } => { + self.expression(condition, site, context)?; + if let Some(message) = message { + self.expression(message, site, context)?; + } + } + S::Breakpoint { condition, .. } => { + if let Some(condition) = condition { + self.expression( + condition, + site, + CheckedOwnershipContext::BreakpointExpression, + )?; + } + } + _ => {} + } + Ok(()) + } + + fn terminator( + &mut self, + terminator: &'a T, + site: Site, + context: CheckedOwnershipContext, + ) -> Result<(), String> { + match terminator { + T::Return(Some(value)) | T::Respond(value) => self.expression(value, site, context)?, + T::Branch { condition, .. } => self.expression(condition, site, context)?, + T::Switch { scrutinee, .. } => self.expression(scrutinee, site, context)?, + T::ForEach { iterable, .. } => self.expression(iterable, site, context)?, + T::ReflectedTypeDispatch { type_info, .. } => { + self.expression(type_info, site, context)? + } + _ => {} + } + Ok(()) + } + + fn expression( + &mut self, + expression: &'a Expression, + site: Site, + context: CheckedOwnershipContext, + ) -> Result<(), String> { + self.invocation(expression, site, context)?; + match &expression.kind { + E::Call { args, .. } | E::Intrinsic { args, .. } | E::ActorSpawn { args, .. } => { + for value in args { + self.expression(value, site, context)?; + } + } + E::IndirectCall { callee, args, .. } => { + self.expression(callee, site, context)?; + for value in args { + self.expression(value, site, context)?; + } + } + E::Binary { left, right, .. } => { + self.expression(left, site, context)?; + self.expression(right, site, context)?; + } + E::Unary { value, .. } + | E::View(value) + | E::Clone(value) + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::Coarsen(value) + | E::Declassify(value) + | E::RefinementValidated(value) + | E::InterfaceType(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::RuntimeFailureMessage(value) + | E::StateIs { value, .. } + | E::Comptime { value, .. } + | E::FunctionAdapter { value, .. } + | E::InterfaceCoerce { value, .. } => self.expression(value, site, context)?, + E::Field { base, .. } => self.expression(base, site, context)?, + E::StructConstruct { fields, .. } | E::BitfieldConstruct { fields, .. } => { + for value in fields { + self.expression(value, site, context)?; + } + } + E::EnumConstruct { payloads, .. } | E::MachineConstruct { payloads, .. } => { + for value in payloads { + self.expression(value, site, context)?; + } + } + E::MachineTransition { + source, payloads, .. + } => { + self.expression(source, site, context)?; + for value in payloads { + self.expression(value, site, context)?; + } + } + E::ListConstruct { elements } => { + for value in elements { + self.expression(value, site, context)?; + } + } + E::MapConstruct { entries } => { + for value in entries { + self.expression(&value.key, site, context)?; + self.expression(&value.value, site, context)?; + } + } + E::StringInterpolation(parts) => { + for part in parts { + if let hir::StringSegment::Value(value) = part { + self.expression(value, site, context)?; + } + } + } + E::Handle { + target, failure, .. + } => { + self.expression(target, site, context)?; + self.hir_block(failure, site, context)?; + } + E::InlineFunction { body, .. } => { + let saved = std::mem::replace(&mut self.iteration_scope_enabled, false); + let result = self.hir_block(body, site, CheckedOwnershipContext::Ordinary); + self.iteration_scope_enabled = saved; + result?; + } + E::ActorMessage { actor, args, .. } => { + self.expression(actor, site, context)?; + for value in args { + self.expression(value, site, context)?; + } + } + E::Int(_) + | E::Float(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::Local(_) + | E::Constant { .. } + | E::FunctionRef(_) + | E::ClosureRef { .. } + | E::OptionalNone + | E::RuntimeFailure(_) + | E::PropertyCaseContext(_) => {} + } + Ok(()) + } + + fn hir_block( + &mut self, + block: &'a hir::Block, + site: Site, + context: CheckedOwnershipContext, + ) -> Result<(), String> { + use hir::StatementKind as H; + for statement in &block.statements { + match &statement.kind { + H::Let { value, .. } + | H::Expression(value) + | H::HandleDefault(value) + | H::Respond(value) => self.expression(value, site, context)?, + H::Assign { target, value } => { + self.expression(target, site, context)?; + self.expression(value, site, context)?; + } + H::Return(Some(value)) => self.expression(value, site, context)?, + H::If { + condition, + then_block, + else_block, + } => { + self.expression(condition, site, context)?; + self.hir_block(then_block, site, context)?; + if let Some(block) = else_block { + self.hir_block(block, site, context)?; + } + } + H::While { condition, body } => { + self.expression(condition, site, context)?; + self.hir_block(body, site, context)?; + } + H::For { iterable, body, .. } => { + self.expression(iterable, site, context)?; + self.hir_block(body, site, context)?; + } + H::Match { scrutinee, arms } => { + self.expression(scrutinee, site, context)?; + for arm in arms { + self.hir_block(&arm.body, site, context)?; + } + } + H::Assert { condition, message } => { + self.expression(condition, site, context)?; + if let Some(value) = message { + self.expression(value, site, context)?; + } + } + H::Breakpoint { condition, .. } => { + if let Some(value) = condition { + self.expression( + value, + site, + CheckedOwnershipContext::BreakpointExpression, + )?; + } + } + H::Scope(block) => self.hir_block(block, site, context)?, + H::ReflectedTypeDispatch { type_info, arms } => { + self.expression(type_info, site, context)?; + for arm in arms { + self.hir_block(&arm.body, site, context)?; + } + } + H::Return(None) | H::Break | H::Continue | H::Trace(_) => {} + } + } + Ok(()) + } +} + +fn consumes(effect: CheckedCallerEffect) -> bool { + matches!( + effect, + CheckedCallerEffect::TransferOwned | CheckedCallerEffect::RelinquishOwned + ) +} + +fn valid_type(types: &TypeInterner, ty: TypeId) -> bool { + (ty.index() as usize) < types.len() && ty != TypeInterner::ERROR +} + +fn contains(outer: Span, inner: Span) -> bool { + outer.file == inner.file && outer.start <= inner.start && inner.end <= outer.end +} + +fn address_root(mut expression: &Expression) -> Option { + loop { + expression = match &expression.kind { + E::Local(local) => return Some(*local), + E::Field { base, .. } => base, + E::View(value) + | E::Coarsen(value) + | E::Declassify(value) + | E::RefinementValidated(value) => value, + E::InterfaceCoerce { value, adapters } if adapters.is_empty() => value, + _ => return None, + }; + } +} + +fn successors(terminator: &T) -> Vec { + match terminator { + T::Goto(target) => vec![*target], + T::Branch { + then_block, + else_block, + .. + } => vec![*then_block, *else_block], + T::Switch { + variants, + otherwise, + .. + } => variants + .iter() + .map(|(_, target, _)| *target) + .chain(otherwise.iter().copied()) + .collect(), + T::ForEach { body, exit, .. } => vec![*body, *exit], + T::ReflectedTypeDispatch { + arms, otherwise, .. + } => arms + .iter() + .map(|arm| arm.target) + .chain(std::iter::once(*otherwise)) + .collect(), + T::Return(_) | T::Respond(_) | T::Unreachable => Vec::new(), + } +} diff --git a/crates/jett_mir/src/call_ownership/tests.rs b/crates/jett_mir/src/call_ownership/tests.rs new file mode 100644 index 00000000..e8ac9618 --- /dev/null +++ b/crates/jett_mir/src/call_ownership/tests.rs @@ -0,0 +1,5811 @@ +use super::*; +use std::collections::HashMap; + +const SIGNATURE_REUSE_SOURCE: &str = r#"namespace app +function accept(value: int64) returns int64: + return 7 +function first() returns int64: + return accept(1) +function second() returns int64: + return accept(2) +"#; + +#[test] +fn whole_ownership_pass_rejoins_fresh_signatures_and_preserves_error_order() { + let (mut program, types) = source_program(SIGNATURE_REUSE_SOURCE); + crate::validate_call_ownership(&program, &types).expect("original complete ownership pass"); + for function in &program.functions { + validate_function(&program, function, &types).expect("public per-function ownership pass"); + } + + // Keep structural parameter/local agreement while invalidating the exact + // callee signature retained by both already-checked source call packets. + let callee = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "accept") + .expect("declared accept"); + let parameter = &mut callee.params[0]; + parameter.ty = TypeInterner::INT32; + let local = &mut callee.locals[parameter.local.index() as usize]; + local.ty = TypeInterner::INT32; + local.debug_ty = TypeInterner::INT32; + crate::validate(&program).expect("signature mutation remains structurally well formed"); + + let expected = program + .functions + .iter() + .filter_map(|function| { + validate_function(&program, function, &types) + .err() + .map(|message| crate::ValidationError { + span: function.span, + message, + }) + }) + .collect::>(); + assert_eq!( + expected.len(), + 2, + "both unused caller functions retain exact signature obligations" + ); + assert!( + expected.iter().all(|error| error.message + == "source function ownership signature differs from the exact HIR declaration"), + "{expected:?}" + ); + let errors = crate::validate_call_ownership(&program, &types) + .expect_err("a new whole pass cannot reuse the preceding callee signature"); + assert_eq!( + errors, expected, + "complete pass retains per-function diagnostics and declaration order" + ); +} + +#[test] +fn whole_ownership_pass_checks_a_late_disconnected_invocation() { + let (mut program, types) = source_program(SIGNATURE_REUSE_SOURCE); + crate::validate_call_ownership(&program, &types).expect("original complete ownership pass"); + let function = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "second") + .expect("last caller"); + let mut value = read_call(function).clone(); + value.ty = TypeInterner::BOOL; + let id = BlockId(function.blocks.len() as u32); + function.blocks.push(crate::BasicBlock { + id, + statements: vec![crate::Statement { + span: value.span, + kind: S::Evaluate(value), + }], + terminator: crate::Terminator { + span: function.span, + kind: T::Unreachable, + }, + }); + let span = function.span; + crate::validate(&program) + .expect("disconnected expression metadata is structurally well formed"); + let errors = crate::validate_call_ownership(&program, &types) + .expect_err("table reuse cannot skip the unused caller's disconnected block"); + assert_eq!( + errors, + vec![crate::ValidationError { + span, + message: "call ownership result differs from its checked source certificate".into(), + }] + ); +} + +const SOURCE: &str = r#"namespace app +function read(view values: list[int64], extra: int64) returns int64: + return extra +function exercise(values: list[int64], other: list[int64], incoming: optional[int64]) returns int64: + return read(values, incoming handle: + default 3 + ) +"#; + +fn source_program(source: &str) -> (Program, TypeInterner) { + let file = jett_common::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(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let high = hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, jett_common::SourceOrigin::Project)]), + ) + .expect("source HIR"); + ( + crate::lower(&high, &checked.interner).expect("source MIR"), + checked.interner, + ) +} + +fn exercise(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| function.identity.declaration.name == "exercise") + .expect("exercise") +} + +fn exercise_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "exercise") + .expect("exercise") +} + +fn read_call(function: &Function) -> &Expression { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + S::Let { value, .. } | S::Evaluate(value) + if matches!(value.kind, E::Call { .. } | E::IndirectCall { .. }) => + { + Some(value) + } + _ => None, + }) + .or_else(|| { + function + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + T::Return(Some(value)) + if matches!(value.kind, E::Call { .. } | E::IndirectCall { .. }) => + { + Some(value) + } + _ => None, + }) + }) + .expect("read invocation") +} + +fn read_call_mut(function: &mut Function) -> &mut Expression { + // The handled source fixture materializes its actual call as an owning Let. + function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + S::Let { value, .. } | S::Evaluate(value) + if matches!(value.kind, E::Call { .. } | E::IndirectCall { .. }) => + { + Some(value) + } + _ => None, + }) + .expect("materialized read invocation") +} + +fn owning_stage(function: &Function) -> (LocalId, LocalId) { + let E::Call { + ownership: hir::CallOwnership::Source(source), + .. + } = &read_call(function).kind + else { + panic!("source read"); + }; + let hir::ArgumentStaging::Relinquished { owner, loan } = source.arguments[0].staging else { + panic!("relinquished stage"); + }; + (owner, loan) +} + +fn rejects(program: &Program, types: &TypeInterner) { + let error = validate_function(program, exercise(program), types) + .expect_err("malformed acquisition must fail"); + assert!( + error.contains("call ownership") || error.contains("call view"), + "{error}" + ); +} + +#[test] +fn caller_acquisition_uses_original_owner_and_exact_handled_copy_result() { + for retained in [false, true] { + let source = if retained { + SOURCE.replace("return read(values,", "return read(view values,") + } else { + SOURCE.to_owned() + }; + let (program, types) = source_program(&source); + let function = exercise(&program); + let acquisitions = + validate_function(&program, function, &types).expect("valid call acquisition"); + let call = read_call(function); + let arguments = acquisitions + .arguments(call) + .expect("validated call occurrence"); + assert!( + arguments.is_empty(), + "staged owner must be consumed at its initializer" + ); + if retained { + assert_eq!(acquisitions.owner_initializers().count(), 0); + } else { + let (owner, loan) = owning_stage(function); + let acquired = acquisitions + .owner_initializer(owner) + .expect("owning initializer"); + assert_eq!(acquired.binding, Some(function.params[0].local)); + assert_ne!(acquired.binding, Some(owner)); + assert_ne!(acquired.binding, Some(loan)); + assert_eq!(acquired.effect, CheckedCallerEffect::RelinquishOwned); + assert_eq!(acquired.actual_type, function.params[0].ty); + } + } +} + +#[test] +fn caller_acquisition_original_owned_input_keeps_formal_and_source_indices() { + let source = r#"namespace app +function take(values: list[int64], extra: int64) returns int64: + return extra +function exercise(values: list[int64]) returns int64: + return take(extra: 3, values: values) +"#; + let (program, types) = source_program(source); + let function = exercise(&program); + let call = read_call(function); + let acquisitions = validate_function(&program, function, &types).expect("original owned call"); + let [argument] = acquisitions.arguments(call).expect("typed acquisition") else { + panic!("one owner"); + }; + assert_eq!(argument.parameter_index, 0); + assert_eq!(argument.source_index, 1); + assert_eq!(argument.source.binding, Some(function.params[0].local)); + assert_eq!(argument.source.effect, CheckedCallerEffect::TransferOwned); + let E::Call { args, .. } = &call.kind else { + panic!("owned call"); + }; + assert_eq!( + acquisitions.argument_binding(&args[0]), + Some(function.params[0].local) + ); + assert_eq!(acquisitions.argument_binding(&args[1]), None); + assert_eq!( + acquisitions.argument_binding(&args[0].clone()), + None, + "a clone is not a validated occurrence" + ); + assert_eq!(acquisitions.owner_initializers().count(), 0); +} + +#[test] +fn caller_acquisition_rejects_original_laundering_and_wrong_current_slot() { + let (program, types) = source_program(SOURCE); + let mut changed = program.clone(); + let E::Call { + ownership: hir::CallOwnership::Source(source), + .. + } = &mut read_call_mut(exercise_mut(&mut changed)).kind + else { + panic!("call"); + }; + source.arguments[0].staging = hir::ArgumentStaging::Original; + rejects(&changed, &types); + + let mut changed = program.clone(); + let other = exercise(&changed).params[1].local; + let E::Call { args, .. } = &mut read_call_mut(exercise_mut(&mut changed)).kind else { + panic!("call"); + }; + let E::View(value) = &mut args[0].kind else { + panic!("physical view"); + }; + value.kind = E::Local(other); + rejects(&changed, &types); +} + +#[test] +fn caller_acquisition_rejects_same_type_owner_substitution_and_duplicate_initializer() { + let (program, types) = source_program(SOURCE); + let (owner, _) = owning_stage(exercise(&program)); + let other = exercise(&program).params[1].local; + let mut changed = program.clone(); + let initializer = exercise_mut(&mut changed) + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + S::Let { local, value } if *local == owner => Some(value), + _ => None, + }) + .expect("owner initializer"); + initializer.kind = E::Local(other); + rejects(&changed, &types); + + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + let (block, statement) = function + .blocks + .iter() + .enumerate() + .find_map(|(block, value)| { + value + .statements + .iter() + .position( + |statement| matches!(statement.kind, S::Let { local, .. } if local == owner), + ) + .map(|statement| (block, statement)) + }) + .expect("owner statement"); + let duplicate = function.blocks[block].statements[statement].clone(); + function.blocks[block] + .statements + .insert(statement, duplicate); + rejects(&changed, &types); +} + +#[test] +fn caller_acquisition_rejects_wrong_sum_payload_selection_and_missing_end() { + let (program, types) = source_program(SOURCE); + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + let output = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + S::SumTake { success, .. } if *success => Some(success), + _ => None, + }) + .expect("handled success"); + *output = false; + rejects(&changed, &types); + + let mut changed = program.clone(); + for block in &mut exercise_mut(&mut changed).blocks { + block + .statements + .retain(|statement| !matches!(statement.kind, S::EndCallView { .. })); + } + rejects(&changed, &types); +} + +#[test] +fn caller_acquisition_does_not_skip_disconnected_corrupt_call_packets() { + let (mut program, types) = source_program(SOURCE); + let function = exercise_mut(&mut program); + let mut value = read_call(function).clone(); + let E::Call { + ownership: hir::CallOwnership::Source(source), + .. + } = &mut value.kind + else { + panic!("call"); + }; + source.arguments[0].source_index = source.arguments[1].source_index; + let block = BlockId(function.blocks.len() as u32); + function.blocks.push(crate::BasicBlock { + id: block, + statements: vec![crate::Statement { + span: value.span, + kind: S::Evaluate(value), + }], + terminator: crate::Terminator { + span: function.span, + kind: T::Unreachable, + }, + }); + rejects(&program, &types); +} + +#[test] +fn caller_acquisition_defaults_require_contained_spans_and_the_exact_failed_arm() { + let (program, types) = source_program(SOURCE); + validate_function(&program, exercise(&program), &types) + .expect("valid contained handled default"); + for malformed in [ + "statement span", + "value span", + "success arm", + "missing anchor", + "disconnected default", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + let output = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match statement.kind { + S::SumTake { + target, + success: true, + .. + } => Some(target), + _ => None, + }) + .expect("handled success output"); + let (block, index) = function.blocks.iter().enumerate().find_map(|(block, value)| + value.statements.iter().position(|statement| matches!(statement.kind, S::Let { local, .. } if local == output)) + .map(|index| (block, index))).expect("contained default definition"); + match malformed { + "statement span" => { + function.blocks[block].statements[index].span.file = + jett_common::FileId::new(u32::MAX); + } + "value span" => { + let S::Let { value, .. } = &mut function.blocks[block].statements[index].kind + else { + panic!("default Let"); + }; + value.span.file = jett_common::FileId::new(u32::MAX); + } + "success arm" => { + let default = function.blocks[block].statements.remove(index); + let success = function.blocks.iter_mut().find(|block| block.statements.iter().any(|statement| + matches!(statement.kind, S::SumTake { target, success: true, .. } if target == output))).expect("success arm"); + success.statements.push(default); + } + "missing anchor" => { + for block in &mut function.blocks { + block.statements.retain(|statement| { + !matches!(statement.kind, + S::SumTake { target, success: true, .. } if target == output) + }); + } + } + "disconnected default" => { + let default = function.blocks[block].statements[index].clone(); + function.blocks.push(crate::BasicBlock { + id: BlockId(function.blocks.len() as u32), + statements: vec![default], + terminator: crate::Terminator { + span: function.span, + kind: T::Unreachable, + }, + }); + } + _ => unreachable!(), + } + rejects(&changed, &types); + } +} + +#[test] +fn caller_acquisition_written_direct_owned_producer_restores_exact_backing() { + let source = r#"namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "member" +function produce() returns Named: + return 7 +function read(view item: Named, extra: int64) returns int64: + return extra +function exercise(spare: Named, incoming: optional[int64]) returns int64: + return read(view produce(), incoming handle: + default 3 + ) +"#; + let (program, types) = source_program(source); + let function = exercise(&program); + let acquisitions = validate_function(&program, function, &types) + .expect("written View of original owned producer"); + assert_eq!( + acquisitions.owner_initializers().count(), + 0, + "backing is not a source Binding acquisition" + ); + let loan = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match statement.kind { + S::BeginCallView { local, .. } => Some(local), + _ => None, + }) + .expect("direct produced view stage"); + let owner = function + .local(loan) + .unwrap() + .view_source + .expect("exact backing owner"); + assert!(function.local(owner).unwrap().view_source.is_none()); + let produce = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "produce") + .unwrap() + .id; + assert_eq!(function.blocks.iter().flat_map(|block| &block.statements).filter(|statement| matches!(&statement.kind, + S::Let { local, value } if *local == owner && matches!(value.kind, E::Call { function, .. } if function == produce))).count(), 1); + for malformed in [ + "renamed binding", + "missing initializer", + "duplicate initializer", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + let (block, index) = function.blocks.iter().enumerate().find_map(|(block, value)| + value.statements.iter().position(|statement| matches!(statement.kind, S::Let { local, .. } if local == owner)) + .map(|index| (block, index))).expect("owning producer initializer"); + match malformed { + "renamed binding" => { + let spare = function.params[0].local; + let S::Let { value, .. } = &mut function.blocks[block].statements[index].kind + else { + panic!("owner Let"); + }; + value.kind = E::Local(spare); + } + "missing initializer" => { + function.blocks[block].statements.remove(index); + } + "duplicate initializer" => { + let duplicate = function.blocks[block].statements[index].clone(); + function.blocks[block].statements.insert(index, duplicate); + } + _ => unreachable!(), + } + rejects(&changed, &types); + } +} + +#[test] +fn caller_acquisition_handled_storage_keeps_sealed_occurrence_separate_from_actual() { + let source = r#"namespace app +function read(view values: secret[list[int64]], extra: int64) returns int64: + return extra +function exercise(incoming: optional[list[int64]]) returns int64: + return read((incoming handle: + default list(7) + ), 3) +"#; + let (program, types) = source_program(source); + let function = exercise(&program); + let acquisitions = + validate_function(&program, function, &types).expect("handled Secret-qualified occurrence"); + let (owner, _) = owning_stage(function); + let source = acquisitions + .owner_initializer(owner) + .expect("produced owning acquisition"); + assert!(matches!(types.resolve(source.actual_type), Type::List(_))); + assert!( + matches!(types.resolve(source.storage_type), Type::Secret(inner) if *inner == source.actual_type) + ); + assert_eq!(source.binding, None); + assert_eq!(source.effect, CheckedCallerEffect::RelinquishOwned); + let mut changed = program.clone(); + let E::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &mut read_call_mut(exercise_mut(&mut changed)).kind + else { + panic!("qualified source read"); + }; + packet.arguments[0].actual_type = source.storage_type; + rejects(&changed, &types); +} + +const ABSENT_SUCCESS_SOURCE: &str = r#"namespace app +function read(view values: list[int64], extra: int64) returns int64: + return extra +function exercise(values: list[int64]) returns int64: + return read(values, (none handle: + default 3 + )) +"#; + +fn absent_original_plan(program: &Program, types: &TypeInterner) -> AbsentSuccessPlan { + let proof = validate_function(program, exercise(program), types) + .expect("typed original absent success"); + let [plan] = proof.absent_successes.as_slice() else { + panic!("one exact absent-success plan"); + }; + plan.clone() +} + +#[test] +fn caller_acquisition_absent_success_is_original_metadata_then_private_prepared_proof() { + for source in [ + ABSENT_SUCCESS_SOURCE.to_owned(), + ABSENT_SUCCESS_SOURCE.replace("none handle:", "fail(\"missing\") handle error:"), + ] { + let (mut program, types) = source_program(&source); + let plan = absent_original_plan(&program, &types); + assert!( + matches!(types.resolve(plan.source_type), Type::Optional(inner) if *inner == TypeInterner::NEVER) + || matches!(types.resolve(plan.source_type), Type::Result(ok, error) if *ok == TypeInterner::NEVER && *error == TypeInterner::STRING) + ); + assert_eq!(plan.output_type, TypeInterner::INT64); + assert!(exercise(&program).prepared_absent_successes.is_empty()); + 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_call_ownership(&program, &types).expect("all prepared caller proofs"); + let function = exercise(&program); + let [record] = function.prepared_absent_successes.as_slice() else { + panic!("one private preparation certificate"); + }; + let prepared = record.plan(); + assert_eq!(prepared.function, function.id); + assert_eq!(prepared.identity, function.identity); + assert_eq!(prepared.source_type, plan.source_type); + assert_eq!(prepared.output_type, plan.output_type); + assert_eq!(prepared.span, plan.span); + assert!( + matches!(function.blocks[prepared.selecting.index() as usize].terminator.kind, + T::Goto(target) if target == prepared.failure) + ); + assert!(!function.blocks.iter().flat_map(|block| &block.statements).any(|statement| + matches!(statement.kind, S::SumTake { target, success: true, .. } if target == prepared.output))); + assert!( + matches!(function.blocks[prepared.selecting.index() as usize].statements.last().unwrap().kind, + S::SumTag { source, target } if source == prepared.source && target == prepared.tag) + ); + let original = program.clone(); + crate::prepare_native_uninhabited_sums(&mut program, &types); + assert_eq!( + program, original, + "a second preparation never duplicates authority" + ); + } +} + +#[test] +fn caller_acquisition_pending_sum_receiver_and_nested_absent_defaults_keep_order() { + let pending = ABSENT_SUCCESS_SOURCE.replace( + " return read(values, (none handle:", + " optional[int64] incoming = run run none\n return read(values, (incoming handle:", + ); + let (mut program, types) = source_program(&pending); + let proof = validate_function(&program, exercise(&program), &types) + .expect("typed inhabited pending receiver"); + assert!( + proof.absent_successes.is_empty(), + "an inhabited Optional[int64] never receives absent-success authority" + ); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::validate_call_ownership(&program, &types) + .expect("pending receiver remains an ordinary typed sum"); + let function = exercise(&program); + assert!(function.prepared_absent_successes.is_empty()); + let (pending_block, pending_position, incoming) = function.blocks.iter().find_map(|block| { + block.statements.iter().enumerate().find_map(|(position, statement)| match &statement.kind { + S::Let { local, value } if matches!(&value.kind, E::Run(first) if matches!(first.kind, E::Run(_))) => Some((block.id, position, *local)), + _ => None, + }) + }).expect("both original pending layers remain executable"); + assert!( + matches!(types.resolve(function.local(incoming).unwrap().ty), Type::Optional(inner) if *inner == TypeInterner::INT64) + ); + let (block, source, tag, tag_position) = + function + .blocks + .iter() + .find_map(|block| { + block.statements.iter().enumerate().find_map( + |(position, statement)| match statement.kind { + S::SumTag { source, target } => Some((block, source, target, position)), + _ => None, + }, + ) + }) + .expect("ordinary runtime tag remains"); + let source_position = block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::Let { local, .. } if local == source)) + .expect("owned sum source remains"); + assert_eq!(block.id, pending_block); + assert!( + pending_position < source_position && source_position < tag_position, + "original pending creation and ordinary snapshot precede the runtime tag" + ); + let S::Let { value, .. } = &block.statements[source_position].kind else { + unreachable!() + }; + assert!( + matches!(&value.kind, E::Clone(inner) if matches!(inner.kind, E::Local(local) if local == incoming)), + "the existing handle snapshot preserves the pending receiver" + ); + let T::Branch { + condition, + then_block, + else_block, + } = &block.terminator.kind + else { + panic!("inhabited sum retains its branch"); + }; + assert!(matches!(condition.kind, E::Local(local) if local == tag)); + assert_ne!(then_block, else_block); + assert!(function.blocks[then_block.index() as usize].statements.iter().any(|statement| + matches!(statement.kind, S::SumTake { source: owner, success: true, .. } if owner == source)), + "inhabited success extraction remains"); + + let nested = ABSENT_SUCCESS_SOURCE.replace("default 3", "optional[int64] fallback = none\n default (fallback handle:\n default 4\n )"); + let (mut program, types) = source_program(&nested); + absent_original_plan(&program, &types); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::validate_call_ownership(&program, &types).expect("nested prepared original occurrence"); + let function = exercise(&program); + let [record] = function.prepared_absent_successes.as_slice() else { + panic!("one exact nested absent-success certificate"); + }; + let plan = record.plan(); + let block = &function.blocks[plan.selecting.index() as usize]; + let source_position = block + .statements + .iter() + .position( + |statement| matches!(statement.kind, S::Let { local, .. } if local == plan.source), + ) + .expect("owning sum source remains executable"); + let tag_position = block.statements.iter().position(|statement| + matches!(statement.kind, S::SumTag { source, target } if source == plan.source && target == plan.tag)).unwrap(); + assert!( + source_position < tag_position, + "source evaluation precedes the runtime sum guard" + ); + assert_ne!( + plan.selecting, plan.failure, + "nested defaults remain after the exact false edge" + ); + let inner = function.blocks.iter().find(|block| + block.id != plan.selecting && matches!(&block.terminator.kind, T::Branch { .. }) + && block.statements.iter().any(|statement| matches!(statement.kind, + S::SumTag { source, .. } if matches!(types.resolve(function.local(source).unwrap().ty), + Type::Optional(payload) if *payload == TypeInterner::INT64)))) + .expect("typed nested fallback retains its inhabited branch"); + let T::Branch { then_block, .. } = inner.terminator.kind else { + unreachable!() + }; + assert!( + function.blocks[then_block.index() as usize] + .statements + .iter() + .any(|statement| matches!(statement.kind, S::SumTake { success: true, .. })), + "inner inhabited success extraction remains after outer absence preparation" + ); +} + +#[test] +fn caller_acquisition_absent_success_rejects_original_forged_tag_default_and_manual_goto() { + let (program, types) = source_program(ABSENT_SUCCESS_SOURCE); + let plan = absent_original_plan(&program, &types); + for malformed in [ + "tag type", + "same branches", + "false extraction", + "foreign default", + "manual goto", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + match malformed { + "tag type" => function.locals[plan.tag.index() as usize].ty = TypeInterner::INT64, + "same branches" => { + let T::Branch { + then_block, + else_block, + .. + } = &mut function.blocks[plan.selecting.index() as usize] + .terminator + .kind + else { + panic!("original branch"); + }; + *then_block = *else_block; + } + "false extraction" => { + let statement = function.blocks.iter_mut().flat_map(|block| &mut block.statements).find(|statement| + matches!(statement.kind, S::SumTake { target, success: true, .. } if target == plan.output)).unwrap(); + let S::SumTake { success, .. } = &mut statement.kind else { + panic!("anchor"); + }; + *success = false; + } + "foreign default" => { + let statement = function.blocks.iter_mut().flat_map(|block| &mut block.statements).find(|statement| + matches!(statement.kind, S::Let { local, .. } if local == plan.output)).unwrap(); + statement.span.file = jett_common::FileId::new(u32::MAX); + } + "manual goto" => { + function.blocks[plan.selecting.index() as usize] + .terminator + .kind = T::Goto(plan.failure); + crate::sequences::prune::unreachable(function); + assert!(function.prepared_absent_successes.is_empty()); + } + _ => unreachable!(), + } + rejects(&changed, &types); + let before = changed.clone(); + crate::prepare_native_uninhabited_sums(&mut changed, &types); + assert_eq!( + changed, before, + "bad original graph is never rescued by pruning" + ); + } +} + +#[test] +fn caller_acquisition_absent_success_rejoins_every_private_prepared_record() { + let (mut program, types) = source_program(ABSENT_SUCCESS_SOURCE); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::validate_call_ownership(&program, &types).unwrap(); + let plan = exercise(&program).prepared_absent_successes[0] + .plan() + .clone(); + for malformed in [ + "missing record", + "duplicate record", + "changed goto", + "missing tag", + "wrong output type", + "foreign default", + "disconnected default", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + match malformed { + "missing record" => function.prepared_absent_successes.clear(), + "duplicate record" => function + .prepared_absent_successes + .push(function.prepared_absent_successes[0].clone()), + "changed goto" => { + function.blocks[plan.selecting.index() as usize] + .terminator + .kind = T::Goto(plan.selecting) + } + "missing tag" => { + function.blocks[plan.selecting.index() as usize].statements.retain(|statement| + !matches!(statement.kind, S::SumTag { source, target } if source == plan.source && target == plan.tag)); + } + "wrong output type" => { + function.locals[plan.output.index() as usize].debug_ty = TypeInterner::BOOL + } + "foreign default" => { + let statement = function.blocks.iter_mut().flat_map(|block| &mut block.statements).find(|statement| + matches!(statement.kind, S::Let { local, .. } if local == plan.output)).unwrap(); + statement.span.file = jett_common::FileId::new(u32::MAX); + } + "disconnected default" => { + let statement = function.blocks.iter().flat_map(|block| &block.statements).find(|statement| + matches!(statement.kind, S::Let { local, .. } if local == plan.output)).unwrap().clone(); + function.blocks.push(crate::BasicBlock { + id: BlockId(function.blocks.len() as u32), + statements: vec![statement], + terminator: crate::Terminator { + span: function.span, + kind: T::Unreachable, + }, + }); + } + _ => unreachable!(), + } + rejects(&changed, &types); + } +} + +#[test] +fn caller_acquisition_absent_success_certificates_follow_real_dense_remapping() { + let source = ABSENT_SUCCESS_SOURCE.replace(" return read(values,", " function(int64) returns int64 callback = function(ignored: int64) returns int64:\n return ignored\n int64 first = callback(0)\n return read(values,"); + let (mut program, types) = source_program(&source); + let original = absent_original_plan(&program, &types); + crate::prepare_native_uninhabited_sums(&mut program, &types); + let after_sum = exercise(&program).prepared_absent_successes[0] + .plan() + .clone(); + assert_ne!( + after_sum.failure, original.failure, + "removed success block changes the failure ID" + ); + assert_ne!( + after_sum.source, original.source, + "detached inline formal changes the owner ID" + ); + crate::prepare_native_generated_functions(&mut program, &types); + crate::prepare_native_sequences(&mut program, &types); + crate::validate_call_ownership(&program, &types) + .expect("remapped certificate joins actual storage and graph"); + let function = exercise(&program); + let plan = function.prepared_absent_successes[0].plan(); + assert_eq!(function.local(plan.source).unwrap().ty, plan.source_type); + assert_eq!(function.local(plan.output).unwrap().ty, plan.output_type); + assert_eq!(function.local(plan.tag).unwrap().ty, TypeInterner::BOOL); + assert!( + matches!(function.blocks[plan.selecting.index() as usize].terminator.kind, T::Goto(target) if target == plan.failure) + ); +} + +const GENERATED_SNAPSHOT_SOURCE: &str = r#"interface Displayable: + function display(view self: Displayable) returns string +namespace app +struct Item: + value: int64 +implement Displayable for Item: + function display(view self: Item) returns string: + return "shown" +function exercise(view item: optional[Item], view spare: Item) returns string: + return "{item handle: default Item(value: 7)}" +"#; + +const GENERATED_PRODUCER_SOURCE: &str = r#"interface Displayable: + function display(view self: Displayable) returns string +namespace app +struct Item: + value: int64 +implement Displayable for Item: + function display(view self: Item) returns string: + return "shown" +function make(value: int64) returns Item: + return Item(value: value) +function other(value: int64) returns Item: + return Item(value: value) +function exercise(incoming: optional[int64]) returns string: + return "{make(incoming handle: default 7)}" +"#; + +fn generated_in_expression(value: &Expression) -> Option<&Expression> { + if matches!( + &value.kind, + E::Call { + ownership: hir::CallOwnership::Generated(_), + .. + } | E::IndirectCall { + ownership: hir::CallOwnership::Generated(_), + .. + } | E::Intrinsic { + ownership: hir::CallOwnership::Generated(_), + .. + } + ) { + return Some(value); + } + match &value.kind { + E::View(inner) + | E::Clone(inner) + | E::Coarsen(inner) + | E::Declassify(inner) + | E::Run(inner) + | E::Join(inner) + | E::RefinementValidated(inner) + | E::DisplayResult(inner) + | E::EquatableResult(inner) => generated_in_expression(inner), + E::StringInterpolation(parts) => parts.iter().find_map(|part| match part { + hir::StringSegment::Value(value) => generated_in_expression(value), + _ => None, + }), + E::Call { args, .. } | E::IndirectCall { args, .. } | E::Intrinsic { args, .. } => { + args.iter().find_map(generated_in_expression) + } + _ => None, + } +} + +fn generated_in_expression_mut(value: &mut Expression) -> Option<&mut Expression> { + if matches!( + &value.kind, + E::Call { + ownership: hir::CallOwnership::Generated(_), + .. + } | E::IndirectCall { + ownership: hir::CallOwnership::Generated(_), + .. + } | E::Intrinsic { + ownership: hir::CallOwnership::Generated(_), + .. + } + ) { + return Some(value); + } + match &mut value.kind { + E::View(inner) + | E::Clone(inner) + | E::Coarsen(inner) + | E::Declassify(inner) + | E::Run(inner) + | E::Join(inner) + | E::RefinementValidated(inner) + | E::DisplayResult(inner) + | E::EquatableResult(inner) => generated_in_expression_mut(inner), + E::StringInterpolation(parts) => parts.iter_mut().find_map(|part| match part { + hir::StringSegment::Value(value) => generated_in_expression_mut(value), + _ => None, + }), + E::Call { args, .. } | E::IndirectCall { args, .. } | E::Intrinsic { args, .. } => { + args.iter_mut().find_map(generated_in_expression_mut) + } + _ => None, + } +} + +fn generated_call(function: &Function) -> &Expression { + function + .blocks + .iter() + .find_map(|block| { + block + .statements + .iter() + .find_map(|statement| match &statement.kind { + S::Let { value, .. } | S::Evaluate(value) | S::HandleDefault(value) => { + generated_in_expression(value) + } + S::CheckRefinement { call, .. } => generated_in_expression(call), + _ => None, + }) + .or_else(|| match &block.terminator.kind { + T::Return(Some(value)) => generated_in_expression(value), + _ => None, + }) + }) + .expect("source-derived generated invocation") +} + +fn generated_call_mut(function: &mut Function) -> &mut Expression { + for block in &mut function.blocks { + for statement in &mut block.statements { + let found = match &mut statement.kind { + S::Let { value, .. } | S::Evaluate(value) | S::HandleDefault(value) => { + generated_in_expression_mut(value) + } + S::CheckRefinement { call, .. } => generated_in_expression_mut(call), + _ => None, + }; + if let Some(found) = found { + return found; + } + } + if let T::Return(Some(value)) = &mut block.terminator.kind + && let Some(found) = generated_in_expression_mut(value) + { + return found; + } + } + panic!("source-derived generated invocation"); +} + +fn generated_argument(function: &Function) -> &hir::GeneratedArgumentOwnership { + let E::Call { + ownership: hir::CallOwnership::Generated(call), + .. + } = &generated_call(function).kind + else { + panic!("generated direct call"); + }; + &call.arguments[0] +} + +fn generated_owner_stage(function: &Function) -> (LocalId, LocalId) { + match generated_argument(function).staging { + hir::GeneratedArgumentStaging::OwnedProducer { owner, loan } + | hir::GeneratedArgumentStaging::OrdinarySnapshot { owner, loan } => (owner, loan), + other => panic!("generated owning stage: {other:?}"), + } +} + +fn generated_initializer_mut(function: &mut Function, owner: LocalId) -> &mut Expression { + function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + S::Let { local, value } if *local == owner => Some(value), + _ => None, + }) + .expect("generated owning initializer") +} + +fn generated_rejects(program: &Program, types: &TypeInterner) -> String { + let error = validate_function(program, exercise(program), types) + .expect_err("malformed generated storage must fail"); + assert!( + error.contains("call ownership") + || error.contains("call view") + || error.contains("generated") + || error.contains("sealed"), + "{error}" + ); + error +} + +#[test] +fn caller_acquisition_generated_snapshot_keeps_handled_origin_and_empty_source_acquisitions() { + let (mut program, types) = source_program(GENERATED_SNAPSHOT_SOURCE); + let function = exercise(&program); + let argument = generated_argument(function); + assert!( + argument + .original_witness() + .ordinary_snapshot_proof() + .is_some() + ); + assert!(!argument.original_witness().owned_producer()); + assert!(matches!( + argument.staging, + hir::GeneratedArgumentStaging::OrdinarySnapshot { .. } + )); + let (owner, loan) = generated_owner_stage(function); + let initializer = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + S::Let { local, value } if *local == owner => Some(value), + _ => None, + }) + .expect("snapshot initializer"); + assert!(matches!(initializer.kind, E::Clone(_))); + assert_eq!(function.local(loan).expect("loan").view_source, Some(owner)); + let acquisitions = + validate_function(&program, function, &types).expect("generated handled snapshot"); + assert_eq!( + acquisitions.owner_initializers().count(), + 0, + "generated copy must not consume a source binding" + ); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::prepare_native_generated_functions(&mut program, &types); + validate_function(&program, exercise(&program), &types).expect("prepared generated snapshot"); +} + +#[test] +fn caller_acquisition_generated_snapshot_rejects_lost_clone_and_same_type_borrow_substitution() { + let (program, types) = source_program(GENERATED_SNAPSHOT_SOURCE); + let (owner, _) = generated_owner_stage(exercise(&program)); + let mut changed = program.clone(); + let initializer = generated_initializer_mut(exercise_mut(&mut changed), owner); + let E::Clone(inner) = &initializer.kind else { + panic!("snapshot clone"); + }; + *initializer = inner.as_ref().clone(); + generated_rejects(&changed, &types); + + let mut changed = program.clone(); + let spare = exercise(&changed).params[1].local; + let initializer = generated_initializer_mut(exercise_mut(&mut changed), owner); + let E::Clone(observed) = &mut initializer.kind else { + panic!("snapshot clone"); + }; + let E::View(value) = &mut observed.kind else { + panic!("physical observed view"); + }; + value.kind = E::Local(spare); + generated_rejects(&changed, &types); + + let mut changed = program.clone(); + let mut replacement = exercise(&changed) + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + S::Let { value, .. } if matches!(value.kind, E::StructConstruct { .. }) => { + Some(value.clone()) + } + _ => None, + }) + .expect("typed default constructor"); + let initializer = generated_initializer_mut(exercise_mut(&mut changed), owner); + let E::Clone(observed) = &mut initializer.kind else { + panic!("snapshot clone"); + }; + let E::View(value) = &mut observed.kind else { + panic!("physical observed view"); + }; + replacement.span = value.span; + assert_eq!(replacement.ty, value.ty); + **value = replacement; + generated_rejects(&changed, &types); +} + +#[test] +fn caller_acquisition_generated_storage_rejects_duplicate_owner_missing_end_and_public_relabel() { + let (program, types) = source_program(GENERATED_SNAPSHOT_SOURCE); + let (owner, loan) = generated_owner_stage(exercise(&program)); + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + let (block, index) = function + .blocks + .iter() + .enumerate() + .find_map(|(block, value)| { + value + .statements + .iter() + .position( + |statement| matches!(statement.kind, S::Let { local, .. } if local == owner), + ) + .map(|index| (block, index)) + }) + .expect("owner definition"); + let duplicate = function.blocks[block].statements[index].clone(); + function.blocks[block].statements.insert(index, duplicate); + generated_rejects(&changed, &types); + + let mut changed = program.clone(); + for block in &mut exercise_mut(&mut changed).blocks { + block.statements.retain( + |statement| !matches!(statement.kind, S::EndCallView { local } if local == loan), + ); + } + generated_rejects(&changed, &types); + + let mut changed = program.clone(); + let E::Call { + ownership: hir::CallOwnership::Generated(call), + .. + } = &mut generated_call_mut(exercise_mut(&mut changed)).kind + else { + panic!("generated call"); + }; + call.arguments[0].staging = hir::GeneratedArgumentStaging::OwnedProducer { owner, loan }; + generated_rejects(&changed, &types); +} + +#[test] +fn caller_acquisition_generated_owned_producer_rejects_added_clone_and_wrong_exact_target() { + let (program, types) = source_program(GENERATED_PRODUCER_SOURCE); + let function = exercise(&program); + let argument = generated_argument(function); + assert!(argument.original_witness().owned_producer()); + assert!(matches!( + argument.staging, + hir::GeneratedArgumentStaging::OwnedProducer { .. } + )); + let (owner, _) = generated_owner_stage(function); + validate_function(&program, function, &types).expect("complete generated producer"); + + let mut changed = program.clone(); + let initializer = generated_initializer_mut(exercise_mut(&mut changed), owner); + let original = initializer.clone(); + initializer.kind = E::Clone(Box::new(original)); + generated_rejects(&changed, &types); + + let mut changed = program.clone(); + let other = changed + .functions + .iter() + .find(|function| function.identity.declaration.name == "other") + .expect("same-signature control") + .id; + let initializer = generated_initializer_mut(exercise_mut(&mut changed), owner); + let E::Call { function, .. } = &mut initializer.kind else { + panic!("full endpoint producer"); + }; + *function = other; + generated_rejects(&changed, &types); +} + +#[test] +fn caller_acquisition_generated_explicit_clone_and_constructor_producers_remain_distinct() { + for source in [ + GENERATED_PRODUCER_SOURCE.replace("{make(incoming", "{clone make(incoming"), + GENERATED_PRODUCER_SOURCE.replace( + "{make(incoming handle: default 7)}", + "{Item(value: incoming handle: default 7)}", + ), + ] { + let (program, types) = source_program(&source); + let function = exercise(&program); + let argument = generated_argument(function); + assert!(argument.original_witness().owned_producer()); + assert!(matches!( + argument.staging, + hir::GeneratedArgumentStaging::OwnedProducer { .. } + )); + validate_function(&program, function, &types) + .expect("original explicit Clone or exact constructor producer"); + } +} + +#[test] +fn caller_acquisition_generated_constructor_restores_each_exact_materialized_field() { + let source = GENERATED_PRODUCER_SOURCE + .replace( + " value: int64\n", + " value: int64\n marker: int64\n", + ) + .replace("Item(value: value)", "Item(value: value, marker: 1)") + .replace( + "{make(incoming handle: default 7)}", + "{Item(value: incoming handle: default 7, marker: 2)}", + ); + let (program, types) = source_program(&source); + let function = exercise(&program); + let (owner, _) = generated_owner_stage(function); + validate_function(&program, function, &types) + .expect("handled and primitive fields preserve their original nodes"); + let field = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + S::Let { + local, + value: + Expression { + kind: E::StructConstruct { fields, .. }, + .. + }, + } if *local == owner => { + assert_eq!(fields.len(), 2); + let E::Local(field) = fields[1].kind else { + panic!("second field materialized at lexical position"); + }; + Some(field) + } + _ => None, + }) + .expect("complete constructor initializer"); + + let mut changed = program.clone(); + let initializer = generated_initializer_mut(exercise_mut(&mut changed), field); + let original = initializer.clone(); + initializer.kind = E::Clone(Box::new(original)); + generated_rejects(&changed, &types); + + let mut changed = program.clone(); + let initializer = generated_initializer_mut(exercise_mut(&mut changed), field); + initializer.kind = E::Local(field); + generated_rejects(&changed, &types); +} + +#[test] +fn caller_acquisition_generated_storage_and_private_association_follow_dense_local_remapping() { + let source = GENERATED_SNAPSHOT_SOURCE.replace(" return \"{item handle:", + " function(int64) returns int64 callback = function(ignored: int64) returns int64:\n return ignored\n int64 first = callback(0)\n return \"{item handle:"); + let (mut program, types) = source_program(&source); + let before = generated_owner_stage(exercise(&program)); + crate::prepare_native_generated_functions(&mut program, &types); + let function = exercise(&program); + let after = generated_owner_stage(function); + assert_ne!( + after, before, + "detached inline formal must force real stage ID remapping" + ); + assert_eq!( + function.local(after.1).expect("remapped loan").view_source, + Some(after.0) + ); + validate_function(&program, function, &types) + .expect("all original and private Generated stage IDs remapped"); +} + +#[test] +fn caller_acquisition_generated_owned_copy_storage_requires_one_exact_original_initializer() { + let source = r#"namespace app +type Positive = int64 where value > 0 +function exercise(raw: int64) returns Positive: + return raw +"#; + let (mut program, types) = source_program(source); + let function = exercise_mut(&mut program); + let (block, index) = function + .blocks + .iter() + .enumerate() + .find_map(|(block, value)| { + value + .statements + .iter() + .position(|statement| matches!(statement.kind, S::CheckRefinement { .. })) + .map(|index| (block, index)) + }) + .expect("refinement check"); + let (original, span) = { + let S::CheckRefinement { call, .. } = &function.blocks[block].statements[index].kind else { + panic!("predicate"); + }; + let E::Call { + args, + ownership: hir::CallOwnership::Generated(packet), + .. + } = &call.kind + else { + panic!("generated predicate"); + }; + assert_eq!( + packet.arguments[0].acquisition, + hir::GeneratedAcquisition::Copy + ); + assert_eq!( + packet.arguments[0].callee_access, + jett_typecheck::CheckedCalleeAccess::Owned + ); + assert_eq!( + packet.arguments[0].staging, + hir::GeneratedArgumentStaging::Existing { loan: None } + ); + (args[0].clone(), args[0].span) + }; + let owner = LocalId::new(function.locals.len() as u32); + let mut metadata = function.locals[function.params[0].local.index() as usize].clone(); + metadata.id = owner; + metadata.name = "isolated_materialized_operand".into(); + metadata.ty = original.ty; + metadata.debug_ty = original.ty; + metadata.mutable = true; + metadata.view_source = None; + metadata.span = span; + function.locals.push(metadata); + let S::CheckRefinement { call, .. } = &mut function.blocks[block].statements[index].kind else { + panic!("predicate"); + }; + let E::Call { args, .. } = &mut call.kind else { + panic!("call"); + }; + args[0] = Expression { + kind: E::Local(owner), + ty: original.ty, + span, + }; + function.blocks[block].statements.insert( + index, + crate::Statement { + span, + kind: S::Let { + local: owner, + value: original, + }, + }, + ); + let acquisitions = validate_function(&program, exercise(&program), &types) + .expect("finite owned Copy materialization"); + assert_eq!(acquisitions.owner_initializers().count(), 0); + + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + let duplicate = function.blocks[block].statements[index].clone(); + function.blocks[block].statements.insert(index, duplicate); + generated_rejects(&changed, &types); + + let mut changed = program.clone(); + exercise_mut(&mut changed).locals[owner.index() as usize].debug_ty = TypeInterner::BOOL; + generated_rejects(&changed, &types); +} + +const PRESENT_SUCCESS_SOURCE: &str = r#"namespace app +function read(view values: list[int64], extra: int64) returns int64: + return extra +function exercise[E](incoming: result[list[int64], E]) returns int64: + return read((incoming handle error: + default list(7) + ), 3) +function main() returns nothing: + int64 answer = exercise(ok(list(1, 2))) + return nothing +"#; + +fn present_original_plan(program: &Program, types: &TypeInterner) -> PresentSuccessPlan { + let proof = validate_function(program, exercise(program), types) + .expect("typed original inhabited-success edge"); + let [plan] = proof.present_successes.as_slice() else { + panic!("one exact present-success plan"); + }; + plan.clone() +} + +#[test] +fn caller_acquisition_present_success_preserves_tag_and_owned_payload_without_default_authority() { + for source in [ + PRESENT_SUCCESS_SOURCE.to_owned(), + PRESENT_SUCCESS_SOURCE.replace( + "read(view values: list[int64]", + "read(view values: secret[list[int64]]", + ), + ] { + let (mut program, types) = source_program(&source); + let original = present_original_plan(&program, &types); + assert!( + matches!(types.resolve(original.source_type), Type::Result(ok, error) + if *ok != TypeInterner::NEVER && *error == TypeInterner::NEVER) + ); + assert!(exercise(&program).prepared_present_successes.is_empty()); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::validate_call_ownership(&program, &types) + .expect("canonical prepared success retains acquisition authority"); + let function = exercise(&program); + let [record] = function.prepared_present_successes.as_slice() else { + panic!("one private present-success record"); + }; + let plan = record.plan(); + assert_eq!( + ( + plan.function, + &plan.identity, + plan.source_type, + plan.output_type, + plan.span + ), + ( + function.id, + &function.identity, + original.source_type, + original.output_type, + original.span + ) + ); + assert!(function.prepared_absent_successes.is_empty()); + assert!( + matches!(function.blocks[plan.selecting.index() as usize].terminator.kind, + T::Goto(target) if target == plan.success) + ); + assert!( + matches!(function.blocks[plan.selecting.index() as usize].statements.last().unwrap().kind, + S::SumTag { source, target } if source == plan.source && target == plan.tag) + ); + let extractions = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| { + matches!(statement.kind, S::SumTake { source, target, success: true } + if source == plan.source && target == plan.output) + }) + .count(); + assert_eq!( + extractions, 1, + "inhabited successful payload extraction stays executable" + ); + assert!(!function.blocks.iter().flat_map(|block| &block.statements).any(|statement| + matches!(statement.kind, S::Let { local, .. } if local == plan.output)), "erased default gains no prepared authority"); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("successful payload/source ownership and cleanup remain exact"); + let prepared = program.clone(); + crate::prepare_native_uninhabited_sums(&mut program, &types); + assert_eq!( + program, prepared, + "repeated preparation cannot mint a second record from Goto" + ); + } + + let control = PRESENT_SUCCESS_SOURCE.replace("int64 answer = exercise(ok(list(1, 2)))", + "result[list[int64], string] incoming = ok(list(1, 2))\n int64 answer = exercise(incoming)"); + let (mut program, types) = source_program(&control); + assert!( + validate_function(&program, exercise(&program), &types) + .unwrap() + .present_successes + .is_empty() + ); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::validate_call_ownership(&program, &types).unwrap(); + assert!(exercise(&program).prepared_present_successes.is_empty()); + assert!( + exercise(&program) + .blocks + .iter() + .any(|block| matches!(block.terminator.kind, T::Branch { .. })), + "an inhabited error arm keeps its ordinary selecting Branch" + ); +} + +#[test] +fn caller_acquisition_present_pending_receiver_keeps_original_snapshot_before_tag_and_take() { + let source = PRESENT_SUCCESS_SOURCE.replace( + "exercise(ok(list(1, 2)))", + "exercise(run run ok(list(1, 2)))", + ); + let (mut program, types) = source_program(&source); + present_original_plan(&program, &types); + let main = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .unwrap(); + fn double_pending(value: &Expression) -> bool { + match &value.kind { + E::Run(first) if matches!(first.kind, E::Run(_)) => true, + E::Call { args, .. } | E::Intrinsic { args, .. } => args.iter().any(double_pending), + E::View(value) | E::Clone(value) => double_pending(value), + _ => false, + } + } + assert!( + main.blocks + .iter() + .any(|block| block + .statements + .iter() + .any(|statement| match &statement.kind { + S::Let { value, .. } | S::Evaluate(value) => double_pending(value), + _ => false, + })), + "both pending layers remain in the checked caller" + ); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::validate_call_ownership(&program, &types) + .expect("pending exact result keeps prepared source proof"); + let function = exercise(&program); + let plan = function.prepared_present_successes[0].plan(); + let block = &function.blocks[plan.selecting.index() as usize]; + let owning = block + .statements + .iter() + .position(|statement| { + matches!(statement.kind, + S::Let { local, .. } if local == plan.source) + }) + .expect("original source owning snapshot"); + let tag = block + .statements + .iter() + .position(|statement| { + matches!(statement.kind, + S::SumTag { source, target } if source == plan.source && target == plan.tag) + }) + .unwrap(); + assert!(owning < tag); + assert_eq!(tag + 1, block.statements.len()); + assert!(matches!(block.terminator.kind, T::Goto(target) if target == plan.success)); + assert!( + function.blocks[plan.success.index() as usize] + .statements + .iter() + .any( + |statement| matches!(statement.kind, S::SumTake { source, target, success: true } + if source == plan.source && target == plan.output) + ), + "pending guard remains before extraction" + ); +} + +#[test] +fn caller_acquisition_present_success_refuses_original_manual_or_foreign_edges_before_pruning() { + let (program, types) = source_program(PRESENT_SUCCESS_SOURCE); + let plan = present_original_plan(&program, &types); + for malformed in [ + "manual goto", + "wrong tag type", + "wrong tag owner", + "same branches", + "wrong extraction", + "foreign incoming", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + match malformed { + "manual goto" => { + function.blocks[plan.selecting.index() as usize] + .terminator + .kind = T::Goto(plan.success); + crate::sequences::prune::unreachable(function); + assert!(function.prepared_present_successes.is_empty()); + } + "wrong tag type" => function.locals[plan.tag.index() as usize].ty = TypeInterner::INT64, + "wrong tag owner" => { + let foreign = function.params[0].local; + let S::SumTag { source, .. } = &mut function.blocks + [plan.selecting.index() as usize] + .statements + .last_mut() + .unwrap() + .kind + else { + unreachable!() + }; + *source = foreign; + } + "same branches" => { + let T::Branch { + then_block, + else_block, + .. + } = &mut function.blocks[plan.selecting.index() as usize] + .terminator + .kind + else { + unreachable!() + }; + *else_block = *then_block; + } + "wrong extraction" => { + let statement = function.blocks[plan.success.index() as usize].statements.iter_mut().find(|statement| + matches!(statement.kind, S::SumTake { target, .. } if target == plan.output)).unwrap(); + let S::SumTake { success, .. } = &mut statement.kind else { + unreachable!() + }; + *success = false; + } + "foreign incoming" => function.blocks.push(crate::BasicBlock { + id: BlockId(function.blocks.len() as u32), + statements: vec![], + terminator: crate::Terminator { + span: plan.span, + kind: T::Goto(plan.success), + }, + }), + _ => unreachable!(), + } + rejects(&changed, &types); + let before = changed.clone(); + crate::prepare_native_uninhabited_sums(&mut changed, &types); + assert_eq!( + changed, before, + "canonical preparation never rescues an unproved original edge" + ); + } +} + +#[test] +fn caller_acquisition_present_success_rejoins_all_private_records_and_current_extraction() { + let (mut program, types) = source_program(PRESENT_SUCCESS_SOURCE); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::validate_call_ownership(&program, &types).unwrap(); + let plan = exercise(&program).prepared_present_successes[0] + .plan() + .clone(); + for malformed in [ + "missing record", + "duplicate record", + "wrong goto", + "missing tag", + "wrong output type", + "foreign identity", + "wrong extraction", + "foreign incoming", + "disconnected extraction", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + match malformed { + "missing record" => function.prepared_present_successes.clear(), + "duplicate record" => { + let duplicate = function.prepared_present_successes[0].clone(); + function.prepared_present_successes.push(duplicate); + } + "wrong goto" => function.blocks[plan.selecting.index() as usize].terminator.kind = T::Goto(plan.selecting), + "missing tag" => function.blocks[plan.selecting.index() as usize].statements.retain(|statement| + !matches!(statement.kind, S::SumTag { source, target } if source == plan.source && target == plan.tag)), + "wrong output type" => function.locals[plan.output.index() as usize].debug_ty = TypeInterner::BOOL, + "foreign identity" => function.identity.declaration.namespace.push_str("_foreign"), + "wrong extraction" => { + let statement = function.blocks[plan.success.index() as usize].statements.iter_mut().find(|statement| + matches!(statement.kind, S::SumTake { target, .. } if target == plan.output)).unwrap(); + let S::SumTake { source, .. } = &mut statement.kind else { unreachable!() }; + *source = function.params[0].local; + } + "foreign incoming" => function.blocks.push(crate::BasicBlock { + id: BlockId(function.blocks.len() as u32), statements: vec![], + terminator: crate::Terminator { span: plan.span, kind: T::Goto(plan.success) }, + }), + "disconnected extraction" => { + let statement = function.blocks[plan.success.index() as usize].statements.iter().find(|statement| + matches!(statement.kind, S::SumTake { target, .. } if target == plan.output)).unwrap().clone(); + function.blocks.push(crate::BasicBlock { id: BlockId(function.blocks.len() as u32), statements: vec![statement], + terminator: crate::Terminator { span: plan.span, kind: T::Unreachable } }); + } + _ => unreachable!(), + } + rejects(&changed, &types); + } +} + +#[test] +fn caller_acquisition_present_success_private_ids_follow_real_dense_remapping() { + let source = PRESENT_SUCCESS_SOURCE.replace(" return read((incoming", + " function(int64) returns int64 callback = function(ignored: int64) returns int64:\n return ignored\n int64 first = callback(0)\n return read((incoming"); + let (mut program, types) = source_program(&source); + let original = present_original_plan(&program, &types); + crate::prepare_native_uninhabited_sums(&mut program, &types); + let after_sum = exercise(&program).prepared_present_successes[0] + .plan() + .clone(); + assert_ne!( + after_sum.source, original.source, + "unused extracted inline formal causes actual dense local remapping" + ); + crate::prepare_native_generated_functions(&mut program, &types); + crate::prepare_native_sequences(&mut program, &types); + crate::validate_call_ownership(&program, &types) + .expect("every private ID rejoins remapped actual graph and storage"); + let function = exercise(&program); + let plan = function.prepared_present_successes[0].plan(); + assert_eq!(function.local(plan.source).unwrap().ty, plan.source_type); + assert_eq!(function.local(plan.output).unwrap().ty, plan.output_type); + assert_eq!(function.local(plan.tag).unwrap().ty, TypeInterner::BOOL); + assert!( + matches!(function.blocks[plan.selecting.index() as usize].terminator.kind, T::Goto(target) if target == plan.success) + ); +} + +const CONVERTED_OBSERVATION_SOURCE: &str = r#"namespace app +interface Named: + function name(view self: Named) returns string +struct Boxed[T]: + value: T +implement Named for Boxed[string]: + function name(view self: Boxed[string]) returns string: + return "text" +function read(values: list[Named], expected: string) returns bool: + return true +function count(view values: list[Boxed[string]]) returns int64: + return 0 +property observed: + given values: list[Boxed[string]] + given other: list[Boxed[string]] + assert read(values, "text") + assert read(clone values, "text") + assert count(view values) == 0 +"#; + +fn observation_source_program(source: &str) -> (Program, TypeInterner) { + let file = jett_common::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(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let high = hir::lower_with_test_bodies( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, jett_common::SourceOrigin::Project)]), + ) + .expect("property HIR"); + ( + crate::lower(&high, &checked.interner).expect("property MIR"), + checked.interner, + ) +} + +fn observed_property(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.kind == hir::DeclarationKind::Property + && function.identity.declaration.name.starts_with("observed:") + }) + .expect("checked property") +} + +fn observed_read_calls(function: &Function) -> Vec<&Expression> { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| { + let S::Assert { condition, .. } = &statement.kind else { + return None; + }; + matches!(condition.kind, E::Call { .. }).then_some(condition) + }) + .collect() +} + +fn observation_owner(call: &Expression) -> (LocalId, &hir::ArgumentOwnership) { + let E::Call { + ownership: hir::CallOwnership::Source(source), + args, + .. + } = &call.kind + else { + panic!("checked observed invocation"); + }; + let argument = &source.arguments[0]; + assert_eq!(argument.effect, CheckedCallerEffect::ObserveData); + assert_eq!( + argument.physical_access, + jett_typecheck::CheckedCalleeAccess::Owned + ); + let hir::ArgumentStaging::Observed { owner } = argument.staging else { + panic!("observed owning slot"); + }; + assert!(matches!(args[0].kind, E::Local(local) if local == owner)); + (owner, argument) +} + +fn observation_initializer(function: &Function, owner: LocalId) -> &Expression { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + S::Let { local, value } if *local == owner => Some(value), + _ => None, + }) + .expect("observed owning initializer") +} + +#[test] +fn source_observation_snapshots_raw_generic_data_before_interface_conversion() { + let (program, types) = observation_source_program(CONVERTED_OBSERVATION_SOURCE); + let function = observed_property(&program); + let calls = observed_read_calls(function); + assert_eq!(calls.len(), 2); + validate_function(&program, function, &types).expect("converted observed acquisition"); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("original concrete owner stays available"); + for (index, call) in calls.iter().enumerate() { + let (owner, argument) = observation_owner(call); + let initializer = observation_initializer(function, owner); + let E::InterfaceCoerce { + value: snapshot, .. + } = &initializer.kind + else { + panic!("converted snapshot retains its checked contextual conversion"); + }; + assert_ne!(initializer.ty, argument.actual_type); + assert!(hir::observation_data_type(&types, argument.actual_type)); + assert!(!crate::handlers::can_snapshot_view(&types, initializer.ty)); + let E::Clone(original) = &snapshot.kind else { + panic!("raw snapshot"); + }; + assert_eq!(snapshot.ty, argument.actual_type); + assert_eq!(original.ty, argument.actual_type); + assert_eq!(snapshot.span, argument.source_span); + assert_eq!(original.span, argument.source_span); + if index == 0 { + assert!(matches!(original.kind, E::Local(local) if local == function.params[0].local)); + } else { + assert!( + matches!(&original.kind, E::Clone(inner) + if matches!(inner.kind, E::Local(local) if local == function.params[0].local)), + "the explicit source Clone remains inside the compiler snapshot" + ); + } + } +} + +#[test] +fn source_observation_without_conversion_keeps_its_direct_snapshot_shape() { + let source = CONVERTED_OBSERVATION_SOURCE.replace( + "function read(values: list[Named]", + "function read(values: list[Boxed[string]]", + ); + let (program, types) = observation_source_program(&source); + let function = observed_property(&program); + let calls = observed_read_calls(function); + assert_eq!(calls.len(), 2); + for call in calls { + let (owner, argument) = observation_owner(call); + let initializer = observation_initializer(function, owner); + let E::Clone(original) = &initializer.kind else { + panic!("existing direct snapshot"); + }; + assert_eq!(initializer.ty, argument.actual_type); + assert_eq!(original.ty, initializer.ty); + assert!(crate::handlers::can_snapshot_view(&types, initializer.ty)); + } + validate_function(&program, function, &types).expect("direct observed acquisition"); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("direct observation retains its owner"); +} + +#[test] +fn source_observation_conversion_rejects_missing_erased_and_foreign_snapshots() { + for mutation in 0..5 { + let (mut program, types) = observation_source_program(CONVERTED_OBSERVATION_SOURCE); + let function_id = observed_property(&program).id; + let (owner, _) = observation_owner(observed_read_calls(observed_property(&program))[0]); + let function = program + .functions + .iter_mut() + .find(|function| function.id == function_id) + .unwrap(); + if mutation == 4 { + let call = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| { + let S::Assert { condition, .. } = &mut statement.kind else { + return None; + }; + matches!(condition.kind, E::Call { .. }).then_some(condition) + }) + .unwrap(); + let E::Call { + ownership: hir::CallOwnership::Source(source), + .. + } = &mut call.kind + else { + unreachable!(); + }; + source.arguments[0].staging = hir::ArgumentStaging::Original; + } else { + let other = function.params[1].local; + let initializer = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + S::Let { local, value } if *local == owner => Some(value), + _ => None, + }) + .unwrap(); + let E::InterfaceCoerce { + value: snapshot, .. + } = &mut initializer.kind + else { + panic!("checked conversion"); + }; + let E::Clone(original) = &mut snapshot.kind else { + panic!("raw snapshot"); + }; + match mutation { + 0 => *snapshot = Box::new(original.as_ref().clone()), + 1 => { + *snapshot = Box::new(original.as_ref().clone()); + let original_conversion = initializer.clone(); + initializer.kind = E::Clone(Box::new(original_conversion)); + } + 2 => snapshot.ty = TypeInterner::INT64, + 3 => original.kind = E::Local(other), + _ => unreachable!(), + } + } + let error = validate_function(&program, observed_property(&program), &types) + .expect_err("malformed observation snapshot must fail before preparation"); + let expected = match mutation { + 0 | 2 => "observation lost its exact raw snapshot occurrence", + 1 => "observation cannot snapshot an erased or unsupported value", + 3 => "binding does not match its actual typed operand", + 4 => "requires explicit owning staging", + _ => unreachable!(), + }; + assert!(error.contains(expected), "{mutation}: {error}"); + } +} + +// Checked predicate identities and nominal types are taken from real source. +// The extra function mirrors native_property_cases' compiler-owned replay call. +const GENERATED_REFINEMENT_REPLAY_SOURCE: &str = r#"namespace app +type Positive = int64 where value > 0 +type Higher = Positive where value > 1 +type Nonempty = list[int64] where true +property refined: + given number: Higher + given items: Nonempty + assert true +function number() returns Higher: + return 2 +function items() returns Nonempty: + return list(2) +"#; + +fn generated_refinement_program(default: bool) -> (Program, TypeInterner) { + let file = jett_common::FileId::new(0); + let parsed = jett_parser::parse(GENERATED_REFINEMENT_REPLAY_SOURCE, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + 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 mut high = hir::lower_with_test_bodies( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, jett_common::SourceOrigin::Project)]), + ) + .expect("checked refinement HIR"); + let property_declaration = parsed + .module + .items + .iter() + .find_map(|item| match item { + jett_parser::ast::Item::Property(property) if property.name.name == "refined" => { + Some(property) + } + _ => None, + }) + .expect("exact parsed property"); + let targets = high + .functions + .iter() + .filter(|function| { + function.identity.declaration.kind == hir::DeclarationKind::Property + && function.span == property_declaration.span + && function.identity.declaration.name + == format!( + "{}:{}", + property_declaration.name.name, property_declaration.span.start + ) + }) + .collect::>(); + let [property] = targets.as_slice() else { + panic!("one exact checked property target"); + }; + let property = (*property).clone(); + let args = ["number", "items"] + .into_iter() + .map(|name| { + let function = high + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .expect("checked candidate"); + let hir::StatementKind::Return(Some(mut candidate)) = + function.body.statements[0].kind.clone() + else { + panic!("checked return guard"); + }; + let E::Handle { + error_local, + failure, + .. + } = &mut candidate.kind + else { + panic!("checked nominal refinement handle"); + }; + *error_local = None; + let failure_span = failure.span; + let body = if default { + let mut fallback = candidate.clone(); + fn occurrence(value: &mut Expression, span: Span) { + value.span = span; + match &mut value.kind { + E::ListConstruct { elements } => { + for value in elements { + occurrence(value, span); + } + } + E::Handle { + target, + error_local, + failure, + .. + } => { + occurrence(target, span); + *error_local = None; + *failure = hir::Block { + statements: vec![hir::Statement { + kind: hir::StatementKind::Return(None), + span, + }], + span, + }; + } + _ => {} + } + } + occurrence(&mut fallback, failure_span); + hir::StatementKind::HandleDefault(fallback) + } else { + hir::StatementKind::Return(None) + }; + let E::Handle { failure, .. } = &mut candidate.kind else { + unreachable!(); + }; + *failure = hir::Block { + statements: vec![hir::Statement { + kind: body, + span: failure_span, + }], + span: failure_span, + }; + candidate + }) + .collect::>(); + let parameters = property + .params + .iter() + .map(|parameter| parameter.ty) + .collect::>(); + let order = vec![0, 1]; + let ownership = hir::CallOwnership::generated( + hir::GeneratedOperation::NativeSuite { + function: property.id, + }, + &args, + ¶meters, + &[jett_typecheck::CheckedCalleeAccess::Owned; 2], + TypeInterner::NOTHING, + &order, + &checked.interner, + ) + .expect("exact replay factory"); + let span = property.span; + let call = Expression { + kind: E::Call { + function: property.id, + args, + evaluation_order: order, + ownership, + }, + ty: TypeInterner::NOTHING, + span, + }; + let mut identity = property.identity.clone(); + identity.declaration.name = "exercise".into(); + let id = hir::FunctionId::new(high.functions.len() as u32); + high.functions.push(hir::Function { + id, + identity, + debug_kind: hir::FunctionDebugKind::named("app", "exercise"), + source_definition: None, + params: Vec::new(), + capture_count: 0, + return_type: TypeInterner::NOTHING, + locals: Vec::new(), + body: hir::Block { + statements: vec![ + hir::Statement { + kind: hir::StatementKind::Expression(call), + span, + }, + hir::Statement { + kind: hir::StatementKind::Return(None), + span, + }, + ], + span, + }, + span, + }); + hir::validate_backend_types(&high, &checked.interner) + .expect("original immutable replay candidates"); + let program = crate::lower(&high, &checked.interner).expect("canonical refinement replay MIR"); + (program, checked.interner) +} + +#[test] +fn caller_acquisition_generated_refinement_replay_keeps_exact_chain_and_terminal_failure() { + let (program, types) = generated_refinement_program(false); + let function = exercise(&program); + let acquisitions = validate_function(&program, function, &types) + .expect("ordered checked refinement candidates"); + assert_eq!( + acquisitions.owner_initializers().count(), + 0, + "generated candidates do not consume source bindings" + ); + let mut checks = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| match &statement.kind { + S::CheckRefinement { type_name, .. } => Some(type_name.as_str()), + _ => None, + }) + .collect::>(); + checks.sort_unstable(); + assert_eq!(checks, ["app.Higher", "app.Nonempty", "app.Positive"]); + assert_eq!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| matches!( + statement.kind, + S::Let { + value: Expression { + kind: E::RefinementValidated(_), + .. + }, + .. + } + )) + .count(), + 2 + ); + assert!( + function + .blocks + .iter() + .any(|block| matches!(block.terminator.kind, T::Return(None))) + ); +} + +#[test] +fn caller_acquisition_generated_refinement_defaults_and_preparation_preserve_owned_candidates() { + let (mut program, types) = generated_refinement_program(true); + validate_function(&program, exercise(&program), &types) + .expect("defaults remain inside the exact failure body"); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::prepare_native_generated_functions(&mut program, &types); + crate::prepare_native_sequences(&mut program, &types); + crate::validate_call_ownership(&program, &types) + .expect("refinement source and predicate identities survive canonical preparation"); +} + +#[test] +fn caller_acquisition_generated_refinement_rejects_changed_predicates_candidate_and_edges() { + let (original, types) = generated_refinement_program(false); + for mutation in 0..8 { + let mut program = original.clone(); + let other_predicate = program + .functions + .iter() + .find(|function| { + function.identity.declaration.name == "Higher" + && function.identity.declaration.kind + == hir::DeclarationKind::RefinementPredicate + }) + .expect("distinct checked predicate") + .id; + let function = exercise_mut(&mut program); + let (block_index, check_index) = function + .blocks + .iter() + .enumerate() + .find_map(|(block_index, block)| { + block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::CheckRefinement { .. })) + .map(|index| (block_index, index)) + }) + .expect("first exact check"); + match mutation { + 0 => { + let S::CheckRefinement { type_name, .. } = + &mut function.blocks[block_index].statements[check_index].kind + else { + unreachable!(); + }; + *type_name = "app.Higher".into(); + } + 1 => { + let S::CheckRefinement { + call: + Expression { + kind: E::Call { args, .. }, + .. + }, + .. + } = &mut function.blocks[block_index].statements[check_index].kind + else { + unreachable!(); + }; + let E::Clone(inner) = &mut args[0].kind else { + panic!("canonical input clone"); + }; + inner.kind = E::Int(9); + } + 2 => { + let S::Let { value, .. } = + &mut function.blocks[block_index].statements[check_index + 1].kind + else { + panic!("passed local"); + }; + value.kind = E::Bool(true); + } + 3 => { + let T::Branch { + then_block, + else_block, + .. + } = &mut function.blocks[block_index].terminator.kind + else { + panic!("exact branch"); + }; + std::mem::swap(then_block, else_block); + } + 4 => { + let T::Branch { else_block, .. } = function.blocks[block_index].terminator.kind + else { + panic!("exact branch"); + }; + function.blocks[else_block.index() as usize].terminator.kind = T::Return(None); + } + 5 => { + let T::Branch { then_block, .. } = function.blocks[block_index].terminator.kind + else { + panic!("exact branch"); + }; + function.blocks[block_index].terminator.kind = T::Goto(then_block); + } + 6 => { + let success = function + .blocks + .iter_mut() + .find(|block| { + block.statements.iter().any(|statement| { + matches!( + statement.kind, + S::Let { + value: Expression { + kind: E::RefinementValidated(_), + .. + }, + .. + } + ) + }) + }) + .expect("validated output"); + success.statements.push(success.statements[0].clone()); + } + _ => { + let S::CheckRefinement { + call: + Expression { + kind: E::Call { function, .. }, + .. + }, + .. + } = &mut function.blocks[block_index].statements[check_index].kind + else { + unreachable!(); + }; + *function = other_predicate; + } + } + generated_rejects(&program, &types); + } +} + +#[test] +fn caller_acquisition_generated_refinement_rejects_default_failure_bypass_and_borrowed_output() { + let (original, types) = generated_refinement_program(true); + let (output, success) = exercise(&original) + .blocks + .iter() + .find_map(|block| { + block + .statements + .iter() + .find_map(|statement| match statement.kind { + S::Let { + local, + value: + Expression { + kind: E::RefinementValidated(_), + .. + }, + } => Some((local, block.id)), + _ => None, + }) + }) + .expect("first validated output"); + for borrowed in [false, true] { + let mut program = original.clone(); + let function = exercise_mut(&mut program); + let default = function + .blocks + .iter() + .find(|block| { + block.id != success && block.statements.iter().any(|statement| + matches!(statement.kind, S::Let { local, .. } if local == output)) + }) + .expect("exact default block") + .id; + if borrowed { + let S::Let { value, .. } = function.blocks[default.index() as usize] + .statements + .iter_mut() + .find(|statement| matches!(statement.kind, S::Let { local, .. } if local == output)) + .map(|statement| &mut statement.kind) + .expect("default owning Let") + else { + unreachable!(); + }; + *value = Expression { + kind: E::View(Box::new(value.clone())), + ty: value.ty, + span: value.span, + }; + } else { + let block = function + .blocks + .iter_mut() + .find(|block| { + block + .statements + .iter() + .any(|statement| matches!(statement.kind, S::CheckRefinement { .. })) + }) + .expect("selecting predicate"); + let T::Branch { else_block, .. } = &mut block.terminator.kind else { + panic!("canonical selecting Branch"); + }; + *else_block = default; + } + generated_rejects(&program, &types); + } +} + +const GENERATED_REFINEMENT_LOOP_DEFAULT_SOURCE: &str = r#"namespace app +type Nonempty = list[int64] where true +property refined: + given items: Nonempty + assert true +function candidate() returns Nonempty: + return list(2) +function loop_default(view values: list[secret[Nonempty]]) returns Nonempty: + for item in view values: + return clone declassify item + return list(2) +"#; + +fn generated_refinement_loop_default_program() -> (Program, TypeInterner) { + let file = jett_common::FileId::new(0); + let parsed = jett_parser::parse(GENERATED_REFINEMENT_LOOP_DEFAULT_SOURCE, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + 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 mut high = hir::lower_with_test_bodies( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, jett_common::SourceOrigin::Project)]), + ) + .expect("checked viewed-loop HIR"); + let property_span = parsed + .module + .items + .iter() + .find_map(|item| match item { + jett_parser::ast::Item::Property(property) if property.name.name == "refined" => { + Some(property.span) + } + _ => None, + }) + .expect("exact parsed property"); + let targets = high + .functions + .iter() + .filter(|function| { + function.span == property_span + && function.identity.declaration.kind == hir::DeclarationKind::Property + && function.identity.declaration.name == format!("refined:{}", property_span.start) + }) + .collect::>(); + let [property] = targets.as_slice() else { + panic!("one exact checked property"); + }; + let property = (*property).clone(); + let fallback = high + .functions + .iter() + .find(|function| function.identity.declaration.name == "loop_default") + .expect("source checked loop") + .clone(); + let original = high + .functions + .iter() + .find(|function| function.identity.declaration.name == "candidate") + .expect("checked refinement candidate"); + let hir::StatementKind::Return(Some(mut candidate)) = original.body.statements[0].kind.clone() + else { + panic!("refinement guard"); + }; + let mut iteration = fallback.body.statements[0].clone(); + let hir::StatementKind::For { body, .. } = &mut iteration.kind else { + panic!("source viewed For"); + }; + let hir::StatementKind::Return(Some(value)) = body.statements[0].kind.clone() else { + panic!("source checked explicit Clone"); + }; + body.statements[0].kind = hir::StatementKind::HandleDefault(value); + let failure_span = iteration.span; + let E::Handle { + error_local, + failure, + .. + } = &mut candidate.kind + else { + panic!("exact candidate Handle"); + }; + *error_local = None; + *failure = hir::Block { + statements: vec![ + iteration, + hir::Statement { + kind: hir::StatementKind::Return(None), + span: failure_span, + }, + ], + span: failure_span, + }; + let args = vec![candidate]; + let order = vec![0]; + let ownership = hir::CallOwnership::generated( + hir::GeneratedOperation::NativeSuite { + function: property.id, + }, + &args, + &[property.params[0].ty], + &[jett_typecheck::CheckedCalleeAccess::Owned], + TypeInterner::NOTHING, + &order, + &checked.interner, + ) + .expect("exact replay factory"); + let span = property.span; + let call = Expression { + kind: E::Call { + function: property.id, + args, + evaluation_order: order, + ownership, + }, + ty: TypeInterner::NOTHING, + span, + }; + let mut identity = property.identity.clone(); + identity.declaration.name = "exercise".into(); + high.functions.push(hir::Function { + id: hir::FunctionId::new(high.functions.len() as u32), + identity, + debug_kind: hir::FunctionDebugKind::named("app", "exercise"), + source_definition: None, + params: fallback.params, + capture_count: 0, + return_type: TypeInterner::NOTHING, + locals: fallback.locals, + body: hir::Block { + statements: vec![ + hir::Statement { + kind: hir::StatementKind::Expression(call), + span, + }, + hir::Statement { + kind: hir::StatementKind::Return(None), + span, + }, + ], + span, + }, + span, + }); + hir::validate_backend_types(&high, &checked.interner) + .expect("checked explicit Clone default preserves loop view"); + ( + crate::lower(&high, &checked.interner).expect("canonical replay with viewed-loop default"), + checked.interner, + ) +} + +#[test] +fn caller_acquisition_generated_refinement_defaults_distinguish_logical_loop_borrows_from_clone() { + let (original, types) = generated_refinement_loop_default_program(); + validate_function(&original, exercise(&original), &types) + .expect("explicit ordinary-data Clone acquires the default"); + let function = exercise(&original); + let (binding, body) = function + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + T::ForEach { + key, + value: None, + body, + .. + } => Some((*key, *body)), + _ => None, + }) + .expect("original viewed loop binding"); + assert!( + !function.is_view_local(binding), + "loop backing is distinct from ABI/persistent local views" + ); + let target_type = match types.resolve(function.local(binding).unwrap().ty) { + Type::Secret(inner) => *inner, + _ => panic!("exact Secret loop endpoint"), + }; + for wrapped in [false, true] { + let mut program = original.clone(); + let function = exercise_mut(&mut program); + let block = &mut function.blocks[body.index() as usize]; + let value = block + .statements + .iter_mut() + .find_map(|statement| match &mut statement.kind { + S::Let { value, .. } + if value.ty == target_type && matches!(value.kind, E::Clone(_)) => + { + Some(value) + } + _ => None, + }) + .expect("source explicit Clone default"); + let mut borrowed = Expression { + kind: E::Local(binding), + ty: function.locals[binding.index() as usize].ty, + span: value.span, + }; + if wrapped { + borrowed = Expression { + kind: E::View(Box::new(borrowed)), + ty: function.locals[binding.index() as usize].ty, + span: value.span, + }; + } + *value = Expression { + kind: E::Declassify(Box::new(borrowed)), + ty: target_type, + span: value.span, + }; + let error = generated_rejects(&program, &types); + assert!( + error.contains("default lost its exact owned failure occurrence"), + "{error}" + ); + } +} + +fn source_sum_loop_default(result: bool, qualified: bool) -> String { + let mut source = String::from("namespace app\n"); + if qualified { + source.push_str("type Nonempty = list[int64] where true\n"); + } + source.push_str("function count(view values: list[int64]) returns int64:\n return 1\n"); + if result { + source.push_str("function candidate() returns result[list[int64], string]:\n return fail(\"missing\")\n"); + } else { + source.push_str("function candidate() returns optional[list[int64]]:\n return none\n"); + } + let container = if qualified { + "list[secret[Nonempty]]" + } else { + "list[list[int64]]" + }; + let handler = if result { "handle error" } else { "handle" }; + let default = if qualified { + "clone coarsen declassify item" + } else { + "clone item" + }; + source.push_str(&format!("function exercise(view values: {container}) returns int64:\n for item in view values:\n return count(candidate() {handler}:\n default {default}\n )\n return 0\n")); + source +} + +fn source_sum_loop_default_ids(function: &Function) -> (LocalId, LocalId) { + let binding = function + .blocks + .iter() + .find_map(|block| match block.terminator.kind { + T::ForEach { + key, value: None, .. + } => Some(key), + _ => None, + }) + .expect("one original viewed loop binder"); + let outputs = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| match statement.kind { + S::SumTake { + target, + success: true, + .. + } => Some(target), + _ => None, + }) + .collect::>(); + let [output] = outputs.as_slice() else { + panic!("one exact inhabited sum success output"); + }; + (binding, *output) +} + +fn source_sum_loop_default_value(function: &mut Function, output: LocalId) -> &mut Expression { + function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + S::Let { local, value } if *local == output => Some(value), + _ => None, + }) + .expect("exact handled default definition") +} + +#[test] +fn caller_acquisition_source_sum_defaults_keep_explicit_clone_and_exact_sum_proofs() { + for result in [false, true] { + for qualified in [false, true] { + let (original, types) = source_program(&source_sum_loop_default(result, qualified)); + let function = exercise(&original); + let (binding, output) = source_sum_loop_default_ids(function); + assert!( + !function.is_view_local(binding), + "logical loop backing is not a persistent/ABI view slot" + ); + let target = original + .functions + .iter() + .find(|function| function.identity.declaration.name == "count") + .expect("checked count target") + .id; + let call = function.blocks.iter().flat_map(|block| &block.statements).find_map(|statement| match &statement.kind { + S::Let { value, .. } | S::Evaluate(value) if matches!(value.kind, E::Call { function, .. } if function == target) => Some(value), _ => None, + }).expect("exact staged count invocation"); + let E::Call { + ownership: hir::CallOwnership::Source(source), + .. + } = &call.kind + else { + panic!("real Source packet"); + }; + let hir::ArgumentStaging::Relinquished { owner, .. } = source.arguments[0].staging + else { + panic!("owned handled result to view formal"); + }; + let acquisitions = validate_function(&original, function, &types) + .expect("explicit Clone owns the sum default"); + assert_eq!( + acquisitions + .owner_initializer(owner) + .expect("validated handled owner") + .binding, + None + ); + assert!(function.blocks.iter().flat_map(|block| &block.statements).any(|statement| + matches!(&statement.kind, S::Let { local, value } if *local == output && matches!(value.kind, E::Clone(_))))); + let mut prepared = original.clone(); + crate::prepare_native_sequences(&mut prepared, &types); + crate::prepare_native_uninhabited_sums(&mut prepared, &types); + crate::prepare_native_generated_functions(&mut prepared, &types); + validate_function(&prepared, exercise(&prepared), &types) + .expect("canonical preparation preserves explicit owned defaults"); + assert!( + exercise(&prepared).prepared_absent_successes.is_empty(), + "inhabited sums gain no absence authority" + ); + } + } +} + +#[test] +fn caller_acquisition_source_sum_defaults_reject_logical_borrows_under_transparent_wrappers() { + for result in [false, true] { + for qualified in [false, true] { + let (original, types) = source_program(&source_sum_loop_default(result, qualified)); + let (binding, output) = source_sum_loop_default_ids(exercise(&original)); + let binding_ty = exercise(&original).local(binding).expect("loop local").ty; + let nominal = if qualified { + let Type::Secret(inner) = types.resolve(binding_ty) else { + panic!("exact Secret loop endpoint"); + }; + Some(*inner) + } else { + None + }; + for hidden_view in [false, true] { + let mut changed = original.clone(); + let value = source_sum_loop_default_value(exercise_mut(&mut changed), output); + let mut borrowed = Expression { + kind: E::Local(binding), + ty: binding_ty, + span: value.span, + }; + if hidden_view { + borrowed = Expression { + kind: E::View(Box::new(borrowed)), + ty: binding_ty, + span: value.span, + }; + } + if let Some(nominal) = nominal { + borrowed = Expression { + kind: E::Declassify(Box::new(borrowed)), + ty: nominal, + span: value.span, + }; + borrowed = Expression { + kind: E::Coarsen(Box::new(borrowed)), + ty: value.ty, + span: value.span, + }; + } + *value = borrowed; + let error = validate_function(&changed, exercise(&changed), &types) + .err() + .expect("borrowed default must fail"); + let expected = if hidden_view && !qualified { + "call ownership producer result contains an unsupported definition" + } else { + "call ownership handled default cannot acquire a borrowed owner" + }; + assert_eq!(error, expected); + if !hidden_view { + let mut prepared = original.clone(); + crate::prepare_native_sequences(&mut prepared, &types); + let function = exercise_mut(&mut prepared); + let prepared_binding = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match statement.kind { + S::SequenceGet { target, .. } + if function + .local(target) + .is_some_and(|local| local.ty == binding_ty) => + { + Some(target) + } + _ => None, + }) + .expect("canonical viewed element storage"); + let prepared_output = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match statement.kind { + S::SumTake { + target, + success: true, + .. + } => Some(target), + _ => None, + }) + .expect("retained inhabited success output"); + let value = source_sum_loop_default_value(function, prepared_output); + let mut borrowed = Expression { + kind: E::Local(prepared_binding), + ty: binding_ty, + span: value.span, + }; + if let Some(nominal) = nominal { + borrowed = Expression { + kind: E::Declassify(Box::new(borrowed)), + ty: nominal, + span: value.span, + }; + borrowed = Expression { + kind: E::Coarsen(Box::new(borrowed)), + ty: value.ty, + span: value.span, + }; + } + *value = borrowed; + let error = validate_function(&prepared, exercise(&prepared), &types) + .err() + .expect("prepared logical borrow must fail"); + assert_eq!( + error, + "call ownership handled default cannot acquire a borrowed owner" + ); + } + } + } + } +} + +const CONVERTED_HANDLED_SOURCE: &str = r#"namespace app +interface Show: + function show(view self: Show) returns string +implement Show for uint64: + function show(view self: uint64) returns string: + return "uint64" +function exercise(incoming: optional[uint64], spare: uint64) returns string: + uint64 fallback = 0 + return Show.show(view(incoming handle: + default fallback + )) +"#; + +const OWNED_CONVERTED_HANDLED_SOURCE: &str = r#"namespace app +interface Show: + function show(view self: Show) returns string +implement Show for uint64: + function show(view self: uint64) returns string: + return "uint64" +function read(value: Show) returns string: + return "received" +function exercise(incoming: optional[uint64], spare: uint64) returns string: + uint64 fallback = 0 + return read((incoming handle: + default fallback + )) +"#; + +fn converted_handle_stage(function: &Function) -> Option { + let E::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &read_call(function).kind + else { + panic!("checked converted source call"); + }; + match packet.arguments[0].staging { + hir::ArgumentStaging::Original => None, + hir::ArgumentStaging::Borrowed { loan } => Some(loan), + hir::ArgumentStaging::Transferred { owner } + | hir::ArgumentStaging::Relinquished { owner, .. } + | hir::ArgumentStaging::RetainedSnapshot { owner, .. } + | hir::ArgumentStaging::Observed { owner } => Some(owner), + hir::ArgumentStaging::Copied { value } => Some(value), + } +} + +fn converted_handle_operand(function: &Function) -> &Expression { + if let Some(stage) = converted_handle_stage(function) { + return function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + S::Let { local, value } | S::BeginCallView { local, value } if *local == stage => { + Some(value) + } + _ => None, + }) + .expect("exact source staging initializer"); + } + let E::Call { args, .. } = &read_call(function).kind else { + panic!("source read"); + }; + &args[0] +} + +fn converted_handle_operand_mut(function: &mut Function) -> &mut Expression { + if let Some(stage) = converted_handle_stage(function) { + return function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + S::Let { local, value } | S::BeginCallView { local, value } if *local == stage => { + Some(value) + } + _ => None, + }) + .expect("exact source staging initializer"); + } + for block in &mut function.blocks { + for statement in &mut block.statements { + if let S::Let { value, .. } | S::Evaluate(value) = &mut statement.kind + && let E::Call { args, .. } = &mut value.kind + { + return &mut args[0]; + } + } + if let T::Return(Some(value)) = &mut block.terminator.kind + && let E::Call { args, .. } = &mut value.kind + { + return &mut args[0]; + } + } + panic!("source read invocation"); +} + +fn converted_handle_leaf_mut(mut value: &mut Expression) -> &mut Expression { + while matches!( + value.kind, + E::View(_) | E::InterfaceCoerce { .. } | E::FunctionAdapter { .. } + ) { + value = match &mut value.kind { + E::View(inner) + | E::InterfaceCoerce { value: inner, .. } + | E::FunctionAdapter { value: inner, .. } => inner, + _ => unreachable!("transparent conversion checked before borrowing"), + }; + } + value +} + +#[test] +fn caller_acquisition_converted_handle_joins_exact_inner_result_and_full_cfg() { + let (program, types) = source_program(CONVERTED_HANDLED_SOURCE); + let function = exercise(&program); + let E::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &read_call(function).kind + else { + panic!("source read"); + }; + let argument = &packet.arguments[0]; + let backing = hir::validate_source_handled_operand( + converted_handle_operand(function), + argument, + &packet.bridge, + &types, + ) + .expect("exact conversion") + .expect("private original Handle backing"); + let E::Local(local) = backing.kind else { + panic!("lowered Handle result"); + }; + assert_eq!(backing.ty, TypeInterner::UINT64); + assert_ne!(backing.span, argument.source_span); + assert_ne!(local, function.params[1].local); + assert!(function.blocks.iter().flat_map(|block| &block.statements).any(|statement| + matches!(statement.kind, S::SumTake { target, success: true, .. } if target == local))); + validate_function(&program, function, &types).expect("original result and complete CFG proofs"); + // A written View of a statically selected method retains the inner source + // span. A separate owning interface formal exercises actual conversion. + let (converted, converted_types) = source_program(OWNED_CONVERTED_HANDLED_SOURCE); + let converted_function = exercise(&converted); + let E::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &read_call(converted_function).kind + else { + panic!("owning interface source call"); + }; + let operand = converted_handle_operand(converted_function); + assert!(matches!(operand.kind, E::InterfaceCoerce { .. })); + let backing = hir::validate_source_handled_operand( + operand, + &packet.arguments[0], + &packet.bridge, + &converted_types, + ) + .expect("actual finite conversion") + .expect("private original Handle"); + assert!(matches!(backing.kind, E::Local(_))); + assert_eq!(backing.ty, TypeInterner::UINT64); + assert_ne!(operand.ty, backing.ty); + validate_function(&converted, converted_function, &converted_types) + .expect("converted result CFG"); +} + +#[test] +fn caller_acquisition_converted_handle_refuses_foreign_occurrences_and_unproved_slots() { + let (program, types) = source_program(CONVERTED_HANDLED_SOURCE); + for mutation in [ + "inner span", + "inner type", + "parameter substitution", + "outer source span", + "sum occurrence", + "failed tag", + ] { + let mut corrupted = program.clone(); + let function = exercise_mut(&mut corrupted); + let spare = function.params[1].local; + let backing = converted_handle_leaf_mut(converted_handle_operand_mut(function)); + let E::Local(output) = backing.kind else { + panic!("original CFG result"); + }; + match mutation { + "inner span" => backing.span.start += 1, + "inner type" => backing.ty = TypeInterner::INT64, + "parameter substitution" => backing.kind = E::Local(spare), + "outer source span" => converted_handle_operand_mut(function).span.start += 1, + "sum occurrence" | "failed tag" => { + let statement = function.blocks.iter_mut().flat_map(|block| &mut block.statements) + .find(|statement| matches!(statement.kind, S::SumTake { target, success: true, .. } if target == output)) + .expect("exact handled success"); + if mutation == "sum occurrence" { + statement.span.start += 1; + } else { + let S::SumTake { success, .. } = &mut statement.kind else { + unreachable!() + }; + *success = false; + } + } + _ => unreachable!(), + } + rejects(&corrupted, &types); + } + let (mut converted, converted_types) = source_program(OWNED_CONVERTED_HANDLED_SOURCE); + let original = converted_handle_operand_mut(exercise_mut(&mut converted)); + let E::InterfaceCoerce { value, .. } = &original.kind else { + panic!("actual conversion"); + }; + original.kind = value.kind.clone(); + rejects(&converted, &converted_types); +} + +const CONVERTED_VIEW_OWNER_SOURCE: &str = r#"namespace app +interface Named: + function name(view self: Named) returns string +struct Boxed[T]: + value: T +implement Named for Boxed[int8]: + function name(view self: Boxed[int8]) returns string: + return "narrow" +function read(view value: Named) returns int64: + return 7 +function exercise() returns int64: + return read(Boxed[int8](value: 5)) +"#; + +#[test] +fn caller_acquisition_converted_view_producer_owns_the_complete_endpoint_once() { + let (program, types) = source_program(CONVERTED_VIEW_OWNER_SOURCE); + let function = exercise(&program); + let call = read_call(function); + let E::Call { + ownership: hir::CallOwnership::Source(packet), + args, + .. + } = &call.kind + else { + panic!("checked Source call"); + }; + let argument = &packet.arguments[0]; + assert_eq!(argument.syntax, jett_typecheck::CheckedCallerSyntax::Bare); + assert_eq!(argument.effect, CheckedCallerEffect::RelinquishOwned); + assert_eq!( + argument.callee_access, + jett_typecheck::CheckedCalleeAccess::View + ); + assert_eq!( + argument.physical_access, + jett_typecheck::CheckedCalleeAccess::View + ); + assert!(matches!( + argument.origin, + hir::CallerOrigin::OwnedExpression + )); + assert!(matches!( + types.resolve(argument.actual_type), + Type::Struct(_) + )); + let (owner, loan) = owning_stage(function); + assert_ne!(owner, loan); + let initializer = observation_initializer(function, owner); + let E::InterfaceCoerce { value: raw, .. } = &initializer.kind else { + panic!("owning storage contains the full checked conversion"); + }; + assert!(matches!(raw.kind, E::StructConstruct { .. })); + assert_eq!(raw.ty, argument.actual_type); + assert_eq!(raw.span, argument.source_span); + assert_ne!(initializer.ty, raw.ty); + assert!(matches!(types.resolve(initializer.ty), Type::Interface(_))); + assert!(!crate::handlers::can_snapshot_view(&types, initializer.ty)); + let original_view = Expression { + kind: E::View(Box::new(initializer.clone())), + ty: initializer.ty, + span: initializer.span, + }; + assert!( + crate::call_views::temporary_root(&types, &function.locals, &original_view).is_none(), + "the converted endpoint does not acquire raw-temporary borrowing permission" + ); + assert!( + matches!(&args[0].kind, E::View(value) if matches!(value.kind, E::Local(id) if id == loan)) + ); + let owner_metadata = function.local(owner).expect("independent owning slot"); + assert_eq!(owner_metadata.ty, initializer.ty); + assert_eq!(owner_metadata.debug_ty, initializer.ty); + assert_eq!(owner_metadata.view_source, None); + assert_eq!(function.local(loan).unwrap().view_source, Some(owner)); + let block = function + .blocks + .iter() + .find(|block| { + block.statements.iter().any(|statement| + matches!(&statement.kind, S::Let { value, .. } if std::ptr::eq(value, call))) + }) + .expect("materialized consuming operation"); + let owner_at = block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::Let { local, .. } if local == owner)) + .expect("one full owner"); + let begin_at = block + .statements + .iter() + .position( + |statement| matches!(statement.kind, S::BeginCallView { local, .. } if local == loan), + ) + .expect("loan begins"); + let call_at = block.statements.iter().position(|statement| + matches!(&statement.kind, S::Let { value, .. } if std::ptr::eq(value, call))).unwrap(); + let end_at = block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::EndCallView { local } if local == loan)) + .expect("loan ends"); + assert!(owner_at < begin_at && begin_at < call_at && call_at < end_at); + assert_eq!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| matches!(statement.kind, S::Let { local, .. } if local == owner)) + .count(), + 1 + ); + let acquisitions = + validate_function(&program, function, &types).expect("exact owning conversion and loan"); + assert_eq!(acquisitions.owner_initializers().count(), 1); + let acquired = acquisitions.owner_initializer(owner).unwrap(); + assert_eq!(acquired.binding, None); + assert_eq!(acquired.effect, CheckedCallerEffect::RelinquishOwned); + assert_eq!(acquired.actual_type, raw.ty); + assert_eq!(acquired.storage_type, initializer.ty); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("ordinary owning storage and bounded View scope"); +} + +#[test] +fn caller_acquisition_converted_owner_preflight_preserves_written_borrow_paths() { + let written_producer = CONVERTED_VIEW_OWNER_SOURCE.replace( + "return read(Boxed[int8](value: 5))", + "return read(view Boxed[int8](value: 5))", + ); + let (program, types) = source_program(&written_producer); + let function = exercise(&program); + let E::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &read_call(function).kind + else { + panic!("written producer Source call"); + }; + let argument = &packet.arguments[0]; + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + assert_eq!(argument.staging, hir::ArgumentStaging::Original); + assert_eq!( + validate_function(&program, function, &types) + .expect("original written producer") + .owner_initializers() + .count(), + 0 + ); + assert!( + !function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!(statement.kind, S::BeginCallView { .. })), + "no owning-stage permission from written View" + ); + + let source = r#"namespace app +interface Named: + function name(view self: Named) returns string +function read(view value: Named, extra: int64) returns int64: + return extra +function exercise(view value: Named, incoming: optional[int64]) returns int64: + return read(view value, incoming handle: + default 3 + ) +"#; + let (program, types) = source_program(source); + let function = exercise(&program); + let E::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &read_call(function).kind + else { + panic!("written parameter Source call"); + }; + let argument = &packet.arguments[0]; + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + let hir::ArgumentStaging::Borrowed { loan } = argument.staging else { + panic!("retained ABI parameter stages as a borrow"); + }; + assert_eq!( + function.local(loan).unwrap().view_source, + Some(function.params[0].local) + ); + assert_eq!( + function + .local(function.params[0].local) + .unwrap() + .view_source, + None + ); + assert_eq!( + validate_function(&program, function, &types) + .expect("existing written parameter borrow") + .owner_initializers() + .count(), + 0 + ); +} + +#[test] +fn caller_acquisition_converted_view_owner_keeps_original_and_owned_slot_guards() { + let (program, types) = source_program(CONVERTED_VIEW_OWNER_SOURCE); + let (owner, loan) = owning_stage(exercise(&program)); + for mutation in [ + "changed effect", + "missing stage", + "borrowed owner", + "borrowed initializer", + "missing initializer", + "missing conversion", + "foreign loan owner", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + match mutation { + "changed effect" | "missing stage" | "borrowed owner" => { + let E::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &mut read_call_mut(function).kind + else { + panic!("Source converted call"); + }; + match mutation { + "changed effect" => { + packet.arguments[0].effect = CheckedCallerEffect::RetainBorrow + } + "missing stage" => packet.arguments[0].staging = hir::ArgumentStaging::Original, + "borrowed owner" => { + packet.arguments[0].staging = + hir::ArgumentStaging::Relinquished { owner: loan, loan } + } + _ => unreachable!(), + } + } + "borrowed initializer" | "missing initializer" | "missing conversion" => { + let (block, index) = function.blocks.iter().enumerate().find_map(|(block, body)| + body.statements.iter().position(|statement| + matches!(statement.kind, S::Let { local, .. } if local == owner)).map(|index| (block, index))) + .expect("original full owning initializer"); + if mutation == "missing initializer" { + function.blocks[block].statements.remove(index); + } else { + let S::Let { value, .. } = &mut function.blocks[block].statements[index].kind + else { + unreachable!(); + }; + if mutation == "missing conversion" { + let E::InterfaceCoerce { value: raw, .. } = &value.kind else { + panic!("original full owning conversion"); + }; + value.kind = raw.kind.clone(); + } else { + value.kind = E::View(Box::new(value.clone())); + } + } + } + "foreign loan owner" => function.locals[loan.index() as usize].view_source = None, + _ => unreachable!(), + } + let expected = match mutation { + "changed effect" => "call ownership disagrees with its original source witness", + "missing stage" => { + "call ownership requires explicit owning staging at this physical boundary" + } + "borrowed owner" => "relinquished call owner cannot be a borrowed slot", + "borrowed initializer" => { + "call ownership owner initializer changes type, occurrence or ownership" + } + "missing initializer" => "call ownership acquired slot requires one unique initializer", + "missing conversion" => { + "call ownership current operand has no typed conversion from the checked actual" + } + "foreign loan owner" => "call view has no borrowed origin", + _ => unreachable!(), + }; + assert_eq!( + validate_function(&changed, exercise(&changed), &types) + .expect_err("original witness and owning storage remain mandatory"), + expected, + "{mutation}" + ); + } +} + +const RETAINED_SNAPSHOT_DIRECT: &str = r#"namespace app +function read(view values: list[int64], extra: int64) returns int64: + mutable int64 total = extra + for value in view values: + total = total + value + return total +function exercise(incoming: optional[int64]) returns int64: + mutable list[int64] values = list(3) + list[int64] spare = list(9) + int64 answer = read(view values, incoming handle: + values = list(8) + default 2 + ) + int64 after = read(view values, 0) + return answer + after +"#; + +const RETAINED_SNAPSHOT_PROJECTED: &str = r#"namespace app +struct Store: + items: list[int64] +function read(view values: list[int64], extra: int64) returns int64: + mutable int64 total = extra + for value in view values: + total = total + value + return total +function exercise(incoming: optional[int64]) returns int64: + mutable Store store = Store(items: list(3)) + int64 answer = read(view store.items, incoming handle: + store = Store(items: list(8)) + default 2 + ) + int64 after = read(view store.items, 0) + return answer + after +"#; + +fn retained_snapshot_stage(function: &Function) -> (LocalId, LocalId) { + let ownership = match &read_call(function).kind { + E::Call { ownership, .. } | E::IndirectCall { ownership, .. } => ownership, + _ => panic!("checked source invocation"), + }; + let hir::CallOwnership::Source(source) = ownership else { + panic!("Source packet"); + }; + let argument = &source.arguments[0]; + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + let hir::ArgumentStaging::RetainedSnapshot { owner, loan } = argument.staging else { + panic!("independent retained snapshot"); + }; + assert_ne!(owner, loan); + (owner, loan) +} + +fn retained_snapshot_initializer(function: &Function, owner: LocalId) -> &Expression { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + S::Let { local, value } if *local == owner => Some(value), + _ => None, + }) + .expect("unique retained owner initializer") +} + +fn retained_snapshot_initializer_mut(function: &mut Function, owner: LocalId) -> &mut Expression { + function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + S::Let { local, value } if *local == owner => Some(value), + _ => None, + }) + .expect("unique retained owner initializer") +} + +fn retained_snapshot_error(program: &Program, types: &TypeInterner) -> String { + validate_function(program, exercise(program), types).expect_err("corrupted retained snapshot") +} + +#[test] +fn caller_acquisition_retained_snapshots_capture_direct_and_projected_endpoint_before_rebinding() { + for (source, projected) in [ + (RETAINED_SNAPSHOT_DIRECT, false), + (RETAINED_SNAPSHOT_PROJECTED, true), + ] { + let (program, types) = source_program(source); + let function = exercise(&program); + let (owner, loan) = retained_snapshot_stage(function); + let acquisitions = + validate_function(&program, function, &types).expect("retained snapshot proof"); + assert_eq!( + acquisitions.owner_initializers().count(), + 0, + "caller ownership is retained" + ); + assert!( + acquisitions + .arguments(read_call(function)) + .expect("exact call") + .is_empty() + ); + assert_eq!(function.local(loan).expect("loan").view_source, Some(owner)); + assert!( + function + .local(owner) + .expect("snapshot") + .view_source + .is_none() + ); + let initializer = retained_snapshot_initializer(function, owner); + let E::Clone(endpoint) = &initializer.kind else { + panic!("full endpoint snapshot"); + }; + let E::View(endpoint) = &endpoint.kind else { + panic!("original written view"); + }; + assert_eq!(matches!(endpoint.kind, E::Field { .. }), projected); + assert_eq!(matches!(endpoint.kind, E::Local(_)), !projected); + let owner_block = function + .blocks + .iter() + .find(|block| { + block.statements.iter().any( + |statement| matches!(statement.kind, S::Let { local, .. } if local == owner), + ) + }) + .expect("snapshot block"); + let tag_block = function + .blocks + .iter() + .find(|block| { + block + .statements + .iter() + .any(|statement| matches!(statement.kind, S::SumTag { .. })) + }) + .expect("later handled argument"); + assert_eq!(owner_block.id, tag_block.id); + let owner_index = owner_block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::Let { local, .. } if local == owner)) + .expect("owner position"); + let begin_index = owner_block.statements.iter().position(|statement| + matches!(statement.kind, S::BeginCallView { local, .. } if local == loan)).expect("loan position"); + let tag_index = owner_block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::SumTag { .. })) + .expect("tag position"); + assert!(owner_index < begin_index && begin_index < tag_index); + assert!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any( + |statement| matches!(statement.kind, S::EndCallView { local } if local == loan) + ) + ); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("rebind does not free the independent snapshot loan"); + } +} + +#[test] +fn caller_acquisition_retained_snapshot_indirect_and_abort_keep_exact_scope_cleanup() { + let indirect = RETAINED_SNAPSHOT_DIRECT.replace( + " int64 answer = read(view values,", + " function(view list[int64], int64) returns int64 callback = read\n int64 answer = callback(view values,", + ); + let abort = RETAINED_SNAPSHOT_DIRECT.replace(" default 2", " return 11"); + for (source, indirect) in [(&indirect, true), (&abort, false)] { + let (program, types) = source_program(source); + let function = exercise(&program); + let (_, loan) = retained_snapshot_stage(function); + assert_eq!( + matches!(read_call(function).kind, E::IndirectCall { .. }), + indirect + ); + validate_function(&program, function, &types).expect("call and aborted-call scopes"); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("retained owner and independent snapshot cleanup"); + if !indirect { + let returned = function.blocks.iter().find(|block| + matches!(&block.terminator.kind, T::Return(Some(value)) if matches!(value.kind, E::Int(11)))) + .expect("original early handler return"); + assert!(returned.statements.iter().any( + |statement| matches!(statement.kind, S::EndCallView { local } if local == loan) + )); + } + } +} + +#[test] +fn caller_acquisition_retained_snapshot_rejects_removed_clone_and_changed_source_occurrence() { + let (program, types) = source_program(RETAINED_SNAPSHOT_DIRECT); + let (owner, _) = retained_snapshot_stage(exercise(&program)); + for mutation in [ + "remove clone", + "span", + "endpoint type", + "different root", + "effect", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + if mutation == "effect" { + let E::Call { + ownership: hir::CallOwnership::Source(source), + .. + } = &mut read_call_mut(function).kind + else { + panic!("source call"); + }; + source.arguments[0].effect = CheckedCallerEffect::RelinquishOwned; + } else { + let other = function + .locals + .iter() + .find(|local| local.name == "spare") + .expect("same-type unused owner") + .id; + let initializer = retained_snapshot_initializer_mut(function, owner); + match mutation { + "remove clone" => { + let E::Clone(endpoint) = &initializer.kind else { + panic!("snapshot"); + }; + *initializer = endpoint.as_ref().clone(); + } + "span" => initializer.span.start += 1, + "endpoint type" => { + let E::Clone(endpoint) = &mut initializer.kind else { + panic!("snapshot"); + }; + endpoint.ty = TypeInterner::BOOL; + } + "different root" => { + let E::Clone(endpoint) = &mut initializer.kind else { + panic!("snapshot"); + }; + let E::View(endpoint) = &mut endpoint.kind else { + panic!("written view"); + }; + endpoint.kind = E::Local(other); + } + _ => unreachable!(), + } + } + let error = retained_snapshot_error(&changed, &types); + assert!( + error.contains("snapshot") || error.contains("call ownership"), + "{mutation}: {error}" + ); + } +} + +#[test] +fn caller_acquisition_retained_snapshot_rejects_loan_substitution_missing_end_and_late_capture() { + let (program, types) = source_program(RETAINED_SNAPSHOT_DIRECT); + let (owner, loan) = retained_snapshot_stage(exercise(&program)); + for mutation in [ + "loan source", + "missing end", + "late capture", + "aliased owner", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + match mutation { + "loan source" => { + function.locals[loan.index() as usize].view_source = Some(function.params[0].local) + } + "aliased owner" => { + function.locals[owner.index() as usize].view_source = Some(function.params[0].local) + } + "missing end" => { + for block in &mut function.blocks { + block.statements.retain(|statement| !matches!(statement.kind, S::EndCallView { local } if local == loan)); + } + } + "late capture" => { + let block = function + .blocks + .iter_mut() + .find(|block| { + block.statements.iter().any(|statement| + matches!(statement.kind, S::Let { local, .. } if local == owner)) + }) + .expect("capture block"); + let at = block.statements.iter().position(|statement| + matches!(statement.kind, S::Let { local, .. } if local == owner)).expect("capture index"); + let capture = block.statements.remove(at); + let begin = block.statements.iter().position(|statement| + matches!(statement.kind, S::BeginCallView { local, .. } if local == loan)).expect("loan"); + let begin = block.statements.remove(begin); + // Keep the loan valid on both handler arms, but move capture + // after the later source's SumTag evaluation. Lexical snapshot + // order must be proved independently from call dominance. + assert!( + block + .statements + .iter() + .any(|statement| matches!(statement.kind, S::SumTag { .. })) + ); + block.statements.push(capture); + block.statements.push(begin); + } + _ => unreachable!(), + } + let error = retained_snapshot_error(&changed, &types); + assert!( + error.contains("snapshot") + || error.contains("call ownership") + || error.contains("call view"), + "{mutation}: {error}" + ); + } +} + +#[test] +fn caller_acquisition_retained_snapshot_metadata_remaps_both_slots_without_source_reads() { + let (program, types) = source_program(RETAINED_SNAPSHOT_PROJECTED); + let function = exercise(&program); + let (owner, loan) = retained_snapshot_stage(function); + let E::Call { ownership, .. } = &read_call(function).kind else { + panic!("source call"); + }; + let mut changed = ownership.clone(); + let mut ids = Vec::new(); + changed.metadata_local_ids(|id| ids.push(id)); + assert!(ids.contains(&owner) && ids.contains(&loan)); + changed + .remap_metadata_locals(|id| Ok::<_, ()>(LocalId::new(id.index() + 1))) + .expect("structural remap"); + let hir::CallOwnership::Source(source) = changed else { + panic!("Source"); + }; + assert_eq!( + source.arguments[0].staging, + hir::ArgumentStaging::RetainedSnapshot { + owner: LocalId::new(owner.index() + 1), + loan: LocalId::new(loan.index() + 1), + } + ); + assert_eq!( + source.arguments[0].effect, + CheckedCallerEffect::RetainBorrow + ); + assert_eq!( + source.arguments[0].retained_snapshot_type(), + Some(source.arguments[0].source_witness().occurrence_type()) + ); + let mut prepared = program.clone(); + crate::prepare_native_sequences(&mut prepared, &types); + crate::prepare_native_uninhabited_sums(&mut prepared, &types); + crate::prepare_native_generated_functions(&mut prepared, &types); + crate::validate_call_ownership(&prepared, &types) + .expect("actual canonical remaps keep snapshot proof"); + crate::move_values::MoveValuePlan::analyze(&prepared, exercise(&prepared), &types) + .expect("metadata does not reread the rebound source at the call"); +} + +#[test] +fn caller_acquisition_retained_snapshot_does_not_grant_descriptor_signature_clone_authority() { + let source = r#"namespace app +function value() returns int64: + return 7 +function read(view callback: function() returns int64, extra: int64) returns int64: + return callback() + extra +function exercise(incoming: optional[int64]) returns int64: + function() returns int64 callback = value + return read(view callback, incoming handle: + default 2 + ) +"#; + let (program, types) = source_program(source); + let function = exercise(&program); + let E::Call { + ownership: hir::CallOwnership::Source(source), + .. + } = &read_call(function).kind + else { + panic!("descriptor source call"); + }; + assert!(source.arguments[0].retained_snapshot_type().is_none()); + assert!(matches!( + source.arguments[0].staging, + hir::ArgumentStaging::Borrowed { .. } + )); + let mut ownership = hir::CallOwnership::Source(source.clone()); + assert!( + ownership + .stage_retained_snapshot(0, LocalId::new(100), LocalId::new(101)) + .is_err() + ); + validate_function(&program, function, &types) + .expect("existing descriptor raw loan, no clone authority"); + + let produced = r#"namespace app +function read(view values: list[int64], extra: int64) returns int64: + return extra +function exercise(incoming: optional[int64]) returns int64: + return read(view list(3), incoming handle: + default 2 + ) +"#; + let (program, types) = source_program(produced); + let function = exercise(&program); + let E::Call { + ownership: hir::CallOwnership::Source(source), + .. + } = &read_call(function).kind + else { + panic!("written producer Source call"); + }; + assert_eq!( + source.arguments[0].origin, + hir::CallerOrigin::OwnedExpression + ); + assert!(source.arguments[0].retained_snapshot_type().is_none()); + assert_eq!(source.arguments[0].staging, hir::ArgumentStaging::Original); + let mut ownership = hir::CallOwnership::Source(source.clone()); + assert!( + ownership + .stage_retained_snapshot(0, LocalId::new(100), LocalId::new(101)) + .is_err() + ); + validate_function(&program, function, &types) + .expect("existing fresh producer backing, no snapshot permission from type/span"); +} + +fn transparent_retained_source(secret: bool, projected: bool) -> String { + let declaration = if secret { + "" + } else { + "type Numbers = list[int64] where true\n" + }; + let ty = if secret { + "secret[list[int64]]" + } else { + "Numbers" + }; + let wrapper = if secret { "declassify" } else { "coarsen" }; + let (holder, input_ty, setup, endpoint, rebind) = if projected { + ( + format!("struct Store:\n items: {ty}\n"), + "Store", + " mutable Store store = input\n", + "store.items", + "store = replacement", + ) + } else { + ( + String::new(), + ty, + " mutable ", + "values", + "values = replacement", + ) + }; + let setup = if projected { + setup.to_string() + } else { + format!("{setup}{ty} values = input\n") + }; + format!( + "namespace app\n{declaration}{holder}function read(view values: list[int64], extra: int64) returns int64:\n mutable int64 total = extra\n for value in view values:\n total = total + value\n return total\nfunction exercise(input: {input_ty}, replacement: {input_ty}, spare: {input_ty}, incoming: optional[int64]) returns int64:\n{setup} int64 answer = read(view({wrapper} {endpoint}), incoming handle:\n {rebind}\n default 2\n )\n int64 after = read(view({wrapper} {endpoint}), 0)\n return answer + after\n" + ) +} + +fn transparent_retained_program(source: &str, release: bool) -> (Program, TypeInterner) { + let file = jett_common::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 high = hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, jett_common::SourceOrigin::Project)]), + ) + .expect("exact checked transparent-view Source HIR"); + let program = crate::lower(&high, &checked.interner) + .expect("retained transparent place keeps its original Source backing"); + (program, checked.interner) +} + +fn transparent_retained_leaf(mut value: &Expression, secret: bool, projected: bool) -> LocalId { + let E::Clone(endpoint) = &value.kind else { + panic!("one full-endpoint snapshot Clone"); + }; + value = endpoint; + let E::View(endpoint) = &value.kind else { + panic!("original written View"); + }; + value = endpoint; + value = match (&value.kind, secret) { + (E::Declassify(inner), true) | (E::Coarsen(inner), false) => inner, + _ => panic!("original checked qualifier conversion"), + }; + if projected { + let E::Field { base, .. } = &value.kind else { + panic!("original projected endpoint"); + }; + value = base; + } + let E::Local(local) = value.kind else { + panic!("exact original Local backing, no introduced inner Clone"); + }; + local +} + +fn transparent_retained_leaf_mut(mut value: &mut Expression) -> &mut Expression { + while !matches!(value.kind, E::Local(_)) { + value = match &mut value.kind { + E::Clone(inner) | E::View(inner) | E::Coarsen(inner) | E::Declassify(inner) => inner, + E::Field { base, .. } => base, + _ => panic!("checked finite place spine"), + }; + } + value +} + +fn assert_transparent_retained_scope( + program: &Program, + types: &TypeInterner, + secret: bool, + projected: bool, +) { + let function = exercise(program); + let (owner, loan) = retained_snapshot_stage(function); + let original = transparent_retained_leaf( + retained_snapshot_initializer(function, owner), + secret, + projected, + ); + let source_name = if projected { "store" } else { "values" }; + assert_eq!( + function.local(original).expect("original binding").name, + source_name + ); + assert_eq!(function.local(loan).expect("loan").view_source, Some(owner)); + let acquisitions = + validate_function(program, function, types).expect("unchanged Source place proof"); + assert_eq!( + acquisitions.owner_initializers().count(), + 0, + "caller retains its owner" + ); + assert!( + acquisitions + .arguments(read_call(function)) + .expect("exact source call") + .is_empty() + ); + let block = function + .blocks + .iter() + .find(|block| { + block + .statements + .iter() + .any(|statement| matches!(statement.kind, S::Let { local, .. } if local == owner)) + }) + .expect("capture block"); + let capture = block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::Let { local, .. } if local == owner)) + .expect("capture"); + let begin = block + .statements + .iter() + .position( + |statement| matches!(statement.kind, S::BeginCallView { local, .. } if local == loan), + ) + .expect("loan begins"); + let tag = block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::SumTag { .. })) + .expect("later handled selection"); + assert!( + capture < begin && begin < tag, + "snapshot before later handler work" + ); + assert!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!(statement.kind, S::EndCallView { local } if local == loan)) + ); + crate::move_values::MoveValuePlan::analyze(program, function, types) + .expect("independent snapshot survives original owner rebind or abort"); +} + +#[test] +fn caller_acquisition_retained_transparent_direct_places_survive_later_rebind() { + for secret in [false, true] { + let source = transparent_retained_source(secret, false); + for release in [false, true] { + let (program, types) = transparent_retained_program(&source, release); + assert_transparent_retained_scope(&program, &types, secret, false); + } + } +} + +#[test] +fn caller_acquisition_retained_transparent_projected_places_snapshot_only_the_endpoint() { + for secret in [false, true] { + let source = transparent_retained_source(secret, true); + for release in [false, true] { + let (program, types) = transparent_retained_program(&source, release); + assert_transparent_retained_scope(&program, &types, secret, true); + } + } +} + +#[test] +fn caller_acquisition_retained_transparent_indirect_and_return_keep_exact_loan_scope() { + for secret in [false, true] { + let direct = transparent_retained_source(secret, false); + let indirect = direct.replace(" int64 answer = read(view(", + " function(view list[int64], int64) returns int64 callback = read\n int64 answer = callback(view("); + let aborted = direct.replace(" default 2", " return 11"); + for (source, is_indirect) in [(&indirect, true), (&aborted, false)] { + for release in [false, true] { + let (program, types) = transparent_retained_program(source, release); + assert_transparent_retained_scope(&program, &types, secret, false); + assert_eq!( + matches!(read_call(exercise(&program)).kind, E::IndirectCall { .. }), + is_indirect + ); + if !is_indirect { + let (_, loan) = retained_snapshot_stage(exercise(&program)); + let returned = exercise(&program).blocks.iter().find(|block| + matches!(&block.terminator.kind, T::Return(Some(value)) if matches!(value.kind, E::Int(11)))) + .expect("original handler early return"); + assert!(returned.statements.iter().any(|statement| + matches!(statement.kind, S::EndCallView { local } if local == loan))); + } + } + } + } +} + +#[test] +fn caller_acquisition_retained_transparent_spines_reject_added_clone_and_foreign_backing() { + for (secret, projected) in [(false, false), (true, false), (false, true), (true, true)] { + let source = transparent_retained_source(secret, projected); + let (program, types) = transparent_retained_program(&source, false); + let (owner, _) = retained_snapshot_stage(exercise(&program)); + for mutation in [ + "inner clone", + "foreign backing", + "endpoint type", + "endpoint span", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + let spare = function.params[2].local; + let initializer = retained_snapshot_initializer_mut(function, owner); + if matches!(mutation, "endpoint type" | "endpoint span") { + let E::Clone(endpoint) = &mut initializer.kind else { + panic!("snapshot"); + }; + assert!(matches!(endpoint.kind, E::View(_)), "written View"); + if mutation == "endpoint type" { + endpoint.ty = TypeInterner::BOOL; + } else { + endpoint.span.start += 1; + } + } else { + let leaf = transparent_retained_leaf_mut(initializer); + if mutation == "inner clone" { + leaf.kind = E::Clone(Box::new(leaf.clone())); + } else { + leaf.kind = E::Local(spare); + } + } + let error = retained_snapshot_error(&changed, &types); + assert!( + error.contains("call ownership") || error.contains("snapshot"), + "{mutation}: {error}" + ); + } + let mut prepared = program.clone(); + crate::prepare_native_sequences(&mut prepared, &types); + crate::prepare_native_uninhabited_sums(&mut prepared, &types); + crate::prepare_native_generated_functions(&mut prepared, &types); + crate::validate_call_ownership(&prepared, &types) + .expect("exact transparent place survives canonical remaps"); + } +} + +#[test] +fn caller_acquisition_retained_transparent_places_keep_known_borrow_and_owned_formal_policy() { + for (ty, declaration, wrapper) in [ + ( + "Numbers", + "type Numbers = list[int64] where true\n", + "coarsen", + ), + ("secret[list[int64]]", "", "declassify"), + ] { + for (owned_formal, expected) in [(false, 401), (true, 375)] { + let formal = if owned_formal { "" } else { "view " }; + let argument = if owned_formal { + format!("view({wrapper} values)") + } else { + format!("{wrapper} values") + }; + let source = format!( + "namespace app\n{declaration}function read({formal}values: list[int64], extra: int64) returns int64:\n return extra\nfunction rejected(view values: {ty}, incoming: optional[int64]) returns int64:\n return read({argument}, incoming handle:\n default 2\n )\n" + ); + for release in [false, true] { + let parsed = jett_parser::parse(&source, jett_common::FileId::new(0)); + 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 }, + ); + let errors = checked + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert_eq!(errors.len(), 1, "{source}: {errors:?}"); + assert_eq!(errors[0].code.code(), expected, "{source}: {errors:?}"); + } + } + } +} + +const RETAINED_PHYSICAL_PIPELINE: &str = r#"namespace app +function read(view values: list[int64], extra: int64) returns int64: + mutable int64 total = extra + for value in view values: + total = total + value + return total +function exercise(incoming: optional[int64]) returns int64: + mutable list[int64] values = list(3) + list[int64] spare = list(9) + int64 answer = values into view read(incoming handle: + values = list(8) + default 2 + ) + return answer + read(view values, 0) +"#; + +#[test] +fn caller_acquisition_retained_physical_pipeline_span_preserves_raw_source_and_order() { + for release in [false, true] { + let (program, types) = transparent_retained_program(RETAINED_PHYSICAL_PIPELINE, release); + let function = exercise(&program); + let (owner, loan) = retained_snapshot_stage(function); + let E::Call { + ownership: hir::CallOwnership::Source(packet), + args, + evaluation_order, + .. + } = &read_call(function).kind + else { + panic!("real source pipeline"); + }; + let argument = &packet.arguments[0]; + let physical = argument + .retained_snapshot_span() + .expect("private initial physical occurrence"); + assert_ne!(physical, argument.source_span); + assert_eq!(evaluation_order, &[0, 1]); + assert_eq!(argument.source_index, 0); + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + assert_eq!(args[0].span, physical); + let initializer = retained_snapshot_initializer(function, owner); + let E::Clone(endpoint) = &initializer.kind else { + panic!("complete snapshot"); + }; + assert_eq!(initializer.span, physical); + assert_eq!(endpoint.span, physical); + let E::View(raw) = &endpoint.kind else { + panic!("initial physical View"); + }; + assert_eq!(raw.span, argument.source_span); + let E::Local(original) = raw.kind else { + panic!("raw original binding"); + }; + assert_eq!( + function.local(original).expect("source binding").name, + "values" + ); + assert_eq!(function.local(loan).expect("loan").view_source, Some(owner)); + let acquired = + validate_function(&program, function, &types).expect("physical and raw joins"); + assert_eq!(acquired.owner_initializers().count(), 0); + assert!( + acquired + .arguments(read_call(function)) + .expect("call") + .is_empty() + ); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("snapshot before later handler rebind"); + let mut prepared = program.clone(); + crate::prepare_native_sequences(&mut prepared, &types); + crate::prepare_native_uninhabited_sums(&mut prepared, &types); + crate::prepare_native_generated_functions(&mut prepared, &types); + crate::validate_call_ownership(&prepared, &types) + .expect("no span rewrite in structural remaps"); + } +} + +#[test] +fn caller_acquisition_retained_physical_reflected_getter_keeps_initial_view_and_raw_binding() { + let original_source = r#"namespace app +struct Record: + values: list[int64] +function exercise(view source: Record, view field: TypeField) returns list[int64]: + return source into view type.field_value[Record, list[int64]](view field) +"#; + // Match the existing recursive object regression: non-Exact field plans + // enter ordered call staging before the raw read and refinement checks. + let ordered_source = r#"namespace app +type Positive = int64 where value > 0 +struct Record: + values: list[optional[result[map[int64, list[int64]], set[int64]]]] +function exercise(view source: Record, view field: TypeField) returns list[optional[result[map[Positive, list[Positive]], set[Positive]]]]: + return source into view type.field_value[Record, list[optional[result[map[Positive, list[Positive]], set[Positive]]]]](view field) +"#; + for release in [false, true] { + for (source, ordered) in [(original_source, false), (ordered_source, true)] { + let (program, types) = transparent_retained_program(source, release); + let function = exercise(&program); + let call = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + S::Let { value, .. } | S::Evaluate(value) + if matches!( + value.kind, + E::Intrinsic { + intrinsic: hir::IntrinsicId::TypeFieldValue, + .. + } + ) => + { + Some(value) + } + _ => None, + }) + .or_else(|| { + function + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + T::Return(Some(value)) + if matches!( + value.kind, + E::Intrinsic { + intrinsic: hir::IntrinsicId::TypeFieldValue, + .. + } + ) => + { + Some(value) + } + _ => None, + }) + }) + .expect("real reflected getter invocation"); + let E::Intrinsic { + ownership: hir::CallOwnership::Source(packet), + args, + .. + } = &call.kind + else { + panic!("unchanged reflected Source packet"); + }; + let argument = &packet.arguments[0]; + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + if !ordered { + // Exact field plans use the existing unstaged raw getter path. + assert_eq!(argument.staging, hir::ArgumentStaging::Original); + let E::View(raw) = &args[0].kind else { + panic!("initial written physical View"); + }; + assert!(matches!(raw.kind, E::Local(local) if local == function.params[0].local)); + validate_function(&program, function, &types) + .expect("exact field getter stays Original"); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("existing unstaged reflected read"); + continue; + } + let hir::ArgumentStaging::RetainedSnapshot { owner, loan } = argument.staging else { + panic!("ordinary Record endpoint snapshot"); + }; + let physical = argument + .retained_snapshot_span() + .expect("initial pipeline occurrence"); + assert_ne!(physical, argument.source_span); + assert_eq!(args[0].span, physical); + let initializer = retained_snapshot_initializer(function, owner); + let E::Clone(endpoint) = &initializer.kind else { + panic!("full endpoint snapshot"); + }; + let E::View(raw) = &endpoint.kind else { + panic!("original written physical View"); + }; + assert_eq!(initializer.span, physical); + assert_eq!(endpoint.span, physical); + assert_eq!(raw.span, argument.source_span); + assert!(matches!(raw.kind, E::Local(local) if local == function.params[0].local)); + assert_eq!(function.local(loan).expect("loan").view_source, Some(owner)); + validate_function(&program, function, &types) + .expect("reflected certificate and exact raw source stay active"); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("existing reflected owning-result/read analysis"); + } + } +} + +#[test] +fn caller_acquisition_retained_physical_span_does_not_replace_raw_or_backing_authority() { + let (program, types) = transparent_retained_program(RETAINED_PHYSICAL_PIPELINE, false); + let (owner, loan) = retained_snapshot_stage(exercise(&program)); + for mutation in [ + "physical occurrence", + "raw occurrence", + "foreign binding", + "physical loan", + "public raw witness", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + if mutation == "public raw witness" { + let E::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &mut read_call_mut(function).kind + else { + panic!("source pipeline"); + }; + packet.arguments[0].source_span.start += 1; + } else if mutation == "physical loan" { + let E::Call { args, .. } = &mut read_call_mut(function).kind else { + panic!("call"); + }; + args[0].span.start += 1; + } else if mutation == "physical occurrence" { + let span = { + let initializer = retained_snapshot_initializer_mut(function, owner); + initializer.span.start += 1; + let E::Clone(endpoint) = &mut initializer.kind else { + panic!("snapshot"); + }; + endpoint.span = initializer.span; + initializer.span + }; + // Preserve owner/initializer agreement so only the sealed original + // physical occurrence can reject this coordinated replacement. + function.locals[owner.index() as usize].span = span; + } else { + let spare = function + .locals + .iter() + .find(|local| local.name == "spare") + .expect("same-type source") + .id; + let initializer = retained_snapshot_initializer_mut(function, owner); + let E::Clone(endpoint) = &mut initializer.kind else { + panic!("snapshot"); + }; + let E::View(raw) = &mut endpoint.kind else { + panic!("physical pipeline View"); + }; + if mutation == "raw occurrence" { + raw.span.start += 1; + } else { + raw.kind = E::Local(spare); + } + } + let error = retained_snapshot_error(&changed, &types); + assert!( + error.contains("retained snapshot") || error.contains("call ownership"), + "{mutation}: {error}" + ); + } + assert_eq!( + exercise(&program) + .local(loan) + .expect("original loan") + .view_source, + Some(owner) + ); +} +const FLOW_PROJECTED_SOURCE: &str = r#"namespace app +machine Session: + states: + active(items: list[int64]) + cached(items: list[int64]) + empty +function read(view values: list[int64]) returns int64: + return 7 +function exercise(source: Session, other: Session) returns int64: + if source at active: + return read(view source.items) + return 0 +machine TaskState: + states: + ready(value: nothing) + cached(value: nothing) + empty +function depth(value: nothing) returns int64: + return 3 +function copied(source: TaskState) returns int64: + if source at ready: + return depth(source.value) + return 0 +"#; + +fn flow_projected_mir_field(value: &mut Expression) -> &mut Expression { + if matches!(value.kind, E::View(_)) { + let E::View(inner) = &mut value.kind else { + unreachable!() + }; + return flow_projected_mir_field(inner); + } + assert!(matches!(value.kind, E::Field { .. })); + value +} + +fn flow_projected_mir_call_mut(function: &mut Function) -> &mut Expression { + for block in &mut function.blocks { + if let T::Return(Some(value)) = &mut block.terminator.kind { + if matches!(value.kind, E::Call { .. }) { + return value; + } + } + } + panic!("guarded original source call") +} + +#[test] +fn caller_acquisition_flow_projection_keeps_bare_storage_and_exact_original_state() { + let (mut program, types) = source_program(FLOW_PROJECTED_SOURCE); + for name in ["exercise", "copied"] { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + let E::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &read_call(function).kind + else { + panic!("guarded source packet"); + }; + let root = packet.arguments[0] + .source_witness() + .projection_root() + .unwrap(); + assert!(matches!( + types.resolve(root.2), + jett_types::Type::MachineState { .. } + )); + assert!(matches!( + types.resolve(root.3), + jett_types::Type::Machine(_) + )); + assert_eq!(function.local(root.0).unwrap().ty, root.3); + assert_eq!( + packet.arguments[0].effect, + if name == "exercise" { + CheckedCallerEffect::RetainBorrow + } else { + CheckedCallerEffect::Copy + } + ); + assert!(matches!( + packet.arguments[0].staging, + hir::ArgumentStaging::Original + )); + validate_function(&program, function, &types) + .expect("original flow projection Source proof"); + } + 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_call_ownership(&program, &types) + .expect("dense remaps preserve private root without a new loan"); +} + +#[test] +fn caller_acquisition_flow_projection_rejects_changed_state_root_span_and_endpoint() { + let (program, mut types) = source_program(FLOW_PROJECTED_SOURCE); + let function = exercise(&program); + let other = function + .locals + .iter() + .find(|local| local.name == "other") + .unwrap() + .id; + let E::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &read_call(function).kind + else { + panic!("source packet"); + }; + let root = packet.arguments[0] + .source_witness() + .projection_root() + .unwrap(); + let jett_types::Type::Machine(machine) = *types.resolve(root.3) else { + panic!("bare owner"); + }; + let cached = types.resolve_machine(machine).state_id("cached").unwrap(); + let sibling = types.intern(jett_types::Type::MachineState { + machine, + state: cached, + }); + for mutation in ["state", "span", "local", "field", "endpoint"] { + let mut changed = program.clone(); + let E::Call { args, .. } = + &mut flow_projected_mir_call_mut(exercise_mut(&mut changed)).kind + else { + panic!("source call"); + }; + let value = flow_projected_mir_field(&mut args[0]); + if mutation == "endpoint" { + value.ty = TypeInterner::BOOL; + } else { + let E::Field { + base, + owner_type, + field, + } = &mut value.kind + else { + panic!("field"); + }; + match mutation { + "state" => { + *owner_type = sibling; + base.ty = sibling; + } + "span" => base.span.start += 1, + "local" => base.kind = E::Local(other), + "field" => *field = hir::FieldId::new(1), + _ => unreachable!(), + } + } + assert!( + validate_function(&changed, exercise(&changed), &types).is_err(), + "{mutation}" + ); + } +} + +const MAP_GET_PUBLIC_DECLARATION: &str = "export function get[K, V](items: map[K, V], view key: K) returns optional[V]:\n return map.__get[K, V](items, view key)\n"; + +fn intrinsic_owning_map_program(release: bool) -> (Program, TypeInterner) { + assert!(include_str!("../../../../stdlib/map.jett").contains(MAP_GET_PUBLIC_DECLARATION)); + let stdlib_file = jett_common::FileId::new(jett_common::STDLIB_FILE_ID_START); + let project_file = jett_common::FileId::new(0); + let stdlib_source = format!( + "namespace map\nexport interface Named:\n function name(view self: Named) returns string\n{MAP_GET_PUBLIC_DECLARATION}function retained(view items: map[string, Named], incoming: optional[string]) returns optional[Named]:\n return map.__get[string, Named](view items, incoming handle:\n default \"key\"\n )\n" + ); + let project_source = "namespace app\nfunction exercise(items: map[string, map.Named], view key: string) returns optional[map.Named]:\n use map\n return map.get[string, map.Named](items, view key)\n"; + let mut stdlib = jett_parser::parse(&stdlib_source, stdlib_file); + let mut project = jett_parser::parse(project_source, project_file); + assert!(stdlib.errors.is_empty(), "{:?}", stdlib.errors); + assert!(project.errors.is_empty(), "{:?}", project.errors); + stdlib.module.items.append(&mut project.module.items); + let resolved = jett_resolve::resolve(&stdlib.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check_with_options( + &stdlib.module, + &resolved, + jett_typecheck::CheckOptions { release }, + ); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let origins = HashMap::from([ + (stdlib_file, jett_common::SourceOrigin::Stdlib), + (project_file, jett_common::SourceOrigin::Project), + ]); + let high = hir::lower(&stdlib.module, &resolved, &checked, &origins) + .expect("original compiler-origin facade and ordinary source caller"); + ( + crate::lower(&high, &checked.interner).expect("closed intrinsic owning staging"), + checked.interner, + ) +} + +fn map_get_function(program: &Program) -> &Function { + let mut matches = program + .functions + .iter() + .filter(|function| function.identity.declaration.name == "get"); + let function = matches.next().expect("concrete public map.get body"); + assert!( + matches.next().is_none(), + "one selected public instantiation" + ); + function +} + +fn map_get_function_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "get") + .expect("concrete public map.get body") +} + +fn map_get_intrinsic(function: &Function) -> &Expression { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| { + let (S::Let { value, .. } | S::Evaluate(value)) = &statement.kind else { + return None; + }; + matches!( + value.kind, + E::Intrinsic { + intrinsic: hir::IntrinsicId::MapGet, + .. + } + ) + .then_some(value) + }) + .expect("materialized exact MapGet operation") +} + +fn map_get_intrinsic_mut(function: &mut Function) -> &mut Expression { + function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| { + let (S::Let { value, .. } | S::Evaluate(value)) = &mut statement.kind else { + return None; + }; + matches!( + value.kind, + E::Intrinsic { + intrinsic: hir::IntrinsicId::MapGet, + .. + } + ) + .then_some(value) + }) + .expect("materialized exact MapGet operation") +} + +#[test] +fn caller_acquisition_intrinsic_map_get_transfers_opaque_containing_owner_without_clone() { + for release in [false, true] { + let (program, types) = intrinsic_owning_map_program(release); + let function = map_get_function(&program); + let call = map_get_intrinsic(function); + let E::Intrinsic { + ownership: hir::CallOwnership::Source(packet), + args, + evaluation_order, + .. + } = &call.kind + else { + panic!("exact checked intrinsic packet"); + }; + let argument = &packet.arguments[0]; + assert_eq!(argument.syntax, jett_typecheck::CheckedCallerSyntax::Bare); + assert_eq!(argument.effect, CheckedCallerEffect::RelinquishOwned); + assert_eq!( + argument.callee_access, + jett_typecheck::CheckedCalleeAccess::View + ); + assert_eq!( + argument.physical_access, + jett_typecheck::CheckedCalleeAccess::View + ); + assert_eq!(evaluation_order.as_slice(), &[0, 1]); + let hir::ArgumentStaging::Relinquished { owner, loan } = argument.staging else { + panic!("source owning transfer is staged"); + }; + assert_ne!(owner, loan); + let initializer = observation_initializer(function, owner); + assert!(matches!(initializer.kind, E::Local(local) if local == function.params[0].local)); + assert_eq!(initializer.ty, argument.actual_type); + assert_eq!(initializer.span, argument.source_span); + let Type::Map(key, value) = types.resolve(initializer.ty) else { + panic!("exact map owner"); + }; + assert_eq!(*key, TypeInterner::STRING); + assert!(matches!(types.resolve(*value), Type::Interface(_))); + assert!(!crate::handlers::can_snapshot_view(&types, initializer.ty)); + assert_eq!(function.local(owner).unwrap().view_source, None); + assert_eq!(function.local(loan).unwrap().view_source, Some(owner)); + assert!( + matches!(&args[0].kind, E::View(value) if matches!(value.kind, E::Local(id) if id == loan)) + ); + assert_eq!( + packet.arguments[1].syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert_eq!(packet.arguments[1].effect, CheckedCallerEffect::Copy); + let hir::ArgumentStaging::Copied { value: key_owner } = packet.arguments[1].staging else { + panic!("the implicitly copyable string key keeps its separate copied slot"); + }; + let key_initializer = observation_initializer(function, key_owner); + assert_eq!(key_initializer.ty, TypeInterner::STRING); + assert!(matches!(key_initializer.kind, E::Clone(_))); + let block = function + .blocks + .iter() + .find(|block| { + block.statements.iter().any(|statement| + matches!(&statement.kind, S::Let { value, .. } if std::ptr::eq(value, call))) + }) + .expect("one consuming operation owns both loans"); + let owner_at = block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::Let { local, .. } if local == owner)) + .unwrap(); + let begin_at = block.statements.iter().position(|statement| + matches!(statement.kind, S::BeginCallView { local, .. } if local == loan)).unwrap(); + let call_at = block.statements.iter().position(|statement| + matches!(&statement.kind, S::Let { value, .. } if std::ptr::eq(value, call))).unwrap(); + let end_at = block + .statements + .iter() + .position( + |statement| matches!(statement.kind, S::EndCallView { local } if local == loan), + ) + .unwrap(); + assert!(owner_at < begin_at && begin_at < call_at && call_at < end_at); + assert_eq!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter( + |statement| matches!(statement.kind, S::Let { local, .. } if local == owner) + ) + .count(), + 1 + ); + let acquisitions = validate_function(&program, function, &types) + .expect("original map move and exact loan"); + assert_eq!(acquisitions.owner_initializers().count(), 1); + let acquisition = acquisitions.owner_initializer(owner).unwrap(); + assert_eq!(acquisition.binding, Some(function.params[0].local)); + assert_eq!(acquisition.effect, CheckedCallerEffect::RelinquishOwned); + let E::Call { + ownership: hir::CallOwnership::Source(public), + .. + } = &read_call(exercise(&program)).kind + else { + panic!("ordinary caller preserves public source ownership"); + }; + assert_eq!( + public.arguments[0].effect, + CheckedCallerEffect::TransferOwned + ); + assert_eq!( + public.arguments[0].callee_access, + jett_typecheck::CheckedCalleeAccess::Owned + ); + crate::validate_call_ownership(&program, &types) + .expect("all source and intrinsic packets remain exact"); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("map owner consumed once and released after its scoped borrow"); + } +} + +#[test] +fn caller_acquisition_intrinsic_map_get_keeps_retained_maps_borrowed_across_later_handler() { + for release in [false, true] { + let (program, types) = intrinsic_owning_map_program(release); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "retained") + .expect("written view control"); + let E::Intrinsic { + ownership: hir::CallOwnership::Source(packet), + .. + } = &map_get_intrinsic(function).kind + else { + panic!("exact intrinsic packet"); + }; + let argument = &packet.arguments[0]; + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + let hir::ArgumentStaging::Borrowed { loan } = argument.staging else { + panic!("retained map gains only a scoped loan"); + }; + assert_eq!( + function.local(loan).unwrap().view_source, + Some(function.params[0].local) + ); + assert!(!crate::handlers::can_snapshot_view( + &types, + argument.actual_type + )); + let acquisitions = + validate_function(&program, function, &types).expect("retained map and handled key"); + assert_eq!( + acquisitions.owner_initializers().count(), + 0, + "neither the retained map nor the copied key transfers a source owner" + ); + assert_eq!(packet.arguments[1].effect, CheckedCallerEffect::Copy); + let hir::ArgumentStaging::Copied { value: copied } = packet.arguments[1].staging else { + panic!("the handled string key uses its own copied slot"); + }; + assert_eq!( + observation_initializer(function, copied).ty, + TypeInterner::STRING + ); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("later handler does not consume the retained map"); + } +} + +#[test] +fn caller_acquisition_intrinsic_map_get_refuses_missing_or_forged_owner_staging() { + for release in [false, true] { + let (program, types) = intrinsic_owning_map_program(release); + let E::Intrinsic { + ownership: hir::CallOwnership::Source(packet), + .. + } = &map_get_intrinsic(map_get_function(&program)).kind + else { + panic!("source intrinsic packet"); + }; + let hir::ArgumentStaging::Relinquished { owner, loan } = packet.arguments[0].staging else { + panic!("original owning stage"); + }; + for mutation in [ + "stage", + "effect", + "aliased owner", + "added clone", + "missing owner", + "missing end", + "loan backing", + "order", + ] { + let mut changed = program.clone(); + let function = map_get_function_mut(&mut changed); + match mutation { + "stage" | "effect" | "aliased owner" | "order" => { + let E::Intrinsic { + ownership: hir::CallOwnership::Source(packet), + evaluation_order, + .. + } = &mut map_get_intrinsic_mut(function).kind + else { + panic!("source intrinsic packet"); + }; + match mutation { + "stage" => packet.arguments[0].staging = hir::ArgumentStaging::Original, + "effect" => packet.arguments[0].effect = CheckedCallerEffect::RetainBorrow, + "aliased owner" => { + packet.arguments[0].staging = + hir::ArgumentStaging::Relinquished { owner: loan, loan } + } + "order" => *evaluation_order = vec![0, 0], + _ => unreachable!(), + } + } + "added clone" => { + let initializer = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + S::Let { local, value } if *local == owner => Some(value), + _ => None, + }) + .expect("unique original owner initializer"); + initializer.kind = E::Clone(Box::new(initializer.clone())); + } + "missing owner" | "missing end" => { + let (block, index) = function + .blocks + .iter() + .enumerate() + .find_map(|(block, body)| { + body.statements + .iter() + .position(|statement| match statement.kind { + S::Let { local, .. } if mutation == "missing owner" => { + local == owner + } + S::EndCallView { local } if mutation == "missing end" => { + local == loan + } + _ => false, + }) + .map(|index| (block, index)) + }) + .expect("original owner or scope endpoint"); + function.blocks[block].statements.remove(index); + } + "loan backing" => { + function.locals[loan.index() as usize].view_source = + Some(function.params[0].local) + } + _ => unreachable!(), + } + assert!( + validate_function(&changed, map_get_function(&changed), &types).is_err(), + "{mutation}" + ); + } + } +} + +const HANDLED_VIEW_DIRECT_SOURCE: &str = r#"namespace app +struct Item: + values: list[int64] +function read(view value: Item, extra: int64) returns int64: + return extra +function exercise(incoming: optional[Item], later: optional[int64]) returns int64: + return read(view(incoming handle: + default Item(values: list(7)) + ), later handle: + default 3 + ) +"#; + +const HANDLED_VIEW_CONVERTED_SOURCE: &str = r#"namespace app +interface Named: + function name(view self: Named) returns string +struct Item: + values: list[int64] +implement Named for Item: + function name(view self: Item) returns string: + return "item" +function read(view value: Named, extra: int64) returns int64: + return extra +function exercise(incoming: optional[Item], later: optional[int64]) returns int64: + return read(view(incoming handle: + default Item(values: list(7)) + ), later handle: + default 3 + ) +"#; + +fn handled_view_read_call(function: &Function) -> &Expression { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| { + let (S::Let { value, .. } | S::Evaluate(value)) = &statement.kind else { + return None; + }; + matches!(&value.kind, E::Call { ownership: hir::CallOwnership::Source(packet), .. } + if packet.arguments.len() == 2) + .then_some(value) + }) + .expect("the source read invocation, not its zero-argument producer") +} + +fn handled_view_read_call_mut(function: &mut Function) -> &mut Expression { + function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| { + let (S::Let { value, .. } | S::Evaluate(value)) = &mut statement.kind else { + return None; + }; + matches!(&value.kind, E::Call { ownership: hir::CallOwnership::Source(packet), .. } + if packet.arguments.len() == 2) + .then_some(value) + }) + .expect("the materialized source read invocation") +} + +fn handled_view_packet(expression: &Expression) -> &hir::SourceCallOwnership { + let ownership = match &expression.kind { + E::Call { ownership, .. } + | E::IndirectCall { ownership, .. } + | E::Intrinsic { ownership, .. } => ownership, + _ => panic!("source invocation"), + }; + let hir::CallOwnership::Source(packet) = ownership else { + panic!("original checked Source packet"); + }; + packet +} + +fn handled_view_storage( + function: &Function, + call: &Expression, + parameter: usize, +) -> (LocalId, LocalId) { + let packet = handled_view_packet(call); + let argument = &packet.arguments[parameter]; + assert_eq!(argument.origin, hir::CallerOrigin::OwnedExpression); + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + assert_eq!( + argument.physical_access, + jett_typecheck::CheckedCalleeAccess::View + ); + assert_eq!(argument.retained_snapshot_type(), None); + let hir::ArgumentStaging::Borrowed { loan } = argument.staging else { + panic!("handled source producer borrows its own storage"); + }; + let owner = function + .local(loan) + .expect("loan") + .view_source + .expect("exact owner"); + assert_ne!(owner, loan); + assert_eq!(function.local(owner).expect("owner").view_source, None); + let initializer = observation_initializer(function, owner); + assert!(!matches!(initializer.kind, E::Clone(_) | E::View(_))); + assert_eq!(function.local(owner).unwrap().span, initializer.span); + assert_eq!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| matches!(statement.kind, S::Let { local, .. } if local == owner)) + .count(), + 1 + ); + (owner, loan) +} + +fn handled_view_projection(function: &Function, loan: LocalId) -> &Expression { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| { + let S::BeginCallView { local, value } = &statement.kind else { + return None; + }; + (*local == loan).then_some(value) + }) + .expect("unique Begin") +} + +fn handled_view_projection_mut(function: &mut Function, loan: LocalId) -> &mut Expression { + function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| { + let S::BeginCallView { local, value } = &mut statement.kind else { + return None; + }; + (*local == loan).then_some(value) + }) + .expect("unique Begin") +} + +fn handled_view_initializer_mut(function: &mut Function, owner: LocalId) -> &mut Expression { + function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| { + let S::Let { local, value } = &mut statement.kind else { + return None; + }; + (*local == owner).then_some(value) + }) + .expect("unique owner Let") +} + +fn handled_view_raw_result<'a>( + function: &'a Function, + call: &Expression, + parameter: usize, + types: &TypeInterner, +) -> &'a Expression { + let (owner, loan) = handled_view_storage(function, call, parameter); + let packet = handled_view_packet(call); + let argument = &packet.arguments[parameter]; + let initializer = observation_initializer(function, owner); + // Authenticate the original converted initializer directly. The ordinary + // direct path instead restores the original physical View over its result. + if matches!( + initializer.kind, + E::InterfaceCoerce { .. } | E::FunctionAdapter { .. } + ) { + return hir::validate_source_handled_operand(initializer, argument, &packet.bridge, types) + .expect("original conversion") + .expect("private Handle occurrence"); + } + let projection = handled_view_projection(function, loan); + let original = crate::call_views::replace_borrowed_root(projection, initializer.clone()) + .expect("exact original temporary root"); + let backing = hir::validate_source_handled_operand(&original, argument, &packet.bridge, types) + .expect("original written View") + .expect("private Handle occurrence"); + assert_eq!(backing.ty, initializer.ty); + assert_eq!(backing.span, initializer.span); + assert_eq!(backing.kind, initializer.kind); + initializer +} + +#[test] +fn caller_acquisition_handled_written_view_owns_the_exact_optional_and_result_endpoint() { + let outcome = HANDLED_VIEW_DIRECT_SOURCE + .replace("optional[Item]", "result[Item, string]") + .replace("view(incoming handle:", "view(incoming handle error:"); + for release in [false, true] { + for source in [HANDLED_VIEW_DIRECT_SOURCE, outcome.as_str()] { + let (program, types) = transparent_retained_program(source, release); + let function = exercise(&program); + let call = handled_view_read_call(function); + let (owner, loan) = handled_view_storage(function, call, 0); + let backing = handled_view_raw_result(function, call, 0, &types); + let E::Local(output) = backing.kind else { + panic!("handled CFG output"); + }; + assert_ne!(output, owner); + assert_ne!(output, loan); + assert!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!(statement.kind, + S::SumTake { target, success: true, .. } if target == output)) + ); + let begin_block = function.blocks.iter().find(|block| block.statements.iter() + .any(|statement| matches!(statement.kind, S::BeginCallView { local, .. } if local == loan))) + .expect("begin block"); + let begin = begin_block.statements.iter().position(|statement| + matches!(statement.kind, S::BeginCallView { local, .. } if local == loan)).unwrap(); + let later_tag = begin_block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::SumTag { .. })) + .expect("later source handler selection"); + assert!( + begin < later_tag, + "endpoint captured before later source work" + ); + assert!(function.blocks.iter().flat_map(|block| &block.statements) + .any(|statement| matches!(statement.kind, S::EndCallView { local } if local == loan))); + let acquisitions = validate_function(&program, function, &types) + .expect("exact Handle CFG and storage"); + assert_eq!( + acquisitions.owner_initializers().count(), + 0, + "written View retains caller policy" + ); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("owned endpoint and scoped loan"); + } + } +} + +#[test] +fn caller_acquisition_handled_written_view_keeps_the_full_original_interface_conversion() { + for release in [false, true] { + let (program, types) = transparent_retained_program(HANDLED_VIEW_CONVERTED_SOURCE, release); + let function = exercise(&program); + let call = handled_view_read_call(function); + let (owner, _) = handled_view_storage(function, call, 0); + let initializer = observation_initializer(function, owner); + assert!(matches!(initializer.kind, E::InterfaceCoerce { .. })); + assert!(matches!(types.resolve(initializer.ty), Type::Interface(_))); + assert!(!crate::handlers::can_snapshot_view(&types, initializer.ty)); + let raw = handled_view_raw_result(function, call, 0, &types); + assert!(matches!(raw.kind, E::Local(_))); + assert!(matches!(types.resolve(raw.ty), Type::Struct(_))); + assert_ne!(raw.ty, initializer.ty); + assert_ne!(raw.span, initializer.span); + validate_function(&program, function, &types) + .expect("private raw occurrence and full conversion"); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("unsnapshotable converted owner is borrowed, never cloned"); + } +} + +const HANDLED_VIEW_LIST_GET: &str = "export function get[T](view items: list[T], index: int64) returns optional[T]:\n return list.__get_clone[T](view items, index)\n"; + +fn handled_view_reflected_program(release: bool) -> (Program, TypeInterner) { + assert!(include_str!("../../../../stdlib/list.jett").contains(HANDLED_VIEW_LIST_GET)); + let stdlib_file = jett_common::FileId::new(jett_common::STDLIB_FILE_ID_START); + let project_file = jett_common::FileId::new(0); + let mut stdlib = jett_parser::parse( + &format!("namespace list\n{HANDLED_VIEW_LIST_GET}"), + stdlib_file, + ); + let mut project = jett_parser::parse( + include_str!( + "../../../jett_driver/tests/native_conformance/scoped_call_views/12_reflected_read.jett" + ), + project_file, + ); + assert!(stdlib.errors.is_empty(), "{:?}", stdlib.errors); + assert!(project.errors.is_empty(), "{:?}", project.errors); + stdlib.module.items.append(&mut project.module.items); + let resolved = jett_resolve::resolve(&stdlib.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check_with_options( + &stdlib.module, + &resolved, + jett_typecheck::CheckOptions { release }, + ); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let origins = HashMap::from([ + (stdlib_file, jett_common::SourceOrigin::Stdlib), + (project_file, jett_common::SourceOrigin::Project), + ]); + let high = + hir::lower(&stdlib.module, &resolved, &checked, &origins).expect("exact real source HIR"); + let program = + crate::lower(&high, &checked.interner).expect("exact real reflected handled View MIR"); + (program, checked.interner) +} + +#[test] +fn caller_acquisition_handled_written_view_rejoins_the_real_reflected_field_selector() { + for release in [false, true] { + let (program, types) = handled_view_reflected_program(release); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .expect("exact real main"); + let call = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| { + let (S::Let { value, .. } | S::Evaluate(value)) = &statement.kind else { + return None; + }; + matches!( + value.kind, + E::Intrinsic { + intrinsic: hir::IntrinsicId::TypeFieldValue, + .. + } + ) + .then_some(value) + }) + .expect("real reflected getter"); + let (_, loan) = handled_view_storage(function, call, 1); + let raw = handled_view_raw_result(function, call, 1, &types); + assert!(matches!(raw.kind, E::Local(_))); + assert!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any( + |statement| matches!(statement.kind, S::EndCallView { local } if local == loan) + ) + ); + validate_function(&program, function, &types) + .expect("original reflected bridge and handled CFG"); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("caller packet remains usable after the reflected copy"); + } +} + +#[test] +fn caller_acquisition_handled_written_view_ends_on_early_return_and_keeps_nonhandle_controls() { + let source = HANDLED_VIEW_DIRECT_SOURCE.replace("default 3", "return 9"); + for release in [false, true] { + let (program, types) = transparent_retained_program(&source, release); + let function = exercise(&program); + let (_, loan) = handled_view_storage(function, handled_view_read_call(function), 0); + assert!( + function.blocks.iter().any(|block| { + matches!(&block.terminator.kind, + T::Return(Some(value)) if matches!(value.kind, E::Int(9))) + && block.statements.iter().any(|statement| + matches!(statement.kind, S::EndCallView { local } if local == loan)) + }), + "later failure ends the live producer loan before returning" + ); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("abort cleanup"); + let control = HANDLED_VIEW_DIRECT_SOURCE.replace( + "function exercise(incoming: optional[Item], later: optional[int64]) returns int64:\n return read(view(incoming handle:\n default Item(values: list(7))\n ), later handle:", + "function make() returns Item:\n return Item(values: list(7))\nfunction exercise(later: optional[int64]) returns int64:\n return read(view make(), later handle:"); + let (ordinary, ordinary_types) = transparent_retained_program(&control, release); + let ordinary_function = exercise(&ordinary); + let ordinary_call = handled_view_read_call(ordinary_function); + let packet = handled_view_packet(ordinary_call); + let hir::ArgumentStaging::Borrowed { loan } = packet.arguments[0].staging else { + panic!("existing call temporary path"); + }; + let owner = ordinary_function.local(loan).unwrap().view_source.unwrap(); + let initializer = observation_initializer(ordinary_function, owner); + assert!(matches!(initializer.kind, E::Call { .. })); + assert!( + hir::validate_source_handled_operand( + initializer, + &packet.arguments[0], + &packet.bridge, + &ordinary_types + ) + .expect("nonhandled source control") + .is_none() + ); + validate_function(&ordinary, ordinary_function, &ordinary_types) + .expect("unchanged original call temporary"); + } +} + +#[test] +fn caller_acquisition_handled_written_view_refuses_storage_occurrence_conversion_and_cfg_corruption() + { + for source in [HANDLED_VIEW_DIRECT_SOURCE, HANDLED_VIEW_CONVERTED_SOURCE] { + let (program, types) = transparent_retained_program(source, false); + for mutation in [ + "added clone", + "owner span", + "loan outer span", + "loan local span", + "begin span", + "inner span", + "inner type", + "foreign output", + "sum occurrence", + "wrong arm", + "missing initializer", + "missing begin", + "borrowed default", + "missing conversion", + ] { + if mutation == "missing conversion" && source == HANDLED_VIEW_DIRECT_SOURCE { + continue; + } + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + let (owner, loan) = handled_view_storage(function, handled_view_read_call(function), 0); + let E::Local(output) = + handled_view_raw_result(function, handled_view_read_call(function), 0, &types).kind + else { + panic!("original handled output"); + }; + match mutation { + "added clone" => { + let value = handled_view_initializer_mut(function, owner); + value.kind = E::Clone(Box::new(value.clone())); + } + "owner span" => { + let E::View(inner) = &mut handled_view_projection_mut(function, loan).kind + else { + panic!("projection"); + }; + inner.span.start += 1; + } + "loan outer span" | "loan local span" => { + let E::Call { args, .. } = &mut handled_view_read_call_mut(function).kind + else { + panic!("source call"); + }; + if mutation == "loan outer span" { + args[0].span.start += 1; + } else { + let E::View(inner) = &mut args[0].kind else { + panic!("loan"); + }; + inner.span.start += 1; + } + } + "begin span" => handled_view_projection_mut(function, loan).span.start += 1, + "inner span" | "inner type" | "foreign output" => { + let spare = function.params[0].local; + let raw = + converted_handle_leaf_mut(handled_view_initializer_mut(function, owner)); + match mutation { + "inner span" => raw.span.start += 1, + "inner type" => raw.ty = TypeInterner::INT64, + "foreign output" => raw.kind = E::Local(spare), + _ => unreachable!(), + } + } + "sum occurrence" | "wrong arm" => { + let statement = function.blocks.iter_mut().flat_map(|block| &mut block.statements) + .find(|statement| matches!(statement.kind, S::SumTake { target, success: true, .. } if target == output)) + .expect("original success definition"); + if mutation == "sum occurrence" { + statement.span.start += 1; + } else { + let S::SumTake { success, .. } = &mut statement.kind else { + unreachable!(); + }; + *success = false; + } + } + "missing initializer" | "missing begin" => { + for block in &mut function.blocks { + block.statements.retain(|statement| if mutation == "missing initializer" { + !matches!(statement.kind, S::Let { local, .. } if local == owner) + } else { !matches!(statement.kind, S::BeginCallView { local, .. } if local == loan) }); + } + } + "borrowed default" => { + let value = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + S::Let { local, value } if *local == output => Some(value), + _ => None, + }) + .expect("handled default definition"); + value.kind = E::View(Box::new(value.clone())); + } + "missing conversion" => { + let value = handled_view_initializer_mut(function, owner); + let E::InterfaceCoerce { value: raw, .. } = &value.kind else { + panic!("conversion"); + }; + value.kind = raw.kind.clone(); + } + _ => unreachable!(), + } + assert!( + validate_function(&changed, exercise(&changed), &types).is_err(), + "{mutation}" + ); + } + } +} + +#[test] +fn caller_acquisition_converted_handled_view_input_is_exact_and_reconstructed_after_preparation() { + let outcome = HANDLED_VIEW_CONVERTED_SOURCE + .replace("optional[Item]", "result[Item, string]") + .replace("view(incoming handle:", "view(incoming handle error:"); + for release in [false, true] { + for source in [HANDLED_VIEW_CONVERTED_SOURCE, outcome.as_str()] { + let (mut program, types) = transparent_retained_program(source, release); + for prepared in [false, true] { + if prepared { + crate::prepare_native_sequences(&mut program, &types); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::prepare_native_generated_functions(&mut program, &types); + } + let function = exercise(&program); + let (owner, _) = + handled_view_storage(function, handled_view_read_call(function), 0); + let conversion = observation_initializer(function, owner); + let E::InterfaceCoerce { value: input, .. } = &conversion.kind else { + panic!("original converted owner"); + }; + let E::View(raw) = &input.kind else { + panic!("original written View input"); + }; + assert!(matches!(raw.kind, E::Local(_))); + let acquisitions = validate_function(&program, function, &types) + .expect("complete original Source, CFG, owner and Begin proof"); + let proved = acquisitions + .handled_conversion_input(conversion) + .expect("only this exact conversion has its authenticated input read"); + assert!(std::ptr::eq(proved, input.as_ref())); + assert_eq!( + acquisitions.owner_initializers().count(), + 0, + "input read never acquires a source binding" + ); + let independent = conversion.clone(); + assert!( + acquisitions + .handled_conversion_input(&independent) + .is_none(), + "same type/span/tree in another occurrence is no authority" + ); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("borrowed conversion input and independent owned box"); + } + } + } +} + +#[test] +fn caller_acquisition_unassociated_identical_boxing_view_keeps_the_owning_escape_refusal() { + for release in [false, true] { + let (mut program, types) = + transparent_retained_program(HANDLED_VIEW_CONVERTED_SOURCE, release); + let function = exercise_mut(&mut program); + let (owner, _) = handled_view_storage(function, handled_view_read_call(function), 0); + let initializer = observation_initializer(function, owner).clone(); + let mut local = function + .local(owner) + .expect("converted owner metadata") + .clone(); + local.id = LocalId::new(function.locals.len() as u32); + let extra = local.id; + function.locals.push(local); + let block = function + .blocks + .iter_mut() + .find(|block| { + block.statements.iter().any( + |statement| matches!(statement.kind, S::Let { local, .. } if local == owner), + ) + }) + .expect("original converted initializer block"); + let index = block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::Let { local, .. } if local == owner)) + .unwrap(); + block.statements.insert( + index, + crate::Statement { + span: initializer.span, + kind: S::Let { + local: extra, + value: initializer, + }, + }, + ); + let function = exercise(&program); + let acquisitions = validate_function(&program, function, &types) + .expect("existing Source still authenticates only its original conversion"); + let extra_initializer = observation_initializer(function, extra); + assert!( + acquisitions + .handled_conversion_input(extra_initializer) + .is_none() + ); + let error = crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect_err("no blanket InterfaceCoerce or View-to-owner waiver"); + assert!( + error.contains("native view cannot escape into an owning value"), + "{error}" + ); + } +} + +#[test] +fn caller_acquisition_converted_handled_view_refuses_same_typed_borrowed_backing_and_lost_end() { + let parameter_source = HANDLED_VIEW_CONVERTED_SOURCE.replace( + "incoming: optional[Item], later: optional[int64])", + "incoming: optional[Item], later: optional[int64], view spare: Item)", + ); + let body = " return read(view(incoming handle:\n default Item(values: list(7))\n ), later handle:\n default 3\n )\n"; + let loop_body = " for item in view items:\n return read(view(incoming handle:\n default Item(values: list(7))\n ), later handle:\n default 3\n )\n return 0\n"; + assert!(HANDLED_VIEW_CONVERTED_SOURCE.contains(body)); + let loop_source = HANDLED_VIEW_CONVERTED_SOURCE + .replace( + "incoming: optional[Item], later: optional[int64])", + "incoming: optional[Item], later: optional[int64], items: list[Item])", + ) + .replace(body, loop_body); + for release in [false, true] { + for (source, iteration) in [ + (parameter_source.as_str(), false), + (loop_source.as_str(), true), + ] { + let (program, types) = transparent_retained_program(source, release); + let function = exercise(&program); + let (owner, _) = handled_view_storage(function, handled_view_read_call(function), 0); + let raw = + handled_view_raw_result(function, handled_view_read_call(function), 0, &types); + let replacement = if iteration { + function + .blocks + .iter() + .find_map(|block| match block.terminator.kind { + T::ForEach { + key, + value: None, + by_view: true, + .. + } => Some(key), + _ => None, + }) + .expect("logical viewed-loop endpoint") + } else { + function.params[2].local + }; + assert_eq!(function.local(replacement).unwrap().ty, raw.ty); + if iteration { + assert!( + !function.is_view_local(replacement), + "logical iteration borrowing is not an ABI/persistent view flag" + ); + } else { + assert!(function.is_view_local(replacement)); + } + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + converted_handle_leaf_mut(handled_view_initializer_mut(function, owner)).kind = + E::Local(replacement); + assert!( + validate_function(&changed, exercise(&changed), &types).is_err(), + "same-typed borrowed backing cannot mint the converted input association" + ); + } + let (mut changed, types) = + transparent_retained_program(HANDLED_VIEW_CONVERTED_SOURCE, release); + let function = exercise_mut(&mut changed); + let (_, loan) = handled_view_storage(function, handled_view_read_call(function), 0); + for block in &mut function.blocks { + block.statements.retain( + |statement| !matches!(statement.kind, S::EndCallView { local } if local == loan), + ); + } + assert!( + validate_function(&changed, exercise(&changed), &types).is_err(), + "whole exact loan lifetime is required before input-read authority" + ); + } +} diff --git a/crates/jett_mir/src/call_views.rs b/crates/jett_mir/src/call_views.rs index 34c7ee83..726acfb0 100644 --- a/crates/jett_mir/src/call_views.rs +++ b/crates/jett_mir/src/call_views.rs @@ -230,7 +230,7 @@ struct ScopeFacts { fn referenced_locals(block: &BasicBlock) -> BTreeSet { let mut block = block.clone(); let mut locals = BTreeSet::new(); - crate::sequences::prune::block_locals( + crate::sequences::prune::block_runtime_locals( &mut block, &mut |local| { locals.insert(local.index() as usize); diff --git a/crates/jett_mir/src/call_views/tests.rs b/crates/jett_mir/src/call_views/tests.rs index de7e2796..cebbbd39 100644 --- a/crates/jett_mir/src/call_views/tests.rs +++ b/crates/jett_mir/src/call_views/tests.rs @@ -397,7 +397,7 @@ fn scoped_view_origin_and_end_survive_native_local_compaction() { let stage_before = stages(original)[0]; let owner_before = original.local(stage_before).unwrap().view_source.unwrap(); let count_before = original.locals.len(); - crate::prepare_native_generated_functions(&mut program); + crate::prepare_native_generated_functions(&mut program, &types); let function = exercise(&program); validate(function, &types).unwrap(); crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); diff --git a/crates/jett_mir/src/copy_values.rs b/crates/jett_mir/src/copy_values.rs index f46b1381..3cb665f4 100644 --- a/crates/jett_mir/src/copy_values.rs +++ b/crates/jett_mir/src/copy_values.rs @@ -16,8 +16,12 @@ pub struct CopyValuePlan { pub live_after_statement: Vec>, /// Bound on owned intermediates in one full expression, including formatting. pub temporary_slots: usize, + generation_storage: crate::CallGenerationStoragePlan, } impl CopyValuePlan { + pub fn generation_storage(&self) -> &crate::CallGenerationStoragePlan { + &self.generation_storage + } pub fn analyze(function: &Function, types: &TypeInterner) -> Result { Self::analyze_storage(function, types, None) } @@ -26,6 +30,10 @@ impl CopyValuePlan { types: &TypeInterner, program: Option<&crate::Program>, ) -> Result { + 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()); + } plan_type(types, function.return_type, program)?; for local in &function.locals { plan_type(types, local.ty, program)?; @@ -44,6 +52,16 @@ impl CopyValuePlan { let mut temporaries = 0; let mut killed = None; let definition = match &statement.kind { + StatementKind::OpenCallOwnerGeneration { .. } + | StatementKind::CloseCallOwnerGeneration { .. } => None, + StatementKind::ReplaceCallOwnerGeneration { + root, rhs_owner, .. + } => { + reads.insert(root.index() as usize); + reads.insert(rhs_owner.index() as usize); + killed = Some(rhs_owner.index() as usize); + Some(root.index() as usize) + } StatementKind::ReflectedContainerReady { source, .. } => { reads.insert(source.index() as usize); None @@ -344,6 +362,7 @@ impl CopyValuePlan { live_out, live_after_statement: after, temporary_slots: max_temporaries, + generation_storage, }) } } diff --git a/crates/jett_mir/src/generated_functions.rs b/crates/jett_mir/src/generated_functions.rs index 395ec284..facee1f0 100644 --- a/crates/jett_mir/src/generated_functions.rs +++ b/crates/jett_mir/src/generated_functions.rs @@ -7,15 +7,15 @@ mod tests; /// Compact generated inline functions independently of their enclosing function. /// Enclosing functions lose only unused local slots, preserving every block. /// Capture and ordinary parameter order remain the existing callable ABI. -pub fn prepare_native_generated_functions(program: &mut Program) { - if validate(program).is_err() { +pub fn prepare_native_generated_functions(program: &mut Program, types: &TypeInterner) { + if validate_call_ownership(program, types).is_err() { return; } for function in &mut program.functions { - if function.debug_kind == hir::FunctionDebugKind::Inline { - sequences::prune::unreachable(function); + let _prepared = if function.debug_kind == hir::FunctionDebugKind::Inline { + sequences::prune::unreachable(function) } else { - sequences::prune::unused_locals(function); - } + sequences::prune::unused_locals(function) + }; } } diff --git a/crates/jett_mir/src/generated_functions/tests.rs b/crates/jett_mir/src/generated_functions/tests.rs index e16f1946..09d8c3b7 100644 --- a/crates/jett_mir/src/generated_functions/tests.rs +++ b/crates/jett_mir/src/generated_functions/tests.rs @@ -122,7 +122,7 @@ fn live_captured_callback_discards_parent_never_slots_without_changing_capture_a .filter(|function| function.debug_kind != hir::FunctionDebugKind::Inline) .cloned() .collect::>(); - prepare_native_generated_functions(&mut program); + prepare_native_generated_functions(&mut program, &types); validate(&program).unwrap(); let function = &program.functions[original.id.index() as usize]; assert_dense_reachable(function); @@ -173,7 +173,7 @@ fn live_captured_callback_discards_parent_never_slots_without_changing_capture_a move_values::MoveValuePlan::analyze(&program, function, &types) .expect("captured parameter and body references remain valid"); let prepared = program.clone(); - prepare_native_generated_functions(&mut program); + prepare_native_generated_functions(&mut program, &types); assert_eq!(program, prepared); } @@ -199,7 +199,7 @@ function main() returns int64: .count(), 2 ); - prepare_native_generated_functions(&mut program); + prepare_native_generated_functions(&mut program, &types); validate(&program).unwrap(); assert_eq!(program.functions.len(), original.functions.len()); for (function, previous) in program.functions.iter().zip(&original.functions) { @@ -260,7 +260,7 @@ function main() returns int64: .unwrap() .clone(); assert_eq!(original.params[0].ty, TypeInterner::NEVER); - prepare_native_generated_functions(&mut program); + prepare_native_generated_functions(&mut program, &types); validate(&program).unwrap(); let function = &program.functions[original.id.index() as usize]; assert_dense_reachable(function); @@ -277,7 +277,7 @@ function main() returns int64: #[test] fn generated_metadata_selection_preserves_named_functions_even_with_inline_like_names() { - let (mut program, _) = lower_source(AFTER_LOOP); + let (mut program, types) = lower_source(AFTER_LOOP); let function = program .functions .iter_mut() @@ -287,7 +287,7 @@ fn generated_metadata_selection_preserves_named_functions_even_with_inline_like_ function.debug_kind = hir::FunctionDebugKind::Named("app.source_visible_name".into()); validate(&program).unwrap(); let original = program.clone(); - prepare_native_generated_functions(&mut program); + prepare_native_generated_functions(&mut program, &types); validate(&program).unwrap(); for (function, previous) in program.functions.iter().zip(&original.functions) { assert_named_control_flow_and_abi_preserved(function, previous); @@ -297,7 +297,7 @@ fn generated_metadata_selection_preserves_named_functions_even_with_inline_like_ #[test] fn generated_compaction_does_not_hide_invalid_unreachable_or_parameter_ids() { for corruption in 0..3 { - let (mut program, _) = lower_source(AFTER_LOOP); + let (mut program, types) = lower_source(AFTER_LOOP); let function = program .functions .iter_mut() @@ -320,7 +320,7 @@ fn generated_compaction_does_not_hide_invalid_unreachable_or_parameter_ids() { } assert!(validate(&program).is_err()); let original = program.clone(); - prepare_native_generated_functions(&mut program); + prepare_native_generated_functions(&mut program, &types); assert_eq!(program, original); assert!(validate(&program).is_err()); } @@ -362,7 +362,7 @@ function root() returns int64: assert_eq!(descriptor.params[1].mode, ParamMode::Owned); let mut compacted = original.clone(); - prepare_native_generated_functions(&mut compacted); + prepare_native_generated_functions(&mut compacted, &types); validate(&compacted).unwrap(); let compacted_factory = &compacted.functions[factory_id.index() as usize]; assert_named_control_flow_and_abi_preserved(compacted_factory, factory); @@ -420,7 +420,7 @@ function root() returns int64: }); validate(&with_dependency).unwrap(); let before = with_dependency.functions[factory_id.index() as usize].clone(); - prepare_native_generated_functions(&mut with_dependency); + prepare_native_generated_functions(&mut with_dependency, &types); validate(&with_dependency).unwrap(); let factory = &with_dependency.functions[factory_id.index() as usize]; assert_named_control_flow_and_abi_preserved(factory, &before); diff --git a/crates/jett_mir/src/handlers.rs b/crates/jett_mir/src/handlers.rs index c7795e59..a0f26390 100644 --- a/crates/jett_mir/src/handlers.rs +++ b/crates/jett_mir/src/handlers.rs @@ -5,80 +5,106 @@ use jett_types::{Type, TypeInterner}; mod reflected; +#[derive(Clone, Copy, PartialEq, Eq)] +enum LoweringRequirement { + #[cfg(test)] + Handlers, + HandlersAndCallOwnership, +} + +#[cfg(test)] fn has_extractable_handle(expression: &Expression) -> bool { + expression_needs_lowering(expression, LoweringRequirement::Handlers) +} + +fn needs_eager_lowering(expression: &Expression) -> bool { + expression_needs_lowering(expression, LoweringRequirement::HandlersAndCallOwnership) +} + +fn expression_needs_lowering(expression: &Expression, requirement: LoweringRequirement) -> bool { + let needs = |value: &Expression| expression_needs_lowering(value, requirement); + let includes_call_ownership = requirement == LoweringRequirement::HandlersAndCallOwnership; match &expression.kind { ExpressionKind::Handle { kind: HandleKind::Result | HandleKind::Optional | HandleKind::Refinement { .. }, .. } => true, - ExpressionKind::View(value) | ExpressionKind::FunctionAdapter { value, .. } => has_extractable_handle(value), - ExpressionKind::Clone(value) => has_extractable_handle(value), - ExpressionKind::Run(value) | ExpressionKind::Join(value) => has_extractable_handle(value), - ExpressionKind::Coarsen(value) | ExpressionKind::Declassify(value) + ExpressionKind::View(value) | ExpressionKind::FunctionAdapter { value, .. } => needs(value), + ExpressionKind::Clone(value) => needs(value), + ExpressionKind::Run(value) | ExpressionKind::Join(value) => needs(value), + ExpressionKind::Coarsen(value) + | ExpressionKind::Declassify(value) | ExpressionKind::RefinementValidated(value) | ExpressionKind::DisplayResult(value) | ExpressionKind::EquatableResult(value) - | ExpressionKind::RuntimeFailureMessage(value) - | ExpressionKind::InterfaceCoerce { value, .. } | ExpressionKind::InterfaceType(value) => { - has_extractable_handle(value) - } - ExpressionKind::Field { base, .. } => has_extractable_handle(base), - ExpressionKind::StateIs { value, .. } => has_extractable_handle(value), - ExpressionKind::Unary { value, .. } => has_extractable_handle(value), - ExpressionKind::Binary { left, right, .. } => { - has_extractable_handle(left) || has_extractable_handle(right) - } - ExpressionKind::Call { args, .. } => args.iter().any(has_extractable_handle), - ExpressionKind::ActorSpawn { args, .. } => args.iter().any(has_extractable_handle), - ExpressionKind::ActorMessage { actor, args, .. } => { - has_extractable_handle(actor) || args.iter().any(has_extractable_handle) + | ExpressionKind::RuntimeFailureMessage(value) + | ExpressionKind::InterfaceCoerce { value, .. } + | ExpressionKind::InterfaceType(value) => needs(value), + ExpressionKind::Field { base, .. } => needs(base), + ExpressionKind::StateIs { value, .. } => needs(value), + ExpressionKind::Unary { value, .. } => needs(value), + ExpressionKind::Binary { left, right, .. } => needs(left) || needs(right), + ExpressionKind::Call { + args, ownership, .. + } => { + (includes_call_ownership && needs_call_owner_staging(ownership)) + || args.iter().any(needs) } + ExpressionKind::ActorSpawn { args, .. } => args.iter().any(needs), + ExpressionKind::ActorMessage { actor, args, .. } => needs(actor) || args.iter().any(needs), ExpressionKind::Intrinsic { args, refinement_predicates, field_validation, + ownership, .. } => { - args.iter().any(has_extractable_handle) + (includes_call_ownership && needs_call_owner_staging(ownership)) + || args.iter().any(needs) || refinement_predicates.iter().any(|chain| !chain.is_empty()) - || matches!(field_validation, Some(hir::ReflectedFieldValidation::Validate(_))) - } - ExpressionKind::IndirectCall { callee, args, .. } => { - has_extractable_handle(callee) || args.iter().any(has_extractable_handle) - } - ExpressionKind::ListConstruct { elements } => elements.iter().any(has_extractable_handle), - ExpressionKind::StringInterpolation(segments) => segments.iter().any(|segment| { - matches!(segment, hir::StringSegment::Value(value) if has_extractable_handle(value)) - }), - ExpressionKind::MapConstruct { entries } => entries.iter().any(|entry| { - has_extractable_handle(&entry.key) || has_extractable_handle(&entry.value) - }), + || matches!( + field_validation, + Some(hir::ReflectedFieldValidation::Validate(_)) + ) + } + ExpressionKind::IndirectCall { + callee, + args, + ownership, + .. + } => { + (includes_call_ownership && needs_call_owner_staging(ownership)) + || needs(callee) + || args.iter().any(needs) + } + ExpressionKind::ListConstruct { elements } => elements.iter().any(needs), + ExpressionKind::StringInterpolation(segments) => segments + .iter() + .any(|segment| matches!(segment, hir::StringSegment::Value(value) if needs(value))), + ExpressionKind::MapConstruct { entries } => entries + .iter() + .any(|entry| needs(&entry.key) || needs(&entry.value)), ExpressionKind::StructConstruct { fields, refinement_predicates, .. } => { - fields.iter().any(has_extractable_handle) - || refinement_predicates.iter().any(|chain| !chain.is_empty()) - } - ExpressionKind::BitfieldConstruct { fields, .. } => { - fields.iter().any(has_extractable_handle) + fields.iter().any(needs) || refinement_predicates.iter().any(|chain| !chain.is_empty()) } + ExpressionKind::BitfieldConstruct { fields, .. } => fields.iter().any(needs), ExpressionKind::EnumConstruct { payloads, .. } - | ExpressionKind::MachineConstruct { payloads, .. } => { - payloads.iter().any(has_extractable_handle) - } + | ExpressionKind::MachineConstruct { payloads, .. } => payloads.iter().any(needs), ExpressionKind::MachineTransition { source, payloads, .. - } => has_extractable_handle(source) || payloads.iter().any(has_extractable_handle), + } => needs(source) || payloads.iter().any(needs), ExpressionKind::ResultOk(value) | ExpressionKind::ResultFail(value) - | ExpressionKind::OptionalSome(value) => has_extractable_handle(value), + | ExpressionKind::OptionalSome(value) => needs(value), _ => false, } } -fn can_snapshot_view(types: &TypeInterner, ty: TypeId) -> bool { +pub(super) fn can_snapshot_view(types: &TypeInterner, ty: TypeId) -> bool { fn supported( types: &TypeInterner, ty: TypeId, @@ -201,16 +227,130 @@ fn stable_deferred_view(locals: &[Local], expression: &Expression) -> bool { .is_some_and(|local| local.id == root && !local.mutable) } -/// Only original explicit borrowed positions may receive an internal borrow -/// scope. Formal-mode wrappers cannot convert a bare owned argument into one. +/// Preserve an already-checked retained place until its one endpoint snapshot. +/// Generic value lowering may snapshot a Local below Coarsen/Declassify; that +/// inner acquisition would change the original Source binding/field witness. +fn retained_snapshot_place(expression: &Expression) -> Option { + let mut place = expression; + loop { + place = match &place.kind { + ExpressionKind::Local(_) => return Some(expression.clone()), + ExpressionKind::Field { base, .. } => base, + ExpressionKind::View(value) + | ExpressionKind::Coarsen(value) + | ExpressionKind::Declassify(value) => value, + _ => return None, + }; + } +} + +/// Ordered staging distinguishes actual borrows from an endpoint whose checked +/// Source effect requires an owning transfer before physical View access. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum OrderedCallViewInput { + Unstaged, + Borrowed, + SourceOwningTransfer, + SourceHandledTemporary, +} + +/// Only the private original Source Handle witness can authorize this storage. +/// The physical View wrapper is distinct from its checked conversion endpoint. +fn source_handled_view_endpoint<'a>( + types: &TypeInterner, + ownership: &hir::CallOwnership, + index: usize, + expression: &'a Expression, +) -> Option<&'a Expression> { + use jett_typecheck::{ + CheckedCalleeAccess as Access, CheckedCallerEffect as Effect, CheckedCallerSyntax as Syntax, + }; + let hir::CallOwnership::Source(source) = ownership else { + return None; + }; + let argument = source.arguments.get(index)?; + if argument.staging != hir::ArgumentStaging::Original + || argument.origin != hir::CallerOrigin::OwnedExpression + || argument.syntax != Syntax::WrittenView + || argument.effect != Effect::RetainBorrow + || argument.physical_access != Access::View + { + return None; + } + let ExpressionKind::View(endpoint) = &expression.kind else { + return None; + }; + if endpoint.ty != expression.ty || matches!(endpoint.kind, ExpressionKind::View(_)) { + return None; + } + let original = + hir::validate_source_handled_operand(expression, argument, &source.bridge, types) + .ok() + .flatten() + .or_else(|| { + // A canonical physical View can wrap the original conversion tree. + // That tree must itself rejoin the same private checked occurrence. + hir::validate_source_handled_operand(endpoint, argument, &source.bridge, types) + .ok() + .flatten() + })?; + matches!( + original.kind, + ExpressionKind::Handle { + kind: HandleKind::Optional | HandleKind::Result, + .. + } + ) + .then_some(endpoint.as_ref()) +} + +fn ordered_call_view_input(ownership: &hir::CallOwnership, index: usize) -> OrderedCallViewInput { + use jett_typecheck::{CheckedCalleeAccess as Access, CheckedCallerEffect as Effect}; + match ownership { + hir::CallOwnership::Source(source) => { + let Some(argument) = source.arguments.get(index) else { + return OrderedCallViewInput::Unstaged; + }; + if argument.staging != hir::ArgumentStaging::Original + || argument.physical_access != Access::View + { + return OrderedCallViewInput::Unstaged; + } + match argument.effect { + Effect::RelinquishOwned => OrderedCallViewInput::SourceOwningTransfer, + Effect::TransferOwned if source.bridge != hir::CallBridge::Direct => { + OrderedCallViewInput::SourceOwningTransfer + } + Effect::RetainBorrow | Effect::ObserveData => OrderedCallViewInput::Borrowed, + _ => OrderedCallViewInput::Unstaged, + } + } + hir::CallOwnership::Generated(generated) => { + if generated.arguments.get(index).is_some_and(|argument| { + argument.callee_access == Access::View + && matches!( + argument.acquisition, + hir::GeneratedAcquisition::Borrow { .. } | hir::GeneratedAcquisition::Copy + ) + }) { + OrderedCallViewInput::Borrowed + } else { + OrderedCallViewInput::Unstaged + } + } + } +} + +/// Retained/generated raw borrows keep their existing origin eligibility. +/// A checked Source owning transfer instead materializes the full endpoint. fn valid_ordered_borrowed_values( types: &TypeInterner, locals: &[Local], values: &[Expression], order: &[usize], - eligible: &[bool], + inputs: &[OrderedCallViewInput], ) -> bool { - eligible.len() == values.len() + inputs.len() == values.len() && order.len() == values.len() && order.iter().all(|&index| index < values.len()) && order @@ -221,14 +361,62 @@ fn valid_ordered_borrowed_values( == values.len() && values.iter().enumerate().all(|(index, value)| { !matches!(value.kind, ExpressionKind::View(_)) + || matches!( + inputs[index], + OrderedCallViewInput::SourceOwningTransfer + | OrderedCallViewInput::SourceHandledTemporary + ) || can_snapshot_view(types, value.ty) || stable_deferred_view(locals, value) - || (eligible[index] + || (inputs[index] == OrderedCallViewInput::Borrowed && (crate::call_views::borrowed_source(types, locals, value).is_some() || crate::call_views::temporary_root(types, locals, value).is_some())) }) } +#[derive(Clone, Copy)] +enum ObservationSnapshot { + Direct, + Converted { + actual_type: TypeId, + source_span: Span, + }, +} + +fn observation_snapshot( + types: &TypeInterner, + value: &Expression, + ownership: &hir::CallOwnership, + index: usize, +) -> Option { + let hir::CallOwnership::Source(source) = ownership else { + return None; + }; + let argument = source.arguments.get(index)?; + argument.observation_proof()?; + if can_snapshot_view(types, value.ty) { + return Some(ObservationSnapshot::Direct); + } + let ExpressionKind::InterfaceCoerce { .. } = &value.kind else { + return None; + }; + let mut raw = value; + while let ExpressionKind::InterfaceCoerce { value, .. } = &raw.kind { + raw = value; + } + if raw.ty != argument.actual_type + || raw.span != argument.source_span + || !hir::observation_data_type(types, raw.ty) + || !can_snapshot_view(types, raw.ty) + { + return None; + } + Some(ObservationSnapshot::Converted { + actual_type: argument.actual_type, + source_span: argument.source_span, + }) +} + fn snapshotable_local(types: &TypeInterner, expression: &Expression) -> bool { if let ExpressionKind::InterfaceCoerce { value, .. } = &expression.kind { return snapshotable_local(types, value); @@ -291,7 +479,476 @@ fn call_staging_inputs(values: &[Expression], modes: &[ParamMode]) -> Option bool { + (0..ownership.parameter_count()).any(|index| { + ownership.source_parameter_effect(index) + == Some(jett_typecheck::CheckedCallerEffect::ObserveData) + || (ownership.parameter_physical_access(index) + == Some(jett_typecheck::CheckedCalleeAccess::View) + && matches!( + ownership.source_parameter_effect(index), + Some( + jett_typecheck::CheckedCallerEffect::RelinquishOwned + | jett_typecheck::CheckedCallerEffect::TransferOwned + ) + )) + }) +} + impl Builder<'_> { + /// Stage a checked invocation in lexical source order. Physical borrowing + /// never grants retention: a relinquished input first acquires its own slot. + fn lower_ordered_call_values( + &mut self, + original_call: &Expression, + values: &[Expression], + order: &[usize], + ownership: &mut hir::CallOwnership, + ) -> Option> { + // 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(); + let original_ownership = ownership.clone(); + let result = self.try_lower_ordered_call_values(original_call, values, order, ownership); + if result.is_none() { + *self = checkpoint; + *ownership = original_ownership; + } + result + } + + fn lower_converted_observation_snapshot( + &mut self, + value: &Expression, + actual_type: TypeId, + source_span: Span, + ) -> Option { + if let ExpressionKind::InterfaceCoerce { + value: raw, + adapters, + } = &value.kind + { + return Some(Expression { + kind: ExpressionKind::InterfaceCoerce { + value: Box::new(self.lower_converted_observation_snapshot( + raw, + actual_type, + source_span, + )?), + adapters: adapters.clone(), + }, + ty: value.ty, + span: value.span, + }); + } + if value.ty != actual_type || value.span != source_span { + return None; + } + Some(Expression { + kind: ExpressionKind::Clone(Box::new(self.lower_value(value))), + ty: value.ty, + span: value.span, + }) + } + + fn try_lower_ordered_call_values( + &mut self, + original_call: &Expression, + values: &[Expression], + order: &[usize], + ownership: &mut hir::CallOwnership, + ) -> Option> { + if ownership.parameter_count() != values.len() { + return None; + } + // Authenticate original handled endpoints before scopes/CFG are changed. + let handled = values + .iter() + .enumerate() + .map(|(index, value)| { + source_handled_view_endpoint(self.types, ownership, index, value).cloned() + }) + .collect::>(); + let inputs = (0..values.len()) + .map(|index| { + if handled[index].is_some() { + OrderedCallViewInput::SourceHandledTemporary + } else { + ordered_call_view_input(ownership, index) + } + }) + .collect::>(); + if !valid_ordered_borrowed_values(self.types, &self.locals, values, order, &inputs) { + return None; + } + if let hir::CallOwnership::Source(source) = ownership { + if source + .arguments + .iter() + .any(|argument| argument.staging != hir::ArgumentStaging::Original) + { + return None; + } + } + // Refuse an unsupported snapshot before changing the builder or scopes. + if values.iter().enumerate().any(|(index, value)| { + ownership.source_parameter_effect(index) + == Some(jett_typecheck::CheckedCallerEffect::ObserveData) + && ownership.parameter_physical_access(index) + == Some(jett_typecheck::CheckedCalleeAccess::Owned) + && observation_snapshot(self.types, value, ownership, index).is_none() + }) { + return None; + } + self.call_view_scopes.push(Vec::new()); + self.generation_capture + .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; + } + 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 { + 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: 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()?; + } + } + Some(lowered) + } + + fn lower_generated_call_view( + &mut self, + index: usize, + value: &Expression, + ownership: &mut hir::CallOwnership, + ) -> Option { + let hir::CallOwnership::Generated(generated) = ownership else { + return None; + }; + let argument = generated.arguments.get(index)?; + if argument.staging != (hir::GeneratedArgumentStaging::Existing { loan: None }) { + return None; + } + let witness = argument.original_witness(); + let producer = witness.owned_producer(); + let snapshot = + witness.ordinary_snapshot_proof().is_some() && can_snapshot_view(self.types, value.ty); + let ExpressionKind::View(endpoint) = &value.kind else { + return None; + }; + let (source, projection, backing) = if producer || snapshot { + // Capture the full endpoint at this lexical argument position. + // A produced owner moves once; ordinary data uses its sealed copy. + let initial = if producer { + self.lower_value(endpoint) + } else { + Expression { + kind: ExpressionKind::Clone(Box::new(self.lower_value(value))), + ty: value.ty, + span: value.span, + } + }; + let owner = self.temporary(initial.ty, initial.span); + self.push( + StatementKind::Let { + local: owner, + value: initial, + }, + value.span, + ); + let projection = Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(owner), + ty: value.ty, + span: endpoint.span, + })), + ty: value.ty, + span: value.span, + }; + (owner, projection, Some((owner, producer))) + } else { + let source = crate::call_views::borrowed_source(self.types, &self.locals, value)?; + (source, value.clone(), None) + }; + let loan = self.call_view_temporary(projection.ty, source, projection.span); + let staging = match backing { + Some((owner, true)) => hir::GeneratedArgumentStaging::OwnedProducer { owner, loan }, + Some((owner, false)) => hir::GeneratedArgumentStaging::OrdinarySnapshot { owner, loan }, + None => hir::GeneratedArgumentStaging::Existing { loan: Some(loan) }, + }; + ownership.stage_generated_parameter(index, staging).ok()?; + self.push( + StatementKind::BeginCallView { + local: loan, + value: projection.clone(), + }, + projection.span, + ); + self.call_view_scopes.last_mut()?.push(loan); + Some(Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(loan), + ty: projection.ty, + span: projection.span, + })), + ty: projection.ty, + span: projection.span, + }) + } + + fn stage_checked_call_view( + &mut self, + index: usize, + acquired_owner: Option, + source: LocalId, + projection: Expression, + ownership: &mut hir::CallOwnership, + ) -> Option { + let generation = if acquired_owner.is_none() { + self.generation_capture + .candidate( + index, + source, + &projection, + self.current, + self.blocks[self.current.index() as usize].statements.len(), + &self.locals, + self.types, + ) + .ok()? + } else { + None + }; + if let Some((generation, true)) = generation { + self.push( + StatementKind::OpenCallOwnerGeneration { + generation, + root: source, + }, + projection.span, + ); + } + let local = self.call_view_temporary(projection.ty, source, projection.span); + if let Some((generation, _)) = generation { + self.generation_capture + .capture_loan( + generation, + local, + index, + self.current, + self.blocks[self.current.index() as usize].statements.len(), + ) + .ok()?; + } + ownership + .stage_parameter(index, acquired_owner, local) + .ok()?; + self.push( + StatementKind::BeginCallView { + local, + value: projection.clone(), + }, + projection.span, + ); + self.call_view_scopes.last_mut()?.push(local); + Some(Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(local), + ty: projection.ty, + span: projection.span, + })), + ty: projection.ty, + span: projection.span, + }) + } + fn refinement_predicate_input( &self, source: LocalId, @@ -326,7 +983,7 @@ impl Builder<'_> { input } - fn temporary(&mut self, ty: TypeId, span: Span) -> LocalId { + pub(super) fn temporary(&mut self, ty: TypeId, span: Span) -> LocalId { let id = LocalId::new(self.locals.len() as u32); self.locals.push(Local { id, @@ -390,64 +1047,13 @@ impl Builder<'_> { Some((source, rewritten)) } - fn lower_ordered_borrowed_values( - &mut self, - values: &[Expression], - order: &[usize], - eligible: &[bool], - ) -> Option> { - if !valid_ordered_borrowed_values(self.types, &self.locals, values, order, eligible) { - return None; - } - self.call_view_scopes.push(Vec::new()); - let mut lowered = values.to_vec(); - for &index in order { - if eligible[index] - && !can_snapshot_view(self.types, values[index].ty) - && let Some((source, projection)) = self.call_view_initializer(&values[index]) - { - let local = self.call_view_temporary(values[index].ty, source, values[index].span); - self.push( - StatementKind::BeginCallView { - local, - value: projection, - }, - values[index].span, - ); - if let Some(scope) = self.call_view_scopes.last_mut() { - scope.push(local); - } - lowered[index] = Expression { - kind: ExpressionKind::View(Box::new(Expression { - kind: ExpressionKind::Local(local), - ty: values[index].ty, - span: values[index].span, - })), - ty: values[index].ty, - span: values[index].span, - }; - continue; - } - // The established snapshot/deferred-local route is unchanged for - // every other value. Global eligibility above validates this input. - let Some(single) = - self.lower_ordered_owned_values(std::slice::from_ref(&values[index]), &[0]) - else { - self.call_view_scopes.pop(); - return None; - }; - if let Some(value) = single.into_iter().next() { - lowered[index] = value; - } - } - Some(lowered) - } - fn finish_call_view_scope(&mut self, expression: Expression) -> Expression { let Some(scope) = self.call_view_scopes.pop() else { return expression; }; + let generations = self.generation_capture.end_scope(); if scope.is_empty() { + debug_assert!(generations.is_empty()); return expression; } // Materialize the complete consuming operation before ending its loans. @@ -462,6 +1068,13 @@ impl Builder<'_> { for stage in scope.into_iter().rev() { self.push(StatementKind::EndCallView { local: stage }, expression.span); } + for generation in generations.into_iter().rev() { + self.push( + StatementKind::CloseCallOwnerGeneration { generation }, + expression.span, + ); + self.generation_capture.stop(generation); + } Expression { kind: ExpressionKind::Local(local), ty: expression.ty, @@ -480,6 +1093,9 @@ impl Builder<'_> { for local in abandoned { self.push(StatementKind::EndCallView { local }, span); } + for generation in self.generation_capture.abandoned(depth) { + self.push(StatementKind::CloseCallOwnerGeneration { generation }, span); + } } fn lower_ordered_owned_values( @@ -570,7 +1186,7 @@ impl Builder<'_> { return lowered; } if let ExpressionKind::Clone(value) = &expression.kind - && has_extractable_handle(value) + && needs_eager_lowering(value) { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::Clone(Box::new(self.lower_value(value))); @@ -578,7 +1194,7 @@ impl Builder<'_> { } if let ExpressionKind::Run(value) = &expression.kind { let needs_snapshot = snapshotable_local(self.types, value); - if has_extractable_handle(value) || needs_snapshot { + if needs_eager_lowering(value) || needs_snapshot { let mut lowered = expression.clone(); let source = self.lower_value(value); // `run` does not consume a cloneable local in the interpreter. @@ -592,14 +1208,14 @@ impl Builder<'_> { } } if let ExpressionKind::Join(value) = &expression.kind - && has_extractable_handle(value) + && needs_eager_lowering(value) { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::Join(Box::new(self.lower_value(value))); return lowered; } if let ExpressionKind::Coarsen(value) = &expression.kind - && (has_extractable_handle(value) || snapshotable_local(self.types, value)) + && (needs_eager_lowering(value) || snapshotable_local(self.types, value)) { let mut lowered = expression.clone(); let source = self.lower_value(value); @@ -612,7 +1228,7 @@ impl Builder<'_> { return lowered; } if let ExpressionKind::Declassify(value) = &expression.kind - && (has_extractable_handle(value) || snapshotable_local(self.types, value)) + && (needs_eager_lowering(value) || snapshotable_local(self.types, value)) { let mut lowered = expression.clone(); let source = self.lower_value(value); @@ -629,7 +1245,7 @@ impl Builder<'_> { owner_type, field, } = &expression.kind - && has_extractable_handle(base) + && needs_eager_lowering(base) { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::Field { @@ -640,7 +1256,7 @@ impl Builder<'_> { return lowered; } if let ExpressionKind::StateIs { value, state } = &expression.kind - && has_extractable_handle(value) + && needs_eager_lowering(value) { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::StateIs { @@ -650,7 +1266,7 @@ impl Builder<'_> { return lowered; } if let ExpressionKind::Unary { op, value } = &expression.kind - && has_extractable_handle(value) + && needs_eager_lowering(value) { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::Unary { @@ -661,10 +1277,10 @@ impl Builder<'_> { } if let ExpressionKind::Binary { left, op, right } = &expression.kind && matches!(op, hir::BinaryOp::And | hir::BinaryOp::Or) - && (has_extractable_handle(left) || has_extractable_handle(right)) + && (needs_eager_lowering(left) || needs_eager_lowering(right)) { let left_value = self.lower_value(left); - if !has_extractable_handle(right) { + if !needs_eager_lowering(right) { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::Binary { left: Box::new(left_value), @@ -736,7 +1352,7 @@ impl Builder<'_> { if let ExpressionKind::Binary { left, op, right } = &expression.kind && !matches!(op, hir::BinaryOp::And | hir::BinaryOp::Or) && can_snapshot_view(self.types, left.ty) - && (has_extractable_handle(left) || has_extractable_handle(right)) + && (needs_eager_lowering(left) || needs_eager_lowering(right)) { // Save the left value before extracting a handler from the right. // Its failure block may mutate locals that the left side reads. @@ -832,7 +1448,7 @@ impl Builder<'_> { return self.lower_refinement_builder_finish(expression, refinement_predicates); } if let ExpressionKind::ListConstruct { elements } = &expression.kind - && elements.iter().any(has_extractable_handle) + && elements.iter().any(needs_eager_lowering) && let Some(elements) = self.lower_ordered_owned_values(elements, &(0..elements.len()).collect::>()) { @@ -842,7 +1458,7 @@ impl Builder<'_> { } if let ExpressionKind::StringInterpolation(segments) = &expression.kind && segments.iter().any(|segment| { - matches!(segment, hir::StringSegment::Value(value) if has_extractable_handle(value)) + matches!(segment, hir::StringSegment::Value(value) if needs_eager_lowering(value)) }) { let values = segments @@ -871,9 +1487,9 @@ impl Builder<'_> { } } if let ExpressionKind::MapConstruct { entries } = &expression.kind - && entries.iter().any(|entry| { - has_extractable_handle(&entry.key) || has_extractable_handle(&entry.value) - }) + && entries + .iter() + .any(|entry| needs_eager_lowering(&entry.key) || needs_eager_lowering(&entry.value)) { let values: Vec<_> = entries .iter() @@ -901,7 +1517,7 @@ impl Builder<'_> { validates_refinements, refinement_predicates, } = &expression.kind - && fields.iter().any(has_extractable_handle) + && fields.iter().any(needs_eager_lowering) && let Some(fields) = self.lower_ordered_owned_values(fields, evaluation_order) { let mut lowered = expression.clone(); @@ -920,7 +1536,7 @@ impl Builder<'_> { evaluation_order, validates_widths, } = &expression.kind - && fields.iter().any(has_extractable_handle) + && fields.iter().any(needs_eager_lowering) && let Some(fields) = self.lower_ordered_owned_values(fields, evaluation_order) { let mut lowered = expression.clone(); @@ -938,7 +1554,7 @@ impl Builder<'_> { payloads, evaluation_order, } = &expression.kind - && payloads.iter().any(has_extractable_handle) + && payloads.iter().any(needs_eager_lowering) && let Some(payloads) = self.lower_ordered_owned_values(payloads, evaluation_order) { let mut lowered = expression.clone(); @@ -955,7 +1571,7 @@ impl Builder<'_> { state, payloads, } = &expression.kind - && payloads.iter().any(has_extractable_handle) + && payloads.iter().any(needs_eager_lowering) && let Some(payloads) = self.lower_ordered_owned_values(payloads, &(0..payloads.len()).collect::>()) { @@ -973,7 +1589,7 @@ impl Builder<'_> { target, payloads, } = &expression.kind - && (has_extractable_handle(source) || payloads.iter().any(has_extractable_handle)) + && (needs_eager_lowering(source) || payloads.iter().any(needs_eager_lowering)) { let values = std::iter::once(source.as_ref().clone()) .chain(payloads.iter().cloned()) @@ -991,21 +1607,21 @@ impl Builder<'_> { } } if let ExpressionKind::ResultOk(value) = &expression.kind - && has_extractable_handle(value) + && needs_eager_lowering(value) { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::ResultOk(Box::new(self.lower_value(value))); return lowered; } if let ExpressionKind::ResultFail(value) = &expression.kind - && has_extractable_handle(value) + && needs_eager_lowering(value) { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::ResultFail(Box::new(self.lower_value(value))); return lowered; } if let ExpressionKind::OptionalSome(value) = &expression.kind - && has_extractable_handle(value) + && needs_eager_lowering(value) { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::OptionalSome(Box::new(self.lower_value(value))); @@ -1019,22 +1635,18 @@ impl Builder<'_> { field_validation, args, evaluation_order, + ownership, } = &expression.kind - && args.iter().any(has_extractable_handle) - && args.iter().enumerate().all(|(index, arg)| { - !crate::move_values::intrinsic_borrows(*intrinsic, index, arg) - || can_snapshot_view(self.types, arg.ty) - || stable_deferred_view(&self.locals, arg) - || crate::call_views::borrowed_source(self.types, &self.locals, arg).is_some() - || crate::call_views::temporary_root(self.types, &self.locals, arg).is_some() - }) + && (args.iter().any(needs_eager_lowering) || needs_call_owner_staging(ownership)) { + // Normalize closed physical View roles before the common preflight + // distinguishes a source owner transfer from a retained/raw borrow. let inputs = args .iter() .enumerate() .map(|(index, arg)| { - if crate::move_values::intrinsic_borrows(*intrinsic, index, arg) - && crate::move_values::is_linear(self.types, arg.ty) + if ownership.parameter_physical_access(index) + == Some(jett_typecheck::CheckedCalleeAccess::View) && !matches!(arg.kind, ExpressionKind::View(_)) { Expression { @@ -1047,17 +1659,13 @@ impl Builder<'_> { } }) .collect::>(); - let eligible = args - .iter() - .enumerate() - .map(|(index, arg)| { - crate::move_values::intrinsic_borrows(*intrinsic, index, arg) - && matches!(arg.kind, ExpressionKind::View(_)) - }) - .collect::>(); - if let Some(args) = - self.lower_ordered_borrowed_values(&inputs, evaluation_order, &eligible) - { + let mut ownership = ownership.clone(); + if let Some(args) = self.lower_ordered_call_values( + expression, + &inputs, + evaluation_order, + &mut ownership, + ) { let mut lowered = expression.clone(); lowered.kind = ExpressionKind::Intrinsic { intrinsic: *intrinsic, @@ -1067,6 +1675,7 @@ impl Builder<'_> { field_validation: field_validation.clone(), args, evaluation_order: evaluation_order.clone(), + ownership, }; return self.finish_call_view_scope(lowered); } @@ -1077,7 +1686,7 @@ impl Builder<'_> { evaluation_order, constructor, } = &expression.kind - && args.iter().any(has_extractable_handle) + && args.iter().any(needs_eager_lowering) && let Some(args) = self.lower_ordered_owned_values(args, evaluation_order) { let mut lowered = expression.clone(); @@ -1097,7 +1706,7 @@ impl Builder<'_> { evaluation_order, kind, } = &expression.kind - && (has_extractable_handle(actor) || args.iter().any(has_extractable_handle)) + && (needs_eager_lowering(actor) || args.iter().any(needs_eager_lowering)) && valid_ordered_owned_values(self.types, &self.locals, args, evaluation_order) { let actor_value = self.lower_value(actor); @@ -1131,8 +1740,11 @@ impl Builder<'_> { callee, args, evaluation_order, + ownership, } = &expression.kind - && (has_extractable_handle(callee) || args.iter().any(has_extractable_handle)) + && (needs_eager_lowering(callee) + || args.iter().any(needs_eager_lowering) + || needs_call_owner_staging(ownership)) && !matches!(callee.kind, ExpressionKind::View(_)) && let Type::Function { view_params, .. } = self.types.resolve( crate::move_values::representation_type(self.types, callee.ty), @@ -1151,13 +1763,13 @@ impl Builder<'_> { .collect::>(), ) { - let eligible = args - .iter() - .map(|arg| matches!(arg.kind, ExpressionKind::View(_))) - .collect::>(); - let Some(args) = - self.lower_ordered_borrowed_values(&inputs, evaluation_order, &eligible) - else { + let mut ownership = ownership.clone(); + let Some(args) = self.lower_ordered_call_values( + expression, + &inputs, + evaluation_order, + &mut ownership, + ) else { return expression.clone(); }; // Source evaluates call arguments before resolving a function value @@ -1185,6 +1797,7 @@ impl Builder<'_> { }), args, evaluation_order: evaluation_order.clone(), + ownership, }; return self.finish_call_view_scope(lowered); } @@ -1192,21 +1805,19 @@ impl Builder<'_> { function, args, evaluation_order, + ownership, } = &expression.kind - && args.iter().any(has_extractable_handle) + && (args.iter().any(needs_eager_lowering) || needs_call_owner_staging(ownership)) && let Some(modes) = self.function_param_modes.get(function) && let Some(inputs) = call_staging_inputs(args, modes) { - let eligible = args - .iter() - .zip(modes) - .map(|(arg, mode)| { - *mode == ParamMode::View && matches!(arg.kind, ExpressionKind::View(_)) - }) - .collect::>(); - let Some(args) = - self.lower_ordered_borrowed_values(&inputs, evaluation_order, &eligible) - else { + let mut ownership = ownership.clone(); + let Some(args) = self.lower_ordered_call_values( + expression, + &inputs, + evaluation_order, + &mut ownership, + ) else { return expression.clone(); }; let mut lowered = expression.clone(); @@ -1214,6 +1825,7 @@ impl Builder<'_> { function: *function, args, evaluation_order: evaluation_order.clone(), + ownership, }; return self.finish_call_view_scope(lowered); } @@ -1314,6 +1926,7 @@ impl Builder<'_> { type_arguments, args, evaluation_order, + ownership, .. } = &expression.kind else { @@ -1360,7 +1973,8 @@ impl Builder<'_> { .iter() .enumerate() .map(|(index, arg)| { - if crate::move_values::intrinsic_borrows(*intrinsic, index, arg) + if ownership.parameter_physical_access(index) + == Some(jett_typecheck::CheckedCalleeAccess::View) && !matches!(arg.kind, ExpressionKind::View(_)) { Expression { @@ -1373,24 +1987,20 @@ impl Builder<'_> { } }) .collect(); - let eligible = args - .iter() - .enumerate() - .map(|(index, arg)| { - crate::move_values::intrinsic_borrows(*intrinsic, index, arg) - && matches!(arg.kind, ExpressionKind::View(_)) - }) - .collect::>(); - let args = self.lower_ordered_borrowed_values(&inputs, evaluation_order, &eligible)?; + let mut ownership = ownership.clone(); + let args = + self.lower_ordered_call_values(expression, &inputs, evaluation_order, &mut ownership)?; let mut raw = expression.clone(); if let ExpressionKind::Intrinsic { field_validation, args: raw_args, + ownership: raw_ownership, .. } = &mut raw.kind { *field_validation = Some(hir::ReflectedFieldValidation::Read); *raw_args = args.clone(); + *raw_ownership = ownership; } let span = expression.span; let candidate = self.temporary(expression.ty, span); @@ -1548,6 +2158,19 @@ impl Builder<'_> { predicate: &hir::RefinementPredicate, span: Span, ) -> (LocalId, LocalId) { + let args = vec![input]; + let ownership = hir::CallOwnership::generated( + hir::GeneratedOperation::RefinementPredicate { + function: predicate.function, + }, + &args, + &[predicate.input_type], + &[jett_typecheck::CheckedCalleeAccess::Owned], + TypeInterner::BOOL, + &[0], + self.types, + ) + .expect("checked refinement predicate has a unary owned input"); let error_text = self.temporary(TypeInterner::STRING, span); self.push( StatementKind::CheckRefinement { @@ -1555,8 +2178,9 @@ impl Builder<'_> { call: Expression { kind: ExpressionKind::Call { function: predicate.function, - args: vec![input], + args, evaluation_order: vec![0], + ownership, }, ty: TypeInterner::BOOL, span, @@ -2489,8 +3113,72 @@ mod tests { program .functions .iter() - .find(|function| function.identity.declaration.name == "inspect") - .unwrap() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap() + } + + #[test] + fn failed_checked_call_staging_restores_all_builder_state() { + let source = "function take(view first: list[int64], view second: list[int64]) returns int64:\n return 7\nfunction exercise(first: list[int64], second: list[int64]) returns int64:\n return take(first, second)\n"; + let (program, types) = handler_source_hir(source); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "exercise") + .unwrap(); + let call = function + .body + .statements + .iter() + .find_map(|statement| match &statement.kind { + hir::StatementKind::Return(Some(value)) => Some(value), + _ => None, + }) + .unwrap(); + let ExpressionKind::Call { + function: target, + args, + evaluation_order, + ownership, + } = &call.kind + else { + panic!("checked source invocation"); + }; + let modes = program + .functions + .iter() + .map(|function| { + ( + function.id, + function + .params + .iter() + .map(|param| param.mode) + .collect::>(), + ) + }) + .collect::>(); + let mut inputs = call_staging_inputs(args, &modes[target]).unwrap(); + // The first argument will already have acquired an owner and loan + // when the later malformed physical operand fails its stage proof. + inputs[1] = args[1].clone(); + let mut builder = Builder::new(function.span, &types, &modes); + builder.locals = function.locals.clone(); + let previous = builder.clone(); + let mut packet = ownership.clone(); + assert!( + builder + .lower_ordered_call_values(&call, &inputs, evaluation_order, &mut packet) + .is_none() + ); + assert_eq!(packet, *ownership); + assert_eq!(builder.blocks, previous.blocks); + assert_eq!(builder.current, previous.current); + assert_eq!(builder.locals, previous.locals); + assert_eq!(builder.loops, previous.loops); + assert_eq!(builder.handlers, previous.handlers); + assert_eq!(builder.view_params, previous.view_params); + assert_eq!(builder.call_view_scopes, previous.call_view_scopes); } #[test] @@ -2775,10 +3463,13 @@ function inspect(view source: Record, view field: TypeField) returns Positive: panic!("source plans"); }; plans[0].source_type = TypeInterner::BOOL; - let malformed = lower(&hir, &types).unwrap(); + let errors = lower(&hir, &types) + .expect_err("malformed source type plan cannot become unchecked Read"); assert!( - validate(&malformed).is_err(), - "malformed source type plan cannot become unchecked Read" + errors.iter().any(|error| error + .message + .contains("reflected validation types disagree with declared field and request")), + "{errors:?}" ); } @@ -2979,6 +3670,255 @@ function inspect(items: list[int64]) returns Nonempty: assert!(plan.owned_locals.contains(&(output.index() as usize))); } + const NESTED_BYTE_CALL_SOURCE: &str = r#"namespace app +function render(view value: bytes) returns string: + return "hex" +function quote(value: string) returns string: + return value +function inspect(view value: bytes) returns string: + bytes item = clone value + return quote("0x{render(item)}") +"#; + + fn nested_byte_calls( + function: &Function, + target: hir::FunctionId, + ) -> Vec<(BlockId, usize, &Expression)> { + function + .blocks + .iter() + .flat_map(|block| { + block.statements.iter().enumerate().filter_map(move |(index, statement)| { + let StatementKind::Let { value, .. } = &statement.kind else { return None; }; + matches!(value.kind, ExpressionKind::Call { function, .. } if function == target) + .then_some((block.id, index, value)) + }) + }) + .collect() + } + + #[test] + fn nested_checked_byte_call_stages_owned_input_inside_interpolation_and_outer_call() { + let (hir, types) = handler_source_hir(NESTED_BYTE_CALL_SOURCE); + let original = hir + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let result = original + .body + .statements + .iter() + .find_map(|statement| match &statement.kind { + hir::StatementKind::Return(Some(value)) => Some(value), + _ => None, + }) + .unwrap(); + assert!( + !has_extractable_handle(result), + "no source handler caused the original refusal" + ); + assert!(needs_eager_lowering(result)); + let target = hir + .functions + .iter() + .find(|function| function.identity.declaration.name == "render") + .unwrap() + .id; + let program = lower(&hir, &types).expect("nested ownership demand must reach render"); + let function = inspected_handler_function(&program); + let calls = nested_byte_calls(function, target); + assert_eq!(calls.len(), 1); + let (block, call_index, call) = calls[0]; + let ExpressionKind::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &call.kind + else { + panic!("source-backed render call"); + }; + assert_eq!( + packet.arguments[0].effect, + jett_typecheck::CheckedCallerEffect::RelinquishOwned + ); + let hir::ArgumentStaging::Relinquished { owner, loan } = packet.arguments[0].staging else { + panic!("bare byte input acquired once before its physical View"); + }; + let item = function + .locals + .iter() + .find(|local| local.name == "item") + .unwrap() + .id; + let acquisitions = crate::validate_caller_acquisitions(&program, function, &types).unwrap(); + assert_eq!( + acquisitions.owner_initializer(owner).unwrap().binding, + Some(item) + ); + assert_eq!(function.local(loan).unwrap().view_source, Some(owner)); + let statements = &function.blocks[block.index() as usize].statements; + let owner_index = statements + .iter() + .position(|statement| { + matches!(&statement.kind, + StatementKind::Let { local, value } if *local == owner + && matches!(value.kind, ExpressionKind::Local(source) if source == item)) + }) + .expect("full endpoint moves without Clone"); + let begin = statements + .iter() + .position(|statement| { + matches!(statement.kind, + StatementKind::BeginCallView { local, .. } if local == loan) + }) + .unwrap(); + let end = statements + .iter() + .position(|statement| { + matches!(statement.kind, + StatementKind::EndCallView { local } if local == loan) + }) + .unwrap(); + assert!(owner_index < begin && begin < call_index && call_index < end); + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + } + + #[test] + fn nested_checked_byte_call_keeps_written_view_before_last_bare_consumption() { + let source = NESTED_BYTE_CALL_SOURCE + .replace("0x{render(item)}", "0x{render(view item)}:{render(item)}"); + let (hir, types) = handler_source_hir(&source); + let target = hir + .functions + .iter() + .find(|function| function.identity.declaration.name == "render") + .unwrap() + .id; + let program = lower(&hir, &types).expect("written view followed by one last owning input"); + let function = inspected_handler_function(&program); + let calls = nested_byte_calls(function, target); + assert_eq!(calls.len(), 2); + assert_eq!(calls[0].0, calls[1].0); + assert!( + calls[0].1 < calls[1].1, + "interpolation retains lexical evaluation order" + ); + let effects = calls + .iter() + .map(|(_, _, call)| { + let ExpressionKind::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &call.kind + else { + panic!("source call"); + }; + (packet.arguments[0].effect, packet.arguments[0].staging) + }) + .collect::>(); + assert_eq!( + effects[0], + ( + jett_typecheck::CheckedCallerEffect::RetainBorrow, + hir::ArgumentStaging::Original + ) + ); + assert_eq!( + effects[1].0, + jett_typecheck::CheckedCallerEffect::RelinquishOwned + ); + assert!(matches!( + effects[1].1, + hir::ArgumentStaging::Relinquished { .. } + )); + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + } + + #[test] + fn nested_checked_byte_call_cannot_revert_to_original_owning_stage() { + let (hir, types) = handler_source_hir(NESTED_BYTE_CALL_SOURCE); + let target = hir + .functions + .iter() + .find(|function| function.identity.declaration.name == "render") + .unwrap() + .id; + let mut program = lower(&hir, &types).expect("valid staged control"); + let function = inspected_handler_function(&program); + let (block, index, _) = nested_byte_calls(function, target)[0]; + let identity = function.id; + let function = program + .functions + .iter_mut() + .find(|function| function.id == identity) + .unwrap(); + let StatementKind::Let { value, .. } = + &mut function.blocks[block.index() as usize].statements[index].kind + else { + panic!("eager result"); + }; + let ExpressionKind::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &mut value.kind + else { + panic!("render call"); + }; + packet.arguments[0].staging = hir::ArgumentStaging::Original; + let errors = crate::validate_call_ownership(&program, &types) + .expect_err("mandatory owning stage still enforced"); + assert!( + errors + .iter() + .any(|error| error.message.contains("requires explicit owning staging")), + "{errors:?}" + ); + } + + #[test] + fn nested_checked_byte_call_keeps_lazy_boolean_rhs_in_its_selected_cfg_arm() { + for operator in ["&&", "||"] { + let source = r#"namespace app +function check(view value: bytes) returns bool: + return true +function inspect(gate: bool, item: bytes) returns bool: + return gate OPERATOR check(item) +"# + .replace("OPERATOR", operator); + let (hir, types) = handler_source_hir(&source); + let target = hir + .functions + .iter() + .find(|function| function.identity.declaration.name == "check") + .unwrap() + .id; + let program = lower(&hir, &types).expect("lazy checked call demand"); + let function = inspected_handler_function(&program); + let calls = nested_byte_calls(function, target); + assert_eq!(calls.len(), 1); + let entry = &function.blocks[function.entry.index() as usize]; + let TerminatorKind::Branch { + then_block, + else_block, + .. + } = entry.terminator.kind + else { + panic!("lazy branch required"); + }; + let (selected, bypass) = if operator == "&&" { + (then_block, else_block) + } else { + (else_block, then_block) + }; + assert_eq!(calls[0].0, selected); + assert_ne!(calls[0].0, function.entry); + assert!(function.blocks[bypass.index() as usize].statements.iter().any(|statement| + matches!(statement.kind, StatementKind::Let { value: Expression { kind: ExpressionKind::Bool(value), .. }, .. } + if value == (operator == "||")))); + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + } + } + #[test] fn equatable_result_precedes_later_handlers_without_extra_owned_storage() { for operator in ["==", "!="] { @@ -3080,6 +4020,91 @@ function inspect(view left: Item, view right: Item) returns bool: } } + fn eager_generated_boundary_call<'a>( + function: &'a Function, + block: BlockId, + boundary_index: usize, + result: &Expression, + equality: bool, + ) -> &'a Expression { + let ExpressionKind::Local(output) = result.kind else { + panic!("checked boundary must observe its eagerly completed call result"); + }; + let metadata = function.local(output).expect("exact call result metadata"); + assert_eq!(metadata.ty, result.ty); + assert_eq!(metadata.debug_ty, result.ty); + assert_eq!(metadata.span, result.span); + assert!(metadata.view_source.is_none()); + assert!(function.parameter_for_local(output).is_none()); + let definitions = function + .blocks + .iter() + .flat_map(|block| { + block + .statements + .iter() + .enumerate() + .filter_map(move |(index, statement)| { + let StatementKind::Let { local, value } = &statement.kind else { + return None; + }; + (*local == output).then_some((block.id, index, value)) + }) + }) + .collect::>(); + assert_eq!(definitions.len(), 1, "one eagerly completed operation"); + let (defined_block, call_index, call) = definitions[0]; + assert_eq!(defined_block, block); + assert!(call_index < boundary_index); + assert_eq!(call.ty, result.ty); + assert_eq!(call.span, result.span); + let ExpressionKind::Call { + function: method, + args, + ownership: hir::CallOwnership::Generated(packet), + .. + } = &call.kind + else { + panic!("exact eagerly materialized generated method call"); + }; + match (&packet.operation, equality) { + (hir::GeneratedOperation::Equality { method: expected }, true) + | (hir::GeneratedOperation::Display { method: expected }, false) => { + assert_eq!(method, expected) + } + _ => panic!("result must belong to its exact checked boundary operation"), + } + assert_eq!(args.len(), if equality { 2 } else { 1 }); + assert_eq!(packet.arguments.len(), args.len()); + let statements = &function.blocks[block.index() as usize].statements; + for (value, argument) in args.iter().zip(&packet.arguments) { + let loan = match argument.staging { + hir::GeneratedArgumentStaging::Existing { loan: Some(loan) } + | hir::GeneratedArgumentStaging::OwnedProducer { loan, .. } + | hir::GeneratedArgumentStaging::OrdinarySnapshot { loan, .. } => loan, + _ => panic!("eager call operand must retain its exact scoped loan"), + }; + assert!(matches!(&value.kind, ExpressionKind::View(inner) + if matches!(inner.kind, ExpressionKind::Local(local) if local == loan))); + let begins = statements.iter().enumerate().filter_map(|(index, statement)| + matches!(statement.kind, StatementKind::BeginCallView { local, .. } if local == loan) + .then_some(index)).collect::>(); + let ends = statements + .iter() + .enumerate() + .filter_map(|(index, statement)| { + matches!(statement.kind, StatementKind::EndCallView { local } if local == loan) + .then_some(index) + }) + .collect::>(); + assert_eq!(begins.len(), 1); + assert_eq!(ends.len(), 1); + assert!(begins[0] < call_index); + assert!(call_index < ends[0] && ends[0] < boundary_index); + } + call + } + #[test] fn equatable_result_retains_operand_handlers_before_call_and_negation() { for operator in ["==", "!="] { @@ -3111,6 +4136,16 @@ function inspect(view left: optional[Item], view right: Item) returns bool: let TerminatorKind::Return(Some(value)) = &block.terminator.kind else { return None; }; + assert_eq!( + matches!( + value.kind, + ExpressionKind::Unary { + op: hir::UnaryOp::Not, + .. + } + ), + operator == "!=" + ); let boundary = if let ExpressionKind::Unary { op: hir::UnaryOp::Not, value, @@ -3129,7 +4164,15 @@ function inspect(view left: optional[Item], view right: Item) returns bool: assert_eq!(guards.len(), 1, "{operator}"); let (block, boundary, call) = guards[0]; assert_ne!(block, function.entry); + let call = eager_generated_boundary_call( + function, + block, + function.blocks[block.index() as usize].statements.len(), + call, + true, + ); assert!(matches!(call.kind, ExpressionKind::Call { .. })); + assert!(!has_extractable_handle(call)); assert!(!has_extractable_handle(boundary)); } } @@ -3252,24 +4295,27 @@ function inspect(view item: optional[Item]) returns string: block .statements .iter() - .map(move |statement| (block.id, statement)) + .enumerate() + .map(move |(index, statement)| (block.id, index, statement)) }) - .filter_map(|(block, statement)| match &statement.kind { + .filter_map(|(block, index, statement)| match &statement.kind { StatementKind::Let { value, .. } if matches!(value.kind, ExpressionKind::DisplayResult(_)) => { - Some((block, value)) + Some((block, index, value)) } _ => None, }) .collect::>(); assert_eq!(guards.len(), 1); - let (block, guard) = guards[0]; + let (block, guard_index, guard) = guards[0]; assert_ne!(block, function.entry); let ExpressionKind::DisplayResult(call) = &guard.kind else { unreachable!(); }; + let call = eager_generated_boundary_call(function, block, guard_index, call, false); assert!(matches!(call.kind, ExpressionKind::Call { .. })); + assert!(!has_extractable_handle(call)); assert!(!has_extractable_handle(guard)); } @@ -3457,20 +4503,215 @@ function inspect(view source: list[int64], view other: list[int64]) returns list hir::validate_backend_types(&hir, &types).is_err(), "{invalid}" ); - let program = lower(&hir, &types).expect("structural HIR lowering"); - validate(&program).expect("malformed alias remains structurally valid MIR"); - let error = crate::move_values::MoveValuePlan::analyze( - &program, - inspected_handler_function(&program), - &types, - ) - .unwrap_err(); - assert!(error.contains("stable backing local"), "{invalid}: {error}"); + let errors = + lower(&hir, &types).expect_err("malformed alias refused before MIR acquisition"); + assert!( + errors + .iter() + .any(|error| error.message.contains("stable backing local")), + "{invalid}: {errors:?}" + ); } } + fn staged_alias_source_argument( + function: &Function, + alias: LocalId, + ) -> (&hir::ArgumentOwnership, &Expression, BlockId, usize) { + let calls = function.blocks.iter().flat_map(|block| { + block.statements.iter().enumerate().filter_map(move |(index, statement)| { + let value = match &statement.kind { + StatementKind::Let { value, .. } | StatementKind::Evaluate(value) => value, + _ => return None, + }; + let ExpressionKind::Call { ownership: hir::CallOwnership::Source(packet), args, .. } = &value.kind + else { return None; }; + let argument = packet.arguments.iter().position(|argument| + matches!(&argument.origin, hir::CallerOrigin::Binding(fact) if fact.local == alias))?; + Some((&packet.arguments[argument], &args[argument], block.id, index)) + }) + }).collect::>(); + assert_eq!(calls.len(), 1, "one exact Source argument for the alias"); + calls[0] + } + + fn assert_staged_alias_has_retained_snapshot( + program: &Program, + function: &Function, + alias: LocalId, + types: &TypeInterner, + ) { + let (argument, actual, call_block, call_index) = + staged_alias_source_argument(function, alias); + assert_eq!( + argument.effect, + jett_typecheck::CheckedCallerEffect::RetainBorrow + ); + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + let hir::ArgumentStaging::RetainedSnapshot { owner, loan } = argument.staging else { + panic!("ordinary alias endpoint snapshot"); + }; + let physical = argument + .retained_snapshot_span() + .expect("sealed physical occurrence"); + assert_eq!(argument.retained_snapshot_type(), Some(actual.ty)); + assert_eq!(actual.span, physical); + assert!(matches!(&actual.kind, ExpressionKind::View(value) + if matches!(value.kind, ExpressionKind::Local(id) if id == loan))); + + let statements = || { + function.blocks.iter().flat_map(|block| { + block + .statements + .iter() + .enumerate() + .map(move |(index, statement)| (block.id, index, statement)) + }) + }; + let owners = statements() + .filter_map(|(block, index, statement)| match &statement.kind { + StatementKind::Let { local, value } if *local == owner => { + Some((block, index, value)) + } + _ => None, + }) + .collect::>(); + assert_eq!(owners.len(), 1, "unique owning endpoint initializer"); + let (owner_block, owner_index, initializer) = owners[0]; + let ExpressionKind::Clone(endpoint) = &initializer.kind else { + panic!("full endpoint Clone"); + }; + let ExpressionKind::View(original) = &endpoint.kind else { + panic!("unchanged written View"); + }; + assert!(matches!(original.kind, ExpressionKind::Local(id) if id == alias)); + assert_eq!(initializer.span, physical); + assert_eq!(endpoint.span, physical); + assert_eq!( + endpoint.span, argument.source_span, + "original written View occurrence" + ); + assert!(function.local(owner).unwrap().view_source.is_none()); + assert_eq!(function.local(loan).unwrap().view_source, Some(owner)); + + let begins = statements() + .filter_map(|(block, index, statement)| match &statement.kind { + StatementKind::BeginCallView { local, value } if *local == loan => { + Some((block, index, value)) + } + _ => None, + }) + .collect::>(); + assert_eq!(begins.len(), 1, "unique owner-backed Begin"); + let (begin_block, begin_index, projection) = begins[0]; + assert_eq!(begin_block, owner_block); + assert!(owner_index < begin_index); + assert!(matches!(&projection.kind, ExpressionKind::View(value) + if matches!(value.kind, ExpressionKind::Local(id) if id == owner))); + let tag_index = function.blocks[owner_block.index() as usize] + .statements + .iter() + .position(|statement| matches!(statement.kind, StatementKind::SumTag { .. })) + .expect("later argument handler tag"); + assert!( + begin_index < tag_index, + "capture alias endpoint before later handler" + ); + + let ends = statements() + .filter_map(|(block, index, statement)| { + matches!(statement.kind, StatementKind::EndCallView { local } if local == loan) + .then_some((block, index)) + }) + .collect::>(); + assert_eq!(ends.len(), 1, "unique completion End"); + let (end_block, end_index) = ends[0]; + assert_eq!(end_block, call_block); + assert!(call_index < end_index); + + // Persistent alias storage remains precisely its checked initializer. + let alias_values = statements() + .filter_map(|(_, _, statement)| match &statement.kind { + StatementKind::Let { local, value } if *local == alias => Some(value), + _ => None, + }) + .collect::>(); + assert_eq!(alias_values.len(), 1); + let metadata = function.local(alias).unwrap(); + let backing = metadata + .view_source + .expect("original alias stays nonowning"); + hir::validate_local_view_initializer( + alias_values[0], + backing, + function.local(backing).unwrap().ty, + metadata.ty, + types, + ) + .unwrap(); + let plan = crate::move_values::MoveValuePlan::analyze(program, function, types).unwrap(); + assert!(plan.owned_locals.contains(&(owner.index() as usize))); + assert!(!plan.owned_locals.contains(&(alias.index() as usize))); + assert!(!plan.owned_locals.contains(&(loan.index() as usize))); + assert!( + plan.live_after_statement[call_block.index() as usize][call_index] + .contains(&(owner.index() as usize)) + ); + assert!( + !plan.live_after_statement[end_block.index() as usize][end_index] + .contains(&(owner.index() as usize)), + "owner becomes dead after End" + ); + } + + fn assert_staged_alias_has_raw_borrow(function: &Function, alias: LocalId) { + let (argument, _, _, _) = staged_alias_source_argument(function, alias); + assert_eq!( + argument.effect, + jett_typecheck::CheckedCallerEffect::RetainBorrow + ); + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert!( + argument.retained_snapshot_type().is_none(), + "no descriptor data-copy proof" + ); + assert!(matches!( + argument.staging, + hir::ArgumentStaging::Borrowed { .. } + )); + assert_staged_alias_is_borrowed(function, alias); + } + fn assert_staged_alias_is_borrowed(function: &Function, alias: LocalId) { + let statements = || function.blocks.iter().flat_map(|block| &block.statements); + let loans = statements() + .filter_map(|statement| match &statement.kind { + StatementKind::BeginCallView { local, value } + if matches!(&value.kind, ExpressionKind::View(value) + if matches!(value.kind, ExpressionKind::Local(id) if id == alias)) => + { + Some(*local) + } + _ => None, + }) + .collect::>(); + assert_eq!(loans.len(), 1, "exact alias loan"); + assert!(statements().any(|statement| matches!(statement.kind, + StatementKind::EndCallView { local } if local == loans[0]))); + assert!(!statements().any(|statement| matches!(&statement.kind, + StatementKind::Let { value, .. } if matches!(&value.kind, + ExpressionKind::Clone(value) if matches!(&value.kind, + ExpressionKind::View(value) if matches!(value.kind, + ExpressionKind::Local(id) if id == alias)))))); + } + #[test] - fn handler_call_staging_preserves_bare_local_alias_view_parameters() { + fn handler_call_staging_preserves_explicit_local_alias_view_parameters() { let (program, types) = lower_handler_source( r#"namespace app function take(view values: list[int64], amount: int64) returns int64: @@ -3479,7 +4720,7 @@ function inspect() returns int64: list[int64] source = list(1) list[int64] borrowed = view source list[int64] forwarded = borrowed - int64 answer = take(forwarded, (none handle: default 3)) + int64 answer = take(view forwarded, (none handle: default 3)) trace source return answer "#, @@ -3493,18 +4734,7 @@ function inspect() returns int64: .find(|local| local.name == "forwarded") .unwrap() .id; - assert!( - function - .blocks - .iter() - .flat_map(|block| &block.statements) - .any(|statement| { - matches!(&statement.kind, StatementKind::Let { value, .. } - if matches!(&value.kind, ExpressionKind::Clone(view) - if matches!(&view.kind, ExpressionKind::View(local) - if matches!(local.kind, ExpressionKind::Local(id) if id == forwarded)))) - }) - ); + assert_staged_alias_has_retained_snapshot(&program, function, forwarded, &types); } #[test] @@ -3518,7 +4748,7 @@ function inspect() returns int64: list[int64] borrowed = view source function(view list[int64], int64) returns int64 callback = take function(view list[int64], int64) returns int64 callable = view callback - int64 answer = callable(borrowed, (none handle: default 4)) + int64 answer = callable(view borrowed, (none handle: default 4)) trace source trace callback trace callable @@ -3571,7 +4801,7 @@ function inspect() returns int64: } #[test] - fn handler_call_staging_snapshots_copy_owned_view_carriers() { + fn handler_call_staging_snapshots_data_but_keeps_function_alias_borrowed() { let (program, types) = lower_handler_source( r#"namespace app function increment(value: int64) returns int64: @@ -3592,27 +4822,16 @@ function inspect() returns int64: validate(&program).expect("copy-owned staged views"); let function = inspected_handler_function(&program); crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); - for name in ["borrowed", "callable"] { - let alias = function + let alias = |name| { + function .locals .iter() .find(|local| local.name == name) .unwrap() - .id; - assert!( - function - .blocks - .iter() - .flat_map(|block| &block.statements) - .any(|statement| { - matches!(&statement.kind, StatementKind::Let { value, .. } - if matches!(&value.kind, ExpressionKind::Clone(view) - if matches!(&view.kind, ExpressionKind::View(local) - if matches!(local.kind, ExpressionKind::Local(id) if id == alias)))) - }), - "{name}" - ); - } + .id + }; + assert_staged_alias_has_retained_snapshot(&program, function, alias("borrowed"), &types); + assert_staged_alias_has_raw_borrow(function, alias("callable")); } #[test] @@ -3625,7 +4844,7 @@ function take(view values: list[int64], amount: int64) returns int64: function inspect() returns int64: list[int64] source = list(1) list[int64] borrowed = view source - return take(borrowed, (none handle: default 3)) + return take(view borrowed, (none handle: default 3)) "#, file, ); @@ -3653,16 +4872,13 @@ function inspect() returns int64: .unwrap() .params[0] .mode = ParamMode::Owned; - let program = lower(&hir, &checked.interner).unwrap(); - let error = crate::move_values::MoveValuePlan::analyze( - &program, - inspected_handler_function(&program), - &checked.interner, - ) - .unwrap_err(); + let errors = lower(&hir, &checked.interner) + .expect_err("physical formal mutation refused before staging"); assert!( - error.contains("cannot move borrowed native place"), - "{error}" + errors.iter().any(|error| error.message.contains( + "source function ownership signature differs from the exact HIR declaration" + )), + "{errors:?}" ); let span = Span::new(file, 0, 1); diff --git a/crates/jett_mir/src/iteration_views.rs b/crates/jett_mir/src/iteration_views.rs new file mode 100644 index 00000000..fa2fab52 --- /dev/null +++ b/crates/jett_mir/src/iteration_views.rs @@ -0,0 +1,710 @@ +//! Logical source views backed by an exact original or prepared sequence loop. +use super::*; +use jett_hir::{BinaryOp, ExpressionKind as E, IterationPart, ViewIterationBinding}; +use std::collections::BTreeSet; +#[cfg(test)] +mod tests; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct OriginalIteration { + function: FunctionId, + identity: FunctionIdentity, + header: BlockId, + body: BlockId, + exit: BlockId, + bindings: Vec<(LocalId, ViewIterationBinding)>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct PreparedViewIteration { + original: OriginalIteration, + source: SequenceSource, + cursor: LocalId, + length: LocalId, + preheader: BlockId, + runtime_span: Span, +} + +impl PreparedViewIteration { + pub(super) fn remap_blocks(&mut self, blocks: &[Option]) -> bool { + let get = |id: BlockId| blocks.get(id.index() as usize).copied().flatten(); + let (Some(header), Some(body), Some(exit), Some(preheader)) = ( + get(self.original.header), + get(self.original.body), + get(self.original.exit), + get(self.preheader), + ) else { + return false; + }; + self.original.header = header; + self.original.body = body; + self.original.exit = exit; + self.preheader = preheader; + true + } + + pub(super) fn remap_locals(&mut self, locals: &[Option]) -> bool { + let get = |id: LocalId| locals.get(id.index() as usize).copied().flatten(); + let (Some(source), Some(cursor), Some(length)) = + (get(self.source.root()), get(self.cursor), get(self.length)) + else { + return false; + }; + let Some(bindings) = self + .original + .bindings + .iter() + .map(|(local, proof)| get(*local).map(|local| (local, *proof))) + .collect::>>() + else { + return false; + }; + match &mut self.source { + SequenceSource::Local(local) | SequenceSource::Projected { owner: local, .. } => { + *local = source + } + } + self.cursor = cursor; + self.length = length; + self.original.bindings = bindings; + true + } +} + +/// A finite body region supplies backing at an invocation site only. +pub(super) struct IterationScope { + bindings: Vec<(LocalId, ViewIterationBinding)>, + region: BTreeSet, +} + +pub(super) fn apply( + scopes: &[IterationScope], + block: BlockId, + locals: &mut [hir::OwnershipLocalInfo], + types: &TypeInterner, +) { + for local in locals.iter_mut() { + local.view_iteration = None; + } + for scope in scopes + .iter() + .filter(|scope| scope.region.contains(&block.index())) + { + for &(local, proof) in &scope.bindings { + if !jett_typecheck::ownership::is_implicitly_copyable(types, proof.binder_type()) { + locals[local.index() as usize].view_iteration = Some(proof); + } + } + } +} + +/// Forwarded aliases retain the binder's region instead of escaping it. +pub(super) fn ensure_scope( + function: &Function, + scopes: &[IterationScope], + mut local: LocalId, + block: BlockId, +) -> Result<(), String> { + let mut seen = BTreeSet::new(); + loop { + if !seen.insert(local.index()) { + return Err("call ownership iteration alias backing is cyclic".into()); + } + for scope in scopes { + if scope.bindings.iter().any(|(binder, _)| *binder == local) + && !scope.region.contains(&block.index()) + { + return Err( + "call ownership iteration binding is used outside its exact body".into(), + ); + } + } + let Some(source) = function.local(local).and_then(|local| local.view_source) else { + return Ok(()); + }; + local = source; + } +} + +fn read_plan( + function: &Function, + header: BlockId, + types: &TypeInterner, +) -> Result, String> { + let block = function + .blocks + .get(header.index() as usize) + .ok_or("call ownership iteration header is absent")?; + let TerminatorKind::ForEach { + key, + value, + by_view, + iterable, + body, + exit, + } = &block.terminator.kind + else { + return Ok(None); + }; + if !by_view { + return Ok(None); + } + if iterable.ty == TypeInterner::ERROR || iterable.ty.index() as usize >= types.len() { + return Err("call ownership viewed iterable has an invalid exact type".into()); + } + let key_part = match types.resolve(iterable.ty) { + jett_types::Type::Map(..) => IterationPart::Key, + jett_types::Type::List(_) | jett_types::Type::Set(_) | jett_types::Type::String + if value.is_none() => + { + IterationPart::Element + } + _ => return Err("call ownership viewed iteration has invalid binder roles".into()), + }; + let mut bindings = Vec::new(); + for (id, part) in + std::iter::once((*key, key_part)).chain(value.map(|id| (id, IterationPart::Value))) + { + if bindings.iter().any(|(existing, _)| *existing == id) { + return Err("call ownership iteration reuses its binder".into()); + } + let local = function + .local(id) + .ok_or("call ownership iteration binder is absent")?; + let proof = hir::checked_view_iteration_binding( + types, + block.terminator.span, + iterable.ty, + local.ty, + part, + )?; + binding_metadata(function, id, proof)?; + if definitions(function, id) != 1 { + return Err("call ownership iteration binder has another definition".into()); + } + bindings.push((id, proof)); + } + Ok(Some(OriginalIteration { + function: function.id, + identity: function.identity.clone(), + header, + body: *body, + exit: *exit, + bindings, + })) +} + +/// Capture only loops emitted from HIR already validated by the MIR entry gate. +pub(super) fn capture_original(function: &mut Function, types: &TypeInterner) { + function.original_view_iterations = function + .blocks + .iter() + .filter_map(|block| read_plan(function, block.id, types).ok().flatten()) + .collect(); +} + +pub(super) fn original_plan( + function: &Function, + header: BlockId, + types: &TypeInterner, +) -> Result, String> { + let actual = read_plan(function, header, types)?; + let saved = function + .original_view_iterations + .iter() + .filter(|plan| plan.header == header) + .collect::>(); + match (&actual, saved.as_slice()) { + (None, []) => Ok(None), + (Some(actual), [saved]) if actual == *saved => Ok(Some(actual.clone())), + _ => { + Err("call ownership original viewed loop differs from its validated HIR handoff".into()) + } + } +} + +impl OriginalIteration { + pub(super) fn remap_blocks(&mut self, blocks: &[Option]) -> bool { + let get = |id: BlockId| blocks.get(id.index() as usize).copied().flatten(); + let (Some(header), Some(body), Some(exit)) = + (get(self.header), get(self.body), get(self.exit)) + else { + return false; + }; + self.header = header; + self.body = body; + self.exit = exit; + true + } + pub(super) fn remap_locals(&mut self, locals: &[Option]) -> bool { + let Some(bindings) = self + .bindings + .iter() + .map(|(local, proof)| { + locals + .get(local.index() as usize) + .copied() + .flatten() + .map(|local| (local, *proof)) + }) + .collect::>>() + else { + return false; + }; + self.bindings = bindings; + true + } +} + +/// Only the canonical sequence rewrite calls this beside its actual mutation. +pub(super) fn retain_prepared( + function: &mut Function, + original: OriginalIteration, + source: SequenceSource, + cursor: LocalId, + length: LocalId, + preheader: BlockId, + runtime_span: Span, +) { + function + .prepared_view_iterations + .push(PreparedViewIteration { + original, + source, + cursor, + length, + preheader, + runtime_span, + }); +} + +pub(super) fn scopes( + function: &Function, + types: &TypeInterner, +) -> Result, String> { + let cfg = ControlFlowGraph::analyze(function) + .map_err(|_| "call ownership iteration CFG is invalid")?; + let mut result = Vec::new(); + let mut claimed = BTreeSet::new(); + for original in &function.original_view_iterations { + claim_bindings(original, &mut claimed)?; + let prepared = function + .prepared_view_iterations + .iter() + .filter(|record| record.original.header == original.header) + .collect::>(); + match prepared.as_slice() { + [] => { + if original_plan(function, original.header, types)?.as_ref() != Some(original) { + return Err("call ownership original viewed loop lost its exact raw or prepared association".into()); + } + result.push(IterationScope { + region: body_region( + function, + &cfg, + original.header, + original.body, + original.exit, + &BTreeSet::new(), + )?, + bindings: original.bindings.clone(), + }); + } + [record] if &record.original == original => { + result.push(validate_prepared(function, types, &cfg, record)?) + } + _ => { + return Err( + "call ownership viewed loop has duplicate or changed prepared association" + .into(), + ); + } + } + } + for block in &function.blocks { + if matches!( + block.terminator.kind, + TerminatorKind::ForEach { by_view: true, .. } + ) { + original_plan(function, block.id, types)? + .ok_or("call ownership viewed loop has no original association")?; + } + } + for record in &function.prepared_view_iterations { + if !function + .original_view_iterations + .iter() + .any(|original| original == &record.original) + { + return Err("call ownership prepared iteration has no validated original loop".into()); + } + } + Ok(result) +} + +fn claim_bindings(plan: &OriginalIteration, claimed: &mut BTreeSet) -> Result<(), String> { + for &(local, _) in &plan.bindings { + if !claimed.insert(local.index()) { + return Err("call ownership duplicate iteration binding authority".into()); + } + } + Ok(()) +} + +fn binding_metadata( + function: &Function, + id: LocalId, + proof: ViewIterationBinding, +) -> Result<(), String> { + let local = function + .local(id) + .ok_or("call ownership viewed iteration binder is absent")?; + let span = proof.loop_span(); + if local.mutable + || local.view_source.is_some() + || function.parameter_for_local(id).is_some() + || local.ty != proof.binder_type() + || local.debug_ty != local.ty + || local.span.file != span.file + || local.span.start < span.start + || local.span.end > span.end + { + return Err( + "call ownership viewed iteration binder changed its declaration or ABI role".into(), + ); + } + Ok(()) +} + +fn definitions(function: &Function, id: LocalId) -> usize { + let mut count = 0; + for block in &function.blocks { + for statement in &block.statements { + count += usize::from(match &statement.kind { + StatementKind::Let { local, .. } + | StatementKind::BeginCallView { local, .. } + | StatementKind::CheckRefinement { local, .. } => *local == id, + StatementKind::SequenceGet { target, .. } + | StatementKind::SequenceLength { target, .. } + | StatementKind::SumTake { target, .. } + | StatementKind::SumTag { target, .. } => *target == id, + StatementKind::Assign { target, .. } => { + matches!(target.kind, E::Local(local) if local == id) + } + _ => false, + }); + } + match &block.terminator.kind { + TerminatorKind::ForEach { key, value, .. } => { + count += usize::from(*key == id) + usize::from(*value == Some(id)) + } + TerminatorKind::Switch { variants, .. } => { + count += variants + .iter() + .map(|(_, _, bindings)| bindings.iter().filter(|&&local| local == id).count()) + .sum::() + } + _ => {} + } + } + count +} + +fn body_region( + function: &Function, + cfg: &ControlFlowGraph, + header: BlockId, + body: BlockId, + exit: BlockId, + ends: &BTreeSet, +) -> Result, String> { + let valid = |id: BlockId| (id.index() as usize) < function.blocks.len(); + if !valid(header) + || !valid(body) + || !valid(exit) + || header == body + || body == exit + || header == exit + { + return Err("call ownership iteration has invalid body/exit association".into()); + } + let mut outside = BTreeSet::new(); + let mut pending = vec![function.entry, exit]; + while let Some(block) = pending.pop() { + if block == header || !outside.insert(block.index()) { + continue; + } + pending.extend_from_slice(cfg.successors(block)); + } + let mut region = BTreeSet::new(); + let mut pending = vec![body]; + while let Some(block) = pending.pop() { + if block == header + || outside.contains(&block.index()) + || ends.contains(&block.index()) + || !region.insert(block.index()) + { + continue; + } + pending.extend_from_slice(cfg.successors(block)); + } + if !region.contains(&body.index()) { + return Err("call ownership viewed iteration body is reachable outside its loop".into()); + } + for &index in ®ion { + if cfg.predecessors(BlockId(index)).iter().any(|id| { + (*id == header && BlockId(index) != body) + || (*id != header && !region.contains(&id.index())) + }) { + return Err("call ownership iteration body has an external entry".into()); + } + } + Ok(region) +} + +fn local_expression(value: &Expression, id: LocalId, ty: TypeId, span: Span) -> bool { + value.ty == ty && value.span == span && matches!(value.kind, E::Local(local) if local == id) +} + +fn sequence_part(part: IterationPart) -> SequencePart { + match part { + IterationPart::Element => SequencePart::Element, + IterationPart::Key => SequencePart::Key, + IterationPart::Value => SequencePart::Value, + } +} + +fn validate_prepared( + function: &Function, + types: &TypeInterner, + cfg: &ControlFlowGraph, + record: &PreparedViewIteration, +) -> Result { + let plan = &record.original; + if plan.function != function.id + || plan.identity != function.identity + || plan.bindings.is_empty() + || plan.bindings.len() > 2 + { + return Err("call ownership prepared iteration lost its original function/binders".into()); + } + let iterable_type = plan.bindings[0].1.iterable_type(); + if record.source.ty(function) != Some(iterable_type) { + return Err("call ownership prepared iteration changed its exact source type".into()); + } + for &(local, proof) in &plan.bindings { + let checked = hir::checked_view_iteration_binding( + types, + proof.loop_span(), + iterable_type, + proof.binder_type(), + proof.part(), + )?; + if checked != proof + || proof.loop_span() != plan.bindings[0].1.loop_span() + || proof.iterable_type() != iterable_type + { + return Err("call ownership prepared iteration changed its exact endpoint".into()); + } + binding_metadata(function, local, proof)?; + if definitions(function, local) != 1 { + return Err("call ownership prepared iteration binder has another definition".into()); + } + } + for id in [record.cursor, record.length] { + if function.local(id).is_none_or(|local| { + local.ty != TypeInterner::INT64 || local.debug_ty != TypeInterner::INT64 + }) || function.parameter_for_local(id).is_some() + { + return Err("call ownership prepared iterator cursor/length metadata changed".into()); + } + } + let header = function + .blocks + .get(plan.header.index() as usize) + .ok_or("call ownership prepared iteration header is absent")?; + let TerminatorKind::Branch { + condition, + then_block, + else_block, + } = &header.terminator.kind + else { + return Err("call ownership prepared iteration lost its selecting Branch".into()); + }; + let E::Binary { + left, + op: BinaryOp::Less, + right, + } = &condition.kind + else { + return Err("call ownership prepared iteration lost its exact cursor comparison".into()); + }; + if header.terminator.span != plan.bindings[0].1.loop_span() + || *then_block != plan.body + || condition.ty != TypeInterner::BOOL + || condition.span != record.runtime_span + || !local_expression( + left, + record.cursor, + TypeInterner::INT64, + record.runtime_span, + ) + || !local_expression( + right, + record.length, + TypeInterner::INT64, + record.runtime_span, + ) + { + return Err("call ownership prepared iteration changed its cursor or entered body".into()); + } + let preheader = function + .blocks + .get(record.preheader.index() as usize) + .ok_or("call ownership iterator preheader is absent")?; + if !matches!(preheader.terminator.kind, TerminatorKind::Goto(target) if target == plan.header) { + return Err("call ownership iterator preheader bypasses its original header".into()); + } + let [start, zero, length] = preheader + .statements + .get(preheader.statements.len().saturating_sub(3)..) + .ok_or("call ownership iterator initialization is absent")? + else { + return Err("call ownership iterator initialization is incomplete".into()); + }; + if [start, zero, length] + .iter() + .any(|statement| statement.span != record.runtime_span) + || !matches!(&start.kind, StatementKind::IterationBorrow { source, token, start: true } + if *source == record.source && *token == record.cursor) + || !matches!(&zero.kind, StatementKind::Let { local, value } + if *local == record.cursor && value.ty == TypeInterner::INT64 && value.span == record.runtime_span && matches!(value.kind, E::Int(0))) + || !matches!(&length.kind, StatementKind::SequenceLength { source, target } + if *source == record.source && *target == record.length) + { + return Err("call ownership iterator lost its exact borrow/source initialization".into()); + } + let body = function + .blocks + .get(plan.body.index() as usize) + .ok_or("call ownership prepared iteration body is absent")?; + for (index, &(local, proof)) in plan.bindings.iter().enumerate() { + let statement = body + .statements + .get(index) + .ok_or("call ownership viewed extraction is absent")?; + if statement.span != record.runtime_span + || !matches!(&statement.kind, StatementKind::SequenceGet { + consume: false, source, index, target, part } if *source == record.source && *index == record.cursor + && *target == local && *part == sequence_part(proof.part())) + { + return Err( + "call ownership viewed extraction changed source/cursor/target/role or consumption" + .into(), + ); + } + } + let advance = body + .statements + .get(plan.bindings.len()) + .ok_or("call ownership iterator advance is absent")?; + let StatementKind::Assign { target, value } = &advance.kind else { + return Err("call ownership iterator advance changed its statement".into()); + }; + let E::Binary { + left, + op: BinaryOp::Add, + right, + } = &value.kind + else { + return Err("call ownership iterator advance lost its checked addition".into()); + }; + if advance.span != record.runtime_span + || value.ty != TypeInterner::INT64 + || value.span != record.runtime_span + || !local_expression( + target, + record.cursor, + TypeInterner::INT64, + record.runtime_span, + ) + || !local_expression( + left, + record.cursor, + TypeInterner::INT64, + record.runtime_span, + ) + || right.ty != TypeInterner::INT64 + || right.span != record.runtime_span + || !matches!(right.kind, E::Int(1)) + { + return Err("call ownership iterator advance changed its exact cursor".into()); + } + let mut starts = 0; + let mut ends = BTreeSet::new(); + for block in &function.blocks { + for statement in &block.statements { + if let StatementKind::IterationBorrow { + source, + token, + start, + } = &statement.kind + && *token == record.cursor + { + if *source != record.source || statement.span != record.runtime_span { + return Err("call ownership iterator borrow changed its exact source".into()); + } + if *start { + starts += 1; + } else { + if block.statements.len() != 1 + || !matches!(block.terminator.kind, TerminatorKind::Goto(_)) + { + return Err("call ownership iterator end lost its canonical edge".into()); + } + ends.insert(block.id.index()); + } + } + } + } + if starts != 1 { + return Err("call ownership iterator has missing/duplicate borrow start".into()); + } + if !ends.contains(&else_block.index()) + || !matches!(function.blocks[else_block.index() as usize].terminator.kind, + TerminatorKind::Goto(exit) if exit == plan.exit) + { + return Err( + "call ownership iterator false edge bypasses its exact end and original exit".into(), + ); + } + let region = body_region(function, cfg, plan.header, plan.body, plan.exit, &ends)?; + if ends.iter().any(|&end| { + matches!(function.blocks[end as usize].terminator.kind, + TerminatorKind::Goto(target) if target == plan.header || region.contains(&target.index())) + }) { + return Err("call ownership iterator end re-enters its completed region".into()); + } + for block in std::iter::once(plan.header).chain(region.iter().copied().map(BlockId)) { + if cfg.successors(block).iter().any(|id| { + *id != plan.header && !region.contains(&id.index()) && !ends.contains(&id.index()) + }) { + return Err( + "call ownership iterator leaves its region without the exact borrow end".into(), + ); + } + } + if ends.iter().any(|&end| { + cfg.predecessors(BlockId(end)).is_empty() + || cfg + .predecessors(BlockId(end)) + .iter() + .any(|id| *id != plan.header && !region.contains(&id.index())) + }) { + return Err("call ownership iterator end is outside its exact loop region".into()); + } + Ok(IterationScope { + bindings: plan.bindings.clone(), + region, + }) +} diff --git a/crates/jett_mir/src/iteration_views/tests.rs b/crates/jett_mir/src/iteration_views/tests.rs new file mode 100644 index 00000000..312dca77 --- /dev/null +++ b/crates/jett_mir/src/iteration_views/tests.rs @@ -0,0 +1,227 @@ +use super::*; +use std::collections::HashMap; + +const SOURCE: &str = r#"namespace app +struct Item: + code: int64 +function inspect(view item: Item) returns int64: + return item.code +function exercise(view items: list[Item], view other: list[Item]) returns int64: + mutable int64 total = 0 + for item in view items: + Item alias = view item + total = total + inspect(view alias) + return total +"#; + +fn source_program(source: &str) -> (Program, TypeInterner) { + let file = jett_common::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(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|d| d.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let high = hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, jett_common::SourceOrigin::Project)]), + ) + .expect("source HIR"); + ( + crate::lower(&high, &checked.interner).expect("source MIR"), + checked.interner, + ) +} + +fn exercise(program: &Program) -> &Function { + program + .functions + .iter() + .find(|f| f.identity.declaration.name == "exercise") + .unwrap() +} + +fn exercise_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|f| f.identity.declaration.name == "exercise") + .unwrap() +} + +fn rejects(program: &Program, types: &TypeInterner, case: impl std::fmt::Display) { + let error = match crate::validate_call_ownership(program, types) { + Err(error) => error, + Ok(()) => panic!("forged iteration provenance accepted case {case}"), + }; + assert!( + error.iter().any(|e| e.message.contains("call ownership") + || e.message.contains("call view") + || e.message.contains("viewed iteration")), + "{error:?}" + ); +} + +#[test] +fn viewed_iteration_provenance_survives_native_sequence_preparation() { + let (mut program, types) = source_program(SOURCE); + assert!(exercise(&program).prepared_view_iterations.is_empty()); + crate::validate_call_ownership(&program, &types).expect("original typed loop"); + crate::prepare_native_sequences(&mut program, &types); + let function = exercise(&program); + assert_eq!(function.prepared_view_iterations.len(), 1); + let record = &function.prepared_view_iterations[0]; + let alias = function.locals.iter().find(|l| l.name == "alias").unwrap(); + let proved = scopes(function, &types).unwrap(); + ensure_scope(function, &proved, alias.id, record.original.body) + .expect("forwarded alias inside exact loop"); + assert!(ensure_scope(function, &proved, alias.id, record.original.exit).is_err()); + crate::validate_call_ownership(&program, &types).expect("prepared typed loop"); + let once = program.clone(); + crate::prepare_native_sequences(&mut program, &types); + assert_eq!(program, once, "preparation is idempotent"); + crate::sequences::prune::unused_locals(exercise_mut(&mut program)); + crate::validate_call_ownership(&program, &types).expect("metadata follows local compaction"); +} + +#[test] +fn viewed_iteration_prepared_association_rejects_changed_extraction_and_loans() { + let (mut original, types) = source_program(SOURCE); + crate::prepare_native_sequences(&mut original, &types); + assert_eq!(exercise(&original).prepared_view_iterations.len(), 1); + for case in 0..12 { + let mut program = original.clone(); + let function = exercise_mut(&mut program); + let record = function.prepared_view_iterations[0].clone(); + match case { + 0 => function.prepared_view_iterations.clear(), + 1 => function.prepared_view_iterations.push(record.clone()), + 2..=6 => { + let StatementKind::SequenceGet { + consume, + source, + index, + target, + part, + } = &mut function.blocks[record.original.body.index() as usize].statements[0].kind + else { + panic!("exact extraction") + }; + match case { + 2 => *consume = true, + 3 => *source = SequenceSource::Local(function.params[1].local), + 4 => *index = record.length, + 5 => *target = function.params[0].local, + 6 => *part = SequencePart::Value, + _ => unreachable!(), + } + } + 7 => { + let start = function.blocks[record.preheader.index() as usize] + .statements + .iter_mut() + .find(|s| matches!(s.kind, StatementKind::IterationBorrow { start: true, .. })) + .unwrap(); + let StatementKind::IterationBorrow { token, .. } = &mut start.kind else { + unreachable!() + }; + *token = record.length; + } + 8 => { + let end = function.blocks.iter_mut().find(|b| b.statements.iter().any(|s| + matches!(s.kind, StatementKind::IterationBorrow { token, start: false, .. } if token == record.cursor))).unwrap(); + end.statements.clear(); + } + 9 => function.prepared_view_iterations[0].original.function = FunctionId::new(999), + 10 => { + let TerminatorKind::Branch { else_block, .. } = &mut function.blocks + [record.original.header.index() as usize] + .terminator + .kind + else { + unreachable!() + }; + *else_block = record.original.body; + } + 11 => { + let end = function.blocks.iter_mut().find(|b| b.statements.iter().any(|s| + matches!(s.kind, StatementKind::IterationBorrow { token, start: false, .. } if token == record.cursor))).unwrap(); + end.terminator.kind = TerminatorKind::Goto(record.original.header); + } + _ => unreachable!(), + } + rejects(&program, &types, case); + } +} + +#[test] +fn viewed_iteration_original_association_rejects_wrong_binder_and_mode() { + let (original, types) = source_program(SOURCE); + for case in 0..3 { + let mut program = original.clone(); + let function = exercise_mut(&mut program); + let header = function + .blocks + .iter_mut() + .find(|b| matches!(b.terminator.kind, TerminatorKind::ForEach { .. })) + .unwrap(); + let TerminatorKind::ForEach { + key, + by_view, + iterable, + .. + } = &mut header.terminator.kind + else { + unreachable!() + }; + match case { + 0 => *by_view = false, + 1 => *key = function.params[0].local, + 2 => iterable.ty = TypeInterner::STRING, + _ => unreachable!(), + } + rejects(&program, &types, case); + } +} + +#[test] +fn viewed_iteration_map_roles_and_nested_binders_remain_exact() { + let source = r#"namespace app +struct Item: + code: int64 +function inspect(view item: Item) returns int64: + return item.code +function exercise(view groups: map[string, list[Item]]) returns int64: + mutable int64 total = 0 + for key, items in view groups: + for item in view items: + total = total + inspect(view item) + return total +"#; + let (mut program, types) = source_program(source); + crate::prepare_native_sequences(&mut program, &types); + assert_eq!(exercise(&program).prepared_view_iterations.len(), 2); + crate::validate_call_ownership(&program, &types).expect("nested exact regions and map roles"); + let function = exercise_mut(&mut program); + let outer = function + .prepared_view_iterations + .iter() + .find(|r| r.original.bindings.len() == 2) + .unwrap() + .clone(); + let StatementKind::SequenceGet { part, .. } = + &mut function.blocks[outer.original.body.index() as usize].statements[1].kind + else { + panic!("map value extraction") + }; + *part = SequencePart::Key; + rejects(&program, &types, "map value role"); +} diff --git a/crates/jett_mir/src/lib.rs b/crates/jett_mir/src/lib.rs index c6531005..3f30eb8a 100644 --- a/crates/jett_mir/src/lib.rs +++ b/crates/jett_mir/src/lib.rs @@ -1,10 +1,18 @@ //! Jett's backend-neutral control-flow graph representation. mod analysis; +mod breakpoint_regions; +mod call_owner_generations; +mod call_ownership; +pub use call_owner_generations::{ + CallGenerationSlot, CallGenerationStoragePlan, CallOwnerEscrowId, CallOwnerGenerationId, +}; +pub use call_ownership::{CallerAcquisitions, SourceAcquisition}; mod call_views; pub mod copy_values; mod generated_functions; mod handlers; +mod iteration_views; pub use generated_functions::prepare_native_generated_functions; mod sequences; pub use sequences::prepare_native_sequences; @@ -19,7 +27,7 @@ use jett_common::Span; use jett_hir::{self as hir, Expression, FieldId, FunctionIdentity, VariantId}; use jett_types::{TypeId, TypeInterner}; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct BlockId(u32); impl BlockId { @@ -46,6 +54,13 @@ pub struct Function { pub entry: BlockId, pub blocks: Vec, pub span: Span, + // Only canonical MIR preparation can authenticate a removed success edge. + prepared_absent_successes: Vec, + prepared_present_successes: Vec, + prepared_view_iterations: Vec, + original_view_iterations: Vec, + breakpoint_regions: Vec, + call_owner_generations: Vec, } impl Function { @@ -159,6 +174,21 @@ impl SequenceSource { #[derive(Debug, Clone, PartialEq)] pub enum StatementKind { + /// Empty, internal generation escrow; this is not an initialized Jett value. + OpenCallOwnerGeneration { + generation: CallOwnerGenerationId, + root: LocalId, + }, + /// Transfer the old owner once, after the independent RHS has been acquired. + ReplaceCallOwnerGeneration { + generation: CallOwnerGenerationId, + root: LocalId, + rhs_owner: LocalId, + }, + /// End the matching captured generation after its last call loan. + CloseCallOwnerGeneration { + generation: CallOwnerGenerationId, + }, /// A compiler-internal, call-bounded borrow with an exact eager borrow proof. /// Unlike a source Let alias, only this typed statement grants End authority. BeginCallView { @@ -335,6 +365,39 @@ struct FunctionValidator<'function, 'errors> { errors: &'errors mut Vec, } +/// Validate every invocation before preparation, including unused bodies and +/// disconnected blocks. Metadata origins must never become execution liveness. +pub fn validate_call_ownership( + program: &Program, + types: &TypeInterner, +) -> Result<(), Vec> { + validate(program)?; + let mut errors = Vec::new(); + let validation = call_ownership::ProgramValidation::new(program); + for function in &program.functions { + if let Err(message) = validation.validate_function(function, types) { + errors.push(ValidationError { + span: function.span, + message, + }); + } + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } +} + +/// Acquire a function's sealed source ownership evidence before native lowering. +pub fn validate_caller_acquisitions<'a>( + program: &Program, + function: &'a Function, + types: &TypeInterner, +) -> Result, String> { + call_ownership::validate_function(program, function, types) +} + impl FunctionValidator<'_, '_> { fn validate(&mut self) { let function = self.function; @@ -519,6 +582,16 @@ impl FunctionValidator<'_, '_> { fn statement(&mut self, statement: &Statement) { match &statement.kind { + StatementKind::OpenCallOwnerGeneration { root, .. } => { + self.check_local(*root, statement.span, "generation root") + } + StatementKind::ReplaceCallOwnerGeneration { + root, rhs_owner, .. + } => { + self.check_local(*root, statement.span, "generation root"); + self.check_local(*rhs_owner, statement.span, "generation acquired RHS"); + } + StatementKind::CloseCallOwnerGeneration { .. } => {} StatementKind::ReflectedContainerReady { source, .. } => { self.check_local(*source, statement.span, "reflected container source"); } @@ -823,6 +896,17 @@ impl FunctionValidator<'_, '_> { } fn expression(&mut self, expression: &Expression) { + let ownership = match &expression.kind { + hir::ExpressionKind::Call { ownership, .. } + | hir::ExpressionKind::Intrinsic { ownership, .. } + | hir::ExpressionKind::IndirectCall { ownership, .. } => Some(ownership), + _ => None, + }; + if let Some(ownership) = ownership { + ownership + .metadata_local_ids(|id| self.check_local(id, expression.span, "caller ownership")); + ownership.metadata_function_ids(|id| self.check_function(id, expression.span)); + } match &expression.kind { hir::ExpressionKind::Local(local) => { self.check_local(*local, expression.span, "expression"); @@ -871,6 +955,7 @@ impl FunctionValidator<'_, '_> { function, args, evaluation_order, + .. } => { self.check_function(*function, expression.span); self.check_evaluation_order(evaluation_order, args.len(), expression.span); @@ -918,6 +1003,7 @@ impl FunctionValidator<'_, '_> { callee, args, evaluation_order, + .. } => { self.expression(callee); self.check_evaluation_order(evaluation_order, args.len(), expression.span); @@ -1049,7 +1135,9 @@ impl FunctionValidator<'_, '_> { } pub fn lower(program: &hir::Program, types: &TypeInterner) -> Result> { - if let Err(errors) = hir::validate(program) { + if let Err(errors) = + hir::validate(program).and_then(|()| hir::validate_backend_types(program, types)) + { return Err(errors .into_iter() .map(|error| LowerError { @@ -1068,21 +1156,32 @@ pub fn lower(program: &hir::Program, types: &TypeInterner) -> Result, _>>() + .map_err(|error| vec![error])?, + }; + validate_call_ownership(&lowered, types).map_err(|errors| { + errors + .into_iter() + .map(|error| LowerError { + span: error.span, + message: error.message, + }) + .collect::>() + })?; + Ok(lowered) } fn lower_function( function: &hir::Function, types: &TypeInterner, function_param_modes: &std::collections::HashMap>, -) -> Function { +) -> Result { let mut builder = Builder::new(function.body.span, types, function_param_modes); builder.locals = function.locals.clone(); builder.view_params = function @@ -1102,7 +1201,9 @@ fn lower_function( if builder.open() && function.return_type == jett_types::TypeInterner::NOTHING { builder.terminate(TerminatorKind::Return(None), function.body.span); } - Function { + let breakpoint_capture = std::mem::take(&mut builder.breakpoint_capture); + let generation_capture = std::mem::take(&mut builder.generation_capture); + let mut lowered = Function { id: function.id, identity: function.identity.clone(), debug_kind: function.debug_kind.clone(), @@ -1113,9 +1214,32 @@ fn lower_function( entry: BlockId(0), blocks: builder.blocks, span: function.span, - } + prepared_absent_successes: Vec::new(), + prepared_present_successes: Vec::new(), + prepared_view_iterations: Vec::new(), + original_view_iterations: Vec::new(), + breakpoint_regions: Vec::new(), + call_owner_generations: Vec::new(), + }; + lowered.breakpoint_regions = + breakpoint_capture + .seal(&lowered) + .map_err(|message| LowerError { + span: function.span, + message, + })?; + lowered.call_owner_generations = + generation_capture + .seal(&lowered) + .map_err(|message| LowerError { + span: function.span, + message, + })?; + iteration_views::capture_original(&mut lowered, types); + Ok(lowered) } +#[derive(Clone)] struct Builder<'a> { types: &'a TypeInterner, function_param_modes: &'a std::collections::HashMap>, @@ -1126,6 +1250,8 @@ struct Builder<'a> { locals: Vec, handlers: Vec<(LocalId, BlockId)>, view_params: Vec, + breakpoint_capture: breakpoint_regions::Capture, + generation_capture: call_owner_generations::Capture, } impl<'a> Builder<'a> { @@ -1151,6 +1277,8 @@ impl<'a> Builder<'a> { locals: Vec::new(), handlers: Vec::new(), view_params: Vec::new(), + breakpoint_capture: breakpoint_regions::Capture::default(), + generation_capture: call_owner_generations::Capture::default(), } } @@ -1164,6 +1292,8 @@ impl<'a> Builder<'a> { span, }, }); + self.breakpoint_capture.block(id); + self.generation_capture.block(id); id } @@ -1175,13 +1305,22 @@ impl<'a> Builder<'a> { } fn terminate(&mut self, kind: TerminatorKind, span: Span) { - self.blocks[self.current.index() as usize].terminator = Terminator { kind, span }; + let value = Terminator { kind, span }; + self.breakpoint_capture.terminator(self.current, &value); + self.generation_capture.terminator(self.current, &value); + self.blocks[self.current.index() as usize].terminator = value; } fn push(&mut self, kind: StatementKind, span: Span) { + let index = self.blocks[self.current.index() as usize].statements.len(); + let value = Statement { kind, span }; + self.breakpoint_capture + .statement(self.current, index, &value); + self.generation_capture + .statement(self.current, index, &value); self.blocks[self.current.index() as usize] .statements - .push(Statement { kind, span }); + .push(value); } fn close_to(&mut self, target: BlockId, span: Span) { @@ -1232,14 +1371,52 @@ impl<'a> Builder<'a> { ); } hir::StatementKind::Assign { target, value } => { + // Keep the current owner readable throughout the entire RHS. + let generation = match target.kind { + hir::ExpressionKind::Local(root) => self + .generation_capture + .protected(root) + .map(|generation| (generation, root)), + _ => None, + }; let value = self.lower_value(value); - self.push( - StatementKind::Assign { - target: target.clone(), - value, - }, - statement.span, - ); + if let Some((generation, root)) = generation { + let rhs_owner = self.temporary(value.ty, value.span); + let acquired = ( + self.current, + self.blocks[self.current.index() as usize].statements.len(), + ); + self.push( + StatementKind::Let { + local: rhs_owner, + value, + }, + statement.span, + ); + let replacement = ( + self.current, + self.blocks[self.current.index() as usize].statements.len(), + ); + self.generation_capture + .replacement(generation, statement, rhs_owner, acquired, replacement) + .expect("constructor-selected generation preserves its checked Assign"); + self.push( + StatementKind::ReplaceCallOwnerGeneration { + generation, + root, + rhs_owner, + }, + statement.span, + ); + } else { + self.push( + StatementKind::Assign { + target: target.clone(), + value, + }, + statement.span, + ); + } } hir::StatementKind::Expression(value) => { let value = self.lower_value(value); @@ -1259,8 +1436,10 @@ impl<'a> Builder<'a> { // operand may move their owner. Other CFG branches retain them. 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.terminate(TerminatorKind::Return(value), statement.span); } hir::StatementKind::Break => { @@ -1308,6 +1487,14 @@ impl<'a> Builder<'a> { condition, bindings, } => { + let capturing = condition.is_some(); + if capturing { + self.breakpoint_capture.begin( + statement, + self.current, + self.blocks[self.current.index() as usize].statements.len(), + ); + } let condition = condition.as_ref().map(|value| self.lower_value(value)); self.push( StatementKind::Breakpoint { @@ -1316,6 +1503,12 @@ impl<'a> Builder<'a> { }, statement.span, ); + if capturing { + self.breakpoint_capture.end( + self.current, + self.blocks[self.current.index() as usize].statements.len() - 1, + ); + } } hir::StatementKind::Respond(value) => { self.end_call_views_since(0, statement.span); @@ -1416,7 +1609,7 @@ impl<'a> Builder<'a> { body: body_block, exit, }, - iterable.span, + statement_span, ); self.loops.push((header, exit, self.call_view_scopes.len())); self.current = body_block; @@ -2020,6 +2213,18 @@ function first(left: int64, right: int64) returns int64: field_validation: None, args: Vec::new(), evaluation_order: vec![0], + ownership: hir::CallOwnership::generated( + hir::GeneratedOperation::ReflectionMetadata { + intrinsic: hir::IntrinsicId::TypeName, + }, + &[], + &[], + &[], + TypeInterner::STRING, + &[], + &TypeInterner::new(), + ) + .expect("closed metadata source"), }; arms[0].target = BlockId(u32::MAX); *otherwise = BlockId(u32::MAX - 1); diff --git a/crates/jett_mir/src/move_values.rs b/crates/jett_mir/src/move_values.rs index f15e5df8..2b4f3417 100644 --- a/crates/jett_mir/src/move_values.rs +++ b/crates/jett_mir/src/move_values.rs @@ -169,6 +169,7 @@ impl MoveValuePlan { ) -> Result { 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))?; if function.identity.declaration.kind == jett_hir::DeclarationKind::ActorHandler { // Captured state is written back after a return/respond terminator. @@ -262,6 +263,8 @@ impl MoveValuePlan { borrow_sources: &borrow_sources, alias_sources: &alias_sources, call_views: &call_views, + caller_acquisitions: &caller_acquisitions, + taking_binding: None, validate: false, } .block(id)?; @@ -288,6 +291,8 @@ impl MoveValuePlan { borrow_sources: &borrow_sources, alias_sources: &alias_sources, call_views: &call_views, + caller_acquisitions: &caller_acquisitions, + taking_binding: None, validate: true, } .block(id)?; @@ -348,6 +353,8 @@ struct Flow<'a> { borrow_sources: &'a BTreeMap, alias_sources: &'a BTreeMap, call_views: &'a BTreeMap, + caller_acquisitions: &'a crate::CallerAcquisitions<'a>, + taking_binding: Option, validate: bool, } impl Flow<'_> { @@ -355,6 +362,56 @@ impl Flow<'_> { let block = &self.function.blocks[id.index() as usize]; for statement in &block.statements { match &statement.kind { + StatementKind::OpenCallOwnerGeneration { root, .. } => { + self.read(*root, "generation owner")?; + } + StatementKind::CloseCallOwnerGeneration { .. } => {} + StatementKind::ReplaceCallOwnerGeneration { + generation, + root, + rhs_owner, + } => { + let plan = self.caller_acquisitions.generation_storage(); + let slot = plan + .slot(*generation) + .ok_or("generation replacement has no validated storage")?; + if slot.root() != *root { + return Err("generation replacement changes its owner".into()); + } + let actual_root = self.read(*root, "generation owner")?; + self.read(*rhs_owner, "generation RHS owner")?; + self.require_owned_definition(*root)?; + self.require_owned_definition(*rhs_owner)?; + if self.validate + && self.aliases.iter().any(|loan| { + self.call_views.get(loan) == Some(&actual_root) + && !plan.covers_loan(*generation, crate::LocalId::new(*loan as u32)) + }) + { + return Err( + "generation replacement has an unrelated active call loan".into() + ); + } + if self.validate + && self.aliases.iter().any(|alias| { + !self.call_views.contains_key(alias) + && self.alias_sources.get(alias) == Some(&actual_root) + }) + { + return Err("generation replacement has a persistent source alias".into()); + } + if self.validate + && self + .active + .iter() + .any(|token| self.borrow_sources.get(token) == Some(&actual_root)) + { + return Err("generation replacement has an active iteration loan".into()); + } + self.reject_aliased_owner_change(rhs_owner.index() as usize)?; + self.state.remove(&(rhs_owner.index() as usize)); + self.state.insert(root.index() as usize); + } StatementKind::ReflectedContainerReady { source, .. } => { self.read(*source, "reflected container source")?; } @@ -451,7 +508,14 @@ impl Flow<'_> { self.aliases.insert(local.index() as usize); } else { self.reject_aliased_owner_change(local.index() as usize)?; - self.expr(value, false)?; + let source = self + .caller_acquisitions + .owner_initializer(*local) + .and_then(|source| source.binding); + let previous = std::mem::replace(&mut self.taking_binding, source); + let result = self.expr(value, false); + self.taking_binding = previous; + result?; } self.state.insert(local.index() as usize); } @@ -578,6 +642,17 @@ impl Flow<'_> { } fn expr(&mut self, value: &Expression, borrowed: bool) -> Result<(), String> { + let source = self.caller_acquisitions.argument_binding(value); + let previous = self.taking_binding; + if source.is_some() { + self.taking_binding = source; + } + let result = self.expr_inner(value, borrowed); + self.taking_binding = previous; + result + } + + fn expr_inner(&mut self, value: &Expression, borrowed: bool) -> Result<(), String> { match &value.kind { ExpressionKind::Local(local) => { let id = local.index() as usize; @@ -593,18 +668,20 @@ impl Flow<'_> { return Err(format!("cannot move borrowed native place {id}")); } if borrowed + && self.taking_binding != Some(*local) && (is_linear(self.types, value.ty) || is_function(self.types, value.ty)) { self.loans.insert(root); } // Verify/property owner reads clone, but the borrowed-local // rejection above applies before that independent-owner path. - if is_linear(self.types, value.ty) - && !borrowed - && !matches!( - self.function.identity.declaration.kind, - jett_hir::DeclarationKind::Verify | jett_hir::DeclarationKind::Property - ) + if self.taking_binding == Some(*local) + || (is_linear(self.types, value.ty) + && !borrowed + && !matches!( + self.function.identity.declaration.kind, + jett_hir::DeclarationKind::Verify | jett_hir::DeclarationKind::Property + )) { self.reject_aliased_owner_change(root)?; if self.validate @@ -639,6 +716,7 @@ impl Flow<'_> { function, args, evaluation_order, + .. } => { let saved = self.loans.clone(); for &index in evaluation_order { @@ -690,6 +768,7 @@ impl Flow<'_> { callee, args, evaluation_order, + .. } => { let Type::Function { view_params, .. } = self .types @@ -773,7 +852,11 @@ impl Flow<'_> { ExpressionKind::InterfaceCoerce { value: inner, .. } => { let unbox = is_erased_interface(self.types, inner.ty) && !is_erased_interface(self.types, value.ty); - self.expr(inner, borrowed || unbox)?; + let handled_input = self + .caller_acquisitions + .handled_conversion_input(value) + .is_some_and(|proved| std::ptr::eq(proved, inner.as_ref())); + self.expr(inner, borrowed || unbox || handled_input)?; } ExpressionKind::OptionalNone => {} ExpressionKind::StructConstruct { @@ -1612,7 +1695,7 @@ function inspect(view source: Packet) returns nothing: fn local_view_alias_plan_rejects_wrapped_qualified_string_owned_escapes() { for ty in ["secret[string]", "Label"] { let source = format!( - "type Label = string where true\nfunction consume(value: {ty}) returns nothing:\n return nothing\nfunction inspect(view source: {ty}) returns {ty}:\n {ty} borrowed = view source\n return clone borrowed\n" + "type Label = string where true\nfunction consume(value: {ty}) returns nothing:\n return nothing\nfunction inspect(view source: {ty}) returns {ty}:\n {ty} borrowed = view source\n consume(clone borrowed)\n return clone borrowed\n" ); for kind in [ DeclarationKind::Function, @@ -1645,33 +1728,38 @@ function inspect(view source: Packet) returns nothing: let function = &mut program.functions[index]; function.identity.declaration.kind = kind; if call { - let value = Expression { - span: view.span, - ty: TypeInterner::NOTHING, - kind: ExpressionKind::Call { - function: consume, - args: vec![view], - evaluation_order: vec![0], - }, - }; - let exit = function + // Retain the exact source-derived target/signature packet, + // then forge only the owning argument's physical tree. + let value = function .blocks .iter_mut() - .find(|block| { - matches!(block.terminator.kind, TerminatorKind::Return(Some(_))) + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + StatementKind::Evaluate(value) + if matches!(value.kind, ExpressionKind::Call { function, .. } if function == consume) => Some(value), + _ => None, }) - .unwrap(); - exit.statements.push(crate::Statement { - span: value.span, - kind: StatementKind::Evaluate(value), - }); + .expect("checked consuming call"); + let ExpressionKind::Call { + args, ownership, .. + } = &mut value.kind + else { + unreachable!("selected call expression"); + }; + assert!(matches!(ownership, jett_hir::CallOwnership::Source(_))); + assert!(matches!(args[0].kind, ExpressionKind::Clone(_))); + args[0] = view; } else { replace_return(function, view); } let error = MoveValuePlan::analyze(&program, &program.functions[index], &types) .unwrap_err(); assert!( - error.contains("native view cannot escape into an owning value"), + error.contains("native view cannot escape into an owning value") + || error.contains("call ownership") + || error.contains("source operand") + || error.contains("source caller") + || error.contains("source occurrence"), "{ty}, {kind:?}, call={call}: {error}" ); } @@ -1741,10 +1829,12 @@ function inspect() returns function(int64) returns int64: function.identity.declaration.kind = kind; let error = MoveValuePlan::analyze(&program, &program.functions[index], &types).unwrap_err(); - assert!( - error.contains("cannot move borrowed native place"), - "{kind:?}: {error}" - ); + let expected = if kind == DeclarationKind::Function { + "cannot move borrowed native place" + } else { + "call ownership source context is not its lexical owner" + }; + assert!(error.contains(expected), "{kind:?}: {error}"); } } diff --git a/crates/jett_mir/src/sequences.rs b/crates/jett_mir/src/sequences.rs index 193eb6da..b5f9cdd2 100644 --- a/crates/jett_mir/src/sequences.rs +++ b/crates/jett_mir/src/sequences.rs @@ -67,10 +67,14 @@ fn projected_source( pub fn prepare_native_sequences(program: &mut Program, types: &TypeInterner) { // This pass must not make malformed, unreachable input disappear before // the caller reports its original validation errors. - if validate(program).is_err() { + if validate_call_ownership(program, types).is_err() { return; } for function in &mut program.functions { + let before_pass = (!function.breakpoint_regions.is_empty() + || crate::call_owner_generations::has_records(function)) + .then(|| function.clone()); + let mut valid_regions = true; let mut removed_uninhabited_body = false; let count = function.blocks.len(); for index in 0..count { @@ -86,6 +90,10 @@ pub fn prepare_native_sequences(program: &mut Program, types: &TypeInterner) { else { continue; }; + let Ok(iteration_plan) = crate::iteration_views::original_plan(function, header, types) + else { + continue; + }; if iterable.ty.index() as usize >= types.len() { continue; } @@ -93,7 +101,7 @@ pub fn prepare_native_sequences(program: &mut Program, types: &TypeInterner) { Type::List(element) | Type::Set(element) if value_binding.is_none() => { (*element, None) } - Type::Map(key, map_value) if value_binding.is_some() => (*key, Some(*map_value)), + Type::Map(key, map_value) => (*key, value_binding.map(|_| *map_value)), Type::String if value_binding.is_none() => (TypeInterner::STRING, None), _ => continue, }; @@ -138,6 +146,18 @@ 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)) + .then(|| function.clone()); + let mut region_edit = crate::breakpoint_regions::SequenceEdit { + header, + before: function.blocks[index].terminator.clone(), + after: function.blocks[index].terminator.clone(), + append: Vec::new(), + prefix: None, + redirects: Vec::new(), + blocks: Vec::new(), + }; let span = iterable.span; let cursor = temporary(function, TypeInterner::INT64, span); let length = temporary(function, TypeInterner::INT64, span); @@ -207,22 +227,47 @@ pub fn prepare_native_sequences(program: &mut Program, types: &TypeInterner) { }, span, }); + region_edit + .append + .push((BlockId(preheader as u32), init.clone())); function.blocks[preheader].statements.extend(init); if uninhabited { // Keep evaluation and SequenceLength: it validates pending // containers and projected ancestors even when no item exists. // No runtime value can be extracted for the checked binder. if by_view { - function.blocks[index].statements.push(Statement { + let end = Statement { kind: StatementKind::IterationBorrow { source, token: cursor, start: false, }, span, - }); + }; + region_edit.append.push((header, vec![end.clone()])); + function.blocks[index].statements.push(end); } 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( + function, + before_iteration, + ®ion_edit, + types, + ) + .is_err() + || crate::breakpoint_regions::sequence_transition( + function, + before_iteration, + region_edit, + types, + ) + .is_err()) + { + valid_regions = false; + break; + } removed_uninhabited_body = true; continue; } @@ -246,7 +291,7 @@ pub fn prepare_native_sequences(program: &mut Program, types: &TypeInterner) { source: source.clone(), index: cursor, target: key, - part: if map_value.is_some() { + part: if matches!(types.resolve(iterable.ty), Type::Map(..)) { SequencePart::Key } else { SequencePart::Element @@ -289,6 +334,7 @@ pub fn prepare_native_sequences(program: &mut Program, types: &TypeInterner) { }, ); } + region_edit.prefix = Some((body, prefix.clone())); prefix.append(&mut function.blocks[body.index() as usize].statements); function.blocks[body.index() as usize].statements = prefix; if by_view { @@ -336,13 +382,68 @@ pub fn prepare_native_sequences(program: &mut Program, types: &TypeInterner) { span, }, }); + region_edit.blocks.push( + function + .blocks + .last() + .expect("canonical edge split was appended") + .clone(), + ); + let old = function.blocks[i].terminator.clone(); redirect_edge(&mut function.blocks[i].terminator.kind, target, end); + region_edit.redirects.push(( + function.blocks[i].id, + old, + function.blocks[i].terminator.clone(), + )); } } } + region_edit.after = function.blocks[index].terminator.clone(); + // Redirect edits operate on the just-created canonical header, so + // its initial replacement precedes the exact redirect snapshots. + if let Some((_, old, _)) = region_edit + .redirects + .iter() + .find(|(block, _, _)| *block == header) + { + region_edit.after = old.clone(); + } + if let Some(before_iteration) = &before_iteration + && (crate::call_owner_generations::sequence_transition( + function, + before_iteration, + ®ion_edit, + types, + ) + .is_err() + || crate::breakpoint_regions::sequence_transition( + function, + before_iteration, + region_edit, + types, + ) + .is_err()) + { + valid_regions = false; + break; + } + if let Some(original) = iteration_plan { + crate::iteration_views::retain_prepared( + function, + original, + source, + cursor, + length, + BlockId(preheader as u32), + span, + ); + } } - if removed_uninhabited_body { - prune::unreachable(function); + if !valid_regions || (removed_uninhabited_body && !prune::unreachable(function)) { + if let Some(before_pass) = before_pass { + *function = before_pass; + } } } } diff --git a/crates/jett_mir/src/sequences/prune.rs b/crates/jett_mir/src/sequences/prune.rs index 41292c0a..36d9f02b 100644 --- a/crates/jett_mir/src/sequences/prune.rs +++ b/crates/jett_mir/src/sequences/prune.rs @@ -3,9 +3,12 @@ //! below therefore name dense local/block tables, including unreachable input. use super::*; -pub(crate) fn unreachable(function: &mut Function) { +pub(crate) fn unreachable(function: &mut Function) -> bool { + let before = (!function.breakpoint_regions.is_empty() + || crate::call_owner_generations::has_records(function)) + .then(|| function.clone()); let Ok(cfg) = ControlFlowGraph::analyze(function) else { - return; + return false; }; let mut reachable = vec![false; function.blocks.len()]; let mut pending = vec![function.entry]; @@ -32,13 +35,38 @@ pub(crate) fn unreachable(function: &mut Function) { for block in &mut function.blocks { block_targets(&mut block.terminator.kind, &remap_block); } + function + .prepared_absent_successes + .retain_mut(|record| record.remap_blocks(&blocks)); + function + .prepared_present_successes + .retain_mut(|record| record.remap_blocks(&blocks)); + function + .prepared_view_iterations + .retain_mut(|record| record.remap_blocks(&blocks)); + function + .original_view_iterations + .retain_mut(|record| record.remap_blocks(&blocks)); - unused_locals(function); + if before.as_ref().is_some_and(|before| { + crate::call_owner_generations::remap_blocks(function, before, &blocks).is_err() + }) || crate::breakpoint_regions::remap_blocks(function, &blocks).is_err() + || !unused_locals(function) + { + if let Some(before) = before { + *function = before; + } + return false; + } + true } /// 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) { +pub(crate) fn unused_locals(function: &mut Function) -> bool { + let before = (!function.breakpoint_regions.is_empty() + || crate::call_owner_generations::has_records(function)) + .then(|| function.clone()); let mut used = vec![false; function.locals.len()]; for parameter in &function.params { used[parameter.local.index() as usize] = true; @@ -96,9 +124,31 @@ pub(crate) fn unused_locals(function: &mut Function) { for block in &mut function.blocks { block_locals(block, &mut remap_local, &mut remap_floor); } + // Certificate metadata creates no execution reads or otherwise dead slots. + function + .prepared_absent_successes + .retain_mut(|record| record.remap_locals(&locals)); + function + .prepared_present_successes + .retain_mut(|record| record.remap_locals(&locals)); + function + .prepared_view_iterations + .retain_mut(|record| record.remap_locals(&locals)); + function + .original_view_iterations + .retain_mut(|record| record.remap_locals(&locals)); + if crate::call_owner_generations::remap_locals(function, &locals).is_err() + || crate::breakpoint_regions::remap_locals(function, &locals).is_err() + { + if let Some(before) = before { + *function = before; + } + return false; + } + true } -fn block_targets(kind: &mut TerminatorKind, visit: &impl Fn(&mut BlockId)) { +pub(crate) fn block_targets(kind: &mut TerminatorKind, visit: &impl Fn(&mut BlockId)) { match kind { TerminatorKind::Goto(target) => visit(target), TerminatorKind::Branch { @@ -149,9 +199,35 @@ pub(crate) fn block_locals( block: &mut BasicBlock, visit: &mut impl FnMut(&mut LocalId), floor: &mut impl FnMut(&mut u32), +) { + visit_block_locals(block, visit, floor, true); +} + +/// Execution reads exclude structural caller provenance and staging witnesses. +pub(crate) fn block_runtime_locals( + block: &mut BasicBlock, + visit: &mut impl FnMut(&mut LocalId), + floor: &mut impl FnMut(&mut u32), +) { + visit_block_locals(block, visit, floor, false); +} + +fn visit_block_locals( + block: &mut BasicBlock, + visit: &mut impl FnMut(&mut LocalId), + floor: &mut impl FnMut(&mut u32), + metadata: bool, ) { for statement in &mut block.statements { match &mut statement.kind { + StatementKind::OpenCallOwnerGeneration { root, .. } => visit(root), + StatementKind::ReplaceCallOwnerGeneration { + root, rhs_owner, .. + } => { + visit(root); + visit(rhs_owner); + } + StatementKind::CloseCallOwnerGeneration { .. } => {} StatementKind::ReflectedContainerReady { source, .. } => visit(source), StatementKind::SequenceLength { source, target } => { source_local(source, visit); @@ -182,20 +258,20 @@ pub(crate) fn block_locals( local, call: value, .. } => { visit(local); - expression(value, visit, floor); + expression(value, visit, floor, metadata); } StatementKind::EndCallView { local } => visit(local), StatementKind::Assign { target, value } => { - expression(target, visit, floor); - expression(value, visit, floor); + expression(target, visit, floor, metadata); + expression(value, visit, floor, metadata); } StatementKind::Evaluate(value) | StatementKind::HandleDefault(value) => { - expression(value, visit, floor) + expression(value, visit, floor, metadata) } StatementKind::Assert { condition, message } => { - expression(condition, visit, floor); + expression(condition, visit, floor, metadata); if let Some(message) = message { - expression(message, visit, floor); + expression(message, visit, floor, metadata); } } StatementKind::Trace(local) => visit(local), @@ -204,7 +280,7 @@ pub(crate) fn block_locals( bindings, } => { if let Some(condition) = condition { - expression(condition, visit, floor); + expression(condition, visit, floor, metadata); } for binding in bindings { visit(binding); @@ -215,19 +291,19 @@ pub(crate) fn block_locals( match &mut block.terminator.kind { TerminatorKind::Return(value) => { if let Some(value) = value { - expression(value, visit, floor); + expression(value, visit, floor, metadata); } } TerminatorKind::Respond(value) | TerminatorKind::Branch { condition: value, .. - } => expression(value, visit, floor), + } => expression(value, visit, floor, metadata), TerminatorKind::Switch { scrutinee, variants, .. } => { - expression(scrutinee, visit, floor); + expression(scrutinee, visit, floor, metadata); for (_, _, bindings) in variants { for binding in bindings { visit(binding); @@ -244,10 +320,10 @@ pub(crate) fn block_locals( if let Some(value) = value { visit(value); } - expression(iterable, visit, floor); + expression(iterable, visit, floor, metadata); } TerminatorKind::ReflectedTypeDispatch { type_info, .. } => { - expression(type_info, visit, floor) + expression(type_info, visit, floor, metadata) } TerminatorKind::Goto(_) | TerminatorKind::Unreachable => {} } @@ -257,7 +333,27 @@ fn expression( value: &mut Expression, visit: &mut impl FnMut(&mut LocalId), floor: &mut impl FnMut(&mut u32), + metadata: bool, ) { + if metadata { + let packet = match &mut value.kind { + ExpressionKind::Call { ownership, .. } + | ExpressionKind::IndirectCall { ownership, .. } + | ExpressionKind::Intrinsic { ownership, .. } => Some(ownership), + _ => None, + }; + if let Some(packet) = packet { + // Infallible structural remapping does not create execution reads. + let result = packet.remap_metadata_locals(|mut id| { + visit(&mut id); + Ok::<_, std::convert::Infallible>(id) + }); + match result { + Ok(()) => {} + Err(impossible) => match impossible {}, + } + } + } match &mut value.kind { ExpressionKind::Local(local) => visit(local), ExpressionKind::ClosureRef { captures, .. } => { @@ -266,8 +362,8 @@ fn expression( } } ExpressionKind::Binary { left, right, .. } => { - expression(left, visit, floor); - expression(right, visit, floor); + expression(left, visit, floor, metadata); + expression(right, visit, floor, metadata); } ExpressionKind::Unary { value, .. } | ExpressionKind::ResultOk(value) @@ -289,7 +385,7 @@ fn expression( | ExpressionKind::Cancel(value) | ExpressionKind::Field { base: value, .. } | ExpressionKind::View(value) - | ExpressionKind::Clone(value) => expression(value, visit, floor), + | ExpressionKind::Clone(value) => expression(value, visit, floor, metadata), ExpressionKind::Call { args, .. } | ExpressionKind::Intrinsic { args, .. } | ExpressionKind::ActorSpawn { args, .. } @@ -299,7 +395,7 @@ fn expression( | ExpressionKind::EnumConstruct { payloads: args, .. } | ExpressionKind::ListConstruct { elements: args } => { for arg in args { - expression(arg, visit, floor); + expression(arg, visit, floor, metadata); } } ExpressionKind::IndirectCall { callee, args, .. } @@ -313,15 +409,15 @@ fn expression( payloads: args, .. } => { - expression(callee, visit, floor); + expression(callee, visit, floor, metadata); for arg in args { - expression(arg, visit, floor); + expression(arg, visit, floor, metadata); } } ExpressionKind::MapConstruct { entries } => { for entry in entries { - expression(&mut entry.key, visit, floor); - expression(&mut entry.value, visit, floor); + expression(&mut entry.key, visit, floor, metadata); + expression(&mut entry.value, visit, floor, metadata); } } ExpressionKind::Handle { @@ -330,16 +426,16 @@ fn expression( failure, .. } => { - expression(target, visit, floor); + expression(target, visit, floor, metadata); if let Some(local) = error_local { visit(local); } - hir_block(failure, visit, floor); + hir_block(failure, visit, floor, metadata); } ExpressionKind::StringInterpolation(parts) => { for part in parts { if let hir::StringSegment::Value(value) = part { - expression(value, visit, floor); + expression(value, visit, floor, metadata); } } } @@ -354,7 +450,7 @@ fn expression( visit(local); } floor(local_floor); - hir_block(body, visit, floor); + hir_block(body, visit, floor, metadata); } ExpressionKind::Int(_) | ExpressionKind::Float(_) @@ -373,39 +469,40 @@ fn hir_block( block: &mut hir::Block, visit: &mut impl FnMut(&mut LocalId), floor: &mut impl FnMut(&mut u32), + metadata: bool, ) { for statement in &mut block.statements { match &mut statement.kind { hir::StatementKind::Let { local, value } => { visit(local); - expression(value, visit, floor); + expression(value, visit, floor, metadata); } hir::StatementKind::Assign { target, value } => { - expression(target, visit, floor); - expression(value, visit, floor); + expression(target, visit, floor, metadata); + expression(value, visit, floor, metadata); } hir::StatementKind::Return(value) => { if let Some(value) = value { - expression(value, visit, floor); + expression(value, visit, floor, metadata); } } hir::StatementKind::HandleDefault(value) | hir::StatementKind::Expression(value) - | hir::StatementKind::Respond(value) => expression(value, visit, floor), + | hir::StatementKind::Respond(value) => expression(value, visit, floor, metadata), hir::StatementKind::If { condition, then_block, else_block, } => { - expression(condition, visit, floor); - hir_block(then_block, visit, floor); + expression(condition, visit, floor, metadata); + hir_block(then_block, visit, floor, metadata); if let Some(block) = else_block { - hir_block(block, visit, floor); + hir_block(block, visit, floor, metadata); } } hir::StatementKind::While { condition, body } => { - expression(condition, visit, floor); - hir_block(body, visit, floor); + expression(condition, visit, floor, metadata); + hir_block(body, visit, floor, metadata); } hir::StatementKind::For { key, @@ -418,22 +515,22 @@ fn hir_block( if let Some(value) = value { visit(value); } - expression(iterable, visit, floor); - hir_block(body, visit, floor); + expression(iterable, visit, floor, metadata); + hir_block(body, visit, floor, metadata); } hir::StatementKind::Match { scrutinee, arms } => { - expression(scrutinee, visit, floor); + expression(scrutinee, visit, floor, metadata); for arm in arms { for binding in &mut arm.bindings { visit(binding); } - hir_block(&mut arm.body, visit, floor); + hir_block(&mut arm.body, visit, floor, metadata); } } hir::StatementKind::Assert { condition, message } => { - expression(condition, visit, floor); + expression(condition, visit, floor, metadata); if let Some(value) = message { - expression(value, visit, floor); + expression(value, visit, floor, metadata); } } hir::StatementKind::Trace(local) => visit(local), @@ -442,17 +539,17 @@ fn hir_block( bindings, } => { if let Some(value) = condition { - expression(value, visit, floor); + expression(value, visit, floor, metadata); } for binding in bindings { visit(binding); } } - hir::StatementKind::Scope(block) => hir_block(block, visit, floor), + hir::StatementKind::Scope(block) => hir_block(block, visit, floor, metadata), hir::StatementKind::ReflectedTypeDispatch { type_info, arms } => { - expression(type_info, visit, floor); + expression(type_info, visit, floor, metadata); for arm in arms { - hir_block(&mut arm.body, visit, floor); + hir_block(&mut arm.body, visit, floor, metadata); } } hir::StatementKind::Break | hir::StatementKind::Continue => {} diff --git a/crates/jett_mir/src/uninhabited_sums.rs b/crates/jett_mir/src/uninhabited_sums.rs index 411a670a..af587c93 100644 --- a/crates/jett_mir/src/uninhabited_sums.rs +++ b/crates/jett_mir/src/uninhabited_sums.rs @@ -3,6 +3,85 @@ use super::*; use jett_hir::ExpressionKind; use jett_types::Type; +/// Private preparation authority. The original plan is never a public setter +/// and this record is minted only beside the canonical Branch-to-Goto rewrite. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct PreparedAbsentSuccess { + plan: call_ownership::AbsentSuccessPlan, +} + +impl PreparedAbsentSuccess { + pub(super) fn plan(&self) -> &call_ownership::AbsentSuccessPlan { + &self.plan + } + + pub(super) fn remap_blocks(&mut self, blocks: &[Option]) -> bool { + let get = |id: BlockId| blocks.get(id.index() as usize).copied().flatten(); + let (Some(selecting), Some(failure)) = (get(self.plan.selecting), get(self.plan.failure)) + else { + return false; + }; + self.plan.selecting = selecting; + self.plan.failure = failure; + true + } + + pub(super) fn remap_locals(&mut self, locals: &[Option]) -> bool { + let get = |id: LocalId| locals.get(id.index() as usize).copied().flatten(); + let (Some(source), Some(tag), Some(output)) = ( + get(self.plan.source), + get(self.plan.tag), + get(self.plan.output), + ) else { + return false; + }; + self.plan.source = source; + self.plan.tag = tag; + self.plan.output = output; + true + } +} + +/// Distinct private authority for an inhabited successful payload whose error +/// arm is Never. Original validation proves the removed false edge; this record +/// retains only the actual selected blocks, runtime tag and successful take. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct PreparedPresentSuccess { + plan: call_ownership::PresentSuccessPlan, +} + +impl PreparedPresentSuccess { + pub(super) fn plan(&self) -> &call_ownership::PresentSuccessPlan { + &self.plan + } + + pub(super) fn remap_blocks(&mut self, blocks: &[Option]) -> bool { + let get = |id: BlockId| blocks.get(id.index() as usize).copied().flatten(); + let (Some(selecting), Some(success)) = (get(self.plan.selecting), get(self.plan.success)) + else { + return false; + }; + self.plan.selecting = selecting; + self.plan.success = success; + true + } + + pub(super) fn remap_locals(&mut self, locals: &[Option]) -> bool { + let get = |id: LocalId| locals.get(id.index() as usize).copied().flatten(); + let (Some(source), Some(tag), Some(output)) = ( + get(self.plan.source), + get(self.plan.tag), + get(self.plan.output), + ) else { + return false; + }; + self.plan.source = source; + self.plan.tag = tag; + self.plan.output = output; + true + } +} + #[cfg(test)] mod tests; @@ -10,10 +89,26 @@ mod tests; /// `SumTag` still evaluates the sum and rejects pending values at runtime. pub fn prepare_native_uninhabited_sums(program: &mut Program, types: &TypeInterner) { // Never hide an invalid ID in an arm that compaction would otherwise erase. - if validate(program).is_err() { + if validate_call_ownership(program, types).is_err() { return; } - for function in &mut program.functions { + // Obtain all plans while every original block and source occurrence exists. + // Any failure leaves the entire input unchanged, as does the gate above. + let Ok(plans) = program + .functions + .iter() + .map(|function| { + call_ownership::validate_function(program, function, types) + .map(|proof| (proof.absent_successes, proof.present_successes)) + }) + .collect::, _>>() + else { + return; + }; + for (function, (absent_plans, present_plans)) in program.functions.iter_mut().zip(plans) { + let before = (!function.breakpoint_regions.is_empty() + || crate::call_owner_generations::has_records(function)) + .then(|| function.clone()); let mut changed = false; for block in &mut function.blocks { let Some(Statement { @@ -61,13 +156,41 @@ pub fn prepare_native_uninhabited_sums(program: &mut Program, types: &TypeIntern }; // Retain every statement, including the source snapshot and SumTag. // Replacing only this edge preserves evaluation and failure order. + for plan in absent_plans.iter().filter(|plan| { + plan.selecting == block.id + && plan.failure == selected + && plan.source == *source + && plan.tag == *target + }) { + function + .prepared_absent_successes + .push(PreparedAbsentSuccess { plan: plan.clone() }); + } + for plan in present_plans.iter().filter(|plan| { + plan.selecting == block.id + && plan.success == selected + && plan.source == *source + && plan.tag == *target + }) { + function + .prepared_present_successes + .push(PreparedPresentSuccess { plan: plan.clone() }); + } block.terminator.kind = TerminatorKind::Goto(selected); changed = true; } if changed { // Every nested tag has its own checked source type, so one scan // selects all eligible arms before a single dense compaction. - sequences::prune::unreachable(function); + let regions_valid = before.as_ref().is_none_or(|before| { + 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 let Some(before) = before { + *function = before; + } + } } } } diff --git a/crates/jett_typecheck/src/caller_ownership.rs b/crates/jett_typecheck/src/caller_ownership.rs new file mode 100644 index 00000000..96890b95 --- /dev/null +++ b/crates/jett_typecheck/src/caller_ownership.rs @@ -0,0 +1,444 @@ +//! Checked caller disposition, independent of physical callee access. +//! +//! These facts belong to one resolution session and one concrete checked body. +//! An inserted physical borrow never supplies a source `view` witness. +use jett_common::Span; +use jett_intrinsics::IntrinsicId; +use jett_resolve::scope::DefId; +use jett_types::TypeId; + +use crate::{ + CheckedBindingMode, CheckedGenericCall, CheckedInterfaceCall, CheckedMethodCall, + CheckedViewSource, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CheckedCallerSyntax { + Bare, + WrittenView, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CheckedCalleeAccess { + Owned, + View, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CheckedCallerEffect { + Copy, + RetainBorrow, + TransferOwned, + RelinquishOwned, + ObserveData, +} + +/// This context describes the source occurrence, never its instantiating caller. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum CheckedOwnershipContext { + #[default] + Ordinary, + Verify, + Property, + BreakpointExpression, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CheckedBindingFact { + pub definition: DefId, + pub declaration_span: Span, + pub ty: TypeId, + pub mode: CheckedBindingMode, + pub mutable: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CheckedCallerOrigin { + Binding(CheckedBindingFact), + BorrowedProjection { source: CheckedViewSource }, + OwnedFieldCopy { parent: CheckedViewSource }, + OwnedExpression, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CheckedArgumentOwnership { + pub source_span: Span, + pub source_index: usize, + pub parameter_index: usize, + pub actual_type: TypeId, + pub parameter_type: TypeId, + pub syntax: CheckedCallerSyntax, + pub origin: CheckedCallerOrigin, + pub callee_access: CheckedCalleeAccess, + pub effect: CheckedCallerEffect, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CheckedInvocationTarget { + Resolved(DefId), + Generic(CheckedGenericCall), + Method(CheckedMethodCall), + Interface(CheckedInterfaceCall), + Indirect(TypeId), + Intrinsic(IntrinsicId), +} + +/// Each role is checked against a closed intrinsic's source operand contract. +/// HIR-added metadata is separate from these original source occurrences. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CheckedIntrinsicOperandRole { + Copy { ty: TypeId }, + Owned { ty: TypeId }, + View { ty: TypeId }, + PrintArgument { ty: TypeId }, +} + +impl CheckedIntrinsicOperandRole { + pub fn ty(self) -> TypeId { + match self { + Self::Copy { ty } + | Self::Owned { ty } + | Self::View { ty } + | Self::PrintArgument { ty } => ty, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CheckedInvocationShape { + Function { + signature_type: TypeId, + }, + Intrinsic { + intrinsic: IntrinsicId, + result_type: TypeId, + operands: Vec, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CheckedCallOwnership { + pub target: CheckedInvocationTarget, + pub shape: CheckedInvocationShape, + pub context: CheckedOwnershipContext, + /// Original occurrences in lexical source order, with explicit formal joins. + pub arguments: Vec, +} + +/// Closed source operand access; this never authorizes retaining a bare caller. +/// The successful checker signature supplies types, while this table seals arity +/// and access independently from backend physical borrowing. +pub fn intrinsic_operand_access( + id: IntrinsicId, + index: usize, + arity: usize, +) -> Option { + use IntrinsicId::*; + let accepted_arity = match id { + Print | Println => true, + Range => (1..=3).contains(&arity), + BytesNew + | ListNew + | MapNew + | SetNew + | MathPi + | MathE + | TypeBitfieldFields + | TypeBitfieldLayout + | TypeConstructStart + | TypeFields + | TypeHasSecret + | TypeInfo + | TypeKind + | TypeKindTag + | TypeMachineLayout + | TypeMachineStates + | TypeMachineTransitions + | TypeName + | TypePrimitiveTag + | TypeVariants + | UuidNew => arity == 0, + BitfieldFromBytes + | BitfieldToBytes + | BytesFromHex + | BytesFromString + | BytesLength + | BytesToHex + | BytesToString + | ClockNow + | CryptoMd5 + | CryptoSha256 + | CryptoSha512 + | CsvParse + | CsvParseWithHeader + | CsvStringify + | EncodingBase64Decode + | EncodingBase64Encode + | EncodingFormDecode + | EncodingFormEncode + | EncodingHexDecode + | EncodingUrlDecode + | EncodingUrlEncode + | EnvironmentArgs + | Float32FromFloat64 + | Float64FromInt64 + | Float64FromString + | Int64FromFloat64 + | Int64FromString + | JsonParse + | JsonParseExact + | JsonSerialize + | JsonSerializePublic + | ListIsSorted + | ListLength + | ListSort + | ListSum + | MapLength + | MathAbs + | MathAverage + | MathCeil + | MathCos + | MathFactorial + | MathFloor + | MathKernelAbs + | MathLog + | MathLog10 + | MathLog2 + | MathMedian + | MathRound + | MathSin + | MathSqrt + | MathTan + | SecretRedact + | SetLength + | StringCharCount + | StringChars + | StringFromBool + | StringFromFloat64 + | StringFromInt64 + | StringFromUint64 + | StringIsAlpha + | StringIsNumeric + | StringLines + | StringLower + | StringSlugify + | StringToLowerFirst + | StringToUpperFirst + | StringTrim + | StringTrimEnd + | StringTrimStart + | StringUpper + | StringWords + | TestMockClock + | TestMockRandom + | TypeArg + | TypeConstructFinish + | TypeConstructMachineStart + | TypeConstructVariantStart + | TypeMachineStateValue + | TypeVariantValue + | Uint64FromString + | RandomBool + | RandomUnitFloat64 => arity == 1, + BytesConcat + | BytesGet + | CryptoHmacSha256 + | EnvironmentGet + | FilesystemReadFile + | ListAppend + | ListGetClone + | ListGroupBy + | ListRemoveAt + | ListSortBy + | ListSortByIndex + | LogEmit + | MapFromLists + | MapGet + | MapHas + | MapRemove + | MathGcd + | MathKernelMax + | MathKernelMin + | MathLcm + | MathMax + | MathMin + | MathMod + | MathPow + | SecretCompare + | SetAdd + | SetContains + | SetRemove + | StdoutWrite + | StringCount + | StringIndexOf + | StringJoin + | StringRepeat + | StringSplit + | TestMockEnvironment + | TypeFieldValue + | TypeMachineFieldValue + | TypeVariantFieldValue => arity == 2, + BytesSlice | FilesystemWriteFile | ListInsertAt | ListSwap | MapInsert | MathClamp + | StringReplace | StringSlice | TypeConstructPut | RandomBounded => arity == 3, + GraphicsRun => arity == 5, + }; + if !accepted_arity || index >= arity { + return None; + } + let view = match id { + Print | Println => false, + BytesLength + | BytesSlice + | BytesToString + | BytesGet + | BytesToHex + | EncodingBase64Encode + | ListLength + | ListGetClone + | ListIsSorted + | ListSum + | MathAverage + | MathMedian + | MapLength + | SetLength + | JsonSerialize + | JsonSerializePublic + | TypeVariantValue + | TypeMachineStateValue + | TypeConstructVariantStart + | TypeConstructMachineStart + | ClockNow + | EnvironmentGet + | EnvironmentArgs + | FilesystemReadFile + | FilesystemWriteFile + | StdoutWrite + | RandomBounded + | RandomBool + | RandomUnitFloat64 + | LogEmit + | GraphicsRun => index == 0, + MapGet + | MapHas + | SetContains + | TypeFieldValue + | TypeVariantFieldValue + | TypeMachineFieldValue + | CryptoHmacSha256 + | SecretCompare => true, + CryptoMd5 | CryptoSha256 | CryptoSha512 | SecretRedact => true, + MapRemove | SetRemove | TypeConstructPut => index == 1, + BitfieldFromBytes + | BitfieldToBytes + | BytesConcat + | BytesFromHex + | BytesFromString + | BytesNew + | CsvParse + | CsvParseWithHeader + | CsvStringify + | EncodingBase64Decode + | EncodingFormDecode + | EncodingFormEncode + | EncodingHexDecode + | EncodingUrlDecode + | EncodingUrlEncode + | Float32FromFloat64 + | Float64FromInt64 + | Float64FromString + | Int64FromFloat64 + | Int64FromString + | JsonParse + | JsonParseExact + | ListAppend + | ListGroupBy + | ListInsertAt + | ListNew + | ListRemoveAt + | ListSort + | ListSortBy + | ListSortByIndex + | ListSwap + | MapFromLists + | MapInsert + | MapNew + | MathAbs + | MathCeil + | MathClamp + | MathCos + | MathE + | MathFactorial + | MathFloor + | MathGcd + | MathKernelAbs + | MathKernelMax + | MathKernelMin + | MathLcm + | MathLog + | MathLog10 + | MathLog2 + | MathMax + | MathMin + | MathMod + | MathPi + | MathPow + | MathRound + | MathSin + | MathSqrt + | MathTan + | Range + | SetAdd + | SetNew + | StringCharCount + | StringChars + | StringCount + | StringFromBool + | StringFromFloat64 + | StringFromInt64 + | StringFromUint64 + | StringIndexOf + | StringIsAlpha + | StringIsNumeric + | StringJoin + | StringLines + | StringLower + | StringRepeat + | StringReplace + | StringSlice + | StringSlugify + | StringSplit + | StringToLowerFirst + | StringToUpperFirst + | StringTrim + | StringTrimEnd + | StringTrimStart + | StringUpper + | StringWords + | TestMockClock + | TestMockEnvironment + | TestMockRandom + | TypeArg + | TypeBitfieldFields + | TypeBitfieldLayout + | TypeConstructFinish + | TypeConstructStart + | TypeFields + | TypeHasSecret + | TypeInfo + | TypeKind + | TypeKindTag + | TypeMachineLayout + | TypeMachineStates + | TypeMachineTransitions + | TypeName + | TypePrimitiveTag + | TypeVariants + | Uint64FromString + | UuidNew => false, + }; + Some(if view { + CheckedCalleeAccess::View + } else { + CheckedCalleeAccess::Owned + }) +} diff --git a/crates/jett_typecheck/src/checker.rs b/crates/jett_typecheck/src/checker.rs index 486196d0..0a1bf97a 100644 --- a/crates/jett_typecheck/src/checker.rs +++ b/crates/jett_typecheck/src/checker.rs @@ -22,6 +22,12 @@ use jett_types::{ StructDef as TypeStructDef, StructId, Type, TypeId, TypeInterner, VariantDef, }; +use crate::caller_ownership::{ + CheckedArgumentOwnership, CheckedBindingFact, CheckedCallOwnership, CheckedCalleeAccess, + CheckedCallerEffect, CheckedCallerOrigin, CheckedCallerSyntax, CheckedIntrinsicOperandRole, + CheckedInvocationShape, CheckedInvocationTarget, CheckedOwnershipContext, + intrinsic_operand_access, +}; use crate::capability; use crate::errors; use crate::resource_hooks::{CheckedResourceHook, ResourceHookError}; @@ -54,10 +60,14 @@ pub struct CheckedGenericFunctionInstantiation { pub return_type: TypeId, /// Expression types captured while checking this concrete body. pub type_map: HashMap, + /// Contextually inferred source type before expectation-added outer Secret promotion. + pub source_type_map: HashMap, /// Checked declared type labels, retaining aliases per concrete body. pub debug_type_names: HashMap, /// Physical copy/borrow facts, keyed by local declaration-name span. pub binding_modes: HashMap, + pub binding_facts: HashMap, + pub call_ownership: HashMap, /// Nested generic calls selected while checking this concrete body. pub generic_calls: HashMap, /// Closed compiler operation selected for each accepted intrinsic call. @@ -79,6 +89,8 @@ pub struct CheckedGenericFunctionInstantiation { pub method_values: HashMap, /// Concrete struct construction targets selected in this body. pub struct_constructions: HashMap, + /// Physical incoming pipeline types sealed before callee checking. + pub pipeline_step_input_types: HashMap, /// Raw call-result types for pipeline steps before any step-local handle. pub pipeline_step_call_types: HashMap, /// Checker-owned compile-time control-flow choices for this concrete body. @@ -126,9 +138,13 @@ pub enum CheckedComptimeTypeSelection { #[derive(Debug, Clone, Default)] pub struct CheckedBodyFacts { pub type_map: HashMap, + /// Contextually inferred source type before expectation-added outer Secret promotion. + pub source_type_map: HashMap, /// Checked declared type labels, retaining aliases per concrete body. pub debug_type_names: HashMap, pub binding_modes: HashMap, + pub binding_facts: HashMap, + pub call_ownership: HashMap, pub generic_calls: HashMap, pub intrinsic_ids: HashMap, pub intrinsic_type_arguments: HashMap>, @@ -138,6 +154,8 @@ pub struct CheckedBodyFacts { pub interface_calls: HashMap, pub method_values: HashMap, pub struct_constructions: HashMap, + /// Physical incoming pipeline types sealed before callee checking. + pub pipeline_step_input_types: HashMap, pub pipeline_step_call_types: HashMap, pub static_selections: HashMap, pub comptime_type_bindings: HashMap>, @@ -258,10 +276,14 @@ pub struct CheckResult { pub diagnostics: Vec, /// Map from expression spans to their inferred type. pub type_map: HashMap, + /// Contextually inferred source type before expectation-added outer Secret promotion. + pub source_type_map: HashMap, /// Checked declared type labels, retaining aliases per concrete body. pub debug_type_names: HashMap, /// Physical copy/borrow facts outside generic bodies, by declaration name. pub binding_modes: HashMap, + pub binding_facts: HashMap, + pub call_ownership: HashMap, /// Session-local resolved definitions and their checked types. /// /// Lowering uses this map with the resolver's `DefId` join keys. Durable @@ -294,6 +316,8 @@ pub struct CheckResult { pub method_value_definitions: HashSet, /// Checked struct construction targets, keyed by call span. pub struct_constructions: HashMap, + /// Physical incoming pipeline types sealed before callee checking. + pub pipeline_step_input_types: HashMap, /// Raw call-result types for pipeline steps before any step-local handle. pub pipeline_step_call_types: HashMap, /// Accepted concrete generic bodies in deterministic discovery order. @@ -378,17 +402,13 @@ fn finish_check( // Run ownership analysis (linear type checking) after type checking. let (ownership_diagnostics, breakpoint_exclusions) = if checked_bodies { crate::ownership::OwnershipChecker::new(&checker.interner) - .with_debug_types( - checker - .type_map - .iter() - .chain( - checker - .generic_function_instantiations - .iter() - .flat_map(|body| body.type_map.iter()), - ) - .map(|(&span, &ty)| (span, ty)), + .with_checked_facts( + checker.resolve, + &checker.binding_facts, + &checker.call_ownership, + &checker.type_map, + &checker.comptime_type_bindings, + &checker.generic_function_instantiations, ) .check_module_with_debug(module) } else { @@ -420,6 +440,11 @@ fn finish_check( let mut diagnostics = checker.sink.into_diagnostics(); diagnostics.extend(complexity_diagnostics); + let typed_rebinding_errors = diagnostics + .iter() + .filter(|diagnostic| diagnostic.code.code() == 404) + .map(|diagnostic| (diagnostic.span, diagnostic.code, diagnostic.message.clone())) + .collect::>(); diagnostics.extend(ownership_diagnostics.into_iter().filter(|diagnostic| { // The typed boundary check has the precise declaration provenance and // guidance. Suppress only its exact legacy consume-view counterpart. @@ -429,6 +454,12 @@ fn finish_check( .is_some_and(|previous| { previous.code == diagnostic.code && previous.message == diagnostic.message }) + && !(diagnostic.code.code() == 404 + && typed_rebinding_errors.iter().any(|(span, code, message)| { + *span == diagnostic.span + && *code == diagnostic.code + && *message == diagnostic.message + })) })); let method_value_definitions = checker @@ -444,8 +475,11 @@ fn finish_check( breakpoint_exclusions, diagnostics, type_map: checker.type_map, + source_type_map: checker.source_type_map, debug_type_names: checker.debug_type_names, binding_modes: checker.binding_modes, + binding_facts: checker.binding_facts, + call_ownership: checker.call_ownership, definition_types: checker.type_env, resource_hooks: checker.resource_hooks, generic_calls: checker.generic_calls, @@ -460,6 +494,7 @@ fn finish_check( method_values: checker.method_values, method_value_definitions, struct_constructions: checker.struct_constructions, + pipeline_step_input_types: checker.pipeline_step_input_types, pipeline_step_call_types: checker.pipeline_step_call_types, generic_function_instantiations: checker.generic_function_instantiations, comptime_type_bindings: checker.comptime_type_bindings, @@ -537,8 +572,12 @@ struct ClosureCaptureScope { struct ActiveGenericInstantiation { manifest_index: usize, type_map: HashMap, + source_type_map: HashMap, debug_type_names: HashMap, binding_modes: HashMap, + binding_facts: HashMap, + call_ownership: HashMap, + invocation_signatures: HashMap, TypeId)>, generic_calls: HashMap, intrinsic_ids: HashMap, intrinsic_type_arguments: HashMap>, @@ -548,6 +587,7 @@ struct ActiveGenericInstantiation { interface_calls: HashMap, method_values: HashMap, struct_constructions: HashMap, + pipeline_step_input_types: HashMap, pipeline_step_call_types: HashMap, static_selections: HashMap, comptime_type_bindings: HashMap>, @@ -594,6 +634,7 @@ struct TypeChecker<'a> { resolving_type_aliases: HashSet, /// Expression span → TypeId (the output type map). type_map: HashMap, + source_type_map: HashMap, debug_type_names: HashMap, /// Source declarations retain nested aliases when inferring generic witnesses. declared_source_types: HashMap, @@ -636,6 +677,7 @@ struct TypeChecker<'a> { assignment_bindings: HashMap, /// Exact legacy consume-view diagnostics replaced by typed boundary errors. diagnosed_owned_view_consumptions: HashMap, + diagnosed_caller_view_arguments: HashSet, /// A generic recheck may revisit the same invalid owning payload source. diagnosed_resource_payload_views: HashSet, /// A written return annotation may be resolved in several specializations. @@ -644,6 +686,7 @@ struct TypeChecker<'a> { current_function_pure: bool, /// Whether we are inside a verify block. in_verify_block: bool, + ownership_context: CheckedOwnershipContext, /// Whether we are inside a property block. in_property_block: bool, /// Nesting depth inside an explicit `comptime` expression. @@ -726,6 +769,8 @@ struct TypeChecker<'a> { call_argument_orders: HashMap, /// Source method bodies exported to HIR. method_definitions: Vec, + /// Exact source-formal access selected with each checked method body. + method_parameter_modes: HashMap>, /// (owner type, method name) -> exported method definition index. method_definitions_by_owner: HashMap<(TypeId, String), usize>, /// (interface type, owner type, method name) -> definition index. @@ -737,8 +782,13 @@ struct TypeChecker<'a> { method_values: HashMap, /// Physical local binding modes outside generic bodies. binding_modes: HashMap, + binding_facts: HashMap, + call_ownership: HashMap, + invocation_signatures: HashMap, TypeId)>, /// Checked struct constructions outside generic bodies. struct_constructions: HashMap, + /// Physical incoming pipeline types outside generic bodies. + pipeline_step_input_types: HashMap, /// Raw pipeline call-result types outside generic bodies. pipeline_step_call_types: HashMap, /// Trusted `comptime type` expansions outside generic function bodies. @@ -780,6 +830,7 @@ impl<'a> TypeChecker<'a> { type_aliases: HashMap::new(), resolving_type_aliases: HashSet::new(), type_map: HashMap::new(), + source_type_map: HashMap::new(), debug_type_names: HashMap::new(), declared_source_types: HashMap::new(), current_return_type: None, @@ -802,10 +853,12 @@ impl<'a> TypeChecker<'a> { constant_declarations: HashSet::new(), assignment_bindings: HashMap::new(), diagnosed_owned_view_consumptions: HashMap::new(), + diagnosed_caller_view_arguments: HashSet::new(), diagnosed_resource_payload_views: HashSet::new(), diagnosed_view_return_annotations: HashSet::new(), current_function_pure: false, in_verify_block: false, + ownership_context: CheckedOwnershipContext::Ordinary, in_property_block: false, comptime_expr_depth: 0, nonzero_fact_scopes: Vec::new(), @@ -841,13 +894,18 @@ impl<'a> TypeChecker<'a> { intrinsic_reflection_arguments: HashMap::new(), call_argument_orders: HashMap::new(), method_definitions: Vec::new(), + method_parameter_modes: HashMap::new(), method_definitions_by_owner: HashMap::new(), interface_method_definitions: HashMap::new(), method_calls: HashMap::new(), interface_calls: HashMap::new(), method_values: HashMap::new(), binding_modes: HashMap::new(), + binding_facts: HashMap::new(), + call_ownership: HashMap::new(), + invocation_signatures: HashMap::new(), struct_constructions: HashMap::new(), + pipeline_step_input_types: HashMap::new(), pipeline_step_call_types: HashMap::new(), comptime_type_bindings: HashMap::new(), active_generic_instantiations: Vec::new(), @@ -3937,7 +3995,14 @@ impl<'a> TypeChecker<'a> { return_type: TypeId, ) -> Option<(Vec, TypeId)> { self.expect_no_type_args(builtin_name, type_args, span); - Some((params, return_type)) + // Keep the operand shape for independent checking, but an invalid + // invocation cannot select an ownership handoff or a usable result. + let checked_return = if type_args.is_empty() { + return_type + } else { + TypeInterner::ERROR + }; + Some((params, checked_return)) } /// Extract T from an optional single-argument builtin type list. @@ -4063,6 +4128,23 @@ impl<'a> TypeChecker<'a> { callee: &Expr, type_args: &[TypeExpr], span: Span, + ) -> Option<(Vec, TypeId)> { + let signature = self.builtin_signature_unrecorded(callee, type_args, span); + if let Some(signature) = &signature { + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.invocation_signatures.insert(span, signature.clone()); + } else { + self.invocation_signatures.insert(span, signature.clone()); + } + } + signature + } + + fn builtin_signature_unrecorded( + &mut self, + callee: &Expr, + type_args: &[TypeExpr], + span: Span, ) -> Option<(Vec, TypeId)> { let name = self.resolved_expr_name(callee)?; // This is the sole acceptance boundary for compiler-provided call @@ -7190,8 +7272,11 @@ impl<'a> TypeChecker<'a> { parameter_types: parameter_types.clone(), return_type, type_map: HashMap::new(), + source_type_map: HashMap::new(), debug_type_names: HashMap::new(), binding_modes: HashMap::new(), + binding_facts: HashMap::new(), + call_ownership: HashMap::new(), generic_calls: HashMap::new(), intrinsic_ids: HashMap::new(), intrinsic_type_arguments: HashMap::new(), @@ -7201,6 +7286,7 @@ impl<'a> TypeChecker<'a> { interface_calls: HashMap::new(), method_values: HashMap::new(), struct_constructions: HashMap::new(), + pipeline_step_input_types: HashMap::new(), pipeline_step_call_types: HashMap::new(), static_selections: HashMap::new(), comptime_type_bindings: HashMap::new(), @@ -7236,8 +7322,12 @@ impl<'a> TypeChecker<'a> { .push(ActiveGenericInstantiation { manifest_index, type_map: HashMap::new(), + source_type_map: HashMap::new(), debug_type_names: HashMap::new(), binding_modes: HashMap::new(), + binding_facts: HashMap::new(), + call_ownership: HashMap::new(), + invocation_signatures: HashMap::new(), generic_calls: HashMap::new(), intrinsic_ids: HashMap::new(), intrinsic_type_arguments: HashMap::new(), @@ -7247,6 +7337,7 @@ impl<'a> TypeChecker<'a> { interface_calls: HashMap::new(), method_values: HashMap::new(), struct_constructions: HashMap::new(), + pipeline_step_input_types: HashMap::new(), pipeline_step_call_types: HashMap::new(), static_selections: HashMap::new(), comptime_type_bindings: HashMap::new(), @@ -7265,8 +7356,11 @@ impl<'a> TypeChecker<'a> { let conflicts = { let entry = &mut self.generic_function_instantiations[active.manifest_index]; entry.type_map.extend(active.type_map); + entry.source_type_map.extend(active.source_type_map); entry.debug_type_names.extend(active.debug_type_names); entry.binding_modes.extend(active.binding_modes); + entry.binding_facts.extend(active.binding_facts); + entry.call_ownership.extend(active.call_ownership); entry.generic_calls.extend(active.generic_calls); entry.intrinsic_ids.extend(active.intrinsic_ids); entry @@ -7284,6 +7378,9 @@ impl<'a> TypeChecker<'a> { entry .struct_constructions .extend(active.struct_constructions); + entry + .pipeline_step_input_types + .extend(active.pipeline_step_input_types); entry .pipeline_step_call_types .extend(active.pipeline_step_call_types); @@ -7336,6 +7433,21 @@ impl<'a> TypeChecker<'a> { } } + fn record_source_expression_type(&mut self, span: Span, ty: TypeId) { + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.source_type_map.insert(span, ty); + } else { + self.source_type_map.insert(span, ty); + } + } + + fn checked_source_expression_type(&self, span: Span) -> Option { + match self.active_generic_instantiations.last() { + Some(active) => active.source_type_map.get(&span).copied(), + None => self.source_type_map.get(&span).copied(), + } + } + fn register_checked_method_definition( &mut self, owner_type: TypeId, @@ -7345,6 +7457,10 @@ impl<'a> TypeChecker<'a> { interface_type: Option, ) { let index = self.method_definitions.len(); + self.method_parameter_modes.insert( + method.span, + signature.params.iter().map(|(_, _, view)| *view).collect(), + ); self.method_definitions.push(CheckedMethodDefinition { source_span: method.span, owner_type, @@ -7480,1153 +7596,1802 @@ impl<'a> TypeChecker<'a> { } } - fn record_binding_mode(&mut self, span: Span, mode: CheckedBindingMode) { - if let Some(active) = self.active_generic_instantiations.last_mut() { - active.binding_modes.insert(span, mode); - } else { - self.binding_modes.insert(span, mode); - } - } - - fn clear_interface_method_value(&mut self, span: Span) { - let values = if let Some(active) = self.active_generic_instantiations.last_mut() { - &mut active.method_values - } else { - &mut self.method_values - }; - if matches!( - values.get(&span), - Some(CheckedMethodValue::Interface { .. }) - ) { - values.remove(&span); + fn checked_expression_type(&self, span: Span) -> Option { + match self.active_generic_instantiations.last() { + Some(active) => active.type_map.get(&span).copied(), + None => self.type_map.get(&span).copied(), } } - fn record_struct_construction( - &mut self, - span: Span, - struct_type: TypeId, - validates_refinements: bool, - ) { - let construction = CheckedStructConstruction { - struct_type, - validates_refinements, - }; + fn record_binding_fact(&mut self, fact: CheckedBindingFact) { if let Some(active) = self.active_generic_instantiations.last_mut() { - active.struct_constructions.insert(span, construction); + active.binding_facts.insert(fact.declaration_span, fact); } else { - self.struct_constructions.insert(span, construction); + self.binding_facts.insert(fact.declaration_span, fact); } } - fn record_pipeline_step_call_type(&mut self, span: Span, ty: TypeId) { - if let Some(active) = self.active_generic_instantiations.last_mut() { - active.pipeline_step_call_types.insert(span, ty); - } else { - self.pipeline_step_call_types.insert(span, ty); - } + fn caller_binding_fact(&self, definition: DefId) -> Option { + let span = self.resolve.scope_table.def(definition).span; + let recorded = match self.active_generic_instantiations.last() { + Some(active) => active.binding_facts.get(&span), + None => self.binding_facts.get(&span), + }; + if let Some(fact) = recorded { + return Some(*fact); + } + // Captures may be declared outside this body. + // Their identity and concrete type still come from this checked session. + let ty = *self.type_env.get(&definition)?; + let assignment = self.assignment_bindings.get(&span); + Some(CheckedBindingFact { + definition, + declaration_span: span, + ty, + mode: if assignment == Some(&AssignmentBinding::View) + && !crate::ownership::is_implicitly_copyable(&self.interner, ty) + { + CheckedBindingMode::View { + source: CheckedViewSource::Other, + } + } else { + CheckedBindingMode::Owned + }, + mutable: assignment == Some(&AssignmentBinding::OwnedMutable), + }) } - fn span_is_within(span: Span, owner: Span) -> bool { - span.file == owner.file && span.start >= owner.start && span.end <= owner.end + fn caller_definition_is_producer(&self, definition: DefId) -> bool { + let declared = self.resolve.scope_table.def(definition); + matches!(declared.kind, DefKind::Function | DefKind::Constant) + || self.constant_declarations.contains(&declared.span) } - fn facts_in_span(source: &HashMap, owner: Span) -> HashMap { - source + fn caller_is_qualified_unit_enum(&self, expression: &Expr) -> bool { + let Expr::FieldAccess(_, variant, span) = expression else { + return false; + }; + let Some(definition) = self.resolve.resolutions.get(span) else { + return false; + }; + if self.resolve.scope_table.def(*definition).kind != DefKind::Enum { + return false; + } + let Some(&enum_ty) = self.type_env.get(definition) else { + return false; + }; + if self.checked_source_expression_type(*span) != Some(enum_ty) { + return false; + } + let Type::Enum(id) = *self.interner.resolve(enum_ty) else { + return false; + }; + self.interner + .resolve_enum(id) + .variants .iter() - .filter(|(span, _)| Self::span_is_within(**span, owner)) - .map(|(span, value)| (*span, value.clone())) - .collect() - } - - fn clear_facts_in_span(source: &mut HashMap, owner: Span) { - source.retain(|span, _| !Self::span_is_within(*span, owner)); + .any(|declared| declared.name == variant.name && declared.fields.is_empty()) } - fn clear_checked_body_facts(&mut self, owner: Span) { - // Expression types are also retained in the root map for the existing - // interpreter handoff, even while a generic fact scope is active. - Self::clear_facts_in_span(&mut self.type_map, owner); - Self::clear_facts_in_span(&mut self.debug_type_names, owner); - if let Some(active) = self.active_generic_instantiations.last_mut() { - Self::clear_facts_in_span(&mut active.type_map, owner); - Self::clear_facts_in_span(&mut active.debug_type_names, owner); - Self::clear_facts_in_span(&mut active.binding_modes, owner); - Self::clear_facts_in_span(&mut active.generic_calls, owner); - Self::clear_facts_in_span(&mut active.intrinsic_ids, owner); - Self::clear_facts_in_span(&mut active.intrinsic_type_arguments, owner); - Self::clear_facts_in_span(&mut active.intrinsic_reflection_arguments, owner); - Self::clear_facts_in_span(&mut active.call_argument_orders, owner); - Self::clear_facts_in_span(&mut active.method_calls, owner); - Self::clear_facts_in_span(&mut active.interface_calls, owner); - Self::clear_facts_in_span(&mut active.method_values, owner); - Self::clear_facts_in_span(&mut active.struct_constructions, owner); - Self::clear_facts_in_span(&mut active.pipeline_step_call_types, owner); - Self::clear_facts_in_span(&mut active.static_selections, owner); - Self::clear_facts_in_span(&mut active.comptime_type_bindings, owner); - } else { - Self::clear_facts_in_span(&mut self.binding_modes, owner); - Self::clear_facts_in_span(&mut self.generic_calls, owner); - Self::clear_facts_in_span(&mut self.intrinsic_ids, owner); - Self::clear_facts_in_span(&mut self.intrinsic_type_arguments, owner); - Self::clear_facts_in_span(&mut self.intrinsic_reflection_arguments, owner); - Self::clear_facts_in_span(&mut self.call_argument_orders, owner); - Self::clear_facts_in_span(&mut self.method_calls, owner); - Self::clear_facts_in_span(&mut self.interface_calls, owner); - Self::clear_facts_in_span(&mut self.method_values, owner); - Self::clear_facts_in_span(&mut self.struct_constructions, owner); - Self::clear_facts_in_span(&mut self.pipeline_step_call_types, owner); - Self::clear_facts_in_span(&mut self.comptime_type_bindings, owner); + fn caller_origin( + &mut self, + expression: Option<&Expr>, + written_view: bool, + ) -> CheckedCallerOrigin { + let Some(mut expression) = expression else { + return CheckedCallerOrigin::OwnedExpression; + }; + while let Expr::Paren(inner, _) + | Expr::View(inner, _) + | Expr::Coarsen(inner, _) + | Expr::Declassify(inner, _) = expression + { + expression = inner; } - } - - fn checked_body_facts(&self, owner: Span) -> CheckedBodyFacts { - if let Some(active) = self.active_generic_instantiations.last() { - return CheckedBodyFacts { - type_map: Self::facts_in_span(&active.type_map, owner), - debug_type_names: Self::facts_in_span(&active.debug_type_names, owner), - binding_modes: Self::facts_in_span(&active.binding_modes, owner), - generic_calls: Self::facts_in_span(&active.generic_calls, owner), - intrinsic_ids: Self::facts_in_span(&active.intrinsic_ids, owner), - intrinsic_type_arguments: Self::facts_in_span( - &active.intrinsic_type_arguments, - owner, - ), - intrinsic_reflection_arguments: Self::facts_in_span( - &active.intrinsic_reflection_arguments, - owner, - ), - call_argument_orders: Self::facts_in_span(&active.call_argument_orders, owner), - method_calls: Self::facts_in_span(&active.method_calls, owner), - interface_calls: Self::facts_in_span(&active.interface_calls, owner), - method_values: Self::facts_in_span(&active.method_values, owner), - struct_constructions: Self::facts_in_span(&active.struct_constructions, owner), - pipeline_step_call_types: Self::facts_in_span( - &active.pipeline_step_call_types, - owner, - ), - static_selections: Self::facts_in_span(&active.static_selections, owner), - comptime_type_bindings: Self::facts_in_span(&active.comptime_type_bindings, owner), + if matches!(expression, Expr::FieldAccess(..)) { + let resolved_function = self + .resolve + .resolutions + .get(&expression.span()) + .is_some_and(|definition| { + self.resolve.scope_table.def(*definition).kind == DefKind::Function + }); + let checked_method = match self.active_generic_instantiations.last() { + Some(active) => active.method_values.contains_key(&expression.span()), + None => self.method_values.contains_key(&expression.span()), }; + // HIR lowers these exact checked declarations to FunctionRef or + // zero-payload EnumConstruct, not to a runtime field. A type or + // namespace spelling alone cannot replace declaration identity. + if resolved_function || checked_method || self.caller_is_qualified_unit_enum(expression) + { + return CheckedCallerOrigin::OwnedExpression; + } } - CheckedBodyFacts { - type_map: Self::facts_in_span(&self.type_map, owner), - debug_type_names: Self::facts_in_span(&self.debug_type_names, owner), - binding_modes: Self::facts_in_span(&self.binding_modes, owner), - generic_calls: Self::facts_in_span(&self.generic_calls, owner), - intrinsic_ids: Self::facts_in_span(&self.intrinsic_ids, owner), - intrinsic_type_arguments: Self::facts_in_span(&self.intrinsic_type_arguments, owner), - intrinsic_reflection_arguments: Self::facts_in_span( - &self.intrinsic_reflection_arguments, - owner, - ), - call_argument_orders: Self::facts_in_span(&self.call_argument_orders, owner), - method_calls: Self::facts_in_span(&self.method_calls, owner), - interface_calls: Self::facts_in_span(&self.interface_calls, owner), - method_values: Self::facts_in_span(&self.method_values, owner), - struct_constructions: Self::facts_in_span(&self.struct_constructions, owner), - pipeline_step_call_types: Self::facts_in_span(&self.pipeline_step_call_types, owner), - static_selections: HashMap::new(), - comptime_type_bindings: Self::facts_in_span(&self.comptime_type_bindings, owner), + let root = Self::assignment_root(expression).and_then(|ident| self.ident_def_id(ident)); + if root.is_some_and(|definition| { + self.caller_definition_is_producer(definition) + || !matches!( + self.resolve.scope_table.def(definition).kind, + DefKind::Variable | DefKind::Param + ) + }) { + // Named descriptors, baked constants and static type/namespace + // projections produce values; none supplies a caller-local root. + return CheckedCallerOrigin::OwnedExpression; + } + let binding = root.and_then(|definition| self.caller_binding_fact(definition)); + if let Some(fact) = binding { + // Captured roots may be declared outside this body. Retain only the + // exact fact referenced by this occurrence in its concrete frame. + self.record_binding_fact(fact); + } + let source = root + .map(CheckedViewSource::Binding) + .unwrap_or(CheckedViewSource::Other); + match expression { + Expr::Ident(_) => binding + .map(CheckedCallerOrigin::Binding) + .unwrap_or(CheckedCallerOrigin::OwnedExpression), + Expr::FieldAccess(..) if written_view => { + CheckedCallerOrigin::BorrowedProjection { source } + } + Expr::FieldAccess(..) => CheckedCallerOrigin::OwnedFieldCopy { parent: source }, + _ => CheckedCallerOrigin::OwnedExpression, } } - fn record_comptime_type_bindings( - &mut self, + fn invocation_facts( + &self, span: Span, - bindings: Vec, + ) -> ( + Option, + Option, + Option, + Option, + Option, + Option<(Vec, TypeId)>, ) { - if let Some(active) = self.active_generic_instantiations.last_mut() { - active.comptime_type_bindings.insert(span, bindings); + if let Some(active) = self.active_generic_instantiations.last() { + ( + active.generic_calls.get(&span).cloned(), + active.method_calls.get(&span).cloned(), + active.interface_calls.get(&span).cloned(), + active.intrinsic_ids.get(&span).copied(), + active.call_argument_orders.get(&span).cloned(), + active.invocation_signatures.get(&span).cloned(), + ) } else { - self.comptime_type_bindings.insert(span, bindings); + ( + self.generic_calls.get(&span).cloned(), + self.method_calls.get(&span).cloned(), + self.interface_calls.get(&span).cloned(), + self.intrinsic_ids.get(&span).copied(), + self.call_argument_orders.get(&span).cloned(), + self.invocation_signatures.get(&span).cloned(), + ) } } - fn record_static_selection(&mut self, span: Span, selection: CheckedStaticSelection) { - let conflict = self - .active_generic_instantiations - .last_mut() - .is_some_and(|active| match active.static_selections.get(&span) { - Some(existing) => *existing != selection, - None => { - active.static_selections.insert(span, selection); - false - } - }); - if conflict { - self.sink - .emit(errors::conflicting_generic_static_selection(span)); + fn caller_observation_data(&self, ty: TypeId, visited: &mut HashSet) -> bool { + if !visited.insert(ty) { + return true; } - } - - fn record_generic_call_for_template( - &mut self, - span: Span, - func: &FunctionDef, - concrete_args: &[TypeId], - kind_subst: &HashMap, - type_argument_reflections: &[ReflectionTypeInfo], - param_facts: &ReflectionParamFacts, - ) { - let Some(definition) = self.declaration_def_id(func.name.span) else { - return; - }; - self.record_generic_call_target( - span, - CheckedGenericCall { - definition, - concrete_args: concrete_args.to_vec(), - specialization: Self::generic_specialization( - func, - kind_subst, - type_argument_reflections, - param_facts, - ), - }, - ); - } - - fn generic_specialization( - func: &FunctionDef, - kind_subst: &HashMap, - type_argument_reflections: &[ReflectionTypeInfo], - param_facts: &ReflectionParamFacts, - ) -> CheckedGenericSpecialization { - CheckedGenericSpecialization { - type_argument_kinds: func - .type_params + match self.interner.resolve(ty) { + Type::Capability(_) + | Type::Resource(_) + | Type::Actor(_) + | Type::Interface(_) + | Type::TypeConstruction => false, + Type::Secret(inner) + | Type::Refinement { base: inner, .. } + | Type::List(inner) + | Type::Set(inner) + | Type::Optional(inner) => self.caller_observation_data(*inner, visited), + Type::Map(left, right) | Type::Result(left, right) => { + self.caller_observation_data(*left, visited) + && self.caller_observation_data(*right, visited) + } + Type::Struct(id) => self + .interner + .resolve_struct(*id) + .fields .iter() - .map(|param| { - kind_subst - .get(¶m.name) - .cloned() - .unwrap_or_else(|| "unknown_type".to_string()) - }) - .collect(), - type_argument_reflections: type_argument_reflections.to_vec(), - type_info_kinds: param_facts.type_info_kinds.clone(), - type_info_primitives: param_facts.type_info_primitives.clone(), - type_kind_values: param_facts.type_kind_values.clone(), - type_primitive_values: param_facts.type_primitive_values.clone(), + .all(|(_, ty)| self.caller_observation_data(*ty, visited)), + Type::Enum(id) => self + .interner + .resolve_enum(*id) + .variants + .iter() + .all(|variant| { + variant + .fields + .iter() + .all(|(_, ty)| self.caller_observation_data(*ty, visited)) + }), + Type::Machine(id) => self + .interner + .resolve_machine(*id) + .states + .iter() + .all(|state| { + state + .fields + .iter() + .all(|(_, ty)| self.caller_observation_data(*ty, visited)) + }), + Type::MachineState { machine, state } => self + .interner + .resolve_machine(*machine) + .state(*state) + .is_some_and(|state| { + state + .fields + .iter() + .all(|(_, ty)| self.caller_observation_data(*ty, visited)) + }), + Type::Bitfield(_) + | Type::Function { .. } + | 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 + | Type::Error => true, } } - fn generic_function_uses_type_param_reflection(&self, func: &FunctionDef) -> bool { - let type_params = func - .type_params - .iter() - .map(|param| param.name.clone()) - .collect::>(); - self.block_uses_type_param_reflection(&func.body, &type_params) - } - - fn type_kind_literal_name(&self, expr: &Expr) -> Option { - match expr { - Expr::Paren(inner, _) => self.type_kind_literal_name(inner), - Expr::EnumVariant(type_name, variant, _) if type_name.name == "TypeKind" => { - Some(variant.name.clone()) + fn record_invocation_ownership( + &mut self, + callee: &Expr, + span: Span, + result: TypeId, + occurrences: &[(Option<&Expr>, Span, bool, Option)], + ) { + if result == TypeInterner::ERROR { + return; + } + let callee = Self::unparenthesized(callee); + let (mut generic, method, interface, intrinsic, order, intrinsic_signature) = + self.invocation_facts(span); + // The closed public JSON operation owns the source disposition. Its + // checked generic helper is an independent implementation handoff. + if intrinsic.is_some_and(|id| { + matches!( + id, + IntrinsicId::JsonSerialize + | IntrinsicId::JsonSerializePublic + | IntrinsicId::JsonParse + | IntrinsicId::JsonParseExact + ) + }) { + generic = None; + } + let (target, mut params, modes, intrinsic_shape, signature_result) = if let Some(call) = + generic + { + let Some(body) = self.generic_function_instantiations.iter().find(|body| { + body.definition == call.definition + && body.concrete_args == call.concrete_args + && body.specialization == call.specialization + }) else { + return; + }; + let Some(template) = self.generic_function_templates.values().find(|template| { + self.declaration_def_id(template.name.span) == Some(call.definition) + }) else { + return; + }; + ( + CheckedInvocationTarget::Generic(call), + body.parameter_types.clone(), + template + .params + .iter() + .map(|param| param.view || Self::type_is_view(¶m.ty)) + .collect::>(), + None, + body.return_type, + ) + } else if let Some(call) = method { + let Some(definition) = self + .method_definitions + .iter() + .find(|definition| definition.source_span == call.source_span) + else { + return; + }; + let Some(modes) = self.method_parameter_modes.get(&call.source_span).cloned() else { + return; + }; + ( + CheckedInvocationTarget::Method(call), + definition.parameter_types.clone(), + modes, + None, + definition.return_type, + ) + } else if let Some(call) = interface { + let Type::Interface(id) = self.interner.resolve(call.interface_type) else { + return; + }; + let Some(signature) = self + .interner + .resolve_interface(*id) + .methods + .get(call.method_index) + else { + return; + }; + ( + CheckedInvocationTarget::Interface(call), + signature.params.iter().map(|(_, ty, _)| *ty).collect(), + signature.params.iter().map(|(_, _, view)| *view).collect(), + None, + signature.return_type, + ) + } else if let Some(intrinsic) = intrinsic { + let params = if let Some((params, _)) = intrinsic_signature { + params + } else if matches!( + intrinsic, + IntrinsicId::Print + | IntrinsicId::Println + | IntrinsicId::Range + | IntrinsicId::MathAbs + | IntrinsicId::MathMin + | IntrinsicId::MathMax + ) { + let Some(types) = occurrences + .iter() + .map(|(_, _, _, ty)| *ty) + .collect::>>() + else { + return; + }; + types + } else { + return; + }; + let Some(modes) = (0..params.len()) + .map(|index| { + intrinsic_operand_access(intrinsic, index, params.len()) + .map(|access| access == CheckedCalleeAccess::View) + }) + .collect::>>() + else { + return; + }; + ( + CheckedInvocationTarget::Intrinsic(intrinsic), + params, + modes, + Some(intrinsic), + result, + ) + } else { + let definition = self.resolve.resolutions.get(&callee.span()).copied(); + let signature_type = definition + .and_then(|definition| self.type_env.get(&definition).copied()) + .or_else(|| self.checked_expression_type(callee.span())); + let Some(signature_type) = signature_type else { + return; + }; + let Type::Function { + params, + view_params, + return_type, + } = self.interner.resolve(signature_type).clone() + else { + return; + }; + let target = if let Some(definition) = definition.filter(|definition| { + self.resolve.scope_table.def(*definition).kind == DefKind::Function + }) { + CheckedInvocationTarget::Resolved(definition) + } else { + CheckedInvocationTarget::Indirect(signature_type) + }; + (target, params, view_params, None, return_type) + }; + if params.len() != occurrences.len() || modes.len() != params.len() { + return; + } + // Secret operations have a closed polymorphic payload role, not a + // function signature containing the builtin's internal ERROR wildcard. + if intrinsic_shape.is_some_and(|intrinsic| { + matches!( + intrinsic, + IntrinsicId::SecretCompare | IntrinsicId::SecretRedact + ) + }) { + let Some(concrete) = occurrences + .iter() + .map(|(_, _, _, ty)| *ty) + .collect::>>() + else { + return; + }; + params = concrete; + } + let order = order + .map(|order| order.source_indices) + .unwrap_or_else(|| (0..params.len()).collect()); + if order.len() != params.len() { + return; + } + let mut inverse = vec![None; params.len()]; + for (parameter_index, &source_index) in order.iter().enumerate() { + let Some(slot) = inverse.get_mut(source_index) else { + return; + }; + if slot.replace(parameter_index).is_some() { + return; } - Expr::FieldAccess(_, _, _) => { - let name = self.expanded_dotted_expr_name(expr)?; - let (type_name, variant_name) = name.rsplit_once('.')?; - (type_name == "TypeKind").then(|| variant_name.to_string()) + } + let mut arguments = Vec::with_capacity(params.len()); + let mut roles = Vec::with_capacity(params.len()); + for (source_index, &(expression, source_span, written_view, actual_type)) in + occurrences.iter().enumerate() + { + let (Some(parameter_index), Some(actual_type)) = (inverse[source_index], actual_type) + else { + return; + }; + if actual_type == TypeInterner::ERROR || params[parameter_index] == TypeInterner::ERROR + { + return; } - _ => None, + let origin = self.caller_origin(expression, written_view); + let access = if modes[parameter_index] { + CheckedCalleeAccess::View + } else { + CheckedCalleeAccess::Owned + }; + let access = if intrinsic_shape.is_some_and(|intrinsic| { + matches!(intrinsic, IntrinsicId::Print | IntrinsicId::Println) + }) && written_view + { + CheckedCalleeAccess::View + } else { + access + }; + let copyable = crate::ownership::is_implicitly_copyable(&self.interner, actual_type); + let observe = self.ownership_context != CheckedOwnershipContext::Ordinary + && self.caller_observation_data(actual_type, &mut HashSet::new()); + // Ordinary function checking has already validated source syntax + // (including the pipeline step's original E0375 label). Closed + // intrinsic operands have their own sealed role gate here. + if intrinsic_shape.is_some() { + self.check_argument_view( + access == CheckedCalleeAccess::View, + written_view, + source_span, + ); + } + if !observe + && !written_view + && let Some(expression) = expression + { + self.check_owned_argument( + access == CheckedCalleeAccess::View, + expression, + params[parameter_index], + actual_type, + ); + } + let effect = if copyable { + CheckedCallerEffect::Copy + } else if observe { + CheckedCallerEffect::ObserveData + } else if written_view { + CheckedCallerEffect::RetainBorrow + } else if access == CheckedCalleeAccess::View { + CheckedCallerEffect::RelinquishOwned + } else { + CheckedCallerEffect::TransferOwned + }; + arguments.push(CheckedArgumentOwnership { + source_span, + source_index, + parameter_index, + actual_type, + parameter_type: params[parameter_index], + syntax: if written_view { + CheckedCallerSyntax::WrittenView + } else { + CheckedCallerSyntax::Bare + }, + origin, + callee_access: access, + effect, + }); } - } - - fn type_primitive_literal_name(&self, expr: &Expr) -> Option { - match expr { - Expr::Paren(inner, _) => self.type_primitive_literal_name(inner), - Expr::EnumVariant(type_name, variant, _) if type_name.name == "TypePrimitive" => { - Some(variant.name.clone()) + let shape = if let Some(intrinsic) = intrinsic_shape { + for (parameter_index, &ty) in params.iter().enumerate() { + roles.push( + if matches!(intrinsic, IntrinsicId::Print | IntrinsicId::Println) { + CheckedIntrinsicOperandRole::PrintArgument { ty } + } else if crate::ownership::is_implicitly_copyable(&self.interner, ty) { + CheckedIntrinsicOperandRole::Copy { ty } + } else if modes[parameter_index] { + CheckedIntrinsicOperandRole::View { ty } + } else { + CheckedIntrinsicOperandRole::Owned { ty } + }, + ); } - Expr::FieldAccess(_, _, _) => { - let name = self.expanded_dotted_expr_name(expr)?; - let (type_name, variant_name) = name.rsplit_once('.')?; - (type_name == "TypePrimitive").then(|| variant_name.to_string()) + CheckedInvocationShape::Intrinsic { + intrinsic, + result_type: result, + operands: roles, } - _ => None, + } else { + let signature_type = self.interner.intern(Type::Function { + params, + view_params: modes, + return_type: signature_result, + }); + CheckedInvocationShape::Function { signature_type } + }; + let packet = CheckedCallOwnership { + target, + shape, + context: self.ownership_context, + arguments, + }; + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.call_ownership.insert(span, packet); + } else { + self.call_ownership.insert(span, packet); } } - fn handle_default_expr(body: &Block) -> Option<&Expr> { - let Stmt::Expr(expr_stmt) = body.stmts.last()? else { - return None; + fn record_binding_mode(&mut self, span: Span, mode: CheckedBindingMode) { + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.binding_modes.insert(span, mode); + } else { + self.binding_modes.insert(span, mode); + } + } + + fn clear_interface_method_value(&mut self, span: Span) { + let values = if let Some(active) = self.active_generic_instantiations.last_mut() { + &mut active.method_values + } else { + &mut self.method_values }; - let Expr::Default(default_value, _) = &expr_stmt.expr else { - return None; + if matches!( + values.get(&span), + Some(CheckedMethodValue::Interface { .. }) + ) { + values.remove(&span); + } + } + + fn record_struct_construction( + &mut self, + span: Span, + struct_type: TypeId, + validates_refinements: bool, + ) { + let construction = CheckedStructConstruction { + struct_type, + validates_refinements, }; - Some(default_value) + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.struct_constructions.insert(span, construction); + } else { + self.struct_constructions.insert(span, construction); + } } - fn eval_static_type_condition(&mut self, expr: &Expr) -> Option { - match expr { - Expr::BoolLiteral(value, _) => Some(*value), - Expr::Paren(inner, _) => self.eval_static_type_condition(inner), - Expr::GenericCall(callee, type_args, args, _) - if args.is_empty() - && self - .resolved_expr_name(callee) - .is_some_and(|name| name == "type.has_secret") => - { - let type_id = self.reflection_type_arg_id(type_args)?; - Some(self.type_contains_secret_data(type_id)) - } - Expr::Unary(UnaryOp::Not, inner, _) => { - self.eval_static_type_condition(inner).map(|value| !value) - } - Expr::Binary(lhs, BinOp::And, rhs, _) => match self.eval_static_type_condition(lhs)? { - false => Some(false), - true => self.eval_static_type_condition(rhs), - }, - Expr::Binary(lhs, BinOp::Or, rhs, _) => match self.eval_static_type_condition(lhs)? { - true => Some(true), - false => self.eval_static_type_condition(rhs), - }, - Expr::Binary(lhs, BinOp::Eq | BinOp::NotEq, rhs, _) => { - let lhs_name = self.eval_static_type_name_value(lhs); - let rhs_name = self.eval_static_type_name_value(rhs); - let equal = if let (Some(name), Some(literal)) = - (lhs_name, Self::static_string_literal(rhs)) - { - name == literal - } else if let (Some(literal), Some(name)) = - (Self::static_string_literal(lhs), rhs_name) - { - literal == name - } else { - let lhs_value = self.eval_static_reflection_enum_value(lhs)?; - let rhs_value = self.eval_static_reflection_enum_value(rhs)?; - lhs_value == rhs_value - }; - Some(if matches!(expr, Expr::Binary(_, BinOp::Eq, _, _)) { - equal - } else { - !equal - }) - } - // Calls and detached bool locals permanently remain outside - // generic branch specialization. Both branches must typecheck; - // later optimization may fold a value but cannot accept new code. - _ => None, + fn record_pipeline_step_input_type(&mut self, span: Span, ty: TypeId) { + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.pipeline_step_input_types.insert(span, ty); + } else { + self.pipeline_step_input_types.insert(span, ty); } } - fn eval_static_reflection_enum_value( - &mut self, - expr: &Expr, - ) -> Option { - if let Some(kind_tag) = self.eval_type_kind_value(expr) { - return Some(StaticReflectionEnumValue::TypeKind(kind_tag)); + fn record_pipeline_step_call_type(&mut self, span: Span, ty: TypeId) { + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.pipeline_step_call_types.insert(span, ty); + } else { + self.pipeline_step_call_types.insert(span, ty); } - self.eval_type_primitive_value(expr) - .map(StaticReflectionEnumValue::TypePrimitive) } - fn eval_static_type_name_value(&mut self, expr: &Expr) -> Option { - match expr { - Expr::Paren(inner, _) => self.eval_static_type_name_value(inner), - Expr::GenericCall(callee, type_args, args, _) - if args.is_empty() - && self - .resolved_expr_name(callee) - .is_some_and(|name| name == "type.name") => - { - let [type_arg] = type_args.as_slice() else { - return None; - }; - let type_id = self.reflection_type_arg_id(type_args)?; - let namespace = self.current_function_name.as_deref().and_then(|name| { - name.rsplit_once('.') - .map(|(namespace, _)| namespace.to_string()) - }); - Some( - self.reflection_type_info_for_type_expr( - type_arg, - namespace.as_deref(), - type_id, - ) - .type_name, - ) - } - _ => None, - } + fn span_is_within(span: Span, owner: Span) -> bool { + span.file == owner.file && span.start >= owner.start && span.end <= owner.end } - fn static_string_literal(expr: &Expr) -> Option<&str> { - match expr { - Expr::Paren(inner, _) => Self::static_string_literal(inner), - Expr::StringLiteral(value, _) => Some(value), - _ => None, - } + fn facts_in_span(source: &HashMap, owner: Span) -> HashMap { + source + .iter() + .filter(|(span, _)| Self::span_is_within(**span, owner)) + .map(|(span, value)| (*span, value.clone())) + .collect() } - fn eval_type_kind_value(&mut self, expr: &Expr) -> Option { - match expr { - Expr::Paren(inner, _) => self.eval_type_kind_value(inner), - Expr::Ident(ident) => self.reflection_type_kind_value_for_ident(ident), - Expr::GenericCall(callee, type_args, args, _) - if args.is_empty() - && self - .resolved_expr_name(callee) - .is_some_and(|name| name == "type.kind_tag") => - { - self.reflection_type_arg_kind_tag(type_args) - } - Expr::FieldAccess(base, field, _) if field.name == "kind_tag" => { - self.eval_type_info_type_arg_kind_tag(base) - } - _ => self.type_kind_literal_name(expr), - } + fn clear_facts_in_span(source: &mut HashMap, owner: Span) { + source.retain(|span, _| !Self::span_is_within(*span, owner)); } - fn eval_type_primitive_value(&mut self, expr: &Expr) -> Option { - match expr { - Expr::Paren(inner, _) => self.eval_type_primitive_value(inner), - Expr::Ident(ident) => self.reflection_type_primitive_value_for_ident(ident), - Expr::Handle(target, _, body, _) => { - match self.eval_optional_type_primitive_value(target)? { - Some(primitive_tag) => Some(primitive_tag), - None => Self::handle_default_expr(body) - .and_then(|default| self.eval_type_primitive_value(default)), + fn body_binding_facts( + &self, + bindings: &HashMap, + calls: &HashMap, + owner: Span, + ) -> HashMap { + let mut facts = Self::facts_in_span(bindings, owner); + for (_, call) in calls + .iter() + .filter(|(span, _)| Self::span_is_within(**span, owner)) + { + for argument in &call.arguments { + match &argument.origin { + CheckedCallerOrigin::Binding(fact) => { + facts.insert(fact.declaration_span, *fact); + } + CheckedCallerOrigin::BorrowedProjection { + source: CheckedViewSource::Binding(definition), + } + | CheckedCallerOrigin::OwnedFieldCopy { + parent: CheckedViewSource::Binding(definition), + } => { + let span = self.resolve.scope_table.def(*definition).span; + if let Some(fact) = bindings + .get(&span) + .filter(|fact| fact.definition == *definition) + { + facts.insert(span, *fact); + } + } + _ => {} } } - _ => self.type_primitive_literal_name(expr), } + facts } - fn eval_optional_type_primitive_value(&mut self, expr: &Expr) -> Option> { - match expr { - Expr::Paren(inner, _) => self.eval_optional_type_primitive_value(inner), - Expr::GenericCall(callee, type_args, args, _) - if args.is_empty() - && self - .resolved_expr_name(callee) - .is_some_and(|name| name == "type.primitive_tag") => - { - Some(self.reflection_type_arg_primitive_tag(type_args)) - } - Expr::FieldAccess(base, field, _) if field.name == "primitive_tag" => { - self.eval_type_info_primitive_tag(base) - } - _ => None, + fn clear_checked_body_facts(&mut self, owner: Span) { + // Expression types are also retained in the root map for the existing + // interpreter handoff, even while a generic fact scope is active. + Self::clear_facts_in_span(&mut self.type_map, owner); + Self::clear_facts_in_span(&mut self.debug_type_names, owner); + if let Some(active) = self.active_generic_instantiations.last_mut() { + Self::clear_facts_in_span(&mut active.type_map, owner); + Self::clear_facts_in_span(&mut active.source_type_map, owner); + Self::clear_facts_in_span(&mut active.debug_type_names, owner); + Self::clear_facts_in_span(&mut active.binding_modes, owner); + Self::clear_facts_in_span(&mut active.binding_facts, owner); + Self::clear_facts_in_span(&mut active.call_ownership, owner); + Self::clear_facts_in_span(&mut active.invocation_signatures, owner); + Self::clear_facts_in_span(&mut active.generic_calls, owner); + Self::clear_facts_in_span(&mut active.intrinsic_ids, owner); + Self::clear_facts_in_span(&mut active.intrinsic_type_arguments, owner); + Self::clear_facts_in_span(&mut active.intrinsic_reflection_arguments, owner); + Self::clear_facts_in_span(&mut active.call_argument_orders, owner); + Self::clear_facts_in_span(&mut active.method_calls, owner); + Self::clear_facts_in_span(&mut active.interface_calls, owner); + Self::clear_facts_in_span(&mut active.method_values, owner); + Self::clear_facts_in_span(&mut active.struct_constructions, owner); + Self::clear_facts_in_span(&mut active.pipeline_step_input_types, owner); + Self::clear_facts_in_span(&mut active.pipeline_step_call_types, owner); + Self::clear_facts_in_span(&mut active.static_selections, owner); + Self::clear_facts_in_span(&mut active.comptime_type_bindings, owner); + } else { + Self::clear_facts_in_span(&mut self.source_type_map, owner); + Self::clear_facts_in_span(&mut self.binding_modes, owner); + Self::clear_facts_in_span(&mut self.binding_facts, owner); + Self::clear_facts_in_span(&mut self.call_ownership, owner); + Self::clear_facts_in_span(&mut self.invocation_signatures, owner); + Self::clear_facts_in_span(&mut self.generic_calls, owner); + Self::clear_facts_in_span(&mut self.intrinsic_ids, owner); + Self::clear_facts_in_span(&mut self.intrinsic_type_arguments, owner); + Self::clear_facts_in_span(&mut self.intrinsic_reflection_arguments, owner); + Self::clear_facts_in_span(&mut self.call_argument_orders, owner); + Self::clear_facts_in_span(&mut self.method_calls, owner); + Self::clear_facts_in_span(&mut self.interface_calls, owner); + Self::clear_facts_in_span(&mut self.method_values, owner); + Self::clear_facts_in_span(&mut self.struct_constructions, owner); + Self::clear_facts_in_span(&mut self.pipeline_step_input_types, owner); + Self::clear_facts_in_span(&mut self.pipeline_step_call_types, owner); + Self::clear_facts_in_span(&mut self.comptime_type_bindings, owner); } } - fn eval_type_info_type_arg_kind_tag(&mut self, expr: &Expr) -> Option { - self.eval_type_info_facts(expr).map(|facts| facts.kind_tag) - } - - fn eval_type_info_primitive_tag(&mut self, expr: &Expr) -> Option> { - self.eval_type_info_facts(expr) - .map(|facts| facts.primitive_tag) + fn checked_body_facts(&self, owner: Span) -> CheckedBodyFacts { + if let Some(active) = self.active_generic_instantiations.last() { + return CheckedBodyFacts { + type_map: Self::facts_in_span(&active.type_map, owner), + source_type_map: Self::facts_in_span(&active.source_type_map, owner), + debug_type_names: Self::facts_in_span(&active.debug_type_names, owner), + binding_modes: Self::facts_in_span(&active.binding_modes, owner), + binding_facts: self.body_binding_facts( + &active.binding_facts, + &active.call_ownership, + owner, + ), + call_ownership: Self::facts_in_span(&active.call_ownership, owner), + generic_calls: Self::facts_in_span(&active.generic_calls, owner), + intrinsic_ids: Self::facts_in_span(&active.intrinsic_ids, owner), + intrinsic_type_arguments: Self::facts_in_span( + &active.intrinsic_type_arguments, + owner, + ), + intrinsic_reflection_arguments: Self::facts_in_span( + &active.intrinsic_reflection_arguments, + owner, + ), + call_argument_orders: Self::facts_in_span(&active.call_argument_orders, owner), + method_calls: Self::facts_in_span(&active.method_calls, owner), + interface_calls: Self::facts_in_span(&active.interface_calls, owner), + method_values: Self::facts_in_span(&active.method_values, owner), + struct_constructions: Self::facts_in_span(&active.struct_constructions, owner), + pipeline_step_input_types: Self::facts_in_span( + &active.pipeline_step_input_types, + owner, + ), + pipeline_step_call_types: Self::facts_in_span( + &active.pipeline_step_call_types, + owner, + ), + static_selections: Self::facts_in_span(&active.static_selections, owner), + comptime_type_bindings: Self::facts_in_span(&active.comptime_type_bindings, owner), + }; + } + CheckedBodyFacts { + type_map: Self::facts_in_span(&self.type_map, owner), + source_type_map: Self::facts_in_span(&self.source_type_map, owner), + debug_type_names: Self::facts_in_span(&self.debug_type_names, owner), + binding_modes: Self::facts_in_span(&self.binding_modes, owner), + binding_facts: self.body_binding_facts( + &self.binding_facts, + &self.call_ownership, + owner, + ), + call_ownership: Self::facts_in_span(&self.call_ownership, owner), + generic_calls: Self::facts_in_span(&self.generic_calls, owner), + intrinsic_ids: Self::facts_in_span(&self.intrinsic_ids, owner), + intrinsic_type_arguments: Self::facts_in_span(&self.intrinsic_type_arguments, owner), + intrinsic_reflection_arguments: Self::facts_in_span( + &self.intrinsic_reflection_arguments, + owner, + ), + call_argument_orders: Self::facts_in_span(&self.call_argument_orders, owner), + method_calls: Self::facts_in_span(&self.method_calls, owner), + interface_calls: Self::facts_in_span(&self.interface_calls, owner), + method_values: Self::facts_in_span(&self.method_values, owner), + struct_constructions: Self::facts_in_span(&self.struct_constructions, owner), + pipeline_step_input_types: Self::facts_in_span(&self.pipeline_step_input_types, owner), + pipeline_step_call_types: Self::facts_in_span(&self.pipeline_step_call_types, owner), + static_selections: HashMap::new(), + comptime_type_bindings: Self::facts_in_span(&self.comptime_type_bindings, owner), + } } - fn eval_type_info_facts(&mut self, expr: &Expr) -> Option { - match expr { - Expr::Paren(inner, _) => self.eval_type_info_facts(inner), - Expr::View(inner, _) => self.eval_type_info_facts(inner), - Expr::Ident(ident) => { - let kind_tag = self.reflection_type_info_kind_for_ident(ident)?; - let primitive_tag = self - .reflection_type_info_primitive_for_ident(ident) - .unwrap_or(None); - Some(ReflectionTypeInfoStaticFacts { - kind_tag, - primitive_tag, - }) - } - Expr::GenericCall(callee, type_args, args, _) - if args.is_empty() - && self - .resolved_expr_name(callee) - .is_some_and(|name| name == "type.info") => - { - let kind_tag = self.reflection_type_arg_kind_tag(type_args)?; - let primitive_tag = self.reflection_type_arg_primitive_tag(type_args); - Some(ReflectionTypeInfoStaticFacts { - kind_tag, - primitive_tag, - }) - } - _ => None, + fn record_comptime_type_bindings( + &mut self, + span: Span, + bindings: Vec, + ) { + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.comptime_type_bindings.insert(span, bindings); + } else { + self.comptime_type_bindings.insert(span, bindings); } } - fn reflection_type_arg_kind_tag(&mut self, type_args: &[TypeExpr]) -> Option { - let [type_arg] = type_args else { - return None; - }; - let type_id = self.reflection_type_arg_id(type_args)?; - Some(self.reflection_kind_tag_for_type_expr(type_arg, type_id)) + fn record_static_selection(&mut self, span: Span, selection: CheckedStaticSelection) { + let conflict = self + .active_generic_instantiations + .last_mut() + .is_some_and(|active| match active.static_selections.get(&span) { + Some(existing) => *existing != selection, + None => { + active.static_selections.insert(span, selection); + false + } + }); + if conflict { + self.sink + .emit(errors::conflicting_generic_static_selection(span)); + } } - fn reflection_type_arg_primitive_tag(&mut self, type_args: &[TypeExpr]) -> Option { - let [type_arg] = type_args else { - return None; + fn record_generic_call_for_template( + &mut self, + span: Span, + func: &FunctionDef, + concrete_args: &[TypeId], + kind_subst: &HashMap, + type_argument_reflections: &[ReflectionTypeInfo], + param_facts: &ReflectionParamFacts, + ) { + let Some(definition) = self.declaration_def_id(func.name.span) else { + return; }; - let type_id = self.reflection_type_arg_id(type_args)?; - self.reflection_primitive_tag_for_type_expr_static(type_arg, type_id) + self.record_generic_call_target( + span, + CheckedGenericCall { + definition, + concrete_args: concrete_args.to_vec(), + specialization: Self::generic_specialization( + func, + kind_subst, + type_argument_reflections, + param_facts, + ), + }, + ); } - fn reflection_type_arg_id(&mut self, type_args: &[TypeExpr]) -> Option { - let [type_arg] = type_args else { - return None; - }; - if let TypeExpr::Named(ident) = type_arg { - if let Some(&type_id) = self.type_var_subst.get(&ident.name) { - return Some(type_id); - } + fn generic_specialization( + func: &FunctionDef, + kind_subst: &HashMap, + type_argument_reflections: &[ReflectionTypeInfo], + param_facts: &ReflectionParamFacts, + ) -> CheckedGenericSpecialization { + CheckedGenericSpecialization { + type_argument_kinds: func + .type_params + .iter() + .map(|param| { + kind_subst + .get(¶m.name) + .cloned() + .unwrap_or_else(|| "unknown_type".to_string()) + }) + .collect(), + type_argument_reflections: type_argument_reflections.to_vec(), + type_info_kinds: param_facts.type_info_kinds.clone(), + type_info_primitives: param_facts.type_info_primitives.clone(), + type_kind_values: param_facts.type_kind_values.clone(), + type_primitive_values: param_facts.type_primitive_values.clone(), } - let type_id = self.resolve_type_expr(type_arg); - (type_id != TypeInterner::ERROR).then_some(type_id) } - fn block_uses_type_param_reflection( - &self, - block: &Block, - type_params: &HashSet, - ) -> bool { - block - .stmts + fn generic_function_uses_type_param_reflection(&self, func: &FunctionDef) -> bool { + let type_params = func + .type_params .iter() - .any(|stmt| self.stmt_uses_type_param_reflection(stmt, type_params)) + .map(|param| param.name.clone()) + .collect::>(); + self.block_uses_type_param_reflection(&func.body, &type_params) } - fn stmt_uses_type_param_reflection(&self, stmt: &Stmt, type_params: &HashSet) -> bool { - match stmt { - Stmt::VarDecl(decl) => self.expr_uses_type_param_reflection(&decl.value, type_params), - Stmt::Assign(assign) => { - self.expr_uses_type_param_reflection(&assign.target, type_params) - || self.expr_uses_type_param_reflection(&assign.value, type_params) - } - Stmt::Return(ret) => ret - .value - .as_ref() - .is_some_and(|expr| self.expr_uses_type_param_reflection(expr, type_params)), - Stmt::Respond(resp) => self.expr_uses_type_param_reflection(&resp.value, type_params), - Stmt::ComptimeTypeBind(bind) => { - self.expr_uses_type_param_reflection(&bind.value, type_params) - || self.block_uses_type_param_reflection(&bind.body, type_params) - } - Stmt::If(if_stmt) => { - self.expr_uses_type_param_reflection(&if_stmt.condition, type_params) - || self.block_uses_type_param_reflection(&if_stmt.then_block, type_params) - || if_stmt.else_ifs.iter().any(|(condition, block)| { - self.expr_uses_type_param_reflection(condition, type_params) - || self.block_uses_type_param_reflection(block, type_params) - }) - || if_stmt.else_block.as_ref().is_some_and(|block| { - self.block_uses_type_param_reflection(block, type_params) - }) - } - Stmt::For(for_stmt) => { - self.expr_uses_type_param_reflection(&for_stmt.iterable, type_params) - || self.block_uses_type_param_reflection(&for_stmt.body, type_params) - } - Stmt::While(while_stmt) => { - self.expr_uses_type_param_reflection(&while_stmt.condition, type_params) - || self.block_uses_type_param_reflection(&while_stmt.body, type_params) + fn type_kind_literal_name(&self, expr: &Expr) -> Option { + match expr { + Expr::Paren(inner, _) => self.type_kind_literal_name(inner), + Expr::EnumVariant(type_name, variant, _) if type_name.name == "TypeKind" => { + Some(variant.name.clone()) } - Stmt::Match(match_stmt) => { - self.expr_uses_type_param_reflection(&match_stmt.expr, type_params) - || match_stmt - .arms - .iter() - .any(|arm| self.block_uses_type_param_reflection(&arm.body, type_params)) + Expr::FieldAccess(_, _, _) => { + let name = self.expanded_dotted_expr_name(expr)?; + let (type_name, variant_name) = name.rsplit_once('.')?; + (type_name == "TypeKind").then(|| variant_name.to_string()) } - Stmt::Expr(expr_stmt) => { - self.expr_uses_type_param_reflection(&expr_stmt.expr, type_params) + _ => None, + } + } + + fn type_primitive_literal_name(&self, expr: &Expr) -> Option { + match expr { + Expr::Paren(inner, _) => self.type_primitive_literal_name(inner), + Expr::EnumVariant(type_name, variant, _) if type_name.name == "TypePrimitive" => { + Some(variant.name.clone()) } - Stmt::Assert(assert_stmt) => { - self.expr_uses_type_param_reflection(&assert_stmt.condition, type_params) - || assert_stmt.message.as_ref().is_some_and(|message| { - self.expr_uses_type_param_reflection(message, type_params) - }) + Expr::FieldAccess(_, _, _) => { + let name = self.expanded_dotted_expr_name(expr)?; + let (type_name, variant_name) = name.rsplit_once('.')?; + (type_name == "TypePrimitive").then(|| variant_name.to_string()) } - Stmt::Breakpoint(breakpoint_stmt) => breakpoint_stmt - .condition - .as_ref() - .is_some_and(|expr| self.expr_uses_type_param_reflection(expr, type_params)), - Stmt::Trace(_) | Stmt::Use(_) | Stmt::Break(_) | Stmt::Continue(_) => false, + _ => None, } } - fn expr_uses_type_param_reflection(&self, expr: &Expr, type_params: &HashSet) -> bool { + fn handle_default_expr(body: &Block) -> Option<&Expr> { + let Stmt::Expr(expr_stmt) = body.stmts.last()? else { + return None; + }; + let Expr::Default(default_value, _) = &expr_stmt.expr else { + return None; + }; + Some(default_value) + } + + fn eval_static_type_condition(&mut self, expr: &Expr) -> Option { match expr { - Expr::GenericCall(callee, type_args, args, _) => { - let is_type_reflection = self - .resolved_expr_name(callee) - .is_some_and(|name| name.starts_with("type.")); - (is_type_reflection - && type_args - .iter() - .any(|arg| Self::type_expr_mentions_type_param(arg, type_params))) - || self.expr_uses_type_param_reflection(callee, type_params) - || args - .iter() - .any(|arg| self.expr_uses_type_param_reflection(&arg.value, type_params)) - } - Expr::Call(callee, args, _) => { - self.expr_uses_type_param_reflection(callee, type_params) - || args - .iter() - .any(|arg| self.expr_uses_type_param_reflection(&arg.value, type_params)) + Expr::BoolLiteral(value, _) => Some(*value), + Expr::Paren(inner, _) => self.eval_static_type_condition(inner), + Expr::GenericCall(callee, type_args, args, _) + if args.is_empty() + && self + .resolved_expr_name(callee) + .is_some_and(|name| name == "type.has_secret") => + { + let type_id = self.reflection_type_arg_id(type_args)?; + Some(self.type_contains_secret_data(type_id)) } - Expr::Binary(lhs, _, rhs, _) => { - self.expr_uses_type_param_reflection(lhs, type_params) - || self.expr_uses_type_param_reflection(rhs, type_params) + Expr::Unary(UnaryOp::Not, inner, _) => { + self.eval_static_type_condition(inner).map(|value| !value) } - 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, _) => self.expr_uses_type_param_reflection(inner, type_params), - Expr::ListConstruct(items, _) => items - .iter() - .any(|item| self.expr_uses_type_param_reflection(item, type_params)), - Expr::MapConstruct(entries, _) => entries.iter().any(|(key, value)| { - self.expr_uses_type_param_reflection(key, type_params) - || self.expr_uses_type_param_reflection(value, type_params) - }), - Expr::Handle(target, _, body, _) => { - self.expr_uses_type_param_reflection(target, type_params) - || self.block_uses_type_param_reflection(body, type_params) - } - Expr::StringInterpolation(parts, _) => parts.iter().any(|part| match part { - StringPart::Literal(_) => false, - StringPart::Expr(expr) => self.expr_uses_type_param_reflection(expr, type_params), - }), - Expr::Pipeline(base, steps, _) => { - self.expr_uses_type_param_reflection(base, type_params) - || steps.iter().any(|step| { - self.expr_uses_type_param_reflection(&step.function, type_params) - || step.extra_args.iter().any(|arg| { - self.expr_uses_type_param_reflection(&arg.value, type_params) - }) - }) - } - // Generic signature annotations require ordinary substitution; - // only operations inside the closure body can use reflection. - Expr::InlineFn(_, _, body, _) => { - self.block_uses_type_param_reflection(body, type_params) + Expr::Binary(lhs, BinOp::And, rhs, _) => match self.eval_static_type_condition(lhs)? { + false => Some(false), + true => self.eval_static_type_condition(rhs), + }, + Expr::Binary(lhs, BinOp::Or, rhs, _) => match self.eval_static_type_condition(lhs)? { + true => Some(true), + false => self.eval_static_type_condition(rhs), + }, + Expr::Binary(lhs, BinOp::Eq | BinOp::NotEq, rhs, _) => { + let lhs_name = self.eval_static_type_name_value(lhs); + let rhs_name = self.eval_static_type_name_value(rhs); + let equal = if let (Some(name), Some(literal)) = + (lhs_name, Self::static_string_literal(rhs)) + { + name == literal + } else if let (Some(literal), Some(name)) = + (Self::static_string_literal(lhs), rhs_name) + { + literal == name + } else { + let lhs_value = self.eval_static_reflection_enum_value(lhs)?; + let rhs_value = self.eval_static_reflection_enum_value(rhs)?; + lhs_value == rhs_value + }; + Some(if matches!(expr, Expr::Binary(_, BinOp::Eq, _, _)) { + equal + } else { + !equal + }) } - Expr::IntLiteral(_, _) - | Expr::FloatLiteral(_, _) - | Expr::StringLiteral(_, _) - | Expr::BoolLiteral(_, _) - | Expr::Nothing(_) - | Expr::Ident(_) - | Expr::None(_) - | Expr::EnumVariant(_, _, _) - | Expr::Error(_) => false, + // Calls and detached bool locals permanently remain outside + // generic branch specialization. Both branches must typecheck; + // later optimization may fold a value but cannot accept new code. + _ => None, } } - fn type_expr_mentions_type_param(ty: &TypeExpr, type_params: &HashSet) -> bool { - match ty { - TypeExpr::Named(ident) => type_params.contains(&ident.name), - TypeExpr::Generic(_, args, _) => args - .iter() - .any(|arg| Self::type_expr_mentions_type_param(arg, type_params)), - TypeExpr::View(inner, _) => Self::type_expr_mentions_type_param(inner, type_params), - TypeExpr::StateQualified(inner, _, _) => { - Self::type_expr_mentions_type_param(inner, type_params) - } - TypeExpr::Function(params, return_ty, _) => { - params - .iter() - .any(|param| Self::type_expr_mentions_type_param(param, type_params)) - || Self::type_expr_mentions_type_param(return_ty, type_params) + fn eval_static_reflection_enum_value( + &mut self, + expr: &Expr, + ) -> Option { + if let Some(kind_tag) = self.eval_type_kind_value(expr) { + return Some(StaticReflectionEnumValue::TypeKind(kind_tag)); + } + self.eval_type_primitive_value(expr) + .map(StaticReflectionEnumValue::TypePrimitive) + } + + fn eval_static_type_name_value(&mut self, expr: &Expr) -> Option { + match expr { + Expr::Paren(inner, _) => self.eval_static_type_name_value(inner), + Expr::GenericCall(callee, type_args, args, _) + if args.is_empty() + && self + .resolved_expr_name(callee) + .is_some_and(|name| name == "type.name") => + { + let [type_arg] = type_args.as_slice() else { + return None; + }; + let type_id = self.reflection_type_arg_id(type_args)?; + let namespace = self.current_function_name.as_deref().and_then(|name| { + name.rsplit_once('.') + .map(|(namespace, _)| namespace.to_string()) + }); + Some( + self.reflection_type_info_for_type_expr( + type_arg, + namespace.as_deref(), + type_id, + ) + .type_name, + ) } + _ => None, } } - fn check_implement_block(&mut self, block: &ast::ImplementBlock) { - let owner_ty = self.resolve_type_expr(&block.for_type); - let owner_name = self.type_name(owner_ty); - for method in &block.methods { - self.check_method(&owner_name, method); + fn static_string_literal(expr: &Expr) -> Option<&str> { + match expr { + Expr::Paren(inner, _) => Self::static_string_literal(inner), + Expr::StringLiteral(value, _) => Some(value), + _ => None, } } - fn check_function_impl(&mut self, func: &FunctionDef, function_name: String) { - self.graphics_method_definitions - .insert(func.span, func.clone()); - let is_main = func.name.name == "main" && !function_name.contains('.'); - for param in &func.params { - if matches!(¶m.ty, TypeExpr::Named(name) if name.name == "Graphics") - && let Some(definition) = self.declaration_def_id(param.name.span) + fn eval_type_kind_value(&mut self, expr: &Expr) -> Option { + match expr { + Expr::Paren(inner, _) => self.eval_type_kind_value(inner), + Expr::Ident(ident) => self.reflection_type_kind_value_for_ident(ident), + Expr::GenericCall(callee, type_args, args, _) + if args.is_empty() + && self + .resolved_expr_name(callee) + .is_some_and(|name| name == "type.kind_tag") => { - self.graphics_authority_params.insert(definition); - } - if !capability::type_expr_is_capability(¶m.ty) { - continue; + self.reflection_type_arg_kind_tag(type_args) } - if is_main && param.view { - self.sink - .emit(errors::viewed_main_capability(¶m.name.name, param.span)); - } else if !is_main && !param.view { - self.sink.emit(errors::owned_capability_outside_main( - &function_name, - ¶m.name.name, - param.span, - )); + Expr::FieldAccess(base, field, _) if field.name == "kind_tag" => { + self.eval_type_info_type_arg_kind_tag(base) } + _ => self.type_kind_literal_name(expr), } + } - let return_type = func - .return_type - .as_ref() - .map(|t| self.resolve_type_expr(t)) - .unwrap_or(TypeInterner::NOTHING); - - self.current_return_type = Some(return_type); - - // Set the purity context for this function. - let is_pure = Self::function_is_pure(func); - self.current_function_name = Some(function_name); - self.current_function_pure = is_pure; - - // Bind parameter types into the type environment. - for param in &func.params { - self.record_debug_type_name(¶m.name, ¶m.ty); - let param_type = self.resolve_type_expr(¶m.ty); - if let Some(def_id) = self.declaration_def_id(param.name.span) { - self.type_env.insert(def_id, param_type); + fn eval_type_primitive_value(&mut self, expr: &Expr) -> Option { + match expr { + Expr::Paren(inner, _) => self.eval_type_primitive_value(inner), + Expr::Ident(ident) => self.reflection_type_primitive_value_for_ident(ident), + Expr::Handle(target, _, body, _) => { + match self.eval_optional_type_primitive_value(target)? { + Some(primitive_tag) => Some(primitive_tag), + None => Self::handle_default_expr(body) + .and_then(|default| self.eval_type_primitive_value(default)), + } } - self.record_assignment_binding( - ¶m.name, - param.mutable, - param.view || Self::type_is_view(¶m.ty), - ); + _ => self.type_primitive_literal_name(expr), } - - self.check_block(&func.body); - - self.current_return_type = None; - self.current_function_name = None; - self.current_function_pure = false; - } - - fn check_verify_block(&mut self, verify: &VerifyBlock) { - self.in_verify_block = true; - self.current_verify_name = Some(verify.name.name.clone()); - self.check_block(&verify.body); - self.in_verify_block = false; - self.current_verify_name = None; } - fn check_property_block(&mut self, prop: &ast::PropertyBlock) { - self.in_property_block = true; - for given in &prop.givens { - self.record_debug_type_name(&given.name, &given.ty); - let given_type = self.resolve_type_expr(&given.ty); - if let Some(def_id) = self.declaration_def_id(given.name.span) { - self.type_env.insert(def_id, given_type); + fn eval_optional_type_primitive_value(&mut self, expr: &Expr) -> Option> { + match expr { + Expr::Paren(inner, _) => self.eval_optional_type_primitive_value(inner), + Expr::GenericCall(callee, type_args, args, _) + if args.is_empty() + && self + .resolved_expr_name(callee) + .is_some_and(|name| name == "type.primitive_tag") => + { + Some(self.reflection_type_arg_primitive_tag(type_args)) + } + Expr::FieldAccess(base, field, _) if field.name == "primitive_tag" => { + self.eval_type_info_primitive_tag(base) } + _ => None, } - self.check_block(&prop.body); - self.in_property_block = false; } - // ------------------------------------------------------------------ - // Block - // ------------------------------------------------------------------ - - fn check_block(&mut self, block: &Block) { - self.push_reflection_local_fact_scope(); - self.nonzero_fact_scopes.push(HashSet::new()); - for stmt in &block.stmts { - self.check_stmt(stmt); - } - self.nonzero_fact_scopes.pop(); - self.pop_reflection_local_fact_scope(); + fn eval_type_info_type_arg_kind_tag(&mut self, expr: &Expr) -> Option { + self.eval_type_info_facts(expr).map(|facts| facts.kind_tag) } - // ------------------------------------------------------------------ - // Statements - // ------------------------------------------------------------------ + fn eval_type_info_primitive_tag(&mut self, expr: &Expr) -> Option> { + self.eval_type_info_facts(expr) + .map(|facts| facts.primitive_tag) + } - fn check_stmt(&mut self, stmt: &Stmt) { - match stmt { - Stmt::VarDecl(decl) => self.check_var_decl(decl), - Stmt::Assign(assign) => self.check_assign(assign), - Stmt::Return(ret) => self.check_return(ret), - Stmt::ComptimeTypeBind(bind) => self.check_comptime_type_bind(bind), - Stmt::If(if_stmt) => self.check_if(if_stmt), - Stmt::For(for_stmt) => self.check_for(for_stmt), - Stmt::While(while_stmt) => self.check_while(while_stmt), - Stmt::Expr(expr_stmt) => { - let ty = self.check_expr(&expr_stmt.expr); - // Warn if a result or optional value is silently discarded. - if ty != TypeInterner::ERROR { - let resolved = self.interner.resolve(ty); - if matches!(resolved, Type::Result(_, _)) { - self.sink.emit(errors::unhandled_result(expr_stmt.span)); - } else if matches!(resolved, Type::Optional(_)) { - self.sink.emit(errors::unhandled_optional(expr_stmt.span)); - } - } + fn eval_type_info_facts(&mut self, expr: &Expr) -> Option { + match expr { + Expr::Paren(inner, _) => self.eval_type_info_facts(inner), + Expr::View(inner, _) => self.eval_type_info_facts(inner), + Expr::Ident(ident) => { + let kind_tag = self.reflection_type_info_kind_for_ident(ident)?; + let primitive_tag = self + .reflection_type_info_primitive_for_ident(ident) + .unwrap_or(None); + Some(ReflectionTypeInfoStaticFacts { + kind_tag, + primitive_tag, + }) } - Stmt::Assert(assert_stmt) => self.check_assert(assert_stmt), - Stmt::Trace(trace_stmt) => self.check_trace(trace_stmt), - Stmt::Breakpoint(breakpoint_stmt) => self.check_breakpoint(breakpoint_stmt), - Stmt::Match(match_stmt) => self.check_match(match_stmt), - Stmt::Respond(resp) => self.check_respond(resp), - Stmt::Use(_) | Stmt::Break(_) | Stmt::Continue(_) => {} + Expr::GenericCall(callee, type_args, args, _) + if args.is_empty() + && self + .resolved_expr_name(callee) + .is_some_and(|name| name == "type.info") => + { + let kind_tag = self.reflection_type_arg_kind_tag(type_args)?; + let primitive_tag = self.reflection_type_arg_primitive_tag(type_args); + Some(ReflectionTypeInfoStaticFacts { + kind_tag, + primitive_tag, + }) + } + _ => None, } } - fn check_comptime_type_bind(&mut self, bind: &ast::ComptimeTypeBindStmt) { - self.check_expr(&bind.value); + fn reflection_type_arg_kind_tag(&mut self, type_args: &[TypeExpr]) -> Option { + let [type_arg] = type_args else { + return None; + }; + let type_id = self.reflection_type_arg_id(type_args)?; + Some(self.reflection_kind_tag_for_type_expr(type_arg, type_id)) + } - if let Some(bound_type_expr) = comptime_type_info_binding(&bind.value) { - let bound_ty = self.resolve_type_expr(bound_type_expr); - if bound_ty == TypeInterner::ERROR { - self.check_block(&bind.body); - return; - } + fn reflection_type_arg_primitive_tag(&mut self, type_args: &[TypeExpr]) -> Option { + let [type_arg] = type_args else { + return None; + }; + let type_id = self.reflection_type_arg_id(type_args)?; + self.reflection_primitive_tag_for_type_expr_static(type_arg, type_id) + } - let namespace = self.current_function_name.as_deref().and_then(|name| { - name.rsplit_once('.') - .map(|(namespace, _)| namespace.to_string()) - }); - let reflection = self.reflection_type_info_for_type_expr( - bound_type_expr, - namespace.as_deref(), - bound_ty, - ); - let body = self.check_comptime_type_bind_body( - &bind.name.name, - bound_ty, - reflection.clone(), - &bind.body, - ); - self.record_comptime_type_bindings( - bind.span, - vec![CheckedComptimeTypeBinding { - selection: CheckedComptimeTypeSelection::Unconditional, - bound_type: bound_ty, - reflection, - body, - }], - ); - return; + fn reflection_type_arg_id(&mut self, type_args: &[TypeExpr]) -> Option { + let [type_arg] = type_args else { + return None; + }; + if let TypeExpr::Named(ident) = type_arg { + if let Some(&type_id) = self.type_var_subst.get(&ident.name) { + return Some(type_id); + } } + let type_id = self.resolve_type_expr(type_arg); + (type_id != TypeInterner::ERROR).then_some(type_id) + } - if let Some((source_type_expr, index)) = comptime_type_arg_binding(&bind.value) { - let source_ty = self.resolve_type_expr(source_type_expr); - if source_ty == TypeInterner::ERROR { - self.check_block(&bind.body); - return; - } + fn block_uses_type_param_reflection( + &self, + block: &Block, + type_params: &HashSet, + ) -> bool { + block + .stmts + .iter() + .any(|stmt| self.stmt_uses_type_param_reflection(stmt, type_params)) + } - let arg_types = self.type_info_arg_types_for_type_expr(source_type_expr); - if let Some(&bound_ty) = arg_types.get(index) { - if bound_ty != TypeInterner::ERROR { - let namespace = self.current_function_name.as_deref().and_then(|name| { - name.rsplit_once('.') - .map(|(namespace, _)| namespace.to_string()) - }); - let source_info = self.reflection_type_info_for_type_expr( - source_type_expr, - namespace.as_deref(), - source_ty, - ); - let reflection = source_info - .args - .get(index) - .cloned() - .unwrap_or_else(|| self.reflection_type_info_for_type(bound_ty)); - let body = self.check_comptime_type_bind_body( - &bind.name.name, - bound_ty, - reflection.clone(), - &bind.body, - ); - self.record_comptime_type_bindings( - bind.span, - vec![CheckedComptimeTypeBinding { - selection: CheckedComptimeTypeSelection::Unconditional, - bound_type: bound_ty, - reflection, - body, - }], - ); - } - return; + fn stmt_uses_type_param_reflection(&self, stmt: &Stmt, type_params: &HashSet) -> bool { + match stmt { + Stmt::VarDecl(decl) => self.expr_uses_type_param_reflection(&decl.value, type_params), + Stmt::Assign(assign) => { + self.expr_uses_type_param_reflection(&assign.target, type_params) + || self.expr_uses_type_param_reflection(&assign.value, type_params) } - - self.sink - .emit(errors::invalid_comptime_type_binding(bind.value.span())); - self.check_block(&bind.body); - return; + Stmt::Return(ret) => ret + .value + .as_ref() + .is_some_and(|expr| self.expr_uses_type_param_reflection(expr, type_params)), + Stmt::Respond(resp) => self.expr_uses_type_param_reflection(&resp.value, type_params), + Stmt::ComptimeTypeBind(bind) => { + self.expr_uses_type_param_reflection(&bind.value, type_params) + || self.block_uses_type_param_reflection(&bind.body, type_params) + } + Stmt::If(if_stmt) => { + self.expr_uses_type_param_reflection(&if_stmt.condition, type_params) + || self.block_uses_type_param_reflection(&if_stmt.then_block, type_params) + || if_stmt.else_ifs.iter().any(|(condition, block)| { + self.expr_uses_type_param_reflection(condition, type_params) + || self.block_uses_type_param_reflection(block, type_params) + }) + || if_stmt.else_block.as_ref().is_some_and(|block| { + self.block_uses_type_param_reflection(block, type_params) + }) + } + Stmt::For(for_stmt) => { + self.expr_uses_type_param_reflection(&for_stmt.iterable, type_params) + || self.block_uses_type_param_reflection(&for_stmt.body, type_params) + } + Stmt::While(while_stmt) => { + self.expr_uses_type_param_reflection(&while_stmt.condition, type_params) + || self.block_uses_type_param_reflection(&while_stmt.body, type_params) + } + Stmt::Match(match_stmt) => { + self.expr_uses_type_param_reflection(&match_stmt.expr, type_params) + || match_stmt + .arms + .iter() + .any(|arm| self.block_uses_type_param_reflection(&arm.body, type_params)) + } + Stmt::Expr(expr_stmt) => { + self.expr_uses_type_param_reflection(&expr_stmt.expr, type_params) + } + Stmt::Assert(assert_stmt) => { + self.expr_uses_type_param_reflection(&assert_stmt.condition, type_params) + || assert_stmt.message.as_ref().is_some_and(|message| { + self.expr_uses_type_param_reflection(message, type_params) + }) + } + Stmt::Breakpoint(breakpoint_stmt) => breakpoint_stmt + .condition + .as_ref() + .is_some_and(|expr| self.expr_uses_type_param_reflection(expr, type_params)), + Stmt::Trace(_) | Stmt::Use(_) | Stmt::Break(_) | Stmt::Continue(_) => false, } + } - if let Some(field_name) = reflected_field_type_info_binding(&bind.value) { - if let Some(field_types) = self.reflected_field_candidates_for_name(field_name) { - let mut bindings = Vec::new(); - for (iteration_index, (field_ty, reflection)) in field_types.into_iter().enumerate() - { - if field_ty != TypeInterner::ERROR { - let body = self.check_comptime_type_bind_body( - &bind.name.name, - field_ty, - reflection.clone(), - &bind.body, - ); - bindings.push(CheckedComptimeTypeBinding { - selection: CheckedComptimeTypeSelection::ReflectedIteration( - iteration_index, - ), - bound_type: field_ty, - reflection, - body, - }); - } - } - self.record_comptime_type_bindings(bind.span, bindings); - return; + fn expr_uses_type_param_reflection(&self, expr: &Expr, type_params: &HashSet) -> bool { + match expr { + Expr::GenericCall(callee, type_args, args, _) => { + let is_type_reflection = self + .resolved_expr_name(callee) + .is_some_and(|name| name.starts_with("type.")); + (is_type_reflection + && type_args + .iter() + .any(|arg| Self::type_expr_mentions_type_param(arg, type_params))) + || self.expr_uses_type_param_reflection(callee, type_params) + || args + .iter() + .any(|arg| self.expr_uses_type_param_reflection(&arg.value, type_params)) + } + Expr::Call(callee, args, _) => { + self.expr_uses_type_param_reflection(callee, type_params) + || args + .iter() + .any(|arg| self.expr_uses_type_param_reflection(&arg.value, type_params)) + } + Expr::Binary(lhs, _, rhs, _) => { + self.expr_uses_type_param_reflection(lhs, type_params) + || self.expr_uses_type_param_reflection(rhs, type_params) + } + 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, _) => self.expr_uses_type_param_reflection(inner, type_params), + Expr::ListConstruct(items, _) => items + .iter() + .any(|item| self.expr_uses_type_param_reflection(item, type_params)), + Expr::MapConstruct(entries, _) => entries.iter().any(|(key, value)| { + self.expr_uses_type_param_reflection(key, type_params) + || self.expr_uses_type_param_reflection(value, type_params) + }), + Expr::Handle(target, _, body, _) => { + self.expr_uses_type_param_reflection(target, type_params) + || self.block_uses_type_param_reflection(body, type_params) + } + Expr::StringInterpolation(parts, _) => parts.iter().any(|part| match part { + StringPart::Literal(_) => false, + StringPart::Expr(expr) => self.expr_uses_type_param_reflection(expr, type_params), + }), + Expr::Pipeline(base, steps, _) => { + self.expr_uses_type_param_reflection(base, type_params) + || steps.iter().any(|step| { + self.expr_uses_type_param_reflection(&step.function, type_params) + || step.extra_args.iter().any(|arg| { + self.expr_uses_type_param_reflection(&arg.value, type_params) + }) + }) + } + // Generic signature annotations require ordinary substitution; + // only operations inside the closure body can use reflection. + Expr::InlineFn(_, _, body, _) => { + self.block_uses_type_param_reflection(body, type_params) } + Expr::IntLiteral(_, _) + | Expr::FloatLiteral(_, _) + | Expr::StringLiteral(_, _) + | Expr::BoolLiteral(_, _) + | Expr::Nothing(_) + | Expr::Ident(_) + | Expr::None(_) + | Expr::EnumVariant(_, _, _) + | Expr::Error(_) => false, } + } - if let Some(info_name) = reflected_type_info_binding(&bind.value) { - if let Some(info_types) = self.reflected_type_info_types_for_name(info_name) { - let mut bindings = Vec::new(); - for (iteration_index, (info_ty, reflection)) in info_types.into_iter().enumerate() { - if info_ty != TypeInterner::ERROR { - let body = self.check_comptime_type_bind_body( - &bind.name.name, - info_ty, - reflection.clone(), - &bind.body, - ); - bindings.push(CheckedComptimeTypeBinding { - selection: CheckedComptimeTypeSelection::ReflectedIteration( - iteration_index, - ), - bound_type: info_ty, - reflection, - body, - }); - } - } - self.record_comptime_type_bindings(bind.span, bindings); - return; + fn type_expr_mentions_type_param(ty: &TypeExpr, type_params: &HashSet) -> bool { + match ty { + TypeExpr::Named(ident) => type_params.contains(&ident.name), + TypeExpr::Generic(_, args, _) => args + .iter() + .any(|arg| Self::type_expr_mentions_type_param(arg, type_params)), + TypeExpr::View(inner, _) => Self::type_expr_mentions_type_param(inner, type_params), + TypeExpr::StateQualified(inner, _, _) => { + Self::type_expr_mentions_type_param(inner, type_params) + } + TypeExpr::Function(params, return_ty, _) => { + params + .iter() + .any(|param| Self::type_expr_mentions_type_param(param, type_params)) + || Self::type_expr_mentions_type_param(return_ty, type_params) } } - - self.sink - .emit(errors::invalid_comptime_type_binding(bind.value.span())); - self.check_block(&bind.body); } - fn check_comptime_type_bind_body( - &mut self, - name: &str, - bound_ty: TypeId, - reflection: ReflectionTypeInfo, - body: &Block, - ) -> CheckedBodyFacts { - // The same source spans may be checked repeatedly for different - // reflected elements. Clear the previous flat facts so this snapshot - // contains only the current concrete expansion; the last expansion - // remains in the legacy flat maps for interpreter compatibility. - self.clear_checked_body_facts(body.span); - let kind_tag = ReflectionTypeInfo::kind_tag_variant(&reflection.kind).to_string(); - let previous = self.type_var_subst.insert(name.to_string(), bound_ty); - let previous_reflection = self - .type_var_reflections - .insert(name.to_string(), reflection); - let previous_kind = self.type_var_kind_tags.insert(name.to_string(), kind_tag); - self.check_block(body); - if let Some(previous) = previous { - self.type_var_subst.insert(name.to_string(), previous); - } else { - self.type_var_subst.remove(name); + fn check_implement_block(&mut self, block: &ast::ImplementBlock) { + let owner_ty = self.resolve_type_expr(&block.for_type); + let owner_name = self.type_name(owner_ty); + for method in &block.methods { + self.check_method(&owner_name, method); } - if let Some(previous) = previous_reflection { - self.type_var_reflections.insert(name.to_string(), previous); - } else { - self.type_var_reflections.remove(name); + } + + fn check_function_impl(&mut self, func: &FunctionDef, function_name: String) { + let saved_ownership_context = std::mem::replace( + &mut self.ownership_context, + CheckedOwnershipContext::Ordinary, + ); + self.graphics_method_definitions + .insert(func.span, func.clone()); + let is_main = func.name.name == "main" && !function_name.contains('.'); + for param in &func.params { + if matches!(¶m.ty, TypeExpr::Named(name) if name.name == "Graphics") + && let Some(definition) = self.declaration_def_id(param.name.span) + { + self.graphics_authority_params.insert(definition); + } + if !capability::type_expr_is_capability(¶m.ty) { + continue; + } + if is_main && param.view { + self.sink + .emit(errors::viewed_main_capability(¶m.name.name, param.span)); + } else if !is_main && !param.view { + self.sink.emit(errors::owned_capability_outside_main( + &function_name, + ¶m.name.name, + param.span, + )); + } } - if let Some(previous) = previous_kind { - self.type_var_kind_tags.insert(name.to_string(), previous); - } else { - self.type_var_kind_tags.remove(name); + + let return_type = func + .return_type + .as_ref() + .map(|t| self.resolve_type_expr(t)) + .unwrap_or(TypeInterner::NOTHING); + + self.current_return_type = Some(return_type); + + // Set the purity context for this function. + let is_pure = Self::function_is_pure(func); + self.current_function_name = Some(function_name); + self.current_function_pure = is_pure; + + // Bind parameter types into the type environment. + for param in &func.params { + self.record_debug_type_name(¶m.name, ¶m.ty); + let param_type = self.resolve_type_expr(¶m.ty); + if let Some(def_id) = self.declaration_def_id(param.name.span) { + self.type_env.insert(def_id, param_type); + } + self.record_assignment_binding( + ¶m.name, + param.mutable, + param.view || Self::type_is_view(¶m.ty), + ); } - self.checked_body_facts(body.span) - } - fn reflected_field_candidates_for_name( - &self, - name: &str, - ) -> Option> { - self.reflected_field_type_scopes - .iter() - .rev() - .find_map(|scope| scope.get(name)) - .cloned() + self.check_block(&func.body); + + self.current_return_type = None; + self.current_function_name = None; + self.current_function_pure = false; + self.ownership_context = saved_ownership_context; } - fn reflected_type_info_types_for_name( - &self, - name: &str, - ) -> Option> { - self.reflected_type_info_scopes - .iter() - .rev() - .find_map(|scope| scope.get(name)) - .cloned() + fn check_verify_block(&mut self, verify: &VerifyBlock) { + let saved_context = + std::mem::replace(&mut self.ownership_context, CheckedOwnershipContext::Verify); + self.in_verify_block = true; + self.current_verify_name = Some(verify.name.name.clone()); + self.check_block(&verify.body); + self.in_verify_block = false; + self.current_verify_name = None; + self.ownership_context = saved_context; } - fn reflected_variant_owner_for_name(&self, name: &str) -> Option { - self.reflected_variant_type_scopes - .iter() - .rev() - .find_map(|scope| scope.get(name)) - .copied() - } - - fn reflected_machine_state_owner_for_name(&self, name: &str) -> Option { - self.reflected_machine_state_type_scopes - .iter() - .rev() - .find_map(|scope| scope.get(name)) - .copied() - } - - fn reflection_type_info_kind_for_ident(&self, ident: &ast::Ident) -> Option { - let def_id = self.ident_def_id(ident)?; - self.reflection_type_info_kind_scopes - .iter() - .rev() - .find_map(|scope| scope.get(&def_id)) - .cloned() - } - - fn reflection_type_info_primitive_for_ident( - &self, - ident: &ast::Ident, - ) -> Option> { - let def_id = self.ident_def_id(ident)?; - self.reflection_type_info_primitive_scopes - .iter() - .rev() - .find_map(|scope| scope.get(&def_id)) - .cloned() - } - - fn reflection_type_kind_value_for_ident(&self, ident: &ast::Ident) -> Option { - let def_id = self.ident_def_id(ident)?; - self.reflection_type_kind_value_scopes - .iter() - .rev() - .find_map(|scope| scope.get(&def_id)) - .cloned() - } - - fn reflection_type_primitive_value_for_ident(&self, ident: &ast::Ident) -> Option { - let def_id = self.ident_def_id(ident)?; - self.reflection_type_primitive_value_scopes - .iter() - .rev() - .find_map(|scope| scope.get(&def_id)) - .cloned() + fn check_property_block(&mut self, prop: &ast::PropertyBlock) { + let saved_context = std::mem::replace( + &mut self.ownership_context, + CheckedOwnershipContext::Property, + ); + self.in_property_block = true; + for given in &prop.givens { + self.record_debug_type_name(&given.name, &given.ty); + let given_type = self.resolve_type_expr(&given.ty); + if let Some(def_id) = self.declaration_def_id(given.name.span) { + self.type_env.insert(def_id, given_type); + self.record_assignment_binding(&given.name, false, false); + } + } + self.check_block(&prop.body); + self.in_property_block = false; + self.ownership_context = saved_context; } - fn current_reflection_type_info_kind_scope_mut( - &mut self, - ) -> Option<&mut HashMap> { - self.reflection_type_info_kind_scopes.last_mut() - } + // ------------------------------------------------------------------ + // Block + // ------------------------------------------------------------------ - fn current_reflection_type_info_primitive_scope_mut( - &mut self, - ) -> Option<&mut HashMap>> { - self.reflection_type_info_primitive_scopes.last_mut() + fn check_block(&mut self, block: &Block) { + self.push_reflection_local_fact_scope(); + self.nonzero_fact_scopes.push(HashSet::new()); + for stmt in &block.stmts { + self.check_stmt(stmt); + } + self.nonzero_fact_scopes.pop(); + self.pop_reflection_local_fact_scope(); } - fn current_reflection_type_kind_value_scope_mut( - &mut self, - ) -> Option<&mut HashMap> { - self.reflection_type_kind_value_scopes.last_mut() - } + // ------------------------------------------------------------------ + // Statements + // ------------------------------------------------------------------ - fn current_reflection_type_primitive_value_scope_mut( - &mut self, - ) -> Option<&mut HashMap> { - self.reflection_type_primitive_value_scopes.last_mut() + fn check_stmt(&mut self, stmt: &Stmt) { + match stmt { + Stmt::VarDecl(decl) => self.check_var_decl(decl), + Stmt::Assign(assign) => self.check_assign(assign), + Stmt::Return(ret) => self.check_return(ret), + Stmt::ComptimeTypeBind(bind) => self.check_comptime_type_bind(bind), + Stmt::If(if_stmt) => self.check_if(if_stmt), + Stmt::For(for_stmt) => self.check_for(for_stmt), + Stmt::While(while_stmt) => self.check_while(while_stmt), + Stmt::Expr(expr_stmt) => { + let ty = self.check_expr(&expr_stmt.expr); + // Warn if a result or optional value is silently discarded. + if ty != TypeInterner::ERROR { + let resolved = self.interner.resolve(ty); + if matches!(resolved, Type::Result(_, _)) { + self.sink.emit(errors::unhandled_result(expr_stmt.span)); + } else if matches!(resolved, Type::Optional(_)) { + self.sink.emit(errors::unhandled_optional(expr_stmt.span)); + } + } + } + Stmt::Assert(assert_stmt) => self.check_assert(assert_stmt), + Stmt::Trace(trace_stmt) => self.check_trace(trace_stmt), + Stmt::Breakpoint(breakpoint_stmt) => self.check_breakpoint(breakpoint_stmt), + Stmt::Match(match_stmt) => self.check_match(match_stmt), + Stmt::Respond(resp) => self.check_respond(resp), + Stmt::Use(_) | Stmt::Break(_) | Stmt::Continue(_) => {} + } } - fn push_reflection_local_fact_scope(&mut self) { - self.reflection_type_info_kind_scopes.push(HashMap::new()); - self.reflection_type_info_primitive_scopes - .push(HashMap::new()); - self.reflection_type_kind_value_scopes.push(HashMap::new()); - self.reflection_type_primitive_value_scopes - .push(HashMap::new()); - } + fn check_comptime_type_bind(&mut self, bind: &ast::ComptimeTypeBindStmt) { + self.check_expr(&bind.value); - fn push_reflection_param_fact_scope( - &mut self, - func: &FunctionDef, - facts: &ReflectionParamFacts, - ) { - let mut type_info_scope = HashMap::new(); - for (index, kind_tag) in &facts.type_info_kinds { - if let Some(param) = func.params.get(*index) { - if let Some(def_id) = self.declaration_def_id(param.name.span) { - type_info_scope.insert(def_id, kind_tag.clone()); - } + if let Some(bound_type_expr) = comptime_type_info_binding(&bind.value) { + let bound_ty = self.resolve_type_expr(bound_type_expr); + if bound_ty == TypeInterner::ERROR { + self.check_block(&bind.body); + return; } + + let namespace = self.current_function_name.as_deref().and_then(|name| { + name.rsplit_once('.') + .map(|(namespace, _)| namespace.to_string()) + }); + let reflection = self.reflection_type_info_for_type_expr( + bound_type_expr, + namespace.as_deref(), + bound_ty, + ); + let body = self.check_comptime_type_bind_body( + &bind.name.name, + bound_ty, + reflection.clone(), + &bind.body, + ); + self.record_comptime_type_bindings( + bind.span, + vec![CheckedComptimeTypeBinding { + selection: CheckedComptimeTypeSelection::Unconditional, + bound_type: bound_ty, + reflection, + body, + }], + ); + return; } - let mut type_info_primitive_scope = HashMap::new(); - for (index, primitive_tag) in &facts.type_info_primitives { - if let Some(param) = func.params.get(*index) { - if let Some(def_id) = self.declaration_def_id(param.name.span) { - type_info_primitive_scope.insert(def_id, primitive_tag.clone()); - } + if let Some((source_type_expr, index)) = comptime_type_arg_binding(&bind.value) { + let source_ty = self.resolve_type_expr(source_type_expr); + if source_ty == TypeInterner::ERROR { + self.check_block(&bind.body); + return; } - } - let mut type_kind_scope = HashMap::new(); - for (index, kind_tag) in &facts.type_kind_values { - if let Some(param) = func.params.get(*index) { - if let Some(def_id) = self.declaration_def_id(param.name.span) { - type_kind_scope.insert(def_id, kind_tag.clone()); + let arg_types = self.type_info_arg_types_for_type_expr(source_type_expr); + if let Some(&bound_ty) = arg_types.get(index) { + if bound_ty != TypeInterner::ERROR { + let namespace = self.current_function_name.as_deref().and_then(|name| { + name.rsplit_once('.') + .map(|(namespace, _)| namespace.to_string()) + }); + let source_info = self.reflection_type_info_for_type_expr( + source_type_expr, + namespace.as_deref(), + source_ty, + ); + let reflection = source_info + .args + .get(index) + .cloned() + .unwrap_or_else(|| self.reflection_type_info_for_type(bound_ty)); + let body = self.check_comptime_type_bind_body( + &bind.name.name, + bound_ty, + reflection.clone(), + &bind.body, + ); + self.record_comptime_type_bindings( + bind.span, + vec![CheckedComptimeTypeBinding { + selection: CheckedComptimeTypeSelection::Unconditional, + bound_type: bound_ty, + reflection, + body, + }], + ); } + return; } + + self.sink + .emit(errors::invalid_comptime_type_binding(bind.value.span())); + self.check_block(&bind.body); + return; } - let mut type_primitive_scope = HashMap::new(); - for (index, primitive_tag) in &facts.type_primitive_values { - if let Some(param) = func.params.get(*index) { - if let Some(def_id) = self.declaration_def_id(param.name.span) { - type_primitive_scope.insert(def_id, primitive_tag.clone()); + if let Some(field_name) = reflected_field_type_info_binding(&bind.value) { + if let Some(field_types) = self.reflected_field_candidates_for_name(field_name) { + let mut bindings = Vec::new(); + for (iteration_index, (field_ty, reflection)) in field_types.into_iter().enumerate() + { + if field_ty != TypeInterner::ERROR { + let body = self.check_comptime_type_bind_body( + &bind.name.name, + field_ty, + reflection.clone(), + &bind.body, + ); + bindings.push(CheckedComptimeTypeBinding { + selection: CheckedComptimeTypeSelection::ReflectedIteration( + iteration_index, + ), + bound_type: field_ty, + reflection, + body, + }); + } } + self.record_comptime_type_bindings(bind.span, bindings); + return; } } - self.reflection_type_info_kind_scopes.push(type_info_scope); + if let Some(info_name) = reflected_type_info_binding(&bind.value) { + if let Some(info_types) = self.reflected_type_info_types_for_name(info_name) { + let mut bindings = Vec::new(); + for (iteration_index, (info_ty, reflection)) in info_types.into_iter().enumerate() { + if info_ty != TypeInterner::ERROR { + let body = self.check_comptime_type_bind_body( + &bind.name.name, + info_ty, + reflection.clone(), + &bind.body, + ); + bindings.push(CheckedComptimeTypeBinding { + selection: CheckedComptimeTypeSelection::ReflectedIteration( + iteration_index, + ), + bound_type: info_ty, + reflection, + body, + }); + } + } + self.record_comptime_type_bindings(bind.span, bindings); + return; + } + } + + self.sink + .emit(errors::invalid_comptime_type_binding(bind.value.span())); + self.check_block(&bind.body); + } + + fn check_comptime_type_bind_body( + &mut self, + name: &str, + bound_ty: TypeId, + reflection: ReflectionTypeInfo, + body: &Block, + ) -> CheckedBodyFacts { + // The same source spans may be checked repeatedly for different + // reflected elements. Clear the previous flat facts so this snapshot + // contains only the current concrete expansion; the last expansion + // remains in the legacy flat maps for interpreter compatibility. + self.clear_checked_body_facts(body.span); + let kind_tag = ReflectionTypeInfo::kind_tag_variant(&reflection.kind).to_string(); + let previous = self.type_var_subst.insert(name.to_string(), bound_ty); + let previous_reflection = self + .type_var_reflections + .insert(name.to_string(), reflection); + let previous_kind = self.type_var_kind_tags.insert(name.to_string(), kind_tag); + self.check_block(body); + if let Some(previous) = previous { + self.type_var_subst.insert(name.to_string(), previous); + } else { + self.type_var_subst.remove(name); + } + if let Some(previous) = previous_reflection { + self.type_var_reflections.insert(name.to_string(), previous); + } else { + self.type_var_reflections.remove(name); + } + if let Some(previous) = previous_kind { + self.type_var_kind_tags.insert(name.to_string(), previous); + } else { + self.type_var_kind_tags.remove(name); + } + self.checked_body_facts(body.span) + } + + fn reflected_field_candidates_for_name( + &self, + name: &str, + ) -> Option> { + self.reflected_field_type_scopes + .iter() + .rev() + .find_map(|scope| scope.get(name)) + .cloned() + } + + fn reflected_type_info_types_for_name( + &self, + name: &str, + ) -> Option> { + self.reflected_type_info_scopes + .iter() + .rev() + .find_map(|scope| scope.get(name)) + .cloned() + } + + fn reflected_variant_owner_for_name(&self, name: &str) -> Option { + self.reflected_variant_type_scopes + .iter() + .rev() + .find_map(|scope| scope.get(name)) + .copied() + } + + fn reflected_machine_state_owner_for_name(&self, name: &str) -> Option { + self.reflected_machine_state_type_scopes + .iter() + .rev() + .find_map(|scope| scope.get(name)) + .copied() + } + + fn reflection_type_info_kind_for_ident(&self, ident: &ast::Ident) -> Option { + let def_id = self.ident_def_id(ident)?; + self.reflection_type_info_kind_scopes + .iter() + .rev() + .find_map(|scope| scope.get(&def_id)) + .cloned() + } + + fn reflection_type_info_primitive_for_ident( + &self, + ident: &ast::Ident, + ) -> Option> { + let def_id = self.ident_def_id(ident)?; + self.reflection_type_info_primitive_scopes + .iter() + .rev() + .find_map(|scope| scope.get(&def_id)) + .cloned() + } + + fn reflection_type_kind_value_for_ident(&self, ident: &ast::Ident) -> Option { + let def_id = self.ident_def_id(ident)?; + self.reflection_type_kind_value_scopes + .iter() + .rev() + .find_map(|scope| scope.get(&def_id)) + .cloned() + } + + fn reflection_type_primitive_value_for_ident(&self, ident: &ast::Ident) -> Option { + let def_id = self.ident_def_id(ident)?; + self.reflection_type_primitive_value_scopes + .iter() + .rev() + .find_map(|scope| scope.get(&def_id)) + .cloned() + } + + fn current_reflection_type_info_kind_scope_mut( + &mut self, + ) -> Option<&mut HashMap> { + self.reflection_type_info_kind_scopes.last_mut() + } + + fn current_reflection_type_info_primitive_scope_mut( + &mut self, + ) -> Option<&mut HashMap>> { + self.reflection_type_info_primitive_scopes.last_mut() + } + + fn current_reflection_type_kind_value_scope_mut( + &mut self, + ) -> Option<&mut HashMap> { + self.reflection_type_kind_value_scopes.last_mut() + } + + fn current_reflection_type_primitive_value_scope_mut( + &mut self, + ) -> Option<&mut HashMap> { + self.reflection_type_primitive_value_scopes.last_mut() + } + + fn push_reflection_local_fact_scope(&mut self) { + self.reflection_type_info_kind_scopes.push(HashMap::new()); + self.reflection_type_info_primitive_scopes + .push(HashMap::new()); + self.reflection_type_kind_value_scopes.push(HashMap::new()); + self.reflection_type_primitive_value_scopes + .push(HashMap::new()); + } + + fn push_reflection_param_fact_scope( + &mut self, + func: &FunctionDef, + facts: &ReflectionParamFacts, + ) { + let mut type_info_scope = HashMap::new(); + for (index, kind_tag) in &facts.type_info_kinds { + if let Some(param) = func.params.get(*index) { + if let Some(def_id) = self.declaration_def_id(param.name.span) { + type_info_scope.insert(def_id, kind_tag.clone()); + } + } + } + + let mut type_info_primitive_scope = HashMap::new(); + for (index, primitive_tag) in &facts.type_info_primitives { + if let Some(param) = func.params.get(*index) { + if let Some(def_id) = self.declaration_def_id(param.name.span) { + type_info_primitive_scope.insert(def_id, primitive_tag.clone()); + } + } + } + + let mut type_kind_scope = HashMap::new(); + for (index, kind_tag) in &facts.type_kind_values { + if let Some(param) = func.params.get(*index) { + if let Some(def_id) = self.declaration_def_id(param.name.span) { + type_kind_scope.insert(def_id, kind_tag.clone()); + } + } + } + + let mut type_primitive_scope = HashMap::new(); + for (index, primitive_tag) in &facts.type_primitive_values { + if let Some(param) = func.params.get(*index) { + if let Some(def_id) = self.declaration_def_id(param.name.span) { + type_primitive_scope.insert(def_id, primitive_tag.clone()); + } + } + } + + self.reflection_type_info_kind_scopes.push(type_info_scope); self.reflection_type_info_primitive_scopes .push(type_info_primitive_scope); self.reflection_type_kind_value_scopes.push(type_kind_scope); @@ -8953,7 +9718,12 @@ impl<'a> TypeChecker<'a> { fn check_breakpoint(&mut self, breakpoint_stmt: &ast::BreakpointStmt) { if let Some(condition) = &breakpoint_stmt.condition { + let saved_context = std::mem::replace( + &mut self.ownership_context, + CheckedOwnershipContext::BreakpointExpression, + ); let cond_type = self.check_expr(condition); + self.ownership_context = saved_context; if cond_type != TypeInterner::ERROR && cond_type != TypeInterner::BOOL { self.sink.emit(errors::condition_not_bool( &self.type_name(cond_type), @@ -8991,28 +9761,44 @@ impl<'a> TypeChecker<'a> { expected_ty: TypeId, allow_refinement_handle: bool, ) -> TypeId { + let mut source_ty = None; let ty = match expr { Expr::Paren(inner, _) => { - self.check_expr_for_expected(inner, expected_ty, allow_refinement_handle) + let ty = self.check_expr_for_expected(inner, expected_ty, allow_refinement_handle); + source_ty = self.checked_source_expression_type(inner.span()); + ty } Expr::Run(inner, _) => { let saved_in_property_block = self.in_property_block; self.in_property_block = false; let ty = self.check_expr_for_expected(inner, expected_ty, allow_refinement_handle); self.in_property_block = saved_in_property_block; + source_ty = self.checked_source_expression_type(inner.span()); ty } Expr::IntLiteral(value, _) if self.int_literal_matches_expected_type(*value, expected_ty) => { + source_ty = Some( + self.direct_secret_inner_type(expected_ty) + .unwrap_or(expected_ty), + ); expected_ty } Expr::FloatLiteral(_, _) if self.float_literal_matches_expected_type(expected_ty) => { + source_ty = Some( + self.direct_secret_inner_type(expected_ty) + .unwrap_or(expected_ty), + ); expected_ty } Expr::Unary(UnaryOp::Neg, inner, _) if self.negated_literal_matches_expected_type(inner, expected_ty) => { + source_ty = Some( + self.direct_secret_inner_type(expected_ty) + .unwrap_or(expected_ty), + ); expected_ty } Expr::Binary(lhs, op, rhs, span) @@ -9030,7 +9816,7 @@ impl<'a> TypeChecker<'a> { let expected_inner = self .direct_secret_inner_type(expected_ty) .unwrap_or(expected_ty); - match self.interner.resolve(expected_inner).clone() { + let ty = match self.interner.resolve(expected_inner).clone() { Type::List(expected_element) => self.check_list_construct_for_expected( elems, expected_ty, @@ -9038,13 +9824,17 @@ impl<'a> TypeChecker<'a> { allow_refinement_handle, ), _ => self.check_expr(expr), + }; + if ty == expected_ty { + source_ty = Some(expected_inner); } + ty } Expr::MapConstruct(entries, _span) => { let expected_inner = self .direct_secret_inner_type(expected_ty) .unwrap_or(expected_ty); - match self.interner.resolve(expected_inner).clone() { + let ty = match self.interner.resolve(expected_inner).clone() { Type::Map(expected_key, expected_value) => self .check_map_construct_for_expected( entries, @@ -9054,13 +9844,17 @@ impl<'a> TypeChecker<'a> { allow_refinement_handle, ), _ => self.check_expr(expr), + }; + if ty == expected_ty { + source_ty = Some(expected_inner); } + ty } Expr::Some(inner, _span) => { let expected_inner = self .direct_secret_inner_type(expected_ty) .unwrap_or(expected_ty); - match self.interner.resolve(expected_inner).clone() { + let ty = match self.interner.resolve(expected_inner).clone() { Type::Optional(expected_payload) => self.check_wrapper_payload_for_expected( inner, expected_ty, @@ -9068,13 +9862,17 @@ impl<'a> TypeChecker<'a> { allow_refinement_handle, ), _ => self.check_expr(expr), + }; + if ty == expected_ty { + source_ty = Some(expected_inner); } + ty } Expr::Ok(inner, _span) => { let expected_inner = self .direct_secret_inner_type(expected_ty) .unwrap_or(expected_ty); - match self.interner.resolve(expected_inner).clone() { + let ty = match self.interner.resolve(expected_inner).clone() { Type::Result(expected_payload, _) => self.check_wrapper_payload_for_expected( inner, expected_ty, @@ -9082,13 +9880,17 @@ impl<'a> TypeChecker<'a> { allow_refinement_handle, ), _ => self.check_expr(expr), + }; + if ty == expected_ty { + source_ty = Some(expected_inner); } + ty } Expr::Fail(inner, _span) => { let expected_inner = self .direct_secret_inner_type(expected_ty) .unwrap_or(expected_ty); - match self.interner.resolve(expected_inner).clone() { + let ty = match self.interner.resolve(expected_inner).clone() { Type::Result(_, expected_payload) => self.check_wrapper_payload_for_expected( inner, expected_ty, @@ -9096,21 +9898,30 @@ impl<'a> TypeChecker<'a> { allow_refinement_handle, ), _ => self.check_expr(expr), + }; + if ty == expected_ty { + source_ty = Some(expected_inner); } + ty } Expr::None(_) => { let expected_inner = self .direct_secret_inner_type(expected_ty) .unwrap_or(expected_ty); - match self.interner.resolve(expected_inner) { + let ty = match self.interner.resolve(expected_inner) { Type::Optional(_) => expected_ty, _ => self.check_expr(expr), + }; + if ty == expected_ty { + source_ty = Some(expected_inner); } + ty } Expr::Comptime(inner, _) => { self.comptime_expr_depth += 1; let ty = self.check_expr_for_expected(inner, expected_ty, allow_refinement_handle); self.comptime_expr_depth -= 1; + source_ty = self.checked_source_expression_type(inner.span()); ty } Expr::Coarsen(inner, span) => { @@ -9209,11 +10020,14 @@ impl<'a> TypeChecker<'a> { )); expected_ty } else { + source_ty = Some(actual_ty); actual_ty } } }; + self.record_source_expression_type(expr.span(), source_ty.unwrap_or(ty)); + let ty = if let Type::Secret(inner) = self.interner.resolve(expected_ty) && *inner != TypeInterner::ERROR && ty == *inner @@ -9260,6 +10074,7 @@ impl<'a> TypeChecker<'a> { if let Some(def_id) = self.declaration_def_id(name.span) { self.type_env.insert(def_id, TypeInterner::STRING); self.record_expression_type(name.span, TypeInterner::STRING); + self.record_assignment_binding(name, false, false); self.record_closure_local(def_id); } } @@ -9457,6 +10272,35 @@ impl<'a> TypeChecker<'a> { return; }; let span = self.resolve.scope_table.def(definition).span; + if let Some(&ty) = self.type_env.get(&definition) { + let checked_mode = self + .active_generic_instantiations + .last() + .and_then(|active| active.binding_modes.get(&name.span)) + .copied() + .or_else(|| { + self.active_generic_instantiations + .is_empty() + .then(|| self.binding_modes.get(&name.span).copied()) + .flatten() + }) + .unwrap_or( + if view && !crate::ownership::is_implicitly_copyable(&self.interner, ty) { + CheckedBindingMode::View { + source: CheckedViewSource::Other, + } + } else { + CheckedBindingMode::Owned + }, + ); + self.record_binding_fact(CheckedBindingFact { + definition, + declaration_span: name.span, + ty, + mode: checked_mode, + mutable, + }); + } let mode = if view { Some(AssignmentBinding::View) } else if mutable { @@ -10270,6 +11114,7 @@ impl<'a> TypeChecker<'a> { if let Some(def_id) = self.declaration_def_id(binding.span) { self.type_env.insert(def_id, *field_ty); self.record_expression_type(binding.span, *field_ty); + self.record_assignment_binding(binding, false, false); self.record_closure_local(def_id); } } @@ -10525,6 +11370,10 @@ impl<'a> TypeChecker<'a> { let saved_fn_name = self.current_function_name.take(); let saved_pure = self.current_function_pure; let saved_in_verify_block = self.in_verify_block; + let saved_ownership_context = std::mem::replace( + &mut self.ownership_context, + CheckedOwnershipContext::Ordinary, + ); let saved_in_property_block = self.in_property_block; let saved_comptime_expr_depth = self.comptime_expr_depth; let saved_graphics_callback = self.in_graphics_callback; @@ -10570,6 +11419,7 @@ impl<'a> TypeChecker<'a> { self.current_function_name = saved_fn_name; self.current_function_pure = saved_pure; self.in_verify_block = saved_in_verify_block; + self.ownership_context = saved_ownership_context; self.in_property_block = saved_in_property_block; self.comptime_expr_depth = saved_comptime_expr_depth; self.in_graphics_callback = saved_graphics_callback; @@ -10582,19 +11432,30 @@ impl<'a> TypeChecker<'a> { } }; - // Record the type for this expression span. + // Record the source operation before any surrounding expectation adds + // its physical conversion at the same expression occurrence. + self.record_source_expression_type(expr.span(), ty); self.record_expression_type(expr.span(), ty); ty } fn check_pipeline(&mut self, initial: &Expr, steps: &[ast::PipelineStep]) -> TypeId { let mut current_ty = self.check_expr(initial); + let mut input_span = initial.span(); for (index, step) in steps.iter().enumerate() { + // Callee expressions can share this span. Seal the physical virtual + // input independently before checking that source occurrence. + self.record_pipeline_step_input_type(step.span, current_ty); // The interpreter uses the checked input type to infer source-generic // arguments for the synthetic first argument of a pipeline call. self.record_expression_type(step.span, current_ty); - current_ty = - self.check_pipeline_step(current_ty, step, (index == 0).then_some(initial)); + current_ty = self.check_pipeline_step( + current_ty, + step, + (index == 0).then_some(initial), + input_span, + ); + input_span = step.span; } current_ty } @@ -10630,15 +11491,16 @@ impl<'a> TypeChecker<'a> { current_ty: TypeId, step: &ast::PipelineStep, initial: Option<&Expr>, + input_span: Span, ) -> TypeId { let (function, type_args, extra_args, piped_as_view) = Self::pipeline_step_call_parts(step); let step_ty = if self.resolved_expr_name(function).as_deref() == Some("graphics.run") { // Preserve the source authority expression while sharing every gate // with ordinary calls, including named arguments and type inference. // A prior pipeline result cannot be a declared capability parameter. - let input = initial.cloned().unwrap_or(Expr::Error(step.span)); + let input = initial.cloned().unwrap_or(Expr::Error(input_span)); let authority = if piped_as_view { - Expr::View(Box::new(input), step.span) + Expr::View(Box::new(input), input_span) } else { input }; @@ -10650,18 +11512,21 @@ impl<'a> TypeChecker<'a> { args.extend_from_slice(extra_args); self.check_call(function, type_args, &args, step.span, None) } else { - self.check_pipeline_step_call(current_ty, step, initial) + self.check_pipeline_step_call(current_ty, step, initial, input_span) }; self.record_pipeline_step_call_type(step.span, step_ty); - if let Some(handle) = &step.handle { - return self.check_handle_with_target_type( + let output_ty = if let Some(handle) = &step.handle { + self.check_handle_with_target_type( step_ty, handle.error_name.as_ref(), &handle.body, handle.span, - ); - } - step_ty + ) + } else { + step_ty + }; + self.record_source_expression_type(step.span, output_ty); + output_ty } fn check_pipeline_step_call( @@ -10669,6 +11534,33 @@ impl<'a> TypeChecker<'a> { current_ty: TypeId, step: &ast::PipelineStep, initial: Option<&Expr>, + input_span: Span, + ) -> TypeId { + let result = self.check_pipeline_step_call_inner(current_ty, step, initial); + let (callee, _, extra_args, piped_as_view) = Self::pipeline_step_call_parts(step); + let mut occurrences = vec![( + initial, + input_span, + piped_as_view || initial.is_some_and(Self::is_explicit_view), + self.checked_source_expression_type(input_span), + )]; + occurrences.extend(extra_args.iter().map(|arg| { + ( + Some(&arg.value), + arg.value.span(), + Self::is_explicit_view(&arg.value), + self.checked_source_expression_type(arg.value.span()), + ) + })); + self.record_invocation_ownership(callee, step.span, result, &occurrences); + result + } + + fn check_pipeline_step_call_inner( + &mut self, + current_ty: TypeId, + step: &ast::PipelineStep, + initial: Option<&Expr>, ) -> TypeId { let (function, type_args, extra_args, piped_as_view) = Self::pipeline_step_call_parts(step); let piped_as_view = piped_as_view || initial.is_some_and(Self::is_explicit_view); @@ -12431,6 +13323,30 @@ impl<'a> TypeChecker<'a> { args: &[ast::CallArg], span: Span, expected_return_type: Option, + ) -> TypeId { + let result = self.check_call_inner(callee, type_args, args, span, expected_return_type); + let occurrences = args + .iter() + .map(|arg| { + ( + Some(&arg.value), + arg.value.span(), + Self::is_explicit_view(&arg.value), + self.checked_source_expression_type(arg.value.span()), + ) + }) + .collect::>(); + self.record_invocation_ownership(callee, span, result, &occurrences); + result + } + + fn check_call_inner( + &mut self, + callee: &Expr, + type_args: &[TypeExpr], + args: &[ast::CallArg], + span: Span, + expected_return_type: Option, ) -> TypeId { let callee = Self::unparenthesized(callee); let callee_name = self.resolved_expr_name(callee); @@ -13535,14 +14451,25 @@ impl<'a> TypeChecker<'a> { fn is_explicit_view(expression: &Expr) -> bool { match expression { - Expr::Paren(inner, _) => Self::is_explicit_view(inner), + Expr::Paren(inner, _) | Expr::Coarsen(inner, _) | Expr::Declassify(inner, _) => { + Self::is_explicit_view(inner) + } Expr::View(_, _) => true, + // Clone acquires ownership, and FieldAccess reads a copied endpoint. + // Neither propagates an inner explicit View to this argument. _ => false, } } fn check_argument_view(&mut self, view_parameter: bool, explicit_view: bool, span: Span) { - if explicit_view && !view_parameter { + if explicit_view + && !view_parameter + && !self + .sink + .diagnostics() + .iter() + .any(|diagnostic| diagnostic.code.code() == 375 && diagnostic.span == span) + { self.sink .emit(errors::view_argument_requires_view_parameter(span)); } @@ -13555,7 +14482,8 @@ impl<'a> TypeChecker<'a> { expected: TypeId, actual: TypeId, ) { - if view_parameter + if (self.ownership_context != CheckedOwnershipContext::Ordinary + && self.caller_observation_data(actual, &mut HashSet::new())) || Self::is_explicit_view(argument) || expected == TypeInterner::ERROR || actual == TypeInterner::ERROR @@ -13564,10 +14492,18 @@ impl<'a> TypeChecker<'a> { { return; } - self.sink - .emit(crate::ownership::cannot_pass_view_to_owned_parameter( + if !self.diagnosed_caller_view_arguments.insert(argument.span()) { + return; + } + self.sink.emit(if view_parameter { + Diagnostic::error( + 401, + "cannot pass a borrowed value bare; write `view` to retain its owner", argument.span(), - )); + ) + } else { + crate::ownership::cannot_pass_view_to_owned_parameter(argument.span()) + }); let mut subject = argument; while let Expr::View(inner, _) @@ -15222,6 +16158,7 @@ impl<'a> TypeChecker<'a> { if let Some(def_id) = self.declaration_def_id(name.span) { self.type_env.insert(def_id, err_ty); self.record_expression_type(name.span, err_ty); + self.record_assignment_binding(name, false, false); self.record_closure_local(def_id); } } @@ -23323,1150 +24260,3149 @@ actor Store: } #[test] - fn owned_rebinding_rejects_views_with_parenthesized_identifier_targets() { - let errors = check_source_errors( - r#"struct Record: - value: int64 -function replace(view source: Record) returns nothing: - mutable Record alias = Record(value: 1) - (alias) = view source - ((alias)) = view source -"#, + fn owned_rebinding_rejects_views_with_parenthesized_identifier_targets() { + let errors = check_source_errors( + r#"struct Record: + value: int64 +function replace(view source: Record) returns nothing: + mutable Record alias = Record(value: 1) + (alias) = view source + ((alias)) = view source +"#, + ); + assert_eq!(errors.len(), 2, "{errors:?}"); + for diagnostic in &errors { + assert_eq!(diagnostic.code.code(), 401, "{diagnostic:?}"); + assert_eq!( + diagnostic.message, + "cannot rebind an owned value from a view; clone the value instead" + ); + } + } + + #[test] + fn owned_rebinding_preserves_immutable_target_diagnostics() { + let errors = check_source_errors( + r#"namespace app +struct Record: + value: int64 +Record constant = comptime Record(value: 1) +function local(view source: Record) returns nothing: + Record fixed = Record(value: 1) + fixed = view source + constant = view source +"#, + ); + assert_eq!(errors.len(), 2, "{errors:?}"); + assert!( + errors + .iter() + .all(|diagnostic| diagnostic.code.code() == 404) + ); + } + + #[test] + fn written_view_return_annotations_reject_headers_and_callable_results() { + for (source, count) in [ + ( + "function copy(view values: list[int64]) returns view list[int64]:\n return clone values\n", + 1, + ), + ( + "function unused[T](value: T) returns view T:\n return value\n", + 1, + ), + ( + "mutual:\n function value(number: int64) returns view int64\nfunction value(number: int64) returns view int64:\n return number\n", + 2, + ), + ( + "struct Item:\n value: int64\n function read(view self: Item) returns view int64:\n return self.value\n", + 1, + ), + ( + "interface Reader:\n function read(view self: Reader) returns view int64\nstruct Item:\n value: int64\nimplement Reader for Item:\n function read(view self: Item) returns view int64:\n return self.value\n", + 2, + ), + ( + "function callback() returns nothing:\n function() returns int64 value = function() returns view int64:\n return 1\n", + 1, + ), + ( + "function callback() returns nothing:\n mutable function() returns view int64 selected = function() returns int64:\n return 1\n", + 1, + ), + ( + "function unused[T](value: T, callback: function() returns view T) returns nothing:\n return nothing\n", + 1, + ), + ( + "struct Unused[T]:\n callback: function() returns view T\n", + 1, + ), + ( + "function factory() returns function() returns view int64:\n return function() returns int64:\n return 1\n", + 1, + ), + ] { + let errors = check_source_errors(source); + assert_eq!(errors.len(), count, "{source}\n{errors:?}"); + for error in errors { + assert_eq!(error.code.code(), 401, "{error:?}"); + assert_eq!( + error.message, + "function return type cannot be a view; return an owned value instead" + ); + assert!( + source[error.span.start as usize..error.span.end as usize].starts_with("view ") + ); + } + } + } + + #[test] + fn written_view_return_annotations_are_reported_once_per_source_span() { + let errors = check_source_errors( + r#"function identity[T](value: T) returns view T: + return value +function main() returns nothing: + int64 first = identity[int64](1) + int64 second = identity[int64](2) + string third = identity[string]("three") +"#, + ); + assert_eq!(errors.len(), 1, "{errors:?}"); + assert!(errors[0].message.contains("return type cannot be a view")); + + let errors = check_source_errors( + "function escape(view values: list[int64]) returns view list[int64]:\n return values\n", + ); + assert_eq!(errors.len(), 2, "{errors:?}"); + assert!(errors[0].message.contains("return type cannot be a view")); + assert!(errors[1].message.contains("cannot return a view")); + assert_ne!(errors[0].span, errors[1].span); + } + + #[test] + fn owned_returns_reject_known_views_and_forwarded_aliases() { + for (parameter, returned) in [ + ("view values: list[int64]", "values"), + ("values: view list[int64]", "values"), + ("view values: list[int64]", "((values))"), + ("view values: list[int64]", "view values"), + ("view values: list[int64]", "(view values)"), + ("view values: list[int64]", "forwarded"), + ] { + let source = format!( + "function escape({parameter}) returns list[int64]:\n list[int64] borrowed = view values\n list[int64] forwarded = borrowed\n return {returned}\n" + ); + let errors = check_source_errors(&source); + assert_eq!(errors.len(), 1, "{source}\n{errors:?}"); + assert_eq!(errors[0].code.code(), 401, "{errors:?}"); + assert_eq!( + errors[0].message, + "cannot return a view; clone the value or take ownership instead" + ); + assert_eq!( + &source[errors[0].span.start as usize..errors[0].span.end as usize], + returned + ); + } + } + + #[test] + fn owned_returns_check_inline_and_instantiated_generic_bodies() { + for source in [ + r#"function make() returns function(view list[int64]) returns list[int64]: + return function(view values: list[int64]) returns list[int64]: + return values +"#, + r#"function escape[T](view value: T) returns T: + return value +function main() returns nothing: + list[int64] source = list(7) + list[int64] escaped = escape[list[int64]](view source) +"#, + ] { + let errors = check_source_errors(source); + assert_eq!(errors.len(), 1, "{source}\n{errors:?}"); + assert_eq!(errors[0].code.code(), 401, "{errors:?}"); + assert!(errors[0].message.contains("cannot return a view")); + } + } + + #[test] + fn owned_call_boundaries_reject_views_across_call_forms_once() { + let prefix = r#"function consume(values: list[int64]) returns nothing: + return nothing +function generic[T](values: T) returns nothing: + return nothing +"#; + for call in [ + "consume(values)", + "consume((values))", + "consume(forwarded)", + "(consume)(forwarded)", + "generic[list[int64]](forwarded)", + "generic(forwarded)", + "callback(forwarded)", + ] { + let source = format!( + "{prefix}function reject(view values: list[int64], callback: function(list[int64]) returns nothing) returns nothing:\n list[int64] borrowed = view values\n list[int64] forwarded = borrowed\n {call}\n" + ); + let errors = check_source_errors(&source); + assert_eq!(errors.len(), 1, "{call}: {errors:?}"); + assert_eq!(errors[0].code.code(), 401, "{call}: {errors:?}"); + assert_eq!( + errors[0].message, + "cannot pass a view to an owned parameter; clone the value or take ownership instead" + ); + } + + let errors = check_source_errors( + r#"struct Item: + value: int64 + function consume(self: Item) returns nothing: + return nothing +function reject(view source: Item) returns nothing: + Item borrowed = source + Item.consume(borrowed) +"#, + ); + assert_eq!(errors.len(), 1, "{errors:?}"); + assert_eq!(errors[0].code.code(), 401, "{errors:?}"); + assert!(errors[0].message.contains("owned parameter")); + } + + #[test] + fn owned_view_call_diagnostics_do_not_hide_other_view_errors() { + let errors = check_source_errors( + r#"struct Record: + value: int64 +function consume(item: Record) returns nothing: + return nothing +function reject(view item: Record) returns nothing: + consume(item) + item.value = 2 +"#, + ); + assert_eq!(errors.len(), 2, "{errors:?}"); + assert!(errors[0].message.contains("owned parameter")); + assert!(errors[1].message.contains("views are read-only")); + assert!(errors.iter().all(|error| error.code.code() == 401)); + } + + #[test] + fn owned_pipeline_boundaries_check_initial_values_and_extra_arguments() { + let prefix = r#"function consume(values: list[int64]) returns nothing: + return nothing +function second(number: int64, values: list[int64]) returns nothing: + return nothing +function generic[T](values: T) returns nothing: + return nothing +function generic_second[T](number: int64, values: T) returns nothing: + return nothing +"#; + for call in [ + "values into consume()", + "borrowed into consume()", + "borrowed into generic[list[int64]]()", + "borrowed into generic()", + "borrowed into callback()", + "1 into second(borrowed)", + "1 into generic_second[list[int64]](borrowed)", + "1 into generic_second(borrowed)", + ] { + let source = format!( + "{prefix}function reject(view values: list[int64], callback: function(list[int64]) returns nothing) returns nothing:\n list[int64] borrowed = view values\n {call}\n" + ); + let errors = check_source_errors(&source); + assert_eq!(errors.len(), 1, "{call}: {errors:?}"); + assert_eq!(errors[0].code.code(), 401, "{call}: {errors:?}"); + assert!(errors[0].message.contains("owned parameter")); + let reported = &source[errors[0].span.start as usize..errors[0].span.end as usize]; + assert!(matches!(reported, "values" | "borrowed"), "{reported}"); + } + } + + #[test] + fn owned_view_boundaries_preserve_explicit_view_call_diagnostics() { + for call in [ + "consume(view values)", + "consume((view values))", + "generic[list[int64]](view values)", + "generic(view values)", + "callback(view values)", + "values into view consume()", + "(view values) into consume()", + "values into view generic[list[int64]]()", + "values into view generic()", + ] { + let source = format!( + "function consume(values: list[int64]) returns nothing:\n return nothing\nfunction generic[T](values: T) returns nothing:\n return nothing\nfunction reject(view values: list[int64], callback: function(list[int64]) returns nothing) returns nothing:\n {call}\n" + ); + let errors = check_source_errors(&source); + assert_eq!(errors.len(), 1, "{call}: {errors:?}"); + assert_eq!(errors[0].code.code(), 375, "{call}: {errors:?}"); + } + } + + #[test] + fn owned_view_boundaries_follow_transparent_wrappers_but_not_type_errors() { + let prefix = r#"type Numbers = list[int64] where true +function consume(values: list[int64]) returns nothing: + return nothing +"#; + for (parameter, value, explicit_view) in [ + ("view values: Numbers", "coarsen values", false), + ("view values: Numbers", "coarsen (view values)", true), + ( + "view values: secret[list[int64]]", + "declassify values", + false, + ), + ( + "view values: secret[list[int64]]", + "declassify (view values)", + true, + ), + ] { + for (body, expected_code) in [ + (format!(" return {value}\n"), 401), + ( + format!(" consume({value})\n return list()\n"), + if explicit_view { 375 } else { 401 }, + ), + ] { + let source = + format!("{prefix}function reject({parameter}) returns list[int64]:\n{body}"); + let errors = check_source_errors(&source); + assert_eq!(errors.len(), 1, "{source}\n{errors:?}"); + assert_eq!(errors[0].code.code(), expected_code, "{errors:?}"); + } + } + for body in [ + " return declassify values\n", + " consume(declassify values)\n return list()\n", + ] { + let source = format!( + "{prefix}function invalid(view values: list[int64]) returns list[int64]:\n{body}" + ); + let errors = check_source_errors(&source); + assert_eq!(errors.len(), 1, "{source}\n{errors:?}"); + assert!(errors[0].message.contains("declassify"), "{errors:?}"); + assert_ne!(errors[0].code.code(), 401, "{errors:?}"); + } + } + + #[test] + fn owned_view_boundaries_apply_to_actor_verify_and_property_bodies() { + let errors = check_source_errors( + r#"namespace app +function consume(values: list[int64]) returns int64: + return 1 +actor Store: + receive borrowed(view values: list[int64]): + list[int64] alias = view values + int64 count = consume(alias) + trace count +verify borrowed_alias: + list[int64] source = list(7) + list[int64] alias = view source + list[int64] forwarded = alias + assert consume(forwarded) == 1 +property borrowed_property_alias: + given source: list[int64] + list[int64] alias = view source + list[int64] forwarded = alias + assert consume(forwarded) == 1 +"#, + ); + assert_eq!(errors.len(), 1, "{errors:?}"); + for error in errors { + assert_eq!(error.code.code(), 401, "{error:?}"); + assert!(error.message.contains("owned parameter")); + } + } + + #[test] + fn owned_view_boundaries_preserve_copies_clones_fields_and_call_results() { + let errors = check_source_errors( + r#"struct Record: + values: list[int64] +function consume(values: list[int64]) returns nothing: + return nothing +function copy(view values: list[int64]) returns list[int64]: + list[int64] borrowed = view values + list[int64] forwarded = borrowed + return clone forwarded +function scalar(view value: int64) returns int64: + int64 copied = value + return copied +function text(view value: string) returns string: + return value +function field(view record: Record) returns list[int64]: + return record.values +function copies(view values: list[int64], view record: Record) returns nothing: + consume(clone values) + consume(clone (view values)) + consume(record.values) + consume(copy(view values)) + values into view copy() into consume() + if true: + list[int64] alias = view values + consume(clone alias) + list[int64] alias = list(7) + consume(alias) +"#, + ); + assert!(errors.is_empty(), "{errors:?}"); + } + + #[test] + fn owned_view_boundaries_preserve_copyable_transformation_results() { + let errors = check_source_errors( + r#"type Count = int64 where true +function consume(value: int64) returns nothing: + return nothing +function refined(view value: Count) returns int64: + return coarsen value +function classified(view value: secret[int64]) returns int64: + return declassify value +function copies(view count: Count, view hidden: secret[int64]) returns nothing: + consume(coarsen count) + consume(declassify hidden) +"#, + ); + assert!(errors.is_empty(), "{errors:?}"); + + let errors = check_source_errors( + "function explicit(view value: int64) returns int64:\n return view value\n", + ); + assert_eq!(errors.len(), 1, "{errors:?}"); + assert!(errors[0].message.contains("cannot return a view")); + } + + #[test] + fn handle_default_is_rechecked_with_its_concrete_success_type() { + let source = r#"function fallback(value: optional[list[int64]]) returns list[int64]: + return value handle: + default list() +"#; + let result = check_source_result(source); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "unexpected diagnostics: {:?}", + result.diagnostics + ); + let start = source.rfind("list()").expect("default list") as u32; + let span = Span::new(FileId::new(0), start, start + 6); + let default_ty = *result + .type_map + .get(&span) + .expect("default list should retain its contextual type"); + assert_eq!( + result.interner.resolve(default_ty), + &Type::List(TypeInterner::INT64) + ); + } + + #[test] + fn never_assignment_compatibility_is_directional_through_value_shapes() { + let resolve = TestEnv::new().into_resolve_result(); + let mut checker = TypeChecker::new(&resolve, CheckOptions::default()); + let absent = [ + Type::Never, + Type::List(TypeInterner::NEVER), + Type::Set(TypeInterner::NEVER), + Type::Optional(TypeInterner::NEVER), + Type::Secret(TypeInterner::NEVER), + Type::Map(TypeInterner::STRING, TypeInterner::NEVER), + Type::Result(TypeInterner::NEVER, TypeInterner::STRING), + Type::Result(TypeInterner::STRING, TypeInterner::NEVER), + ]; + let inhabited = [ + Type::Int64, + Type::List(TypeInterner::INT64), + Type::Set(TypeInterner::INT64), + Type::Optional(TypeInterner::INT64), + Type::Secret(TypeInterner::INT64), + Type::Map(TypeInterner::STRING, TypeInterner::INT64), + Type::Result(TypeInterner::INT64, TypeInterner::STRING), + Type::Result(TypeInterner::STRING, TypeInterner::INT64), + ]; + for (absent, inhabited) in absent.into_iter().zip(inhabited) { + let absent = checker.interner.intern(absent); + let inhabited = checker.interner.intern(inhabited); + assert!(!checker.types_compatible(absent, inhabited)); + assert!(checker.types_compatible(inhabited, absent)); + } + let bottom_input = checker.interner.intern(Type::Function { + params: vec![TypeInterner::NEVER], + view_params: vec![false], + return_type: TypeInterner::INT64, + }); + let concrete = checker.interner.intern(Type::Function { + params: vec![TypeInterner::INT64], + view_params: vec![false], + return_type: TypeInterner::INT64, + }); + assert!(checker.types_compatible(bottom_input, concrete)); + assert!(!checker.types_compatible(concrete, bottom_input)); + assert_eq!( + checker.merge_inferred_never(bottom_input, concrete, false), + Some(bottom_input) + ); + assert_eq!( + checker.merge_inferred_never(TypeInterner::INT8, TypeInterner::INT64, false), + None + ); + let secret = checker.interner.intern(Type::Secret(TypeInterner::NEVER)); + assert_eq!( + checker.merge_inferred_never(secret, TypeInterner::INT64, false), + None + ); + } + + #[test] + fn generic_never_places_reject_concrete_payloads_at_every_construction_boundary() { + let cases = [ + ("list[T]", " T item = 7\n return list(item)", 311), + ( + "list[T]", + " T item = none handle:\n default 7\n return list(item)", + 300, + ), + ( + "list[T]", + " mutable T item = none handle: return list()\n item = 7\n return list(item)", + 312, + ), + ("T", " return 7", 305), + ("list[T]", " return list(7)", 300), + ("list[T]", " return append[T](list(), 7)", 300), + ("optional[T]", " return some(7)", 300), + ("result[T, string]", " return ok(7)", 300), + ("result[string, T]", " return fail(7)", 300), + ("map[string, T]", " return map(\"key\": 7)", 300), + ( + "nothing", + " for item in items:\n T forged = 7\n trace forged\n return nothing", + 311, + ), + ]; + for (result_type, body, expected_code) in cases { + for comptime in ["", "comptime "] { + let source = format!( + "function append[T](values: list[T], value: T) returns list[T]:\n return list(value)\nfunction manufacture[T](view items: list[T]) returns {result_type}:\n{body}\nfunction main() returns nothing:\n println({comptime}manufacture(list()))\n return nothing\n" + ); + let errors = check_source_errors(&source); + assert!( + errors + .iter() + .any(|error| error.code.code() == expected_code), + "{source}\n{errors:?}" + ); + assert!( + errors.iter().any(|error| error.message.contains("")), + "{source}\n{errors:?}" + ); + } + } + } + + #[test] + fn inferred_literals_fill_only_never_slots_in_both_element_orders() { + let cases = [ + ("list(none, some(7))", "list[optional[int64]]"), + ("list(some(7), none)", "list[optional[int64]]"), + ("list(ok(7), fail(\"bad\"))", "list[result[int64, string]]"), + ("list(fail(\"bad\"), ok(7))", "list[result[int64, string]]"), + ("list(list(), list(7))", "list[list[int64]]"), + ("list(list(7), list())", "list[list[int64]]"), + ( + "list(some(list()), some(list(7)))", + "list[optional[list[int64]]]", + ), + ( + "list(some(list(7)), some(list()))", + "list[optional[list[int64]]]", + ), + ( + "map(\"a\": none, \"b\": some(7))", + "map[string, optional[int64]]", + ), + ( + "map(\"a\": some(7), \"b\": none)", + "map[string, optional[int64]]", + ), + ("list(map(), map(\"key\": 7))", "list[map[string, int64]]"), + ("list(map(\"key\": 7), map())", "list[map[string, int64]]"), + ("list(none, some(small))", "list[optional[int8]]"), + ("list(some(small), none)", "list[optional[int8]]"), + ]; + for (literal, expected) in cases { + let map_binding = if literal.starts_with("map(") { + "key, value" + } else { + "value" + }; + let source = format!( + "type Tiny = int8\nfunction main() returns nothing:\n Tiny small = 7\n for {map_binding} in {literal}:\n trace value\n return nothing\n" + ); + let result = check_source_result(&source); + let errors = result + .diagnostics + .iter() + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{source}\n{errors:?}"); + let start = source.find(literal).unwrap() as u32; + let span = sp(start, start + literal.len() as u32); + let ty = result.type_map[&span]; + assert_eq!(result.interner.type_name(ty), expected, "{literal}"); + } + } + + #[test] + fn contextual_absent_defaults_and_generic_returns_keep_concrete_success_types() { + let source = r#"function identity[T](values: list[T]) returns list[T]: + return values +function fallback[T](view items: list[T], value: T) returns T: + return none handle: + default value +function main() returns nothing: + int64 absent = none handle: + default 7 + int64 failure = fail("bad") handle error: + trace error + default 8 + list[int64] empty = identity(list()) + int64 generic = fallback(list(), 9) + int64 baked = comptime (none handle: default 10) + trace absent + trace failure + trace empty + trace generic + trace baked + return nothing +"#; + let errors = check_source_errors(source); + assert!(errors.is_empty(), "{errors:?}"); + } + + #[test] + fn inferred_callback_return_sums_use_a_concrete_common_signature() { + for callbacks in [ + "list(make_absent(list()), present)", + "list(present, make_absent(list()))", + ] { + let source = format!( + r#"function make_absent[T](view items: list[T]) returns function() returns optional[T]: + return function() returns optional[T]: return none +function present() returns optional[int64]: + return some(7) +function main() returns nothing: + for callback in {callbacks}: + optional[int64] value = callback() + trace value + return nothing +"# + ); + let result = check_source_result(&source); + let errors = result + .diagnostics + .iter() + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{source}\n{errors:?}"); + let start = source.find(callbacks).unwrap() as u32; + let ty = result.type_map[&sp(start, start + callbacks.len() as u32)]; + assert_eq!( + result.interner.type_name(ty), + "list[function() returns optional[int64]]" + ); + } + } + + #[test] + fn mapped_callback_inference_uses_joined_absent_and_present_elements() { + for values in ["list(none, some(7))", "list(some(7), none)"] { + let source = format!( + r#"function append[T](values: list[T], value: T) returns list[T]: + return list(value) +function mapped[A, B](values: list[A], callback: function(A) returns B) returns list[B]: + mutable list[B] output = list() + for value in values: + output = append[B](output, callback(value)) + return output +function unwrap(value: optional[int64]) returns int64: + return value handle: default 0 +function main() returns nothing: + for value in mapped({values}, unwrap): + trace value + return nothing +"# + ); + let result = check_source_result(&source); + let errors = result + .diagnostics + .iter() + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{source}\n{errors:?}"); + let call = format!("mapped({values}, unwrap)"); + let start = source.find(&call).unwrap() as u32; + let ty = result.type_map[&sp(start, start + call.len() as u32)]; + assert_eq!(result.interner.type_name(ty), "list[int64]"); + } + } + + #[test] + fn filling_inferred_never_does_not_merge_existing_primitive_widths() { + let errors = check_source_errors( + r#"type Tiny = int8 +function main() returns nothing: + Tiny small = 7 + for value in list(none, some(small), some(7)): + trace value + return nothing +"#, + ); + assert_eq!(errors.len(), 1, "{errors:?}"); + assert_eq!(errors[0].code.code(), 300); + assert!(errors[0].message.contains("optional[int8]")); + assert!(errors[0].message.contains("optional[int64]")); + } + + #[test] + fn reverse_literal_compatibility_cannot_hide_a_never_callable_input() { + for callbacks in ["list(broad, narrow(list()))", "list(narrow(list()), broad)"] { + let source = format!( + r#"function broad(value: optional[int64]) returns int64: + return 0 +function narrow[T](view items: list[T]) returns function(optional[T]) returns secret[int64]: + return function(value: optional[T]) returns secret[int64]: return 7 +function main() returns nothing: + for callback in {callbacks}: + trace callback + return nothing +"# + ); + let result = check_source_result(&source); + let errors = result + .diagnostics + .iter() + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + if callbacks.starts_with("list(broad,") { + assert!( + errors.iter().any(|error| error.code.code() == 300), + "{source}\n{errors:?}" + ); + } else { + assert!(errors.is_empty(), "{source}\n{errors:?}"); + let start = source.find(callbacks).unwrap() as u32; + let ty = result.type_map[&sp(start, start + callbacks.len() as u32)]; + assert_eq!( + result.interner.type_name(ty), + "list[function(optional[]) returns secret[int64]]" + ); + } + } + } + + #[test] + fn repeated_generic_arguments_fill_nested_never_slots_in_both_orders() { + for (first, second, expected) in [ + ("none", "some(7)", "optional[int64]"), + ("ok(7)", "fail(\"bad\")", "result[int64, string]"), + ("list()", "list(\"ready\")", "list[string]"), + ("map()", "map(\"key\": 7)", "map[string, int64]"), + ( + "some(list())", + "some(list(\"ready\"))", + "optional[list[string]]", + ), + ] { + for (first, second) in [(first, second), (second, first)] { + let call = format!("second({first}, {second})"); + let source = format!( + "function second[T](first: T, last: T) returns T:\n return last\nfunction main() returns nothing:\n println({call})\n return nothing\n" + ); + let result = check_source_result(&source); + let errors = result + .diagnostics + .iter() + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{source}\n{errors:?}"); + let start = source.find(&call).unwrap() as u32; + let ty = result.type_map[&sp(start, start + call.len() as u32)]; + assert_eq!(result.interner.type_name(ty), expected); + } + } + } + + #[test] + fn generic_return_context_fills_nested_empty_and_absent_slots() { + assert!( + check_source_errors( + r#"function identity[T](value: T) returns T: + return value +function main() returns nothing: + list[string] values = identity(list()) + optional[list[string]] absent = identity(none) + result[list[string], string] success = identity(ok(list())) + trace values + trace absent + trace success + return nothing +"#, + ) + .is_empty() + ); + } + + #[test] + fn inferred_generic_witnesses_peel_root_aliases_and_preserve_nested_aliases() { + for (parameter, setup, argument, expected_name, expected_kind) in [ + ( + "T", + " Count value = 7\n", + "value", + "int64", + "primitive_type", + ), + ( + "result[T, string]", + " result[Count, string] value = ok(7)\n", + "value", + "app.Count", + "alias_type", + ), + ( + "list[T]", + " list[Count] value = list(7)\n", + "value", + "app.Count", + "alias_type", + ), + ( + "result[T, string]", + " Wrapped value = ok(7)\n", + "value", + "app.Count", + "alias_type", + ), + ( + "function() returns T", + "", + "count_callback", + "app.Count", + "alias_type", + ), + ( + "T", + "", + "count_callback", + "function() returns app.Count", + "function_type", + ), + ( + "function(view T) returns T", + "", + "view_callback", + "app.Count", + "alias_type", + ), + ] { + let source = format!( + r#"namespace app +type Count = int64 +type Wrapped = result[Count, string] +function count_callback() returns Count: + return 7 +function view_callback(view value: Count) returns Count: + return value +function choose[T](input: {parameter}) returns string: + return type.name[T]() +function main() returns string: +{setup} return choose({argument}) +"# + ); + let result = check_source_result(&source); + let errors = result + .diagnostics + .iter() + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{source}\n{errors:?}"); + let call = result + .generic_calls + .values() + .next() + .expect("inferred choose call"); + assert_eq!( + call.specialization.type_argument_reflections[0].type_name, expected_name, + "{source}" + ); + assert_eq!( + call.specialization.type_argument_kinds, + [expected_kind], + "{source}" + ); + assert_eq!( + call.concrete_args[0], + if parameter == "T" && argument == "count_callback" { + result.generic_function_instantiations[0].parameter_types[0] + } else { + TypeInterner::INT64 + } + ); + } + } + + #[test] + fn inferred_alias_witnesses_are_unchanged_by_view_and_parentheses() { + for (parameter, setup, value, expected_name, expected_kind) in [ + ( + "T", + " Count value = 7\n", + "value", + "int64", + "primitive_type", + ), + ( + "optional[T]", + " optional[Count] value = some(7)\n", + "value", + "app.Count", + "alias_type", + ), + ( + "T", + " list[Count] value = list(7)\n", + "value", + "list[app.Count]", + "list_type", + ), + ( + "function() returns T", + "", + "count_callback", + "app.Count", + "alias_type", + ), + ] { + for argument in [ + value.to_string(), + format!("({value})"), + format!("view {value}"), + format!("(view ({value}))"), + ] { + let source = format!( + r#"namespace app +type Count = int64 +function count_callback() returns Count: + return 7 +function choose[T](view input: {parameter}) returns string: + return type.name[T]() +function main() returns string: +{setup} return choose({argument}) +"# + ); + let result = check_source_result(&source); + let errors = result + .diagnostics + .iter() + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{source}\n{errors:?}"); + let call = result + .generic_calls + .values() + .next() + .expect("inferred choose call"); + assert_eq!( + call.specialization.type_argument_reflections[0].type_name, expected_name, + "{source}" + ); + assert_eq!( + call.specialization.type_argument_kinds, + [expected_kind], + "{source}" + ); + } + } + } + + #[test] + fn deep_inferred_alias_witnesses_have_distinct_checked_specializations() { + let source = r#"namespace app +type Count = int64 +function inspect[T](input: T) returns string: + return type.name[T]() +function main() returns string: + optional[Count] counted = some(7) + optional[int64] plain = some(7) + string first = inspect(counted) + string second = inspect(plain) + return "{first}:{second}" +"#; + let result = check_source_result(source); + let errors = result + .diagnostics + .iter() + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{errors:?}"); + let [counted, plain] = result.generic_function_instantiations.as_slice() else { + panic!("expected distinct source-aware instances"); + }; + assert_eq!(counted.concrete_args, plain.concrete_args); + assert_eq!( + counted.specialization.type_argument_kinds, + plain.specialization.type_argument_kinds + ); + assert_eq!( + counted.specialization.type_argument_reflections[0].type_name, + "optional[app.Count]" + ); + assert_eq!( + plain.specialization.type_argument_reflections[0].type_name, + "optional[int64]" + ); + assert_ne!(counted.specialization, plain.specialization); + } + + #[test] + fn inferred_alias_witnesses_follow_declaration_argument_order() { + let source = r#"namespace app +type Count = int64 +type Other = int64 +function choose[T](first: list[T], second: list[T]) returns string: + return type.name[T]() +function main() returns string: + list[Count] counted = list(7) + list[Other] others = list(8) + return choose(second: others, first: counted) +"#; + let result = check_source_result(source); + let errors = result + .diagnostics + .iter() + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{errors:?}"); + let call = result.generic_calls.values().next().unwrap(); + assert_eq!( + call.specialization.type_argument_reflections[0].type_name, + "app.Count" ); - assert_eq!(errors.len(), 2, "{errors:?}"); - for diagnostic in &errors { - assert_eq!(diagnostic.code.code(), 401, "{diagnostic:?}"); - assert_eq!( - diagnostic.message, - "cannot rebind an owned value from a view; clone the value instead" - ); - } + assert_eq!(call.specialization.type_argument_kinds, ["alias_type"]); } #[test] - fn owned_rebinding_preserves_immutable_target_diagnostics() { - let errors = check_source_errors( - r#"namespace app -struct Record: - value: int64 -Record constant = comptime Record(value: 1) -function local(view source: Record) returns nothing: - Record fixed = Record(value: 1) - fixed = view source - constant = view source -"#, + fn inferred_never_witness_merging_preserves_known_alias_siblings() { + let source = r#"namespace app +type Count = int64 +function describe[T](first: T, second: T) returns string: + return type.name[T]() +function outer[E](view empty: list[E]) returns string: + result[Count, E] partial = ok(7) + result[int64, string] full = fail("bad") + return describe(partial, full) +function main() returns string: + return outer(list()) +"#; + let result = check_source_result(source); + let errors = result + .diagnostics + .iter() + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{errors:?}"); + let instance = result + .generic_function_instantiations + .iter() + .find(|instance| { + instance.specialization.type_argument_reflections[0].type_name + == "result[app.Count, string]" + }) + .expect("filled result with preserved Count witness"); + assert_eq!(instance.specialization.type_argument_kinds, ["result_type"]); + let Type::Result(ok, error) = result.interner.resolve(instance.concrete_args[0]) else { + panic!("expected result"); + }; + assert_eq!((*ok, *error), (TypeInterner::INT64, TypeInterner::STRING)); + } + + #[test] + fn inferred_alias_kind_and_info_select_the_same_reflection_branch() { + let source = r#"namespace app +type Count = int64 +function inspect[T](input: list[T]) returns string: + if type.kind_tag[T]() == TypeKind.alias_type: + return type.name[T]() + else: + return "primitive" +function main() returns string: + list[Count] values = list(7) + return inspect(values) +"#; + let result = check_source_result(source); + let errors = result + .diagnostics + .iter() + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{errors:?}"); + let instance = result.generic_function_instantiations.first().unwrap(); + assert_eq!(instance.specialization.type_argument_kinds, ["alias_type"]); + assert_eq!( + instance.specialization.type_argument_reflections[0].type_name, + "app.Count" ); - assert_eq!(errors.len(), 2, "{errors:?}"); assert!( - errors - .iter() - .all(|diagnostic| diagnostic.code.code() == 404) + instance + .static_selections + .values() + .any(|selection| *selection == CheckedStaticSelection::IfThen) ); } +} - #[test] - fn written_view_return_annotations_reject_headers_and_callable_results() { - for (source, count) in [ - ( - "function copy(view values: list[int64]) returns view list[int64]:\n return clone values\n", - 1, - ), - ( - "function unused[T](value: T) returns view T:\n return value\n", - 1, - ), - ( - "mutual:\n function value(number: int64) returns view int64\nfunction value(number: int64) returns view int64:\n return number\n", - 2, - ), - ( - "struct Item:\n value: int64\n function read(view self: Item) returns view int64:\n return self.value\n", - 1, - ), - ( - "interface Reader:\n function read(view self: Reader) returns view int64\nstruct Item:\n value: int64\nimplement Reader for Item:\n function read(view self: Item) returns view int64:\n return self.value\n", - 2, - ), - ( - "function callback() returns nothing:\n function() returns int64 value = function() returns view int64:\n return 1\n", - 1, - ), - ( - "function callback() returns nothing:\n mutable function() returns view int64 selected = function() returns int64:\n return 1\n", - 1, - ), - ( - "function unused[T](value: T, callback: function() returns view T) returns nothing:\n return nothing\n", - 1, - ), - ( - "struct Unused[T]:\n callback: function() returns view T\n", - 1, - ), - ( - "function factory() returns function() returns view int64:\n return function() returns int64:\n return 1\n", - 1, - ), - ] { - let errors = check_source_errors(source); - assert_eq!(errors.len(), count, "{source}\n{errors:?}"); - for error in errors { - assert_eq!(error.code.code(), 401, "{error:?}"); - assert_eq!( - error.message, - "function return type cannot be a view; return an owned value instead" - ); - assert!( - source[error.span.start as usize..error.span.end as usize].starts_with("view ") - ); - } - } - } +#[cfg(test)] +mod resource_hook_span_tests { + use super::*; + use jett_common::{STDLIB_FILE_ID_START, SourceOrigin}; + use jett_resolve::{ResourceKernelSpec, resolve_with_resource_kernels}; + use jett_types::ResourceKernelRecipe; #[test] - fn written_view_return_annotations_are_reported_once_per_source_span() { - let errors = check_source_errors( - r#"function identity[T](value: T) returns view T: - return value -function main() returns nothing: - int64 first = identity[int64](1) - int64 second = identity[int64](2) - string third = identity[string]("three") -"#, + fn synthetic_resource_functions_do_not_replace_source_declaration_fallback() { + let file = FileId::new(STDLIB_FILE_ID_START); + let parsed = jett_parser::parse("namespace resource_probe\nresource TestHandle\n", file); + assert!(parsed.errors.is_empty()); + let Item::Resource(resource) = &parsed.module.items[1] else { + panic!("missing resource") + }; + let specs = [ResourceKernelSpec { + resource_declaration: resource.name.span, + member: "kernel_close".into(), + recipe: ResourceKernelRecipe::Finalize, + }]; + let resolved = resolve_with_resource_kernels( + &parsed.module, + &HashMap::from([(file, SourceOrigin::Stdlib)]), + &specs, + ) + .unwrap(); + let resource_definition = resolved.resolutions[&resource.name.span]; + let kernel_definition = resolved.resource_kernels.iter().next().unwrap().definition; + assert_eq!( + resolved.scope_table.def(resource_definition).span, + resolved.scope_table.def(kernel_definition).span ); - assert_eq!(errors.len(), 1, "{errors:?}"); - assert!(errors[0].message.contains("return type cannot be a view")); - - let errors = check_source_errors( - "function escape(view values: list[int64]) returns view list[int64]:\n return values\n", + let checker = TypeChecker::new(&resolved, CheckOptions::default()); + assert_eq!( + checker.decl_defs.get(&resource.name.span), + Some(&resource_definition) ); - assert_eq!(errors.len(), 2, "{errors:?}"); - assert!(errors[0].message.contains("return type cannot be a view")); - assert!(errors[1].message.contains("cannot return a view")); - assert_ne!(errors[0].span, errors[1].span); } +} - #[test] - fn owned_returns_reject_known_views_and_forwarded_aliases() { - for (parameter, returned) in [ - ("view values: list[int64]", "values"), - ("values: view list[int64]", "values"), - ("view values: list[int64]", "((values))"), - ("view values: list[int64]", "view values"), - ("view values: list[int64]", "(view values)"), - ("view values: list[int64]", "forwarded"), - ] { - let source = format!( - "function escape({parameter}) returns list[int64]:\n list[int64] borrowed = view values\n list[int64] forwarded = borrowed\n return {returned}\n" - ); - let errors = check_source_errors(&source); - assert_eq!(errors.len(), 1, "{source}\n{errors:?}"); - assert_eq!(errors[0].code.code(), 401, "{errors:?}"); - assert_eq!( - errors[0].message, - "cannot return a view; clone the value or take ownership instead" - ); - assert_eq!( - &source[errors[0].span.start as usize..errors[0].span.end as usize], - returned - ); - } +#[cfg(test)] +mod caller_ownership_source_tests { + use super::*; + use jett_common::STDLIB_FILE_ID_START; + + fn checked(source: &str, release: bool, stdlib: bool) -> CheckResult { + let parsed = jett_parser::parse( + source, + FileId::new(if stdlib { STDLIB_FILE_ID_START } else { 0 }), + ); + assert!(parsed.errors.is_empty(), "parse: {:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "resolve: {:?}", + resolved.diagnostics + ); + check_with_options(&parsed.module, &resolved, CheckOptions { release }) } - #[test] - fn owned_returns_check_inline_and_instantiated_generic_bodies() { - for source in [ - r#"function make() returns function(view list[int64]) returns list[int64]: - return function(view values: list[int64]) returns list[int64]: - return values -"#, - r#"function escape[T](view value: T) returns T: - return value -function main() returns nothing: - list[int64] source = list(7) - list[int64] escaped = escape[list[int64]](view source) -"#, - ] { - let errors = check_source_errors(source); - assert_eq!(errors.len(), 1, "{source}\n{errors:?}"); - assert_eq!(errors[0].code.code(), 401, "{errors:?}"); - assert!(errors[0].message.contains("cannot return a view")); - } + fn errors(checked: &CheckResult) -> Vec<&Diagnostic> { + checked + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect() } + const OBSERVER: &str = + "namespace app\nfunction inspect(view values: list[int64]) returns int64:\n return 7\n"; + #[test] - fn owned_call_boundaries_reject_views_across_call_forms_once() { - let prefix = r#"function consume(values: list[int64]) returns nothing: - return nothing -function generic[T](values: T) returns nothing: - return nothing -"#; - for call in [ - "consume(values)", - "consume((values))", - "consume(forwarded)", - "(consume)(forwarded)", - "generic[list[int64]](forwarded)", - "generic(forwarded)", - "callback(forwarded)", + fn caller_facts_bare_view_formals_consume_exact_source_bindings() { + for invocation in [ + "inspect(values)", + "app.inspect(values)", + "(inspect)(values)", ] { let source = format!( - "{prefix}function reject(view values: list[int64], callback: function(list[int64]) returns nothing) returns nothing:\n list[int64] borrowed = view values\n list[int64] forwarded = borrowed\n {call}\n" - ); - let errors = check_source_errors(&source); - assert_eq!(errors.len(), 1, "{call}: {errors:?}"); - assert_eq!(errors[0].code.code(), 401, "{call}: {errors:?}"); - assert_eq!( - errors[0].message, - "cannot pass a view to an owned parameter; clone the value or take ownership instead" + "{OBSERVER}function test(values: list[int64]) returns list[int64]:\n int64 number = {invocation}\n return values\n" ); + for release in [false, true] { + let result = checked(&source, release, false); + let failures = errors(&result); + assert_eq!(failures.len(), 1, "{invocation}: {failures:?}"); + assert_eq!(failures[0].code.code(), 400); + let packet = result + .call_ownership + .values() + .next() + .expect("checked source call"); + let [argument] = packet.arguments.as_slice() else { + panic!("one source operand"); + }; + assert_eq!(argument.syntax, CheckedCallerSyntax::Bare); + assert_eq!(argument.callee_access, CheckedCalleeAccess::View); + assert_eq!(argument.effect, CheckedCallerEffect::RelinquishOwned); + let CheckedCallerOrigin::Binding(binding) = &argument.origin else { + panic!("exact binding origin"); + }; + assert_eq!(binding.ty, argument.actual_type); + assert_eq!(binding.mode, CheckedBindingMode::Owned); + assert!( + result + .binding_facts + .values() + .any(|fact| fact.definition == binding.definition + && fact.declaration_span == binding.declaration_span) + ); + } } - - let errors = check_source_errors( - r#"struct Item: - value: int64 - function consume(self: Item) returns nothing: - return nothing -function reject(view source: Item) returns nothing: - Item borrowed = source - Item.consume(borrowed) -"#, - ); - assert_eq!(errors.len(), 1, "{errors:?}"); - assert_eq!(errors[0].code.code(), 401, "{errors:?}"); - assert!(errors[0].message.contains("owned parameter")); } #[test] - fn owned_view_call_diagnostics_do_not_hide_other_view_errors() { - let errors = check_source_errors( - r#"struct Record: - value: int64 -function consume(item: Record) returns nothing: - return nothing -function reject(view item: Record) returns nothing: - consume(item) - item.value = 2 -"#, + fn caller_facts_written_views_retain_owners_but_cannot_supply_owned_formals() { + let source = format!( + "{OBSERVER}function test(values: list[int64]) returns list[int64]:\n int64 number = inspect(view values)\n return values\n" ); - assert_eq!(errors.len(), 2, "{errors:?}"); - assert!(errors[0].message.contains("owned parameter")); - assert!(errors[1].message.contains("views are read-only")); - assert!(errors.iter().all(|error| error.code.code() == 401)); + for release in [false, true] { + let result = checked(&source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let argument = &result.call_ownership.values().next().unwrap().arguments[0]; + assert_eq!(argument.syntax, CheckedCallerSyntax::WrittenView); + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + let result = checked( + "function consume(values: list[int64]) returns nothing:\n return nothing\nfunction test(view values: list[int64]) returns nothing:\n consume(view values)\n", + release, + false, + ); + let failures = errors(&result); + assert_eq!(failures.len(), 1, "{failures:?}"); + assert_eq!(failures[0].code.code(), 375); + } } #[test] - fn owned_pipeline_boundaries_check_initial_values_and_extra_arguments() { - let prefix = r#"function consume(values: list[int64]) returns nothing: - return nothing -function second(number: int64, values: list[int64]) returns nothing: - return nothing -function generic[T](values: T) returns nothing: - return nothing -function generic_second[T](number: int64, values: T) returns nothing: - return nothing -"#; - for call in [ - "values into consume()", - "borrowed into consume()", - "borrowed into generic[list[int64]]()", - "borrowed into generic()", - "borrowed into callback()", - "1 into second(borrowed)", - "1 into generic_second[list[int64]](borrowed)", - "1 into generic_second(borrowed)", + fn caller_facts_known_borrowed_bare_arguments_have_one_precise_error() { + for body in [ + " int64 number = inspect(values)\n", + " list[int64] borrowed = view values\n list[int64] forwarded = borrowed\n int64 number = inspect(forwarded)\n", ] { let source = format!( - "{prefix}function reject(view values: list[int64], callback: function(list[int64]) returns nothing) returns nothing:\n list[int64] borrowed = view values\n {call}\n" + "{OBSERVER}function test(view values: list[int64]) returns nothing:\n{body} return nothing\n" ); - let errors = check_source_errors(&source); - assert_eq!(errors.len(), 1, "{call}: {errors:?}"); - assert_eq!(errors[0].code.code(), 401, "{call}: {errors:?}"); - assert!(errors[0].message.contains("owned parameter")); - let reported = &source[errors[0].span.start as usize..errors[0].span.end as usize]; - assert!(matches!(reported, "values" | "borrowed"), "{reported}"); + for release in [false, true] { + let result = checked(&source, release, false); + let failures = errors(&result); + assert_eq!(failures.len(), 1, "{failures:?}"); + assert_eq!(failures[0].code.code(), 401); + assert!(failures[0].message.contains("borrowed value bare")); + assert!(result.binding_facts.values().any(|fact| matches!( + fact.mode, + CheckedBindingMode::View { + source: CheckedViewSource::Other + } + ))); + } } } #[test] - fn owned_view_boundaries_preserve_explicit_view_call_diagnostics() { - for call in [ - "consume(view values)", - "consume((view values))", - "generic[list[int64]](view values)", - "generic(view values)", - "callback(view values)", - "values into view consume()", - "(view values) into consume()", - "values into view generic[list[int64]]()", - "values into view generic()", - ] { - let source = format!( - "function consume(values: list[int64]) returns nothing:\n return nothing\nfunction generic[T](values: T) returns nothing:\n return nothing\nfunction reject(view values: list[int64], callback: function(list[int64]) returns nothing) returns nothing:\n {call}\n" + fn caller_facts_named_calls_join_formals_without_reordering_sources() { + let source = "function choose(view first: list[int64], view second: list[int64]) returns int64:\n return 1\nfunction test(left: list[int64], right: list[int64]) returns int64:\n return choose(second: right, first: left)\n"; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let packet = result.call_ownership.values().next().unwrap(); + assert_eq!( + packet + .arguments + .iter() + .map(|argument| (argument.source_index, argument.parameter_index)) + .collect::>(), + [(0, 1), (1, 0)] + ); + assert!( + packet + .arguments + .iter() + .all(|argument| argument.effect == CheckedCallerEffect::RelinquishOwned) + ); + let source_text = packet + .arguments + .iter() + .map(|argument| { + &source[argument.source_span.start as usize..argument.source_span.end as usize] + }) + .collect::>(); + assert!( + source_text[0].contains("right") && source_text[1].contains("left"), + "{source_text:?}" ); - let errors = check_source_errors(&source); - assert_eq!(errors.len(), 1, "{call}: {errors:?}"); - assert_eq!(errors[0].code.code(), 375, "{call}: {errors:?}"); } } #[test] - fn owned_view_boundaries_follow_transparent_wrappers_but_not_type_errors() { - let prefix = r#"type Numbers = list[int64] where true -function consume(values: list[int64]) returns nothing: - return nothing -"#; - for (parameter, value) in [ - ("view values: Numbers", "coarsen values"), - ("view values: Numbers", "coarsen (view values)"), - ("view values: secret[list[int64]]", "declassify values"), - ( - "view values: secret[list[int64]]", - "declassify (view values)", - ), - ] { - for body in [ - format!(" return {value}\n"), - format!(" consume({value})\n return list()\n"), - ] { - let source = - format!("{prefix}function reject({parameter}) returns list[int64]:\n{body}"); - let errors = check_source_errors(&source); - assert_eq!(errors.len(), 1, "{source}\n{errors:?}"); - assert_eq!(errors[0].code.code(), 401, "{errors:?}"); - } - } - for body in [ - " return declassify values\n", - " consume(declassify values)\n return list()\n", + fn caller_facts_pipeline_source_zero_keeps_its_written_view_witness() { + for (step, expected) in [ + ("into inspect()", CheckedCallerEffect::RelinquishOwned), + ("into view inspect()", CheckedCallerEffect::RetainBorrow), ] { let source = format!( - "{prefix}function invalid(view values: list[int64]) returns list[int64]:\n{body}" + "{OBSERVER}function test(values: list[int64]) returns int64:\n return values {step}\n" ); - let errors = check_source_errors(&source); - assert_eq!(errors.len(), 1, "{source}\n{errors:?}"); - assert!(errors[0].message.contains("declassify"), "{errors:?}"); - assert_ne!(errors[0].code.code(), 401, "{errors:?}"); - } - } - - #[test] - fn owned_view_boundaries_apply_to_actor_verify_and_property_bodies() { - let errors = check_source_errors( - r#"namespace app -function consume(values: list[int64]) returns int64: - return 1 -actor Store: - receive borrowed(view values: list[int64]): - list[int64] alias = view values - int64 count = consume(alias) - trace count -verify borrowed_alias: - list[int64] source = list(7) - list[int64] alias = view source - list[int64] forwarded = alias - assert consume(forwarded) == 1 -property borrowed_property_alias: - given source: list[int64] - list[int64] alias = view source - list[int64] forwarded = alias - assert consume(forwarded) == 1 -"#, - ); - assert_eq!(errors.len(), 3, "{errors:?}"); - for error in errors { - assert_eq!(error.code.code(), 401, "{error:?}"); - assert!(error.message.contains("owned parameter")); + for release in [false, true] { + let result = checked(&source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let packet = result.call_ownership.values().next().unwrap(); + assert_eq!(packet.arguments[0].source_index, 0); + assert_eq!(packet.arguments[0].parameter_index, 0); + assert_eq!(packet.arguments[0].effect, expected); + assert_eq!( + packet.arguments[0].syntax == CheckedCallerSyntax::WrittenView, + expected == CheckedCallerEffect::RetainBorrow + ); + } } } #[test] - fn owned_view_boundaries_preserve_copies_clones_fields_and_call_results() { - let errors = check_source_errors( - r#"struct Record: - values: list[int64] -function consume(values: list[int64]) returns nothing: - return nothing -function copy(view values: list[int64]) returns list[int64]: - list[int64] borrowed = view values - list[int64] forwarded = borrowed - return clone forwarded -function scalar(view value: int64) returns int64: - int64 copied = value - return copied -function text(view value: string) returns string: - return value -function field(view record: Record) returns list[int64]: - return record.values -function copies(view values: list[int64], view record: Record) returns nothing: - consume(clone values) - consume(clone (view values)) - consume(record.values) - consume(copy(view values)) - values into view copy() into consume() - if true: - list[int64] alias = view values - consume(clone alias) - list[int64] alias = list(7) - consume(alias) -"#, - ); - assert!(errors.is_empty(), "{errors:?}"); + fn caller_facts_generic_bodies_keep_concrete_types_and_lexical_contexts() { + let source = "function terminal[T](view value: T) returns int64:\n return 7\nfunction inspect[T](view value: T) returns int64:\n return terminal[T](view value)\nfunction ordinary(values: list[int64]) returns int64:\n int64 scalar = inspect[int64](3)\n return inspect[list[int64]](values)\nverify observations:\n list[int64] values = list(7)\n assert inspect[list[int64]](values) == 7\n assert inspect[list[int64]](values) == 7\n"; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let observed = result + .call_ownership + .values() + .filter(|packet| packet.context == CheckedOwnershipContext::Verify) + .collect::>(); + assert_eq!(observed.len(), 2); + assert!(observed.iter().all(|packet| packet.arguments[0].effect + == CheckedCallerEffect::ObserveData + && packet.arguments[0].syntax == CheckedCallerSyntax::Bare)); + let inner = result + .generic_function_instantiations + .iter() + .flat_map(|body| body.call_ownership.values()) + .collect::>(); + assert_eq!(inner.len(), 2); + assert!( + inner + .iter() + .all(|packet| packet.context == CheckedOwnershipContext::Ordinary) + ); + assert!(inner.iter().any(|packet| packet.arguments[0].effect + == CheckedCallerEffect::Copy + && packet.arguments[0].actual_type == TypeInterner::INT64)); + assert!(inner.iter().any(|packet| packet.arguments[0].effect + == CheckedCallerEffect::RetainBorrow + && matches!( + result.interner.resolve(packet.arguments[0].actual_type), + Type::List(_) + ))); + } } #[test] - fn owned_view_boundaries_preserve_copyable_transformation_results() { - let errors = check_source_errors( - r#"type Count = int64 where true -function consume(value: int64) returns nothing: - return nothing -function refined(view value: Count) returns int64: - return coarsen value -function classified(view value: secret[int64]) returns int64: - return declassify value -function copies(view count: Count, view hidden: secret[int64]) returns nothing: - consume(coarsen count) - consume(declassify hidden) -"#, - ); - assert!(errors.is_empty(), "{errors:?}"); + fn caller_facts_reflected_body_packets_do_not_flatten_field_types() { + let source = "struct Sample:\n count: int64\n values: list[int64]\nfunction probe[T](view value: T) returns nothing:\n return nothing\nfunction inspect[T](view value: T) returns nothing:\n for field in type.fields[T]():\n comptime type Field = field.type_info:\n Field original = type.field_value[T, Field](view value, view field)\n probe[Field](view original)\n return nothing\nfunction main() returns nothing:\n Sample sample = Sample(count: 1, values: list(2))\n inspect[Sample](view sample)\n"; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let bodies = result + .generic_function_instantiations + .iter() + .flat_map(|body| body.comptime_type_bindings.values()) + .flatten() + .collect::>(); + assert_eq!(bodies.len(), 2); + let arguments = bodies + .iter() + .flat_map(|body| body.body.call_ownership.values()) + .filter(|packet| matches!(packet.target, CheckedInvocationTarget::Generic(_))) + .map(|packet| &packet.arguments[0]) + .collect::>(); + assert_eq!(arguments.len(), 2); + assert!( + arguments + .iter() + .any(|argument| argument.actual_type == TypeInterner::INT64 + && argument.effect == CheckedCallerEffect::Copy) + ); + assert!(arguments.iter().any(|argument| matches!( + result.interner.resolve(argument.actual_type), + Type::List(_) + ) && argument.effect + == CheckedCallerEffect::RetainBorrow)); + assert!( + bodies + .iter() + .all(|body| !body.body.binding_facts.is_empty()) + ); + } + } - let errors = check_source_errors( - "function explicit(view value: int64) returns int64:\n return view value\n", - ); - assert_eq!(errors.len(), 1, "{errors:?}"); - assert!(errors[0].message.contains("cannot return a view")); + #[test] + fn caller_facts_resource_parameters_use_same_source_rule_without_runtime_authority() { + let prefix = "namespace audit\nresource Token\nfunction observe(view token: Token) returns int64:\n return 1\n"; + for release in [false, true] { + let result = checked( + &format!( + "{prefix}function last(token: Token) returns int64:\n return observe(token)\n" + ), + release, + true, + ); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + assert!(result.resource_hooks.is_empty()); + let packet = result.call_ownership.values().next().unwrap(); + assert_eq!( + packet.arguments[0].effect, + CheckedCallerEffect::RelinquishOwned + ); + let result = checked( + &format!( + "{prefix}function reject(token: Token) returns Token:\n int64 number = observe(token)\n return token\n" + ), + release, + true, + ); + let failures = errors(&result); + assert_eq!(failures.len(), 1, "{failures:?}"); + assert_eq!(failures[0].code.code(), 400); + let result = checked( + &format!( + "{prefix}function retain(token: Token) returns Token:\n int64 number = observe(view token)\n return token\n" + ), + release, + true, + ); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + } } #[test] - fn handle_default_is_rechecked_with_its_concrete_success_type() { - let source = r#"function fallback(value: optional[list[int64]]) returns list[int64]: - return value handle: - default list() -"#; - let result = check_source_result(source); + fn caller_facts_closed_variadic_print_records_each_source_occurrence() { + let source = "function sample(values: list[int64]) returns list[int64]:\n println(1, view values, \"tail\")\n return values\n"; + let result = checked(source, false, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let packet = result.call_ownership.values().next().unwrap(); + assert_eq!( + packet.target, + CheckedInvocationTarget::Intrinsic(IntrinsicId::Println) + ); + let CheckedInvocationShape::Intrinsic { operands, .. } = &packet.shape else { + panic!("closed variadic shape"); + }; + assert_eq!(operands.len(), 3); assert!( - result - .diagnostics + operands .iter() - .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), - "unexpected diagnostics: {:?}", - result.diagnostics + .all(|role| matches!(role, CheckedIntrinsicOperandRole::PrintArgument { .. })) ); - let start = source.rfind("list()").expect("default list") as u32; - let span = Span::new(FileId::new(0), start, start + 6); - let default_ty = *result - .type_map - .get(&span) - .expect("default list should retain its contextual type"); assert_eq!( - result.interner.resolve(default_ty), - &Type::List(TypeInterner::INT64) + packet + .arguments + .iter() + .map(|argument| argument.effect) + .collect::>(), + [ + CheckedCallerEffect::Copy, + CheckedCallerEffect::RetainBorrow, + CheckedCallerEffect::Copy + ] ); } - #[test] - fn never_assignment_compatibility_is_directional_through_value_shapes() { - let resolve = TestEnv::new().into_resolve_result(); - let mut checker = TypeChecker::new(&resolve, CheckOptions::default()); - let absent = [ - Type::Never, - Type::List(TypeInterner::NEVER), - Type::Set(TypeInterner::NEVER), - Type::Optional(TypeInterner::NEVER), - Type::Secret(TypeInterner::NEVER), - Type::Map(TypeInterner::STRING, TypeInterner::NEVER), - Type::Result(TypeInterner::NEVER, TypeInterner::STRING), - Type::Result(TypeInterner::STRING, TypeInterner::NEVER), - ]; - let inhabited = [ - Type::Int64, - Type::List(TypeInterner::INT64), - Type::Set(TypeInterner::INT64), - Type::Optional(TypeInterner::INT64), - Type::Secret(TypeInterner::INT64), - Type::Map(TypeInterner::STRING, TypeInterner::INT64), - Type::Result(TypeInterner::INT64, TypeInterner::STRING), - Type::Result(TypeInterner::STRING, TypeInterner::INT64), - ]; - for (absent, inhabited) in absent.into_iter().zip(inhabited) { - let absent = checker.interner.intern(absent); - let inhabited = checker.interner.intern(inhabited); - assert!(!checker.types_compatible(absent, inhabited)); - assert!(checker.types_compatible(inhabited, absent)); + fn caller_facts_owned_mode_preserves_run_pending_initialization() { + let source = "function main() returns nothing:\n int64 task = run 7\n cancel task\n return nothing\n"; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + } + } + #[test] + fn caller_facts_observation_context_does_not_supply_owned_intrinsic_access() { + let source = "verify invalid_source_access:\n int64 count = 3\n list[int64] generated = range(view count)\n assert true\n"; + for release in [false, true] { + let result = checked(source, release, false); + let failures = errors(&result); + assert_eq!(failures.len(), 1, "{failures:?}"); + assert_eq!(failures[0].code.code(), 375); } - let bottom_input = checker.interner.intern(Type::Function { - params: vec![TypeInterner::NEVER], - view_params: vec![false], - return_type: TypeInterner::INT64, - }); - let concrete = checker.interner.intern(Type::Function { - params: vec![TypeInterner::INT64], - view_params: vec![false], - return_type: TypeInterner::INT64, - }); - assert!(checker.types_compatible(bottom_input, concrete)); - assert!(!checker.types_compatible(concrete, bottom_input)); - assert_eq!( - checker.merge_inferred_never(bottom_input, concrete, false), - Some(bottom_input) - ); - assert_eq!( - checker.merge_inferred_never(TypeInterner::INT8, TypeInterner::INT64, false), - None - ); - let secret = checker.interner.intern(Type::Secret(TypeInterner::NEVER)); - assert_eq!( - checker.merge_inferred_never(secret, TypeInterner::INT64, false), - None - ); } #[test] - fn generic_never_places_reject_concrete_payloads_at_every_construction_boundary() { - let cases = [ - ("list[T]", " T item = 7\n return list(item)", 311), - ( - "list[T]", - " T item = none handle:\n default 7\n return list(item)", - 300, - ), - ( - "list[T]", - " mutable T item = none handle: return list()\n item = 7\n return list(item)", - 312, - ), - ("T", " return 7", 305), - ("list[T]", " return list(7)", 300), - ("list[T]", " return append[T](list(), 7)", 300), - ("optional[T]", " return some(7)", 300), - ("result[T, string]", " return ok(7)", 300), - ("result[string, T]", " return fail(7)", 300), - ("map[string, T]", " return map(\"key\": 7)", 300), - ( - "nothing", - " for item in items:\n T forged = 7\n trace forged\n return nothing", - 311, - ), - ]; - for (result_type, body, expected_code) in cases { - for comptime in ["", "comptime "] { - let source = format!( - "function append[T](values: list[T], value: T) returns list[T]:\n return list(value)\nfunction manufacture[T](view items: list[T]) returns {result_type}:\n{body}\nfunction main() returns nothing:\n println({comptime}manufacture(list()))\n return nothing\n" - ); - let errors = check_source_errors(&source); - assert!( - errors - .iter() - .any(|error| error.code.code() == expected_code), - "{source}\n{errors:?}" - ); - assert!( - errors.iter().any(|error| error.message.contains("")), - "{source}\n{errors:?}" - ); + fn caller_facts_scoped_calls_keep_only_exact_referenced_capture_roots() { + let source = r#"namespace app +struct Sample: + count: int64 + values: list[int64] +function probe[T](view value: T) returns nothing: + return nothing +function probe_count(value: int64) returns int64: + return value +function inspect[T](view value: T, size: int64, spare: int64) returns nothing: + for field in type.fields[T](): + comptime type Field = field.type_info: + probe[T](view value) + probe[list[int64]](view value.values) + int64 count = probe_count(value.count) + function() returns int64 read = function() returns int64: + return probe_count(size) + int64 captured = read() + return nothing +function main() returns nothing: + Sample sample = Sample(count: 1, values: list(2)) + inspect[Sample](view sample, 3, 4) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let bodies = result + .generic_function_instantiations + .iter() + .flat_map(|body| body.comptime_type_bindings.values()) + .flatten() + .collect::>(); + assert_eq!(bodies.len(), 2); + for body in bodies { + let facts = &body.body.binding_facts; + assert!(facts.values().any(|fact| &source + [fact.declaration_span.start as usize..fact.declaration_span.end as usize] + == "size")); + assert!(!facts.values().any(|fact| &source + [fact.declaration_span.start as usize..fact.declaration_span.end as usize] + == "spare")); + let mut direct = false; + let mut projected = false; + let mut copied = false; + for packet in body.body.call_ownership.values() { + for argument in &packet.arguments { + match &argument.origin { + CheckedCallerOrigin::Binding(fact) => { + assert_eq!(facts.get(&fact.declaration_span), Some(fact)); + direct = true; + } + CheckedCallerOrigin::BorrowedProjection { + source: CheckedViewSource::Binding(definition), + } => { + assert!(facts.values().any(|fact| fact.definition == *definition)); + projected = true; + } + CheckedCallerOrigin::OwnedFieldCopy { + parent: CheckedViewSource::Binding(definition), + } => { + assert!(facts.values().any(|fact| fact.definition == *definition)); + copied = true; + } + _ => {} + } + } + } + assert!(direct && projected && copied); } } } #[test] - fn inferred_literals_fill_only_never_slots_in_both_element_orders() { - let cases = [ - ("list(none, some(7))", "list[optional[int64]]"), - ("list(some(7), none)", "list[optional[int64]]"), - ("list(ok(7), fail(\"bad\"))", "list[result[int64, string]]"), - ("list(fail(\"bad\"), ok(7))", "list[result[int64, string]]"), - ("list(list(), list(7))", "list[list[int64]]"), - ("list(list(7), list())", "list[list[int64]]"), - ( - "list(some(list()), some(list(7)))", - "list[optional[list[int64]]]", - ), - ( - "list(some(list(7)), some(list()))", - "list[optional[list[int64]]]", - ), - ( - "map(\"a\": none, \"b\": some(7))", - "map[string, optional[int64]]", - ), - ( - "map(\"a\": some(7), \"b\": none)", - "map[string, optional[int64]]", - ), - ("list(map(), map(\"key\": 7))", "list[map[string, int64]]"), - ("list(map(\"key\": 7), map())", "list[map[string, int64]]"), - ("list(none, some(small))", "list[optional[int8]]"), - ("list(some(small), none)", "list[optional[int8]]"), - ]; - for (literal, expected) in cases { - let map_binding = if literal.starts_with("map(") { - "key, value" - } else { - "value" + fn caller_facts_named_descriptors_and_baked_constants_are_producers() { + let source = r#"namespace app +int64 answer = 7 +function identity(value: int64) returns int64: + return value +function accept(callback: function(int64) returns int64, count: int64) returns nothing: + return nothing +function main() returns nothing: + accept(identity, answer) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let packet = result + .call_ownership + .values() + .next() + .expect("one source call"); + assert_eq!(packet.arguments.len(), 2); + assert!( + packet + .arguments + .iter() + .all(|argument| argument.origin == CheckedCallerOrigin::OwnedExpression) + ); + assert_eq!( + packet.arguments[0].effect, + CheckedCallerEffect::TransferOwned + ); + assert_eq!(packet.arguments[1].effect, CheckedCallerEffect::Copy); + } + } + + #[test] + fn caller_facts_local_descriptors_keep_their_exact_binding_identity() { + let source = r#"namespace app +function identity(value: int64) returns int64: + return value +function inspect(view callback: function(int64) returns int64) returns nothing: + return nothing +function main() returns nothing: + function(int64) returns int64 stored = identity + inspect(view stored) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let argument = &result + .call_ownership + .values() + .next() + .expect("one source call") + .arguments[0]; + let CheckedCallerOrigin::Binding(fact) = &argument.origin else { + panic!("local descriptor binding"); }; - let source = format!( - "type Tiny = int8\nfunction main() returns nothing:\n Tiny small = 7\n for {map_binding} in {literal}:\n trace value\n return nothing\n" + assert_eq!( + &source[fact.declaration_span.start as usize..fact.declaration_span.end as usize], + "stored" ); - let result = check_source_result(&source); - let errors = result - .diagnostics - .iter() - .filter(|error| error.severity == jett_diagnostics::Severity::Error) - .collect::>(); - assert!(errors.is_empty(), "{source}\n{errors:?}"); - let start = source.find(literal).unwrap() as u32; - let span = sp(start, start + literal.len() as u32); - let ty = result.type_map[&span]; - assert_eq!(result.interner.type_name(ty), expected, "{literal}"); + assert_eq!(result.binding_facts.get(&fact.declaration_span), Some(fact)); + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); } } #[test] - fn contextual_absent_defaults_and_generic_returns_keep_concrete_success_types() { - let source = r#"function identity[T](values: list[T]) returns list[T]: - return values -function fallback[T](view items: list[T], value: T) returns T: - return none handle: - default value -function main() returns nothing: - int64 absent = none handle: - default 7 - int64 failure = fail("bad") handle error: - trace error - default 8 - list[int64] empty = identity(list()) - int64 generic = fallback(list(), 9) - int64 baked = comptime (none handle: default 10) - trace absent - trace failure - trace empty - trace generic - trace baked - return nothing + fn caller_facts_pipeline_sources_keep_each_preceding_output_span() { + let source = r#"namespace app +function identity(view values: list[int64]) returns list[int64]: + return clone values +function main(values: list[int64]) returns list[int64]: + return values into view identity() into identity() "#; - let errors = check_source_errors(source); - assert!(errors.is_empty(), "{errors:?}"); + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut calls = result.call_ownership.iter().collect::>(); + calls.sort_by_key(|(span, _)| span.start); + assert_eq!(calls.len(), 2); + assert_eq!( + &source[calls[0].1.arguments[0].source_span.start as usize + ..calls[0].1.arguments[0].source_span.end as usize], + "values" + ); + assert_eq!( + calls[0].1.arguments[0].effect, + CheckedCallerEffect::RetainBorrow + ); + assert_eq!(calls[1].1.arguments[0].source_span, *calls[0].0); + assert_eq!( + calls[1].1.arguments[0].origin, + CheckedCallerOrigin::OwnedExpression + ); + assert_eq!( + calls[1].1.arguments[0].effect, + CheckedCallerEffect::RelinquishOwned + ); + } } #[test] - fn inferred_callback_return_sums_use_a_concrete_common_signature() { - for callbacks in [ - "list(make_absent(list()), present)", - "list(present, make_absent(list()))", - ] { - let source = format!( - r#"function make_absent[T](view items: list[T]) returns function() returns optional[T]: - return function() returns optional[T]: return none -function present() returns optional[int64]: - return some(7) -function main() returns nothing: - for callback in {callbacks}: - optional[int64] value = callback() - trace value - return nothing -"# + fn caller_facts_interface_qualified_concrete_calls_use_the_selected_body_signature() { + let source = r#"namespace app +interface Scored: + function score(view self: Scored, bonus: int64) returns int64 +struct User: + points: int64 +implement Scored for User: + function score(view self: User, bonus: int64) returns int64: + return self.points + bonus +function main() returns int64: + User user = User(points: 7) + return Scored.score(bonus: 5, self: view user) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let packet = result + .call_ownership + .values() + .next() + .expect("selected method source call"); + let CheckedInvocationTarget::Method(call) = &packet.target else { + panic!("concrete source method"); + }; + let definition = result + .method_definitions + .iter() + .find(|definition| definition.source_span == call.source_span) + .unwrap(); + let CheckedInvocationShape::Function { signature_type } = &packet.shape else { + panic!("function shape"); + }; + let Type::Function { + params, + view_params, + return_type, + } = result.interner.resolve(*signature_type) + else { + panic!("function signature"); + }; + assert_eq!(params, &definition.parameter_types); + assert_eq!(*return_type, definition.return_type); + assert_eq!(view_params, &[true, false]); + assert_eq!(params[0], definition.owner_type); + assert!(matches!( + result.interner.resolve(params[0]), + Type::Struct(_) + )); + assert_eq!( + packet + .arguments + .iter() + .map(|argument| (argument.source_index, argument.parameter_index)) + .collect::>(), + [(0, 1), (1, 0)] ); - let result = check_source_result(&source); - let errors = result - .diagnostics + assert_eq!(packet.arguments[1].callee_access, CheckedCalleeAccess::View); + } + } + + #[test] + fn caller_facts_inherent_calls_keep_registered_source_formal_modes() { + let source = r#"namespace app +struct User: + points: int64 + function score(view self: User, view values: list[int64]) returns int64: + return self.points +function main() returns int64: + User user = User(points: 7) + list[int64] values = list(5) + return User.score(view user, values) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let packet = result + .call_ownership + .values() + .next() + .expect("selected inherent source call"); + let CheckedInvocationTarget::Method(call) = &packet.target else { + panic!("source method"); + }; + let definition = result + .method_definitions .iter() - .filter(|error| error.severity == jett_diagnostics::Severity::Error) - .collect::>(); - assert!(errors.is_empty(), "{source}\n{errors:?}"); - let start = source.find(callbacks).unwrap() as u32; - let ty = result.type_map[&sp(start, start + callbacks.len() as u32)]; + .find(|definition| definition.source_span == call.source_span) + .unwrap(); + let CheckedInvocationShape::Function { signature_type } = &packet.shape else { + panic!("function shape"); + }; + let Type::Function { + params, + view_params, + return_type, + } = result.interner.resolve(*signature_type) + else { + panic!("function signature"); + }; + assert_eq!(params, &definition.parameter_types); + assert_eq!(*return_type, definition.return_type); + assert_eq!(view_params, &[true, true]); assert_eq!( - result.interner.type_name(ty), - "list[function() returns optional[int64]]" + packet.arguments[0].effect, + CheckedCallerEffect::RetainBorrow + ); + assert_eq!( + packet.arguments[1].effect, + CheckedCallerEffect::RelinquishOwned ); } } #[test] - fn mapped_callback_inference_uses_joined_absent_and_present_elements() { - for values in ["list(none, some(7))", "list(some(7), none)"] { - let source = format!( - r#"function append[T](values: list[T], value: T) returns list[T]: - return list(value) -function mapped[A, B](values: list[A], callback: function(A) returns B) returns list[B]: - mutable list[B] output = list() - for value in values: - output = append[B](output, callback(value)) - return output -function unwrap(value: optional[int64]) returns int64: - return value handle: default 0 -function main() returns nothing: - for value in mapped({values}, unwrap): - trace value - return nothing -"# + fn caller_facts_erased_calls_keep_the_selected_interface_slot_signature() { + let source = r#"namespace app +interface Scored: + function score(view self: Scored, bonus: int64) returns int64 +struct User: + points: int64 +implement Scored for User: + function score(view self: User, bonus: int64) returns int64: + return self.points + bonus +function inspect(view item: Scored) returns int64: + return Scored.score(view item, 5) +function main() returns int64: + User user = User(points: 7) + return inspect(view user) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let packet = result + .call_ownership + .values() + .find(|packet| matches!(packet.target, CheckedInvocationTarget::Interface(_))) + .expect("selected interface source call"); + let CheckedInvocationTarget::Interface(call) = &packet.target else { + unreachable!(); + }; + let Type::Interface(id) = result.interner.resolve(call.interface_type) else { + panic!("interface owner"); + }; + let slot = &result.interner.resolve_interface(*id).methods[call.method_index]; + let CheckedInvocationShape::Function { signature_type } = &packet.shape else { + panic!("function shape"); + }; + let Type::Function { + params, + view_params, + return_type, + } = result.interner.resolve(*signature_type) + else { + panic!("function signature"); + }; + assert_eq!( + params, + &slot.params.iter().map(|(_, ty, _)| *ty).collect::>() ); - let result = check_source_result(&source); - let errors = result - .diagnostics + assert_eq!( + view_params, + &slot + .params + .iter() + .map(|(_, _, view)| *view) + .collect::>() + ); + assert_eq!(*return_type, slot.return_type); + assert_eq!(params[0], call.interface_type); + assert_eq!( + packet.arguments[0].effect, + CheckedCallerEffect::RetainBorrow + ); + } + } + + #[test] + fn caller_facts_source_actual_types_survive_binding_field_and_producer_promotion() { + let source = r#"namespace app +struct Holder: + values: list[int64] +function inspect(view values: secret[list[int64]]) returns nothing: + return nothing +function produce() returns list[int64]: + return list(7) +function main() returns nothing: + list[int64] values = list(7) + inspect(view values) + inspect(view produce()) + Holder holder = Holder(values: list(8)) + inspect(view holder.values) + secret[list[int64]] classified = clone values + inspect(view classified) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut calls = result + .call_ownership .iter() - .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .filter(|(_, packet)| packet.arguments.len() == 1) .collect::>(); - assert!(errors.is_empty(), "{source}\n{errors:?}"); - let call = format!("mapped({values}, unwrap)"); - let start = source.find(&call).unwrap() as u32; - let ty = result.type_map[&sp(start, start + call.len() as u32)]; - assert_eq!(result.interner.type_name(ty), "list[int64]"); + calls.sort_by_key(|(span, _)| span.start); + assert_eq!(calls.len(), 4); + for (index, (_, packet)) in calls.iter().enumerate() { + let argument = &packet.arguments[0]; + assert_eq!( + result.source_type_map.get(&argument.source_span), + Some(&argument.actual_type) + ); + assert_eq!( + result.type_map.get(&argument.source_span), + Some(&argument.parameter_type) + ); + assert!(matches!( + result.interner.resolve(argument.parameter_type), + Type::Secret(_) + )); + if index < 3 { + assert!(matches!( + result.interner.resolve(argument.actual_type), + Type::List(_) + )); + assert_ne!(argument.actual_type, argument.parameter_type); + } else { + assert_eq!(argument.actual_type, argument.parameter_type); + } + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + } + let CheckedCallerOrigin::Binding(binding) = calls[0].1.arguments[0].origin else { + panic!("ordinary binding"); + }; + assert_eq!(binding.ty, calls[0].1.arguments[0].actual_type); + assert_eq!( + calls[1].1.arguments[0].origin, + CheckedCallerOrigin::OwnedExpression + ); + assert!(matches!( + calls[2].1.arguments[0].origin, + CheckedCallerOrigin::BorrowedProjection { .. } + )); } } #[test] - fn filling_inferred_never_does_not_merge_existing_primitive_widths() { - let errors = check_source_errors( - r#"type Tiny = int8 -function main() returns nothing: - Tiny small = 7 - for value in list(none, some(small), some(7)): - trace value + fn caller_facts_source_actual_types_keep_contextual_width_and_container_inference() { + let source = r#"namespace app +function scalar(view value: secret[int8]) returns nothing: return nothing -"#, - ); - assert_eq!(errors.len(), 1, "{errors:?}"); - assert_eq!(errors[0].code.code(), 300); - assert!(errors[0].message.contains("optional[int8]")); - assert!(errors[0].message.contains("optional[int64]")); +function sequence(view values: secret[list[int8]]) returns nothing: + return nothing +function optional_value(view value: secret[optional[int8]]) returns nothing: + return nothing +function result_value(view value: secret[result[int8, string]]) returns nothing: + return nothing +function mapped(view values: secret[map[string, int8]]) returns nothing: + return nothing +function main() returns nothing: + scalar(7) + sequence(list()) + optional_value(some(7)) + optional_value(none) + result_value(ok(7)) + result_value(fail("error")) + mapped(map("one": 7)) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut calls = result.call_ownership.iter().collect::>(); + calls.sort_by_key(|(span, _)| span.start); + assert_eq!(calls.len(), 7); + let expected = [ + "scalar", + "sequence", + "optional_value", + "optional_value", + "result_value", + "result_value", + "mapped", + ]; + for ((_, packet), name) in calls.iter().zip(expected) { + let [argument] = packet.arguments.as_slice() else { + panic!("one source occurrence"); + }; + let Type::Secret(inner) = result.interner.resolve(argument.parameter_type) else { + panic!("outer Secret formal for {name}"); + }; + assert_eq!(argument.actual_type, *inner, "{name}"); + assert_eq!( + result.source_type_map.get(&argument.source_span), + Some(inner) + ); + assert_eq!( + result.type_map.get(&argument.source_span), + Some(&argument.parameter_type) + ); + } + assert_eq!(calls[0].1.arguments[0].actual_type, TypeInterner::INT8); + assert!( + matches!(result.interner.resolve(calls[1].1.arguments[0].actual_type), Type::List(inner) if *inner == TypeInterner::INT8) + ); + } } #[test] - fn reverse_literal_compatibility_cannot_hide_a_never_callable_input() { - for callbacks in ["list(broad, narrow(list()))", "list(narrow(list()), broad)"] { - let source = format!( - r#"function broad(value: optional[int64]) returns int64: - return 0 -function narrow[T](view items: list[T]) returns function(optional[T]) returns secret[int64]: - return function(value: optional[T]) returns secret[int64]: return 7 -function main() returns nothing: - for callback in {callbacks}: - trace callback + fn caller_facts_source_actual_types_are_concrete_in_generic_and_reflected_frames() { + let source = r#"namespace app +struct Sample: + count: int64 + values: list[int64] +function observe[T](view value: secret[T]) returns nothing: return nothing -"# - ); - let result = check_source_result(&source); - let errors = result - .diagnostics +function forward[T](view value: T) returns nothing: + observe[T](view value) +function reflected[T](view value: T) returns nothing: + for field in type.fields[T](): + comptime type Field = field.type_info: + Field original = type.field_value[T, Field](view value, view field) + observe[Field](view original) +function main() returns nothing: + list[int64] values = list(7) + forward[int64](1) + forward[list[int64]](view values) + Sample sample = Sample(count: 2, values: list(3)) + reflected[Sample](view sample) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let direct = result + .generic_function_instantiations .iter() - .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .filter(|body| body.comptime_type_bindings.is_empty()) + .flat_map(|body| { + body.call_ownership + .values() + .filter(|packet| { + matches!(packet.target, CheckedInvocationTarget::Generic(_)) + }) + .map(move |packet| (body, packet)) + }) .collect::>(); - if callbacks.starts_with("list(broad,") { - assert!( - errors.iter().any(|error| error.code.code() == 300), - "{source}\n{errors:?}" + // Reflected calls live only in the two exact scoped body snapshots. + assert_eq!(direct.len(), 2); + for (body, packet) in direct { + let argument = &packet.arguments[0]; + assert_eq!( + body.source_type_map.get(&argument.source_span), + Some(&argument.actual_type) ); - } else { - assert!(errors.is_empty(), "{source}\n{errors:?}"); - let start = source.find(callbacks).unwrap() as u32; - let ty = result.type_map[&sp(start, start + callbacks.len() as u32)]; assert_eq!( - result.interner.type_name(ty), - "list[function(optional[]) returns secret[int64]]" + body.type_map.get(&argument.source_span), + Some(&argument.parameter_type) + ); + assert!(!result.source_type_map.contains_key(&argument.source_span)); + } + let scoped = result + .generic_function_instantiations + .iter() + .flat_map(|body| body.comptime_type_bindings.values()) + .flatten() + .collect::>(); + assert_eq!(scoped.len(), 2); + let mut actuals = Vec::new(); + for scope in scoped { + let packet = scope + .body + .call_ownership + .values() + .find(|packet| matches!(packet.target, CheckedInvocationTarget::Generic(_))) + .unwrap(); + let argument = &packet.arguments[0]; + assert_eq!( + scope.body.source_type_map.get(&argument.source_span), + Some(&argument.actual_type) + ); + assert_eq!( + scope.body.type_map.get(&argument.source_span), + Some(&argument.parameter_type) ); + actuals.push(argument.actual_type); } + assert!(actuals.contains(&TypeInterner::INT64)); + assert!( + actuals + .iter() + .any(|ty| matches!(result.interner.resolve(*ty), Type::List(_))) + ); } } #[test] - fn repeated_generic_arguments_fill_nested_never_slots_in_both_orders() { - for (first, second, expected) in [ - ("none", "some(7)", "optional[int64]"), - ("ok(7)", "fail(\"bad\")", "result[int64, string]"), - ("list()", "list(\"ready\")", "list[string]"), - ("map()", "map(\"key\": 7)", "map[string, int64]"), - ( - "some(list())", - "some(list(\"ready\"))", - "optional[list[string]]", - ), - ] { - for (first, second) in [(first, second), (second, first)] { - let call = format!("second({first}, {second})"); - let source = format!( - "function second[T](first: T, last: T) returns T:\n return last\nfunction main() returns nothing:\n println({call})\n return nothing\n" + fn caller_facts_source_actual_types_preserve_secret_children_and_binary_taint() { + let source = r#"namespace app +function sequence(view values: secret[list[secret[int8]]]) returns nothing: + return nothing +function optional_value(view value: secret[optional[secret[int8]]]) returns nothing: + return nothing +function result_value(view value: secret[result[secret[int8], string]]) returns nothing: + return nothing +function scalar(view value: secret[int8]) returns nothing: + return nothing +function main() returns nothing: + secret[int8] sequence_child = 7 + secret[int8] optional_child = 8 + secret[int8] result_child = 9 + secret[int8] hidden = 10 + sequence(list(sequence_child)) + optional_value(some(optional_child)) + result_value(ok(result_child)) + scalar(1 + 2) + scalar(hidden + 2) + scalar(2 + hidden) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut calls = result.call_ownership.iter().collect::>(); + calls.sort_by_key(|(span, _)| span.start); + assert_eq!(calls.len(), 6); + for (_, packet) in &calls { + let [argument] = packet.arguments.as_slice() else { + panic!("one source occurrence"); + }; + assert_eq!( + result.source_type_map.get(&argument.source_span), + Some(&argument.actual_type) + ); + assert_eq!( + result.type_map.get(&argument.source_span), + Some(&argument.parameter_type) + ); + } + let raw_payload = |index: usize| { + let actual = calls[index].1.arguments[0].actual_type; + let payload = match result.interner.resolve(actual) { + Type::List(payload) | Type::Optional(payload) | Type::Result(payload, _) => { + *payload + } + other => panic!("inner container or sum, got {other:?}"), + }; + assert!( + matches!(result.interner.resolve(payload), Type::Secret(inner) if *inner == TypeInterner::INT8) + ); + let Type::Secret(expected_inner) = result + .interner + .resolve(calls[index].1.arguments[0].parameter_type) + else { + panic!("outer Secret formal"); + }; + assert_eq!(*expected_inner, actual); + }; + for index in 0..3 { + raw_payload(index); + } + assert_eq!(calls[3].1.arguments[0].actual_type, TypeInterner::INT8); + for (_, packet) in &calls[4..] { + let argument = &packet.arguments[0]; + assert_eq!(argument.actual_type, argument.parameter_type); + assert!( + matches!(result.interner.resolve(argument.actual_type), Type::Secret(inner) if *inner == TypeInterner::INT8) ); - let result = check_source_result(&source); - let errors = result - .diagnostics - .iter() - .filter(|error| error.severity == jett_diagnostics::Severity::Error) - .collect::>(); - assert!(errors.is_empty(), "{source}\n{errors:?}"); - let start = source.find(&call).unwrap() as u32; - let ty = result.type_map[&sp(start, start + call.len() as u32)]; - assert_eq!(result.interner.type_name(ty), expected); } } } #[test] - fn generic_return_context_fills_nested_empty_and_absent_slots() { - assert!( - check_source_errors( - r#"function identity[T](value: T) returns T: - return value -function main() returns nothing: - list[string] values = identity(list()) - optional[list[string]] absent = identity(none) - result[list[string], string] success = identity(ok(list())) - trace values - trace absent - trace success - return nothing -"#, - ) - .is_empty() - ); + fn caller_facts_pipeline_inputs_survive_indirect_field_callee_spans() { + let source = r#"namespace app +enum Choice: + chosen(value: int64) + other(value: int64) +function choose(value: int64) returns Choice: + return Choice.chosen(value) +function increment(value: int64) returns int64: + return value + 1 +struct ChoiceHolder: + item: function(int64) returns Choice +struct NumberHolder: + item: function(int64) returns int64 +function direct() returns Choice: + ChoiceHolder holder = ChoiceHolder(item: choose) + return 4 into holder.item +function chained() returns int64: + NumberHolder holder = NumberHolder(item: increment) + return 4 into holder.item into holder.item +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut inputs = result.pipeline_step_input_types.iter().collect::>(); + inputs.sort_by_key(|(span, _)| span.start); + assert_eq!(inputs.len(), 3); + for (&span, &input) in inputs.iter().copied() { + assert_eq!(input, TypeInterner::INT64); + assert!(matches!( + result.interner.resolve(result.type_map[&span]), + Type::Function { .. } + )); + let packet = &result.call_ownership[&span]; + assert!(matches!( + packet.target, + CheckedInvocationTarget::Indirect(_) + )); + assert_eq!(packet.arguments[0].actual_type, input); + assert_eq!(packet.arguments[0].parameter_type, input); + assert_eq!( + result.source_type_map[&span], + result.pipeline_step_call_types[&span] + ); + } + assert!(matches!( + result.interner.resolve(result.source_type_map[inputs[0].0]), + Type::Enum(_) + )); + } } #[test] - fn inferred_generic_witnesses_peel_root_aliases_and_preserve_nested_aliases() { - for (parameter, setup, argument, expected_name, expected_kind) in [ - ( - "T", - " Count value = 7\n", - "value", - "int64", - "primitive_type", - ), - ( - "result[T, string]", - " result[Count, string] value = ok(7)\n", - "value", - "app.Count", - "alias_type", - ), - ( - "list[T]", - " list[Count] value = list(7)\n", - "value", - "app.Count", - "alias_type", - ), - ( - "result[T, string]", - " Wrapped value = ok(7)\n", - "value", - "app.Count", - "alias_type", - ), - ( - "function() returns T", - "", - "count_callback", - "app.Count", - "alias_type", - ), - ( - "T", - "", - "count_callback", - "function() returns app.Count", - "function_type", - ), - ( - "function(view T) returns T", - "", - "view_callback", - "app.Count", - "alias_type", - ), - ] { - let source = format!( - r#"namespace app -type Count = int64 -type Wrapped = result[Count, string] -function count_callback() returns Count: - return 7 -function view_callback(view value: Count) returns Count: - return value -function choose[T](input: {parameter}) returns string: - return type.name[T]() -function main() returns string: -{setup} return choose({argument}) -"# + fn caller_facts_pipeline_inputs_distinguish_handled_output_and_reject_bad_inputs() { + let prefix = r#"namespace app +function parse(value: string) returns result[int64, string]: + return fail("bad") +function increment(value: int64) returns int64: + return value + 1 +struct Holder: + parse: function(string) returns result[int64, string] + finish: function(int64) returns int64 +"#; + let source = format!( + "{prefix}function main() returns int64:\n Holder holder = Holder(parse: parse, finish: increment)\n return \"x\"\n into holder.parse handle error:\n default 7\n into holder.finish\n" + ); + let invalid = format!( + "{prefix}function invalid() returns result[int64, string]:\n Holder holder = Holder(parse: parse, finish: increment)\n return 4 into holder.parse\n" + ); + for release in [false, true] { + let result = checked(&source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut inputs = result.pipeline_step_input_types.iter().collect::>(); + inputs.sort_by_key(|(span, _)| span.start); + assert_eq!(inputs.len(), 2); + assert_eq!(*inputs[0].1, TypeInterner::STRING); + assert_eq!(*inputs[1].1, TypeInterner::INT64); + let first = *inputs[0].0; + assert!( + matches!(result.interner.resolve(result.pipeline_step_call_types[&first]), Type::Result(value, error) if *value == TypeInterner::INT64 && *error == TypeInterner::STRING) ); - let result = check_source_result(&source); - let errors = result - .diagnostics - .iter() - .filter(|error| error.severity == jett_diagnostics::Severity::Error) - .collect::>(); - assert!(errors.is_empty(), "{source}\n{errors:?}"); - let call = result - .generic_calls - .values() - .next() - .expect("inferred choose call"); + assert_eq!(result.source_type_map[&first], TypeInterner::INT64); assert_eq!( - call.specialization.type_argument_reflections[0].type_name, expected_name, - "{source}" + result.call_ownership[inputs[1].0].arguments[0].source_span, + first ); - assert_eq!( - call.specialization.type_argument_kinds, - [expected_kind], - "{source}" + // The handle extends the first step span; only the following + // unhandled field target shares its complete occurrence span. + assert_eq!(result.type_map[&first], TypeInterner::STRING); + assert!(matches!( + result.interner.resolve(result.type_map[inputs[1].0]), + Type::Function { .. } + )); + let rejected = checked(&invalid, release, false); + assert!( + !errors(&rejected).is_empty(), + "invalid indirect input was admitted" ); assert_eq!( - call.concrete_args[0], - if parameter == "T" && argument == "count_callback" { - result.generic_function_instantiations[0].parameter_types[0] - } else { - TypeInterner::INT64 - } + rejected + .pipeline_step_input_types + .values() + .copied() + .collect::>(), + vec![TypeInterner::INT64] + ); + assert!( + errors(&rejected) + .iter() + .any(|diagnostic| diagnostic.message.contains("string") + && diagnostic.message.contains("int64")) ); } } #[test] - fn inferred_alias_witnesses_are_unchanged_by_view_and_parentheses() { - for (parameter, setup, value, expected_name, expected_kind) in [ + fn caller_facts_pipeline_inputs_stay_in_exact_generic_and_reflected_frames() { + let source = r#"namespace app +struct Sample: + count: int64 + values: list[int64] +function scalar(value: int64) returns int64: + return value +function text(value: string) returns string: + return value +function apply[T](value: T, mapper: function(T) returns T) returns T: + return value into mapper +function inspect[T](view value: T) returns nothing: + for field in type.fields[T](): + comptime type Field = field.type_info: + Field original = type.field_value[T, Field](view value, view field) + function(Field) returns Field mapper = function(item: Field) returns Field: + return item + Field copied = original into mapper + return nothing +function main() returns nothing: + int64 count = apply[int64](7, scalar) + string label = apply[string]("x", text) + Sample sample = Sample(count: 1, values: list(2)) + inspect[Sample](view sample) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + assert!(result.pipeline_step_input_types.is_empty()); + let direct = result + .generic_function_instantiations + .iter() + .filter(|body| body.comptime_type_bindings.is_empty()) + .flat_map(|body| { + body.pipeline_step_input_types + .iter() + .map(move |(span, input)| (body, span, input)) + }) + .collect::>(); + assert_eq!(direct.len(), 2); + let mut direct_types = Vec::new(); + for (body, span, input) in direct { + let packet = &body.call_ownership[span]; + assert_eq!(packet.arguments[0].actual_type, *input); + assert!(matches!( + result.interner.resolve(body.type_map[span]), + Type::Function { .. } + )); + assert!(!result.pipeline_step_input_types.contains_key(span)); + direct_types.push(*input); + } + assert!(direct_types.contains(&TypeInterner::INT64)); + assert!(direct_types.contains(&TypeInterner::STRING)); + let scopes = result + .generic_function_instantiations + .iter() + .flat_map(|body| body.comptime_type_bindings.values()) + .flatten() + .collect::>(); + assert_eq!(scopes.len(), 2); + let mut scoped_types = Vec::new(); + for scope in scopes { + assert_eq!(scope.body.pipeline_step_input_types.len(), 1); + let (&span, &input) = scope.body.pipeline_step_input_types.iter().next().unwrap(); + assert_eq!( + scope.body.call_ownership[&span].arguments[0].actual_type, + input + ); + assert!(matches!( + result.interner.resolve(scope.body.type_map[&span]), + Type::Function { .. } + )); + scoped_types.push(input); + } + assert!(scoped_types.contains(&TypeInterner::INT64)); + assert!(scoped_types.iter().any(|input| matches!(result.interner.resolve(*input), Type::List(element) if *element == TypeInterner::INT64))); + } + } + + #[test] + fn caller_facts_transformed_explicit_views_retain_borrows_across_call_forms() { + let source = r#"namespace app +type Numbers = list[int64] where true +function inspect(view values: list[int64]) returns nothing: + return nothing +function generic[T](view value: T) returns nothing: + return nothing +function retained(view numbers: Numbers, view hidden: secret[list[int64]], callback: function(view list[int64]) returns nothing) returns nothing: + inspect(coarsen (view numbers)) + generic[list[int64]](declassify (view hidden)) + callback((coarsen ((view numbers)))) + (declassify (view hidden)) into inspect + list[int64] first_copy = clone (coarsen numbers) + list[int64] second_copy = clone (declassify hidden) + return nothing +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + assert_eq!(result.call_ownership.len(), 4); + for packet in result.call_ownership.values() { + let [argument] = packet.arguments.as_slice() else { + panic!("one source argument"); + }; + assert_eq!(argument.syntax, CheckedCallerSyntax::WrittenView); + assert_eq!(argument.callee_access, CheckedCalleeAccess::View); + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + assert!(matches!(argument.origin, CheckedCallerOrigin::Binding(_))); + assert!( + matches!(result.interner.resolve(argument.actual_type), Type::List(element) if *element == TypeInterner::INT64) + ); + } + } + } + + #[test] + fn caller_facts_transformed_views_keep_explicit_and_bare_diagnostics_distinct() { + let prefix = r#"namespace app +type Numbers = list[int64] where true +function consume(values: list[int64]) returns nothing: + return nothing +function inspect(view values: list[int64]) returns nothing: + return nothing +"#; + for (parameter, call, expected_code) in [ ( - "T", - " Count value = 7\n", - "value", - "int64", - "primitive_type", + "view values: Numbers", + "consume(coarsen (view values))", + 375, ), ( - "optional[T]", - " optional[Count] value = some(7)\n", - "value", - "app.Count", - "alias_type", + "view values: secret[list[int64]]", + "consume(declassify (view values))", + 375, ), + ("view values: Numbers", "consume(coarsen values)", 401), ( - "T", - " list[Count] value = list(7)\n", - "value", - "list[app.Count]", - "list_type", + "view values: secret[list[int64]]", + "consume(declassify values)", + 401, ), + ("view values: Numbers", "inspect(coarsen values)", 401), ( - "function() returns T", - "", - "count_callback", - "app.Count", - "alias_type", + "view values: secret[list[int64]]", + "inspect(declassify values)", + 401, ), ] { - for argument in [ - value.to_string(), - format!("({value})"), - format!("view {value}"), - format!("(view ({value}))"), - ] { - let source = format!( - r#"namespace app -type Count = int64 -function count_callback() returns Count: - return 7 -function choose[T](view input: {parameter}) returns string: - return type.name[T]() -function main() returns string: -{setup} return choose({argument}) -"# + let source = format!( + "{prefix}function rejected({parameter}) returns nothing:\n {call}\n return nothing\n" + ); + for release in [false, true] { + let result = checked(&source, release, false); + let failures = errors(&result); + assert_eq!(failures.len(), 1, "{source}: {failures:?}"); + assert_eq!( + failures[0].code.code(), + expected_code, + "{source}: {failures:?}" ); - let result = check_source_result(&source); - let errors = result - .diagnostics + } + } + } + + #[test] + fn caller_facts_transparent_view_syntax_stops_at_clones_and_copied_fields() { + let source = r#"namespace app +type Numbers = list[int64] where true +struct Holder: + values: list[int64] + numbers: Numbers + hidden: secret[list[int64]] +function consume(values: list[int64]) returns nothing: + return nothing +function acquired(view numbers: Numbers, view hidden: secret[list[int64]], view holder: Holder) returns nothing: + consume(coarsen (clone (view numbers))) + consume(declassify (clone (view hidden))) + consume((view holder).values) + consume(coarsen ((view holder).numbers)) + consume(declassify ((view holder).hidden)) + return nothing +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut packets = result.call_ownership.iter().collect::>(); + packets.sort_by_key(|(span, _)| span.start); + assert_eq!(packets.len(), 5); + for (index, (_, packet)) in packets.iter().enumerate() { + let [argument] = packet.arguments.as_slice() else { + panic!("one source argument"); + }; + assert_eq!(argument.syntax, CheckedCallerSyntax::Bare); + assert_eq!(argument.callee_access, CheckedCalleeAccess::Owned); + assert_eq!(argument.effect, CheckedCallerEffect::TransferOwned); + if index < 2 { + assert_eq!(argument.origin, CheckedCallerOrigin::OwnedExpression); + } else { + assert!(matches!( + argument.origin, + CheckedCallerOrigin::OwnedFieldCopy { .. } + )); + } + } + } + } + + #[test] + fn caller_facts_invalid_calls_preserve_dependency_ownership_diagnostics() { + let take = "function take(values: list[int64]) returns int64:\n return 1\n"; + let cases = [ + (format!("{OBSERVER}function test(values: list[int64]) returns list[int64]:\n int64 ignored = inspect[int64](values)\n return values\n"), vec![309], false), + (format!("{OBSERVER}{take}function test(values: list[int64]) returns list[int64]:\n int64 ignored = inspect[int64](take(values))\n return values\n"), vec![309, 400], false), + (format!("{OBSERVER}{take}function test(view values: list[int64]) returns int64:\n return inspect[int64](take(values))\n"), vec![309, 401], false), + (format!("{OBSERVER}{take}function test(values: list[int64]) returns int64:\n int64 consumed = take(values)\n return inspect[int64](values)\n"), vec![309, 400], false), + ("namespace app\nfunction test(view values: bytes) returns string:\n return bytes.__to_hex[string](values)\n".to_string(), vec![309], true), + ("namespace app\nfunction test() returns string:\n return bytes.__to_hex[string](1)\n".to_string(), vec![304, 309], true), + ("namespace app\nfunction test(view values: bytes) returns string:\n return bytes.__to_hex(values)\n".to_string(), vec![401], true), + ("namespace app\nfunction test(view values: bytes) returns string:\n return bytes.__to_hex(view values)\n".to_string(), vec![], true), + ]; + for (source, expected, stdlib) in cases { + for release in [false, true] { + let result = checked(&source, release, stdlib); + let mut codes = errors(&result) .iter() - .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .map(|error| error.code.code()) .collect::>(); - assert!(errors.is_empty(), "{source}\n{errors:?}"); - let call = result - .generic_calls - .values() - .next() - .expect("inferred choose call"); + codes.sort_unstable(); assert_eq!( - call.specialization.type_argument_reflections[0].type_name, expected_name, - "{source}" + codes, expected, + "release={release}: {:?}", + result.diagnostics + ); + } + } + } + + #[test] + fn caller_facts_qualified_descriptors_keep_source_order_and_written_views() { + let source = r#"namespace app +function identity(value: int64) returns int64: + return value +function inspect(view callback: function(int64) returns int64, marker: int64) returns int64: + return marker +function exercise() returns int64: + int64 direct = inspect(marker: 1, callback: app.identity) + int64 retained = inspect(view app.identity, 2) + return app.identity into view inspect(3) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut packets = result.call_ownership.iter().collect::>(); + packets.sort_by_key(|(span, _)| span.start); + assert_eq!(packets.len(), 3); + for (index, (_, packet)) in packets.iter().enumerate() { + let argument = packet + .arguments + .iter() + .find(|argument| argument.parameter_index == 0) + .expect("descriptor formal"); + assert_eq!(argument.origin, CheckedCallerOrigin::OwnedExpression); + assert_eq!(argument.callee_access, CheckedCalleeAccess::View); + assert_eq!( + argument.effect, + if index == 0 { + CheckedCallerEffect::RelinquishOwned + } else { + CheckedCallerEffect::RetainBorrow + } ); assert_eq!( - call.specialization.type_argument_kinds, - [expected_kind], - "{source}" + argument.syntax, + if index == 0 { + CheckedCallerSyntax::Bare + } else { + CheckedCallerSyntax::WrittenView + } ); } + assert_eq!( + packets[0] + .1 + .arguments + .iter() + .map(|argument| (argument.source_index, argument.parameter_index)) + .collect::>(), + [(0, 1), (1, 0)] + ); + } + } + + #[test] + fn caller_facts_method_descriptor_producers_use_exact_concrete_frames() { + let source = r#"namespace models +export struct Point: + value: int64 + function amount(view self: Point) returns int64: + return self.value +namespace app +function inspect(view callback: function(view models.Point) returns int64) returns nothing: + return nothing +function forward[T](seed: T) returns nothing: + use models as m + inspect(m.Point.amount) + return nothing +function exercise() returns nothing: + use models as m + inspect(view m.Point.amount) + forward[int64](1) + forward[string]("two") + return nothing +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let root = result + .call_ownership + .values() + .find(|packet| { + packet.arguments.first().is_some_and(|argument| { + matches!( + result.interner.resolve(argument.actual_type), + Type::Function { .. } + ) + }) + }) + .expect("root method descriptor call"); + assert_eq!( + root.arguments[0].origin, + CheckedCallerOrigin::OwnedExpression + ); + assert_eq!(root.arguments[0].effect, CheckedCallerEffect::RetainBorrow); + assert_eq!(result.generic_function_instantiations.len(), 2); + for body in &result.generic_function_instantiations { + assert_eq!(body.method_values.len(), 1); + assert_eq!(body.call_ownership.len(), 1); + let argument = &body.call_ownership.values().next().unwrap().arguments[0]; + assert_eq!(argument.origin, CheckedCallerOrigin::OwnedExpression); + assert_eq!(argument.effect, CheckedCallerEffect::RelinquishOwned); + assert!(body.method_values.contains_key(&argument.source_span)); + assert!(!result.method_values.contains_key(&argument.source_span)); + } } } #[test] - fn deep_inferred_alias_witnesses_have_distinct_checked_specializations() { + fn caller_facts_function_value_fields_and_locals_remain_value_origins() { let source = r#"namespace app -type Count = int64 -function inspect[T](input: T) returns string: - return type.name[T]() -function main() returns string: - optional[Count] counted = some(7) - optional[int64] plain = some(7) - string first = inspect(counted) - string second = inspect(plain) - return "{first}:{second}" +struct Holder: + callback: function(int64) returns int64 +function identity(value: int64) returns int64: + return value +function inspect(view callback: function(int64) returns int64) returns nothing: + return nothing +function exercise(view holder: Holder) returns nothing: + function(int64) returns int64 stored = identity + inspect(view stored) + inspect(holder.callback) + inspect(view holder.callback) + return nothing "#; - let result = check_source_result(source); - let errors = result - .diagnostics - .iter() - .filter(|error| error.severity == jett_diagnostics::Severity::Error) - .collect::>(); - assert!(errors.is_empty(), "{errors:?}"); - let [counted, plain] = result.generic_function_instantiations.as_slice() else { - panic!("expected distinct source-aware instances"); - }; - assert_eq!(counted.concrete_args, plain.concrete_args); - assert_eq!( - counted.specialization.type_argument_kinds, - plain.specialization.type_argument_kinds - ); - assert_eq!( - counted.specialization.type_argument_reflections[0].type_name, - "optional[app.Count]" - ); - assert_eq!( - plain.specialization.type_argument_reflections[0].type_name, - "optional[int64]" - ); - assert_ne!(counted.specialization, plain.specialization); + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut packets = result.call_ownership.iter().collect::>(); + packets.sort_by_key(|(span, _)| span.start); + assert_eq!(packets.len(), 3); + assert!(matches!( + packets[0].1.arguments[0].origin, + CheckedCallerOrigin::Binding(_) + )); + assert!(matches!( + packets[1].1.arguments[0].origin, + CheckedCallerOrigin::OwnedFieldCopy { + parent: CheckedViewSource::Binding(_) + } + )); + assert!(matches!( + packets[2].1.arguments[0].origin, + CheckedCallerOrigin::BorrowedProjection { + source: CheckedViewSource::Binding(_) + } + )); + } + } + + #[test] + fn caller_facts_generic_mutual_forwarding_joins_resolved_definition_not_body_span() { + let source = r#"namespace sample +mutual: + function leaf[T](view value: T) returns int64 + function forward[T](view value: T) returns int64 +function leaf[T](view value: T) returns int64: + return 7 +function forward[T](view value: T) returns int64: + return leaf[T](view value) +function exercise(values: list[int64]) returns int64: + int64 retained = forward[list[int64]](view values) + return forward[list[int64]](values) + retained +"#; + 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(|d| d.severity != jett_diagnostics::Severity::Error), + "{:?}", + resolved.diagnostics + ); + let result = check_with_options(&parsed.module, &resolved, CheckOptions { release }); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let forward = parsed + .module + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "forward" => Some(function), + _ => None, + }) + .expect("actual mutual function body"); + let definition = resolved.resolutions[&forward.name.span]; + assert_ne!(resolved.scope_table.def(definition).span, forward.name.span); + let mut root = result.call_ownership.iter().collect::>(); + root.sort_by_key(|(span, _)| span.start); + assert_eq!(root.len(), 2, "both selected source calls require packets"); + for (index, (_, packet)) in root.iter().enumerate() { + let CheckedInvocationTarget::Generic(target) = &packet.target else { + panic!("exact generic source target"); + }; + assert_eq!(target.definition, definition); + assert_eq!( + packet.arguments[0].syntax, + if index == 0 { + CheckedCallerSyntax::WrittenView + } else { + CheckedCallerSyntax::Bare + } + ); + assert_eq!(packet.arguments[0].callee_access, CheckedCalleeAccess::View); + assert_eq!( + packet.arguments[0].effect, + if index == 0 { + CheckedCallerEffect::RetainBorrow + } else { + CheckedCallerEffect::RelinquishOwned + } + ); + assert_eq!( + result.source_type_map.get(&packet.arguments[0].source_span), + Some(&packet.arguments[0].actual_type) + ); + } + let body = result + .generic_function_instantiations + .iter() + .find(|body| body.definition == definition) + .expect("concrete forward body"); + assert_eq!(body.call_ownership.len(), 1); + let nested = body.call_ownership.values().next().unwrap(); + let argument = &nested.arguments[0]; + assert_eq!(argument.syntax, CheckedCallerSyntax::WrittenView); + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + assert_eq!( + body.source_type_map.get(&argument.source_span), + Some(&argument.actual_type) + ); + assert!( + !result + .call_ownership + .contains_key(body.call_ownership.keys().next().unwrap()) + ); + } + } + + #[test] + fn caller_facts_mutual_generic_same_leaf_names_keep_exact_modes_and_targets() { + let source = r#"namespace left +mutual: + export function inspect[T](view value: T) returns int64 +export function inspect[T](view value: T) returns int64: + return 1 +namespace right +mutual: + export function inspect[T](value: T) returns int64 +export function inspect[T](value: T) returns int64: + return 2 +namespace app +function exercise(first: list[int64], second: list[int64]) returns int64: + use left + use right + int64 observed = left.inspect[list[int64]](view first) + return right.inspect[list[int64]](second) + observed +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut calls = result.call_ownership.iter().collect::>(); + calls.sort_by_key(|(span, _)| span.start); + assert_eq!(calls.len(), 2); + let CheckedInvocationTarget::Generic(left) = &calls[0].1.target else { + panic!("left"); + }; + let CheckedInvocationTarget::Generic(right) = &calls[1].1.target else { + panic!("right"); + }; + assert_ne!(left.definition, right.definition); + assert_eq!( + calls[0].1.arguments[0].callee_access, + CheckedCalleeAccess::View + ); + assert_eq!( + calls[0].1.arguments[0].effect, + CheckedCallerEffect::RetainBorrow + ); + assert_eq!( + calls[1].1.arguments[0].callee_access, + CheckedCalleeAccess::Owned + ); + assert_eq!( + calls[1].1.arguments[0].effect, + CheckedCallerEffect::TransferOwned + ); + } } #[test] - fn inferred_alias_witnesses_follow_declaration_argument_order() { + fn caller_facts_machine_state_forwarded_views_preserve_immediate_bindings() { let source = r#"namespace app -type Count = int64 -type Other = int64 -function choose[T](first: list[T], second: list[T]) returns string: - return type.name[T]() -function main() returns string: - list[Count] counted = list(7) - list[Other] others = list(8) - return choose(second: others, first: counted) +machine Packet: + states: + ready(values: list[int64]) +struct Envelope: + packet: Packet at ready +function inspect(view packet: Packet at ready) returns nothing: + return nothing +function exercise(view envelope: Envelope) returns nothing: + Packet at ready borrowed = view envelope.packet + Packet at ready forwarded = borrowed + inspect(view forwarded) + inspect(view borrowed) + inspect(view envelope.packet) + return nothing "#; - let result = check_source_result(source); - let errors = result - .diagnostics - .iter() - .filter(|error| error.severity == jett_diagnostics::Severity::Error) - .collect::>(); - assert!(errors.is_empty(), "{errors:?}"); - let call = result.generic_calls.values().next().unwrap(); - assert_eq!( - call.specialization.type_argument_reflections[0].type_name, - "app.Count" - ); - assert_eq!(call.specialization.type_argument_kinds, ["alias_type"]); + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let fact = |name| { + result + .binding_facts + .values() + .find(|fact| { + &source[fact.declaration_span.start as usize + ..fact.declaration_span.end as usize] + == name + }) + .expect("exact checked binding") + }; + let borrowed = fact("borrowed"); + let forwarded = fact("forwarded"); + assert_eq!( + forwarded.mode, + CheckedBindingMode::View { + source: CheckedViewSource::Binding(borrowed.definition), + } + ); + assert_eq!(forwarded.ty, borrowed.ty); + assert!(matches!( + result.interner.resolve(forwarded.ty), + Type::MachineState { .. } + )); + assert_eq!(result.call_ownership.len(), 3); + assert!( + result + .call_ownership + .values() + .all(|packet| packet.arguments[0].effect == CheckedCallerEffect::RetainBorrow) + ); + } } #[test] - fn inferred_never_witness_merging_preserves_known_alias_siblings() { + fn caller_facts_machine_view_forwarding_stays_in_each_concrete_generic_body() { let source = r#"namespace app -type Count = int64 -function describe[T](first: T, second: T) returns string: - return type.name[T]() -function outer[E](view empty: list[E]) returns string: - result[Count, E] partial = ok(7) - result[int64, string] full = fail("bad") - return describe(partial, full) -function main() returns string: - return outer(list()) +machine Packet: + states: + ready(values: list[int64]) +struct Envelope[T]: + packet: Packet at ready + extra: T +function inspect(view packet: Packet at ready) returns nothing: + return nothing +function forward[T](view envelope: Envelope[T]) returns nothing: + Packet at ready borrowed = view envelope.packet + Packet at ready forwarded = borrowed + inspect(view forwarded) + inspect(view envelope.packet) + return nothing +function exercise(first: Envelope[int64], second: Envelope[list[int64]]) returns nothing: + forward[int64](view first) + forward[list[int64]](view second) + return nothing "#; - let result = check_source_result(source); - let errors = result - .diagnostics - .iter() - .filter(|error| error.severity == jett_diagnostics::Severity::Error) - .collect::>(); - assert!(errors.is_empty(), "{errors:?}"); - let instance = result - .generic_function_instantiations - .iter() - .find(|instance| { - instance.specialization.type_argument_reflections[0].type_name - == "result[app.Count, string]" - }) - .expect("filled result with preserved Count witness"); - assert_eq!(instance.specialization.type_argument_kinds, ["result_type"]); - let Type::Result(ok, error) = result.interner.resolve(instance.concrete_args[0]) else { - panic!("expected result"); - }; - assert_eq!((*ok, *error), (TypeInterner::INT64, TypeInterner::STRING)); + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + assert_eq!(result.generic_function_instantiations.len(), 2); + for body in &result.generic_function_instantiations { + let fact = |name| { + body.binding_facts + .values() + .find(|fact| { + &source[fact.declaration_span.start as usize + ..fact.declaration_span.end as usize] + == name + }) + .expect("concrete body binding") + }; + let borrowed = fact("borrowed"); + let forwarded = fact("forwarded"); + assert_eq!( + forwarded.mode, + CheckedBindingMode::View { + source: CheckedViewSource::Binding(borrowed.definition), + } + ); + assert_eq!(body.call_ownership.len(), 2); + assert!( + body.call_ownership + .values() + .all(|packet| packet.arguments[0].effect + == CheckedCallerEffect::RetainBorrow) + ); + } + } } #[test] - fn inferred_alias_kind_and_info_select_the_same_reflection_branch() { - let source = r#"namespace app -type Count = int64 -function inspect[T](input: list[T]) returns string: - if type.kind_tag[T]() == TypeKind.alias_type: - return type.name[T]() - else: - return "primitive" -function main() returns string: - list[Count] values = list(7) - return inspect(values) + fn caller_facts_machine_forwarding_does_not_relax_owned_erasure_or_bare_views() { + let prefix = "namespace app\nmachine Packet:\n states:\n ready(values: list[int64])\nfunction inspect(view packet: Packet at ready) returns nothing:\n return nothing\n"; + let cases = [ + ( + "function exercise(packet: Packet at ready) returns nothing:\n Packet erased = packet\n inspect(view packet)\n return nothing\n", + vec![400], + ), + ( + "function exercise(packet: Packet at ready) returns nothing:\n Packet erased = clone packet\n inspect(view packet)\n return nothing\n", + vec![], + ), + ( + "function exercise(view packet: Packet at ready) returns nothing:\n inspect(packet)\n return nothing\n", + vec![401], + ), + ( + "function exercise(view packet: Packet at ready) returns Packet at ready:\n Packet at ready forwarded = packet\n return forwarded\n", + vec![401], + ), + ]; + for (body, expected) in cases { + for release in [false, true] { + let result = checked(&format!("{prefix}{body}"), release, false); + let codes = errors(&result) + .iter() + .map(|error| error.code.code()) + .collect::>(); + assert_eq!(codes, expected, "{body}: {:?}", result.diagnostics); + } + } + } + + #[test] + fn caller_facts_qualified_unit_enums_keep_named_order_and_pipeline_syntax() { + let source = r#"namespace models +export enum Choice: + empty + full(value: int64) +namespace app +function inspect(view choice: models.Choice, marker: int64) returns int64: + return marker +function exercise() returns int64: + use models as m + int64 first = inspect(marker: 1, choice: m.Choice.empty) + int64 second = inspect(view m.Choice.empty, 2) + return m.Choice.empty into view inspect(3) "#; - let result = check_source_result(source); - let errors = result - .diagnostics - .iter() - .filter(|error| error.severity == jett_diagnostics::Severity::Error) - .collect::>(); - assert!(errors.is_empty(), "{errors:?}"); - let instance = result.generic_function_instantiations.first().unwrap(); - assert_eq!(instance.specialization.type_argument_kinds, ["alias_type"]); - assert_eq!( - instance.specialization.type_argument_reflections[0].type_name, - "app.Count" - ); - assert!( - instance - .static_selections - .values() - .any(|selection| *selection == CheckedStaticSelection::IfThen) - ); + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut packets = result.call_ownership.iter().collect::>(); + packets.sort_by_key(|(span, _)| span.start); + assert_eq!(packets.len(), 3); + for (index, (_, packet)) in packets.iter().enumerate() { + let argument = packet + .arguments + .iter() + .find(|argument| argument.parameter_index == 0) + .expect("unit enum actual"); + assert_eq!(argument.origin, CheckedCallerOrigin::OwnedExpression); + assert_eq!(argument.callee_access, CheckedCalleeAccess::View); + assert_eq!( + argument.effect, + if index == 0 { + CheckedCallerEffect::RelinquishOwned + } else { + CheckedCallerEffect::RetainBorrow + } + ); + assert_eq!( + argument.syntax, + if index == 0 { + CheckedCallerSyntax::Bare + } else { + CheckedCallerSyntax::WrittenView + } + ); + assert_eq!( + result.source_type_map.get(&argument.source_span), + Some(&argument.actual_type) + ); + let Type::Enum(id) = result.interner.resolve(argument.actual_type) else { + panic!("exact nominal enum source type"); + }; + assert_eq!(result.interner.resolve_enum(*id).name, "models.Choice"); + } + assert_eq!( + packets[0] + .1 + .arguments + .iter() + .map(|argument| (argument.source_index, argument.parameter_index)) + .collect::>(), + [(0, 1), (1, 0)] + ); + } } -} -#[cfg(test)] -mod resource_hook_span_tests { - use super::*; - use jett_common::{STDLIB_FILE_ID_START, SourceOrigin}; - use jett_resolve::{ResourceKernelSpec, resolve_with_resource_kernels}; - use jett_types::ResourceKernelRecipe; + #[test] + fn caller_facts_qualified_unit_enums_use_concrete_and_reflected_source_maps() { + let source = r#"namespace models +export enum Choice: + empty + full(value: int64) +namespace app +struct First: + count: int64 + text: string +struct Second: + count: int64 +function inspect(view choice: models.Choice) returns nothing: + return nothing +function forward[T](view seed: T) returns nothing: + use models + inspect(models.Choice.empty) + for field in type.fields[T](): + comptime type Field = field.type_info: + inspect(view models.Choice.empty) + return nothing +function exercise(first: First, second: Second) returns nothing: + forward[First](view first) + forward[Second](view second) + return nothing +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + assert_eq!(result.generic_function_instantiations.len(), 2); + let mut scoped_count = 0; + for body in &result.generic_function_instantiations { + let packet = body + .call_ownership + .values() + .find(|packet| { + packet.arguments.first().is_some_and(|argument| { + &source[argument.source_span.start as usize + ..argument.source_span.end as usize] + == "models.Choice.empty" + }) + }) + .expect("outside reflected scope"); + let argument = &packet.arguments[0]; + assert_eq!(argument.origin, CheckedCallerOrigin::OwnedExpression); + assert_eq!(argument.effect, CheckedCallerEffect::RelinquishOwned); + assert_eq!( + body.source_type_map.get(&argument.source_span), + Some(&argument.actual_type) + ); + assert!(!result.source_type_map.contains_key(&argument.source_span)); + for scoped in body.comptime_type_bindings.values().flatten() { + scoped_count += 1; + let packet = scoped + .body + .call_ownership + .values() + .find(|packet| { + packet.arguments.first().is_some_and(|argument| { + source[argument.source_span.start as usize + ..argument.source_span.end as usize] + .contains("models.Choice.empty") + }) + }) + .expect("reflected scope packet"); + let argument = &packet.arguments[0]; + assert_eq!(argument.origin, CheckedCallerOrigin::OwnedExpression); + assert_eq!(argument.syntax, CheckedCallerSyntax::WrittenView); + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + assert_eq!( + scoped.body.source_type_map.get(&argument.source_span), + Some(&argument.actual_type) + ); + assert_eq!( + body.source_type_map.get(&argument.source_span), + Some(&argument.actual_type) + ); + assert!(!result.source_type_map.contains_key(&argument.source_span)); + } + } + assert_eq!( + scoped_count, 3, + "one exact body per declared reflected field" + ); + } + } #[test] - fn synthetic_resource_functions_do_not_replace_source_declaration_fallback() { - let file = FileId::new(STDLIB_FILE_ID_START); - let parsed = jett_parser::parse("namespace resource_probe\nresource TestHandle\n", file); - assert!(parsed.errors.is_empty()); - let Item::Resource(resource) = &parsed.module.items[1] else { - panic!("missing resource") - }; - let specs = [ResourceKernelSpec { - resource_declaration: resource.name.span, - member: "kernel_close".into(), - recipe: ResourceKernelRecipe::Finalize, - }]; - let resolved = resolve_with_resource_kernels( - &parsed.module, - &HashMap::from([(file, SourceOrigin::Stdlib)]), - &specs, - ) - .unwrap(); - let resource_definition = resolved.resolutions[&resource.name.span]; - let kernel_definition = resolved.resource_kernels.iter().next().unwrap().definition; - assert_eq!( - resolved.scope_table.def(resource_definition).span, - resolved.scope_table.def(kernel_definition).span - ); - let checker = TypeChecker::new(&resolved, CheckOptions::default()); - assert_eq!( - checker.decl_defs.get(&resource.name.span), - Some(&resource_definition) - ); + fn caller_facts_enum_valued_bindings_and_real_fields_remain_value_origins() { + let source = r#"namespace models +export enum Choice: + empty + full(value: int64) +namespace app +struct Holder: + choice: models.Choice +function inspect(view choice: models.Choice, marker: int64) returns nothing: + return nothing +function exercise(view holder: Holder, view stored: models.Choice) returns nothing: + use models + inspect(view stored, 1) + inspect(holder.choice, 2) + inspect(view holder.choice, 3) + inspect(Holder(choice: models.Choice.empty).choice, 4) + return nothing +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut packets = result.call_ownership.iter().collect::>(); + packets.sort_by_key(|(span, _)| span.start); + assert_eq!(packets.len(), 4); + assert!(matches!( + packets[0].1.arguments[0].origin, + CheckedCallerOrigin::Binding(_) + )); + assert!(matches!( + packets[1].1.arguments[0].origin, + CheckedCallerOrigin::OwnedFieldCopy { + parent: CheckedViewSource::Binding(_) + } + )); + assert!(matches!( + packets[2].1.arguments[0].origin, + CheckedCallerOrigin::BorrowedProjection { + source: CheckedViewSource::Binding(_) + } + )); + assert_eq!( + packets[3].1.arguments[0].origin, + CheckedCallerOrigin::OwnedFieldCopy { + parent: CheckedViewSource::Other, + } + ); + assert!( + packets + .iter() + .all(|(_, packet)| packet.arguments[0].actual_type + == packets[0].1.arguments[0].actual_type) + ); + } } } diff --git a/crates/jett_typecheck/src/lib.rs b/crates/jett_typecheck/src/lib.rs index 4bdc1687..e605bde7 100644 --- a/crates/jett_typecheck/src/lib.rs +++ b/crates/jett_typecheck/src/lib.rs @@ -1,5 +1,6 @@ // Type checking for the Jett compiler. +pub mod caller_ownership; pub mod capability; pub mod checker; pub mod complexity; @@ -23,3 +24,10 @@ pub use resource_hooks::{CheckedResourceHook, ResourceHookError, validate_resour mod resource_hook_tests; pub use resource_program::{CheckedResourceProgram, ResourceProgramError}; + +pub use caller_ownership::{ + CheckedArgumentOwnership, CheckedBindingFact, CheckedCallOwnership, CheckedCalleeAccess, + CheckedCallerEffect, CheckedCallerOrigin, CheckedCallerSyntax, CheckedIntrinsicOperandRole, + CheckedInvocationShape, CheckedInvocationTarget, CheckedOwnershipContext, + intrinsic_operand_access, +}; diff --git a/crates/jett_typecheck/src/ownership.rs b/crates/jett_typecheck/src/ownership.rs index b3d5ced3..dccf7eb4 100644 --- a/crates/jett_typecheck/src/ownership.rs +++ b/crates/jett_typecheck/src/ownership.rs @@ -1,8 +1,14 @@ use std::collections::{HashMap, HashSet}; -use jett_common::{Span, is_json_implicit_view_facade}; +use crate::{ + CheckedBindingFact, CheckedBindingMode, CheckedBodyFacts, CheckedCallOwnership, + CheckedCallerEffect, CheckedComptimeTypeBinding, CheckedGenericFunctionInstantiation, + CheckedStaticSelection, +}; +use jett_common::Span; use jett_diagnostics::Diagnostic; use jett_parser::ast::{self, Block, CallArg, Expr, FunctionDef, Item, Module, Stmt, StringPart}; +use jett_resolve::{resolver::ResolveResult, scope::DefId}; use jett_types::{CapabilityKind, Type, TypeId, TypeInterner}; // --------------------------------------------------------------------------- @@ -172,6 +178,44 @@ pub fn is_implicitly_copyable(interner: &TypeInterner, type_id: TypeId) -> bool // Ownership checker // --------------------------------------------------------------------------- +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum BindingKey { + Definition(DefId), + // Used only by the standalone AST test helper; production never joins names. + Legacy(String), + Unresolved(Span), +} + +#[derive(Clone, Copy)] +struct OwnershipBodyFacts<'a> { + binding_facts: &'a HashMap, + call_ownership: &'a HashMap, + type_map: &'a HashMap, + static_selections: Option<&'a HashMap>, + comptime_type_bindings: &'a HashMap>, +} + +impl<'a> OwnershipBodyFacts<'a> { + fn scoped(body: &'a CheckedBodyFacts) -> Self { + Self { + binding_facts: &body.binding_facts, + call_ownership: &body.call_ownership, + type_map: &body.type_map, + static_selections: Some(&body.static_selections), + comptime_type_bindings: &body.comptime_type_bindings, + } + } + fn generic(body: &'a CheckedGenericFunctionInstantiation) -> Self { + Self { + binding_facts: &body.binding_facts, + call_ownership: &body.call_ownership, + type_map: &body.type_map, + static_selections: Some(&body.static_selections), + comptime_type_bindings: &body.comptime_type_bindings, + } + } +} + /// Performs ownership analysis (linear type checking) on a type-checked module. /// /// This is Phase C from the architecture: tracking move/view/consume states for @@ -179,15 +223,16 @@ pub fn is_implicitly_copyable(interner: &TypeInterner, type_id: TypeId) -> bool /// to the diagnostic list. pub struct OwnershipChecker<'a> { /// Maps variable names to their ownership info within the current scope. - states: HashMap, + states: HashMap, /// Collected diagnostics. diagnostics: Vec, breakpoint_exclusions: HashMap>, debug_copyable: HashMap, /// The type interner, needed to check whether a type is implicitly copyable. interner: &'a TypeInterner, - /// Source-defined functions whose first parameter is declared as a view. - source_first_argument_views: HashSet, + resolve: Option<&'a ResolveResult>, + body_facts: Option>, + generic_bodies: &'a [CheckedGenericFunctionInstantiation], } impl<'a> OwnershipChecker<'a> { @@ -198,10 +243,151 @@ impl<'a> OwnershipChecker<'a> { breakpoint_exclusions: HashMap::new(), debug_copyable: HashMap::new(), interner, - source_first_argument_views: HashSet::new(), + resolve: None, + body_facts: None, + generic_bodies: &[], + } + } + + pub(crate) fn with_checked_facts( + mut self, + resolve: &'a ResolveResult, + binding_facts: &'a HashMap, + call_ownership: &'a HashMap, + type_map: &'a HashMap, + comptime_type_bindings: &'a HashMap>, + generic_bodies: &'a [CheckedGenericFunctionInstantiation], + ) -> Self { + self.resolve = Some(resolve); + self.body_facts = Some(OwnershipBodyFacts { + binding_facts, + call_ownership, + type_map, + static_selections: None, + comptime_type_bindings, + }); + self.generic_bodies = generic_bodies; + self + } + + fn binding_key(&self, name: &str, span: Span) -> BindingKey { + match self.resolve { + Some(resolve) => resolve + .resolutions + .get(&span) + .copied() + .or_else(|| { + resolve + .scope_table + .definitions + .iter() + .find(|definition| definition.span == span) + .map(|definition| definition.id) + }) + .map(BindingKey::Definition) + .unwrap_or(BindingKey::Unresolved(span)), + None => BindingKey::Legacy(name.to_string()), + } + } + + fn binding_label(&self, key: &BindingKey) -> Option { + match key { + BindingKey::Definition(definition) => self + .resolve + .map(|resolve| resolve.scope_table.def(*definition).name.clone()), + BindingKey::Legacy(name) => Some(name.clone()), + BindingKey::Unresolved(_) => None, + } + } + + fn binding_fact(&self, name: &ast::Ident) -> Option { + self.body_facts + .and_then(|facts| facts.binding_facts.get(&name.span)) + .copied() + } + + fn binding_type(&self, name: &ast::Ident, syntax: &ast::TypeExpr) -> TypeId { + self.binding_fact(name) + .map(|fact| fact.ty) + .unwrap_or_else(|| { + if self.resolve.is_some() { + TypeInterner::ERROR + } else { + self.resolve_type_for_ownership(syntax) + } + }) + } + + fn binding_state(&self, name: &ast::Ident) -> Option { + self.binding_fact(name).map(|fact| match fact.mode { + CheckedBindingMode::Owned => OwnershipState::Owned, + CheckedBindingMode::View { .. } => OwnershipState::Viewed, + }) + } + + fn register_checked_binding(&mut self, name: &ast::Ident) { + if let Some(fact) = self.binding_fact(name) { + self.states.insert( + BindingKey::Definition(fact.definition), + VarInfo { + state: self.binding_state(name).unwrap_or(OwnershipState::Owned), + mutable: fact.mutable, + type_id: fact.ty, + state_qualified_machine: matches!( + self.interner.resolve(fact.ty), + Type::MachineState { .. } + ), + consumed_span: None, + debug_consumed: false, + }, + ); + } + } + + fn check_argument_effect( + &mut self, + expression: &Expr, + span: Span, + effect: CheckedCallerEffect, + ) { + match effect { + CheckedCallerEffect::Copy + | CheckedCallerEffect::RetainBorrow + | CheckedCallerEffect::ObserveData => self.check_expr_ownership(expression), + CheckedCallerEffect::TransferOwned | CheckedCallerEffect::RelinquishOwned => { + self.consume_expr(expression, span) + } } } + fn check_scoped_type_body(&mut self, binding: &ast::ComptimeTypeBindStmt) { + let Some(bodies) = self + .body_facts + .and_then(|facts| facts.comptime_type_bindings.get(&binding.span)) + else { + // Failed checking has no concrete handoff. Do not guess scoped types + // or walk a body the checker intentionally did not select. + if self.resolve.is_none() { + self.check_block(&binding.body); + } + return; + }; + let baseline = self.states.clone(); + let mut outcomes = Vec::new(); + for body in bodies { + let saved_facts = self + .body_facts + .replace(OwnershipBodyFacts::scoped(&body.body)); + self.states = baseline.clone(); + self.check_block(&binding.body); + if Self::block_can_fall_through(&binding.body) { + outcomes.push(self.states.clone()); + } + self.body_facts = saved_facts; + } + self.states = self.merge_fallthrough_states(&baseline, &outcomes); + } + pub fn with_debug_types(mut self, types: impl IntoIterator) -> Self { for (span, ty) in types { let copyable = self.is_copyable(ty); @@ -222,10 +408,25 @@ impl<'a> OwnershipChecker<'a> { mut self, module: &Module, ) -> (Vec, HashMap>) { - self.collect_source_first_argument_views(module); for item in &module.items { match item { - Item::Function(func) => self.check_function(func), + Item::Function(func) => { + if self.resolve.is_none() || func.type_params.is_empty() { + self.check_function(func); + } else { + let bodies = self.generic_bodies; + for body in bodies { + if self.resolve.is_some_and(|resolve| { + resolve.scope_table.def(body.definition).span == func.name.span + }) { + let saved = + self.body_facts.replace(OwnershipBodyFacts::generic(body)); + self.check_function(func); + self.body_facts = saved; + } + } + } + } Item::Implement(block) => { for method in &block.methods { self.check_function(method); @@ -238,6 +439,19 @@ impl<'a> OwnershipChecker<'a> { } Item::Actor(actor) => self.observe_actor_breakpoints(actor), Item::VarDecl(decl) => self.check_var_decl(decl), + Item::Verify(verify) if self.resolve.is_some() => { + let saved = self.states.clone(); + self.check_block(&verify.body); + self.states = saved; + } + Item::Property(property) if self.resolve.is_some() => { + let saved = self.states.clone(); + for given in &property.givens { + self.register_checked_binding(&given.name); + } + self.check_block(&property.body); + self.states = saved; + } _ => {} } } @@ -253,11 +467,11 @@ impl<'a> OwnershipChecker<'a> { self.states.clear(); for field in &actor.state_fields { self.states.insert( - field.name.name.clone(), + self.binding_key(&field.name.name, field.name.span), VarInfo { state: OwnershipState::Owned, mutable: field.mutable, - type_id: self.resolve_type_for_ownership(&field.ty), + type_id: self.binding_type(&field.name, &field.ty), state_qualified_machine: matches!( field.ty, ast::TypeExpr::StateQualified(_, _, _) @@ -269,15 +483,15 @@ impl<'a> OwnershipChecker<'a> { } for param in actor.capability_params.iter().chain(&handler.params) { self.states.insert( - param.name.name.clone(), + self.binding_key(¶m.name.name, param.name.span), VarInfo { - state: if param.view { + state: self.binding_state(¶m.name).unwrap_or(if param.view { OwnershipState::Viewed } else { OwnershipState::Owned - }, + }), mutable: param.mutable, - type_id: self.resolve_type_for_ownership(¶m.ty), + type_id: self.binding_type(¶m.name, ¶m.ty), state_qualified_machine: matches!( param.ty, ast::TypeExpr::StateQualified(_, _, _) @@ -293,49 +507,6 @@ impl<'a> OwnershipChecker<'a> { self.states = saved; } - fn collect_source_first_argument_views(&mut self, module: &Module) { - let mut current_file = None; - let mut current_namespace: Option<&str> = None; - for item in &module.items { - let item_file = match item { - Item::Namespace(value) => value.span.file, - Item::Function(value) => value.span.file, - Item::Mutual(value) => value.span.file, - Item::Interface(value) => value.span.file, - Item::Implement(value) => value.span.file, - Item::Struct(value) => value.span.file, - Item::Bitfield(value) => value.span.file, - Item::Enum(value) => value.span.file, - Item::Machine(value) => value.span.file, - Item::Actor(value) => value.span.file, - Item::VarDecl(value) => value.span.file, - Item::Verify(value) => value.span.file, - Item::Property(value) => value.span.file, - Item::Resource(value) => value.span.file, - Item::TypeAlias(value) => value.span.file, - }; - if current_file.is_some_and(|file| file != item_file) { - current_namespace = None; - } - current_file = Some(item_file); - - match item { - Item::Namespace(namespace) => { - current_namespace = Some(&namespace.name.name); - } - Item::Function(function) - if function.params.first().is_some_and(|param| param.view) => - { - let name = current_namespace - .map(|namespace| format!("{namespace}.{}", function.name.name)) - .unwrap_or_else(|| function.name.name.clone()); - self.source_first_argument_views.insert(name); - } - _ => {} - } - } - } - // ------------------------------------------------------------------ // Implicitly copyable types // ------------------------------------------------------------------ @@ -356,16 +527,16 @@ impl<'a> OwnershipChecker<'a> { // Register parameters. for param in &func.params { - let type_id = self.resolve_type_for_ownership(¶m.ty); + let type_id = self.binding_type(¶m.name, ¶m.ty); let state = if param.view { OwnershipState::Viewed } else { OwnershipState::Owned }; self.states.insert( - param.name.name.clone(), + self.binding_key(¶m.name.name, param.name.span), VarInfo { - state, + state: self.binding_state(¶m.name).unwrap_or(state), mutable: param.mutable, type_id, state_qualified_machine: matches!( @@ -406,7 +577,7 @@ impl<'a> OwnershipChecker<'a> { Stmt::VarDecl(decl) => self.check_var_decl(decl), Stmt::Assign(assign) => self.check_assign(assign), Stmt::Return(ret) => self.check_return(ret), - Stmt::ComptimeTypeBind(bind) => self.check_block(&bind.body), + Stmt::ComptimeTypeBind(bind) => self.check_scoped_type_body(bind), Stmt::If(if_stmt) => self.check_if(if_stmt), Stmt::For(for_stmt) => self.check_for(for_stmt), Stmt::While(while_stmt) => self.check_while(while_stmt), @@ -444,7 +615,7 @@ impl<'a> OwnershipChecker<'a> { OwnershipState::Consumed | OwnershipState::Uninitialized ) }) - .map(|(name, _)| name.clone()) + .filter_map(|(key, _)| self.binding_label(key)) .collect(), ); } @@ -454,15 +625,16 @@ impl<'a> OwnershipChecker<'a> { fn record_debug_move(&mut self, expression: &Expr) { match expression { Expr::Ident(ident) => { - let info = self.states.get(&ident.name); + let info = self.states.get(&self.binding_key(&ident.name, ident.span)); let copyable = self - .debug_copyable - .get(&ident.span) - .copied() + .body_facts + .and_then(|facts| facts.type_map.get(&ident.span)) + .map(|&ty| self.is_copyable(ty)) + .or_else(|| self.debug_copyable.get(&ident.span).copied()) .unwrap_or_else(|| info.is_none_or(|info| self.is_copyable(info.type_id))); if !copyable && info.is_none_or(|info| info.state != OwnershipState::Viewed) { self.states - .entry(ident.name.clone()) + .entry(self.binding_key(&ident.name, ident.span)) .or_insert(VarInfo { state: OwnershipState::Owned, mutable: false, @@ -483,10 +655,12 @@ impl<'a> OwnershipChecker<'a> { let initial_state = self.initial_task_state(&decl.value); // Erasing an exact machine state into another owned local transfers // its value. Reusing the source requires an explicit clone. - if let Expr::Ident(source) = &decl.value + let destination_is_view = self.binding_state(&decl.name) == Some(OwnershipState::Viewed); + if !destination_is_view + && let Expr::Ident(source) = &decl.value && self .states - .get(&source.name) + .get(&self.binding_key(&source.name, source.span)) .is_some_and(|info| info.state_qualified_machine) { self.consume_expr(&decl.value, decl.value.span()); @@ -495,11 +669,15 @@ impl<'a> OwnershipChecker<'a> { } self.record_debug_move(&decl.value); - let type_id = self.resolve_type_for_ownership(&decl.ty); + let type_id = self.binding_type(&decl.name, &decl.ty); self.states.insert( - decl.name.name.clone(), + self.binding_key(&decl.name.name, decl.name.span), VarInfo { - state: initial_state, + state: if self.binding_state(&decl.name) == Some(OwnershipState::Viewed) { + OwnershipState::Viewed + } else { + initial_state + }, mutable: decl.mutable, type_id, state_qualified_machine: matches!(decl.ty, ast::TypeExpr::StateQualified(_, _, _)), @@ -516,7 +694,10 @@ impl<'a> OwnershipChecker<'a> { // If the target is an identifier, check if it's mutable and handle rebinding. if let Expr::Ident(ident) = &assign.target { - if let Some(info) = self.states.get_mut(&ident.name) { + if let Some(info) = self + .states + .get_mut(&self.binding_key(&ident.name, ident.span)) + { if info.mutable { // Rebinding may start a fresh pending task. info.state = initial_state; @@ -545,7 +726,7 @@ impl<'a> OwnershipChecker<'a> { Expr::Ident(ident) if self .states - .get(&ident.name) + .get(&self.binding_key(&ident.name, ident.span)) .is_some_and(|info| info.state == OwnershipState::Pending) => { OwnershipState::Pending @@ -556,6 +737,30 @@ impl<'a> OwnershipChecker<'a> { fn check_if(&mut self, if_stmt: &ast::IfStmt) { self.check_expr_ownership(&if_stmt.condition); + if let Some(selection) = self + .body_facts + .and_then(|facts| facts.static_selections) + .and_then(|choices| choices.get(&if_stmt.span)) + .copied() + { + match selection { + CheckedStaticSelection::IfThen => self.check_block(&if_stmt.then_block), + CheckedStaticSelection::IfElseIf(index) => { + if let Some((condition, body)) = if_stmt.else_ifs.get(index) { + self.check_expr_ownership(condition); + self.check_block(body); + } + } + CheckedStaticSelection::IfElse => { + if let Some(body) = &if_stmt.else_block { + self.check_block(body); + } + } + CheckedStaticSelection::IfNoBranch => {} + CheckedStaticSelection::MatchArm(_) => {} + } + return; + } let mut condition_state = self.states.clone(); let mut fallthrough_states = Vec::new(); @@ -588,9 +793,9 @@ impl<'a> OwnershipChecker<'a> { fn check_block_from_state( &mut self, - state: &HashMap, + state: &HashMap, block: &Block, - ) -> HashMap { + ) -> HashMap { self.states = state.clone(); self.check_block(block); self.states.clone() @@ -598,9 +803,9 @@ impl<'a> OwnershipChecker<'a> { fn merge_fallthrough_states( &self, - baseline: &HashMap, - branches: &[HashMap], - ) -> HashMap { + baseline: &HashMap, + branches: &[HashMap], + ) -> HashMap { if branches.is_empty() { return baseline.clone(); } @@ -690,7 +895,7 @@ impl<'a> OwnershipChecker<'a> { // We don't know the exact type here without the type map, so we use ERROR // as a placeholder — it will be treated as copyable for ownership purposes. self.states.insert( - for_stmt.variable.name.clone(), + self.binding_key(&for_stmt.variable.name, for_stmt.variable.span), VarInfo { state: if for_stmt.view { OwnershipState::Viewed @@ -698,19 +903,30 @@ impl<'a> OwnershipChecker<'a> { OwnershipState::Owned }, mutable: false, - type_id: TypeInterner::ERROR, // Element type not tracked here + type_id: self + .binding_fact(&for_stmt.variable) + .map(|fact| fact.ty) + .unwrap_or(TypeInterner::ERROR), state_qualified_machine: false, consumed_span: None, debug_consumed: false, }, ); + if let Some(name) = &for_stmt.value_variable { + self.register_checked_binding(name); + } self.check_block(&for_stmt.body); let mut body_state = self.states.clone(); - if let Some(outer) = entry_state.get(&for_stmt.variable.name) { - body_state.insert(for_stmt.variable.name.clone(), outer.clone()); + if let Some(outer) = + entry_state.get(&self.binding_key(&for_stmt.variable.name, for_stmt.variable.span)) + { + body_state.insert( + self.binding_key(&for_stmt.variable.name, for_stmt.variable.span), + outer.clone(), + ); } else { - body_state.remove(&for_stmt.variable.name); + body_state.remove(&self.binding_key(&for_stmt.variable.name, for_stmt.variable.span)); } let mut fallthrough_states = vec![entry_state.clone()]; @@ -744,13 +960,30 @@ impl<'a> OwnershipChecker<'a> { let baseline = self.states.clone(); let mut fallthrough_states = Vec::new(); - for arm in &match_stmt.arms { - let arm_state = self.check_block_from_state(&baseline, &arm.body); + let selected = self + .body_facts + .and_then(|facts| facts.static_selections) + .and_then(|choices| choices.get(&match_stmt.span)) + .and_then(|choice| match choice { + CheckedStaticSelection::MatchArm(index) => Some(*index), + _ => None, + }); + for (index, arm) in match_stmt.arms.iter().enumerate() { + if selected.is_some_and(|selected| selected != index) { + continue; + } + self.states = baseline.clone(); + if let ast::Pattern::Variant(_, bindings) = &arm.pattern { + for binding in bindings { + self.register_checked_binding(binding); + } + } + self.check_block(&arm.body); + let arm_state = self.states.clone(); if Self::block_can_fall_through(&arm.body) { fallthrough_states.push(arm_state); } } - self.states = self.merge_fallthrough_states(&baseline, &fallthrough_states); } @@ -765,7 +998,7 @@ impl<'a> OwnershipChecker<'a> { match expr { Expr::Ident(ident) => { // Reading a variable: check if it has been consumed. - if let Some(info) = self.states.get(&ident.name) { + if let Some(info) = self.states.get(&self.binding_key(&ident.name, ident.span)) { if info.state == OwnershipState::Consumed { if let Some(consumed_span) = info.consumed_span { self.diagnostics.push(use_after_move( @@ -815,11 +1048,14 @@ impl<'a> OwnershipChecker<'a> { self.consume_expr(value, value.span()); } } - Expr::Handle(target, _, body, _) => { + Expr::Handle(target, error_name, body, _) => { self.check_expr_ownership(target); self.record_debug_move(target); let success_state = self.states.clone(); - let handler_state = self.check_block_from_state(&success_state, body); + if let Some(name) = error_name { + self.register_checked_binding(name); + } + let handler_state = self.check_block_from_state(&self.states.clone(), body); let mut fallthrough_states = vec![success_state.clone()]; if Self::block_can_fall_through(body) { fallthrough_states.push(handler_state); @@ -846,25 +1082,50 @@ impl<'a> OwnershipChecker<'a> { self.check_expr_ownership(inner); } Expr::Pipeline(initial, steps, _) => { - if let Some(first_step) = steps.first() { - let (callee, _, piped_as_view) = Self::pipeline_step_call_parts(first_step); - if piped_as_view || self.first_argument_is_implicit_view(callee) { - self.check_expr_ownership(initial); - } else { - self.consume_expr(initial, initial.span()); - } - } else { + if steps.is_empty() { self.check_expr_ownership(initial); } - - for step in steps { - let (callee, extra_args, _) = Self::pipeline_step_call_parts(step); - self.check_call_arguments_ownership(callee, extra_args, 1); + for (step_index, step) in steps.iter().enumerate() { + let (callee, extra_args, piped_as_view) = Self::pipeline_step_call_parts(step); + if let Some(packet) = self + .body_facts + .and_then(|facts| facts.call_ownership.get(&step.span)) + { + for argument in &packet.arguments { + if argument.source_index == 0 { + if step_index == 0 { + self.check_argument_effect( + initial, + initial.span(), + argument.effect, + ); + } + } else if let Some(source) = extra_args.get(argument.source_index - 1) { + self.check_argument_effect( + &source.value, + source.span, + argument.effect, + ); + } + } + } else { + if step_index == 0 { + if piped_as_view { + self.check_expr_ownership(initial); + } else { + self.consume_expr(initial, initial.span()); + } + } + self.check_call_arguments_ownership(callee, extra_args, 1); + } self.check_expr_ownership(callee); if let Some(handle) = &step.handle { let success_state = self.states.clone(); + if let Some(name) = &handle.error_name { + self.register_checked_binding(name); + } let handler_state = - self.check_block_from_state(&success_state, &handle.body); + self.check_block_from_state(&self.states.clone(), &handle.body); let mut fallthrough_states = vec![success_state.clone()]; if Self::block_can_fall_through(&handle.body) { fallthrough_states.push(handler_state); @@ -890,15 +1151,15 @@ impl<'a> OwnershipChecker<'a> { let saved = std::mem::take(&mut self.states); for param in params { - let type_id = self.resolve_type_for_ownership(¶m.ty); + let type_id = self.binding_type(¶m.name, ¶m.ty); self.states.insert( - param.name.name.clone(), + self.binding_key(¶m.name.name, param.name.span), VarInfo { - state: if param.view { + state: self.binding_state(¶m.name).unwrap_or(if param.view { OwnershipState::Viewed } else { OwnershipState::Owned - }, + }), mutable: param.mutable, type_id, state_qualified_machine: matches!( @@ -941,11 +1202,14 @@ impl<'a> OwnershipChecker<'a> { // Only cancellation requires a still-pending task. if resolves_task { if let Expr::Ident(ident) = operand - && let Some(info) = self.states.get(&ident.name) + && let Some(info) = self.states.get(&self.binding_key(&ident.name, ident.span)) && info.state == OwnershipState::Pending { let copyable = self.is_copyable(info.type_id); - let info = self.states.get_mut(&ident.name).unwrap(); + let info = self + .states + .get_mut(&self.binding_key(&ident.name, ident.span)) + .unwrap(); info.state = if copyable { OwnershipState::Owned } else { @@ -963,7 +1227,10 @@ impl<'a> OwnershipChecker<'a> { .push(task_control_requires_pending(operation, span)); return; }; - let Some(info) = self.states.get_mut(&ident.name) else { + let Some(info) = self + .states + .get_mut(&self.binding_key(&ident.name, ident.span)) + else { self.diagnostics .push(task_control_requires_pending(operation, span)); return; @@ -993,7 +1260,32 @@ impl<'a> OwnershipChecker<'a> { /// Arguments passed with `view` (i.e., `Expr::View(inner)`) do not consume /// the inner value. Arguments passed without `view` consume the value if it /// is a non-copyable variable. - fn check_call_ownership(&mut self, callee: &Expr, args: &[CallArg], _span: Span) { + fn check_call_ownership(&mut self, callee: &Expr, args: &[CallArg], span: Span) { + if self + .body_facts + .is_some_and(|facts| facts.type_map.get(&span) == Some(&TypeInterner::ERROR)) + { + // An invalid invocation has no selected ownership handoff. Keep + // checking its evaluated dependencies, including valid nested + // calls, without consuming bare values on behalf of this call. + for argument in args { + self.check_expr_ownership(&argument.value); + } + self.check_expr_ownership(callee); + return; + } + if let Some(packet) = self + .body_facts + .and_then(|facts| facts.call_ownership.get(&span)) + { + for argument in &packet.arguments { + if let Some(source) = args.get(argument.source_index) { + self.check_argument_effect(&source.value, source.span, argument.effect); + } + } + self.check_expr_ownership(callee); + return; + } // Source evaluates arguments before selecting a function value. An // argument may consume an owner that the callee expression then reads. self.check_call_arguments_ownership(callee, args, 0); @@ -1006,19 +1298,15 @@ impl<'a> OwnershipChecker<'a> { args: &[CallArg], position_offset: usize, ) { - let first_arg_is_view = self.first_argument_is_implicit_view(callee); + let _ = (callee, position_offset); for (index, arg) in args.iter().enumerate() { - let argument_position = position_offset + index; + let _ = index; match &arg.value { Expr::View(inner, _) => { // `view x` — the argument is passed as a view; no consumption. self.check_expr_ownership(inner); } - _ if first_arg_is_view && argument_position == 0 => { - // First argument to a collection builtin is implicitly viewed. - self.check_expr_ownership(&arg.value); - } _ => { // Non-view argument — consumes the value. self.consume_expr(&arg.value, arg.span); @@ -1027,41 +1315,28 @@ impl<'a> OwnershipChecker<'a> { } } - fn first_argument_is_implicit_view(&self, callee: &Expr) -> bool { - // Source-owned stdlib views are collected from their declarations. - let collection_view_builtins: &[&str] = &["math.average", "math.median"]; - Self::dotted_name_str(callee) - .as_deref() - .map(|n| { - self.source_first_argument_views.contains(n) - || collection_view_builtins.contains(&n) - || is_json_implicit_view_facade(n) - }) - .unwrap_or(false) - } - - fn dotted_name_str(expr: &Expr) -> Option { - match expr { - Expr::Ident(ident) => Some(ident.name.clone()), - Expr::Paren(inner, _) => Self::dotted_name_str(inner), - Expr::FieldAccess(inner, field, _) => { - let prefix = Self::dotted_name_str(inner)?; - Some(format!("{prefix}.{}", field.name)) - } - _ => None, - } - } - /// Consume an expression. If the expression is a simple identifier for a /// non-copyable variable, mark it as consumed. If it was already consumed, /// emit a use-after-move error. fn consume_expr(&mut self, expr: &Expr, span: Span) { + if self + .body_facts + .is_some_and(|facts| facts.type_map.get(&expr.span()) == Some(&TypeInterner::ERROR)) + { + // A failed typed expression cannot acquire its child's ownership. + // Still read its dependencies so independent use-after-move errors + // are not hidden by recovery. + self.check_expr_ownership(expr); + return; + } match expr { Expr::Ident(ident) => { self.consume_variable(&ident.name, ident.span, span); self.record_debug_move(expr); } - Expr::Paren(inner, _) => self.consume_expr(inner, span), + Expr::Paren(inner, _) | Expr::Coarsen(inner, _) | Expr::Declassify(inner, _) => { + self.consume_expr(inner, span) + } Expr::View(inner, _) => { // `view x` at the expression level — does not consume. self.check_expr_ownership(inner); @@ -1076,7 +1351,7 @@ impl<'a> OwnershipChecker<'a> { /// Consume a named variable. Handles all the ownership state transitions /// and error reporting. fn consume_variable(&mut self, name: &str, name_span: Span, consume_span: Span) { - if let Some(info) = self.states.get(name) { + if let Some(info) = self.states.get(&self.binding_key(name, name_span)) { // Implicitly copyable values are never consumed. if self.is_copyable(info.type_id) { return; @@ -1096,7 +1371,10 @@ impl<'a> OwnershipChecker<'a> { } OwnershipState::Owned => { // Consume it. - let info = self.states.get_mut(name).unwrap(); + let info = self + .states + .get_mut(&self.binding_key(name, name_span)) + .unwrap(); info.state = OwnershipState::Consumed; info.consumed_span = Some(consume_span); } diff --git a/docs/active/native_acceptance_audit.md b/docs/active/native_acceptance_audit.md index fb80791a..a2e4cb2e 100644 --- a/docs/active/native_acceptance_audit.md +++ b/docs/active/native_acceptance_audit.md @@ -1912,3 +1912,34 @@ cases, 182 object obligations and 92 result groups recorded above. The same b718 follow-up. The exact94 workflow 37126837754 is still in progress. No linked native case or object obligation is added: fixed 207 inventory/182 obligations, 464 supplemental cases, about-85% planning and the whole-language 100% goal remain unchanged. + +## Typed caller ownership integration + +The isolated caller-ownership change over `a9a6cd63` implements Rule24 caller +disposition and Rule25 ordinary-data observation. Sealed source-call facts rejoin +exact declarations, argument permutations and raw/physical types through HIR, +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 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. diff --git a/docs/active/native_call_ownership_facts.md b/docs/active/native_call_ownership_facts.md new file mode 100644 index 00000000..bdd3da51 --- /dev/null +++ b/docs/active/native_call_ownership_facts.md @@ -0,0 +1,411 @@ +# 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 +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. + +A stable Windows native snapshot completed 472 of 479 supplemental test groups +successfully, with seven failures. Five failures subsequently passed focused +native replays: the nested breakpoint condition, primitive and refined interface +identity, contextual secret constructors in both profiles, and the 64-fixture +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 bounded builder-generation and handled-producer repairs are now implemented. +The actual focused gate passes all 66 MIR caller acquisition groups and all seven +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. + +Earlier integration receipts passed 598 frontend fixtures, 15 native caller groups +with source deletion and exact debug/release streams, and both property replays +that failed the earlier driver run. Their scope remains historical. The unchanged +exact-a9 baseline passed 464 native cases and all 182 object obligations; workflow +37130142896 passed all four Windows/Ubuntu build and installed jobs. Those baseline +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. +The full 100% native-codegen objective and rough 85% feature estimate remain open. + +## Existing authority and actual proof + +Rule10 (`docs/design.md:2091-2095`) and Rule24 (`:5794-5848`, especially the +four-row table at5821) determine caller ownership independently of callee access. +Written `view` keeps a caller owner; bare owned data is relinquished even when +the formal is View. Explicit view to Owned is E0375. A known move-only borrowed +binding cannot supply ownership; retain conservative E0401 for its bare input, +including a formal View destination. Exact implicitly copyable values copy. +Field reads borrow the parent and acquire the endpoint by the already selected +copy operation; Resource-bearing field copies remain prohibited. A view of a +producer needs a temporary backing owner, rather than an existing binding to +retain. Root flattening, general owner-changing overlap, alias expiry and projected +write policies remain unselected. The bounded builder-generation follow-up below +implements existing admitted source behavior. + +I independently read the immutable root-executed exact94 report with SHA +`a9dbc168c298b84d75a88c15442849ede9cac182bc572af0deff713651041e04`. +All10 ordinary sources have matching debug/release check outcomes (20checks), +no timeout/schema failure, and the five admitted default-profile reference runs +have exit0, empty stderr and debug0. Cases02/03/07/08 are admitted retention +holes; cases04/05/06/09/10 already give sole E0400. Case01's explicit views are +admitted. Case07 proves a source-first Owned destination is retained because +legacy analysis ignores the actual formal-to-source permutation. These are +historical exact94 observations, not current-a9 ordinary CLI re-execution. + +Root also actually built and executed the fixed checking-only a9v2 probe; +root-execution-proof-attempt2.json reports build0/execution0 and stable guards. +The separate actual results SHA is +`a18000c88557644514b6b64b30c495d4796de26192a18a50c9198ed34d966e89`. +All12 synthetic checks (six sources x debug/release) agree between profiles: +Resource01/03/04/06 are accepted;02/05 are rejected by the Check phase with +sole E0400 `` `owner` was consumed and cannot be used again ``. Accepted03's +actual formal-to-source permutation is [1,0]. Accepted snapshots retain exact +private Construct [View Network,Owned int64], BorrowOperation [View Network, +View Resource], and Close (the Finalize recipe) [Owned Resource] shapes. Formal modes and +permutations are present, but a caller retention handoff table is explicitly +unavailable. Thus the qualified/mixed/pipeline holes affect actual checked +Resource signatures, while the indirect/private-bare controls already reject. +Resource06 preserves explicit borrows then an owned Close boundary. + +The probe has no provider installation, interpreter/native invocation or +application stdout. Its historical hard-coded compiler_provenance string says +build proof pending; the separate root execution proof supplies actual build +and linkage provenance, without modifying that saved result. No runtime/native +cleanup result is inferred from reference value copies or synthetic preparation. + +## Schema + +Put the closed checker types in a new `jett_typecheck/src/caller_ownership.rs`. +The following describes concrete fields/variants, not applied Rust: + +- `CheckedCallerSyntax::{Bare, WrittenView}` records original spelling. Written + view is recovered only through Paren and already checked carrier-preserving + Coarsen/Declassify wrappers; it is never inferred from the formal or inserted + HIR/MIR View. The existing E0375 source-boundary check remains authoritative. +- `CheckedCalleeAccess::{Owned, View}` records the exact destination access. +- `CheckedCallerEffect::{Copy, RetainBorrow, TransferOwned, RelinquishOwned, + ObserveData}` records the separately validated caller disposition. ObserveData + is available only in the selected lexical test/debug context below. +- `CheckedBindingFact { definition: DefId, declaration_span: Span, ty: TypeId, + mode: CheckedBindingMode, mutable: bool }` anchors a binding to the same + resolution session and concrete body. Parameter facts include their actual + concrete types and View modes. Forwarded alias modes retain the existing + immediate `CheckedViewSource`; they are not replaced by the final owner. +- `CheckedCallerOrigin` is `Binding(CheckedBindingFact)`, + `BorrowedProjection { source: CheckedViewSource }`, + `OwnedFieldCopy { parent: CheckedViewSource }`, or `OwnedExpression`. + BorrowedProjection applies only to a checked explicit view of a field path. + A plain field is OwnedFieldCopy. Clone and owned calls/constructors are + OwnedExpression, including explicit views of their produced result. The + original typed field expression proves every owner/index/endpoint; no second + vector of mutable field indices is added. Source Other means no stable + original binding and cannot be upgraded to a retained caller owner. +- `CheckedArgumentOwnership { source_span: Span, source_index: usize, + parameter_index: usize, actual_type: TypeId, parameter_type: TypeId, + syntax: CheckedCallerSyntax, origin: CheckedCallerOrigin, + callee_access: CheckedCalleeAccess, effect: CheckedCallerEffect }`. +- `CheckedInvocationTarget` is an exact resolved source DefId, the existing + `CheckedGenericCall`, existing `CheckedMethodCall` or `CheckedInterfaceCall`, + an indirect Function TypeId, or closed IntrinsicId. No name/namespace/raw + spelling is an authority. +- `CheckedInvocationShape` is `Function { signature_type: TypeId }` for the + exact substituted source/indirect Function type, or `Intrinsic { intrinsic, + result_type: TypeId, operands: Vec }`. The closed + operand roles validate source arity/variadic shape and allowed source access, + rather than fabricating a source Function DefId or function arity. + Print/Println have one checked PrintArgument role per variadic source actual + and identity source order; the current checker has no ordinary signature or + call-order fact for them. Their permitted written views remain physically + borrowed and bare linear arguments remain consuming. Metadata appended by + HIR has a distinct compiler-added role, never a missing source actual. +- `CheckedCallOwnership { target, shape: CheckedInvocationShape, + context: CheckedOwnershipContext, arguments: Vec }` + stores arguments in lexical SOURCE order. `CheckedOwnershipContext` is + Ordinary, Verify, Property, or BreakpointExpression. Invocation syntax and + observing context are distinct facts; Trace is its existing statement. + +The effect is checked from the complete tuple rather than accepted as a free +Retain flag. Ordinary noncopyable Binding/Owned + Bare yields TransferOwned to +Owned, RelinquishOwned to View. WrittenView + View yields RetainBorrow, whether +its backing is an owned binding, known borrow, projected borrow or an owned +producer temporary. Known borrowed Binding + Bare is E0401 before an effect is +accepted. A new owned endpoint/producer follows the same callee access but has +no original binding to invalidate. Copyable type proof yields Copy and does not +invent authority for a Resource/capability. WrittenView + Owned remains E0375. +A temporary's RetainBorrow means protect its one acquired backing through this +operation, not retain a non-existent caller binding after it. + +The schema contains no public ticket ID, registry key, Resource runtime value, +Finalize permission, raw DefId foreign-session token or synthetic carrier. + +## Checker ownership contexts and integration + +Add `call_ownership: HashMap` and +`binding_facts: HashMap` to CheckResult, +CheckedGenericFunctionInstantiation, CheckedBodyFacts, TypeChecker and +ActiveGenericInstantiation. Binding maps use declaration-name Span to fit +existing capture/clear operations, while each value carries the exact DefId. +A scoped body's ownership walker inherits the exact incoming lexical binding +state/context; local facts are extracted by declaration span. Captured incoming +bindings use their parent concrete context, not a top-level definition_types +entry overwritten/restored by a different generic instantiation. + +Ownership context is lexical, not inherited from whoever triggered checking. +Use a private current-context stack/save-restore field for recording: entering +an ordinary named/generic/inline function body selects Ordinary, even when its +instantiation was requested from a verify/property/breakpoint expression. +Entering the actual verify/property body or breakpoint expression selects its +observation context; nested reflected scopes inherit that exact lexical owner. +Do not derive new caller effects solely from ambient in_verify_block/ +in_property_block flags. Existing generic checking already resets those flags +(checker.rs:7227-7228); follow that boundary and restore it for the new ownership +context too. check_function_impl itself has no equivalent reset. The caller site may +ObserveData while the checked callee body still consumes its Owned formals. +Add a source test instantiating the same generic body from a test block and an +ordinary function, proving that only the lexical test call has ObserveData. + +Recording routes to the last active generic frame or root, matching +record_call_argument_order/record_binding_mode. Wire initialization, completion, +clone/swap, checked_body_facts and clear_checked_body_facts. Nested +CheckedComptimeTypeBinding owns its own complete facts. Inline parameters, +locals, handler/match/loop bindings and incoming captures need exact facts +where they enter the existing checked environment. CheckResult.definition_types +is useful for ordinary declarations, but cannot substitute for per-instance +local facts after generic type_env restoration. + +After argument binding/type checking, record each accepted source invocation +once, using the same target/signature and formal-to-source permutation. This +covers ordinary direct, explicit/inferred generic, source method/interface, +indirect and pipeline call routes. Error recovery still checks malformed/excess +arguments normally; it must not manufacture a successful ownership fact. +Pipeline source0 is an explicit virtual source occurrence: first step uses the +original input origin; later steps use the previous evaluated producer result. +`into view` supplies WrittenView for source0. Extras retain their own syntax. + +Replace source-name first-View retention in ownership.rs with a borrowed +`OwnershipFacts` context over the same Module/ResolveResult/checker maps. +Use DefId-keyed state and exact declaration facts for typed invocation effects; +names are diagnostic labels only. Walk actual arguments in lexical source +order, looking up destination mode by each checked parameter_index. This +consumes a bare owner before any later actual can reread it, even when named +arguments were checked in formal order. Preserve the existing args-before- +indirect-callee evaluation order. Transparent wrappers recurse to the proven +binding; a field copy observes the parent without moving the whole parent. + +Ordinary bodies use root facts. Generic templates are checked at each concrete +instantiation (checker.rs:5748-5793,5843-5848); analyze each selected concrete +body with its own parameter types/facts, not the legacy AST `bytes` stand-in. +Nested scoped/reflected bodies push their own frame and consume the existing +CheckedStaticSelection/CheckedComptimeTypeSelection. Do not re-evaluate type +reflection or visit deliberately unchecked branches. Existing flow join/loop +conservatism is retained; this work grants no new borrow lifetime. Any legacy +untyped helper retained for AST unit tests must be separate from production +checked analysis and must not authorize source-name retention. + +Do not flatten generic maps by Span. finish_check's current flattened +with_debug_types copyability conjunction is observation compatibility only; it +is not caller authority. Concrete breakpoint/consumption observations should +stay with their owning frame, with any compatibility summary conservative and +never used to decide caller ownership. Keep existing exact span/code/message +suppression when a typed E0401 replaces its legacy counterpart, preserving +unrelated ownership diagnostics. + +## Math, JSON and debug are separate audits + +1. `stdlib/math.jett:28-31` declares Owned list parameters for average/median. + Rule24 therefore consumes a bare owning list. Their private compiler + intrinsic may physically read the operand; that does not retain the caller. + ownership.rs:1030-1041's literal math names lack independent selected + retention authority. The design's1648-1649 example reuses scores without + written view/clone although both formals are Owned; record/repair that stale + example during aligned documentation rather than adding an exception. + Likewise design5770/5807 forwards a borrowed data binding bare into the + SOURCE list.length wrapper. IntrinsicId::ListLength is list.__length; + from_callable_name does not classify list.length as that intrinsic, and the + real wrapper itself forwards with explicit view (stdlib/list.jett:11-12). + Correct those stale examples to written view. They are not independent + authority for implicitly forwarding into ordinary source wrappers. +2. `stdlib/json/90_public_api.jett:16-64` declares View for raw queries and + serializers, Owned copyable string for parsers. design3990 and6033/7057 + require explicit caller view for retained data and pipelines. Remove + is_json_implicit_view_facade as ownership authority for source calls. Keep + the public JSON policy/type/source handoff identities intact. When HIR + substitutes a trusted source facade/helper, preserve the ORIGINAL caller + disposition; the new implementation callee cannot retroactively retain it. +3. design6455-6475 explicitly allows ordinary-data observation in verify, + property and breakpoint expressions; represent that lexical context. + Authority-bearing capability/Resource input cannot gain an owner or clone + from it. Keep existing Resource observation/printing guards. Trace retains + its selected non-destructive statement semantics. Print/Println remain + closed variadic compiler operations: bare linear actuals do not gain a new + retention exception; explicit view physically borrows. Release checking, + debug event ordering and unresolved hidden-Secret rendering stay unchanged. + ObserveData preserves source availability only with an independently + validated ordinary-data acquisition: copyable inputs copy; a supported + cloneable data input to Owned gets one checked snapshot owner, and to View + is borrowed. It never changes an Owned formal into a borrowed physical + parameter. A function descriptor also needs its existing capture-ownership + validation; its Function signature alone does not prove authority-free + captures. Where supported snapshot acquisition is not proved, retain the + current lowering refusal rather than expanding cloning authority. This + slice changes neither absent/type-only controls nor the unsettled nested + hidden observation domain. Existing can_snapshot_view is a physical + supported-data predicate, not standalone source permission. + +For compiler-owned operations, enumerate callee access by closed IntrinsicId +and exact source-operand role, with evidence from the existing selected syntax +and checker. MIR intrinsic_borrows is a PHYSICAL access map, not sufficient +proof of caller retention. No blanket IntrinsicId exemption is allowed. Pure +scalar/metadata operands remain copyable; constructor payload transfer and +already selected explicit borrowed operands keep their operation-specific +rules. A source Resource hook uses its exact checked declaration/signature and +ordinary Rule24, not this intrinsic route. Unproven operation-specific source +syntax is a separate prerequisite to implementing that entry, not a fallback +Retain fact. + +## HIR, MIR and supported ordinary native lowering + +Attach a required typed call-ownership packet to HIR Call/IndirectCall/Intrinsic +(or one shared invocation operand structure used by those variants). Lower +source facts into formal argument order using the existing checked +source_indices and retain evaluation_order as the inverse lexical permutation. +Convert origin DefId to current-function LocalId through the existing local +join. Keep signature/target and original occurrence separate from the physical +operand. The packet retains actual source input type and expected parameter +type separately: an already checked interface/coarsen/secret conversion may +change the lowered operand type, so validate the existing typed conversion tree +instead of demanding an incorrect raw actual-equals-parameter equality. +Validate count, unique complete source/formal permutation, TypeIds, exact +mode/signature, valid immediate local source and typed field path before +any pruning, including dead/unused source calls. + +Compiler-generated calls use explicit generated-operation metadata with fully +validated argument acquisition/access; they do not use missing/None to retain +arbitrary source inputs. Helper dispatch, equality/refinement adapters and JSON +handoffs keep the original source packet when representing that invocation. +Extra reflected metadata operands are identified as generated copyable metadata +in a derived LOWERED operand shape, without changing the original source +argument count/permutation. Any generated linear operand still requires its +own checked acquisition. This +is not a caller-name or all-generated-call escape hatch. + +MIR preserves the packet through handlers, inline extraction, scoped views, +compaction and every LocalId remap. An inserted View for formal callee access +never changes WrittenView or RelinquishOwned into RetainBorrow. Existing +explicit source aliases remain persistent; compiler call-view scopes retain +their existing bounded lifetime. Original source roots are provenance, not +implicit liveness loans created from bare inputs. + +For supported ordinary linear data, lower RelinquishOwned-to-View by moving +the evaluated owned input once into an internal owning temporary at that +source-order position, then lending that temporary to the call. The original +binding is unavailable to later actuals/continuations. The temporary remains +owned across later-argument failure/early return and the complete call, then +normal owned cleanup releases it. No clone acquires authority. Physical View +access stays separate from ownership acquisition. Existing explicit +view-of-producer handling uses its one backing acquisition and no original +caller retention. At the initial HIR join, validate WrittenView against the original AST actual +and keep that source witness distinct from subsequently inserted physical +View. Later MIR validation checks the preserved packet/staging relation rather +than rediscovering caller syntax from the current operand wrapper. Validate +forged caller packets before MoveValuePlan and native eligibility, not after +successful optimizer pruning. + +This supported-data path is a caller ownership/ordinary cleanup proof only. +Type::Resource still has no native scalar/layout/runtime carrier or drop ledger; +this slice must not enable it in is_linear/scalar_kind/emission. Future live +Resource requires typed hook dispatch/effects, sealed dynamic identity, exact +owning tickets, partial-argument reverse cleanup and Resource finalization on +every exit. The new packet will be a prerequisite, not that implementation. + +## Bounded implementation files and proof + +Likely files: new typecheck/caller_ownership.rs; checker.rs exports/context wiring; +ownership.rs typed flow; HIR lib.rs plus call validation/helper consumers; +MIR lib.rs, handlers.rs, move_values.rs, sequences/prune.rs and generated/inline +remap consumers; ordinary native call emit/verification where acquisition must +survive physical borrowing. Resource runtime/providers are excluded. Update +aligned design/architecture and active Resource notes after root selects scope. + +Required tests are source-driven and run both frontend profiles: all20 old +characterizations are motivations, not expected legacy compatibility. Only01 +keeps caller owners;02/03/07/08 now reject the invalid reread, and04/05/06/09/10 +retain E0400. Add valid last-use versions, transparent wrappers, named mixed +permutation, direct/alias/indirect/pipeline forms, first versus later pipeline +producer, known-view aliases/parameters (exact E0401), copyable scalars, field +copy versus projected view, owned clone/result and explicit producer temporary. + +Pin exact caller packets for generic copyable versus linear instantiations and +nested reflected/scoped bodies sharing Span. Source order must catch duplicate +owner use across permuted actuals. Independently pin lexical observation context +and math/JSON source signatures without bare-retention whitelist. Use the six +exact synthetic hook sources with the checking-only catalog harness: all12 +pre-change outcomes are now actual root evidence. After repair, Resource01/03/04 +must reject their invalid rereads,02/05 remain E0400, and06 remains admitted. +Add valid last-use counterparts proving RelinquishOwned without requiring +Finalize execution. No provider executes. + +Original HIR/MIR malformed metadata tests must reject missing/count/permutation, +wrong signature/mode/type, forged source origin, alias claimed Owned and inserted +physical View claimed WrittenView, including unused calls before pruning. Native +supported-data controls cover both profiles, source deletion, exact stdout/debug +streams, partial-argument failure and balanced cleanup. Registration or a passing +metadata test is not live Resource or whole-language coverage acceptance. + +At selection, no implementation or tests have run for this change. The full100% objective, +about85% broad estimate, fixed207 inventory/182 object obligations and464 +supplemental native cases remain unchanged. + +## Root selection receipt + +Selected source contract SHA256: 963a045fbe05e40aa59c1ba6a6729a92fa6ccf83b9883b9dd4fcb8f09622ba17. +Manifest SHA256: 2b7bc6319cb79e0bb6d97d904bebf0820bb764d71b57be59b79234cd094b5e2a. +All 31 input guards and four artifact guards were checked before this write. +The selected behavior follows existing Rules 10 and 24. This note precedes +tracked Rust changes; the complete checker/HIR/MIR/ordinary cleanup scope above +remains required. The overall objective remains whole-language native parity. + +## Exact producer and capture handoff + +Root selected the additive caller-origin handoff before its Rust implementation at 2026-10-03T17:28:13.4850053Z (contract SHA256 dda43905e81f5d74ce939a9e1c3ad26098e1f06c42079a84b4f9f2fd21035665). Capture facts retain exact declaration DefIds in their concrete frame and are included by in-body source call occurrences even when their declarations are outside the body span. Resolved named functions, compiler-baked namespace constants and static namespace/type members are produced expressions; they cannot supply nonexistent caller-local roots. Local function-valued variables retain Binding provenance. No signatures alone prove closure capture cloning, no root/generic maps are flattened and no Resource authority is added. The selected handoff is implemented in this candidate; the header records current verification and remaining acceptance gates. + + +## Captured builder generations and handled conversion inputs + +The generation path is restricted to an original checked written view of an +owned mutable plain `TypeConstruction` binding. Its complete original call, +Source identifier fact, full declaration header and assignments remain sealed; +only current loan/root/RHS/site identities undergo verified canonical remaps. +The replacement RHS is evaluated before the captured old generation enters +private escrow. The current replacement and captured backing have separate, +once-only cleanup on completion and abandoned call paths. No builder clone, +public carrier, source ownership rule or ABI permission is introduced. Broader +simultaneous cohorts and other owner kinds remain unaccepted breadth. + +The handled-conversion path preserves original OwnedExpression/WrittenView/ +RetainBorrow authority, its private Handle witness, exact owning storage, +Begin/End lifetime and produced-result CFG. Fresh caller acquisitions associate +only a fully validated interface boxing occurrence with its actual concrete View +input. Move analysis borrows that input; the boxing result is independently owned +and the runtime may copy its payload. Other same-type/span occurrences grant no +permission, and this does not authorize a general View-to-owner escape or a +zero-runtime-copy claim. + +Root's actual focused receipts pass 66 MIR acquisition groups (2722a1), seven +generation groups (5f71c0), three native emission groups (76179f), the original +builder native regression (f9b4a4), and the new converted linked fixture (161f8b). +The last fixture observes payload lengths one and two, matches both profiles and +runs after source deletion. +The generation gate's earlier six-of-seven result was an invalid total-local-count +test assertion; its separate amendment checks the exact unused seed removal, +dense current IDs and immutable archives. Full acceptance remains pending. +Resource lifecycle/providers/native cleanup, concurrency breadth and the full +100% objective remain whole-goal work, with the broad estimate still about 85%. diff --git a/docs/active/native_codegen_parity_plan.md b/docs/active/native_codegen_parity_plan.md index 0e40cfc7..5bb0e60d 100644 --- a/docs/active/native_codegen_parity_plan.md +++ b/docs/active/native_codegen_parity_plan.md @@ -2271,3 +2271,37 @@ native case passes an isolated rerun, not whole-workspace acceptance. See [centralized acceptance context](native_acceptance_audit.md#retained-reference-preparation). The about-85% estimate, fixed 207 inventory/182 obligations, 464 supplemental native cases and whole-language 100% goal are unchanged. + +### Typed caller ownership integration + +The caller disposition selected by Rule24 now survives exact typed source, +HIR, MIR and native handoffs. Separate sealed proofs cover generated calls, +temporary staging, viewed loops and handled defaults; Rule25 data snapshots +preserve existing interface conversion. An earlier formatted integration +checkpoint passed 293 checker, 598 frontend, 168 HIR, 147 MIR, 67 backend and +63 object tests. At that checkpoint all 15 linked caller groups passed in both +profiles after source deletion, and the two failed driver property replays +passed independently. + +The current formatted compiler-core gate passes 304 checker, 188 HIR, 196 MIR, +70 backend and 63 object-emission tests, plus the MIR doctest. These current +counts remain separate from full-workspace, complete-native and supported-host +acceptance. Per-pass signature projection reuse preserves all validation +boundaries and exact diagnostics. + +The bounded handled-conversion follow-up now passes all 66 MIR acquisition +groups and a new linked payload-reading fixture in both profiles after source +deletion. Captured mutable plain `TypeConstruction` generations pass seven MIR +groups, three native emission groups and the original builder native regression. +All seven failures of the historical 479-group run now pass individual replays. +These paths preserve exact private Source/CFG authority and add no source clone +or ABI permission; runtime interface boxing may copy its payload. Broader +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 +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. diff --git a/docs/active/native_scoped_call_view_staging.md b/docs/active/native_scoped_call_view_staging.md index 066ab297..3a9f2106 100644 --- a/docs/active/native_scoped_call_view_staging.md +++ b/docs/active/native_scoped_call_view_staging.md @@ -44,14 +44,22 @@ eligibility; this change does not store borrowed views in owned data. No runtime ABI, reference count, owner pin, resource clone or escaping-view permission is introduced. -An erased owner consumed or rebound by a later operand while the first view is -still needed remains a separate source/checker reconciliation. Actual frontend -and interpreter admission of those overlap sources is recorded in -`target/native-call-view-owner-overlap-audit/contract.md` and the owner-change -witness package. Ending a loan cannot retain backing storage destroyed during -the operation. Existing borrowed-place safety is preserved; this implementation -selects neither a new source rejection rule nor arbitrary implicit authority -snapshots. Whole-language parity remains unfinished at that boundary. +The admitted owner-change path now includes an exact source-checked mutable +plain `TypeConstruction` binding captured by written `view`. Its current binding +and captured generation remain separate through later replacement RHS evaluation, +assignment and complete/aborted call cleanup. Private constructor-owned original +Source/local-header/assignment witnesses and exact CFG/loan joins authorize the +internal generation escrow. Archival identities do not become runtime locals or +liveness roots, and canonical preparation validates every current remap. + +Written views of handled owned producers preserve their exact original conversion +and normal owning backing. For interface boxing, an occurrence-specific validated +association permits the concrete input read while the new box remains owned. +Runtime boxing may copy that payload; no zero-runtime-copy guarantee, source +clone permission or ABI is added. Ending a loan alone does not preserve destroyed +backing. Broader simultaneous cohorts and other owner kinds, persistent source +alias owner changes, arbitrary erased-authority snapshots and whole-language +parity remain open. Root applied the guarded 90d82701 candidate only in the isolated managed staging worktree. Root repaired one Rust mutable-walk diagnostic with a typed recursive diff --git a/docs/architecture.md b/docs/architecture.md index 59173792..4a8129ff 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -639,7 +639,9 @@ This sub-phase tracks the ownership state of every variable through the control - A variable can only be used once unless it is a `view` or has an explicitly copyable type. Numeric primitives, `bool`, `nothing`, and immutable `string` are implicitly copyable; the primitive `bytes` type is move-only. -- After a variable is passed to a non-`view` parameter, it becomes `Consumed`. +- A bare move-only owned argument becomes `Consumed` even when its formal + parameter is `view`. Written `view` retains the caller owner only at a View + formal; an Owned formal requires an owned value or an explicit clone. - Using a `Consumed` variable is a compile error. - `view` parameters can read but not consume. - `view` values cannot be returned, stored in structs, or sent to actors. @@ -680,15 +682,88 @@ reject execution. The [open contract](open_design/projected_field_assignment.md) must settle safe updates or rejection of all projected writes before extending this handoff. -The checker also rejects known move-only views at owned return and argument -boundaries with E0401. It reuses declaration facts for view parameters, local -views, and forwarded aliases, retaining the borrow through parentheses, -`coarsen`, and `declassify` when the checked result is move-only. These checks -cover ordinary, generic, indirect, and pipeline calls, including actor handlers -and verify/property bodies. Explicit written view arguments retain E0375; -implicit-copy results, valid clones, ordinary owned field copies without -Resource data, and owned call results remain valid. Resource-bearing field -acquisition must use a nonowning view or a genuine owned transfer. +The checker rejects known move-only views at owned return boundaries and +ordinary bare argument boundaries with E0401, including calls whose formal is +View. It reuses declaration facts for view parameters, local views, and forwarded +aliases, retaining the borrow through parentheses, `coarsen`, and `declassify` +when the checked result is move-only. Ordinary, generic, indirect, pipeline and +actor calls obey the same boundary. Lexical verify, property and breakpoint +expressions retain Rule25's ordinary-data observation behavior only through a +validated copy, borrow or owning snapshot. Capability and Resource authority +cannot be acquired through observation. Explicit written view arguments to Owned +formals retain E0375; implicit-copy results, valid clones, ordinary owned field +copies without Resource data, and owned call results remain valid. +Resource-bearing field acquisition requires a nonowning view or a genuine owned +transfer. + +A rejected typed invocation has no ownership handoff. Ownership recovery reads +its dependencies in source order, preserving independent nested-call effects and +use-after-move errors. Invalid builtin type arguments retain the operand shape for +checking but have an ERROR result and cannot select an acquisition packet. + +Caller ownership facts retain the resolved target, raw source occurrence type, +formal access, original written syntax, exact binding identity, and lexical +argument order in each ordinary, concrete generic, and reflected body. Pipeline +inputs have a separate checked input fact from the step's callee and output; +sharing a syntax span does not merge these roles. HIR seals source facts before +physical conversions and retains generated metadata operands separately. A +physical View wrapper cannot turn a bare transfer into caller retention. + +MIR records finite owning, copying, observation and loan stages while preserving +the original source certificate. A relinquished View input first moves its full +endpoint into one owning temporary at its lexical position, then borrows that +temporary through the call. Compiler-generated calls seal their exact operation, +result, target and argument order through one fallible factory. Their stages +retain an immutable acquisition witness and a once-only private storage +association. Private generated-producer shapes preserve the exact operation, +target, typed occurrence, ordered fields and backing identities. Restoring a +materialized local requires its unique dominating owning initializer and the +same immutable producer shape. Ordinary snapshots require recursive ordinary-data +proof; a callable signature or opaque carrier cannot grant cloning authority. +MIR joins the packet to its exact initializer, Begin/End loan and typed original +occurrence before pruning, move planning or native eligibility. +Observation snapshots can precede existing interface conversion only when the +sealed raw source is ordinary data with an independently supported clone. MIR +clones that exact backing once and restores the existing conversion spine; +an erased Interface signature does not grant cloning authority. + +Intrinsic operands normalize their closed physical View roles before the common +ordered caller staging check. That check distinguishes relinquished owners from +retained borrows without using a collection's clone support as transfer authority. + +Extracted breakpoint conditions retain private constructor-owned lexical regions. +The ownership walker rejoins their exact statements, terminators, CFG boundaries +and final endpoints before selecting observation context. Canonical sequence and +uninhabited-sum preparation update these records through checked edits and remaps; +copied, moved or forged markers cannot grant observation context. Archived source +conditions do not keep removed blocks or locals alive. + +Compiler-generated refinement handlers retain their ordered exact predicate +identities, candidate backing, error-empty tests, selecting branches and shared +failure entry. One validated output and every owning default must reach the use +through that checked graph. Transparent default conversions cannot acquire a +borrowed loop binder. Canonical compaction remaps the private proof without +turning it into an execution read or cleanup owner. + +View-loop binders carry a private typed element/key/value proof only inside their +exact lexical loop body. MIR retains the original checked loop inventory while +canonical sequence preparation records the exact borrowed source, cursor, length, +binders and body/exit graph. Prepared records cannot manufacture a missing original +loop, authorize a binder outside its body or acquire its borrowed storage. Dense +compaction remaps these identities without treating proof metadata as execution +reads or cleanup owners. + +Canonical simplification of a checked uninhabited sum arm carries private +preparation metadata across compaction. Distinct absent-success and present-success +records preserve their exact source, output, runtime tag and selected edge. An +arbitrary Goto or default cannot replace the checked sum edge, and a retained +successful payload still requires its exact extraction and owning source. + +Expression lowering descends through existing containers and conversions when a +nested checked call requires owning staging, even if it contains no handler. +Short-circuit boolean operands retain their selected control-flow arm and lexical +evaluation order. These facts do not supply Resource runtime layout, providers, +async or actor execution. Directly written return-view annotations report E0401 by unique annotation span, including unused generic headers, interface/inherent/implementation @@ -1218,9 +1293,25 @@ operand, while break/continue respect the target loop's scope depth. Original metadata and every Begin/End path are validated before pruning; source alias names cannot authorize scope expiry. The emitter allocates no destructor slot for the internal alias and End emits no runtime ownership action. Existing -source alias loans remain persistent. Owner-changing overlap inside later -arguments stays a separate reconciliation; no implicit erased-authority clone, -owner pin, or runtime ABI is introduced. See the +source alias loans remain persistent. For an exact source-checked mutable plain +`TypeConstruction` root, constructor-owned records now preserve a captured +generation across later assignments. The original call, Source binding fact, +full local header and assignments remain sealed independently of current dense +IDs. Exact CFG and loan checks authorize a private escrow: the replacement RHS +is acquired before the first old generation moves there; subsequent replacement +owners and the captured generation retain separate cleanup. Canonical preparation +validates and remaps current records without turning archival IDs into reads. +Broader simultaneous cohorts and other owner kinds remain separate work; no +source clone, owner pin or runtime ABI permission is introduced. + +A handled owned producer under written `view` is materialized once through its +original checked endpoint conversion and exact owning Let/Begin/End proof. A +fresh validated `CallerAcquisitions` map associates only that exact interface +boxing occurrence with its proved concrete View input. Move analysis reads that +input borrowed while keeping the produced box owned; unrelated View initializers +retain their escape refusal. Runtime boxing may copy its payload, so this is not +a zero-runtime-copy guarantee. The original Source/Handle and produced-result CFG +checks remain mandatory. See the [scoped implementation contract](active/native_scoped_call_view_staging.md). Interpreter function and closure invocations expose their own lexical scopes and the initial global environment, excluding unrelated caller locals and diff --git a/docs/design.md b/docs/design.md index b074fa09..140c411e 100644 --- a/docs/design.md +++ b/docs/design.md @@ -5767,7 +5767,7 @@ Jett introduces one concept: the **view**. A view is a read-only, non-owning ref ``` function count_items(view data: list[Item]) returns int64: - return list.length[Item](data) + return list.length[Item](view data) function total_price(view items: list[Item]) returns float64: mutable float64 sum = 0.0 @@ -5804,7 +5804,7 @@ The `view` keyword appears in **both** declarations and call sites. The function ``` function count(view data: list[int64]) returns int64: - return list.length[int64](data) + return list.length[int64](view data) # Keep the value — view at call site: int64 len = count(view items) @@ -5924,8 +5924,17 @@ preserve pending metadata, while field reads retain their eager receiver checks. The operation's result is materialized before the internal scope ends. An aborted handler ends abandoned scopes before returning an owned operand, and loop exits abandon only scopes inside the exited operation. Persistent source -aliases retain the limits above. Owner changes during a later argument remain -a separate checker/native reconciliation; see the +aliases retain the limits above. Native staging now preserves the captured +generation of an exact source-checked mutable plain `TypeConstruction` binding +when a later argument assigns its replacement. The current binding and captured +backing remain separate, without a new source clone or ABI permission. Broader +simultaneous cohorts and other owner kinds still require reconciliation. + +A written view of an original handled owned producer also keeps its exact +checked conversion and temporary backing. An interface boxing conversion may +read that proved concrete input borrowed while producing an independently owned +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). **Rule 2: A view cannot be sent to another thread.** @@ -7054,7 +7063,7 @@ Jett keeps its symbol set **as small as possible**. Symbols are only used where | `:` | Type annotations, block starts | | `"` | Strings | -The `view` keyword is explicit at both call sites and declarations. When passing a value to a function that declares a `view` parameter, the caller must write `view`: `process(view data)`. In pipelines: `data into view json.serialize[Type]()`. +The `view` keyword appears at declarations and call sites. To retain an owned move-only value, or forward a borrowed value to a `view` parameter, write `view`: `process(view data)`. In pipelines: `data into view json.serialize[Type]()`. Rule24 also permits a bare owned last-use argument to a `view` parameter; the caller relinquishes that owner after the call. **Replaced by keywords:** diff --git a/stdlib/json/00_types_and_decls.jett b/stdlib/json/00_types_and_decls.jett index a72cee92..ccc71ef4 100644 --- a/stdlib/json/00_types_and_decls.jett +++ b/stdlib/json/00_types_and_decls.jett @@ -112,7 +112,7 @@ function json_reflected_raw_type(view info: TypeInfo) returns bool: if base.type_name == info.type_name: return false return json_reflected_raw_type(view base) -function json_type_primitive_is_signed_integer(primitive: TypePrimitive) returns bool: +function json_type_primitive_is_signed_integer(view primitive: TypePrimitive) returns bool: if primitive == TypePrimitive.int8_type: return true if primitive == TypePrimitive.int16_type: @@ -120,16 +120,16 @@ function json_type_primitive_is_signed_integer(primitive: TypePrimitive) returns if primitive == TypePrimitive.int32_type: return true return primitive == TypePrimitive.int64_type -function json_type_primitive_is_narrow_unsigned_integer(primitive: TypePrimitive) returns bool: +function json_type_primitive_is_narrow_unsigned_integer(view primitive: TypePrimitive) returns bool: if primitive == TypePrimitive.uint8_type: return true if primitive == TypePrimitive.uint16_type: return true return primitive == TypePrimitive.uint32_type -function json_type_primitive_uses_int64_json_integer(primitive: TypePrimitive) returns bool: - if json_type_primitive_is_signed_integer(primitive): +function json_type_primitive_uses_int64_json_integer(view primitive: TypePrimitive) returns bool: + if json_type_primitive_is_signed_integer(view primitive): return true - return json_type_primitive_is_narrow_unsigned_integer(primitive) + return json_type_primitive_is_narrow_unsigned_integer(view primitive) mutual: export function json_tree_parse(raw: string) returns result[JsonTree, string] function json_tree_parse_span(view input: bytes, start: int64, end: int64) returns result[JsonTree, string] diff --git a/stdlib/json/60_reflect_serialize.jett b/stdlib/json/60_reflect_serialize.jett index 4937b22b..0b9bd280 100644 --- a/stdlib/json/60_reflect_serialize.jett +++ b/stdlib/json/60_reflect_serialize.jett @@ -161,7 +161,7 @@ function json_serialize_public_primitive[T](view value: T, primitive: TypePrimit return json_serialize_public_bytes_value[T](view value) return "null" function json_serialize_public_integer_primitive[T](view value: T, primitive: TypePrimitive) returns optional[string]: - if json_type_primitive_uses_int64_json_integer(primitive): + if json_type_primitive_uses_int64_json_integer(view primitive): return some(json_serialize_public_integer_value[T](view value)) if primitive == TypePrimitive.uint64_type: return some(json_serialize_public_uint64_value[T](view value)) diff --git a/stdlib/json/70_reflect_decode.jett b/stdlib/json/70_reflect_decode.jett index 6fe4659d..2e124d77 100644 --- a/stdlib/json/70_reflect_decode.jett +++ b/stdlib/json/70_reflect_decode.jett @@ -368,7 +368,7 @@ function json_decode_tree_primitive_reflected[T](view raw: JsonTree, primitive: return ok(decoded_float) return ok(decoded_integer) function json_decode_tree_integer_primitive_reflected[T](view raw: JsonTree, primitive: TypePrimitive) returns result[optional[T], string]: - if json_type_primitive_uses_int64_json_integer(primitive): + if json_type_primitive_uses_int64_json_integer(view primitive): return json_decode_tree_integer_some_reflected[T](view raw, primitive) if primitive == TypePrimitive.uint64_type: return json_decode_tree_uint64_some_reflected[T](view raw) @@ -567,7 +567,7 @@ function json_tree_result_payload(view raw: JsonTree) returns result[JsonTreeObj return fail("expected result object, got {json_tree_kind(view raw)}") list[string] keys = json_tree_object_keys(view raw) handle error: return fail(error) - if list.length[string](keys) != 1: + if list.length[string](view keys) != 1: return fail("expected result object with exactly one key") string key = list.get[string](keys, 0) handle: return fail("expected result object with exactly one key") @@ -610,7 +610,7 @@ function json_decode_tree_enum_string_reflected[T](view raw: JsonTree, type_name function json_decode_tree_enum_object_reflected[T](view raw: JsonTree, type_name: string) returns result[T, string]: list[string] keys = json_tree_object_keys(view raw) handle error: return fail("expected enum string or object, got {json_tree_kind(view raw)}") - if list.length[string](keys) != 1: + if list.length[string](view keys) != 1: return fail("expected enum object with exactly one variant key") string variant_name = list.get[string](keys, 0) handle: return fail("expected enum object with exactly one variant key") diff --git a/stdlib/json/80_reflect_validate_exact.jett b/stdlib/json/80_reflect_validate_exact.jett index fae1fe1d..8d38784f 100644 --- a/stdlib/json/80_reflect_validate_exact.jett +++ b/stdlib/json/80_reflect_validate_exact.jett @@ -145,7 +145,7 @@ function json_validate_exact_enum_reflected[T](view raw: JsonTree, type_name: st return json_validate_exact_enum_string_reflected[T](view raw, type_name) list[string] keys = json_tree_object_keys(view raw) handle error: return fail("expected enum string or object, got {json_tree_kind(view raw)}") - if list.length[string](keys) != 1: + if list.length[string](view keys) != 1: return fail("expected enum object with exactly one variant key") string variant_name = list.get[string](keys, 0) handle: return fail("expected enum object with exactly one variant key") diff --git a/tests/compile_fail/view_owned_arguments.jett b/tests/compile_fail/view_owned_arguments.jett index f7adea38..b580e92c 100644 --- a/tests/compile_fail/view_owned_arguments.jett +++ b/tests/compile_fail/view_owned_arguments.jett @@ -1,4 +1,4 @@ -# ERROR: E0401 x11 +# ERROR: E0401 x9 namespace app function consume(values: list[int64]) returns int64: return list.length[int64](view values) diff --git a/tests/native/borrowed_return_clone_controls.jett b/tests/native/borrowed_return_clone_controls.jett index 859db300..19ddad2f 100644 --- a/tests/native/borrowed_return_clone_controls.jett +++ b/tests/native/borrowed_return_clone_controls.jett @@ -2,7 +2,7 @@ namespace app struct Record: values: list[int64] label: string -type NonEmptyNumbers = list[int64] where list.length(value) > 0 +type NonEmptyNumbers = list[int64] where list.length(view value) > 0 function copy_values[T](view values: list[T]) returns list[T]: return clone values function copy_record(view source: Record) returns Record: diff --git a/tests/native/coarsen_local_refinement_reuse.jett b/tests/native/coarsen_local_refinement_reuse.jett index 45a6fed6..4539aade 100644 --- a/tests/native/coarsen_local_refinement_reuse.jett +++ b/tests/native/coarsen_local_refinement_reuse.jett @@ -1,5 +1,5 @@ namespace app -type NonEmptyNumbers = list[int64] where list.length(value) > 0 +type NonEmptyNumbers = list[int64] where list.length(view value) > 0 function main() returns nothing: NonEmptyNumbers checked = list(2, 3) handle error: println(error) diff --git a/tests/native/interface_refined_containers.jett b/tests/native/interface_refined_containers.jett index 3bd42808..486a6563 100644 --- a/tests/native/interface_refined_containers.jett +++ b/tests/native/interface_refined_containers.jett @@ -9,11 +9,11 @@ type Callback = function(int8) returns int8 where true implement Named for Mapping: function name(view self: Mapping) returns string: map[string, int8] plain = coarsen self - return "mapping:{map.length[string, int8](plain)}" + return "mapping:{map.length[string, int8](view plain)}" implement Named for Members: function name(view self: Members) returns string: set[int8] plain = coarsen self - return "members:{set.length[int8](plain)}" + return "members:{set.length[int8](view plain)}" implement Named for Maybe: function name(view self: Maybe) returns string: int8 plain = coarsen self handle: diff --git a/tests/native/interface_refined_small_values.jett b/tests/native/interface_refined_small_values.jett index 101fccb2..ac283f7b 100644 --- a/tests/native/interface_refined_small_values.jett +++ b/tests/native/interface_refined_small_values.jett @@ -22,11 +22,11 @@ implement Named for Enabled: return "enabled:{coarsen self}" implement Named for bytes: function name(view self: bytes) returns string: - return "bytes:{bytes.to_hex(self)}" + return "bytes:{bytes.to_hex(view self)}" implement Named for Data: function name(view self: Data) returns string: - string text = bytes.to_hex(coarsen self) - int64 length = bytes.length(coarsen self) + string text = bytes.to_hex(view coarsen self) + int64 length = bytes.length(view coarsen self) if length == 0: return "empty" return "data:{text}" @@ -34,9 +34,9 @@ struct Packet: data: Data hidden: secret[bytes] function hex(view data: bytes) returns string: - return bytes.to_hex(data) + return bytes.to_hex(view data) function sized(view data: bytes, length: int64) returns string: - return "{bytes.to_hex(data)}:{length}" + return "{bytes.to_hex(view data)}:{length}" function borrowed() returns string: Packet packet = Packet(data: bytes.from_string("ok"), hidden: bytes.from_string("sh")) handle error: return error diff --git a/tests/native/local_view_aliases.jett b/tests/native/local_view_aliases.jett index 3c344839..844a62af 100644 --- a/tests/native/local_view_aliases.jett +++ b/tests/native/local_view_aliases.jett @@ -107,14 +107,14 @@ function handler_report() returns string: list[int64] borrowed = view source function(view list[int64], int64) returns int64 callback = selected_length function(view list[int64], int64) returns int64 callback_alias = view callback - int64 direct = selected_length(borrowed,(none handle: default 1)) + int64 direct = selected_length(view borrowed,(none handle: default 1)) list[int64] indirect_borrowed = view source - int64 indirect = callback_alias(indirect_borrowed,(none handle: default 2)) + int64 indirect = callback_alias(view indirect_borrowed,(none handle: default 2)) secret[string] hidden = "secret" secret[string] hidden_alias = view hidden string explicit = selected_secret(view hidden_alias,(none handle: default 3)) secret[string] implicit_alias = view hidden - string implicit = selected_secret(implicit_alias,(none handle: default 4)) + string implicit = selected_secret(view implicit_alias,(none handle: default 4)) Label label = "refined" handle error: return error Label label_alias = view label @@ -125,7 +125,7 @@ function copied_callbacks(view source: function(int64) returns int64) returns li return list(clone borrowed) function copied_secret_text(view source: secret[string]) returns optional[string]: secret[string] borrowed = view source - return some(declassify borrowed) + return some(declassify clone borrowed) function copied_texts(view source: string) returns list[string]: return list(view source, clone view source) function copied_text_sum(view source: string) returns optional[string]: diff --git a/tests/native/map_values.jett b/tests/native/map_values.jett index 9f019535..082c5e74 100644 --- a/tests/native/map_values.jett +++ b/tests/native/map_values.jett @@ -1,14 +1,14 @@ namespace app function main() returns nothing: mutable map[string, int64] scores = map("ada": 1, "bob": 2, "ada": 3) - println(map.length[string, int64](scores), map.has[string, int64](scores, "ada")) + println(map.length[string, int64](view scores), map.has[string, int64](view scores, "ada")) println(map.get_or[string, int64](clone scores, "ada", 0)) for key, value in clone scores: println(key, value) scores = map.insert[string, int64](scores, "bob", 7) println(map.get_or[string, int64](clone scores, "bob", 0)) scores = map.remove[string, int64](scores, "ada") - println(map.length[string, int64](scores), map.has[string, int64](scores, "ada")) + println(map.length[string, int64](view scores), map.has[string, int64](view scores, "ada")) mutable map[int64, string] names = map.new[int64, string]() names = map.insert[int64, string](names, 1, "first") names = map.insert[int64, string](names, 1, "second") @@ -17,9 +17,9 @@ function main() returns nothing: println(map.get_or[int64, string](clone names, 1, "missing")) println(map.get_or[int64, string](names, 2, "missing")) map[string, int64] zipped = map.from_lists[string, int64](list("x", "x", "y"), list(2, 5)) - println(map.length[string, int64](zipped), map.get_or[string, int64](zipped, "x", 0)) + println(map.length[string, int64](view zipped), map.get_or[string, int64](zipped, "x", 0)) map[string, int64] merged = map.merge[string, int64](map("left": 1), map("left": 4, "right": 5)) - println(map.length[string, int64](merged), map.get_or[string, int64](clone merged, "left", 0)) + println(map.length[string, int64](view merged), map.get_or[string, int64](clone merged, "left", 0)) for key in map.keys[string, int64](clone merged): println(key) for value in map.values[string, int64](merged): diff --git a/tests/native/refinement_local_source_reuse.jett b/tests/native/refinement_local_source_reuse.jett index 54d2037d..7ade38cd 100644 --- a/tests/native/refinement_local_source_reuse.jett +++ b/tests/native/refinement_local_source_reuse.jett @@ -1,5 +1,5 @@ namespace app -type NonEmptyNumbers = list[int64] where list.length(value) > 0 +type NonEmptyNumbers = list[int64] where list.length(view value) > 0 function inspect_source(source: list[int64]) returns string: NonEmptyNumbers checked = source handle error: trace source diff --git a/tests/native/set_values.jett b/tests/native/set_values.jett index dad28b7a..fc57d24d 100644 --- a/tests/native/set_values.jett +++ b/tests/native/set_values.jett @@ -4,9 +4,9 @@ function main() returns nothing: numbers = set.add[int64](numbers, -2) numbers = set.add[int64](numbers, 7) numbers = set.add[int64](numbers, -2) - println(set.length[int64](numbers), set.contains[int64](numbers, 7)) + println(set.length[int64](view numbers), set.contains[int64](view numbers, 7)) numbers = set.remove[int64](numbers, -2) - println(set.length[int64](numbers), set.contains[int64](numbers, -2)) + println(set.length[int64](view numbers), set.contains[int64](view numbers, -2)) mutable int64 total = 0 for item in numbers: total = total + item @@ -17,8 +17,8 @@ function main() returns nothing: names = set.add[string](names, "ada") set[string] copy = clone names names = set.remove[string](names, "ada") - println(set.length[string](names), set.contains[string](names, "ada")) - println(set.length[string](copy), set.contains[string](copy, "ada")) + println(set.length[string](view names), set.contains[string](view names, "ada")) + println(set.length[string](view copy), set.contains[string](view copy, "ada")) for item in view names: println(item) for item in copy: diff --git a/tests/run_pass/bytes_operations.jett b/tests/run_pass/bytes_operations.jett index 276d7c47..57577f1d 100644 --- a/tests/run_pass/bytes_operations.jett +++ b/tests/run_pass/bytes_operations.jett @@ -133,12 +133,12 @@ verify concat_empty_test: # --- ownership contract --- function observer_views_preserve_owner() returns string: bytes original = bytes.from_string("Hi") - int64 length = bytes.length(original) - int64 first = bytes.get(original, 0) handle: + int64 length = bytes.length(view original) + int64 first = bytes.get(view original, 0) handle: default -1 - bytes piece = bytes.slice(original, 0, 1) - string hex = bytes.to_hex(original) - string text = bytes.to_string(original) handle error: + bytes piece = bytes.slice(view original, 0, 1) + string hex = bytes.to_hex(view original) + string text = bytes.to_string(view original) handle error: default error return "{length}:{first}:{bytes.length(piece)}:{hex}:{text}:{bytes.length(original)}" verify observer_views_preserve_owner: diff --git a/tests/run_pass/csv_escaping_helpers.jett b/tests/run_pass/csv_escaping_helpers.jett index c89d313e..df0753e8 100644 --- a/tests/run_pass/csv_escaping_helpers.jett +++ b/tests/run_pass/csv_escaping_helpers.jett @@ -13,7 +13,7 @@ verify csv_escaped_quote_test: function csv_trailing_crlf_test() returns string: list[list[string]] rows = csv.parse("a,b\r\n1,2\r\n") handle error: return error - list[string] second_row = list.get[list[string]](rows, 1) handle: + list[string] second_row = list.get[list[string]](view rows, 1) handle: default list() return "{list.length(rows)}:{list.length[string](second_row)}" verify csv_trailing_crlf_test: diff --git a/tests/run_pass/csv_operations.jett b/tests/run_pass/csv_operations.jett index a36caa34..795acc4e 100644 --- a/tests/run_pass/csv_operations.jett +++ b/tests/run_pass/csv_operations.jett @@ -56,9 +56,9 @@ verify csv_parse_empty_test: function csv_parse_single_blank_line_test() returns string: list[list[string]] rows = csv.parse("\n") handle error: return error - list[string] blank = list.get[list[string]](rows, 0) handle: + list[string] blank = list.get[list[string]](view rows, 0) handle: return "missing blank record" - string value = list.get[string](blank, 0) handle: + string value = list.get[string](view blank, 0) handle: return "missing empty field" return "{list.length[list[string]](rows)}:{list.length[string](blank)}:{value}" verify csv_parse_single_blank_line_test: @@ -66,9 +66,9 @@ verify csv_parse_single_blank_line_test: function csv_parse_blank_record_test() returns string: list[list[string]] rows = csv.parse("first\n\nthird") handle error: return error - list[string] blank = list.get[list[string]](rows, 1) handle: + list[string] blank = list.get[list[string]](view rows, 1) handle: return "missing blank record" - string value = list.get[string](blank, 0) handle: + string value = list.get[string](view blank, 0) handle: return "missing empty field" return "{list.length[list[string]](rows)}:{list.length[string](blank)}:{value}" verify csv_parse_blank_record_test: @@ -76,9 +76,9 @@ verify csv_parse_blank_record_test: function csv_parse_ragged_rows_test() returns string: list[list[string]] rows = csv.parse("a,b\n1\n2,3,4") handle error: return error - list[string] short = list.get[list[string]](rows, 1) handle: + list[string] short = list.get[list[string]](view rows, 1) handle: return "missing short row" - list[string] long = list.get[list[string]](rows, 2) handle: + list[string] long = list.get[list[string]](view rows, 2) handle: return "missing long row" return "{list.length[list[string]](rows)}:{list.length[string](short)}:{list.length[string](long)}" verify csv_parse_ragged_rows_test: diff --git a/tests/run_pass/json_parse_exact.jett b/tests/run_pass/json_parse_exact.jett index db256cfa..88b73f01 100644 --- a/tests/run_pass/json_parse_exact.jett +++ b/tests/run_pass/json_parse_exact.jett @@ -161,7 +161,7 @@ function parse_exact_accepts_legacy_json_value_payload() returns string: string raw = "{{\"answer\":43,\"extra\":{{\"nested\":true}}}}" json.JsonTree tree = json.parse_exact[json.JsonTree](raw) handle error: return error - json.JsonTree answer = json.field(tree, "answer") handle: + json.JsonTree answer = json.field(view tree, "answer") handle: return "missing" int64 value = json.as_int64(answer) handle error: return error diff --git a/tests/run_pass/json_raw_facade_tree_surface.jett b/tests/run_pass/json_raw_facade_tree_surface.jett index 22805133..f812c273 100644 --- a/tests/run_pass/json_raw_facade_tree_surface.jett +++ b/tests/run_pass/json_raw_facade_tree_surface.jett @@ -4,39 +4,39 @@ function raw_tree_facade_root() returns json.JsonTree: return json.parse_raw("{{\"name\":\"Ada\",\"scores\":[3,5],\"active\":true,\"ratio\":2.5,\"missing\":null}}") handle error: default json.JsonTree.string_value(error) function raw_tree_facade_name(view root: json.JsonTree) returns string: - json.JsonTree name = json.field(root, "name") handle: + json.JsonTree name = json.field(view root, "name") handle: return "missing name" return json.as_string(name) handle error: default error function raw_tree_facade_scores(view root: json.JsonTree) returns string: - json.JsonTree scores = json.field(root, "scores") handle: + json.JsonTree scores = json.field(view root, "scores") handle: return "missing scores" - int64 score_count = json.array_length(scores) handle error: + int64 score_count = json.array_length(view scores) handle error: return error - json.JsonTree second = json.index(scores, 1) handle: + json.JsonTree second = json.index(view scores, 1) handle: return "missing score" - int64 second_score = json.as_int64(second) handle error: + int64 second_score = json.as_int64(view second) handle error: return error return "{json.is_array(scores)}:{score_count}:{second_score}:{json.is_number(second)}" function raw_tree_facade_scalars(view root: json.JsonTree) returns string: - json.JsonTree active = json.field(root, "active") handle: + json.JsonTree active = json.field(view root, "active") handle: return "missing active" - bool active_value = json.as_bool(active) handle error: + bool active_value = json.as_bool(view active) handle error: return error - json.JsonTree ratio = json.field(root, "ratio") handle: + json.JsonTree ratio = json.field(view root, "ratio") handle: return "missing ratio" float64 ratio_value = json.as_float64(ratio) handle error: return error - json.JsonTree missing = json.field(root, "missing") handle: + json.JsonTree missing = json.field(view root, "missing") handle: return "missing null" return "{active_value}:{json.is_bool(active)}:{ratio_value}:{json.is_null(missing)}" function raw_tree_facade_root_summary(view root: json.JsonTree) returns string: - string root_kind = json.kind(root) - bool root_is_object = json.is_object(root) - list[string] keys = json.object_keys(root) handle error: + string root_kind = json.kind(view root) + bool root_is_object = json.is_object(view root) + list[string] keys = json.object_keys(view root) handle error: return error - string raw = json.serialize_raw(root) - string root_kind_after_reads = json.kind(root) + string raw = json.serialize_raw(view root) + string root_kind_after_reads = json.kind(view root) bool raw_mentions_name = string.contains(raw, "Ada") return "{root_kind}:{root_is_object}:{list.length[string](view keys)}:{raw_mentions_name}:{root_kind_after_reads}" function raw_tree_facade_summary() returns string: diff --git a/tests/run_pass/json_raw_strict_accessors.jett b/tests/run_pass/json_raw_strict_accessors.jett index 87805c07..db8e51be 100644 --- a/tests/run_pass/json_raw_strict_accessors.jett +++ b/tests/run_pass/json_raw_strict_accessors.jett @@ -149,15 +149,15 @@ function strict_optional_accessors_accept_namespaced_value() returns string: string item_text = json.as_string(view item) handle error: return error return "{field_text}:{item_text}" -function strict_accessors_accept_implicit_view_calls() returns string: +function strict_accessors_retain_with_explicit_view() returns string: json.JsonTree root = strict_root() - json.JsonTree name = json.require_field(root, "name") handle error: + json.JsonTree name = json.require_field(view root, "name") handle error: return error optional[json.JsonTree] maybe_scores = json.object_field(root, "scores") handle error: return error json.JsonTree scores = maybe_scores handle: return "missing scores" - optional[json.JsonTree] maybe_second = json.array_index(scores, 1) handle error: + optional[json.JsonTree] maybe_second = json.array_index(view scores, 1) handle error: return error json.JsonTree second = maybe_second handle: return "missing second" @@ -189,4 +189,4 @@ verify json_raw_strict_accessors: assert strict_accessors_accept_legacy_value() == "Grace" assert strict_accessors_accept_namespaced_value() == "Lin:5" assert strict_optional_accessors_accept_namespaced_value() == "field absent:index absent" - assert strict_accessors_accept_implicit_view_calls() == "Ada:5:5" + assert strict_accessors_retain_with_explicit_view() == "Ada:5:5" diff --git a/tests/run_pass/json_raw_tree_parity.jett b/tests/run_pass/json_raw_tree_parity.jett index f5d00dd0..a09a9dc8 100644 --- a/tests/run_pass/json_raw_tree_parity.jett +++ b/tests/run_pass/json_raw_tree_parity.jett @@ -12,7 +12,7 @@ function raw_tree_object_kind_summary() returns string: return "raw parse: {error}" json.JsonTree tree = json.json_tree_parse(body) handle error: return "tree parse: {error}" - return "{json.kind(raw)}:{json.is_object(raw)}|{json.json_tree_kind(view tree)}:{json.json_tree_is_object(view tree)}" + return "{json.kind(view raw)}:{json.is_object(raw)}|{json.json_tree_kind(view tree)}:{json.json_tree_is_object(view tree)}" function raw_tree_string_field_summary() returns string: string body = "{{\"name\":\"Ada\"}}" json.JsonTree raw = json.parse_raw(body) handle error: @@ -34,7 +34,7 @@ function raw_array_field_summary() returns string: return "raw parse: {error}" json.JsonTree raw_scores = json.field(raw, "scores") handle: return "raw missing scores" - int64 raw_score_count = json.array_length(raw_scores) handle error: + int64 raw_score_count = json.array_length(view raw_scores) handle error: return "raw score length: {error}" json.JsonTree raw_second = json.index(raw_scores, 1) handle: return "raw missing second" @@ -58,9 +58,9 @@ function raw_scalar_field_summary() returns string: string body = "{{\"active\":true,\"missing\":null,\"ratio\":2.5}}" json.JsonTree raw = json.parse_raw(body) handle error: return "raw parse: {error}" - json.JsonTree raw_active = json.field(raw, "active") handle: + json.JsonTree raw_active = json.field(view raw, "active") handle: return "raw missing active" - json.JsonTree raw_missing = json.field(raw, "missing") handle: + json.JsonTree raw_missing = json.field(view raw, "missing") handle: return "raw missing null" json.JsonTree raw_ratio = json.field(raw, "ratio") handle: return "raw missing ratio" diff --git a/tests/run_pass/json_tree_value.jett b/tests/run_pass/json_tree_value.jett index f2fc2ef5..9fb90233 100644 --- a/tests/run_pass/json_tree_value.jett +++ b/tests/run_pass/json_tree_value.jett @@ -23,7 +23,7 @@ function json_tree_roundtrip_through_raw() returns bool: return decoded == "Ada" function json_tree_serialize_does_not_consume_tree() returns string: json.JsonTree tree = sample_tree() - string raw = json.json_tree_serialize(tree) + string raw = json.json_tree_serialize(view tree) string kind = json.json_tree_kind(tree) bool has_name = string.contains(raw, "\"Ada\"") return "{kind}:{has_name}" diff --git a/tests/run_pass/list_access_source.jett b/tests/run_pass/list_access_source.jett index 6c745bb7..94a431ac 100644 --- a/tests/run_pass/list_access_source.jett +++ b/tests/run_pass/list_access_source.jett @@ -1,14 +1,14 @@ namespace test function inferred_first_reuses_list() returns int64: list[int64] items = list(10, 20, 30) - optional[int64] first = list.first(items) + optional[int64] first = list.first(view items) int64 value = first handle: default 0 return value + list.length(items) verify inferred_first_reuses_list: assert inferred_first_reuses_list() == 13 function inferred_first_from_view(view items: list[int64]) returns int64: - optional[int64] first = list.first(items) + optional[int64] first = list.first(view items) return first handle: default 0 function inferred_first_view_parameter() returns int64: @@ -20,7 +20,7 @@ verify inferred_first_view_parameter: function inferred_pipeline_first_reuses_list() returns int64: list[int64] items = list(60, 70) int64 first = items - into list.first() handle: + into view list.first() handle: default 0 return first + list.length(items) verify inferred_pipeline_first_reuses_list: @@ -35,8 +35,8 @@ verify explicit_last_reuses_list: assert explicit_last_reuses_list() == 33 function empty_first_and_last() returns bool: list[int64] items = list() - optional[int64] first = list.first(items) - optional[int64] last = list.last[int64](items) + optional[int64] first = list.first(view items) + optional[int64] last = list.last[int64](view items) int64 first_value = first handle: default 7 int64 last_value = last handle: @@ -47,8 +47,8 @@ verify empty_first_and_last: function is_empty_outcomes_reuse_lists() returns bool: list[int64] empty = list() list[int64] filled = list(1) - bool empty_result = list.is_empty[int64](empty) - bool filled_result = list.is_empty(filled) + bool empty_result = list.is_empty[int64](view empty) + bool filled_result = list.is_empty(view filled) return empty_result and not filled_result and list.length(empty) == 0 and list.length(filled) == 1 verify is_empty_outcomes_reuse_lists: assert is_empty_outcomes_reuse_lists() == true