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)* -[![Build Status](https://img.shields.io/badge/Build-Passing-brightgreen.svg)](#) +[![Selfhost Minimal](https://github.com/moe-charm/nyash/actions/workflows/selfhost-minimal.yml/badge.svg?branch=selfhosting-dev)](https://github.com/moe-charm/nyash/actions/workflows/selfhost-minimal.yml) +[![Core Smoke](https://github.com/moe-charm/nyash/actions/workflows/smoke.yml/badge.svg)](https://github.com/moe-charm/nyash/actions/workflows/smoke.yml) [![Everything is Box](https://img.shields.io/badge/Philosophy-Everything%20is%20Box-blue.svg)](#philosophy) [![Performance](https://img.shields.io/badge/Performance-13.5x%20高速化-ff6b6b.svg)](#performance) [![JIT Ready](https://img.shields.io/badge/JIT-Cranelift%20搭載%20(実行封印)-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)* -[![Build Status](https://img.shields.io/badge/Build-Passing-brightgreen.svg)](#) +[![Selfhost Minimal](https://github.com/moe-charm/nyash/actions/workflows/selfhost-minimal.yml/badge.svg?branch=selfhosting-dev)](https://github.com/moe-charm/nyash/actions/workflows/selfhost-minimal.yml) +[![Core Smoke](https://github.com/moe-charm/nyash/actions/workflows/smoke.yml/badge.svg)](https://github.com/moe-charm/nyash/actions/workflows/smoke.yml) [![Everything is Box](https://img.shields.io/badge/Philosophy-Everything%20is%20Box-blue.svg)](#philosophy) [![Performance](https://img.shields.io/badge/Performance-13.5x%20Faster-ff6b6b.svg)](#performance) [![JIT Ready](https://img.shields.io/badge/JIT-Cranelift%20Powered%20(runtime%20disabled)-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>> { match (iface_name, method_name) { ("env.console", "log") => { for a in args { println!("{}", a.to_string_box().value); } Ok(None) } + ("env.modules", "set") => { + if args.len() >= 2 { + let key = args[0].to_string_box().value; + let val = args[1].clone_box(); + crate::runtime::modules_registry::set(key, val); + } + Ok(None) + } + ("env.modules", "get") => { + if let Some(k) = args.get(0) { + let key = k.to_string_box().value; + if let Some(v) = crate::runtime::modules_registry::get(&key) { return Ok(Some(v)); } + } + Ok(Some(Box::new(crate::box_trait::VoidBox::new()))) + } ("env.task", "cancelCurrent") => { let tok = crate::runtime::global_hooks::current_group_token(); tok.cancel(); Ok(None) } ("env.task", "currentToken") => { let tok = crate::runtime::global_hooks::current_group_token(); let tb = crate::boxes::token_box::TokenBox::from_token(tok); Ok(Some(Box::new(tb))) } ("env.debug", "trace") => { if std::env::var("NYASH_DEBUG_TRACE").ok().as_deref() == Some("1") { for a in args { eprintln!("[debug.trace] {}", a.to_string_box().value); } } Ok(None) } diff --git a/src/tests/aot_plan_import.rs b/src/tests/aot_plan_import.rs new file mode 100644 index 00000000..050153db --- /dev/null +++ b/src/tests/aot_plan_import.rs @@ -0,0 +1,13 @@ +#[cfg(feature = "aot-plan-import")] +#[test] +fn import_plan_v1_min_and_run_vm() { + // Use the embedded minimal plan JSON + let plan = include_str!("../../tools/aot_plan/samples/plan_v1_min.json"); + let module = crate::mir::aot_plan_import::import_from_str(plan).expect("import plan v1"); + + // Execute via VM; expect string "42" + let mut vm = crate::backend::vm::VM::new(); + let out = vm.execute_module(&module).expect("vm exec"); + assert_eq!(out.to_string_box().value, "42"); +} + diff --git a/src/tests/mir_pure_e2e_arith.rs b/src/tests/mir_pure_e2e_arith.rs new file mode 100644 index 00000000..506ee1a7 --- /dev/null +++ b/src/tests/mir_pure_e2e_arith.rs @@ -0,0 +1,19 @@ +#[cfg(test)] +mod tests { + use crate::parser::NyashParser; + use crate::backend::VM; + + #[test] + fn vm_exec_addition_under_pure_mode() { + std::env::set_var("NYASH_MIR_CORE13_PURE", "1"); + let code = "\nreturn 7 + 35\n"; + let ast = NyashParser::parse_from_string(code).expect("parse"); + let mut compiler = crate::mir::MirCompiler::new(); + let result = compiler.compile(ast).expect("compile"); + let mut vm = VM::new(); + let out = vm.execute_module(&result.module).expect("vm exec"); + assert_eq!(out.to_string_box().value, "42"); + std::env::remove_var("NYASH_MIR_CORE13_PURE"); + } +} + diff --git a/src/tests/mir_pure_e2e_branch.rs b/src/tests/mir_pure_e2e_branch.rs new file mode 100644 index 00000000..2b93065f --- /dev/null +++ b/src/tests/mir_pure_e2e_branch.rs @@ -0,0 +1,19 @@ +#[cfg(test)] +mod tests { + use crate::parser::NyashParser; + use crate::backend::VM; + + #[test] + fn vm_exec_if_then_return_under_pure_mode() { + std::env::set_var("NYASH_MIR_CORE13_PURE", "1"); + let code = "\nif (1) { return 1 }\nreturn 2\n"; + let ast = NyashParser::parse_from_string(code).expect("parse"); + let mut compiler = crate::mir::MirCompiler::new(); + let result = compiler.compile(ast).expect("compile"); + let mut vm = VM::new(); + let out = vm.execute_module(&result.module).expect("vm exec"); + assert_eq!(out.to_string_box().value, "1"); + std::env::remove_var("NYASH_MIR_CORE13_PURE"); + } +} + diff --git a/src/tests/mir_pure_e2e_vm.rs b/src/tests/mir_pure_e2e_vm.rs new file mode 100644 index 00000000..98b7fe93 --- /dev/null +++ b/src/tests/mir_pure_e2e_vm.rs @@ -0,0 +1,30 @@ +#[cfg(test)] +mod tests { + use crate::parser::NyashParser; + use crate::backend::VM; + + #[test] + fn vm_exec_new_string_length_under_pure_mode() { + // Enable Core-13 pure mode + std::env::set_var("NYASH_MIR_CORE13_PURE", "1"); + + // Nyash code: return (new StringBox("Hello")).length() + let code = r#" +return (new StringBox("Hello")).length() +"#; + + // Parse -> MIR -> VM execute + let ast = NyashParser::parse_from_string(code).expect("parse"); + let mut compiler = crate::mir::MirCompiler::new(); + let result = compiler.compile(ast).expect("compile"); + + let mut vm = VM::new(); + let out = vm.execute_module(&result.module).expect("vm exec"); + // Expect 5 as string (to_string_box) for convenience + assert_eq!(out.to_string_box().value, "5"); + + // Cleanup + std::env::remove_var("NYASH_MIR_CORE13_PURE"); + } +} + diff --git a/src/tests/mir_pure_envbox.rs b/src/tests/mir_pure_envbox.rs new file mode 100644 index 00000000..7708b31e --- /dev/null +++ b/src/tests/mir_pure_envbox.rs @@ -0,0 +1,23 @@ +mod tests { + use crate::ast::{ASTNode, LiteralValue, Span}; + use crate::mir::{MirCompiler, MirPrinter}; + + #[test] + fn pure_mode_new_emits_env_box_new() { + // Enable pure mode + std::env::set_var("NYASH_MIR_CORE13_PURE", "1"); + // new StringBox("Hello") + let ast = ASTNode::New { + class: "StringBox".to_string(), + arguments: vec![ASTNode::Literal { value: LiteralValue::String("Hello".into()), span: Span::unknown() }], + type_arguments: vec![], + span: Span::unknown(), + }; + let mut c = MirCompiler::new(); + let result = c.compile(ast).expect("compile"); + let dump = MirPrinter::new().print_module(&result.module); + assert!(dump.contains("extern_call env.box.new"), "expected env.box.new in MIR. dump=\n{}", dump); + std::env::remove_var("NYASH_MIR_CORE13_PURE"); + } +} + diff --git a/src/tests/mir_pure_llvm_build.rs b/src/tests/mir_pure_llvm_build.rs new file mode 100644 index 00000000..6acfacc4 --- /dev/null +++ b/src/tests/mir_pure_llvm_build.rs @@ -0,0 +1,35 @@ +#[cfg(all(test, feature = "llvm"))] +mod tests { + use crate::parser::NyashParser; + use std::fs; + + #[test] + fn llvm_can_build_object_under_pure_mode() { + // Enable Core-13 pure mode + std::env::set_var("NYASH_MIR_CORE13_PURE", "1"); + + // A simple program that exercises env.box.new and locals + let code = r#" +local s +s = new StringBox("abc") +return s.length() +"#; + + let ast = NyashParser::parse_from_string(code).expect("parse"); + let mut compiler = crate::mir::MirCompiler::new(); + let result = compiler.compile(ast).expect("compile"); + + // Build object via LLVM backend + let out = "nyash_pure_llvm_build_test"; + crate::backend::llvm::compile_to_object(&result.module, &format!("{}.o", out)).expect("llvm object build"); + + // Verify object exists and has content + let meta = fs::metadata(format!("{}.o", out)).expect("obj exists"); + assert!(meta.len() > 0, "object file should be non-empty"); + + // Cleanup + let _ = fs::remove_file(format!("{}.o", out)); + std::env::remove_var("NYASH_MIR_CORE13_PURE"); + } +} + diff --git a/src/tests/mir_pure_llvm_parity.rs b/src/tests/mir_pure_llvm_parity.rs new file mode 100644 index 00000000..dd8771bb --- /dev/null +++ b/src/tests/mir_pure_llvm_parity.rs @@ -0,0 +1,27 @@ +#[cfg(all(test, feature = "llvm"))] +mod tests { + use crate::parser::NyashParser; + use crate::backend::VM; + + #[test] + fn llvm_exec_matches_vm_for_addition_under_pure_mode() { + std::env::set_var("NYASH_MIR_CORE13_PURE", "1"); + let code = "\nreturn 7 + 35\n"; + let ast = NyashParser::parse_from_string(code).expect("parse"); + let mut compiler = crate::mir::MirCompiler::new(); + let result = compiler.compile(ast).expect("compile"); + + // VM result + let mut vm = VM::new(); + let vm_out = vm.execute_module(&result.module).expect("vm exec"); + let vm_s = vm_out.to_string_box().value; + + // LLVM result (compile+execute parity path) + let llvm_out = crate::backend::llvm::compile_and_execute(&result.module, "pure_llvm_parity").expect("llvm exec"); + let llvm_s = llvm_out.to_string_box().value; + + assert_eq!(vm_s, llvm_s, "VM and LLVM outputs should match"); + std::env::remove_var("NYASH_MIR_CORE13_PURE"); + } +} + diff --git a/src/tests/mir_pure_locals_normalized.rs b/src/tests/mir_pure_locals_normalized.rs new file mode 100644 index 00000000..d8e062d4 --- /dev/null +++ b/src/tests/mir_pure_locals_normalized.rs @@ -0,0 +1,31 @@ +#[cfg(test)] +mod tests { + use crate::parser::NyashParser; + use crate::mir::MirPrinter; + + #[test] + fn locals_rewritten_to_env_local_calls_in_pure_mode() { + // Enable Core-13 pure mode + std::env::set_var("NYASH_MIR_CORE13_PURE", "1"); + + // Use locals and arithmetic so Load/Store would appear without normalization + let code = r#" +local x +x = 10 +x = x + 32 +return x +"#; + + let ast = NyashParser::parse_from_string(code).expect("parse"); + let mut compiler = crate::mir::MirCompiler::new(); + let result = compiler.compile(ast).expect("compile"); + + let dump = MirPrinter::new().print_module(&result.module); + // Expect env.local.get/set present (pure-mode normalization) + assert!(dump.contains("extern_call env.local.get"), "expected env.local.get in MIR. dump=\n{}", dump); + assert!(dump.contains("extern_call env.local.set"), "expected env.local.set in MIR. dump=\n{}", dump); + + std::env::remove_var("NYASH_MIR_CORE13_PURE"); + } +} + diff --git a/src/tests/mir_pure_only_core13.rs b/src/tests/mir_pure_only_core13.rs new file mode 100644 index 00000000..776ea722 --- /dev/null +++ b/src/tests/mir_pure_only_core13.rs @@ -0,0 +1,48 @@ +#[cfg(test)] +mod tests { + use crate::parser::NyashParser; + + fn is_allowed_core13(inst: &crate::mir::MirInstruction) -> bool { + use crate::mir::MirInstruction as I; + matches!(inst, + I::Const { .. } + | I::BinOp { .. } + | I::Compare { .. } + | I::Jump { .. } + | I::Branch { .. } + | I::Return { .. } + | I::Phi { .. } + | I::Call { .. } + | I::BoxCall { .. } + | I::ExternCall { .. } + | I::TypeOp { .. } + | I::Safepoint + | I::Barrier { .. } + ) + } + + #[test] + fn final_mir_contains_only_core13_instructions() { + std::env::set_var("NYASH_MIR_CORE13_PURE", "1"); + let code = r#" +local x +x = 1 +if (x == 1) { x = x + 41 } +return new StringBox("ok").length() +"#; + let ast = NyashParser::parse_from_string(code).expect("parse"); + let mut compiler = crate::mir::MirCompiler::new(); + let result = compiler.compile(ast).expect("compile"); + // Count non-Core13 instructions + let mut bad = 0usize; + for (_name, f) in &result.module.functions { + for (_bb, b) in &f.blocks { + for i in &b.instructions { if !is_allowed_core13(i) { bad += 1; } } + if let Some(t) = &b.terminator { if !is_allowed_core13(t) { bad += 1; } } + } + } + assert_eq!(bad, 0, "final MIR must contain only Core-13 instructions"); + std::env::remove_var("NYASH_MIR_CORE13_PURE"); + } +} + diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 3cf77e35..e91a3aac 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -22,3 +22,7 @@ pub mod sugar_comp_assign_test; pub mod sugar_coalesce_test; pub mod sugar_safe_access_test; pub mod sugar_range_test; +pub mod policy_mutdeny; +pub mod plugin_hygiene; +#[cfg(feature = "aot-plan-import")] +pub mod aot_plan_import; diff --git a/src/tests/plugin_hygiene.rs b/src/tests/plugin_hygiene.rs new file mode 100644 index 00000000..0c8b9cff --- /dev/null +++ b/src/tests/plugin_hygiene.rs @@ -0,0 +1,34 @@ +#[test] +fn plugin_invoke_hygiene_prefers_hostcall_for_mapped() { + use crate::jit::policy::invoke::{decide_box_method, InvokeDecision}; + use crate::jit::r#extern::collections as c; + + // Ensure plugin builtins are not forced + std::env::remove_var("NYASH_USE_PLUGIN_BUILTINS"); + + // For ArrayBox.get, policy should map to hostcall symbol, not plugin invoke + let decision = decide_box_method("ArrayBox", "get", 2, true); + match decision { + InvokeDecision::HostCall { symbol, reason, .. } => { + assert_eq!(symbol, c::SYM_ARRAY_GET_H); + assert_eq!(reason, "mapped_symbol"); + } + other => panic!("expected HostCall(mapped_symbol), got: {:?}", other), + } +} + +#[test] +fn plugin_invoke_hygiene_string_len_is_hostcall() { + use crate::jit::policy::invoke::{decide_box_method, InvokeDecision}; + use crate::jit::r#extern::collections as c; + + std::env::remove_var("NYASH_USE_PLUGIN_BUILTINS"); + let decision = decide_box_method("StringBox", "len", 1, true); + match decision { + InvokeDecision::HostCall { symbol, reason, .. } => { + assert_eq!(symbol, c::SYM_STRING_LEN_H); + assert_eq!(reason, "mapped_symbol"); + } + other => panic!("expected HostCall(mapped_symbol) for String.len, got: {:?}", other), + } +} diff --git a/src/tests/policy_mutdeny.rs b/src/tests/policy_mutdeny.rs new file mode 100644 index 00000000..73824764 --- /dev/null +++ b/src/tests/policy_mutdeny.rs @@ -0,0 +1,106 @@ +#[cfg(feature = "cranelift-jit")] +#[test] +#[ignore] +fn jit_readonly_array_push_denied() { + use crate::mir::{MirModule, MirFunction, FunctionSignature, MirInstruction, EffectMask, BasicBlockId, ConstValue, MirType}; + // Ensure read-only policy is on + std::env::set_var("NYASH_JIT_READ_ONLY", "1"); + + // Build: a = new ArrayBox(); a.push(3); ret a.len() + let sig = FunctionSignature { name: "main".into(), params: vec![], return_type: MirType::Integer, effects: EffectMask::PURE }; + let mut f = MirFunction::new(sig, BasicBlockId::new(0)); + let bb = f.entry_block; + let a = f.next_value_id(); + f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::NewBox { dst: a, box_type: "ArrayBox".into(), args: vec![] }); + let three = f.next_value_id(); f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::Const { dst: three, value: ConstValue::Integer(3) }); + // push should be denied under read-only policy, effectively no-op for length + f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::BoxCall { dst: None, box_val: a, method: "push".into(), args: vec![three], method_id: None, effects: EffectMask::PURE }); + let ln = f.next_value_id(); f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::BoxCall { dst: Some(ln), box_val: a, method: "len".into(), args: vec![], method_id: None, effects: EffectMask::PURE }); + f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::Return { value: Some(ln) }); + let mut m = MirModule::new("jit_readonly_array_push_denied".into()); m.add_function(f); + let out = crate::backend::cranelift_compile_and_execute(&m, "jit_readonly_array_push_denied").expect("JIT exec"); + assert_eq!(out.to_string_box().value, "0", "Array.push must be denied under read-only policy"); +} + +#[cfg(feature = "cranelift-jit")] +#[test] +#[ignore] +fn jit_readonly_map_set_denied() { + use crate::mir::{MirModule, MirFunction, FunctionSignature, MirInstruction, EffectMask, BasicBlockId, ConstValue, MirType}; + // Ensure read-only policy is on + std::env::set_var("NYASH_JIT_READ_ONLY", "1"); + + // Build: m = new MapBox(); m.set("a", 2); ret m.size() + let sig = FunctionSignature { name: "main".into(), params: vec![], return_type: MirType::Integer, effects: EffectMask::PURE }; + let mut f = MirFunction::new(sig, BasicBlockId::new(0)); + let bb = f.entry_block; + let mbox = f.next_value_id(); + f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::NewBox { dst: mbox, box_type: "MapBox".into(), args: vec![] }); + let key = f.next_value_id(); f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::Const { dst: key, value: ConstValue::String("a".into()) }); + let val = f.next_value_id(); f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::Const { dst: val, value: ConstValue::Integer(2) }); + // set should be denied under read-only policy + f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::BoxCall { dst: None, box_val: mbox, method: "set".into(), args: vec![key, val], method_id: None, effects: EffectMask::PURE }); + let sz = f.next_value_id(); f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::BoxCall { dst: Some(sz), box_val: mbox, method: "size".into(), args: vec![], method_id: None, effects: EffectMask::PURE }); + f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::Return { value: Some(sz) }); + let mut module = MirModule::new("jit_readonly_map_set_denied".into()); module.add_function(f); + let out = crate::backend::cranelift_compile_and_execute(&module, "jit_readonly_map_set_denied").expect("JIT exec"); + assert_eq!(out.to_string_box().value, "0", "Map.set must be denied under read-only policy"); +} + +// Engine-independent smoke: validate policy denial via host externs +#[test] +fn extern_readonly_array_push_denied() { + use std::sync::Arc; + use crate::boxes::array::ArrayBox; + use crate::backend::vm::VMValue; + use crate::jit::r#extern::collections as c; + + std::env::set_var("NYASH_JIT_READ_ONLY", "1"); + let arr = Arc::new(ArrayBox::new()); + let recv = VMValue::BoxRef(arr.clone()); + let val = VMValue::Integer(3); + let _ = c::array_push(&[recv.clone(), val]); + let len = c::array_len(&[recv]); + assert_eq!(len.to_string(), "0"); +} + +#[test] +fn extern_readonly_map_set_denied() { + use std::sync::Arc; + use crate::boxes::map_box::MapBox; + use crate::backend::vm::VMValue; + use crate::jit::r#extern::collections as c; + + std::env::set_var("NYASH_JIT_READ_ONLY", "1"); + let map = Arc::new(MapBox::new()); + let recv = VMValue::BoxRef(map); + let key = VMValue::from_nyash_box(Box::new(crate::box_trait::StringBox::new("a"))); + let val = VMValue::Integer(2); + let _ = c::map_set(&[recv.clone(), key, val]); + let sz = c::map_size(&[recv]); + assert_eq!(sz.to_string(), "0"); +} + +#[test] +fn extern_readonly_read_ops_allowed() { + use std::sync::Arc; + use crate::boxes::{array::ArrayBox, map_box::MapBox}; + use crate::backend::vm::VMValue; + use crate::jit::r#extern::collections as c; + + std::env::set_var("NYASH_JIT_READ_ONLY", "1"); + // Array: len/get are read-only + let arr = Arc::new(ArrayBox::new()); + let recv_a = VMValue::BoxRef(arr.clone()); + let len = c::array_len(&[recv_a.clone()]); + assert_eq!(len.to_string(), "0"); + let zero = VMValue::Integer(0); + let got = c::array_get(&[recv_a.clone(), zero]); + assert_eq!(got.to_string(), "void"); + + // Map: size is read-only + let map = Arc::new(MapBox::new()); + let recv_m = VMValue::BoxRef(map); + let size = c::map_size(&[recv_m]); + assert_eq!(size.to_string(), "0"); +} diff --git a/tools/aot_plan/README.md b/tools/aot_plan/README.md new file mode 100644 index 00000000..0b3835f7 --- /dev/null +++ b/tools/aot_plan/README.md @@ -0,0 +1,14 @@ +# Nyash AOT-Plan (Phase 15.1) — Scripts Skeleton + +This folder will contain Nyash scripts that analyze a project (following `using` imports) and emit `aot_plan.v1.json` per docs/specs/aot_plan_v1.md. + +Phase 15.1 scope: +- Keep scripts minimal and deterministic +- Do not invoke native toolchains +- Output a single plan JSON for small smokes + +Placeholder files included: +- `analyze.ny` — entry script (skeleton) +- `samples/mini_project/` — a tiny sample project with `using` +- `samples/plan_v1_min.json` — a minimal plan JSON used by importer tests + diff --git a/tools/aot_plan/analyze.ny b/tools/aot_plan/analyze.ny new file mode 100644 index 00000000..d08370e2 --- /dev/null +++ b/tools/aot_plan/analyze.ny @@ -0,0 +1,14 @@ +// Phase 15.1 placeholder Nyash script for AOT-Plan analysis +// Input: project root; follows `using` to collect functions/externs (future work) +// Output: prints a minimal plan JSON to stdout + +let plan = { + version: "1", + name: "mini_project", + functions: [ + { name: "main", return_type: "integer", body: { kind: "const_return", value: 42 } } + ] +}; + +Console.log(JSON.stringify(plan)); + diff --git a/tools/aot_plan/samples/mini_project/main.ny b/tools/aot_plan/samples/mini_project/main.ny new file mode 100644 index 00000000..ebfd6a52 --- /dev/null +++ b/tools/aot_plan/samples/mini_project/main.ny @@ -0,0 +1,5 @@ +// Sample Nyash project root +using "./util.ny"; + +return 42; + diff --git a/tools/aot_plan/samples/mini_project/util.ny b/tools/aot_plan/samples/mini_project/util.ny new file mode 100644 index 00000000..950849ce --- /dev/null +++ b/tools/aot_plan/samples/mini_project/util.ny @@ -0,0 +1,5 @@ +// Utility file for using-chain smoke +let x = 21; +let y = 2; +// not evaluated in this sample + diff --git a/tools/aot_plan/samples/plan_v1_min.json b/tools/aot_plan/samples/plan_v1_min.json new file mode 100644 index 00000000..256b363b --- /dev/null +++ b/tools/aot_plan/samples/plan_v1_min.json @@ -0,0 +1,8 @@ +{ + "version": "1", + "name": "mini_project", + "functions": [ + { "name": "main", "return_type": "integer", "body": { "kind": "const_return", "value": 42 } } + ] +} + diff --git a/tools/aot_smoke_cranelift.sh b/tools/aot_smoke_cranelift.sh index ee73125e..1708e677 100644 --- a/tools/aot_smoke_cranelift.sh +++ b/tools/aot_smoke_cranelift.sh @@ -20,8 +20,20 @@ cargo build --release --features cranelift-jit >/dev/null 2>&1 || true echo "[AOT] lowering: $APP -> $OBJ" NYASH_DISABLE_PLUGINS=1 NYASH_AOT_OBJECT_OUT="$OBJ" "$BIN" --jit-direct "$APP" +# Ensure NyRT static library exists (libnyrt.a) +if [ ! -f crates/nyrt/target/release/libnyrt.a ] && [ ! -f target/release/libnyrt.a ] && [ ! -f target/release/nyrt.lib ]; then + echo "[AOT] building NyRT (static runtime)" + (cd crates/nyrt && cargo build --release >/dev/null) +fi + echo "[AOT] linking: $EXE" -cc "$OBJ" -L target/release -Wl,--whole-archive -lnyrt -Wl,--no-whole-archive -lpthread -ldl -lm -o "$EXE" +# Prefer the workspace root target, then crates/nyrt/target +cc "$OBJ" \ + -L target/release \ + -L crates/nyrt/target/release \ + -Wl,--whole-archive -lnyrt -Wl,--no-whole-archive \ + -lpthread -ldl -lm \ + -o "$EXE" echo "[AOT] run: ./$EXE" "./$EXE" || true diff --git a/tools/modules_smoke.sh b/tools/modules_smoke.sh new file mode 100644 index 00000000..af5c0ab9 --- /dev/null +++ b/tools/modules_smoke.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +ROOT_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd) +BIN="$ROOT_DIR/target/release/nyash" + +if [ ! -x "$BIN" ]; then + cargo build --release --features cranelift-jit >/dev/null +fi + +TMPJSON=$(mktemp) +cat >"$TMPJSON" <<'JSON' +{"version":0,"kind":"Program","body":[ + {"type":"Extern","iface":"env.modules","method":"set","args":[{"type":"Str","value":"acme.mod"},{"type":"Int","value":42}]}, + {"type":"Return","expr":{"type":"Binary","op":"-","lhs":{"type":"Extern","iface":"env.modules","method":"get","args":[{"type":"Str","value":"acme.mod"}]},"rhs":{"type":"Int","value":42}}} +]} +JSON + +NYASH_DISABLE_PLUGINS=1 "$BIN" --backend vm --json-file "$TMPJSON" > /tmp/nyash-modules-smoke.out || true +if rg -q '^Result:\s*0\b' /tmp/nyash-modules-smoke.out; then + echo "PASS: modules (env.modules set/get)" >&2 +else + echo "FAIL: modules (env.modules set/get)" >&2 + sed -n '1,120p' /tmp/nyash-modules-smoke.out >&2 + exit 1 +fi + +echo "All PASS" >&2 diff --git a/tools/ny_parser_run.sh b/tools/ny_parser_run.sh index 759e2c55..16e8c3ab 100644 --- a/tools/ny_parser_run.sh +++ b/tools/ny_parser_run.sh @@ -3,4 +3,6 @@ set -euo pipefail SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) ROOT_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd) -${ROOT_DIR}/target/release/nyash ${ROOT_DIR}/apps/ny-parser-nyash/main.nyash +NYASH_JSON_ONLY=1 NYASH_DISABLE_PLUGINS=1 NYASH_CLI_VERBOSE=0 \ + ${ROOT_DIR}/target/release/nyash ${ROOT_DIR}/apps/ny-parser-nyash/main.nyash \ + | awk 'BEGIN{printed=0} { if (!printed && $0 ~ /^\s*\{/){ print; printed=1 } }' diff --git a/tools/ny_roundtrip_smoke.sh b/tools/ny_roundtrip_smoke.sh index 255d9268..d55b04b1 100644 --- a/tools/ny_roundtrip_smoke.sh +++ b/tools/ny_roundtrip_smoke.sh @@ -11,15 +11,16 @@ if [ ! -x "$BIN" ]; then cargo build --release --features cranelift-jit >/dev/null fi -echo "[Roundtrip] Case A: Ny → JSON(v0) → MIR-Interp (pipe)" >&2 -set -o pipefail -# Use a subset-friendly program (no parentheses) compatible with current tokenizer/desugar -printf 'return 1+2*3\n' | "$NY_PARSER" | "$BIN" --ny-parser-pipe > /tmp/nyash-rt-a.out || true +echo "[Roundtrip] Case A: Ny (source v0) → MIR-Interp (direct bridge)" >&2 +TMPNY=$(mktemp) +printf 'return 1+2*3\n' > "$TMPNY" +NYASH_DISABLE_PLUGINS=1 "$BIN" --parser ny --backend vm "$TMPNY" > /tmp/nyash-rt-a.out || true if rg -q '^Result:\s*7\b' /tmp/nyash-rt-a.out; then - echo "PASS: Case A (pipe)" >&2 + echo "PASS: Case A (direct bridge)" >&2 else - echo "SKIP: Case A (pipe) - parser pipeline not ready; proceeding with Case B" >&2 + echo "FAIL: Case A (direct bridge)" >&2 cat /tmp/nyash-rt-a.out >&2 || true + exit 1 fi echo "[Roundtrip] Case B: JSON(v0) file → MIR-Interp" >&2 diff --git a/tools/using_e2e_smoke.sh b/tools/using_e2e_smoke.sh index 2c202c40..f6e6b4d0 100644 --- a/tools/using_e2e_smoke.sh +++ b/tools/using_e2e_smoke.sh @@ -15,12 +15,13 @@ if [ ! -f "$APP" ]; then echo "[using-e2e] scaffolding sample..." >&2 mkdir -p "$ROOT_DIR/apps/using-e2e" cat > "$APP" <<'NYCODE' -// using/nyash.link E2E sample (placeholder) +// using/nyash.link E2E sample (MVP) +using acme.util + static box Main { init { } main(args) { - // When using/nyash.link is active, modules can be resolved here. - // Placeholder just returns 0 for now. + // using line should be accepted when NYASH_ENABLE_USING=1 return 0 } } diff --git a/tools/using_resolve_smoke.sh b/tools/using_resolve_smoke.sh new file mode 100644 index 00000000..2008d72b --- /dev/null +++ b/tools/using_resolve_smoke.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +ROOT_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd) +BIN="$ROOT_DIR/target/release/nyash" + +if [ ! -x "$BIN" ]; then + cargo build --release --features cranelift-jit >/dev/null +fi + +JSON=$(mktemp) +cat >"$JSON" <<'JSON' +{"version":0,"kind":"Program","body":[ + {"type":"Return","expr":{"type":"Extern","iface":"env.modules","method":"get","args":[{"type":"Str","value":"Util"}]}} +]} +JSON + +# Use direct path using via CLI +NYASH_DISABLE_PLUGINS=1 "$BIN" --backend vm --json-file "$JSON" \ + --using '"apps/selfhost-minimal/main.nyash" as Util' > /tmp/nyash-using-resolve.out + +if rg -q '\.nyash' /tmp/nyash-using-resolve.out; then + echo "PASS: using resolve (CLI direct path)" >&2 +else + echo "FAIL: using resolve (CLI direct path)" >&2 + sed -n '1,120p' /tmp/nyash-using-resolve.out >&2 + exit 1 +fi + +echo "All PASS" >&2 + diff --git a/tools/using_strict_path_fail_smoke.sh b/tools/using_strict_path_fail_smoke.sh new file mode 100644 index 00000000..f0c6663c --- /dev/null +++ b/tools/using_strict_path_fail_smoke.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +ROOT_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd) +BIN="$ROOT_DIR/target/release/nyash" + +if [ ! -x "$BIN" ]; then + cargo build --release --features cranelift-jit >/dev/null +fi + +TMP=$(mktemp) +cat >"$TMP" <<'NY' +using "./definitely_missing_12345.nyash" as Miss +static box Main { main(args) { return 0 } } +NY + +set +e +NYASH_ENABLE_USING=1 NYASH_USING_STRICT=1 "$BIN" --backend vm "$TMP" >/tmp/nyash-using-strict.out 2>&1 +rc=$? +set -e + +if [ $rc -ne 0 ]; then + echo "PASS: strict path missing fails (rc=$rc)" >&2 +else + echo "FAIL: strict path missing did not fail" >&2 + cat /tmp/nyash-using-strict.out >&2 || true + exit 1 +fi + +echo "All PASS" >&2 + diff --git a/tools/using_unresolved_smoke.sh b/tools/using_unresolved_smoke.sh new file mode 100644 index 00000000..2354eb4b --- /dev/null +++ b/tools/using_unresolved_smoke.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +ROOT_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd) +BIN="$ROOT_DIR/target/release/nyash" + +if [ ! -x "$BIN" ]; then + cargo build --release --features cranelift-jit >/dev/null +fi + +JSON=$(mktemp) +cat >"$JSON" <<'JSON' +{"version":0,"kind":"Program","body":[{"type":"Return","expr":{"type":"Int","value":0}}]} +JSON + +set +e +out=$(NYASH_CLI_VERBOSE=1 "$BIN" --backend vm --json-file "$JSON" --using "no.such.ns as X" 2>&1) +rc=$? +set -e + +echo "$out" | rg -q "\[using\] unresolved 'no\.such\.ns'" || { echo "FAIL: unresolved hint not shown" >&2; echo "$out" >&2; exit 1; } +echo "$out" | rg -q '^Result:\s*0\b' || { echo "FAIL: execution result not 0" >&2; echo "$out" >&2; exit 1; } +echo "PASS: using unresolved hint (CLI)" >&2 +echo "All PASS" >&2