373 lines
11 KiB
Rust
373 lines
11 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"));
|
|
}
|
|
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::<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 {
|
|
try_execute(vm, globals, source, mode).unwrap()
|
|
}
|
|
|
|
fn try_execute(
|
|
vm: &mut Vm,
|
|
globals: ObjectRef,
|
|
source: &str,
|
|
mode: ParseMode,
|
|
) -> Result<Value, VmError> {
|
|
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);
|
|
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()
|
|
);
|
|
}
|
|
}
|
|
|
|
#[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);
|
|
}
|