json: add v2 JsonDoc/JsonNode plugin with runtime provider switch; vendored yyjson + FFI; loader resolve(name)->method_id; PyVM JSON shims; smokes + CI gate; disable MiniVmPrints fallbacks by default

- plugin_loader_v2: store per-Box resolve() from TypeBox FFI; add resolve_method_id() and use in invoke_instance_method
- plugin_loader_unified: resolve_method() falls back to loader’s resolve when TOML lacks method entries
- nyash.toml: register JsonDocBox/JsonNodeBox methods (birth/parse/root/error; kind/get/size/at/str/int/bool)
- plugins/nyash-json-plugin:
  * serde/yyjson provider switch via env NYASH_JSON_PROVIDER (default serde)
  * vendored yyjson (c/yyjson) + shim; parse/root/get/size/at/str/int/bool implemented for yyjson
  * TLV void returns aligned to tag=9
- PyVM: add minimal JsonDocBox/JsonNodeBox shims in ops_box.py (for dev path)
- tests/smokes: add jsonbox_{parse_ok,parse_err,nested,collect_prints}; wire two into min-gate CI
- tools: collect_prints_mixed now uses JSON-based app
- MiniVmPrints: move BinaryOp and fallback heuristics behind a dev toggle (default OFF)
- CURRENT_TASK.md: updated with provider policy and fallback stance
This commit is contained in:
Selfhosting Dev
2025-09-22 06:16:20 +09:00
parent 0f96f2297d
commit 27568eb4a6
25 changed files with 20816 additions and 60 deletions

View File

@ -45,6 +45,12 @@ jobs:
- name: Run PyVM Stage-2 smokes - name: Run PyVM Stage-2 smokes
run: bash tools/pyvm_stage2_smoke.sh run: bash tools/pyvm_stage2_smoke.sh
- name: JSON smoke — collect_prints (PyVM+plugins)
run: bash tools/test/smoke/selfhost/jsonbox_collect_prints.sh
- name: JSON smoke — parse error (PyVM+plugins)
run: bash tools/test/smoke/selfhost/jsonbox_parse_err.sh
macro-golden: macro-golden:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20 timeout-minutes: 20

View File

