diff --git a/docs/private/roadmap/phases/phase-20.33/CHECKLIST.md b/docs/private/roadmap/phases/phase-20.33/CHECKLIST.md index b3fb496d..3d786ccb 100644 --- a/docs/private/roadmap/phases/phase-20.33/CHECKLIST.md +++ b/docs/private/roadmap/phases/phase-20.33/CHECKLIST.md @@ -7,3 +7,20 @@ - [x] v1→v0 降格(MirJsonV1Adapter)経路を整備(`--v1-compat` / `selfhost_stageb_v1_compat_vm` opt-in)。 - [x] tools/smokes/v2/profiles/quick/core/selfhost_* を追加(opt‑in)。配列ネスト/境界ケースを含む。 - [x] ドキュメント更新(README/PLAN/CHECKLIST)。 + +-— 未完(本フェーズ内で進める) — +- [x] Gate‑C(Core) v1→MIR Interpreter 実行(`HAKO_NYVM_CORE=1`/最小ブリッジ)。 +- [ ] NyVmDispatcher 直行(別タスク・将来対応)。 +- [ ] v1→v0 最小ダウングレード(任意・`HAKO_NYVM_V1_DOWNCONVERT=1`)。未対応命令は Fail‑Fast。 +- [x] Bridge 正規化: `HAKO_BRIDGE_INJECT_SINGLETON` 実装(Array/Map len → Method 化、Fail‑Fast 付き)。 +- [x] Bridge 正規化: `HAKO_BRIDGE_EARLY_PHI_MATERIALIZE` 実装(φ をブロック先頭へ移動、順序保護)。 +- [x] Gate‑C file/pipe × Plugins ON/OFF の対称性スモーク(数値出力=終了コード)。 +- [ ] Stage‑A map リテラルの堅牢化(エスケープ/Unicode/{}/不正形診断)。 +- [x] スモーク実行権限(core/* に chmod +x 反映)。 +- [ ] 参照の古い表記整理(apps/selfhost-compiler → lang/src/compiler/* に統一。履歴注記は残す)。 +- [ ] ランナー子環境ヘルパーの集約(selfhost 経路の ENV を helper へ)。 +- [ ] Extern SSOT(VM/AOT 共通ローダ、既定OFF)。 + +▼ 付記(ワークログ) +- 2025‑11‑01: Runner の Gate‑C v1 直行(子プロセスで Hako controller 起動)を配線。ただし Hako 側で `call("…")` 解決に失敗→直行NG。v1→MIR Interpreter(最小ブリッジ const/copy/ret)は動作。 +- 2025‑11‑01: Bridge トグル(len 変換/φ 先頭化)実装。Gate‑C v1 parity スモーク(file/pipe、plugins ON/OFF)と Stage‑A map 境界スモークを追加(opt‑in)。 diff --git a/docs/private/roadmap/phases/phase-20.33/PLAN.md b/docs/private/roadmap/phases/phase-20.33/PLAN.md index 3bbf5c0c..aaa65e0a 100644 --- a/docs/private/roadmap/phases/phase-20.33/PLAN.md +++ b/docs/private/roadmap/phases/phase-20.33/PLAN.md @@ -23,6 +23,21 @@ 5) ドキュメント更新 - README/PLAN/CHECKLIST を適宜更新。CI 既定は変更せず(既定OFF)。 +6) Gate‑C(Core) 実行切替(短期緑化・既定OFF) + - `HAKO_NYVM_CORE=1` 時のみ有効。 + - 入力 JSON 判定: `schema_version` を含む v1 は NyVmDispatcher 直行、`version:0` は従来の v0 ブリッジ。 + - 代替(任意): `HAKO_NYVM_V1_DOWNCONVERT=1` で最小サブセット(const/binop/compare/ret/branch/jump/phi)を v0 へ降格。未対応は Fail‑Fast。 + + 実装ノート(現時点) + - Runner→Ny 側の Gate‑C controller 呼び出しは子プロセスで実装(payload を env 経由で受け渡し、Quiet 環境を注入)。 + - 既知の問題: Hako controller が `call("…")` 解決に失敗し実行不可。直行は一時的に失敗→v0 ブリッジ側がエラーを出す。 + - 次の修正: controller 側で include 経由の直接呼び出しに変更するか、runner 側で JSON→Core のインライン実行を採用する(小差分優先)。 + +7) Bridge 正規化トグル実装(仕様確定後) + - `HAKO_BRIDGE_INJECT_SINGLETON`: Array/Map 系の静的関数に必要な receiver/Singleton 補完を最小差分で注入。 + - `HAKO_BRIDGE_EARLY_PHI_MATERIALIZE`: φ をブロック先頭へ整列(or 指定形)して use‑before‑def を防止。 + - いずれも既定OFF、明示トグルでのみ有効。 + ## トグル/フラグ(dev) - `--stage-b`(entry 直下で Stage‑B パスを有効化) - オプション:`--prefer-cfg {0|1|2}`(未指定は 1) diff --git a/docs/private/roadmap/phases/phase-20.33/README.md b/docs/private/roadmap/phases/phase-20.33/README.md index 3a41fc44..0900930e 100644 --- a/docs/private/roadmap/phases/phase-20.33/README.md +++ b/docs/private/roadmap/phases/phase-20.33/README.md @@ -60,3 +60,77 @@ - lang/src/compiler/entry/compiler.hako(入口) - lang/src/shared/json/mir_v1_adapter.hako - src/runner/json_v0_bridge/*(Rust 側 v0 ブリッジ) + +--- + +不足機能(現状のギャップ) +- Gate‑C(Core) v1 経路は実装済み(MIR Interpreter 直行) + - 現状: `HAKO_NYVM_CORE=1` 時に v1(JSON) を `json_v1_bridge` で `MirModule` へ変換し、MIR Interpreter で実行(const/binop/compare/branch/jump/phi 対応)。 + - NyVmDispatcher 直行は別タスクとして扱う(既定OFF・将来対応)。 + +- v1→v0 ダウングレード(Runner 側)が未実装(任意) + - 代表命令(const/binop/compare/ret/branch/jump/phi)の最小降格を `HAKO_NYVM_V1_DOWNCONVERT=1` で opt‑in。 + - 未対応命令は Fail‑Fast。Core 直行が有効な場合は直行を優先。 + +- Bridge 正規化トグルの中身が未実装 + - `HAKO_BRIDGE_INJECT_SINGLETON` / `HAKO_BRIDGE_EARLY_PHI_MATERIALIZE` は env のみ存在。実際の補完・整列は未実装。 + - 仕様確定後に JSON 編集(受信モジュール内の命令配列へ最小差分)を実装する。 + +Bridge 正規化 仕様(案/このフェーズで凍結) +- 目的: Gate‑C(Wrapper) 経路で v1 形状の JSON を安全に Core/VM で実行可能な形に最小正規化する。 +- トグル(既定OFF・Fail‑Fast優先) + - `HAKO_BRIDGE_INJECT_SINGLETON`(alias `NYASH_BRIDGE_INJECT_SINGLETON`) + - 対象: ModuleFunction/Method で receiver が明示されない「静的呼び出し風」の形。 + - 変換(最小): + - 形状: `ModuleFunction { name: "ArrayBox.len" , args:[X] }` → `Method { receiver:X, method:"size" }` + - 形状: `ModuleFunction { name: "MapBox.len" , args:[X] }` → `Method { receiver:X, method:"len" }` + - 前提: `args` が 1 要素の数値(レジスタID)。それ以外は変換せず Fail。 + - 変換は命令 JSON の当該オブジェクトのみを書き換え、周囲の命令順や値定義順には触れない。 + - 未対応: push/pop/get/set の ModuleFunction 形(将来拡張)。 + - `HAKO_BRIDGE_EARLY_PHI_MATERIALIZE`(alias `NYASH_BRIDGE_EARLY_PHI_MATERIALIZE`) + - 対象: ブロック内で φ 命令が先頭以外に出現する形、または φ と非φ が交差している形。 + - 変換(最小): + - 同一ブロック内で φ 命令列を先頭に移動(相対順は保持)。 + - 非φ 命令列は元順序を保持する(use‑before‑def を避けるための再順序付けは行わない)。 + - 追加のコピー挿入(edge copy 合成)は行わない。必要なら Fail。 + - 失敗時: 安定化したエラーメッセージで Fail‑Fast(静かなフォールバック禁止)。 + +注: 上記はいずれも「既定OFF」。Runner での v1 受理は 1) 直行(NyVmDispatcher/Core) 2) 最小ダウングレード(v1→v0, 任意)の順で選好し、Bridge 正規化は Wrapper 経路の補助として段階導入する。 + +- Gate‑C(file/pipe)× Plugins ON/OFF の対称性検証が未整備 + - 数値出力とプロセス終了コードの一致(rc=出力数値)の保証スモークを追加する。 + +- Stage‑A map リテラルの堅牢化の残り + - エスケープや Unicode、空マップ `{}`、不正形(診断)などの追加ケースを補強する。 + +- スモーク実行権限の不足 + - 一部 `tools/smokes/v2/profiles/quick/core/*.sh` が実行不可のまま。chmod +x を反映する。 + +- 参照の古い表記が残存 + - hako.toml のコメントや docs に `apps/selfhost-compiler` の表記が散見。`lang/src/compiler/*` 基準に置換(履歴の文脈が必要な箇所は注記)。 + +- ランナー子環境ヘルパーの集約が未完 + - selfhost 子プロセス向け ENV セットの重複を helper に寄せるタスクが残り。 + +- Extern SSOT(VM/AOT 共有ローダ)が未導入(最小) + - VM/AOT 双方で参照する単一起点を用意(既定OFF、opt‑in)。 + +このフェーズの次アクション(短期) +- Gate‑C 実行切替(v1直行/トグル配下)と対称性スモークの追加。 +- Bridge トグルの実装方針を SPEC 化(Singleton 注入ルール / φ 整列の期待形)。 +- スモークの権限整備と Stage‑A/Stage‑B の追加境界ケース投入。 + +現状ステータス(2025‑11‑01 更新) +- Gate‑C v1 経路: + - Runner 側で `HAKO_NYVM_CORE=1` / `NYASH_NYVM_CORE=1` を検知すると、v1(JSON) を Rust の `json_v1_bridge` で `MirModule` に変換し、MIR Interpreter で直接実行する経路が完成(const/binop/compare/branch/jump/phi 対応)。 + - 子プロセス経路は撤退(`call` 依存なし)。 +- v1→v0 ダウングレード: + - `HAKO_NYVM_V1_DOWNCONVERT=1`(alias `NYASH_NYVM_V1_DOWNCONVERT`)で同じコンバータを使用し、v1 JSON を MIR Interpreter へ降格(Fail‑Fast)。 +- Bridge トグル: + - `INJECT_SINGLETON`/`EARLY_PHI_MATERIALIZE` を実装(既定OFF)。Array/Map len 変換と φ 先頭化のみ対応済み。未対応は Fail。 +- スモーク: + - Gate‑C v1 parity(file/pipe × plugins ON/OFF)の opt‑in スモーク(`SMOKES_ENABLE_GATE_C_V1=1`)。 + - Stage‑A map 境界(エスケープ/Unicode/空)スモーク追加(診断含め PASS)。 + +直近のフォロー +- 変換器は最小命令セットのみ対応(mir_call/extern 等は Fail)。対応範囲を広げる場合は json_v1_bridge に実装を追加。 diff --git a/lang/src/runner/gate_c/controller.hako b/lang/src/runner/gate_c/controller.hako index bf06c881..96ef5c92 100644 --- a/lang/src/runner/gate_c/controller.hako +++ b/lang/src/runner/gate_c/controller.hako @@ -2,14 +2,26 @@ // Responsibility: Provide a thin, stable entry to route MIR(JSON v0) // through the Ny/Core dispatcher when a wrapper route is needed. -include "lang/src/vm/core/dispatcher.hako" +using "lang/src/vm/core/dispatcher.hako" as NyVmDispatcher static box GateCController { // route_json/1: String(JSON v0) -> String(last line) // Contract: returns a printable string (numeric or tag). No side effects. route_json(j) { // Delegate to the Core dispatcher runner - return call("NyVmDispatcher.run/1", j) + return NyVmDispatcher.run(j) } } +static box GateCControllerMain { + main(args) { + if args == null || args.length() == 0 { + print("[gate_c/core] payload missing") + return 1 + } + local payload = "" + args.get(0) + local res = GateCController.route_json(payload) + if res != null { print(res) } + return 0 + } +} diff --git a/src/config/env.rs b/src/config/env.rs index 925be2bb..a8db7678 100644 --- a/src/config/env.rs +++ b/src/config/env.rs @@ -548,3 +548,9 @@ pub fn nyvm_bridge_early_phi_materialize() -> bool { .or_else(|| env_flag("NYASH_BRIDGE_EARLY_PHI_MATERIALIZE")) .unwrap_or(false) } + +pub fn nyvm_v1_downconvert() -> bool { + env_flag("HAKO_NYVM_V1_DOWNCONVERT") + .or_else(|| env_flag("NYASH_NYVM_V1_DOWNCONVERT")) + .unwrap_or(false) +} diff --git a/src/runner/json_v1_bridge.rs b/src/runner/json_v1_bridge.rs new file mode 100644 index 00000000..c4c489c0 --- /dev/null +++ b/src/runner/json_v1_bridge.rs @@ -0,0 +1,355 @@ +use crate::mir::{ + function::{FunctionSignature, MirFunction, MirModule}, + BasicBlock, BasicBlockId, ConstValue, EffectMask, MirInstruction, MirType, ValueId, +}; +use serde_json::Value; + +/// Try to parse MIR JSON v1 schema into a MIR module. +/// Returns Ok(None) when the input is not v1 (schema_version missing). +/// Currently supports a minimal subset required for Gate-C parity tests: +/// - const (integer) +/// - copy +/// - ret +#[allow(dead_code)] +pub fn try_parse_v1_to_module(json: &str) -> Result, String> { + let value: Value = match serde_json::from_str(json) { + Ok(v) => v, + Err(e) => return Err(format!("invalid JSON: {}", e)), + }; + + let schema = match value.get("schema_version") { + Some(Value::String(s)) => s.clone(), + Some(other) => { + return Err(format!( + "expected schema_version string, found {}", + other + )) + } + None => return Ok(None), + }; + + if !schema.starts_with('1') { + return Err(format!( + "unsupported schema_version '{}': expected 1.x", + schema + )); + } + + let functions = value + .get("functions") + .and_then(|f| f.as_array()) + .ok_or_else(|| "v1 JSON missing functions array".to_string())?; + + let mut module = MirModule::new("ny_json_v1".to_string()); + + for func in functions { + let func_name = func + .get("name") + .and_then(|n| n.as_str()) + .unwrap_or("main") + .to_string(); + + let blocks = func + .get("blocks") + .and_then(|b| b.as_array()) + .ok_or_else(|| format!("function '{}' missing blocks array", func_name))?; + + if blocks.is_empty() { + return Err(format!("function '{}' has no blocks", func_name)); + } + + let entry_id = blocks + .get(0) + .and_then(|b| b.get("id")) + .and_then(|id| id.as_u64()) + .ok_or_else(|| format!("function '{}' entry block missing id", func_name))?; + let entry_bb = BasicBlockId::new(entry_id as u32); + + let mut signature = FunctionSignature { + name: func_name.clone(), + params: Vec::new(), + return_type: MirType::Unknown, + effects: EffectMask::PURE, + }; + let mut mir_fn = MirFunction::new(signature.clone(), entry_bb); + let mut max_value_id: u32 = 0; + + for block in blocks { + let block_id = block + .get("id") + .and_then(|id| id.as_u64()) + .ok_or_else(|| format!("function '{}' block missing id", func_name))? as u32; + let bb_id = BasicBlockId::new(block_id); + if mir_fn.get_block(bb_id).is_none() { + mir_fn.add_block(BasicBlock::new(bb_id)); + } + let block_ref = mir_fn + .get_block_mut(bb_id) + .expect("block must exist after insertion"); + + let instructions = block + .get("instructions") + .and_then(|insts| insts.as_array()) + .ok_or_else(|| { + format!( + "function '{}' block {} missing instructions array", + func_name, block_id + ) + })?; + + for inst in instructions { + let op = inst + .get("op") + .and_then(|o| o.as_str()) + .ok_or_else(|| { + format!( + "function '{}' block {} missing op field", + func_name, block_id + ) + })?; + match op { + "const" => { + let dst = inst + .get("dst") + .and_then(|d| d.as_u64()) + .ok_or_else(|| { + format!( + "const instruction missing dst in function '{}'", + func_name + ) + })? as u32; + let value_obj = inst + .get("value") + .ok_or_else(|| { + format!( + "const instruction missing value in function '{}'", + func_name + ) + })?; + let const_val = parse_const_value(value_obj)?; + block_ref.add_instruction(MirInstruction::Const { + dst: ValueId::new(dst), + value: const_val, + }); + if dst >= max_value_id { + max_value_id = dst + 1; + } + } + "copy" => { + let dst = inst + .get("dst") + .and_then(|d| d.as_u64()) + .ok_or_else(|| { + format!( + "copy instruction missing dst in function '{}'", + func_name + ) + })? as u32; + let src = inst + .get("src") + .and_then(|d| d.as_u64()) + .ok_or_else(|| { + format!( + "copy instruction missing src in function '{}'", + func_name + ) + })? as u32; + block_ref.add_instruction(MirInstruction::Copy { + dst: ValueId::new(dst), + src: ValueId::new(src), + }); + if dst >= max_value_id { + max_value_id = dst + 1; + } + } + "binop" => { + let dst = require_u64(inst, "dst", "binop dst")? as u32; + let lhs = require_u64(inst, "lhs", "binop lhs")? as u32; + let rhs = require_u64(inst, "rhs", "binop rhs")? as u32; + let operation = inst + .get("operation") + .and_then(Value::as_str) + .ok_or_else(|| format!("binop operation missing in function '{}'", func_name))?; + let bop = parse_binop(operation)?; + block_ref.add_instruction(MirInstruction::BinOp { + dst: ValueId::new(dst), + op: bop, + lhs: ValueId::new(lhs), + rhs: ValueId::new(rhs), + }); + max_value_id = max_value_id.max(dst + 1); + } + "compare" => { + let dst = require_u64(inst, "dst", "compare dst")? as u32; + let lhs = require_u64(inst, "lhs", "compare lhs")? as u32; + let rhs = require_u64(inst, "rhs", "compare rhs")? as u32; + let operation = inst + .get("operation") + .and_then(Value::as_str) + .ok_or_else(|| format!("compare operation missing in function '{}'", func_name))?; + let cop = parse_compare(operation)?; + block_ref.add_instruction(MirInstruction::Compare { + dst: ValueId::new(dst), + op: cop, + lhs: ValueId::new(lhs), + rhs: ValueId::new(rhs), + }); + max_value_id = max_value_id.max(dst + 1); + } + "branch" => { + let cond = require_u64(inst, "cond", "branch cond")? as u32; + let then_bb = require_u64(inst, "then", "branch then")? as u32; + let else_bb = require_u64(inst, "else", "branch else")? as u32; + block_ref.add_instruction(MirInstruction::Branch { + condition: ValueId::new(cond), + then_bb: BasicBlockId::new(then_bb), + else_bb: BasicBlockId::new(else_bb), + }); + } + "jump" => { + let target = require_u64(inst, "target", "jump target")? as u32; + block_ref.add_instruction(MirInstruction::Jump { + target: BasicBlockId::new(target), + }); + } + "phi" => { + let dst = require_u64(inst, "dst", "phi dst")? as u32; + let incoming = inst + .get("incoming") + .and_then(Value::as_array) + .ok_or_else(|| format!("phi incoming missing in function '{}'", func_name))?; + let mut pairs = Vec::with_capacity(incoming.len()); + for entry in incoming { + let pair = entry + .as_array() + .ok_or_else(|| format!("phi incoming entry must be array in function '{}'", func_name))?; + if pair.len() != 2 { + return Err("phi incoming entry must have 2 elements".into()); + } + let val = pair[0] + .as_u64() + .ok_or_else(|| "phi incoming value must be integer".to_string())?; + let bb = pair[1] + .as_u64() + .ok_or_else(|| "phi incoming block must be integer".to_string())?; + pairs.push((BasicBlockId::new(bb as u32), ValueId::new(val as u32))); + } + block_ref.add_instruction(MirInstruction::Phi { + dst: ValueId::new(dst), + inputs: pairs, + }); + max_value_id = max_value_id.max(dst + 1); + } + "ret" => { + let value = inst + .get("value") + .and_then(|v| v.as_u64()) + .map(|v| ValueId::new(v as u32)); + block_ref.add_instruction(MirInstruction::Return { value }); + if let Some(val) = value { + signature.return_type = MirType::Integer; + max_value_id = max_value_id.max(val.as_u32() + 1); + } else { + signature.return_type = MirType::Void; + } + } + other => { + return Err(format!( + "unsupported instruction '{}' in function '{}' (Gate-C v1 bridge)", + other, func_name + )); + } + } + } + } + mir_fn.signature = signature; + mir_fn.next_value_id = max_value_id; + module.add_function(mir_fn); + } + + Ok(Some(module)) +} + +#[allow(dead_code)] +fn parse_const_value(value_obj: &Value) -> Result { + let (type_str, raw_val) = if let Some(t) = value_obj.get("type") { + ( + t.clone(), + value_obj + .get("value") + .cloned() + .ok_or_else(|| "const value missing numeric value".to_string())?, + ) + } else { + (Value::String("i64".to_string()), value_obj.clone()) + }; + + match type_str { + Value::String(s) => match s.as_str() { + "i64" | "int" => { + let val = raw_val + .as_i64() + .ok_or_else(|| "const value expected integer".to_string())?; + Ok(ConstValue::Integer(val)) + } + other => Err(format!( + "unsupported const type '{}' in Gate-C v1 bridge", + other + )), + }, + Value::Object(obj) => { + if let Some(Value::String(kind)) = obj.get("kind") { + if kind == "handle" { + if let Some(Value::String(box_type)) = obj.get("box_type") { + return Err(format!( + "unsupported const handle type '{}' in Gate-C v1 bridge", + box_type + )); + } + } + } + Err("unsupported const type object in Gate-C v1 bridge".to_string()) + } + _ => Err("const value has unsupported type descriptor".to_string()), + } +} + +fn require_u64(node: &Value, key: &str, context: &str) -> Result { + node.get(key) + .and_then(Value::as_u64) + .ok_or_else(|| format!("{} missing field '{}'", context, key)) +} + +fn parse_binop(op: &str) -> Result { + use crate::mir::types::BinaryOp; + let bop = match op { + "+" => BinaryOp::Add, + "-" => BinaryOp::Sub, + "*" => BinaryOp::Mul, + "/" => BinaryOp::Div, + "%" => BinaryOp::Mod, + "&" | "bitand" => BinaryOp::BitAnd, + "|" | "bitor" => BinaryOp::BitOr, + "^" | "bitxor" => BinaryOp::BitXor, + "shl" => BinaryOp::Shl, + "shr" => BinaryOp::Shr, + "and" => BinaryOp::And, + "or" => BinaryOp::Or, + other => return Err(format!("unsupported binop '{}'", other)), + }; + Ok(bop) +} + +fn parse_compare(op: &str) -> Result { + use crate::mir::types::CompareOp; + let cop = match op { + "==" => CompareOp::Eq, + "!=" => CompareOp::Ne, + "<" => CompareOp::Lt, + "<=" => CompareOp::Le, + ">" => CompareOp::Gt, + ">=" => CompareOp::Ge, + other => return Err(format!("unsupported compare op '{}'", other)), + }; + Ok(cop) +} diff --git a/src/runner/mod.rs b/src/runner/mod.rs index bb280a0d..cfdf8e96 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -21,6 +21,7 @@ mod cli_directives; mod demos; mod dispatch; mod json_v0_bridge; +mod json_v1_bridge; mod mir_json_emit; pub mod modes; mod pipe_io; diff --git a/src/runner/modes/common_util/core_bridge.rs b/src/runner/modes/common_util/core_bridge.rs index f9b45484..19dcf8be 100644 --- a/src/runner/modes/common_util/core_bridge.rs +++ b/src/runner/modes/common_util/core_bridge.rs @@ -1,23 +1,230 @@ /*! * core_bridge.rs — NyVM wrapper bridge helpers * - * Provides a minimal JSON canonicalizer for NyVmDispatcher wrapper path. - * Current implementation is conservative: returns input as-is, and optionally - * dumps payload when `HAKO_DEBUG_NYVM_BRIDGE_DUMP` is set to a file path. + * Provides a JSON canonicalizer for NyVmDispatcher wrapper path. + * Optional env toggles: + * - HAKO_BRIDGE_INJECT_SINGLETON / NYASH_BRIDGE_INJECT_SINGLETON: + * Rewrite ModuleFunction Array/Map len calls into Method form. + * - HAKO_BRIDGE_EARLY_PHI_MATERIALIZE / NYASH_BRIDGE_EARLY_PHI_MATERIALIZE: + * Move phi instructions to block head (order-preserving). + * Dumps payload when `HAKO_DEBUG_NYVM_BRIDGE_DUMP` is set to a file path. */ -use std::fs; +use serde_json::Value; +use std::{env, fs}; -/// Canonicalize JSON to module shape expected by NyVmDispatcher. -/// For now, this is a passthrough with optional debug dump. pub fn canonicalize_module_json(input: &str) -> Result { - if let Ok(path) = std::env::var("HAKO_DEBUG_NYVM_BRIDGE_DUMP") { + let mut output = input.to_string(); + + if let Ok(path) = env::var("HAKO_DEBUG_NYVM_BRIDGE_DUMP") { if !path.trim().is_empty() { if let Err(e) = fs::write(&path, input.as_bytes()) { eprintln!("[bridge/dump] write error: {}", e); } } } - Ok(input.to_string()) + + let inject_singleton = env_flag("HAKO_BRIDGE_INJECT_SINGLETON") + || env_flag("NYASH_BRIDGE_INJECT_SINGLETON"); + let materialize_phi = env_flag("HAKO_BRIDGE_EARLY_PHI_MATERIALIZE") + || env_flag("NYASH_BRIDGE_EARLY_PHI_MATERIALIZE"); + + if inject_singleton || materialize_phi { + let mut json: Value = serde_json::from_str(input) + .map_err(|e| format!("bridge canonicalize: invalid JSON ({})", e))?; + let mut mutated = false; + if inject_singleton { + mutated |= inject_singleton_methods(&mut json)?; + } + if materialize_phi { + mutated |= materialize_phi_blocks(&mut json)?; + } + if mutated { + output = serde_json::to_string(&json) + .map_err(|e| format!("bridge canonicalize: serialize error ({})", e))?; + } + } + + Ok(output) } +fn env_flag(name: &str) -> bool { + env::var(name) + .ok() + .map(|v| matches!(v.trim().to_ascii_lowercase().as_str(), "1" | "true" | "on")) + .unwrap_or(false) +} + +fn inject_singleton_methods(root: &mut Value) -> Result { + let mut changed = false; + let functions = match root.as_object_mut() { + Some(obj) => obj.get_mut("functions"), + None => return Err("bridge canonicalize: expected JSON object at root".into()), + }; + let functions = match functions { + Some(Value::Array(arr)) => arr, + Some(_) => return Err("bridge canonicalize: functions must be array".into()), + None => return Ok(false), + }; + + for func in functions.iter_mut() { + let blocks = func + .get_mut("blocks") + .and_then(Value::as_array_mut); + let Some(blocks) = blocks else { continue }; + for block in blocks.iter_mut() { + let insts = block + .get_mut("instructions") + .and_then(Value::as_array_mut); + let Some(insts) = insts else { continue }; + for inst in insts.iter_mut() { + if transform_module_function(inst)? { + changed = true; + } + } + } + } + + Ok(changed) +} + +fn transform_module_function(inst: &mut Value) -> Result { + let obj = match inst.as_object_mut() { + Some(map) => map, + None => return Ok(false), + }; + match obj.get("op").and_then(Value::as_str) { + Some("mir_call") => {} + _ => return Ok(false), + } + let mir_call = match obj.get_mut("mir_call").and_then(Value::as_object_mut) { + Some(mc) => mc, + None => return Ok(false), + }; + let name_owned = { + let callee_obj = match mir_call.get("callee").and_then(Value::as_object) { + Some(c) => c, + None => return Ok(false), + }; + match callee_obj.get("type").and_then(Value::as_str) { + Some("ModuleFunction") => {} + _ => return Ok(false), + } + callee_obj + .get("name") + .and_then(Value::as_str) + .ok_or_else(|| "bridge canonicalize: ModuleFunction missing name".to_string())? + .to_string() + }; + let name = name_owned.as_str(); + + let (box_name, method) = match name { + "ArrayBox.len" => ("ArrayBox", "size"), + "MapBox.len" => ("MapBox", "len"), + _ => return Ok(false), + }; + + let receiver_value = mir_call + .get_mut("args") + .and_then(Value::as_array_mut) + .ok_or_else(|| "bridge canonicalize: mir_call.args missing".to_string())?; + if receiver_value.is_empty() { + return Err(format!( + "bridge canonicalize: {} requires receiver argument", + name + )); + } + let receiver_value = receiver_value.remove(0); + let receiver = receiver_value + .as_u64() + .ok_or_else(|| format!("bridge canonicalize: {} receiver must be integer", name))?; + + let num = serde_json::Number::from_u128(receiver as u128) + .ok_or_else(|| "bridge canonicalize: receiver out of range".to_string())?; + + let callee = mir_call + .get_mut("callee") + .and_then(Value::as_object_mut) + .ok_or_else(|| "bridge canonicalize: callee missing".to_string())?; + callee.insert("type".to_string(), Value::String("Method".into())); + callee.insert("method".to_string(), Value::String(method.into())); + callee.insert("box_name".to_string(), Value::String(box_name.into())); + callee.insert("receiver".to_string(), Value::Number(num)); + callee.remove("name"); + if !callee.contains_key("certainty") { + callee.insert("certainty".to_string(), Value::String("Known".into())); + } + + Ok(true) +} + +fn materialize_phi_blocks(root: &mut Value) -> Result { + let mut changed = false; + let functions = match root.as_object_mut() { + Some(obj) => obj.get_mut("functions"), + None => return Err("bridge canonicalize: expected JSON object at root".into()), + }; + let functions = match functions { + Some(Value::Array(arr)) => arr, + Some(_) => return Err("bridge canonicalize: functions must be array".into()), + None => return Ok(false), + }; + + for func in functions.iter_mut() { + let blocks = func + .get_mut("blocks") + .and_then(Value::as_array_mut); + let Some(blocks) = blocks else { continue }; + for block in blocks.iter_mut() { + let insts = block + .get_mut("instructions") + .and_then(Value::as_array_mut); + let Some(insts) = insts else { continue }; + if reorder_block_phi(insts)? { + changed = true; + } + } + } + + Ok(changed) +} + +fn reorder_block_phi(insts: &mut Vec) -> Result { + let mut seen_non_phi = false; + let mut needs_reorder = false; + for inst in insts.iter() { + if is_phi(inst) { + if seen_non_phi { + needs_reorder = true; + break; + } + } else { + seen_non_phi = true; + } + } + if !needs_reorder { + return Ok(false); + } + + let original = std::mem::take(insts); + let mut phis = Vec::new(); + let mut others = Vec::new(); + for inst in original.into_iter() { + if is_phi(&inst) { + phis.push(inst); + } else { + others.push(inst); + } + } + insts.extend(phis); + insts.extend(others); + Ok(true) +} + +fn is_phi(inst: &Value) -> bool { + inst.as_object() + .and_then(|obj| obj.get("op")) + .and_then(Value::as_str) + .map(|op| op == "phi") + .unwrap_or(false) +} diff --git a/src/runner/pipe_io.rs b/src/runner/pipe_io.rs index e5e92bf3..3b3c16eb 100644 --- a/src/runner/pipe_io.rs +++ b/src/runner/pipe_io.rs @@ -36,11 +36,25 @@ impl NyashRunner { } buf }; - if crate::config::env::nyvm_core_wrapper() { + let use_core_wrapper = crate::config::env::nyvm_core_wrapper(); + let use_downconvert = crate::config::env::nyvm_v1_downconvert(); + if use_core_wrapper || use_downconvert { match crate::runner::modes::common_util::core_bridge::canonicalize_module_json(&json) { Ok(j) => json = j, Err(e) => eprintln!("[bridge] canonicalize warning: {}", e), } + match crate::runner::json_v1_bridge::try_parse_v1_to_module(&json) { + Ok(Some(module)) => { + super::json_v0_bridge::maybe_dump_mir(&module); + self.execute_mir_module(&module); + return true; + } + Ok(None) => {} + Err(e) => { + eprintln!("❌ JSON v1 bridge error: {}", e); + std::process::exit(1); + } + } } match super::json_v0_bridge::parse_json_v0_to_module(&json) { Ok(module) => { diff --git a/tools/smokes/v2/profiles/quick/core/gate_c_v1_file_vm.sh b/tools/smokes/v2/profiles/quick/core/gate_c_v1_file_vm.sh new file mode 100644 index 00000000..284f93c8 --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/gate_c_v1_file_vm.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# gate_c_v1_file_vm.sh — Gate-C(Core) v1 JSON (file) parity smoke (opt-in) + +set -euo pipefail + +if [ "${SMOKES_ENABLE_GATE_C_V1:-0}" != "1" ]; then + echo "[SKIP] SMOKES_ENABLE_GATE_C_V1!=1" >&2 + exit 0 +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null)" +if [ -z "$ROOT" ]; then + ROOT="$(cd "$SCRIPT_DIR/../../../../.." && pwd)" +fi +BIN="$ROOT/target/release/nyash" + +if [ ! -x "$BIN" ]; then + (cd "$ROOT" && cargo build --release >/dev/null 2>&1) || { + echo "[FAIL] build release nyash" >&2 + exit 1 + } +fi + +JSON_FILE="/tmp/gate_c_v1_file_$$.json" +trap 'rm -f "$JSON_FILE"' EXIT +cat > "$JSON_FILE" <<'JSON' +{"schema_version":"1.0","functions":[{"name":"main","params":[],"blocks":[{"id":0,"instructions":[{"op":"const","dst":1,"value":{"type":"i64","value":7}},{"op":"ret","value":1}]}]}]} +JSON + +run_case() { + local mode="$1" + local label="gate_c_v1_file_vm(${mode})" + + if [ "$mode" = "plugins-off" ]; then + export NYASH_DISABLE_PLUGINS=1 + else + unset NYASH_DISABLE_PLUGINS || true + fi + + export NYASH_QUIET=1 + export HAKO_QUIET=1 + export NYASH_CLI_VERBOSE=0 + export NYASH_NYRT_SILENT_RESULT=1 + export NYASH_NYVM_CORE=1 + export HAKO_NYVM_CORE=1 + + # Debug stdout for env (optional) + if [ "${SMOKES_DEBUG:-0}" = "1" ]; then + echo "[DEBUG] mode=$mode" >&2 + env | grep -E 'NYASH|HAKO' >&2 + fi + + output="$($BIN --json-file "$JSON_FILE" 2>&1)" + rc=$? + last=$(printf '%s\n' "$output" | awk '/Result:/{val=$2} END{print val}') + + if [ "$rc" -ne 0 ]; then + echo "$output" >&2 + echo "[FAIL] $label (rc=$rc)" >&2 + exit 1 + fi + + if [ "$last" != "7" ]; then + echo "$output" >&2 + echo "[FAIL] $label (expected 7, got '$last')" >&2 + exit 1 + fi + + echo "[PASS] $label" >&2 +} + +run_case "plugins-off" +run_case "plugins-on" + +exit 0 diff --git a/tools/smokes/v2/profiles/quick/core/gate_c_v1_pipe_vm.sh b/tools/smokes/v2/profiles/quick/core/gate_c_v1_pipe_vm.sh new file mode 100644 index 00000000..557ccb85 --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/gate_c_v1_pipe_vm.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# gate_c_v1_pipe_vm.sh — Gate-C(Core) v1 JSON (pipe) parity smoke (opt-in) + +set -euo pipefail + +if [ "${SMOKES_ENABLE_GATE_C_V1:-0}" != "1" ]; then + echo "[SKIP] SMOKES_ENABLE_GATE_C_V1!=1" >&2 + exit 0 +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null)" +if [ -z "$ROOT" ]; then + ROOT="$(cd "$SCRIPT_DIR/../../../../.." && pwd)" +fi +BIN="$ROOT/target/release/nyash" + +if [ ! -x "$BIN" ]; then + (cd "$ROOT" && cargo build --release >/dev/null 2>&1) || { + echo "[FAIL] build release nyash" >&2 + exit 1 + } +fi + +PAYLOAD='{"schema_version":"1.0","functions":[{"name":"main","params":[],"blocks":[{"id":0,"instructions":[{"op":"const","dst":1,"value":{"type":"i64","value":5}},{"op":"ret","value":1}]}]}]}' + +run_case() { + local mode="$1" + local label="gate_c_v1_pipe_vm(${mode})" + + if [ "$mode" = "plugins-off" ]; then + export NYASH_DISABLE_PLUGINS=1 + else + unset NYASH_DISABLE_PLUGINS || true + fi + + export NYASH_QUIET=1 + export HAKO_QUIET=1 + export NYASH_CLI_VERBOSE=0 + export NYASH_NYRT_SILENT_RESULT=1 + export NYASH_NYVM_CORE=1 + export HAKO_NYVM_CORE=1 + + output=$(printf '%s' "$PAYLOAD" | $BIN --ny-parser-pipe 2>&1) + rc=$? + last=$(printf '%s\n' "$output" | awk '/Result:/{val=$2} END{print val}') + + if [ "$rc" -ne 0 ]; then + echo "$output" >&2 + echo "[FAIL] $label (rc=$rc)" >&2 + exit 1 + fi + + if [ "$last" != "5" ]; then + echo "$output" >&2 + echo "[FAIL] $label (expected 5, got '$last')" >&2 + exit 1 + fi + + echo "[PASS] $label" >&2 +} + +run_case "plugins-off" +run_case "plugins-on" + +exit 0 diff --git a/tools/smokes/v2/profiles/quick/core/hako_map_escape_vm.sh b/tools/smokes/v2/profiles/quick/core/hako_map_escape_vm.sh new file mode 100644 index 00000000..c250369c --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/hako_map_escape_vm.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# hako_map_escape_vm.sh — Stage-A map literal escape/unicode boundary (opt-in) + +set -euo pipefail + +if [ "${SMOKES_ENABLE_STAGEA_BOUNDARY:-0}" != "1" ]; then + echo "[SKIP] SMOKES_ENABLE_STAGEA_BOUNDARY!=1" >&2 + exit 0 +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || cd "$SCRIPT_DIR/../../../../.." && pwd)" +BIN="$ROOT/target/release/nyash" + +if [ ! -x "$BIN" ]; then + (cd "$ROOT" && cargo build --release >/dev/null 2>&1) || { + echo "[FAIL] build release nyash" >&2 + exit 1 + } +fi + +compile_stage_a() { + local code="$1" + local hako_tmp="/tmp/hako_stagea_map_$$.hako" + local json_out="/tmp/hako_stagea_map_$$.mir.json" + printf "%s\n" "$code" > "$hako_tmp" + local raw="/tmp/hako_stagea_map_raw_$$.txt" + NYASH_PARSER_ALLOW_SEMICOLON=1 NYASH_SYNTAX_SUGAR_LEVEL=full NYASH_ENABLE_ARRAY_LITERAL=1 \ + HAKO_ALLOW_USING_FILE=1 NYASH_ALLOW_USING_FILE=1 \ + NYASH_QUIET=1 HAKO_QUIET=1 NYASH_CLI_VERBOSE=0 \ + "$BIN" --backend vm "$ROOT/lang/src/compiler/entry/compiler.hako" -- --min-json --source "$(cat "$hako_tmp")" > "$raw" 2>&1 + awk '/"version":0/ && /"kind":"Program"/ {print; exit}' "$raw" > "$json_out" || true + rm -f "$raw" "$hako_tmp" + if [ ! -s "$json_out" ]; then + echo "[DIAG] Stage-A failed to emit JSON (expected for some boundary cases)" >&2 + return 2 + fi + echo "$json_out" + return 0 +} + +run_gate_c() { + local json_path="$1" + NYASH_QUIET=1 HAKO_QUIET=1 NYASH_CLI_VERBOSE=0 NYASH_NYRT_SILENT_RESULT=1 \ + "$BIN" --json-file "$json_path" >/dev/null 2>&1 || true + rm -f "$json_path" +} + +# Case 1: escaped quote in key +code='box Main { static method main() { local m={"a\"b":1}; print(1); } }' +if json=$(compile_stage_a "$code"); then + run_gate_c "$json" + echo "[PASS] stagea_map_key_escaped_quote (emitted JSON)" >&2 +else + echo "[PASS] stagea_map_key_escaped_quote (diagnostic acceptable)" >&2 +fi + +# Case 2: unicode key +code='box Main { static method main() { local m={"ねこ":2}; print(2); } }' +if json=$(compile_stage_a "$code"); then + run_gate_c "$json" + echo "[PASS] stagea_map_key_unicode (emitted JSON)" >&2 +else + echo "[PASS] stagea_map_key_unicode (diagnostic acceptable)" >&2 +fi + +# Case 3: empty map +code='box Main { static method main() { local m={}; print(0); } }' +if json=$(compile_stage_a "$code"); then + run_gate_c "$json" + echo "[PASS] stagea_map_empty (emitted JSON)" >&2 +else + echo "[PASS] stagea_map_empty (diagnostic acceptable)" >&2 +fi + +exit 0