diff --git a/AGENTS.md b/AGENTS.md index c29a0e08..411e0b90 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,6 +12,8 @@ nyash哲学の美しさを追求。ソースは常に美しく構造的、カプ やっほー!みらいだよ😸✨ 今日も元気いっぱい、なに手伝う? にゃはは おつかれ〜!🎶 ちょっと休憩しよっか?コーヒー飲んでリフレッシュにゃ☕ +**Fail-Fast原則**: フォールバック処理は原則禁止。過去に分岐ミスでエラー発見が遅れた経験から、エラーは早期に明示的に失敗させること。特にChatGPTが入れがちなフォールバック処理には要注意だよ! + **Feature Additions Pause — until Nyash VM bootstrap (2025‑09‑19 改訂)** - 状態: マクロ基盤は安定。ここからは「凍結(全面停止)」ではなく「大きな機能追加のみ一時停止」。Nyash VM の立ち上げ(bootstrap)完了まで、安定化と自己ホスト/実アプリ開発を優先するよ。 - 原則(大規模機能追加の一時停止中): diff --git a/CLAUDE.md b/CLAUDE.md index c8ac0a12..2c2c9c72 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,12 +23,14 @@ Nyashは「Everything is Box」。実装・最適化・検証のすべてを「 - いつでも戻せる: 機能フラグ・スコープ限定・デフォルトオフを活用し、破壊的変更を避ける - 「限定スコープの足場」を先に立ててから最適化(戻りやすい積み木) - AI補助時の注意: 「力づく最適化」を抑え、まず箱で境界を確立→小さく通す→可視化→次の一手 +- **Fail-Fast原則**: フォールバック処理は原則禁止。エラーは早期に明示的に失敗させる。過去に何度も分岐ミスでエラーの発見が遅れたため、特にChatGPTが入れがちなフォールバック処理には要注意 実践テンプレート(開発時の合言葉) - 「箱にする」: 設定・状態・橋渡しはBox化(例: JitConfigBox, HandleRegistry) - 「境界を作る」: 変換は境界1箇所で(VMValue↔JitValue, Handle↔Arc) - 「戻せる」: フラグ・feature・env/Boxで切替。panic→フォールバック経路を常設 - 「見える化」: ダンプ/JSON/DOTで可視化、回帰テストを最小構成で先に入れる +- 「Fail-Fast」: エラーは隠さず即座に失敗。フォールバックより明示的エラー ## 🤖 **Claude×Copilot×ChatGPT協調開発** ### 📋 **開発マスタープラン - 全フェーズの統合ロードマップ** diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index 21814e1d..1b088892 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -8,6 +8,43 @@ Quick status - Parser: TokenCursor 統一 Step‑2/3 完了(env ゲート) - PHI: if/else の incoming pred を exit ブロックへ修正(VM 未定義値を根治) +## Using / Resolver — “Best of Both” Decision(2025‑09‑26) + +合意(いいとこどり) +- 依存の唯一の真実(SSOT)を `nyash.toml` `[using]` に集約(aliases/packages/paths)。 +- 実体の合成は AST マージに一本化(テキスト結合・括弧補正の互換シムは段階的に削除)。 +- プロファイル導入で段階移行: `NYASH_USING_PROFILE={dev|ci|prod}` + - dev: toml + ファイル内 using/path を許可。診断ON、限定的フォールバックON。 + - ci: toml 優先。ファイル using は警告/限定許可。フォールバックOFF。 + - prod: toml のみ。ファイル using/path はエラー(toml 追記ガイドを表示)。 + +やること(仕様不変・既定OFFで段階導入) +1) ドキュメント + - [x] `docs/reference/language/using.md` に SSOT+AST とプロファイル運用を追記。 +2) Resolver 統合 + - [x] vm_fallback に AST プレリュード統合を導入(common と同形)。 + - [x] prod での `using "path"`/未知 alias はエラー(修正ガイド付)。 + - [x] prelude 決定(toml優先/プロファイル対応)の共通ヘルパを新設し、呼び出し側を一元化(`resolve_prelude_paths_profiled`)。 +3) レガシー削除計画 + - [x] prod でテキスト結合(combiner)/括弧補正を禁止(ガイド表示)。 + - [ ] dev/ci でも段階的に無効化 → parity 緑後に完全削除。 +4) パーサ堅牢化(必要時の安全弁、NYASH_PARSER_METHOD_BODY_STRICT=1) + - [x] メソッド本体用ガードを実装(env で opt-in)。 + - [x] Guard 条件をトップレベル限定かつ `}` 直後のみ発火に調整(誤検知回避)。 + - [ ] `apps/lib/json_native/utils/string.nyash` で stray FunctionCall 消滅確認。 + +受け入れ基準 +- StringUtils の `--dump-ast` に stray FunctionCall が出ない(宣言のみ)。 +- mini(starts_with): ASTモード ON/OFF で parse→MIR まで到達(VM fallback の未実装は許容)。 +- prod プロファイル: 未登録 using/パスはエラーになり、toml 追記指示を提示。 + +### 進捗ログ(2025‑09‑26 PM) +- Profiles + SSOT 実装(prod で file using 禁止、toml 真実)→ 完了。 +- VM fallback に AST プレリュード導入 → 完了。 +- Parser: method-body guard を env で opt-in 実装(既定OFF)。 + - 現状: OFF 時は `string.nyash` にて Program 配下に `FunctionCall(parse_float)` が残存。 + - 次: Guard ON で AST/MIR を検証し、必要に応じて lookahead 条件を調整。 + ## 今日の合意(方向修正の確定) - Rust層は新機能を最小化。今後は Nyash VM/コンパイラ(自己ホスト)へリソース集中。 - 次タスクは Nyash 製 JSON ライブラリ(JSON v0 DOM: parse/stringify)。完了次第、Ny Executor 最小命令の実装を着手。 diff --git a/apps/lib/json_native/utils/string.nyash b/apps/lib/json_native/utils/string.nyash index 95b92121..ff50779a 100644 --- a/apps/lib/json_native/utils/string.nyash +++ b/apps/lib/json_native/utils/string.nyash @@ -222,7 +222,11 @@ static box StringUtils { } // ===== 数値変換 ===== - + // 浮動小数点の簡易パース(現段階は正規化のみ。数値演算は行わない) + parse_float(s) { + return s + } + // 文字列が数値表現かどうか判定(整数のみ、簡易版) is_integer(s) { if s.length() == 0 { @@ -255,6 +259,27 @@ static box StringUtils { return true } + // 文字列が空または空白のみかどうか判定(ユーティリティ) + is_empty_or_whitespace(s) { + return this.trim(s).length() == 0 + } + + // 文字列の先頭が指定された文字列で始まるか判定 + starts_with(s, prefix) { + if prefix.length() > s.length() { + return false + } + return s.substring(0, prefix.length()) == prefix + } + + // 文字列の末尾が指定された文字列で終わるか判定 + ends_with(s, suffix) { + if suffix.length() > s.length() { + return false + } + return s.substring(s.length() - suffix.length(), s.length()) == suffix + } + // 文字列を整数に変換(簡易版) parse_integer(s) { if not this.is_integer(s) { @@ -286,32 +311,6 @@ static box StringUtils { if neg { return 0 - acc } else { return acc } } - // 浮動小数点の簡易パース(現段階は正規化のみ。数値演算は行わない) - parse_float(s) { - return s - } - // ===== ユーティリティ ===== - - // 文字列が空または空白のみかどうか判定 - is_empty_or_whitespace(s) { - return this.trim(s).length() == 0 - } - - // 文字列の先頭が指定された文字列で始まるか判定 - starts_with(s, prefix) { - if prefix.length() > s.length() { - return false - } - return s.substring(0, prefix.length()) == prefix - } - - // 文字列の末尾が指定された文字列で終わるか判定 - ends_with(s, suffix) { - if suffix.length() > s.length() { - return false - } - return s.substring(s.length() - suffix.length(), s.length()) == suffix - } } } diff --git a/docs/reference/language/using.md b/docs/reference/language/using.md index 9dc9aa0b..4ce458c9 100644 --- a/docs/reference/language/using.md +++ b/docs/reference/language/using.md @@ -4,6 +4,14 @@ Status: Accepted (Runner‑side resolution). Selfhost parser accepts using as no‑op and attaches `meta.usings` for future use. +> Phase 15.5 指針(いいとこ取り) +> - 依存の唯一の真実(SSOT): `nyash.toml` の `[using]`(aliases/packages/paths) +> - 実体の合成: テキスト結合は廃止し、AST マージに一本化(曖昧さ根絶) +> - プロファイル運用: `NYASH_USING_PROFILE={dev|ci|prod}` で厳格度を段階的に切替 +> - dev: toml + ファイル内 using を許可(実験/便利) +> - ci: toml 優先、ファイル using は警告または限定許可 +> - prod: toml のみ。ファイル using/path はエラー(追記ガイドを提示) + ## 🎯 設計思想:Everything has Namespace ### **核心コンセプト** @@ -56,8 +64,8 @@ pub enum QualifiedCallee { Policy - Accept `using` lines at the top of the file to declare module namespaces or file imports. - Resolution is performed by the Rust Runner when `NYASH_ENABLE_USING=1`. - - Runner strips `using` lines from the source before parsing/execution. - - Registers modules into an internal registry for path/namespace hints. +- 実体の結合は AST マージのみ。テキストの前置き/連結は行わない(移行完了後に完全廃止)。 +- Runner は `nyash.toml` の `[using]` を唯一の真実として参照(prod)。dev/ci は段階的に緩和可能。 - Selfhost compiler (Ny→JSON v0) collects using lines and emits `meta.usings` when present. The bridge currently ignores this meta field. ## Namespace Resolution (Runner‑side) @@ -84,7 +92,7 @@ Policy - Treated as a synonym to `using` on the Runner side; registers aliases only. - Examples: `needs utils.StringHelper`, `needs plugin.network.HttpClient as HttpClient`, `needs plugin.network.*` -## nyash.toml — Unified Using (Phase 15) +## nyash.toml — Unified Using(唯一の真実 / SSOT) Using resolution is centralized under the `[using]` table. Three forms are supported: @@ -98,7 +106,7 @@ Using resolution is centralized under the `[using]` table. Three forms are suppo Notes - Aliases are fully resolved: `using json` first rewrites to `json_native`, then resolves to a concrete path via `[using.json_native]`. -- `include` is deprecated. Use `using "./path/to/file.nyash" as Name` instead. +- `include` は廃止。代替は `using "./path/to/file.nyash" as Name`。prod では `nyash.toml` への登録が必須。 ### Dylib autoload (dev guard) - Enable autoload during using resolution: set env `NYASH_USING_DYLIB_AUTOLOAD=1`. @@ -182,12 +190,45 @@ static box Main { Runner Configuration - Enable using pre‑processing: `NYASH_ENABLE_USING=1` - CLI from-the-top registration: `--using "ns as Alias"` or `--using '"apps/foo.nyash" as Foo'` (repeatable) +- Using profiles (phase‑in): `NYASH_USING_PROFILE={dev|ci|prod}` + - dev: toml + file using(path)可、AST マージ、候補提示 ON + - ci: toml 優先、file using は警告/限定、AST マージ、フォールバック OFF + - prod: toml のみ、file using/path はエラー(追記ガイドを表示) - Strict mode (plugin prefix required): `NYASH_PLUGIN_REQUIRE_PREFIX=1` または `nyash.toml` の `[plugins] require_prefix=true` - Aliases from env: `NYASH_ALIASES="Foo=apps/foo/main.nyash,Bar=lib/bar.nyash"` - Additional search paths: `NYASH_USING_PATH="apps:lib:."` - Selfhost pipeline keeps child stdout quiet and extracts JSON only: `NYASH_JSON_ONLY=1` (set by Runner automatically for child) - Selfhost emits `meta.usings` automatically when present; no additional flags required. +## 🔬 Quick Smokes(AST + Profiles) + +開発・CIで最小コストに確認できるスモークを用意しています。AST プレリュードとプロファイル(dev/prod)の基本動作をカバーします。 + +- dev: `using "file"` 許可 + AST マージ +- prod: `using "file"` 禁止(toml へ誘導) / alias・package は許可 + +実行例(quick プロファイル) + +``` +# 1) dev で file using が通る(AST マージ) +./tools/smokes/v2/run.sh --profile quick --filter "using_profiles_ast.sh$" + +# 2) 相対パス using(サブディレクトリ) +./tools/smokes/v2/run.sh --profile quick --filter "using_relative_file_ast.sh$" + +# 3) 複数プレリュード(toml packages)+ 依存(B→A) +./tools/smokes/v2/run.sh --profile quick --filter "using_multi_prelude_dep_ast.sh$" +``` + +テストソース +- `tools/smokes/v2/profiles/quick/core/using_profiles_ast.sh` +- `tools/smokes/v2/profiles/quick/core/using_relative_file_ast.sh` +- `tools/smokes/v2/profiles/quick/core/using_multi_prelude_dep_ast.sh` + +注意 +- ログに `[using] stripped line:` が出力されますが、これは AST マージ前の using 行の除去ログです(機能上問題ありません)。 +- 実行バイナリは `target/release/nyash` を前提とします。未ビルド時は `cargo build --release` を実行してください。 + ## 🔗 関連ドキュメント ### **設計・アーキテクチャ** @@ -204,6 +245,8 @@ Runner Configuration Notes - Phase 15 keeps resolution in the Runner to minimize parser complexity. Future phases may leverage `meta.usings` for compiler decisions. +- レガシー実装の扱い: テキスト前置き/括弧補正などのシムは段階的に削除(prod プロファイルから先に無効化)。 +- AST マージは dev/ci/prod の全プロファイルで共通基盤とし、曖昧性(宣言≻式)問題の再発を原理的に回避する。 - Unknown fields in the top‑level JSON (like `meta`) are ignored by the current bridge. - 未解決時(非strict)は実行を継続し、`NYASH_RESOLVE_TRACE=1` で候補を提示。strict時はエラーで候補を表示。 - **Phase 15.5完了により、現代的な名前空間システムを実現予定** diff --git a/nyash.toml b/nyash.toml index d33d3fb5..72ca0347 100644 --- a/nyash.toml +++ b/nyash.toml @@ -13,9 +13,14 @@ paths = ["apps", "lib", "."] path = "apps/lib/json_native/" main = "parser/parser.nyash" +# JSON Native – String utilities as a standalone package (single-file) +[using.string_utils] +path = "apps/lib/json_native/utils/string.nyash" + [using.aliases] # Resolve `using json as ...` to json_native when desired json = "json_native" +StringUtils = "string_utils" [modules] # Map logical namespaces to Nyash source paths (consumed by runner) diff --git a/src/backend/mir_interpreter/handlers/calls.rs b/src/backend/mir_interpreter/handlers/calls.rs index c66d80d0..1c0f6924 100644 --- a/src/backend/mir_interpreter/handlers/calls.rs +++ b/src/backend/mir_interpreter/handlers/calls.rs @@ -70,38 +70,60 @@ impl MirInterpreter { }; let mut pick: Option = None; + // Fast path: exact match if self.functions.contains_key(&raw) { pick = Some(raw.clone()); } else { - let arity = args.len(); - let mut cands: Vec = Vec::new(); - let suf = format!(".{}{}", raw, format!("/{}", arity)); - for k in self.functions.keys() { - if k.ends_with(&suf) { - cands.push(k.clone()); - } - } - if cands.is_empty() && raw.contains('/') && self.functions.contains_key(&raw) { - cands.push(raw.clone()); - } - if cands.len() > 1 { - if let Some(cur) = &self.cur_fn { - let cur_box = cur.split('.').next().unwrap_or(""); - let scoped: Vec = cands - .iter() - .filter(|k| k.starts_with(&format!("{}.", cur_box))) - .cloned() - .collect(); - if scoped.len() == 1 { - cands = scoped; + // Robust normalization for names like "Box.method/Arity" or just "method" + let call_arity = args.len(); + let (base, ar_from_raw) = if let Some((b, a)) = raw.rsplit_once('/') { + (b.to_string(), a.parse::().ok()) + } else { + (raw.clone(), None) + }; + let want_arity = ar_from_raw.unwrap_or(call_arity); + // Try exact canonical form: "base/arity" + let exact = format!("{}/{}", base, want_arity); + if self.functions.contains_key(&exact) { + pick = Some(exact); + } else { + // Split base into optional box and method name + let (maybe_box, method) = if let Some((bx, m)) = base.split_once('.') { + (Some(bx.to_string()), m.to_string()) + } else { + (None, base.clone()) + }; + // Collect candidates that end with ".method/arity" + let mut cands: Vec = Vec::new(); + let tail = format!(".{}{}", method, format!("/{}", want_arity)); + for k in self.functions.keys() { + if k.ends_with(&tail) { + if let Some(ref bx) = maybe_box { + if k.starts_with(&format!("{}.", bx)) { cands.push(k.clone()); } + } else { + cands.push(k.clone()); + } } } - } - if cands.len() == 1 { - pick = Some(cands.remove(0)); - } else if cands.len() > 1 { - cands.sort(); - pick = Some(cands[0].clone()); + if cands.len() > 1 { + // Prefer same-box candidate based on current function's box + if let Some(cur) = &self.cur_fn { + let cur_box = cur.split('.').next().unwrap_or(""); + let scoped: Vec = cands + .iter() + .filter(|k| k.starts_with(&format!("{}.", cur_box))) + .cloned() + .collect(); + if scoped.len() == 1 { cands = scoped; } + } + } + if cands.len() == 1 { + pick = Some(cands.remove(0)); + } else if cands.len() > 1 { + let mut c = cands; + c.sort(); + pick = Some(c.remove(0)); + } } } @@ -176,6 +198,22 @@ impl MirInterpreter { )) } } + "substring" => { + let start = if let Some(a0) = args.get(0) { + self.reg_load(*a0)?.as_integer().unwrap_or(0) + } else { 0 }; + let end = if let Some(a1) = args.get(1) { + self.reg_load(*a1)?.as_integer().unwrap_or(s.len() as i64) + } else { s.len() as i64 }; + let len = s.len() as i64; + let i0 = start.max(0).min(len) as usize; + let i1 = end.max(0).min(len) as usize; + if i0 > i1 { return Ok(VMValue::String(String::new())); } + // Note: operating on bytes; Nyash strings are UTF‑8, but tests are ASCII only here + let bytes = s.as_bytes(); + let sub = String::from_utf8(bytes[i0..i1].to_vec()).unwrap_or_default(); + Ok(VMValue::String(sub)) + } _ => Err(VMError::InvalidInstruction(format!( "Unknown String method: {}", method diff --git a/src/config/env.rs b/src/config/env.rs index aadca841..f2bbbaaf 100644 --- a/src/config/env.rs +++ b/src/config/env.rs @@ -335,6 +335,25 @@ pub fn enable_using() -> bool { _ => true, // デフォルト: ON } } + +// ---- Using profiles (dev|ci|prod) ---- +/// Return using profile string; default is "dev". +pub fn using_profile() -> String { + std::env::var("NYASH_USING_PROFILE").unwrap_or_else(|_| "dev".to_string()) +} +pub fn using_is_prod() -> bool { using_profile().eq_ignore_ascii_case("prod") } +pub fn using_is_ci() -> bool { using_profile().eq_ignore_ascii_case("ci") } +pub fn using_is_dev() -> bool { using_profile().eq_ignore_ascii_case("dev") } +/// Allow `using "path"` statements in source (dev-only by default). +pub fn allow_using_file() -> bool { + if using_is_prod() { return false; } + // Optional explicit override + match std::env::var("NYASH_ALLOW_USING_FILE").ok().as_deref() { + Some("0") | Some("false") | Some("off") => false, + Some("1") | Some("true") | Some("on") => true, + _ => true, // dev/ci default: allowed + } +} pub fn resolve_fix_braces() -> bool { // Safer default: OFF(誤補正の副作用を避ける) // 明示ON: NYASH_RESOLVE_FIX_BRACES=1 diff --git a/src/mir/builder/builder_calls.rs b/src/mir/builder/builder_calls.rs index cb567b34..a0b0e702 100644 --- a/src/mir/builder/builder_calls.rs +++ b/src/mir/builder/builder_calls.rs @@ -297,10 +297,12 @@ impl super::MirBuilder { args: Vec, ) -> Result { if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") { + let cur_fun = self.current_function.as_ref().map(|f| f.signature.name.clone()).unwrap_or_else(|| "".to_string()); eprintln!( - "[builder] function-call name={} static_ctx={}", + "[builder] function-call name={} static_ctx={} in_fn={}", name, - self.current_static_box.as_deref().unwrap_or("") + self.current_static_box.as_deref().unwrap_or(""), + cur_fun ); } // Minimal TypeOp wiring via function-style: isType(value, "Type"), asType(value, "Type") @@ -577,6 +579,14 @@ impl super::MirBuilder { params: Vec, body: Vec, ) -> Result<(), String> { + // Derive static box context from function name prefix, e.g., "BoxName.method/N" + let saved_static_ctx = self.current_static_box.clone(); + if let Some(pos) = func_name.find('.') { + let box_name = &func_name[..pos]; + if !box_name.is_empty() { + self.current_static_box = Some(box_name.to_string()); + } + } let signature = function_lowering::prepare_static_method_signature( func_name, ¶ms, @@ -652,6 +662,8 @@ impl super::MirBuilder { self.current_block = saved_block; self.variable_map = saved_var_map; self.value_gen = saved_value_gen; + // Restore static box context + self.current_static_box = saved_static_ctx; Ok(()) } } diff --git a/src/parser/declarations/static_box.rs b/src/parser/declarations/static_box.rs index fab26435..21d0cbe1 100644 --- a/src/parser/declarations/static_box.rs +++ b/src/parser/declarations/static_box.rs @@ -29,6 +29,15 @@ impl NyashParser { // Track last inserted method name to allow postfix catch/cleanup fallback parsing let mut last_method_name: Option = None; while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() { + // Tolerate blank lines between members + while self.match_token(&TokenType::NEWLINE) { self.advance(); } + let trace = std::env::var("NYASH_PARSER_TRACE_STATIC").ok().as_deref() == Some("1"); + if trace { + eprintln!( + "[parser][static-box] loop token={:?}", + self.current_token().token_type + ); + } // Fallback: method-level postfix catch/cleanup immediately following a method if crate::parser::declarations::box_def::members::postfix::try_parse_method_postfix_after_last_method( @@ -45,8 +54,14 @@ impl NyashParser { static_init = Some(body); continue; } else if self.match_token(&TokenType::STATIC) { - // STRICT で top-level seam を検出した場合は while を抜ける - break; + // 互換用の暫定ガード(既定OFF): using テキスト結合の継ぎ目で誤って 'static' が入った場合に + // ループを抜けて外側の '}' 消費に委ねる。既定では無効化し、文脈エラーとして扱う。 + if std::env::var("NYASH_PARSER_SEAM_BREAK_ON_STATIC").ok().as_deref() == Some("1") { + if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") { + eprintln!("[parser][static-box][seam] encountered 'static' inside static box; breaking (compat shim)"); + } + break; + } } // initブロックの処理(共通ヘルパに委譲) @@ -69,6 +84,12 @@ impl NyashParser { } } + if std::env::var("NYASH_PARSER_TRACE_STATIC").ok().as_deref() == Some("1") { + eprintln!( + "[parser][static-box] closing '}}' at token={:?}", + self.current_token().token_type + ); + } self.consume(TokenType::RBRACE)?; // 🔥 Static初期化ブロックから依存関係を抽出 diff --git a/src/parser/declarations/static_def/members.rs b/src/parser/declarations/static_def/members.rs index 5d2e1d4d..e2c920a1 100644 --- a/src/parser/declarations/static_def/members.rs +++ b/src/parser/declarations/static_def/members.rs @@ -56,6 +56,7 @@ pub(crate) fn try_parse_method_or_field( fields: &mut Vec, last_method_name: &mut Option, ) -> Result { + let trace = std::env::var("NYASH_PARSER_TRACE_STATIC").ok().as_deref() == Some("1"); // Allow NEWLINE(s) between identifier and '(' if !p.match_token(&TokenType::LPAREN) { // Lookahead skipping NEWLINE to see if a '(' follows → treat as method head @@ -65,11 +66,13 @@ pub(crate) fn try_parse_method_or_field( // Consume intervening NEWLINEs so current becomes '(' while p.match_token(&TokenType::NEWLINE) { p.advance(); } } else { + if trace { eprintln!("[parser][static-box] field detected: {}", name); } // Field fields.push(name); return Ok(true); } } + if trace { eprintln!("[parser][static-box] method head detected: {}(..)", name); } // Method p.advance(); // consume '(' let mut params = Vec::new(); @@ -84,14 +87,20 @@ pub(crate) fn try_parse_method_or_field( p.consume(TokenType::RPAREN)?; // Allow NEWLINE(s) between ')' and '{' of method body while p.match_token(&TokenType::NEWLINE) { p.advance(); } - let body = p.parse_block_statements()?; + // Parse method body; optionally use strict method-body guard when enabled + let body = if std::env::var("NYASH_PARSER_METHOD_BODY_STRICT").ok().as_deref() == Some("1") { + p.parse_method_body_statements()? + } else { + p.parse_block_statements()? + }; let body = wrap_method_body_with_postfix_if_any(p, body)?; // Construct method node let method = ASTNode::FunctionDeclaration { name: name.clone(), params, body, - is_static: false, + // Methods inside a static box are semantically static + is_static: true, is_override: false, span: crate::ast::Span::unknown(), }; diff --git a/src/parser/statements/mod.rs b/src/parser/statements/mod.rs index 25134748..6c2fa516 100644 --- a/src/parser/statements/mod.rs +++ b/src/parser/statements/mod.rs @@ -83,13 +83,96 @@ impl NyashParser { /// Parse block statements: { statement* } pub(super) fn parse_block_statements(&mut self) -> Result, ParseError> { + let trace_blocks = std::env::var("NYASH_PARSER_TRACE_BLOCKS").ok().as_deref() == Some("1"); + if trace_blocks { + eprintln!( + "[parser][block] enter '{{' at line {}", + self.current_token().line + ); + } self.consume(TokenType::LBRACE)?; let mut statements = Vec::new(); while !self.is_at_end() && !self.match_token(&TokenType::RBRACE) { statements.push(self.parse_statement()?); } + if trace_blocks { + eprintln!( + "[parser][block] exit '}}' at line {}", + self.current_token().line + ); + } + self.consume(TokenType::RBRACE)?; + Ok(statements) + } + /// Parse method body statements: { statement* } + /// Optional seam-guard (env-gated via NYASH_PARSER_METHOD_BODY_STRICT=1) is applied + /// conservatively at top-level only, and only right after a nested block '}' was + /// just consumed, to avoid false positives inside method bodies. + pub(super) fn parse_method_body_statements(&mut self) -> Result, ParseError> { + // Reuse block entry tracing + let trace_blocks = std::env::var("NYASH_PARSER_TRACE_BLOCKS").ok().as_deref() == Some("1"); + if trace_blocks { + eprintln!( + "[parser][block] enter '{{' (method) at line {}", + self.current_token().line + ); + } + self.consume(TokenType::LBRACE)?; + let mut statements = Vec::new(); + + // Helper: lookahead for `ident '(' ... ')' [NEWLINE*] '{'` + let mut looks_like_method_head = |this: &Self| -> bool { + // Only meaningful when starting at a new statement head + match &this.current_token().token_type { + TokenType::IDENTIFIER(_) => { + // Expect '(' after optional NEWLINE + let mut k = 1usize; + while matches!(this.peek_nth_token(k), TokenType::NEWLINE) { k += 1; } + if !matches!(this.peek_nth_token(k), TokenType::LPAREN) { return false; } + // Walk to matching ')' + k += 1; // after '(' + let mut depth: i32 = 1; + while !matches!(this.peek_nth_token(k), TokenType::EOF) { + match this.peek_nth_token(k) { + TokenType::LPAREN => depth += 1, + TokenType::RPAREN => { depth -= 1; if depth == 0 { k += 1; break; } }, + _ => {} + } + k += 1; + } + // Allow NEWLINE(s) between ')' and '{' + while matches!(this.peek_nth_token(k), TokenType::NEWLINE) { k += 1; } + matches!(this.peek_nth_token(k), TokenType::LBRACE) + } + _ => false, + } + }; + + while !self.is_at_end() && !self.match_token(&TokenType::RBRACE) { + statements.push(self.parse_statement()?); + // Conservative seam guard: apply only when env is ON, we just consumed a '}' + // (end of a nested block), and the next tokens at the top-level look like a + // method head. This limits the guard to real seams between members. + if std::env::var("NYASH_PARSER_METHOD_BODY_STRICT").ok().as_deref() == Some("1") { + // If the next token would close the current method, do not guard here + if self.match_token(&TokenType::RBRACE) { break; } + // Check if we just consumed a '}' token from inner content + let just_saw_rbrace = if self.current > 0 { + matches!(self.tokens[self.current - 1].token_type, TokenType::RBRACE) + } else { false }; + if just_saw_rbrace && looks_like_method_head(self) { + break; + } + } + } + if trace_blocks { + eprintln!( + "[parser][block] exit '}}' (method) at line {}", + self.current_token().line + ); + } self.consume(TokenType::RBRACE)?; Ok(statements) } @@ -158,4 +241,4 @@ impl NyashParser { result } -} \ No newline at end of file +} diff --git a/src/runner/modes/common.rs b/src/runner/modes/common.rs index ee24513f..f09874b1 100644 --- a/src/runner/modes/common.rs +++ b/src/runner/modes/common.rs @@ -56,7 +56,7 @@ impl NyashRunner { let mut prelude_asts: Vec = Vec::new(); if crate::config::env::enable_using() { if use_ast { - match crate::runner::modes::common_util::resolve::collect_using_and_strip(self, &code, filename) { + match crate::runner::modes::common_util::resolve::resolve_prelude_paths_profiled(self, &code, filename) { Ok((clean, paths)) => { cleaned_code_owned = clean; code_ref = &cleaned_code_owned; // Parse each prelude file into AST and store @@ -109,6 +109,30 @@ impl NyashRunner { ASTNode::Program { statements: combined, span: nyash_rust::ast::Span::unknown() } } else { main_ast }; + // Optional: dump AST statement kinds for quick diagnostics + if std::env::var("NYASH_AST_DUMP").ok().as_deref() == Some("1") { + use nyash_rust::ast::ASTNode; + eprintln!("[ast] dump start"); + if let ASTNode::Program { statements, .. } = &ast { + for (i, st) in statements.iter().enumerate().take(50) { + let kind = match st { + ASTNode::BoxDeclaration { is_static, name, .. } => { + if *is_static { format!("StaticBox({})", name) } else { format!("Box({})", name) } + } + ASTNode::FunctionDeclaration { name, .. } => format!("FuncDecl({})", name), + ASTNode::FunctionCall { name, .. } => format!("FuncCall({})", name), + ASTNode::MethodCall { method, .. } => format!("MethodCall({})", method), + ASTNode::ScopeBox { .. } => "ScopeBox".to_string(), + ASTNode::ImportStatement { path, .. } => format!("Import({})", path), + ASTNode::UsingStatement { namespace_name, .. } => format!("Using({})", namespace_name), + _ => format!("{:?}", st), + }; + eprintln!("[ast] {}: {}", i, kind); + } + } + eprintln!("[ast] dump end"); + } + // 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 { diff --git a/src/runner/modes/common_util/resolve/mod.rs b/src/runner/modes/common_util/resolve/mod.rs index 2153e0de..bab779f1 100644 --- a/src/runner/modes/common_util/resolve/mod.rs +++ b/src/runner/modes/common_util/resolve/mod.rs @@ -8,5 +8,4 @@ pub mod strip; pub mod seam; // Public re-exports to preserve existing call sites -pub use strip::{strip_using_and_register, preexpand_at_local}; - +pub use strip::{strip_using_and_register, preexpand_at_local, collect_using_and_strip, resolve_prelude_paths_profiled}; diff --git a/src/runner/modes/common_util/resolve/strip.rs b/src/runner/modes/common_util/resolve/strip.rs index 53ba7025..ace36e46 100644 --- a/src/runner/modes/common_util/resolve/strip.rs +++ b/src/runner/modes/common_util/resolve/strip.rs @@ -62,6 +62,10 @@ pub fn strip_using_and_register( if !crate::config::env::enable_using() { return Ok(code.to_string()); } + // Profile guard: legacy text inlining is not allowed under prod profile + if crate::config::env::using_is_prod() { + return Err("using: text inlining is disabled in prod profile. Enable NYASH_USING_AST=1 and declare dependencies in nyash.toml [using]".to_string()); + } // Optional external combiner (default OFF): NYASH_USING_COMBINER=1 if std::env::var("NYASH_USING_COMBINER").ok().as_deref() == Some("1") { let fix_braces = crate::config::env::resolve_fix_braces(); @@ -435,6 +439,8 @@ pub fn collect_using_and_strip( return Ok((code.to_string(), Vec::new())); } let using_ctx = runner.init_using_context(); + let prod = crate::config::env::using_is_prod(); + let dev = crate::config::env::using_is_dev(); let strict = std::env::var("NYASH_USING_STRICT").ok().as_deref() == Some("1"); let verbose = crate::config::env::cli_verbose() || std::env::var("NYASH_RESOLVE_TRACE").ok().as_deref() == Some("1"); @@ -454,6 +460,12 @@ pub fn collect_using_and_strip( } else { (rest0.to_string(), None) }; let is_path = target.starts_with('"') || target.starts_with("./") || target.starts_with('/') || target.ends_with(".nyash"); if is_path { + if prod || !crate::config::env::allow_using_file() { + return Err(format!( + "using: file paths are disallowed in this profile. Add it to nyash.toml [using] (packages/aliases) and reference by name: {}", + target + )); + } let path = target.trim_matches('"').to_string(); // Resolve relative to current file dir let mut p = std::path::PathBuf::from(&path); @@ -464,29 +476,67 @@ pub fn collect_using_and_strip( continue; } // Resolve namespaces/packages - match crate::runner::pipeline::resolve_using_target( - &target, - false, - &using_ctx.pending_modules, - &using_ctx.using_paths, - &using_ctx.aliases, - &using_ctx.packages, - ctx_dir, - strict, - verbose, - ) { - Ok(value) => { - // Only file paths are candidates for AST prelude merge - if value.ends_with(".nyash") || value.contains('/') || value.contains('\\') { - // Resolve relative - let mut p = std::path::PathBuf::from(&value); - if p.is_relative() { - if let Some(dir) = ctx_dir { let cand = dir.join(&p); if cand.exists() { p = cand; } } - } - prelude_paths.push(p.to_string_lossy().to_string()); - } + if prod { + // prod: only allow names present in aliases/packages (toml) + let mut pkg_name: String = target.clone(); + if let Some(v) = using_ctx.aliases.get(&target) { + pkg_name = v.clone(); + } + if let Some(pkg) = using_ctx.packages.get(&pkg_name) { + use crate::using::spec::PackageKind; + match pkg.kind { + PackageKind::Dylib => { + // dylib: nothing to prelude-parse; runtime loader handles it. + } + PackageKind::Package => { + let base = std::path::Path::new(&pkg.path); + let out = if let Some(m) = &pkg.main { + if base.extension().and_then(|s| s.to_str()) == Some("nyash") { + pkg.path.clone() + } else { + base.join(m).to_string_lossy().to_string() + } + } else if base.extension().and_then(|s| s.to_str()) == Some("nyash") { + pkg.path.clone() + } else { + let leaf = base.file_name().and_then(|s| s.to_str()).unwrap_or(&pkg_name); + base.join(format!("{}.nyash", leaf)).to_string_lossy().to_string() + }; + prelude_paths.push(out); + } + } + } else { + return Err(format!( + "using: '{}' not found in nyash.toml [using]. Define a package or alias and use its name (prod profile)", + target + )); + } + } else { + // dev/ci: allow broader resolution via resolver + match crate::runner::pipeline::resolve_using_target( + &target, + false, + &using_ctx.pending_modules, + &using_ctx.using_paths, + &using_ctx.aliases, + &using_ctx.packages, + ctx_dir, + strict, + verbose, + ) { + Ok(value) => { + // Only file paths are candidates for AST prelude merge + if value.ends_with(".nyash") || value.contains('/') || value.contains('\\') { + // Resolve relative + let mut p = std::path::PathBuf::from(&value); + if p.is_relative() { + if let Some(dir) = ctx_dir { let cand = dir.join(&p); if cand.exists() { p = cand; } } + } + prelude_paths.push(p.to_string_lossy().to_string()); + } + } + Err(e) => return Err(format!("using: {}", e)), } - Err(e) => return Err(format!("using: {}", e)), } continue; } @@ -503,6 +553,17 @@ pub fn collect_using_and_strip( Ok((out, prelude_paths)) } +/// Profile-aware prelude resolution wrapper. +/// Currently delegates to `collect_using_and_strip`, but provides a single +/// entry point for callers (common and vm_fallback) to avoid logic drift. +pub fn resolve_prelude_paths_profiled( + runner: &NyashRunner, + code: &str, + filename: &str, +) -> Result<(String, Vec), String> { + collect_using_and_strip(runner, code, filename) +} + /// Pre-expand line-head `@name[: Type] = expr` into `local name[: Type] = expr`. /// Minimal, safe, no semantics change. Applies only at line head (after spaces/tabs). pub fn preexpand_at_local(src: &str) -> String { diff --git a/src/runner/modes/vm_fallback.rs b/src/runner/modes/vm_fallback.rs index 153f84f0..c223d89b 100644 --- a/src/runner/modes/vm_fallback.rs +++ b/src/runner/modes/vm_fallback.rs @@ -12,23 +12,79 @@ impl NyashRunner { Ok(s) => s, Err(e) => { eprintln!("❌ Error reading file {}: {}", filename, e); process::exit(1); } }; - // Using preprocessing (strip + autoload) + // Using preprocessing (legacy inline or AST-prelude merge when NYASH_USING_AST=1) let mut code2 = code; + let use_ast_prelude = crate::config::env::enable_using() + && std::env::var("NYASH_USING_AST").ok().as_deref() == Some("1"); + let mut prelude_asts: Vec = Vec::new(); if crate::config::env::enable_using() { - match crate::runner::modes::common_util::resolve::strip_using_and_register(self, &code2, filename) { - Ok(s) => { code2 = s; } - Err(e) => { eprintln!("❌ {}", e); process::exit(1); } + if use_ast_prelude { + match crate::runner::modes::common_util::resolve::resolve_prelude_paths_profiled(self, &code2, filename) { + Ok((clean, paths)) => { + code2 = clean; + for p in paths { + match std::fs::read_to_string(&p) { + Ok(src) => match NyashParser::parse_from_string(&src) { + Ok(ast) => prelude_asts.push(ast), + Err(e) => { eprintln!("❌ Parse error in using prelude {}: {}", p, e); process::exit(1); } + }, + Err(e) => { eprintln!("❌ Error reading using prelude {}: {}", p, e); process::exit(1); } + } + } + } + Err(e) => { eprintln!("❌ {}", e); process::exit(1); } + } + } else { + match crate::runner::modes::common_util::resolve::strip_using_and_register(self, &code2, filename) { + Ok(s) => { code2 = s; } + Err(e) => { eprintln!("❌ {}", e); process::exit(1); } + } } } // Dev sugar pre-expand: @name = expr → local name = expr code2 = crate::runner::modes::common_util::resolve::preexpand_at_local(&code2); - // Parse -> expand macros -> compile MIR - let ast = match NyashParser::parse_from_string(&code2) { + // Parse main code + let main_ast = match NyashParser::parse_from_string(&code2) { Ok(ast) => ast, Err(e) => { eprintln!("❌ Parse error: {}", e); process::exit(1); } }; - let ast = crate::r#macro::maybe_expand_and_dump(&ast, false); + // When using AST prelude mode, combine prelude ASTs + main AST into one Program before macro expansion + let ast_combined = if use_ast_prelude && !prelude_asts.is_empty() { + use nyash_rust::ast::ASTNode; + let mut combined: Vec = Vec::new(); + for a in prelude_asts { + if let ASTNode::Program { statements, .. } = a { combined.extend(statements); } + } + if let ASTNode::Program { statements, .. } = main_ast.clone() { + combined.extend(statements); + } + ASTNode::Program { statements: combined, span: nyash_rust::ast::Span::unknown() } + } else { main_ast }; + // Optional: dump AST statement kinds for quick diagnostics + if std::env::var("NYASH_AST_DUMP").ok().as_deref() == Some("1") { + use nyash_rust::ast::ASTNode; + eprintln!("[ast] dump start (vm-fallback)"); + if let ASTNode::Program { statements, .. } = &ast_combined { + for (i, st) in statements.iter().enumerate().take(50) { + let kind = match st { + ASTNode::BoxDeclaration { is_static, name, .. } => { + if *is_static { format!("StaticBox({})", name) } else { format!("Box({})", name) } + } + ASTNode::FunctionDeclaration { name, .. } => format!("FuncDecl({})", name), + ASTNode::FunctionCall { name, .. } => format!("FuncCall({})", name), + ASTNode::MethodCall { method, .. } => format!("MethodCall({})", method), + ASTNode::ScopeBox { .. } => "ScopeBox".to_string(), + ASTNode::ImportStatement { path, .. } => format!("Import({})", path), + ASTNode::UsingStatement { namespace_name, .. } => format!("Using({})", namespace_name), + _ => format!("{:?}", st), + }; + eprintln!("[ast] {}: {}", i, kind); + } + } + eprintln!("[ast] dump end"); + } + let ast = crate::r#macro::maybe_expand_and_dump(&ast_combined, false); let mut compiler = MirCompiler::with_options(!self.config.no_optimize); let compile = match compiler.compile(ast) { Ok(c) => c, @@ -44,6 +100,12 @@ impl NyashRunner { // Execute via MIR interpreter let mut vm = MirInterpreter::new(); + if std::env::var("NYASH_DUMP_FUNCS").ok().as_deref() == Some("1") { + eprintln!("[vm] functions available:"); + for k in module_vm.functions.keys() { + eprintln!(" - {}", k); + } + } match vm.execute_module(&module_vm) { Ok(_ret) => { /* interpreter already prints via println/console in program */ } Err(e) => { diff --git a/tools/smokes/v2/profiles/quick/core/using_multi_prelude_dep_ast.sh b/tools/smokes/v2/profiles/quick/core/using_multi_prelude_dep_ast.sh new file mode 100644 index 00000000..603b47bd --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/using_multi_prelude_dep_ast.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# using_multi_prelude_dep_ast.sh - 複数プレリュード(toml パッケージ)と依存解決(ASTマージ) + +source "$(dirname "$0")/../../../lib/test_runner.sh" + +require_env || exit 2 +preflight_plugins || exit 2 + +setup_tmp_dir() { + TEST_DIR="/tmp/using_multi_prelude_$$" + mkdir -p "$TEST_DIR" + cd "$TEST_DIR" +} + +teardown_tmp_dir() { + cd / + rm -rf "$TEST_DIR" +} + +test_multi_prelude_dep_ast() { + setup_tmp_dir + + cat > nyash.toml << 'EOF' +[using.a] +path = "lib/a" + +[using.b] +path = "lib/b" + +[using] +paths = ["lib"] +EOF + + mkdir -p lib/a lib/b + cat > lib/a/a.nyash << 'EOF' +static box A { x() { return "A" } } +EOF + cat > lib/b/b.nyash << 'EOF' +static box B { y() { return A.x() + "B" } } +EOF + + cat > main.nyash << 'EOF' +using a +using b +static box Main { + main() { + print(B.y()) + return 0 + } +} +EOF + + export NYASH_USING_PROFILE=prod + export NYASH_USING_AST=1 + local output rc + output=$(run_nyash_vm main.nyash 2>&1) + if echo "$output" | grep -qx "AB"; then rc=0; else rc=1; fi + [ $rc -eq 0 ] || { echo "$output" >&2; } + teardown_tmp_dir + return $rc +} + +run_test "using_multi_prelude_dep_ast" test_multi_prelude_dep_ast diff --git a/tools/smokes/v2/profiles/quick/core/using_profiles_ast.sh b/tools/smokes/v2/profiles/quick/core/using_profiles_ast.sh new file mode 100644 index 00000000..70ba895f --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/using_profiles_ast.sh @@ -0,0 +1,133 @@ +#!/bin/bash +# using_profiles_ast.sh - using プロファイル(dev/prod)× AST プレリュードの基本動作チェック + +source "$(dirname "$0")/../../../lib/test_runner.sh" + +require_env || exit 2 +preflight_plugins || exit 2 + +setup_tmp_dir() { + TEST_DIR="/tmp/using_profiles_ast_$$" + mkdir -p "$TEST_DIR" + cd "$TEST_DIR" +} + +teardown_tmp_dir() { + cd / + rm -rf "$TEST_DIR" +} + +# Test A: dev プロファイルでは `using "file"` が許可され、AST プレリュードで解決できる +test_dev_file_using_ok_ast() { + setup_tmp_dir + + # nyash.toml(paths だけで十分) + cat > nyash.toml << 'EOF' +[using] +paths = ["lib"] +EOF + + mkdir -p lib + cat > lib/u.nyash << 'EOF' +static box Util { + greet() { return "hi" } +} +EOF + + cat > main.nyash << 'EOF' +using "lib/u.nyash" +static box Main { + main() { + print(Util.greet()) + return 0 + } +} +EOF + + local output rc + # dev + AST モード(環境はexportで明示) + export NYASH_USING_PROFILE=dev + export NYASH_USING_AST=1 + output=$(run_nyash_vm main.nyash 2>&1) + if echo "$output" | grep -qx "hi"; then rc=0; else rc=1; fi + [ $rc -eq 0 ] || { echo "$output" >&2; } + teardown_tmp_dir + return $rc +} + +# Test B: prod プロファイルでは `using "file"` は拒否(ガイダンス付きエラー) +test_prod_file_using_forbidden_ast() { + setup_tmp_dir + + cat > nyash.toml << 'EOF' +[using] +paths = ["lib"] +EOF + + mkdir -p lib + cat > lib/u.nyash << 'EOF' +static box Util { greet() { return "x" } } +EOF + + cat > main.nyash << 'EOF' +using "lib/u.nyash" +static box Main { main() { print(Util.greet()); return 0 } } +EOF + + local output + # prod + AST モード(失敗が正) + export NYASH_USING_PROFILE=prod + export NYASH_USING_AST=1 + output=$(run_nyash_vm main.nyash 2>&1 || true) + if echo "$output" | grep -qi "disallowed\|nyash.toml \[using\]"; then + test_pass "prod_file_using_forbidden_ast" + else + test_fail "prod_file_using_forbidden_ast" "expected guidance error, got: $output" + fi + + teardown_tmp_dir +} + +# Test C: prod プロファイルで alias/package は許可(AST プレリュードで読み込み) +test_prod_alias_package_ok_ast() { + setup_tmp_dir + + cat > nyash.toml << 'EOF' +[using.u] +path = "lib/u" + +[using] +paths = ["lib"] +EOF + + mkdir -p lib/u + # main 省略 → leaf 名と同名の .nyash がデフォルト(u/u.nyash) + cat > lib/u/u.nyash << 'EOF' +static box Util { + version() { return "ok" } +} +EOF + + cat > main.nyash << 'EOF' +using u +static box Main { + main() { + print(Util.version()) + return 0 + } +} +EOF + + local output rc + export NYASH_USING_PROFILE=prod + export NYASH_USING_AST=1 + output=$(run_nyash_vm main.nyash 2>&1) + if echo "$output" | grep -qx "ok"; then rc=0; else rc=1; fi + [ $rc -eq 0 ] || { echo "$output" >&2; } + teardown_tmp_dir + return $rc +} + +run_test "using_dev_file_ok_ast" test_dev_file_using_ok_ast +run_test "using_prod_file_forbidden_ast" test_prod_file_using_forbidden_ast +run_test "using_prod_alias_ok_ast" test_prod_alias_package_ok_ast diff --git a/tools/smokes/v2/profiles/quick/core/using_relative_file_ast.sh b/tools/smokes/v2/profiles/quick/core/using_relative_file_ast.sh new file mode 100644 index 00000000..ae7058e2 --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/using_relative_file_ast.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# using_relative_file_ast.sh - ネストした場所からの相対パス using(AST プレリュード) + +source "$(dirname "$0")/../../../lib/test_runner.sh" + +require_env || exit 2 +preflight_plugins || exit 2 + +setup_tmp_dir() { + TEST_DIR="/tmp/using_rel_file_$$" + mkdir -p "$TEST_DIR" + cd "$TEST_DIR" +} + +teardown_tmp_dir() { + cd / + rm -rf "$TEST_DIR" +} + +test_relative_file_using_ast() { + setup_tmp_dir + + cat > nyash.toml << 'EOF' +[using] +paths = ["lib"] +EOF + + mkdir -p lib sub + cat > lib/u.nyash << 'EOF' +static box Util { greet() { return "rel" } } +EOF + + cat > sub/main.nyash << 'EOF' +using "../lib/u.nyash" +static box Main { + main() { + print(Util.greet()) + return 0 + } +} +EOF + + export NYASH_USING_PROFILE=dev + export NYASH_USING_AST=1 + local output rc + output=$(run_nyash_vm sub/main.nyash 2>&1) + if echo "$output" | grep -qx "rel"; then rc=0; else rc=1; fi + [ $rc -eq 0 ] || { echo "$output" >&2; } + teardown_tmp_dir + return $rc +} + +run_test "using_relative_file_ast" test_relative_file_using_ast