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::(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::(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::(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::(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 { 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::(dict, "dict").unwrap().len(), 0 ); }