Validate method substitution before definition updates and execution

This commit is contained in:
2026-10-22 14:13:46 +08:00
parent 6b4cce1fee
commit e26e227dd0
14 changed files with 803 additions and 14 deletions
+292
View File
@@ -0,0 +1,292 @@
//! Method substitution rules. This is a child of types so validation can inspect
//! method tables without exporting mutable definition internals.
use super::{AnnotationType, EndorTypeObject};
use crate::{
CallArity, CallEffect, CallSignature, CallType, EndorCodeObject, EndorFunctionObject,
ObjectRef, ObjectStore, VmError,
};
struct Contract {
arity: CallArity,
parameters: Vec<CallType>,
variadic: Option<CallType>,
returns: CallType,
effect: Option<CallEffect>,
}
impl Contract {
fn parameter(&self, index: usize) -> Option<&CallType> {
self.parameters.get(index).or(self.variadic.as_ref())
}
}
impl From<CallSignature> for Contract {
fn from(value: CallSignature) -> Self {
Self {
arity: value.arity,
parameters: value.parameters,
variadic: value.variadic,
returns: value.returns,
effect: Some(value.effect),
}
}
}
#[derive(Clone, Copy)]
struct Patch<'a> {
owner: ObjectRef,
name: &'a str,
function: ObjectRef,
}
impl CallType {
/// Whether every value admitted by provided is admitted by self.
/// Nominal inheritance and explicit capabilities participate; no structural
/// or callable-signature inference is implied by this relation.
pub fn accepts_type(&self, store: &ObjectStore, provided: &Self) -> Result<bool, VmError> {
self.validate(store)?;
provided.validate(store)?;
for actual in provided.alternatives() {
let mut accepted = false;
for expected in self.alternatives() {
accepted = match (*expected, *actual) {
(AnnotationType::Any, _) => true,
(AnnotationType::Nominal(base), AnnotationType::Nominal(child)) => {
store.is_subtype(child, base)?
}
(
AnnotationType::Capability(capability),
AnnotationType::Nominal(definition),
) => store.implements_trait(definition, capability)?,
(expected, actual) => expected == actual,
};
if accepted {
break;
}
}
if !accepted {
return Ok(false);
}
}
Ok(true)
}
}
impl ObjectStore {
fn method_contract(
&self,
function: ObjectRef,
owner: ObjectRef,
) -> Result<Option<Contract>, VmError> {
if let Some(native) = self.native_signature(function)? {
return Ok(Some(native.into()));
}
let Some(data) = self
.object(function)?
.payload()
.as_any()
.downcast_ref::<EndorFunctionObject>()
else {
return Ok(None);
};
let code = self.payload::<EndorCodeObject>(data.code(), "code")?.code();
let Some(signature) = code.source_signature() else {
return Ok(None);
};
let resolve = |name: &str| {
if signature.method_owner.as_deref() == Some(name) {
let mut current = Some(owner);
while let Some(definition) = current {
let data = self.payload::<EndorTypeObject>(definition, "Type")?;
if data.name == name
&& (definition == owner
|| data
.methods
.values()
.any(|candidate| *candidate == function))
{
return Ok(AnnotationType::Nominal(definition));
}
current = data.base;
}
}
self.annotation_type(function, name, None)
};
let parameters = signature
.parameters
.iter()
.map(|name| resolve(name).map(CallType::from))
.collect::<Result<Vec<_>, _>>()?;
Ok(Some(Contract {
arity: self
.call_arity(function)?
.ok_or(VmError::InvalidArgument("source method lacks arity"))?,
parameters,
variadic: None,
returns: resolve(&signature.return_type)?.into(),
effect: None,
}))
}
fn own_method(
&self,
owner: ObjectRef,
name: &str,
patch: Option<Patch<'_>>,
) -> Result<Option<ObjectRef>, VmError> {
if let Some(patch) = patch
&& patch.owner == owner
&& patch.name == name
{
return Ok(Some(patch.function));
}
Ok(self
.payload::<EndorTypeObject>(owner, "Type")?
.methods
.get(name)
.copied())
}
fn inherited_method(
&self,
mut owner: Option<ObjectRef>,
name: &str,
patch: Option<Patch<'_>>,
) -> Result<Option<(ObjectRef, ObjectRef)>, VmError> {
while let Some(definition) = owner {
if let Some(function) = self.own_method(definition, name, patch)? {
return Ok(Some((definition, function)));
}
owner = self.payload::<EndorTypeObject>(definition, "Type")?.base;
}
Ok(None)
}
fn check_override(
&self,
owner: ObjectRef,
name: &str,
function: ObjectRef,
patch: Option<Patch<'_>>,
defer_unknown: bool,
) -> Result<(), VmError> {
let definition = self.payload::<EndorTypeObject>(owner, "Type")?;
let Some((base_owner, base_function)) =
self.inherited_method(definition.base, name, patch)?
else {
return Ok(());
};
let checked = (|| {
let Some(base) = self.method_contract(base_function, base_owner)? else {
return Ok(());
};
let invalid = |reason| VmError::OverrideViolation {
definition: definition.name.clone(),
method: name.into(),
reason,
};
let derived = self
.method_contract(function, owner)?
.ok_or_else(|| invalid("override lacks type metadata"))?;
if derived.arity.required > base.arity.required
|| derived.arity.maximum < base.arity.maximum
{
return Err(invalid("override narrows accepted argument counts"));
}
let receiver = CallType::from(AnnotationType::Nominal(owner));
if !derived
.parameter(0)
.ok_or_else(|| invalid("override lacks receiver"))?
.accepts_type(self, &receiver)?
{
return Err(invalid("override receiver rejects its declaring type"));
}
let fixed = base
.parameters
.len()
.max(derived.parameters.len())
.min(base.arity.maximum);
for index in 1..fixed {
let expected = base
.parameter(index)
.ok_or_else(|| invalid("incomplete base parameter metadata"))?;
let actual = derived
.parameter(index)
.ok_or_else(|| invalid("incomplete override parameter metadata"))?;
if !actual.accepts_type(self, expected)? {
return Err(invalid("override narrows a parameter type"));
}
}
if base.arity.maximum > fixed {
let expected = base
.parameter(fixed)
.ok_or_else(|| invalid("incomplete base variadic metadata"))?;
let actual = derived
.parameter(fixed)
.ok_or_else(|| invalid("incomplete override variadic metadata"))?;
if !actual.accepts_type(self, expected)? {
return Err(invalid("override narrows a variadic parameter type"));
}
}
if !base.returns.accepts_type(self, &derived.returns)? {
return Err(invalid("override widens the return type"));
}
if let (Some(expected), Some(actual)) = (base.effect, derived.effect) {
let compatible = match expected {
CallEffect::ReadOnly => actual == CallEffect::ReadOnly,
CallEffect::Mutating => actual != CallEffect::External,
CallEffect::External => true,
};
if !compatible {
return Err(invalid("override expands native effects"));
}
}
Ok(())
})();
match checked {
Err(VmError::UnknownAnnotation(_)) if defer_unknown => Ok(()),
result => result,
}
}
pub(super) fn validate_class_overrides(&self, definition: ObjectRef) -> Result<(), VmError> {
if self.field_type_checks {
for (name, function) in &self.payload::<EndorTypeObject>(definition, "Type")?.methods {
self.check_override(definition, name, *function, None, true)?;
}
}
Ok(())
}
pub(super) fn validate_override_update(
&self,
owner: ObjectRef,
name: &str,
function: ObjectRef,
) -> Result<(), VmError> {
if self.field_type_checks {
let patch = Some(Patch {
owner,
name,
function,
});
for descendant in self.descendant_types(owner)? {
if let Some(function) = self.own_method(descendant, name, patch)? {
self.check_override(descendant, name, function, patch, true)?;
}
}
}
Ok(())
}
/// Revalidate dynamic annotation names before a source method body executes.
/// Ordinary functions whose first argument is an object are not methods.
pub fn check_method_override_call(
&self,
function: ObjectRef,
receiver: ObjectRef,
) -> Result<(), VmError> {
let mut owner = self.type_of(receiver)?;
while let Some(definition) = owner {
let data = self.payload::<EndorTypeObject>(definition, "Type")?;
for (name, candidate) in &data.methods {
if *candidate == function {
self.check_override(definition, name, function, None, false)?;
}
}
owner = data.base;
}
Ok(())
}
}