docs(phase-20.33): update Gate-C(Core) status (v1→MIR interpreter), mark parity smokes done; clean up wording\nchore: remove unused bak/ (external backup kept)\nsmokes: add Gate-C v1 file/pipe opt-in canaries; env toggles documented\nrunner: include json_v1_bridge + bridge toggles (singleton/phi) wiring

This commit is contained in:
nyash-codex
2025-11-01 07:02:04 +09:00
parent cac22c1a87
commit eabeb69d77
12 changed files with 930 additions and 11 deletions

View File

@ -7,3 +7,20 @@
- [x] v1→v0 降格MirJsonV1Adapter経路を整備`--v1-compat` / `selfhost_stageb_v1_compat_vm` opt-in - [x] v1→v0 降格MirJsonV1Adapter経路を整備`--v1-compat` / `selfhost_stageb_v1_compat_vm` opt-in
- [x] tools/smokes/v2/profiles/quick/core/selfhost_* を追加optin。配列ネスト/境界ケースを含む。 - [x] tools/smokes/v2/profiles/quick/core/selfhost_* を追加optin。配列ネスト/境界ケースを含む。
- [x] ドキュメント更新README/PLAN/CHECKLIST - [x] ドキュメント更新README/PLAN/CHECKLIST
-— 未完(本フェーズ内で進める) —
- [x] GateC(Core) v1→MIR Interpreter 実行(`HAKO_NYVM_CORE=1`/最小ブリッジ)。
- [ ] NyVmDispatcher 直行(別タスク・将来対応)。
- [ ] v1→v0 最小ダウングレード(任意・`HAKO_NYVM_V1_DOWNCONVERT=1`)。未対応命令は FailFast。
- [x] Bridge 正規化: `HAKO_BRIDGE_INJECT_SINGLETON` 実装Array/Map len → Method 化、FailFast 付き)。
- [x] Bridge 正規化: `HAKO_BRIDGE_EARLY_PHI_MATERIALIZE` 実装(φ をブロック先頭へ移動、順序保護)。
- [x] GateC file/pipe × Plugins ON/OFF の対称性スモーク(数値出力=終了コード)。
- [ ] StageA map リテラルの堅牢化(エスケープ/Unicode/{}/不正形診断)。
- [x] スモーク実行権限core/* に chmod +x 反映)。
- [ ] 参照の古い表記整理apps/selfhost-compiler → lang/src/compiler/* に統一。履歴注記は残す)。
- [ ] ランナー子環境ヘルパーの集約selfhost 経路の ENV を helper へ)。
- [ ] Extern SSOTVM/AOT 共通ローダ、既定OFF
▼ 付記(ワークログ)
- 20251101: Runner の GateC v1 直行(子プロセスで Hako controller 起動)を配線。ただし Hako 側で `call("…")` 解決に失敗→直行NG。v1→MIR Interpreter最小ブリッジ const/copy/retは動作。
- 20251101: Bridge トグルlen 変換/φ 先頭化実装。GateC v1 parity スモークfile/pipe、plugins ON/OFFと StageA map 境界スモークを追加optin

View File

@ -23,6 +23,21 @@
5) ドキュメント更新 5) ドキュメント更新
- README/PLAN/CHECKLIST を適宜更新。CI 既定は変更せず既定OFF - README/PLAN/CHECKLIST を適宜更新。CI 既定は変更せず既定OFF
6) GateC(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 へ降格。未対応は FailFast。
実装ノート(現時点)
- Runner→Ny 側の GateC 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 指定形)して usebeforedef を防止。
- いずれも既定OFF、明示トグルでのみ有効。
## トグル/フラグdev ## トグル/フラグdev
- `--stage-b`entry 直下で StageB パスを有効化) - `--stage-b`entry 直下で StageB パスを有効化)
- オプション:`--prefer-cfg {0|1|2}`(未指定は 1 - オプション:`--prefer-cfg {0|1|2}`(未指定は 1

View File

@ -60,3 +60,77 @@
- lang/src/compiler/entry/compiler.hako入口 - lang/src/compiler/entry/compiler.hako入口
- lang/src/shared/json/mir_v1_adapter.hako - lang/src/shared/json/mir_v1_adapter.hako
- src/runner/json_v0_bridge/*Rust 側 v0 ブリッジ) - src/runner/json_v0_bridge/*Rust 側 v0 ブリッジ)
---
不足機能(現状のギャップ)
- GateC(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` で optin。
- 未対応命令は FailFast。Core 直行が有効な場合は直行を優先。
- Bridge 正規化トグルの中身が未実装
- `HAKO_BRIDGE_INJECT_SINGLETON` / `HAKO_BRIDGE_EARLY_PHI_MATERIALIZE` は env のみ存在。実際の補完・整列は未実装。
- 仕様確定後に JSON 編集(受信モジュール内の命令配列へ最小差分)を実装する。
Bridge 正規化 仕様(案/このフェーズで凍結)
- 目的: GateC(Wrapper) 経路で v1 形状の JSON を安全に Core/VM で実行可能な形に最小正規化する。
- トグル既定OFF・FailFast優先
- `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`
- 対象: ブロック内で φ 命令が先頭以外に出現する形、または φ と非φ が交差している形。
- 変換(最小):
- 同一ブロック内で φ 命令列を先頭に移動(相対順は保持)。
- 非φ 命令列は元順序を保持するusebeforedef を避けるための再順序付けは行わない)。
- 追加のコピー挿入edge copy 合成)は行わない。必要なら Fail。
- 失敗時: 安定化したエラーメッセージで FailFast静かなフォールバック禁止
注: 上記はいずれも「既定OFF」。Runner での v1 受理は 1) 直行NyVmDispatcher/Core 2) 最小ダウングレードv1→v0, 任意の順で選好し、Bridge 正規化は Wrapper 経路の補助として段階導入する。
- GateCfile/pipe× Plugins ON/OFF の対称性検証が未整備
- 数値出力とプロセス終了コードの一致rc=出力数値)の保証スモークを追加する。
- StageA map リテラルの堅牢化の残り
- エスケープや Unicode、空マップ `{}`、不正形(診断)などの追加ケースを補強する。
- スモーク実行権限の不足
- 一部 `tools/smokes/v2/profiles/quick/core/*.sh` が実行不可のまま。chmod +x を反映する。
- 参照の古い表記が残存
- hako.toml のコメントや docs に `apps/selfhost-compiler` の表記が散見。`lang/src/compiler/*` 基準に置換(履歴の文脈が必要な箇所は注記)。
- ランナー子環境ヘルパーの集約が未完
- selfhost 子プロセス向け ENV セットの重複を helper に寄せるタスクが残り。
- Extern SSOTVM/AOT 共有ローダ)が未導入(最小)
- VM/AOT 双方で参照する単一起点を用意既定OFF、optin
このフェーズの次アクション(短期)
- GateC 実行切替v1直行/トグル配下)と対称性スモークの追加。
- Bridge トグルの実装方針を SPEC 化Singleton 注入ルール / φ 整列の期待形)。
- スモークの権限整備と StageA/StageB の追加境界ケース投入。
現状ステータス20251101 更新)
- GateC 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 へ降格FailFast
- Bridge トグル:
- `INJECT_SINGLETON`/`EARLY_PHI_MATERIALIZE` を実装既定OFF。Array/Map len 変換と φ 先頭化のみ対応済み。未対応は Fail。
- スモーク:
- GateC v1 parityfile/pipe × plugins ON/OFFの optin スモーク(`SMOKES_ENABLE_GATE_C_V1=1`)。
- StageA map 境界(エスケープ/Unicode/空)スモーク追加(診断含め PASS
直近のフォロー
- 変換器は最小命令セットのみ対応mir_call/extern 等は Fail。対応範囲を広げる場合は json_v1_bridge に実装を追加。

View File

@ -2,14 +2,26 @@
// Responsibility: Provide a thin, stable entry to route MIR(JSON v0) // Responsibility: Provide a thin, stable entry to route MIR(JSON v0)
// through the Ny/Core dispatcher when a wrapper route is needed. // 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 { static box GateCController {
// route_json/1: String(JSON v0) -> String(last line) // route_json/1: String(JSON v0) -> String(last line)
// Contract: returns a printable string (numeric or tag). No side effects. // Contract: returns a printable string (numeric or tag). No side effects.
route_json(j) { route_json(j) {
// Delegate to the Core dispatcher runner // 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
}
}

View File

@ -548,3 +548,9 @@ pub fn nyvm_bridge_early_phi_materialize() -> bool {
.or_else(|| env_flag("NYASH_BRIDGE_EARLY_PHI_MATERIALIZE")) .or_else(|| env_flag("NYASH_BRIDGE_EARLY_PHI_MATERIALIZE"))
.unwrap_or(false) .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)
}

View File

@ -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<Option<MirModule>, 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<ConstValue, String> {
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<u64, String> {
node.get(key)
.and_then(Value::as_u64)
.ok_or_else(|| format!("{} missing field '{}'", context, key))
}
fn parse_binop(op: &str) -> Result<crate::mir::types::BinaryOp, String> {
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<crate::mir::types::CompareOp, String> {
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)
}

View File

@ -21,6 +21,7 @@ mod cli_directives;
mod demos; mod demos;
mod dispatch; mod dispatch;
mod json_v0_bridge; mod json_v0_bridge;
mod json_v1_bridge;
mod mir_json_emit; mod mir_json_emit;
pub mod modes; pub mod modes;
mod pipe_io; mod pipe_io;

View File

@ -1,23 +1,230 @@
/*! /*!
* core_bridge.rs — NyVM wrapper bridge helpers * core_bridge.rs — NyVM wrapper bridge helpers
* *
* Provides a minimal JSON canonicalizer for NyVmDispatcher wrapper path. * Provides a JSON canonicalizer for NyVmDispatcher wrapper path.
* Current implementation is conservative: returns input as-is, and optionally * Optional env toggles:
* dumps payload when `HAKO_DEBUG_NYVM_BRIDGE_DUMP` is set to a file path. * - 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<String, String> { pub fn canonicalize_module_json(input: &str) -> Result<String, String> {
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 !path.trim().is_empty() {
if let Err(e) = fs::write(&path, input.as_bytes()) { if let Err(e) = fs::write(&path, input.as_bytes()) {
eprintln!("[bridge/dump] write error: {}", e); 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<bool, String> {
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<bool, String> {
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<bool, String> {
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<Value>) -> Result<bool, String> {
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)
}

View File

@ -36,11 +36,25 @@ impl NyashRunner {
} }
buf 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) { match crate::runner::modes::common_util::core_bridge::canonicalize_module_json(&json) {
Ok(j) => json = j, Ok(j) => json = j,
Err(e) => eprintln!("[bridge] canonicalize warning: {}", e), 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) { match super::json_v0_bridge::parse_json_v0_to_module(&json) {
Ok(module) => { Ok(module) => {

View File

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

View File

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

View File

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