Enforce native type signatures and effects across delegated calls

This commit is contained in:
2026-10-22 13:51:42 +08:00
parent f9243c77c7
commit 680caf81e3
14 changed files with 676 additions and 24 deletions
+3 -3
View File
@@ -4,10 +4,10 @@
| 文件 | 职责 |
| --- | --- |
| [lib.rs](lib.rs) | VM 类型导出、objects 类型兼容重导出(含 ReadAccess)、宿主调用与字节码 rustdoc 示例 |
| [lib.rs](lib.rs) | VM 类型导出、objects 类型兼容重导出(含 ReadAccess/CallSignature/CallEffect)、宿主调用与字节码 rustdoc 示例 |
| [vm.rs](vm.rs) | Vm 拥有 ObjectStore 与错误位置记录;对象操作和宿主 new_class 和显式Trait注册/关系查询显式委托给 objects;函数/字段检查统一开关、统一参数区间查询与函数类型配置 |
| [frame.rs](frame.rs) | EndorFrame、栈条目、返回类型执行根、执行位置元数据、ExecutionLimits |
| [eval.rs](eval.rs) | execute/call、只读调用与依赖结果、参数绑定、原生实现前数量检查、可选注解检查、源码类字段环境绑定、帧切换、取指分派、MakeFunction、结果根转移与失败清理 |
| [frame.rs](frame.rs) | EndorFrame、栈条目、源码与原生委托返回契约执行根、执行位置元数据、ExecutionLimits |
| [eval.rs](eval.rs) | execute/call、只读调用与依赖结果、参数绑定、原生实现前数量/类型/效果检查和委托返回检查、可选注解检查、源码类字段环境绑定、帧切换、取指分派、MakeFunction、结果根转移与失败清理 |
## 常见修改入口
+49 -5
View File
@@ -1,4 +1,4 @@
use crate::frame::StackEntry;
use crate::frame::{NativeReturn, StackEntry};
use crate::{
CompareOp, EndorCellObject, EndorCodeObject, EndorFrame, EndorFunctionObject,
EndorStringObject, ExecutionLimits, ExecutionLocation, Instruction, ObjectRef, Opcode, Value,
@@ -125,6 +125,7 @@ impl Vm {
}
fn clear_frame(&mut self, frame: EndorFrame) {
self.clear_native_returns(frame.native_returns);
for entry in frame.stack {
if let StackEntry::Value(value) = entry {
self.drop_root(value);
@@ -204,6 +205,7 @@ impl Vm {
}
let mut frame = EndorFrame {
return_type,
native_returns: Vec::new(),
function,
previous: None,
code: data.code(),
@@ -273,7 +275,15 @@ impl Vm {
if let Some(callable) = self.object(function)?.payload().native_callable() {
return NativeContext::execute(&mut self.objects, function, arguments, callable);
}
let signature = self.objects.native_signature(function)?;
let name = self.object(function)?.payload().kind_name().to_owned();
if let Some(signature) = &signature {
signature.check_arguments(&self.objects, &name, arguments)?;
}
let result = self.object(function)?.payload().call(arguments)?;
if let Some(signature) = &signature {
signature.check_return(&self.objects, &name, &result)?;
}
self.retain_value(&result)?;
Ok(NativeResult::Return(result))
}
@@ -286,6 +296,7 @@ impl Vm {
limits: ExecutionLimits,
) -> Result<PreparedCall, VmError> {
let mut owned = false;
let mut returns = Vec::new();
let result = (|| loop {
if self
.object(function)?
@@ -293,16 +304,36 @@ impl Vm {
.as_any()
.is::<EndorFunctionObject>()
{
return self
.prepare_frame(function, &arguments)
.map(PreparedCall::Frame);
let mut frame = self.prepare_frame(function, &arguments)?;
frame.native_returns = std::mem::take(&mut returns);
return Ok(PreparedCall::Frame(frame));
}
if *executed >= limits.max_instructions {
return Err(VmError::ExecutionLimit("instructions"));
}
*executed += 1;
if let Some(signature) = self.objects.native_signature(function)?
&& !matches!(signature.returns, crate::AnnotationType::Any)
{
let check = NativeReturn {
name: self.object(function)?.payload().kind_name().into(),
expected: signature.returns,
};
if let Some(reference) = check.expected.reference() {
self.retain(reference)?;
}
returns.push(check);
}
match self.invoke_native(function, &arguments)? {
NativeResult::Return(value) => return Ok(PreparedCall::Value(value)),
NativeResult::Return(value) => {
for check in returns.iter().rev() {
if let Err(error) = check.check(&self.objects, &value) {
self.drop_root(value);
return Err(error);
}
}
return Ok(PreparedCall::Value(value));
}
NativeResult::Invoke {
function: next,
arguments: values,
@@ -319,6 +350,7 @@ impl Vm {
}
}
})();
self.clear_native_returns(returns);
if owned {
self.release(function)
.expect("delegated call owns its function");
@@ -329,6 +361,15 @@ impl Vm {
result
}
fn clear_native_returns(&mut self, returns: Vec<NativeReturn>) {
for check in returns {
if let Some(reference) = check.expected.reference() {
self.release(reference)
.expect("native return check owns its type");
}
}
}
fn stack_value(frame: &EndorFrame, index: usize) -> Result<Value, VmError> {
match frame.stack.get(index) {
Some(StackEntry::Value(value)) => Ok(value.clone()),
@@ -681,6 +722,9 @@ impl Vm {
actual: self.objects.value_type_name(&result)?,
});
}
for check in frame.native_returns.iter().rev() {
check.check(&self.objects, &result)?;
}
frame.stack.pop();
self.clear_frame(frames.pop().unwrap());
if let Some(caller) = frames.last_mut() {
+20
View File
@@ -10,12 +10,32 @@ pub(crate) enum StackEntry {
Value(Value),
}
#[derive(Debug)]
pub(crate) struct NativeReturn {
pub name: String,
pub expected: crate::AnnotationType,
}
impl NativeReturn {
pub fn check(&self, store: &objects::ObjectStore, value: &Value) -> Result<(), crate::VmError> {
if !store.matches_annotation(value, self.expected)? {
return Err(crate::VmError::TypeMismatch {
function: self.name.clone(),
position: "return".into(),
expected: store.annotation_type_name(self.expected)?,
actual: store.value_type_name(value)?,
});
}
Ok(())
}
}
/// Internal activation record, analogous to _PyInterpreterFrame. It has no
/// EndorObject header; introspection objects can be added independently later.
#[derive(Debug)]
pub struct EndorFrame {
pub(crate) function: ObjectRef,
pub(crate) return_type: Option<crate::AnnotationType>,
pub(crate) native_returns: Vec<NativeReturn>,
/// Index of the caller in the execution's frame vector, never a pointer.
pub(crate) previous: Option<usize>,
pub(crate) code: ObjectRef,
+8 -7
View File
@@ -50,12 +50,13 @@ pub use objects::{
TypeConstructor,
};
pub use objects::{
CallArity, ChangeBatch, ChangeKey, CodeLayout, CompareOp, ContextNativeFunction,
DecodedInstruction, EndorBaseExceptionObject, EndorCellObject, EndorCode, EndorCodeObject,
EndorDecimal, EndorDictObject, EndorFunctionObject, EndorInteger, EndorNativeCall,
EndorNativeFunctionObject, EndorNumber, EndorNumberObject, EndorObject, EndorRational,
EndorReal, EndorStringObject, EndorTracebackEntry, Instruction, NativeCall, NativeCallable,
NativeContext, NativeFunction, NativeResult, ObjectChange, ObjectHeader, ObjectPayload,
ObjectRef, Opcode, ReadAccess, Record, SourceSignature, Value, VmError, WeakObjectRef,
CallArity, CallEffect, CallSignature, ChangeBatch, ChangeKey, CodeLayout, CompareOp,
ContextNativeFunction, DecodedInstruction, EndorBaseExceptionObject, EndorCellObject,
EndorCode, EndorCodeObject, EndorDecimal, EndorDictObject, EndorFunctionObject, EndorInteger,
EndorNativeCall, EndorNativeFunctionObject, EndorNumber, EndorNumberObject, EndorObject,
EndorRational, EndorReal, EndorStringObject, EndorTracebackEntry, Instruction, NativeCall,
NativeCallable, NativeContext, NativeFunction, NativeResult, ObjectChange, ObjectHeader,
ObjectPayload, ObjectRef, Opcode, ReadAccess, Record, SourceSignature, Value, VmError,
WeakObjectRef,
};
pub use vm::Vm;
+1 -1
View File
@@ -8,7 +8,7 @@
| [builtins.rs](builtins.rs) | 无损 Number、Unicode 字符串、Dict 顺序与键、Cell、函数环境/共享代码、异常数据 |
| [execution.rs](execution.rs) | wordcode 与校验、控制流、参数/默认值、嵌套帧、字节码函数创建/默认值/环境捕获、闭包、返回值存活、失败清理、限额 |
| [types.rs](types.rs) | 名义身份、继承与实例关系、类型边保活/循环回收、只读类型元数据与外来句柄、显式Trait身份/继承/参数约束及后代更新原子性;统一函数分类、原生数量元数据/副作用前拒绝/能力更新/默认参数与返回类型错误根清理;仅远端执行 |
| [native_context.rs](native_context.rs) | 原生作用域根、分配失败清理、委托、只读结果分配/写入拒绝/依赖/校验与普通读取区分及失败恢复、弱引用过期及有界变更;仅远端执行 |
| [native_context.rs](native_context.rs) | 原生作用域根、分配失败清理、原生签名/效果和委托返回根、绑定方法元数据、委托、只读结果分配/写入拒绝/依赖/校验与普通读取区分及失败恢复、弱引用过期及有界变更;仅远端执行 |
核心对象现由 objects 定义,本目录保留既有 endor_core 导入与 Vm 方法的兼容性检查。独立对象存储检查见 [objects/tests](../../objects/tests/README.md)。堆内部伪造句柄与计数溢出检查在 objects 的 src/heap.rs,公开示例由 src/lib.rs rustdoc 提供。文件职责见 [src](../src/README.md),检查方式和结果边界见 [验证](../../../docs/operations/validation.md)。本地不执行测试;存在测试文件不代表某次提交已通过。返回 [core](../README.md)。
+349
View File
@@ -268,3 +268,352 @@ fn annotation_reads_remain_dependencies_without_becoming_program_content_reads()
store.release(object).unwrap();
assert_eq!(store.object_count(), 0);
}
#[derive(Debug)]
struct TypedNative {
signature: endor_core::CallSignature,
calls: std::sync::Arc<std::sync::atomic::AtomicUsize>,
delegate: Option<ObjectRef>,
allocate_result: bool,
}
impl NativeCallable for TypedNative {
fn signature(&self) -> Option<endor_core::CallSignature> {
Some(self.signature.clone())
}
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
if let Some(function) = self.delegate {
return Ok(NativeResult::Invoke {
function,
arguments: args.to_vec(),
});
}
Ok(NativeResult::Return(if self.allocate_result {
Value::Object(cx.new_object()?)
} else {
args.last().cloned().unwrap_or(Value::Nil)
}))
}
}
#[test]
fn native_types_preflight_arguments_and_clean_wrong_result_roots() {
use endor_core::{AnnotationType as T, CallEffect, CallSignature};
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
let mut vm = Vm::new().unwrap();
let calls = Arc::new(AtomicUsize::new(0));
let function = vm
.allocate(ContextNativeFunction::new(
"typed",
TypedNative {
signature: CallSignature::exact(vec![T::Number], T::Number, CallEffect::ReadOnly),
calls: calls.clone(),
delegate: None,
allocate_result: true,
},
vec![],
))
.unwrap();
assert!(
matches!(vm.call(function, &[Value::Text("bad".into())]), Err(VmError::TypeMismatch { position, .. }) if position == "parameter 1")
);
assert_eq!(calls.load(Ordering::SeqCst), 0);
assert!(
matches!(vm.call(function, &[Value::Number(7.into())]), Err(VmError::TypeMismatch { position, .. }) if position == "return")
);
assert_eq!(calls.load(Ordering::SeqCst), 1);
assert_eq!(vm.object_count(), 1);
vm.release(function).unwrap();
assert_eq!(vm.object_count(), 0);
}
#[test]
fn delegated_source_results_obey_every_native_contract_without_extra_frames() {
use endor_core::{
AnnotationType as T, CallEffect, CallSignature, CodeLayout, EndorCode, Instruction, Opcode,
};
use std::sync::{Arc, atomic::AtomicUsize};
let mut vm = Vm::new().unwrap();
let code = EndorCode::from_instructions(
&[
Instruction::new(Opcode::LoadConst, 0),
Instruction::simple(Opcode::ReturnValue),
],
vec![Value::Text("source result".into())],
CodeLayout::default(),
)
.unwrap();
let code = vm.new_code(code).unwrap();
let globals = vm.new_dict().unwrap();
let source = vm
.new_function("source", code, globals, vec![], vec![])
.unwrap();
let calls = Arc::new(AtomicUsize::new(0));
let inner = vm
.allocate(ContextNativeFunction::new(
"inner",
TypedNative {
signature: CallSignature::exact(vec![], T::String, CallEffect::ReadOnly),
calls: calls.clone(),
delegate: Some(source),
allocate_result: false,
},
vec![source],
))
.unwrap();
let outer = vm
.allocate(ContextNativeFunction::new(
"outer",
TypedNative {
signature: CallSignature::exact(vec![], T::Number, CallEffect::ReadOnly),
calls,
delegate: Some(inner),
allocate_result: false,
},
vec![inner],
))
.unwrap();
let limits = ExecutionLimits {
max_frames: 1,
max_instructions: 100,
};
assert_eq!(
vm.call_with_limits(inner, &[], limits).unwrap(),
Value::Text("source result".into())
);
assert!(
matches!(vm.call_with_limits(outer, &[], limits), Err(VmError::TypeMismatch { function, position, .. }) if function == "outer" && position == "return")
);
assert!(matches!(
vm.call_with_limits(
outer,
&[],
ExecutionLimits {
max_frames: 0,
..limits
}
),
Err(VmError::ExecutionLimit("frames"))
));
for object in [outer, inner, source, globals, code] {
vm.release(object).unwrap();
}
vm.collect_cycles();
assert_eq!(vm.object_count(), 0);
}
#[test]
fn signatures_trace_nominal_types_and_bound_receivers_use_the_actual_contract() {
use endor_core::{AnnotationType as T, CallEffect, CallSignature, TypeConstructor};
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
let mut vm = Vm::new().unwrap();
let roots = vm.bootstrap_types().unwrap();
let definition = vm
.new_type("Receiver", roots.object, TypeConstructor::Record)
.unwrap();
let calls = Arc::new(AtomicUsize::new(0));
let function = vm
.allocate(ContextNativeFunction::new(
"echo",
TypedNative {
signature: CallSignature::exact(
vec![T::Nominal(definition), T::Number],
T::Number,
CallEffect::ReadOnly,
),
calls: calls.clone(),
delegate: None,
allocate_result: false,
},
vec![],
))
.unwrap(); // Metadata alone traces the definition.
let weak = vm.downgrade(definition).unwrap();
vm.define_method(definition, "echo", function).unwrap();
let receiver = vm.new_object().unwrap();
vm.attach_type(receiver, definition).unwrap();
vm.release(definition).unwrap();
let Value::Object(bound) = vm.bind_attr(receiver, "echo").unwrap() else {
panic!("bound method")
};
assert_eq!(
vm.call_arity(bound).unwrap(),
Some(endor_core::CallArity::exact(1))
);
assert!(vm.call(bound, &[Value::Bool(true)]).is_err());
assert_eq!(calls.load(Ordering::SeqCst), 0);
assert_eq!(
vm.call(bound, &[Value::Number(9.into())]).unwrap(),
Value::Number(9.into())
);
for object in [bound, receiver, function, roots.object, roots.type_type] {
vm.release(object).unwrap();
}
vm.collect_cycles();
assert!(vm.upgrade(weak).unwrap().is_none());
assert_eq!(vm.object_count(), 0);
}
#[test]
fn declared_host_effects_are_rejected_before_read_only_dispatch() {
use endor_core::{AnnotationType as T, CallEffect, CallSignature};
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
for effect in [CallEffect::Mutating, CallEffect::External] {
let mut vm = Vm::new().unwrap();
let calls = Arc::new(AtomicUsize::new(0));
let function = vm
.allocate(ContextNativeFunction::new(
"effect",
TypedNative {
signature: CallSignature::exact(vec![], T::Unit, effect),
calls: calls.clone(),
delegate: None,
allocate_result: false,
},
vec![],
))
.unwrap();
assert!(
vm.call_read_only_with_limits(function, &[], ExecutionLimits::default())
.is_err()
);
assert_eq!(calls.load(Ordering::SeqCst), 0);
assert_eq!(vm.call(function, &[]).unwrap(), Value::Nil);
vm.release(function).unwrap();
assert_eq!(vm.object_count(), 0);
}
}
#[test]
fn legacy_native_signatures_check_optional_variadic_types_and_metadata() {
use endor_core::{AnnotationType as T, CallArity, CallEffect, CallSignature, NativeFunction};
fn echo(args: &[Value]) -> Result<Value, VmError> {
Ok(args.last().cloned().unwrap_or(Value::Nil))
}
let mut vm = Vm::new().unwrap();
let function = vm
.allocate(
NativeFunction::new("legacy", echo).with_signature(CallSignature {
arity: CallArity::range(0, usize::MAX),
parameters: vec![],
variadic: Some(T::Number),
returns: T::Number,
effect: CallEffect::ReadOnly,
}),
)
.unwrap();
assert_eq!(
vm.call(
function,
&[Value::Number(1.into()), Value::Number(2.into())]
)
.unwrap(),
Value::Number(2.into())
);
assert!(vm.call(function, &[Value::Bool(true)]).is_err());
assert!(
matches!(vm.call(function, &[]), Err(VmError::TypeMismatch { position, .. }) if position == "return")
);
let invalid = vm
.allocate(
NativeFunction::new("invalid", echo)
.with_signature(CallSignature::exact(
vec![T::Number],
T::Number,
CallEffect::ReadOnly,
))
.with_arity(CallArity::exact(0)),
)
.unwrap();
assert!(vm.call_arity(invalid).is_err());
assert!(vm.call(invalid, &[]).is_err());
vm.release(invalid).unwrap();
vm.release(function).unwrap();
assert_eq!(vm.object_count(), 0);
}
#[derive(Debug)]
struct EraseThenDelegate {
signature: endor_core::CallSignature,
target: ObjectRef,
}
impl NativeCallable for EraseThenDelegate {
fn signature(&self) -> Option<endor_core::CallSignature> {
Some(self.signature.clone())
}
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
cx.replace_payload(cx.callee(), endor_core::Record)?;
Ok(NativeResult::Invoke {
function: self.target,
arguments: vec![],
})
}
}
#[test]
fn delegated_return_type_survives_removal_of_the_original_signature_edges() {
use endor_core::{
AnnotationType as T, CallEffect, CallSignature, CodeLayout, EndorCode, Instruction, Opcode,
TypeConstructor,
};
let mut vm = Vm::new().unwrap();
let roots = vm.bootstrap_types().unwrap();
let definition = vm
.new_type("Transient", roots.object, TypeConstructor::Record)
.unwrap();
let weak = vm.downgrade(definition).unwrap();
let code = EndorCode::from_instructions(
&[
Instruction::new(Opcode::LoadConst, 0),
Instruction::simple(Opcode::ReturnValue),
],
vec![Value::Nil],
CodeLayout::default(),
)
.unwrap();
let code = vm.new_code(code).unwrap();
let globals = vm.new_dict().unwrap();
let source = vm
.new_function("source", code, globals, vec![], vec![])
.unwrap();
let wrapper = vm
.allocate(ContextNativeFunction::new(
"erased",
EraseThenDelegate {
signature: CallSignature::exact(
vec![],
T::Nominal(definition),
CallEffect::Mutating,
),
target: source,
},
vec![source],
))
.unwrap();
vm.release(definition).unwrap();
assert!(
matches!(vm.call(wrapper, &[]), Err(VmError::TypeMismatch { function, expected, .. }) if function == "erased" && expected == "Transient")
);
assert!(vm.upgrade(weak).unwrap().is_none());
for object in [
wrapper,
source,
code,
globals,
roots.object,
roots.type_type,
] {
vm.release(object).unwrap();
}
vm.collect_cycles();
assert_eq!(vm.object_count(), 0);
}
+2 -2
View File
@@ -4,10 +4,10 @@
| 文件 | 职责 |
| --- | --- |
| [lib.rs](lib.rs) | 包边界、对象类型/ObjectStore 公开导出(含Trait/MethodRequirement/SourceSignature/AnnotationType/CallArity/ReadAccess)与独立存储 rustdoc 示例 |
| [lib.rs](lib.rs) | 包边界、对象类型/ObjectStore 公开导出(含Trait/MethodRequirement/SourceSignature/AnnotationType/CallArity/CallSignature/CallEffect/ReadAccess)与独立存储 rustdoc 示例 |
| [object.rs](object.rs) | Value、CompareOp、ObjectPayload(含显式函数标记)、ObjectHeader、EndorObject、Record、可发布数量元数据的 NativeFunction |
| [types.rs](types.rs) | 托管名义类型、显式 Object/Type 引导、继承查询、受管方法表/接收者绑定、类字段、显式Trait定义/声明/方法参数约束、原生构造委托、统一源码/原生/绑定方法参数区间查询与能力验证、声明环境托管边、字段构造/写入检查与标记为校验读取的注解解析/匹配 |
| [native.rs](native.rs) | NativeContext、CallArity 元数据与调用前数量检查、上下文原生函数、作用域语言函数工厂、显式能力声明入口、函数标记、根与非递归委托调用 |
| [native.rs](native.rs) | NativeContext、CallArity/CallSignature 类型与效果元数据、调用前检查、签名类型强边、上下文原生函数、作用域语言函数工厂、显式能力声明入口、函数标记、根与非递归委托调用 |
| [changes.rs](changes.rs) | 合并变更批次、变更键与有界通知额度 |
| [number.rs](number.rs) | Integer/Decimal/Rational/BinaryFloat/Complex 的无损表示和装箱载荷 |
| [builtins.rs](builtins.rs) | String、Dict、Cell、Function、BaseException 载荷与 trace 边、语言函数标记 |
+4 -1
View File
@@ -35,7 +35,10 @@ pub use code::{CodeLayout, EndorCode, EndorCodeObject, SourceSignature};
pub use error::VmError;
pub use heap::{ObjectRef, ReadAccess, WeakObjectRef};
pub use instruction::{DecodedInstruction, Instruction, Opcode};
pub use native::{CallArity, ContextNativeFunction, NativeCallable, NativeContext, NativeResult};
pub use native::{
CallArity, CallEffect, CallSignature, ContextNativeFunction, NativeCallable, NativeContext,
NativeResult,
};
pub use number::{
EndorDecimal, EndorInteger, EndorNumber, EndorNumberObject, EndorRational, EndorReal,
};
+155 -1
View File
@@ -50,10 +50,141 @@ impl CallArity {
}
}
/// Declared host effect. ReadOnly may allocate results but must not mutate
/// existing objects or perform external I/O. Unknown legacy calls remain unknown.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CallEffect {
ReadOnly,
Mutating,
External,
}
/// Immutable native contract, including an explicit receiver where applicable.
/// Missing fixed slots are covered by variadic; optionality comes from arity.
#[derive(Debug, Clone)]
pub struct CallSignature {
pub arity: CallArity,
pub parameters: Vec<crate::AnnotationType>,
pub variadic: Option<crate::AnnotationType>,
pub returns: crate::AnnotationType,
pub effect: CallEffect,
}
impl CallSignature {
pub fn exact(
parameters: Vec<crate::AnnotationType>,
returns: crate::AnnotationType,
effect: CallEffect,
) -> Self {
Self {
arity: CallArity::exact(parameters.len()),
parameters,
variadic: None,
returns,
effect,
}
}
pub fn references(&self) -> impl Iterator<Item = ObjectRef> + '_ {
self.parameters
.iter()
.copied()
.chain(self.variadic)
.chain([self.returns])
.filter_map(crate::AnnotationType::reference)
}
pub fn validate(&self, store: &ObjectStore) -> Result<(), VmError> {
self.arity.validate()?;
if self.parameters.len() > self.arity.maximum
|| (self.variadic.is_none() && self.parameters.len() != self.arity.maximum)
{
return Err(VmError::InvalidArgument("incomplete native signature"));
}
for expected in self
.parameters
.iter()
.copied()
.chain(self.variadic)
.chain([self.returns])
{
match expected {
crate::AnnotationType::Nominal(reference) => {
store.payload::<crate::EndorTypeObject>(reference, "Type")?;
}
crate::AnnotationType::Capability(reference) => {
store.payload::<crate::EndorTraitObject>(reference, "Trait")?;
}
_ => {}
}
}
Ok(())
}
pub fn check_arguments(
&self,
store: &ObjectStore,
name: &str,
arguments: &[Value],
) -> Result<(), VmError> {
self.validate(store)?;
self.arity.check(name.into(), arguments.len())?;
if store.is_read_only() && self.effect != CallEffect::ReadOnly {
return Err(VmError::InvalidArgument(
"native effect is forbidden during read-only execution",
));
}
for (index, value) in arguments.iter().enumerate() {
let expected = self
.parameters
.get(index)
.copied()
.or(self.variadic)
.ok_or(VmError::InvalidArgument("incomplete native signature"))?;
self.check_value(
store,
name,
format!("parameter {}", index + 1),
value,
expected,
)?;
}
Ok(())
}
pub fn check_return(
&self,
store: &ObjectStore,
name: &str,
value: &Value,
) -> Result<(), VmError> {
self.check_value(store, name, "return".into(), value, self.returns)
}
fn check_value(
&self,
store: &ObjectStore,
name: &str,
position: String,
value: &Value,
expected: crate::AnnotationType,
) -> Result<(), VmError> {
if !store.matches_annotation(value, expected)? {
return Err(VmError::TypeMismatch {
function: name.into(),
position,
expected: store.annotation_type_name(expected)?,
actual: store.value_type_name(value)?,
});
}
Ok(())
}
}
pub trait NativeCallable: fmt::Debug + Send + Sync + 'static {
/// Immutable resolved types; referenced definitions must be traced by the
/// payload. ContextNativeFunction snapshots and traces them automatically.
fn signature(&self) -> Option<CallSignature> {
None
}
/// Optional positional contract. Missing metadata remains an explicit legacy gap.
fn arity(&self) -> Option<CallArity> {
None
self.signature().map(|signature| signature.arity)
}
fn invoke(
@@ -197,16 +328,29 @@ impl<'a> NativeContext<'a> {
arguments.len(),
)?;
}
let signature = store.native_signature(function)?;
let name = store.object(function)?.payload().kind_name().to_owned();
if let Some(signature) = &signature {
signature.check_arguments(store, &name, arguments)?;
}
let mut context = Self {
store,
roots: Vec::new(),
callee: function,
};
context.protect(&Value::Object(function))?;
if let Some(signature) = &signature {
for reference in signature.references() {
context.protect(&Value::Object(reference))?;
}
}
for argument in arguments {
context.protect(argument)?;
}
let result = callable.invoke(&mut context, arguments)?;
if let (Some(signature), NativeResult::Return(value)) = (&signature, &result) {
signature.check_return(context.store(), &name, value)?;
}
let transferred_from = context.roots.len();
match &result {
NativeResult::Return(value) => context.protect(value)?,
@@ -242,6 +386,7 @@ pub struct ContextNativeFunction {
name: &'static str,
callable: Arc<dyn NativeCallable>,
captures: Vec<ObjectRef>,
signature: Option<CallSignature>,
}
impl ContextNativeFunction {
@@ -252,6 +397,7 @@ impl ContextNativeFunction {
) -> Self {
Self {
name,
signature: callable.signature(),
callable: Arc::new(callable),
captures,
}
@@ -272,10 +418,18 @@ impl ObjectPayload for ContextNativeFunction {
self.name
}
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
if let Some(signature) = &self.signature {
for reference in signature.references() {
visit(reference);
}
}
for capture in &self.captures {
visit(*capture);
}
}
fn call_signature(&self) -> Option<CallSignature> {
self.signature.clone()
}
fn native_callable(&self) -> Option<Arc<dyn NativeCallable>> {
Some(self.callable.clone())
}
+22
View File
@@ -118,6 +118,11 @@ pub trait ObjectPayload: Any + fmt::Debug + Send + Sync {
/// validates/retains the result before exposing it as an owned call result.
/// Language functions are dispatched by Vm's frame evaluator instead.
/// Positional metadata for native call protocols; source code is queried by the store.
fn call_signature(&self) -> Option<crate::CallSignature> {
self.native_callable()
.and_then(|callable| callable.signature())
}
fn call_arity(&self) -> Option<crate::CallArity> {
self.native_callable().and_then(|callable| callable.arity())
}
@@ -182,6 +187,7 @@ pub struct EndorNativeFunctionObject {
name: String,
function: NativeCall,
arity: Option<crate::CallArity>,
signature: Option<crate::CallSignature>,
}
impl EndorNativeFunctionObject {
@@ -190,12 +196,18 @@ impl EndorNativeFunctionObject {
name: name.into(),
function,
arity: None,
signature: None,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn with_signature(mut self, signature: crate::CallSignature) -> Self {
self.arity = Some(signature.arity);
self.signature = Some(signature);
self
}
pub fn with_arity(mut self, arity: crate::CallArity) -> Self {
self.arity = Some(arity);
self
@@ -224,6 +236,16 @@ impl ObjectPayload for EndorNativeFunctionObject {
Ok(format!("<native-function {}>", self.name))
}
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
if let Some(signature) = &self.signature {
for reference in signature.references() {
visit(reference);
}
}
}
fn call_signature(&self) -> Option<crate::CallSignature> {
self.signature.clone()
}
fn call_arity(&self) -> Option<crate::CallArity> {
self.arity
}
+43 -1
View File
@@ -434,6 +434,26 @@ impl ObjectStore {
}
}
/// Validated direct native metadata. Bound methods delegate to their actual
/// target, so receiver slots remain explicit here. Returned handles are borrowed.
pub fn native_signature(
&self,
function: ObjectRef,
) -> Result<Option<crate::CallSignature>, VmError> {
let payload = self.object(function)?.payload();
let signature = payload.call_signature();
if let Some(signature) = &signature {
signature.validate(self)?;
if payload
.call_arity()
.is_some_and(|arity| arity != signature.arity)
{
return Err(VmError::InvalidArgument("native signature arity mismatch"));
}
}
Ok(signature)
}
/// Unified source/native positional interval, after any bound receivers.
/// None means no published native metadata (including generic Type construction).
pub fn call_arity(&self, function: ObjectRef) -> Result<Option<crate::CallArity>, VmError> {
@@ -462,7 +482,9 @@ impl ObjectStore {
maximum,
))
} else {
payload.call_arity()
self.native_signature(current)?
.map(|signature| signature.arity)
.or_else(|| payload.call_arity())
};
return arity
.map(|arity| {
@@ -1040,6 +1062,26 @@ impl ObjectStore {
},
})
}
pub fn annotation_type_name(&self, expected: AnnotationType) -> Result<String, VmError> {
Ok(match expected {
AnnotationType::Any => "Object".into(),
AnnotationType::Unit => "Unit".into(),
AnnotationType::Number => "Number".into(),
AnnotationType::String => "String".into(),
AnnotationType::Bool => "Bool".into(),
AnnotationType::Function => "Function".into(),
AnnotationType::Type => "Type".into(),
AnnotationType::Trait => "Trait".into(),
AnnotationType::Nominal(reference) => self
.payload::<EndorTypeObject>(reference, "Type")?
.name
.clone(),
AnnotationType::Capability(reference) => self
.payload::<EndorTraitObject>(reference, "Trait")?
.name
.clone(),
})
}
pub fn value_type_name(&self, value: &Value) -> Result<String, VmError> {
Ok(match value {
Value::Nil => "Unit".into(),
+1 -1
View File
@@ -5,7 +5,7 @@
| 引擎与宿主边界 | [架构](architecture/README.md) | [crates](../crates/README.md) |
| 解释执行、资源修订与引擎升级 | [执行与版本](architecture/execution-and-versions.md) | frontend/compiler/core |
| EndorObject、句柄、默认操作 | [对象模型](runtime/objects.md) | objects/src |
| 原生上下文、只读调用、子调用读取依赖及校验分类、统一参数数量、委托与变更通知 | [原生调用](runtime/native-calls.md) | objects/src/native.rs、changes.rs、core/src/eval.rs |
| 原生上下文、只读调用、子调用读取依赖及校验分类、统一参数数量/类型/效果、委托返回与变更通知 | [原生调用](runtime/native-calls.md) | objects/src/native.rs、changes.rs、core/src/eval.rs |
| 托管类型、宿主类注册、原生分配继承、字段存储、显式能力、函数分类、声明字段与可选源码注解检查 | [名义类型](runtime/types.md) | objects/src/types.rs |
| Number、String、Dict、Cell、Function | [基本对象](runtime/builtins.md) | objects/src |
| 源码读取与来源 | [Reader](runtime/reader.md) | frontend/src/reader.rs |
+1 -1
View File
@@ -4,7 +4,7 @@
| --- | --- |
| [types.md](types.md) | 托管 Object/Type、名义继承、声明字段/函数注解检查、构造与类型边生命周期 |
| [objects.md](objects.md) | 对象、句柄与默认操作 |
| [native-calls.md](native-calls.md) | 原生上下文、读取分类、统一参数数量、非递归委托、弱引用、托管赋值与变更批次 |
| [native-calls.md](native-calls.md) | 原生上下文、读取分类、统一参数数量/类型/效果、非递归委托返回、弱引用、托管赋值与变更批次 |
| [builtins.md](builtins.md) | Number、String、Dict、Cell、Function、异常数据 |
| [reader.md](reader.md) | 输入、来源和修订标签 |
| [frontend.md](frontend.md) | 词法、语法和诊断 |
+18 -1
View File
@@ -70,7 +70,7 @@ take_changes 按对象与变更键合并同一批写入,返回最终版本。
## 统一位置参数数量
`CallArity { required, maximum }` 表示接受的位置参数区间,包含未绑定的显式接收者;`exact(n)` 与 `range(min,max)` 提供构造方式,`accepts(n)` 查询接受性。`usize::MAX` 表示开放上界。公开字段不能绕过使用时校验:required 大于 maximum 时,查询/调用报告 InvalidArgument,不能进入实现。此元数据只描述数量,不描述参数类型、返回类型或效果。
`CallArity { required, maximum }` 表示接受的位置参数区间,包含未绑定的显式接收者;`exact(n)` 与 `range(min,max)` 提供构造方式,`accepts(n)` 查询接受性。`usize::MAX` 表示开放上界。公开字段不能绕过使用时校验:required 大于 maximum 时,查询/调用报告 InvalidArgument,不能进入实现。CallArity 本身只描述数量;可选的 CallSignature 在此基础上声明类型与宿主效果。
上下文原生实现通过 `NativeCallable::arity()` 提供 `Some(CallArity)`;默认 None 明确表示旧实现没有元数据。ObjectPayload::call_arity 默认委托该入口,也供直接 call 协议发布元数据;传统 NativeFunction 可使用 `with_arity(...)`。VM 在进入原生实现前检查数量,NativeContext::execute 也检查其 NativeCallable 元数据,失败不调用业务实现。传统无元数据的普通原生调用仍保留原协议,不伪造精确签名。
@@ -86,3 +86,20 @@ Tessera 为原生方法、回调和内部视觉 continuation 发布数量元数
只读子区域新增 `ObjectStore::end_read_tracking_details(token) -> Vec<ReadAccess>`,每项仍是借用 ObjectRef,content_read 表示是否存在普通内容读取。matches_annotation 内部的名义/基础值检查标为校验读取;同一区域随后发生普通读取时,content_read 合并为 true。嵌套区域均保留两类读取,不增加对象根。原 end_read_tracking 与完整 end_read_only 仍返回两类读取的完整并集,没有删除依赖或放宽只读写屏障。
校验分类按线程记录嵌套深度,作用域退出、错误和展开时通过 RAII 恢复,不让另一线程继承标签。公开 ReadAccess 仅提供信息,不保证调用者可忽略任何对象;动态类型/能力/全局定义等校验依赖仍须参与失效。Tessera 独立验证每次活动 Mount/归属/方法后,仅跳过该 Mount 的纯参数类型校验读取;程序实际读取 Mount 内容仍禁止复用测量缓存。其他读取继续记录版本。详见 core/tests/native_context.rs 的合并、完整依赖并集与错误恢复回归源码,仅编译未本地执行。
## 原生参数、返回类型与宿主效果
CallSignature 保存 CallArity、固定位置的 AnnotationType 列表、可选 variadic 类型、returns 与 CallEffect。exact 提供无默认参数的便捷构造;可选参数仍通过 arity.required/maximum 表示,列表覆盖所有固定位置。开放上界必须有 variadic,有限上界未被固定列表覆盖的部分也必须有 variadic;错误区间、不完整列表、非 Type/Trait 的名义/能力引用和独立 arity 与签名不一致在调用前拒绝。名义类型引用是实际定义,不从字符串或调用者全局环境重新解析。
NativeCallable::signature 默认 None,明确表示尚未发布类型/效果契约;默认 arity 从签名取得。ObjectPayload::call_signature 供自定义直接调用协议发布。ContextNativeFunction 在创建时保存不可变签名并在 trace 中自动登记每个类型/能力引用;其他捕获仍须显式传 captures。NativeFunction::with_signature 同样追踪签名边并设置数量。自定义载荷须在 trace 中登记自己的签名引用,元数据与其他载荷边一样禁止原地改变;更换使用 replace_payload。ObjectStore::native_signature 校验并返回借用描述,不创建隐藏强注册表。
已发布的原生签名总是检查,不依赖默认关闭的源码注解开关。所有实参在进入原生实现前匹配类型,失败不产生该实现的业务副作用;可选的未提供参数不检查,实际提供的重复参数逐个匹配。Return 在交付前匹配返回类型,错误释放本次作用域分配和结果根,但不回滚已经执行的业务写入。传统直接 call 结果仍按借用协议处理。NativeContext 的独立执行桥同样执行参数、效果与直接返回检查。
NativeResult::Invoke 仍是尾委托,不递归进入 Rust/VM。VM 为每层已声明的返回契约保留检查项;委托原生立即返回或 Endor 帧返回时,按由内到外顺序验证全部契约。返回定义作为执行根保留,原生载荷替换、释放原委托者或源码全局重绑定不会使检查丢失。准备失败、帧限额、指令限额、错误返回与正常完成都释放这些执行根。委托不会为检查额外创建 VM 帧;仍共享每次原生分派和 Endor 指令预算。NativeContext::execute 单独返回 Invoke 时,调用者须使用具有等效最终返回检查的求值器;通常直接使用 Vm::call。
CallEffect::ReadOnly 声明只读现存对象,可分配临时结果;Mutating 与 External 在 ObjectStore 只读作用域内、进入实现前拒绝。普通执行允许这些效果。ReadOnly 声明不绕过已有写屏障,也不证明可信 Rust 不会进行隐藏 I/O;未知旧原生函数仍沿原调用/宿主检查,不能把 None 理解为纯函数。这不是静态效果推断、完整 I/O 沙箱或对阻塞原生代码的抢占。
Tessera 的 measure/arrange/paint/hitTest、Point 几何查询、Window 方法及可表达的 Block/Container/Page 方法发布签名。需要 nullable/union 参数的 Page.open/window 与 Block.on 尚保留无类型元数据;构造器、其他管理 API 和完整源码覆写方差仍未全部接入。能力注册目前继续检查参数数量,不声称已经比较签名类型或效果。
回归源码 native_context.rs 覆盖参数副作用前拒绝、错误结果清理、跨原生/源码委托检查、单帧限制、名义签名边与绑定方法循环回收、只读效果拒绝、传统直接函数及重复参数和元数据错误。只编译,不本地执行。