feat: Phase 10 reorganization + GC switchable runtime + VM improvements

## 📚 Documentation Updates
- Phase 10 reorganized with comprehensive README
  - Cranelift JIT as main implementation
  - NEW: Phase 10.4 GC Switchable Runtime (world's first\!)
  - Phase 10.5 Self-hosting (parallel)
  - Application migration tests
- Phase 11 created for LLVM AOT research (deferred)
  - Moved phase10_aot_scaffolding.md → Phase 11
  - Moved phase_10_x_llvm_backend_skeleton.md → Phase 11
- Master roadmap updated with GC runtime feature
- Ideas: GC switchable language concept documented

## 🚀 VM Implementation Progress (by ChatGPT5)
- src/backend/vm.rs: Enhanced VM execution
- src/backend/vm_instructions.rs: Instruction improvements
- src/runtime/type_meta.rs: NEW - Type metadata system
- src/boxes/buffer/mod.rs: Buffer optimizations
- src/runtime/mod.rs & plugin_ffi_common.rs: Runtime enhancements

## 🌟 Revolutionary Feature: GC Switchable Runtime
- Development mode: GC on (convenience)
- Production mode: GC off (performance)
- Technical feasibility confirmed by Codex GPT-5
- Implementation plan: After Cranelift JIT

## 📋 Phase 10 Structure
Phase 10.0: Cranelift JIT foundation
Phase 10.1-10.3: JIT implementation & optimization
Phase 10.4: GC Switchable Runtime ← NEW\!
Phase 10.5: Self-hosting (String/Array/Map in Nyash)
Phase 10.9: Application migration tests

🤖 ChatGPT5 says: Ready for Phase 10\! どきどきにゃ!
This commit is contained in:
Moe Charm
2025-08-27 01:03:55 +09:00
parent e5515ea5e9
commit 61800a37a7
14 changed files with 1052 additions and 21 deletions

View File

@ -58,6 +58,48 @@ impl VM {
format!("VT@v{}:{}#{}{}", ver, class_name, method_id, format!("/{}", arity))
}
/// Poly-PIC probe: try up to 4 cached entries for this call-site
fn try_poly_pic(&mut self, pic_site_key: &str, recv: &VMValue) -> Option<String> {
let label = self.cache_label_for_recv(recv);
let ver = self.cache_version_for_label(&label);
if let Some(entries) = self.boxcall_poly_pic.get_mut(pic_site_key) {
// find match and move to end for naive LRU behavior
if let Some(idx) = entries.iter().position(|(l, v, _)| *l == label && *v == ver) {
let entry = entries.remove(idx);
entries.push(entry.clone());
return Some(entry.2);
}
}
None
}
/// Poly-PIC record: insert or refresh an entry for this call-site
fn record_poly_pic(&mut self, pic_site_key: &str, recv: &VMValue, func_name: &str) {
let label = self.cache_label_for_recv(recv);
let ver = self.cache_version_for_label(&label);
use std::collections::hash_map::Entry;
match self.boxcall_poly_pic.entry(pic_site_key.to_string()) {
Entry::Occupied(mut e) => {
let v = e.get_mut();
if let Some(idx) = v.iter().position(|(l, vv, _)| *l == label && *vv == ver) {
v.remove(idx);
}
if v.len() >= 4 { v.remove(0); }
v.push((label.clone(), ver, func_name.to_string()));
}
Entry::Vacant(v) => {
v.insert(vec![(label.clone(), ver, func_name.to_string())]);
}
}
if std::env::var("NYASH_VM_PIC_STATS").ok().as_deref() == Some("1") {
// minimal per-site size log
if let Some(v) = self.boxcall_poly_pic.get(pic_site_key) {
eprintln!("[PIC] site={} size={} last=({}, v{}) -> {}",
pic_site_key, v.len(), label, ver, func_name);
}
}
}
/// Compute cache label for a receiver
fn cache_label_for_recv(&self, recv: &VMValue) -> String {
match recv {
@ -579,11 +621,136 @@ impl VM {
let pic_key = self.build_pic_key(&recv, method, method_id);
self.pic_record_hit(&pic_key);
// VTable-like direct call using method_id for InstanceBox (no threshold)
// VTable-like direct call using method_id via TypeMeta thunk table (InstanceBox/PluginBoxV2/Builtin)
if let (Some(mid), VMValue::BoxRef(arc_box)) = (method_id, &recv) {
// Determine class label for TypeMeta
let mut class_label: Option<String> = None;
let mut is_instance = false;
let mut is_plugin = false;
let mut is_builtin = false;
if let Some(inst) = arc_box.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
let vkey = self.build_vtable_key(&inst.class_name, mid, args.len());
if let Some(func_name) = self.boxcall_vtable_funcname.get(&vkey).cloned() {
class_label = Some(inst.class_name.clone());
is_instance = true;
} else if let Some(p) = arc_box.as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>() {
class_label = Some(p.box_type.clone());
is_plugin = true;
} else {
class_label = Some(arc_box.type_name().to_string());
is_builtin = true;
}
if let Some(label) = class_label {
let tm = crate::runtime::type_meta::get_or_create_type_meta(&label);
if let Some(th) = tm.get_thunk(mid as usize) {
if let Some(target) = th.get_target() {
match target {
crate::runtime::type_meta::ThunkTarget::MirFunction(func_name) => {
if std::env::var("NYASH_VM_VT_STATS").ok().as_deref() == Some("1") {
eprintln!("[VT] hit class={} slot={} -> {}", label, mid, func_name);
}
let mut vm_args = Vec::with_capacity(1 + args.len());
vm_args.push(recv.clone());
for a in args { vm_args.push(self.get_value(*a)?); }
let res = self.call_function_by_name(&func_name, vm_args)?;
if let Some(dst_id) = dst { self.set_value(dst_id, res); }
return Ok(ControlFlow::Continue);
}
crate::runtime::type_meta::ThunkTarget::PluginInvoke { method_id: mid2 } => {
if is_plugin {
if let Some(p) = arc_box.as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>() {
// Convert args prepared earlier (we need NyashBox args)
let nyash_args: Vec<Box<dyn NyashBox>> = args.iter()
.map(|arg| {
let val = self.get_value(*arg)?;
Ok(val.to_nyash_box())
})
.collect::<Result<Vec<_>, VMError>>()?;
// Encode minimal TLV (int/string/handle) same as fast-path
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,
mid2 as u32,
p.inner.instance_id,
tlv.as_ptr(),
tlv.len(),
out.as_mut_ptr(),
&mut out_len,
)
};
if code == 0 {
let vm_out = if let Some((_tag, _sz, payload)) = crate::runtime::plugin_ffi_common::decode::tlv_first(&out[..out_len]) {
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);
}
}
}
}
}
crate::runtime::type_meta::ThunkTarget::BuiltinCall { method: ref m } => {
if is_builtin {
// Prepare NyashBox args and call by name
let nyash_args: Vec<Box<dyn NyashBox>> = args.iter()
.map(|arg| {
let val = self.get_value(*arg)?;
Ok(val.to_nyash_box())
})
.collect::<Result<Vec<_>, VMError>>()?;
let cloned_box = arc_box.share_box();
let out = self.call_box_method(cloned_box, m, nyash_args)?;
let vm_out = VMValue::from_nyash_box(out);
if let Some(dst_id) = dst { self.set_value(dst_id, vm_out); }
return Ok(ControlFlow::Continue);
}
}
}
}
}
// Backward-compat: consult legacy vtable cache for InstanceBox if TypeMeta empty
if is_instance {
let inst = arc_box.as_any().downcast_ref::<crate::instance_v2::InstanceBox>().unwrap();
let vkey = self.build_vtable_key(&inst.class_name, mid, args.len());
if let Some(func_name) = self.boxcall_vtable_funcname.get(&vkey).cloned() {
let mut vm_args = Vec::with_capacity(1 + args.len());
vm_args.push(recv.clone());
for a in args { vm_args.push(self.get_value(*a)?); }
let res = self.call_function_by_name(&func_name, vm_args)?;
if let Some(dst_id) = dst { self.set_value(dst_id, res); }
return Ok(ControlFlow::Continue);
}
}
}
}
// Poly-PIC direct call: if we cached a target for this site and receiver is InstanceBox, use it
if let VMValue::BoxRef(arc_box) = &recv {
if arc_box.as_any().downcast_ref::<crate::instance_v2::InstanceBox>().is_some() {
if let Some(func_name) = self.try_poly_pic(&pic_key, &recv) {
let mut vm_args = Vec::with_capacity(1 + args.len());
vm_args.push(recv.clone());
for a in args { vm_args.push(self.get_value(*a)?); }
@ -591,12 +758,7 @@ impl VM {
if let Some(dst_id) = dst { self.set_value(dst_id, res); }
return Ok(ControlFlow::Continue);
}
}
}
// Mono-PIC direct call: if we cached a target for this site and receiver is InstanceBox, use it
if let VMValue::BoxRef(arc_box) = &recv {
if arc_box.as_any().downcast_ref::<crate::instance_v2::InstanceBox>().is_some() {
// Fallback to Mono-PIC (legacy) if present
if let Some(func_name) = self.boxcall_pic_funcname.get(&pic_key).cloned() {
// Build VM args: receiver first, then original args
let mut vm_args = Vec::with_capacity(1 + args.len());
@ -632,10 +794,39 @@ impl VM {
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 {
// Prefer BufferBox → bytes
if let Some(buf) = a.as_any().downcast_ref::<crate::boxes::buffer::BufferBox>() {
let snapshot = buf.to_vec();
crate::runtime::plugin_ffi_common::encode::bytes(&mut tlv, &snapshot);
continue;
}
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);
// Prefer 32-bit if fits, else i64
if i.value >= i32::MIN as i64 && i.value <= i32::MAX as i64 {
crate::runtime::plugin_ffi_common::encode::i32(&mut tlv, i.value as i32);
} else {
crate::runtime::plugin_ffi_common::encode::i64(&mut tlv, i.value as i64);
}
} else if let Some(b) = a.as_any().downcast_ref::<crate::box_trait::BoolBox>() {
crate::runtime::plugin_ffi_common::encode::bool(&mut tlv, b.value);
} else if let Some(f) = a.as_any().downcast_ref::<crate::boxes::math_box::FloatBox>() {
crate::runtime::plugin_ffi_common::encode::f64(&mut tlv, f.value);
} else if let Some(arr) = a.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() {
// Try encode as bytes if all elements are 0..255 integers
let items = arr.items.read().unwrap();
let mut tmp = Vec::with_capacity(items.len());
let mut ok = true;
for item in items.iter() {
if let Some(intb) = item.as_any().downcast_ref::<crate::box_trait::IntegerBox>() {
if intb.value >= 0 && intb.value <= 255 { tmp.push(intb.value as u8); } else { ok = false; break; }
} else {
ok = false; break;
}
}
if ok { crate::runtime::plugin_ffi_common::encode::bytes(&mut tlv, &tmp); }
else { enc_failed = true; break; }
} 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 {
@ -657,16 +848,17 @@ impl VM {
)
};
if code == 0 {
// Record TypeMeta thunk for plugin invoke so next time VT path can hit
let tm = crate::runtime::type_meta::get_or_create_type_meta(&p.box_type);
tm.set_thunk_plugin_invoke(mid as usize, mid as u16);
// 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
let vm_out = if let Some((tag, _sz, payload)) = crate::runtime::plugin_ffi_common::decode::tlv_first(&out[..out_len]) {
match tag {
1 => crate::runtime::plugin_ffi_common::decode::bool(payload).map(VMValue::Bool).unwrap_or(VMValue::Void),
2 => crate::runtime::plugin_ffi_common::decode::i32(payload).map(|v| VMValue::Integer(v as i64)).unwrap_or(VMValue::Void),
5 => crate::runtime::plugin_ffi_common::decode::f64(payload).map(VMValue::Float).unwrap_or(VMValue::Void),
6 => VMValue::String(crate::runtime::plugin_ffi_common::decode::string(payload)),
_ => VMValue::Void,
}
} else {
VMValue::Void
@ -688,12 +880,18 @@ impl VM {
// If this is a user InstanceBox, redirect to lowered function: Class.method/arity
if let Some(inst) = arc_box.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
let func_name = format!("{}.{}{}", inst.class_name, method, format!("/{}", args.len()));
// Populate vtable cache if method_id is known
// Populate TypeMeta thunk table and legacy vtable cache if method_id is known
if let Some(mid) = method_id {
// TypeMeta preferred store (MIR target)
let tm = crate::runtime::type_meta::get_or_create_type_meta(&inst.class_name);
tm.set_thunk_mir_target(mid as usize, func_name.clone());
// Legacy cache retained for compatibility
let vkey = self.build_vtable_key(&inst.class_name, mid, args.len());
self.boxcall_vtable_funcname.entry(vkey).or_insert(func_name.clone());
}
// If this call-site is hot, cache the function name for direct calls next time
// Record in Poly-PIC immediately (size <= 4)
self.record_poly_pic(&pic_key, &recv, &func_name);
// If this call-site is hot, also cache in legacy Mono-PIC for backward behavior
const PIC_THRESHOLD: u32 = 8;
if self.pic_hits(&pic_key) >= PIC_THRESHOLD {
self.boxcall_pic_funcname.insert(pic_key.clone(), func_name.clone());
@ -711,6 +909,12 @@ impl VM {
}
// Otherwise, direct box method call
if debug_boxcall { eprintln!("[BoxCall] Taking BoxRef path for method '{}'", method); }
// Populate TypeMeta for builtin path if method_id present (BuiltinCall thunk)
if let Some(mid) = method_id {
let label = arc_box.type_name().to_string();
let tm = crate::runtime::type_meta::get_or_create_type_meta(&label);
tm.set_thunk_builtin(mid as usize, method.to_string());
}
let cloned_box = arc_box.share_box();
self.call_box_method(cloned_box, method, nyash_args)?
}