use compiler::{CompileOptions, compile_source}; use endor_core::{ ContextNativeFunction, EndorTypeObject, NativeCallable, NativeContext, NativeResult, ObjectPayload, TypeConstructor, VmError, }; use endor_core::{ObjectRef, Value, Vm}; use frontend::{EndorReader, ParseMode}; #[derive(Debug)] struct NativeWidget(String); impl ObjectPayload for NativeWidget { fn as_any(&self) -> &dyn std::any::Any { self } fn kind_name(&self) -> &'static str { "widget" } fn allows_attributes(&self) -> bool { true } } #[derive(Debug)] struct WidgetFactory; impl NativeCallable for WidgetFactory { fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { let [ Value::Object(requested), Value::Text(native_label), values @ .., ] = args else { return Err(VmError::InvalidArgument("widget constructor arguments")); }; let fields = cx .store() .payload::(*requested, "Type")? .fields() .to_vec(); if values.len() != fields.len() { return Err(VmError::InvalidArgument("widget field count")); } cx.store().check_class_field_arguments(*requested, values)?; let instance = cx.allocate(NativeWidget(native_label.clone()))?; cx.attach_type(instance, *requested)?; for (field, value) in fields.iter().zip(values) { cx.set_attr(instance, &field.name, value.clone())?; } Ok(NativeResult::Return(Value::Object(instance))) } } #[test] fn source_subclasses_preserve_native_allocation_fields_and_nominal_identity() { let mut vm = Vm::new().unwrap(); vm.set_source_type_checks(true).unwrap(); let globals = vm.new_dict().unwrap(); let roots = vm.bootstrap_types().unwrap(); let factory = vm .allocate(ContextNativeFunction::new("widget", WidgetFactory, vec![])) .unwrap(); let widget = vm .new_type( "Widget", roots.object, TypeConstructor::NativeTyped(factory), ) .unwrap(); for (name, object) in [ ("Object", roots.object), ("Type", roots.type_type), ("Widget", widget), ] { vm.dict_set(globals, Value::Text(name.into()), Value::Object(object)) .unwrap(); vm.release(object).unwrap(); } vm.release(factory).unwrap(); let source = "class Child: Widget { value: Number }\nclass Grand: Child { label: String }"; assert_eq!( execute(&mut vm, globals, source, ParseMode::Module), Value::Nil ); assert!(matches!( try_execute( &mut vm, globals, r#"Grand("native", "bad", "extra")"#, ParseMode::Expression ), Err(VmError::TypeMismatch { .. }) )); let value = execute( &mut vm, globals, "Grand(\"native\", 9, \"extra\")", ParseMode::Expression, ); let Value::Object(instance) = value else { panic!("native instance") }; assert_eq!( vm.payload::(instance, "widget").unwrap().0, "native" ); assert!(vm.is_instance(instance, widget).unwrap()); assert_eq!( vm.get_attr(instance, "value").unwrap(), Value::Number(9_i64.into()) ); assert_eq!( vm.get_attr(instance, "label").unwrap(), Value::Text("extra".into()) ); vm.dict_clear(globals).unwrap(); vm.release(globals).unwrap(); vm.collect_cycles(); assert!(vm.is_instance(instance, widget).unwrap()); vm.release(instance).unwrap(); vm.collect_cycles(); assert_eq!(vm.object_count(), 0); } fn execute(vm: &mut Vm, globals: ObjectRef, source: &str, mode: ParseMode) -> Value { try_execute(vm, globals, source, mode).unwrap() } fn try_execute( vm: &mut Vm, globals: ObjectRef, source: &str, mode: ParseMode, ) -> Result { let names = vm .dict_items(globals) .unwrap() .iter() .filter_map(|(name, _)| { if let Value::Text(name) = name { Some(name.clone()) } else { None } }) .collect(); let source = EndorReader::from_text("", source); let template = compile_source(&source, mode, &CompileOptions { globals: names }).unwrap(); let code = template.load(vm).unwrap(); let result = vm.execute(code, globals); vm.release(code).unwrap(); result } #[test] fn source_inheritance_binding_and_dynamic_dispatch_keep_the_real_receiver_alive() { let mut vm = Vm::new().unwrap(); let globals = vm.new_dict().unwrap(); let roots = vm.bootstrap_types().unwrap(); for (name, object) in [("Object", roots.object), ("Type", roots.type_type)] { vm.dict_set(globals, Value::Text(name.into()), Value::Object(object)) .unwrap(); vm.release(object).unwrap(); } let source = r#" class Base { value: Number function read(self: Base): Number { return self.value } function via(self: Base): Number { return self.read() } function withEvent(self: Base, event: Object): Object { return event } } class Child: Base { label: String function read(self: Child): Number { return 12 } } "#; assert_eq!( execute(&mut vm, globals, source, ParseMode::Module), Value::Nil ); let callback = execute( &mut vm, globals, "Child(9, \"ok\").via", ParseMode::Expression, ); let Value::Object(callback) = callback else { panic!("bound callback") }; assert_eq!( vm.object_store() .positional_parameter_count(callback) .unwrap(), Some(0) ); let with_event = execute( &mut vm, globals, "Child(9, \"ok\").withEvent", ParseMode::Expression, ); let Value::Object(with_event) = with_event else { panic!("bound callback") }; assert_eq!( vm.object_store() .positional_parameter_count(with_event) .unwrap(), Some(1) ); vm.release(with_event).unwrap(); vm.dict_clear(globals).unwrap(); vm.release(globals).unwrap(); vm.collect_cycles(); // No global name or external instance root remains. The bound callback // alone retains its receiver and can still dispatch to Child.read. assert_eq!( vm.call(callback, &[]).unwrap(), Value::Number(12_i64.into()) ); vm.release(callback).unwrap(); vm.collect_cycles(); assert_eq!(vm.object_count(), 0); } #[test] fn duplicate_members_and_missing_receivers_fail_before_execution() { for text in [ "class Bad { value: Number value: String }", "class Bad { function read(): Number { return 1 } }", ] { let source = EndorReader::from_text("", text); assert!( compile_source( &source, ParseMode::Module, &CompileOptions { globals: vec!["Object".into()] } ) .is_err() ); } } #[test] fn declared_field_checks_cover_construction_inheritance_writes_and_gc() { let mut vm = Vm::new().unwrap(); vm.set_source_type_checks(true).unwrap(); let globals = vm.new_dict().unwrap(); let roots = vm.bootstrap_types().unwrap(); vm.dict_set( globals, Value::Text("Object".into()), Value::Object(roots.object), ) .unwrap(); vm.release(roots.object).unwrap(); vm.release(roots.type_type).unwrap(); execute( &mut vm, globals, r#" class Base { count: Number } class Child: Base { label: String } value = Child(9, "ok") class Holder { child: Base } holder = Holder(value) class Unknown { value: Missing } "#, ParseMode::Interactive, ); let Value::Object(value) = vm .dict_get(globals, &Value::Text("value".into())) .unwrap() .unwrap() else { panic!("instance") }; let version = vm.object_store().change_version(value).unwrap(); for source in [ r#"Child("bad", "ok")"#, "Child(9, 2)", "Holder(1)", "Unknown(1)", r#"value.count = "bad""#, ] { assert!(matches!( try_execute(&mut vm, globals, source, ParseMode::Interactive), Err(VmError::TypeMismatch { .. } | VmError::UnknownAnnotation(_)) )); } assert_eq!( vm.get_attr(value, "count").unwrap(), Value::Number(9.into()) ); assert_eq!(vm.object_store().change_version(value).unwrap(), version); assert!(vm.del_attr(value, "count").is_err()); vm.set_attr(value, "count", Value::Number(10.into())) .unwrap(); execute(&mut vm, globals, "class Base {}", ParseMode::Interactive); // Other nominal names resolve dynamically in the declaring environment. assert!(matches!( try_execute(&mut vm, globals, "Holder(value)", ParseMode::Expression), Err(VmError::TypeMismatch { .. }) )); // An inherited scalar field still belongs to its original declaration. vm.set_attr(value, "count", Value::Number(11.into())) .unwrap(); vm.dict_clear(globals).unwrap(); vm.release(globals).unwrap(); vm.collect_cycles(); assert_eq!(vm.object_count(), 0); } #[test] fn checked_fields_keep_declaring_self_type_and_can_be_opted_out() { let mut vm = Vm::new().unwrap(); vm.set_source_type_checks(true).unwrap(); let globals = vm.new_dict().unwrap(); let roots = vm.bootstrap_types().unwrap(); vm.dict_set( globals, Value::Text("Object".into()), Value::Object(roots.object), ) .unwrap(); vm.release(roots.object).unwrap(); vm.release(roots.type_type).unwrap(); // Object permits nil, so a separate numeric field provides an initial instance. execute( &mut vm, globals, r#" class Link { next: Link } class Seed: Link {} "#, ParseMode::Interactive, ); // A recursive nonnullable field can be initialized by the trusted host in two steps. let Value::Object(link) = vm .dict_get(globals, &Value::Text("Link".into())) .unwrap() .unwrap() else { panic!("class") }; let seed = vm.new_object().unwrap(); vm.attach_type(seed, link).unwrap(); vm.set_attr(seed, "next", Value::Object(seed)).unwrap(); vm.dict_set(globals, Value::Text("old".into()), Value::Object(seed)) .unwrap(); execute( &mut vm, globals, "class Link { value: Number }", ParseMode::Interactive, ); vm.set_attr(seed, "next", Value::Object(seed)).unwrap(); assert!(matches!( vm.set_attr(seed, "next", Value::Nil), Err(VmError::TypeMismatch { .. }) )); vm.set_source_type_checks(false).unwrap(); vm.set_attr(seed, "next", Value::Nil).unwrap(); vm.set_source_type_checks(true).unwrap(); vm.set_attr(seed, "next", Value::Object(seed)).unwrap(); vm.release(seed).unwrap(); vm.dict_clear(globals).unwrap(); vm.release(globals).unwrap(); vm.collect_cycles(); assert_eq!(vm.object_count(), 0); }