From 680caf81e39c6df7f5e1cf6a5c28770380d523db Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 22 Oct 2026 13:51:42 +0800 Subject: [PATCH] Enforce native type signatures and effects across delegated calls --- crates/core/src/README.md | 6 +- crates/core/src/eval.rs | 54 ++++- crates/core/src/frame.rs | 20 ++ crates/core/src/lib.rs | 15 +- crates/core/tests/README.md | 2 +- crates/core/tests/native_context.rs | 349 ++++++++++++++++++++++++++++ crates/objects/src/README.md | 4 +- crates/objects/src/lib.rs | 5 +- crates/objects/src/native.rs | 156 ++++++++++++- crates/objects/src/object.rs | 22 ++ crates/objects/src/types.rs | 44 +++- docs/README.md | 2 +- docs/runtime/README.md | 2 +- docs/runtime/native-calls.md | 19 +- 14 files changed, 676 insertions(+), 24 deletions(-) diff --git a/crates/core/src/README.md b/crates/core/src/README.md index e31b833..8ae35d7 100644 --- a/crates/core/src/README.md +++ b/crates/core/src/README.md @@ -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、结果根转移与失败清理 | ## 常见修改入口 diff --git a/crates/core/src/eval.rs b/crates/core/src/eval.rs index 50a84dd..38cd724 100644 --- a/crates/core/src/eval.rs +++ b/crates/core/src/eval.rs @@ -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 { 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::() { - 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) { + 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 { 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() { diff --git a/crates/core/src/frame.rs b/crates/core/src/frame.rs index 91afeec..1172f9d 100644 --- a/crates/core/src/frame.rs +++ b/crates/core/src/frame.rs @@ -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, + pub(crate) native_returns: Vec, /// Index of the caller in the execution's frame vector, never a pointer. pub(crate) previous: Option, pub(crate) code: ObjectRef, diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 4e4c71e..bd82c18 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -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; diff --git a/crates/core/tests/README.md b/crates/core/tests/README.md index cfaa6e1..24ff9ba 100644 --- a/crates/core/tests/README.md +++ b/crates/core/tests/README.md @@ -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)。 diff --git a/crates/core/tests/native_context.rs b/crates/core/tests/native_context.rs index 2f957e5..7ec4774 100644 --- a/crates/core/tests/native_context.rs +++ b/crates/core/tests/native_context.rs @@ -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, + delegate: Option, + allocate_result: bool, +} +impl NativeCallable for TypedNative { + fn signature(&self) -> Option { + Some(self.signature.clone()) + } + fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { + 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 { + 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 { + Some(self.signature.clone()) + } + fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result { + 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); +} diff --git a/crates/objects/src/README.md b/crates/objects/src/README.md index 884aac6..227b300 100644 --- a/crates/objects/src/README.md +++ b/crates/objects/src/README.md @@ -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 边、语言函数标记 | diff --git a/crates/objects/src/lib.rs b/crates/objects/src/lib.rs index cb9434c..aadc271 100644 --- a/crates/objects/src/lib.rs +++ b/crates/objects/src/lib.rs @@ -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, }; diff --git a/crates/objects/src/native.rs b/crates/objects/src/native.rs index c05262a..576a6d2 100644 --- a/crates/objects/src/native.rs +++ b/crates/objects/src/native.rs @@ -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, + pub variadic: Option, + pub returns: crate::AnnotationType, + pub effect: CallEffect, +} +impl CallSignature { + pub fn exact( + parameters: Vec, + returns: crate::AnnotationType, + effect: CallEffect, + ) -> Self { + Self { + arity: CallArity::exact(parameters.len()), + parameters, + variadic: None, + returns, + effect, + } + } + pub fn references(&self) -> impl Iterator + '_ { + 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::(reference, "Type")?; + } + crate::AnnotationType::Capability(reference) => { + store.payload::(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 { + None + } + /// Optional positional contract. Missing metadata remains an explicit legacy gap. fn arity(&self) -> Option { - 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, captures: Vec, + signature: Option, } 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 { + self.signature.clone() + } fn native_callable(&self) -> Option> { Some(self.callable.clone()) } diff --git a/crates/objects/src/object.rs b/crates/objects/src/object.rs index 291d28b..c7746ec 100644 --- a/crates/objects/src/object.rs +++ b/crates/objects/src/object.rs @@ -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 { + self.native_callable() + .and_then(|callable| callable.signature()) + } + fn call_arity(&self) -> Option { self.native_callable().and_then(|callable| callable.arity()) } @@ -182,6 +187,7 @@ pub struct EndorNativeFunctionObject { name: String, function: NativeCall, arity: Option, + signature: Option, } 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!("", 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 { + self.signature.clone() + } fn call_arity(&self) -> Option { self.arity } diff --git a/crates/objects/src/types.rs b/crates/objects/src/types.rs index dbe63ae..f1e41ef 100644 --- a/crates/objects/src/types.rs +++ b/crates/objects/src/types.rs @@ -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, 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, 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 { + 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::(reference, "Type")? + .name + .clone(), + AnnotationType::Capability(reference) => self + .payload::(reference, "Trait")? + .name + .clone(), + }) + } pub fn value_type_name(&self, value: &Value) -> Result { Ok(match value { Value::Nil => "Unit".into(), diff --git a/docs/README.md b/docs/README.md index 3fd5a70..2eb95ff 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 | diff --git a/docs/runtime/README.md b/docs/runtime/README.md index b3f1b5e..94d8348 100644 --- a/docs/runtime/README.md +++ b/docs/runtime/README.md @@ -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) | 词法、语法和诊断 | diff --git a/docs/runtime/native-calls.md b/docs/runtime/native-calls.md index 85d7791..fc07f67 100644 --- a/docs/runtime/native-calls.md +++ b/docs/runtime/native-calls.md @@ -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`,每项仍是借用 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 覆盖参数副作用前拒绝、错误结果清理、跨原生/源码委托检查、单帧限制、名义签名边与绑定方法循环回收、只读效果拒绝、传统直接函数及重复参数和元数据错误。只编译,不本地执行。