use std::any::Any; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use aster_core::{ CodeLayout, CompareOp, EndorCode, EndorCodeObject, EndorNativeFunctionObject, ExecutionLimits, Instruction, ObjectPayload, ObjectRef, Opcode, Value, Vm, VmError, }; fn op(opcode: Opcode, argument: u32) -> Instruction { Instruction::new(opcode, argument) } fn simple(opcode: Opcode) -> Instruction { Instruction::simple(opcode) } fn text(value: &str) -> Value { Value::Text(value.into()) } fn make_code( vm: &mut Vm, instructions: &[Instruction], constants: Vec, layout: CodeLayout, ) -> ObjectRef { vm.new_code(EndorCode::from_instructions(instructions, constants, layout).unwrap()) .unwrap() } fn return_constant(vm: &mut Vm, value: Value) -> ObjectRef { make_code( vm, &[op(Opcode::LoadConst, 0), simple(Opcode::ReturnValue)], vec![value], CodeLayout::default(), ) } #[test] fn wordcode_extended_operands_decode_and_execute_without_truncation() { let encoded = Instruction::encode_all(&[op(Opcode::LoadConst, 0x01020304)]).unwrap(); assert_eq!(encoded, vec![255, 1, 255, 2, 255, 3, 1, 4]); let mut vm = Vm::new().unwrap(); let mut constants = vec![Value::Nil; 257]; constants[256] = text("extended"); let code = make_code( &mut vm, &[op(Opcode::LoadConst, 256), simple(Opcode::ReturnValue)], constants, CodeLayout::default(), ); let executable = vm.payload::(code, "code").unwrap().code(); assert_eq!(executable.instructions()[0].offset, 0); assert_eq!(executable.instructions()[0].next_offset, 2); assert_eq!(executable.stack_size(), 1); let globals = vm.new_dict().unwrap(); assert_eq!(vm.execute(code, globals).unwrap(), text("extended")); } #[test] fn malformed_wordcode_and_unaligned_jumps_are_rejected_at_construction() { for bytes in [ vec![], vec![0], vec![254, 0], vec![255, 1], vec![255, 1, 255, 1, 255, 1, 255, 1, 1, 0], vec![13, 1], ] { assert!(matches!( EndorCode::new(bytes, vec![Value::Nil], vec![], vec![]), Err(VmError::InvalidBytecode { .. }) )); } // Jump target 2 is inside a prefixed LOAD_CONST starting at code unit 1. let bytes = vec![14, 1, 255, 1, 1, 0, 13, 0]; assert!(EndorCode::new(bytes, vec![Value::Nil; 257], vec![], vec![]).is_err()); assert!( EndorCode::from_instructions( &[op(Opcode::LoadConst, 1), simple(Opcode::ReturnValue)], vec![Value::Nil], CodeLayout::default() ) .is_err() ); } #[test] fn verifier_rejects_underflow_fallthrough_bad_cells_and_inconsistent_stack_joins() { let cases = [ vec![simple(Opcode::PopTop), simple(Opcode::ReturnValue)], vec![op(Opcode::LoadConst, 0)], vec![op(Opcode::LoadConst, 0), op(Opcode::JumpBackward, 2)], vec![op(Opcode::LoadDeref, 0), simple(Opcode::ReturnValue)], vec![ op(Opcode::LoadConst, 0), op(Opcode::PopJumpIfFalse, 2), op(Opcode::LoadConst, 0), op(Opcode::JumpForward, 1), simple(Opcode::Nop), op(Opcode::LoadConst, 0), simple(Opcode::ReturnValue), ], ]; for instructions in cases { assert!( EndorCode::from_instructions(&instructions, vec![Value::Nil], CodeLayout::default()) .is_err() ); } let layout = CodeLayout { locals: vec!["captured".into()], cell_variables: vec!["captured".into()], ..CodeLayout::default() }; assert!( EndorCode::from_instructions( &[op(Opcode::LoadFast, 0), simple(Opcode::ReturnValue)], vec![], layout ) .is_err() ); } #[test] fn execute_moves_values_between_constants_local_slots_and_globals() { let mut vm = Vm::new().unwrap(); assert_eq!(vm.object_count(), 0); let globals = vm.new_dict().unwrap(); let code = make_code( &mut vm, &[ op(Opcode::LoadConst, 0), op(Opcode::StoreFast, 0), op(Opcode::LoadFast, 0), op(Opcode::StoreGlobal, 0), op(Opcode::LoadGlobal, 0), simple(Opcode::ReturnValue), ], vec![text("hello")], CodeLayout { locals: vec!["local".into()], names: vec!["saved".into()], ..CodeLayout::default() }, ); assert_eq!(vm.execute(code, globals).unwrap(), text("hello")); assert_eq!( vm.dict_get(globals, &text("saved")).unwrap(), Some(text("hello")) ); assert!(vm.last_traceback().is_empty()); vm.release(code).unwrap(); vm.release(globals).unwrap(); assert_eq!(vm.object_count(), 0); } #[derive(Debug)] struct Tick(Arc); impl ObjectPayload for Tick { fn as_any(&self) -> &dyn Any { self } fn kind_name(&self) -> &'static str { "tick" } fn call(&self, arguments: &[Value]) -> Result { assert!(arguments.is_empty()); Ok(Value::Bool(self.0.fetch_add(1, Ordering::SeqCst) < 3)) } } #[test] fn relative_branches_and_back_edges_execute_a_loop() { let mut vm = Vm::new().unwrap(); let count = Arc::new(AtomicUsize::new(0)); let tick = vm.allocate(Tick(count.clone())).unwrap(); let globals = vm.new_dict().unwrap(); vm.dict_set(globals, text("tick"), Value::Object(tick)) .unwrap(); let code = make_code( &mut vm, &[ op(Opcode::LoadGlobal, 0), simple(Opcode::PushNull), op(Opcode::Call, 0), op(Opcode::PopJumpIfFalse, 1), op(Opcode::JumpBackward, 5), op(Opcode::LoadConst, 0), simple(Opcode::ReturnValue), ], vec![text("done")], CodeLayout { names: vec!["tick".into()], ..CodeLayout::default() }, ); assert_eq!(vm.execute(code, globals).unwrap(), text("done")); assert_eq!(count.load(Ordering::SeqCst), 4); } #[test] fn nested_language_calls_bind_defaults_and_return_owned_argument_objects() { let mut vm = Vm::new().unwrap(); let globals = vm.new_dict().unwrap(); let identity = make_code( &mut vm, &[op(Opcode::LoadFast, 0), simple(Opcode::ReturnValue)], vec![], CodeLayout { parameters: vec!["value".into()], locals: vec!["value".into()], ..CodeLayout::default() }, ); let default = vm.new_string("default").unwrap(); let function = vm .new_function( "identity", identity, globals, vec![Value::Object(default)], vec![], ) .unwrap(); let before = vm.ref_count(default).unwrap(); let returned = vm.call(function, &[]).unwrap(); assert_eq!(returned, Value::Object(default)); assert_eq!(vm.ref_count(default).unwrap(), before + 1); vm.release_value(returned).unwrap(); assert_eq!(vm.ref_count(default).unwrap(), before); vm.dict_set(globals, text("identity"), Value::Object(function)) .unwrap(); let argument = vm.new_string("argument").unwrap(); let wrapper = make_code( &mut vm, &[ op(Opcode::LoadGlobal, 0), simple(Opcode::PushNull), op(Opcode::LoadConst, 0), op(Opcode::Call, 1), simple(Opcode::ReturnValue), ], vec![Value::Object(argument)], CodeLayout { names: vec!["identity".into()], ..CodeLayout::default() }, ); let before = vm.ref_count(argument).unwrap(); let returned = vm.execute(wrapper, globals).unwrap(); assert_eq!(returned, Value::Object(argument)); assert_eq!(vm.ref_count(argument).unwrap(), before + 1); vm.release_value(returned).unwrap(); assert_eq!(vm.ref_count(argument).unwrap(), before); } #[test] fn argument_count_and_foreign_argument_failures_do_not_retain_inputs() { let mut vm = Vm::new().unwrap(); let globals = vm.new_dict().unwrap(); let code = make_code( &mut vm, &[op(Opcode::LoadFast, 0), simple(Opcode::ReturnValue)], vec![], CodeLayout { parameters: vec!["value".into()], locals: vec!["value".into()], ..CodeLayout::default() }, ); let function = vm .new_function("identity", code, globals, vec![], vec![]) .unwrap(); assert!(matches!( vm.call(function, &[]), Err(VmError::ArgumentCount { required: 1, provided: 0, .. }) )); assert!(matches!( vm.call(function, &[Value::Nil, Value::Nil]), Err(VmError::ArgumentCount { maximum: 1, provided: 2, .. }) )); let mut other = Vm::new().unwrap(); let foreign = other.new_object().unwrap(); let before = vm.ref_count(function).unwrap(); assert_eq!( vm.call(function, &[Value::Object(foreign)]), Err(VmError::ForeignHandle) ); assert_eq!(vm.ref_count(function).unwrap(), before); } #[test] fn freshly_built_maps_and_their_children_survive_frame_cleanup() { let mut vm = Vm::new().unwrap(); let globals = vm.new_dict().unwrap(); let child = vm.new_string("owned by result").unwrap(); let code = make_code( &mut vm, &[ op(Opcode::LoadConst, 0), op(Opcode::LoadConst, 1), op(Opcode::BuildMap, 1), simple(Opcode::ReturnValue), ], vec![text("child"), Value::Object(child)], CodeLayout::default(), ); vm.release(child).unwrap(); let result = vm.execute(code, globals).unwrap(); vm.release(code).unwrap(); vm.release(globals).unwrap(); let Value::Object(dictionary) = result else { panic!("expected a map"); }; assert_eq!(vm.ref_count(dictionary).unwrap(), 1); assert_eq!( vm.dict_get(dictionary, &text("child")).unwrap(), Some(Value::Object(child)) ); assert_eq!(vm.str(child).unwrap(), "owned by result"); vm.release(dictionary).unwrap(); assert_eq!(vm.object_count(), 0); } fn echo(arguments: &[Value]) -> Result { arguments .first() .cloned() .ok_or(VmError::InvalidArgument("expected an argument")) } #[test] fn native_return_of_a_temporary_argument_is_retained_before_inputs_are_popped() { let mut vm = Vm::new().unwrap(); let globals = vm.new_dict().unwrap(); let function = vm .allocate(EndorNativeFunctionObject::new("echo", echo)) .unwrap(); vm.dict_set(globals, text("echo"), Value::Object(function)) .unwrap(); vm.release(function).unwrap(); let code = make_code( &mut vm, &[ op(Opcode::LoadGlobal, 0), simple(Opcode::PushNull), op(Opcode::BuildMap, 0), op(Opcode::Call, 1), simple(Opcode::ReturnValue), ], vec![], CodeLayout { names: vec!["echo".into()], ..CodeLayout::default() }, ); let result = vm.execute(code, globals).unwrap(); vm.release(code).unwrap(); vm.release(globals).unwrap(); assert_eq!(vm.object_count(), 1); vm.release_value(result).unwrap(); assert_eq!(vm.object_count(), 0); } fn fail(_arguments: &[Value]) -> Result { Err(VmError::InvalidArgument("host failure")) } #[test] fn native_failure_unwinds_temporary_roots_and_keeps_execution_locations() { let mut vm = Vm::new().unwrap(); let globals = vm.new_dict().unwrap(); let function = vm .allocate(EndorNativeFunctionObject::new("fail", fail)) .unwrap(); vm.dict_set(globals, text("fail"), Value::Object(function)) .unwrap(); let code = EndorCode::from_instructions( &[ op(Opcode::LoadGlobal, 0), simple(Opcode::PushNull), op(Opcode::BuildMap, 0), op(Opcode::Call, 1), simple(Opcode::ReturnValue), ], vec![], CodeLayout { names: vec!["fail".into()], ..CodeLayout::default() }, ) .unwrap() .with_location("outer", "demo.endor", 12); let code = vm.new_code(code).unwrap(); let before = vm.object_count(); assert_eq!( vm.execute(code, globals), Err(VmError::InvalidArgument("host failure")) ); assert_eq!(vm.object_count(), before); assert_eq!(vm.last_traceback()[0].function, "outer"); assert_eq!(vm.last_traceback()[0].instruction_offset, 3); assert_eq!(vm.last_traceback()[0].filename, "demo.endor"); assert_eq!(vm.last_traceback()[0].first_line, 12); } #[test] fn recursion_uses_explicit_frames_and_unwinds_at_the_frame_limit() { let mut vm = Vm::new().unwrap(); let globals = vm.new_dict().unwrap(); let code = make_code( &mut vm, &[ op(Opcode::LoadGlobal, 0), simple(Opcode::PushNull), op(Opcode::Call, 0), simple(Opcode::ReturnValue), ], vec![], CodeLayout { names: vec!["recurse".into()], ..CodeLayout::default() }, ); let function = vm .new_function("recurse", code, globals, vec![], vec![]) .unwrap(); vm.dict_set(globals, text("recurse"), Value::Object(function)) .unwrap(); let roots = vm.ref_count(function).unwrap(); assert_eq!( vm.call_with_limits( function, &[], ExecutionLimits { max_frames: 64, max_instructions: 1000 } ), Err(VmError::ExecutionLimit("frames")) ); assert_eq!(vm.last_traceback().len(), 64); assert_eq!(vm.ref_count(function).unwrap(), roots); vm.release(function).unwrap(); vm.release(code).unwrap(); vm.release(globals).unwrap(); assert_eq!(vm.collect_cycles(), 3); } #[test] fn instruction_limit_stops_a_valid_infinite_loop_without_leaking_frames() { let mut vm = Vm::new().unwrap(); let globals = vm.new_dict().unwrap(); let code = make_code( &mut vm, &[op(Opcode::JumpBackward, 1)], vec![], CodeLayout::default(), ); let before = vm.object_count(); assert_eq!( vm.execute_with_limits( code, globals, ExecutionLimits { max_frames: 8, max_instructions: 3 } ), Err(VmError::ExecutionLimit("instructions")) ); assert_eq!(vm.object_count(), before); assert_eq!(vm.last_traceback().len(), 1); } #[test] fn function_globals_shadow_builtins_and_capture_the_original_builtins_dictionary() { let mut vm = Vm::new().unwrap(); let globals = vm.new_dict().unwrap(); let builtins = vm.new_dict().unwrap(); vm.dict_set(builtins, text("answer"), text("builtin")) .unwrap(); vm.dict_set(globals, text("__builtins__"), Value::Object(builtins)) .unwrap(); let code = make_code( &mut vm, &[op(Opcode::LoadGlobal, 0), simple(Opcode::ReturnValue)], vec![], CodeLayout { names: vec!["answer".into()], ..CodeLayout::default() }, ); let function = vm .new_function("read", code, globals, vec![], vec![]) .unwrap(); let replacement = vm.new_dict().unwrap(); vm.dict_set(globals, text("__builtins__"), Value::Object(replacement)) .unwrap(); vm.release(builtins).unwrap(); assert_eq!(vm.call(function, &[]).unwrap(), text("builtin")); vm.dict_set(globals, text("answer"), text("global")) .unwrap(); assert_eq!(vm.call(function, &[]).unwrap(), text("global")); } #[test] fn shared_free_variable_cells_are_read_written_and_deleted_by_bytecode() { let mut vm = Vm::new().unwrap(); let globals = vm.new_dict().unwrap(); let cell = vm.new_cell(Some(text("first"))).unwrap(); let read_code = make_code( &mut vm, &[op(Opcode::LoadDeref, 0), simple(Opcode::ReturnValue)], vec![], CodeLayout { free_variables: vec!["x".into()], ..CodeLayout::default() }, ); let write_code = make_code( &mut vm, &[ op(Opcode::LoadFast, 0), op(Opcode::StoreDeref, 1), op(Opcode::LoadConst, 0), simple(Opcode::ReturnValue), ], vec![Value::Nil], CodeLayout { parameters: vec!["value".into()], locals: vec!["value".into()], free_variables: vec!["x".into()], ..CodeLayout::default() }, ); let clear_code = make_code( &mut vm, &[ op(Opcode::DeleteDeref, 0), op(Opcode::LoadConst, 0), simple(Opcode::ReturnValue), ], vec![Value::Nil], CodeLayout { free_variables: vec!["x".into()], ..CodeLayout::default() }, ); let read = vm .new_function("read", read_code, globals, vec![], vec![cell]) .unwrap(); let write = vm .new_function("write", write_code, globals, vec![], vec![cell]) .unwrap(); let clear = vm .new_function("clear", clear_code, globals, vec![], vec![cell]) .unwrap(); assert_eq!(vm.call(read, &[]).unwrap(), text("first")); vm.call(write, &[text("updated")]).unwrap(); assert_eq!(vm.call(read, &[]).unwrap(), text("updated")); vm.call(clear, &[]).unwrap(); assert_eq!(vm.call(read, &[]), Err(VmError::UnboundLocal("x".into()))); } #[test] fn captured_argument_cells_keep_their_contents_after_the_frame_returns() { let mut vm = Vm::new().unwrap(); let globals = vm.new_dict().unwrap(); let code = make_code( &mut vm, &[op(Opcode::LoadClosure, 0), simple(Opcode::ReturnValue)], vec![], CodeLayout { parameters: vec!["x".into()], locals: vec!["x".into()], cell_variables: vec!["x".into()], ..CodeLayout::default() }, ); let function = vm .new_function("capture", code, globals, vec![], vec![]) .unwrap(); let value = vm.new_string("captured").unwrap(); let returned = vm.call(function, &[Value::Object(value)]).unwrap(); vm.release(value).unwrap(); let Value::Object(cell) = returned else { panic!("expected cell"); }; assert_eq!(vm.cell_get(cell).unwrap(), Value::Object(value)); assert_eq!(vm.str(value).unwrap(), "captured"); vm.release(cell).unwrap(); assert!(matches!(vm.object(value), Err(VmError::InvalidHandle))); } #[test] fn attribute_operations_and_stack_copies_transfer_roots_without_leaks() { let mut vm = Vm::new().unwrap(); let globals = vm.new_dict().unwrap(); let code = make_code( &mut vm, &[ op(Opcode::LoadFast, 1), op(Opcode::LoadFast, 0), op(Opcode::StoreAttr, 0), op(Opcode::LoadFast, 0), op(Opcode::LoadAttr, 0), op(Opcode::Copy, 1), op(Opcode::Swap, 2), simple(Opcode::PopTop), simple(Opcode::ReturnValue), ], vec![], CodeLayout { parameters: vec!["record".into(), "value".into()], locals: vec!["record".into(), "value".into()], names: vec!["x".into()], ..CodeLayout::default() }, ); let function = vm .new_function("assign", code, globals, vec![], vec![]) .unwrap(); let record = vm.new_object().unwrap(); let value = vm.new_string("value").unwrap(); let returned = vm .call(function, &[Value::Object(record), Value::Object(value)]) .unwrap(); assert_eq!(vm.ref_count(value).unwrap(), 3); // host, attribute, return value vm.release_value(returned).unwrap(); vm.release(record).unwrap(); assert_eq!(vm.ref_count(value).unwrap(), 1); } #[test] fn comparison_truth_and_forward_conditionals_use_runtime_values() { let mut vm = Vm::new().unwrap(); let globals = vm.new_dict().unwrap(); let code = make_code( &mut vm, &[ op(Opcode::LoadConst, 0), op(Opcode::LoadConst, 1), Instruction::compare(CompareOp::Lt), simple(Opcode::ToBool), simple(Opcode::UnaryNot), op(Opcode::PopJumpIfTrue, 2), op(Opcode::LoadConst, 2), simple(Opcode::ReturnValue), op(Opcode::LoadConst, 3), simple(Opcode::ReturnValue), ], vec![text("a"), text("b"), text("less"), text("other")], CodeLayout::default(), ); assert_eq!(vm.execute(code, globals).unwrap(), text("less")); } #[test] fn unbound_names_deleted_locals_and_invalid_null_values_fail_cleanly() { let mut vm = Vm::new().unwrap(); let globals = vm.new_dict().unwrap(); let missing = make_code( &mut vm, &[op(Opcode::LoadGlobal, 0), simple(Opcode::ReturnValue)], vec![], CodeLayout { names: vec!["missing".into()], ..CodeLayout::default() }, ); assert_eq!( vm.execute(missing, globals), Err(VmError::NameNotFound("missing".into())) ); let deleted = make_code( &mut vm, &[ op(Opcode::LoadConst, 0), op(Opcode::StoreFast, 0), op(Opcode::DeleteFast, 0), op(Opcode::LoadFast, 0), simple(Opcode::ReturnValue), ], vec![Value::Nil], CodeLayout { locals: vec!["x".into()], ..CodeLayout::default() }, ); assert_eq!( vm.execute(deleted, globals), Err(VmError::UnboundLocal("x".into())) ); let null = make_code( &mut vm, &[simple(Opcode::PushNull), simple(Opcode::ReturnValue)], vec![], CodeLayout::default(), ); assert!(matches!( vm.execute(null, globals), Err(VmError::InvalidBytecode { .. }) )); let success = return_constant(&mut vm, Value::Nil); assert_eq!(vm.execute(success, globals).unwrap(), Value::Nil); assert!(vm.last_traceback().is_empty()); } #[test] fn failed_map_construction_cleans_both_partial_map_and_temporary_input() { let mut vm = Vm::new().unwrap(); let globals = vm.new_dict().unwrap(); let code = make_code( &mut vm, &[ op(Opcode::LoadConst, 0), op(Opcode::BuildMap, 0), op(Opcode::BuildMap, 1), simple(Opcode::ReturnValue), ], vec![Value::Number(1_i64.into())], CodeLayout::default(), ); let before = vm.object_count(); assert_eq!( vm.execute(code, globals), Err(VmError::UnsupportedOperation("numeric hash")) ); assert_eq!(vm.object_count(), before); } #[test] fn implicit_receiver_slot_is_distinct_from_the_null_call_marker() { let mut vm = Vm::new().unwrap(); let globals = vm.new_dict().unwrap(); let function = vm .allocate(EndorNativeFunctionObject::new("echo", echo)) .unwrap(); let code = make_code( &mut vm, &[ op(Opcode::LoadConst, 0), op(Opcode::LoadConst, 1), op(Opcode::Call, 0), simple(Opcode::ReturnValue), ], vec![Value::Object(function), Value::Nil], CodeLayout::default(), ); // Nil is an actual implicit argument here, not the internal NULL marker. assert_eq!(vm.execute(code, globals).unwrap(), Value::Nil); }