Files
endorphin-aster-vm/crates/compiler/tests/classes.rs
T

224 lines
6.6 KiB
Rust

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<NativeResult, VmError> {
let [
Value::Object(requested),
Value::Text(native_label),
values @ ..,
] = args
else {
return Err(VmError::InvalidArgument("widget constructor arguments"));
};
let fields = cx
.store()
.payload::<EndorTypeObject>(*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::<NativeWidget>(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("<classes>", 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("<invalid-class>", text);
assert!(
compile_source(
&source,
ParseMode::Module,
&CompileOptions {
globals: vec!["Object".into()]
}
)
.is_err()
);
}
}