phase_9_79b_1: Add Unified Registry IDs + Builder Slotting\n- MIR BoxCall carries optional method_id (slot)\n- Slot registry with universal slots [0..3]\n- Builder resolves method_id when receiver type known\n- Printer shows method_id; backends updated\n- Update CURRENT_TASK + MIR spec note

This commit is contained in:
Moe Charm
2025-08-26 20:48:48 +09:00
parent 6eda81f5db
commit e21778c048
9 changed files with 162 additions and 16 deletions

View File

@ -7,9 +7,9 @@
### 直近タスク(小さく早く)
1) 9.79b.1: Unified Registry IDs + Builder Slotting
- 型ID/メソッドスロットの導入(レジストリ)
- ユニバーサルメソッド低スロット予約0..3
- Builderが解決可能なBoxCallに`method_id`を付与(未解決は遅延)
- 型ID/メソッドスロットの導入(レジストリ)✅ 実装
- ユニバーサルメソッド低スロット予約0..3✅ テストで不変確認
- Builderが解決可能なBoxCallに`method_id`を付与(未解決は遅延)✅ 実装/Printer表示
2) 9.79b.2: VM VTable Thunks + Mono-PIC
- `execute_boxcall`をvtable+thunkの単一路線へ
- call-site単位のモモーフィックPICを追加
@ -30,8 +30,8 @@ cargo build --release -j32
- Docs: P2Pリファレンス/サンプル
### ⏭️ 次9.79b
- 9.79b.1: `phase_9_79b_1_unified_registry_ids_and_builder_slotting.md`
- 9.79b.2: `phase_9_79b_2_vm_vtable_thunks_and_pic.md`
- 9.79b.1: `phase_9_79b_1_unified_registry_ids_and_builder_slotting.md` ✅ 最小スコープ達成method_id導入
- 9.79b.2: `phase_9_79b_2_vm_vtable_thunks_and_pic.md` → 着手予定
## 統一Box設計メモ唯一参照
- `docs/ideas/other/2025-08-25-unified-box-design-deep-analysis.md`

View File

