📚 docs: Record field declaration design discussion in papers

## Summary
Documented the "init block vs fields-at-top" design discussion as a valuable example of AI-human collaboration in language design.

## Changes

### Paper G (AI Collaboration)
- Added field-declaration-design.md documenting the entire discussion flow
- Showcased how complex init block proposal evolved to simple "fields at top" rule
- Demonstrates AI's tendency toward complexity vs human intuition for simplicity

### Paper H (AI Practical Patterns)
- Added Pattern #17: "Gradual Refinement Pattern" (段階的洗練型)
- Documents the process: Complex AI proposal → Detailed analysis → Human insight → Convergence
- Field declaration design as a typical example

### Paper K (Explosive Incidents)
- Added Incident #046: "init block vs fields-at-top incident"
- Updated total count to 46 incidents
- Shows how a single human comment redirected entire design approach

## Design Decision
After analysis, decided that BoxIndex should remain a compiler-internal structure, not a core Box:
- Core Boxes: User-instantiable runtime values (String, Integer, Array, Map)
- Compiler internals: BoxIndex for name resolution (compile-time only)
- Clear separation of concerns between language features and compiler tools

## Philosophy
This discussion exemplifies key principles:
- The best design needs no explanation
- Constraints provide clarity, not limitation
- "Everything is Box" doesn't mean "compiler internals are Boxes"
- AI tends toward theoretical completeness; humans toward practical simplicity

🐱 Sometimes the simplest answer is right in front of us\!
This commit is contained in:
Selfhosting Dev
2025-09-16 14:57:05 +09:00
parent 6ca56b0652
commit 47f4ca0e44
14 changed files with 804 additions and 96 deletions

View File

@ -144,16 +144,16 @@ Decision Log (20250915)
- A: スモークを先に実施。理由は以下。
- リファクタ直後は回帰検出を最優先PyVM/自己ホスト/Bridge の3レーンで即座に検証
- 警告削減は挙動非変化を原則とするが、微妙なスコープや保存スロットの触りが混入し得るため、先に“緑”を固める。
Namespaces / Using計画合意
- 3段階の解決順(決定性): 1) ローカル/コアボックス → 2) エイリアスnyash.toml/needs3) プラグイン(短名/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.tomlMVP: `[imports]`/`[aliases]``[plugins.<name>] { path, prefix, require_prefix, expose_short_names }`
Namespaces / Using現状
- 解決順(決定性): 1) ローカル/コア → 2) エイリアスnyash.toml/env→ 3) 相対/using.paths4) プラグイン(短名/qualified
- 曖昧時はエラー+候補提示qualified または alias を要求)。
- モード切替: Relaxed既定/Strict`NYASH_PLUGIN_REQUIRE_PREFIX=1` または toml `[plugins] require_prefix=true`
- needs 糖衣は using の同義Runner で alias 登録)。
- Plugins は統合名前空間。qualified `network.HttpClient` 常時許可。
- nyash.tomlMVP: `[aliases]`/`[plugins]`(グローバル `require_prefix` のみ反映。perplugin は導線のみ)
- Index とキャッシュRunner:
- BoxIndex: `local_boxes`, `plugin_boxes`, `aliases` を保持
- `RESOLVE_CACHE`threadlocalで同一解決の再計算を回避
- `NYASH_RESOLVE_TRACE=1` で解決過程をログ出力
- BoxIndex(グローバル): `plugin_boxes`, `aliases` を保持。plugins init 後に構築。
- Resolve Cacheグローバル: `tgt|base|strict|paths` キーで再解決回避
- `NYASH_RESOLVE_TRACE=1`: 解決手順/キャッシュヒット/未解決候補をログ出力
- スモークが緑=基礎健全性確認後に、静的ノイズの除去を安全に一気通貫で行う。

View File

