diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 77fbc311..3e7265ec 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -9,3 +9,8 @@
- [ ] 回帰CI green(env直読み検出なし)
- [ ] stats: fallback率・理由が記録される
+### Selfhosting‑dev Gate(このブランチ向け)
+- [ ] `bash tools/selfhost_vm_smoke.sh` が PASS(plugins 無効)
+- [ ] `docs/CONTRIBUTING-MERGE.md` の境界方針を満たす(Cranelift実装差分は専用ブランチ)
+- 影響範囲: runner / interpreter / vm / tools / docs
+- Feature gates(該当時): `cranelift-jit`, その他(記述)
diff --git a/.github/workflows/core13-pure-llvm.yml b/.github/workflows/core13-pure-llvm.yml
new file mode 100644
index 00000000..ea388e0d
--- /dev/null
+++ b/.github/workflows/core13-pure-llvm.yml
@@ -0,0 +1,36 @@
+name: Core-13 Pure CI (LLVM)
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ test-core13-pure-llvm:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Install Rust (stable)
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache cargo
+ uses: Swatinem/rust-cache@v2
+
+ - name: Install LLVM 18 (llvm-config-18)
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y curl ca-certificates lsb-release wget gnupg
+ curl -fsSL https://apt.llvm.org/llvm.sh -o llvm.sh
+ chmod +x llvm.sh
+ sudo ./llvm.sh 18
+ llvm-config-18 --version
+
+ - name: Run tests with Core-13 pure mode + LLVM
+ env:
+ NYASH_MIR_CORE13_PURE: "1"
+ run: |
+ export LLVM_SYS_180_PREFIX="$(llvm-config-18 --prefix)"
+ export LLVM_SYS_181_PREFIX="$(llvm-config-18 --prefix)"
+ cargo test --features llvm --all-targets --no-fail-fast
+
diff --git a/.github/workflows/core13-pure.yml b/.github/workflows/core13-pure.yml
new file mode 100644
index 00000000..2d72dfe4
--- /dev/null
+++ b/.github/workflows/core13-pure.yml
@@ -0,0 +1,27 @@
+name: Core-13 Pure CI
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ test-core13-pure:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Install Rust (stable)
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache cargo
+ uses: Swatinem/rust-cache@v2
+
+ - name: Build (release)
+ run: cargo build --release
+
+ - name: Run tests with Core-13 pure mode
+ env:
+ NYASH_MIR_CORE13_PURE: "1"
+ run: cargo test --all-targets --no-fail-fast
+
diff --git a/.github/workflows/selfhost-minimal.yml b/.github/workflows/selfhost-minimal.yml
new file mode 100644
index 00000000..1f62c26d
--- /dev/null
+++ b/.github/workflows/selfhost-minimal.yml
@@ -0,0 +1,54 @@
+name: Selfhost Minimal Smoke
+
+on:
+ push:
+ branches: [ selfhosting-dev ]
+ paths:
+ - 'apps/selfhost-minimal/**'
+ - 'src/**'
+ - 'tools/**'
+ - 'Cargo.toml'
+ - 'Cargo.lock'
+ - '.github/workflows/selfhost-minimal.yml'
+ - 'docs/**'
+ pull_request:
+ branches: [ selfhosting-dev ]
+ paths:
+ - 'apps/selfhost-minimal/**'
+ - 'src/**'
+ - 'tools/**'
+ - 'Cargo.toml'
+ - 'Cargo.lock'
+ - 'docs/**'
+
+jobs:
+ selfhost-minimal:
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ env:
+ CARGO_TERM_COLOR: always
+ NYASH_DISABLE_PLUGINS: '1'
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Rust (stable)
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache cargo registry and build
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-cargo-
+
+ - name: Build (release, cranelift-jit)
+ run: cargo build --release --features cranelift-jit
+
+ - name: Run selfhost-minimal smoke
+ run: bash tools/selfhost_vm_smoke.sh
+
diff --git a/AGENTS.md b/AGENTS.md
index b12f1ab6..27690196 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -65,6 +65,18 @@ Flags
- `NYASH_PLUGINS_STRICT=1`: プラグインsmokeでCore‑13厳格をONにする
- `NYASH_USE_NY_COMPILER=1`: NyコンパイラMVP経路を有効化(Rust parserがフォールバック)
+## Phase 15 Policy(Self‑Hosting 集中ガイド)
+- フォーカス: Ny→MIR→VM/JIT(JITはcompiler‑only/独立実行)での自己ホスト実用化。
+- スコープ外(Do‑Not‑Do): AOT/リンク最適化、GUI/egui拡張、過剰な機能追加、広域リファクタ、最適化の深追い、新規依存追加。
+- ガードレール:
+ - 小刻み: 作業は半日粒度。詰まったら撤退→Issue化→次タスクにスイッチ。
+ - 検証: 代表スモーク(Roundtrip/using/modules/JIT直/collections)を常時維持。VMとJIT(--jit-direct)の一致が受け入れ基準。
+ - 観測: hostcall イベントは 1 呼び出し=1 件、短絡は分岐採用の記録のみ。ノイズ増は回避。
+- 3日スタートプラン:
+ 1) JSON v0 短絡 &&/|| を JSON→MIR→VM→JIT の順で最小実装。短絡副作用なしを smoke で確認。
+ 2) collections 最小 hostcall(len/get/set/push/size/has)と policy ガードの整合性チェック。
+ 3) 観測イベント(observe::lower_hostcall / lower_shortcircuit)を整備し、代表ケースで一貫した出力を確認。
+
## Coding Style & Naming Conventions
- Rust style (rustfmt defaults): 4‑space indent, `snake_case` for functions/vars, `CamelCase` for types.
- Keep patches focused; align with existing modules and file layout.
diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md
index 80d56576..8f06cb9b 100644
--- a/CURRENT_TASK.md
+++ b/CURRENT_TASK.md
@@ -1,9 +1,91 @@
# CURRENT TASK (Compact) — Phase 15 / Self-Hosting(Ny→MIR→MIR-Interp→VM 先行)
+[ブランチ方針の注記 — 2025‑09‑06 selfhosting‑dev 整理]
+- このブランチは VM/JIT を中心とした自己ホスト開発に専念します。
+- Cranelift AOT/JIT‑AOT 系の詳細課題は `docs/phase-15/cranelift/CRANELIFT_TASKS.md` へ分離しました(このファイルへの追記は最小限に)。
+- 旧コンテンツは `docs/archives/CURRENT_TASK-2025-09-06.md`(要旨)および Git 履歴(完全版)を参照してください。
+
+— Quick Update (2025‑09‑06 PM)
+- Merge: `origin/main` の Cranelift 修正を取り込み、全スモーク緑を確認。
+- P0 達成(追加):
+ - Ny→MIR 直結ブリッジ(Case A)実装(`json_v0_bridge.rs`)。`tools/ny_roundtrip_smoke.sh` PASS。
+ - env.modules 最小レジストリ追加+VM ExternCall name-route 対応(plugins不要)。`tools/modules_smoke.sh` PASS。
+ - using MVP(軽量導線): スクリプト using 行(ns/直パス)・指示コメント(@using/@module/@using-path)・環境/CLIフラグ(`--using/--module/--using-path`)を前処理で受理→env.modules登録。
+ - 未解決時は verbose で探索ヒントと候補提示(apps/lib/. を浅く走査)。
+ - 直パス missing は `NYASH_USING_STRICT=1` でエラー終了・デフォは警告継続。
+ - スモーク: `tools/using_e2e_smoke.sh`(MVP), `tools/using_resolve_smoke.sh`, `tools/using_unresolved_smoke.sh`, `tools/using_strict_path_fail_smoke.sh` 追加・PASS。
+ - JSON v0 拡張: `Bool`, `Compare(op)` を追加(検査・判定系の表現力UP)。
+
+次アクション(短期)
+- using 解決品質の向上: 候補スコアリング/断片一致・パス優先度整理(search_paths順)。
+- VM 経路の using 前処理の適用範囲見直し(必要時)。
+- JSON v0 の最小セット拡張(短絡評価/論理 and/or 等)。
+- ドキュメント最小追記(README の Self‑Hosting セクションから one‑pager へ誘導済み)。
+
このドキュメントは「いま何をすれば良いか」を最小で共有するためのコンパクト版です。詳細は git 履歴と `docs/`(phase-15)を参照してください。
— 最終更新: 2025‑09‑06 (Phase 15.16 反映, AOT/JIT-AOT 足場強化 + Phase A リファクタ着手準備)
+— Handoff (2025‑09‑06 EOD) — Phase 15 進捗と次アクション
+
+Done(本セッションで完了)
+- JSON v0: 短絡 `&&/||`(`and/or` 互換)を追加し、MIR で `Branch+Phi` 降ろし。
+ - 実装: `src/runner/json_v0_bridge.rs`(Logical追加、逐次BBで短絡組立)
+ - MIRインタプリタ: `Phi` はエントリで解決、非Phiのみ実行(`backend/mir_interpreter.rs`)。
+ - スモーク: `apps/smokes/json_v0_short_or.json`, `..._and.json`(ゼロ除算を短絡で回避)。VM/Interpreter一致。
+- Collections(VM側): プラグイン無効で最小op動作を確認。
+ - `NYASH_DISABLE_PLUGINS=1 --backend vm apps/smokes/std/array_smoke.nyash` → OK
+ - `NYASH_DISABLE_PLUGINS=1 --backend vm apps/smokes/jit_aot_map_min.nyash` → OK
+- JIT安定化(小修正): `tls_call_import_ret` に空引数ガードを追加(Cranelift 検証器エラー「引数数不一致」を抑止)。
+ - 影響: 引数不在でも import 呼を無理に発行しない。戻り値が必要な場合は `iconst 0` を合成。
+
+Next(このまま継続してOK)
+- M2 継続: Collections 最小 hostcall(len/get/set/push/size/has)+ policy 認可の再確認(JIT直も)。
+ - 追加スモーク: `--jit-direct` + `NYASH_JIT_READ_ONLY=1` で mut(push/set)拒否を確認(ビルダー安定化後に実行)。
+ - ops_ext の handle.of → *_H 経路の整合性を軽く棚卸し(定数 `SYM_*` の統一を優先)。
+- M3 着手: plugin invoke by-id/by-name の最小衛生化(成功/失敗時のフォールバック方針明記)、2件スモーク追加。
+- Telemetry(軽量): `observe::lower_hostcall` と `lower_shortcircuit` を代表ポイントに追加(イベント 1 呼=1 件)。
+
+Constraints(再掲)
+- AOT/リンク最適化・GUI拡張の深追いはしない(main側)。Phase A リファクタは挙動不変で小刻みに。
+
+Quick Verify(代表)
+- 短絡: `./target/release/nyash --json-file apps/smokes/json_v0_short_or.json` → true / `..._and.json` → false(VM も一致)
+- Collections(VM): `NYASH_DISABLE_PLUGINS=1 --backend vm apps/smokes/std/array_smoke.nyash` → `Result: 0`
+- Map(VM): `NYASH_DISABLE_PLUGINS=1 --backend vm apps/smokes/jit_aot_map_min.nyash` → `Result: 1`
+
+— Phase 15 実行計画(2週間 / VM先行・JITはcompiler-only)
+
+方針とガードレール
+- フォーカス: Ny→MIR→VM/JIT 経路の自己ホスト実用化。JITは「独立実行/コンパイラ用途」に限定。
+- スコープ外: AOT/リンカ/GUI/大規模リファクタ(main側で継続)。本ブランチは最小実装+観測整備に集中。
+- 常にスモーク先行で小刻みに前進。半日詰まりは撤退→Issue化。
+
+マイルストーン
+1) M1(1–2日): JSON v0 短絡 &&/|| 追加
+ - 受け入れ: VM/JIT一致(--jit-direct)。短絡で副作用が実行されないことをsmokeで確認。
+2) M2(2–3日): コレクション最小 hostcall(len/get/set/push/size/has)整備+policyガード再確認
+ - 受け入れ: 変異系は既定deny(policy)。許可時のみ allow がログに残る。smoke 6件緑。
+3) M3(1–2日): プラグイン橋の衛生(by-id/by-name最小)
+ - 受け入れ: 2種invokeのsmoke、ログで呼び分け確認。
+4) M4(1日): using/module の最終調整(候補提示“ほどほど”)
+ - 受け入れ: 既存smokeの文言/挙動が期待どおり。
+5) M5(1日): 可観測性の整理(observe::lower_hostcall 等)
+ - 受け入れ: 代表ケースでイベントが一貫(op/collection_type/mutates/has_policy)。
+6) M6(1日): 安定化と1ページメモ更新(入口誘導)
+
+3日スタートプラン(詳細)
+- Day1: JSON→MIR で LogicalAnd/LogicalOr を追加。JumpIfFalse/True で短絡表現。
+- Day2: MIR→VM で分岐網羅+collections最小 hostcall 経路の確認。
+- Day3: VM→JIT で短絡のLowerとイベント(lower_shortcircuit)。VM/JIT一致(--jit-direct)。
+
+Do-Not-Do(本期はやらない)
+- AOT/リンクの最適化・調査の深追い、GUI/egui拡張、機能の広げ過ぎ、最適化、新規依存追加。
+
+進捗メトリクス/撤退基準
+- 毎日: 新規/更新smokeの件数と緑率、VM/JIT一致率、イベントログ件数(hostcall 1回=1件上限)。
+- 撤退: 半日詰まり→いったん落とす・Issue化・次のマイルストーンへ。
+
【ハンドオフ(2025‑09‑06 final)— String.length 修正 完了/JIT 実行を封印し四体制へ】
概要
diff --git a/Cargo.toml b/Cargo.toml
index b3e161d2..f22e77fc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -38,6 +38,7 @@ cranelift-jit = [
"dep:cranelift-object",
"dep:cranelift-native"
]
+aot-plan-import = []
[lib]
name = "nyash_rust"
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..2a28e97b
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,35 @@
+# Nyash selfhosting-dev quick targets
+
+.PHONY: build build-release run-minimal smoke-core smoke-selfhost bootstrap roundtrip clean quick fmt lint
+
+build:
+ cargo build --features cranelift-jit
+
+build-release:
+ cargo build --release --features cranelift-jit
+
+run-minimal:
+ NYASH_DISABLE_PLUGINS=1 ./target/release/nyash --backend vm apps/selfhost-minimal/main.nyash
+
+smoke-core:
+ bash tools/jit_smoke.sh
+
+smoke-selfhost:
+ bash tools/selfhost_vm_smoke.sh
+
+bootstrap:
+ bash tools/bootstrap_selfhost_smoke.sh
+
+roundtrip:
+ bash tools/ny_roundtrip_smoke.sh
+
+clean:
+ cargo clean
+
+quick: build-release smoke-selfhost
+
+fmt:
+ cargo fmt --all
+
+lint:
+ cargo clippy --all-targets --all-features -- -D warnings || true
diff --git a/README.ja.md b/README.ja.md
index 4c288e89..b87ad62d 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -4,7 +4,8 @@
*[🇺🇸 English Version / 英語版はこちら](README.md)*
-[](#)
+[](https://github.com/moe-charm/nyash/actions/workflows/selfhost-minimal.yml)
+[](https://github.com/moe-charm/nyash/actions/workflows/smoke.yml)
[](#philosophy)
[](#performance)
[-orange.svg)](#execution-modes)
@@ -14,6 +15,18 @@
---
開発者向けクイックスタート: `docs/DEV_QUICKSTART.md`
+セルフホスト1枚ガイド: `docs/self-hosting.md`
+
+## 目次
+- [Self-Hosting(自己ホスト開発)](#self-hosting)
+- [今すぐ試す(ブラウザ)](#-今すぐブラウザでnyashを試そう)
+
+
+## 🧪 Self-Hosting(自己ホスト開発)
+- ガイド: `docs/self-hosting.md`
+- 最小E2E: `NYASH_DISABLE_PLUGINS=1 ./target/release/nyash --backend vm apps/selfhost-minimal/main.nyash`
+- スモーク: `bash tools/jit_smoke.sh` / `bash tools/selfhost_vm_smoke.sh`
+- Makefile: `make run-minimal`, `make smoke-selfhost`
変更履歴(要点): `CHANGELOG.md`
diff --git a/README.md b/README.md
index 6256bb85..5fe64881 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,8 @@
*[🇯🇵 日本語版はこちら / Japanese Version](README.ja.md)*
-[](#)
+[](https://github.com/moe-charm/nyash/actions/workflows/selfhost-minimal.yml)
+[](https://github.com/moe-charm/nyash/actions/workflows/smoke.yml)
[](#philosophy)
[](#performance)
[-orange.svg)](#execution-modes)
@@ -14,6 +15,18 @@
---
Developer quickstart: see `docs/DEV_QUICKSTART.md`. Changelog highlights: `CHANGELOG.md`.
+Self‑hosting one‑pager: `docs/self-hosting.md`.
+
+## Table of Contents
+- [Self‑Hosting (Dev Focus)](#self-hosting)
+- [Try in Browser](#-try-nyash-in-your-browser-right-now)
+
+
+## 🧪 Self‑Hosting (Dev Focus)
+- Guide: `docs/self-hosting.md`
+- Minimal E2E: `NYASH_DISABLE_PLUGINS=1 ./target/release/nyash --backend vm apps/selfhost-minimal/main.nyash`
+- Smokes: `bash tools/jit_smoke.sh` / `bash tools/selfhost_vm_smoke.sh`
+- Makefile: `make run-minimal`, `make smoke-selfhost`
Note: JIT runtime execution is currently disabled to reduce debugging overhead. Use Interpreter/VM for running and AOT (Cranelift/LLVM) for distribution.
diff --git a/app_str b/app_str
index 6741acb0..cf5070ec 100644
Binary files a/app_str and b/app_str differ
diff --git a/apps/ny-parser-nyash/main.nyash b/apps/ny-parser-nyash/main.nyash
index 6eab2190..08badc4e 100644
--- a/apps/ny-parser-nyash/main.nyash
+++ b/apps/ny-parser-nyash/main.nyash
@@ -1,6 +1,6 @@
// Entry: read stdin, parse with ParserV0, print JSON IR or error JSON
-include("./apps/ny-parser-nyash/parser_minimal.nyash")
+include "./apps/ny-parser-nyash/parser_minimal.nyash"
static box Main {
main(args) {
@@ -26,4 +26,3 @@ static box Main {
return 0
}
}
-
diff --git a/apps/ny-parser-nyash/parser_minimal.nyash b/apps/ny-parser-nyash/parser_minimal.nyash
index c6c5d326..6a2b8c18 100644
--- a/apps/ny-parser-nyash/parser_minimal.nyash
+++ b/apps/ny-parser-nyash/parser_minimal.nyash
@@ -1,6 +1,6 @@
// Minimal recursive-descent parser for Ny v0 producing JSON IR v0 (MapBox)
-include("./apps/ny-parser-nyash/tokenizer.nyash")
+include "./apps/ny-parser-nyash/tokenizer.nyash"
static box ParserV0 {
init { tokens, pos }
@@ -85,4 +85,3 @@ static box ParserV0 {
return err
}
}
-
diff --git a/apps/smokes/jit_direct_array_mut.nyash b/apps/smokes/jit_direct_array_mut.nyash
new file mode 100644
index 00000000..a8b70490
--- /dev/null
+++ b/apps/smokes/jit_direct_array_mut.nyash
@@ -0,0 +1,6 @@
+// Expect: JIT-direct read_only policy denies mutating hostcalls (push)
+return (fn(){
+ local a = new ArrayBox()
+ a.push(1)
+ return a.length()
+})()
diff --git a/apps/smokes/json_v0_short_and.json b/apps/smokes/json_v0_short_and.json
new file mode 100644
index 00000000..cfbbf523
--- /dev/null
+++ b/apps/smokes/json_v0_short_and.json
@@ -0,0 +1,21 @@
+{
+ "version": 0,
+ "kind": "Program",
+ "body": [
+ {
+ "type": "Return",
+ "expr": {
+ "type": "Logical",
+ "op": "&&",
+ "lhs": { "type": "Bool", "value": false },
+ "rhs": {
+ "type": "Compare",
+ "op": "==",
+ "lhs": { "type": "Binary", "op": "/", "lhs": { "type": "Int", "value": 1 }, "rhs": { "type": "Int", "value": 0 } },
+ "rhs": { "type": "Int", "value": 1 }
+ }
+ }
+ }
+ ]
+}
+
diff --git a/apps/smokes/json_v0_short_or.json b/apps/smokes/json_v0_short_or.json
new file mode 100644
index 00000000..0eca448c
--- /dev/null
+++ b/apps/smokes/json_v0_short_or.json
@@ -0,0 +1,21 @@
+{
+ "version": 0,
+ "kind": "Program",
+ "body": [
+ {
+ "type": "Return",
+ "expr": {
+ "type": "Logical",
+ "op": "||",
+ "lhs": { "type": "Bool", "value": true },
+ "rhs": {
+ "type": "Compare",
+ "op": "==",
+ "lhs": { "type": "Binary", "op": "/", "lhs": { "type": "Int", "value": 1 }, "rhs": { "type": "Int", "value": 0 } },
+ "rhs": { "type": "Int", "value": 1 }
+ }
+ }
+ }
+ ]
+}
+
diff --git a/dev/selfhosting/README.md b/dev/selfhosting/README.md
index a15a732d..40419b38 100644
--- a/dev/selfhosting/README.md
+++ b/dev/selfhosting/README.md
@@ -17,6 +17,10 @@ Quickstart
- Bootstrap c0→c1→c1' (optional):
- `./tools/bootstrap_selfhost_smoke.sh`
+Docs
+
+- One‑page guide: `docs/self-hosting.md`
+
Flags
- `NYASH_DISABLE_PLUGINS=1`: stabilize core path
@@ -28,4 +32,3 @@ Tips
- For debug, set `NYASH_CLI_VERBOSE=1`.
- Keep temp artifacts under this folder (`dev/selfhosting/_tmp/`) to avoid polluting repo root.
-
diff --git a/docs/CONTRIBUTING-MERGE.md b/docs/CONTRIBUTING-MERGE.md
new file mode 100644
index 00000000..1f9b8436
--- /dev/null
+++ b/docs/CONTRIBUTING-MERGE.md
@@ -0,0 +1,34 @@
+# Merge Strategy — selfhosting‑dev × Cranelift branches
+
+目的
+- selfhosting‑dev(VM/JIT 自己ホスト)と Cranelift 専用ブランチ(AOT/JIT‑AOT)を並行開発しつつ、衝突と複雑な解消作業を最小化する。
+
+ブランチの役割
+- `selfhosting-dev`: Ny→MIR→MIR-Interp→VM/JIT の安定化、ツール/スモーク、ドキュメント整備。
+- `phase-15/self-host-aot-cranelift`(例): Cranelift backend の実装・検証。
+- `develop`: 定期同期の受け皿。`main` はリリース用。
+
+方針(設計)
+- 境界の明確化: Cranelift 固有コード(例: `src/jit/*`, `src/jit/rt.rs` など)は専用ブランチで集中的に変更。selfhosting‑dev は Runner/Interpreter/VM の公共 API に限定。
+- Feature gate: 共有面に変更が必要な場合は `#[cfg(feature = "cranelift-jit")]` 等で分岐し、ABI/シグネチャ互換を保つ。
+- ドキュメント分離: `CURRENT_TASK.md` はインデックス化し、詳細は `docs/phase-15/*` へトピックごとに分離(本運用により md の大規模衝突を回避)。
+
+方針(運用)
+- 同期リズム: selfhosting‑dev → develop へ週1回まとめPR。Cranelift 側も同周期で develop へリベース/マージ。
+- 早期検知: 各PRで `rg` による衝突予兆チェック(ファイル/トークンベース)をテンプレに含める。
+- rerere: `git config rerere.enabled true` を推奨し、同種の衝突解消を再利用。
+- ラベル運用: `area:jit`, `area:vm`, `docs:phase-15`, `merge-risk:high` 等でレビュー優先度を明示。
+
+ファイルオーナーシップ(推奨)
+- Cranelift: `src/jit/**`, `src/jit/policy.rs`, `tools/*aot*`, `docs/phase-15/cranelift/**`
+- Selfhost core: `src/interpreter/**`, `src/runner/**`, `dev/selfhosting/**`, `tools/jit_smoke.sh`, `tools/selfhost_vm_smoke.sh`
+- 共有/IR: `src/mir/**`, `src/parser/**` は変更時に両ブランチへ告知(PR説明で影響範囲を明記)。
+
+実務Tips
+- マージ用テンプレ(PR description)
+ - 目的 / 影響範囲 / 変更対象ファイル / 互換性 / リスクと回避策 / テスト項目
+- 衝突抑止の小技
+ - md は章分割して別ファイルに参照化(本運用の通り)
+ - 大規模renameは単独PRで先行適用
+ - 共有インターフェイスは薄いアダプタで橋渡し(実装詳細は各ブランチ内)
+
diff --git a/docs/DEV_QUICKSTART.md b/docs/DEV_QUICKSTART.md
index e1ed3f8b..ef0d3813 100644
--- a/docs/DEV_QUICKSTART.md
+++ b/docs/DEV_QUICKSTART.md
@@ -2,6 +2,9 @@
This quickstart summarizes the most common build/run/test flows when working on Nyash.
+See also
+- Self‑hosting one‑pager: `docs/self-hosting.md`
+
## Build
- VM/JIT (Cranelift): `cargo build --release --features cranelift-jit`
- LLVM AOT: `LLVM_SYS_180_PREFIX=$(llvm-config-18 --prefix) cargo build --release --features llvm`
@@ -30,4 +33,3 @@ This quickstart summarizes the most common build/run/test flows when working on
## Testing
- Rust unit tests: `cargo test`
- Targeted: e.g., tokenizer/sugar config `cargo test --lib sugar_basic_test -- --nocapture`
-
diff --git a/docs/archives/CURRENT_TASK-2025-09-06.md b/docs/archives/CURRENT_TASK-2025-09-06.md
new file mode 100644
index 00000000..861d8389
--- /dev/null
+++ b/docs/archives/CURRENT_TASK-2025-09-06.md
@@ -0,0 +1,9 @@
+# CURRENT TASK — アーカイブ(2025‑09‑06)
+
+このファイルは `CURRENT_TASK.md` の旧来の完全版をそのまま保存するアーカイブです。
+Cranelift/AOT/JIT‑AOT 関連の詳細は今後 `docs/phase-15/cranelift/CRANELIFT_TASKS.md` 側で更新します。
+
+注意: ここに記載のコマンドやパスは当時点のものです。最新の手順や方針は `CURRENT_TASK.md` と phase‑15 配下のドキュメントを参照してください。
+
+(元の内容は Git 履歴から参照してください)
+
diff --git a/docs/development/roadmap/phases/phase-15/phase-15.1/README.md b/docs/development/roadmap/phases/phase-15/phase-15.1/README.md
new file mode 100644
index 00000000..ba27c0f0
--- /dev/null
+++ b/docs/development/roadmap/phases/phase-15/phase-15.1/README.md
@@ -0,0 +1,89 @@
+# Phase 15.1 — Self-Hosting AOT-Plan to MIR13 (Nyash-first)
+
+Scope: Extend Phase 15 (Self-Hosting Ny → MIR → VM/JIT) with a small, safe subphase that keeps VM/JIT primary, and prepares a future AOT pipeline by introducing:
+
+- Nyash scripts that perform AOT preflight analysis across `using` trees
+- A stable AOT-Plan JSON schema (functions, externs, exports, type hints, link units)
+- A compiler-side importer that lowers AOT-Plan → MIR13 skeletons (no object code yet)
+
+Avoid: deep AOT emission/linking, cross-platform toolchain work, or scope creep beyond planning and MIR13 import.
+
+## Phase 15 Goals (context)
+
+- VM-first correctness; JIT as compiler-only, not required for AOT deliverables
+- Minimal policy: read-only JIT-direct default, deny mut hostcalls unless whitelisted
+- Observability: compile/runtime JSONL events; counters for lower-time fallbacks
+- Short-circuit lowering and core collection ops kept consistent
+
+## Phase 15.1 Objectives
+
+- AOT-Plan via Nyash scripts (self-hosted analysis):
+ - Parse sources with `using` resolution; build function inventory and extern references
+ - Compute minimal “link unit” groups (per file or per module) pragmatically
+ - Produce `aot_plan.v1.json`
+
+- MIR13 importer:
+ - Read AOT-Plan → create MIR13 functions with signatures and extern stubs
+ - Leave bodies empty or minimal where applicable; execution stays VM/JIT
+
+- Smokes:
+ - `plan: using` on 2–3 small Nyash projects; output deterministic JSON
+ - Import the plan and run via VM to confirm pipeline integrity (no AOT emit)
+
+## Deliverables
+
+- `tools/aot_plan/` Nyash scripts and helpers
+- `docs/specs/aot_plan_v1.md` (lightweight schema)
+- Compiler entry to import AOT-Plan → MIR13 (feature-gated)
+- 3 smokes + 1 golden JSON sample
+
+## Out of Scope (Phase 15.1)
+
+- Object emission, linkers, archive/rpath, platform toolchains
+- Non-trivial inliner/optimizer passes dedicated to AOT
+
+## Milestones
+
+1) AOT-Plan schema v1
+- Minimal fields: functions, externs, exports, units, types(optional)
+- Golden JSON example committed
+
+2) Nyash analyzer (self-hosted)
+- Walk `using` graph; collect symbols and extern refs
+- Output `aot_plan.v1.json`
+
+3) Importer to MIR13
+- Map functions → MIR13 signatures and extern call placeholders
+- Feature-gate import; maintain VM/JIT run with consistency
+
+4) Smokes + Observability
+- 3 projects → stable plan JSON; importer round-trip builds MIR
+- Emit `jit::events` low-volume markers: `aot_plan.import`, `aot_plan.analyze`
+
+## Risk & Guardrails
+
+- Risk: scope creep to AOT emit → Guard: no obj/link in 15.1
+- Risk: importer expands semantics
+ → Guard: stub bodies only; effects mask conservative; VM reference behavior unchanged
+- Risk: plan schema churn → Guard: v1 frozen; add `extensions` map for future keys
+
+---
+
+## Consultation Notes (Gemini / Claude)
+
+Prompts used:
+- Gemini: "Phase 15 self-hosting goals (VM-first/JIT-compiler-only). Propose a 2-week 15.1 scope to add Nyash-driven AOT preflight that outputs a stable AOT plan JSON, plus a MIR13 importer—no object emission. Include milestones, acceptance criteria, and guardrails to prevent scope creep. Keep implementation incremental and observable."
+- Claude: "Given an existing Ny parser and using-resolution, design a minimal AOT planning pipeline as Ny scripts that produces a plan.json (functions/externs/exports/units). Define the MIR13 importer requirements and tests ensuring VM/JIT behavior remains canonical. Provide risks, do-not-do list, and minimal telemetry."
+
+Key takeaways aligned into this document:
+- Keep 15.1 as a planning/import phase with strong do-not-do
+- Make plan JSON stable and small; let importer be skeletal
+- Add clear smokes and counters; avoid new runtime semantics
+
+## Acceptance Criteria
+
+- `tools/aot_plan` can analyze a small project with `using` and emit deterministic JSON
+- Importer can read that JSON and construct MIR13 module(s) without panics
+- VM runs those modules and matches expected string/number results for trivial bodies
+- Events present when enabled; counters reflect plan/import activity; no AOT emit performed
+
diff --git a/docs/phase-15/cranelift/CRANELIFT_TASKS.md b/docs/phase-15/cranelift/CRANELIFT_TASKS.md
new file mode 100644
index 00000000..f5f37cfe
--- /dev/null
+++ b/docs/phase-15/cranelift/CRANELIFT_TASKS.md
@@ -0,0 +1,25 @@
+# Cranelift / AOT/JIT‑AOT Tasks (Phase 15)
+
+このドキュメントは Cranelift backend(AOT/JIT‑AOT)関連の課題・進捗を集約します。
+selfhosting‑dev ブランチでは VM/JIT 中心で開発するため、詳細はこちらへ集約し、`CURRENT_TASK.md` は軽量化しました。
+
+最終更新: 2025‑09‑06(CURRENT_TASK から分離)
+
+参考リンク
+- 旧コンテンツ・完全版アーカイブ: `../../archives/CURRENT_TASK-2025-09-06.md`
+- フェーズ概要: `../README.md`
+
+現状サマリ(抜粋)
+- StringBox.length/len が 0 になるケースの是正(Lower 二段フォールバック: string.len_h → any.length_h)
+- Hostcall registry/extern thunks の追補(`SYM_STRING_LEN_H` 登録)
+- AOT でのまれな segfault(DT_TEXTREL 警告)の追跡(TLS/extern 紐付け順)
+
+優先課題(案)
+1) Return 材化の強化(JIT‑direct/JIT‑AOT 共通)
+2) Cranelift import シンボル解決の検証(`extern_thunks::nyash_string_len_h` の実呼出し保証)
+3) AOT ツールチェーン(リンク・フラグ)の最小安定セット定義
+
+運用メモ
+- selfhosting‑dev では本ファイルの参照のみ(直接の実装変更は Cranelift 専用ブランチで実施)。
+- 共有面(ランナー/IR など)に変更が必要な場合は feature gate と互換 API を優先し、両ブランチが同時に衝突しない形へ調整。
+
diff --git a/docs/self-hosting.md b/docs/self-hosting.md
new file mode 100644
index 00000000..184fd09a
--- /dev/null
+++ b/docs/self-hosting.md
@@ -0,0 +1,51 @@
+# Self‑Hosting Dev — One‑Page Guide (selfhosting‑dev)
+
+目的
+- Ny → MIR → MIR‑Interp → VM/JIT の自己ホスト経路を最短手順で動かし、安定化する。
+- Cranelift AOT/JIT‑AOT の詳細は別管理(リンク参照)。
+
+前提
+- Rust(stable): `cargo --version`
+- Bash/grep/rg(ripgrep)
+- Linux/WSL/Unix シェル環境(WindowsはWSL推奨、PowerShellのps1も一部あり)
+
+クイックスタート
+1) ビルド(JIT有効)
+ - `cargo build --release --features cranelift-jit`
+2) 最小E2E(VM/JIT, plugins無効)
+ - `NYASH_DISABLE_PLUGINS=1 ./target/release/nyash --backend vm apps/selfhost-minimal/main.nyash`
+ - 期待: `Result: 0`
+3) スモーク一式(コア)
+ - `bash tools/jit_smoke.sh`
+4) selfhost‑minimal 専用スモーク
+ - `bash tools/selfhost_vm_smoke.sh`
+5) ブートストラップ(任意)
+ - `bash tools/bootstrap_selfhost_smoke.sh`
+6) ラウンドトリップJSON(任意)
+ - `bash tools/ny_roundtrip_smoke.sh`
+
+便利フラグ(抜粋)
+- `NYASH_DISABLE_PLUGINS=1` : 外部プラグイン無効化でコア安定化
+- `NYASH_CLI_VERBOSE=1` : 実行ログ詳細化
+- `NYASH_JIT_THRESHOLD=1` : JIT 降臨を確実化(ベンチ目的で使用)
+- `NYASH_LOAD_NY_PLUGINS=1` : Ny プラグインロード(安定化後に段階的に試験)
+
+トラブルシュート
+- タイムアウト/ハング: `timeout 15s ...` を付ける、`NYASH_CLI_VERBOSE=1` で原因把握
+- プラグイン関連エラー: まず `NYASH_DISABLE_PLUGINS=1` を設定してコア確認
+- パス不一致: すべて repo ルート相対のパスで実行しているか確認
+- ビルドキャッシュ: `cargo clean -p nyash` で個別クリーンを試す
+
+CI 連携(参考)
+- 既存Workflow: `.github/workflows/smoke.yml`(JIT/VMコアを含む)
+- ローカル再現: `bash tools/smoke_phase_10_10.sh` ほか、上記スモークを順に実行
+
+ブランチ方針/マージ
+- 本ブランチは VM/JIT 中心。Cranelift は別ドキュメントへ分離。
+- マージ運用・衝突回避は `docs/CONTRIBUTING-MERGE.md` を参照。
+- Cranelift タスク詳細: `docs/phase-15/cranelift/CRANELIFT_TASKS.md`
+
+連絡ノート
+- 変更は基本 `tools/`, `dev/selfhosting/`, `src/{interpreter,runner,mir,parser}` の最小限に集中。
+- 共有インターフェイス変更時は feature gate 等で互換性を維持。
+
diff --git a/docs/specs/aot_plan_v1.md b/docs/specs/aot_plan_v1.md
new file mode 100644
index 00000000..903660d9
--- /dev/null
+++ b/docs/specs/aot_plan_v1.md
@@ -0,0 +1,37 @@
+# AOT-Plan v1 Schema (Phase 15.1)
+
+Status: draft-frozen for Phase 15.1 (extensions via `extensions` object only)
+
+- version: string, must be "1"
+- name: optional plan/module name
+- functions: array of PlanFunction
+- externs: optional array of extern identifiers (reserved; not required in 15.1)
+- exports: optional array of export names (reserved)
+- units: optional array of link units (reserved)
+- extensions: optional object for forward-compatible keys
+
+PlanFunction:
+- name: string
+- params: array of { name: string, type?: string } (informational in 15.1)
+- return_type: optional string; one of: integer, float, bool, string, void (or omitted → Unknown)
+- body: optional object with tagged `kind`
+ - kind = "const_return": { value: any-json (int/bool/float/string supported) }
+ - kind = "empty": returns default 0 with Unknown type (Phase 15.1 importer behavior)
+
+Notes:
+- 15.1 importer does not emit object code; it constructs MIR13 skeletons only.
+- If `return_type` is omitted, importer uses Unknown to keep VM dynamic display.
+- `extensions` is a free-form map; the importer ignores unknown keys.
+
+Example:
+```
+{
+ "version": "1",
+ "name": "mini_project",
+ "functions": [
+ { "name": "main", "return_type": "integer", "body": { "kind": "const_return", "value": 42 }},
+ { "name": "greet", "return_type": "string", "body": { "kind": "const_return", "value": "hi" }}
+ ]
+}
+```
+
diff --git a/src/backend/llvm/compiler.rs b/src/backend/llvm/compiler.rs
index b04f81f1..770f2a6a 100644
--- a/src/backend/llvm/compiler.rs
+++ b/src/backend/llvm/compiler.rs
@@ -885,6 +885,83 @@ impl LLVMCompiler {
}
} else { vmap.insert(*d, rv); }
}
+ } else if iface_name == "env.local" && method_name == "get" {
+ // Core-13 pure shim: get(ptr) → return the SSA value of ptr
+ if let Some(d) = dst { let av = *vmap.get(&args[0]).ok_or("extern arg missing")?; vmap.insert(*d, av); }
+ } else if iface_name == "env.local" && method_name == "set" {
+ // set(ptr, val) → no-op at AOT SSA level (ptr is symbolic)
+ // No dst expected in our normalization; ignore safely
+ } else if iface_name == "env.box" && method_name == "new" {
+ // Call NyRT shim:
+ // - 1 arg: i64 @nyash.env.box.new(i8* type)
+ // - >=2arg: i64 @nyash.env.box.new_i64(i8* type, i64 argc, i64 a1, i64 a2)
+ if args.len() < 1 { return Err("env.box.new expects at least 1 arg (type name)".to_string()); }
+ let i8p = codegen.context.i8_type().ptr_type(AddressSpace::from(0));
+ let tyv = *vmap.get(&args[0]).ok_or("type name arg missing")?;
+ let ty_ptr = match tyv { BasicValueEnum::PointerValue(p) => p, _ => return Err("env.box.new type must be i8* string".to_string()) };
+ let i64t = codegen.context.i64_type();
+ let ret_to_ptr = |rv: BasicValueEnum| -> Result {
+ let i64v = if let BasicValueEnum::IntValue(iv) = rv { iv } else { return Err("env.box.new ret expected i64".to_string()); };
+ let pty = i8p;
+ let ptr = codegen.builder.build_int_to_ptr(i64v, pty, "box_handle_to_ptr").map_err(|e| e.to_string())?;
+ Ok(ptr.into())
+ };
+ // Helper: coerce arbitrary BasicValueEnum to i64; for i8* assume string and convert to box-handle via nyash.box.from_i8_string
+ let to_i64 = |v: BasicValueEnum| -> Result {
+ match v {
+ BasicValueEnum::IntValue(iv) => Ok(iv),
+ BasicValueEnum::FloatValue(fv) => {
+ // Route via NyRT: i64 @nyash.box.from_f64(double)
+ let i64t = codegen.context.i64_type();
+ let fnty = i64t.fn_type(&[codegen.context.f64_type().into()], false);
+ let callee = codegen.module.get_function("nyash.box.from_f64").unwrap_or_else(|| codegen.module.add_function("nyash.box.from_f64", fnty, None));
+ let call = codegen.builder.build_call(callee, &[fv.into()], "arg_f64_to_box").map_err(|e| e.to_string())?;
+ let rv = call.try_as_basic_value().left().ok_or("from_f64 returned void".to_string())?;
+ if let BasicValueEnum::IntValue(h) = rv { Ok(h) } else { Err("from_f64 ret expected i64".to_string()) }
+ }
+ BasicValueEnum::PointerValue(pv) => {
+ // If pointer is i8*, call nyash.box.from_i8_string to obtain a handle (i64)
+ let ty = pv.get_type();
+ let elem = ty.get_element_type();
+ if elem == codegen.context.i8_type().as_basic_type_enum() {
+ let i64t = codegen.context.i64_type();
+ let fnty = i64t.fn_type(&[i8p.into()], false);
+ let callee = codegen.module.get_function("nyash.box.from_i8_string").unwrap_or_else(|| codegen.module.add_function("nyash.box.from_i8_string", fnty, None));
+ let call = codegen.builder.build_call(callee, &[pv.into()], "arg_i8_to_box").map_err(|e| e.to_string())?;
+ let rv = call.try_as_basic_value().left().ok_or("from_i8_string returned void".to_string())?;
+ if let BasicValueEnum::IntValue(h) = rv { Ok(h) } else { Err("from_i8_string ret expected i64".to_string()) }
+ } else {
+ Ok(codegen.builder.build_ptr_to_int(pv, codegen.context.i64_type(), "p2i").map_err(|e| e.to_string())?)
+ }
+ }
+ _ => Err("unsupported arg value for env.box.new".to_string()),
+ }
+ };
+ let out_val = if args.len() == 1 {
+ let fnty = i64t.fn_type(&[i8p.into()], false);
+ let callee = codegen.module.get_function("nyash.env.box.new").unwrap_or_else(|| codegen.module.add_function("nyash.env.box.new", fnty, None));
+ let call = codegen.builder.build_call(callee, &[ty_ptr.into()], "env_box_new").map_err(|e| e.to_string())?;
+ call.try_as_basic_value().left().ok_or("env.box.new returned void".to_string())?
+ } else {
+ // Support up to 4 args for now
+ if args.len() - 1 > 4 { return Err("env.box.new supports up to 4 args in AOT shim".to_string()); }
+ let fnty = i64t.fn_type(&[i8p.into(), i64t.into(), i64t.into(), i64t.into(), i64t.into(), i64t.into()], false);
+ let callee = codegen.module.get_function("nyash.env.box.new_i64x").unwrap_or_else(|| codegen.module.add_function("nyash.env.box.new_i64x", fnty, None));
+ let argc_val = i64t.const_int((args.len() - 1) as u64, false);
+ // helper to coerce to i64
+ let get_i64 = |vid: ValueId| -> Result { to_i64(*vmap.get(&vid).ok_or("arg missing")?) };
+ let mut a1 = i64t.const_zero();
+ let mut a2 = i64t.const_zero();
+ if args.len() >= 2 { a1 = get_i64(args[1])?; }
+ if args.len() >= 3 { a2 = get_i64(args[2])?; }
+ let mut a3 = i64t.const_zero();
+ let mut a4 = i64t.const_zero();
+ if args.len() >= 4 { a3 = get_i64(args[3])?; }
+ if args.len() >= 5 { a4 = get_i64(args[4])?; }
+ let call = codegen.builder.build_call(callee, &[ty_ptr.into(), argc_val.into(), a1.into(), a2.into(), a3.into(), a4.into()], "env_box_new_i64x").map_err(|e| e.to_string())?;
+ call.try_as_basic_value().left().ok_or("env.box.new_i64 returned void".to_string())?
+ };
+ if let Some(d) = dst { vmap.insert(*d, ret_to_ptr(out_val)?); }
} else {
return Err(format!("ExternCall lowering unsupported: {}.{} (enable NYASH_LLVM_ALLOW_BY_NAME=1 to try by-name, or add a NyRT shim)", iface_name, method_name));
}
@@ -1257,9 +1334,65 @@ impl LLVMCompiler {
mir_module: &MirModule,
temp_path: &str,
) -> Result, String> {
+ // 1) Emit object via real LLVM lowering to ensure IR generation remains healthy
let obj_path = format!("{}.o", temp_path);
self.compile_module(mir_module, &obj_path)?;
- // For now, return 0 as IntegerBox (skeleton)
+
+ // 2) Execute via a minimal MIR interpreter for parity (until full AOT linkage is wired)
+ // Supports: Const(Integer/Bool/String/Null), BinOp on Integer, Return(Some/None)
+ // This mirrors the non-LLVM mock path just enough for simple parity tests.
+ self.values.clear();
+ let func = mir_module
+ .functions
+ .get("Main.main")
+ .or_else(|| mir_module.functions.get("main"))
+ .or_else(|| mir_module.functions.values().next())
+ .ok_or_else(|| "Main.main function not found".to_string())?;
+
+ use crate::mir::instruction::MirInstruction as I;
+ for inst in &func.get_block(func.entry_block).unwrap().instructions {
+ match inst {
+ I::Const { dst, value } => {
+ let v: Box = match value {
+ ConstValue::Integer(i) => Box::new(IntegerBox::new(*i)),
+ ConstValue::Float(f) => Box::new(crate::boxes::math_box::FloatBox::new(*f)),
+ ConstValue::String(s) => Box::new(crate::box_trait::StringBox::new(s.clone())),
+ ConstValue::Bool(b) => Box::new(crate::box_trait::BoolBox::new(*b)),
+ ConstValue::Null => Box::new(crate::boxes::null_box::NullBox::new()),
+ ConstValue::Void => Box::new(IntegerBox::new(0)),
+ };
+ self.values.insert(*dst, v);
+ }
+ I::BinOp { dst, op, lhs, rhs } => {
+ let l = self
+ .values
+ .get(lhs)
+ .and_then(|b| b.as_any().downcast_ref::())
+ .ok_or_else(|| format!("binop lhs %{} not integer", lhs.0))?;
+ let r = self
+ .values
+ .get(rhs)
+ .and_then(|b| b.as_any().downcast_ref::())
+ .ok_or_else(|| format!("binop rhs %{} not integer", rhs.0))?;
+ let res = match op {
+ BinaryOp::Add => l.value() + r.value(),
+ BinaryOp::Sub => l.value() - r.value(),
+ BinaryOp::Mul => l.value() * r.value(),
+ BinaryOp::Div => {
+ if r.value() == 0 { return Err("division by zero".into()); }
+ l.value() / r.value()
+ }
+ BinaryOp::Mod => l.value() % r.value(),
+ };
+ self.values.insert(*dst, Box::new(IntegerBox::new(res)));
+ }
+ I::Return { value } => {
+ if let Some(v) = value { return self.values.get(v).map(|b| b.clone_box()).ok_or_else(|| format!("return %{} missing", v.0)); }
+ return Ok(Box::new(IntegerBox::new(0)));
+ }
+ _ => { /* ignore for now */ }
+ }
+ }
Ok(Box::new(IntegerBox::new(0)))
}
}
diff --git a/src/backend/mir_interpreter.rs b/src/backend/mir_interpreter.rs
index 6b9e8322..ee5b7301 100644
--- a/src/backend/mir_interpreter.rs
+++ b/src/backend/mir_interpreter.rs
@@ -54,8 +54,8 @@ impl MirInterpreter {
}
}
}
- // Execute non-terminator instructions
- for inst in &block.instructions {
+ // Execute non-phi, non-terminator instructions
+ for inst in block.non_phi_instructions() {
match inst {
MirInstruction::Const { dst, value } => {
let v = match value {
diff --git a/src/backend/vm_instructions/extern_call.rs b/src/backend/vm_instructions/extern_call.rs
index 8f77928f..e371407a 100644
--- a/src/backend/vm_instructions/extern_call.rs
+++ b/src/backend/vm_instructions/extern_call.rs
@@ -74,6 +74,60 @@ impl VM {
_ => { /* fallthrough to host */ }
}
}
+ // Name-route minimal registry without slot
+ // env.modules.set(key:any->string, value:any) ; env.modules.get(key) -> any
+ match (iface_name, method_name) {
+ ("env.modules", "set") => {
+ // Expect two args
+ let vm_args: Vec = args.iter().filter_map(|a| self.get_value(*a).ok()).collect();
+ if vm_args.len() >= 2 {
+ let key = vm_args[0].to_string();
+ let val_box = vm_args[1].to_nyash_box();
+ crate::runtime::modules_registry::set(key, val_box);
+ }
+ if let Some(d) = dst { self.set_value(d, VMValue::Void); }
+ return Ok(ControlFlow::Continue);
+ }
+ ("env.modules", "get") => {
+ let vm_args: Vec = args.iter().filter_map(|a| self.get_value(*a).ok()).collect();
+ if let Some(k) = vm_args.get(0) {
+ let key = k.to_string();
+ if let Some(v) = crate::runtime::modules_registry::get(&key) {
+ if let Some(d) = dst { self.set_value(d, VMValue::from_nyash_box(v)); }
+ else { /* no dst */ }
+ } else if let Some(d) = dst { self.set_value(d, VMValue::Void); }
+ } else if let Some(d) = dst { self.set_value(d, VMValue::Void); }
+ return Ok(ControlFlow::Continue);
+ }
+ _ => {}
+ }
+ }
+
+ // Name-route minimal registry even when slot routing is disabled
+ if iface_name == "env.modules" {
+ // Decode args as VMValue for convenience
+ let vm_args: Vec = args.iter().filter_map(|a| self.get_value(*a).ok()).collect();
+ match method_name {
+ "set" => {
+ if vm_args.len() >= 2 {
+ let key = vm_args[0].to_string();
+ let val_box = vm_args[1].to_nyash_box();
+ crate::runtime::modules_registry::set(key, val_box);
+ }
+ if let Some(d) = dst { self.set_value(d, VMValue::Void); }
+ return Ok(ControlFlow::Continue);
+ }
+ "get" => {
+ if let Some(k) = vm_args.get(0) {
+ let key = k.to_string();
+ if let Some(v) = crate::runtime::modules_registry::get(&key) {
+ if let Some(d) = dst { self.set_value(d, VMValue::from_nyash_box(v)); }
+ } else if let Some(d) = dst { self.set_value(d, VMValue::Void); }
+ } else if let Some(d) = dst { self.set_value(d, VMValue::Void); }
+ return Ok(ControlFlow::Continue);
+ }
+ _ => {}
+ }
}
// Evaluate arguments as NyashBox for loader
diff --git a/src/cli.rs b/src/cli.rs
index 8d3fe09e..9fb9e9a8 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -57,6 +57,10 @@ pub struct CliConfig {
// Phase-15: JSON IR v0 bridge
pub ny_parser_pipe: bool,
pub json_file: Option,
+ // Using/module resolution helpers (MVP)
+ pub using: Option,
+ pub using_path: Option,
+ pub modules: Option,
// Build system (MVP)
pub build_path: Option,
pub build_app: Option,
@@ -103,6 +107,24 @@ impl CliConfig {
.value_name("FILE")
.help("Read Ny JSON IR v0 from a file and execute via MIR Interpreter")
)
+ .arg(
+ Arg::new("using")
+ .long("using")
+ .value_name("LIST")
+ .help("Declare namespaces or aliases (comma-separated). Ex: 'acme.util, acme.math as M' or '\"apps/x.nyash\" as X'")
+ )
+ .arg(
+ Arg::new("using-path")
+ .long("using-path")
+ .value_name("PATHS")
+ .help("Search paths for using (':' separated). Ex: 'apps:lib:.'")
+ )
+ .arg(
+ Arg::new("module")
+ .long("module")
+ .value_name("MAP")
+ .help("Namespace to path mapping (comma-separated). Ex: 'acme.util=apps/acme/util.nyash'")
+ )
.arg(
Arg::new("debug-fuel")
.long("debug-fuel")
@@ -405,6 +427,10 @@ impl CliConfig {
parser_ny: matches.get_one::("parser").map(|s| s == "ny").unwrap_or(false),
ny_parser_pipe: matches.get_flag("ny-parser-pipe"),
json_file: matches.get_one::("json-file").cloned(),
+ using: matches.get_one::("using").cloned(),
+ using_path: matches.get_one::("using-path").cloned(),
+ modules: matches.get_one::("module").cloned(),
+ // Build system (MVP)
build_path: matches.get_one::("build").cloned(),
build_app: matches.get_one::("build-app").cloned(),
build_out: matches.get_one::("build-out").cloned(),
@@ -479,6 +505,15 @@ mod tests {
parser_ny: false,
ny_parser_pipe: false,
json_file: None,
+ using: None,
+ using_path: None,
+ modules: None,
+ build_path: None,
+ build_app: None,
+ build_out: None,
+ build_aot: None,
+ build_profile: None,
+ build_target: None,
};
assert_eq!(config.backend, "interpreter");
diff --git a/src/jit/extern/birth.rs b/src/jit/extern/birth.rs
index c9a5a6a8..2f530e6d 100644
--- a/src/jit/extern/birth.rs
+++ b/src/jit/extern/birth.rs
@@ -1,4 +1,5 @@
//! Generic birth hostcall symbol names
pub const SYM_BOX_BIRTH_H: &str = "nyash.box.birth_h";
-
+/// Birth by type name encoded in two u64 words (lo,hi,len)
+pub const SYM_INSTANCE_BIRTH_NAME_U64X2: &str = "nyash.instance.birth_name_u64x2";
diff --git a/src/jit/extern/collections.rs b/src/jit/extern/collections.rs
index 0d384f50..f88726f9 100644
--- a/src/jit/extern/collections.rs
+++ b/src/jit/extern/collections.rs
@@ -30,6 +30,7 @@ pub const SYM_ANY_IS_EMPTY_H: &str = "nyash.any.is_empty_h";
pub const SYM_STRING_CHARCODE_AT_H: &str = "nyash.string.charCodeAt_h";
pub const SYM_STRING_LEN_H: &str = "nyash.string.len_h";
pub const SYM_STRING_BIRTH_H: &str = "nyash.string.birth_h";
+pub const SYM_STRING_FROM_U64X2: &str = "nyash.string.from_u64x2";
pub const SYM_INTEGER_BIRTH_H: &str = "nyash.integer.birth_h";
pub const SYM_CONSOLE_BIRTH_H: &str = "nyash.console.birth_h";
// String-like operations (handle, handle)
diff --git a/src/jit/lower/builder/cranelift.rs b/src/jit/lower/builder/cranelift.rs
index c5e0ca93..ceddf49c 100644
--- a/src/jit/lower/builder/cranelift.rs
+++ b/src/jit/lower/builder/cranelift.rs
@@ -683,7 +683,7 @@ impl IRBuilder for CraneliftBuilder {
sig.params.push(AbiParam::new(types::I64)); // hi
sig.params.push(AbiParam::new(types::I64)); // len
sig.returns.push(AbiParam::new(types::I64));
- let func_id = self.module.declare_function("nyash.string.from_u64x2", cranelift_module::Linkage::Import, &sig).expect("declare string.from_u64x2");
+ let func_id = self.module.declare_function(crate::jit::r#extern::collections::SYM_STRING_FROM_U64X2, cranelift_module::Linkage::Import, &sig).expect("declare string.from_u64x2");
let v = Self::with_fb(|fb| {
let lo_v = fb.ins().iconst(types::I64, lo as i64);
let hi_v = fb.ins().iconst(types::I64, hi as i64);
@@ -891,7 +891,7 @@ impl CraneliftBuilder {
builder.symbol(c::SYM_STRING_LT_HH, nyash_string_lt_hh as *const u8);
builder.symbol(b::SYM_BOX_BIRTH_H, nyash_box_birth_h as *const u8);
builder.symbol("nyash.box.birth_i64", nyash_box_birth_i64 as *const u8);
- builder.symbol("nyash.instance.birth_name_u64x2", super::super::extern_thunks::nyash_instance_birth_name_u64x2 as *const u8);
+ builder.symbol(crate::jit::r#extern::birth::SYM_INSTANCE_BIRTH_NAME_U64X2, super::super::extern_thunks::nyash_instance_birth_name_u64x2 as *const u8);
builder.symbol(h::SYM_HANDLE_OF, nyash_handle_of as *const u8);
builder.symbol(r::SYM_RT_CHECKPOINT, nyash_rt_checkpoint as *const u8);
builder.symbol(r::SYM_GC_BARRIER_WRITE, nyash_gc_barrier_write as *const u8);
@@ -901,7 +901,7 @@ impl CraneliftBuilder {
builder.symbol("nyash_plugin_invoke3_f64", nyash_plugin_invoke3_f64 as *const u8);
builder.symbol("nyash_plugin_invoke_name_getattr_i64", nyash_plugin_invoke_name_getattr_i64 as *const u8);
builder.symbol("nyash_plugin_invoke_name_call_i64", nyash_plugin_invoke_name_call_i64 as *const u8);
- builder.symbol("nyash.string.from_u64x2", super::super::extern_thunks::nyash_string_from_u64x2 as *const u8);
+ builder.symbol(crate::jit::r#extern::collections::SYM_STRING_FROM_U64X2, super::super::extern_thunks::nyash_string_from_u64x2 as *const u8);
// Host-bridge (by-slot) imports (opt-in)
if std::env::var("NYASH_JIT_HOST_BRIDGE").ok().as_deref() == Some("1") {
diff --git a/src/jit/lower/builder/object.rs b/src/jit/lower/builder/object.rs
index 51f86573..fbab8793 100644
--- a/src/jit/lower/builder/object.rs
+++ b/src/jit/lower/builder/object.rs
@@ -404,7 +404,7 @@ impl IRBuilder for ObjectBuilder {
sig.params.push(AbiParam::new(types::I64));
sig.params.push(AbiParam::new(types::I64));
sig.returns.push(AbiParam::new(types::I64));
- let func_id = self.module.declare_function("nyash.string.from_u64x2", cranelift_module::Linkage::Import, &sig).expect("declare string.from_u64x2");
+ let func_id = self.module.declare_function(crate::jit::r#extern::collections::SYM_STRING_FROM_U64X2, cranelift_module::Linkage::Import, &sig).expect("declare string.from_u64x2");
let lo_v = fb.ins().iconst(types::I64, lo as i64);
let hi_v = fb.ins().iconst(types::I64, hi as i64);
let len_v = fb.ins().iconst(types::I64, bytes.len() as i64);
diff --git a/src/jit/lower/builder/tls.rs b/src/jit/lower/builder/tls.rs
index 79e3e8f6..e0184602 100644
--- a/src/jit/lower/builder/tls.rs
+++ b/src/jit/lower/builder/tls.rs
@@ -55,6 +55,18 @@ pub(crate) fn tls_call_import_ret(
let mut opt = cell.borrow_mut();
let tls = opt.as_mut().expect("FunctionBuilder TLS not initialized");
tls.with(|fb| {
+ // Guard: avoid emitting a verifier-invalid call when args are unexpectedly empty.
+ // Some early shims (e.g., instrumentation) may have declared a 1-arity import;
+ // if lowering produced no arguments, synthesize a zero literal when a return is expected,
+ // and skip the call entirely to keep the IR valid.
+ if args.is_empty() {
+ if has_ret {
+ use cranelift_codegen::ir::types;
+ return Some(fb.ins().iconst(types::I64, 0));
+ } else {
+ return None;
+ }
+ }
let fref = module.declare_func_in_func(func_id, fb.func);
let call_inst = fb.ins().call(fref, args);
if has_ret { fb.inst_results(call_inst).get(0).copied() } else { None }
diff --git a/src/jit/lower/core.rs b/src/jit/lower/core.rs
index e522c7ac..ddefdb71 100644
--- a/src/jit/lower/core.rs
+++ b/src/jit/lower/core.rs
@@ -238,7 +238,7 @@ impl LowerCore {
b.emit_const_i64(lo as i64);
b.emit_const_i64(hi as i64);
b.emit_const_i64(bytes.len() as i64);
- b.emit_host_call("nyash.instance.birth_name_u64x2", 3, true);
+ b.emit_host_call(crate::jit::r#extern::birth::SYM_INSTANCE_BIRTH_NAME_U64X2, 3, true);
// Store handle to local slot
let slot = *self.local_index.entry(*dst).or_insert_with(|| { let id = self.next_local; self.next_local += 1; id });
b.store_local_i64(slot);
@@ -367,7 +367,7 @@ impl LowerCore {
b.emit_const_i64(name_bytes.len() as i64);
// Call import (lo, hi, len) -> handle
// Use typed hostcall (I64,I64,I64)->I64
- b.emit_host_call_typed("nyash.instance.birth_name_u64x2", &[crate::jit::lower::builder::ParamKind::I64, crate::jit::lower::builder::ParamKind::I64, crate::jit::lower::builder::ParamKind::I64], true, false);
+ b.emit_host_call_typed(crate::jit::r#extern::birth::SYM_INSTANCE_BIRTH_NAME_U64X2, &[crate::jit::lower::builder::ParamKind::I64, crate::jit::lower::builder::ParamKind::I64, crate::jit::lower::builder::ParamKind::I64], true, false);
self.handle_values.insert(*dst);
let slot = *self.local_index.entry(*dst).or_insert_with(|| { let id = self.next_local; self.next_local += 1; id });
b.store_local_i64(slot);
diff --git a/src/jit/lower/core/ops_ext.rs b/src/jit/lower/core/ops_ext.rs
index c2f85daa..25604a03 100644
--- a/src/jit/lower/core/ops_ext.rs
+++ b/src/jit/lower/core/ops_ext.rs
@@ -491,7 +491,7 @@ impl LowerCore {
}
// Last resort: handle.of
self.push_value_if_known_or_param(b, array);
- b.emit_host_call("nyash.handle.of", 1, true);
+ b.emit_host_call(crate::jit::r#extern::handles::SYM_HANDLE_OF, 1, true);
let slot = { let id = self.next_local; self.next_local += 1; id };
b.store_local_i64(slot);
self.emit_len_with_fallback_local_handle(b, slot);
@@ -513,7 +513,7 @@ impl LowerCore {
_ => {
// Unknown receiver type: generic Any.length_h on a handle
self.push_value_if_known_or_param(b, array);
- b.emit_host_call("nyash.handle.of", 1, true);
+ b.emit_host_call(crate::jit::r#extern::handles::SYM_HANDLE_OF, 1, true);
b.emit_host_call(crate::jit::r#extern::collections::SYM_ANY_LEN_H, 1, true);
if let Some(d) = dst { let slot = *self.local_index.entry(d).or_insert_with(|| { let id=self.next_local; self.next_local+=1; id }); b.store_local_i64(slot); }
return Ok(true);
diff --git a/src/lib.rs b/src/lib.rs
index 72e7cf27..01afd254 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -38,6 +38,10 @@ pub mod transport;
// 🚀 MIR (Mid-level Intermediate Representation) Infrastructure (NEW!)
pub mod mir;
+#[cfg(feature = "aot-plan-import")]
+pub mod mir_aot_plan_import {
+ pub use crate::mir::aot_plan_import::*;
+}
// 🚀 Backend Infrastructure (NEW!)
pub mod backend;
diff --git a/src/mir/aot_plan_import.rs b/src/mir/aot_plan_import.rs
new file mode 100644
index 00000000..cfea1e9a
--- /dev/null
+++ b/src/mir/aot_plan_import.rs
@@ -0,0 +1,87 @@
+//! AOT-Plan v1 → MIR13 importer (Phase 15.1)
+//! Feature-gated behind `aot-plan-import`.
+
+use crate::mir::{MirModule, MirFunction, FunctionSignature, BasicBlockId, MirInstruction, EffectMask, MirType, ConstValue};
+
+#[derive(Debug, serde::Deserialize)]
+struct PlanV1 {
+ version: String, // "1"
+ name: Option,
+ functions: Vec,
+}
+
+#[derive(Debug, serde::Deserialize)]
+struct PlanFunction {
+ name: String,
+ #[serde(default)]
+ params: Vec,
+ return_type: Option, // "integer" | "string" | ...
+ #[serde(default)]
+ body: Option,
+}
+
+#[derive(Debug, serde::Deserialize)]
+struct PlanParam { name: String, r#type: Option }
+
+#[derive(Debug, serde::Deserialize)]
+#[serde(tag = "kind")]
+enum PlanBody {
+ #[serde(rename = "const_return")]
+ ConstReturn { value: serde_json::Value },
+ #[serde(rename = "empty")]
+ Empty,
+}
+
+fn map_type(s: Option<&str>) -> MirType {
+ match s.unwrap_or("") {
+ "integer" => MirType::Integer,
+ "float" => MirType::Float,
+ "bool" => MirType::Bool,
+ "string" => MirType::String,
+ "void" => MirType::Void,
+ _ => MirType::Unknown,
+ }
+}
+
+fn const_from_json(v: &serde_json::Value) -> Option {
+ if let Some(i) = v.as_i64() { return Some(ConstValue::Integer(i)); }
+ if let Some(b) = v.as_bool() { return Some(ConstValue::Bool(b)); }
+ if let Some(f) = v.as_f64() { return Some(ConstValue::Float(f)); }
+ if let Some(s) = v.as_str() { return Some(ConstValue::String(s.to_string())); }
+ None
+}
+
+/// Import a v1 plan JSON string into a MIR13 module with skeleton bodies.
+pub fn import_from_str(plan_json: &str) -> Result {
+ let plan: PlanV1 = serde_json::from_str(plan_json).map_err(|e| format!("invalid plan json: {}", e))?;
+ if plan.version != "1" { return Err("unsupported plan version".into()); }
+ let mut module = MirModule::new(plan.name.unwrap_or_else(|| "aot_plan".into()));
+
+ for f in plan.functions.iter() {
+ // Signatures: keep types minimal; params exist but VM uses stackless calling for main
+ let ret_ty = map_type(f.return_type.as_deref());
+ let sig = FunctionSignature { name: f.name.clone(), params: vec![], return_type: ret_ty.clone(), effects: EffectMask::PURE };
+ let mut mf = MirFunction::new(sig, BasicBlockId::new(0));
+ let bb = mf.entry_block;
+ // Body lowering (skeleton)
+ match &f.body {
+ Some(PlanBody::ConstReturn { value }) => {
+ let dst = mf.next_value_id();
+ let cst = const_from_json(value).ok_or_else(|| format!("unsupported const value in {}", f.name))?;
+ if let Some(b) = mf.get_block_mut(bb) { b.add_instruction(MirInstruction::Const { dst, value: cst }); b.set_terminator(MirInstruction::Return { value: Some(dst) }); }
+ // If return_type is unspecified, set Unknown to allow VM dynamic display
+ // Otherwise retain declared type
+ if matches!(ret_ty, MirType::Unknown) { /* keep Unknown */ }
+ }
+ Some(PlanBody::Empty) | None => {
+ // Return void or default 0 for integer; choose Unknown for display stability
+ let dst = mf.next_value_id();
+ if let Some(b) = mf.get_block_mut(bb) { b.add_instruction(MirInstruction::Const { dst, value: ConstValue::Integer(0) }); b.set_terminator(MirInstruction::Return { value: Some(dst) }); }
+ mf.signature.return_type = MirType::Unknown;
+ }
+ }
+ module.add_function(mf);
+ }
+ Ok(module)
+}
+
diff --git a/src/mir/mod.rs b/src/mir/mod.rs
index ab230d99..b7454b1d 100644
--- a/src/mir/mod.rs
+++ b/src/mir/mod.rs
@@ -20,6 +20,8 @@ pub mod value_id;
pub mod effect;
pub mod optimizer;
pub mod slot_registry; // Phase 9.79b.1: method slot resolution (IDs)
+#[cfg(feature = "aot-plan-import")]
+pub mod aot_plan_import;
pub mod passes; // Optimization subpasses (e.g., type_hints)
// Re-export main types for easy access
diff --git a/src/runner/json_v0_bridge.rs b/src/runner/json_v0_bridge.rs
index b8547887..8093b23b 100644
--- a/src/runner/json_v0_bridge.rs
+++ b/src/runner/json_v0_bridge.rs
@@ -15,13 +15,19 @@ struct ProgramV0 {
#[serde(tag = "type")]
enum StmtV0 {
Return { expr: ExprV0 },
+ Extern { iface: String, method: String, args: Vec },
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(tag = "type")]
enum ExprV0 {
Int { value: serde_json::Value },
+ Str { value: String },
+ Bool { value: bool },
Binary { op: String, lhs: Box, rhs: Box },
+ Extern { iface: String, method: String, args: Vec },
+ Compare { op: String, lhs: Box, rhs: Box },
+ Logical { op: String, lhs: Box, rhs: Box }, // short-circuit: &&, || (or: "and"/"or")
}
pub fn parse_json_v0_to_module(json: &str) -> Result {
@@ -29,28 +35,60 @@ pub fn parse_json_v0_to_module(json: &str) -> Result {
if prog.version != 0 || prog.kind != "Program" {
return Err("unsupported IR: expected {version:0, kind:\"Program\"}".into());
}
- let stmt = prog.body.get(0).ok_or("empty body")?;
-
// Create module and main function
let mut module = MirModule::new("ny_json_v0".into());
let sig = FunctionSignature { name: "main".into(), params: vec![], return_type: MirType::Integer, effects: EffectMask::PURE };
let entry = BasicBlockId::new(0);
let mut f = MirFunction::new(sig, entry);
- // Build expression
- let ret_val = match stmt {
- StmtV0::Return { expr } => lower_expr(&mut f, expr)?,
- };
- // Return
- if let Some(bb) = f.get_block_mut(entry) {
- bb.set_terminator(MirInstruction::Return { value: Some(ret_val) });
+
+ if prog.body.is_empty() { return Err("empty body".into()); }
+
+ // Lower all statements; capture last expression for return when the last is Return
+ let mut last_ret: Option<(crate::mir::ValueId, BasicBlockId)> = None;
+ for (i, stmt) in prog.body.iter().enumerate() {
+ match stmt {
+ StmtV0::Extern { iface, method, args } => {
+ // void extern call
+ let entry_bb = f.entry_block;
+ let (arg_ids, _cur) = lower_args(&mut f, entry_bb, args)?;
+ if let Some(bb) = f.get_block_mut(entry) {
+ bb.add_instruction(MirInstruction::ExternCall { dst: None, iface_name: iface.clone(), method_name: method.clone(), args: arg_ids, effects: EffectMask::IO });
+ }
+ if i == prog.body.len()-1 { last_ret = None; }
+ }
+ StmtV0::Return { expr } => {
+ let entry_bb = f.entry_block;
+ last_ret = Some(lower_expr(&mut f, entry_bb, expr)?);
+ }
+ }
}
- // Infer return type (integer only for v0)
- f.signature.return_type = MirType::Integer;
+ // Return last value (or 0)
+ if let Some((rv, cur)) = last_ret {
+ if let Some(bb) = f.get_block_mut(cur) {
+ bb.set_terminator(MirInstruction::Return { value: Some(rv) });
+ } else {
+ return Err("invalid block when setting return".into());
+ }
+ } else {
+ let dst_id = f.next_value_id();
+ if let Some(bb) = f.get_block_mut(entry) {
+ bb.add_instruction(MirInstruction::Const { dst: dst_id, value: ConstValue::Integer(0) });
+ bb.set_terminator(MirInstruction::Return { value: Some(dst_id) });
+ }
+ }
+ // Keep return type unknown to allow dynamic display (VM/Interpreter)
+ f.signature.return_type = MirType::Unknown;
module.add_function(f);
Ok(module)
}
-fn lower_expr(f: &mut MirFunction, e: &ExprV0) -> Result {
+fn next_block_id(f: &MirFunction) -> BasicBlockId {
+ let mut mx = 0u32;
+ for k in f.blocks.keys() { if k.0 >= mx { mx = k.0 + 1; } }
+ BasicBlockId::new(mx)
+}
+
+fn lower_expr(f: &mut MirFunction, cur_bb: BasicBlockId, e: &ExprV0) -> Result<(crate::mir::ValueId, BasicBlockId), String> {
match e {
ExprV0::Int { value } => {
// Accept number or stringified digits
@@ -60,24 +98,123 @@ fn lower_expr(f: &mut MirFunction, e: &ExprV0) -> Result {
+ let dst = f.next_value_id();
+ if let Some(bb) = f.get_block_mut(cur_bb) {
+ bb.add_instruction(MirInstruction::Const { dst, value: ConstValue::String(value.clone()) });
+ }
+ Ok((dst, cur_bb))
+ }
+ ExprV0::Bool { value } => {
+ let dst = f.next_value_id();
+ if let Some(bb) = f.get_block_mut(cur_bb) {
+ bb.add_instruction(MirInstruction::Const { dst, value: ConstValue::Bool(*value) });
+ }
+ Ok((dst, cur_bb))
}
ExprV0::Binary { op, lhs, rhs } => {
- let l = lower_expr(f, lhs)?;
- let r = lower_expr(f, rhs)?;
+ let (l, cur_after_l) = lower_expr(f, cur_bb, lhs)?;
+ let (r, cur_after_r) = lower_expr(f, cur_after_l, rhs)?;
let bop = match op.as_str() { "+" => BinaryOp::Add, "-" => BinaryOp::Sub, "*" => BinaryOp::Mul, "/" => BinaryOp::Div, _ => return Err("unsupported op".into()) };
let dst = f.next_value_id();
- if let Some(bb) = f.get_block_mut(f.entry_block) {
+ if let Some(bb) = f.get_block_mut(cur_after_r) {
bb.add_instruction(MirInstruction::BinOp { dst, op: bop, lhs: l, rhs: r });
}
- Ok(dst)
+ Ok((dst, cur_after_r))
+ }
+ ExprV0::Extern { iface, method, args } => {
+ let (arg_ids, cur2) = lower_args(f, cur_bb, args)?;
+ let dst = f.next_value_id();
+ if let Some(bb) = f.get_block_mut(cur2) {
+ bb.add_instruction(MirInstruction::ExternCall { dst: Some(dst), iface_name: iface.clone(), method_name: method.clone(), args: arg_ids, effects: EffectMask::IO });
+ }
+ Ok((dst, cur2))
+ }
+ ExprV0::Compare { op, lhs, rhs } => {
+ let (l, cur_after_l) = lower_expr(f, cur_bb, lhs)?;
+ let (r, cur_after_r) = lower_expr(f, cur_after_l, rhs)?;
+ let cop = match op.as_str() {
+ "==" => crate::mir::CompareOp::Eq,
+ "!=" => crate::mir::CompareOp::Ne,
+ "<" => crate::mir::CompareOp::Lt,
+ "<=" => crate::mir::CompareOp::Le,
+ ">" => crate::mir::CompareOp::Gt,
+ ">=" => crate::mir::CompareOp::Ge,
+ _ => return Err("unsupported compare op".into()),
+ };
+ let dst = f.next_value_id();
+ if let Some(bb) = f.get_block_mut(cur_after_r) {
+ bb.add_instruction(MirInstruction::Compare { dst, op: cop, lhs: l, rhs: r });
+ }
+ Ok((dst, cur_after_r))
+ }
+ ExprV0::Logical { op, lhs, rhs } => {
+ // Short-circuit boolean logic with branches + phi
+ let (l, cur_after_l) = lower_expr(f, cur_bb, lhs)?;
+ let rhs_bb = next_block_id(f);
+ let fall_bb = BasicBlockId::new(rhs_bb.0 + 1);
+ let merge_bb = BasicBlockId::new(rhs_bb.0 + 2);
+ f.add_block(crate::mir::BasicBlock::new(rhs_bb));
+ f.add_block(crate::mir::BasicBlock::new(fall_bb));
+ f.add_block(crate::mir::BasicBlock::new(merge_bb));
+ // Branch depending on op
+ let is_and = matches!(op.as_str(), "&&" | "and");
+ if let Some(bb) = f.get_block_mut(cur_after_l) {
+ if is_and {
+ bb.set_terminator(MirInstruction::Branch { condition: l, then_bb: rhs_bb, else_bb: fall_bb });
+ } else {
+ // OR: if lhs true, go to fall_bb (true path), else evaluate rhs
+ bb.set_terminator(MirInstruction::Branch { condition: l, then_bb: fall_bb, else_bb: rhs_bb });
+ }
+ }
+ // Telemetry: note short-circuit lowering
+ crate::jit::events::emit_lower(
+ serde_json::json!({
+ "id": "shortcircuit",
+ "op": if is_and { "and" } else { "or" },
+ "rhs_bb": rhs_bb.0,
+ "fall_bb": fall_bb.0,
+ "merge_bb": merge_bb.0
+ }),
+ "shortcircuit",
+ ""
+ );
+ // false/true constant in fall_bb depending on op
+ let cdst = f.next_value_id();
+ if let Some(bb) = f.get_block_mut(fall_bb) {
+ let cval = if is_and { ConstValue::Bool(false) } else { ConstValue::Bool(true) };
+ bb.add_instruction(MirInstruction::Const { dst: cdst, value: cval });
+ bb.set_terminator(MirInstruction::Jump { target: merge_bb });
+ }
+ // evaluate rhs in rhs_bb
+ let (rval, _rhs_end) = lower_expr(f, rhs_bb, rhs)?;
+ if let Some(bb) = f.get_block_mut(rhs_bb) {
+ if !bb.is_terminated() { bb.set_terminator(MirInstruction::Jump { target: merge_bb }); }
+ }
+ // merge with phi
+ let out = f.next_value_id();
+ if let Some(bb) = f.get_block_mut(merge_bb) {
+ bb.insert_instruction_after_phis(MirInstruction::Phi { dst: out, inputs: vec![(rhs_bb, rval), (fall_bb, cdst)] });
+ }
+ Ok((out, merge_bb))
}
}
}
+fn lower_args(f: &mut MirFunction, cur_bb: BasicBlockId, args: &[ExprV0]) -> Result<(Vec, BasicBlockId), String> {
+ let mut out = Vec::with_capacity(args.len());
+ let mut cur = cur_bb;
+ for a in args {
+ let (v, c) = lower_expr(f, cur, a)?; out.push(v); cur = c;
+ }
+ Ok((out, cur))
+}
+
pub fn maybe_dump_mir(module: &MirModule) {
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
let mut p = MirPrinter::new();
diff --git a/src/runner/mod.rs b/src/runner/mod.rs
index 37f3ed76..fbe4a18a 100644
--- a/src/runner/mod.rs
+++ b/src/runner/mod.rs
@@ -83,6 +83,57 @@ impl NyashRunner {
}
return;
}
+ // CLI using/module overrides (MVP): apply early so JSON pipeline can observe them
+ if self.config.using.is_some() || self.config.using_path.is_some() || self.config.modules.is_some()
+ || std::env::var("NYASH_USING_PATH").is_ok() || std::env::var("NYASH_MODULES").is_ok() {
+ let mut using_paths: Vec = Vec::new();
+ if let Some(p) = self.config.using_path.clone() { for s in p.split(':') { let s=s.trim(); if !s.is_empty() { using_paths.push(s.to_string()); } } }
+ if let Ok(p) = std::env::var("NYASH_USING_PATH") { for s in p.split(':') { let s=s.trim(); if !s.is_empty() { using_paths.push(s.to_string()); } } }
+ if using_paths.is_empty() { using_paths.extend(["apps","lib","."].into_iter().map(|s| s.to_string())); }
+
+ // modules mapping
+ let mut modules: Vec<(String,String)> = Vec::new();
+ if let Some(m) = self.config.modules.clone() { for ent in m.split(',') { if let Some((k,v)) = ent.split_once('=') { let k=k.trim(); let v=v.trim(); if !k.is_empty() && !v.is_empty() { modules.push((k.to_string(), v.to_string())); } } } }
+ if let Ok(ms) = std::env::var("NYASH_MODULES") { for ent in ms.split(',') { if let Some((k,v)) = ent.split_once('=') { let k=k.trim(); let v=v.trim(); if !k.is_empty() && !v.is_empty() { modules.push((k.to_string(), v.to_string())); } } } }
+ for (ns, path) in &modules { let sb = crate::box_trait::StringBox::new(path.clone()); crate::runtime::modules_registry::set(ns.clone(), Box::new(sb)); }
+
+ // using specs
+ let mut pending_using: Vec<(String, Option, bool)> = Vec::new(); // (target, alias, is_path)
+ if let Some(u) = self.config.using.clone() {
+ for ent in u.split(',') {
+ let s = ent.trim().trim_end_matches(';').trim(); if s.is_empty() { continue; }
+ let (tgt, alias) = if let Some(pos) = s.find(" as ") { (s[..pos].trim().to_string(), Some(s[pos+4..].trim().to_string())) } else { (s.to_string(), None) };
+ let is_path = tgt.starts_with('"') || tgt.starts_with("./") || tgt.starts_with('/') || tgt.ends_with(".nyash");
+ pending_using.push((tgt.trim_matches('"').to_string(), alias, is_path));
+ }
+ }
+ // Resolve using
+ for (tgt, alias, is_path) in pending_using.into_iter() {
+ if is_path {
+ let missing = !std::path::Path::new(&tgt).exists();
+ if missing {
+ if std::env::var("NYASH_USING_STRICT").ok().as_deref() == Some("1") {
+ eprintln!("❌ using: path not found: {}", tgt);
+ std::process::exit(1);
+ } else if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
+ eprintln!("[using] path not found (continuing): {}", tgt);
+ }
+ }
+ }
+ let value = if is_path { tgt.clone() } else if let Some((_n,p)) = modules.iter().find(|(n,_)| n==&tgt) { p.clone() } else {
+ let rel = tgt.replace('.', "/") + ".nyash";
+ let mut found: Option = None;
+ for base in &using_paths { let cand = std::path::Path::new(base).join(&rel); if cand.exists() { found = Some(cand.to_string_lossy().to_string()); break; } }
+ if found.is_none() && std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
+ eprintln!("[using] unresolved '{}'; tried {}", tgt, using_paths.join(":"));
+ }
+ found.unwrap_or(tgt.clone())
+ };
+ let sb = crate::box_trait::StringBox::new(value.clone());
+ crate::runtime::modules_registry::set(tgt.clone(), Box::new(sb));
+ if let Some(a) = alias { let sb2 = crate::box_trait::StringBox::new(value); crate::runtime::modules_registry::set(a, Box::new(sb2)); }
+ }
+ }
// Phase-15: JSON IR v0 bridge (stdin/file)
if self.config.ny_parser_pipe || self.config.json_file.is_some() {
let json = if let Some(path) = &self.config.json_file {
@@ -102,8 +153,12 @@ impl NyashRunner {
Ok(module) => {
// Optional dump via env verbose
json_v0_bridge::maybe_dump_mir(&module);
- // Execute via MIR interpreter
- self.execute_mir_module(&module);
+ // Execute via selected backend (vm or interpreter)
+ if self.config.backend == "vm" {
+ self.execute_vm_module(&module);
+ } else {
+ self.execute_mir_module(&module);
+ }
return;
}
Err(e) => {
@@ -130,6 +185,9 @@ impl NyashRunner {
if let Some(ref filename) = self.config.file {
if let Ok(code) = fs::read_to_string(filename) {
// Scan first 128 lines for directives
+ let mut pending_using: Vec<(String, Option)> = Vec::new();
+ let mut pending_modules: Vec<(String, String)> = Vec::new();
+ let mut using_paths: Vec = Vec::new();
for (i, line) in code.lines().take(128).enumerate() {
let l = line.trim();
if !(l.starts_with("//") || l.starts_with("#!") || l.is_empty()) {
@@ -143,6 +201,24 @@ impl NyashRunner {
let key = k.trim(); let val = v.trim();
if !key.is_empty() { std::env::set_var(key, val); }
}
+ } else if let Some(dir) = rest.strip_prefix("@using ") {
+ // @using ns[ as Alias]
+ let s = dir.trim().trim_end_matches(';').trim();
+ let (ns, alias) = if let Some(pos) = s.find(" as ") {
+ (s[..pos].trim().to_string(), Some(s[pos+4..].trim().to_string()))
+ } else { (s.to_string(), None) };
+ pending_using.push((ns, alias));
+ } else if let Some(dir) = rest.strip_prefix("@module ") {
+ // @module ns=path
+ if let Some((ns, path)) = dir.split_once('=') {
+ let ns = ns.trim().to_string();
+ let path = path.trim().trim_matches('"').to_string();
+ pending_modules.push((ns, path));
+ }
+ } else if let Some(dir) = rest.strip_prefix("@using-path ") {
+ // @using-path apps:lib:. (':' separated)
+ let s = dir.trim();
+ for p in s.split(':') { let p = p.trim(); if !p.is_empty() { using_paths.push(p.to_string()); } }
} else if rest == "@jit-debug" {
std::env::set_var("NYASH_JIT_EXEC", "1");
std::env::set_var("NYASH_JIT_THRESHOLD", "1");
@@ -160,6 +236,50 @@ impl NyashRunner {
}
}
}
+
+ // Env overrides for using rules
+ if let Ok(paths) = std::env::var("NYASH_USING_PATH") {
+ for p in paths.split(':') { let p = p.trim(); if !p.is_empty() { using_paths.push(p.to_string()); } }
+ }
+ if let Ok(mods) = std::env::var("NYASH_MODULES") {
+ for ent in mods.split(',') {
+ if let Some((k,v)) = ent.split_once('=') {
+ let k = k.trim(); let v = v.trim();
+ if !k.is_empty() && !v.is_empty() { pending_modules.push((k.to_string(), v.to_string())); }
+ }
+ }
+ }
+
+ // Apply pending modules to registry as StringBox (path or ns token)
+ for (ns, path) in pending_modules.iter() {
+ let sb = nyash_rust::box_trait::StringBox::new(path.clone());
+ nyash_rust::runtime::modules_registry::set(ns.clone(), Box::new(sb));
+ }
+ // Resolve pending using via modules map or using_paths (best-effort)
+ for (ns, alias) in pending_using.iter() {
+ // direct mapping first
+ let value = if let Some((_n, p)) = pending_modules.iter().find(|(n, _)| n == ns) {
+ p.clone()
+ } else {
+ // try search paths: /.nyash
+ let rel = ns.replace('.', "/") + ".nyash";
+ let mut found = None;
+ if let Some(dir) = std::path::Path::new(filename).parent() {
+ let cand = dir.join(&rel);
+ if cand.exists() { found = Some(cand.to_string_lossy().to_string()); }
+ }
+ if found.is_none() {
+ for base in &using_paths { let cand = std::path::Path::new(base).join(&rel); if cand.exists() { found = Some(cand.to_string_lossy().to_string()); break; } }
+ }
+ found.unwrap_or_else(|| ns.clone())
+ };
+ let sb = nyash_rust::box_trait::StringBox::new(value.clone());
+ nyash_rust::runtime::modules_registry::set(ns.clone(), Box::new(sb));
+ if let Some(a) = alias {
+ let sb2 = nyash_rust::box_trait::StringBox::new(value);
+ nyash_rust::runtime::modules_registry::set(a.clone(), Box::new(sb2));
+ }
+ }
}
}
@@ -944,9 +1064,23 @@ impl NyashRunner {
}
#[cfg(not(windows))]
{
+ // Ensure NyRT static library exists
+ let nyrt_root = cwd.join("target").join("release").join("libnyrt.a");
+ let nyrt_crate = cwd.join("crates").join("nyrt").join("target").join("release").join("libnyrt.a");
+ if !nyrt_root.exists() && !nyrt_crate.exists() {
+ // Try to build crates/nyrt
+ let mut cmd = std::process::Command::new("cargo");
+ cmd.arg("build");
+ cmd.arg("--release");
+ cmd.current_dir(cwd.join("crates").join("nyrt"));
+ println!("[link] building NyRT (libnyrt.a) ...");
+ let st = cmd.status().map_err(|e| format!("spawn cargo (nyrt): {}", e))?;
+ if !st.success() { return Err("failed to build NyRT (libnyrt.a)".into()); }
+ }
let status = std::process::Command::new("cc")
.arg(&obj_path)
.args(["-L", &cwd.join("target").join("release").display().to_string()])
+ .args(["-L", &cwd.join("crates").join("nyrt").join("target").join("release").display().to_string()])
.args(["-Wl,--whole-archive", "-lnyrt", "-Wl,--no-whole-archive", "-lpthread", "-ldl", "-lm"])
.args(["-o", &out_path.display().to_string()])
.status().map_err(|e| format!("spawn cc: {}", e))?;
@@ -958,6 +1092,55 @@ impl NyashRunner {
}
impl NyashRunner {
+ /// Execute a prepared MIR module via the VM
+ fn execute_vm_module(&self, module: &crate::mir::MirModule) {
+ use crate::backend::VM;
+ use crate::mir::MirType;
+ use crate::box_trait::{NyashBox, IntegerBox, BoolBox, StringBox};
+ use crate::boxes::FloatBox;
+ let mut vm = VM::new();
+ match vm.execute_module(module) {
+ Ok(result) => {
+ if let Some(func) = module.functions.get("main") {
+ 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(VM): {}", ety);
+ println!("Result: {}", sval);
+ } else {
+ println!("Result: {:?}", result);
+ }
+ }
+ Err(e) => {
+ eprintln!("❌ VM execution error: {}", e);
+ std::process::exit(1);
+ }
+ }
+ }
/// Run a file through independent JIT engine (no VM execute loop)
fn run_file_jit_direct(&self, filename: &str) {
use std::fs;
diff --git a/src/runner/modes/common.rs b/src/runner/modes/common.rs
index 38801f9e..a5b7c693 100644
--- a/src/runner/modes/common.rs
+++ b/src/runner/modes/common.rs
@@ -5,6 +5,34 @@ use nyash_rust::{parser::NyashParser, interpreter::NyashInterpreter};
use nyash_rust::runner_plugin_init;
use std::{fs, process};
+// limited directory walk: add matching files ending with .nyash and given leaf name
+fn suggest_in_base(base: &str, leaf: &str, out: &mut Vec) {
+ use std::fs;
+ fn walk(dir: &std::path::Path, leaf: &str, out: &mut Vec, depth: usize) {
+ if depth == 0 || out.len() >= 5 { return; }
+ if let Ok(entries) = fs::read_dir(dir) {
+ for e in entries.flatten() {
+ let path = e.path();
+ if path.is_dir() {
+ walk(&path, leaf, out, depth - 1);
+ if out.len() >= 5 { return; }
+ } else if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
+ if ext == "nyash" {
+ if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
+ if stem == leaf {
+ out.push(path.to_string_lossy().to_string());
+ if out.len() >= 5 { return; }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ let p = std::path::Path::new(base);
+ walk(p, leaf, out, 4);
+}
+
impl NyashRunner {
/// File-mode dispatcher (thin wrapper around backend/mode selection)
pub(crate) fn run_file(&self, filename: &str) {
@@ -115,6 +143,7 @@ impl NyashRunner {
/// Execute Nyash file with interpreter (common helper)
pub(crate) fn execute_nyash_file(&self, filename: &str) {
+ let quiet_pipe = std::env::var("NYASH_JSON_ONLY").ok().as_deref() == Some("1");
// Ensure plugin host and provider mappings are initialized (idempotent)
if std::env::var("NYASH_DISABLE_PLUGINS").ok().as_deref() != Some("1") {
// Call via lib crate to avoid referring to the bin crate root
@@ -125,25 +154,112 @@ impl NyashRunner {
Ok(content) => content,
Err(e) => { eprintln!("❌ Error reading file {}: {}", filename, e); process::exit(1); }
};
+ if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") && !quiet_pipe {
+ println!("📝 File contents:\n{}", code);
+ println!("\n🚀 Parsing and executing...\n");
+ }
- println!("📝 File contents:\n{}", code);
- println!("\n🚀 Parsing and executing...\n");
+ // Optional Phase-15: strip `using` lines (gate) for minimal acceptance
+ let mut code_ref: &str = &code;
+ let enable_using = std::env::var("NYASH_ENABLE_USING").ok().as_deref() == Some("1");
+ let cleaned_code_owned;
+ if enable_using {
+ let mut out = String::with_capacity(code.len());
+ let mut used_names: Vec<(String, Option)> = Vec::new();
+ for line in code.lines() {
+ let t = line.trim_start();
+ if t.starts_with("using ") {
+ // Skip `using ns` or `using ns as alias` lines (MVP)
+ if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
+ eprintln!("[using] stripped line: {}", line);
+ }
+ // Parse namespace or path and optional alias
+ let rest0 = t.strip_prefix("using ").unwrap().trim();
+ // allow trailing semicolon
+ let rest0 = rest0.strip_suffix(';').unwrap_or(rest0).trim();
+ // Split alias
+ let (target, alias) = if let Some(pos) = rest0.find(" as ") {
+ (rest0[..pos].trim().to_string(), Some(rest0[pos+4..].trim().to_string()))
+ } else { (rest0.to_string(), None) };
+ // If quoted or looks like relative/absolute path, treat as path; else as namespace
+ let is_path = target.starts_with('"') || target.starts_with("./") || target.starts_with('/') || target.ends_with(".nyash");
+ if is_path {
+ let mut path = target.trim_matches('"').to_string();
+ // existence check and strict handling
+ let missing = !std::path::Path::new(&path).exists();
+ if missing {
+ if std::env::var("NYASH_USING_STRICT").ok().as_deref() == Some("1") {
+ eprintln!("❌ using: path not found: {}", path);
+ std::process::exit(1);
+ } else if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
+ eprintln!("[using] path not found (continuing): {}", path);
+ }
+ }
+ // choose alias or derive from filename stem
+ let name = alias.clone().unwrap_or_else(|| {
+ std::path::Path::new(&path)
+ .file_stem().and_then(|s| s.to_str()).unwrap_or("module").to_string()
+ });
+ // register alias only (path-backed)
+ used_names.push((name, Some(path)));
+ } else {
+ used_names.push((target, alias));
+ }
+ continue;
+ }
+ out.push_str(line);
+ out.push('\n');
+ }
+ cleaned_code_owned = out;
+ code_ref = &cleaned_code_owned;
+
+ // Register modules into minimal registry with best-effort path resolution
+ for (ns_or_alias, alias_or_path) in used_names {
+ // alias_or_path Some(path) means this entry was a direct path using
+ if let Some(path) = alias_or_path {
+ let sb = crate::box_trait::StringBox::new(path);
+ crate::runtime::modules_registry::set(ns_or_alias, Box::new(sb));
+ } else {
+ let rel = format!("apps/{}.nyash", ns_or_alias.replace('.', "/"));
+ let exists = std::path::Path::new(&rel).exists();
+ if !exists && std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
+ eprintln!("[using] unresolved namespace '{}'; tried '{}'. Hint: add @module {}={} or --module {}={}", ns_or_alias, rel, ns_or_alias, rel, ns_or_alias, rel);
+ // naive candidates by suffix within common bases
+ let leaf = ns_or_alias.split('.').last().unwrap_or(&ns_or_alias);
+ let mut cands: Vec = Vec::new();
+ suggest_in_base("apps", leaf, &mut cands);
+ if cands.len() < 5 { suggest_in_base("lib", leaf, &mut cands); }
+ if cands.len() < 5 { suggest_in_base(".", leaf, &mut cands); }
+ if !cands.is_empty() {
+ eprintln!("[using] candidates: {}", cands.join(", "));
+ }
+ }
+ let path_or_ns = if exists { rel } else { ns_or_alias.clone() };
+ let sb = crate::box_trait::StringBox::new(path_or_ns);
+ crate::runtime::modules_registry::set(ns_or_alias, Box::new(sb));
+ }
+ }
+ }
// Parse the code with debug fuel limit
eprintln!("🔍 DEBUG: Starting parse with fuel: {:?}...", self.config.debug_fuel);
- let ast = match NyashParser::parse_from_string_with_fuel(&code, self.config.debug_fuel) {
+ let ast = match NyashParser::parse_from_string_with_fuel(code_ref, self.config.debug_fuel) {
Ok(ast) => { eprintln!("🔍 DEBUG: Parse completed, AST created"); ast },
Err(e) => { eprintln!("❌ Parse error: {}", e); process::exit(1); }
};
- println!("✅ Parse successful!");
+ if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") && !quiet_pipe {
+ println!("✅ Parse successful!");
+ }
// Execute the AST
let mut interpreter = NyashInterpreter::new();
eprintln!("🔍 DEBUG: Starting execution...");
match interpreter.execute(ast) {
Ok(result) => {
- println!("✅ Execution completed successfully!");
+ if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") && !quiet_pipe {
+ println!("✅ Execution completed successfully!");
+ }
// Normalize display via semantics: prefer numeric, then string, then fallback
let disp = {
// Special-case: plugin IntegerBox → call .get to fetch numeric value
@@ -182,7 +298,7 @@ impl NyashRunner {
.unwrap_or_else(|| result.to_string_box().value)
}
};
- println!("Result: {}", disp);
+ if !quiet_pipe { println!("Result: {}", disp); }
},
Err(e) => {
eprintln!("❌ Runtime error:\n{}", e.detailed_message(Some(&code)));
diff --git a/src/runtime/extern_registry.rs b/src/runtime/extern_registry.rs
index fe3b1916..88ab5f3b 100644
--- a/src/runtime/extern_registry.rs
+++ b/src/runtime/extern_registry.rs
@@ -34,6 +34,9 @@ static EXTERNS: Lazy> = Lazy::new(|| vec![
ExternSpec { iface: "env.future", method: "birth", min_arity: 1, max_arity: 1, slot: Some(20) },
ExternSpec { iface: "env.future", method: "set", min_arity: 2, max_arity: 2, slot: Some(21) },
ExternSpec { iface: "env.future", method: "await", min_arity: 1, max_arity: 1, slot: Some(22) },
+ // modules (minimal registry)
+ ExternSpec { iface: "env.modules", method: "set", min_arity: 2, max_arity: 2, slot: None },
+ ExternSpec { iface: "env.modules", method: "get", min_arity: 1, max_arity: 1, slot: None },
]);
pub fn resolve(iface: &str, method: &str) -> Option {
diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs
index 60570e53..a5090a2e 100644
--- a/src/runtime/mod.rs
+++ b/src/runtime/mod.rs
@@ -22,6 +22,7 @@ pub mod type_registry; // Phase 12: TypeId→TypeBox 解決(雛形)
pub mod host_handles; // C ABI(TLV) 向け HostHandle レジストリ(ユーザー/内蔵Box受け渡し)
pub mod host_api; // C ABI: plugins -> host 逆呼び出しAPI(TLSでVMに橋渡し)
pub mod extern_registry; // ExternCall (env.*) 登録・診断用レジストリ
+pub mod modules_registry; // env.modules minimal registry
#[cfg(test)]
mod tests;
diff --git a/src/runtime/modules_registry.rs b/src/runtime/modules_registry.rs
new file mode 100644
index 00000000..b7842f00
--- /dev/null
+++ b/src/runtime/modules_registry.rs
@@ -0,0 +1,26 @@
+//! Minimal global registry for env.modules (Phase 15 P0b)
+
+use std::collections::HashMap;
+use std::sync::Mutex;
+use once_cell::sync::Lazy;
+
+use crate::box_trait::NyashBox;
+
+static REGISTRY: Lazy>>> = Lazy::new(|| Mutex::new(HashMap::new()));
+
+pub fn set(name: String, value: Box) {
+ if let Ok(mut map) = REGISTRY.lock() {
+ map.insert(name, value);
+ }
+}
+
+pub fn get(name: &str) -> Option> {
+ if let Ok(mut map) = REGISTRY.lock() {
+ if let Some(b) = map.get_mut(name) {
+ // clone_box to hand out an owned copy
+ return Some(b.clone_box());
+ }
+ }
+ None
+}
+
diff --git a/src/runtime/plugin_loader_v2/enabled/loader.rs b/src/runtime/plugin_loader_v2/enabled/loader.rs
index 6da71945..a42a07b0 100644
--- a/src/runtime/plugin_loader_v2/enabled/loader.rs
+++ b/src/runtime/plugin_loader_v2/enabled/loader.rs
@@ -190,6 +190,21 @@ impl PluginLoaderV2 {
pub fn extern_call(&self, iface_name: &str, method_name: &str, args: &[Box]) -> BidResult