diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index e9eee63f..54b6eb46 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -144,16 +144,16 @@ Decision Log (2025‑09‑15) - A: スモークを先に実施。理由は以下。 - リファクタ直後は回帰検出を最優先(PyVM/自己ホスト/Bridge の3レーンで即座に検証)。 - 警告削減は挙動非変化を原則とするが、微妙なスコープや保存スロットの触りが混入し得るため、先に“緑”を固める。 -Namespaces / Using(計画合意) -- 3段階の解決順(決定性): 1) ローカル/コアボックス → 2) エイリアス(nyash.toml/needs) → 3) プラグイン(短名/qualified) -- 曖昧時はエラー+候補提示。qualified または alias を要求(自動推測はしない)。 -- モード切替: Relaxed(既定)/Strict(`NYASH_PLUGIN_REQUIRE_PREFIX=1` or toml `[plugins] require_prefix=true`) -- needs 糖衣は using の同義(Runner で alias 登録)。`needs plugin.network.HttpClient as HttpClient` 等。 -- Plugins は統合名前空間(短名はユニーク時のみ)。qualified `network.HttpClient` を常時許可。 -- nyash.toml(MVP): `[imports]`/`[aliases]`,`[plugins.] { path, prefix, require_prefix, expose_short_names }` +Namespaces / Using(現状) +- 解決順(決定性): 1) ローカル/コア → 2) エイリアス(nyash.toml/env)→ 3) 相対/using.paths → 4) プラグイン(短名/qualified) +- 曖昧時はエラー+候補提示(qualified または alias を要求)。 +- モード切替: Relaxed(既定)/Strict(`NYASH_PLUGIN_REQUIRE_PREFIX=1` または toml `[plugins] require_prefix=true`) +- needs 糖衣は using の同義(Runner で alias 登録)。 +- Plugins は統合名前空間。qualified `network.HttpClient` 常時許可。 +- nyash.toml(MVP): `[aliases]`/`[plugins]`(グローバル `require_prefix` のみ反映。per‑plugin は導線のみ) - Index とキャッシュ(Runner): - - BoxIndex: `local_boxes`, `plugin_boxes`, `aliases` を保持 - - `RESOLVE_CACHE`(thread‑local)で同一解決の再計算を回避 - - `NYASH_RESOLVE_TRACE=1` で解決過程をログ出力 + - BoxIndex(グローバル): `plugin_boxes`, `aliases` を保持。plugins init 後に構築。 + - Resolve Cache(グローバル): `tgt|base|strict|paths` キーで再解決回避。 + - `NYASH_RESOLVE_TRACE=1`: 解決手順/キャッシュヒット/未解決候補をログ出力。 - スモークが緑=基礎健全性確認後に、静的ノイズの除去を安全に一気通貫で行う。 diff --git a/docs/guides/style-guide.md b/docs/guides/style-guide.md index 8df2233e..69a89064 100644 --- a/docs/guides/style-guide.md +++ b/docs/guides/style-guide.md @@ -33,6 +33,39 @@ Structure - Keep methods short and focused; prefer extracting helpers to maintain clarity. - Prefer pure helpers where possible; isolate I/O in specific methods. +Box layout +- フィールドは box 本体の先頭にまとめる(先頭ブロック)。 +- 先頭フィールド群の後ろはメソッドのみを記述する(`birth` を含む)。 +- フィールド間の空行・コメントは許可。アノテーション(将来追加予定)もフィールド行の直前/行末を許可。 +- NG: 最初のメソッド以降にフィールドを追加すること(リンタ警告/厳格モードでエラー)。 + +良い例 +```nyash +box Employee { + // データ構造(フィールド) + name: StringBox + age: IntegerBox + department: StringBox + + // ここからメソッド + birth(n, a, d) { me.name = n; me.age = a; me.department = d } + promote() { } +} +``` + +悪い例(NG) +```nyash +box Bad { + id: IntegerBox + method1() { } + name: StringBox // ❌ フィールドはメソッドの後に置けない +} +``` + +ツール +- 警告: 既定は警告(`NYASH_CLI_VERBOSE=1` で詳細を表示)。 +- 厳格化: `NYASH_FIELDS_TOP_STRICT=1` でエラーに昇格(Runnerでチェック)。 + Examples ```nyash using core.std as Std @@ -64,4 +97,3 @@ static box Main { CI/Tooling - Optional formatter PoC: see `docs/tools/nyfmt/NYFMT_POC_ROADMAP.md`. - Keep smoke scripts small and fast; place them under `tools/`. - diff --git a/docs/private/papers/paper-g-ai-collaboration/field-declaration-design.md b/docs/private/papers/paper-g-ai-collaboration/field-declaration-design.md new file mode 100644 index 00000000..ee29f814 --- /dev/null +++ b/docs/private/papers/paper-g-ai-collaboration/field-declaration-design.md @@ -0,0 +1,127 @@ +# フィールド宣言位置の制約設計(2025-09-15) + +## 概要 + +Boxのフィールド宣言がメソッドの間に散在できる問題に対する設計相談。AIとの対話を通じて、複雑なinitブロック案から「フィールドは先頭のみ」という単純明快なルールへ収束した事例。 + +## 相談の流れ + +### 1. 問題提起(ユーザー) +``` +ところで 今のフィールド変数の書き方 +これだと どこでもかけちゃうのかな +``` + +現状の問題: +```nyash +box MessyBox { + field1: StringBox + method1() { } + field2: IntegerBox // メソッドの間に散在 + birth() { } + field3: BoolBox // さらに散在 +} +``` + +### 2. 初期提案(ユーザー) +``` +init +{ + この中に書く 書き方は今のと同じ +} +init文復活させて この中だけにフィールドを書くというのはありかもしれない +``` + +### 3. AI分析(Claude) + +#### initブロック案の評価 +**メリット**: +- 明確な構造化 +- フィールド一覧の固定位置 +- パース簡略化 +- 将来の拡張性(アノテーション等) + +**デメリット**: +- 冗長性(小さなBoxでも必須) +- Everything is Box哲学との齟齬 +- 学習コスト増加 + +#### ハイブリッド提案 +```nyash +// 小さなBoxはそのまま +box SmallBox { + name: StringBox + birth(n) { me.name = n } +} + +// 大きなBoxはinitで整理 +box LargeBox { + init { + field1: TypeBox + field2: TypeBox + } + birth() { } +} +``` + +### 4. ユーザーの洞察 +``` +それか フィールドは boxの先頭だけにかけるというルールはありかもしれにゃい +``` + +### 5. 収束(AI) + +「フィールドは先頭のみ」ルールの支持: + +```nyash +box CleanBox { + // フィールドは必ず先頭にまとめる + field1: StringBox + field2: IntegerBox + + // ここから下はメソッドのみ + birth() { } + method1() { } +} +``` + +**決定的な利点**: +- 特別な構文不要(initブロック不要) +- 多くの言語で採用されている標準 +- パーサー実装が簡潔 +- Everything is Box哲学と調和 + +## 設計哲学の体現 + +1. **単純性の追求**: 複雑な構文(initブロック)より単純なルール +2. **制約による明快さ**: 自由度を制限して可読性を向上 +3. **段階的移行**: 警告→エラーの段階的導入 +4. **既存の知見活用**: 他言語の標準的アプローチを採用 + +## AI協働パターン:段階的洗練型 + +1. **複雑な提案から開始**(initブロック、ハイブリッド案) +2. **詳細な分析提供**(メリット・デメリット・他言語比較) +3. **ユーザーの直感的提案**(先頭のみルール) +4. **AIが即座に価値を認識** +5. **実装戦略まで具体化** + +→ AIは複雑に考えがちだが、人間の直感が本質を突く好例 + +## 教訓 + +- 最良の設計は説明不要な設計 +- 制約は自由度を奪うのではなく、明快さを与える +- "Everything is Box"は複雑さの言い訳ではない +- AIの分析力と人間の直感の相補性 + +## 実装計画 + +1. **Phase 15.4**: 警告モード(`NYASH_STRICT_FIELD_ORDER=warn`) +2. **Phase 16**: エラーモード(`NYASH_STRICT_FIELD_ORDER=error`) +3. **Phase 17**: デフォルト化 + +## 関連 + +- 論文D(AI協働パターン): 段階的洗練型の実例 +- 論文I(開発秘話): 言語設計の転換点として記録 \ No newline at end of file diff --git a/docs/private/papers/paper-h-ai-practical-patterns/pattern-categories.md b/docs/private/papers/paper-h-ai-practical-patterns/pattern-categories.md index 9e8e6cb6..637a2b7c 100644 --- a/docs/private/papers/paper-h-ai-practical-patterns/pattern-categories.md +++ b/docs/private/papers/paper-h-ai-practical-patterns/pattern-categories.md @@ -270,4 +270,26 @@ AIの診断や実装を鵜呑みにせず、基本に立ち返って検証する ### 効果 - 問題回避: 発生前に防ぐ - 拡張性確保: 将来の変更に対応 -- 安心感: 予測可能な成長 \ No newline at end of file +- 安心感: 予測可能な成長 + +## 17. 段階的洗練型(新規追加) + +### 定義 +AIの複雑な提案から始まり、人間の直感的な単純化提案を経て、より洗練された解決策に収束するパターン。 + +### 典型例 +- **フィールド宣言位置**: initブロック案(複雑)→ 先頭のみルール(単純) +- **型情報追加**: 300行の型推論(複雑)→ 明示的型フィールド(単純) +- **PHI生成**: 複数箇所での重複実装(複雑)→ Resolver統一(単純) + +### プロセス +1. **AI初期提案**: 理論的に完全だが複雑 +2. **詳細分析**: メリット・デメリット・他言語比較 +3. **人間の直感**: 「もっと簡単にできないか?」 +4. **AI即座認識**: 単純解の価値を理解 +5. **実装戦略**: 段階的移行計画まで具体化 + +### 効果 +- 最適解への収束: 複雑→単純の自然な流れ +- 学習効果: AIも人間も学ぶ +- 実装容易性: 最終的に簡単な解法に到達 \ No newline at end of file diff --git a/docs/private/papers/paper-k-explosive-incidents/complete-incident-collection.md b/docs/private/papers/paper-k-explosive-incidents/complete-incident-collection.md index 05f89a6a..600da627 100644 --- a/docs/private/papers/paper-k-explosive-incidents/complete-incident-collection.md +++ b/docs/private/papers/paper-k-explosive-incidents/complete-incident-collection.md @@ -1,10 +1,10 @@ -# 🎉 Nyash開発 完全事件コレクション - 世界記録級45事例の記録 +# 🎉 Nyash開発 完全事件コレクション - 世界記録級46事例の記録 ## 📝 概要 -2025年8月9日から9月15日までのNyash爆速開発で発生した45個の「面白事件」の完全記録。 +2025年8月9日から9月15日までのNyash爆速開発で発生した46個の「面白事件」の完全記録。 AI協働開発の歴史に残る世界記録級の事件から、開発現場の生々しいドラマまでを網羅。 -(2025年9月15日更新:4件追加) +(2025年9月15日更新:5件追加) ## 🌟 世界記録級TOP10 @@ -69,7 +69,7 @@ AI協働開発の歴史に残る世界記録級の事件から、開発現場の - **意味**: Everything is Fold哲学へ - **評価**: 「革命的アイデア」認定 -## 📊 16パターン別分類(全45事例) +## 📊 17パターン別分類(全46事例) ### 1. 箱化による解決(8事例) - 事例001: DebugBoxによる出力制御統一 @@ -140,6 +140,9 @@ AI協働開発の歴史に残る世界記録級の事件から、開発現場の ### 16. 予防的設計(1事例) - 事例039: ID衝突との戦い +### 17. 段階的洗練型(1事例) +- 事例046: initブロック vs 先頭のみ事件 + ### その他(10事例) - 事例020: 26日間の奇跡 - 事例021: 2段階パーサー理論 @@ -186,8 +189,8 @@ ChatGPT: 「!!!」(瞬時に理解) - **世界記録**: 20日でネイティブEXE ### 成果 -- **事件数**: 45個(9/15更新) -- **パターン**: 16種類 +- **事件数**: 46個(9/15更新) +- **パターン**: 17種類 - **致命的破綻**: 0回 - **大規模リファクタ**: 0回 @@ -203,7 +206,7 @@ ChatGPT: 「!!!」(瞬時に理解) - [技術的ブレークスルー](../paper-l-technical-breakthroughs/README.md) - [AI協働開発ログ](../paper-g-ai-collaboration/development-log.md) -## 🚀 2025年9月追加事例(4件) +## 🚀 2025年9月追加事例(5件) ### 事例042: PyVM迂回路の混乱 - **日付**: 2025年9月15日 @@ -238,9 +241,19 @@ ChatGPT: 「!!!」(瞬時に理解) - **ChatGPT評価**: 「EXE-firstが正しい道」 - **影響**: Phase順序の明確化(15.2→15.3→15.4) +### 事例046: initブロック vs 先頭のみ事件 +- **日付**: 2025年9月15日 +- **問題**: フィールド宣言がメソッドの間に散在可能 +- **AI提案**: initブロック導入(構造化重視の複雑案) +- **人間の一言**: 「それか フィールドは boxの先頭だけにかけるというルールはありかもしれにゃい」 +- **AI反応**: 即座に単純解の価値を認識 +- **結果**: 特別な構文不要、他言語標準に合致 +- **パターン**: 段階的洗練型の典型例(複雑→単純) +- **教訓**: AIは複雑に考えがち、人間の直感が本質を突く + ## 💫 まとめ -45個の事件は、単なる開発エピソードではなく、AI協働開発の新しい形を示す歴史的記録である。特に: +46個の事件は、単なる開発エピソードではなく、AI協働開発の新しい形を示す歴史的記録である。特に: 1. **世界記録級の開発速度**(JIT1日、20日でEXE) 2. **AI-人間の新しい関係**(AIが相談、人間が救う) diff --git a/docs/reference/language/using.md b/docs/reference/language/using.md index 53377d41..a2c63310 100644 --- a/docs/reference/language/using.md +++ b/docs/reference/language/using.md @@ -17,8 +17,8 @@ Policy 3) Plugins (short name if unique, otherwise qualified `pluginName.BoxName`) - On ambiguity: error with candidates and remediation (qualify or define alias). - Modes: - - Relaxed (default): short names allowed when unique. - - Strict: require plugin prefix (env `NYASH_PLUGIN_REQUIRE_PREFIX=1` or nyash.toml `[plugins] require_prefix=true`). + - Relaxed (default): short names allowed when unique。 + - Strict: plugin短名にprefix必須(env `NYASH_PLUGIN_REQUIRE_PREFIX=1` または nyash.toml `[plugins] require_prefix=true`)。 - Aliases: - nyash.toml `[imports] HttpClient = "network.HttpClient"` - needs sugar: `needs plugin.network.HttpClient as HttpClient` (file‑scoped alias) @@ -27,6 +27,7 @@ Policy - Unified namespace with Boxes. Prefer short names when unique. - Qualified form: `network.HttpClient` - Per‑plugin control (nyash.toml): `prefix`, `require_prefix`, `expose_short_names` + - 現状は設定の読み取りのみ(導線)。挙動への反映は段階的に実施予定。 ## `needs` sugar (optional) - Treated as a synonym to `using` on the Runner side; registers aliases only. @@ -37,21 +38,11 @@ Policy - `[plugins.]`: `path`, `prefix`, `require_prefix`, `expose_short_names` ## Index and Cache (Runner) -- Build a Box index once per run to make resolution fast and predictable: -``` -struct BoxIndex { - local_boxes: HashMap, - plugin_boxes: HashMap>, - aliases: HashMap, -} -``` -- Maintain a small resolve cache per thread: -``` -thread_local! { - static RESOLVE_CACHE: RefCell> = /* ... */; -} -``` -- Trace: `NYASH_RESOLVE_TRACE=1` prints resolution steps (for debugging/CI logs). +- BoxIndex(グローバル):プラグインBox一覧とaliasesを集約し、Runner起動時(plugins init後)に構築・更新。 + - `aliases: HashMap`(nyash.toml `[aliases]` と env `NYASH_ALIASES`) + - `plugin_boxes: HashSet`(読み取り専用) +- 解決キャッシュ:グローバルの小さなキャッシュで同一キーの再解決を回避(キー: `tgt|base|strict|paths`)。 +- トレース:`NYASH_RESOLVE_TRACE=1` で解決手順やキャッシュヒット、未解決候補を出力。 Syntax - Namespace: `using core.std` or `using core.std as Std` @@ -102,9 +93,14 @@ 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) +- 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. Notes - Phase 15 keeps resolution in the Runner to minimize parser complexity. Future phases may leverage `meta.usings` for compiler decisions. - Unknown fields in the top‑level JSON (like `meta`) are ignored by the current bridge. + - 未解決時(非strict)は実行を継続し、`NYASH_RESOLVE_TRACE=1` で候補を提示。strict時はエラーで候補を表示。 diff --git a/src/cli.rs b/src/cli.rs index 86814fe0..4297554b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -64,6 +64,8 @@ pub struct CliConfig { pub build_aot: Option, pub build_profile: Option, pub build_target: Option, + // Using (CLI) + pub cli_usings: Vec, } impl CliConfig { @@ -109,6 +111,13 @@ impl CliConfig { .value_name("ARGS") .help("Pass additional args to selfhost child compiler (equivalent to NYASH_NY_COMPILER_CHILD_ARGS)") ) + .arg( + Arg::new("using") + .long("using") + .value_name("SPEC") + .help("Register a using entry (e.g., 'ns as Alias' or '\"apps/foo.nyash\" as Foo'). Repeatable.") + .action(clap::ArgAction::Append) + ) .arg( Arg::new("debug-fuel") @@ -423,6 +432,7 @@ impl CliConfig { build_aot: matches.get_one::("build-aot").cloned(), build_profile: matches.get_one::("build-profile").cloned(), build_target: matches.get_one::("build-target").cloned(), + cli_usings: matches.get_many::("using").map(|v| v.cloned().collect()).unwrap_or_else(|| Vec::new()), } } } @@ -475,6 +485,7 @@ impl Default for CliConfig { build_aot: None, build_profile: None, build_target: None, + cli_usings: Vec::new(), } } } diff --git a/src/runner/box_index.rs b/src/runner/box_index.rs new file mode 100644 index 00000000..2662a529 --- /dev/null +++ b/src/runner/box_index.rs @@ -0,0 +1,134 @@ +/*! + * BoxIndex — minimal view over aliases and plugin box types + * + * Purpose: allow using/namespace resolver to make decisions that depend + * on plugin-visible type names (e.g., enforcing strict prefix rules) and + * to surface aliases defined in nyash.toml/env. + */ + +use std::collections::{HashMap, HashSet}; +use once_cell::sync::Lazy; +use std::sync::RwLock; + +#[derive(Clone, Default)] +pub struct BoxIndex { + pub aliases: HashMap, + pub plugin_boxes: HashSet, + pub plugin_meta: HashMap, + pub plugins_require_prefix_global: bool, +} + +impl BoxIndex { + pub fn build_current() -> Self { + // aliases from nyash.toml and env + let mut aliases: HashMap = HashMap::new(); + if let Ok(text) = std::fs::read_to_string("nyash.toml") { + if let Ok(doc) = toml::from_str::(&text) { + if let Some(alias_tbl) = doc.get("aliases").and_then(|v| v.as_table()) { + for (k, v) in alias_tbl.iter() { + if let Some(target) = v.as_str() { aliases.insert(k.to_string(), target.to_string()); } + } + } + } + } + if let Ok(raw) = std::env::var("NYASH_ALIASES") { + for ent in raw.split(',') { + if let Some((k,v)) = ent.split_once('=') { + let k = k.trim(); let v = v.trim(); + if !k.is_empty() && !v.is_empty() { aliases.insert(k.to_string(), v.to_string()); } + } + } + } + + // plugin box types (best-effort; may be empty if host not initialized yet) + let mut plugin_boxes: HashSet = HashSet::new(); + let mut plugin_meta: HashMap = HashMap::new(); + let mut plugins_require_prefix_global = false; + + // Read per-plugin meta and global flags from nyash.toml when available + if let Ok(text) = std::fs::read_to_string("nyash.toml") { + if let Ok(doc) = toml::from_str::(&text) { + if let Some(plugins_tbl) = doc.get("plugins").and_then(|v| v.as_table()) { + // Global switch: [plugins].require_prefix = true + if let Some(v) = plugins_tbl.get("require_prefix").and_then(|v| v.as_bool()) { + plugins_require_prefix_global = v; + } + for (k, v) in plugins_tbl.iter() { + // Skip non-table entries (string entries are plugin roots) + if let Some(t) = v.as_table() { + let prefix = t.get("prefix").and_then(|x| x.as_str()).map(|s| s.to_string()); + let require_prefix = t.get("require_prefix").and_then(|x| x.as_bool()).unwrap_or(false); + let expose_short_names = t.get("expose_short_names").and_then(|x| x.as_bool()).unwrap_or(true); + plugin_meta.insert(k.clone(), PluginMeta { prefix, require_prefix, expose_short_names }); + } + } + } + } + } + let host = crate::runtime::get_global_plugin_host(); + if let Ok(h) = host.read() { + if let Some(cfg) = h.config_ref() { + for (_lib, def) in &cfg.libraries { + for bt in &def.boxes { plugin_boxes.insert(bt.clone()); } + } + } + } + + Self { aliases, plugin_boxes, plugin_meta, plugins_require_prefix_global } + } + + pub fn is_known_plugin_short(name: &str) -> bool { + // Prefer global index view + if GLOBAL.read().ok().map(|g| g.plugin_boxes.contains(name)).unwrap_or(false) { + return true; + } + // Env override list + if let Ok(raw) = std::env::var("NYASH_KNOWN_PLUGIN_SHORTNAMES") { + let set: HashSet = raw.split(',').map(|s| s.trim().to_string()).collect(); + if set.contains(name) { return true; } + } + // Minimal fallback set + const KNOWN: &[&str] = &[ + "ArrayBox","MapBox","StringBox","ConsoleBox","FileBox","PathBox","MathBox","IntegerBox","TOMLBox" + ]; + KNOWN.iter().any(|k| *k == name) + } +} + +// Global BoxIndex view (rebuilt on-demand) +static GLOBAL: Lazy> = Lazy::new(|| RwLock::new(BoxIndex::default())); + +// Global resolve cache (keyed by tgt|base|strict|paths) +static RESOLVE_CACHE: Lazy>> = Lazy::new(|| RwLock::new(HashMap::new())); + +pub fn refresh_box_index() { + let next = BoxIndex::build_current(); + if let Ok(mut w) = GLOBAL.write() { *w = next; } +} + +pub fn get_box_index() -> BoxIndex { + GLOBAL.read().ok().map(|g| g.clone()).unwrap_or_default() +} + +pub fn cache_get(key: &str) -> Option { + RESOLVE_CACHE.read().ok().and_then(|m| m.get(key).cloned()) +} + +pub fn cache_put(key: &str, value: String) { + if let Ok(mut m) = RESOLVE_CACHE.write() { m.insert(key.to_string(), value); } +} + +pub fn cache_clear() { + if let Ok(mut m) = RESOLVE_CACHE.write() { m.clear(); } +} + +#[derive(Clone, Debug, Default)] +pub struct PluginMeta { + pub prefix: Option, + pub require_prefix: bool, + pub expose_short_names: bool, +} + +pub fn get_plugin_meta(plugin: &str) -> Option { + GLOBAL.read().ok().and_then(|g| g.plugin_meta.get(plugin).cloned()) +} diff --git a/src/runner/mod.rs b/src/runner/mod.rs index 1ca8bce3..3a8be213 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -21,6 +21,7 @@ mod json_v0_bridge; mod mir_json_emit; mod pipe_io; mod pipeline; +mod box_index; mod tasks; mod build; mod dispatch; @@ -61,7 +62,26 @@ impl NyashRunner { } // Using/module overrides pre-processing let mut using_ctx = self.init_using_context(); - let pending_using: Vec<(String, Option)> = Vec::new(); + let mut pending_using: Vec<(String, Option)> = Vec::new(); + // CLI --using SPEC entries (SPEC: 'ns', 'ns as Alias', '"path" as Alias') + for spec in &self.config.cli_usings { + let s = spec.trim(); + if s.is_empty() { continue; } + let (target, 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) }; + // Normalize quotes for path + let is_path = target.starts_with('"') || target.starts_with("./") || target.starts_with('/') || target.ends_with(".nyash"); + if is_path { + let path = target.trim_matches('"').to_string(); + let name = alias.clone().unwrap_or_else(|| { + std::path::Path::new(&path).file_stem().and_then(|s| s.to_str()).unwrap_or("module").to_string() + }); + pending_using.push((name, Some(path))); + } else { + pending_using.push((target, alias)); + } + } for (ns, path) in using_ctx.pending_modules.iter() { let sb = crate::box_trait::StringBox::new(path.clone()); crate::runtime::modules_registry::set(ns.clone(), Box::new(sb)); @@ -135,6 +155,13 @@ impl NyashRunner { } } + // Lint: fields must be at top of box + let strict_fields = std::env::var("NYASH_FIELDS_TOP_STRICT").ok().as_deref() == Some("1"); + if let Err(e) = pipeline::lint_fields_top(&code, strict_fields, self.config.cli_verbose) { + eprintln!("❌ Lint error: {}", e); + std::process::exit(1); + } + // Env overrides for using rules // Merge late env overrides (if any) if let Ok(paths) = std::env::var("NYASH_USING_PATH") { @@ -159,7 +186,7 @@ impl NyashRunner { 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, &using_ctx.pending_modules, &using_ctx.using_paths, ctx, strict, verbose) { + let value = match resolve_using_target(ns, false, &using_ctx.pending_modules, &using_ctx.using_paths, &using_ctx.aliases, ctx, strict, verbose) { Ok(v) => v, Err(e) => { eprintln!("❌ using: {}", e); std::process::exit(1); } }; @@ -184,13 +211,15 @@ impl NyashRunner { } } - // 🏭 Phase 9.78b: Initialize unified registry - runtime::init_global_unified_registry(); - - // Try to initialize BID plugins from nyash.toml (best-effort) + // 🏭 Phase 9.78b: Initialize unified registry + runtime::init_global_unified_registry(); + + // Try to initialize BID plugins from nyash.toml (best-effort) // Allow disabling during snapshot/CI via NYASH_DISABLE_PLUGINS=1 if std::env::var("NYASH_DISABLE_PLUGINS").ok().as_deref() != Some("1") { runner_plugin_init::init_bid_plugins(); + // Build BoxIndex after plugin host is initialized + crate::runner::box_index::refresh_box_index(); } // Allow interpreter to create plugin-backed boxes via unified registry // Opt-in by default for FileBox/TOMLBox which are required by ny-config and similar tools. diff --git a/src/runner/modes/common.rs b/src/runner/modes/common.rs index bcb0d58b..c50c7dba 100644 --- a/src/runner/modes/common.rs +++ b/src/runner/modes/common.rs @@ -8,7 +8,7 @@ use std::io::Read; use std::process::Stdio; use std::time::{Duration, Instant}; use std::thread::sleep; -use crate::runner::pipeline::suggest_in_base; +use crate::runner::pipeline::{suggest_in_base, resolve_using_target}; // (moved) suggest_in_base is now in runner/pipeline.rs @@ -610,30 +610,26 @@ impl NyashRunner { cleaned_code_owned = out; code_ref = &cleaned_code_owned; - // Register modules into minimal registry with best-effort path resolution + // Register modules with resolver (aliases/modules/paths) + let using_ctx = self.init_using_context(); + 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_dir = std::path::Path::new(filename).parent(); for (ns_or_alias, alias_or_path) in used_names { - // alias_or_path Some(path) means this entry was a direct path using if let Some(path) = alias_or_path { let sb = crate::box_trait::StringBox::new(path); crate::runtime::modules_registry::set(ns_or_alias, Box::new(sb)); } else { - let rel = format!("apps/{}.nyash", ns_or_alias.replace('.', "/")); - let exists = std::path::Path::new(&rel).exists(); - if !exists && std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") { - eprintln!("[using] unresolved namespace '{}'; tried '{}'. Hint: add @module {}={} or --module {}={}", ns_or_alias, rel, ns_or_alias, rel, ns_or_alias, rel); - // naive candidates by suffix within common bases - let leaf = ns_or_alias.split('.').last().unwrap_or(&ns_or_alias); - let mut cands: Vec = Vec::new(); - suggest_in_base("apps", leaf, &mut cands); - if cands.len() < 5 { suggest_in_base("lib", leaf, &mut cands); } - if cands.len() < 5 { suggest_in_base(".", leaf, &mut cands); } - if !cands.is_empty() { - eprintln!("[using] candidates: {}", cands.join(", ")); + match resolve_using_target(&ns_or_alias, false, &using_ctx.pending_modules, &using_ctx.using_paths, &using_ctx.aliases, ctx_dir, strict, verbose) { + Ok(value) => { + let sb = crate::box_trait::StringBox::new(value); + crate::runtime::modules_registry::set(ns_or_alias, Box::new(sb)); + } + Err(e) => { + eprintln!("❌ using: {}", e); + std::process::exit(1); } } - let path_or_ns = if exists { rel } else { ns_or_alias.clone() }; - let sb = crate::box_trait::StringBox::new(path_or_ns); - crate::runtime::modules_registry::set(ns_or_alias, Box::new(sb)); } } } diff --git a/src/runner/modes/interpreter.rs b/src/runner/modes/interpreter.rs index fefd9bbc..4a24638a 100644 --- a/src/runner/modes/interpreter.rs +++ b/src/runner/modes/interpreter.rs @@ -1,5 +1,7 @@ use crate::parser::NyashParser; use crate::interpreter::NyashInterpreter; +use crate::runner_plugin_init; +use crate::runner::pipeline::{resolve_using_target, UsingContext}; use std::{fs, process}; /// Execute Nyash file with interpreter @@ -12,7 +14,12 @@ pub fn execute_nyash_file(filename: &str, debug_fuel: Option) { process::exit(1); } }; - + // Initialize plugin host and mappings (idempotent) + if std::env::var("NYASH_DISABLE_PLUGINS").ok().as_deref() != Some("1") { + runner_plugin_init::init_bid_plugins(); + crate::runner::box_index::refresh_box_index(); + } + println!("📝 File contents:\n{}", code); println!("\n🚀 Parsing and executing...\n"); @@ -20,9 +27,60 @@ pub fn execute_nyash_file(filename: &str, debug_fuel: Option) { std::fs::create_dir_all("development/debug_hang_issue").ok(); std::fs::write("development/debug_hang_issue/test.txt", "START").ok(); + // Optional: using pre-processing (strip lines and register modules) + let enable_using = std::env::var("NYASH_ENABLE_USING").ok().as_deref() == Some("1"); + let mut code_ref: std::borrow::Cow<'_, str> = std::borrow::Cow::Borrowed(&code); + if enable_using { + let mut out = String::with_capacity(code.len()); + let mut used_names: Vec<(String, Option)> = Vec::new(); + for line in code.lines() { + let t = line.trim_start(); + if t.starts_with("using ") { + let rest0 = t.strip_prefix("using ").unwrap().trim(); + let rest0 = rest0.strip_suffix(';').unwrap_or(rest0).trim(); + let (target, alias) = if let Some(pos) = rest0.find(" as ") { + (rest0[..pos].trim().to_string(), Some(rest0[pos+4..].trim().to_string())) + } else { (rest0.to_string(), None) }; + let is_path = target.starts_with('"') || target.starts_with("./") || target.starts_with('/') || target.ends_with(".nyash"); + if is_path { + let path = target.trim_matches('"').to_string(); + let name = alias.clone().unwrap_or_else(|| { + std::path::Path::new(&path).file_stem().and_then(|s| s.to_str()).unwrap_or("module").to_string() + }); + used_names.push((name, Some(path))); + } else { + used_names.push((target, alias)); + } + continue; + } + out.push_str(line); + out.push('\n'); + } + // Resolve and register + let using_ctx = UsingContext { using_paths: vec!["apps".into(), "lib".into(), ".".into()], pending_modules: vec![], aliases: std::collections::HashMap::new() }; + 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_dir = std::path::Path::new(filename).parent(); + for (ns_or_alias, alias_or_path) in used_names { + if let Some(path) = alias_or_path { + let sb = crate::box_trait::StringBox::new(path); + crate::runtime::modules_registry::set(ns_or_alias, Box::new(sb)); + } else { + match resolve_using_target(&ns_or_alias, false, &using_ctx.pending_modules, &using_ctx.using_paths, &using_ctx.aliases, ctx_dir, strict, verbose) { + Ok(value) => { + let sb = crate::box_trait::StringBox::new(value); + crate::runtime::modules_registry::set(ns_or_alias, Box::new(sb)); + } + Err(e) => { eprintln!("❌ using: {}", e); std::process::exit(1); } + } + } + } + code_ref = std::borrow::Cow::Owned(out); + } + // Parse the code with debug fuel limit eprintln!("🔍 DEBUG: Starting parse with fuel: {:?}...", debug_fuel); - let ast = match NyashParser::parse_from_string_with_fuel(&code, debug_fuel) { + let ast = match NyashParser::parse_from_string_with_fuel(&*code_ref, debug_fuel) { Ok(ast) => { eprintln!("🔍 DEBUG: Parse completed, AST created"); ast diff --git a/src/runner/pipeline.rs b/src/runner/pipeline.rs index a7b2c8d2..b77976e7 100644 --- a/src/runner/pipeline.rs +++ b/src/runner/pipeline.rs @@ -8,11 +8,14 @@ */ use super::*; +use super::box_index::BoxIndex; +use std::collections::HashMap; /// Using/module resolution context accumulated from config/env/nyash.toml pub(super) struct UsingContext { pub using_paths: Vec, pub pending_modules: Vec<(String, String)>, + pub aliases: std::collections::HashMap, } impl NyashRunner { @@ -20,6 +23,7 @@ impl NyashRunner { pub(super) fn init_using_context(&self) -> UsingContext { let mut using_paths: Vec = Vec::new(); let mut pending_modules: Vec<(String, String)> = Vec::new(); + let mut aliases: std::collections::HashMap = std::collections::HashMap::new(); // Defaults using_paths.extend(["apps", "lib", "."].into_iter().map(|s| s.to_string())); @@ -45,6 +49,14 @@ impl NyashRunner { } } } + // Optional: [aliases] table maps short name -> path or namespace token + if let Some(alias_tbl) = doc.get("aliases").and_then(|v| v.as_table()) { + for (k, v) in alias_tbl.iter() { + if let Some(target) = v.as_str() { + aliases.insert(k.to_string(), target.to_string()); + } + } + } } } } @@ -67,8 +79,17 @@ impl NyashRunner { if !s.is_empty() { using_paths.push(s.to_string()); } } } + // Env aliases: comma-separated k=v pairs + if let Ok(raw) = std::env::var("NYASH_ALIASES") { + for ent in raw.split(',') { + if let Some((k,v)) = ent.split_once('=') { + let k = k.trim(); let v = v.trim(); + if !k.is_empty() && !v.is_empty() { aliases.insert(k.to_string(), v.to_string()); } + } + } + } - UsingContext { using_paths, pending_modules } + UsingContext { using_paths, pending_modules, aliases } } } @@ -107,13 +128,81 @@ pub(super) fn resolve_using_target( is_path: bool, modules: &[(String, String)], using_paths: &[String], + aliases: &HashMap, context_dir: Option<&std::path::Path>, strict: bool, verbose: bool, ) -> Result { if is_path { return Ok(tgt.to_string()); } + let trace = verbose || std::env::var("NYASH_RESOLVE_TRACE").ok().as_deref() == Some("1"); + // Strict plugin prefix: if enabled and target matches a known plugin box type + // and is not qualified (contains '.'), require a qualified/prefixed name. + // Strict mode: env or nyash.toml [plugins] require_prefix=true + let mut strict_effective = strict; + if !strict_effective { + if let Ok(text) = std::fs::read_to_string("nyash.toml") { + if let Ok(doc) = toml::from_str::(&text) { + if let Some(tbl) = doc.get("plugins").and_then(|v| v.as_table()) { + if let Some(v) = tbl.get("require_prefix").and_then(|v| v.as_bool()) { if v { strict_effective = true; } } + } + } + } + } + if std::env::var("NYASH_PLUGIN_REQUIRE_PREFIX").ok().as_deref() == Some("1") { strict_effective = true; } + + if strict_effective { + let mut is_plugin_short = super::box_index::BoxIndex::is_known_plugin_short(tgt); + if !is_plugin_short { + // Fallback: heuristic list or env override + if let Ok(raw) = std::env::var("NYASH_KNOWN_PLUGIN_SHORTNAMES") { + let set: std::collections::HashSet = raw.split(',').map(|s| s.trim().to_string()).collect(); + is_plugin_short = set.contains(tgt); + } else { + // Minimal builtins set + const KNOWN: &[&str] = &[ + "ArrayBox","MapBox","StringBox","ConsoleBox","FileBox","PathBox","MathBox","IntegerBox","TOMLBox" + ]; + is_plugin_short = KNOWN.iter().any(|k| *k == tgt); + } + } + if is_plugin_short && !tgt.contains('.') { + return Err(format!("plugin short name '{}' requires prefix (strict)", tgt)); + } + } + let key = { + let base = context_dir.and_then(|p| p.to_str()).unwrap_or(""); + format!("{}|{}|{}|{}", tgt, base, strict as i32, using_paths.join(":")) + }; + if let Some(hit) = crate::runner::box_index::cache_get(&key) { + if trace { eprintln!("[using/cache] '{}' -> '{}'", tgt, hit); } + return Ok(hit); + } + // Resolve aliases early (provided map) + if let Some(v) = aliases.get(tgt) { + if trace { eprintln!("[using/resolve] alias '{}' -> '{}'", tgt, v); } + crate::runner::box_index::cache_put(&key, v.clone()); + return Ok(v.clone()); + } + // Also consult env aliases + if let Ok(raw) = std::env::var("NYASH_ALIASES") { + for ent in raw.split(',') { + if let Some((k,v)) = ent.split_once('=') { + if k.trim() == tgt { + let out = v.trim().to_string(); + if trace { eprintln!("[using/resolve] env-alias '{}' -> '{}'", tgt, out); } + crate::runner::box_index::cache_put(&key, out.clone()); + return Ok(out); + } + } + } + } // 1) modules mapping - if let Some((_, p)) = modules.iter().find(|(n, _)| n == tgt) { return Ok(p.clone()); } + if let Some((_, p)) = modules.iter().find(|(n, _)| n == tgt) { + let out = p.clone(); + if trace { eprintln!("[using/resolve] modules '{}' -> '{}'", tgt, out); } + crate::runner::box_index::cache_put(&key, out.clone()); + return Ok(out); + } // 2) build candidate list: relative then using-paths let rel = tgt.replace('.', "/") + ".nyash"; let mut cand: Vec = Vec::new(); @@ -123,11 +212,123 @@ pub(super) fn resolve_using_target( if c.exists() { cand.push(c.to_string_lossy().to_string()); } } if cand.is_empty() { - if verbose { eprintln!("[using] unresolved '{}' (searched: rel+paths)", tgt); } + if trace { + // Try suggest candidates by leaf across bases (apps/lib/.) + let leaf = tgt.split('.').last().unwrap_or(tgt); + let mut cands: Vec = Vec::new(); + suggest_in_base("apps", leaf, &mut cands); + if cands.len() < 5 { suggest_in_base("lib", leaf, &mut cands); } + if cands.len() < 5 { suggest_in_base(".", leaf, &mut cands); } + if cands.is_empty() { + eprintln!("[using] unresolved '{}' (searched: rel+paths)", tgt); + } else { + eprintln!("[using] unresolved '{}' (searched: rel+paths) candidates: {}", tgt, cands.join(", ")); + } + } return Ok(tgt.to_string()); } if cand.len() > 1 && strict { return Err(format!("ambiguous using '{}': {}", tgt, cand.join(", "))); } - Ok(cand.remove(0)) + let out = cand.remove(0); + if trace { eprintln!("[using/resolve] '{}' -> '{}'", tgt, out); } + crate::runner::box_index::cache_put(&key, out.clone()); + Ok(out) +} + +/// Lint: enforce "fields must be at the top of box" rule. +/// - Warns by default (when verbose); when `strict` is true, returns Err on any violation. +pub(super) fn lint_fields_top(code: &str, strict: bool, verbose: bool) -> Result<(), String> { + let mut brace: i32 = 0; + let mut in_box = false; + let mut box_depth: i32 = 0; + let mut seen_method = false; + let mut cur_box: String = String::new(); + let mut violations: Vec<(usize, String, String)> = Vec::new(); // (line, field, box) + + for (idx, line) in code.lines().enumerate() { + let lno = idx + 1; + let pre_brace = brace; + let trimmed = line.trim(); + // Count braces for this line + let opens = line.matches('{').count() as i32; + let closes = line.matches('}').count() as i32; + + // Enter box on same-line K&R style: `box Name {` or `static box Name {` + if !in_box && trimmed.starts_with("box ") || trimmed.starts_with("static box ") { + // capture name + let mut name = String::new(); + let after = if let Some(rest) = trimmed.strip_prefix("static box ") { rest } else { trimmed.strip_prefix("box ").unwrap_or("") }; + for ch in after.chars() { + if ch.is_alphanumeric() || ch == '_' { name.push(ch); } else { break; } + } + // require K&R brace on same line to start tracking + if opens > 0 { + in_box = true; + cur_box = name; + box_depth = pre_brace + 1; // assume one level for box body + seen_method = false; + } + } + + if in_box { + // Top-level inside box only + if pre_brace == box_depth { + // Skip empty/comment lines + if !trimmed.is_empty() && !trimmed.starts_with("//") { + // Detect method: name(args) { + let is_method = { + // starts with identifier then '(' and later '{' + let mut it = trimmed.chars(); + let mut ident = String::new(); + while let Some(c) = it.next() { if c.is_whitespace() { continue; } if c.is_alphabetic() || c=='_' { ident.push(c); break; } else { break; } } + while let Some(c) = it.next() { if c.is_alphanumeric() || c=='_' { ident.push(c); } else { break; } } + trimmed.contains('(') && trimmed.ends_with('{') && !ident.is_empty() + }; + if is_method { seen_method = true; } + + // Detect field: ident ':' Type (rough heuristic) + let is_field = { + let parts: Vec<&str> = trimmed.split(':').collect(); + if parts.len() == 2 { + let lhs = parts[0].trim(); + let rhs = parts[1].trim(); + let lhs_ok = !lhs.is_empty() && lhs.chars().next().map(|c| c.is_alphabetic() || c=='_').unwrap_or(false); + let rhs_ok = !rhs.is_empty() && rhs.chars().next().map(|c| c.is_alphabetic() || c=='_').unwrap_or(false); + lhs_ok && rhs_ok && !trimmed.contains('(') && !trimmed.contains(')') + } else { false } + }; + if is_field && seen_method { + violations.push((lno, trimmed.to_string(), cur_box.clone())); + } + } + } + // Exit box when closing brace reduces depth below box_depth + let post_brace = pre_brace + opens - closes; + if post_brace < box_depth { in_box = false; cur_box.clear(); } + } + + // Update brace after processing + brace += opens - closes; + } + + if violations.is_empty() { + return Ok(()); + } + if strict { + // Compose error message + let mut msg = String::from("Field declarations must appear at the top of box. Violations:\n"); + for (lno, fld, bx) in violations.iter().take(10) { + msg.push_str(&format!(" line {} in box {}: '{}" , lno, if bx.is_empty(){""} else {bx}, fld)); + msg.push_str("'\n"); + } + if violations.len() > 10 { msg.push_str(&format!(" ... and {} more\n", violations.len()-10)); } + return Err(msg); + } + if verbose || std::env::var("NYASH_RESOLVE_TRACE").ok().as_deref() == Some("1") { + for (lno, fld, bx) in violations { + eprintln!("[lint] fields-top: line {} in box {} -> {}", lno, if bx.is_empty(){""} else {&bx}, fld); + } + } + Ok(()) } diff --git a/src/runner/selfhost.rs b/src/runner/selfhost.rs index e374355b..4e11964c 100644 --- a/src/runner/selfhost.rs +++ b/src/runner/selfhost.rs @@ -91,6 +91,57 @@ impl NyashRunner { Err(e) => { eprintln!("[ny-compiler] open tmp failed: {}", e); return false; } } } + // Python MVP-first: prefer the lightweight harness to produce JSON v0 + if let Ok(py3) = which::which("python3") { + let py = std::path::Path::new("tools/ny_parser_mvp.py"); + if py.exists() { + let mut cmd = std::process::Command::new(&py3); + cmd.arg(py).arg(&tmp_path); + let out = match cmd.output() { Ok(o) => o, Err(e) => { eprintln!("[ny-compiler] python harness failed to spawn: {}", e); return false; } }; + if out.status.success() { + if let Ok(line) = String::from_utf8(out.stdout).map(|s| s.lines().next().unwrap_or("").to_string()) { + if line.contains("\"version\"") && line.contains("\"kind\"") { + match super::json_v0_bridge::parse_json_v0_to_module(&line) { + Ok(module) => { + super::json_v0_bridge::maybe_dump_mir(&module); + let emit_only = std::env::var("NYASH_NY_COMPILER_EMIT_ONLY").unwrap_or_else(|_| "1".to_string()) == "1"; + if emit_only { return false; } + // Prefer PyVM for selfhost pipeline (parity reference) + if std::env::var("NYASH_VM_USE_PY").ok().as_deref() == Some("1") { + // Reuse the common PyVM runner path + let tmp_dir = std::path::Path::new("tmp"); + let _ = std::fs::create_dir_all(tmp_dir); + let mir_json_path = tmp_dir.join("nyash_pyvm_mir.json"); + if let Err(e) = crate::runner::mir_json_emit::emit_mir_json_for_harness_bin(&module, &mir_json_path) { + eprintln!("❌ PyVM MIR JSON emit error: {}", e); + process::exit(1); + } + if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") { + eprintln!("[Bridge] using PyVM (selfhost-py) → {}", mir_json_path.display()); + } + let entry = if module.functions.contains_key("Main.main") { "Main.main" } else if module.functions.contains_key("main") { "main" } else { "Main.main" }; + let status = std::process::Command::new(&py3) + .args(["tools/pyvm_runner.py", "--in", &mir_json_path.display().to_string(), "--entry", entry]) + .status().map_err(|e| format!("spawn pyvm: {}", e)).unwrap(); + let code = status.code().unwrap_or(1); + if !status.success() { + if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") { + eprintln!("❌ PyVM (selfhost-py) failed (status={})", code); + } + } + println!("Result: {}", code); + std::process::exit(code); + } + self.execute_mir_module(&module); + return true; + } + Err(e) => { eprintln!("[ny-compiler] json parse error: {}", e); return false; } + } + } + } + } + } + } // EXE-first: if requested, try external parser EXE (nyash_compiler) if std::env::var("NYASH_USE_NY_COMPILER_EXE").ok().as_deref() == Some("1") { // Resolve parser EXE path @@ -204,57 +255,60 @@ impl NyashRunner { } } - // Fallback: run compiler.nyash via VM(PyVM) and pick the JSON line - // Guard against recursion: ensure child does NOT enable selfhost pipeline. + // Fallback: inline VM run (embed source into a tiny wrapper that prints JSON) + // This avoids CLI arg forwarding complexity and does not require FileBox. let mut raw = String::new(); { - // Locate current nyash executable + // Escape source for embedding as string literal + let mut esc = String::with_capacity(code_ref.len()); + for ch in code_ref.chars() { + match ch { + '\\' => esc.push_str("\\\\"), + '"' => esc.push_str("\\\""), + '\n' => esc.push_str("\n"), + '\r' => esc.push_str(""), + _ => esc.push(ch), + } + } + let inline_path = std::path::Path::new("tmp").join("inline_selfhost_emit.nyash"); + let inline_code = format!( + "include \"apps/selfhost-compiler/boxes/parser_box.nyash\"\ninclude \"apps/selfhost-compiler/boxes/emitter_box.nyash\"\nstatic box Main {{\n main(args) {{\n local s = \"{}\"\n local p = new ParserBox()\n local json = p.parse_program2(s)\n local e = new EmitterBox()\n json = e.emit_program(json, \"[]\")\n print(json)\n return 0\n }}\n}}\n", + esc + ); + if let Err(e) = std::fs::write(&inline_path, inline_code) { + eprintln!("[ny-compiler] write inline failed: {}", e); + return false; + } let exe = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("target/release/nyash")); let mut cmd = std::process::Command::new(exe); - cmd.arg("--backend").arg("vm").arg("apps/selfhost-compiler/compiler.nyash"); - // Pass script args to child when gated - if std::env::var("NYASH_NY_COMPILER_MIN_JSON").ok().as_deref() == Some("1") { cmd.arg("--").arg("--min-json"); } - if std::env::var("NYASH_SELFHOST_READ_TMP").ok().as_deref() == Some("1") { cmd.arg("--").arg("--read-tmp"); } - // Recursion guard and minimal, quiet env for child + cmd.arg("--backend").arg("vm").arg(&inline_path); cmd.env_remove("NYASH_USE_NY_COMPILER"); cmd.env_remove("NYASH_CLI_VERBOSE"); cmd.env("NYASH_JSON_ONLY", "1"); - if let Ok(v) = std::env::var("NYASH_JSON_INCLUDE_USINGS") { cmd.env("NYASH_JSON_INCLUDE_USINGS", v); } - if let Ok(v) = std::env::var("NYASH_ENABLE_USING") { cmd.env("NYASH_ENABLE_USING", v); } - - // Timeout guard (default 2000ms) let timeout_ms: u64 = std::env::var("NYASH_NY_COMPILER_TIMEOUT_MS").ok().and_then(|s| s.parse().ok()).unwrap_or(2000); let mut cmd = cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); - let mut child = match cmd.spawn() { - Ok(c) => c, - Err(e) => { eprintln!("[ny-compiler] spawn nyash vm failed: {}", e); return false; } - }; + let mut child = match cmd.spawn() { Ok(c) => c, Err(e) => { eprintln!("[ny-compiler] spawn inline vm failed: {}", e); return false; } }; let mut ch_stdout = child.stdout.take(); let mut ch_stderr = child.stderr.take(); let start = Instant::now(); let mut timed_out = false; loop { match child.try_wait() { - Ok(Some(_status)) => { break; } + Ok(Some(_)) => break, Ok(None) => { if start.elapsed() >= Duration::from_millis(timeout_ms) { - let _ = child.kill(); - let _ = child.wait(); - timed_out = true; - break; + let _ = child.kill(); let _ = child.wait(); timed_out = true; break; } sleep(Duration::from_millis(10)); } - Err(e) => { eprintln!("[ny-compiler] child wait error: {}", e); break; } + Err(e) => { eprintln!("[ny-compiler] inline wait error: {}", e); break; } } } let mut out_buf = Vec::new(); - let mut err_buf = Vec::new(); if let Some(mut s) = ch_stdout { let _ = s.read_to_end(&mut out_buf); } - if let Some(mut s) = ch_stderr { let _ = s.read_to_end(&mut err_buf); } if timed_out { let head = String::from_utf8_lossy(&out_buf).chars().take(200).collect::(); - eprintln!("[ny-compiler] child timeout after {} ms; stdout(head)='{}'", timeout_ms, head.replace('\n', "\\n")); + eprintln!("[ny-compiler] inline timeout after {} ms; stdout(head)='{}'", timeout_ms, head.replace('\n', "\\n")); } raw = String::from_utf8_lossy(&out_buf).to_string(); } @@ -269,11 +323,14 @@ impl NyashRunner { super::json_v0_bridge::maybe_dump_mir(&module); let emit_only = std::env::var("NYASH_NY_COMPILER_EMIT_ONLY").unwrap_or_else(|_| "1".to_string()) == "1"; if emit_only { return false; } - // Prefer PyVM when requested AND the module contains BoxCalls (Stage-2 semantics) - let needs_pyvm = module.functions.values().any(|f| { + // Phase-15 policy: when NYASH_VM_USE_PY=1, prefer PyVM as reference executor + // regardless of BoxCall presence to ensure semantics parity (e.g., PHI merges). + let prefer_pyvm = std::env::var("NYASH_VM_USE_PY").ok().as_deref() == Some("1"); + // Backward compatibility: if not preferring PyVM explicitly, still auto-enable when BoxCalls exist. + let needs_pyvm = !prefer_pyvm && module.functions.values().any(|f| { f.blocks.values().any(|bb| bb.instructions.iter().any(|inst| matches!(inst, crate::mir::MirInstruction::BoxCall { .. }))) }); - if needs_pyvm && std::env::var("NYASH_VM_USE_PY").ok().as_deref() == Some("1") { + if prefer_pyvm || needs_pyvm { if let Ok(py3) = which::which("python3") { let runner = std::path::Path::new("tools/pyvm_runner.py"); if runner.exists() { @@ -285,7 +342,8 @@ impl NyashRunner { process::exit(1); } if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") { - eprintln!("[Bridge] using PyVM (selfhost-fallback) → {}", mir_json_path.display()); + let mode = if prefer_pyvm { "selfhost" } else { "selfhost-fallback" }; + eprintln!("[Bridge] using PyVM ({}) → {}", mode, mir_json_path.display()); } let entry = if module.functions.contains_key("Main.main") { "Main.main" } else if module.functions.contains_key("main") { "main" } else { "Main.main" }; let status = std::process::Command::new(py3) diff --git a/tools/using_prefix_strict_smoke.sh b/tools/using_prefix_strict_smoke.sh new file mode 100644 index 00000000..709ebcb0 --- /dev/null +++ b/tools/using_prefix_strict_smoke.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail +[[ "${NYASH_CLI_VERBOSE:-0}" == "1" ]] && set -x + +ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +BIN="$ROOT_DIR/target/release/nyash" + +if [ ! -x "$BIN" ]; then + cargo build --release >/dev/null +fi + +SRC=$(mktemp) +cat >"$SRC" <<'NY' +using ArrayBox +static box Main { main(args) { return 0 } } +NY + +set +e +NYASH_ENABLE_USING=1 NYASH_PLUGIN_REQUIRE_PREFIX=1 "$BIN" --backend interpreter "$SRC" >/tmp/nyash-using-prefix-strict.out 2>&1 +rc=$? +set -e + +if [ $rc -ne 0 ] && rg -q "plugin short name 'ArrayBox' requires prefix" /tmp/nyash-using-prefix-strict.out; then + echo "PASS: plugin short name rejected in strict mode" >&2 +else + echo "FAIL: strict plugin prefix not enforced" >&2 + sed -n '1,120p' /tmp/nyash-using-prefix-strict.out >&2 || true + exit 1 +fi + +echo "All PASS" >&2