From a970fd7707009951c0290b3d6155e71702ffb4a6 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Wed, 21 Oct 2026 01:47:08 +0800 Subject: [PATCH] feat: add managed types, source classes and scoped native calls --- crates/compiler/src/README.md | 4 +- crates/compiler/src/analysis.rs | 83 ++++- crates/compiler/src/codegen.rs | 41 ++- crates/compiler/tests/README.md | 2 + crates/compiler/tests/classes.rs | 115 ++++++ crates/core/README.md | 2 +- crates/core/src/eval.rs | 188 +++++++--- crates/core/src/lib.rs | 14 +- crates/core/src/vm.rs | 82 ++++- crates/core/tests/README.md | 2 + crates/core/tests/native_context.rs | 150 ++++++++ crates/core/tests/types.rs | 65 ++++ crates/frontend/Endor.asdl | 4 + crates/frontend/editor/ast.schema.json | 100 +++++- crates/frontend/editor/ast.ts | 31 +- crates/frontend/src/README.md | 2 + crates/frontend/src/ast.rs | 10 +- crates/frontend/src/ast/generated.rs | 33 +- crates/frontend/src/ast/visitor.rs | 48 ++- crates/frontend/src/grammar.peg | 37 +- crates/frontend/src/parser.rs | 12 +- crates/frontend/tests/README.md | 2 +- crates/frontend/tests/ast.rs | 39 +- crates/frontend/tests/syntax.rs | 7 +- crates/objects/README.md | 4 +- crates/objects/src/README.md | 3 + crates/objects/src/changes.rs | 26 ++ crates/objects/src/code.rs | 1 + crates/objects/src/heap.rs | 84 ++++- crates/objects/src/instruction.rs | 3 + crates/objects/src/lib.rs | 8 +- crates/objects/src/native.rs | 206 +++++++++++ crates/objects/src/object.rs | 42 ++- crates/objects/src/store.rs | 94 ++++- crates/objects/src/types.rs | 474 +++++++++++++++++++++++++ docs/README.md | 2 + docs/architecture/README.md | 2 +- docs/contributing/handoff.md | 2 +- docs/runtime/README.md | 2 + docs/runtime/ast.md | 9 +- docs/runtime/builtins.md | 2 +- docs/runtime/bytecode.md | 15 +- docs/runtime/compiler.md | 11 +- docs/runtime/frontend.md | 11 +- docs/runtime/lifecycle.md | 2 + docs/runtime/native-calls.md | 55 +++ docs/runtime/objects.md | 16 +- docs/runtime/types.md | 56 +++ 48 files changed, 2069 insertions(+), 134 deletions(-) create mode 100644 crates/compiler/tests/classes.rs create mode 100644 crates/core/tests/native_context.rs create mode 100644 crates/core/tests/types.rs create mode 100644 crates/objects/src/changes.rs create mode 100644 crates/objects/src/native.rs create mode 100644 crates/objects/src/types.rs create mode 100644 docs/runtime/native-calls.md create mode 100644 docs/runtime/types.md diff --git a/crates/compiler/src/README.md b/crates/compiler/src/README.md index 069d398..c02822f 100644 --- a/crates/compiler/src/README.md +++ b/crates/compiler/src/README.md @@ -3,7 +3,7 @@ | 文件 | 职责 | | --- | --- | | [lib.rs](lib.rs) | CompileOptions、公开分析/编译入口、AST 原文范围检查与带来源诊断 | -| [analysis.rs](analysis.rs) | 模块/函数/表达式/单次交互作用域、局部槽、全局名称、逐引用绑定和初始化检查 | -| [codegen.rs](codegen.rs) | AST 到已有指令、嵌套代码模板、EndorCode 验证、显式 VM 装载与临时根释放 | +| [analysis.rs](analysis.rs) | 模块/函数/类方法/表达式/单次交互作用域、局部槽、全局名称、逐引用绑定和初始化检查 | +| [codegen.rs](codegen.rs) | AST 到函数与类创建指令、嵌套代码模板、EndorCode 验证、显式 VM 装载与临时根释放 | 节点定义在 frontend/Endor.asdl;这里匹配生成的 AST,不复制节点类型。源码和调用方提供的 AST 都先分析后生成,不执行 AST。修改名称语义、代码布局或所有权时同步 [编译契约](../../../docs/runtime/compiler.md)、[检查](../tests/README.md) 与 [示例](../examples/README.md)。返回 [compiler](../README.md)。 diff --git a/crates/compiler/src/analysis.rs b/crates/compiler/src/analysis.rs index 061967d..80c5025 100644 --- a/crates/compiler/src/analysis.rs +++ b/crates/compiler/src/analysis.rs @@ -39,7 +39,7 @@ pub struct Scope { #[derive(Debug, Clone)] pub struct SymbolTable { pub root: Scope, - /// Top-level function scopes in declaration order. Nested declarations + /// Function and class-method scopes in declaration order. Nested declarations /// are rejected until source closure semantics are implemented. pub functions: Vec, } @@ -48,9 +48,11 @@ fn error(span: Span, message: impl Into) -> Diagnostic { diagnostic(DiagnosticKind::Semantic, span, message) } -fn definition(declaration: &Decl) -> &Function { - let DeclKind::Function(function) = &declaration.kind; - function +fn declaration_name(declaration: &Decl) -> &frontend::ast::Name { + match &declaration.kind { + DeclKind::Function(function) => &function.name, + DeclKind::Class(class) => &class.name, + } } /// Resolve a closed source unit plus explicitly declared host globals. @@ -69,21 +71,25 @@ pub fn analyze(root: &AstRoot, options: &CompileOptions) -> Result = match root { - AstRoot::Module(program) => program.declarations.iter().map(definition).collect(), + let declarations: Vec<&Decl> = match root { + AstRoot::Module(program) => program.declarations.iter().collect(), AstRoot::Interactive(input) => input .statements .iter() - .filter_map(|statement| { - if let StmtKind::Declaration(declaration) = &statement.kind { - Some(definition(declaration)) - } else { - None - } + .filter_map(|statement| match &statement.kind { + StmtKind::Declaration(declaration) => Some(declaration), + _ => None, }) .collect(), AstRoot::Expression(_) => Vec::new(), }; + let functions: Vec<&Function> = declarations + .iter() + .flat_map(|declaration| match &declaration.kind { + DeclKind::Function(function) => vec![function], + DeclKind::Class(class) => class.methods.iter().collect(), + }) + .collect(); let (kind, name) = match root { AstRoot::Module(_) => (ScopeKind::Module, ""), AstRoot::Expression(_) => (ScopeKind::Expression, ""), @@ -91,10 +97,43 @@ pub fn analyze(root: &AstRoot, options: &CompileOptions) -> Result Result { + root_scope.expression(&assignment.object, &globals)?; + root_scope.expression(&assignment.value, &globals)?; + root_scope.intern(&assignment.name.text); + } StmtKind::Declaration(_) => {} StmtKind::Expression(expression) => { root_scope.expression(expression, &globals)? @@ -181,7 +225,7 @@ fn function_scope(function: &Function, globals: &BTreeSet) -> Result { return Err(error( statement.span, - "nested function declarations are not compiled yet", + "nested declarations are not compiled yet", )); } _ => {} @@ -210,6 +254,11 @@ fn function_scope(function: &Function, globals: &BTreeSet) -> Result { + builder.expression(&assignment.object, globals)?; + builder.expression(&assignment.value, globals)?; + builder.intern(&assignment.name.text); + } StmtKind::Expression(expression) => builder.expression(expression, globals)?, StmtKind::Declaration(_) => unreachable!("nested declaration rejected above"), } diff --git a/crates/compiler/src/codegen.rs b/crates/compiler/src/codegen.rs index 5143102..a8950d8 100644 --- a/crates/compiler/src/codegen.rs +++ b/crates/compiler/src/codegen.rs @@ -150,7 +150,39 @@ impl<'a> Emitter<'a> { } fn declaration(&mut self, declaration: &Decl, symbols: &SymbolTable) -> Result<(), Diagnostic> { - let DeclKind::Function(function) = &declaration.kind; + match &declaration.kind { + DeclKind::Function(function) => { + self.function_value(function, symbols)?; + self.emit(Opcode::StoreGlobal, self.name_index(&function.name.text)) + } + DeclKind::Class(class) => { + self.constant(Value::Text(class.name.text.clone()))?; + let base = class + .base + .as_ref() + .map_or("Object", |base| base.text.as_str()); + self.emit(Opcode::LoadGlobal, self.name_index(base))?; + for field in &class.fields { + self.constant(Value::Text(field.name.text.clone()))?; + self.constant(Value::Text(field.type_name.text.clone()))?; + } + self.emit(Opcode::BuildMap, class.fields.len())?; + for method in &class.methods { + self.constant(Value::Text(method.name.text.clone()))?; + self.function_value(method, symbols)?; + } + self.emit(Opcode::BuildMap, class.methods.len())?; + self.simple(Opcode::BuildClass); + self.emit(Opcode::StoreGlobal, self.name_index(&class.name.text)) + } + } + } + + fn function_value( + &mut self, + function: &frontend::ast::Function, + symbols: &SymbolTable, + ) -> Result<(), Diagnostic> { let scope = symbols .functions .iter() @@ -176,7 +208,7 @@ impl<'a> Emitter<'a> { self.children.push((index, child)); self.constant(Value::Nil)?; // Replaced by a managed code object during load(). self.instructions.push(Instruction::make_function(0, 0)); - self.emit(Opcode::StoreGlobal, self.name_index(&function.name.text)) + Ok(()) } fn store_binding(&mut self, name: &str, value: &Expr) -> Result<(), Diagnostic> { @@ -202,6 +234,11 @@ impl<'a> Emitter<'a> { StmtKind::Assignment(assignment) => { self.store_binding(&assignment.name.text, &assignment.value)? } + StmtKind::MemberAssignment(assignment) => { + self.expression(&assignment.value)?; + self.expression(&assignment.object)?; + self.emit(Opcode::StoreAttr, self.name_index(&assignment.name.text))?; + } StmtKind::Return(statement) => { if let Some(value) = &statement.value { self.expression(value)?; diff --git a/crates/compiler/tests/README.md b/crates/compiler/tests/README.md index 109a5e1..19209ef 100644 --- a/crates/compiler/tests/README.md +++ b/crates/compiler/tests/README.md @@ -1,5 +1,7 @@ # 编译链路检查 +[classes.rs](classes.rs) 覆盖源码 class、继承字段构造、动态方法覆盖、绑定接收者保活/回收、绑定后参数数量和重复成员/缺少 self 的预执行诊断。只编译,不在本地运行。 + [pipeline.rs](pipeline.rs) 将真实 Endor 示例交给 reader/parser/名称分析/代码生成/VM,检查函数值、前向引用、局部遮蔽、精确数字常量、语义诊断、宿主全局与属性回调、对象结果保活、跨 VM 模板装载、反复调用、错误帧清理、递归限额、共享交互命名空间及超过 255 的操作数。 裸赋值检查覆盖跨交互输入创建/更新 globals、参数重赋值、局部槽复用及未初始化读取;执行结束检查空堆。 diff --git a/crates/compiler/tests/classes.rs b/crates/compiler/tests/classes.rs new file mode 100644 index 0000000..a62c288 --- /dev/null +++ b/crates/compiler/tests/classes.rs @@ -0,0 +1,115 @@ +use compiler::{CompileOptions, compile_source}; +use endor_core::{ObjectRef, Value, Vm}; +use frontend::{EndorReader, ParseMode}; + +fn execute(vm: &mut Vm, globals: ObjectRef, source: &str, mode: ParseMode) -> Value { + let names = vm + .dict_items(globals) + .unwrap() + .iter() + .filter_map(|(name, _)| { + if let Value::Text(name) = name { + Some(name.clone()) + } else { + None + } + }) + .collect(); + let source = EndorReader::from_text("", source); + let template = compile_source(&source, mode, &CompileOptions { globals: names }).unwrap(); + let code = template.load(vm).unwrap(); + let result = vm.execute(code, globals).unwrap(); + vm.release(code).unwrap(); + result +} + +#[test] +fn source_inheritance_binding_and_dynamic_dispatch_keep_the_real_receiver_alive() { + let mut vm = Vm::new().unwrap(); + let globals = vm.new_dict().unwrap(); + let roots = vm.bootstrap_types().unwrap(); + for (name, object) in [("Object", roots.object), ("Type", roots.type_type)] { + vm.dict_set(globals, Value::Text(name.into()), Value::Object(object)) + .unwrap(); + vm.release(object).unwrap(); + } + let source = r#" +class Base { + value: Number + function read(self: Base): Number { return self.value } + function via(self: Base): Number { return self.read() } + function withEvent(self: Base, event: Object): Object { return event } +} +class Child: Base { + label: String + function read(self: Child): Number { return 12 } +} +"#; + assert_eq!( + execute(&mut vm, globals, source, ParseMode::Module), + Value::Nil + ); + let callback = execute( + &mut vm, + globals, + "Child(9, \"ok\").via", + ParseMode::Expression, + ); + let Value::Object(callback) = callback else { + panic!("bound callback") + }; + assert_eq!( + vm.object_store() + .positional_parameter_count(callback) + .unwrap(), + Some(0) + ); + let with_event = execute( + &mut vm, + globals, + "Child(9, \"ok\").withEvent", + ParseMode::Expression, + ); + let Value::Object(with_event) = with_event else { + panic!("bound callback") + }; + assert_eq!( + vm.object_store() + .positional_parameter_count(with_event) + .unwrap(), + Some(1) + ); + vm.release(with_event).unwrap(); + vm.dict_clear(globals).unwrap(); + vm.release(globals).unwrap(); + vm.collect_cycles(); + // No global name or external instance root remains. The bound callback + // alone retains its receiver and can still dispatch to Child.read. + assert_eq!( + vm.call(callback, &[]).unwrap(), + Value::Number(12_i64.into()) + ); + vm.release(callback).unwrap(); + vm.collect_cycles(); + assert_eq!(vm.object_count(), 0); +} + +#[test] +fn duplicate_members_and_missing_receivers_fail_before_execution() { + for text in [ + "class Bad { value: Number value: String }", + "class Bad { function read(): Number { return 1 } }", + ] { + let source = EndorReader::from_text("", text); + assert!( + compile_source( + &source, + ParseMode::Module, + &CompileOptions { + globals: vec!["Object".into()] + } + ) + .is_err() + ); + } +} diff --git a/crates/core/README.md b/crates/core/README.md index fea1a05..893978f 100644 --- a/crates/core/README.md +++ b/crates/core/README.md @@ -10,6 +10,6 @@ Endor VM 包,仅负责 Vm、执行帧、调用协议和 wordcode 求值循环 Vm::new 创建独立 ObjectStore 和空执行状态;代码对象、全局字典和函数仍按宿主需要显式创建,再 execute/call。EndorObject、Number、ObjectRef、内置载荷、代码对象/指令定义与 VmError 在 objects 中实现;endor_core 保留这些类型的兼容重导出,既有 Vm 对象方法委托到 ObjectStore。 -没有 Python type/元类或语言根类。网络、数据库和进程环境属于宿主,源码阅读器与 lexer/parser 属于 [frontend](../frontend/README.md),AST 名称分析与字节码生成属于独立 [compiler](../compiler/README.md),见 [执行与版本模型](../../docs/architecture/execution-and-versions.md)。 +宿主可显式引导托管 Object/Type 和名义继承,见[类型机制](../../docs/runtime/types.md);普通源码 class 和继承方法分派已经接入,trait、原生类分配继承与完整元类尚未接入。网络、数据库和进程环境属于宿主,源码阅读器与 lexer/parser 属于 [frontend](../frontend/README.md),AST 名称分析与字节码生成属于独立 [compiler](../compiler/README.md),见 [执行与版本模型](../../docs/architecture/execution-and-versions.md)。 返回值所有权见 [生命周期](../../docs/runtime/lifecycle.md),执行契约见 [字节码 VM](../../docs/runtime/bytecode.md)。本地不构建或测试,见 [验证](../../docs/operations/validation.md)。返回 [工作区](../README.md)。 diff --git a/crates/core/src/eval.rs b/crates/core/src/eval.rs index cb87617..36a97d7 100644 --- a/crates/core/src/eval.rs +++ b/crates/core/src/eval.rs @@ -4,6 +4,12 @@ use crate::{ EndorStringObject, ExecutionLimits, ExecutionLocation, Instruction, ObjectRef, Opcode, Value, Vm, VmError, }; +use crate::{NativeContext, NativeResult}; + +enum PreparedCall { + Value(Value), + Frame(EndorFrame), +} fn invalid(offset: usize, reason: &'static str) -> VmError { VmError::InvalidBytecode { offset, reason } @@ -49,20 +55,17 @@ impl Vm { limits: ExecutionLimits, ) -> Result { self.last_traceback.clear(); - if !self - .object(function)? - .payload() - .as_any() - .is::() - { - return self.invoke_native(function, arguments); - } + let mut executed = 0; + let frame = match self.prepare_call(function, arguments.to_vec(), &mut executed, limits)? { + PreparedCall::Value(value) => return Ok(value), + PreparedCall::Frame(frame) => frame, + }; if limits.max_frames == 0 { + self.clear_frame(frame); return Err(VmError::ExecutionLimit("frames")); } - let frame = self.prepare_frame(function, arguments)?; let mut frames = vec![frame]; - let result = self.eval_frames(&mut frames, limits); + let result = self.eval_frames(&mut frames, limits, &mut executed); if result.is_err() { self.last_traceback = frames .iter() @@ -196,14 +199,68 @@ impl Vm { &mut self, function: ObjectRef, arguments: &[Value], - ) -> Result { + ) -> Result { self.object(function)?; for argument in arguments { self.validate_value(argument)?; } + if let Some(callable) = self.object(function)?.payload().native_callable() { + return NativeContext::execute(&mut self.objects, function, arguments, callable); + } let result = self.object(function)?.payload().call(arguments)?; self.retain_value(&result)?; - Ok(result) + Ok(NativeResult::Return(result)) + } + + fn prepare_call( + &mut self, + mut function: ObjectRef, + mut arguments: Vec, + executed: &mut usize, + limits: ExecutionLimits, + ) -> Result { + let mut owned = false; + let result = (|| loop { + if self + .object(function)? + .payload() + .as_any() + .is::() + { + return self + .prepare_frame(function, &arguments) + .map(PreparedCall::Frame); + } + if *executed >= limits.max_instructions { + return Err(VmError::ExecutionLimit("instructions")); + } + *executed += 1; + match self.invoke_native(function, &arguments)? { + NativeResult::Return(value) => return Ok(PreparedCall::Value(value)), + NativeResult::Invoke { + function: next, + arguments: values, + } => { + if owned { + self.release(function)?; + for value in arguments.drain(..) { + self.release_value(value)?; + } + } + function = next; + arguments = values; + owned = true; + } + } + })(); + if owned { + self.release(function) + .expect("delegated call owns its function"); + for value in arguments { + self.drop_root(value); + } + } + result } fn stack_value(frame: &EndorFrame, index: usize) -> Result { @@ -220,16 +277,15 @@ impl Vm { Self::stack_value(frame, frame.stack.len() - 1) } - fn assignment_value(&self, previous: Option, incoming: Value) -> Result { - if let (Some(Value::Object(old)), Value::Object(new)) = (&previous, &incoming) { - if old == new - || self - .object(*old)? - .payload() - .assign_from(self.object(*new)?.payload())? - { - return Ok(Value::Object(*old)); - } + fn assignment_value( + &mut self, + previous: Option, + incoming: Value, + ) -> Result { + if let (Some(Value::Object(old)), Value::Object(new)) = (&previous, &incoming) + && (old == new || self.objects.assign_from(*old, *new)?) + { + return Ok(Value::Object(*old)); } Ok(incoming) } @@ -356,17 +412,53 @@ impl Vm { self.new_function(name, code, frame.globals, defaults, closure) } + fn build_class(&mut self, frame: &EndorFrame) -> Result { + let start = frame.stack.len() - 4; + let Value::Text(name) = Self::stack_value(frame, start)? else { + return Err(VmError::ObjectKindMismatch("class name")); + }; + let Value::Object(base) = Self::stack_value(frame, start + 1)? else { + return Err(VmError::ObjectKindMismatch("base Type")); + }; + let Value::Object(fields) = Self::stack_value(frame, start + 2)? else { + return Err(VmError::ObjectKindMismatch("field map")); + }; + let Value::Object(methods) = Self::stack_value(frame, start + 3)? else { + return Err(VmError::ObjectKindMismatch("method map")); + }; + let fields = self + .dict_items(fields)? + .into_iter() + .map(|(name, type_name)| match (name, type_name) { + (Value::Text(name), Value::Text(type_name)) => Ok(crate::ClassField { + name: name.clone(), + type_name: type_name.clone(), + }), + _ => Err(VmError::ObjectKindMismatch("class field annotation")), + }) + .collect::, _>>()?; + let methods = self + .dict_items(methods)? + .into_iter() + .map(|(name, function)| match (name, function) { + (Value::Text(name), Value::Object(function)) => Ok((name.clone(), *function)), + _ => Err(VmError::ObjectKindMismatch("class method")), + }) + .collect::, _>>()?; + self.objects.new_class(name, base, fields, methods) + } + fn eval_frames( &mut self, frames: &mut Vec, limits: ExecutionLimits, + executed: &mut usize, ) -> Result { - let mut executed = 0; loop { - if executed >= limits.max_instructions { + if *executed >= limits.max_instructions { return Err(VmError::ExecutionLimit("instructions")); } - executed += 1; + *executed += 1; let frame = frames.last_mut().expect("an evaluation frame is active"); let decoded = frame.executable.instruction_at(frame.instruction_offset)?; frame.current_offset = decoded.offset; @@ -485,27 +577,24 @@ impl Vm { for index in start + 2..frame.stack.len() { arguments.push(Self::stack_value(frame, index)?); } - let language_function = self - .object(function)? - .payload() - .as_any() - .is::(); - if language_function { - if frames.len() >= limits.max_frames { - return Err(VmError::ExecutionLimit("frames")); + match self.prepare_call(function, arguments, executed, limits)? { + PreparedCall::Frame(mut callee) => { + if frames.len() >= limits.max_frames { + self.clear_frame(callee); + return Err(VmError::ExecutionLimit("frames")); + } + callee.previous = Some(frames.len() - 1); + self.discard(frames.last_mut().unwrap(), argument + 2); + frames.push(callee); + } + PreparedCall::Value(value) => { + self.discard(frames.last_mut().unwrap(), argument + 2); + frames + .last_mut() + .unwrap() + .stack + .push(StackEntry::Value(value)); } - let mut callee = self.prepare_frame(function, &arguments)?; - callee.previous = Some(frames.len() - 1); - self.discard(frames.last_mut().unwrap(), argument + 2); - frames.push(callee); - } else { - let value = self.invoke_native(function, &arguments)?; - self.discard(frames.last_mut().unwrap(), argument + 2); - frames - .last_mut() - .unwrap() - .stack - .push(StackEntry::Value(value)); } } Opcode::ReturnValue => { @@ -554,11 +643,16 @@ impl Vm { let Value::Object(owner) = Self::top_value(frame)? else { return Err(VmError::ObjectKindMismatch("object")); }; - let value = self.get_attr(owner, &frame.executable.layout().names[argument])?; - self.retain_value(&value)?; + let value = + self.bind_attr(owner, &frame.executable.layout().names[argument])?; self.discard(frame, 1); frame.stack.push(StackEntry::Value(value)); } + Opcode::BuildClass => { + let class = self.build_class(frame)?; + self.discard(frame, 4); + frame.stack.push(StackEntry::Value(Value::Object(class))); + } Opcode::StoreAttr => { let Value::Object(owner) = Self::top_value(frame)? else { return Err(VmError::ObjectKindMismatch("object")); diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 464a00c..9d638ce 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -44,11 +44,15 @@ mod vm; pub use frame::{EndorFrame, ExecutionLimits, ExecutionLocation}; // Preserve existing embedding imports; objects owns these definitions. +pub use objects::ObjectStore; pub use objects::{ - CodeLayout, CompareOp, DecodedInstruction, EndorBaseExceptionObject, EndorCellObject, - EndorCode, EndorCodeObject, EndorDecimal, EndorDictObject, EndorFunctionObject, EndorInteger, - EndorNativeCall, EndorNativeFunctionObject, EndorNumber, EndorNumberObject, EndorObject, - EndorRational, EndorReal, EndorStringObject, EndorTracebackEntry, Instruction, NativeCall, - NativeFunction, ObjectHeader, ObjectPayload, ObjectRef, Opcode, Record, Value, VmError, + 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, Record, Value, + VmError, WeakObjectRef, }; +pub use objects::{ClassField, EndorTypeObject, RootTypes, TypeConstructor}; pub use vm::Vm; diff --git a/crates/core/src/vm.rs b/crates/core/src/vm.rs index cb31da5..2591c88 100644 --- a/crates/core/src/vm.rs +++ b/crates/core/src/vm.rs @@ -9,11 +9,91 @@ use crate::{ /// reference accounting and builtin operations live in the objects crate. #[derive(Debug)] pub struct Vm { - objects: ObjectStore, + pub(crate) objects: ObjectStore, pub(crate) last_traceback: Vec, } impl Vm { + pub fn define_method( + &mut self, + type_object: ObjectRef, + name: impl Into, + function: ObjectRef, + ) -> Result<(), VmError> { + self.objects.define_method(type_object, name, function) + } + + /// Owned result; release_value when finished. Unlike get_attr, methods bind + /// their receiver and retain it until the bound method itself is released. + pub fn bind_attr(&mut self, object: ObjectRef, name: &str) -> Result { + self.objects.bind_attr(object, name) + } + + pub fn bootstrap_types(&mut self) -> Result { + self.objects.bootstrap_types() + } + + pub fn new_type( + &mut self, + name: impl Into, + base: ObjectRef, + constructor: crate::TypeConstructor, + ) -> Result { + self.objects.new_type(name, base, constructor) + } + + pub fn attach_type( + &mut self, + object: ObjectRef, + type_object: ObjectRef, + ) -> Result<(), VmError> { + self.objects.attach_type(object, type_object) + } + + pub fn type_of(&self, object: ObjectRef) -> Result, VmError> { + self.objects.type_of(object) + } + + pub fn is_subtype(&self, candidate: ObjectRef, expected: ObjectRef) -> Result { + self.objects.is_subtype(candidate, expected) + } + + pub fn is_instance(&self, object: ObjectRef, expected: ObjectRef) -> Result { + self.objects.is_instance(object, expected) + } + + /// Read-only access for embedding protocols sharing the same object graph. + pub fn object_store(&self) -> &ObjectStore { + &self.objects + } + + pub fn downgrade(&self, reference: ObjectRef) -> Result { + self.objects.downgrade(reference) + } + + pub fn upgrade( + &mut self, + reference: crate::WeakObjectRef, + ) -> Result, VmError> { + self.objects.upgrade(reference) + } + + pub fn take_changes(&mut self) -> crate::ChangeBatch { + self.objects.take_changes() + } + + pub fn change_version(&self, reference: ObjectRef) -> Result { + self.objects.change_version(reference) + } + + pub fn replace_payload( + &mut self, + reference: ObjectRef, + payload: impl ObjectPayload, + ) -> Result<(), VmError> { + self.objects.replace_payload(reference, payload) + } + pub fn new() -> Result { Ok(Self { objects: ObjectStore::new()?, diff --git a/crates/core/tests/README.md b/crates/core/tests/README.md index b21bd5f..f2ba7cc 100644 --- a/crates/core/tests/README.md +++ b/crates/core/tests/README.md @@ -7,6 +7,8 @@ | [runtime.rs](runtime.rs) | 空堆启动、默认操作、引用/借用、属性原子性、稳定地址、长链/循环、原生回调、VM 销毁 | | [builtins.rs](builtins.rs) | 无损 Number、Unicode 字符串、Dict 顺序与键、Cell、函数环境/共享代码、异常数据 | | [execution.rs](execution.rs) | wordcode 与校验、控制流、参数/默认值、嵌套帧、字节码函数创建/默认值/环境捕获、闭包、返回值存活、失败清理、限额 | +| [types.rs](types.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 new file mode 100644 index 0000000..79526ba --- /dev/null +++ b/crates/core/tests/native_context.rs @@ -0,0 +1,150 @@ +//! Remote-only regression coverage. Do not execute locally. +use endor_core::{ + ChangeKey, ContextNativeFunction, ExecutionLimits, NativeCallable, NativeContext, NativeResult, + ObjectRef, Value, Vm, VmError, +}; + +#[derive(Debug)] +struct Allocate(bool); +impl NativeCallable for Allocate { + fn invoke(&self, cx: &mut NativeContext<'_>, _args: &[Value]) -> Result { + let value = cx.new_object()?; + cx.set_attr(value, "label", Value::Text("scoped".into()))?; + let _temporary = cx.new_object()?; + if self.0 { + return Err(VmError::InvalidArgument("intentional failure")); + } + Ok(NativeResult::Return(Value::Object(value))) + } +} + +#[test] +fn scoped_allocations_return_one_root_and_clean_up_errors() { + let mut vm = Vm::new().unwrap(); + let function = vm + .allocate(ContextNativeFunction::new( + "allocate", + Allocate(false), + vec![], + )) + .unwrap(); + 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, + ContextNativeFunction::new("fail", Allocate(true), vec![]), + ) + .unwrap(); + assert!(vm.call(function, &[]).is_err()); + assert_eq!(vm.object_count(), 1); + vm.release(function).unwrap(); + assert_eq!(vm.object_count(), 0); +} + +#[derive(Debug)] +struct Delegate(ObjectRef); +impl NativeCallable for Delegate { + fn invoke(&self, _cx: &mut NativeContext<'_>, args: &[Value]) -> Result { + Ok(NativeResult::Invoke { + function: self.0, + arguments: args.to_vec(), + }) + } +} + +#[test] +fn native_delegation_shares_budget_and_cleans_owned_arguments() { + let mut vm = Vm::new().unwrap(); + let target = vm + .allocate(ContextNativeFunction::new( + "target", + Allocate(false), + vec![], + )) + .unwrap(); + let delegate = vm + .allocate(ContextNativeFunction::new( + "delegate", + Delegate(target), + vec![target], + )) + .unwrap(); + let arg = vm.new_object().unwrap(); + assert!( + vm.call_with_limits( + delegate, + &[Value::Object(arg)], + ExecutionLimits { + max_frames: 1, + max_instructions: 1 + } + ) + .is_err() + ); + assert_eq!(vm.ref_count(arg).unwrap(), 1); + assert_eq!(vm.ref_count(target).unwrap(), 2); + let value = vm.call(delegate, &[]).unwrap(); + vm.release_value(value).unwrap(); + vm.release(delegate).unwrap(); + vm.release(target).unwrap(); + vm.release(arg).unwrap(); + assert_eq!(vm.object_count(), 0); +} + +#[test] +fn weak_references_expire_without_retaining_reused_slots() { + let mut vm = Vm::new().unwrap(); + let object = vm.new_object().unwrap(); + let weak = vm.downgrade(object).unwrap(); + let upgraded = vm.upgrade(weak).unwrap().unwrap(); + vm.release(upgraded).unwrap(); + vm.release(object).unwrap(); + let replacement = vm.new_object().unwrap(); + assert!(vm.upgrade(weak).unwrap().is_none()); + let mut foreign = Vm::new().unwrap(); + assert_eq!(foreign.upgrade(weak), Err(VmError::ForeignHandle)); + vm.release(replacement).unwrap(); +} + +#[test] +fn changes_coalesce_cover_containers_and_do_not_keep_sources_alive() { + 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(); + 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); + let cell = vm.new_cell(None).unwrap(); + vm.cell_set(cell, Value::Object(object)).unwrap(); + vm.cell_clear(cell).unwrap(); + assert_eq!(vm.take_changes().changes[0].key, ChangeKey::Contents); + vm.set_attr(object, "self", Value::Object(object)).unwrap(); + vm.release(object).unwrap(); + assert_eq!(vm.collect_cycles(), 1); + vm.release(cell).unwrap(); + assert_eq!(vm.object_count(), 0); +} + +#[test] +fn notification_overflow_requires_rescan_instead_of_losing_changes_silently() { + let mut vm = Vm::new().unwrap(); + let object = vm.new_object().unwrap(); + for index in 0..4100 { + vm.set_attr(object, index.to_string(), Value::Nil).unwrap(); + } + let changes = vm.take_changes(); + assert!(changes.rescan_required); + assert!(changes.changes.is_empty()); + vm.set_attr(object, "after", Value::Nil).unwrap(); + assert_eq!(vm.take_changes().changes.len(), 1); + vm.release(object).unwrap(); +} diff --git a/crates/core/tests/types.rs b/crates/core/tests/types.rs new file mode 100644 index 0000000..4dd88fb --- /dev/null +++ b/crates/core/tests/types.rs @@ -0,0 +1,65 @@ +use endor_core::{TypeConstructor, Value, Vm, VmError}; + +#[test] +fn nominal_types_have_distinct_inheritance_and_instance_relations() { + let mut vm = Vm::new().unwrap(); + assert_eq!(vm.object_count(), 0); + let roots = vm.bootstrap_types().unwrap(); + let block = vm + .new_type("Block", roots.object, TypeConstructor::Abstract) + .unwrap(); + let page = vm.new_type("Page", block, TypeConstructor::Record).unwrap(); + let other = vm.new_type("Page", block, TypeConstructor::Record).unwrap(); + assert!(vm.is_subtype(page, block).unwrap()); + assert!(!vm.is_subtype(block, page).unwrap()); + assert!(!vm.is_subtype(page, other).unwrap()); + assert!(vm.is_instance(page, roots.type_type).unwrap()); + assert!(vm.is_instance(roots.object, roots.type_type).unwrap()); + assert!(vm.is_instance(roots.type_type, roots.type_type).unwrap()); + assert!(!vm.is_subtype(roots.object, roots.type_type).unwrap()); + let Value::Object(instance) = vm.call(page, &[]).unwrap() else { + panic!("instance required") + }; + assert!(vm.is_instance(instance, block).unwrap()); + assert!(!vm.is_instance(instance, roots.type_type).unwrap()); + assert_eq!(vm.get_attr(instance, "type").unwrap(), Value::Object(page)); + assert_eq!(vm.get_attr(page, "base").unwrap(), Value::Object(block)); + assert!(vm.set_attr(instance, "type", Value::Object(other)).is_err()); + assert!(vm.attach_type(instance, other).is_err()); + assert!(vm.replace_payload(page, endor_core::Record).is_err()); + for object in [roots.object, roots.type_type, block, page, other] { + vm.release(object).unwrap(); + } + vm.collect_cycles(); + // The instance keeps its definition, base and metatype reachable without + // any host-owned type registry. The unrelated same-name type was collected. + assert_eq!(vm.object_count(), 5); + assert!(vm.is_instance(instance, block).unwrap()); + vm.release(instance).unwrap(); + vm.collect_cycles(); + assert_eq!(vm.object_count(), 0); +} + +#[test] +fn foreign_type_edges_fail_without_changing_the_target() { + let mut first = Vm::new().unwrap(); + let mut second = Vm::new().unwrap(); + let roots = first.bootstrap_types().unwrap(); + let object = second.new_object().unwrap(); + assert_eq!( + second.attach_type(object, roots.object), + Err(VmError::ForeignHandle) + ); + assert_eq!(second.type_of(object).unwrap(), None); + assert_eq!(second.ref_count(object).unwrap(), 1); + second + .set_attr(object, "type", Value::Text("existing field".into())) + .unwrap(); + let local = second.bootstrap_types().unwrap(); + assert!(second.attach_type(object, local.object).is_err()); + assert_eq!( + second.get_attr(object, "type").unwrap(), + Value::Text("existing field".into()) + ); + assert_eq!(second.type_of(object).unwrap(), None); +} diff --git a/crates/frontend/Endor.asdl b/crates/frontend/Endor.asdl index 6c7e7c5..04a44b4 100644 --- a/crates/frontend/Endor.asdl +++ b/crates/frontend/Endor.asdl @@ -12,17 +12,21 @@ module Endor { Program = (Decl* declarations, span span) Decl = (DeclKind kind, span span) DeclKind = Function(Function definition) + | Class(Class definition) + Class = (Name name, Name? base, Parameter* fields, Function* methods, span span) Function = (Name name, Parameter* parameters, Name return_type, Block body, span span) Parameter = (Name name, Name type_name, span span) Block = (Stmt* statements, span span) Stmt = (StmtKind kind, span span) StmtKind = Binding(Binding binding) | Assignment(Assignment assignment) + | MemberAssignment(MemberAssignment assignment) | Return(ReturnStatement statement) | Expression(Expr expression) | Declaration(Decl declaration) Binding = (Name name, Expr value, span span) Assignment = (Name name, Expr value, span span) + MemberAssignment = (Expr object, Name name, Expr value, span span) ReturnStatement = (Expr? value, span span) InteractiveInput = (Stmt* statements, span span) diff --git a/crates/frontend/editor/ast.schema.json b/crates/frontend/editor/ast.schema.json index 5aa7376..72c4dba 100644 --- a/crates/frontend/editor/ast.schema.json +++ b/crates/frontend/editor/ast.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "urn:endor:ast:sha256:8c89fde6c029357a0809751c41cb46d59972b987bbe1b25c0cced4bfcdbe2a7b", + "$id": "urn:endor:ast:sha256:ad2ac52943dd26e08395c0d2698861da98bb89c360241c4712c769de86709fd0", "$ref": "#/$defs/AstDocument", "$defs": { "Span": { @@ -27,7 +27,7 @@ "type": "object", "properties": { "schema": { - "const": "urn:endor:ast:sha256:8c89fde6c029357a0809751c41cb46d59972b987bbe1b25c0cced4bfcdbe2a7b" + "const": "urn:endor:ast:sha256:ad2ac52943dd26e08395c0d2698861da98bb89c360241c4712c769de86709fd0" }, "root": { "$ref": "#/$defs/AstRoot" @@ -159,9 +159,65 @@ "data" ], "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "Class" + }, + "data": { + "$ref": "#/$defs/Class" + } + }, + "required": [ + "kind", + "data" + ], + "additionalProperties": false } ] }, + "Class": { + "type": "object", + "properties": { + "name": { + "$ref": "#/$defs/Name" + }, + "base": { + "anyOf": [ + { + "$ref": "#/$defs/Name" + }, + { + "type": "null" + } + ] + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/$defs/Parameter" + } + }, + "methods": { + "type": "array", + "items": { + "$ref": "#/$defs/Function" + } + }, + "span": { + "$ref": "#/$defs/Span" + } + }, + "required": [ + "name", + "fields", + "methods", + "span" + ], + "additionalProperties": false + }, "Function": { "type": "object", "properties": { @@ -282,6 +338,22 @@ ], "additionalProperties": false }, + { + "type": "object", + "properties": { + "kind": { + "const": "MemberAssignment" + }, + "data": { + "$ref": "#/$defs/MemberAssignment" + } + }, + "required": [ + "kind", + "data" + ], + "additionalProperties": false + }, { "type": "object", "properties": { @@ -372,6 +444,30 @@ ], "additionalProperties": false }, + "MemberAssignment": { + "type": "object", + "properties": { + "object": { + "$ref": "#/$defs/Expr" + }, + "name": { + "$ref": "#/$defs/Name" + }, + "value": { + "$ref": "#/$defs/Expr" + }, + "span": { + "$ref": "#/$defs/Span" + } + }, + "required": [ + "object", + "name", + "value", + "span" + ], + "additionalProperties": false + }, "ReturnStatement": { "type": "object", "properties": { diff --git a/crates/frontend/editor/ast.ts b/crates/frontend/editor/ast.ts index b56c597..50e9516 100644 --- a/crates/frontend/editor/ast.ts +++ b/crates/frontend/editor/ast.ts @@ -1,20 +1,22 @@ // Generated by scripts/generate_ast.py from Endor.asdl. Do not edit. -export const AST_SCHEMA_ID = "urn:endor:ast:sha256:8c89fde6c029357a0809751c41cb46d59972b987bbe1b25c0cced4bfcdbe2a7b" as const; +export const AST_SCHEMA_ID = "urn:endor:ast:sha256:ad2ac52943dd26e08395c0d2698861da98bb89c360241c4712c769de86709fd0" as const; export interface Span { start: number; end: number; } export type AstDocument = { schema: typeof AST_SCHEMA_ID; root: AstRoot; }; export type AstRoot = { kind: "Module"; data: Program; } | { kind: "Expression"; data: Expr; } | { kind: "Interactive"; data: InteractiveInput; }; export type Name = { text: string; span: Span; }; export type Program = { declarations: Array; span: Span; }; export type Decl = { kind: DeclKind; span: Span; }; -export type DeclKind = { kind: "Function"; data: Function; }; +export type DeclKind = { kind: "Function"; data: Function; } | { kind: "Class"; data: Class; }; +export type Class = { name: Name; base?: Name | null; fields: Array; methods: Array; span: Span; }; export type Function = { name: Name; parameters: Array; return_type: Name; body: Block; span: Span; }; export type Parameter = { name: Name; type_name: Name; span: Span; }; export type Block = { statements: Array; span: Span; }; export type Stmt = { kind: StmtKind; span: Span; }; -export type StmtKind = { kind: "Binding"; data: Binding; } | { kind: "Assignment"; data: Assignment; } | { kind: "Return"; data: ReturnStatement; } | { kind: "Expression"; data: Expr; } | { kind: "Declaration"; data: Decl; }; +export type StmtKind = { kind: "Binding"; data: Binding; } | { kind: "Assignment"; data: Assignment; } | { kind: "MemberAssignment"; data: MemberAssignment; } | { kind: "Return"; data: ReturnStatement; } | { kind: "Expression"; data: Expr; } | { kind: "Declaration"; data: Decl; }; export type Binding = { name: Name; value: Expr; span: Span; }; export type Assignment = { name: Name; value: Expr; span: Span; }; +export type MemberAssignment = { object: Expr; name: Name; value: Expr; span: Span; }; export type ReturnStatement = { value?: Expr | null; span: Span; }; export type InteractiveInput = { statements: Array; span: Span; }; export type Expr = { kind: ExprKind; span: Span; }; @@ -29,6 +31,7 @@ visitName?(node: Name): boolean | void; visitProgram?(node: Program): boolean | void; visitDecl?(node: Decl): boolean | void; visitDeclKind?(node: DeclKind): boolean | void; +visitClass?(node: Class): boolean | void; visitFunction?(node: Function): boolean | void; visitParameter?(node: Parameter): boolean | void; visitBlock?(node: Block): boolean | void; @@ -36,6 +39,7 @@ visitStmt?(node: Stmt): boolean | void; visitStmtKind?(node: StmtKind): boolean | void; visitBinding?(node: Binding): boolean | void; visitAssignment?(node: Assignment): boolean | void; +visitMemberAssignment?(node: MemberAssignment): boolean | void; visitReturnStatement?(node: ReturnStatement): boolean | void; visitInteractiveInput?(node: InteractiveInput): boolean | void; visitExpr?(node: Expr): boolean | void; @@ -82,8 +86,19 @@ switch (node.kind) { case "Function": walkFunction(node.data, visitor); break; +case "Class": +walkClass(node.data, visitor); +break; } } +export function walkClass(node: Class, visitor: Visitor): void { +if (visitor.visitClass?.(node) === false) return; +walkName(node.name, visitor); +if (node.base != null) walkName(node.base, visitor); +for (const value of node.fields) walkParameter(value, visitor); +for (const value of node.methods) walkFunction(value, visitor); +walkSpan(node.span, visitor); +} export function walkFunction(node: Function, visitor: Visitor): void { if (visitor.visitFunction?.(node) === false) return; walkName(node.name, visitor); @@ -117,6 +132,9 @@ break; case "Assignment": walkAssignment(node.data, visitor); break; +case "MemberAssignment": +walkMemberAssignment(node.data, visitor); +break; case "Return": walkReturnStatement(node.data, visitor); break; @@ -140,6 +158,13 @@ walkName(node.name, visitor); walkExpr(node.value, visitor); walkSpan(node.span, visitor); } +export function walkMemberAssignment(node: MemberAssignment, visitor: Visitor): void { +if (visitor.visitMemberAssignment?.(node) === false) return; +walkExpr(node.object, visitor); +walkName(node.name, visitor); +walkExpr(node.value, visitor); +walkSpan(node.span, visitor); +} export function walkReturnStatement(node: ReturnStatement, visitor: Visitor): void { if (visitor.visitReturnStatement?.(node) === false) return; if (node.value != null) walkExpr(node.value, visitor); diff --git a/crates/frontend/src/README.md b/crates/frontend/src/README.md index 11d0944..961409c 100644 --- a/crates/frontend/src/README.md +++ b/crates/frontend/src/README.md @@ -14,4 +14,6 @@ | [parser.rs](parser.rs) | 统一根/ParseMode 与具体解析 API、错误映射、AST 辅助函数,以及独立语法文件的编译期引入 | | [grammar.peg](grammar.peg) | token 输入的 PEG 规则、运算符优先级与 AST 构造动作;保留 peg::parser! 外层声明 | +函数体允许独立方法调用等表达式语句;末尾 return 可省略,已有编译器隐式返回 Nil。 + 改节点先编辑 ../Endor.asdl,再用 scripts/generate_ast.py 更新 Rust/编辑器产物,契约见 [AST 模型](../../../docs/runtime/ast.md)。改输入适配时同步 [阅读器契约](../../../docs/runtime/reader.md) 与读取错误检查;改词法从 lexer.peg 进入,并同步 token、诊断和错误检查;改语法从 grammar.peg 进入,并同步 AST、[测试](../tests/README.md)、样例与 [前端契约](../../../docs/runtime/frontend.md)。词法与语法两份 PEG 动作均不做 VM 分配、I/O 或修改外部状态,避免回溯造成副作用。返回 [frontend](../README.md)。 diff --git a/crates/frontend/src/ast.rs b/crates/frontend/src/ast.rs index dc10940..b6cc395 100644 --- a/crates/frontend/src/ast.rs +++ b/crates/frontend/src/ast.rs @@ -26,9 +26,11 @@ pub trait Spanned { impl Program { /// Iterate the currently supported top-level function declarations. pub fn functions(&self) -> impl Iterator { - self.declarations.iter().map(|declaration| { - let DeclKind::Function(function) = &declaration.kind; - function - }) + self.declarations + .iter() + .filter_map(|declaration| match &declaration.kind { + DeclKind::Function(function) => Some(function), + DeclKind::Class(_) => None, + }) } } diff --git a/crates/frontend/src/ast/generated.rs b/crates/frontend/src/ast/generated.rs index a0487b9..d654a99 100644 --- a/crates/frontend/src/ast/generated.rs +++ b/crates/frontend/src/ast/generated.rs @@ -3,7 +3,7 @@ use super::Spanned; use crate::Span; pub const AST_SCHEMA_ID: &str = - "urn:endor:ast:sha256:8c89fde6c029357a0809751c41cb46d59972b987bbe1b25c0cced4bfcdbe2a7b"; + "urn:endor:ast:sha256:ad2ac52943dd26e08395c0d2698861da98bb89c360241c4712c769de86709fd0"; #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] pub struct AstDocument { @@ -63,16 +63,32 @@ impl Spanned for Decl { #[serde(tag = "kind", content = "data", deny_unknown_fields)] pub enum DeclKind { Function(Function), + Class(Class), } impl Spanned for DeclKind { fn span(&self) -> Span { match self { Self::Function(node) => node.span(), + Self::Class(node) => node.span(), } } } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] +pub struct Class { + pub name: Name, + pub base: Option, + pub fields: Vec, + pub methods: Vec, + pub span: Span, +} +impl Spanned for Class { + fn span(&self) -> Span { + self.span + } +} +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] pub struct Function { pub name: Name, pub parameters: Vec, @@ -124,6 +140,7 @@ impl Spanned for Stmt { pub enum StmtKind { Binding(Binding), Assignment(Assignment), + MemberAssignment(MemberAssignment), Return(ReturnStatement), Expression(Expr), Declaration(Decl), @@ -133,6 +150,7 @@ impl Spanned for StmtKind { match self { Self::Binding(node) => node.span(), Self::Assignment(node) => node.span(), + Self::MemberAssignment(node) => node.span(), Self::Return(node) => node.span(), Self::Expression(node) => node.span(), Self::Declaration(node) => node.span(), @@ -165,6 +183,19 @@ impl Spanned for Assignment { } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] +pub struct MemberAssignment { + pub object: Expr, + pub name: Name, + pub value: Expr, + pub span: Span, +} +impl Spanned for MemberAssignment { + fn span(&self) -> Span { + self.span + } +} +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] pub struct ReturnStatement { pub value: Option, pub span: Span, diff --git a/crates/frontend/src/ast/visitor.rs b/crates/frontend/src/ast/visitor.rs index f615a88..285214a 100644 --- a/crates/frontend/src/ast/visitor.rs +++ b/crates/frontend/src/ast/visitor.rs @@ -1,9 +1,9 @@ // Generated by scripts/generate_ast.py from Endor.asdl. Do not edit. use super::{ - Assignment, AstDocument, AstRoot, BinaryOp, Binding, Block, Decl, DeclKind, Expr, ExprKind, - Function, InteractiveInput, Name, Parameter, Program, ReturnStatement, Spanned, Stmt, StmtKind, - UnaryOp, + Assignment, AstDocument, AstRoot, BinaryOp, Binding, Block, Class, Decl, DeclKind, Expr, + ExprKind, Function, InteractiveInput, MemberAssignment, Name, Parameter, Program, + ReturnStatement, Spanned, Stmt, StmtKind, UnaryOp, }; use crate::Span; pub trait Visitor { @@ -48,6 +48,12 @@ pub trait Visitor { self.leave_node(); result } + fn visit_class(&mut self, node: &Class) -> Result<(), Self::Error> { + self.enter_node(node.span())?; + let result = walk_class(self, node); + self.leave_node(); + result + } fn visit_function(&mut self, node: &Function) -> Result<(), Self::Error> { self.enter_node(node.span())?; let result = walk_function(self, node); @@ -90,6 +96,12 @@ pub trait Visitor { self.leave_node(); result } + fn visit_member_assignment(&mut self, node: &MemberAssignment) -> Result<(), Self::Error> { + self.enter_node(node.span())?; + let result = walk_member_assignment(self, node); + self.leave_node(); + result + } fn visit_return_statement(&mut self, node: &ReturnStatement) -> Result<(), Self::Error> { self.enter_node(node.span())?; let result = walk_return_statement(self, node); @@ -163,9 +175,26 @@ pub fn walk_decl_kind( DeclKind::Function(value) => { visitor.visit_function(value)?; } + DeclKind::Class(value) => { + visitor.visit_class(value)?; + } } Ok(()) } +pub fn walk_class(visitor: &mut V, node: &Class) -> Result<(), V::Error> { + visitor.visit_name(&node.name)?; + if let Some(value) = &node.base { + visitor.visit_name(value)?; + } + for value in &node.fields { + visitor.visit_parameter(value)?; + } + for value in &node.methods { + visitor.visit_function(value)?; + } + visitor.visit_span(&node.span)?; + Ok(()) +} pub fn walk_function( visitor: &mut V, node: &Function, @@ -211,6 +240,9 @@ pub fn walk_stmt_kind( StmtKind::Assignment(value) => { visitor.visit_assignment(value)?; } + StmtKind::MemberAssignment(value) => { + visitor.visit_member_assignment(value)?; + } StmtKind::Return(value) => { visitor.visit_return_statement(value)?; } @@ -238,6 +270,16 @@ pub fn walk_assignment( visitor.visit_span(&node.span)?; Ok(()) } +pub fn walk_member_assignment( + visitor: &mut V, + node: &MemberAssignment, +) -> Result<(), V::Error> { + visitor.visit_expr(&node.object)?; + visitor.visit_name(&node.name)?; + visitor.visit_expr(&node.value)?; + visitor.visit_span(&node.span)?; + Ok(()) +} pub fn walk_return_statement( visitor: &mut V, node: &ReturnStatement, diff --git a/crates/frontend/src/grammar.peg b/crates/frontend/src/grammar.peg index 9624afb..97f9543 100644 --- a/crates/frontend/src/grammar.peg +++ b/crates/frontend/src/grammar.peg @@ -51,6 +51,23 @@ peg::parser! { = function:function() { Decl { span: function.span, kind: DeclKind::Function(function) } } + / start:keyword("class") name:name() base:(colon() base:name() { base })? + lbrace() members:class_member()* end:rbrace() { + let mut fields = Vec::new(); + let mut methods = Vec::new(); + for member in members { + match member { + ClassMember::Field(field) => fields.push(field), + ClassMember::Method(method) => methods.push(method), + } + } + let span = start.through(end); + Decl { span, kind: DeclKind::Class(Class { name, base, fields, methods, span }) } + } + + rule class_member() -> ClassMember + = function:function() { ClassMember::Method(function) } + / field:parameter() { ClassMember::Field(field) } pub rule interactive() -> InteractiveInput = statement:interactive_statement() { @@ -62,6 +79,7 @@ peg::parser! { Stmt { span: declaration.span, kind: StmtKind::Declaration(declaration) } } / binding:binding() { binding_statement(binding) } + / assignment:member_assignment() { assignment } / assignment:assignment() { assignment } / expression:expression() { Stmt { span: expression.span, kind: StmtKind::Expression(expression) } @@ -79,15 +97,19 @@ peg::parser! { } rule block() -> Block - = start:lbrace() locals:local_statement()* returned:return_statement() end:rbrace() { + = start:lbrace() locals:local_statement()* returned:return_statement()? end:rbrace() { let mut statements = locals; - statements.push(returned); + if let Some(returned) = returned { statements.push(returned); } Block { statements, span: start.through(end) } } rule local_statement() -> Stmt = binding:binding() { binding_statement(binding) } + / assignment:member_assignment() { assignment } / assignment:assignment() { assignment } + / !return_keyword() expression:expression() { + Stmt { span: expression.span, kind: StmtKind::Expression(expression) } + } rule assignment() -> Stmt = name:name() equal() value:expression() { @@ -95,6 +117,17 @@ peg::parser! { Stmt { span, kind: StmtKind::Assignment(Assignment { name, value, span }) } } + rule member_assignment() -> Stmt + = target:postfix_expression() equal() value:expression() {? + let span = target.span.through(value.span); + match target.kind { + ExprKind::Member { object, name } => Ok(Stmt { + span, kind: StmtKind::MemberAssignment(MemberAssignment { object: *object, name, value, span }), + }), + _ => Err("member assignment target"), + } + } + rule binding() -> Binding = start:let_keyword() name:name() equal() value:expression() { Binding { span: start.through(value.span), name, value } diff --git a/crates/frontend/src/parser.rs b/crates/frontend/src/parser.rs index d5fca84..3ef9fed 100644 --- a/crates/frontend/src/parser.rs +++ b/crates/frontend/src/parser.rs @@ -1,6 +1,7 @@ use crate::ast::{ - Assignment, AstRoot, BinaryOp, Binding, Block, Decl, DeclKind, Expr, ExprKind, Function, - InteractiveInput, Name, Parameter, Program, ReturnStatement, Stmt, StmtKind, UnaryOp, + Assignment, AstRoot, BinaryOp, Binding, Block, Class, Decl, DeclKind, Expr, ExprKind, Function, + InteractiveInput, MemberAssignment, Name, Parameter, Program, ReturnStatement, Stmt, StmtKind, + UnaryOp, }; use crate::lexer::decode_string; use crate::{Diagnostic, DiagnosticKind, Span, Token, TokenKind, lex}; @@ -44,7 +45,7 @@ pub fn parse_source_expression( parse_expression(source.text()).map_err(|error| source.annotate(error)) } -/// Parse one or more basic typed function declarations. This is a syntax +/// Parse one or more typed function or class declarations. This is a syntax /// operation: type names and name references are retained without resolution. pub fn parse(source: &str) -> Result { let tokens = lex(source)?; @@ -93,6 +94,11 @@ enum Suffix { Call(Vec, Span), } +enum ClassMember { + Field(Parameter), + Method(Function), +} + fn postfix(expression: Expr, suffix: Suffix) -> Expr { match suffix { Suffix::Member(name) => Expr { diff --git a/crates/frontend/tests/README.md b/crates/frontend/tests/README.md index 992c628..9cdfab4 100644 --- a/crates/frontend/tests/README.md +++ b/crates/frontend/tests/README.md @@ -2,7 +2,7 @@ [syntax.rs](syntax.rs) 覆盖 UTF-8 token 范围、中文/日文名称、数字原文、负号、字符串转义与错误、算术优先级和结合性、成员/调用链、函数与绑定、上下文名称、完整输入、非法/未支持语法以及 CRLF 和 EOF 诊断位置。 -[ast.rs](ast.rs) 覆盖根模式、声明/语句结构、生成 Visitor 与错误退出、Spanned 位置、带 schema 的 JSON 往返及错误输入;同时核对编辑器产物标识,并验证裸名称赋值的独立节点、Unicode 范围、JSON 往返和不支持的赋值目标。 +[ast.rs](ast.rs) 覆盖根模式、声明/语句结构、生成 Visitor 与错误退出、Spanned 位置、带 schema 的 JSON 往返及错误输入;同时核对编辑器产物标识,并验证裸名称赋值的独立节点、Unicode 范围、JSON 往返和属性赋值目标和不支持的调用结果赋值目标。 [lexical.rs](lexical.rs) 专门覆盖字符 PEG 迁移后的原文位置换算、首个错误及详细诊断、Unicode 非法转义、未闭合字符串、注释/除法/成员/数字/字符串边界和空输入/尾随注释。 diff --git a/crates/frontend/tests/ast.rs b/crates/frontend/tests/ast.rs index 8eb7a6f..816e2e9 100644 --- a/crates/frontend/tests/ast.rs +++ b/crates/frontend/tests/ast.rs @@ -4,6 +4,35 @@ use frontend::ast::{ }; use frontend::{EndorReader, ParseMode, Span, parse_ast, parse_source_ast}; +#[test] +fn function_bodies_accept_method_statements_and_optional_trailing_return() { + let root = parse_ast( + "function click(): Unit { title.setText(\"changed\") card.setColor(\"#FFEEDD\") }", + ParseMode::Module, + ) + .unwrap(); + let AstRoot::Module(program) = root else { + panic!("expected module") + }; + let function = program.functions().next().unwrap(); + assert_eq!(function.body.statements.len(), 2); + assert!( + function + .body + .statements + .iter() + .all(|s| matches!(s.kind, StmtKind::Expression(_))) + ); + assert!(parse_ast("function empty(): Unit {}", ParseMode::Module).is_ok()); + assert!( + parse_ast( + "function invalid(): Unit { return value other() }", + ParseMode::Module + ) + .is_err() + ); +} + #[test] fn parse_modes_produce_distinct_roots_and_preserve_source_diagnostics() { let source = "function echo(value: String): String { let copy = value return copy }"; @@ -198,5 +227,13 @@ fn bare_assignment_has_its_own_ast_and_unicode_source_ranges() { let json = document.to_json().unwrap(); assert_eq!(AstDocument::from_json(&json).unwrap(), document); assert!(parse_ast("value = 1 = 2", ParseMode::Interactive).is_err()); - assert!(parse_ast("object.field = 1", ParseMode::Interactive).is_err()); + let member = parse_ast("object.field = 1", ParseMode::Interactive).unwrap(); + let AstRoot::Interactive(member) = member else { + panic!("interactive") + }; + assert!(matches!( + &member.statements[0].kind, + StmtKind::MemberAssignment(_) + )); + assert!(parse_ast("object() = 1", ParseMode::Interactive).is_err()); } diff --git a/crates/frontend/tests/syntax.rs b/crates/frontend/tests/syntax.rs index 2c0a540..668e478 100644 --- a/crates/frontend/tests/syntax.rs +++ b/crates/frontend/tests/syntax.rs @@ -4,7 +4,9 @@ use frontend::ast::{ use frontend::{DiagnosticKind, Span, TokenKind, lex, parse, parse_expression}; fn function(program: &Program, index: usize) -> &Function { - let DeclKind::Function(function) = &program.declarations[index].kind; + let DeclKind::Function(function) = &program.declarations[index].kind else { + panic!("expected function") + }; function } @@ -258,14 +260,11 @@ fn parser_requires_complete_input_and_rejects_unimplemented_or_malformed_syntax( "// empty", "let x = 1", "use atelier:shop.data", - "class Product {}", "agent Worker {}", "function f() { return 1 }", "function f(x): Number { return x }", "function f(x: Number,): Number { return x }", - "function f(): Number {}", "function f(): Number { return 1 let x = 2 }", - "function f(): Number { let x = 1 }", "function f(): Number { return 1 } extra", "function f(): Number { function g(): Number { return 1 } return 2 }", ] { diff --git a/crates/objects/README.md b/crates/objects/README.md index 33bd769..003eaf1 100644 --- a/crates/objects/README.md +++ b/crates/objects/README.md @@ -1,6 +1,6 @@ # objects -Endor 核心对象与对象存储包,零外部依赖,不依赖 core。包含 EndorObject、ObjectPayload、Value、ObjectRef、Number、字符串/字典/Cell/函数/异常载荷,以及代码对象与 wordcode 数据定义、编解码和静态验证。没有 Python type/元类或语言根类。 +Endor 核心对象与对象存储包,零外部依赖,不依赖 core。包含 EndorObject、ObjectPayload、Value、ObjectRef、Number、字符串/字典/Cell/函数/异常载荷,以及代码对象与 wordcode 数据定义、编解码和静态验证。提供显式引导的 Object/Type 名义类型,已接入普通源码 class 和继承方法分派,trait、原生分配继承与完整元类行为仍待实现。 | 内容 | 职责 | | --- | --- | @@ -8,7 +8,7 @@ Endor 核心对象与对象存储包,零外部依赖,不依赖 core。包含 | [src](src/README.md) | 对象定义、私有堆、ObjectStore、对象工厂、字节码数据与共享错误 | | [tests](tests/README.md) | 不依赖 VM 的对象存储、事务失败、回收和共享代码检查 | -ObjectStore 独立拥有空堆,负责对象分配、代际校验、引用计数、循环回收、属性与内置容器操作;底层 Heap、句柄字段和可变载荷替换仍为私有实现。代码对象只保存及验证执行数据,求值循环与调用帧属于 [core](../core/README.md)。共享 VmError 定义在本包,保留现有名称以避免对象操作接口依赖 VM。 +ObjectStore 独立拥有空堆,负责对象分配、代际校验、引用计数、循环回收、属性与内置容器操作;底层 Heap 与句柄字段保持私有。replace_payload 通过公开的受校验事务替换载荷及所有托管边;NativeContext 提供受限原生分配与变更能力。代码对象只保存及验证执行数据,求值循环与调用帧属于 [core](../core/README.md)。共享 VmError 定义在本包,保留现有名称以避免对象操作接口依赖 VM。 新代码可以直接从 objects 引用对象类型;endor_core 继续重导出现有类型,Vm 的对象方法委托给本包,原有宿主入口保持可用。复制句柄不增加引用;独立 ObjectStore 的对象句柄不能交给另一存储或 VM 使用。 diff --git a/crates/objects/src/README.md b/crates/objects/src/README.md index ed39b9e..954cce2 100644 --- a/crates/objects/src/README.md +++ b/crates/objects/src/README.md @@ -6,6 +6,9 @@ | --- | --- | | [lib.rs](lib.rs) | 包边界、对象类型/ObjectStore 公开导出与独立存储 rustdoc 示例 | | [object.rs](object.rs) | Value、CompareOp、ObjectPayload、ObjectHeader、EndorObject、Record、NativeFunction | +| [types.rs](types.rs) | 托管名义类型、显式 Object/Type 引导、继承查询、受管方法表/接收者绑定、类字段与原生构造委托 | +| [native.rs](native.rs) | NativeContext、上下文原生函数、作用域根与非递归委托调用 | +| [changes.rs](changes.rs) | 合并变更批次、变更键与有界通知额度 | | [number.rs](number.rs) | Integer/Decimal/Rational/BinaryFloat/Complex 的无损表示和装箱载荷 | | [builtins.rs](builtins.rs) | String、Dict、Cell、Function、BaseException 载荷与 trace 边 | | [heap.rs](heap.rs) | 私有 Heap、不可伪造 ObjectRef、稳定 Box、引用事务、迭代释放、循环回收及私有边界测试 | diff --git a/crates/objects/src/changes.rs b/crates/objects/src/changes.rs new file mode 100644 index 0000000..a48d5d8 --- /dev/null +++ b/crates/objects/src/changes.rs @@ -0,0 +1,26 @@ +use crate::ObjectRef; + +/// A precise field change or a replacement of an object's native contents. +/// Entries are notifications, never roots; a source can be dead when consumed. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum ChangeKey { + Attribute(String), + Contents, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ObjectChange { + pub object: ObjectRef, + pub key: ChangeKey, + pub version: u64, +} + +/// Coalesced changes since the previous drain. Overflow requires the consumer +/// to rescan its dependencies, rather than silently missing notifications. +#[derive(Debug, Default)] +pub struct ChangeBatch { + pub changes: Vec, + pub rescan_required: bool, +} + +pub(crate) const MAX_PENDING_CHANGES: usize = 4096; diff --git a/crates/objects/src/code.rs b/crates/objects/src/code.rs index cef74e1..7a14c84 100644 --- a/crates/objects/src/code.rs +++ b/crates/objects/src/code.rs @@ -344,6 +344,7 @@ fn verify( let consumed = defaults + closure + 1; (consumed, consumed, 1) } + Opcode::BuildClass => (4, 4, 1), Opcode::ReturnValue => (1, 1, 0), _ => (0, 0, 0), }; diff --git a/crates/objects/src/heap.rs b/crates/objects/src/heap.rs index f16270e..5415499 100644 --- a/crates/objects/src/heap.rs +++ b/crates/objects/src/heap.rs @@ -2,6 +2,8 @@ use std::collections::BTreeMap; use std::fmt; use std::sync::atomic::{AtomicU64, Ordering}; +use crate::changes::MAX_PENDING_CHANGES; +use crate::{ChangeBatch, ChangeKey, ObjectChange}; use crate::{EndorObject, ObjectHeader, ObjectPayload, Value, VmError}; static NEXT_HEAP_ID: AtomicU64 = AtomicU64::new(1); @@ -21,6 +23,11 @@ impl fmt::Display for ObjectRef { } } +/// A non-owning reference. Only the originating store can upgrade it; slot +/// reuse never makes a dead weak reference point at a different object. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WeakObjectRef(pub(crate) ObjectRef); + #[derive(Debug)] struct Slot { generation: u64, @@ -33,6 +40,8 @@ pub(crate) struct Heap { slots: Vec, free: Vec, live: usize, + changes: BTreeMap<(ObjectRef, ChangeKey), u64>, + changes_overflow: bool, } impl Heap { @@ -45,6 +54,8 @@ impl Heap { slots: Vec::new(), free: Vec::new(), live: 0, + changes: BTreeMap::new(), + changes_overflow: false, }) } @@ -72,6 +83,8 @@ impl Heap { identity: reference, strong_refs: 1, external_refs: 1, + change_version: 0, + type_object: None, }, payload, attributes: BTreeMap::new(), @@ -92,6 +105,27 @@ impl Heap { .ok_or(VmError::InvalidHandle) } + pub(crate) fn attach_type( + &mut self, + reference: ObjectRef, + type_object: ObjectRef, + ) -> Result<(), VmError> { + if self.get(reference)?.header.type_object.is_some() { + return Err(VmError::InvalidArgument("object already has a type")); + } + if self.get(reference)?.attributes.contains_key("type") { + return Err(VmError::InvalidArgument( + "type attribute conflicts with managed type", + )); + } + let increments = self.prepare_increments(&[type_object])?; + self.prepare_change(reference)?; + self.apply_increments(increments); + self.get_mut(reference)?.header.type_object = Some(type_object); + self.record_change(reference, ChangeKey::Contents); + Ok(()) + } + /// Transactional copy-on-update for built-in containers. The EndorObject /// box and identity stay stable; only its payload and edge snapshot change. /// All new edges are validated/retained before any old edge is released. @@ -104,10 +138,12 @@ impl Heap { let mut edges = Vec::new(); payload.trace(&mut |target| edges.push(target)); let increments = self.prepare_increments(&edges)?; + self.prepare_change(reference)?; self.apply_increments(increments); let object = self.get_mut(reference)?; let old_edges = std::mem::replace(&mut object.payload_edges, edges); let old_payload = std::mem::replace(&mut object.payload, Box::new(payload)); + self.record_change(reference, ChangeKey::Contents); self.release_edges(old_edges); drop(old_payload); Ok(()) @@ -194,8 +230,13 @@ impl Heap { _ => Vec::new(), }; let increments = self.prepare_increments(&edges)?; + self.prepare_change(reference)?; self.apply_increments(increments); - let old = self.get_mut(reference)?.attributes.insert(name, value); + let old = self + .get_mut(reference)? + .attributes + .insert(name.clone(), value); + self.record_change(reference, ChangeKey::Attribute(name)); if let Some(Value::Object(target)) = old { self.release_edges(vec![target]); } @@ -213,17 +254,58 @@ impl Heap { pub(crate) fn del_attr(&mut self, reference: ObjectRef, name: &str) -> Result<(), VmError> { self.check_attributes(reference)?; + self.prepare_change(reference)?; let old = self .get_mut(reference)? .attributes .remove(name) .ok_or_else(|| VmError::AttributeNotFound(name.into()))?; + self.record_change(reference, ChangeKey::Attribute(name.into())); if let Value::Object(target) = old { self.release_edges(vec![target]); } Ok(()) } + pub(crate) fn prepare_change(&self, reference: ObjectRef) -> Result<(), VmError> { + self.get(reference)? + .header + .change_version + .checked_add(1) + .ok_or(VmError::ExecutionLimit("object change version"))?; + Ok(()) + } + + pub(crate) fn record_change(&mut self, reference: ObjectRef, key: ChangeKey) { + let object = self.get_mut(reference).expect("validated change target"); + object.header.change_version += 1; + let version = object.header.change_version; + if self.changes_overflow { + return; + } + let entry = (reference, key); + if self.changes.len() >= MAX_PENDING_CHANGES && !self.changes.contains_key(&entry) { + self.changes.clear(); + self.changes_overflow = true; + } else { + self.changes.insert(entry, version); + } + } + + pub(crate) fn take_changes(&mut self) -> ChangeBatch { + ChangeBatch { + changes: std::mem::take(&mut self.changes) + .into_iter() + .map(|((object, key), version)| ObjectChange { + object, + key, + version, + }) + .collect(), + rescan_required: std::mem::take(&mut self.changes_overflow), + } + } + fn check_attributes(&self, reference: ObjectRef) -> Result<(), VmError> { if !self.get(reference)?.payload.allows_attributes() { return Err(VmError::UnsupportedOperation("attributes")); diff --git a/crates/objects/src/instruction.rs b/crates/objects/src/instruction.rs index 35b3aad..157bb1c 100644 --- a/crates/objects/src/instruction.rs +++ b/crates/objects/src/instruction.rs @@ -34,6 +34,7 @@ pub enum Opcode { StoreAttr = 26, BuildMap = 27, MakeFunction = 28, + BuildClass = 29, ExtendedArg = 255, } @@ -69,6 +70,7 @@ impl Opcode { 26 => Self::StoreAttr, 27 => Self::BuildMap, 28 => Self::MakeFunction, + 29 => Self::BuildClass, 255 => Self::ExtendedArg, _ => return None, }) @@ -83,6 +85,7 @@ impl Opcode { | Self::ReturnValue | Self::ToBool | Self::UnaryNot + | Self::BuildClass ) } } diff --git a/crates/objects/src/lib.rs b/crates/objects/src/lib.rs index cc16e9c..e70b9f3 100644 --- a/crates/objects/src/lib.rs +++ b/crates/objects/src/lib.rs @@ -14,23 +14,28 @@ //! ``` mod builtins; +mod changes; mod code; mod error; mod heap; mod instruction; +mod native; mod number; mod object; mod store; mod store_builtins; +mod types; pub use builtins::{ EndorBaseExceptionObject, EndorCellObject, EndorDictObject, EndorFunctionObject, EndorStringObject, EndorTracebackEntry, }; +pub use changes::{ChangeBatch, ChangeKey, ObjectChange}; pub use code::{CodeLayout, EndorCode, EndorCodeObject}; pub use error::VmError; -pub use heap::ObjectRef; +pub use heap::{ObjectRef, WeakObjectRef}; pub use instruction::{DecodedInstruction, Instruction, Opcode}; +pub use native::{ContextNativeFunction, NativeCallable, NativeContext, NativeResult}; pub use number::{ EndorDecimal, EndorInteger, EndorNumber, EndorNumberObject, EndorRational, EndorReal, }; @@ -39,3 +44,4 @@ pub use object::{ ObjectHeader, ObjectPayload, Record, Value, }; pub use store::ObjectStore; +pub use types::{ClassField, EndorTypeObject, RootTypes, TypeConstructor}; diff --git a/crates/objects/src/native.rs b/crates/objects/src/native.rs new file mode 100644 index 0000000..13c87a7 --- /dev/null +++ b/crates/objects/src/native.rs @@ -0,0 +1,206 @@ +use std::{fmt, sync::Arc}; + +use crate::{ObjectPayload, ObjectRef, ObjectStore, Value, VmError, WeakObjectRef}; + +/// Borrowed results produced inside a native call. Invoke delegates to another +/// callable through the VM's existing frame loop, without recursively entering it. +#[derive(Debug)] +pub enum NativeResult { + Return(Value), + Invoke { + function: ObjectRef, + arguments: Vec, + }, +} + +pub trait NativeCallable: fmt::Debug + Send + Sync + 'static { + fn invoke( + &self, + context: &mut NativeContext<'_>, + arguments: &[Value], + ) -> Result; +} + +/// Native allocation/mutation scope. Allocations and upgraded weak references +/// are temporary owned roots. Return/Invoke promotes only its output roots; +/// Drop releases everything else, including on error or unwinding. +pub struct NativeContext<'a> { + store: &'a mut ObjectStore, + roots: Vec, + callee: ObjectRef, +} + +impl<'a> NativeContext<'a> { + /// Callable currently dispatched by the VM, borrowed for this scope. + pub fn callee(&self) -> ObjectRef { + self.callee + } + + pub fn attach_type( + &mut self, + object: ObjectRef, + type_object: ObjectRef, + ) -> Result<(), VmError> { + self.store.attach_type(object, type_object) + } + + pub fn store(&self) -> &ObjectStore { + self.store + } + + pub fn allocate(&mut self, payload: impl ObjectPayload) -> Result { + let object = self.store.allocate(payload)?; + self.roots.push(object); + Ok(object) + } + + pub fn new_object(&mut self) -> Result { + self.allocate(crate::Record) + } + + pub fn new_dict(&mut self) -> Result { + let object = self.store.new_dict()?; + self.roots.push(object); + Ok(object) + } + + pub fn new_cell(&mut self, value: Option) -> Result { + let object = self.store.new_cell(value)?; + self.roots.push(object); + Ok(object) + } + + pub fn set_attr( + &mut self, + object: ObjectRef, + name: impl Into, + value: Value, + ) -> Result<(), VmError> { + self.store.set_attr(object, name, value) + } + + pub fn del_attr(&mut self, object: ObjectRef, name: &str) -> Result<(), VmError> { + self.store.del_attr(object, name) + } + + pub fn dict_set(&mut self, object: ObjectRef, key: Value, value: Value) -> Result<(), VmError> { + self.store.dict_set(object, key, value) + } + + pub fn dict_delete(&mut self, object: ObjectRef, key: &Value) -> Result { + self.store.dict_delete(object, key) + } + + pub fn cell_set(&mut self, object: ObjectRef, value: Value) -> Result<(), VmError> { + self.store.cell_set(object, value) + } + + pub fn replace_payload( + &mut self, + object: ObjectRef, + payload: impl ObjectPayload, + ) -> Result<(), VmError> { + self.store.replace_payload(object, payload) + } + + pub fn upgrade(&mut self, weak: WeakObjectRef) -> Result, VmError> { + let object = self.store.upgrade(weak)?; + if let Some(object) = object { + self.roots.push(object); + } + Ok(object) + } + + fn protect(&mut self, value: &Value) -> Result<(), VmError> { + if let Value::Object(object) = value { + self.store.retain(*object)?; + self.roots.push(*object); + } + Ok(()) + } + + /// Internal execution bridge. Every returned object reference owns a root; + /// consumers must release Return values or all Invoke arguments/function. + pub fn execute( + store: &'a mut ObjectStore, + function: ObjectRef, + arguments: &[Value], + callable: Arc, + ) -> Result { + let mut context = Self { + store, + roots: Vec::new(), + callee: function, + }; + context.protect(&Value::Object(function))?; + for argument in arguments { + context.protect(argument)?; + } + let result = callable.invoke(&mut context, arguments)?; + let transferred_from = context.roots.len(); + match &result { + NativeResult::Return(value) => context.protect(value)?, + NativeResult::Invoke { + function, + arguments, + } => { + context.protect(&Value::Object(*function))?; + for argument in arguments { + context.protect(argument)?; + } + } + } + // Transfer exactly the output roots; all other roots remain scoped. + context.roots.truncate(transferred_from); + Ok(result) + } +} + +impl Drop for NativeContext<'_> { + fn drop(&mut self) { + for root in self.roots.drain(..).rev() { + self.store + .release(root) + .expect("native scope owns its roots"); + } + } +} + +/// A context-aware function with explicit, immutable captured object edges. +#[derive(Debug)] +pub struct ContextNativeFunction { + name: &'static str, + callable: Arc, + captures: Vec, +} + +impl ContextNativeFunction { + pub fn new( + name: &'static str, + callable: impl NativeCallable, + captures: Vec, + ) -> Self { + Self { + name, + callable: Arc::new(callable), + captures, + } + } +} + +impl ObjectPayload for ContextNativeFunction { + fn as_any(&self) -> &dyn std::any::Any { + self + } + fn kind_name(&self) -> &'static str { + self.name + } + fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) { + for capture in &self.captures { + visit(*capture); + } + } + fn native_callable(&self) -> Option> { + Some(self.callable.clone()) + } +} diff --git a/crates/objects/src/object.rs b/crates/objects/src/object.rs index 35a9b91..88b48f3 100644 --- a/crates/objects/src/object.rs +++ b/crates/objects/src/object.rs @@ -51,6 +51,19 @@ impl CompareOp { /// Rust Drop releases host resources when the payload is destroyed. /// Equal payloads must provide compatible hashes, or reject hashing explicitly. pub trait ObjectPayload: Any + fmt::Debug + Send + Sync { + /// Shared-reference facades may expose the current referent's type. The + /// referent must also be held by traced managed edges. Ordinary objects + /// keep their immutable header type; this does not change inheritance. + fn type_target(&self, _store: &crate::ObjectStore) -> Result, VmError> { + Ok(None) + } + + /// Optional context-aware native entry. The VM clones this dispatcher before + /// borrowing the store mutably; callbacks never borrow a live payload. + fn native_callable(&self) -> Option> { + None + } + fn as_any(&self) -> &dyn Any; /// A diagnostic label only, with no language type or inheritance semantics. @@ -104,6 +117,18 @@ pub trait ObjectPayload: Any + fmt::Debug + Send + Sync { fn assign_from(&self, _source: &dyn ObjectPayload) -> Result { Ok(false) } + + /// Optional managed shared-reference assignment. Return a built-in Cell + /// owned by this payload after validating the source and any domain cycles. + /// The store writes the source object to the Cell transactionally. This + /// keeps mutable edges inside the GC graph, unlike native assign_from. + fn assignment_cell( + &self, + _source: ObjectRef, + _store: &crate::ObjectStore, + ) -> Result, VmError> { + Ok(None) + } } #[derive(Debug, Default)] @@ -166,15 +191,25 @@ impl ObjectPayload for EndorNativeFunctionObject { } } -/// Common runtime state. There is deliberately no language type pointer. +/// Common runtime state. Embedders may attach an immutable managed type. #[derive(Debug)] pub struct ObjectHeader { pub(crate) identity: ObjectRef, pub(crate) strong_refs: usize, pub(crate) external_refs: usize, + pub(crate) change_version: u64, + pub(crate) type_object: Option, } impl ObjectHeader { + pub fn type_object(&self) -> Option { + self.type_object + } + + pub fn change_version(&self) -> u64 { + self.change_version + } + pub fn identity(&self) -> ObjectRef { self.identity } @@ -190,7 +225,7 @@ impl ObjectHeader { } /// Runtime object foundation, analogous in role to CPython's common object -/// structure, without its type system or binary layout. The heap boxes this +/// structure, without its binary layout. The heap boxes this /// structure so its address remains stable throughout its lifetime. #[derive(Debug)] pub struct EndorObject { @@ -213,6 +248,9 @@ impl EndorObject { /// allocation and refreshed on VM-managed payload replacement; attributes /// are maintained by the VM's reference accounting. pub fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) { + if let Some(reference) = self.header.type_object { + visit(reference); + } for reference in &self.payload_edges { visit(*reference); } diff --git a/crates/objects/src/store.rs b/crates/objects/src/store.rs index 39b0b63..485fb62 100644 --- a/crates/objects/src/store.rs +++ b/crates/objects/src/store.rs @@ -1,5 +1,8 @@ use crate::heap::Heap; -use crate::{CompareOp, EndorObject, ObjectPayload, ObjectRef, Record, Value, VmError}; +use crate::{ + ChangeBatch, ChangeKey, CompareOp, EndorObject, ObjectPayload, ObjectRef, Record, Value, + VmError, WeakObjectRef, +}; /// Object storage independent of the VM evaluator. Construction creates an empty /// heap; dropping it releases all objects and their host resources. Mutable @@ -10,6 +13,80 @@ pub struct ObjectStore { } impl ObjectStore { + pub fn downgrade(&self, reference: ObjectRef) -> Result { + self.object(reference)?; + Ok(WeakObjectRef(reference)) + } + + /// Returns a new owned root, or None when the original object was released. + /// A foreign store is an error, not an expired reference. + pub fn upgrade(&mut self, weak: WeakObjectRef) -> Result, VmError> { + match self.object(weak.0) { + Ok(_) => { + self.retain(weak.0)?; + Ok(Some(weak.0)) + } + Err(VmError::InvalidHandle) => Ok(None), + Err(error) => Err(error), + } + } + + pub fn take_changes(&mut self) -> ChangeBatch { + self.heap.take_changes() + } + + pub fn change_version(&self, reference: ObjectRef) -> Result { + Ok(self.object(reference)?.header().change_version()) + } + + /// Replace a host payload transactionally, including all owned GC edges. + pub fn replace_payload( + &mut self, + reference: ObjectRef, + payload: impl ObjectPayload, + ) -> Result<(), VmError> { + if self + .object(reference)? + .payload() + .as_any() + .is::() + { + return Err(VmError::UnsupportedOperation( + "replace immutable type definition", + )); + } + self.validate_builtin(&payload)?; + self.heap.replace_payload(reference, payload) + } + + pub fn assign_from(&mut self, target: ObjectRef, source: ObjectRef) -> Result { + self.heap.prepare_change(target)?; + self.object(source)?; + if let Some(cell) = self + .object(target)? + .payload() + .assignment_cell(source, self)? + { + // Replacing the Cell edge may otherwise reclaim a target which is + // reachable only through the edge being removed. + self.retain(target)?; + let result = self.cell_set(cell, Value::Object(source)); + if result.is_ok() { + self.heap.record_change(target, ChangeKey::Contents); + } + self.release(target)?; + return result.map(|()| true); + } + let changed = self + .object(target)? + .payload() + .assign_from(self.object(source)?.payload())?; + if changed { + self.heap.record_change(target, ChangeKey::Contents); + } + Ok(changed) + } + pub fn new() -> Result { Ok(Self { heap: Heap::new()? }) } @@ -91,7 +168,11 @@ impl ObjectStore { /// Reads a borrowed value. An Object result needs retain() if the host /// wants it to survive removal of the attribute that currently owns it. pub fn get_attr(&self, reference: ObjectRef, name: &str) -> Result { - self.heap.get_attr(reference, name) + self.lookup_attr(reference, name) + .map(|attribute| match attribute { + crate::types::AttributeLookup::Value(value) => value, + crate::types::AttributeLookup::Method { function, .. } => Value::Object(function), + }) } /// Object-valued attributes own internal strong references independently @@ -102,10 +183,17 @@ impl ObjectStore { name: impl Into, value: Value, ) -> Result<(), VmError> { - self.heap.set_attr(reference, name.into(), value) + let name = name.into(); + if name == "type" && self.type_of(reference)?.is_some() { + return Err(VmError::UnsupportedOperation("write object type")); + } + self.heap.set_attr(reference, name, value) } pub fn del_attr(&mut self, reference: ObjectRef, name: &str) -> Result<(), VmError> { + if name == "type" && self.type_of(reference)?.is_some() { + return Err(VmError::UnsupportedOperation("delete object type")); + } self.heap.del_attr(reference, name) } diff --git a/crates/objects/src/types.rs b/crates/objects/src/types.rs new file mode 100644 index 0000000..d40155b --- /dev/null +++ b/crates/objects/src/types.rs @@ -0,0 +1,474 @@ +//! Nominal type identities in the same managed heap as their instances. +use std::{collections::BTreeMap, sync::Arc}; + +use crate::{ + NativeCallable, NativeContext, NativeResult, ObjectPayload, ObjectRef, ObjectStore, Value, + VmError, +}; + +#[derive(Debug, Clone, Copy)] +pub enum TypeConstructor { + /// Allocate a plain, attribute-bearing instance of this type. + Record, + /// Delegate to a managed callable. It must return an instance of this type. + Native(ObjectRef), + Abstract, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClassField { + pub name: String, + /// Preserved source annotation, like current function annotations. + pub type_name: String, +} + +/// Nominal definition with immutable identity/base and a managed method table. +/// Its base, constructor and metatype are traced +/// edges; no registry outside the heap keeps unused definitions alive. +#[derive(Debug)] +pub struct EndorTypeObject { + name: String, + base: Option, + constructor: TypeConstructor, + methods: BTreeMap, + fields: Vec, +} + +impl EndorTypeObject { + pub fn name(&self) -> &str { + &self.name + } + pub fn base(&self) -> Option { + self.base + } +} + +impl ObjectPayload for EndorTypeObject { + fn as_any(&self) -> &dyn std::any::Any { + self + } + fn kind_name(&self) -> &'static str { + "type" + } + fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) { + if let Some(base) = self.base { + visit(base); + } + if let TypeConstructor::Native(function) = self.constructor { + visit(function); + } + for method in self.methods.values() { + visit(*method); + } + } + fn repr(&self, _: ObjectRef) -> Result { + Ok(format!("", self.name)) + } + fn native_callable(&self) -> Option> { + Some(Arc::new(Construct)) + } +} + +#[derive(Debug)] +struct Construct; +impl NativeCallable for Construct { + fn invoke( + &self, + cx: &mut NativeContext<'_>, + arguments: &[Value], + ) -> Result { + let type_object = cx.callee(); + let definition = cx.store().payload::(type_object, "Type")?; + match definition.constructor { + TypeConstructor::Native(function) => Ok(NativeResult::Invoke { + function, + arguments: arguments.to_vec(), + }), + TypeConstructor::Abstract => { + Err(VmError::UnsupportedOperation("abstract type construction")) + } + TypeConstructor::Record => { + if arguments.len() != definition.fields.len() { + return Err(VmError::ArgumentCount { + name: definition.name.clone(), + required: definition.fields.len(), + maximum: definition.fields.len(), + provided: arguments.len(), + }); + } + let fields = definition.fields.clone(); + let instance = cx.new_object()?; + cx.attach_type(instance, type_object)?; + for (field, value) in fields.iter().zip(arguments) { + cx.set_attr(instance, &field.name, value.clone())?; + } + Ok(NativeResult::Return(Value::Object(instance))) + } + } + } +} + +/// Each field owns one external root; the caller must release both. +#[derive(Debug, Clone, Copy)] +pub struct RootTypes { + pub object: ObjectRef, + pub type_type: ObjectRef, +} + +impl ObjectStore { + /// Explicit bootstrap preserves the empty-store contract. Inheritance: + /// Type -> Object; instance relations: Object : Type and Type : Type. + pub fn bootstrap_types(&mut self) -> Result { + let object = self.allocate(EndorTypeObject { + name: "Object".into(), + base: None, + constructor: TypeConstructor::Record, + methods: BTreeMap::new(), + fields: Vec::new(), + })?; + let mut allocated_type = None; + let result = (|| { + let type_type = self.allocate(EndorTypeObject { + name: "Type".into(), + base: Some(object), + constructor: TypeConstructor::Abstract, + methods: BTreeMap::new(), + fields: Vec::new(), + })?; + allocated_type = Some(type_type); + self.attach_type(object, type_type)?; + self.attach_type(type_type, type_type)?; + Ok(RootTypes { object, type_type }) + })(); + if result.is_err() { + if let Some(type_type) = allocated_type { + self.release(type_type)?; + } + self.release(object)?; + self.collect_cycles(); + } + result + } + + /// Base links are fixed at allocation, preventing inheritance cycles. + pub fn new_type( + &mut self, + name: impl Into, + base: ObjectRef, + constructor: TypeConstructor, + ) -> Result { + self.payload::(base, "Type")?; + let metatype = self + .type_of(base)? + .ok_or(VmError::InvalidArgument("base type has no metatype"))?; + let name = name.into(); + if name.is_empty() { + return Err(VmError::InvalidArgument("type name must not be empty")); + } + let definition = self.allocate(EndorTypeObject { + name, + base: Some(base), + constructor, + methods: BTreeMap::new(), + fields: Vec::new(), + })?; + if let Err(error) = self.attach_type(definition, metatype) { + self.release(definition)?; + return Err(error); + } + Ok(definition) + } + + pub fn attach_type( + &mut self, + object: ObjectRef, + type_object: ObjectRef, + ) -> Result<(), VmError> { + self.payload::(type_object, "Type")?; + self.heap.attach_type(object, type_object) + } + + /// Borrowed type reference; legacy/untyped host objects return None. + pub fn type_of(&self, object: ObjectRef) -> Result, VmError> { + let mut current = object; + let mut visited = std::collections::BTreeSet::new(); + loop { + if !visited.insert(current) { + return Err(VmError::InvalidArgument( + "cyclic shared-reference type target", + )); + } + let instance = self.object(current)?; + match instance.payload().type_target(self)? { + Some(target) => current = target, + None => return Ok(instance.header().type_object()), + } + } + } + + pub fn is_subtype(&self, candidate: ObjectRef, expected: ObjectRef) -> Result { + self.payload::(expected, "Type")?; + let mut current = Some(candidate); + while let Some(type_object) = current { + let definition = self.payload::(type_object, "Type")?; + if type_object == expected { + return Ok(true); + } + current = definition.base; + } + Ok(false) + } + + pub fn is_instance(&self, object: ObjectRef, expected: ObjectRef) -> Result { + self.payload::(expected, "Type")?; + match self.type_of(object)? { + Some(type_object) => self.is_subtype(type_object, expected), + None => Ok(false), + } + } + + /// Updating the managed method table preserves nominal identity and base. + pub fn define_method( + &mut self, + type_object: ObjectRef, + name: impl Into, + function: ObjectRef, + ) -> Result<(), VmError> { + self.object(function)?; + let name = name.into(); + if name.is_empty() || name == "type" { + return Err(VmError::InvalidArgument("reserved or empty method name")); + } + let definition = self.payload::(type_object, "Type")?; + if definition.fields.iter().any(|field| field.name == name) { + return Err(VmError::InvalidArgument( + "method conflicts with class field", + )); + } + let mut methods = definition.methods.clone(); + methods.insert(name, function); + let updated = EndorTypeObject { + name: definition.name.clone(), + base: definition.base, + constructor: definition.constructor, + methods, + fields: definition.fields.clone(), + }; + self.heap.replace_payload(type_object, updated) + } + + /// Atomically build a source class, including inherited constructor fields. + /// Native allocation needs its own protocol; never forge a Record payload + /// claiming to be a native visual instance. + pub fn new_class( + &mut self, + name: impl Into, + base: ObjectRef, + own_fields: Vec, + methods: BTreeMap, + ) -> Result { + let definition = self.payload::(base, "Type")?; + if !matches!(definition.constructor, TypeConstructor::Record) { + return Err(VmError::UnsupportedOperation( + "source subclass of native or abstract allocator", + )); + } + let mut fields = definition.fields.clone(); + for field in own_fields { + if field.name.is_empty() + || field.name == "type" + || fields.iter().any(|old| old.name == field.name) + || self.lookup_method(base, &field.name)?.is_some() + { + return Err(VmError::InvalidArgument( + "duplicate or reserved class field", + )); + } + fields.push(field); + } + for (name, method) in &methods { + self.object(*method)?; + if name.is_empty() || name == "type" || fields.iter().any(|field| field.name == *name) { + return Err(VmError::InvalidArgument( + "method conflicts with class field", + )); + } + } + let metatype = self + .type_of(base)? + .ok_or(VmError::InvalidArgument("base type has no metatype"))?; + let name = name.into(); + if name.is_empty() { + return Err(VmError::InvalidArgument("type name must not be empty")); + } + let class = self.allocate(EndorTypeObject { + name, + base: Some(base), + constructor: TypeConstructor::Record, + methods, + fields, + })?; + if let Err(error) = self.attach_type(class, metatype) { + self.release(class)?; + return Err(error); + } + Ok(class) + } + + pub fn lookup_method( + &self, + type_object: ObjectRef, + name: &str, + ) -> Result, VmError> { + let mut current = Some(type_object); + while let Some(type_object) = current { + let definition = self.payload::(type_object, "Type")?; + if let Some(function) = definition.methods.get(name) { + return Ok(Some(*function)); + } + current = definition.base; + } + Ok(None) + } + + /// Source positional parameters after receiver binding. None means the + /// callable uses a native protocol without a source signature. + pub fn positional_parameter_count( + &self, + function: ObjectRef, + ) -> Result, 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::() { + receivers = receivers + .checked_add(1) + .ok_or(VmError::ExecutionLimit("bound receivers"))?; + current = bound.function; + } else if let Some(function) = payload + .as_any() + .downcast_ref::() + { + let count = self + .payload::(function.code(), "code")? + .code() + .parameters() + .len(); + return count + .checked_sub(receivers) + .map(Some) + .ok_or(VmError::InvalidArgument( + "method receiver exceeds parameter count", + )); + } else { + return Ok(None); + } + } + } + + /// Returns one owned root, including for a newly bound inherited method. + pub fn bind_attr(&mut self, receiver: ObjectRef, name: &str) -> Result { + match self.lookup_attr(receiver, name)? { + AttributeLookup::Value(value) => { + if let Value::Object(object) = value { + self.retain(object)?; + } + Ok(value) + } + AttributeLookup::Method { function, receiver } => self + .allocate(BoundMethod { function, receiver }) + .map(Value::Object), + } + } + + pub(crate) fn lookup_attr( + &self, + receiver: ObjectRef, + name: &str, + ) -> Result { + let object = self.object(receiver)?; + if name == "type" + && let Some(type_object) = self.type_of(receiver)? + { + return Ok(AttributeLookup::Value(Value::Object(type_object))); + } + if let Some(definition) = object.payload().as_any().downcast_ref::() { + let metadata = match name { + "name" => Some(Value::Text(definition.name.clone())), + "base" => Some(definition.base.map(Value::Object).unwrap_or(Value::Nil)), + _ => None, + }; + if let Some(value) = metadata { + return Ok(AttributeLookup::Value(value)); + } + if let Some(function) = self.lookup_method(receiver, name)? { + return Ok(AttributeLookup::Value(Value::Object(function))); + } + } + match self.heap.get_attr(receiver, name) { + Ok(value) => return Ok(AttributeLookup::Value(value)), + Err(VmError::AttributeNotFound(_) | VmError::UnsupportedOperation("attributes")) => {} + Err(error) => return Err(error), + } + if let Some(type_object) = self.type_of(receiver)? { + if let Some(function) = self.lookup_method(type_object, name)? { + return Ok(AttributeLookup::Method { function, receiver }); + } + return Err(VmError::AttributeNotFound(name.into())); + } + self.heap + .get_attr(receiver, name) + .map(AttributeLookup::Value) + } +} + +pub(crate) enum AttributeLookup { + Value(Value), + Method { + function: ObjectRef, + receiver: ObjectRef, + }, +} + +#[derive(Debug)] +struct BoundMethod { + receiver: ObjectRef, + function: ObjectRef, +} +impl ObjectPayload for BoundMethod { + fn as_any(&self) -> &dyn std::any::Any { + self + } + fn kind_name(&self) -> &'static str { + "bound-method" + } + fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) { + visit(self.receiver); + visit(self.function); + } + fn native_callable(&self) -> Option> { + Some(Arc::new(CallBoundMethod)) + } +} + +#[derive(Debug)] +struct CallBoundMethod; +impl NativeCallable for CallBoundMethod { + fn invoke( + &self, + cx: &mut NativeContext<'_>, + arguments: &[Value], + ) -> Result { + let bound = cx + .store() + .payload::(cx.callee(), "bound method")?; + let mut values = Vec::with_capacity(arguments.len() + 1); + values.push(Value::Object(bound.receiver)); + values.extend_from_slice(arguments); + Ok(NativeResult::Invoke { + function: bound.function, + arguments: values, + }) + } +} diff --git a/docs/README.md b/docs/README.md index 38fc93d..452cbff 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,6 +5,8 @@ | 引擎与宿主边界 | [架构](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/types.md) | objects/src/types.rs | | Number、String、Dict、Cell、Function | [基本对象](runtime/builtins.md) | objects/src | | 源码读取与来源 | [Reader](runtime/reader.md) | frontend/src/reader.rs | | 词法、语法、裸赋值和诊断 | [Frontend](runtime/frontend.md) | frontend/src/lexer.peg/grammar.peg | diff --git a/docs/architecture/README.md b/docs/architecture/README.md index fdaa33a..4e360bb 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -19,6 +19,6 @@ Objects 拥有对象、稳定 Box、代际句柄、引用事务、私有堆和 宿主以子模块固定 Aster 提交,并通过 Cargo 路径依赖编译到宿主进程中。每个 Vm 拥有独立 ObjectStore。ObjectRef 不跨 VM 或进程使用;EndorFrame 是语言调用帧,不是 GUI 页面。宿主决定线程、进程、输入、显示与执行任务之间的边界。 -引擎从空堆启动,不预设 Python type/元类或语言根类。代码共享不可变对象,函数用显式帧向量执行;对象返回值拥有根引用,借用读取不自动 retain。Number 保留无损表示,数学运算语义尚未实现。现有帧数/指令数限制不构成内存配额或沙箱。 +引擎从空堆启动,Object/Type 名义类型由宿主显式引导;普通源码 class 已接入,完整元类和 trait 尚未接入。代码共享不可变对象,函数用显式帧向量执行;对象返回值拥有根引用,借用读取不自动 retain。Number 保留无损表示,数学运算语义尚未实现。现有帧数/指令数限制不构成内存配额或沙箱。 宿主文件信息和数据库适配现归 [Hestia](https://gitea.entroia.com/Zayden-Jung/endorphin-hestia-server),不保存 VM 堆。原生产服务、ASTER_* 配置、数据库、卷和路径由 Hestia 保持兼容;拆仓本身不迁移或删除这些资源。Aster 不设置 CI/CD;Lyra CI 显式验证固定引擎版本及 CLI,数据库和 HTTP 检查由 Hestia CI 承担。返回 [文档索引](../README.md)。 diff --git a/docs/contributing/handoff.md b/docs/contributing/handoff.md index 4c31fd1..b714453 100644 --- a/docs/contributing/handoff.md +++ b/docs/contributing/handoff.md @@ -10,7 +10,7 @@ Aster 是可嵌入引擎,只保留 objects、core、frontend、compiler。Obje ## 已实现 -- EndorObject、稳定 Box、代际句柄、显式强引用及手动循环回收;VM 从空堆启动,没有 Python type/元类或语言根类。 +- EndorObject、稳定 Box、代际句柄、显式强引用及手动循环回收;VM 从空堆启动,宿主可显式引导托管 Object/Type 和名义继承;普通源码 class、字段构造和继承方法分派已接入,trait 和原生类分配继承仍待完成,见[类型机制](../runtime/types.md)。 - Function、NativeFunction、Dict、Cell、异常数据、UTF-8 String 和无损表示优先的 Number。 - 小型 wordcode、共享代码对象、操作数/控制流/栈校验、显式执行帧及 execute/call;MakeFunction 可在执行中创建带默认参数与共享闭包 Cell 的函数。对象返回值持有根,用后 release_value;借用读取不自动 retain。 - EndorReader 在 lexer 前提供字符串/流/文件/宿主 URI provider 输入,产出不可变 SourceUnit,保存来源与不透明修订标签;带来源的词法/语法错误保留行列,Lyra parse 支持文件和 stdin。没有平台 URI 后端或版本锁实现,见 [阅读器](../runtime/reader.md)。 diff --git a/docs/runtime/README.md b/docs/runtime/README.md index fd55ac0..a402ea5 100644 --- a/docs/runtime/README.md +++ b/docs/runtime/README.md @@ -2,7 +2,9 @@ | 文档 | 内容 | | --- | --- | +| [types.md](types.md) | 托管 Object/Type、名义继承、构造与类型边生命周期 | | [objects.md](objects.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/ast.md b/docs/runtime/ast.md index c366575..8627900 100644 --- a/docs/runtime/ast.md +++ b/docs/runtime/ast.md @@ -30,17 +30,18 @@ Rust 用 struct/enum、Vec/Option 表达结构;生成器在按值依赖图 | 层级 | 当前数据模型 | | --- | --- | | AstRoot | Module(Program)、Expression(Expr)、Interactive(InteractiveInput) | -| Program | declarations: Vec 与 span;当前声明种类为 Function | -| Decl/DeclKind | 声明的位置及其具体种类;Function 保存参数、返回类型名称和 Block | +| Program | declarations: Vec 与 span;声明种类为 Function、Class | +| Decl/DeclKind | 声明的位置及其具体种类;Function 保存参数、返回类型名称和 Block;Class 保存名称、可选基类、字段及方法 | | Block | statements: Vec 与 span,不再用固定的 bindings/result 字段表示 | -| Stmt/StmtKind | Binding、Assignment、Return、Expression、Declaration;每条语句有自身范围 | +| Stmt/StmtKind | Binding、Assignment、MemberAssignment、Return、Expression、Declaration;每条语句有自身范围 | +| MemberAssignment | 对象表达式、属性名称、右侧表达式与原文范围 | | Assignment | 名称目标、右侧表达式及原文范围;独立于 let 的 Binding 节点 | | ReturnStatement | 可选表达式与范围;当前源码中的 return 仍要求表达式 | | Expr/ExprKind | 名称、数字原文、字符串、符号、一元/二元运算、成员和调用 | 参数与返回类型仍为 Name,只保留语法,不实施类型或名称检查。Program::functions() 提供函数迭代便捷接口。旧的 Program.functions、Block.bindings/result/return_span 字段已移除;源码构造和使用方改为声明/语句列表。ExprKind 的既有表达式结构保留。 -AST 数据模型允许一般语句列表,不代表 parser 接受了所有组合。模块仍要求一个或多个函数;函数体接受 let/裸赋值序列后跟一个带值 return。Interactive 模式接受一次函数声明、let 绑定、裸名称赋值或表达式,必须消费完整输入;不创建交互会话,不处理续行提示,不执行语言代码。多条交互提交由宿主分别解析。条件、循环、导入、类等尚未实现,也不加入 FunctionType 这种 Python 专用入口。 +AST 数据模型允许一般语句列表,不代表 parser 接受了所有组合。模块要求一个或多个函数/类声明;函数体接受 let/名称赋值/属性赋值/表达式语句,以及可选的末尾带值 return。Interactive 模式接受一次函数/类声明、let 绑定、名称/属性赋值或表达式,必须消费完整输入;不创建交互会话,不处理续行提示,不执行语言代码。多条交互提交由宿主分别解析。条件、循环、导入、trait 等尚未实现,也不加入 FunctionType 这种 Python 专用入口。 ## 解析与位置 diff --git a/docs/runtime/builtins.md b/docs/runtime/builtins.md index f308701..fdbaea6 100644 --- a/docs/runtime/builtins.md +++ b/docs/runtime/builtins.md @@ -1,6 +1,6 @@ # Endor 基本对象与统一 Number -这些对象统一定义在独立 [objects 包](../../crates/objects/README.md),属于实现层的数据结构与宿主 API,不定义 Python type、语言根类或解析器;语言函数已接入 [小型字节码执行器](bytecode.md)。它们通过 `ObjectPayload` 接入现有 `EndorObject`,共同使用对象头、稳定 Box、代际句柄、引用计数和循环回收。不是每种对象重新实现一套堆管理。 +这些对象统一定义在独立 [objects 包](../../crates/objects/README.md),属于实现层的数据结构与宿主 API,尚未自动注册到[托管名义类型](types.md),不定义解析器;语言函数已接入 [小型字节码执行器](bytecode.md)。它们通过 `ObjectPayload` 接入现有 `EndorObject`,共同使用对象头、稳定 Box、代际句柄、引用计数和循环回收。不是每种对象重新实现一套堆管理。 ## 对应关系 diff --git a/docs/runtime/bytecode.md b/docs/runtime/bytecode.md index 3c2720a..96ca872 100644 --- a/docs/runtime/bytecode.md +++ b/docs/runtime/bytecode.md @@ -26,7 +26,7 @@ Endor 是解释型语言,字节码是内部执行表示;从 AST 生成字节 所有执行位置和跳转距离以**两字节代码单元**计数,包含扩展参数单元;不是字节偏移,也不一定等于逻辑指令下标。前跳从当前逻辑指令之后的位置加距离;后跳从该位置减距离。目标必须是完整逻辑指令的开头,不能跳进扩展参数中间。 -当前有 29 个可执行操作码和一个由编解码器处理的 EXTENDED_ARG: +当前有 30 个可执行操作码和一个由编解码器处理的 EXTENDED_ARG: | 指令组 | 内容 | | --- | --- | @@ -37,7 +37,7 @@ Endor 是解释型语言,字节码是内部执行表示;从 AST 生成字节 | 控制流 | `JumpForward`、`JumpBackward`、`PopJumpIfFalse`、`PopJumpIfTrue` | | 真值与比较 | `ToBool`、`UnaryNot`、`CompareOp` | | 闭包 | `LoadDeref`、`StoreDeref`、`DeleteDeref`、`LoadClosure` | -| 容器 | `BuildMap` | +| 容器与类型 | `BuildMap`、`BuildClass` | 没有把 Number 转为 f64 来补运算。数字仍以无损表示为主;真假可以根据是否为零判断,数值比较和算术尚未定义。文本可作内容比较,普通对象使用现有比较接口。 @@ -152,11 +152,11 @@ fn main() -> Result<(), endor_core::VmError> { } ``` -该示例直接提供指令,不经过源码前端;当前 Endor 前端可生成 AST,但尚不能从源码生成字节码,也不解析 Python 源码。宿主仍负责准备代码对象和执行环境;函数既可由宿主 new_function 工厂创建,也可由上述 MakeFunction 在字节码运行期间创建。 +该示例直接提供指令,不经过源码前端;当前 Endor 的 frontend/compiler 已能从源码生成 AST 和字节码,也不解析 Python 源码。宿主仍负责准备代码对象和执行环境;函数既可由宿主 new_function 工厂创建,也可由上述 MakeFunction 在字节码运行期间创建。 ## 限额、诊断与边界 -`ExecutionLimits` 默认限制 1000 个活动帧,指令配额为 `usize::MAX`,正常使用不设置有限的指令预算。`execute_with_limits`/`call_with_limits` 可显式指定限额,所有嵌套语言调用共享计步器;扩展参数不单独计步。可选指令预算是 Endor 的宿主运行边界,不是 CPython 默认行为,也不会抢占正在执行的宿主回调。 +`ExecutionLimits` 默认限制 1000 个活动帧,指令配额为 `usize::MAX`。`execute_with_limits`/`call_with_limits` 可显式指定限额,嵌套语言调用和原生分派共享计步器;每次原生分派额外计一步,扩展参数不单独计步。原生 Invoke 委托通过同一帧循环推进,不重置预算;详见 [原生调用](native-calls.md)。指令预算不会抢占正在执行的宿主回调。 运行错误仍返回 `VmError`。`last_traceback()` 另提供从外到内的函数名、文件、代码首行和出错指令偏移;它保存独立元数据,不保活已经清理的帧。首行是代码声明的 first_line,不是由行号表推导出的实际错误源码行。成功的新一次调用会清空旧记录。尚无语言异常对象传播、raise/try/except 或完整源码行号表。 @@ -175,3 +175,10 @@ fn main() -> Result<(), endor_core::VmError> { `ObjectPayload::assign_from(source)` 是可选协议,默认返回 false,普通值与对象仍替换变量绑定。StoreGlobal、StoreFast、StoreDeref 在既有值和新值均为对象时调用此协议;返回 true 表示宿主对象已替换自己的目标,绑定保留旧对象身份,其他别名随共享引用观察更新。相同句柄赋值为无操作。此协议同时适用于这些存储指令实现的声明初始化/赋值;字典 API 与 StoreAttr 不隐式调用它。 协议不传递变量名字或 UI 元数据,也不引入渲染器。实现必须先验证再修改,拒绝替换时保留旧目标;不得通过可变 payload 悄悄增加未跟踪的 VM 对象边。宿主原生关系如持有受管理对象,必须通过 VM retain/release 登记根。原生资源的内部引用可以由宿主维护,但其管理对象身份和存活根仍由 Aster 校验和计数。StoreFast 会先持有选择后的值,再释放栈值与旧槽,失败执行仍走统一帧清理。 + + +## 类创建与方法读取 + +BuildClass(编号 29,无参数)消费四个栈值 `[name, baseType, fieldsDict, methodsDict]`,产生一个受管类对象;验证器按 4 入 1 出检查栈。字段字典按声明顺序保存字段名与注解名,方法字典保存 MakeFunction 创建的函数。VM 先验证基类、字段与方法关系,再创建完整类型;输入根在成功前保持在执行栈上,失败由帧统一清理。 + +LoadAttr 对实例自有属性返回原值;缺失时沿类型父链找方法并创建绑定方法。绑定方法持有 receiver 和选定的函数,调用时经已有非递归委托路径补入 self。类对象上的方法读取提供未绑定函数;type/name/base 元数据优先。宿主读取继承方法应使用返回拥有式结果的 bind_attr;get_attr 保持借用式读取,类型方法结果为未绑定函数。所有委托消耗同一指令预算。 diff --git a/docs/runtime/compiler.md b/docs/runtime/compiler.md index 2aef947..aeb050d 100644 --- a/docs/runtime/compiler.md +++ b/docs/runtime/compiler.md @@ -15,7 +15,7 @@ ## 名称与作用域 -首版是模块作用域和函数作用域。模块先登记全部函数名,再分析函数体,因此支持前向引用、自递归和函数作为普通值传递。函数参数占据最前面的局部槽,let 与首次赋值目标按出现顺序分配后续槽;引用优先使用函数局部,其次使用已声明全局。 +首版是模块作用域和函数作用域。模块先登记全部函数/类名,再分析函数体,因此支持前向引用、自递归和函数作为普通值传递。函数参数占据最前面的局部槽,let 与首次赋值目标按出现顺序分配后续槽;引用优先使用函数局部,其次使用已声明全局。 函数内先收集全部局部名称,再按语句顺序检查初始化。后面声明的 let 会遮蔽整个函数中的同名全局,但提前读取报错;let x = x 也会报未初始化。参数、局部绑定和同一模块的函数不允许在各自作用域内重复声明;参数或局部可以遮蔽模块函数。裸名称赋值 `x = expression` 可在交互全局创建或更新名字;函数内赋值复用已有参数/局部槽,不存在时分配局部槽。赋值右侧先求值再写入,失败不会覆盖该目标;函数内赋值目标仍遮蔽同名全局,首次初始化前读取报错。`let` 的重复声明检查不变。嵌套函数/闭包分析和块级作用域尚未实现。 @@ -31,10 +31,12 @@ CompileOptions.globals 显式声明宿主会提供的 globals/builtins 名称 | 全局读取/赋值 | LoadGlobal/StoreGlobal,使用该代码对象的 names 表 | | 函数声明 | 子代码模板 → LoadConst(code) → MakeFunction(0, 0) → StoreGlobal | | 普通调用 | 求值 callee → PushNull → 从左到右求值位置参数 → Call(argc) | -| 成员读取 | 求值对象 → LoadAttr;沿用 VM 属性协议,无自动 self 绑定 | +| 成员读取 | 求值对象 → LoadAttr;沿用 VM 属性协议;类型方法返回持有接收者的绑定方法 | +| 属性赋值 | 求值右侧,再求值目标对象,使用 StoreAttr 维护托管引用与变更 | +| 类声明 | 名称、基类、字段映射、MakeFunction 构造的方法映射 → BuildClass → StoreGlobal | | return | 求值结果 → ReturnValue;无值 return 或 AST 函数末尾无 return 时生成 nil | -源码 parser 仍要求函数体为 let/裸赋值序列加一个带值 return;更通用的表达式语句和空 return 可以由 AST 调用方构造,但不能因此宣称源码语法已开放。模块初始化只发布函数并返回 nil,不自动调用 main。Lyra 宿主选择入口属于宿主行为。 +源码 parser 接受绑定/名称及属性赋值/表达式语句与可选末尾带值 return;空 return 仍仅由 AST 调用方构造。模块初始化发布函数/类并返回 nil,不自动调用 main。Lyra 宿主选择入口属于宿主行为。 二元算术、非字面量的一元负号和符号值 .name 目前产生明确 Compilation 诊断,不伪造运行结果。导入、类、Agent、条件/循环、关键字参数等仍未实现。函数参数个数和对象是否可调用由 VM 在运行时检查。 @@ -53,3 +55,6 @@ Expression 模式返回表达式值;Interactive 模式严格编译一次声明 lyra run [--entry name] 编译模块、执行初始化、调用无参数入口(默认 main),输出返回值一行。lyra eval 编译执行一个表达式。字符串输出原始内容,数字输出无损表示,对象使用 VM str;失败仅输出 stderr 并返回非零退出码。文件和 stdin 严格 UTF-8,stdin 到 EOF 后才处理。Lyra 在成功或失败后释放代码/结果并清空自己创建的命名空间。 可执行文件见 [示例目录](../../crates/compiler/examples/README.md),API/所有权与错误路径见 [pipeline.rs](../../crates/compiler/tests/pipeline.rs),进程入口见 [CLI 检查](https://gitea.entroia.com/Zayden-Jung/endorphin-lyra-cli/src/branch/main/crates/cli/tests/README.md)。引擎和进程入口检查由独立 Lyra CI 分别显式执行;本地只做静态检查,不据此宣称实际执行通过,见 [验证约定](../operations/validation.md)。返回 [运行时索引](README.md)。 + + +类方法使用显式首参数 self;编译器检查本类重复字段/方法和缺少 self。方法代码仍是共享的普通代码对象,参数类型名目前不实施运行时类型检查。BuildClass 在 VM 中检查继承字段冲突和原生分配边界;构造失败不发布类型到目标全局名。默认基类 Object 须由宿主通过 CompileOptions 声明并在运行全局中提供,编译器不自行引导类型。普通类构造和继承分派见[类型机制](types.md)。 diff --git a/docs/runtime/frontend.md b/docs/runtime/frontend.md index 012228c..f4516d5 100644 --- a/docs/runtime/frontend.md +++ b/docs/runtime/frontend.md @@ -52,12 +52,12 @@ AST 结构及 Visitor、Spanned 和跨工具 JSON 契约见 [AST 模型](ast.md) program = function+ function = "function" name "(" parameters? ")" ":" name block parameter = name ":" name -block = "{" (binding | assignment)* "return" expression "}" +block = "{" (binding | assignment | expression)* ("return" expression)? "}" binding = "let" name "=" expression assignment = name "=" expression ``` -parameters 和调用参数列表使用逗号分隔,不接受尾随逗号。函数体要求一个末尾 return;函数体尚不支持独立表达式语句、属性赋值、嵌套函数声明或默认参数源码写法。新增 Interactive 解析模式接受一次函数声明、let 绑定、裸名称赋值或表达式,完整消费输入,但不创建 REPL 或执行代码;AST 可表示的语句组合比当前源码子集更广。参数和返回类型目前只是名称节点;并不据此创建语言 type 对象或检查类型。 +parameters 和调用参数列表使用逗号分隔,不接受尾随逗号。函数体支持独立表达式语句(包括方法调用)和可选的末尾 return;省略 return 时编译器生成 Nil 结果。函数体支持属性赋值,尚不支持嵌套声明或默认参数源码写法。Interactive 解析模式接受一次函数/类声明、let 绑定、名称/属性赋值或表达式,完整消费输入,但不创建 REPL 或执行代码。参数和返回类型目前只是名称节点;并不据此创建语言 type 对象或检查类型,省略 return 不代表已实施 Unit 类型检查。 表达式包括名称、数字、字符串、`.name` 符号、括号分组、成员访问与位置参数调用、一元负号以及四则运算。优先级从高到低为: @@ -68,7 +68,7 @@ parameters 和调用参数列表使用逗号分隔,不接受尾随逗号。函 符号字面量、名称与成员分别保留为独立 AST 结构。true、false、Nil 等词目前只是普通名称,没有额外的源码字面量或内建值绑定。 -use、namespace、class、agent、完整类型语法、条件/循环、比较/布尔运算和闭包声明等尚未加入首版。当前语法可修改;扩展时同步 AST、PEG 规则、样例和测试,不将旧草案视作不可修改的 ABI。 +use、namespace、trait、agent、完整类型语法、条件/循环、比较/布尔运算和闭包声明等尚未加入首版。当前语法可修改;扩展时同步 AST、PEG 规则、样例和测试,不将旧草案视作不可修改的 ABI。 ## 诊断和命令 @@ -78,4 +78,7 @@ use、namespace、class、agent、完整类型语法、条件/循环、比较 当前 Compiler 将 AST 经模块/函数名称分析转换为现有字节码,再交给 VM 解释执行。这是解释器内部的编译/执行准备步骤,不要求使用者先构建程序,见 [执行与版本模型](../architecture/execution-and-versions.md)。MakeFunction 已接入源码函数编译,但解析接受的二元算术和符号值仍没有执行语义;编译器会给出明确诊断,见 [编译链路](compiler.md)。源码闭包尚未实现。解析器也不是不可信代码沙箱,当前没有通用输入大小或解析递归深度限额。 -裸赋值 `name = expression` 创建或更新当前作用域中的名称;它不是表达式,不能链式赋值,当前也不接受属性/索引目标。`let` 仍表示声明,保留重复声明检查;名称与初始化规则见 [Compiler](compiler.md)。两种源码写法产生独立的 Binding/Assignment 节点,宿主不改写输入。 +裸赋值 `name = expression` 创建或更新当前作用域中的名称;它不是表达式,不能链式赋值,属性目标使用独立 MemberAssignment 节点;索引目标尚不支持。`let` 仍表示声明,保留重复声明检查;名称与初始化规则见 [Compiler](compiler.md)。两种源码写法产生独立的 Binding/Assignment 节点,宿主不改写输入。 + + +源码类实现子集采用 `class Name: Base { field: TypeName function method(self: Name): ResultType { ... } }`;基类可省略,执行环境须提供 Object。成员函数沿用既有 function 语法,接收者 self 为显式首参数。普通类字段按基类字段、再本类字段的顺序接收位置构造实参。这里记录实现范围,不确认语言文档中的全部候选构造语法;详细限制见[名义类型](types.md)。 diff --git a/docs/runtime/lifecycle.md b/docs/runtime/lifecycle.md index df837b6..73646ba 100644 --- a/docs/runtime/lifecycle.md +++ b/docs/runtime/lifecycle.md @@ -29,6 +29,8 @@ Rust panic 尚无专门的捕获恢复或执行根 RAII 清理机制。宿主捕 ## 回收和整体销毁 +NativeContext 的分配与弱引用升级使用作用域根,成功时仅移交 Return 或 Invoke 结果,失败时释放临时持有。WeakObjectRef 和变更通知不保活对象;具体接口见 [原生调用](native-calls.md)。 + 通常释放在总引用数归零时立即执行。堆使用显式工作列表递减出边,长链不会递归析构。不可达循环通过手动 collect_cycles 从根标记后批量移除;没有自动阈值、分代或增量 GC。 Vm 没有独立后台线程、暂停状态或显式 shutdown 状态机。Rust 所有权销毁它所拥有的堆和剩余载荷,包括没有先做循环回收的对象;原生共享资源继续遵循各自的 Rust 所有权。外部 ObjectRef 不保活 Vm。 diff --git a/docs/runtime/native-calls.md b/docs/runtime/native-calls.md new file mode 100644 index 0000000..cc41cc7 --- /dev/null +++ b/docs/runtime/native-calls.md @@ -0,0 +1,55 @@ +# 原生对象调用、弱引用与变更记录 + +本页定义宿主扩展使用的通用接口,不引入渲染或窗口语义。托管名义类型见[类型机制](types.md)。 + +## 有上下文的调用 + +旧 ObjectPayload::call 保留借用参数和借用结果协议。新载荷可通过 native_callable 返回 +Arc;VM 先取得独立分派器,再可变访问 ObjectStore,避免同时借用 +堆中载荷与可变对象存储。ContextNativeFunction 提供名称、分派器和不可变捕获边列表。 +捕获中的 ObjectRef 必须全部在 captures 中登记,未登记句柄不能保活对象。 + +NativeCallable::invoke 接收 NativeContext 和借用参数。context 提供对象、字典、Cell +分配、托管属性和容器写入、载荷事务替换及弱引用升级。store() 仅暴露只读访问; +callee() 返回当前可调用对象的借用句柄;attach_type 通过受校验的类型边事务附加实例类型。 +没有不受控的 &mut Vm、手动释放根或重入执行器入口。 + +context 内分配或升级得到的对象由本次作用域临时持有。NativeResult::Return(value) +返回借用结果;作用域为结果额外持有一份根,再释放其余临时根。出错或 unwind 也会 +清理本作用域根,但此前完成的业务写入不会回滚;这不承诺整个执行帧的 panic 恢复。 + +NativeResult::Invoke { function, arguments } 将调用委托给另一个原生或语言函数。 +function 和每个对象参数在移交前取得根;VM 完成调用准备后释放这些临时输入。 +委托通过原有帧循环推进,不递归调用 Vm::call;语言函数使用原有帧限额。每次原生 +分派计入同一指令预算,避免无界原生委托链。预算仍不能抢占一次长时间原生操作。 +委托函数的结果就是该次调用结果,当前不提供任意原生调用后续 continuation。 + +## 托管共享引用赋值 + +assignment_cell(source, store) 是与原生 assign_from 并存的可选协议。实现先只读验证 +目标及组合环,返回自身持有的内建 Cell;对象存储再事务性地将 source 写入 Cell。 +修改的引用属于可追踪堆边,旧对象身份与方法不变。默认返回 None,沿用既有赋值规则。 +Cell 的持有边也必须通过 trace 或托管字段登记;字典 API 和属性写入不隐式执行此协议。 + +## 弱引用 + +downgrade 验证对象后产生 WeakObjectRef,不增加强引用。upgrade 在同一个对象存储中 +重新验证堆、槽位与代次;存活时返回一份新的拥有式根,过期返回 None,其他存储返回 +ForeignHandle。释放后复用槽位不会使旧弱引用指向新对象。弱引用不保活 VM 本身。 +宿主使用升级结果后须 release;NativeContext::upgrade 的升级根归当前调用作用域。 + +## 对象变更 + +每个对象头有从 0 开始的 change_version。成功的属性写入或删除产生 Attribute(name), +字典、Cell、通用载荷替换及共享引用赋值产生 Contents;失败写入不产生变更。 +同值写入仍视为成功写入,消费方可自行比较派生结果。版本耗尽拒绝修改。 + +take_changes 按对象与变更键合并同一批写入,返回最终版本。记录不持有对象,消费时 +对象可能已经回收,应重新验证身份。通知只保证修改信息,不包含旧值或回滚日志。 +最多保留 4096 个不同变更键,超额后清空细项并设置 rescan_required,直到下一次提取; +消费方必须完整重查依赖,不能把溢出当成没有变化。版本仍继续递增。 + +引擎只记录通用对象变化,依赖订阅、视觉失效和事件提交由宿主负责。托管原生状态须 +通过 replace_payload、属性或容器事务更新;载荷内部自行变动不会自动通知。 + +远端回归用例见 [native_context.rs](../../crates/core/tests/native_context.rs);本地不执行测试。 diff --git a/docs/runtime/objects.md b/docs/runtime/objects.md index 1cfd50d..bb126d7 100644 --- a/docs/runtime/objects.md +++ b/docs/runtime/objects.md @@ -1,6 +1,6 @@ # EndorObject 代码说明 -当前实现的是 Endor 的 VM 运行时基础对象,角色类似 CPython 的公共对象结构。这里没有语言根类、元类、继承或 Python type 系统。标量仍然可以直接保存在 Value 中,不要求一切装箱。 +当前实现的是 Endor 的 VM 运行时基础对象,角色类似 CPython 的公共对象结构。宿主可显式引导托管 Object/Type 与名义继承,见[类型机制](types.md);普通源码类和继承方法分派已实现,trait 和原生类分配继承尚待接入。标量仍然可以直接保存在 Value 中,不要求一切装箱。 基本对象及统一数字表示已在此基础上加入,详见 [基本对象与 Number](builtins.md)。`builtins.rs` 定义字符串、字典、Cell、函数和异常数据;`number.rs` 定义无损数字表示;`store_builtins.rs` 提供容器操作及关联对象校验。 @@ -12,15 +12,15 @@ ```text EndorObject - header 身份、总强引用数、外部强引用数 + header 身份、总强引用数、外部强引用数、变更版本、可选类型引用 payload 原生 Rust 数据及操作接口 attributes VM 管理的可变属性表 payload_edges 分配时记录的载荷引用快照 ``` -`ObjectHeader` 不保存语言类型指针。`identity` 是 ObjectRef,用于识别同一个对象。`strong_refs` 是所有强引用的总数,`external_refs` 统计宿主持有和执行帧持有的根引用,也是循环回收的根来源。 +`ObjectHeader.type_object` 是可选的、受 GC 追踪的类型对象引用,普通实例附加后不可更改。`identity` 是 ObjectRef,用于识别同一个对象。`strong_refs` 是所有强引用的总数,`external_refs` 统计宿主持有和执行帧持有的根引用,也是循环回收的根来源。 -`payload` 是 `Box`。Box 保存原生数据,dyn trait 让运行时通过统一接口调用不同 Rust 实现。Rust 会为 trait 分发生成所需机制;不需要创建一个 Python 式的 Type 对象。`kind_name` 只用于诊断,`as_any` 只用于原生 Rust 扩展判断自己能否处理另一份载荷。 +`payload` 是 `Box`。Box 保存原生数据,dyn trait 让运行时通过统一接口调用不同 Rust 实现。Rust 会为 trait 分发生成所需机制;其原生操作接口与托管 Type 身份分别保存。`kind_name` 只用于诊断,`as_any` 只用于原生 Rust 扩展判断自己能否处理另一份载荷。 `attributes` 保存字段名和 Value。对象值会被 VM 计为内部强引用;字符串和整数等直接保存。载荷可选择是否允许属性,例如 Record 允许,NativeFunction 默认不允许。 @@ -77,7 +77,7 @@ ObjectPayload 提供以下默认行为,载荷可以逐项覆盖: Record 使用身份语义并开启属性。NativeFunction 保存名称和 Rust 函数指针,覆盖 repr 和 call。原生函数收到 Value 参数并返回 Result;VM 在调用前后检查参数和返回值中的对象句柄。 -原生回调当前不能修改 VM 或直接在该 VM 中分配新对象;其参数和原始返回对象值是借用句柄。Vm::call 会验证并 retain 对象结果,对外统一返回一份持有式结果。语言函数已通过 [wordcode 执行器](bytecode.md) 使用显式帧、参数槽、闭包和求值栈执行;call/execute 的结果完成使用后须 release_value。 +旧 call 回调不能修改 VM 或分配新对象;需要这些能力的原生方法可使用 NativeCallable 与受限 NativeContext。两种原生入口都接受借用参数,Vm::call 对外统一返回一份持有式结果。上下文临时根、非递归委托与捕获规则见 [原生调用](native-calls.md)。语言函数通过 [wordcode 执行器](bytecode.md) 使用显式帧、参数槽、闭包和求值栈执行;call/execute 的结果完成使用后须 release_value。 compare 的反向规则是 Lt 对应 Gt、Le 对应 Ge,Eq/Ne 保持不变。它不实现 Python 的子类型优先规则。内容比较由扩展定义;如果两个对象内容相等,它们必须给出相同哈希,或者明确拒绝哈希。 @@ -92,7 +92,7 @@ compare 的反向规则是 Lt 对应 Gt、Le 对应 Ge,Eq/Ne 保持不变。 引用计数无法自行释放 A → B → A,所以提供显式 `collect_cycles()`: 1. 把 external_refs 大于零的对象作为根。 -2. 沿托管属性和载荷边标记所有可达对象。 +2. 沿类型、托管属性和载荷边标记所有可达对象。 3. 先从槽位中摘除全部不可达对象。 4. 扣减垃圾指向幸存对象的引用,再 Drop 全部垃圾。 @@ -100,8 +100,8 @@ compare 的反向规则是 Lt 对应 Gt、Le 对应 Ge,Eq/Ne 保持不变。 ## 当前边界 -已实现:统一对象结构、稳定地址、代际句柄、身份、显式引用计数、托管属性、原生操作覆盖、比较与哈希、原生调用、资源清理和显式循环回收。 +已实现:统一对象结构、稳定地址、代际句柄、身份、显式引用计数、托管属性、原生操作覆盖、比较与哈希、原生调用、资源清理和显式循环回收。另有受限原生可变上下文、安全弱引用、托管 Cell 赋值及有界对象变更记录,见 [原生调用](native-calls.md)。 -尚未实现:Python type 系统、语言根类、继承和描述符、可变 VM 调用上下文、自动/分代/增量 GC、语言终结器/复活、完整资源限额和 VM 快照;独立 frontend 已提供阅读器、lexer/parser 与 AST,基础源码执行由独立 [Compiler](compiler.md) 接入,见 [源码前端](frontend.md);执行器已有帧数和指令数限额。数据库仅为未来特殊类提供按需连接,普通对象或宿主基础信息不自动持久化,详见 [数据库特殊类](https://gitea.entroia.com/Zayden-Jung/endorphin-hestia-server/src/branch/main/docs/runtime/database-objects.md)。 +尚未实现:Python type 兼容、trait、原生类分配继承和描述符、任意 VM 重入调用上下文、自动/分代/增量 GC、语言终结器/复活、完整资源限额和 VM 快照;独立 frontend 已提供阅读器、lexer/parser 与 AST,基础源码执行由独立 [Compiler](compiler.md) 接入,见 [源码前端](frontend.md);执行器已有帧数和指令数限额。数据库仅为未来特殊类提供按需连接,普通对象或宿主基础信息不自动持久化,详见 [数据库特殊类](https://gitea.entroia.com/Zayden-Jung/endorphin-hestia-server/src/branch/main/docs/runtime/database-objects.md)。 验证范围与执行约定见 [验证](../operations/validation.md)。 diff --git a/docs/runtime/types.md b/docs/runtime/types.md new file mode 100644 index 0000000..6ac696e --- /dev/null +++ b/docs/runtime/types.md @@ -0,0 +1,56 @@ +# 托管名义类型 + +类型与实例保存在同一个 ObjectStore 中。此处是已实现的运行时机制;源码普通 `class`、字段构造和继承方法分派已经接入;trait、原生类分配继承和完整初始化协议尚未接入,不把 Rust 的 ObjectPayload 当成 Endor 的类定义。 + +## 创建与关系 + +ObjectStore/Vm 仍从空堆启动。宿主显式调用 `bootstrap_types()` 得到 Object 和 Type 两个类型对象,每个返回字段各拥有一份外部根,需要分别释放。 + +- Object 没有父类型。 +- Type 的父类型是 Object。 +- Object 的实例类型是 Type。 +- Type 的实例类型也是 Type。 + +`new_type(name, base, constructor)` 创建一份新的名义定义,并沿 base 取得元类型。名字相同不代表类型相同。base 在创建时固定,不能通过替换载荷或修改属性改变,因此继承链不会因后续修改出现循环。`attach_type(instance, definition)` 只允许给尚未附加类型的对象设置一次类型,验证同堆、存活状态和 Type 载荷。已有 `type` 普通字段时拒绝附加,避免遮蔽数据。旧的原生对象可以继续保持未分类。 + +`type_of` 返回借用式类型引用;`is_subtype(candidate, expected)` 检查名义父链,含自身;`is_instance(instance, expected)` 先取得实例类型,再检查父链。非法句柄和非 Type 参数报错,不返回假值来掩盖错误。这些接口目前面向堆对象,直接保存在 Value 中的标量尚未统一接入。 + +## 源码可见操作与构造 + +带类型的对象通过 `get_attr(object, "type")` 暴露类型对象,不能写入或删除。Type 对象额外提供只读的 `name`、`base`;根 Object 的 base 为 Nil。源码前端已有的成员读取和调用表达式可以直接使用这些对象。类型名称由宿主注册到全局字典,Aster 不硬编码 Block 或 Page。 + +TypeConstructor 三种入口: + +- Record:按声明字段接收位置参数,创建带此类型的普通 Record 实例;无字段时不接收参数。 +- Native(callable):通过现有显式 VM 调用循环委托给托管可调用对象;宿主构造器负责返回该类型的实例。 +- Abstract:拒绝直接构造。 + +原生 Type 调用通过 NativeContext::callee 取得当前定义,委托计入现有指令限额,不递归调用 Rust 求值器。尚不提供构造后初始化 continuation。 + +共享引用载荷可显式实现 `type_target(store)`:外壳身份保持稳定,类型查询沿当前被引用对象解析。目标必须由已登记的托管边保活;查询验证句柄并检测循环。普通实例使用固定 header 类型。Tessera 的 Block 共享赋值选择此协议,使 TextBlock 引用换成 RectBlock 后,类型查询与实际展示一致。这不是修改类继承链。 + +## 生命周期 + +header 的类型引用、Type 的 base 和原生构造器都是计数和循环回收可见的强边。实例保活其定义;类型对象及元类型没有隐藏的全局根。释放宿主根后,仍有实例的类型保留,无人引用的定义、Object/Type 环和原生构造器捕获环可以一起回收。 + +远端回归用例见 [types.rs](../../crates/core/tests/types.rs),覆盖名义身份、关系区分、类型边保活与回收、只读元数据及外来句柄失败原子性。本地仅编译检查,不执行测试。 + + +## 源码普通类与方法 + +```endor +class Counter { + value: Number + function read(self: Counter): Number { return self.value } +} +class DetailedCounter: Counter { + label: String + function read(self: DetailedCounter): Number { return 12 } +} +``` + +当前可用 `DetailedCounter(9, "demo")` 构造,位置参数依次对应继承字段及本类字段;`object.field = value` 经 StoreAttr 更新同一对象并记录属性变更。方法需显式声明 self;`object.read` 创建持有该接收者的绑定方法,保存后不因原变量重新赋值而失效。方法体通过 self 再次取方法时按实例实际类型分派,子类可覆盖方法。字段与方法不能相互重名,子类不能重新声明继承字段;类型身份仍按定义对象区分。 + +方法表本身是类型对象拥有的托管边。宿主 define_method 可显式替换条目,不改变类型身份、基类或已有绑定方法选中的函数。bind_attr 返回一份拥有式根;get_attr 返回借用值和未绑定方法,宿主不能把两者的调用/释放协议混用。positional_parameter_count 会扣除绑定的接收者,供宿主判断事件回调的剩余源码参数。 + +这是一份实现子集说明:构造器只按字段赋值,没有默认值、命名字段构造或隐式 init;字段/参数/返回注解保留名称,尚未实施类型检查。普通类当前只能继承普通 Record 分配的类,继承原生或抽象分配器明确报错,不能伪造带 Block 名义身份的普通 Record。trait、原生视觉子类和用户自定义测量/绘制协议仍待实现。正式语言文档的候选语法不因这一实现被自动确认为规范。