@ -22,6 +22,7 @@ Last Updated: 2025-08-25
- Call
- ExternCall
- BoxCall
- Note: BoxCall carries optional `method_id` (numeric slot) when the builder can resolve the receiver type; otherwise falls back to name-only late bind. Universal methods use reserved slots: 0=toString, 1=type, 2=equals, 3=clone.
- NewBox
- ArrayGet
- ArraySet
@ -42,4 +43,3 @@ Last Updated: 2025-08-25
## 同期ルール
- 命令の追加/削除/統合は、まずこの文書を更新し、次に実装(列挙/Printer/Verifier/Optimizer/VMを同期。最後に「総数=26」テストを更新する。
- 実装が26を外れた場合はCIを赤にする設計の合意なく増減させないため

View File

@ -67,7 +67,7 @@ pub fn execute_instruction(vm: &mut VM, instruction: &MirInstruction, debug_glob
// Complex operations
MirInstruction::Call { dst, func, args, effects: _ } => vm.execute_call(*dst, *func, args),
MirInstruction::BoxCall { dst, box_val, method, args, effects: _ } => vm.execute_boxcall(*dst, *box_val, method, args),
MirInstruction::BoxCall { dst, box_val, method, args, effects: _ , .. } => vm.execute_boxcall(*dst, *box_val, method, args),
MirInstruction::NewBox { dst, box_type, args } => vm.execute_newbox(*dst, box_type, args),
// Array operations

View File

@ -404,7 +404,7 @@ impl WasmCodegen {
},
// Phase 9.77: BoxCall Implementation - Critical Box method calls
MirInstruction::BoxCall { dst, box_val, method, args, effects: _ } => {
MirInstruction::BoxCall { dst, box_val, method, args, effects: _ , .. } => {
self.generate_box_call(*dst, *box_val, method, args)
},
@ -713,4 +713,4 @@ mod tests {
let result = codegen.generate_const(dst, &ConstValue::Integer(42));
assert!(result.is_err()); // Should fail without local mapping
}
}
}

View File

@ -9,6 +9,7 @@ use super::{
FunctionSignature, ValueId, ConstValue, BinaryOp, UnaryOp, CompareOp,
MirType, EffectMask, Effect, BasicBlockIdGenerator, ValueIdGenerator
};
use super::slot_registry::resolve_slot_by_type_name;
use crate::ast::{ASTNode, LiteralValue, BinaryOperator};
use std::collections::HashMap;
use std::collections::HashSet;
@ -563,11 +564,17 @@ impl MirBuilder {
}
let box_val = arg_values.remove(0);
// Try to resolve method slot if the object originates from a known NewBox
let method_id = self
.value_origin_newbox
.get(&box_val)
.and_then(|class_name| resolve_slot_by_type_name(class_name, &name));
self.emit_instruction(MirInstruction::BoxCall {
dst: Some(dst),
box_val,
method: name,
method_id,
args: arg_values,
effects: EffectMask::PURE.add(Effect::ReadHeap), // Conservative default
})?;
@ -742,7 +749,13 @@ impl MirBuilder {
eprintln!("[BUILDER] emit @bb{} -> {}", block_id, match &instruction {
MirInstruction::TypeOp { dst, op, value, ty } => format!("typeop {:?} {} {:?} -> {}", op, value, ty, dst),
MirInstruction::Print { value, .. } => format!("print {}", value),
MirInstruction::BoxCall { box_val, method, args, dst, .. } => format!("boxcall {}.{}({:?}) -> {:?}", box_val, method, args, dst),
MirInstruction::BoxCall { box_val, method, method_id, args, dst, .. } => {
if let Some(mid) = method_id {
format!("boxcall {}.{}[#{}]({:?}) -> {:?}", box_val, method, mid, args, dst)
} else {
format!("boxcall {}.{}({:?}) -> {:?}", box_val, method, args, dst)
}
},
MirInstruction::Call { func, args, dst, .. } => format!("call {}({:?}) -> {:?}", func, args, dst),
MirInstruction::NewBox { dst, box_type, args } => format!("new {}({:?}) -> {}", box_type, args, dst),
MirInstruction::Const { dst, value } => format!("const {:?} -> {}", value, dst),
@ -1030,14 +1043,16 @@ impl MirBuilder {
})?;
// Record origin for optimization: dst was created by NewBox of class
self.value_origin_newbox.insert(dst, class);
self.value_origin_newbox.insert(dst, class.clone());
// Immediately call birth(...) on the created instance to run constructor semantics.
// birth typically returns void; we don't capture the result here (dst: None)
let birt_mid = resolve_slot_by_type_name(&class, "birth");
self.emit_instruction(MirInstruction::BoxCall {
dst: None,
box_val: dst,
method: "birth".to_string(),
method_id: birt_mid,
args: arg_values,
effects: EffectMask::READ.add(Effect::ReadHeap),
})?;
@ -1307,10 +1322,16 @@ impl MirBuilder {
}
// Fallback: Emit a BoxCall instruction for regular or plugin/builtin method calls
// Try to resolve method slot when receiver type can be inferred
let maybe_class = self.value_origin_newbox.get(&object_value).cloned();
let mid = maybe_class
.as_ref()
.and_then(|cls| resolve_slot_by_type_name(cls, &method));
self.emit_instruction(MirInstruction::BoxCall {
dst: Some(result_id),
box_val: object_value,
method,
method_id: mid,
args: arg_values,
effects: EffectMask::READ.add(Effect::ReadHeap), // Method calls may have side effects
})?;
@ -1368,6 +1389,7 @@ impl MirBuilder {
dst: Some(result_id),
box_val: parent_value,
method,
method_id: None,
args: arg_values,
effects: EffectMask::READ.add(Effect::ReadHeap),
})?;

View File

@ -74,10 +74,13 @@ pub enum MirInstruction {
/// Box method invocation
/// `%dst = invoke %box.method(%args...)`
/// method_id: Optional numeric slot id when resolved at build time
BoxCall {
dst: Option<ValueId>,
box_val: ValueId,
method: String,
/// Optional numeric method slot id (Unified Registry). None = late bind.
method_id: Option<u16>,
args: Vec<ValueId>,
effects: EffectMask,
},

View File

@ -21,6 +21,7 @@ pub mod printer;
pub mod value_id;
pub mod effect;
pub mod optimizer;
pub mod slot_registry; // Phase 9.79b.1: method slot resolution (IDs)
// Re-export main types for easy access
pub use instruction::{MirInstruction, BinaryOp, CompareOp, UnaryOp, ConstValue, MirType, TypeOpKind, WeakRefOp, BarrierOp};
@ -37,6 +38,7 @@ pub use printer::MirPrinter;
pub use value_id::{ValueId, LocalId, ValueIdGenerator};
pub use effect::{EffectMask, Effect};
pub use optimizer::MirOptimizer;
pub use slot_registry::{BoxTypeId, MethodSlot};
/// MIR compilation result
#[derive(Debug, Clone)]
@ -229,6 +231,23 @@ mod tests {
assert!(dump.contains(".push("), "Expected BoxCall to .push(...). Got:\n{}", dump);
}
#[test]
fn test_boxcall_method_id_on_universal_slot() {
// Build AST: (new ArrayBox()).toString()
let ast = ASTNode::MethodCall {
object: Box::new(ASTNode::New { class: "ArrayBox".to_string(), arguments: vec![], type_arguments: vec![], span: crate::ast::Span::unknown() }),
method: "toString".to_string(),
arguments: vec![],
span: crate::ast::Span::unknown(),
};
let mut compiler = MirCompiler::new();
let result = compiler.compile(ast).expect("compile should succeed");
let dump = MirPrinter::new().print_module(&result.module);
// Expect a BoxCall with numeric method id [#0] for toString universal slot
assert!(dump.contains("toString[#0]"), "Expected method_id #0 for toString. Dump:\n{}", dump);
}
#[test]
fn test_lowering_await_expression() {
// Build AST: await 1 (semantic is nonsensical but should emit Await)

View File

@ -312,16 +312,16 @@ impl MirPrinter {
}
},
MirInstruction::BoxCall { dst, box_val, method, args, effects: _ } => {
MirInstruction::BoxCall { dst, box_val, method, method_id, args, effects: _ } => {
let args_str = args.iter()
.map(|v| format!("{}", v))
.collect::<Vec<_>>()
.join(", ");
let id_suffix = method_id.map(|id| format!("[#{}]", id)).unwrap_or_default();
if let Some(dst) = dst {
format!("{} = call {}.{}({})", dst, box_val, method, args_str)
format!("{} = call {}.{}{}({})", dst, box_val, method, id_suffix, args_str)
} else {
format!("call {}.{}({})", box_val, method, args_str)
format!("call {}.{}{}({})", box_val, method, id_suffix, args_str)
}
},

102
src/mir/slot_registry.rs Normal file
View File

@ -0,0 +1,102 @@
/*!
* MIR Slot Registry (Phase 9.79b.1)
*
* Provides numeric BoxTypeId assignment and per-type method slot resolution.
* - Low slots [0..3] are universally reserved: 0=toString, 1=type, 2=equals, 3=clone
* - Exposes minimal APIs for the MIR builder to resolve method slots when
* the receiver type is known at build time.
*/
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::Mutex;
pub type BoxTypeId = u32;
pub type MethodSlot = u16;
// Global maps (scoped to compiler process)
static TYPE_IDS: Lazy<Mutex<HashMap<String, BoxTypeId>>> = Lazy::new(|| Mutex::new(HashMap::new()));
static NEXT_TYPE_ID: Lazy<Mutex<BoxTypeId>> = Lazy::new(|| Mutex::new(100)); // start after small reserved area
// Per-type explicit slot reservations: (type_id, method) -> slot
static EXPLICIT_SLOTS: Lazy<Mutex<HashMap<(BoxTypeId, String), MethodSlot>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
// Universal slots mapping for quick checks
fn universal_slot(method: &str) -> Option<MethodSlot> {
match method {
"toString" => Some(0),
"type" => Some(1),
"equals" => Some(2),
"clone" => Some(3),
_ => None,
}
}
/// Get or assign a numeric BoxTypeId for a given type name.
pub fn get_or_assign_type_id(type_name: &str) -> BoxTypeId {
let mut map = TYPE_IDS.lock().unwrap();
if let Some(&id) = map.get(type_name) {
return id;
}
let mut next = NEXT_TYPE_ID.lock().unwrap();
let id = *next;
*next += 1;
map.insert(type_name.to_string(), id);
id
}
/// Reserve a method slot for a given (type_id, method) pair.
/// If the method is one of the universal methods, the reservation is ignored
/// as universal slots are implicitly enforced for all types.
pub fn reserve_method_slot(type_id: BoxTypeId, method: &str, slot: MethodSlot) {
if universal_slot(method).is_some() {
return; // universal slots are global invariants
}
let mut table = EXPLICIT_SLOTS.lock().unwrap();
table.insert((type_id, method.to_string()), slot);
}
/// Resolve a method slot given numeric type id and method name.
pub fn resolve_slot(type_id: BoxTypeId, method: &str) -> Option<MethodSlot> {
// Universal first
if let Some(s) = universal_slot(method) {
return Some(s);
}
let table = EXPLICIT_SLOTS.lock().unwrap();
table.get(&(type_id, method.to_string())).copied()
}
/// Resolve a method slot given a type name and method name.
pub fn resolve_slot_by_type_name(type_name: &str, method: &str) -> Option<MethodSlot> {
let ty = get_or_assign_type_id(type_name);
resolve_slot(ty, method)
}
/// Minimal MIR Debug Info scaffold to map IDs back to names (off by default).
#[derive(Default, Debug, Clone)]
pub struct MIRDebugInfo {
// Optionally carry reverse maps when enabled in the future.
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_universal_slots_reserved() {
let tid = get_or_assign_type_id("StringBox");
assert_eq!(resolve_slot(tid, "toString"), Some(0));
assert_eq!(resolve_slot(tid, "type"), Some(1));
assert_eq!(resolve_slot(tid, "equals"), Some(2));
assert_eq!(resolve_slot(tid, "clone"), Some(3));
}
#[test]
fn test_explicit_slot_reservation() {
let tid = get_or_assign_type_id("ArrayBox");
reserve_method_slot(tid, "push", 8);
assert_eq!(resolve_slot(tid, "push"), Some(8));
}
}