@ -1,4 +1,4 @@
# Current Task — Freeze Polish (Concise) # Current Task — Stability Polish (Concise)
Updated: 20250921 Updated: 20250921
@ -21,12 +21,29 @@ Updated: 20250921
This page is trimmed to reflect the active work only. The previous long form has been archived at `CURRENT_TASK_restored.md`. This page is trimmed to reflect the active work only. The previous long form has been archived at `CURRENT_TASK_restored.md`.
Principles (freeze) Principles (featurepause)
- Selfhosting first. Macro normalization preMIR; PyVM semantics are authoritative. - Selfhosting first. Macro normalization preMIR; PyVM semantics are authoritative.
- New features are paused; allow only bug fixes, docs, smokes/goldens, CI polish. - Big feature additions are paused until Nyash VM bootstrap completes. Bug fixes, docs, smokes/goldens, CI polish, robustness (specpreserving) continue.
- Keep changes minimal/local; no spec changes unless to fix critical issues. - Keep changes minimal/local; no spec changes unless to fix critical issues, and guard any optional paths behind defaultOFF flags.
### Delta (since last update) ### Delta (since last update)
- JSON provideryyjsonベンダリング完了・切替はランタイム
- `plugins/nyash-json-plugin/c/yyjson/{yyjson.h,yyjson.c,LICENSE}` を同梱し、`build.rs + cc` で自己完結ビルド。
- env `NYASH_JSON_PROVIDER=serde|yyjson`(既定=serde。nyash.toml の [env] からも設定可能。
- yyjson 経路で parse/root/get/size/at/str/int/bool を実装TLVタグは従来準拠。失敗時は安全に serde にフォールバック。
- JSON スモークparse_ok/err/nested/collect_printsは serde/yyjson 両経路で緑。専用の yyjson CI ジョブは追加しない(不要)。
- MiniVmPrints 揺れ対応の既定OFF化
- BinaryOp('+') の 2 値抽出や未知形スキップのフォールバックは「開発用トグル」のみ有効化既定OFF
- 本線は JSON Box 経路に統一collect_prints_mixed は JSON ベースのアプリに切替)。
- Plugin v2 (TypeBox) — resolve→invoke 経路の堅牢化(仕様不変)
- v2 ローダが perBox TypeBox FFI の `resolve(name)->method_id` を保持し、`nyash.toml` 未定義メソッドでも動的解決→キャッシュ
- Unified Host の `resolve_method` も config→TypeBox.resolve の順で解決
- 影響範囲はローダ/ホストのみ。既定動作不変、失敗時のフォールバック精度が向上
- JSON Boxbringup
- 追加: `plugins/nyash-json-plugin`JsonDocBox/JsonNodeBox、serde_json backend
- `nyash.toml` に JsonDocBox/JsonNodeBox の methods を登録birth/parse/root/error, kind/get/size/at/str/int/bool/fini
- PyVM 経路: `src/llvm_py/pyvm/ops_box.py` に最小シムJsonDoc/JsonNodeを追加parse/root/get/size/at/str/int/bool
- Smoke: `tools/test/smoke/selfhost/jsonbox_collect_prints.sh`A/B/7/1/7/5を追加し緑を確認
- Using inliner/seam (dev toggles defaultOFF) - Using inliner/seam (dev toggles defaultOFF)
- Resolver seam join normalized; optional brace safety valve addedstrings/comments除外カウント - Resolver seam join normalized; optional brace safety valve addedstrings/comments除外カウント
- Python combineroptional hook: `tools/using_combine.py` を追加(--fix-braces/--seam-debug など) - Python combineroptional hook: `tools/using_combine.py` を追加(--fix-braces/--seam-debug など)
@ -43,17 +60,17 @@ Principles (freeze)
- CI/Smokes - CI/Smokes
- Added UTF8 CP smoke (PyVM): `tools/test/smoke/strings/utf8_cp_smoke.sh` using `apps/tests/strings/utf8_cp_demo.nyash` (green) - Added UTF8 CP smoke (PyVM): `tools/test/smoke/strings/utf8_cp_smoke.sh` using `apps/tests/strings/utf8_cp_demo.nyash` (green)
- Wired into mingate CI alongside MacroCtx smoke (green) - Wired into mingate CI alongside MacroCtx smoke (green)
- Added using mix smoke (PyVM, using ON): `tools/test/smoke/selfhost/collect_prints_using_mixed.sh` (in progress) - Added using mix smoke (PyVM, using ON): `tools/test/smoke/selfhost/collect_prints_using_mixed.sh` — green
- Current: A/B prints OK via MiniVmPrints; ints/compare/binop pending - Fix: MiniVmBinOp.try_print_binop_sum_any gains a lightweight typeddirect fallback scoped to the current Print slice when expression bounds are missing. Spec unchanged; only robustness improved.
- Use dev flags when reproducing: `NYASH_RESOLVE_FIX_BRACES=1 NYASH_PARSER_STATIC_INIT_STRICT=1` - Repro flags: default (NYASH_RESOLVE_FIX_BRACES/NYASH_PARSER_STATIC_INIT_STRICT remain available but not required)
- Goal: fully green (A/B/7/1/7/5) with defaultOFF toggles kept OFF by default
- Added loaderpath dev smoke (MiniVm.collect_prints focus): `tools/test/smoke/selfhost/collect_prints_loader.sh`(任意/開発用) - Added loaderpath dev smoke (MiniVm.collect_prints focus): `tools/test/smoke/selfhost/collect_prints_loader.sh`(任意/開発用)
- Added emptyargs using smoke (PyVM, using ON): `tools/test/smoke/selfhost/collect_empty_args_using_smoke.sh` (uses seam brace safety valve; defaultOFF) - Added emptyargs using smoke (PyVM, using ON): `tools/test/smoke/selfhost/collect_empty_args_using_smoke.sh` (uses seam brace safety valve; defaultOFF)
- Runtime (Rust) - Runtime (Rust)
- StringBox.length: CP/Byte gate via env `NYASH_STR_CP=1` (default remains byte length; freezesafe) - StringBox.length: CP/Byte gate via env `NYASH_STR_CP=1` (default remains byte length; pausesafe)
- StringBox.indexOf/lastIndexOf: CP gate via env `NYASH_STR_CP=1`既定はByte index; PyVMはCP挙動 - StringBox.indexOf/lastIndexOf: CP gate via env `NYASH_STR_CP=1`既定はByte index; PyVMはCP挙動
Notes / Risks Notes / Risks
- PyVM はプラグイン未連携のため、JsonBox は最小シムで対応仕様不変、既定OFFの機能変更なし。将来的に PyVM→Host ブリッジを導入する場合はデフォルトOFFで段階導入。
- 現在の赤は 2 系統の複合が原因: - 現在の赤は 2 系統の複合が原因:
1) Nyash 側の `||` 連鎖短絡による digit 判定崩れ(→ if チェーン化で解消) 1) Nyash 側の `||` 連鎖短絡による digit 判定崩れ(→ if チェーン化で解消)
2) 同一 Box 内の `me.*` 呼びが PyVM で未解決(→ `__me__` ディスパッチ導入)。 2) 同一 Box 内の `me.*` 呼びが PyVM で未解決(→ `__me__` ディスパッチ導入)。
@ -135,7 +152,7 @@ Notes / Risks
Nyash スクリプトの基本ボックス(標準 libs Nyash スクリプトの基本ボックス(標準 libs
- 既存: `json_cur.nyash`, `string_ext.nyash`, `array_ext.nyash`, `string_builder.nyash`, `test_assert.nyash`, `utf8_cursor.nyash`, `byte_cursor.nyash` - 既存: `json_cur.nyash`, `string_ext.nyash`, `array_ext.nyash`, `string_builder.nyash`, `test_assert.nyash`, `utf8_cursor.nyash`, `byte_cursor.nyash`
- 追加候補(凍結順守: libs 配下・任意採用・互換保持) - 追加候補(機能追加ポーズ遵守: libs 配下・任意採用・互換保持)
- MapExtBoxkeys/values/entries - MapExtBoxkeys/values/entries
- PathBox minidirname/join の最小) - PathBox minidirname/join の最小)
- PrintfExt`StringBuilderBox` 補助) - PrintfExt`StringBuilderBox` 補助)
@ -163,6 +180,17 @@ Pending / Skipped未導入・任意
## 80/20 Plan小粒で高効果 ## 80/20 Plan小粒で高効果
JSON / Plugin v2現状に追記
- [x] v2 resolve→invoke 配線TypeBox.resolve フォールバック + キャッシュ)
- [x] JsonBox methods を nyash.toml に登録
- [x] PyVM 最小シムJsonDoc/JsonNodeを追加
- [x] JSON collect_prints スモーク追加(緑)
- [x] yyjson ベンダリングード操作実装parse/root/get/size/at/str/int/bool
- [x] ランタイム切替env `NYASH_JSON_PROVIDER`)— 既定は serde。yyjson 専用 CI は追加しない。
- [ ] TLV void タグ整合(任意:共通ヘルパへ寄せる)
- [ ] method_id キャッシュの統一化loader 内で Box単位の LRU/Hash で維持)
- [x] MiniVmPrints フォールバックは開発用トグルのみ既定OFF
Checklist更新済み Checklist更新済み
- [x] Selfhost 前展開の固定スモーク 1 本upper_string - [x] Selfhost 前展開の固定スモーク 1 本upper_string
- [x] MacroCtx ctx JSON スモーク 1 本CI 組み込み) - [x] MacroCtx ctx JSON スモーク 1 本CI 組み込み)
@ -205,7 +233,7 @@ Progress
- 前展開: `NYASH_MACRO_SELFHOST_PRE_EXPAND=auto`dev/CI - 前展開: `NYASH_MACRO_SELFHOST_PRE_EXPAND=auto`dev/CI
- テスト: VM/goldens は軽量維持、IR は任意ジョブ - テスト: VM/goldens は軽量維持、IR は任意ジョブ
## PostFreeze BacklogDocs only ## PostBootstrap BacklogDocs only
- Language: Scope reuse blocksdesign — docs/proposals/scope-reuse.md - Language: Scope reuse blocksdesign — docs/proposals/scope-reuse.md
- Language: Flow blocks & `->` pipingdesign — docs/design/flow-blocks.md - Language: Flow blocks & `->` pipingdesign — docs/design/flow-blocks.md
- Guards: Range/CharClass sugarreference — docs/reference/language/match-guards.md - Guards: Range/CharClass sugarreference — docs/reference/language/match-guards.md
@ -229,3 +257,9 @@ Trigger: nyash_vm の安定(主要スモーク緑・自己ホスト経路が
- MiniVmPrints.print_prints_in_slice の binop/compare/int リテラル境界obj_end/p_obj_endを調整 - MiniVmPrints.print_prints_in_slice の binop/compare/int リテラル境界obj_end/p_obj_endを調整
- ArrayBox 経路の size/get 依存を避け、直接 print する経路の安定化を優先 - ArrayBox 経路の size/get 依存を避け、直接 print する経路の安定化を優先
- 再現フラグ(開発のみ): `NYASH_RESOLVE_FIX_BRACES=1 NYASH_PARSER_STATIC_INIT_STRICT=1` - 再現フラグ(開発のみ): `NYASH_RESOLVE_FIX_BRACES=1 NYASH_PARSER_STATIC_INIT_STRICT=1`
### Next Up (JSON line)
- TLV void タグの統一(任意)
- method_id 解決キャッシュの整備・計測
- YYJSON backend のスケルトン追加既定OFF・プラグイン切替可能設計
- JSON smokes をCI最小ゲートへ追加

View File

@ -7,6 +7,8 @@ using selfhost.vm.json as MiniJsonLoader
static box MiniVmPrints { static box MiniVmPrints {
// dev trace flag (0=OFF) // dev trace flag (0=OFF)
_trace_enabled() { return 0 } _trace_enabled() { return 0 }
// fallback toggle for legacy heuristics (0=OFF, 1=ON)
_fallback_enabled() { return 0 }
// literal string within Print // literal string within Print
try_print_string_value_at(json, end, print_pos) { try_print_string_value_at(json, end, print_pos) {
local scan = new MiniVmScan() local scan = new MiniVmScan()
@ -127,39 +129,40 @@ static box MiniVmPrints {
local next_p = scan.index_of_from(json, k_print, p + k_print.length()) local next_p = scan.index_of_from(json, k_print, p + k_print.length())
local p_slice_end = end local p_slice_end = end
if next_p > 0 { p_slice_end = next_p } if next_p > 0 { p_slice_end = next_p }
// dev trace hook (no-op) // 1) BinaryOp fallbacks開発用トグル。既定OFF
// 1) BinaryOp if (new MiniVmPrints()._fallback_enabled() == 1) {
local nextp = bin.try_print_binop_sum_any(json, end, p) local nextp = bin.try_print_binop_sum_any(json, end, p)
if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue } if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue }
nextp = bin.try_print_binop_at(json, end, p) nextp = bin.try_print_binop_at(json, end, p)
if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue } if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue }
nextp = bin.try_print_binop_int_greedy(json, end, p) nextp = bin.try_print_binop_int_greedy(json, end, p)
if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue } if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue }
nextp = bin.try_print_binop_sum_any(json, end, p) nextp = bin.try_print_binop_sum_any(json, end, p)
if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue } if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue }
// Inline typed sum within this Print BinaryOp('+') // Inline typed sum within this Print BinaryOp('+')
{ {
local k_expr = "\"expression\":{" local k_expr = "\"expression\":{"
local epos = scan.index_of_from(json, k_expr, p) local epos = scan.index_of_from(json, k_expr, p)
if epos > 0 { if epos < p_obj_end { if epos > 0 { if epos < p_obj_end {
if scan.index_of_from(json, "\"kind\":\"BinaryOp\"", epos) > 0 { if scan.index_of_from(json, "\"kind\":\"BinaryOp\"", epos) > 0 {
if scan.index_of_from(json, "\"operator\":\"+\"", epos) > 0 { if scan.index_of_from(json, "\"operator\":\"+\"", epos) > 0 {
local k_l = "\"left\":{\"kind\":\"Literal\",\"value\":{\"type\":\"int\",\"value\":" local k_l = "\"left\":{\"kind\":\"Literal\",\"value\":{\"type\":\"int\",\"value\":"
local k_r = "\"right\":{\"kind\":\"Literal\",\"value\":{\"type\":\"int\",\"value\":" local k_r = "\"right\":{\"kind\":\"Literal\",\"value\":{\"type\":\"int\",\"value\":"
local lp = scan.index_of_from(json, k_l, epos) local lp = scan.index_of_from(json, k_l, epos)
if lp > 0 { if lp < p_obj_end { if lp > 0 { if lp < p_obj_end {
local ld = scan.read_digits(json, lp + k_l.length()) local ld = scan.read_digits(json, lp + k_l.length())
if ld != "" { if ld != "" {
local rp = scan.index_of_from(json, k_r, lp + k_l.length()) local rp = scan.index_of_from(json, k_r, lp + k_l.length())
if rp > 0 { if rp < p_obj_end { if rp > 0 { if rp < p_obj_end {
local rd = scan.read_digits(json, rp + k_r.length()) local rd = scan.read_digits(json, rp + k_r.length())
if rd != "" { print(new MiniVmScan()._int_to_str(new MiniVmScan()._str_to_int(ld) + new MiniVmScan()._str_to_int(rd))) printed = printed + 1 pos = p_obj_end + 1 continue } if rd != "" { print(new MiniVmScan()._int_to_str(new MiniVmScan()._str_to_int(ld) + new MiniVmScan()._str_to_int(rd))) printed = printed + 1 pos = p_obj_end + 1 continue }
}} }}
} }
}} }}
}
} }
} }}
}} }
} }
// 2) Compare // 2) Compare
nextp = cmp.try_print_compare_at(json, end, p) nextp = cmp.try_print_compare_at(json, end, p)
@ -173,18 +176,21 @@ static box MiniVmPrints {
// 5) literal int via type // 5) literal int via type
nextp = new MiniVmPrints().try_print_int_value_at(json, end, p) nextp = new MiniVmPrints().try_print_int_value_at(json, end, p)
if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue } if nextp > 0 { printed = printed + 1 pos = p_obj_end + 1 continue }
// 5b) literal int (simple pattern inside current Print object) // 5b) literal int簡易フォールバック既定OFF
{ if (new MiniVmPrints()._fallback_enabled() == 1) {
local ki = "\"type\":\"int\",\"value\":" local ki = "\"type\":\"int\",\"value\":"
local pi = scan.index_of_from(json, ki, p) local pi = scan.index_of_from(json, ki, p)
if pi > 0 { if pi < p_slice_end { if pi > 0 { if pi < p_slice_end {
local digits = scan.read_digits(json, pi + ki.length()) local digits = scan.read_digits(json, pi + ki.length())
if digits != "" { print(digits) printed = printed + 1 pos = p_slice_end continue } if digits != "" { print(digits) printed = printed + 1 pos = p_slice_end continue }
}} }}
}
// Unknown shape: skip this Print object entirely to avoid stalls // Unknown shape: skip this Print object entirely to avoid stalls
pos = p_obj_end + 1 pos = p_obj_end + 1
if pos <= p { pos = p + k_print.length() } if pos <= p { pos = p + k_print.length() }
} else {
// 既定は最小前進(次の探索へ)
pos = p + k_print.length()
}
} }
return printed return printed
} }

