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")); } 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(); 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 ); 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 { 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).unwrap(); 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() ); } }