From edf5ccfcb4759bd6a4ece7c1614dd296d9e95195 Mon Sep 17 00:00:00 2001 From: Moe Charm Date: Wed, 27 Aug 2025 00:03:48 +0900 Subject: [PATCH] Docs: Add phase_9_79b_3_vm_vtable_thunks_and_pic.md\n- Plan to formalize TypeMeta+Thunk and upgrade PIC to poly\n- Diagnostics, risks, milestones, exit criteria, and Phase 10 readiness --- .../phase_9_79b_3_vm_vtable_thunks_and_pic.md | 108 ++++++++++++++++++ .../2025-08-26-ai-agent-challenge-strategy.md} | 0 .../2025-08-26-when-pattern-matching.md} | 0 docs/予定/native-plan/copilot_issues.txt | 1 - src/backend/vm.rs | 73 ++++++++++++ src/backend/vm_instructions.rs | 105 ++++++++++++++--- src/instance_v2.rs | 4 +- src/runtime/cache_versions.rs | 30 +++++ src/runtime/mod.rs | 1 + src/runtime/plugin_loader_v2.rs | 12 ++ 10 files changed, 313 insertions(+), 21 deletions(-) create mode 100644 docs/development/roadmap/phases/phase-9/phase_9_79b_3_vm_vtable_thunks_and_pic.md rename docs/{予定/ai-agent-challenge-strategy.md => ideas/new-features/2025-08-26-ai-agent-challenge-strategy.md} (100%) rename docs/{予定/when-pattern-matching.md => ideas/new-features/2025-08-26-when-pattern-matching.md} (100%) delete mode 100644 docs/予定/native-plan/copilot_issues.txt create mode 100644 src/runtime/cache_versions.rs diff --git a/docs/development/roadmap/phases/phase-9/phase_9_79b_3_vm_vtable_thunks_and_pic.md b/docs/development/roadmap/phases/phase-9/phase_9_79b_3_vm_vtable_thunks_and_pic.md new file mode 100644 index 00000000..76d57bbe --- /dev/null +++ b/docs/development/roadmap/phases/phase-9/phase_9_79b_3_vm_vtable_thunks_and_pic.md @@ -0,0 +1,108 @@ +# Phase 9.79b.3: VM VTable Thunks + Poly-PIC (Scaffolding → Production) + +Status: Planned (scaffolding in-progress) +Owner: core-runtime +Target: Before Phase 10 (Cranelift JIT mainline) +Last Updated: 2025-08-26 + +## Goals +- Replace ad-hoc direct calls with a unified vtable+thunk layer for all Box kinds (builtin/user/plugin). +- Upgrade PIC from monomorphic → polymorphic (2–4 entries) per call-site with versioned validity. +- Stabilize method_id (slot) usage end-to-end and make late-bind explicit. +- Provide robust diagnostics (registry dumps, PIC/VT stats) to support JIT handoff in Phase 10. + +## Current Baseline (9.79b.1/2 recap) +- Method slots (0..3 universal; user 4+; builtin/plugin seeded) with builder-side method_id emission when resolvable. +- VM fast-paths: + - Universal thunks (0..3): toString/type/equals/clone. + - InstanceBox vtable-like caching (function-name cache per slot/arity), plus monomorphic PIC (threshold=8). + - PluginBoxV2 method_id fast-path (direct invoke_fn with minimal TLV args; fallback to name-based path). +- Cache invalidation: versioned keys by label `BoxRef:Type` (global map; loader/decl-side bump wired). +- Docs updated; builds green. + +## Scope (this step) +1) VTable/Thunk layer +- Define `TypeMeta` and per-type thunk table (fixed address, atomic target pointer). +- Uniform entry for builtin/user/plugin: `slot -> thunk -> target`. +- Integrate version into `TypeMeta` for cache validation; connect to global version map. + +2) PIC (Polymorphic) +- Extend current mono-PIC to poly (up to 4 entries): `(type_id, version) -> target`. +- Fast-path ladder: universal → vtable(thunk) → PIC(poly) → slow path. + +3) Builder/MIR alignment +- Prefer `method_id` emission; introduce explicit late-bind op/path for unresolved calls (keeps current behavior during transition). +- MIRDebugInfo (opt-in) to map id→name for dumps and JIT logs. + +4) Diagnostics +- Registry dump: type→id, (type,method)→slot, slot→name (NYASH_REG_DUMP=1). +- PIC/VT stats: per-function summary lines (hit/miss/evict/thresholds) (NYASH_VM_PIC_STATS=1, NYASH_VM_VT_STATS=1). +- Cache version debug: bump events with label/new version (NYASH_CACHE_DEBUG=1). + +## Non-Goals +- Full JIT codegen (Phase 10). +- Full TLV coverage for all plugin arg/ret kinds (incremental; only required for fast-path coverage hotspots). + +## Architecture Plan (target) +- UnifiedBoxRegistry + - type_name → BoxTypeId (stable), `(type_id, method)` → SlotIdx (stable), `TypeMeta{version, vtable_base, slot_count}`. + - Thunk pool: fixed table of `MethodThunk{target: AtomicPtr, sig, flags}`. +- VM dispatch + - Given `(recv, method_id, args)` → read `TypeMeta` (id, version) → `thunk = vtable[slot]` → `target = thunk.target.load()` → call. + - PIC adds per-call-site multi-entry check by `(type_id, version)` before slow path. +- Versioning + - Global label-driven bump (`BoxRef:Type`); epoch for global resets if needed. + +## Implementation Steps +1. Add `TypeMeta` + thunk table scaffolding (no-op thunk targets initially). +2. Migrate InstanceBox path to use thunk table (populate with current function targets). +3. Migrate PluginBoxV2 path to thunk target (direct invoke_fn target). +4. Optionally migrate selected builtin methods (hot set) to thunk targets (incremental). +5. Upgrade PIC to poly entries (size=4) with simple LRU/round-robin. +6. Add diagnostics (registry dump, PIC/VT stats, cache bump logs). +7. Add tests (see Test Plan). +8. Documentation update and developer guide for slots/thunks/PIC. + +## Test Plan +- Unit: slot reservation invariants (universal 0..3; override keeps parent slot if inheritance used later). +- Unit: builder emits method_id for resolvable cases; late-bind path intact for unresolved. +- VM: universal thunk correctness; InstanceBox second-call via vtable; PIC threshold/upgrade; cache bump invalidation and re-learn. +- Plugin: method_id fast-path (invoke_fn) arg/ret common kinds; fallback correctness. +- Perf sanity: ensure fast-paths reduce instruction count vs slow path on micro benchmarks. + +## Risks & Mitigations +- Slot drift across reloads: enforce stable slot policy (never reuse freed slots; regen adds at tail). +- Cache staleness: version-mixed keys + bump triggers already wired; add epoch for global invalidation. +- Partial type inference: keep late-bind path; add diagnostics to reveal non-resolvable sites. +- Plugin ABI variability: keep name-based fallback; expand TLV support stepwise; document supported kinds. + +## Milestones & Timeline +- M1 (1–2d): TypeMeta/thunk scaffolding + InstanceBox migration + diagnostics framework. +- M2 (1–2d): Plugin thunk targets + poly-PIC (2–4 entries) + stats. +- M3 (1d): Builtin hotset to thunk targets + test coverage + docs polish. +- Handoff: Phase 10 JIT connects to same thunks/PIC; add codegen stubs. + +## Exit Criteria (Phase 9.79b.3) +- All Box kinds use vtable+thunk path on hot calls with correct fallbacks. +- Poly-PIC active with version-aware entries (observed hits in stats). +- Registry dump + MIRDebugInfo yield consistent id/name/slot mapping. +- Tests pass; micro benchmarks show expected fast-path usage. + +## Open Items / Nice-to-Haves +- TLV encode/decode coverage: Bool/Float/bytes/array/map for plugin fast-path. +- Fine-grained bump hooks (method-level) if/when method-level updates are supported. +- Unified late-bind MIR op (explicit), deprecate name-only BoxCall over time. +- Developer tooling: `nyash --dump-registry` and `--vm-stats verbose` presets. + +## Phase 10 (Cranelift JIT) Readiness +- Thunks serve as stable call targets for JIT stubs. +- PIC structure maps directly to inline cache checks in generated code. +- Versioning model allows safe invalidation from JIT side. +- MIR with stable method_id reduces dynamic name lookup in codegen. + +--- + +Notes: +- Current code already includes: method_id slots, universal thunks, InstanceBox VT cache, mono-PIC, Plugin fast-path via method_id, global version invalidation. +- This step formalizes the structure (TypeMeta+Thunk) and upgrades PIC to poly before Phase 10. + diff --git a/docs/予定/ai-agent-challenge-strategy.md b/docs/ideas/new-features/2025-08-26-ai-agent-challenge-strategy.md similarity index 100% rename from docs/予定/ai-agent-challenge-strategy.md rename to docs/ideas/new-features/2025-08-26-ai-agent-challenge-strategy.md diff --git a/docs/予定/when-pattern-matching.md b/docs/ideas/new-features/2025-08-26-when-pattern-matching.md similarity index 100% rename from docs/予定/when-pattern-matching.md rename to docs/ideas/new-features/2025-08-26-when-pattern-matching.md diff --git a/docs/予定/native-plan/copilot_issues.txt b/docs/予定/native-plan/copilot_issues.txt deleted file mode 100644 index 987a993c..00000000 --- a/docs/予定/native-plan/copilot_issues.txt +++ /dev/null @@ -1 +0,0 @@ -# Moved: copilot_issues\n\nこのファイルは移動しました。最新は下記を参照してください。\n\n- 新しい場所: ../../development/roadmap/native-plan/copilot_issues.txt\n diff --git a/src/backend/vm.rs b/src/backend/vm.rs index 96622a61..fb7b99a4 100644 --- a/src/backend/vm.rs +++ b/src/backend/vm.rs @@ -214,6 +214,8 @@ pub struct VM { pub(super) boxcall_pic_funcname: std::collections::HashMap, /// VTable-like cache: (type, method_id, arity) → direct target (InstanceBox method) pub(super) boxcall_vtable_funcname: std::collections::HashMap, + /// Version map for cache invalidation: label -> version + pub(super) type_versions: std::collections::HashMap, // Phase 9.78a: Add unified Box handling components // TODO: Re-enable when interpreter refactoring is complete // /// Box registry for creating all Box types @@ -277,6 +279,7 @@ impl VM { boxcall_pic_hits: std::collections::HashMap::new(), boxcall_pic_funcname: std::collections::HashMap::new(), boxcall_vtable_funcname: std::collections::HashMap::new(), + type_versions: std::collections::HashMap::new(), // TODO: Re-enable when interpreter refactoring is complete // box_registry: Arc::new(UnifiedBoxRegistry::new()), // #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] @@ -305,6 +308,7 @@ impl VM { boxcall_pic_hits: std::collections::HashMap::new(), boxcall_pic_funcname: std::collections::HashMap::new(), boxcall_vtable_funcname: std::collections::HashMap::new(), + type_versions: std::collections::HashMap::new(), } } @@ -1027,6 +1031,75 @@ console.log("ok") assert_eq!(result.to_string_box().value, "void"); } + #[test] + fn test_vm_user_box_vtable_caching_two_calls() { + // Defines a simple user box and calls the same method twice; ensures correctness. + // Builder reserves slots for user methods (from 4), so method_id should be present, + // enabling vtable cache population on first call and (conceptually) direct call on second. + let code = r#" +box Greeter { + init { name } + birth(n) { me.name = n } + greet() { return "Hi, " + me.name } +} + +local g +g = new Greeter("Bob") +g.greet() +g.greet() +"#; + + // Parse to AST + let ast = crate::parser::NyashParser::parse_from_string(code).expect("parse failed"); + + // Prepare runtime with user-defined declarations and factory + let runtime = { + let rt = crate::runtime::NyashRuntime::new(); + // Collect box declarations into runtime so that user-defined factory works + fn collect_box_decls(ast: &crate::ast::ASTNode, runtime: &crate::runtime::NyashRuntime) { + use crate::core::model::BoxDeclaration as CoreBoxDecl; + fn walk(node: &crate::ast::ASTNode, runtime: &crate::runtime::NyashRuntime) { + match node { + crate::ast::ASTNode::Program { statements, .. } => for st in statements { walk(st, runtime); }, + crate::ast::ASTNode::BoxDeclaration { name, fields, public_fields, private_fields, methods, constructors, init_fields, weak_fields, is_interface, extends, implements, type_parameters, .. } => { + let decl = CoreBoxDecl { + name: name.clone(), + fields: fields.clone(), + public_fields: public_fields.clone(), + private_fields: private_fields.clone(), + methods: methods.clone(), + constructors: constructors.clone(), + init_fields: init_fields.clone(), + weak_fields: weak_fields.clone(), + is_interface: *is_interface, + extends: extends.clone(), + implements: implements.clone(), + type_parameters: type_parameters.clone(), + }; + if let Ok(mut map) = runtime.box_declarations.write() { map.insert(name.clone(), decl); } + } + _ => {} + } + } + walk(ast, runtime); + } + collect_box_decls(&ast, &rt); + // Register user-defined factory + let mut shared = crate::interpreter::SharedState::new(); + shared.box_declarations = rt.box_declarations.clone(); + let udf = std::sync::Arc::new(crate::box_factory::user_defined::UserDefinedBoxFactory::new(shared)); + if let Ok(mut reg) = rt.box_registry.lock() { reg.register(udf); } + rt + }; + + // Compile and execute + let mut compiler = crate::mir::MirCompiler::new(); + let compile_result = compiler.compile(ast).expect("mir compile failed"); + let mut vm = VM::with_runtime(runtime); + let result = vm.execute_module(&compile_result.module).expect("vm exec failed"); + assert_eq!(result.to_string_box().value, "Hi, Bob"); + } + #[test] fn test_vm_fastpath_universal_type_and_equals() { // toString/type/equals/cloneのうち、typeとequalsのfast-pathを検証 diff --git a/src/backend/vm_instructions.rs b/src/backend/vm_instructions.rs index a9ba1507..a6478015 100644 --- a/src/backend/vm_instructions.rs +++ b/src/backend/vm_instructions.rs @@ -17,26 +17,12 @@ use super::vm::ControlFlow; impl VM { /// Build a PIC key from receiver and method identity fn build_pic_key(&self, recv: &VMValue, method: &str, method_id: Option) -> String { - let rkey = match recv { - VMValue::Integer(_) => "Int", - VMValue::Float(_) => "Float", - VMValue::Bool(_) => "Bool", - VMValue::String(_) => "String", - VMValue::Future(_) => "Future", - VMValue::Void => "Void", - VMValue::BoxRef(b) => { - // Using dynamic type name as fingerprint (will migrate to numeric id later) - return if let Some(mid) = method_id { - format!("BoxRef:{}#{}", b.type_name(), mid) - } else { - format!("BoxRef:{}#{}", b.type_name(), method) - }; - } - }; + let label = self.cache_label_for_recv(recv); + let ver = self.cache_version_for_label(&label); if let Some(mid) = method_id { - format!("{}#{}", rkey, mid) + format!("v{}:{}#{}", ver, label, mid) } else { - format!("{}#{}", rkey, method) + format!("v{}:{}#{}", ver, label, method) } } @@ -66,7 +52,35 @@ impl VM { /// Build vtable cache key for InstanceBox: TypeName#slot/arity fn build_vtable_key(&self, class_name: &str, method_id: u16, arity: usize) -> String { - format!("VT:{}#{}{}", class_name, method_id, format!("/{}", arity)) + // Use same versioning as PIC for BoxRef + let label = format!("BoxRef:{}", class_name); + let ver = self.cache_version_for_label(&label); + format!("VT@v{}:{}#{}{}", ver, class_name, method_id, format!("/{}", arity)) + } + + /// Compute cache label for a receiver + fn cache_label_for_recv(&self, recv: &VMValue) -> String { + match recv { + VMValue::Integer(_) => "Int".to_string(), + VMValue::Float(_) => "Float".to_string(), + VMValue::Bool(_) => "Bool".to_string(), + VMValue::String(_) => "String".to_string(), + VMValue::Future(_) => "Future".to_string(), + VMValue::Void => "Void".to_string(), + VMValue::BoxRef(b) => format!("BoxRef:{}", b.type_name()), + } + } + + /// Get current version for a cache label (default 0) + fn cache_version_for_label(&self, label: &str) -> u32 { + // Prefer global cache versions so that loaders can invalidate across VMs + crate::runtime::cache_versions::get_version(label) + } + + /// Bump version for a label (used to invalidate caches) + #[allow(dead_code)] + pub fn bump_cache_version(&mut self, label: &str) { + crate::runtime::cache_versions::bump_version(label) } /// Execute a constant instruction pub(super) fn execute_const(&mut self, dst: ValueId, value: &ConstValue) -> Result { @@ -610,6 +624,59 @@ impl VM { Ok(val.to_nyash_box()) }) .collect::, VMError>>()?; + + // PluginBoxV2 fast-path via method_id -> direct invoke_fn (skip name->id resolution) + if let (Some(mid), VMValue::BoxRef(arc_box)) = (method_id, &recv) { + if let Some(p) = arc_box.as_any().downcast_ref::() { + // Encode TLV args (support: int, string, plugin handle) + let mut tlv = crate::runtime::plugin_ffi_common::encode_tlv_header(nyash_args.len() as u16); + let mut enc_failed = false; + for a in &nyash_args { + if let Some(s) = a.as_any().downcast_ref::() { + crate::runtime::plugin_ffi_common::encode::string(&mut tlv, &s.value); + } else if let Some(i) = a.as_any().downcast_ref::() { + crate::runtime::plugin_ffi_common::encode::i32(&mut tlv, i.value as i32); + } else if let Some(h) = a.as_any().downcast_ref::() { + crate::runtime::plugin_ffi_common::encode::plugin_handle(&mut tlv, h.inner.type_id, h.inner.instance_id); + } else { + enc_failed = true; break; + } + } + if !enc_failed { + let mut out = vec![0u8; 4096]; + let mut out_len: usize = out.len(); + let code = unsafe { + (p.inner.invoke_fn)( + p.inner.type_id, + mid as u32, + p.inner.instance_id, + tlv.as_ptr(), + tlv.len(), + out.as_mut_ptr(), + &mut out_len, + ) + }; + if code == 0 { + // Try decode TLV first entry (string/i32); else return void + let vm_out = if let Some((_tag, _sz, payload)) = crate::runtime::plugin_ffi_common::decode::tlv_first(&out[..out_len]) { + // naive: try string, then i32 + let s = crate::runtime::plugin_ffi_common::decode::string(payload); + if !s.is_empty() { + VMValue::String(s) + } else if let Some(v) = crate::runtime::plugin_ffi_common::decode::i32(payload) { + VMValue::Integer(v as i64) + } else { + VMValue::Void + } + } else { + VMValue::Void + }; + if let Some(dst_id) = dst { self.set_value(dst_id, vm_out); } + return Ok(ControlFlow::Continue); + } + } + } + } if debug_boxcall { self.debug_log_boxcall(&recv, method, &nyash_args, "START", None); diff --git a/src/instance_v2.rs b/src/instance_v2.rs index 436da869..fe77e71c 100644 --- a/src/instance_v2.rs +++ b/src/instance_v2.rs @@ -65,6 +65,8 @@ impl InstanceBox { /// ユーザー定義Box専用コンストラクタ pub fn from_declaration(class_name: String, fields: Vec, methods: HashMap) -> Self { + // Invalidate caches for this class since methods layout may change between runs + crate::runtime::cache_versions::bump_version(&format!("BoxRef:{}", class_name)); let mut field_map = HashMap::new(); let mut legacy_field_map = HashMap::new(); @@ -451,4 +453,4 @@ mod tests { let _box_ref: &dyn NyashBox = &instance; } } -} \ No newline at end of file +} diff --git a/src/runtime/cache_versions.rs b/src/runtime/cache_versions.rs new file mode 100644 index 00000000..0df4ab66 --- /dev/null +++ b/src/runtime/cache_versions.rs @@ -0,0 +1,30 @@ +//! Global cache version map for vtable/PIC invalidation + +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::sync::Mutex; + +static CACHE_VERSIONS: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); + +/// Get current version for a cache label (default 0) +pub fn get_version(label: &str) -> u32 { + let map = CACHE_VERSIONS.lock().unwrap(); + *map.get(label).unwrap_or(&0) +} + +/// Bump version for a cache label +pub fn bump_version(label: &str) { + let mut map = CACHE_VERSIONS.lock().unwrap(); + let e = map.entry(label.to_string()).or_insert(0); + *e = e.saturating_add(1); +} + +/// Convenience: bump for multiple labels +pub fn bump_many(labels: &[String]) { + let mut map = CACHE_VERSIONS.lock().unwrap(); + for l in labels { + let e = map.entry(l.clone()).or_insert(0); + *e = e.saturating_add(1); + } +} + diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index 18bdcafb..04763b41 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -20,6 +20,7 @@ pub use plugin_config::PluginConfig; pub use box_registry::{BoxFactoryRegistry, BoxProvider, get_global_registry}; pub use plugin_loader_v2::{PluginLoaderV2, get_global_loader_v2, init_global_loader_v2}; pub use plugin_loader_unified::{PluginHost, get_global_plugin_host, init_global_plugin_host, PluginLibraryHandle, PluginBoxType, MethodHandle}; +pub mod cache_versions; pub use unified_registry::{get_global_unified_registry, init_global_unified_registry, register_user_defined_factory}; pub use nyash_runtime::{NyashRuntime, NyashRuntimeBuilder}; // pub use plugin_box::PluginBox; // legacy diff --git a/src/runtime/plugin_loader_v2.rs b/src/runtime/plugin_loader_v2.rs index e94225fc..2e472844 100644 --- a/src/runtime/plugin_loader_v2.rs +++ b/src/runtime/plugin_loader_v2.rs @@ -246,6 +246,16 @@ impl PluginBoxV2 { eprintln!("Failed to load config: {}", e); BidError::PluginError })?); + // Bump cache versions for all box types (config reload may change method layout) + if let Some(cfg) = self.config.as_ref() { + let mut labels: Vec = Vec::new(); + for (_lib, def) in &cfg.libraries { + for bt in &def.boxes { + labels.push(format!("BoxRef:{}", bt)); + } + } + crate::runtime::cache_versions::bump_many(&labels); + } Ok(()) } @@ -309,6 +319,8 @@ impl PluginBoxV2 { finalized: std::sync::atomic::AtomicBool::new(false), }); self.singletons.write().unwrap().insert((lib_name.to_string(), box_type.to_string()), handle); + // bump version for this box type to invalidate caches + crate::runtime::cache_versions::bump_version(&format!("BoxRef:{}", box_type)); Ok(()) }