feat(naming): Phase 25.4-A - NamingBox SSOT化完了

🎯 目的: static/global 呼び出しの名前決定を src/mir/naming.rs に一本化

 実装完了:
- NamingBox(src/mir/naming.rs)実装
  - encode_static_method(box, method, arity)
  - normalize_static_global_name(func_name)
  - static/global 名前の正規化ロジック統一

- MIR Builder統合(SSOT使用)
  - src/mir/builder/decls.rs: build_static_main_box
  - src/mir/builder/exprs.rs: 静的メソッド呼び出し
  - src/mir/builder/metadata/propagate.rs: メタデータ伝播
  - src/mir/builder/observe/mod.rs: Observe機能
  - src/mir/builder/observe/types.rs: 型観測(新規)

- VM実行器統合(SSOT使用)
  - src/backend/mir_interpreter/handlers/calls/global.rs
  - normalize_static_global_name使用
  - レガシーフォールバック削除済み確認

- テスト追加
  - src/tests/mir_static_box_naming.rs
  - encode/normalize の動作検証

📚 ドキュメント:
- docs/development/architecture/mir-naming-box.md
  - NamingBoxの設計思想
  - SSOT原則の説明
  - 使用例

🎯 効果:
- 名前決定ロジックが1箇所に集約
- Builder/VM で同じ正規化ルールを使用
- 将来の名前空間拡張が容易

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-11-21 09:01:43 +09:00
parent 380a724b9c
commit 28a312ea0d
11 changed files with 274 additions and 10 deletions

View File

@ -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.7Methodize Static Boxesでは `Global("Box.method")` を「単一インスタンスを持つ Method 呼び出し」に寄せる予定。
- NamingBox はその前段として「名前の正規化」を共有化する箱。Method 化するときもこのルールを踏襲し、Box 名のゆらぎを防ぐ。

View File

@ -6,11 +6,17 @@ impl MirInterpreter {
func_name: &str,
args: &[ValueId],
) -> Result<VMValue, VMError> {
// 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<VMValue> = 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)),
),
}
}
}

View File

@ -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())?;
}
}

View File

@ -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);

View File

@ -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);
}

View File

@ -5,3 +5,4 @@
pub mod resolve;
pub mod ssa;
pub mod types;

View File

@ -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<bool> = 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
);
}
}

View File

@ -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 rulesNamingBox
pub mod instruction;
pub mod instruction_introspection; // Introspection helpers for tests (instruction names)
pub mod instruction_kinds; // small kind-specific metadata (Const/BinOp)

46
src/mir/naming.rs Normal file
View File

@ -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()
}

View File

@ -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 Stage3 + 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<String> = 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::<Vec<_>>()
.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");
}

View File

@ -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;