View File

@ -0,0 +1,53 @@
static box Main {
main(args) {
// JSON v0 Program with mixed Print expressions
local json = "{\"kind\":\"Program\",\"statements\":[{\"kind\":\"Print\",\"expression\":{\"kind\":\"Literal\",\"value\":{\"type\":\"string\",\"value\":\"A\"}}},{\"kind\":\"Print\",\"expression\":{\"kind\":\"FunctionCall\",\"name\":\"echo\",\"arguments\":[{\"kind\":\"Literal\",\"value\":{\"type\":\"string\",\"value\":\"B\"}}]}},{\"kind\":\"Print\",\"expression\":{\"kind\":\"FunctionCall\",\"name\":\"itoa\",\"arguments\":[{\"kind\":\"Literal\",\"value\":{\"type\":\"int\",\"value\":7}}]}},{\"kind\":\"Print\",\"expression\":{\"kind\":\"Compare\",\"operation\":\"<\",\"lhs\":{\"kind\":\"Literal\",\"value\":{\"type\":\"int\",\"value\":1}},\"rhs\":{\"kind\":\"Literal\",\"value\":{\"type\":\"int\",\"value\":2}}}},{\"kind\":\"Print\",\"expression\":{\"kind\":\"BinaryOp\",\"operator\":\"+\",\"left\":{\"kind\":\"Literal\",\"value\":{\"type\":\"int\",\"value\":3}},\"right\":{\"kind\":\"Literal\",\"value\":{\"type\":\"int\",\"value\":4}}}},{\"kind\":\"Print\",\"expression\":{\"kind\":\"Literal\",\"value\":{\"type\":\"int\",\"value\":5}}}]}"
local doc = new JsonDocBox()
doc.parse(json)
local root = doc.root()
local stmts = root.get("statements")
local n = stmts.size()
local i = 0
loop (i < n) {
local node = stmts.at(i)
local expr = node.get("expression")
local kind = expr.get("kind").str()
if kind == "Literal" {
local val = expr.get("value")
local ty = val.get("type").str()
if ty == "string" { print(val.get("value").str()) } else { print(val.get("value").int()) }
} else if kind == "FunctionCall" {
local name = expr.get("name").str()
// First argument as typed literal object { type, value }
local arg0 = expr.get("arguments").at(0).get("value")
if name == "echo" {
if arg0.get("type").str() == "string" { print(arg0.get("value").str()) } else { print(arg0.get("value").int()) }
}
if name == "itoa" { print(arg0.get("value").int()) }
} else if kind == "Compare" {
local op = expr.get("operation").str()
local lhs = expr.get("lhs").get("value").get("value").int()
local rhs = expr.get("rhs").get("value").get("value").int()
if op == "<" {
if lhs < rhs { print(1) } else { print(0) }
}
if op == "==" {
if lhs == rhs { print(1) } else { print(0) }
}
if op == ">" {
if lhs > rhs { print(1) } else { print(0) }
}
} else if kind == "BinaryOp" {
local op = expr.get("operator").str()
if op == "+" {
local left = expr.get("left").get("value").get("value").int()
local right = expr.get("right").get("value").get("value").int()
print(left + right)
}
}
i = i + 1
}
return 0
}
}

View File

@ -0,0 +1,16 @@
static box Main {
main(args) {
local json = "{\"a\":{\"n\":7,\"s\":\"B\"},\"arr\":[1,2,3]}"
local doc = new JsonDocBox()
doc.parse(json)
local root = doc.root()
// prints: B, 7, 3, 2
print(root.get("a").get("s").str())
print(root.get("a").get("n").int())
local arr = root.get("arr")
print(arr.size())
print(arr.at(1).int())
return 0
}
}

View File

@ -0,0 +1,12 @@
static box Main {
main(args) {
// invalid JSON (missing value after kind)
local json = "{\"kind\": }"
local doc = new JsonDocBox()
doc.parse(json)
local err = doc.error()
if err == "" { print("OK") } else { print("ERR") }
return 0
}
}

View File

@ -0,0 +1,11 @@
static box Main {
main(args) {
local json = "{\"kind\":\"Program\",\"statements\":[]}"
local doc = new JsonDocBox()
doc.parse(json)
local root = doc.root()
print(root.get("kind").str())
return 0
}
}

View File

@ -0,0 +1,12 @@
static box Main {
main(args) {
local json = "{\"kind\":\"Program\",\"statements\":[]}"
local doc = new JsonDocBox()
doc.parse(json)
// print parse error (if any) for debugging
print(doc.error())
local root = doc.root()
print(root.get("kind").str())
return 0
}
}

View File

@ -1,5 +1,9 @@
[env] [env]
# Put environment defaults used by tools or examples if needed # Put environment defaults used by tools or examples if needed
# Enable using resolver by default (can be disabled with NYASH_SKIP_TOML_ENV=1)
NYASH_ENABLE_USING = "1"
# Enable dev sugar preexpand for @ local alias (line-head) during parsing
NYASH_DEV_AT_LOCAL = "1"
[using] [using]
paths = ["apps", "lib", "."] paths = ["apps", "lib", "."]
@ -17,6 +21,7 @@ selfhost.vm.scan = "apps/selfhost-vm/boxes/mini_vm_scan.nyash"
selfhost.vm.binop = "apps/selfhost-vm/boxes/mini_vm_binop.nyash" selfhost.vm.binop = "apps/selfhost-vm/boxes/mini_vm_binop.nyash"
selfhost.vm.compare = "apps/selfhost-vm/boxes/mini_vm_compare.nyash" selfhost.vm.compare = "apps/selfhost-vm/boxes/mini_vm_compare.nyash"
selfhost.vm.prints = "apps/selfhost-vm/boxes/mini_vm_prints.nyash" selfhost.vm.prints = "apps/selfhost-vm/boxes/mini_vm_prints.nyash"
selfhost.vm.seam = "apps/selfhost-vm/boxes/seam_inspector.nyash"
# v2 Plugin libraries (loader reads these for TypeBox ABI) # v2 Plugin libraries (loader reads these for TypeBox ABI)
[libraries] [libraries]
@ -213,6 +218,38 @@ hexEncode = { method_id = 5 }
hexDecode = { method_id = 6 } hexDecode = { method_id = 6 }
fini = { method_id = 4294967295 } fini = { method_id = 4294967295 }
[libraries."libnyash_json_plugin.so"]
boxes = ["JsonDocBox", "JsonNodeBox"]
path = "plugins/nyash-json-plugin/target/release/libnyash_json_plugin.so"
[libraries."libnyash_json_plugin.so".JsonDocBox]
type_id = 70
abi_version = 1
singleton = false
[libraries."libnyash_json_plugin.so".JsonDocBox.methods]
birth = { method_id = 0 }
parse = { method_id = 1 }
root = { method_id = 2 }
error = { method_id = 3 }
fini = { method_id = 4294967295 }
[libraries."libnyash_json_plugin.so".JsonNodeBox]
type_id = 71
abi_version = 1
singleton = false
[libraries."libnyash_json_plugin.so".JsonNodeBox.methods]
birth = { method_id = 0 }
kind = { method_id = 1 }
get = { method_id = 2 }
size = { method_id = 3 }
at = { method_id = 4 }
str = { method_id = 5 }
int = { method_id = 6 }
bool = { method_id = 7 }
fini = { method_id = 4294967295 }
[libraries."libnyash_toml_plugin.so"] [libraries."libnyash_toml_plugin.so"]
boxes = ["TOMLBox"] boxes = ["TOMLBox"]
path = "plugins/nyash-toml-plugin/target/release/libnyash_toml_plugin.so" path = "plugins/nyash-toml-plugin/target/release/libnyash_toml_plugin.so"

View File