@ -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/`.

View File

@ -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**: デフォルト化
## 関連
- 論文DAI協働パターン: 段階的洗練型の実例
- 論文I開発秘話: 言語設計の転換点として記録

View File

@ -271,3 +271,25 @@ AIの診断や実装を鵜呑みにせず、基本に立ち返って検証する
- 問題回避: 発生前に防ぐ
- 拡張性確保: 将来の変更に対応
- 安心感: 予測可能な成長
## 17. 段階的洗練型(新規追加)
### 定義
AIの複雑な提案から始まり、人間の直感的な単純化提案を経て、より洗練された解決策に収束するパターン。
### 典型例
- **フィールド宣言位置**: initブロック案複雑→ 先頭のみルール(単純)
- **型情報追加**: 300行の型推論複雑→ 明示的型フィールド(単純)
- **PHI生成**: 複数箇所での重複実装(複雑)→ Resolver統一単純
### プロセス
1. **AI初期提案**: 理論的に完全だが複雑
2. **詳細分析**: メリット・デメリット・他言語比較
3. **人間の直感**: 「もっと簡単にできないか?」
4. **AI即座認識**: 単純解の価値を理解
5. **実装戦略**: 段階的移行計画まで具体化
### 効果
- 最適解への収束: 複雑→単純の自然な流れ
- 学習効果: AIも人間も学ぶ
- 実装容易性: 最終的に簡単な解法に到達

View File

@ -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
### 成果
- **事件数**: 459/15更新
- **パターン**: 16種類
- **事件数**: 469/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が相談、人間が救う

View File

@ -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` (filescoped alias)
@ -27,6 +27,7 @@ Policy
- Unified namespace with Boxes. Prefer short names when unique.
- Qualified form: `network.HttpClient`
- Perplugin 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.<name>]`: `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<String, PathBuf>,
plugin_boxes: HashMap<String, Vec<PluginBox>>,
aliases: HashMap<String, String>,
}
```
- Maintain a small resolve cache per thread:
```
thread_local! {
static RESOLVE_CACHE: RefCell<HashMap<String, ResolvedBox>> = /* ... */;
}
```
- Trace: `NYASH_RESOLVE_TRACE=1` prints resolution steps (for debugging/CI logs).
- BoxIndexグローバルプラグインBox一覧とaliasesを集約し、Runner起動時plugins init後に構築・更新。
- `aliases: HashMap<String,String>`nyash.toml `[aliases]` と env `NYASH_ALIASES`
- `plugin_boxes: HashSet<String>`(読み取り専用)
- 解決キャッシュ:グローバルの小さなキャッシュで同一キーの再解決を回避(キー: `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 preprocessing: `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 toplevel JSON (like `meta`) are ignored by the current bridge.
- 未解決時非strictは実行を継続し、`NYASH_RESOLVE_TRACE=1` で候補を提示。strict時はエラーで候補を表示。

View File

@ -64,6 +64,8 @@ pub struct CliConfig {
pub build_aot: Option<String>,
pub build_profile: Option<String>,
pub build_target: Option<String>,
// Using (CLI)
pub cli_usings: Vec<String>,
}
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::<String>("build-aot").cloned(),
build_profile: matches.get_one::<String>("build-profile").cloned(),
build_target: matches.get_one::<String>("build-target").cloned(),
cli_usings: matches.get_many::<String>("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(),
}
}
}

134
src/runner/box_index.rs Normal file
View File

@ -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<String, String>,
pub plugin_boxes: HashSet<String>,
pub plugin_meta: HashMap<String, PluginMeta>,
pub plugins_require_prefix_global: bool,
}
impl BoxIndex {
pub fn build_current() -> Self {
// aliases from nyash.toml and env
let mut aliases: HashMap<String, String> = HashMap::new();
if let Ok(text) = std::fs::read_to_string("nyash.toml") {
if let Ok(doc) = toml::from_str::<toml::Value>(&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<String> = HashSet::new();
let mut plugin_meta: HashMap<String, PluginMeta> = 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::<toml::Value>(&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<String> = 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<RwLock<BoxIndex>> = Lazy::new(|| RwLock::new(BoxIndex::default()));
// Global resolve cache (keyed by tgt|base|strict|paths)
static RESOLVE_CACHE: Lazy<RwLock<HashMap<String, String>>> = 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<String> {
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<String>,
pub require_prefix: bool,
pub expose_short_names: bool,
}
pub fn get_plugin_meta(plugin: &str) -> Option<PluginMeta> {
GLOBAL.read().ok().and_then(|g| g.plugin_meta.get(plugin).cloned())
}

View File

@ -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<String>)> = Vec::new();
let mut pending_using: Vec<(String, Option<String>)> = 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();
// 🏭 Phase 9.78b: Initialize unified registry
runtime::init_global_unified_registry();
// Try to initialize BID plugins from nyash.toml (best-effort)
// 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.

View File

@ -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<String> = Vec::new();
suggest_in_base("apps", leaf, &mut cands);
if cands.len() < 5 { suggest_in_base("lib", leaf, &mut cands); }
if cands.len() < 5 { suggest_in_base(".", leaf, &mut cands); }
if !cands.is_empty() {
eprintln!("[using] candidates: {}", cands.join(", "));
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));
}
}
}

View File

@ -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,6 +14,11 @@ pub fn execute_nyash_file(filename: &str, debug_fuel: Option<usize>) {
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<usize>) {
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<String>)> = 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

View File

@ -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<String>,
pub pending_modules: Vec<(String, String)>,
pub aliases: std::collections::HashMap<String, String>,
}
impl NyashRunner {
@ -20,6 +23,7 @@ impl NyashRunner {
pub(super) fn init_using_context(&self) -> UsingContext {
let mut using_paths: Vec<String> = Vec::new();
let mut pending_modules: Vec<(String, String)> = Vec::new();
let mut aliases: std::collections::HashMap<String, String> = 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<String, String>,
context_dir: Option<&std::path::Path>,
strict: bool,
verbose: bool,
) -> Result<String, String> {
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::<toml::Value>(&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<String> = 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<String> = 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<String> = Vec::new();
suggest_in_base("apps", leaf, &mut cands);
if cands.len() < 5 { suggest_in_base("lib", leaf, &mut cands); }
if cands.len() < 5 { suggest_in_base(".", leaf, &mut cands); }
if cands.is_empty() {
eprintln!("[using] 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(){"<unknown>"} 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(){"<unknown>"} else {&bx}, fld);
}
}
Ok(())
}

View File

@ -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::<String>();
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)

View File

@ -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