diff --git a/docs/development/architecture/mir-naming-box.md b/docs/development/architecture/mir-naming-box.md new file mode 100644 index 00000000..668a9339 --- /dev/null +++ b/docs/development/architecture/mir-naming-box.md @@ -0,0 +1,30 @@ +# MIR NamingBox — static box naming rules + +目的: Builder と VM で「static box メソッドの名前」を一箇所で定義し、`main._nop/0` などのケースズレを防ぐ。 + +## 役割 +- `canonical_box_name(raw)`: + - `main` → `Main` に正規化(最小限の補正) + - それ以外はそのまま返す(仕様拡張は別フェーズで検討) +- `encode_static_method(box, method, arity)`: + - `Box.method/arity` 形式にまとめる(Builder 側で defs→MIR 関数化時に使用) +- `normalize_static_global_name(func_name)`: + - `main._nop/0` のような global 呼び出しを `Main._nop/0` に正規化(VM 側で実行前に使用) + +## 呼び出し箇所 +- Builder (`src/mir/builder/decls.rs`): + - `build_static_main_box` が static メソッドを関数化する際に `encode_static_method` を使用。 + - `Main._nop/0` のような名前がここで確定する。 +- VM (`src/backend/mir_interpreter/handlers/calls/global.rs`): + - `execute_global_function` が `normalize_static_global_name` を通してから function table を検索。 + - canonical 名(例: `Main._nop/0`)→元の名(互換用)の順に探す。 + +## 追加テスト +- `src/tests/mir_static_box_naming.rs`: + - `Main._nop/0` の defs が MIR module に存在することを確認。 + - `me._nop()` 呼び出しが Global call として `_nop/0` を指していることを観測。 + - `NYASH_TO_I64_FORCE_ZERO=1` 下で `apps/tests/minimal_to_i64_void.hako` を VM 実行し、静的メソッド呼び出し経路が通ることを確認。 + +## Phase 21.7 との関係 +- Phase 21.7(Methodize Static Boxes)では `Global("Box.method")` を「単一インスタンスを持つ Method 呼び出し」に寄せる予定。 +- NamingBox はその前段として「名前の正規化」を共有化する箱。Method 化するときもこのルールを踏襲し、Box 名のゆらぎを防ぐ。 diff --git a/src/backend/mir_interpreter/handlers/calls/global.rs b/src/backend/mir_interpreter/handlers/calls/global.rs index 429e1c89..35b132f6 100644 --- a/src/backend/mir_interpreter/handlers/calls/global.rs +++ b/src/backend/mir_interpreter/handlers/calls/global.rs @@ -6,11 +6,17 @@ impl MirInterpreter { func_name: &str, args: &[ValueId], ) -> Result { - // Normalize arity suffix for extern-like dispatch, but keep original name + // NamingBox: static box 名の正規化(main._nop/0 → Main._nop/0 など) + let canonical = crate::mir::naming::normalize_static_global_name(func_name); + + // Normalize arity suffix for extern-like dispatch, but keep canonical/original name // for module-local function table lookup (functions may carry arity suffix). - let base = super::super::utils::normalize_arity_suffix(func_name); - // Module-local/global function: execute by function table if present (use original name) - if let Some(func) = self.functions.get(func_name).cloned() { + let base = super::super::utils::normalize_arity_suffix(&canonical); + + // Module-local/global function: execute by function table if present. + // まず canonical 名で探す(Main._nop/0 など)。Phase 25.x 時点では + // レガシー名での再探索は廃止し、NamingBox 側の正規化に一本化する。 + if let Some(func) = self.functions.get(&canonical).cloned() { let mut argv: Vec = Vec::with_capacity(args.len()); for a in args { argv.push(self.reg_load(*a)?); @@ -136,7 +142,9 @@ impl MirInterpreter { } Ok(VMValue::Void) } - _ => Err(self.err_with_context("global function", &format!("Unknown: {}", func_name))), + _ => Err( + self.err_with_context("global function", &format!("Unknown: {}", func_name)), + ), } } } diff --git a/src/mir/builder/decls.rs b/src/mir/builder/decls.rs index 258f596e..5e392769 100644 --- a/src/mir/builder/decls.rs +++ b/src/mir/builder/decls.rs @@ -19,7 +19,12 @@ impl super::MirBuilder { continue; } if let ASTNode::FunctionDeclaration { params, body, .. } = mast { - let func_name = format!("{}.{}{}", box_name, mname, format!("/{}", params.len())); + // NamingBox 経由で static メソッド名を一元管理する + let func_name = crate::mir::naming::encode_static_method( + &box_name, + mname, + params.len(), + ); self.lower_static_method_as_function(func_name, params.clone(), body.clone())?; } } diff --git a/src/mir/builder/exprs.rs b/src/mir/builder/exprs.rs index 6e3fa490..8586a8c8 100644 --- a/src/mir/builder/exprs.rs +++ b/src/mir/builder/exprs.rs @@ -3,6 +3,7 @@ use super::{MirInstruction, ValueId}; use crate::ast::{ ASTNode, AssignStmt, BinaryExpr, CallExpr, FieldAccessExpr, MethodCallExpr, ReturnStmt, }; +use crate::mir::builder::observe::types as type_trace; impl super::MirBuilder { // Main expression dispatcher @@ -295,10 +296,20 @@ impl super::MirBuilder { args: vec![], effects: super::EffectMask::MUT, })?; - self.value_origin_newbox - .insert(arr_id, "ArrayBox".to_string()); + self.value_origin_newbox.insert(arr_id, "ArrayBox".to_string()); self.value_types .insert(arr_id, super::MirType::Box("ArrayBox".to_string())); + // TypeRegistry + trace for deterministic debug + self.type_registry + .record_newbox(arr_id, "ArrayBox".to_string()); + self.type_registry + .record_type(arr_id, super::MirType::Box("ArrayBox".to_string())); + type_trace::origin("newbox:ArrayLiteral", arr_id, "ArrayBox"); + type_trace::ty( + "newbox:ArrayLiteral", + arr_id, + &super::MirType::Box("ArrayBox".to_string()), + ); for e in elements { let v = self.build_expression_impl(e)?; self.emit_instruction(MirInstruction::BoxCall { @@ -328,10 +339,19 @@ impl super::MirBuilder { args: vec![], effects: super::EffectMask::MUT, })?; - self.value_origin_newbox - .insert(map_id, "MapBox".to_string()); + self.value_origin_newbox.insert(map_id, "MapBox".to_string()); self.value_types .insert(map_id, super::MirType::Box("MapBox".to_string())); + self.type_registry + .record_newbox(map_id, "MapBox".to_string()); + self.type_registry + .record_type(map_id, super::MirType::Box("MapBox".to_string())); + type_trace::origin("newbox:MapLiteral", map_id, "MapBox"); + type_trace::ty( + "newbox:MapLiteral", + map_id, + &super::MirType::Box("MapBox".to_string()), + ); for (k, expr) in entries { // const string key let k_id = crate::mir::builder::emission::constant::emit_string(self, k); diff --git a/src/mir/builder/metadata/propagate.rs b/src/mir/builder/metadata/propagate.rs index d6b66128..e38cb2cf 100644 --- a/src/mir/builder/metadata/propagate.rs +++ b/src/mir/builder/metadata/propagate.rs @@ -5,6 +5,7 @@ //! NYASH_USE_TYPE_REGISTRY=1 で TypeRegistry 経由に切り替え(段階的移行) use crate::mir::builder::MirBuilder; +use crate::mir::builder::observe::types as type_trace; use crate::mir::{MirType, ValueId}; /// src から dst へ builder 内メタデータ(value_types / value_origin_newbox)を伝播する。 @@ -25,6 +26,7 @@ pub fn propagate(builder: &mut MirBuilder, src: ValueId, dst: ValueId) { builder.value_origin_newbox.insert(dst, cls); } } + type_trace::propagate("meta", src, dst); } /// dst に型注釈を明示的に設定し、必要ならば起源情報を消去/維持する。 @@ -34,6 +36,8 @@ pub fn propagate(builder: &mut MirBuilder, src: ValueId, dst: ValueId) { pub fn propagate_with_override(builder: &mut MirBuilder, dst: ValueId, ty: MirType) { let use_registry = std::env::var("NYASH_USE_TYPE_REGISTRY").ok().as_deref() == Some("1"); + // clone once for dual paths + trace + let ty_clone = ty.clone(); if use_registry { // 🎯 新: TypeRegistry 経由 builder.type_registry.record_type(dst, ty); @@ -41,4 +45,5 @@ pub fn propagate_with_override(builder: &mut MirBuilder, dst: ValueId, ty: MirTy // 従来: 直接アクセス builder.value_types.insert(dst, ty); } + type_trace::ty("override", dst, &ty_clone); } diff --git a/src/mir/builder/observe/mod.rs b/src/mir/builder/observe/mod.rs index 01e68b10..11baa62b 100644 --- a/src/mir/builder/observe/mod.rs +++ b/src/mir/builder/observe/mod.rs @@ -5,3 +5,4 @@ pub mod resolve; pub mod ssa; +pub mod types; diff --git a/src/mir/builder/observe/types.rs b/src/mir/builder/observe/types.rs new file mode 100644 index 00000000..337f970a --- /dev/null +++ b/src/mir/builder/observe/types.rs @@ -0,0 +1,37 @@ +//! Type trace helpers (dev-only; default OFF) +//! +//! Enable with `NYASH_MIR_TYPE_TRACE=1` to dump type/origin events during MIR build. +//! 既存の value_types / value_origin_newbox に触れる箇所へ薄く差し込むだけで、 +//! TypeRegistry の移行なしに観測ラインを確保する小粒ガードだよ。 + +use crate::mir::{MirType, ValueId}; +use std::sync::OnceLock; + +fn enabled() -> bool { + static FLAG: OnceLock = OnceLock::new(); + *FLAG.get_or_init(|| std::env::var("NYASH_MIR_TYPE_TRACE").ok().as_deref() == Some("1")) +} + +/// Trace when a newbox/class origin is registered. +pub fn origin(event: &str, vid: ValueId, class: &str) { + if enabled() { + eprintln!("[type-trace] origin:{} %{} ← {}", event, vid.0, class); + } +} + +/// Trace when a concrete MirType is recorded. +pub fn ty(event: &str, vid: ValueId, ty: &MirType) { + if enabled() { + eprintln!("[type-trace] type:{} %{} ← {:?}", event, vid.0, ty); + } +} + +/// Trace propagation between ValueIds. +pub fn propagate(event: &str, src: ValueId, dst: ValueId) { + if enabled() { + eprintln!( + "[type-trace] propagate:{} %{} → %{}", + event, src.0, dst.0 + ); + } +} diff --git a/src/mir/mod.rs b/src/mir/mod.rs index b617fb16..d606a613 100644 --- a/src/mir/mod.rs +++ b/src/mir/mod.rs @@ -12,6 +12,7 @@ pub mod builder; pub mod definitions; // Unified MIR definitions (MirCall, Callee, etc.) pub mod effect; pub mod function; +pub mod naming; // Static box / entry naming rules(NamingBox) pub mod instruction; pub mod instruction_introspection; // Introspection helpers for tests (instruction names) pub mod instruction_kinds; // small kind-specific metadata (Const/BinOp) diff --git a/src/mir/naming.rs b/src/mir/naming.rs new file mode 100644 index 00000000..29dad92e --- /dev/null +++ b/src/mir/naming.rs @@ -0,0 +1,46 @@ +//! MIR NamingBox — static box / entry naming rules +//! +// 責務: +// - static box メソッドを MIR 関数名にエンコード/デコードする。 +// - 「Main._nop/0」などの名前付けを一箇所に集約し、Builder/Interpreter 間で共有する。 +// - 当面は minimal: `main.*` → `Main.*` のケースを安全に扱う。 +//! +// 非責務: +// - 動的 dispatch や BoxFactory 側の名前解決。 +// - エントリポイント選択(NYASH_ENTRY)のポリシー決定。 + +/// Encode a static box method into a MIR function name: `BoxName.method/arity`. +pub fn encode_static_method(box_name: &str, method: &str, arity: usize) -> String { + format!("{}.{}{}", canonical_box_name(box_name), method, format!("/{}", arity)) +} + +/// Canonicalize a static box name for MIR-level usage. +/// +/// 現状のルール: +/// - "main" → "Main"(最小限の補正) +/// - それ以外はそのまま返す(広域な仕様変更は避ける)。 +pub fn canonical_box_name(raw: &str) -> String { + match raw { + "main" => "Main".to_string(), + _ => raw.to_string(), + } +} + +/// If `func_name` looks like a static box method like `main._nop/0`, +/// normalize the box part (`main` → `Main`) and return canonical form. +/// +/// 例: +/// - "main._nop/0" → "Main._nop/0" +/// - "Main._nop/0" → "Main._nop/0"(変化なし) +/// - その他の名前は入力そのまま返す。 +pub fn normalize_static_global_name(func_name: &str) -> String { + if let Some((box_part, rest)) = func_name.split_once('.') { + // rest には "method/arity" が入る想定 + let canon = canonical_box_name(box_part); + if canon != box_part { + return format!("{}.{}", canon, rest); + } + } + func_name.to_string() +} + diff --git a/src/tests/mir_static_box_naming.rs b/src/tests/mir_static_box_naming.rs new file mode 100644 index 00000000..ef5f97a2 --- /dev/null +++ b/src/tests/mir_static_box_naming.rs @@ -0,0 +1,110 @@ +//! Static box naming tests (Main._nop/0 canonicalization) +//! +//! Repro: apps/tests/minimal_to_i64_void.hako +//! - static box Main { _nop(); main(args) { me._nop(); ... } } +//! - VM runtime reported Unknown: main._nop/0 +//! This test compiles the fixture and inspects the MIR module to ensure: +//! 1) Static methods are materialized as canonical names (Main._nop/0) +//! 2) Calls inside Main.main use the canonical/global name + +use crate::ast::ASTNode; +use crate::mir::instruction::MirInstruction; +use crate::mir::{definitions::Callee, MirCompiler}; +use crate::parser::NyashParser; + +fn load_fixture_with_string_helpers() -> String { + // Bundle StringHelpers directly to avoid relying on using resolution in this unit test. + let string_helpers = include_str!("../../lang/src/shared/common/string_helpers.hako"); + let fixture = + std::fs::read_to_string("apps/tests/minimal_to_i64_void.hako").expect("read fixture"); + format!("{}\n\n{}", string_helpers, fixture) +} + +/// Compile minimal_to_i64_void.hako and assert Main._nop/0 exists and is targeted. +#[test] +fn mir_static_main_box_emits_canonical_static_methods() { + // Enable Stage‑3 + using for the fixture. + std::env::set_var("NYASH_PARSER_STAGE3", "1"); + std::env::set_var("HAKO_PARSER_STAGE3", "1"); + std::env::set_var("NYASH_PARSER_ALLOW_SEMICOLON", "1"); + std::env::set_var("NYASH_ENABLE_USING", "1"); + std::env::set_var("HAKO_ENABLE_USING", "1"); + + let src = load_fixture_with_string_helpers(); + let ast: ASTNode = NyashParser::parse_from_string(&src).expect("parse"); + + let mut mc = MirCompiler::with_options(false); + let compiled = mc.compile(ast).expect("compile"); + + // 1) Ensure Main._nop/0 is materialized in the function table. + let mut fn_names: Vec = compiled.module.functions.keys().cloned().collect(); + fn_names.sort(); + assert!( + fn_names.iter().any(|n| n == "Main._nop/0"), + "Main._nop/0 missing. Functions:\n{}", + fn_names.join("\n") + ); + + // 2) Collect global call targets to see how me._nop() was lowered. + let mut global_targets: Vec<(String, String)> = Vec::new(); + for (fname, func) in compiled.module.functions.iter() { + for bb in func.blocks.values() { + for inst in &bb.instructions { + if let MirInstruction::Call { + callee: Some(Callee::Global(t)), + .. + } = inst + { + global_targets.push((fname.clone(), t.clone())); + } + } + if let Some(term) = &bb.terminator { + if let MirInstruction::Call { + callee: Some(Callee::Global(t)), + .. + } = term + { + global_targets.push((fname.clone(), t.clone())); + } + } + } + } + + if !global_targets.iter().any(|(_, t)| t.contains("_nop/0")) { + panic!( + "Expected a global call to *_nop/0; got:\n{}", + global_targets + .iter() + .map(|(f, t)| format!("{} -> {}", f, t)) + .collect::>() + .join("\n") + ); + } +} + +/// Execute the minimal fixture with Void-returning _nop() and confirm it runs through to_i64. +#[test] +fn mir_static_main_box_executes_void_path_with_guard() { + std::env::set_var("NYASH_PARSER_STAGE3", "1"); + std::env::set_var("HAKO_PARSER_STAGE3", "1"); + std::env::set_var("NYASH_PARSER_ALLOW_SEMICOLON", "1"); + std::env::set_var("NYASH_ENABLE_USING", "1"); + std::env::set_var("HAKO_ENABLE_USING", "1"); + + // Accept Void inputs as 0 for this execution path. + std::env::set_var("NYASH_TO_I64_FORCE_ZERO", "1"); + + let src = load_fixture_with_string_helpers(); + let ast: ASTNode = NyashParser::parse_from_string(&src).expect("parse"); + + let mut mc = MirCompiler::with_options(false); + let compiled = mc.compile(ast).expect("compile"); + + use crate::backend::VM; + let mut vm = VM::new(); + let out = vm + .execute_module(&compiled.module) + .expect("VM should execute fixture"); + let s = out.to_string_box().value; + assert_eq!(s, "0", "VM return value should be 0"); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index c7d73e7c..7a940a41 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -13,6 +13,7 @@ pub mod mir_locals_ssa; pub mod mir_loopform_conditional_reassign; pub mod mir_loopform_exit_phi; pub mod mir_loopform_complex; +pub mod mir_static_box_naming; pub mod mir_stage1_using_resolver_verify; pub mod stage1_cli_entry_ssa_smoke; pub mod mir_stageb_like_args_length;