Merge selfhosting-dev into main (Core-13 pure CI/tests + LLVM bridge) (#126)
* WIP: sync before merging origin/main * fix: unify using/module + build CLI; add missing helper in runner; build passes; core smokes green; jit any.len string now returns 3 * Apply local changes after merging main; keep docs/phase-15 removed per main; add phase-15.1 docs and tests * Remove legacy docs/phase-15/README.md to align with main * integration: add Core-13 pure CI, tests, and minimal LLVM execute bridge (no docs) (#125) Co-authored-by: Tomoaki <tomoaki@example.com> --------- Co-authored-by: Selfhosting Dev <selfhost@example.invalid> Co-authored-by: Tomoaki <tomoaki@example.com>
This commit is contained in:
5
.github/pull_request_template.md
vendored
5
.github/pull_request_template.md
vendored
@ -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`, その他(記述)
|
||||
|
||||
36
.github/workflows/core13-pure-llvm.yml
vendored
Normal file
36
.github/workflows/core13-pure-llvm.yml
vendored
Normal file
@ -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
|
||||
|
||||
27
.github/workflows/core13-pure.yml
vendored
Normal file
27
.github/workflows/core13-pure.yml
vendored
Normal file
@ -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
|
||||
|
||||
54
.github/workflows/selfhost-minimal.yml
vendored
Normal file
54
.github/workflows/selfhost-minimal.yml
vendored
Normal file
@ -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
|
||||
|
||||
12
AGENTS.md
12
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.
|
||||
|
||||
@ -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 実行を封印し四体制へ】
|
||||
|
||||
概要
|
||||
|
||||
@ -38,6 +38,7 @@ cranelift-jit = [
|
||||
"dep:cranelift-object",
|
||||
"dep:cranelift-native"
|
||||
]
|
||||
aot-plan-import = []
|
||||
|
||||
[lib]
|
||||
name = "nyash_rust"
|
||||
|
||||
35
Makefile
Normal file
35
Makefile
Normal file
@ -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
|
||||
15
README.ja.md
15
README.ja.md
@ -4,7 +4,8 @@
|
||||
|
||||
*[🇺🇸 English Version / 英語版はこちら](README.md)*
|
||||
|
||||
[](#)
|
||||
[](https://github.com/moe-charm/nyash/actions/workflows/selfhost-minimal.yml)
|
||||
[](https://github.com/moe-charm/nyash/actions/workflows/smoke.yml)
|
||||
[](#philosophy)
|
||||
[](#performance)
|
||||
[-orange.svg)](#execution-modes)
|
||||
@ -14,6 +15,18 @@
|
||||
---
|
||||
|
||||
開発者向けクイックスタート: `docs/DEV_QUICKSTART.md`
|
||||
セルフホスト1枚ガイド: `docs/self-hosting.md`
|
||||
|
||||
## 目次
|
||||
- [Self-Hosting(自己ホスト開発)](#self-hosting)
|
||||
- [今すぐ試す(ブラウザ)](#-今すぐブラウザでnyashを試そう)
|
||||
|
||||
<a id="self-hosting"></a>
|
||||
## 🧪 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`
|
||||
|
||||
|
||||
15
README.md
15
README.md
@ -4,7 +4,8 @@
|
||||
|
||||
*[🇯🇵 日本語版はこちら / Japanese Version](README.ja.md)*
|
||||
|
||||
[](#)
|
||||
[](https://github.com/moe-charm/nyash/actions/workflows/selfhost-minimal.yml)
|
||||
[](https://github.com/moe-charm/nyash/actions/workflows/smoke.yml)
|
||||
[](#philosophy)
|
||||
[](#performance)
|
||||
[-orange.svg)](#execution-modes)
|
||||
@ -14,6 +15,18 @@
|
||||
---
|
||||
|
||||
Developer quickstart: see `docs/DEV_QUICKSTART.md`. Changelog highlights: `CHANGELOG.md`.
|
||||
Self‑hosting one‑pager: `docs/self-hosting.md`.
|
||||
|
||||
## Table of Contents
|
||||
- [Self‑Hosting (Dev Focus)](#self-hosting)
|
||||
- [Try in Browser](#-try-nyash-in-your-browser-right-now)
|
||||
|
||||
<a id="self-hosting"></a>
|
||||
## 🧪 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.
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
6
apps/smokes/jit_direct_array_mut.nyash
Normal file
6
apps/smokes/jit_direct_array_mut.nyash
Normal file
@ -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()
|
||||
})()
|
||||
21
apps/smokes/json_v0_short_and.json
Normal file
21
apps/smokes/json_v0_short_and.json
Normal file
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
21
apps/smokes/json_v0_short_or.json
Normal file
21
apps/smokes/json_v0_short_or.json
Normal file
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
34
docs/CONTRIBUTING-MERGE.md
Normal file
34
docs/CONTRIBUTING-MERGE.md
Normal file
@ -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で先行適用
|
||||
- 共有インターフェイスは薄いアダプタで橋渡し(実装詳細は各ブランチ内)
|
||||
|
||||
@ -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`
|
||||
|
||||
|
||||
9
docs/archives/CURRENT_TASK-2025-09-06.md
Normal file
9
docs/archives/CURRENT_TASK-2025-09-06.md
Normal file
@ -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 履歴から参照してください)
|
||||
|
||||
@ -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
|
||||
|
||||
25
docs/phase-15/cranelift/CRANELIFT_TASKS.md
Normal file
25
docs/phase-15/cranelift/CRANELIFT_TASKS.md
Normal file
@ -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 を優先し、両ブランチが同時に衝突しない形へ調整。
|
||||
|
||||
51
docs/self-hosting.md
Normal file
51
docs/self-hosting.md
Normal file
@ -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 等で互換性を維持。
|
||||
|
||||
37
docs/specs/aot_plan_v1.md
Normal file
37
docs/specs/aot_plan_v1.md
Normal file
@ -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" }}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@ -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<BasicValueEnum, String> {
|
||||
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<inkwell::values::IntValue, String> {
|
||||
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<inkwell::values::IntValue, String> { 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<Box<dyn NyashBox>, 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<dyn NyashBox> = 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::<IntegerBox>())
|
||||
.ok_or_else(|| format!("binop lhs %{} not integer", lhs.0))?;
|
||||
let r = self
|
||||
.values
|
||||
.get(rhs)
|
||||
.and_then(|b| b.as_any().downcast_ref::<IntegerBox>())
|
||||
.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)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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<VMValue> = 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<VMValue> = 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<VMValue> = 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
|
||||
|
||||
35
src/cli.rs
35
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<String>,
|
||||
// Using/module resolution helpers (MVP)
|
||||
pub using: Option<String>,
|
||||
pub using_path: Option<String>,
|
||||
pub modules: Option<String>,
|
||||
// Build system (MVP)
|
||||
pub build_path: Option<String>,
|
||||
pub build_app: Option<String>,
|
||||
@ -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::<String>("parser").map(|s| s == "ny").unwrap_or(false),
|
||||
ny_parser_pipe: matches.get_flag("ny-parser-pipe"),
|
||||
json_file: matches.get_one::<String>("json-file").cloned(),
|
||||
using: matches.get_one::<String>("using").cloned(),
|
||||
using_path: matches.get_one::<String>("using-path").cloned(),
|
||||
modules: matches.get_one::<String>("module").cloned(),
|
||||
// Build system (MVP)
|
||||
build_path: matches.get_one::<String>("build").cloned(),
|
||||
build_app: matches.get_one::<String>("build-app").cloned(),
|
||||
build_out: matches.get_one::<String>("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");
|
||||
|
||||
3
src/jit/extern/birth.rs
vendored
3
src/jit/extern/birth.rs
vendored
@ -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";
|
||||
|
||||
1
src/jit/extern/collections.rs
vendored
1
src/jit/extern/collections.rs
vendored
@ -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)
|
||||
|
||||
@ -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") {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
87
src/mir/aot_plan_import.rs
Normal file
87
src/mir/aot_plan_import.rs
Normal file
@ -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<String>,
|
||||
functions: Vec<PlanFunction>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct PlanFunction {
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
params: Vec<PlanParam>,
|
||||
return_type: Option<String>, // "integer" | "string" | ...
|
||||
#[serde(default)]
|
||||
body: Option<PlanBody>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct PlanParam { name: String, r#type: Option<String> }
|
||||
|
||||
#[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<ConstValue> {
|
||||
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<MirModule, String> {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -15,13 +15,19 @@ struct ProgramV0 {
|
||||
#[serde(tag = "type")]
|
||||
enum StmtV0 {
|
||||
Return { expr: ExprV0 },
|
||||
Extern { iface: String, method: String, args: Vec<ExprV0> },
|
||||
}
|
||||
|
||||
#[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<ExprV0>, rhs: Box<ExprV0> },
|
||||
Extern { iface: String, method: String, args: Vec<ExprV0> },
|
||||
Compare { op: String, lhs: Box<ExprV0>, rhs: Box<ExprV0> },
|
||||
Logical { op: String, lhs: Box<ExprV0>, rhs: Box<ExprV0> }, // short-circuit: &&, || (or: "and"/"or")
|
||||
}
|
||||
|
||||
pub fn parse_json_v0_to_module(json: &str) -> Result<MirModule, String> {
|
||||
@ -29,28 +35,60 @@ pub fn parse_json_v0_to_module(json: &str) -> Result<MirModule, String> {
|
||||
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 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.set_terminator(MirInstruction::Return { value: Some(ret_val) });
|
||||
bb.add_instruction(MirInstruction::ExternCall { dst: None, iface_name: iface.clone(), method_name: method.clone(), args: arg_ids, effects: EffectMask::IO });
|
||||
}
|
||||
// Infer return type (integer only for v0)
|
||||
f.signature.return_type = MirType::Integer;
|
||||
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)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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<crate::mir::ValueId, String> {
|
||||
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,22 +98,121 @@ fn lower_expr(f: &mut MirFunction, e: &ExprV0) -> Result<crate::mir::ValueId, St
|
||||
return Err("invalid int literal".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_bb) {
|
||||
bb.add_instruction(MirInstruction::Const { dst, value: ConstValue::Integer(ival) });
|
||||
}
|
||||
Ok(dst)
|
||||
Ok((dst, cur_bb))
|
||||
}
|
||||
ExprV0::Str { 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::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",
|
||||
"<json_v0>"
|
||||
);
|
||||
// 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<crate::mir::ValueId>, 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) {
|
||||
|
||||
@ -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<String> = 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<String>, 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<String> = 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
|
||||
// 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<String>)> = Vec::new();
|
||||
let mut pending_modules: Vec<(String, String)> = Vec::new();
|
||||
let mut using_paths: Vec<String> = 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: <path>/<ns as a/b/c>.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::<FloatBox>() {
|
||||
("Float", format!("{}", fb.value))
|
||||
} else if let Some(ib) = result.as_any().downcast_ref::<IntegerBox>() {
|
||||
("Float", format!("{}", ib.value as f64))
|
||||
} else { ("Float", result.to_string_box().value) }
|
||||
}
|
||||
MirType::Integer => {
|
||||
if let Some(ib) = result.as_any().downcast_ref::<IntegerBox>() {
|
||||
("Integer", ib.value.to_string())
|
||||
} else { ("Integer", result.to_string_box().value) }
|
||||
}
|
||||
MirType::Bool => {
|
||||
if let Some(bb) = result.as_any().downcast_ref::<BoolBox>() {
|
||||
("Bool", bb.value.to_string())
|
||||
} else if let Some(ib) = result.as_any().downcast_ref::<IntegerBox>() {
|
||||
("Bool", (ib.value != 0).to_string())
|
||||
} else { ("Bool", result.to_string_box().value) }
|
||||
}
|
||||
MirType::String => {
|
||||
if let Some(sb) = result.as_any().downcast_ref::<StringBox>() {
|
||||
("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;
|
||||
|
||||
@ -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<String>) {
|
||||
use std::fs;
|
||||
fn walk(dir: &std::path::Path, leaf: &str, out: &mut Vec<String>, 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");
|
||||
}
|
||||
|
||||
// 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<String>)> = 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<String> = 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); }
|
||||
};
|
||||
|
||||
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) => {
|
||||
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)));
|
||||
|
||||
@ -34,6 +34,9 @@ static EXTERNS: Lazy<Vec<ExternSpec>> = 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<ExternSpec> {
|
||||
|
||||
@ -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;
|
||||
|
||||
26
src/runtime/modules_registry.rs
Normal file
26
src/runtime/modules_registry.rs
Normal file
@ -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<Mutex<HashMap<String, Box<dyn NyashBox>>>> = Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
pub fn set(name: String, value: Box<dyn NyashBox>) {
|
||||
if let Ok(mut map) = REGISTRY.lock() {
|
||||
map.insert(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(name: &str) -> Option<Box<dyn NyashBox>> {
|
||||
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
|
||||
}
|
||||
|
||||
@ -190,6 +190,21 @@ impl PluginLoaderV2 {
|
||||
pub fn extern_call(&self, iface_name: &str, method_name: &str, args: &[Box<dyn NyashBox>]) -> BidResult<Option<Box<dyn NyashBox>>> {
|
||||
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) }
|
||||
|
||||
13
src/tests/aot_plan_import.rs
Normal file
13
src/tests/aot_plan_import.rs
Normal file
@ -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");
|
||||
}
|
||||
|
||||
19
src/tests/mir_pure_e2e_arith.rs
Normal file
19
src/tests/mir_pure_e2e_arith.rs
Normal file
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
19
src/tests/mir_pure_e2e_branch.rs
Normal file
19
src/tests/mir_pure_e2e_branch.rs
Normal file
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
30
src/tests/mir_pure_e2e_vm.rs
Normal file
30
src/tests/mir_pure_e2e_vm.rs
Normal file
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
23
src/tests/mir_pure_envbox.rs
Normal file
23
src/tests/mir_pure_envbox.rs
Normal file
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
35
src/tests/mir_pure_llvm_build.rs
Normal file
35
src/tests/mir_pure_llvm_build.rs
Normal file
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
27
src/tests/mir_pure_llvm_parity.rs
Normal file
27
src/tests/mir_pure_llvm_parity.rs
Normal file
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
31
src/tests/mir_pure_locals_normalized.rs
Normal file
31
src/tests/mir_pure_locals_normalized.rs
Normal file
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
48
src/tests/mir_pure_only_core13.rs
Normal file
48
src/tests/mir_pure_only_core13.rs
Normal file
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
34
src/tests/plugin_hygiene.rs
Normal file
34
src/tests/plugin_hygiene.rs
Normal file
@ -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),
|
||||
}
|
||||
}
|
||||
106
src/tests/policy_mutdeny.rs
Normal file
106
src/tests/policy_mutdeny.rs
Normal file
@ -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");
|
||||
}
|
||||
14
tools/aot_plan/README.md
Normal file
14
tools/aot_plan/README.md
Normal file
@ -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
|
||||
|
||||
14
tools/aot_plan/analyze.ny
Normal file
14
tools/aot_plan/analyze.ny
Normal file
@ -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));
|
||||
|
||||
5
tools/aot_plan/samples/mini_project/main.ny
Normal file
5
tools/aot_plan/samples/mini_project/main.ny
Normal file
@ -0,0 +1,5 @@
|
||||
// Sample Nyash project root
|
||||
using "./util.ny";
|
||||
|
||||
return 42;
|
||||
|
||||
5
tools/aot_plan/samples/mini_project/util.ny
Normal file
5
tools/aot_plan/samples/mini_project/util.ny
Normal file
@ -0,0 +1,5 @@
|
||||
// Utility file for using-chain smoke
|
||||
let x = 21;
|
||||
let y = 2;
|
||||
// not evaluated in this sample
|
||||
|
||||
8
tools/aot_plan/samples/plan_v1_min.json
Normal file
8
tools/aot_plan/samples/plan_v1_min.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "mini_project",
|
||||
"functions": [
|
||||
{ "name": "main", "return_type": "integer", "body": { "kind": "const_return", "value": 42 } }
|
||||
]
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
29
tools/modules_smoke.sh
Normal file
29
tools/modules_smoke.sh
Normal file
@ -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
|
||||
@ -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 } }'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
32
tools/using_resolve_smoke.sh
Normal file
32
tools/using_resolve_smoke.sh
Normal file
@ -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
|
||||
|
||||
32
tools/using_strict_path_fail_smoke.sh
Normal file
32
tools/using_strict_path_fail_smoke.sh
Normal file
@ -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
|
||||
|
||||
25
tools/using_unresolved_smoke.sh
Normal file
25
tools/using_unresolved_smoke.sh
Normal file
@ -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
|
||||
Reference in New Issue
Block a user