Files
endorphin-aster-vm/crates/core/tests/execution.rs
T
codex-bot 6fa95b64b0
Verify databases and deploy Aster VM / verify-and-deploy (push) Successful in 14m27s
Implement validated wordcode execution with shared code objects and frames
2026-10-18 20:41:56 +08:00

762 lines
23 KiB
Rust

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<Value>,
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::<EndorCodeObject>(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<AtomicUsize>);
impl ObjectPayload for Tick {
fn as_any(&self) -> &dyn Any {
self
}
fn kind_name(&self) -> &'static str {
"tick"
}
fn call(&self, arguments: &[Value]) -> Result<Value, VmError> {
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<Value, VmError> {
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<Value, VmError> {
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);
}