diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index a1b0d80e..f8071d43 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -24,6 +24,100 @@ Ny 構文(実装時の基準) - Cranelift 側の作業は別ブランチで継続。Self‑Hosting ブランチは Ny 工具の安定化に集中。 - 構文の最終合意後、using/module 版(配列/マップ最小 API)へ拡張。 + +## Grammar Sync — Phase 12.7 合わせ(現状と方針) + +参照ドキュメント(一次ソース) +- Phase 12.7 統合: `docs/development/roadmap/phases/phase-12.7/README.md` +- Grammar 最終決定: `docs/development/roadmap/phases/phase-12.7/grammar-specs/grammar-reform-final-decision.txt` +- Grammar 技術仕様: `docs/development/roadmap/phases/phase-12.7/grammar-specs/grammar-technical-spec.txt` +- 実装チェックリスト: `docs/development/roadmap/phases/phase-12.7/implementation/implementation-final-checklist.txt` +- ANCP Token Spec (参照): `docs/development/roadmap/phases/phase-12.7/ancp-specs/ANCP-Token-Specification-v1.md` + +実装状況(Phase 12.7 との整合) +- OK: `peek`(else必須・ブロック可)、`continue`、`birth` 統一、`fn{}` ラムダ(P0)、糖衣 basic(`|>`, `?.`, `??`, `+=/-=/*=/=`, `..`)ゲート済。 +- OK: `Parent::method` 用 `::` トークン+ `Parent::method(args)` 解析(P1相当の先行)。 +- OK: フィールド宣言 `name: Type` を受理(P0: 型はパースのみ、意味付けは今後)。 +- 差分(非致命): + - import 未実装(現状は `using` のみ Phase0: `nyashstd` 制限)。 + - `public name: Type` の単行は未対応(`public { ... }` ブロックは対応)。 + - レガシー `>>`(ARROW)トークンとASTが残存(12.7 では不要)。 + - Tokenizer に `fn` の重複割り当て(`FN`/`FUNCTION`)が存在(動作は壊していないが整理対象)。 + +合意に向けた軽微タスク(Self‑Hosting 主線を維持したまま) +- T1: `public name: Type` 単行を Box 内で受理(`public { ... }` は後方互換維持)。 +- T2: `import` を追加(現状 `using` と並行運用、Phase0は読み取りのみ/将来の解決器に接続)。 +- T3: 12.7 厳格モードゲート(例: `NYASH_STRICT_12_7=1`) + - `>>` を無効化(パーサ拒否 or トークナイズ抑止)。 + - 追加キーワード群(legacy拡張)の一部を“識別子扱い”へフォールバック(実験用)。 +- T4: Tokenizer の `fn` 重複を解消(`FN` を正とし `FUNCTION` 二重割り当てを削除)。 + +受け入れ基準(Grammar Sync) +- sugar_level=none/basic の双方でスモークが通る。 +- `peek` else 未指定時に適切なエラー(現状維持)。 +- `public name: Type` が Box 内でフィールドとして扱われる(最小P0)。 +- 厳格モードで `>>` が受理されない/互換モードでは現状維持。 + +検証コマンド(例) +- `NYASH_SYNTAX_SUGAR_LEVEL=none cargo test -p nyash_self_main -- tests::sugar_basic_test -- --nocapture`(none でも tokenizer/parseは通ること) +- `NYASH_SYNTAX_SUGAR_LEVEL=basic cargo test -p nyash_self_main -- tests::sugar_pipeline_test -- --nocapture` +- `NYASH_STRICT_12_7=1 ./target/release/nyash --backend vm apps/smokes/grammar/peek_basic.nyash`(`>>` を含むコードが拒否される) + +## Bitwise/Shift — main 取り込みと現状 + +- origin/main でビット演算(&, |, ^, <<, >>)が Grammar/Tokenizer/AST/MIR に統合済み。レガシー `>>` ARROW は撤退。 +- 追加テスト(main): + - `src/tests/parser_bitops_test.rs`(代表式 `1 + 2 << 3 & 7` の構文) + - `src/tests/vm_bitops_test.rs`(`(5&3)+(5|2)+(5^1)+(1<<5)+(32>>3) == 48`、`1<<100` マスク確認) +- 確認結果(ローカル): + - MIR バックエンドは 48 で合格(OK)。 + - VM バックエンドは現状 `Unsupported integer operation: BitAnd`(VM 側の実装が未導入のため) + +取り込み計画(selfhosting-dev に main を統合) +1) ローカル変更を一時退避(stash)し、`origin/main` をマージ。 +2) コンフリクト解消:`src/jit/lower/builder/cranelift.rs` は main 側の更新(ARROW撤退/SHR採用)を優先。 +3) 検証:MIR 経路で bitops スモーク(期待 48)。 +4) 次段:VM の bitops(i64限定、シフトは `rhs&63` マスク)を実装→テスト有効化。 +5) LLVM/E2E:grammar 解禁後に `apps/tests/ny-llvm-bitops/` を有効化(MIR直構築は現状担保)。 + +実行メモ(代表) +- MIR スモーク: `printf "return (5 & 3) + (5 | 2) + (5 ^ 1) + (1 << 5) + (32 >> 3)\n" > tmp/bitops_smoke.nyash && ./target/debug/nyash --backend mir tmp/bitops_smoke.nyash` → `Result: 48` +- VM は現状未対応(実装後に同式で確認)。 + +TODO(bitops) +- [ ] origin/main を selfhosting-dev にマージ(conflict 解消)。 +- [ ] MIR 経路のスモーク確認(48)。 +- [ ] VM: i64 の `& | ^ << >>` 実装(`rhs&63` マスク)。 +- [ ] tests: `vm_bitops_test.rs` を有効化(VM で合格)。 +- [ ] docs: ARROW(>>) 撤退と `|>` への一本化を明記。 + +## Self‑Host — Include‑only Dependency Tree(Phase 0) + +スコープ(Phase 0 最小) +- Nyのみ(Array/Map不使用)で include 依存木を構築し、純JSONを出力。 +- using/module/import は次段。Runner は `NYASH_DEPS_JSON` をログ読み込みのみ。 + +現状 +- ツール: `apps/selfhost/tools/dep_tree_min_string.nyash`(include専用、再帰・文字列走査)。 +- 出力: `make dep-tree` → `tmp/deps.json`(純JSON化、先頭ログの除去は `[tasks].dep_tree` で吸収)。 +- 走査: コメント(`//`, `#`)・文字列内の `include` を無視する状態機械を導入(誤検出抑制)。 +- サンプル: `apps/selfhost/smokes/dep_smoke_root.nyash`(子: `dep_smoke_child.nyash`)。 + +出力仕様・受け入れ基準: docs/selfhost/dep_tree_min_string.md に移設(CURRENT_TASKは要点のみ表記)。 + +残タスク(Phase 0 必須) +- P0-2: スモーク(循環あり/なし)と合わせて確認(追加済み)。 +- P0-3: docs への移設(完了)。 + +任意(Phase 0.5) +- stderr固定の徹底(将来Runner側の冗長出力をenvゲート化)。 +- ルートパスの正規化(`.`,`..` の整理)と最大深さ/件数の安全弁(オプション)。 + +検証(代表) +- `echo apps/selfhost/smokes/dep_smoke_root.nyash | ./target/release/nyash --backend vm apps/selfhost/tools/dep_tree_min_string.nyash` +- `echo apps/selfhost/smokes/dep_smoke_cycle_a.nyash | ./target/release/nyash --backend vm apps/selfhost/tools/dep_tree_min_string.nyash` +- `make dep-tree`(ENTRYは標準入力1行 or 既定パスにフォールバック) + # Quick Plan — Self‑Host (Restart Safe) - Goals: Ny-only dependency tree (include → later using/module), JSON out; simple file-bridge to existing MIR→VM→AOT without tight coupling. diff --git a/apps/selfhost/smokes/dep_smoke_child.nyash b/apps/selfhost/smokes/dep_smoke_child.nyash new file mode 100644 index 00000000..f4e2ab3f --- /dev/null +++ b/apps/selfhost/smokes/dep_smoke_child.nyash @@ -0,0 +1,7 @@ +// child file +box Child { + hello() { + return 1 + } +} + diff --git a/apps/selfhost/smokes/dep_smoke_cycle_a.nyash b/apps/selfhost/smokes/dep_smoke_cycle_a.nyash new file mode 100644 index 00000000..37b6116b --- /dev/null +++ b/apps/selfhost/smokes/dep_smoke_cycle_a.nyash @@ -0,0 +1,7 @@ +// cycle A -> B -> A +include "dep_smoke_cycle_b.nyash" + +box A { + id() { return 1 } +} + diff --git a/apps/selfhost/smokes/dep_smoke_cycle_b.nyash b/apps/selfhost/smokes/dep_smoke_cycle_b.nyash new file mode 100644 index 00000000..7e836b77 --- /dev/null +++ b/apps/selfhost/smokes/dep_smoke_cycle_b.nyash @@ -0,0 +1,7 @@ +// cycle B -> A +include "dep_smoke_cycle_a.nyash" + +box B { + id() { return 2 } +} + diff --git a/apps/selfhost/smokes/dep_smoke_root.nyash b/apps/selfhost/smokes/dep_smoke_root.nyash new file mode 100644 index 00000000..c56bf885 --- /dev/null +++ b/apps/selfhost/smokes/dep_smoke_root.nyash @@ -0,0 +1,14 @@ +// root smoke for include-only tree +include "dep_smoke_child.nyash" + +box Root { + main() { + // a string containing include "should_not_match.nyash" + local s = "text include \"dummy.nyash\" text" + // a comment with include "ignored.nyash" + // include "ignored.nyash" + # include "ignored2.nyash" + return 0 + } +} + diff --git a/apps/selfhost/tools/dep_tree_min_string.nyash b/apps/selfhost/tools/dep_tree_min_string.nyash new file mode 100644 index 00000000..9b2b5f6e --- /dev/null +++ b/apps/selfhost/tools/dep_tree_min_string.nyash @@ -0,0 +1,154 @@ +// dep_tree_min_string.nyash — minimal include-only dependency tree (no Array/Map plugins) + +static box Main { + has_in_stack(stack, p) { + // check if stack contains "\n" + p + "\n" + local t = "\n" + p + "\n" + local n = stack.length() + local m = t.length() + if m == 0 { return 0 } + local i = 0 + loop(i + m <= n) { + if stack.substring(i, i+m) == t { return 1 } + i = i + 1 + } + return 0 + } + read_all(path) { + local fb = new FileBox() + local ok = fb.open(path, "r") + if ok == false { return null } + local s = fb.read() + fb.close() + return s + } + + dirname(path) { + local pb = new PathBox() + local d = pb.dirname(path) + if d != null { return d } + local i = path.lastIndexOf("/") + if i < 0 { return "." } + return path.substring(0, i) + } + + join(base, rel) { + local pb = new PathBox() + local j = pb.join(base, rel) + if j != null { return j } + return base + "/" + rel + } + + esc_json(s) { + // very small escaper: replace \ and " + local out = "" + local i = 0 + local n = s.length() + loop(i < n) { + local ch = s.substring(i, i+1) + if ch == "\\" { out = out + "\\\\" } else { + if ch == "\"" { out = out + "\\\"" } else { out = out + ch } + } + i = i + 1 + } + return out + } + + node_json(path, stack, depth) { + // safety valve: max depth + if depth >= 64 { + return "{\\\"path\\\":\\\"" + me.esc_json(path) + "\\\",\\\"includes\\\":[],\\\"children\\\":[]}" + } + local src = me.read_all(path) + if src == null { + return "{\\\"path\\\":\\\"" + me.esc_json(path) + "\\\",\\\"includes\\\":[],\\\"children\\\":[]}" + } + local base = me.dirname(path) + local incs = "" + local inc_first = 1 + local children = "" + local child_first = 1 + local i = 0 + local n = src.length() + local in_str = 0 + local in_cmt = 0 + loop(i < n) { + local ch = src.substring(i, i+1) + // handle line comments (// or #) + if in_cmt == 1 { + if ch == "\n" { in_cmt = 0 } + i = i + 1 + continue + } + if in_str == 1 { + if ch == "\"" { + // if previous is not backslash, close + if i == 0 { in_str = 0 } else { + local prev = src.substring(i-1, i) + if prev != "\\" { in_str = 0 } + } + } + i = i + 1 + continue + } + // not in string/comment + if ch == "/" && i + 1 < n && src.substring(i+1, i+2) == "/" { in_cmt = 1; i = i + 2; continue } + if ch == "#" { in_cmt = 1; i = i + 1; continue } + if ch == "\"" { in_str = 1; i = i + 1; continue } + // look for include " + if i + 9 <= n && src.substring(i, i+9) == "include \"" { + local j = i + 9 + // find closing quote (respect escapes) + loop(j < n) { + if src.substring(j, j+1) == "\"" { + local prev = src.substring(j-1, j) + if prev != "\\" { break } + } + j = j + 1 + } + if j < n { + local p = src.substring(i+9, j) + if inc_first == 1 { + incs = incs + "\"" + me.esc_json(p) + "\"" + inc_first = 0 + } else { + incs = incs + ",\"" + me.esc_json(p) + "\"" + } + local child_path = me.join(base, p) + // cycle detection: if child_path already in stack, do not recurse + local cj = null + if me.has_in_stack(stack, child_path) == 1 { + cj = "{\\\"path\\\":\\\"" + me.esc_json(child_path) + "\\\",\\\"includes\\\":[],\\\"children\\\":[]}" + } else { + cj = me.node_json(child_path, stack + child_path + "\n", depth + 1) + } + if child_first == 1 { + children = children + cj + child_first = 0 + } else { + children = children + "," + cj + } + i = j + 1 + continue + } + } + i = i + 1 + } + return "{\\\"path\\\":\\\"" + me.esc_json(path) + "\\\",\\\"includes\\\":[" + incs + "],\\\"children\\\":[" + children + "]}" + } + + main(args) { + local console = new ConsoleBox() + local entry = null + if args != null && args.length() >= 1 { entry = args.get(0) } + if entry == null || entry == "" { + // try stdin first line + local line = console.readLine() + if line != null && line != "" { entry = line } else { entry = "apps/selfhost/ny-parser-nyash/main.nyash" } + } + local tree = me.node_json(entry, "\n" + entry + "\n", 0) + local out = "{\\\"version\\\":1,\\\"root_path\\\":\\\"" + me.esc_json(entry) + "\\\",\\\"tree\\\":" + tree + "}" + console.println(out) + return 0 + } +} diff --git a/apps/selfhost/tools/dep_tree_simple.nyash b/apps/selfhost/tools/dep_tree_simple.nyash index ca477b4c..c08d4a81 100644 --- a/apps/selfhost/tools/dep_tree_simple.nyash +++ b/apps/selfhost/tools/dep_tree_simple.nyash @@ -37,26 +37,35 @@ static box Main { // ---- text utils ---- split_lines(src) { + // return { arr, len } + local pair = new MapBox() local out = new ArrayBox() + local len = 0 local i = 0 local n = src.length() local start = 0 loop(true) { - if i == n { out.push(src.substring(start, i)) return out } + if i == n { out.push(src.substring(start, i)) len = len + 1 pair.set("arr", out) pair.set("len", len) return pair } local ch = src.substring(i, i+1) - if ch == "\n" { out.push(src.substring(start, i)) start = i + 1 } + if ch == "\n" { out.push(src.substring(start, i)) len = len + 1 start = i + 1 } i = i + 1 } - return out + pair.set("arr", out) + pair.set("len", len) + return pair } // ---- scanners ---- scan_includes(src) { + local pair = new MapBox() local out = new ArrayBox() - if src == null { return out } - local lines = me.split_lines(src) + local out_len = 0 + if src == null { pair.set("arr", out) pair.set("len", out_len) return pair } + local lp = me.split_lines(src) + local lines = lp.get("arr") + local lines_len = lp.get("len") local i = 0 - loop(i < lines.length()) { + loop(i < lines_len) { local t = lines.get(i).trim() if t.startsWith("include \"") { local rest = t.substring(9, t.length()) @@ -66,19 +75,26 @@ static box Main { if rest.substring(j, j+1) == "\"" { q = j j = rest.length() } j = j + 1 } - if q >= 0 { out.push(rest.substring(0, q)) } + if q >= 0 { out.push(rest.substring(0, q)) out_len = out_len + 1 } } i = i + 1 } - return out + pair.set("arr", out) + pair.set("len", out_len) + return pair } scan_usings(src) { + // return { arr, len } + local pair = new MapBox() local out = new ArrayBox() - if src == null { return out } - local lines = me.split_lines(src) + local out_len = 0 + if src == null { pair.set("arr", out) pair.set("len", out_len) return pair } + local lp = me.split_lines(src) + local lines = lp.get("arr") + local lines_len = lp.get("len") local i = 0 - loop(i < lines.length()) { + loop(i < lines_len) { local t0 = lines.get(i).trim() local matched = false local t = t0 @@ -95,18 +111,26 @@ static box Main { if alias != null { rec.set("alias", alias) } if target.startsWith("./") || target.startsWith("/") || target.endsWith(".nyash") { rec.set("kind", "path") } else { rec.set("kind", "namespace") } out.push(rec) + out_len = out_len + 1 } i = i + 1 } - return out + pair.set("arr", out) + pair.set("len", out_len) + return pair } scan_modules(src) { + // return { arr, len } + local pair = new MapBox() local out = new ArrayBox() - if src == null { return out } - local lines = me.split_lines(src) + local out_len = 0 + if src == null { pair.set("arr", out) pair.set("len", out_len) return pair } + local lp = me.split_lines(src) + local lines = lp.get("arr") + local lines_len = lp.get("len") local i = 0 - loop(i < lines.length()) { + loop(i < lines_len) { local t = lines.get(i).trim() if t.startsWith("// @module ") { local rest = t.substring(11, t.length()) @@ -118,11 +142,14 @@ static box Main { m.set("namespace", ns) m.set("path", path) out.push(m) + out_len = out_len + 1 } } i = i + 1 } - return out + pair.set("arr", out) + pair.set("len", out_len) + return pair } default_using_paths() { @@ -139,8 +166,9 @@ static box Main { local cand0 = me.join(base_dir, rel) if me.file_exists(cand0) { return cand0 } local paths = me.default_using_paths() + local paths_len = 4 local i = 0 - loop(i < paths.length()) { + loop(i < paths_len) { local cand = me.join(paths.get(i), rel) if me.file_exists(cand) { return cand } i = i + 1 @@ -172,28 +200,34 @@ static box Main { return out } local base = me.dirname(path) - local incs = me.scan_includes(src) - local uses = me.scan_usings(src) - local mods = me.scan_modules(src) + local incp = me.scan_includes(src) + local incs = incp.get("arr") + local incs_len = incp.get("len") + local usp = me.scan_usings(src) + local uses = usp.get("arr") + local uses_len = usp.get("len") + local modp = me.scan_modules(src) + local mods = modp.get("arr") + local mods_len = modp.get("len") out.set("includes", incs) out.set("uses", uses) out.set("modules", mods) local mod_map = new MapBox() local mi = 0 - loop(mi < mods.length()) { + loop(mi < mods_len) { local mm = mods.get(mi) mod_map.set(mm.get("namespace"), mm.get("path")) mi = mi + 1 } local children = new ArrayBox() local i = 0 - loop(i < incs.length()) { + loop(i < incs_len) { local child_path = me.join(base, incs.get(i)) children.push(me.node_for(child_path, visited)) i = i + 1 } i = 0 - loop(i < uses.length()) { + loop(i < uses_len) { local u = uses.get(i) if u.get("kind") == "path" { local p = me.join(base, u.get("target")) diff --git a/docs/selfhost/dep_tree_min_string.md b/docs/selfhost/dep_tree_min_string.md new file mode 100644 index 00000000..a63ed35b --- /dev/null +++ b/docs/selfhost/dep_tree_min_string.md @@ -0,0 +1,39 @@ +# Include-only Dependency Tree (Phase 0) + +Goal +- Build a dependency tree using only Ny (no Array/Map), scanning source text for `include "..."`, and output stable JSON. + +Scope (Phase 0) +- Only `include` is supported. `using/module/import` are out of scope. +- Runner bridge: `NYASH_DEPS_JSON=` is read and logged only (no behavior change). + +Tool +- `apps/selfhost/tools/dep_tree_min_string.nyash` + - Recursively reads source files, scans for `include "path"` outside of strings and comments. + - Comments: `//` and `#` (line comments) are ignored. + - Strings: `"..."` with `\"` escapes are honored. + - Cycles: detected via a simple stack string; when detected, the child appears as a leaf node with empty `includes`/`children`. + +Output format +``` +{ "version": 1, + "root_path": "", + "tree": { + "path": "", "includes": ["..."], "children": [ ] + } +} +``` + +Acceptance criteria +- Running `make dep-tree` produces `tmp/deps.json` whose first non-empty line starts with `{` and conforms to the format above. +- The scanner does not pick includes inside strings or comments. +- Cycles do not crash or loop; the repeated node is represented as a leaf. + +Examples +- Root: `apps/selfhost/smokes/dep_smoke_root.nyash` (includes `dep_smoke_child.nyash`) +- Cycle: `apps/selfhost/smokes/dep_smoke_cycle_a.nyash` ↔ `dep_smoke_cycle_b.nyash` + +Validation (examples) +- `echo apps/selfhost/smokes/dep_smoke_root.nyash | ./target/release/nyash --backend vm apps/selfhost/tools/dep_tree_min_string.nyash` +- `make dep-tree` + diff --git a/nyash.toml b/nyash.toml index 74a4657f..69b51fbf 100644 --- a/nyash.toml +++ b/nyash.toml @@ -412,7 +412,7 @@ ny_plugins = [ [tasks] # self-host: dependency tree (Ny-only) -dep_tree = "NYASH_USE_PLUGIN_BUILTINS=1 NYASH_DISABLE_PLUGINS=0 {root}/target/release/nyash --backend vm {root}/apps/selfhost/tools/dep_tree_simple.nyash -- {root}/apps/selfhost/ny-parser-nyash/main.nyash > {root}/tmp/deps.json" +dep_tree = "NYASH_USE_PLUGIN_BUILTINS=1 NYASH_DISABLE_PLUGINS=0 {root}/target/release/nyash --backend vm {root}/apps/selfhost/tools/dep_tree_min_string.nyash | sed -n '/^{/,$p' > {root}/tmp/deps.json" # LLVMビルド(nyash本体) build_llvm = "LLVM_SYS_180_PREFIX=$(llvm-config-18 --prefix) cargo build --release --features llvm" diff --git a/src/ast.rs b/src/ast.rs index 42753ad3..0ad6a6b8 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -443,6 +443,12 @@ pub enum ASTNode { namespace_name: String, span: Span, }, + /// import文: import "path" (as Alias)? + ImportStatement { + path: String, + alias: Option, + span: Span, + }, /// nowait文: nowait variable = expression Nowait { @@ -673,6 +679,7 @@ impl ASTNode { ASTNode::Break { .. } => "Break", ASTNode::Continue { .. } => "Continue", ASTNode::UsingStatement { .. } => "UsingStatement", + ASTNode::ImportStatement { .. } => "ImportStatement", ASTNode::BoxDeclaration { .. } => "BoxDeclaration", ASTNode::FunctionDeclaration { .. } => "FunctionDeclaration", ASTNode::GlobalVar { .. } => "GlobalVar", @@ -742,6 +749,7 @@ impl ASTNode { ASTNode::Break { .. } => ASTNodeType::Statement, ASTNode::Continue { .. } => ASTNodeType::Statement, ASTNode::UsingStatement { .. } => ASTNodeType::Statement, + ASTNode::ImportStatement { .. } => ASTNodeType::Statement, ASTNode::GlobalVar { .. } => ASTNodeType::Statement, ASTNode::Include { .. } => ASTNodeType::Statement, ASTNode::Local { .. } => ASTNodeType::Statement, @@ -794,6 +802,9 @@ impl ASTNode { ASTNode::UsingStatement { namespace_name, .. } => { format!("UsingStatement({})", namespace_name) } + ASTNode::ImportStatement { path, alias, .. } => { + if let Some(a) = alias { format!("ImportStatement({}, as {})", path, a) } else { format!("ImportStatement({})", path) } + } ASTNode::BoxDeclaration { name, fields, methods, constructors, is_interface, extends, implements, .. } => { let mut desc = if *is_interface { format!("InterfaceBox({}, {} methods", name, methods.len()) @@ -907,6 +918,7 @@ impl ASTNode { ASTNode::Break { span, .. } => *span, ASTNode::Continue { span, .. } => *span, ASTNode::UsingStatement { span, .. } => *span, + ASTNode::ImportStatement { span, .. } => *span, ASTNode::Nowait { span, .. } => *span, ASTNode::Arrow { span, .. } => *span, ASTNode::TryCatch { span, .. } => *span, diff --git a/src/grammar/generated.rs b/src/grammar/generated.rs index 0f04d259..afca40cf 100644 --- a/src/grammar/generated.rs +++ b/src/grammar/generated.rs @@ -50,6 +50,7 @@ pub static SYNTAX_ALLOWED_STATEMENTS: &[&str] = &[ "print", "nowait", "include", + "import", "local", "outbox", "try", @@ -66,4 +67,4 @@ pub static SYNTAX_ALLOWED_BINOPS: &[&str] = &[ "or", "eq", "ne", -]; \ No newline at end of file +]; diff --git a/src/interpreter/statements.rs b/src/interpreter/statements.rs index ca393aa1..d5715c97 100644 --- a/src/interpreter/statements.rs +++ b/src/interpreter/statements.rs @@ -100,6 +100,14 @@ impl NyashInterpreter { self.execute_using_statement(namespace_name) } + ASTNode::ImportStatement { path, alias, .. } => { + // Stage-0 import: no-op (record/log only) + if std::env::var("NYASH_IMPORT_TRACE").ok().as_deref() == Some("1") { + if let Some(a) = alias { eprintln!("[import] {} as {}", path, a); } else { eprintln!("[import] {}", path); } + } + Ok(Box::new(VoidBox::new())) + } + ASTNode::BoxDeclaration { name, fields, public_fields, private_fields, methods, constructors, init_fields, weak_fields, is_interface, extends, implements, type_parameters, is_static, static_init, .. } => { if *is_static { // 🔥 Static Box宣言の処理 @@ -613,19 +621,25 @@ impl NyashInterpreter { pub(super) fn execute_using_statement(&mut self, namespace_name: &str) -> Result, RuntimeError> { idebug!("🌟 DEBUG: execute_using_statement called with namespace: {}", namespace_name); - // Phase 0: nyashstdのみサポート - if namespace_name != "nyashstd" { - return Err(RuntimeError::InvalidOperation { - message: format!("Unsupported namespace '{}'. Only 'nyashstd' is supported in Phase 0.", namespace_name) - }); + // First, handle the builtin stdlib namespace + if namespace_name == "nyashstd" { + idebug!("🌟 DEBUG: About to call ensure_stdlib_initialized"); + self.ensure_stdlib_initialized()?; + idebug!("🌟 DEBUG: ensure_stdlib_initialized completed"); + return Ok(Box::new(VoidBox::new())); + } + // Otherwise, consult the modules registry (resolved by runner/CLI/header) + if crate::runtime::modules_registry::get(namespace_name).is_some() { + // Resolved via registry; no further action at runtime stage-0 + return Ok(Box::new(VoidBox::new())); + } + let strict = std::env::var("NYASH_USING_STRICT").ok().as_deref() == Some("1"); + if strict { + return Err(RuntimeError::InvalidOperation { message: format!("Unresolved namespace '{}' (strict)", namespace_name) }); + } + if crate::interpreter::utils::debug_on() { + eprintln!("[using] unresolved '{}' (non-strict, continuing)", namespace_name); } - - // 標準ライブラリを初期化(存在しない場合) - idebug!("🌟 DEBUG: About to call ensure_stdlib_initialized"); - self.ensure_stdlib_initialized()?; - idebug!("🌟 DEBUG: ensure_stdlib_initialized completed"); - - // using nyashstdの場合は特に何もしない(既に標準ライブラリが初期化されている) Ok(Box::new(VoidBox::new())) } diff --git a/src/parser/declarations/box_definition.rs b/src/parser/declarations/box_definition.rs index 9357b8b9..cf6809a1 100644 --- a/src/parser/declarations/box_definition.rs +++ b/src/parser/declarations/box_definition.rs @@ -407,33 +407,58 @@ impl NyashParser { let field_or_method = field_or_method.clone(); self.advance(); - // 可視性ブロック: public { ... } / private { ... } + // 可視性: + // - public { ... } / private { ... } ブロック + // - public name: Type 単行(P0: 型はパースのみ、意味付けは後段) if field_or_method == "public" || field_or_method == "private" { - self.consume(TokenType::LBRACE)?; - self.skip_newlines(); - while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() { - if let TokenType::IDENTIFIER(fname) = &self.current_token().token_type { - let fname = fname.clone(); - // ブロックに追加 - if field_or_method == "public" { public_fields.push(fname.clone()); } else { private_fields.push(fname.clone()); } - // 互換性のため、全体fieldsにも追加 - fields.push(fname); - self.advance(); - // カンマ/改行をスキップ - if self.match_token(&TokenType::COMMA) { self.advance(); } - self.skip_newlines(); - continue; + if self.match_token(&TokenType::LBRACE) { + // ブロック形式 + self.advance(); // consume '{' + self.skip_newlines(); + while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() { + if let TokenType::IDENTIFIER(fname) = &self.current_token().token_type { + let fname = fname.clone(); + // ブロックに追加 + if field_or_method == "public" { public_fields.push(fname.clone()); } else { private_fields.push(fname.clone()); } + // 互換性のため、全体fieldsにも追加 + fields.push(fname); + self.advance(); + // カンマ/改行をスキップ + if self.match_token(&TokenType::COMMA) { self.advance(); } + self.skip_newlines(); + continue; + } + // 予期しないトークン + return Err(ParseError::UnexpectedToken { + expected: "identifier in visibility block".to_string(), + found: self.current_token().token_type.clone(), + line: self.current_token().line, + }); } - // 予期しないトークン - return Err(ParseError::UnexpectedToken { - expected: "identifier in visibility block".to_string(), - found: self.current_token().token_type.clone(), - line: self.current_token().line, - }); + self.consume(TokenType::RBRACE)?; + self.skip_newlines(); + continue; + } else if self.match_token(&TokenType::IDENTIFIER) { + // 単行形式: public name[: Type] + let fname = if let TokenType::IDENTIFIER(n) = &self.current_token().token_type { n.clone() } else { unreachable!() }; + self.advance(); + if self.match_token(&TokenType::COLON) { + self.advance(); // consume ':' + // 型名(識別子)を受理して破棄(P0) + if let TokenType::IDENTIFIER(_ty) = &self.current_token().token_type { + self.advance(); + } else { + return Err(ParseError::UnexpectedToken { found: self.current_token().token_type.clone(), expected: "type name".to_string(), line: self.current_token().line }); + } + } + if field_or_method == "public" { public_fields.push(fname.clone()); } else { private_fields.push(fname.clone()); } + fields.push(fname); + self.skip_newlines(); + continue; + } else { + // public/private の後に '{' でも識別子でもない + return Err(ParseError::UnexpectedToken { found: self.current_token().token_type.clone(), expected: "'{' or field name".to_string(), line: self.current_token().line }); } - self.consume(TokenType::RBRACE)?; - self.skip_newlines(); - continue; } // メソッドかフィールドかを判定 diff --git a/src/parser/statements.rs b/src/parser/statements.rs index 77ca7746..f98cf134 100644 --- a/src/parser/statements.rs +++ b/src/parser/statements.rs @@ -19,6 +19,9 @@ impl NyashParser { TokenType::BOX => { self.parse_box_declaration() }, + TokenType::IMPORT => { + self.parse_import() + }, TokenType::INTERFACE => { self.parse_interface_box_declaration() }, @@ -117,6 +120,32 @@ impl NyashParser { } result } + + /// import文をパース: import "path" (as Alias)? + pub(super) fn parse_import(&mut self) -> Result { + self.advance(); // consume 'import' + let path = if let TokenType::STRING(s) = &self.current_token().token_type { + let v = s.clone(); + self.advance(); + v + } else { + return Err(ParseError::UnexpectedToken { found: self.current_token().token_type.clone(), expected: "string literal".to_string(), line: self.current_token().line }); + }; + // Optional: 'as' Alias (treat 'as' as identifier literal) + let mut alias: Option = None; + if let TokenType::IDENTIFIER(w) = &self.current_token().token_type { + if w == "as" { + self.advance(); + if let TokenType::IDENTIFIER(name) = &self.current_token().token_type { + alias = Some(name.clone()); + self.advance(); + } else { + return Err(ParseError::UnexpectedToken { found: self.current_token().token_type.clone(), expected: "alias name".to_string(), line: self.current_token().line }); + } + } + } + Ok(ASTNode::ImportStatement { path, alias, span: Span::unknown() }) + } /// if文をパース: if (condition) { body } else if ... else { body } pub(super) fn parse_if(&mut self) -> Result { diff --git a/src/runner/mod.rs b/src/runner/mod.rs index 37f3ed76..d4445a77 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -36,6 +36,38 @@ use nyash_rust::runtime; use nyash_rust::runner_plugin_init; use std::path::PathBuf; +/// Resolve a using target according to priority: modules > relative > using-paths +/// Returns Ok(resolved_path_or_token). On strict mode, ambiguous matches cause error. +fn resolve_using_target( + tgt: &str, + is_path: bool, + modules: &[(String, String)], + using_paths: &[String], + context_dir: Option<&std::path::Path>, + strict: bool, + verbose: bool, +) -> Result { + if is_path { return Ok(tgt.to_string()); } + // 1) modules mapping + if let Some((_, p)) = modules.iter().find(|(n, _)| n == tgt) { return Ok(p.clone()); } + // 2) build candidate list: relative then using-paths + let rel = tgt.replace('.', "/") + ".nyash"; + let mut cand: Vec = Vec::new(); + if let Some(dir) = context_dir { let c = dir.join(&rel); if c.exists() { cand.push(c.to_string_lossy().to_string()); } } + for base in using_paths { + let c = std::path::Path::new(base).join(&rel); + if c.exists() { cand.push(c.to_string_lossy().to_string()); } + } + if cand.is_empty() { + if verbose { eprintln!("[using] unresolved '{}' (searched: rel+paths)", tgt); } + return Ok(tgt.to_string()); + } + if cand.len() > 1 && strict { + return Err(format!("ambiguous using '{}': {}", tgt, cand.join(", "))); + } + Ok(cand.remove(0)) +} + /// Main execution coordinator pub struct NyashRunner { config: CliConfig, @@ -83,6 +115,66 @@ impl NyashRunner { } return; } + // CLI using/module overrides (MVP): apply early so JSON pipeline can observe them + if self.config.using.is_some() || self.config.using_path.is_some() || self.config.modules.is_some() + || std::env::var("NYASH_USING_PATH").is_ok() || std::env::var("NYASH_MODULES").is_ok() { + let mut using_paths: Vec = Vec::new(); + if let Some(p) = self.config.using_path.clone() { for s in p.split(':') { let s=s.trim(); if !s.is_empty() { using_paths.push(s.to_string()); } } } + if let Ok(p) = std::env::var("NYASH_USING_PATH") { for s in p.split(':') { let s=s.trim(); if !s.is_empty() { using_paths.push(s.to_string()); } } } + if using_paths.is_empty() { using_paths.extend(["apps","lib","."].into_iter().map(|s| s.to_string())); } + + // modules mapping + let mut modules: Vec<(String,String)> = Vec::new(); + if let Some(m) = self.config.modules.clone() { for ent in m.split(',') { if let Some((k,v)) = ent.split_once('=') { let k=k.trim(); let v=v.trim(); if !k.is_empty() && !v.is_empty() { modules.push((k.to_string(), v.to_string())); } } } } + if let Ok(ms) = std::env::var("NYASH_MODULES") { for ent in ms.split(',') { if let Some((k,v)) = ent.split_once('=') { let k=k.trim(); let v=v.trim(); if !k.is_empty() && !v.is_empty() { modules.push((k.to_string(), v.to_string())); } } } } + for (ns, path) in &modules { let sb = crate::box_trait::StringBox::new(path.clone()); crate::runtime::modules_registry::set(ns.clone(), Box::new(sb)); } + + // using specs + let mut pending_using: Vec<(String, Option, bool)> = Vec::new(); // (target, alias, is_path) + if let Some(u) = self.config.using.clone() { + for ent in u.split(',') { + let s = ent.trim().trim_end_matches(';').trim(); if s.is_empty() { continue; } + let (tgt, alias) = if let Some(pos) = s.find(" as ") { (s[..pos].trim().to_string(), Some(s[pos+4..].trim().to_string())) } else { (s.to_string(), None) }; + let is_path = tgt.starts_with('"') || tgt.starts_with("./") || tgt.starts_with('/') || tgt.ends_with(".nyash"); + pending_using.push((tgt.trim_matches('"').to_string(), alias, is_path)); + } + } + // Resolve using (priority: modules > relative(file) > using-paths; ambiguous=error if strict) + let strict = std::env::var("NYASH_USING_STRICT").ok().as_deref() == Some("1"); + let verbose = std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1"); + let ctx = self.config.file.as_deref().and_then(|f| std::path::Path::new(f).parent()); + for (tgt, alias, is_path) in pending_using.into_iter() { + if is_path && !std::path::Path::new(&tgt).exists() { + if strict { eprintln!("❌ using: path not found: {}", tgt); std::process::exit(1); } + if verbose { eprintln!("[using] path not found (continuing): {}", tgt); } + } + let value = match resolve_using_target(&tgt, is_path, &modules, &using_paths, ctx, strict, verbose) { + Ok(v) => v, + Err(e) => { eprintln!("❌ using: {}", e); std::process::exit(1); } + }; + 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)); } + } + } + // Stage-1: Optional dependency tree bridge (log-only) + if let Ok(dep_path) = std::env::var("NYASH_DEPS_JSON") { + match std::fs::read_to_string(&dep_path) { + Ok(s) => { + let bytes = s.as_bytes().len(); + // Try to extract quick hints without failing + let mut root_info = String::new(); + if let Ok(v) = serde_json::from_str::(&s) { + if let Some(r) = v.get("root_path").and_then(|x| x.as_str()) { root_info = format!(" root='{}'", r); } + } + eprintln!("[deps] loaded {} bytes from{} {}", bytes, if root_info.is_empty(){""} else {":"}, root_info); + } + Err(e) => { + eprintln!("[deps] read error: {}", e); + } + } + } + // 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 { @@ -160,6 +252,41 @@ 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 with clear precedence and ambiguity handling + let strict = std::env::var("NYASH_USING_STRICT").ok().as_deref() == Some("1"); + let verbose = std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1"); + let ctx = std::path::Path::new(filename).parent(); + for (ns, alias) in pending_using.iter() { + let value = match resolve_using_target(ns, false, &pending_modules, &using_paths, ctx, strict, verbose) { + Ok(v) => v, + Err(e) => { eprintln!("❌ using: {}", e); std::process::exit(1); } + }; + 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)); + } + } } } diff --git a/src/runner/modes/common.rs b/src/runner/modes/common.rs index a5b7c693..61d83be3 100644 --- a/src/runner/modes/common.rs +++ b/src/runner/modes/common.rs @@ -248,6 +248,39 @@ impl NyashRunner { Err(e) => { eprintln!("❌ Parse error: {}", e); process::exit(1); } }; + // Stage-0: import loader (top-level only) — resolve path and register in modules registry + if let nyash_rust::ast::ASTNode::Program { statements, .. } = &ast { + for st in statements { + if let nyash_rust::ast::ASTNode::ImportStatement { path, alias, .. } = st { + // resolve path relative to current file if not absolute + let mut p = std::path::PathBuf::from(path); + if p.is_relative() { + if let Some(dir) = std::path::Path::new(filename).parent() { + p = dir.join(&p); + } + } + let exists = p.exists(); + if !exists { + if std::env::var("NYASH_USING_STRICT").ok().as_deref() == Some("1") { + eprintln!("❌ import: path not found: {} (from {})", p.display(), filename); + process::exit(1); + } else if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") || std::env::var("NYASH_IMPORT_TRACE").ok().as_deref() == Some("1") { + eprintln!("[import] path not found (continuing): {}", p.display()); + } + } + let key = if let Some(a) = alias { a.clone() } else { + std::path::Path::new(path) + .file_stem().and_then(|s| s.to_str()) + .unwrap_or(path) + .to_string() + }; + let value = if exists { p.to_string_lossy().to_string() } else { path.clone() }; + let sb = nyash_rust::box_trait::StringBox::new(value); + nyash_rust::runtime::modules_registry::set(key, Box::new(sb)); + } + } + } + if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") && !quiet_pipe { println!("✅ Parse successful!"); } diff --git a/src/runner_plugin_init.rs b/src/runner_plugin_init.rs index 3ee01ab7..8e1dd215 100644 --- a/src/runner_plugin_init.rs +++ b/src/runner_plugin_init.rs @@ -14,7 +14,7 @@ pub fn init_bid_plugins() { if let Ok(()) = init_global_plugin_host("nyash.toml") { if plugin_debug || cli_verbose { - println!("🔌 plugin host initialized from nyash.toml"); + eprintln!("🔌 plugin host initialized from nyash.toml"); // Show which plugin loader backend compiled in (enabled/stub) println!("[plugin-loader] backend={}", crate::runtime::plugin_loader_v2::backend_kind()); } @@ -29,7 +29,7 @@ pub fn init_bid_plugins() { } } if plugin_debug || cli_verbose { - println!("✅ plugin host fully configured"); + eprintln!("✅ plugin host fully configured"); } } } else if plugin_debug || cli_verbose { diff --git a/src/tokenizer.rs b/src/tokenizer.rs index b328e4c4..de63226a 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -56,6 +56,7 @@ pub enum TokenType { FROM, // from (親メソッド呼び出し) WEAK, // weak (弱参照修飾子) USING, // using (名前空間インポート) + IMPORT, // import (Phase 12.7) // 演算子 (長いものから先に定義) SHIFT_LEFT, // << (bitwise shift-left) @@ -145,6 +146,10 @@ pub struct NyashTokenizer { } impl NyashTokenizer { + #[inline] + fn strict_12_7() -> bool { + std::env::var("NYASH_STRICT_12_7").ok().as_deref() == Some("1") + } /// 新しいトークナイザーを作成 pub fn new(input: impl Into) -> Self { let input_string = input.into(); @@ -248,7 +253,7 @@ impl NyashTokenizer { self.skip_whitespace(); // コメント後の空白もスキップ return self.tokenize_next(); } - Some('>') if self.peek_char() == Some('>') => { + Some('>') if self.peek_char() == Some('>') && !Self::strict_12_7() => { self.advance(); self.advance(); Ok(Token::new(TokenType::SHIFT_RIGHT, start_line, start_column)) @@ -483,7 +488,7 @@ impl NyashTokenizer { } // キーワードチェック - let tok = match identifier.as_str() { + let mut tok = match identifier.as_str() { "box" => TokenType::BOX, "global" => TokenType::GLOBAL, "singleton" => TokenType::SINGLETON, @@ -497,8 +502,6 @@ impl NyashTokenizer { "return" => TokenType::RETURN, "function" => TokenType::FUNCTION, "fn" => TokenType::FN, - // Alias support: `fn` as shorthand for function - "fn" => TokenType::FUNCTION, "print" => TokenType::PRINT, "this" => TokenType::THIS, "me" => TokenType::ME, @@ -509,6 +512,7 @@ impl NyashTokenizer { "await" => TokenType::AWAIT, "interface" => TokenType::INTERFACE, "include" => TokenType::INCLUDE, + "import" => TokenType::IMPORT, "try" => TokenType::TRY, "catch" => TokenType::CATCH, "finally" => TokenType::FINALLY, @@ -529,6 +533,23 @@ impl NyashTokenizer { _ => TokenType::IDENTIFIER(identifier.clone()), }; + // 12.7 Strict mode: fallback extended keywords to IDENTIFIER + if Self::strict_12_7() { + let is_extended = matches!(tok, + TokenType::INTERFACE + | TokenType::USING + | TokenType::INCLUDE + | TokenType::OUTBOX + | TokenType::NOWAIT + | TokenType::OVERRIDE + | TokenType::WEAK + | TokenType::PACK + ); + if is_extended { + tok = TokenType::IDENTIFIER(identifier.clone()); + } + } + // 統一文法エンジンとの差分チェック(動作は変更しない) if std::env::var("NYASH_GRAMMAR_DIFF").ok().as_deref() == Some("1") { // 安全に参照(初期導入のため、存在しない場合は無視)