From 2cafa67f1e3cc0dffe2a84c9fb37b4105e074758 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 15 Nov 2026 18:04:15 +0800 Subject: [PATCH] docs(core): explain native scopes delegation and tracked reads --- crates/core/tests/native_context.rs | 71 +++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/crates/core/tests/native_context.rs b/crates/core/tests/native_context.rs index afc7b78..9b344ab 100644 --- a/crates/core/tests/native_context.rs +++ b/crates/core/tests/native_context.rs @@ -1,3 +1,4 @@ +//! NativeContext 临时根、委托、观察依赖与完整签名的公开 API 回归;测试仍遵守远程执行约定。 //! Remote-only regression coverage. Do not execute locally. use endor_core::{ ChangeKey, ContextNativeFunction, ExecutionLimits, NativeCallable, NativeContext, NativeResult, @@ -5,12 +6,17 @@ use endor_core::{ }; #[derive(Debug)] +/// bool 控制是否故意失败,两个临时分配用于观察成功/失败清理差异。 struct Allocate(bool); impl NativeCallable for Allocate { + /// VM 在 NativeContext 内调用此测试分派器,临时分配与输入由上下文保活,返回/委托后移交输出根。 fn invoke(&self, cx: &mut NativeContext<'_>, _args: &[Value]) -> Result { + // 分配一份上下文临时根,成功返回时仅输出另取根移交,其余根由 Drop 归还。 let value = cx.new_object()?; cx.set_attr(value, "label", Value::Text("scoped".into()))?; + // 故意创建不返回对象,验证正常/报错都不会泄漏临时根。 let _temporary = cx.new_object()?; + // 故意在完成分配后报错,检验 RAII 不依赖回调正常返回。 if self.0 { return Err(VmError::InvalidArgument("intentional failure")); } @@ -19,8 +25,11 @@ impl NativeCallable for Allocate { } #[test] +/// 验证成功只移交一个结果根,未返回临时对象及错误路径分配全部回收。 fn scoped_allocations_return_one_root_and_clean_up_errors() { + // 独立 VM 隔离测试对象图,所有结果根由场景显式归还。 let mut vm = Vm::new().unwrap(); + // 分配上下文函数载荷,captures 管理显式托管边,分派器 Arc 不代替它们。 let function = vm .allocate(ContextNativeFunction::new( "allocate", @@ -28,12 +37,15 @@ fn scoped_allocations_return_one_root_and_clean_up_errors() { vec![], )) .unwrap(); + // VM 调用已完成根移交,返回对象应只有一份调用者根。 let value = vm.call(function, &[]).unwrap(); + // 解构返回值只复制身份,不增加引用计数。 let Value::Object(object) = value else { panic!("expected object") }; assert_eq!(vm.ref_count(object).unwrap(), 1); assert_eq!(vm.object_count(), 2); + // 归还成功结果,之后只剩函数载荷,临时对象应已自动清理。 vm.release_value(value).unwrap(); vm.replace_payload( function, @@ -47,8 +59,10 @@ fn scoped_allocations_return_one_root_and_clean_up_errors() { } #[derive(Debug)] +/// 返回目标借用身份,由包装载荷 captures 保活,NativeContext 为 Invoke 输出取得拥有根。 struct Delegate(ObjectRef); impl NativeCallable for Delegate { + /// VM 在 NativeContext 内调用此测试分派器,临时分配与输入由上下文保活,返回/委托后移交输出根。 fn invoke(&self, _cx: &mut NativeContext<'_>, args: &[Value]) -> Result { Ok(NativeResult::Invoke { function: self.0, @@ -58,8 +72,11 @@ impl NativeCallable for Delegate { } #[test] +/// 验证 intrinsic 检查读取当前载荷的真实 Rust 实现,不能仅按不变函数身份推断。 fn native_implementation_inspection_follows_payload_replacement() { + // 独立 VM 隔离测试对象图,所有结果根由场景显式归还。 let mut vm = Vm::new().unwrap(); + // 分配上下文函数载荷,captures 管理显式托管边,分派器 Arc 不代替它们。 let function = vm .allocate(ContextNativeFunction::new( "allocate", @@ -67,12 +84,15 @@ fn native_implementation_inspection_follows_payload_replacement() { vec![], )) .unwrap(); + // 借用当前 ContextNativeFunction 载荷,下转型检查不分配对象或执行回调。 let payload = vm .payload::(function, "native") .unwrap(); assert!(payload.implementation::().is_some()); assert!(payload.implementation::().is_none()); + // 记录更新前版本,替换实现后身份不变但版本必须增加。 let version = vm.change_version(function).unwrap(); + // 委托目标有测试根,也会由 Delegate 包装载荷显式 captures 保留。 let target = vm .allocate(ContextNativeFunction::new( "target", @@ -85,12 +105,14 @@ fn native_implementation_inspection_follows_payload_replacement() { ContextNativeFunction::new("delegate", Delegate(target), vec![target]), ) .unwrap(); + // 借用当前 ContextNativeFunction 载荷,下转型检查不分配对象或执行回调。 let payload = vm .payload::(function, "native") .unwrap(); assert!(payload.implementation::().is_none()); assert_eq!(payload.implementation::().unwrap().0, target); assert!(vm.change_version(function).unwrap() > version); + // 逐项归还测试拥有根,包装载荷内部捕获边由 Heap 自动释放。 for object in [function, target] { vm.release(object).unwrap(); } @@ -98,8 +120,11 @@ fn native_implementation_inspection_follows_payload_replacement() { } #[test] +/// 验证原生委托与目标共享累计预算,预算失败后目标/参数临时根计数恢复。 fn native_delegation_shares_budget_and_cleans_owned_arguments() { + // 独立 VM 隔离测试对象图,所有结果根由场景显式归还。 let mut vm = Vm::new().unwrap(); + // 委托目标有测试根,也会由 Delegate 包装载荷显式 captures 保留。 let target = vm .allocate(ContextNativeFunction::new( "target", @@ -107,6 +132,7 @@ fn native_delegation_shares_budget_and_cleans_owned_arguments() { vec![], )) .unwrap(); + // Delegate 返回 Invoke 而不是递归 vm.call,让外层共享预算与清理流程。 let delegate = vm .allocate(ContextNativeFunction::new( "delegate", @@ -114,6 +140,7 @@ fn native_delegation_shares_budget_and_cleans_owned_arguments() { vec![target], )) .unwrap(); + // 额外对象参数用于观察失败委托是否遗留执行根。 let arg = vm.new_object().unwrap(); assert!( vm.call_with_limits( @@ -128,7 +155,9 @@ fn native_delegation_shares_budget_and_cleans_owned_arguments() { ); assert_eq!(vm.ref_count(arg).unwrap(), 1); assert_eq!(vm.ref_count(target).unwrap(), 2); + // VM 调用已完成根移交,返回对象应只有一份调用者根。 let value = vm.call(delegate, &[]).unwrap(); + // 归还成功结果,之后只剩函数载荷,临时对象应已自动清理。 vm.release_value(value).unwrap(); vm.release(delegate).unwrap(); vm.release(target).unwrap(); @@ -137,32 +166,45 @@ fn native_delegation_shares_budget_and_cleans_owned_arguments() { } #[test] +/// 弱句柄不持根;升级产生根,原对象释放后槽复用也不复活旧弱身份。 fn weak_references_expire_without_retaining_reused_slots() { + // 独立 VM 隔离测试对象图,所有结果根由场景显式归还。 let mut vm = Vm::new().unwrap(); + // 原始对象有一份分配根,弱身份或通知记录都不应额外持有它。 let object = vm.new_object().unwrap(); + // downgrade 只形成借用弱身份,不延长对象寿命。 let weak = vm.downgrade(object).unwrap(); + // 成功升级才取得一份独立外部根,随后必须归还。 let upgraded = vm.upgrade(weak).unwrap().unwrap(); vm.release(upgraded).unwrap(); vm.release(object).unwrap(); + // 新对象可能复用同槽,但代际不同不能复活 weak。 let replacement = vm.new_object().unwrap(); assert!(vm.upgrade(weak).unwrap().is_none()); + // 另一堆升级旧身份应明确报 ForeignHandle,而不是过期 None。 let mut foreign = Vm::new().unwrap(); assert_eq!(foreign.upgrade(weak), Err(VmError::ForeignHandle)); vm.release(replacement).unwrap(); } #[test] +/// 同对象/字段通知合并最新版本,容器更新发 Contents,通知不充当 GC 根。 fn changes_coalesce_cover_containers_and_do_not_keep_sources_alive() { + // 独立 VM 隔离测试对象图,所有结果根由场景显式归还。 let mut vm = Vm::new().unwrap(); + // 原始对象有一份分配根,弱身份或通知记录都不应额外持有它。 let object = vm.new_object().unwrap(); vm.set_attr(object, "value", Value::Bool(false)).unwrap(); vm.set_attr(object, "value", Value::Bool(true)).unwrap(); + // 两次同名写入的最终版本应出现在合并通知中。 let version = vm.change_version(object).unwrap(); + // 取走通知批次同时重置缓存,数据中的源身份不 retain。 let changes = vm.take_changes(); assert!(!changes.rescan_required); assert_eq!(changes.changes.len(), 1); assert_eq!(changes.changes[0].key, ChangeKey::Attribute("value".into())); assert_eq!(changes.changes[0].version, version); + // Cell 更新/清空使用载荷替换,通知键为 Contents,不是普通字段。 let cell = vm.new_cell(None).unwrap(); vm.cell_set(cell, Value::Object(object)).unwrap(); vm.cell_clear(cell).unwrap(); @@ -175,12 +217,17 @@ fn changes_coalesce_cover_containers_and_do_not_keep_sources_alive() { } #[test] +/// 超过通知唯一键上限清空精确条目并标记重扫,取走批次后新通知正常开始。 fn notification_overflow_requires_rescan_instead_of_losing_changes_silently() { + // 独立 VM 隔离测试对象图,所有结果根由场景显式归还。 let mut vm = Vm::new().unwrap(); + // 原始对象有一份分配根,弱身份或通知记录都不应额外持有它。 let object = vm.new_object().unwrap(); + // 制造超过 4096 个不同字段键,重复同键不足以触发通知溢出。 for index in 0..4100 { vm.set_attr(object, index.to_string(), Value::Nil).unwrap(); } + // 取走通知批次同时重置缓存,数据中的源身份不 retain。 let changes = vm.take_changes(); assert!(changes.rescan_required); assert!(changes.changes.is_empty()); @@ -191,15 +238,21 @@ fn notification_overflow_requires_rescan_instead_of_losing_changes_silently() { #[derive(Debug)] struct ReadResult { + /// 输入借用身份,包装载荷 captures 负责保活。 source: ObjectRef, + /// 是否故意写旧输入,触发只读保护错误。 mutate: bool, } impl NativeCallable for ReadResult { + /// VM 在 NativeContext 内调用此测试分派器,临时分配与输入由上下文保活,返回/委托后移交输出根。 fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result { + // 借用读取 source 的输入字段,读取路径纳入当前只读依赖收集。 let value = cx.store().get_attr(self.source, "value")?; + // 非法分支试图修改进入只读前已存在的 source,应在写屏障处失败。 if self.mutate { cx.set_attr(self.source, "value", Value::Number(99_i64.into()))?; } + // 新结果不在进入只读时的保护快照内,允许初始化 copied 字段。 let result = cx.new_object()?; cx.set_attr(result, "copied", value)?; Ok(NativeResult::Return(Value::Object(result))) @@ -207,11 +260,15 @@ impl NativeCallable for ReadResult { } #[test] +/// 只读调用记录输入观察并允许构造新结果,非法改旧对象报错后作用域必须恢复。 fn read_only_calls_track_borrowed_dependencies_allow_results_and_restore_after_error() { + // 独立 VM 隔离测试对象图,所有结果根由场景显式归还。 let mut vm = Vm::new().unwrap(); + // 输入源在调用前创建并初始化,是只读保护与读取依赖的对象。 let source = vm.new_object().unwrap(); vm.set_attr(source, "value", Value::Number(7_i64.into())) .unwrap(); + // 分配上下文函数载荷,captures 管理显式托管边,分派器 Arc 不代替它们。 let function = vm .allocate(ContextNativeFunction::new( "read result", @@ -222,12 +279,15 @@ fn read_only_calls_track_borrowed_dependencies_allow_results_and_restore_after_e vec![source], )) .unwrap(); + // 丢弃场景初始化通知,只检查本次只读派生构造是否发布了输入变更。 vm.take_changes(); + // 结果是拥有值,reads 是借用观察集合,两个所有权语义不能混同。 let (value, reads) = vm .call_read_only_with_limits(function, &[], ExecutionLimits::default()) .unwrap(); assert!(reads.contains(&source)); assert!(vm.take_changes().changes.is_empty()); + // 检查新结果 copied 值后归还结果根,不能 release 读取依赖句柄。 let Value::Object(result) = value else { panic!("result") }; @@ -264,40 +324,51 @@ fn read_only_calls_track_borrowed_dependencies_allow_results_and_restore_after_e } #[test] +/// 注解验证也记录依赖,但单独标为非内容读取;同对象普通读取随后提升为内容读取。 fn annotation_reads_remain_dependencies_without_becoming_program_content_reads() { use endor_core::AnnotationType; + // 本测试直接使用 Store 精确分类入口,不需要执行器帧。 let mut store = objects::ObjectStore::new().unwrap(); + // 普通 Record 不符合 Number,但有效对象应符合 Any。 let object = store.new_object().unwrap(); store .set_attr(object, "value", Value::Number(1.into())) .unwrap(); + // 只读范围收集整体观察,子区间分类不会清空外层依赖。 let scope = store.begin_read_only(); + // 每个子区间重新开始收集,按 LIFO 结束;重复读取也要在新区域出现。 let token = store.begin_read_tracking().unwrap(); assert!( store .matches_annotation(&Value::Object(object), AnnotationType::Any) .unwrap() ); + // 返回 ReadAccess 区分 content_read,句柄仍不拥有根。 let reads = store.end_read_tracking_details(token).unwrap(); assert!( reads .iter() .any(|read| read.object == object && !read.content_read) ); + // 每个子区间重新开始收集,按 LIFO 结束;重复读取也要在新区域出现。 let token = store.begin_read_tracking().unwrap(); assert!( !store .matches_annotation(&Value::Object(object), AnnotationType::Number) .unwrap() ); + // 异堆对象即使匹配 Any 也先拒绝无效归属。 let mut foreign = objects::ObjectStore::new().unwrap(); + // 有效但异堆的测试输入,用于排除 Any 跳过句柄校验的错误实现。 let alien = foreign.new_object().unwrap(); assert!(matches!( store.matches_annotation(&Value::Object(alien), AnnotationType::Any), Err(VmError::ForeignHandle) )); foreign.release(alien).unwrap(); + // 补一次普通字段读取,与前面的验证读取合并时 content_read 必须为 true。 store.get_attr(object, "value").unwrap(); + // 返回 ReadAccess 区分 content_read,句柄仍不拥有根。 let reads = store.end_read_tracking_details(token).unwrap(); assert!( reads