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
This commit is contained in:
@ -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<c_void>, 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.
|
||||
|
||||
@ -1 +0,0 @@
|
||||
# Moved: copilot_issues\n\nこのファイルは移動しました。最新は下記を参照してください。\n\n- 新しい場所: ../../development/roadmap/native-plan/copilot_issues.txt\n
|
||||
@ -214,6 +214,8 @@ pub struct VM {
|
||||
pub(super) boxcall_pic_funcname: std::collections::HashMap<String, String>,
|
||||
/// VTable-like cache: (type, method_id, arity) → direct target (InstanceBox method)
|
||||
pub(super) boxcall_vtable_funcname: std::collections::HashMap<String, String>,
|
||||
/// Version map for cache invalidation: label -> version
|
||||
pub(super) type_versions: std::collections::HashMap<String, u32>,
|
||||
// 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を検証
|
||||
|
||||
@ -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<u16>) -> 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<Class>
|
||||
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<ControlFlow, VMError> {
|
||||
@ -610,6 +624,59 @@ impl VM {
|
||||
Ok(val.to_nyash_box())
|
||||
})
|
||||
.collect::<Result<Vec<_>, 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::<crate::runtime::plugin_loader_v2::PluginBoxV2>() {
|
||||
// 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::box_trait::StringBox>() {
|
||||
crate::runtime::plugin_ffi_common::encode::string(&mut tlv, &s.value);
|
||||
} else if let Some(i) = a.as_any().downcast_ref::<crate::box_trait::IntegerBox>() {
|
||||
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_loader_v2::PluginBoxV2>() {
|
||||
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);
|
||||
|
||||
@ -65,6 +65,8 @@ impl InstanceBox {
|
||||
|
||||
/// ユーザー定義Box専用コンストラクタ
|
||||
pub fn from_declaration(class_name: String, fields: Vec<String>, methods: HashMap<String, ASTNode>) -> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
30
src/runtime/cache_versions.rs
Normal file
30
src/runtime/cache_versions.rs
Normal file
@ -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<Mutex<HashMap<String, u32>>> = 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<String> = 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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user