Files
endorphin-aster-vm/crates/core/tests/method_contracts.rs
T

446 lines
12 KiB
Rust

//! Compile here; execute only in the designated remote validation environment.
use endor_core::{
AnnotationType as T, CallEffect, CallSignature, CallType, CodeLayout, ContextNativeFunction,
EndorCode, Instruction, NativeCallable, NativeContext, NativeResult, ObjectRef, Opcode,
SourceSignature, TypeConstructor, Value, Vm, VmError,
};
use std::collections::BTreeMap;
fn source(
vm: &mut Vm,
globals: ObjectRef,
owner: &str,
parameters: &[&str],
returns: &str,
result: Value,
) -> ObjectRef {
let code = EndorCode::from_instructions(
&[
Instruction::new(Opcode::LoadConst, 0),
Instruction::new(Opcode::StoreGlobal, 0),
Instruction::new(Opcode::LoadConst, 1),
Instruction::simple(Opcode::ReturnValue),
],
vec![Value::Number(42.into()), result],
CodeLayout {
parameters: (0..parameters.len()).map(|i| format!("p{i}")).collect(),
names: vec!["ran".into()],
..CodeLayout::default()
},
)
.unwrap()
.with_source_signature(SourceSignature {
parameters: parameters.iter().map(|s| (*s).into()).collect(),
return_type: returns.into(),
method_owner: Some(owner.into()),
})
.unwrap();
let code = vm.new_code(code).unwrap();
let function = vm
.new_function("check", code, globals, vec![], vec![])
.unwrap();
vm.release(code).unwrap();
function
}
fn class(
vm: &mut Vm,
name: &str,
base: ObjectRef,
function: ObjectRef,
) -> Result<ObjectRef, VmError> {
vm.new_class(
name,
base,
vec![],
BTreeMap::from([("check".into(), function)]),
)
}
#[test]
fn source_parameters_are_contravariant_and_returns_covariant_at_class_creation() {
let mut vm = Vm::new().unwrap();
let roots = vm.bootstrap_types().unwrap();
vm.set_source_type_checks(true).unwrap();
let globals = vm.new_dict().unwrap();
let base_fn = source(
&mut vm,
globals,
"Base",
&["Base", "Number"],
"Object",
Value::Nil,
);
let base = class(&mut vm, "Base", roots.object, base_fn).unwrap();
let good_fn = source(
&mut vm,
globals,
"Good",
&["Good", "Object"],
"Number",
Value::Number(7.into()),
);
let good = class(&mut vm, "Good", base, good_fn).unwrap();
let narrow = source(
&mut vm,
globals,
"Narrow",
&["Narrow", "String"],
"Number",
Value::Nil,
);
assert!(matches!(
class(&mut vm, "Narrow", base, narrow),
Err(VmError::OverrideViolation {
reason: "override narrows a parameter type",
..
})
));
let numeric = source(
&mut vm,
globals,
"Numeric",
&["Numeric", "Object"],
"Number",
Value::Number(0.into()),
);
let numeric_type = class(&mut vm, "Numeric", roots.object, numeric).unwrap();
let broad = source(
&mut vm,
globals,
"Broad",
&["Broad", "Object"],
"Object",
Value::Nil,
);
assert!(matches!(
class(&mut vm, "Broad", numeric_type, broad),
Err(VmError::OverrideViolation {
reason: "override widens the return type",
..
})
));
for object in [
good,
good_fn,
narrow,
broad,
numeric_type,
numeric,
base,
base_fn,
globals,
roots.object,
roots.type_type,
] {
vm.release(object).unwrap();
}
vm.collect_cycles();
assert_eq!(vm.object_count(), 0);
}
#[test]
fn replacing_an_ancestor_validates_overriding_descendants_before_mutation() {
let mut vm = Vm::new().unwrap();
let roots = vm.bootstrap_types().unwrap();
vm.set_source_type_checks(true).unwrap();
let globals = vm.new_dict().unwrap();
let base_fn = source(
&mut vm,
globals,
"Base",
&["Base", "Number"],
"Object",
Value::Nil,
);
let base = class(&mut vm, "Base", roots.object, base_fn).unwrap();
let child_fn = source(
&mut vm,
globals,
"Child",
&["Child", "Number"],
"Number",
Value::Number(0.into()),
);
let child = class(&mut vm, "Child", base, child_fn).unwrap();
let replacement = source(
&mut vm,
globals,
"Base",
&["Base", "Object"],
"Object",
Value::Nil,
);
let version = vm.change_version(base).unwrap();
assert!(matches!(
vm.define_method(base, "check", replacement),
Err(VmError::OverrideViolation { .. })
));
assert_eq!(vm.change_version(base).unwrap(), version);
assert_eq!(
vm.object_store().lookup_method(base, "check").unwrap(),
Some(base_fn)
);
assert_eq!(
vm.object_store().lookup_method(child, "check").unwrap(),
Some(child_fn)
);
for object in [
replacement,
child,
child_fn,
base,
base_fn,
globals,
roots.object,
roots.type_type,
] {
vm.release(object).unwrap();
}
vm.collect_cycles();
assert_eq!(vm.object_count(), 0);
}
#[test]
fn global_rebinding_is_rechecked_before_the_source_body_and_recovers() {
let mut vm = Vm::new().unwrap();
let roots = vm.bootstrap_types().unwrap();
vm.set_source_type_checks(true).unwrap();
let parent = vm
.new_type("Result", roots.object, TypeConstructor::Record)
.unwrap();
let result_type = vm
.new_type("ResultChild", parent, TypeConstructor::Record)
.unwrap();
let other = vm
.new_type("Other", roots.object, TypeConstructor::Record)
.unwrap();
let result = vm.new_object().unwrap();
vm.attach_type(result, result_type).unwrap();
let base_globals = vm.new_dict().unwrap();
let globals = vm.new_dict().unwrap();
vm.dict_set(
base_globals,
Value::Text("Result".into()),
Value::Object(parent),
)
.unwrap();
vm.dict_set(
globals,
Value::Text("Actual".into()),
Value::Object(result_type),
)
.unwrap();
vm.dict_set(globals, Value::Text("ran".into()), Value::Number(0.into()))
.unwrap();
let base_fn = source(
&mut vm,
base_globals,
"Base",
&["Base"],
"Result",
Value::Nil,
);
let base = class(&mut vm, "Base", roots.object, base_fn).unwrap();
let child_fn = source(
&mut vm,
globals,
"Child",
&["Child"],
"Actual",
Value::Object(result),
);
let child = class(&mut vm, "Child", base, child_fn).unwrap();
let receiver = vm.new_object().unwrap();
vm.attach_type(receiver, child).unwrap();
vm.dict_set(globals, Value::Text("Actual".into()), Value::Object(other))
.unwrap();
assert!(matches!(
vm.call(child_fn, &[Value::Object(receiver)]),
Err(VmError::OverrideViolation { .. })
));
assert_eq!(
vm.dict_get(globals, &Value::Text("ran".into())).unwrap(),
Some(Value::Number(0.into()))
);
vm.dict_set(
globals,
Value::Text("Actual".into()),
Value::Object(result_type),
)
.unwrap();
let value = vm.call(child_fn, &[Value::Object(receiver)]).unwrap();
assert_eq!(value, Value::Object(result));
vm.release_value(value).unwrap();
for object in [
receiver,
child,
child_fn,
base,
base_fn,
globals,
base_globals,
result,
other,
result_type,
parent,
roots.object,
roots.type_type,
] {
vm.release(object).unwrap();
}
vm.collect_cycles();
assert_eq!(vm.object_count(), 0);
}
#[derive(Debug)]
struct Native(CallSignature);
impl NativeCallable for Native {
fn signature(&self) -> Option<CallSignature> {
Some(self.0.clone())
}
fn invoke(&self, _: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
Ok(NativeResult::Return(Value::Number(0.into())))
}
}
#[test]
fn native_overrides_preserve_union_inputs_and_do_not_expand_effects() {
let mut vm = Vm::new().unwrap();
let roots = vm.bootstrap_types().unwrap();
vm.set_source_type_checks(true).unwrap();
let base = vm
.new_type("Base", roots.object, TypeConstructor::Record)
.unwrap();
let mut signature = CallSignature::exact(
vec![T::Nominal(base), T::Any],
T::Number,
CallEffect::ReadOnly,
);
signature.parameters[1] = CallType::Union(vec![T::Number, T::String]);
let base_fn = vm
.allocate(ContextNativeFunction::new(
"check",
Native(signature),
vec![],
))
.unwrap();
vm.define_method(base, "check", base_fn).unwrap();
let child = vm.new_type("Child", base, TypeConstructor::Record).unwrap();
let effect_fn = vm
.allocate(ContextNativeFunction::new(
"effect",
Native(CallSignature::exact(
vec![T::Nominal(child), T::Any],
T::Number,
CallEffect::Mutating,
)),
vec![],
))
.unwrap();
assert!(matches!(
vm.define_method(child, "check", effect_fn),
Err(VmError::OverrideViolation {
reason: "override expands native effects",
..
})
));
let narrow_fn = vm
.allocate(ContextNativeFunction::new(
"narrow",
Native(CallSignature::exact(
vec![T::Nominal(child), T::Number],
T::Number,
CallEffect::ReadOnly,
)),
vec![],
))
.unwrap();
assert!(matches!(
vm.define_method(child, "check", narrow_fn),
Err(VmError::OverrideViolation {
reason: "override narrows a parameter type",
..
})
));
for object in [
narrow_fn,
effect_fn,
child,
base_fn,
base,
roots.object,
roots.type_type,
] {
vm.release(object).unwrap();
}
vm.collect_cycles();
assert_eq!(vm.object_count(), 0);
}
#[test]
fn unknown_forward_names_defer_until_call_without_entering_the_body() {
let mut vm = Vm::new().unwrap();
let roots = vm.bootstrap_types().unwrap();
vm.set_source_type_checks(true).unwrap();
let globals = vm.new_dict().unwrap();
vm.dict_set(globals, Value::Text("ran".into()), Value::Number(0.into()))
.unwrap();
let base_fn = source(
&mut vm,
globals,
"Base",
&["Base", "Number"],
"Number",
Value::Number(0.into()),
);
let base = class(&mut vm, "Base", roots.object, base_fn).unwrap();
let child_fn = source(
&mut vm,
globals,
"Child",
&["Child", "Later"],
"Number",
Value::Number(0.into()),
);
let child = class(&mut vm, "Child", base, child_fn).unwrap();
let receiver = vm.new_object().unwrap();
vm.attach_type(receiver, child).unwrap();
assert!(matches!(
vm.call(
child_fn,
&[Value::Object(receiver), Value::Number(1.into())]
),
Err(VmError::UnknownAnnotation(_))
));
let later = vm
.new_type("Later", roots.object, TypeConstructor::Record)
.unwrap();
vm.dict_set(globals, Value::Text("Later".into()), Value::Object(later))
.unwrap();
assert!(matches!(
vm.call(
child_fn,
&[Value::Object(receiver), Value::Number(1.into())]
),
Err(VmError::OverrideViolation { .. })
));
assert_eq!(
vm.dict_get(globals, &Value::Text("ran".into())).unwrap(),
Some(Value::Number(0.into()))
);
for object in [
receiver,
child,
child_fn,
base,
base_fn,
later,
globals,
roots.object,
roots.type_type,
] {
vm.release(object).unwrap();
}
vm.collect_cycles();
assert_eq!(vm.object_count(), 0);
}