1838 lines
58 KiB
Rust
1838 lines
58 KiB
Rust
use std::any::Any;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
|
|
use endor_core::{
|
|
CodeLayout, CompareOp, EndorCode, EndorCodeObject, EndorFunctionObject,
|
|
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(),
|
|
)
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct SharedReference(AtomicUsize);
|
|
impl ObjectPayload for SharedReference {
|
|
fn as_any(&self) -> &dyn Any {
|
|
self
|
|
}
|
|
fn kind_name(&self) -> &'static str {
|
|
"shared-reference"
|
|
}
|
|
fn assign_from(&self, source: &dyn ObjectPayload) -> Result<bool, VmError> {
|
|
let Some(source) = source.as_any().downcast_ref::<Self>() else {
|
|
return Ok(false);
|
|
};
|
|
let value = source.0.load(Ordering::SeqCst);
|
|
if value == 99 {
|
|
return Err(VmError::InvalidArgument("rejected replacement"));
|
|
}
|
|
self.0.store(value, Ordering::SeqCst);
|
|
Ok(true)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn opt_in_assignment_keeps_alias_identity_ordinary_values_and_root_accounting() {
|
|
for local in [false, true] {
|
|
let mut vm = Vm::new().unwrap();
|
|
let old = vm.allocate(SharedReference(AtomicUsize::new(1))).unwrap();
|
|
let new = vm.allocate(SharedReference(AtomicUsize::new(2))).unwrap();
|
|
let globals = vm.new_dict().unwrap();
|
|
vm.dict_set(globals, text("alias"), Value::Object(old))
|
|
.unwrap();
|
|
let (store, load) = if local {
|
|
(Opcode::StoreFast, Opcode::LoadFast)
|
|
} else {
|
|
(Opcode::StoreGlobal, Opcode::LoadGlobal)
|
|
};
|
|
let code = make_code(
|
|
&mut vm,
|
|
&[
|
|
op(Opcode::LoadConst, 0),
|
|
op(store, 0),
|
|
op(Opcode::LoadConst, 1),
|
|
op(store, 0),
|
|
op(load, 0),
|
|
simple(Opcode::ReturnValue),
|
|
],
|
|
vec![Value::Object(old), Value::Object(new)],
|
|
if local {
|
|
CodeLayout {
|
|
locals: vec!["value".into()],
|
|
..CodeLayout::default()
|
|
}
|
|
} else {
|
|
CodeLayout {
|
|
names: vec!["value".into()],
|
|
..CodeLayout::default()
|
|
}
|
|
},
|
|
);
|
|
let value = vm.execute(code, globals).unwrap();
|
|
assert_eq!(value, Value::Object(old));
|
|
assert_eq!(
|
|
vm.dict_get(globals, &text("alias")).unwrap(),
|
|
Some(Value::Object(old))
|
|
);
|
|
assert_eq!(
|
|
vm.payload::<SharedReference>(old, "shared-reference")
|
|
.unwrap()
|
|
.0
|
|
.load(Ordering::SeqCst),
|
|
2
|
|
);
|
|
vm.release_value(value).unwrap();
|
|
vm.release(code).unwrap();
|
|
vm.dict_clear(globals).unwrap();
|
|
vm.release(globals).unwrap();
|
|
vm.release(old).unwrap();
|
|
vm.release(new).unwrap();
|
|
assert_eq!(vm.object_count(), 0);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn failed_opt_in_assignment_preserves_target_and_cleans_execution_roots() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let old = vm.allocate(SharedReference(AtomicUsize::new(1))).unwrap();
|
|
let rejected = vm.allocate(SharedReference(AtomicUsize::new(99))).unwrap();
|
|
let globals = vm.new_dict().unwrap();
|
|
vm.dict_set(globals, text("value"), Value::Object(old))
|
|
.unwrap();
|
|
let code = make_code(
|
|
&mut vm,
|
|
&[
|
|
op(Opcode::LoadConst, 0),
|
|
op(Opcode::StoreGlobal, 0),
|
|
op(Opcode::LoadConst, 1),
|
|
simple(Opcode::ReturnValue),
|
|
],
|
|
vec![Value::Object(rejected), Value::Nil],
|
|
CodeLayout {
|
|
names: vec!["value".into()],
|
|
..CodeLayout::default()
|
|
},
|
|
);
|
|
assert!(vm.execute(code, globals).is_err());
|
|
assert_eq!(
|
|
vm.payload::<SharedReference>(old, "shared-reference")
|
|
.unwrap()
|
|
.0
|
|
.load(Ordering::SeqCst),
|
|
1
|
|
);
|
|
assert_eq!(
|
|
vm.dict_get(globals, &text("value")).unwrap(),
|
|
Some(Value::Object(old))
|
|
);
|
|
vm.release(code).unwrap();
|
|
vm.dict_clear(globals).unwrap();
|
|
vm.release(globals).unwrap();
|
|
vm.release(old).unwrap();
|
|
vm.release(rejected).unwrap();
|
|
assert_eq!(vm.object_count(), 0);
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
|
|
#[test]
|
|
fn function_creation_operands_round_trip_and_require_all_inputs() {
|
|
assert_eq!(
|
|
Instruction::encode_all(&[Instruction::make_function(1, 1)]).unwrap(),
|
|
vec![255, 1, 255, 0, 28, 1]
|
|
);
|
|
assert_eq!(
|
|
Instruction::encode_all(&[Instruction::make_function(u16::MAX, u16::MAX)]).unwrap(),
|
|
vec![255, 255, 255, 255, 255, 255, 28, 255]
|
|
);
|
|
let code = EndorCode::from_instructions(
|
|
&[
|
|
op(Opcode::LoadConst, 0),
|
|
op(Opcode::LoadConst, 0),
|
|
op(Opcode::LoadConst, 0),
|
|
Instruction::make_function(1, 1),
|
|
simple(Opcode::ReturnValue),
|
|
],
|
|
vec![Value::Nil],
|
|
CodeLayout::default(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
code.instructions()[3].instruction,
|
|
Instruction::make_function(1, 1)
|
|
);
|
|
assert_eq!(code.instructions()[3].next_offset, 6);
|
|
assert_eq!(code.stack_size(), 3);
|
|
for (defaults, closure) in [(0, 0), (1, 0), (0, 1), (1, 1), (u16::MAX, u16::MAX)] {
|
|
assert!(matches!(
|
|
EndorCode::from_instructions(
|
|
&[
|
|
Instruction::make_function(defaults, closure),
|
|
simple(Opcode::ReturnValue)
|
|
],
|
|
vec![],
|
|
CodeLayout::default(),
|
|
),
|
|
Err(VmError::InvalidBytecode { .. })
|
|
));
|
|
}
|
|
for creation in [
|
|
Instruction::make_function(1, 0),
|
|
Instruction::make_function(0, 1),
|
|
] {
|
|
assert!(
|
|
EndorCode::from_instructions(
|
|
&[
|
|
op(Opcode::LoadConst, 0),
|
|
creation,
|
|
simple(Opcode::ReturnValue)
|
|
],
|
|
vec![Value::Nil],
|
|
CodeLayout::default(),
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn bytecode_defines_global_functions_and_calls_them_with_defaults() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let globals = vm.new_dict().unwrap();
|
|
let inner = EndorCode::from_instructions(
|
|
&[op(Opcode::LoadFast, 0), simple(Opcode::ReturnValue)],
|
|
vec![],
|
|
CodeLayout {
|
|
parameters: vec!["value".into()],
|
|
locals: vec!["value".into()],
|
|
..CodeLayout::default()
|
|
},
|
|
)
|
|
.unwrap()
|
|
.with_location("identity", "functions.endor", 2);
|
|
let inner = vm.new_code(inner).unwrap();
|
|
let module = make_code(
|
|
&mut vm,
|
|
&[
|
|
op(Opcode::LoadConst, 0),
|
|
op(Opcode::LoadConst, 1),
|
|
Instruction::make_function(1, 0),
|
|
op(Opcode::StoreGlobal, 0),
|
|
op(Opcode::LoadGlobal, 0),
|
|
simple(Opcode::PushNull),
|
|
op(Opcode::Call, 0),
|
|
simple(Opcode::ReturnValue),
|
|
],
|
|
vec![text("default"), Value::Object(inner)],
|
|
CodeLayout {
|
|
names: vec!["identity".into()],
|
|
..CodeLayout::default()
|
|
},
|
|
);
|
|
vm.release(inner).unwrap();
|
|
assert_eq!(vm.execute(module, globals).unwrap(), text("default"));
|
|
let Some(Value::Object(first)) = vm.dict_get(globals, &text("identity")).unwrap() else {
|
|
panic!("expected a function");
|
|
};
|
|
vm.retain(first).unwrap();
|
|
let data = vm
|
|
.payload::<EndorFunctionObject>(first, "function")
|
|
.unwrap();
|
|
assert_eq!(data.name(), "identity");
|
|
assert_eq!(data.code(), inner);
|
|
assert_eq!(data.globals(), globals);
|
|
assert_eq!(
|
|
vm.call(first, &[text("explicit")]).unwrap(),
|
|
text("explicit")
|
|
);
|
|
assert_eq!(vm.execute(module, globals).unwrap(), text("default"));
|
|
let Some(Value::Object(second)) = vm.dict_get(globals, &text("identity")).unwrap() else {
|
|
panic!("expected a second function");
|
|
};
|
|
assert_ne!(first, second);
|
|
assert_eq!(
|
|
vm.payload::<EndorFunctionObject>(second, "function")
|
|
.unwrap()
|
|
.code(),
|
|
inner
|
|
);
|
|
vm.release(first).unwrap();
|
|
vm.release(module).unwrap();
|
|
vm.release(globals).unwrap();
|
|
// The global function, its code and its globals form a collectible cycle.
|
|
assert_eq!(vm.collect_cycles(), 3);
|
|
assert_eq!(vm.object_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn bytecode_function_defaults_keep_temporary_objects_alive_after_factory_returns() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let globals = vm.new_dict().unwrap();
|
|
let inner = make_code(
|
|
&mut vm,
|
|
&[op(Opcode::LoadFast, 1), simple(Opcode::ReturnValue)],
|
|
vec![],
|
|
CodeLayout {
|
|
parameters: vec!["required".into(), "defaulted".into(), "tag".into()],
|
|
locals: vec!["required".into(), "defaulted".into(), "tag".into()],
|
|
..CodeLayout::default()
|
|
},
|
|
);
|
|
let factory = make_code(
|
|
&mut vm,
|
|
&[
|
|
op(Opcode::BuildMap, 0),
|
|
op(Opcode::LoadConst, 1),
|
|
op(Opcode::LoadConst, 0),
|
|
Instruction::make_function(2, 0),
|
|
simple(Opcode::ReturnValue),
|
|
],
|
|
vec![Value::Object(inner), text("tag")],
|
|
CodeLayout::default(),
|
|
);
|
|
vm.release(inner).unwrap();
|
|
let Value::Object(function) = vm.execute(factory, globals).unwrap() else {
|
|
panic!("expected a function");
|
|
};
|
|
vm.release(factory).unwrap();
|
|
vm.release(globals).unwrap();
|
|
assert!(matches!(
|
|
vm.call(function, &[]),
|
|
Err(VmError::ArgumentCount { required: 1, .. })
|
|
));
|
|
let first = vm.call(function, &[Value::Nil]).unwrap();
|
|
let second = vm.call(function, &[Value::Nil]).unwrap();
|
|
assert_eq!(first, second); // Defaults are evaluated once per creation.
|
|
let Value::Object(default) = first else {
|
|
panic!("expected a default map");
|
|
};
|
|
assert_eq!(vm.ref_count(default).unwrap(), 3); // Function and two results.
|
|
vm.release(function).unwrap();
|
|
vm.release_value(first).unwrap();
|
|
assert_eq!(vm.ref_count(default).unwrap(), 1);
|
|
vm.release_value(second).unwrap();
|
|
assert_eq!(vm.object_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn bytecode_nested_functions_share_cells_and_bind_defaults_after_outer_returns() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let globals = vm.new_dict().unwrap();
|
|
let read = make_code(
|
|
&mut vm,
|
|
&[op(Opcode::LoadDeref, 0), simple(Opcode::ReturnValue)],
|
|
vec![],
|
|
CodeLayout {
|
|
free_variables: vec!["x".into(), "sentinel".into()],
|
|
..CodeLayout::default()
|
|
},
|
|
);
|
|
let write = make_code(
|
|
&mut vm,
|
|
&[
|
|
op(Opcode::LoadFast, 0),
|
|
op(Opcode::StoreDeref, 1),
|
|
op(Opcode::LoadDeref, 1),
|
|
simple(Opcode::ReturnValue),
|
|
],
|
|
vec![],
|
|
CodeLayout {
|
|
parameters: vec!["value".into()],
|
|
locals: vec!["value".into()],
|
|
free_variables: vec!["x".into(), "sentinel".into()],
|
|
..CodeLayout::default()
|
|
},
|
|
);
|
|
let outer = make_code(
|
|
&mut vm,
|
|
&[
|
|
op(Opcode::LoadConst, 0),
|
|
op(Opcode::LoadClosure, 0),
|
|
op(Opcode::LoadClosure, 1),
|
|
op(Opcode::LoadConst, 1),
|
|
Instruction::make_function(0, 2),
|
|
op(Opcode::LoadConst, 2),
|
|
op(Opcode::LoadConst, 3),
|
|
op(Opcode::LoadClosure, 0),
|
|
op(Opcode::LoadClosure, 1),
|
|
op(Opcode::LoadConst, 4),
|
|
Instruction::make_function(1, 2),
|
|
op(Opcode::BuildMap, 2),
|
|
simple(Opcode::ReturnValue),
|
|
],
|
|
vec![
|
|
text("read"),
|
|
Value::Object(read),
|
|
text("write"),
|
|
text("default"),
|
|
Value::Object(write),
|
|
],
|
|
CodeLayout {
|
|
parameters: vec!["x".into(), "sentinel".into()],
|
|
locals: vec!["x".into(), "sentinel".into()],
|
|
cell_variables: vec!["x".into(), "sentinel".into()],
|
|
..CodeLayout::default()
|
|
},
|
|
);
|
|
let outer_function = vm
|
|
.new_function("outer", outer, globals, vec![], vec![])
|
|
.unwrap();
|
|
let Value::Object(pair) = vm
|
|
.call(outer_function, &[text("first"), text("sentinel")])
|
|
.unwrap()
|
|
else {
|
|
panic!("expected a function pair");
|
|
};
|
|
for reference in [read, write, outer, outer_function, globals] {
|
|
vm.release(reference).unwrap();
|
|
}
|
|
let Some(Value::Object(read)) = vm.dict_get(pair, &text("read")).unwrap() else {
|
|
panic!("expected read");
|
|
};
|
|
let Some(Value::Object(write)) = vm.dict_get(pair, &text("write")).unwrap() else {
|
|
panic!("expected write");
|
|
};
|
|
assert_eq!(
|
|
vm.payload::<EndorFunctionObject>(read, "function")
|
|
.unwrap()
|
|
.closure(),
|
|
vm.payload::<EndorFunctionObject>(write, "function")
|
|
.unwrap()
|
|
.closure(),
|
|
);
|
|
assert_eq!(vm.collect_cycles(), 0);
|
|
assert_eq!(vm.call(read, &[]).unwrap(), text("first"));
|
|
assert_eq!(vm.call(write, &[text("updated")]).unwrap(), text("updated"));
|
|
assert_eq!(vm.call(read, &[]).unwrap(), text("updated"));
|
|
assert_eq!(vm.call(write, &[]).unwrap(), text("default"));
|
|
assert_eq!(vm.call(read, &[]).unwrap(), text("default"));
|
|
vm.release(pair).unwrap();
|
|
assert_eq!(vm.object_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn bytecode_function_creation_rejects_bad_inputs_without_leaking_roots() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let globals = vm.new_dict().unwrap();
|
|
let plain = return_constant(&mut vm, Value::Nil);
|
|
let captured = make_code(
|
|
&mut vm,
|
|
&[op(Opcode::LoadDeref, 0), simple(Opcode::ReturnValue)],
|
|
vec![],
|
|
CodeLayout {
|
|
free_variables: vec!["x".into()],
|
|
..CodeLayout::default()
|
|
},
|
|
);
|
|
let cell = vm.new_cell(None).unwrap();
|
|
let mismatch = VmError::InvalidArgument("function defaults or closure do not match code");
|
|
let cases = [
|
|
(
|
|
vec![op(Opcode::LoadConst, 0), Instruction::make_function(0, 0)],
|
|
vec![Value::Nil],
|
|
VmError::ObjectKindMismatch("code"),
|
|
),
|
|
(
|
|
vec![op(Opcode::BuildMap, 0), Instruction::make_function(0, 0)],
|
|
vec![],
|
|
VmError::ObjectKindMismatch("code"),
|
|
),
|
|
(
|
|
vec![simple(Opcode::PushNull), Instruction::make_function(0, 0)],
|
|
vec![],
|
|
invalid_null(),
|
|
),
|
|
(
|
|
vec![
|
|
op(Opcode::BuildMap, 0),
|
|
op(Opcode::LoadConst, 0),
|
|
Instruction::make_function(1, 0),
|
|
],
|
|
vec![Value::Object(plain)],
|
|
mismatch.clone(),
|
|
),
|
|
(
|
|
vec![op(Opcode::LoadConst, 0), Instruction::make_function(0, 0)],
|
|
vec![Value::Object(captured)],
|
|
mismatch.clone(),
|
|
),
|
|
(
|
|
vec![
|
|
op(Opcode::LoadConst, 0),
|
|
op(Opcode::LoadConst, 1),
|
|
Instruction::make_function(0, 1),
|
|
],
|
|
vec![Value::Object(cell), Value::Object(plain)],
|
|
mismatch,
|
|
),
|
|
(
|
|
vec![
|
|
op(Opcode::LoadConst, 0),
|
|
op(Opcode::LoadConst, 1),
|
|
Instruction::make_function(0, 1),
|
|
],
|
|
vec![Value::Nil, Value::Object(captured)],
|
|
VmError::ObjectKindMismatch("cell"),
|
|
),
|
|
(
|
|
vec![
|
|
op(Opcode::BuildMap, 0),
|
|
op(Opcode::LoadConst, 0),
|
|
Instruction::make_function(0, 1),
|
|
],
|
|
vec![Value::Object(captured)],
|
|
VmError::ObjectKindMismatch("cell"),
|
|
),
|
|
(
|
|
vec![
|
|
simple(Opcode::PushNull),
|
|
op(Opcode::LoadConst, 0),
|
|
Instruction::make_function(0, 1),
|
|
],
|
|
vec![Value::Object(captured)],
|
|
invalid_null(),
|
|
),
|
|
(
|
|
vec![
|
|
simple(Opcode::PushNull),
|
|
op(Opcode::LoadConst, 0),
|
|
Instruction::make_function(1, 0),
|
|
],
|
|
vec![Value::Object(plain)],
|
|
invalid_null(),
|
|
),
|
|
];
|
|
for (mut instructions, constants, expected) in cases {
|
|
instructions.push(simple(Opcode::ReturnValue));
|
|
let code = make_code(&mut vm, &instructions, constants, CodeLayout::default());
|
|
let count = vm.object_count();
|
|
let roots =
|
|
[plain, captured, cell, globals].map(|reference| vm.ref_count(reference).unwrap());
|
|
let error = vm.execute(code, globals).unwrap_err();
|
|
if matches!(expected, VmError::InvalidBytecode { .. }) {
|
|
assert!(matches!(
|
|
error,
|
|
VmError::InvalidBytecode {
|
|
reason: "NULL call marker used as a value",
|
|
..
|
|
}
|
|
));
|
|
} else {
|
|
assert_eq!(error, expected);
|
|
}
|
|
assert_eq!(vm.object_count(), count);
|
|
assert_eq!(
|
|
[plain, captured, cell, globals].map(|reference| vm.ref_count(reference).unwrap()),
|
|
roots
|
|
);
|
|
vm.release(code).unwrap();
|
|
}
|
|
for reference in [plain, captured, cell, globals] {
|
|
vm.release(reference).unwrap();
|
|
}
|
|
assert_eq!(vm.object_count(), 0);
|
|
}
|
|
|
|
fn invalid_null() -> VmError {
|
|
VmError::InvalidBytecode {
|
|
offset: 0,
|
|
reason: "NULL call marker used as a value",
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn bytecode_functions_capture_builtins_at_creation_and_share_live_globals() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let globals = vm.new_dict().unwrap();
|
|
let original = vm.new_dict().unwrap();
|
|
let replacement = vm.new_dict().unwrap();
|
|
vm.dict_set(original, text("answer"), text("original"))
|
|
.unwrap();
|
|
vm.dict_set(replacement, text("answer"), text("replacement"))
|
|
.unwrap();
|
|
vm.dict_set(globals, text("__builtins__"), Value::Object(original))
|
|
.unwrap();
|
|
let inner = make_code(
|
|
&mut vm,
|
|
&[op(Opcode::LoadGlobal, 0), simple(Opcode::ReturnValue)],
|
|
vec![],
|
|
CodeLayout {
|
|
names: vec!["answer".into()],
|
|
..CodeLayout::default()
|
|
},
|
|
);
|
|
let factory = make_code(
|
|
&mut vm,
|
|
&[
|
|
op(Opcode::LoadConst, 0),
|
|
Instruction::make_function(0, 0),
|
|
simple(Opcode::ReturnValue),
|
|
],
|
|
vec![Value::Object(inner)],
|
|
CodeLayout::default(),
|
|
);
|
|
let Value::Object(first) = vm.execute(factory, globals).unwrap() else {
|
|
panic!("expected first function");
|
|
};
|
|
vm.dict_set(globals, text("__builtins__"), Value::Object(replacement))
|
|
.unwrap();
|
|
let Value::Object(second) = vm.execute(factory, globals).unwrap() else {
|
|
panic!("expected second function");
|
|
};
|
|
for reference in [original, replacement, factory, inner, globals] {
|
|
vm.release(reference).unwrap();
|
|
}
|
|
assert_eq!(vm.call(first, &[]).unwrap(), text("original"));
|
|
assert_eq!(vm.call(second, &[]).unwrap(), text("replacement"));
|
|
vm.dict_set(globals, text("answer"), text("global"))
|
|
.unwrap();
|
|
assert_eq!(vm.call(first, &[]).unwrap(), text("global"));
|
|
assert_eq!(vm.call(second, &[]).unwrap(), text("global"));
|
|
vm.release(first).unwrap();
|
|
vm.release(second).unwrap();
|
|
assert_eq!(vm.object_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn later_execution_failure_releases_a_new_function_and_its_temporary_default() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let globals = vm.new_dict().unwrap();
|
|
let inner = make_code(
|
|
&mut vm,
|
|
&[op(Opcode::LoadFast, 0), simple(Opcode::ReturnValue)],
|
|
vec![],
|
|
CodeLayout {
|
|
parameters: vec!["value".into()],
|
|
locals: vec!["value".into()],
|
|
..CodeLayout::default()
|
|
},
|
|
);
|
|
let fail = vm
|
|
.allocate(EndorNativeFunctionObject::new("fail", fail))
|
|
.unwrap();
|
|
let factory = make_code(
|
|
&mut vm,
|
|
&[
|
|
op(Opcode::LoadConst, 0),
|
|
simple(Opcode::PushNull),
|
|
op(Opcode::BuildMap, 0),
|
|
op(Opcode::LoadConst, 1),
|
|
Instruction::make_function(1, 0),
|
|
op(Opcode::Call, 1),
|
|
simple(Opcode::ReturnValue),
|
|
],
|
|
vec![Value::Object(fail), Value::Object(inner)],
|
|
CodeLayout::default(),
|
|
);
|
|
vm.release(fail).unwrap();
|
|
vm.release(inner).unwrap();
|
|
let count = vm.object_count();
|
|
assert_eq!(
|
|
vm.execute(factory, globals),
|
|
Err(VmError::InvalidArgument("host failure"))
|
|
);
|
|
assert_eq!(vm.object_count(), count);
|
|
vm.release(factory).unwrap();
|
|
vm.release(globals).unwrap();
|
|
assert_eq!(vm.object_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn suspended_read_only_call_keeps_roots_and_releases_them_on_completion() {
|
|
use endor_core::ExecutionProgress;
|
|
let mut vm = Vm::new().unwrap();
|
|
let shared = vm.new_object().unwrap();
|
|
vm.set_attr(shared, "value", Value::Number(7_i64.into()))
|
|
.unwrap();
|
|
let globals = vm.new_dict().unwrap();
|
|
let mut instructions = vec![simple(Opcode::Nop); 16];
|
|
instructions.extend([op(Opcode::LoadConst, 0), simple(Opcode::ReturnValue)]);
|
|
let code = make_code(
|
|
&mut vm,
|
|
&instructions,
|
|
vec![Value::Object(shared)],
|
|
CodeLayout::default(),
|
|
);
|
|
let function = vm
|
|
.new_function("sliced", code, globals, vec![], vec![])
|
|
.unwrap();
|
|
let mut progress = vm
|
|
.start_read_only_call(
|
|
function,
|
|
&[],
|
|
ExecutionLimits {
|
|
max_frames: 8,
|
|
max_instructions: 100,
|
|
},
|
|
1,
|
|
)
|
|
.unwrap();
|
|
assert!(matches!(
|
|
progress,
|
|
ExecutionProgress::Pending { instructions: 1 }
|
|
));
|
|
assert!(
|
|
vm.set_attr(shared, "value", Value::Number(8_i64.into()))
|
|
.is_err()
|
|
);
|
|
assert!(vm.call(function, &[]).is_err());
|
|
vm.release(function).unwrap();
|
|
vm.release(code).unwrap();
|
|
vm.release(globals).unwrap();
|
|
vm.release(shared).unwrap();
|
|
vm.collect_cycles();
|
|
loop {
|
|
match progress {
|
|
ExecutionProgress::Pending { .. } => progress = vm.resume_read_only_call(2).unwrap(),
|
|
ExecutionProgress::Complete {
|
|
value,
|
|
instructions,
|
|
..
|
|
} => {
|
|
assert_eq!(instructions, 18);
|
|
assert_eq!(value, Value::Object(shared));
|
|
assert_eq!(
|
|
vm.get_attr(shared, "value").unwrap(),
|
|
Value::Number(7_i64.into())
|
|
);
|
|
vm.release_value(value).unwrap();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
assert!(!vm.has_suspended_call());
|
|
assert_eq!(vm.object_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn suspended_call_cancellation_and_total_budget_restore_read_only_state() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let shared = vm.new_object().unwrap();
|
|
let globals = vm.new_dict().unwrap();
|
|
let mut instructions = vec![simple(Opcode::Nop); 8];
|
|
instructions.extend([op(Opcode::LoadConst, 0), simple(Opcode::ReturnValue)]);
|
|
let code = make_code(
|
|
&mut vm,
|
|
&instructions,
|
|
vec![Value::Object(shared)],
|
|
CodeLayout::default(),
|
|
);
|
|
let function = vm
|
|
.new_function("budget", code, globals, vec![], vec![])
|
|
.unwrap();
|
|
let roots = vm.ref_count(function).unwrap();
|
|
vm.start_read_only_call(
|
|
function,
|
|
&[],
|
|
ExecutionLimits {
|
|
max_frames: 8,
|
|
max_instructions: 3,
|
|
},
|
|
1,
|
|
)
|
|
.unwrap();
|
|
assert!(vm.resume_read_only_call(0).is_err());
|
|
assert!(vm.has_suspended_call());
|
|
vm.resume_read_only_call(1).unwrap();
|
|
assert_eq!(
|
|
vm.resume_read_only_call(1).unwrap_err(),
|
|
VmError::ExecutionLimit("instructions")
|
|
);
|
|
assert!(!vm.has_suspended_call());
|
|
assert_eq!(vm.ref_count(function).unwrap(), roots);
|
|
vm.set_attr(shared, "after", Value::Bool(true)).unwrap();
|
|
vm.start_read_only_call(function, &[], ExecutionLimits::default(), 1)
|
|
.unwrap();
|
|
assert!(vm.cancel_read_only_call().unwrap());
|
|
assert!(!vm.cancel_read_only_call().unwrap());
|
|
assert_eq!(vm.ref_count(function).unwrap(), roots);
|
|
vm.set_attr(shared, "after", Value::Bool(false)).unwrap();
|
|
vm.release(function).unwrap();
|
|
vm.release(code).unwrap();
|
|
vm.release(globals).unwrap();
|
|
vm.release(shared).unwrap();
|
|
assert_eq!(vm.object_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn managed_coroutines_interleave_without_replaying_writes() {
|
|
use endor_core::CoroutineProgress;
|
|
let mut vm = Vm::new().unwrap();
|
|
let globals = vm.new_dict().unwrap();
|
|
let code = make_code(
|
|
&mut vm,
|
|
&[
|
|
op(Opcode::LoadConst, 0),
|
|
op(Opcode::StoreGlobal, 0),
|
|
op(Opcode::LoadConst, 1),
|
|
op(Opcode::StoreGlobal, 1),
|
|
op(Opcode::LoadConst, 2),
|
|
simple(Opcode::ReturnValue),
|
|
],
|
|
vec![text("first"), text("second"), Value::Nil],
|
|
CodeLayout {
|
|
names: vec!["x".into(), "y".into()],
|
|
..CodeLayout::default()
|
|
},
|
|
);
|
|
let function = vm
|
|
.new_function("work", code, globals, vec![], vec![])
|
|
.unwrap();
|
|
let first = vm
|
|
.new_coroutine(function, &[], ExecutionLimits::default())
|
|
.unwrap();
|
|
let second = vm
|
|
.new_coroutine(function, &[], ExecutionLimits::default())
|
|
.unwrap();
|
|
assert!(matches!(
|
|
vm.resume_coroutine(first, 2).unwrap(),
|
|
CoroutineProgress::Pending { instructions: 2 }
|
|
));
|
|
assert!(matches!(
|
|
vm.resume_coroutine(second, 2).unwrap(),
|
|
CoroutineProgress::Pending { instructions: 2 }
|
|
));
|
|
vm.dict_set(globals, text("x"), text("interleaved"))
|
|
.unwrap();
|
|
assert!(matches!(
|
|
vm.resume_coroutine(first, 16).unwrap(),
|
|
CoroutineProgress::Complete {
|
|
value: Value::Nil,
|
|
instructions: 6
|
|
}
|
|
));
|
|
assert_eq!(
|
|
vm.dict_get(globals, &text("x")).unwrap(),
|
|
Some(text("interleaved"))
|
|
);
|
|
assert_eq!(
|
|
vm.dict_get(globals, &text("y")).unwrap(),
|
|
Some(text("second"))
|
|
);
|
|
assert!(vm.cancel_coroutine(second).unwrap());
|
|
assert!(!vm.cancel_coroutine(second).unwrap());
|
|
assert!(vm.resume_coroutine(first, 1).is_err());
|
|
for object in [first, second, function, code, globals] {
|
|
vm.release(object).unwrap();
|
|
}
|
|
assert_eq!(vm.object_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn parked_coroutine_cycles_are_collectable_without_cancellation() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let globals = vm.new_dict().unwrap();
|
|
let code = return_constant(&mut vm, Value::Nil);
|
|
let function = vm
|
|
.new_function("cycle", code, globals, vec![], vec![])
|
|
.unwrap();
|
|
let coroutine = vm
|
|
.new_coroutine(function, &[], ExecutionLimits::default())
|
|
.unwrap();
|
|
vm.dict_set(globals, text("task"), Value::Object(coroutine))
|
|
.unwrap();
|
|
for object in [coroutine, function, code, globals] {
|
|
vm.release(object).unwrap();
|
|
}
|
|
assert!(vm.collect_cycles() > 0);
|
|
assert_eq!(vm.object_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn managed_coroutine_hard_budget_accumulates_and_error_releases_frames() {
|
|
use endor_core::CoroutineProgress;
|
|
let mut vm = Vm::new().unwrap();
|
|
let globals = vm.new_dict().unwrap();
|
|
let code = return_constant(&mut vm, Value::Nil);
|
|
let function = vm
|
|
.new_function("limited", code, globals, vec![], vec![])
|
|
.unwrap();
|
|
let coroutine = vm
|
|
.new_coroutine(
|
|
function,
|
|
&[],
|
|
ExecutionLimits {
|
|
max_frames: 4,
|
|
max_instructions: 1,
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert!(matches!(
|
|
vm.resume_coroutine(coroutine, 1).unwrap(),
|
|
CoroutineProgress::Pending { instructions: 1 }
|
|
));
|
|
assert!(matches!(
|
|
vm.resume_coroutine(coroutine, 1),
|
|
Err(VmError::ExecutionLimit("instructions"))
|
|
));
|
|
assert!(!vm.last_traceback().is_empty());
|
|
assert!(!vm.cancel_coroutine(coroutine).unwrap());
|
|
for object in [coroutine, function, code, globals] {
|
|
vm.release(object).unwrap();
|
|
}
|
|
assert_eq!(vm.object_count(), 0);
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct AwaitGate {
|
|
ready: Arc<std::sync::atomic::AtomicBool>,
|
|
value: Value,
|
|
}
|
|
impl ObjectPayload for AwaitGate {
|
|
fn as_any(&self) -> &dyn Any {
|
|
self
|
|
}
|
|
fn kind_name(&self) -> &'static str {
|
|
"await-gate"
|
|
}
|
|
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
|
|
if let Value::Object(reference) = self.value {
|
|
visit(reference);
|
|
}
|
|
}
|
|
fn poll_await(&self) -> Result<endor_core::AwaitPoll, VmError> {
|
|
Ok(if self.ready.load(Ordering::Acquire) {
|
|
endor_core::AwaitPoll::Ready(self.value.clone())
|
|
} else {
|
|
endor_core::AwaitPoll::Pending
|
|
})
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn await_keeps_operand_and_owned_object_result_without_replaying_prefix() {
|
|
use endor_core::CoroutineProgress;
|
|
let mut vm = Vm::new().unwrap();
|
|
let ready = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
|
let value = vm.new_object().unwrap();
|
|
vm.set_attr(value, "marker", text("kept")).unwrap();
|
|
let gate = vm
|
|
.allocate(AwaitGate {
|
|
ready: ready.clone(),
|
|
value: Value::Object(value),
|
|
})
|
|
.unwrap();
|
|
let globals = vm.new_dict().unwrap();
|
|
let code = make_code(
|
|
&mut vm,
|
|
&[
|
|
op(Opcode::LoadConst, 0),
|
|
op(Opcode::StoreGlobal, 0),
|
|
op(Opcode::LoadConst, 1),
|
|
simple(Opcode::Await),
|
|
simple(Opcode::ReturnValue),
|
|
],
|
|
vec![text("prefix"), Value::Object(gate)],
|
|
CodeLayout {
|
|
names: vec!["step".into()],
|
|
..CodeLayout::default()
|
|
},
|
|
);
|
|
let function = vm
|
|
.new_function("awaited", code, globals, vec![], vec![])
|
|
.unwrap();
|
|
let coroutine = vm
|
|
.new_coroutine(function, &[], ExecutionLimits::default())
|
|
.unwrap();
|
|
assert!(
|
|
matches!(vm.resume_coroutine(coroutine, 64).unwrap(), CoroutineProgress::Waiting { awaitable, .. } if awaitable == gate)
|
|
);
|
|
vm.release(value).unwrap();
|
|
vm.release(gate).unwrap();
|
|
vm.dict_set(globals, text("step"), text("interleaved"))
|
|
.unwrap();
|
|
vm.collect_cycles();
|
|
ready.store(true, Ordering::Release);
|
|
let CoroutineProgress::Complete { value: result, .. } =
|
|
vm.resume_coroutine(coroutine, 64).unwrap()
|
|
else {
|
|
panic!("ready await must complete")
|
|
};
|
|
assert_eq!(
|
|
vm.dict_get(globals, &text("step")).unwrap(),
|
|
Some(text("interleaved"))
|
|
);
|
|
for object in [coroutine, function, code, globals] {
|
|
vm.release(object).unwrap();
|
|
}
|
|
let Value::Object(reference) = result else {
|
|
panic!("owned object result")
|
|
};
|
|
assert_eq!(vm.get_attr(reference, "marker").unwrap(), text("kept"));
|
|
vm.release_value(result).unwrap();
|
|
assert_eq!(vm.object_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn named_reads_ignore_unrelated_globals_but_track_missing_shadow_and_payload_kind() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let globals = vm.new_dict().unwrap();
|
|
let builtins = vm.new_dict().unwrap();
|
|
vm.dict_set(builtins, text("name"), 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!["name".into()],
|
|
..CodeLayout::default()
|
|
},
|
|
);
|
|
let function = vm
|
|
.new_function("read-name", code, globals, vec![], vec![])
|
|
.unwrap();
|
|
let endor_core::ExecutionProgress::Complete {
|
|
value,
|
|
reads,
|
|
dependencies,
|
|
..
|
|
} = vm
|
|
.start_read_only_call(function, &[], ExecutionLimits::default(), 64)
|
|
.unwrap()
|
|
else {
|
|
panic!("complete")
|
|
};
|
|
assert_eq!(value, text("builtin"));
|
|
assert!(reads.contains(&globals) && reads.contains(&builtins));
|
|
vm.release_value(value).unwrap();
|
|
vm.dict_set(globals, text("_"), text("unrelated")).unwrap();
|
|
let (value, synchronous) = vm
|
|
.call_read_only_precise_with_limits(function, &[], ExecutionLimits::default())
|
|
.unwrap();
|
|
assert_eq!(value, text("builtin"));
|
|
assert_eq!(synchronous.dependencies, dependencies);
|
|
assert_eq!(synchronous.objects, reads);
|
|
vm.release_value(value).unwrap();
|
|
let (value, conservative) = vm
|
|
.call_read_only_with_limits(function, &[], ExecutionLimits::default())
|
|
.unwrap();
|
|
assert_eq!(conservative, reads);
|
|
vm.release_value(value).unwrap();
|
|
assert!(
|
|
dependencies
|
|
.iter()
|
|
.all(|dependency| vm.object_store().read_dependency_valid(dependency))
|
|
);
|
|
vm.dict_set(globals, text("name"), text("shadow")).unwrap();
|
|
assert!(
|
|
dependencies
|
|
.iter()
|
|
.any(|dependency| !vm.object_store().read_dependency_valid(dependency))
|
|
);
|
|
vm.dict_delete(globals, &text("name")).unwrap();
|
|
assert!(
|
|
dependencies
|
|
.iter()
|
|
.all(|dependency| vm.object_store().read_dependency_valid(dependency))
|
|
);
|
|
vm.dict_set(builtins, text("name"), text("changed"))
|
|
.unwrap();
|
|
assert!(
|
|
dependencies
|
|
.iter()
|
|
.any(|dependency| !vm.object_store().read_dependency_valid(dependency))
|
|
);
|
|
for object in [function, code, globals, builtins] {
|
|
vm.release(object).unwrap();
|
|
}
|
|
assert_eq!(vm.object_count(), 0);
|
|
assert!(
|
|
dependencies
|
|
.iter()
|
|
.any(|dependency| !vm.object_store().read_dependency_valid(dependency))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn precise_nested_regions_replay_borrowed_dependencies_without_hiding_full_dict_reads() {
|
|
let mut vm = endor_core::ObjectStore::new().unwrap();
|
|
let globals = vm.new_dict().unwrap();
|
|
vm.dict_set(globals, text("name"), text("value")).unwrap();
|
|
let scope = vm.begin_read_only();
|
|
let token = vm.begin_read_tracking().unwrap();
|
|
vm.dict_get_name(globals, "name").unwrap();
|
|
let tracked = vm.end_read_tracking_precise(token).unwrap();
|
|
assert!(
|
|
tracked
|
|
.accesses
|
|
.iter()
|
|
.any(|access| access.object == globals)
|
|
);
|
|
for dependency in &tracked.dependencies {
|
|
assert!(vm.read_dependency_valid(dependency));
|
|
}
|
|
let reads = vm.end_read_only_precise(scope).unwrap();
|
|
vm.dict_set(globals, text("other"), Value::Nil).unwrap();
|
|
assert!(
|
|
reads
|
|
.dependencies
|
|
.iter()
|
|
.all(|dependency| vm.read_dependency_valid(dependency))
|
|
);
|
|
let scope = vm.begin_read_only();
|
|
vm.dict_items(globals).unwrap();
|
|
let reads = vm.end_read_only_precise(scope).unwrap();
|
|
vm.dict_set(globals, text("third"), Value::Nil).unwrap();
|
|
assert!(
|
|
reads
|
|
.dependencies
|
|
.iter()
|
|
.any(|dependency| !vm.read_dependency_valid(dependency))
|
|
);
|
|
vm.release(globals).unwrap();
|
|
assert_eq!(vm.object_count(), 0);
|
|
}
|