@ -0,0 +1,19 @@
[package]
name = "nyash-json-plugin"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "staticlib"]
[dependencies]
once_cell = "1.20"
serde_json = "1.0"
[build-dependencies]
cc = "1.0"
[profile.release]
lto = true
strip = true
opt-level = "z"

View File

@ -0,0 +1,24 @@
fn main() {
// Build vendored C shim for yyjson provider (skeleton).
// This keeps linkage ready without introducing external deps.
let shim = "c/yyjson_shim.c";
let yyjson_c = "c/yyjson/yyjson.c";
let mut b = cc::Build::new();
let mut need = false;
if std::path::Path::new(yyjson_c).exists() {
b.file(yyjson_c);
println!("cargo:rerun-if-changed={}", yyjson_c);
need = true;
}
if std::path::Path::new(shim).exists() {
b.file(shim);
println!("cargo:rerun-if-changed={}", shim);
need = true;
}
if need {
b.include("c")
.include("c/yyjson")
.warnings(false)
.compile("yyjson_shim");
}
}

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 YaoYuan <ibireme@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,74 @@
// Minimal shim to keep build/link path ready for a future yyjson backend.
// This file does not currently implement parsing; the Rust side still
// uses serde_json for actual parsing until the real yyjson integration lands.
#include <stdint.h>
#include <stddef.h>
#include "yyjson/yyjson.h"
// Parse JSON via yyjson and return 0 on success; non-zero error code on failure.
int nyash_json_shim_parse(const char *text, size_t len) {
yyjson_read_err err;
yyjson_doc *doc = yyjson_read_opts(text, len, 0, NULL, &err);
if (!doc) return (int)err.code;
yyjson_doc_free(doc);
return 0;
}
// Full wrappers exported with stable names for Rust FFI
void *nyjson_parse_doc(const char *text, size_t len, int *out_err_code) {
yyjson_read_err err;
yyjson_doc *doc = yyjson_read_opts(text, len, 0, NULL, &err);
if (!doc) {
if (out_err_code) *out_err_code = (int)err.code;
return NULL;
}
if (out_err_code) *out_err_code = 0;
return (void *)doc;
}
void nyjson_doc_free(void *doc) {
if (doc) yyjson_doc_free((yyjson_doc *)doc);
}
void *nyjson_doc_root(void *doc) {
if (!doc) return NULL;
return (void *)yyjson_doc_get_root((yyjson_doc *)doc);
}
int nyjson_is_null(void *v) { return v && yyjson_is_null((yyjson_val *)v); }
int nyjson_is_bool(void *v) { return v && yyjson_is_bool((yyjson_val *)v); }
int nyjson_is_int(void *v) { return v && yyjson_is_sint((yyjson_val *)v); }
int nyjson_is_real(void *v) { return v && yyjson_is_real((yyjson_val *)v); }
int nyjson_is_str(void *v) { return v && yyjson_is_str((yyjson_val *)v); }
int nyjson_is_arr(void *v) { return v && yyjson_is_arr((yyjson_val *)v); }
int nyjson_is_obj(void *v) { return v && yyjson_is_obj((yyjson_val *)v); }
int nyjson_get_bool_val(void *v) {
if (!v || !yyjson_is_bool((yyjson_val *)v)) return 0;
return yyjson_get_bool((yyjson_val *)v);
}
long long nyjson_get_sint_val(void *v) {
if (!v || !yyjson_is_sint((yyjson_val *)v)) return 0;
return (long long)yyjson_get_sint((yyjson_val *)v);
}
const char *nyjson_get_str_val(void *v) {
if (!v || !yyjson_is_str((yyjson_val *)v)) return NULL;
return yyjson_get_str((yyjson_val *)v);
}
size_t nyjson_arr_size_val(void *v) {
if (!v || !yyjson_is_arr((yyjson_val *)v)) return 0;
return yyjson_arr_size((yyjson_val *)v);
}
void *nyjson_arr_get_val(void *v, size_t idx) {
if (!v || !yyjson_is_arr((yyjson_val *)v)) return NULL;
return (void *)yyjson_arr_get((yyjson_val *)v, idx);
}
size_t nyjson_obj_size_val(void *v) {
if (!v || !yyjson_is_obj((yyjson_val *)v)) return 0;
return yyjson_obj_size((yyjson_val *)v);
}
void *nyjson_obj_get_key(void *v, const char *key) {
if (!v || !yyjson_is_obj((yyjson_val *)v) || !key) return NULL;
return (void *)yyjson_obj_get((yyjson_val *)v, key);
}

View File

@ -0,0 +1,81 @@
[box]
name = "JsonPlugin"
version = "0.1.0"
description = "JSON parsing and traversal via Nyash TypeBox v2 (serde_json backend)"
author = "Nyash Team"
[provides]
boxes = ["JsonDocBox", "JsonNodeBox"]
[JsonDocBox]
type_id = 70
[JsonDocBox.lifecycle]
birth = { id = 0 }
fini = { id = 4294967295 }
[JsonDocBox.methods.parse]
id = 1
args = [{ type = "string" }]
returns = { type = "void" }
[JsonDocBox.methods.root]
id = 2
args = []
returns = { type = "handle", box = "JsonNodeBox" }
[JsonDocBox.methods.error]
id = 3
args = []
returns = { type = "string" }
[JsonNodeBox]
type_id = 71
[JsonNodeBox.lifecycle]
birth = { id = 0 }
fini = { id = 4294967295 }
[JsonNodeBox.methods.kind]
id = 1
args = []
returns = { type = "string" }
[JsonNodeBox.methods.get]
id = 2
args = [{ type = "string" }]
returns = { type = "handle", box = "JsonNodeBox" }
[JsonNodeBox.methods.size]
id = 3
args = []
returns = { type = "i64" }
[JsonNodeBox.methods.at]
id = 4
args = [{ type = "i64" }]
returns = { type = "handle", box = "JsonNodeBox" }
[JsonNodeBox.methods.str]
id = 5
args = []
returns = { type = "string" }
[JsonNodeBox.methods.int]
id = 6
args = []
returns = { type = "i64" }
[JsonNodeBox.methods.bool]
id = 7
args = []
returns = { type = "bool" }
[implementation]
ffi_version = 1
thread_safe = true
[artifacts]
linux = "target/release/libnyash_json_plugin.so"
macos = "target/release/libnyash_json_plugin.dylib"

View File

