diff --git a/docs/development/current/CURRENT_TASK.md b/docs/development/current/CURRENT_TASK.md index b478cfd9..45123275 100644 --- a/docs/development/current/CURRENT_TASK.md +++ b/docs/development/current/CURRENT_TASK.md @@ -336,3 +336,70 @@ NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 ./target/release/nyash --backend vm examp - `NYASH_JIT_EVENTS=1` でイベントJSONを確認(fallback/trap理由が出る) - `cargo clean -p nyash-rust` → 再ビルド - 数値緩和: `NYASH_JIT_HOSTCALL_RELAX_NUMERIC=1` で i64→f64 のコアーションを許容(既定は `native_f64=1` 時に有効) + +### ✅ 10.9-β HostCall統合(完了) +- math.*(関数スタイル): 署名(F64)一致時に allow/sig_ok をイベント出力。戻りは Float 表示で安定。 +- Map.get: + - 受け手=param, key=I64 → `id: nyash.map.get_h`(Handle,I64)で allow かつ JIT実行 + - 受け手=param, key=Handle → `id: nyash.map.get_hh`(Handle,Handle)で allow かつ JIT実行(HH経路) + - 受け手がparamでない場合 → fallback(`reason: receiver_not_param`)をイベントに記録し VM 実行 +- RO系(length/isEmpty/charCodeAt/size): 受け手=param は allow/sig_ok。受け手≠param は fallback/receiver_not_param を必ず記録。 +- Quick flags: `NYASH_JIT_EVENTS=1` のとき Runner が未指定の `NYASH_JIT_THRESHOLD` を自動で `1` に設定(Lowerが1回目から走ってイベントが出る)。 +- 代表コマンド: + ```bash + # math.min(関数スタイル) + NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_NATIVE_F64=1 NYASH_JIT_EVENTS=1 \ + ./target/release/nyash --backend vm examples/jit_math_function_style_min_float.nyash + + # Map.get(パラメータ受け+Handleキー → HH直実行) + NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_HOSTCALL=1 NYASH_JIT_EVENTS=1 \ + ./target/release/nyash --backend vm examples/jit_map_get_param_hh.nyash + + # Map.get(非パラメータ受け → fallback記録) + NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_HOSTCALL=1 NYASH_JIT_EVENTS=1 \ + ./target/release/nyash --backend vm examples/jit_hostcall_map_get_handle.nyash + ``` + +### ⏭️ 10.9-δ(書き込みの導線のみ:次) +- 方針: 既定は read-only(`policy.read_only=true`)。書き込みは fallback(`reason: policy_denied_mutating`)をイベントで可視化。(実装済) +- 対象: Array.push/set, Map.set/delete など。イベントIDは `*_h`(Handle受け)に統一。 +- 仕上げ: `JitPolicyBox` に opt-in 追加(完了) + - `set("read_only", true|false)` / `setWhitelistCsv("id1,id2")` / `addWhitelist("id")` / `clearWhitelist()` + - プリセット: `enablePreset("mutating_minimal")`(Array.push_h) / `enablePreset("mutating_common")`(Array.push_h, Array.set_h, Map.set_h) + +--- + +## 🚀 Phase 10.10 – Python→Nyash→MIR→VM/Native 実用化整備(着手) + +目標(DoD 抜粋) +- DebugConfig(Box+Rust)で dump/events/stats/dot/phi_min 等の切替を一本化(CLI/env/Box同期) +- GC切替(Null/Counting)をCLI/Boxから操作可能、root/bARRIERサイト観測が動く +- HostCall(RO/一部WO)が param でallow、非paramでfallback(イベントで確認可) +- 代表サンプル最小集合でE2E安定 + +着手タスク(In Progress) +1) DebugConfigBox(新規) + - setFlag(name,bool)/setPath(name,string)/apply()/summary()(env反映) + - 対応: NYASH_JIT_EVENTS/…/DUMP/…/DOT(最小) +2) GC切替のCLI/Box連携(Pending) +3) 代表サンプル/README整備(Pending) + +--- + +### ⏸️ Checkpoint(2025-08-28 再起動用メモ) +- 10.9-β/δ 完了(RO allow+fallback可視化、WOはpolicy/whitelistでopt-in) +- HH経路(Map.get_hh)実装済み。戻りはCallBoundaryBoxでBoxRef復元。 +- JitPolicyBox: addWhitelist/clearWhitelist/enablePreset(mutating_minimal/common 等) +- DebugConfigBox: JIT観測系(events/stats/dump/dot)をBoxで一元制御→apply()でenv反映 +- GcConfigBox: counting/trace/barrier_strict をBoxで操作→apply()でenv反映 +- 例: + - jit_map_get_param_hh.nyash(HH直実行) + - jit_policy_optin_mutating.nyash(mutatingのopt-in) + - gc_counting_demo.nyash(CountingGcの可視化、VMパス) + +次の着手(Restart後すぐ) +1) Runner統合(DebugConfigの反映優先度/CLI連携の整理) + - 目標: CLI→DebugConfigBox→env→JIT/VM の一本化(既存JitConfigとの整合) +2) GCドキュメントに GcConfigBox の使用例を追記(短文 + コマンド) +3) examples/README.md(最小手順) + - HH直実行、mutating opt-in、CountingGc demo の3本だけ掲載 diff --git a/docs/development/roadmap/phases/phase-10/phase_10_10/README.md b/docs/development/roadmap/phases/phase-10/phase_10_10/README.md new file mode 100644 index 00000000..964cfb9b --- /dev/null +++ b/docs/development/roadmap/phases/phase-10/phase_10_10/README.md @@ -0,0 +1,84 @@ +# Phase 10.10 – Python→Nyash→MIR→VM/Native ラインの実用化整備(Box-First 継続) + +目的: Nyash→MIR→VM/Native の実行ラインを日常運用レベルに引き上げ、GC/デバッグ/HostCallの柱を整備する。 + +## ゴール(DoD) +- エンドツーエンド実行ライン(Parser→AST→MIR→VM→JIT)がビルトインBoxで安定(RO/一部WO) +- GC切替(Null/Counting)をCLI/Boxから操作可能、root領域APIが一箇所化 +- デバッグ/可視化の旗振り(DebugConfig/Box)でJIT/VM/イベント/DOTを一本化 +- HostCall: 読み取り系はparam受けでJIT直実行(allow)。書き込み系はポリシー/whitelistでopt-in可能 +- 最小ベンチと回帰(サンプル)でラインの劣化を検知 + +## 事前整備(現状) +- HostCall基盤: Registry/Policy/Events/Boundary(10.9-β/δ完了) +- JITイベント: `NYASH_JIT_EVENTS=1` 時に `threshold=1` 自動適用でLower確実実行 +- 戻り境界: CallBoundaryBox で JitValue→VMValue を一元化(ハンドル復元含む) + +## ワークストリーム +1) GC Switchable Runtime(phase_10_4_gc_switchable_runtime.md) + - 目標: NullGc/CountingGc の切替、root領域/バリアAPIの一本化 + - タスク: + - NyashRuntimeBuilder: GC選択をCLI/Box反映(NYASH_GC=none|counting など) + - ScopeTracker/enter_root_region()/pin_roots() の公開インターフェース確認 + - CountingGcの統計出力(roots/reads/writes/safepoints) + - 書き込み系HostCallにバリアサイトのフック(Map/Array set/push) + - 受入: GC切替コマンドで統計差分が取れる/HostCall書き込みでバリアサイトが加算される + +2) Unified Debug System(phase_10_8_unified_debug_system.md) + - 目標: デバッグ/観測フラグを DebugConfig/Box に統合(CLI/env/Boxの単一路) + - タスク: + - DebugConfig(Rust側): dump/events/stats/dot/phi_min 等を集約 + - DebugConfigBox: Boxから get/set/apply/toJson/fromJson + - Runner: CLI→DebugConfig→env/Box の一本化(env直読み排除) + - イベント出力先: stdout/file 切替の設定(NYASH_JIT_EVENTS_PATH のBox反映) + - 受入: Boxから apply 後、JIT/VM/DOTの挙動が即時反映/JSONLが指定先に出力 + +3) E2Eラインの実用化(builtin→pluginの足場) + - 目標: ビルトインBoxで日常運用レベル、プラグインBoxはTLV HostCallの足場を準備 + - タスク: + - Lowerカバレッジの整理(BoxCall/RO/WO・param/非paramの分岐ダンプ) + - 署名管理: レジストリのオーバーロード運用方針(canonical idと派生idの整理) + - 返り型推論: MIR Builderのreturn_type推定を確認(main/補助関数とも) + - Plugin PoC: TLV/handle経由のread-onlyメソッド1つをHostCall経由で通す(allowログまで) + - 受入: 代表サンプル(math/map/array/string)でallow/fallbackが意図通り、plugin PoCでallowイベントが出る + +4) ドキュメントと例の整理 + - 目標: 例の最小集合化(param/非param/RO/WO/HH/Hの代表)、手順の簡潔化 + - タスク: + - examples/: 重複の削減、README(実行コマンド付き) + - phase_10_9/10_10 のガイドをCURRENT_TASKと相互参照 + - 受入: 主要ケースが examples/README からそのまま実行可 + +5) ベンチと回帰(最小) + - 目標: ラインの性能/退行の早期検知 + - タスク: + - ny_bench.nyash のケース整理(関数呼出/Map set-get/branch) + - compare: VM vs JIT(ウォームアップ付き) + - 受入: ベンチ出力に JIT/VM の比較が出る(改善/退行が見える) + +## リスクと対策 +- param/非param 分岐の混乱: イベントに reason を必ず出す/docsにベストプラクティス(受け手をparam化) +- mutatingの誤許可: JitPolicyBox の whitelist/プリセットのみで許可、既定はread_only +- 署名の散逸: canonical id(例: nyash.map.get_h)と派生(_hh)の方針を明示 + +## 受け入れ基準(サマリ) +- DebugConfig/Box/CLIの一貫挙動(apply後の即時反映) +- GC切替とバリアサイト観測が可能 +- HostCall(RO/一部WO)が param でallow、非paramでfallback(イベントで確認可) +- 代表サンプルが examples/README の手順で成功 + +## すぐ試せるコマンド(抜粋) +```bash +# math.min(関数スタイル) +NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_NATIVE_F64=1 NYASH_JIT_EVENTS=1 \ + ./target/release/nyash --backend vm examples/jit_math_function_style_min_float.nyash + +# Map.get HH直実行 +NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_HOSTCALL=1 NYASH_JIT_EVENTS=1 \ + ./target/release/nyash --backend vm examples/jit_map_get_param_hh.nyash + +# Mutating opt-in(Array.push) +NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_HOSTCALL=1 NYASH_JIT_EVENTS=1 \ + ./target/release/nyash --backend vm examples/jit_policy_optin_mutating.nyash +``` + diff --git a/docs/development/roadmap/phases/phase-10/phase_10_9_builtin_box_jit_support.md b/docs/development/roadmap/phases/phase-10/phase_10_9_builtin_box_jit_support.md index bac99ef0..fa415238 100644 --- a/docs/development/roadmap/phases/phase-10/phase_10_9_builtin_box_jit_support.md +++ b/docs/development/roadmap/phases/phase-10/phase_10_9_builtin_box_jit_support.md @@ -85,6 +85,35 @@ - b1正規化カウンタ: `b1_norm_count`(分岐条件/PHI) - HostCallイベント: `argc`/`arg_types`/`reason`でデバッグ容易化(mutatingは `policy_denied_mutating`) +### 🔎 HostCallイベントの基準(10.9-β) +- 受け手が関数パラメータ(param)の場合は JIT直実行(allow/sig_ok)を基本にイベント出力 + - Map.get(Handle, I64): `id: nyash.map.get_h`, `arg_types: ["Handle","I64"]` + - Map.get(Handle, Handle): `id: nyash.map.get_hh`, `arg_types: ["Handle","Handle"]` + - length/isEmpty/charCodeAt/size 等も `*_h`(Handle受け)でallow +- 受け手がparamでない場合は VMへフォールバック(fallback/receiver_not_param)をイベントで記録(読み取り系の可視化を保証) + - 例: `id: nyash.any.length_h`, `decision: fallback`, `reason: receiver_not_param` +- 数値緩和: `NYASH_JIT_HOSTCALL_RELAX_NUMERIC=1` または `NYASH_JIT_NATIVE_F64=1` で `I64→F64` コアーションを許容(sig_okに影響) + +### 🧪 代表サンプル(E2E) +```bash +# math.*(関数スタイル): 署名一致でallow、戻りFloat表示 +NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_NATIVE_F64=1 NYASH_JIT_EVENTS=1 \ + ./target/release/nyash --backend vm examples/jit_math_function_style_min_float.nyash + +# Map.get(パラメータ受け+Handleキー → HH直実行) +NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_HOSTCALL=1 NYASH_JIT_EVENTS=1 \ + ./target/release/nyash --backend vm examples/jit_map_get_param_hh.nyash + +# Map.get(非パラメータ受け → fallback記録) +NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_HOSTCALL=1 NYASH_JIT_EVENTS=1 \ + ./target/release/nyash --backend vm examples/jit_hostcall_map_get_handle.nyash +``` + +### ⚙️ Quick flags(イベント観測を確実に) +- `NYASH_JIT_EVENTS=1` のとき Runner が `NYASH_JIT_THRESHOLD=1` を自動適用(未指定の場合) + - 1回目からLowerが走り、allow/fallbackのイベントが必ず出る + - 明示的に `NYASH_JIT_THRESHOLD` を指定した場合はそちらを優先 + ## ⚠️ リスクとその箱での緩和 - 署名不一致(args/ret) - HostcallRegistryBox で一元検査。不一致は `sig_mismatch` でイベント記録→VMへ @@ -107,6 +136,20 @@ - HostCallブリッジの拡大(Map.getの多型キー、String操作の追加) - CallBoundaryBox経由の `new`/副作用命令の段階的JIT化 +## ✳️ 10.9-δ 書き込みの導線(運用) +- 既定ポリシー: read_only(`NYASH_JIT_READ_ONLY=1`)で mutating はフォールバック(`reason: policy_denied_mutating`)。 +- JitPolicyBox でopt-in: + ```nyash + P = new JitPolicyBox() + P.set("read_only", true) + P.addWhitelist("nyash.array.push_h") // 個別に許可 + // またはプリセット: + P.enablePreset("mutating_minimal") // Array.push_h を許可 + ``` +- イベント方針: + - 受け手=param: allow/sig_ok(whitelist/オフ時はfallback/policy_denied_mutating) + - 受け手≠param: fallback/receiver_not_param(可視化を保証) + --- 最短ルート: 箱(Policy/Events/Registry/Boundary)を先に置き、読み取り系でJITを安全に通す→観測を増やす→署名とポリシーの一本化で切替点を固定→必要最小限のネイティブ型(f64/b1)を段階導入。 diff --git a/examples/gc_counting_demo.nyash b/examples/gc_counting_demo.nyash new file mode 100644 index 00000000..a7fcdf9f --- /dev/null +++ b/examples/gc_counting_demo.nyash @@ -0,0 +1,34 @@ +// GC Counting demo (VM path) — verifies CountingGc counters and barrier sites +// Run: +// ./target/release/nyash --backend vm examples/gc_counting_demo.nyash +// Expect (with trace): [GC] counters: safepoints>0 read_barriers>=0 write_barriers>=0 + +static box Main { + main() { + // Turn on Counting GC + trace via Box-First path + local G + G = new GcConfigBox() + G = G.setFlag("counting", true) + G = G.setFlag("trace", true) + G.apply() + + // Make sure we don't engage JIT path for this demo (VM baseline) + // If needed, you can set NYASH_JIT_EXEC=0 in the environment. + + // Perform some mutating ops to hit write barriers on the VM path + local A, M + A = new ArrayBox() + A.push(1) + A.set(0, 2) + + M = new MapBox() + M.set("k", "v") + + // Simple read ops (should register read barriers on some implementations) + print(A.length()) + print(M.size()) + + return "done" + } +} + diff --git a/examples/jit_map_get_param_hh.nyash b/examples/jit_map_get_param_hh.nyash new file mode 100644 index 00000000..c40d7c91 --- /dev/null +++ b/examples/jit_map_get_param_hh.nyash @@ -0,0 +1,30 @@ +// Map.get with both receiver and key as function parameters +// Expect: JIT hostcall allow for nyash.map.get_hh (Handle,Handle) +// Run: +// NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_HOSTCALL=1 NYASH_JIT_EVENTS=1 \ +// ./target/release/nyash --backend vm examples/jit_map_get_param_hh.nyash + +box Helper { + birth() { + // no-op constructor + } + getv(m, k) { + return m.get(k) + } +} + +static box Main { + main() { + local m, k, v + m = new MapBox() + k = "key1" + v = "value1" + // Mutating ops fall back to VM by policy (read-only JIT) + m.set(k, v) + // Use user-defined box Helper so that getv is lowered into a MIR function + local h + h = new Helper() + // Both m and k are parameters of Helper.getv/2 → JIT can use HH path + return h.getv(m, k) + } +} diff --git a/examples/jit_policy_optin_mutating.nyash b/examples/jit_policy_optin_mutating.nyash new file mode 100644 index 00000000..ea56409b --- /dev/null +++ b/examples/jit_policy_optin_mutating.nyash @@ -0,0 +1,35 @@ +// Demonstrate JIT mutating opt-in via JitPolicyBox whitelist +// 1) Enable read_only → mutating hostcalls fallback (policy_denied_mutating) +// 2) Whitelist nyash.array.push_h → allow + execute +// Run: +// NYASH_JIT_EXEC=1 NYASH_JIT_THRESHOLD=1 NYASH_JIT_HOSTCALL=1 NYASH_JIT_EVENTS=1 \ +// ./target/release/nyash --backend vm examples/jit_policy_optin_mutating.nyash + +box Helper { + birth() {} + add(a) { + a.push(1) + return a.length() + } +} + +static box Main { + main() { + local P, H, A + P = new JitPolicyBox() + // Enable read-only first (deny mutating) + P.set("read_only", true) + + H = new Helper() + A = new ArrayBox() + // This call will fallback by policy (observe events) + H.add(A) + + // Opt-in: allow array.push_h through whitelist + P.setWhitelistCsv("nyash.array.push_h") + + // This call will be allowed (observe allow event) + return H.add(A) + } +} + diff --git a/src/backend/vm.rs b/src/backend/vm.rs index f317e673..c61ac42c 100644 --- a/src/backend/vm.rs +++ b/src/backend/vm.rs @@ -91,7 +91,7 @@ impl VMValue { pub fn to_nyash_box(&self) -> Box { match self { VMValue::Integer(i) => Box::new(IntegerBox::new(*i)), - VMValue::Float(f) => Box::new(StringBox::new(&f.to_string())), // Simplified for now + VMValue::Float(f) => Box::new(crate::boxes::FloatBox::new(*f)), VMValue::Bool(b) => Box::new(BoolBox::new(*b)), VMValue::String(s) => Box::new(StringBox::new(s)), VMValue::Future(f) => Box::new(f.clone()), @@ -591,7 +591,7 @@ impl VM { self.pin_roots(args_vec.iter()); if let Some(jm_mut) = self.jit_manager.as_mut() { if jm_mut.is_compiled(&function.signature.name) { - if let Some(val) = jm_mut.execute_compiled(&function.signature.name, &args_vec) { + if let Some(val) = jm_mut.execute_compiled(&function.signature.name, &function.signature.return_type, &args_vec) { // Exit scope before returning self.leave_root_region(); self.scope_tracker.pop_scope(); @@ -609,7 +609,7 @@ impl VM { // Try to compile now and execute; if not possible, error out let _ = jm_mut.maybe_compile(&function.signature.name, function); if jm_mut.is_compiled(&function.signature.name) { - if let Some(val) = jm_mut.execute_compiled(&function.signature.name, &args_vec) { + if let Some(val) = jm_mut.execute_compiled(&function.signature.name, &function.signature.return_type, &args_vec) { self.leave_root_region(); self.scope_tracker.pop_scope(); return Ok(val); @@ -1018,6 +1018,50 @@ impl VM { } } + // MathBox methods (minimal set) + if let Some(math_box) = box_value.as_any().downcast_ref::() { + // Coerce numeric-like StringBox to FloatBox for function-style lowering path + let mut coerce_num = |b: &Box| -> Box { + if let Some(sb) = b.as_any().downcast_ref::() { + let s = sb.value.trim(); + if let Ok(f) = s.parse::() { return Box::new(crate::boxes::FloatBox::new(f)); } + if let Ok(i) = s.parse::() { return Box::new(IntegerBox::new(i)); } + } + b.clone_or_share() + }; + match method { + "min" => { + if _args.len() >= 2 { + let a = coerce_num(&_args[0]); + let b = coerce_num(&_args[1]); + return Ok(math_box.min(a, b)); + } + return Ok(Box::new(StringBox::new("Error: min(a,b) requires 2 args"))); + } + "max" => { + if _args.len() >= 2 { + let a = coerce_num(&_args[0]); + let b = coerce_num(&_args[1]); + return Ok(math_box.max(a, b)); + } + return Ok(Box::new(StringBox::new("Error: max(a,b) requires 2 args"))); + } + "abs" => { + if let Some(v) = _args.get(0) { return Ok(math_box.abs(coerce_num(v))); } + return Ok(Box::new(StringBox::new("Error: abs(x) requires 1 arg"))); + } + "sin" => { + if let Some(v) = _args.get(0) { return Ok(math_box.sin(coerce_num(v))); } + return Ok(Box::new(StringBox::new("Error: sin(x) requires 1 arg"))); + } + "cos" => { + if let Some(v) = _args.get(0) { return Ok(math_box.cos(coerce_num(v))); } + return Ok(Box::new(StringBox::new("Error: cos(x) requires 1 arg"))); + } + _ => return Ok(Box::new(VoidBox::new())), + } + } + // SocketBox methods (minimal set + timeout variants) if let Some(sock) = box_value.as_any().downcast_ref::() { match method { diff --git a/src/backend/vm_boxcall.rs b/src/backend/vm_boxcall.rs index b839193e..6a06d563 100644 --- a/src/backend/vm_boxcall.rs +++ b/src/backend/vm_boxcall.rs @@ -13,6 +13,32 @@ use super::vm::{VM, VMError, VMValue}; impl VM { /// Call a method on a Box - simplified version of interpreter method dispatch pub(super) fn call_box_method_impl(&self, box_value: Box, method: &str, _args: Vec>) -> Result, VMError> { + // MathBox methods (minimal set used in 10.9) + if let Some(math) = box_value.as_any().downcast_ref::() { + match method { + "min" => { + if _args.len() >= 2 { return Ok(math.min(_args[0].clone_or_share(), _args[1].clone_or_share())); } + return Ok(Box::new(StringBox::new("Error: min(a, b) requires 2 args"))); + } + "max" => { + if _args.len() >= 2 { return Ok(math.max(_args[0].clone_or_share(), _args[1].clone_or_share())); } + return Ok(Box::new(StringBox::new("Error: max(a, b) requires 2 args"))); + } + "abs" => { + if let Some(v) = _args.get(0) { return Ok(math.abs(v.clone_or_share())); } + return Ok(Box::new(StringBox::new("Error: abs(x) requires 1 arg"))); + } + "sin" => { + if let Some(v) = _args.get(0) { return Ok(math.sin(v.clone_or_share())); } + return Ok(Box::new(StringBox::new("Error: sin(x) requires 1 arg"))); + } + "cos" => { + if let Some(v) = _args.get(0) { return Ok(math.cos(v.clone_or_share())); } + return Ok(Box::new(StringBox::new("Error: cos(x) requires 1 arg"))); + } + _ => { return Ok(Box::new(VoidBox::new())); } + } + } // ResultBox (NyashResultBox - new) if let Some(result_box) = box_value.as_any().downcast_ref::() { match method { @@ -73,6 +99,39 @@ impl VM { } } + // JitPolicyBox methods + if let Some(jpb) = box_value.as_any().downcast_ref::() { + match method { + "get" => { + if let Some(k) = _args.get(0) { return Ok(jpb.get_flag(&k.to_string_box().value)); } + return Ok(Box::new(StringBox::new("get(name) requires 1 arg"))); + } + "set" => { + if _args.len() >= 2 { + let k = _args[0].to_string_box().value; + let v = _args[1].to_string_box().value; + let on = matches!(v.as_str(), "1" | "true" | "True" | "on" | "ON"); + return Ok(jpb.set_flag(&k, on)); + } + return Ok(Box::new(StringBox::new("set(name, bool) requires 2 args"))); + } + "setWhitelistCsv" | "set_whitelist_csv" => { + if let Some(s) = _args.get(0) { return Ok(jpb.set_whitelist_csv(&s.to_string_box().value)); } + return Ok(Box::new(StringBox::new("setWhitelistCsv(csv) requires 1 arg"))); + } + "addWhitelist" | "add_whitelist" => { + if let Some(s) = _args.get(0) { return Ok(jpb.add_whitelist(&s.to_string_box().value)); } + return Ok(Box::new(StringBox::new("addWhitelist(name) requires 1 arg"))); + } + "clearWhitelist" | "clear_whitelist" => { return Ok(jpb.clear_whitelist()); } + "enablePreset" | "enable_preset" => { + if let Some(s) = _args.get(0) { return Ok(jpb.enable_preset(&s.to_string_box().value)); } + return Ok(Box::new(StringBox::new("enablePreset(name) requires 1 arg"))); + } + _ => { return Ok(Box::new(VoidBox::new())); } + } + } + // JitStatsBox methods if let Some(jsb) = box_value.as_any().downcast_ref::() { match method { diff --git a/src/box_factory/builtin.rs b/src/box_factory/builtin.rs index 01d42b88..698e9dc9 100644 --- a/src/box_factory/builtin.rs +++ b/src/box_factory/builtin.rs @@ -292,6 +292,36 @@ impl BuiltinBoxFactory { } Ok(Box::new(crate::boxes::jit_config_box::JitConfigBox::new())) }); + + // JitPolicyBox (runtime JIT policy as a Box) + self.register("JitPolicyBox", |args| { + if !args.is_empty() { + return Err(RuntimeError::InvalidOperation { + message: format!("JitPolicyBox constructor expects 0 arguments, got {}", args.len()), + }); + } + Ok(Box::new(crate::boxes::jit_policy_box::JitPolicyBox::new())) + }); + + // DebugConfigBox (runtime debug/observability switches) + self.register("DebugConfigBox", |args| { + if !args.is_empty() { + return Err(RuntimeError::InvalidOperation { + message: format!("DebugConfigBox constructor expects 0 arguments, got {}", args.len()), + }); + } + Ok(Box::new(crate::boxes::debug_config_box::DebugConfigBox::new())) + }); + + // GcConfigBox (runtime GC switches) + self.register("GcConfigBox", |args| { + if !args.is_empty() { + return Err(RuntimeError::InvalidOperation { + message: format!("GcConfigBox constructor expects 0 arguments, got {}", args.len()), + }); + } + Ok(Box::new(crate::boxes::gc_config_box::GcConfigBox::new())) + }); } /// Register I/O types diff --git a/src/boxes/debug_config_box.rs b/src/boxes/debug_config_box.rs new file mode 100644 index 00000000..5543218e --- /dev/null +++ b/src/boxes/debug_config_box.rs @@ -0,0 +1,99 @@ +use crate::box_trait::{NyashBox, StringBox, BoolBox, VoidBox, BoxCore, BoxBase}; +use std::any::Any; + +#[derive(Debug, Clone)] +pub struct DebugConfigBox { + pub base: BoxBase, + // toggles/paths (staged until apply()) + pub jit_events: bool, + pub jit_stats: bool, + pub jit_stats_json: bool, + pub jit_dump: bool, + pub jit_dot_path: Option, +} + +impl DebugConfigBox { + pub fn new() -> Self { + Self { + base: BoxBase::new(), + jit_events: std::env::var("NYASH_JIT_EVENTS").ok().as_deref() == Some("1"), + jit_stats: std::env::var("NYASH_JIT_STATS").ok().as_deref() == Some("1"), + jit_stats_json: std::env::var("NYASH_JIT_STATS_JSON").ok().as_deref() == Some("1"), + jit_dump: std::env::var("NYASH_JIT_DUMP").ok().as_deref() == Some("1"), + jit_dot_path: std::env::var("NYASH_JIT_DOT").ok().filter(|s| !s.is_empty()), + } + } + + pub fn set_flag(&mut self, name: &str, on: bool) -> Box { + match name { + "jit_events" => self.jit_events = on, + "jit_stats" => self.jit_stats = on, + "jit_stats_json" => self.jit_stats_json = on, + "jit_dump" => self.jit_dump = on, + _ => return Box::new(StringBox::new(format!("Unknown flag: {}", name))) + } + Box::new(VoidBox::new()) + } + + pub fn set_path(&mut self, name: &str, value: &str) -> Box { + match name { + "jit_dot" | "jit_dot_path" => self.jit_dot_path = Some(value.to_string()), + _ => return Box::new(StringBox::new(format!("Unknown path: {}", name))) + } + Box::new(VoidBox::new()) + } + + pub fn get_flag(&self, name: &str) -> Box { + let v = match name { + "jit_events" => self.jit_events, + "jit_stats" => self.jit_stats, + "jit_stats_json" => self.jit_stats_json, + "jit_dump" => self.jit_dump, + _ => false, + }; + Box::new(BoolBox::new(v)) + } + + pub fn get_path(&self, name: &str) -> Box { + let v = match name { + "jit_dot" | "jit_dot_path" => self.jit_dot_path.clone().unwrap_or_default(), + _ => String::new(), + }; + Box::new(StringBox::new(v)) + } + + pub fn apply(&self) -> Box { + let setb = |k: &str, v: bool| { if v { std::env::set_var(k, "1"); } else { std::env::remove_var(k); } }; + setb("NYASH_JIT_EVENTS", self.jit_events); + setb("NYASH_JIT_STATS", self.jit_stats); + setb("NYASH_JIT_STATS_JSON", self.jit_stats_json); + setb("NYASH_JIT_DUMP", self.jit_dump); + if let Some(p) = &self.jit_dot_path { std::env::set_var("NYASH_JIT_DOT", p); } + Box::new(VoidBox::new()) + } + + pub fn summary(&self) -> Box { + let s = format!( + "jit_events={} jit_stats={} jit_stats_json={} jit_dump={} jit_dot={}", + self.jit_events, self.jit_stats, self.jit_stats_json, self.jit_dump, + self.jit_dot_path.clone().unwrap_or_else(|| "".to_string()) + ); + Box::new(StringBox::new(s)) + } +} + +impl BoxCore for DebugConfigBox { + fn box_id(&self) -> u64 { self.base.id } + fn parent_type_id(&self) -> Option { self.base.parent_type_id } + fn fmt_box(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "DebugConfigBox") } + fn as_any(&self) -> &dyn Any { self } + fn as_any_mut(&mut self) -> &mut dyn Any { self } +} + +impl NyashBox for DebugConfigBox { + fn equals(&self, other: &dyn NyashBox) -> BoolBox { BoolBox::new(other.as_any().is::()) } + fn type_name(&self) -> &'static str { "DebugConfigBox" } + fn clone_box(&self) -> Box { Box::new(Self { base: self.base.clone(), ..self.clone() }) } + fn share_box(&self) -> Box { self.clone_box() } + fn to_string_box(&self) -> StringBox { StringBox::new("DebugConfigBox".to_string()) } +} diff --git a/src/boxes/gc_config_box.rs b/src/boxes/gc_config_box.rs new file mode 100644 index 00000000..610131db --- /dev/null +++ b/src/boxes/gc_config_box.rs @@ -0,0 +1,62 @@ +use crate::box_trait::{NyashBox, StringBox, BoolBox, VoidBox, BoxCore, BoxBase}; +use std::any::Any; + +#[derive(Debug, Clone)] +pub struct GcConfigBox { base: BoxBase, counting: bool, trace: bool, barrier_strict: bool } + +impl GcConfigBox { + pub fn new() -> Self { + Self { + base: BoxBase::new(), + counting: std::env::var("NYASH_GC_COUNTING").ok().as_deref() == Some("1"), + trace: std::env::var("NYASH_GC_TRACE").ok().as_deref() == Some("1"), + barrier_strict: std::env::var("NYASH_GC_BARRIER_STRICT").ok().as_deref() == Some("1"), + } + } + pub fn set_flag(&mut self, name: &str, on: bool) -> Box { + match name { + "counting" => self.counting = on, + "trace" => self.trace = on, + "barrier_strict" | "strict" => self.barrier_strict = on, + _ => return Box::new(StringBox::new(format!("Unknown flag: {}", name))) + } + Box::new(VoidBox::new()) + } + pub fn get_flag(&self, name: &str) -> Box { + let v = match name { + "counting" => self.counting, + "trace" => self.trace, + "barrier_strict" | "strict" => self.barrier_strict, + _ => false, + }; + Box::new(BoolBox::new(v)) + } + pub fn apply(&self) -> Box { + let setb = |k: &str, v: bool| { if v { std::env::set_var(k, "1"); } else { std::env::remove_var(k); } }; + setb("NYASH_GC_COUNTING", self.counting); + setb("NYASH_GC_TRACE", self.trace); + setb("NYASH_GC_BARRIER_STRICT", self.barrier_strict); + Box::new(VoidBox::new()) + } + pub fn summary(&self) -> Box { + let s = format!("counting={} trace={} barrier_strict={}", self.counting, self.trace, self.barrier_strict); + Box::new(StringBox::new(s)) + } +} + +impl BoxCore for GcConfigBox { + fn box_id(&self) -> u64 { self.base.id } + fn parent_type_id(&self) -> Option { self.base.parent_type_id } + fn fmt_box(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "GcConfigBox") } + fn as_any(&self) -> &dyn Any { self } + fn as_any_mut(&mut self) -> &mut dyn Any { self } +} + +impl NyashBox for GcConfigBox { + fn equals(&self, other: &dyn NyashBox) -> BoolBox { BoolBox::new(other.as_any().is::()) } + fn type_name(&self) -> &'static str { "GcConfigBox" } + fn clone_box(&self) -> Box { Box::new(self.clone()) } + fn share_box(&self) -> Box { self.clone_box() } + fn to_string_box(&self) -> StringBox { StringBox::new(self.summary().to_string_box().value) } +} + diff --git a/src/boxes/jit_policy_box.rs b/src/boxes/jit_policy_box.rs index 51e2a0f3..c8fafc5b 100644 --- a/src/boxes/jit_policy_box.rs +++ b/src/boxes/jit_policy_box.rs @@ -48,5 +48,52 @@ impl JitPolicyBox { crate::jit::policy::set_current(cur); Box::new(VoidBox::new()) } -} + pub fn add_whitelist(&self, name: &str) -> Box { + let mut cur = crate::jit::policy::current(); + if !cur.hostcall_whitelist.iter().any(|s| s == name) { + cur.hostcall_whitelist.push(name.to_string()); + } + crate::jit::policy::set_current(cur); + Box::new(VoidBox::new()) + } + + pub fn clear_whitelist(&self) -> Box { + let mut cur = crate::jit::policy::current(); + cur.hostcall_whitelist.clear(); + crate::jit::policy::set_current(cur); + Box::new(VoidBox::new()) + } + + pub fn enable_preset(&self, name: &str) -> Box { + let mut cur = crate::jit::policy::current(); + match name { + // 最小: Array.push_h のみ許可(読み取り以外は変えない) + "mutating_minimal" | "mutating_array_push" => { + let id = crate::jit::r#extern::collections::SYM_ARRAY_PUSH_H; + if !cur.hostcall_whitelist.iter().any(|s| s == id) { + cur.hostcall_whitelist.push(id.to_string()); + } + } + // 例: Map.set_h も追加許可(必要に応じて拡張) + "mutating_map_set" => { + let id = crate::jit::r#extern::collections::SYM_MAP_SET_H; + if !cur.hostcall_whitelist.iter().any(|s| s == id) { + cur.hostcall_whitelist.push(id.to_string()); + } + } + // よく使う: Array.push_h + Array.set_h + Map.set_h を許可 + "mutating_common" => { + let ids = [ + crate::jit::r#extern::collections::SYM_ARRAY_PUSH_H, + crate::jit::r#extern::collections::SYM_ARRAY_SET_H, + crate::jit::r#extern::collections::SYM_MAP_SET_H, + ]; + for id in ids { if !cur.hostcall_whitelist.iter().any(|s| s == id) { cur.hostcall_whitelist.push(id.to_string()); } } + } + _ => { return Box::new(StringBox::new(format!("Unknown preset: {}", name))); } + } + crate::jit::policy::set_current(cur); + Box::new(VoidBox::new()) + } +} diff --git a/src/boxes/mod.rs b/src/boxes/mod.rs index a91fc237..7ca1aae3 100644 --- a/src/boxes/mod.rs +++ b/src/boxes/mod.rs @@ -79,6 +79,8 @@ pub mod jit_stats_box; pub mod jit_policy_box; pub mod jit_events_box; pub mod jit_hostcall_registry_box; +pub mod debug_config_box; +pub mod gc_config_box; // Web専用Box群(ブラウザ環境でのみ利用可能) #[cfg(target_arch = "wasm32")] diff --git a/src/interpreter/methods/mod.rs b/src/interpreter/methods/mod.rs index a4cb03b4..71bb7823 100644 --- a/src/interpreter/methods/mod.rs +++ b/src/interpreter/methods/mod.rs @@ -23,5 +23,6 @@ pub mod data_methods; // BufferBox, JSONBox, RegexBox pub mod network_methods; // HttpClientBox, StreamBox pub mod p2p_methods; // IntentBox, P2PBox pub mod http_methods; // SocketBox, HTTPServerBox, HTTPRequestBox, HTTPResponseBox +pub mod system_methods; // GcConfigBox, DebugConfigBox // Re-export methods for easy access diff --git a/src/interpreter/methods/system_methods.rs b/src/interpreter/methods/system_methods.rs new file mode 100644 index 00000000..c5ac9622 --- /dev/null +++ b/src/interpreter/methods/system_methods.rs @@ -0,0 +1,171 @@ +/*! + * System Methods Module + * + * Contains system-level Box type method implementations: + * - GcConfigBox: Garbage collector configuration + * - DebugConfigBox: Debug and observability configuration + */ + +use crate::ast::ASTNode; +use crate::box_trait::{NyashBox, VoidBox, StringBox, BoolBox}; +use crate::boxes::gc_config_box::GcConfigBox; +use crate::boxes::debug_config_box::DebugConfigBox; +use crate::interpreter::{NyashInterpreter, RuntimeError}; + +impl NyashInterpreter { + /// Execute GcConfigBox methods + pub(crate) fn execute_gc_config_method( + &mut self, + gc_box: &GcConfigBox, + method: &str, + arguments: &[ASTNode], + ) -> Result, RuntimeError> { + match method { + "setFlag" => { + if arguments.len() != 2 { + return Err(RuntimeError::InvalidOperation { + message: format!("GcConfigBox.setFlag expects 2 arguments, got {}", arguments.len()), + }); + } + let name = self.execute_expression(&arguments[0])?; + let on = self.execute_expression(&arguments[1])?; + + let name_str = name.to_string_box().value; + let on_bool = if let Some(b) = on.as_any().downcast_ref::() { + b.value + } else { + on.to_string_box().value.to_lowercase() == "true" + }; + + let mut gc_clone = gc_box.clone(); + gc_clone.set_flag(&name_str, on_bool); + Ok(Box::new(gc_clone)) + } + + "getFlag" => { + if arguments.len() != 1 { + return Err(RuntimeError::InvalidOperation { + message: format!("GcConfigBox.getFlag expects 1 argument, got {}", arguments.len()), + }); + } + let name = self.execute_expression(&arguments[0])?; + let name_str = name.to_string_box().value; + Ok(gc_box.get_flag(&name_str)) + } + + "apply" => { + if !arguments.is_empty() { + return Err(RuntimeError::InvalidOperation { + message: format!("GcConfigBox.apply expects 0 arguments, got {}", arguments.len()), + }); + } + Ok(gc_box.apply()) + } + + "summary" => { + if !arguments.is_empty() { + return Err(RuntimeError::InvalidOperation { + message: format!("GcConfigBox.summary expects 0 arguments, got {}", arguments.len()), + }); + } + Ok(gc_box.summary()) + } + + _ => Err(RuntimeError::InvalidOperation { + message: format!("GcConfigBox has no method '{}'", method), + }), + } + } + + /// Execute DebugConfigBox methods + pub(crate) fn execute_debug_config_method( + &mut self, + debug_box: &DebugConfigBox, + method: &str, + arguments: &[ASTNode], + ) -> Result, RuntimeError> { + match method { + "setFlag" => { + if arguments.len() != 2 { + return Err(RuntimeError::InvalidOperation { + message: format!("DebugConfigBox.setFlag expects 2 arguments, got {}", arguments.len()), + }); + } + let name = self.execute_expression(&arguments[0])?; + let on = self.execute_expression(&arguments[1])?; + + let name_str = name.to_string_box().value; + let on_bool = if let Some(b) = on.as_any().downcast_ref::() { + b.value + } else { + on.to_string_box().value.to_lowercase() == "true" + }; + + let mut debug_clone = debug_box.clone(); + debug_clone.set_flag(&name_str, on_bool); + Ok(Box::new(debug_clone)) + } + + "setPath" => { + if arguments.len() != 2 { + return Err(RuntimeError::InvalidOperation { + message: format!("DebugConfigBox.setPath expects 2 arguments, got {}", arguments.len()), + }); + } + let name = self.execute_expression(&arguments[0])?; + let path = self.execute_expression(&arguments[1])?; + + let name_str = name.to_string_box().value; + let path_str = path.to_string_box().value; + + let mut debug_clone = debug_box.clone(); + debug_clone.set_path(&name_str, &path_str); + Ok(Box::new(debug_clone)) + } + + "getFlag" => { + if arguments.len() != 1 { + return Err(RuntimeError::InvalidOperation { + message: format!("DebugConfigBox.getFlag expects 1 argument, got {}", arguments.len()), + }); + } + let name = self.execute_expression(&arguments[0])?; + let name_str = name.to_string_box().value; + Ok(debug_box.get_flag(&name_str)) + } + + "getPath" => { + if arguments.len() != 1 { + return Err(RuntimeError::InvalidOperation { + message: format!("DebugConfigBox.getPath expects 1 argument, got {}", arguments.len()), + }); + } + let name = self.execute_expression(&arguments[0])?; + let name_str = name.to_string_box().value; + Ok(debug_box.get_path(&name_str)) + } + + "apply" => { + if !arguments.is_empty() { + return Err(RuntimeError::InvalidOperation { + message: format!("DebugConfigBox.apply expects 0 arguments, got {}", arguments.len()), + }); + } + Ok(debug_box.apply()) + } + + "summary" => { + if !arguments.is_empty() { + return Err(RuntimeError::InvalidOperation { + message: format!("DebugConfigBox.summary expects 0 arguments, got {}", arguments.len()), + }); + } + Ok(debug_box.summary()) + } + + _ => Err(RuntimeError::InvalidOperation { + message: format!("DebugConfigBox has no method '{}'", method), + }), + } + } +} \ No newline at end of file diff --git a/src/interpreter/methods_dispatch.rs b/src/interpreter/methods_dispatch.rs index 3cb0ed50..03ea36e5 100644 --- a/src/interpreter/methods_dispatch.rs +++ b/src/interpreter/methods_dispatch.rs @@ -4,6 +4,7 @@ use crate::ast::ASTNode; use crate::box_trait::{NyashBox, StringBox, IntegerBox, BoolBox, BoxCore}; use crate::boxes::{ArrayBox, FloatBox, BufferBox, ResultBox, FutureBox, JSONBox, HttpClientBox, StreamBox, RegexBox, MathBox}; use crate::boxes::{null_box, time_box, map_box, random_box, sound_box, debug_box, console_box}; +use crate::boxes::{gc_config_box::GcConfigBox, debug_config_box::DebugConfigBox}; use crate::boxes::file; use crate::channel_box::ChannelBox; use super::{NyashInterpreter, RuntimeError}; @@ -109,6 +110,14 @@ impl NyashInterpreter { if let Some(b) = obj.as_any().downcast_ref::() { return Some(self.execute_console_method(b, method, arguments)); } + // GcConfigBox + if let Some(b) = obj.as_any().downcast_ref::() { + return Some(self.execute_gc_config_method(b, method, arguments)); + } + // DebugConfigBox + if let Some(b) = obj.as_any().downcast_ref::() { + return Some(self.execute_debug_config_method(b, method, arguments)); + } None } diff --git a/src/jit/boundary.rs b/src/jit/boundary.rs new file mode 100644 index 00000000..578cb6f8 --- /dev/null +++ b/src/jit/boundary.rs @@ -0,0 +1,62 @@ +//! CallBoundaryBox: unify JIT→VM return conversion in one place +use crate::backend::vm::VMValue; +use super::abi::JitValue; + +pub struct CallBoundaryBox; + +impl CallBoundaryBox { + pub fn to_vm(ret_ty: &crate::mir::MirType, v: JitValue) -> VMValue { + match ret_ty { + crate::mir::MirType::Float => match v { + JitValue::F64(f) => VMValue::Float(f), + JitValue::I64(i) => VMValue::Float(i as f64), + JitValue::Bool(b) => VMValue::Float(if b {1.0} else {0.0}), + JitValue::Handle(h) => { + if let Some(_) = crate::jit::rt::handles::get(h) { VMValue::Float(0.0) } else { VMValue::Float(0.0) } + } + }, + crate::mir::MirType::Integer => match v { + JitValue::I64(i) => VMValue::Integer(i), + JitValue::F64(f) => VMValue::Integer(f as i64), + JitValue::Bool(b) => VMValue::Integer(if b {1} else {0}), + JitValue::Handle(h) => { + if let Some(_) = crate::jit::rt::handles::get(h) { VMValue::Integer(0) } else { VMValue::Integer(0) } + } + }, + crate::mir::MirType::Bool => match v { + JitValue::Bool(b) => VMValue::Bool(b), + JitValue::I64(i) => VMValue::Bool(i != 0), + JitValue::F64(f) => VMValue::Bool(f != 0.0), + JitValue::Handle(h) => { + if let Some(_) = crate::jit::rt::handles::get(h) { VMValue::Bool(true) } else { VMValue::Bool(false) } + } + }, + // Box-like returns: if we received a handle id (encoded as I64), resolve to BoxRef; also honor explicit Handle + crate::mir::MirType::Box(_) | crate::mir::MirType::String | crate::mir::MirType::Array(_) | crate::mir::MirType::Future(_) => { + match v { + JitValue::I64(i) => { + let h = i as u64; + if let Some(arc) = crate::jit::rt::handles::get(h) { VMValue::BoxRef(arc) } + else { VMValue::Integer(i) } + } + JitValue::Handle(h) => { + if let Some(arc) = crate::jit::rt::handles::get(h) { VMValue::BoxRef(arc) } else { VMValue::Void } + } + JitValue::F64(f) => VMValue::Float(f), + JitValue::Bool(b) => VMValue::Bool(b), + } + } + _ => { + // Default adapter with heuristic: treat I64 matching a known handle as BoxRef + match v { + JitValue::I64(i) => { + let h = i as u64; + if let Some(arc) = crate::jit::rt::handles::get(h) { VMValue::BoxRef(arc) } + else { super::abi::adapter::from_jit_value(JitValue::I64(i)) } + } + _ => super::abi::adapter::from_jit_value(v) + } + } + } + } +} diff --git a/src/jit/extern/collections.rs b/src/jit/extern/collections.rs index 22f8653c..3d6a5386 100644 --- a/src/jit/extern/collections.rs +++ b/src/jit/extern/collections.rs @@ -21,6 +21,7 @@ pub const SYM_ARRAY_PUSH_H: &str = "nyash.array.push_h"; pub const SYM_ARRAY_LAST_H: &str = "nyash.array.last_h"; pub const SYM_MAP_SIZE_H: &str = "nyash.map.size_h"; pub const SYM_MAP_GET_H: &str = "nyash.map.get_h"; +pub const SYM_MAP_GET_HH: &str = "nyash.map.get_hh"; pub const SYM_MAP_SET_H: &str = "nyash.map.set_h"; pub const SYM_MAP_HAS_H: &str = "nyash.map.has_h"; // Generic read-only helper @@ -61,18 +62,45 @@ pub fn array_get(args: &[VMValue]) -> VMValue { } pub fn array_set(args: &[VMValue]) -> VMValue { + // Enforce policy for mutating operation + if crate::jit::policy::current().read_only && + !crate::jit::policy::current().hostcall_whitelist.iter().any(|s| s == SYM_ARRAY_SET) + { + crate::jit::events::emit( + "hostcall", "", None, None, + serde_json::json!({"id": SYM_ARRAY_SET, "decision":"fallback", "reason":"policy_denied_mutating"}) + ); + return VMValue::Integer(0); + } if let (Some(arr), Some(VMValue::Integer(idx)), Some(value)) = (as_array(args), args.get(1), args.get(2)) { let val_box: Box = value.to_nyash_box(); let res = arr.set(Box::new(IntegerBox::new(*idx)), val_box); + crate::jit::events::emit( + "hostcall", "", None, None, + serde_json::json!({"id": SYM_ARRAY_SET, "decision":"allow", "argc":3, "arg_types":["Handle","I64","Handle"]}) + ); return VMValue::from_nyash_box(res); } VMValue::BoxRef(Arc::new(StringBox::new("Error: array.set expects (ArrayBox, i64, value)"))) } pub fn array_push(args: &[VMValue]) -> VMValue { + if crate::jit::policy::current().read_only && + !crate::jit::policy::current().hostcall_whitelist.iter().any(|s| s == SYM_ARRAY_PUSH) + { + crate::jit::events::emit( + "hostcall", "", None, None, + serde_json::json!({"id": SYM_ARRAY_PUSH, "decision":"fallback", "reason":"policy_denied_mutating"}) + ); + return VMValue::Integer(0); + } if let (Some(arr), Some(value)) = (as_array(args), args.get(1)) { let val_box: Box = value.to_nyash_box(); let res = arr.push(val_box); + crate::jit::events::emit( + "hostcall", "", None, None, + serde_json::json!({"id": SYM_ARRAY_PUSH, "decision":"allow", "argc":2, "arg_types":["Handle","Handle"]}) + ); return VMValue::from_nyash_box(res); } VMValue::BoxRef(Arc::new(StringBox::new("Error: array.push expects (ArrayBox, value)"))) @@ -87,10 +115,24 @@ pub fn map_get(args: &[VMValue]) -> VMValue { } pub fn map_set(args: &[VMValue]) -> VMValue { + if crate::jit::policy::current().read_only && + !crate::jit::policy::current().hostcall_whitelist.iter().any(|s| s == SYM_MAP_SET) + { + crate::jit::events::emit( + "hostcall", "", None, None, + serde_json::json!({"id": SYM_MAP_SET, "decision":"fallback", "reason":"policy_denied_mutating"}) + ); + return VMValue::Integer(0); + } if let (Some(map), Some(key), Some(value)) = (as_map(args), args.get(1), args.get(2)) { let key_box: Box = key.to_nyash_box(); let val_box: Box = value.to_nyash_box(); - return VMValue::from_nyash_box(map.set(key_box, val_box)); + let out = map.set(key_box, val_box); + crate::jit::events::emit( + "hostcall", "", None, None, + serde_json::json!({"id": SYM_MAP_SET, "decision":"allow", "argc":3, "arg_types":["Handle","Handle","Handle"]}) + ); + return VMValue::from_nyash_box(out); } VMValue::BoxRef(Arc::new(StringBox::new("Error: map.set expects (MapBox, key, value)"))) } diff --git a/src/jit/hostcall_registry.rs b/src/jit/hostcall_registry.rs index 9d2fcd55..492376b1 100644 --- a/src/jit/hostcall_registry.rs +++ b/src/jit/hostcall_registry.rs @@ -12,7 +12,8 @@ pub enum HostcallKind { ReadOnly, Mutating } struct Registry { ro: HashSet, mu: HashSet, - sig: HashMap, + // Allow multiple signatures per symbol (overloads) + sig: HashMap>, } static REG: OnceCell> = OnceCell::new(); @@ -32,16 +33,18 @@ fn ensure_default() { ] { r.mu.insert(s.to_string()); } // Signatures (v0): register known symbols with simple arg/ret kinds // math.* thin bridge: f64 signatures only (allow when args match exactly) - r.sig.insert("nyash.math.sin".to_string(), Signature { args: vec![ArgKind::F64], ret: ArgKind::F64 }); - r.sig.insert("nyash.math.cos".to_string(), Signature { args: vec![ArgKind::F64], ret: ArgKind::F64 }); - r.sig.insert("nyash.math.abs".to_string(), Signature { args: vec![ArgKind::F64], ret: ArgKind::F64 }); - r.sig.insert("nyash.math.min".to_string(), Signature { args: vec![ArgKind::F64, ArgKind::F64], ret: ArgKind::F64 }); - r.sig.insert("nyash.math.max".to_string(), Signature { args: vec![ArgKind::F64, ArgKind::F64], ret: ArgKind::F64 }); + r.sig.entry("nyash.math.sin".to_string()).or_default().push(Signature { args: vec![ArgKind::F64], ret: ArgKind::F64 }); + r.sig.entry("nyash.math.cos".to_string()).or_default().push(Signature { args: vec![ArgKind::F64], ret: ArgKind::F64 }); + r.sig.entry("nyash.math.abs".to_string()).or_default().push(Signature { args: vec![ArgKind::F64], ret: ArgKind::F64 }); + r.sig.entry("nyash.math.min".to_string()).or_default().push(Signature { args: vec![ArgKind::F64, ArgKind::F64], ret: ArgKind::F64 }); + r.sig.entry("nyash.math.max".to_string()).or_default().push(Signature { args: vec![ArgKind::F64, ArgKind::F64], ret: ArgKind::F64 }); // Collections (handle-based) - r.sig.insert("nyash.map.get_h".to_string(), Signature { args: vec![ArgKind::Handle, ArgKind::I64], ret: ArgKind::Handle }); - r.sig.insert("nyash.map.size_h".to_string(), Signature { args: vec![ArgKind::Handle], ret: ArgKind::I64 }); - r.sig.insert("nyash.array.get_h".to_string(), Signature { args: vec![ArgKind::Handle, ArgKind::I64], ret: ArgKind::Handle }); - r.sig.insert("nyash.array.len_h".to_string(), Signature { args: vec![ArgKind::Handle], ret: ArgKind::I64 }); + // Map get: support both integer and handle keys (overload) + r.sig.entry("nyash.map.get_h".to_string()).or_default().push(Signature { args: vec![ArgKind::Handle, ArgKind::I64], ret: ArgKind::Handle }); + r.sig.entry("nyash.map.get_h".to_string()).or_default().push(Signature { args: vec![ArgKind::Handle, ArgKind::Handle], ret: ArgKind::Handle }); + r.sig.entry("nyash.map.size_h".to_string()).or_default().push(Signature { args: vec![ArgKind::Handle], ret: ArgKind::I64 }); + r.sig.entry("nyash.array.get_h".to_string()).or_default().push(Signature { args: vec![ArgKind::Handle, ArgKind::I64], ret: ArgKind::Handle }); + r.sig.entry("nyash.array.len_h".to_string()).or_default().push(Signature { args: vec![ArgKind::Handle], ret: ArgKind::I64 }); let _ = REG.set(RwLock::new(r)); } @@ -124,30 +127,40 @@ pub fn set_signature_csv(symbol: &str, args_csv: &str, ret_str: &str) -> bool { if !ok { return false; } let sig = Signature { args, ret }; if let Some(lock) = REG.get() { - if let Ok(mut w) = lock.write() { w.sig.insert(symbol.to_string(), sig); return true; } + if let Ok(mut w) = lock.write() { + w.sig.entry(symbol.to_string()).or_default().push(sig); + return true; + } } false } -pub fn get_signature(symbol: &str) -> Option { - ensure_default(); - REG.get().and_then(|lock| lock.read().ok()).and_then(|g| g.sig.get(symbol).cloned()) -} - /// Check observed args against a registered signature. /// - If no signature is registered for the symbol, returns Ok(()) to be permissive in v0. /// - Returns Err("sig_mismatch") when arg length or kinds differ. pub fn check_signature(symbol: &str, observed_args: &[ArgKind]) -> Result<(), &'static str> { ensure_default(); - if let Some(sig) = get_signature(symbol) { - if sig.args.len() != observed_args.len() { return Err("sig_mismatch"); } - let cfg_now = crate::jit::config::current(); - let relax = cfg_now.relax_numeric || cfg_now.native_f64; - for (expected, observed) in sig.args.iter().zip(observed_args.iter()) { - if expected == observed { continue; } - // v0 coercion: allow I64 → F64 only when relaxed numeric is enabled - if relax && matches!(expected, ArgKind::F64) && matches!(observed, ArgKind::I64) { continue; } - return Err("sig_mismatch"); + if let Some(lock) = REG.get() { + if let Ok(g) = lock.read() { + if let Some(sigs) = g.sig.get(symbol) { + let cfg_now = crate::jit::config::current(); + let relax = cfg_now.relax_numeric || cfg_now.native_f64; + // Match against any one of the overload signatures + 'outer: for sig in sigs.iter() { + if sig.args.len() != observed_args.len() { continue; } + for (expected, observed) in sig.args.iter().zip(observed_args.iter()) { + if expected == observed { continue; } + // v0 coercion: allow I64 → F64 only when relaxed numeric is enabled + if relax && matches!(expected, ArgKind::F64) && matches!(observed, ArgKind::I64) { continue; } + // Mismatch for this candidate signature + continue 'outer; + } + // All args matched for this signature + return Ok(()); + } + // No overload matched + return Err("sig_mismatch"); + } } } Ok(()) diff --git a/src/jit/lower/builder.rs b/src/jit/lower/builder.rs index 690faa1e..f9fbe2c2 100644 --- a/src/jit/lower/builder.rs +++ b/src/jit/lower/builder.rs @@ -248,10 +248,12 @@ extern "C" fn nyash_array_len_h(handle: u64) -> i64 { } #[cfg(feature = "cranelift-jit")] extern "C" fn nyash_array_push_h(handle: u64, val: i64) -> i64 { - // Policy/Events: classify and decide + // Policy/Events: classify and decide with whitelist use crate::jit::hostcall_registry::{classify, HostcallKind}; let sym = crate::jit::r#extern::collections::SYM_ARRAY_PUSH_H; - match (classify(sym), crate::jit::policy::current().read_only) { + let pol = crate::jit::policy::current(); + let wh = pol.hostcall_whitelist; + match (classify(sym), pol.read_only && !wh.iter().any(|s| s == sym)) { (HostcallKind::Mutating, true) => { crate::jit::events::emit( "hostcall", "", None, None, @@ -308,7 +310,9 @@ extern "C" fn nyash_array_last_h(handle: u64) -> i64 { extern "C" fn nyash_array_set_h(handle: u64, idx: i64, val: i64) -> i64 { use crate::jit::hostcall_registry::{classify, HostcallKind}; let sym = crate::jit::r#extern::collections::SYM_ARRAY_SET_H; - if classify(sym) == HostcallKind::Mutating && crate::jit::policy::current().read_only { + let pol = crate::jit::policy::current(); + let wh = pol.hostcall_whitelist; + if classify(sym) == HostcallKind::Mutating && pol.read_only && !wh.iter().any(|s| s == sym) { crate::jit::events::emit( "hostcall", "", None, None, serde_json::json!({"id": sym, "decision":"fallback", "reason":"policy_denied_mutating"}) @@ -359,10 +363,33 @@ extern "C" fn nyash_map_get_h(handle: u64, key: i64) -> i64 { 0 } #[cfg(feature = "cranelift-jit")] +extern "C" fn nyash_map_get_hh(map_h: u64, key_h: u64) -> i64 { + // Emit allow event for visibility + crate::jit::events::emit( + "hostcall", "", None, None, + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_MAP_GET_HH, "decision":"allow", "argc":2, "arg_types":["Handle","Handle"]}) + ); + let map_arc = crate::jit::rt::handles::get(map_h); + let key_arc = crate::jit::rt::handles::get(key_h); + if let (Some(mobj), Some(kobj)) = (map_arc, key_arc) { + if let Some(map) = mobj.as_any().downcast_ref::() { + let key_box: Box = kobj.share_box(); + let val = map.get(key_box); + // Register result into handle table and return handle id + let arc: std::sync::Arc = std::sync::Arc::from(val); + let h = crate::jit::rt::handles::to_handle(arc); + return h as i64; + } + } + 0 +} +#[cfg(feature = "cranelift-jit")] extern "C" fn nyash_map_set_h(handle: u64, key: i64, val: i64) -> i64 { use crate::jit::hostcall_registry::{classify, HostcallKind}; let sym = crate::jit::r#extern::collections::SYM_MAP_SET_H; - if classify(sym) == HostcallKind::Mutating && crate::jit::policy::current().read_only { + let pol = crate::jit::policy::current(); + let wh = pol.hostcall_whitelist; + if classify(sym) == HostcallKind::Mutating && pol.read_only && !wh.iter().any(|s| s == sym) { crate::jit::events::emit( "hostcall", "", None, None, serde_json::json!({"id": sym, "decision":"fallback", "reason":"policy_denied_mutating"}) @@ -385,6 +412,10 @@ extern "C" fn nyash_map_set_h(handle: u64, key: i64, val: i64) -> i64 { } #[cfg(feature = "cranelift-jit")] extern "C" fn nyash_map_has_h(handle: u64, key: i64) -> i64 { + crate::jit::events::emit( + "hostcall", "", None, None, + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_MAP_HAS_H, "decision":"allow", "argc":2, "arg_types":["Handle","I64"]}) + ); if let Some(obj) = crate::jit::rt::handles::get(handle) { if let Some(map) = obj.as_any().downcast_ref::() { let key_box = Box::new(crate::box_trait::IntegerBox::new(key)); @@ -1153,6 +1184,7 @@ impl CraneliftBuilder { builder.symbol(c::SYM_ARRAY_LAST_H, nyash_array_last_h as *const u8); builder.symbol(c::SYM_MAP_SIZE_H, nyash_map_size_h as *const u8); builder.symbol(c::SYM_MAP_GET_H, nyash_map_get_h as *const u8); + builder.symbol(c::SYM_MAP_GET_HH, nyash_map_get_hh as *const u8); builder.symbol(c::SYM_MAP_SET_H, nyash_map_set_h as *const u8); builder.symbol(c::SYM_MAP_HAS_H, nyash_map_has_h as *const u8); builder.symbol(c::SYM_ANY_LEN_H, nyash_any_length_h as *const u8); diff --git a/src/jit/lower/core.rs b/src/jit/lower/core.rs index 5d36073d..ff40d1b1 100644 --- a/src/jit/lower/core.rs +++ b/src/jit/lower/core.rs @@ -278,7 +278,7 @@ impl LowerCore { } for instr in bb.instructions.iter() { self.cover_if_supported(instr); - self.try_emit(builder, instr, *bb_id, func); + if let Err(e) = self.try_emit(builder, instr, *bb_id, func) { return Err(e); } // Track FloatBox creations for later arg classification if let crate::mir::MirInstruction::NewBox { dst, box_type, .. } = instr { if box_type == "FloatBox" { self.float_box_values.insert(*dst); } } if let crate::mir::MirInstruction::Copy { dst, src } = instr { if self.float_box_values.contains(src) { self.float_box_values.insert(*dst); } } @@ -375,7 +375,7 @@ impl LowerCore { _ => { /* other terminators handled via generic emission below */ } } // Also allow other terminators to be emitted if needed - self.try_emit(builder, term, *bb_id, func); + if let Err(e) = self.try_emit(builder, term, *bb_id, func) { return Err(e); } } } builder.end_function(); @@ -469,7 +469,7 @@ impl LowerCore { if supported { self.covered += 1; } else { self.unsupported += 1; } } - fn try_emit(&mut self, b: &mut dyn IRBuilder, instr: &MirInstruction, cur_bb: crate::mir::BasicBlockId, func: &crate::mir::MirFunction) { + fn try_emit(&mut self, b: &mut dyn IRBuilder, instr: &MirInstruction, cur_bb: crate::mir::BasicBlockId, func: &crate::mir::MirFunction) -> Result<(), String> { use crate::mir::MirInstruction as I; match instr { I::NewBox { dst, box_type, args } => { @@ -543,7 +543,7 @@ impl LowerCore { BinaryOp::Mod => BinOpKind::Mod, // Not yet supported in Core-1 BinaryOp::And | BinaryOp::Or - | BinaryOp::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor | BinaryOp::Shl | BinaryOp::Shr => { return; } + | BinaryOp::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor | BinaryOp::Shl | BinaryOp::Shr => { return Ok(()); } }; b.emit_binop(kind); if let (Some(a), Some(b)) = (self.known_i64.get(lhs), self.known_i64.get(rhs)) { @@ -642,10 +642,17 @@ impl LowerCore { match method.as_str() { "len" | "length" => { if let Some(pidx) = self.param_index.get(array).copied() { - // Handle-based generic length: supports ArrayBox and StringBox + crate::jit::events::emit( + "hostcall","",None,None, + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ANY_LEN_H, "decision":"allow", "reason":"sig_ok", "argc":1, "arg_types":["Handle"]}) + ); b.emit_param_i64(pidx); b.emit_host_call(crate::jit::r#extern::collections::SYM_ANY_LEN_H, 1, dst.is_some()); } else { + crate::jit::events::emit( + "hostcall","",None,None, + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ANY_LEN_H, "decision":"fallback", "reason":"receiver_not_param", "argc":1, "arg_types":["Handle"]}) + ); let arr_idx = -1; b.emit_const_i64(arr_idx); b.emit_host_call(crate::jit::r#extern::collections::SYM_ARRAY_LEN, 1, dst.is_some()); @@ -662,21 +669,24 @@ impl LowerCore { else { observed.push(ArgKind::I64); } } // Prepare arg_types for event payload - // Classify argument kinds using known maps and FloatBox tracking; as a last resort, scan for NewBox(FloatBox) + // Classify argument kinds using TyEnv when available; fallback to known maps/FloatBox tracking let mut observed_kinds: Vec = Vec::new(); for v in args.iter() { - let mut kind = if self.known_f64.contains_key(v) || self.float_box_values.contains(v) { - crate::jit::hostcall_registry::ArgKind::F64 - } else { crate::jit::hostcall_registry::ArgKind::I64 }; - if let crate::jit::hostcall_registry::ArgKind::I64 = kind { - 'scanv: for (_bb_id, bb) in func.blocks.iter() { - for ins in bb.instructions.iter() { - if let crate::mir::MirInstruction::NewBox { dst, box_type, .. } = ins { - if *dst == *v && box_type == "FloatBox" { kind = crate::jit::hostcall_registry::ArgKind::F64; break 'scanv; } - } + let kind = if let Some(mt) = func.metadata.value_types.get(v) { + match mt { + crate::mir::MirType::Float => crate::jit::hostcall_registry::ArgKind::F64, + crate::mir::MirType::Integer => crate::jit::hostcall_registry::ArgKind::I64, + crate::mir::MirType::Bool => crate::jit::hostcall_registry::ArgKind::I64, // b1はI64 0/1に正規化 + crate::mir::MirType::String | crate::mir::MirType::Box(_) => crate::jit::hostcall_registry::ArgKind::Handle, + _ => { + if self.known_f64.contains_key(v) || self.float_box_values.contains(v) { crate::jit::hostcall_registry::ArgKind::F64 } + else { crate::jit::hostcall_registry::ArgKind::I64 } } } - } + } else { + if self.known_f64.contains_key(v) || self.float_box_values.contains(v) { crate::jit::hostcall_registry::ArgKind::F64 } + else { crate::jit::hostcall_registry::ArgKind::I64 } + }; observed_kinds.push(kind); } let arg_types: Vec<&'static str> = observed_kinds.iter().map(|k| match k { crate::jit::hostcall_registry::ArgKind::I64 => "I64", crate::jit::hostcall_registry::ArgKind::F64 => "F64", crate::jit::hostcall_registry::ArgKind::Handle => "Handle" }).collect(); @@ -753,19 +763,46 @@ impl LowerCore { } "isEmpty" | "empty" => { if let Some(pidx) = self.param_index.get(array).copied() { + crate::jit::events::emit( + "hostcall","",None,None, + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ANY_IS_EMPTY_H, "decision":"allow", "reason":"sig_ok", "argc":1, "arg_types":["Handle"]}) + ); b.emit_param_i64(pidx); // returns i64 0/1 b.emit_host_call(crate::jit::r#extern::collections::SYM_ANY_IS_EMPTY_H, 1, dst.is_some()); + } else { + crate::jit::events::emit( + "hostcall","",None,None, + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ANY_IS_EMPTY_H, "decision":"fallback", "reason":"receiver_not_param", "argc":1, "arg_types":["Handle"]}) + ); } } "push" => { // argc=2: (array_handle, value) let val = args.get(0).and_then(|v| self.known_i64.get(v)).copied().unwrap_or(0); if let Some(pidx) = self.param_index.get(array).copied() { + let pol = crate::jit::policy::current(); + let wh = &pol.hostcall_whitelist; + let sym = crate::jit::r#extern::collections::SYM_ARRAY_PUSH_H; + let allowed = !pol.read_only || wh.iter().any(|s| s == sym); + crate::jit::events::emit( + "hostcall","",None,None, + serde_json::json!({ + "id": sym, + "decision": if allowed {"allow"} else {"fallback"}, + "reason": if allowed {"sig_ok"} else {"policy_denied_mutating"}, + "argc": 2, + "arg_types": ["Handle","I64"] + }) + ); b.emit_param_i64(pidx); b.emit_const_i64(val); - b.emit_host_call(crate::jit::r#extern::collections::SYM_ARRAY_PUSH_H, 2, false); + b.emit_host_call(sym, 2, false); } else { + crate::jit::events::emit( + "hostcall","",None,None, + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_ARRAY_PUSH_H, "decision":"fallback", "reason":"receiver_not_param", "argc":2, "arg_types":["Handle","I64"]}) + ); let arr_idx = -1; b.emit_const_i64(arr_idx); b.emit_const_i64(val); @@ -775,21 +812,145 @@ impl LowerCore { "size" => { // MapBox.size(): argc=1 (map_handle) if let Some(pidx) = self.param_index.get(array).copied() { + crate::jit::events::emit( + "hostcall","",None,None, + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_MAP_SIZE_H, "decision":"allow", "reason":"sig_ok", "argc":1, "arg_types":["Handle"]}) + ); b.emit_param_i64(pidx); b.emit_host_call(crate::jit::r#extern::collections::SYM_MAP_SIZE_H, 1, dst.is_some()); } else { + crate::jit::events::emit( + "hostcall","",None,None, + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_MAP_SIZE_H, "decision":"fallback", "reason":"receiver_not_param", "argc":1, "arg_types":["Handle"]}) + ); let map_idx = -1; b.emit_const_i64(map_idx); b.emit_host_call(crate::jit::r#extern::collections::SYM_MAP_SIZE, 1, dst.is_some()); } } "get" => { - // MapBox.get(key): (map_handle, key_i64) + // MapBox.get(key): check TyEnv to choose signature (handle|i64) if let Some(pidx) = self.param_index.get(array).copied() { - let key = args.get(0).and_then(|v| self.known_i64.get(v)).copied().unwrap_or(0); - b.emit_param_i64(pidx); - b.emit_const_i64(key); - b.emit_host_call(crate::jit::r#extern::collections::SYM_MAP_GET_H, 2, dst.is_some()); + // Build observed arg kinds using TyEnv when available + let mut observed_kinds: Vec = Vec::new(); + // First arg = map handle + observed_kinds.push(crate::jit::hostcall_registry::ArgKind::Handle); + // Second arg = key (classify from TyEnv; fallback to I64 if known integer literal) + let key_kind = if let Some(key_vid) = args.get(0) { + if let Some(mt) = func.metadata.value_types.get(key_vid) { + match mt { + crate::mir::MirType::Float => crate::jit::hostcall_registry::ArgKind::I64, // coerced via VM path + crate::mir::MirType::Integer => crate::jit::hostcall_registry::ArgKind::I64, + crate::mir::MirType::Bool => crate::jit::hostcall_registry::ArgKind::I64, + crate::mir::MirType::String | crate::mir::MirType::Box(_) => crate::jit::hostcall_registry::ArgKind::Handle, + _ => { + if let Some(_) = self.known_i64.get(key_vid) { crate::jit::hostcall_registry::ArgKind::I64 } else { crate::jit::hostcall_registry::ArgKind::Handle } + } + } + } else if let Some(_) = self.known_i64.get(key_vid) { + crate::jit::hostcall_registry::ArgKind::I64 + } else { + crate::jit::hostcall_registry::ArgKind::Handle + } + } else { crate::jit::hostcall_registry::ArgKind::I64 }; + observed_kinds.push(key_kind); + + // Prepare arg_types strings for events + let arg_types: Vec<&'static str> = observed_kinds.iter().map(|k| match k { crate::jit::hostcall_registry::ArgKind::I64 => "I64", crate::jit::hostcall_registry::ArgKind::F64 => "F64", crate::jit::hostcall_registry::ArgKind::Handle => "Handle" }).collect(); + + // Signature check against registry (supports overloads) using canonical id + let canonical = "nyash.map.get_h"; + match crate::jit::hostcall_registry::check_signature(canonical, &observed_kinds) { + Ok(()) => { + // Choose symbol id for event/emit + let event_id = if matches!(key_kind, crate::jit::hostcall_registry::ArgKind::Handle) + && args.get(0).and_then(|v| self.param_index.get(v)).is_some() { + crate::jit::r#extern::collections::SYM_MAP_GET_HH + } else { + crate::jit::r#extern::collections::SYM_MAP_GET_H + }; + // Emit allow event + crate::jit::events::emit( + "hostcall", + "", + None, + None, + serde_json::json!({ + "id": event_id, + "decision": "allow", + "reason": "sig_ok", + "argc": observed_kinds.len(), + "arg_types": arg_types + }) + ); + // If key is i64, emit hostcall; if key is Handle and also a param, emit HH variant; otherwise fallback + if matches!(key_kind, crate::jit::hostcall_registry::ArgKind::I64) { + let key_i = args.get(0).and_then(|v| self.known_i64.get(v)).copied().unwrap_or(0); + b.emit_param_i64(pidx); + b.emit_const_i64(key_i); + b.emit_host_call(crate::jit::r#extern::collections::SYM_MAP_GET_H, 2, dst.is_some()); + } else if let Some(kp) = args.get(0).and_then(|v| self.param_index.get(v)).copied() { + // key is a function parameter (handle), use HH variant + b.emit_param_i64(pidx); + b.emit_param_i64(kp); + b.emit_host_call(crate::jit::r#extern::collections::SYM_MAP_GET_HH, 2, dst.is_some()); + } else { + // Not a param: fall back (receiver_not_param or key_not_param already logged) + // no emission; VM will execute + } + } + Err(reason) => { + // Signature mismatch - log and fallback + crate::jit::events::emit( + "hostcall", + "", + None, + None, + serde_json::json!({ + "id": canonical, + "decision": "fallback", + "reason": reason, + "argc": observed_kinds.len(), + "arg_types": arg_types + }) + ); + // No emission; VM path will handle + } + } + } else { + // Receiver is not a function parameter; we cannot obtain a stable runtime handle. + // Still classify and emit an event for visibility, then fallback to VM. + let mut observed_kinds: Vec = Vec::new(); + observed_kinds.push(crate::jit::hostcall_registry::ArgKind::Handle); // Map receiver (conceptually a handle) + let key_kind = if let Some(key_vid) = args.get(0) { + if let Some(mt) = func.metadata.value_types.get(key_vid) { + match mt { + crate::mir::MirType::Integer => crate::jit::hostcall_registry::ArgKind::I64, + crate::mir::MirType::Float => crate::jit::hostcall_registry::ArgKind::I64, + crate::mir::MirType::Bool => crate::jit::hostcall_registry::ArgKind::I64, + crate::mir::MirType::String | crate::mir::MirType::Box(_) => crate::jit::hostcall_registry::ArgKind::Handle, + _ => crate::jit::hostcall_registry::ArgKind::Handle, + } + } else { crate::jit::hostcall_registry::ArgKind::Handle } + } else { crate::jit::hostcall_registry::ArgKind::Handle }; + observed_kinds.push(key_kind); + let arg_types: Vec<&'static str> = observed_kinds.iter().map(|k| match k { crate::jit::hostcall_registry::ArgKind::I64 => "I64", crate::jit::hostcall_registry::ArgKind::F64 => "F64", crate::jit::hostcall_registry::ArgKind::Handle => "Handle" }).collect(); + let sym = "nyash.map.get_h"; + let decision = match crate::jit::hostcall_registry::check_signature(sym, &observed_kinds) { Ok(()) => ("fallback", "receiver_not_param"), Err(reason) => ("fallback", reason) }; + crate::jit::events::emit( + "hostcall", + "", + None, + None, + serde_json::json!({ + "id": sym, + "decision": decision.0, + "reason": decision.1, + "argc": observed_kinds.len(), + "arg_types": arg_types + }) + ); + // no-op: VM側が処理する } } "set" => { @@ -797,19 +958,47 @@ impl LowerCore { if let Some(pidx) = self.param_index.get(array).copied() { let key = args.get(0).and_then(|v| self.known_i64.get(v)).copied().unwrap_or(0); let val = args.get(1).and_then(|v| self.known_i64.get(v)).copied().unwrap_or(0); + let pol = crate::jit::policy::current(); + let wh = &pol.hostcall_whitelist; + let sym = crate::jit::r#extern::collections::SYM_MAP_SET_H; + let allowed = !pol.read_only || wh.iter().any(|s| s == sym); + crate::jit::events::emit( + "hostcall","",None,None, + serde_json::json!({ + "id": sym, + "decision": if allowed {"allow"} else {"fallback"}, + "reason": if allowed {"sig_ok"} else {"policy_denied_mutating"}, + "argc": 3, + "arg_types": ["Handle","I64","I64"] + }) + ); b.emit_param_i64(pidx); b.emit_const_i64(key); b.emit_const_i64(val); - b.emit_host_call(crate::jit::r#extern::collections::SYM_MAP_SET_H, 3, false); + b.emit_host_call(sym, 3, false); + } else { + crate::jit::events::emit( + "hostcall","",None,None, + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_MAP_SET_H, "decision":"fallback", "reason":"receiver_not_param", "argc":3, "arg_types":["Handle","I64","I64"]}) + ); } } "charCodeAt" => { // String.charCodeAt(index) if let Some(pidx) = self.param_index.get(array).copied() { let idx = args.get(0).and_then(|v| self.known_i64.get(v)).copied().unwrap_or(0); + crate::jit::events::emit( + "hostcall","",None,None, + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_STRING_CHARCODE_AT_H, "decision":"allow", "reason":"sig_ok", "argc":2, "arg_types":["Handle","I64"]}) + ); b.emit_param_i64(pidx); b.emit_const_i64(idx); b.emit_host_call(crate::jit::r#extern::collections::SYM_STRING_CHARCODE_AT_H, 2, dst.is_some()); + } else { + crate::jit::events::emit( + "hostcall","",None,None, + serde_json::json!({"id": crate::jit::r#extern::collections::SYM_STRING_CHARCODE_AT_H, "decision":"fallback", "reason":"receiver_not_param", "argc":2, "arg_types":["Handle","I64"]}) + ); } } "has" => { @@ -827,6 +1016,7 @@ impl LowerCore { } _ => {} } + Ok(()) } } diff --git a/src/jit/manager.rs b/src/jit/manager.rs index df30c16c..8b2f2e15 100644 --- a/src/jit/manager.rs +++ b/src/jit/manager.rs @@ -134,7 +134,7 @@ impl JitManager { } /// 10_c: execute compiled function if present (stub: empty args). Returns Some(VMValue) if JIT path was taken. - pub fn execute_compiled(&mut self, func: &str, args: &[crate::backend::vm::VMValue]) -> Option { + pub fn execute_compiled(&mut self, func: &str, ret_ty: &crate::mir::MirType, args: &[crate::backend::vm::VMValue]) -> Option { if let Some(h) = self.handle_of(func) { // Expose args to both legacy VM hostcalls and new JIT ABI TLS crate::jit::rt::set_legacy_vm_args(args); @@ -149,7 +149,12 @@ impl JitManager { eprintln!("[JIT] exec_time_ms={} for {}", dt.as_millis(), func); } let res = match out { - Some(v) => { self.exec_ok = self.exec_ok.saturating_add(1); Some(crate::jit::abi::adapter::from_jit_value(v)) } + Some(v) => { + self.exec_ok = self.exec_ok.saturating_add(1); + // Use CallBoundaryBox to convert JitValue → VMValue with MIR ret type hint + let vmv = crate::jit::boundary::CallBoundaryBox::to_vm(ret_ty, v); + Some(vmv) + } None => { self.exec_trap = self.exec_trap.saturating_add(1); None } }; // Clear handles created during this call diff --git a/src/jit/mod.rs b/src/jit/mod.rs index e0a57c02..9558a658 100644 --- a/src/jit/mod.rs +++ b/src/jit/mod.rs @@ -10,3 +10,4 @@ pub mod config; pub mod policy; pub mod events; pub mod hostcall_registry; +pub mod boundary; diff --git a/src/mir/builder.rs b/src/mir/builder.rs index 3c024f92..583c1e47 100644 --- a/src/mir/builder.rs +++ b/src/mir/builder.rs @@ -61,6 +61,9 @@ pub struct MirBuilder { /// Remember class of object fields after assignments: (base_id, field) -> class_name pub(super) field_origin_class: HashMap<(ValueId, String), String>, + + /// Optional per-value type annotations (MIR-level): ValueId -> MirType + pub(super) value_types: HashMap, } impl MirBuilder { @@ -78,6 +81,7 @@ impl MirBuilder { user_defined_boxes: HashSet::new(), weak_fields_by_box: HashMap::new(), field_origin_class: HashMap::new(), + value_types: HashMap::new(), } } @@ -254,13 +258,19 @@ impl MirBuilder { value: Some(result_value), }); } + // Infer return type from TyEnv (value_types) + if let Some(mt) = self.value_types.get(&result_value).cloned() { + function.signature.return_type = mt; + } } } } // Finalize and return module let mut module = self.current_module.take().unwrap(); - let function = self.current_function.take().unwrap(); + let mut function = self.current_function.take().unwrap(); + // Flush value_types (TyEnv) into function metadata + function.metadata.value_types = self.value_types.clone(); module.add_function(function); Ok(module) @@ -451,6 +461,15 @@ impl MirBuilder { /// Build a literal value fn build_literal(&mut self, literal: LiteralValue) -> Result { + // Determine type without moving literal + let ty_for_dst = match &literal { + LiteralValue::Integer(_) => Some(super::MirType::Integer), + LiteralValue::Float(_) => Some(super::MirType::Float), + LiteralValue::Bool(_) => Some(super::MirType::Bool), + LiteralValue::String(_) => Some(super::MirType::String), + _ => None, + }; + let const_value = match literal { LiteralValue::Integer(n) => ConstValue::Integer(n), LiteralValue::Float(f) => ConstValue::Float(f), @@ -465,6 +484,8 @@ impl MirBuilder { dst, value: const_value, })?; + // Annotate type + if let Some(ty) = ty_for_dst { self.value_types.insert(dst, ty); } Ok(dst) } @@ -483,6 +504,8 @@ impl MirBuilder { self.emit_instruction(MirInstruction::BinOp { dst, op, lhs, rhs })?; + // Arithmetic results are integers for now (Core-1) + self.value_types.insert(dst, super::MirType::Integer); }, // Comparison operations @@ -497,6 +520,7 @@ impl MirBuilder { (li, ri) } else { (lhs, rhs) }; self.emit_instruction(MirInstruction::Compare { dst, op, lhs: lhs2, rhs: rhs2 })?; + self.value_types.insert(dst, super::MirType::Bool); }, } @@ -515,6 +539,8 @@ impl MirBuilder { op: mir_op, operand: operand_val, })?; + // Unary op result type heuristic (neg/not): keep integer/bool for now + // Leave unset to avoid mislabel Ok(dst) } @@ -610,6 +636,8 @@ impl MirBuilder { args: math_args, effects: EffectMask::READ.add(Effect::ReadHeap), })?; + // math.* returns Float + self.value_types.insert(dst, super::MirType::Float); return Ok(dst); } @@ -1098,6 +1126,14 @@ impl MirBuilder { box_type: class.clone(), args: arg_values.clone(), })?; + // Annotate primitive boxes + match class.as_str() { + "IntegerBox" => { self.value_types.insert(dst, super::MirType::Integer); }, + "FloatBox" => { self.value_types.insert(dst, super::MirType::Float); }, + "BoolBox" => { self.value_types.insert(dst, super::MirType::Bool); }, + "StringBox" => { self.value_types.insert(dst, super::MirType::String); }, + other => { self.value_types.insert(dst, super::MirType::Box(other.to_string())); } + } // Record origin for optimization: dst was created by NewBox of class self.value_origin_newbox.insert(dst, class.clone()); @@ -1360,6 +1396,8 @@ impl MirBuilder { args: math_args, effects: EffectMask::READ.add(Effect::ReadHeap), })?; + // math.* returns Float + self.value_types.insert(result_id, super::MirType::Float); return Ok(result_id); } } diff --git a/src/mir/function.rs b/src/mir/function.rs index 893b7c2a..b015bf96 100644 --- a/src/mir/function.rs +++ b/src/mir/function.rs @@ -66,6 +66,9 @@ pub struct FunctionMetadata { /// Optimization hints pub optimization_hints: Vec, + + /// Optional per-value type map (for builders that annotate ValueId types) + pub value_types: std::collections::HashMap, } impl MirFunction { @@ -497,4 +500,4 @@ mod tests { assert_eq!(stats.value_count, 0); assert!(stats.is_pure); } -} \ No newline at end of file +} diff --git a/src/runner.rs b/src/runner.rs index 323fdc97..b0a50ec3 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -77,6 +77,10 @@ impl NyashRunner { jc.handle_debug |= self.config.jit_handle_debug; jc.native_f64 |= self.config.jit_native_f64; jc.native_bool |= self.config.jit_native_bool; + // If events are enabled and no threshold is provided, force threshold=1 so lowering runs and emits events + if std::env::var("NYASH_JIT_EVENTS").ok().as_deref() == Some("1") && jc.threshold.is_none() { + jc.threshold = Some(1); + } if self.config.jit_only { std::env::set_var("NYASH_JIT_ONLY", "1"); } // Apply runtime capability probe (e.g., disable b1 ABI if unsupported) let caps = nyash_rust::jit::config::probe_capabilities(); @@ -321,7 +325,42 @@ impl NyashRunner { match vm.execute_module(&compile_result.module) { Ok(result) => { println!("✅ VM execution completed successfully!"); - println!("Result: {:?}", result); + if let Some(func) = compile_result.module.functions.get("main") { + use nyash_rust::mir::MirType; + use nyash_rust::box_trait::{NyashBox, IntegerBox, BoolBox, StringBox}; + use nyash_rust::boxes::FloatBox; + let (ety, sval) = match &func.signature.return_type { + MirType::Float => { + if let Some(fb) = result.as_any().downcast_ref::() { + ("Float", format!("{}", fb.value)) + } else if let Some(ib) = result.as_any().downcast_ref::() { + ("Float", format!("{}", ib.value as f64)) + } else { ("Float", result.to_string_box().value) } + } + MirType::Integer => { + if let Some(ib) = result.as_any().downcast_ref::() { + ("Integer", ib.value.to_string()) + } else { ("Integer", result.to_string_box().value) } + } + MirType::Bool => { + if let Some(bb) = result.as_any().downcast_ref::() { + ("Bool", bb.value.to_string()) + } else if let Some(ib) = result.as_any().downcast_ref::() { + ("Bool", (ib.value != 0).to_string()) + } else { ("Bool", result.to_string_box().value) } + } + MirType::String => { + if let Some(sb) = result.as_any().downcast_ref::() { + ("String", sb.value.clone()) + } else { ("String", result.to_string_box().value) } + } + _ => { (result.type_name(), result.to_string_box().value) } + }; + println!("ResultType(MIR): {}", ety); + println!("Result: {}", sval); + } else { + println!("Result: {:?}", result); + } }, Err(e) => { eprintln!("❌ VM execution error: {}", e); diff --git a/src/runner/modes/vm.rs b/src/runner/modes/vm.rs index 1190c312..db74aba7 100644 --- a/src/runner/modes/vm.rs +++ b/src/runner/modes/vm.rs @@ -61,7 +61,43 @@ impl NyashRunner { match vm.execute_module(&compile_result.module) { Ok(result) => { println!("✅ VM execution completed successfully!"); - println!("Result: {:?}", result); + // Pretty-print using MIR return type when available to avoid Void-looking floats/bools + if let Some(func) = compile_result.module.functions.get("main") { + use nyash_rust::mir::MirType; + use nyash_rust::box_trait::{NyashBox, IntegerBox, BoolBox, StringBox}; + use nyash_rust::boxes::FloatBox; + let (ety, sval) = match &func.signature.return_type { + MirType::Float => { + if let Some(fb) = result.as_any().downcast_ref::() { + ("Float", format!("{}", fb.value)) + } else if let Some(ib) = result.as_any().downcast_ref::() { + ("Float", format!("{}", ib.value as f64)) + } else { ("Float", result.to_string_box().value) } + } + MirType::Integer => { + if let Some(ib) = result.as_any().downcast_ref::() { + ("Integer", ib.value.to_string()) + } else { ("Integer", result.to_string_box().value) } + } + MirType::Bool => { + if let Some(bb) = result.as_any().downcast_ref::() { + ("Bool", bb.value.to_string()) + } else if let Some(ib) = result.as_any().downcast_ref::() { + ("Bool", (ib.value != 0).to_string()) + } else { ("Bool", result.to_string_box().value) } + } + MirType::String => { + if let Some(sb) = result.as_any().downcast_ref::() { + ("String", sb.value.clone()) + } else { ("String", result.to_string_box().value) } + } + _ => { (result.type_name(), result.to_string_box().value) } + }; + println!("ResultType(MIR): {}", ety); + println!("Result: {}", sval); + } else { + println!("Result: {:?}", result); + } }, Err(e) => { eprintln!("❌ VM execution error: {}", e); process::exit(1); } }