Files
endorphin-aster-vm/crates/core/tests/builtins.rs
T
codex-bot 18cd24a203
Verify databases and deploy Aster VM / verify-and-deploy (push) Successful in 15m7s
Align package and executable names with workspace directories
2026-10-18 21:15:10 +08:00

365 lines
13 KiB
Rust

use endor_core::{
CodeLayout, CompareOp, EndorBaseExceptionObject, EndorCode, EndorDictObject,
EndorFunctionObject, EndorInteger, EndorNativeFunctionObject, EndorNumber, EndorNumberObject,
EndorReal, EndorStringObject, EndorTracebackEntry, Instruction, Opcode, Value, Vm, VmError,
};
fn text(value: &str) -> Value {
Value::Text(value.into())
}
#[test]
fn numbers_preserve_large_digits_decimal_scale_exponents_and_fraction_parts() {
let huge = "123456789012345678901234567890123456789012345678901234567890";
assert_eq!(EndorNumber::integer(huge).unwrap().to_string(), huge);
assert_eq!(EndorNumber::integer("-000").unwrap().to_string(), "0");
assert_eq!(EndorNumber::integer("+0012").unwrap().as_i64(), Some(12));
assert_eq!(EndorNumber::integer(huge).unwrap().as_i64(), None);
for literal in [
"-0.0000",
"1.2300000000000000000001",
".5",
"1.",
"2e+999999999999999999999999999999",
] {
assert_eq!(EndorNumber::decimal(literal).unwrap().to_string(), literal);
}
assert_ne!(
EndorNumber::decimal("1.00").unwrap(),
EndorNumber::decimal("1.0").unwrap()
);
assert_eq!(
EndorNumber::rational("2", "-6").unwrap().to_string(),
"-2/6"
);
assert_eq!(EndorNumber::rational("0", "-6").unwrap().to_string(), "0/6");
assert!(EndorNumber::rational("1", "0").is_err());
for invalid in ["", "+", " 1", "1 ", "1_000", "12", "1.0"] {
assert!(EndorNumber::integer(invalid).is_err(), "{invalid}");
}
for invalid in ["", ".", "1.2.3", "1e", "1e+", "1e2e3", "nan", " 0.1", "--1"] {
assert!(EndorNumber::decimal(invalid).is_err(), "{invalid}");
}
}
#[test]
fn number_objects_preserve_ieee_bits_and_reject_undefined_numeric_operations() {
let mut vm = Vm::new().unwrap();
let bits = 0x7ff8_0000_0000_0042;
let nan = EndorNumber::binary_float(f64::from_bits(bits));
assert_eq!(nan.as_real(), Some(&EndorReal::BinaryFloat(bits)));
assert_ne!(
EndorNumber::binary_float(-0.0),
EndorNumber::binary_float(0.0)
);
assert!(!EndorNumber::binary_float(f64::INFINITY).is_zero());
let zero = vm
.new_number(EndorNumber::decimal("-0.000e99999999999").unwrap())
.unwrap();
assert!(!vm.is_true(zero).unwrap());
let complex = EndorNumber::complex(
EndorReal::Integer(EndorInteger::parse("3").unwrap()),
EndorNumber::rational("1", "3")
.unwrap()
.as_real()
.unwrap()
.clone(),
);
let object = vm.new_number(complex).unwrap();
assert_eq!(vm.str(object).unwrap(), "(3 + (1/3)i)");
assert_eq!(
vm.hash(object),
Err(VmError::UnsupportedOperation("numeric hash"))
);
assert_eq!(
vm.compare(object, zero, CompareOp::Eq),
Err(VmError::UnsupportedOperation("numeric comparison"))
);
assert!(
vm.payload::<EndorNumberObject>(object, "number")
.unwrap()
.value()
.as_real()
.is_none()
);
}
#[test]
fn strings_use_unicode_characters_and_content_equality() {
let mut vm = Vm::new().unwrap();
let a = vm.new_string("Endor中文🦀").unwrap();
let b = vm.new_string("Endor中文🦀").unwrap();
let empty = vm.new_string("").unwrap();
let string = vm.payload::<EndorStringObject>(a, "string").unwrap();
assert_eq!(string.char_count(), 8);
assert_eq!(string.char_at(7), Some('🦀'));
assert_eq!(string.char_at(8), None);
assert!(vm.compare(a, b, CompareOp::Eq).unwrap());
assert_eq!(vm.hash(a).unwrap(), vm.hash(b).unwrap());
assert_eq!(vm.str(a).unwrap(), "Endor中文🦀");
assert!(!vm.is_true(empty).unwrap());
}
#[test]
fn dictionaries_keep_order_and_first_key_and_own_keys_and_values() {
let mut vm = Vm::new().unwrap();
let dict = vm.new_dict().unwrap();
let key = vm.new_string("answer").unwrap();
let old = vm.new_object().unwrap();
vm.dict_set(dict, Value::Object(key), Value::Object(old))
.unwrap();
vm.release(key).unwrap();
vm.release(old).unwrap();
vm.dict_set(dict, text("second"), Value::Nil).unwrap();
vm.dict_set(dict, text("answer"), text("replacement"))
.unwrap();
assert_eq!(
vm.dict_get(dict, &text("answer")).unwrap(),
Some(text("replacement"))
);
assert_eq!(vm.dict_items(dict).unwrap()[0].0, Value::Object(key));
assert_eq!(vm.dict_items(dict).unwrap().len(), 2);
assert!(matches!(vm.object(old), Err(VmError::InvalidHandle)));
assert!(vm.dict_delete(dict, &text("answer")).unwrap());
assert!(matches!(vm.object(key), Err(VmError::InvalidHandle)));
assert!(!vm.dict_delete(dict, &text("missing")).unwrap());
vm.dict_clear(dict).unwrap();
assert!(!vm.is_true(dict).unwrap());
}
#[test]
fn failed_container_writes_leave_values_and_counts_unchanged() {
let mut vm = Vm::new().unwrap();
let mut other = Vm::new().unwrap();
let foreign = other.new_object().unwrap();
let dict = vm.new_dict().unwrap();
let child = vm.new_object().unwrap();
vm.dict_set(dict, text("x"), Value::Object(child)).unwrap();
assert_eq!(
vm.dict_set(dict, text("x"), Value::Object(foreign)),
Err(VmError::ForeignHandle)
);
assert_eq!(
vm.dict_get(dict, &text("x")).unwrap(),
Some(Value::Object(child))
);
assert_eq!(vm.ref_count(child).unwrap(), 2);
assert_eq!(
vm.dict_set(dict, Value::Number(1_i64.into()), Value::Nil),
Err(VmError::UnsupportedOperation("numeric hash"))
);
assert_eq!(
vm.dict_set(dict, Value::Object(dict), Value::Nil),
Err(VmError::UnsupportedOperation("dict hash"))
);
let cell = vm.new_cell(Some(Value::Object(child))).unwrap();
assert_eq!(
vm.cell_set(cell, Value::Object(foreign)),
Err(VmError::ForeignHandle)
);
assert_eq!(vm.cell_get(cell).unwrap(), Value::Object(child));
let dead = vm.new_object().unwrap();
vm.release(dead).unwrap();
assert_eq!(
vm.cell_set(cell, Value::Object(dead)),
Err(VmError::InvalidHandle)
);
assert_eq!(vm.ref_count(child).unwrap(), 3);
assert_eq!(
vm.dict_clear(child),
Err(VmError::ObjectKindMismatch("dict"))
);
}
#[test]
fn cells_share_rebinding_distinguish_empty_and_none_and_keep_stable_objects() {
let mut vm = Vm::new().unwrap();
let cell = vm.new_cell(None).unwrap();
assert_eq!(vm.cell_get(cell), Err(VmError::EmptyCell));
vm.cell_set(cell, Value::Nil).unwrap();
assert_eq!(vm.cell_get(cell).unwrap(), Value::Nil);
let child = vm.new_object().unwrap();
vm.cell_set(cell, Value::Object(child)).unwrap();
vm.release(child).unwrap();
vm.cell_set(cell, Value::Object(child)).unwrap();
assert_eq!(vm.ref_count(child).unwrap(), 1);
vm.cell_clear(cell).unwrap();
assert!(matches!(vm.object(child), Err(VmError::InvalidHandle)));
assert_eq!(vm.cell_get(cell), Err(VmError::EmptyCell));
}
#[test]
fn container_cycles_and_duplicate_edges_are_collected() {
let mut vm = Vm::new().unwrap();
let dict = vm.new_dict().unwrap();
let cell = vm.new_cell(Some(Value::Object(dict))).unwrap();
vm.dict_set(dict, text("a"), Value::Object(cell)).unwrap();
vm.dict_set(dict, text("b"), Value::Object(cell)).unwrap();
assert_eq!(vm.ref_count(cell).unwrap(), 3);
vm.release(dict).unwrap();
vm.release(cell).unwrap();
assert_eq!(vm.collect_cycles(), 2);
assert_eq!(vm.object_count(), 0);
}
#[test]
fn functions_retain_globals_constants_defaults_and_shared_closure_cells() {
let mut vm = Vm::new().unwrap();
let globals = vm.new_dict().unwrap();
let constant = vm.new_string("constant").unwrap();
let cell = vm.new_cell(Some(text("first"))).unwrap();
let code = EndorCode::from_instructions(
&[
Instruction::new(Opcode::LoadDeref, 1),
Instruction::simple(Opcode::ReturnValue),
],
vec![Value::Object(constant)],
CodeLayout {
parameters: vec!["argument".into()],
locals: vec!["argument".into()],
free_variables: vec!["captured".into()],
..CodeLayout::default()
},
)
.unwrap();
let code = vm.new_code(code).unwrap();
let function = vm
.new_function(
"read",
code,
globals,
vec![Value::Object(constant)],
vec![cell],
)
.unwrap();
let second = vm
.new_function("read_again", code, globals, vec![], vec![cell])
.unwrap();
assert_eq!(vm.ref_count(constant).unwrap(), 3);
vm.release(code).unwrap();
vm.release(constant).unwrap();
vm.release(cell).unwrap();
vm.cell_set(cell, text("updated")).unwrap();
for reference in [function, second] {
let f = vm
.payload::<EndorFunctionObject>(reference, "function")
.unwrap();
assert_eq!(vm.cell_get(f.closure()[0]).unwrap(), text("updated"));
}
assert_eq!(vm.call(function, &[]).unwrap(), text("updated"));
vm.dict_set(globals, text("read"), Value::Object(function))
.unwrap();
vm.release(globals).unwrap();
vm.release(function).unwrap();
vm.release(second).unwrap();
assert_eq!(vm.collect_cycles(), 5);
}
#[test]
fn malformed_function_environments_are_rejected_without_retaining_any_edges() {
let mut vm = Vm::new().unwrap();
let globals = vm.new_object().unwrap();
let instructions = [
Instruction::new(Opcode::LoadConst, 0),
Instruction::simple(Opcode::ReturnValue),
];
let code = EndorCode::from_instructions(&instructions, vec![Value::Nil], CodeLayout::default())
.unwrap();
let code = vm.new_code(code).unwrap();
assert_eq!(
vm.new_function("bad", code, globals, vec![], vec![]),
Err(VmError::ObjectKindMismatch("dict"))
);
assert_eq!(vm.ref_count(globals).unwrap(), 1);
assert_eq!(vm.ref_count(code).unwrap(), 1);
assert!(EndorCode::new(vec![], vec![], vec!["x".into(), "x".into()], vec![]).is_err());
let code = EndorCode::from_instructions(
&instructions,
vec![Value::Nil],
CodeLayout {
free_variables: vec!["x".into()],
..CodeLayout::default()
},
)
.unwrap();
let code = vm.new_code(code).unwrap();
let globals = vm.new_dict().unwrap();
assert!(
vm.new_function("bad", code, globals, vec![], vec![])
.is_err()
);
assert_eq!(vm.ref_count(code).unwrap(), 1);
}
#[test]
fn exceptions_preserve_arguments_chains_notes_and_traceback_without_type_classes() {
let mut vm = Vm::new().unwrap();
let argument = vm.new_string("detail").unwrap();
let cause = vm
.allocate(EndorBaseExceptionObject::new(
"ValueError",
vec![Value::Object(argument)],
))
.unwrap();
let exception = vm
.allocate(
EndorBaseExceptionObject::new("RuntimeError", vec![text("failed")])
.with_cause(cause)
.with_context(cause)
.with_note("while loading")
.with_traceback(EndorTracebackEntry {
function: "load".into(),
file: "demo.endor".into(),
line: 12,
}),
)
.unwrap();
assert_eq!(vm.ref_count(cause).unwrap(), 3);
vm.release(argument).unwrap();
vm.release(cause).unwrap();
let data = vm
.payload::<EndorBaseExceptionObject>(exception, "exception")
.unwrap();
assert!(data.context_suppressed());
assert_eq!(data.notes(), &["while loading"]);
assert_eq!(data.traceback()[0].line, 12);
assert_eq!(vm.str(exception).unwrap(), "failed");
vm.release(exception).unwrap();
assert_eq!(vm.object_count(), 0);
let wrong = vm.new_dict().unwrap();
assert_eq!(
vm.allocate(EndorBaseExceptionObject::new("Error", vec![]).with_cause(wrong)),
Err(VmError::ObjectKindMismatch("exception"))
);
}
fn echo(arguments: &[Value]) -> Result<Value, VmError> {
match arguments {
[value] => Ok(value.clone()),
_ => Err(VmError::InvalidArgument("expected one value")),
}
}
#[test]
fn native_functions_pass_lossless_numbers_and_return_owned_objects() {
let mut vm = Vm::new().unwrap();
let function = vm
.allocate(EndorNativeFunctionObject::new("echo", echo))
.unwrap();
let number = Value::Number(EndorNumber::decimal("12345678901234567890.000100").unwrap());
assert_eq!(
vm.call(function, std::slice::from_ref(&number)).unwrap(),
number
);
let dict = vm.new_dict().unwrap();
let returned = vm.call(function, &[Value::Object(dict)]).unwrap();
assert_eq!(returned, Value::Object(dict));
assert_eq!(vm.ref_count(dict).unwrap(), 2);
vm.release_value(returned).unwrap();
assert_eq!(vm.ref_count(dict).unwrap(), 1);
assert_eq!(
vm.payload::<EndorDictObject>(dict, "dict").unwrap().len(),
0
);
}