@ -0,0 +1,570 @@
//! Nyash JSON Plugin — TypeBox v2 (serde_json backend for bring-up)
//! Provides JsonDocBox / JsonNodeBox to parse and traverse JSON safely.
use once_cell::sync::Lazy;
use serde_json::Value;
use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_void};
use std::sync::{
atomic::{AtomicU32, Ordering},
Arc, Mutex,
};
// ---- Result codes ----
const OK: i32 = 0;
const E_SHORT: i32 = -1;
const E_TYPE: i32 = -2;
const E_METHOD: i32 = -3;
const E_ARGS: i32 = -4;
const E_PLUGIN: i32 = -5;
const E_HANDLE: i32 = -8;
// ---- Method IDs ----
// JsonDocBox
const JD_BIRTH: u32 = 0;
const JD_PARSE: u32 = 1;
const JD_ROOT: u32 = 2;
const JD_ERROR: u32 = 3;
const JD_FINI: u32 = u32::MAX;
// JsonNodeBox
const JN_BIRTH: u32 = 0;
const JN_KIND: u32 = 1;
const JN_GET: u32 = 2;
const JN_SIZE: u32 = 3;
const JN_AT: u32 = 4;
const JN_STR: u32 = 5;
const JN_INT: u32 = 6;
const JN_BOOL: u32 = 7;
const JN_FINI: u32 = u32::MAX;
// ---- Type IDs (for Handle TLV) ----
const T_JSON_DOC: u32 = 70;
const T_JSON_NODE: u32 = 71;
// ---- Instances ----
#[derive(Clone)]
enum NodeRep {
Serde(Arc<Value>),
Yy { doc_id: u32, ptr: usize },
}
struct DocInst {
root: Option<Arc<Value>>, // Serde provider
doc_ptr: Option<usize>, // Yyjson provider (opaque pointer value)
last_err: Option<String>,
}
static DOCS: Lazy<Mutex<HashMap<u32, DocInst>>> = Lazy::new(|| Mutex::new(HashMap::new()));
static NODES: Lazy<Mutex<HashMap<u32, NodeRep>>> = Lazy::new(|| Mutex::new(HashMap::new()));
static NEXT_ID: AtomicU32 = AtomicU32::new(1);
// ---- TypeBox v2 FFI ----
#[repr(C)]
pub struct NyashTypeBoxFfi {
pub abi_tag: u32, // 'TYBX'
pub version: u16, // 1
pub struct_size: u16, // sizeof(NyashTypeBoxFfi)
pub name: *const c_char, // C string
pub resolve: Option<extern "C" fn(*const c_char) -> u32>,
pub invoke_id: Option<extern "C" fn(u32, u32, *const u8, usize, *mut u8, *mut usize) -> i32>,
pub capabilities: u64,
}
unsafe impl Sync for NyashTypeBoxFfi {}
// ---- JsonDocBox ----
extern "C" fn jsondoc_resolve(name: *const c_char) -> u32 {
if name.is_null() {
return 0;
}
let s = unsafe { CStr::from_ptr(name) }.to_string_lossy();
match s.as_ref() {
"birth" => JD_BIRTH,
"parse" => JD_PARSE,
"root" => JD_ROOT,
"error" => JD_ERROR,
_ => 0,
}
}
// Provider selection (serde_json vs yyjson skeleton)
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum ProviderKind {
Serde,
Yyjson,
}
fn provider_kind() -> ProviderKind {
match std::env::var("NYASH_JSON_PROVIDER").ok().as_deref() {
Some("yyjson") | Some("YYJSON") => ProviderKind::Yyjson,
_ => ProviderKind::Serde,
}
}
fn provider_parse(text: &str) -> Result<Value, String> {
match provider_kind() {
ProviderKind::Serde => serde_json::from_str::<Value>(text).map_err(|e| e.to_string()),
ProviderKind::Yyjson => {
// Skeleton phase: call into C shim to validate linkage, then fallback to serde_json
unsafe {
if let Ok(c) = CString::new(text.as_bytes()) {
let _ = nyash_json_shim_parse(c.as_ptr(), text.len());
}
}
serde_json::from_str::<Value>(text).map_err(|e| e.to_string())
}
}
}
extern "C" {
fn nyash_json_shim_parse(text: *const std::os::raw::c_char, len: usize) -> i32;
fn nyjson_parse_doc(text: *const c_char, len: usize, out_err_code: *mut i32) -> *mut c_void;
fn nyjson_doc_free(doc: *mut c_void);
fn nyjson_doc_root(doc: *mut c_void) -> *mut c_void;
fn nyjson_is_null(v: *mut c_void) -> i32;
fn nyjson_is_bool(v: *mut c_void) -> i32;
fn nyjson_is_int(v: *mut c_void) -> i32;
fn nyjson_is_real(v: *mut c_void) -> i32;
fn nyjson_is_str(v: *mut c_void) -> i32;
fn nyjson_is_arr(v: *mut c_void) -> i32;
fn nyjson_is_obj(v: *mut c_void) -> i32;
fn nyjson_get_bool_val(v: *mut c_void) -> i32;
fn nyjson_get_sint_val(v: *mut c_void) -> i64;
fn nyjson_get_str_val(v: *mut c_void) -> *const c_char;
fn nyjson_arr_size_val(v: *mut c_void) -> usize;
fn nyjson_arr_get_val(v: *mut c_void, idx: usize) -> *mut c_void;
fn nyjson_obj_size_val(v: *mut c_void) -> usize;
fn nyjson_obj_get_key(v: *mut c_void, key: *const c_char) -> *mut c_void;
}
extern "C" fn jsondoc_invoke_id(
instance_id: u32,
method_id: u32,
args: *const u8,
args_len: usize,
result: *mut u8,
result_len: *mut usize,
) -> i32 {
unsafe {
match method_id {
JD_BIRTH => {
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
if let Ok(mut m) = DOCS.lock() {
m.insert(id, DocInst { root: None, doc_ptr: None, last_err: None });
} else {
return E_PLUGIN;
}
return write_u32(id, result, result_len);
}
JD_PARSE => {
let text = match read_arg_string(args, args_len, 0) {
Some(s) => s,
None => return E_ARGS,
};
if let Ok(mut m) = DOCS.lock() {
if let Some(doc) = m.get_mut(&instance_id) {
match provider_kind() {
ProviderKind::Serde => {
match provider_parse(&text) {
Ok(v) => { doc.root = Some(Arc::new(v)); doc.doc_ptr = None; doc.last_err = None; }
Err(e) => { doc.root = None; doc.doc_ptr = None; doc.last_err = Some(e.to_string()); }
}
return write_tlv_void(result, result_len);
}
ProviderKind::Yyjson => {
let c = CString::new(text.as_bytes()).unwrap_or_default();
let mut ec: i32 = -1;
let p = nyjson_parse_doc(c.as_ptr(), text.len(), &mut ec as *mut i32);
if p.is_null() {
doc.root = None; doc.doc_ptr = None; doc.last_err = Some(format!("E{}", ec));
} else {
doc.root = None; doc.doc_ptr = Some(p as usize); doc.last_err = None;
}
return write_tlv_void(result, result_len);
}
}
} else {
return E_HANDLE;
}
} else {
return E_PLUGIN;
}
}
JD_ROOT => {
if let Ok(m) = DOCS.lock() {
if let Some(doc) = m.get(&instance_id) {
match provider_kind() {
ProviderKind::Serde => {
if let Some(root_arc) = doc.root.as_ref().map(|r| Arc::clone(r)) {
let node_id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
if let Ok(mut nn) = NODES.lock() { nn.insert(node_id, NodeRep::Serde(root_arc)); }
return write_tlv_handle(T_JSON_NODE, node_id, result, result_len);
}
return E_PLUGIN;
}
ProviderKind::Yyjson => {
if let Some(dp) = doc.doc_ptr {
let vp = nyjson_doc_root(dp as *mut c_void);
let node_id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
if let Ok(mut nn) = NODES.lock() { nn.insert(node_id, NodeRep::Yy { doc_id: instance_id, ptr: vp as usize }); }
return write_tlv_handle(T_JSON_NODE, node_id, result, result_len);
}
return E_PLUGIN;
}
}
}
}
return E_PLUGIN;
}
JD_ERROR => {
if let Ok(m) = DOCS.lock() {
if let Some(doc) = m.get(&instance_id) {
let s = doc.last_err.clone().unwrap_or_default();
return write_tlv_string(&s, result, result_len);
} else {
return E_HANDLE;
}
} else {
return E_PLUGIN;
}
}
JD_FINI => {
if let Ok(mut m) = DOCS.lock() {
if let Some(mut di) = m.remove(&instance_id) {
if let Some(dp) = di.doc_ptr.take() { nyjson_doc_free(dp as *mut c_void); }
}
}
return write_tlv_void(result, result_len);
}
_ => E_METHOD,
}
}
}
#[no_mangle]
pub static nyash_typebox_JsonDocBox: NyashTypeBoxFfi = NyashTypeBoxFfi {
abi_tag: 0x54594258, // 'TYBX'
version: 1,
struct_size: std::mem::size_of::<NyashTypeBoxFfi>() as u16,
name: b"JsonDocBox\0".as_ptr() as *const c_char,
resolve: Some(jsondoc_resolve),
invoke_id: Some(jsondoc_invoke_id),
capabilities: 0,
};
// ---- JsonNodeBox ----
extern "C" fn jsonnode_resolve(name: *const c_char) -> u32 {
if name.is_null() {
return 0;
}
let s = unsafe { CStr::from_ptr(name) }.to_string_lossy();
match s.as_ref() {
"birth" => JN_BIRTH,
"kind" => JN_KIND,
"get" => JN_GET,
"size" => JN_SIZE,
"at" => JN_AT,
"str" => JN_STR,
"int" => JN_INT,
"bool" => JN_BOOL,
_ => 0,
}
}
extern "C" fn jsonnode_invoke_id(
instance_id: u32,
method_id: u32,
args: *const u8,
args_len: usize,
result: *mut u8,
result_len: *mut usize,
) -> i32 {
unsafe {
let node_rep = match NODES.lock() {
Ok(m) => match m.get(&instance_id) { Some(v) => v.clone(), None => return E_HANDLE },
Err(_) => return E_PLUGIN,
};
match method_id {
JN_BIRTH => {
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
if let Ok(mut m) = NODES.lock() { m.insert(id, NodeRep::Serde(Arc::new(Value::Null))); } else { return E_PLUGIN; }
return write_u32(id, result, result_len);
}
JN_KIND => {
match provider_kind() {
ProviderKind::Serde => {
let k = match node_rep { NodeRep::Serde(ref a) => match &**a {
Value::Null => "null",
Value::Bool(_) => "bool",
Value::Number(n) => { if n.is_i64() { "int" } else { "real" } },
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}, _ => "null"};
write_tlv_string(k, result, result_len)
}
ProviderKind::Yyjson => {
let v = if let NodeRep::Yy { ptr, .. } = node_rep { ptr as *mut c_void } else { std::ptr::null_mut() };
let k = if v.is_null() { "null" }
else if nyjson_is_obj(v) != 0 { "object" }
else if nyjson_is_arr(v) != 0 { "array" }
else if nyjson_is_str(v) != 0 { "string" }
else if nyjson_is_int(v) != 0 { "int" }
else if nyjson_is_real(v) != 0 { "real" }
else if nyjson_is_bool(v) != 0 { "bool" }
else { "null" };
write_tlv_string(k, result, result_len)
}
}
}
JN_GET => {
let key = match read_arg_string(args, args_len, 0) { Some(s) => s, None => return E_ARGS };
match provider_kind() {
ProviderKind::Serde => {
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
if let NodeRep::Serde(ref a) = node_rep {
if let Value::Object(map) = &**a {
if let Some(child) = map.get(&key) {
if let Ok(mut mm) = NODES.lock() { mm.insert(id, NodeRep::Serde(Arc::new(child.clone()))); }
return write_tlv_handle(T_JSON_NODE, id, result, result_len);
}
}
}
if let Ok(mut mm) = NODES.lock() { mm.insert(id, NodeRep::Serde(Arc::new(Value::Null))); }
write_tlv_handle(T_JSON_NODE, id, result, result_len)
}
ProviderKind::Yyjson => {
let v = if let NodeRep::Yy { ptr, .. } = node_rep { ptr as *mut c_void } else { std::ptr::null_mut() };
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
let mut out_ptr: *mut c_void = std::ptr::null_mut();
if !v.is_null() && nyjson_is_obj(v) != 0 {
let c = CString::new(key).unwrap_or_default();
out_ptr = nyjson_obj_get_key(v, c.as_ptr());
}
let doc_id = if let NodeRep::Yy { doc_id, .. } = node_rep { doc_id } else { 0 };
let rep = if out_ptr.is_null() {
NodeRep::Yy { doc_id, ptr: 0 }
} else {
NodeRep::Yy { doc_id, ptr: out_ptr as usize }
};
if let Ok(mut mm) = NODES.lock() { mm.insert(id, rep); }
write_tlv_handle(T_JSON_NODE, id, result, result_len)
}
}
}
JN_SIZE => {
match provider_kind() {
ProviderKind::Serde => {
let n = match node_rep { NodeRep::Serde(ref a) => match &**a { Value::Array(a) => a.len() as i64, Value::Object(o) => o.len() as i64, _ => 0 }, _ => 0 };
write_tlv_i64(n, result, result_len)
}
ProviderKind::Yyjson => {
let v = if let NodeRep::Yy { ptr, .. } = node_rep { ptr as *mut c_void } else { std::ptr::null_mut() };
let n = if !v.is_null() { if nyjson_is_arr(v) != 0 { nyjson_arr_size_val(v) as i64 } else if nyjson_is_obj(v) != 0 { nyjson_obj_size_val(v) as i64 } else { 0 } } else { 0 };
write_tlv_i64(n, result, result_len)
}
}
}
JN_AT => {
let idx = match read_arg_i64(args, args_len, 0) { Some(v) => v, None => return E_ARGS };
if idx < 0 { return E_ARGS; }
match provider_kind() {
ProviderKind::Serde => {
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
if let NodeRep::Serde(ref a) = node_rep {
if let Value::Array(arr) = &**a {
let i = idx as usize;
if i < arr.len() { if let Ok(mut mm) = NODES.lock() { mm.insert(id, NodeRep::Serde(Arc::new(arr[i].clone()))); } return write_tlv_handle(T_JSON_NODE, id, result, result_len); }
}
}
if let Ok(mut mm) = NODES.lock() { mm.insert(id, NodeRep::Serde(Arc::new(Value::Null))); }
write_tlv_handle(T_JSON_NODE, id, result, result_len)
}
ProviderKind::Yyjson => {
let v = if let NodeRep::Yy { ptr, .. } = node_rep { ptr as *mut c_void } else { std::ptr::null_mut() };
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
let mut child: *mut c_void = std::ptr::null_mut();
if !v.is_null() && nyjson_is_arr(v) != 0 { child = nyjson_arr_get_val(v, idx as usize); }
let doc_id = if let NodeRep::Yy { doc_id, .. } = node_rep { doc_id } else { 0 };
let rep = if child.is_null() {
NodeRep::Yy { doc_id, ptr: 0 }
} else {
NodeRep::Yy { doc_id, ptr: child as usize }
};
if let Ok(mut mm) = NODES.lock() { mm.insert(id, rep); }
write_tlv_handle(T_JSON_NODE, id, result, result_len)
}
}
}
JN_STR => match provider_kind() {
ProviderKind::Serde => {
if let NodeRep::Serde(ref a) = node_rep { match &**a { Value::String(s) => write_tlv_string(s, result, result_len), Value::Object(o) => { if let Some(Value::String(s)) = o.get("value") { write_tlv_string(s, result, result_len) } else { write_tlv_string("", result, result_len) } }, _ => write_tlv_string("", result, result_len) } } else { write_tlv_string("", result, result_len) }
}
ProviderKind::Yyjson => {
let v = if let NodeRep::Yy { ptr, .. } = node_rep { ptr as *mut c_void } else { std::ptr::null_mut() };
if !v.is_null() && nyjson_is_str(v) != 0 { let s = nyjson_get_str_val(v); if s.is_null() { write_tlv_string("", result, result_len) } else { let rs = CStr::from_ptr(s).to_string_lossy().to_string(); write_tlv_string(&rs, result, result_len) } }
else if !v.is_null() && nyjson_is_obj(v) != 0 { let key = CString::new("value").unwrap(); let child = nyjson_obj_get_key(v, key.as_ptr()); if !child.is_null() && nyjson_is_str(child) != 0 { let s = nyjson_get_str_val(child); if s.is_null() { write_tlv_string("", result, result_len) } else { let rs = CStr::from_ptr(s).to_string_lossy().to_string(); write_tlv_string(&rs, result, result_len) } } else { write_tlv_string("", result, result_len) } }
else { write_tlv_string("", result, result_len) }
}
},
JN_INT => match provider_kind() {
ProviderKind::Serde => {
if let NodeRep::Serde(ref a) = node_rep { match &**a { Value::Number(n) => write_tlv_i64(n.as_i64().unwrap_or(0), result, result_len), Value::Object(o) => { if let Some(Value::Number(n)) = o.get("value") { write_tlv_i64(n.as_i64().unwrap_or(0), result, result_len) } else { write_tlv_i64(0, result, result_len) } }, _ => write_tlv_i64(0, result, result_len) } } else { write_tlv_i64(0, result, result_len) }
}
ProviderKind::Yyjson => {
let v = if let NodeRep::Yy { ptr, .. } = node_rep { ptr as *mut c_void } else { std::ptr::null_mut() };
if !v.is_null() && nyjson_is_int(v) != 0 { write_tlv_i64(nyjson_get_sint_val(v) as i64, result, result_len) }
else if !v.is_null() && nyjson_is_obj(v) != 0 { let key = CString::new("value").unwrap(); let child = nyjson_obj_get_key(v, key.as_ptr()); if !child.is_null() && nyjson_is_int(child) != 0 { write_tlv_i64(nyjson_get_sint_val(child) as i64, result, result_len) } else { write_tlv_i64(0, result, result_len) } }
else { write_tlv_i64(0, result, result_len) }
}
},
JN_BOOL => match provider_kind() {
ProviderKind::Serde => { if let NodeRep::Serde(ref a) = node_rep { if let Value::Bool(b) = **a { write_tlv_bool(b, result, result_len) } else { write_tlv_bool(false, result, result_len) } } else { write_tlv_bool(false, result, result_len) } }
ProviderKind::Yyjson => { let v = if let NodeRep::Yy { ptr, .. } = node_rep { ptr as *mut c_void } else { std::ptr::null_mut() }; if !v.is_null() && nyjson_is_bool(v) != 0 { write_tlv_bool(nyjson_get_bool_val(v) != 0, result, result_len) } else { write_tlv_bool(false, result, result_len) } }
},
_ => E_METHOD,
}
}
}
#[no_mangle]
pub static nyash_typebox_JsonNodeBox: NyashTypeBoxFfi = NyashTypeBoxFfi {
abi_tag: 0x54594258, // 'TYBX'
version: 1,
struct_size: std::mem::size_of::<NyashTypeBoxFfi>() as u16,
name: b"JsonNodeBox\0".as_ptr() as *const c_char,
resolve: Some(jsonnode_resolve),
invoke_id: Some(jsonnode_invoke_id),
capabilities: 0,
};
// ---- TLV helpers (copied/minimized) ----
fn preflight(result: *mut u8, result_len: *mut usize, needed: usize) -> bool {
unsafe {
if result_len.is_null() {
return false;
}
if result.is_null() || *result_len < needed {
*result_len = needed;
return true;
}
}
false
}
fn write_tlv_result(payloads: &[(u8, &[u8])], result: *mut u8, result_len: *mut usize) -> i32 {
if result_len.is_null() {
return E_ARGS;
}
let mut buf: Vec<u8> =
Vec::with_capacity(4 + payloads.iter().map(|(_, p)| 4 + p.len()).sum::<usize>());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&(payloads.len() as u16).to_le_bytes());
for (tag, payload) in payloads {
buf.push(*tag);
buf.push(0);
buf.extend_from_slice(&(payload.len() as u16).to_le_bytes());
buf.extend_from_slice(payload);
}
unsafe {
let needed = buf.len();
if result.is_null() || *result_len < needed {
*result_len = needed;
return E_SHORT;
}
std::ptr::copy_nonoverlapping(buf.as_ptr(), result, needed);
*result_len = needed;
}
OK
}
fn write_u32(v: u32, result: *mut u8, result_len: *mut usize) -> i32 {
if result_len.is_null() {
return E_ARGS;
}
unsafe {
if result.is_null() || *result_len < 4 {
*result_len = 4;
return E_SHORT;
}
let b = v.to_le_bytes();
std::ptr::copy_nonoverlapping(b.as_ptr(), result, 4);
*result_len = 4;
}
OK
}
fn write_tlv_void(result: *mut u8, result_len: *mut usize) -> i32 {
// Align with common helpers: use tag=9 for void/host-handle-like empty
write_tlv_result(&[(9u8, &[])], result, result_len)
}
fn write_tlv_i64(v: i64, result: *mut u8, result_len: *mut usize) -> i32 {
write_tlv_result(&[(3u8, &v.to_le_bytes())], result, result_len)
}
fn write_tlv_bool(v: bool, result: *mut u8, result_len: *mut usize) -> i32 {
write_tlv_result(&[(1u8, &[if v { 1u8 } else { 0u8 }])], result, result_len)
}
fn write_tlv_handle(
type_id: u32,
instance_id: u32,
result: *mut u8,
result_len: *mut usize,
) -> i32 {
let mut payload = Vec::with_capacity(8);
payload.extend_from_slice(&type_id.to_le_bytes());
payload.extend_from_slice(&instance_id.to_le_bytes());
write_tlv_result(&[(8u8, &payload)], result, result_len)
}
fn write_tlv_string(s: &str, result: *mut u8, result_len: *mut usize) -> i32 {
write_tlv_result(&[(6u8, s.as_bytes())], result, result_len)
}
fn read_arg_string(args: *const u8, args_len: usize, n: usize) -> Option<String> {
if args.is_null() || args_len < 4 {
return None;
}
let buf = unsafe { std::slice::from_raw_parts(args, args_len) };
let mut off = 4usize;
for i in 0..=n {
if buf.len() < off + 4 {
return None;
}
let tag = buf[off];
let size = u16::from_le_bytes([buf[off + 2], buf[off + 3]]) as usize;
if buf.len() < off + 4 + size {
return None;
}
if i == n {
if tag != 6 {
return None;
}
let s = String::from_utf8_lossy(&buf[off + 4..off + 4 + size]).to_string();
return Some(s);
}
off += 4 + size;
}
None
}
fn read_arg_i64(args: *const u8, args_len: usize, n: usize) -> Option<i64> {
if args.is_null() || args_len < 4 {
return None;
}
let buf = unsafe { std::slice::from_raw_parts(args, args_len) };
let mut off = 4usize;
for i in 0..=n {
if buf.len() < off + 4 {
return None;
}
let tag = buf[off];
let size = u16::from_le_bytes([buf[off + 2], buf[off + 3]]) as usize;
if buf.len() < off + 4 + size {
return None;
}
if i == n {
if tag != 3 || size != 8 {
return None;
}
let mut b = [0u8; 8];
b.copy_from_slice(&buf[off + 4..off + 12]);
return Some(i64::from_le_bytes(b));
}
off += 4 + size;
}
None
}

