Validate unified source and native positional call contracts

This commit is contained in:
2026-10-22 12:02:41 +08:00
parent 20e29e3c5a
commit 44519abf7e
17 changed files with 331 additions and 28 deletions
+4 -4
View File
@@ -4,10 +4,10 @@
| 文件 | 职责 |
| --- | --- |
| [lib.rs](lib.rs) | 包边界、对象类型/ObjectStore 公开导出(含Trait/MethodRequirement/SourceSignature/AnnotationType)与独立存储 rustdoc 示例 |
| [object.rs](object.rs) | Value、CompareOp、ObjectPayload(含显式函数标记)、ObjectHeader、EndorObject、Record、NativeFunction |
| [types.rs](types.rs) | 托管名义类型、显式 Object/Type 引导、继承查询、受管方法表/接收者绑定、类字段、显式Trait定义/声明/方法参数约束、原生构造委托、声明环境托管边、字段构造/写入检查与注解解析/匹配 |
| [native.rs](native.rs) | NativeContext、上下文原生函数、作用域语言函数工厂、显式能力声明入口、函数标记、根与非递归委托调用 |
| [lib.rs](lib.rs) | 包边界、对象类型/ObjectStore 公开导出(含Trait/MethodRequirement/SourceSignature/AnnotationType/CallArity)与独立存储 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 元数据与调用前数量检查、上下文原生函数、作用域语言函数工厂、显式能力声明入口、函数标记、根与非递归委托调用 |
| [changes.rs](changes.rs) | 合并变更批次、变更键与有界通知额度 |
| [number.rs](number.rs) | Integer/Decimal/Rational/BinaryFloat/Complex 的无损表示和装箱载荷 |
| [builtins.rs](builtins.rs) | String、Dict、Cell、Function、BaseException 载荷与 trace 边、语言函数标记 |
+1 -1
View File
@@ -35,7 +35,7 @@ pub use code::{CodeLayout, EndorCode, EndorCodeObject, SourceSignature};
pub use error::VmError;
pub use heap::{ObjectRef, WeakObjectRef};
pub use instruction::{DecodedInstruction, Instruction, Opcode};
pub use native::{ContextNativeFunction, NativeCallable, NativeContext, NativeResult};
pub use native::{CallArity, ContextNativeFunction, NativeCallable, NativeContext, NativeResult};
pub use number::{
EndorDecimal, EndorInteger, EndorNumber, EndorNumberObject, EndorRational, EndorReal,
};
+48
View File
@@ -13,7 +13,49 @@ pub enum NativeResult {
},
}
/// Accepted positional argument interval, including explicit receivers.
/// The store validates metadata before use; usize::MAX denotes an open upper bound.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CallArity {
pub required: usize,
pub maximum: usize,
}
impl CallArity {
pub const fn exact(count: usize) -> Self {
Self::range(count, count)
}
pub const fn range(required: usize, maximum: usize) -> Self {
Self { required, maximum }
}
pub fn accepts(self, count: usize) -> bool {
(self.required..=self.maximum).contains(&count)
}
pub(crate) fn validate(self) -> Result<Self, VmError> {
if self.required > self.maximum {
return Err(VmError::InvalidArgument("invalid callable arity metadata"));
}
Ok(self)
}
pub(crate) fn check(self, name: String, provided: usize) -> Result<(), VmError> {
self.validate()?;
if !self.accepts(provided) {
return Err(VmError::ArgumentCount {
name,
required: self.required,
maximum: self.maximum,
provided,
});
}
Ok(())
}
}
pub trait NativeCallable: fmt::Debug + Send + Sync + 'static {
/// Optional positional contract. Missing metadata remains an explicit legacy gap.
fn arity(&self) -> Option<CallArity> {
None
}
fn invoke(
&self,
context: &mut NativeContext<'_>,
@@ -149,6 +191,12 @@ impl<'a> NativeContext<'a> {
arguments: &[Value],
callable: Arc<dyn NativeCallable>,
) -> Result<NativeResult, VmError> {
if let Some(arity) = callable.arity() {
arity.check(
store.object(function)?.payload().kind_name().into(),
arguments.len(),
)?;
}
let mut context = Self {
store,
roots: Vec::new(),
+17
View File
@@ -117,6 +117,11 @@ pub trait ObjectPayload: Any + fmt::Debug + Send + Sync {
/// Host-call entry: arguments and raw results are borrowed handles. The VM
/// 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_arity(&self) -> Option<crate::CallArity> {
self.native_callable().and_then(|callable| callable.arity())
}
fn call(&self, _arguments: &[Value]) -> Result<Value, VmError> {
Err(VmError::UnsupportedOperation("call"))
}
@@ -176,6 +181,7 @@ pub type NativeCall = EndorNativeCall;
pub struct EndorNativeFunctionObject {
name: String,
function: NativeCall,
arity: Option<crate::CallArity>,
}
impl EndorNativeFunctionObject {
@@ -183,12 +189,17 @@ impl EndorNativeFunctionObject {
Self {
name: name.into(),
function,
arity: None,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn with_arity(mut self, arity: crate::CallArity) -> Self {
self.arity = Some(arity);
self
}
}
/// Compatibility name for existing embedders.
@@ -213,7 +224,13 @@ impl ObjectPayload for EndorNativeFunctionObject {
Ok(format!("<native-function {}>", self.name))
}
fn call_arity(&self) -> Option<crate::CallArity> {
self.arity
}
fn call(&self, arguments: &[Value]) -> Result<Value, VmError> {
if let Some(arity) = self.arity {
arity.check(self.name.clone(), arguments.len())?;
}
(self.function)(arguments)
}
}
+59 -8
View File
@@ -434,6 +434,58 @@ impl ObjectStore {
}
}
/// 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> {
let mut current = function;
let mut receivers = 0usize;
loop {
let payload = self.object(current)?.payload();
if let Some(bound) = payload.as_any().downcast_ref::<BoundMethod>() {
receivers = receivers
.checked_add(1)
.ok_or(VmError::ExecutionLimit("bound receivers"))?;
current = bound.function;
continue;
}
let arity = if let Some(function) = payload
.as_any()
.downcast_ref::<crate::EndorFunctionObject>()
{
let maximum = self
.payload::<crate::EndorCodeObject>(function.code(), "code")?
.code()
.parameters()
.len();
Some(crate::CallArity::range(
maximum - function.defaults().len(),
maximum,
))
} else {
payload.call_arity()
};
return arity
.map(|arity| {
let arity = arity.validate()?;
let maximum = if arity.maximum == usize::MAX {
usize::MAX
} else {
arity
.maximum
.checked_sub(receivers)
.ok_or(VmError::InvalidArgument(
"method receiver exceeds parameter count",
))?
};
Ok(crate::CallArity::range(
arity.required.saturating_sub(receivers),
maximum,
))
})
.transpose();
}
}
/// Returns one owned root, including for a newly bound inherited method.
pub fn bind_attr(&mut self, receiver: ObjectRef, name: &str) -> Result<Value, VmError> {
match self.lookup_attr(receiver, name)? {
@@ -641,14 +693,13 @@ impl ObjectStore {
reason,
};
let function = function.ok_or_else(|| invalid("required method is missing"))?;
if let Some(count) = self.positional_parameter_count(function)? {
if count != requirement.parameters + 1 {
return Err(invalid(
"positional parameter count does not match (including self)",
));
}
} else if self.object(function)?.payload().native_callable().is_none() {
return Err(invalid("required member is not callable"));
let arity = self
.call_arity(function)?
.ok_or_else(|| invalid("required member has no positional signature"))?;
if !arity.accepts(requirement.parameters + 1) {
return Err(invalid(
"positional parameter count does not match (including self)",
));
}
Ok(())
}