View File

@ -22,6 +22,12 @@ def op_newbox(owner, inst: Dict[str, Any], regs: Dict[int, Any]) -> None:
val = {"__box__": "ArrayBox", "__arr": []} val = {"__box__": "ArrayBox", "__arr": []}
elif btype == "MapBox": elif btype == "MapBox":
val = {"__box__": "MapBox", "__map": {}} val = {"__box__": "MapBox", "__map": {}}
elif btype == "JsonDocBox":
# Minimal JsonDocBox (PyVM-only): stores parsed JSON and last error
val = {"__box__": "JsonDocBox", "__doc": None, "__err": None}
elif btype == "JsonNodeBox":
# Minimal JsonNodeBox with empty Null node
val = {"__box__": "JsonNodeBox", "__node": None}
else: else:
# Unknown box -> opaque # Unknown box -> opaque
val = {"__box__": btype} val = {"__box__": btype}
@ -135,7 +141,10 @@ def op_boxcall(owner, fn, inst: Dict[str, Any], regs: Dict[int, Any]) -> None:
# ArrayBox minimal methods # ArrayBox minimal methods
elif isinstance(recv, dict) and recv.get("__box__") == "ArrayBox": elif isinstance(recv, dict) and recv.get("__box__") == "ArrayBox":
arr = recv.get("__arr", []) arr = recv.get("__arr", [])
if method in ("len", "size"): if method in ("birth",):
# No-op initializer for parity with Nyash VM
out = 0
elif method in ("len", "size"):
out = len(arr) out = len(arr)
elif method == "get": elif method == "get":
idx = int(args[0]) if args else 0 idx = int(args[0]) if args else 0
@ -185,6 +194,87 @@ def op_boxcall(owner, fn, inst: Dict[str, Any], regs: Dict[int, Any]) -> None:
out = None out = None
recv["__map"] = m recv["__map"] = m
# JsonDocBox (PyVM-native shim)
elif isinstance(recv, dict) and recv.get("__box__") == "JsonDocBox":
import json
if method == "parse":
s = args[0] if args else ""
try:
recv["__doc"] = json.loads(str(s))
recv["__err"] = None
except Exception as e:
recv["__doc"] = None
recv["__err"] = str(e)
out = 0
elif method == "root":
out = {"__box__": "JsonNodeBox", "__node": recv.get("__doc", None)}
elif method == "error":
out = recv.get("__err") or ""
else:
out = None
# JsonNodeBox (PyVM-native shim)
elif isinstance(recv, dict) and recv.get("__box__") == "JsonNodeBox":
node = recv.get("__node", None)
if method == "kind":
if node is None:
out = "null"
elif isinstance(node, bool):
out = "bool"
elif isinstance(node, int):
out = "int"
elif isinstance(node, float):
out = "real"
elif isinstance(node, str):
out = "string"
elif isinstance(node, list):
out = "array"
elif isinstance(node, dict):
out = "object"
else:
out = "null"
elif method == "get":
key = str(args[0]) if args else ""
if isinstance(node, dict) and key in node:
out = {"__box__": "JsonNodeBox", "__node": node.get(key)}
else:
out = {"__box__": "JsonNodeBox", "__node": None}
elif method == "size":
if isinstance(node, list):
out = len(node)
elif isinstance(node, dict):
out = len(node)
else:
out = 0
elif method == "at":
try:
idx = int(args[0]) if args else 0
except Exception:
idx = 0
if isinstance(node, list) and 0 <= idx < len(node):
out = {"__box__": "JsonNodeBox", "__node": node[idx]}
else:
out = {"__box__": "JsonNodeBox", "__node": None}
elif method == "str":
if isinstance(node, str):
out = node
elif isinstance(node, dict) and isinstance(node.get("value"), str):
out = node.get("value")
else:
out = ""
elif method == "int":
if isinstance(node, int):
out = node
elif isinstance(node, dict):
v = node.get("value")
out = int(v) if isinstance(v, int) else 0
else:
out = 0
elif method == "bool":
out = bool(node) if isinstance(node, bool) else False
else:
out = None
elif method == "esc_json": elif method == "esc_json":
s = args[0] if args else "" s = args[0] if args else ""
s = "" if s is None else str(s) s = "" if s is None else str(s)
@ -236,4 +326,3 @@ def op_boxcall(owner, fn, inst: Dict[str, Any], regs: Dict[int, Any]) -> None:
out = None out = None
owner._set(regs, inst.get("dst"), out) owner._set(regs, inst.get("dst"), out)

View File

@ -94,16 +94,22 @@ impl PluginHost {
let box_conf = cfg let box_conf = cfg
.get_box_config(lib_name, box_type, &toml_value) .get_box_config(lib_name, box_type, &toml_value)
.ok_or(BidError::InvalidType)?; .ok_or(BidError::InvalidType)?;
let m = box_conf // Prefer config mapping; fallback to loader's TypeBox resolve(name)
.methods let (method_id, returns_result) = if let Some(m) = box_conf.methods.get(method_name) {
.get(method_name) (m.method_id, m.returns_result)
.ok_or(BidError::InvalidMethod)?; } else {
let l = self.loader.read().unwrap();
let mid = l
.resolve_method_id(box_type, method_name)
.map_err(|_| BidError::InvalidMethod)?;
(mid, false)
};
Ok(MethodHandle { Ok(MethodHandle {
lib: lib_name.to_string(), lib: lib_name.to_string(),
box_type: box_type.to_string(), box_type: box_type.to_string(),
type_id: box_conf.type_id, type_id: box_conf.type_id,
method_id: m.method_id, method_id,
returns_result: m.returns_result, returns_result,
}) })
} }

View File

@ -23,6 +23,8 @@ struct LoadedBoxSpec {
fini_method_id: Option<u32>, fini_method_id: Option<u32>,
// Optional Nyash ABI v2 per-box invoke entry (not yet used for calls) // Optional Nyash ABI v2 per-box invoke entry (not yet used for calls)
invoke_id: Option<BoxInvokeFn>, invoke_id: Option<BoxInvokeFn>,
// Optional resolve(name)->method_id provided by NyashTypeBoxFfi
resolve_fn: Option<extern "C" fn(*const std::os::raw::c_char) -> u32>,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
struct MethodSpec { struct MethodSpec {
@ -172,7 +174,7 @@ impl PluginLoaderV2 {
} }
continue; continue;
} }
// Remember invoke_id in box_specs for (lib_name, box_type) // Remember invoke_id/resolve in box_specs for (lib_name, box_type)
if let Some(invoke_id) = st.invoke_id { if let Some(invoke_id) = st.invoke_id {
let key = (lib_name.to_string(), box_type.to_string()); let key = (lib_name.to_string(), box_type.to_string());
let mut map = self.box_specs.write().map_err(|_| BidError::PluginError)?; let mut map = self.box_specs.write().map_err(|_| BidError::PluginError)?;
@ -181,8 +183,10 @@ impl PluginLoaderV2 {
methods: HashMap::new(), methods: HashMap::new(),
fini_method_id: None, fini_method_id: None,
invoke_id: None, invoke_id: None,
resolve_fn: None,
}); });
entry.invoke_id = Some(invoke_id); entry.invoke_id = Some(invoke_id);
entry.resolve_fn = st.resolve;
} else if dbg_on() { } else if dbg_on() {
eprintln!( eprintln!(
"[PluginLoaderV2] WARN: TypeBox present but no invoke_id for {}.{} — plugin should export per-Box invoke", "[PluginLoaderV2] WARN: TypeBox present but no invoke_id for {}.{} — plugin should export per-Box invoke",
@ -311,6 +315,56 @@ impl PluginLoaderV2 {
}) })
} }
/// Resolve method_id for (box_type, method_name), consulting config first, then TypeBox resolve() if available.
pub(crate) fn resolve_method_id(&self, box_type: &str, method_name: &str) -> BidResult<u32> {
use std::ffi::CString;
let cfg = self.config.as_ref().ok_or(BidError::PluginError)?;
let cfg_path = self.config_path.as_deref().unwrap_or("nyash.toml");
let toml_value: toml::Value = super::errors::from_toml(toml::from_str(
&std::fs::read_to_string(cfg_path).map_err(|_| BidError::PluginError)?,
))?;
// 1) config mapping
if let Some((lib_name, _)) = cfg.find_library_for_box(box_type) {
if let Some(bc) = cfg.get_box_config(&lib_name, box_type, &toml_value) {
if let Some(m) = bc.methods.get(method_name) {
return Ok(m.method_id);
}
}
// 2) v2 TypeBox resolve (and cache)
let key = (lib_name.to_string(), box_type.to_string());
if let Ok(mut map) = self.box_specs.write() {
if let Some(spec) = map.get_mut(&key) {
if let Some(ms) = spec.methods.get(method_name) {
return Ok(ms.method_id);
}
if let Some(res_fn) = spec.resolve_fn {
if let Ok(cstr) = CString::new(method_name) {
let mid = res_fn(cstr.as_ptr());
if mid != 0 {
// Cache minimal MethodSpec (returns_result unknown → false)
spec.methods.insert(
method_name.to_string(),
MethodSpec {
method_id: mid,
returns_result: false,
},
);
if dbg_on() {
eprintln!(
"[PluginLoaderV2] resolve(name) {}.{} -> id {}",
box_type, method_name, mid
);
}
return Ok(mid);
}
}
}
}
}
}
Err(BidError::InvalidMethod)
}
pub fn construct_existing_instance( pub fn construct_existing_instance(
&self, &self,
type_id: u32, type_id: u32,
@ -634,10 +688,19 @@ impl PluginLoaderV2 {
.get_box_config(lib_name, box_type, &toml_value) .get_box_config(lib_name, box_type, &toml_value)
.ok_or(BidError::InvalidType)?; .ok_or(BidError::InvalidType)?;
let type_id = box_conf.type_id; let type_id = box_conf.type_id;
let method = box_conf // Resolve method id via config or TypeBox resolve()
.methods let method_id = match self.resolve_method_id(box_type, method_name) {
.get(method_name) Ok(mid) => mid,
.ok_or(BidError::InvalidMethod)?; Err(e) => {
if dbg_on() {
eprintln!(
"[PluginLoaderV2] ERR: method resolve failed for {}.{}: {:?}",
box_type, method_name, e
);
}
return Err(BidError::InvalidMethod);
}
};
// Get plugin handle // Get plugin handle
let plugins = self.plugins.read().map_err(|_| BidError::PluginError)?; let plugins = self.plugins.read().map_err(|_| BidError::PluginError)?;
let _plugin = plugins.get(lib_name).ok_or(BidError::PluginError)?; let _plugin = plugins.get(lib_name).ok_or(BidError::PluginError)?;
@ -646,7 +709,7 @@ impl PluginLoaderV2 {
let (_code, out_len, out) = super::host_bridge::invoke_alloc( let (_code, out_len, out) = super::host_bridge::invoke_alloc(
super::super::nyash_plugin_invoke_v2_shim, super::super::nyash_plugin_invoke_v2_shim,
type_id, type_id,
method.method_id, method_id,
instance_id, instance_id,
&tlv, &tlv,
); );

View File

@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR=$(cd "$(dirname "$0")/../../../.." && pwd)
echo "[smoke] collect_prints mixed order ..." >&2
pushd "$ROOT_DIR" >/dev/null
cargo build --release -q
export NYASH_ENABLE_USING=1
export NYASH_VM_USE_PY=1
BIN=./target/release/nyash
# Use JSON Box based app to avoid reliance on MiniVmPrints fallbacks
APP=apps/tests/jsonbox_collect_prints_smoke.nyash
out=$("$BIN" --backend vm "$APP")
expected=$'A\nB\n7\n1\n7\n5'
if [[ "$out" != "$expected" ]]; then
echo "[smoke] FAIL: unexpected output" >&2
echo "--- got ---" >&2
printf '%s\n' "$out" >&2
echo "--- exp ---" >&2
printf '%s\n' "$expected" >&2
exit 1
fi
echo "[smoke] OK: collect_prints mixed order" >&2
popd >/dev/null

View File

@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR=$(cd "$(dirname "$0")/../../../.." && pwd)
echo "[smoke] jsonbox collect_prints ..." >&2
pushd "$ROOT_DIR" >/dev/null
cargo build --release -q --manifest-path plugins/nyash-json-plugin/Cargo.toml
export NYASH_VM_USE_PY=1
export NYASH_LOAD_NY_PLUGINS=1
unset NYASH_DISABLE_PLUGINS || true
BIN=./target/release/nyash
APP=apps/tests/jsonbox_collect_prints_smoke.nyash
out=$("$BIN" --backend vm "$APP")
expected=$'A\nB\n7\n1\n7\n5'
if [[ "$out" != "$expected" ]]; then
echo "[smoke] FAIL: unexpected output" >&2
echo "--- got ---" >&2
printf '%s\n' "$out" >&2
echo "--- exp ---" >&2
printf '%s\n' "$expected" >&2
exit 1
fi
echo "[smoke] OK: jsonbox collect_prints" >&2
popd >/dev/null

View File

@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR=$(cd "$(dirname "$0")/../../../.." && pwd)
echo "[smoke] jsonbox nested ..." >&2
pushd "$ROOT_DIR" >/dev/null
cargo build --release -q --manifest-path plugins/nyash-json-plugin/Cargo.toml
export NYASH_VM_USE_PY=1
export NYASH_LOAD_NY_PLUGINS=1
BIN=./target/release/nyash
APP=apps/tests/jsonbox_nested.nyash
out=$("$BIN" --backend vm "$APP")
expected=$'B\n7\n3\n2'
if [[ "$out" != "$expected" ]]; then
echo "[smoke] FAIL: unexpected output" >&2
echo "--- got ---" >&2
printf '%s\n' "$out" >&2
echo "--- exp ---" >&2
printf '%s\n' "$expected" >&2
exit 1
fi
echo "[smoke] OK: jsonbox nested" >&2
popd >/dev/null

View File

@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR=$(cd "$(dirname "$0")/../../../.." && pwd)
echo "[smoke] jsonbox parse ERR ..." >&2
pushd "$ROOT_DIR" >/dev/null
cargo build --release -q --manifest-path plugins/nyash-json-plugin/Cargo.toml
export NYASH_VM_USE_PY=1
export NYASH_LOAD_NY_PLUGINS=1
BIN=./target/release/nyash
APP=apps/tests/jsonbox_parse_err.nyash
out=$("$BIN" --backend vm "$APP")
expected=$'ERR'
if [[ "$out" != "$expected" ]]; then
echo "[smoke] FAIL: unexpected output" >&2
echo "--- got ---" >&2
printf '%s\n' "$out" >&2
echo "--- exp ---" >&2
printf '%s\n' "$expected" >&2
exit 1
fi
echo "[smoke] OK: jsonbox parse ERR" >&2
popd >/dev/null

View File

@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR=$(cd "$(dirname "$0")/../../../.." && pwd)
echo "[smoke] jsonbox parse OK ..." >&2
pushd "$ROOT_DIR" >/dev/null
cargo build --release -q --manifest-path plugins/nyash-json-plugin/Cargo.toml
export NYASH_VM_USE_PY=1
export NYASH_LOAD_NY_PLUGINS=1
BIN=./target/release/nyash
APP=apps/tests/jsonbox_parse_ok.nyash
out=$("$BIN" --backend vm "$APP")
expected=$'Program'
if [[ "$out" != "$expected" ]]; then
echo "[smoke] FAIL: unexpected output" >&2
echo "--- got ---" >&2
printf '%s\n' "$out" >&2
echo "--- exp ---" >&2
printf '%s\n' "$expected" >&2
exit 1
fi
echo "[smoke] OK: jsonbox parse OK" >&2
popd >/dev/null