From 1fae4f16487430b6f5834548ddad326e4c84f7fc Mon Sep 17 00:00:00 2001 From: nyash-codex Date: Sat, 13 Dec 2025 01:30:04 +0900 Subject: [PATCH] MIR: lexical scoping + builder vars modules --- .../main/phase67-mir-var-identity-survey.md | 87 +++++++++++++++++++ .../reference/language/variables-and-scope.md | 3 +- src/ast.rs | 4 +- src/mir/builder.rs | 59 ++++++++----- src/mir/builder/exprs.rs | 1 + src/mir/builder/stmts.rs | 4 +- src/mir/builder/vars/README.md | 26 ++++++ src/mir/builder/vars/assignment_resolver.rs | 29 +++++++ .../builder/{vars.rs => vars/free_vars.rs} | 2 +- src/mir/builder/vars/lexical_scope.rs | 75 ++++++++++++++++ src/mir/builder/vars/mod.rs | 3 + src/mir/join_ir/lowering/README.md | 9 ++ .../phase215/scope_assign_creates_local_vm.sh | 39 +++++++++ .../core/phase215/scope_loop_body_local_vm.sh | 38 ++++++++ .../quick/core/phase215/scope_shadow_vm.sh | 34 ++++++++ tools/smokes/v2/run.sh | 4 +- 16 files changed, 388 insertions(+), 29 deletions(-) create mode 100644 docs/development/current/main/phase67-mir-var-identity-survey.md create mode 100644 src/mir/builder/vars/README.md create mode 100644 src/mir/builder/vars/assignment_resolver.rs rename src/mir/builder/{vars.rs => vars/free_vars.rs} (98%) create mode 100644 src/mir/builder/vars/lexical_scope.rs create mode 100644 src/mir/builder/vars/mod.rs create mode 100644 tools/smokes/v2/profiles/quick/core/phase215/scope_assign_creates_local_vm.sh create mode 100644 tools/smokes/v2/profiles/quick/core/phase215/scope_loop_body_local_vm.sh create mode 100644 tools/smokes/v2/profiles/quick/core/phase215/scope_shadow_vm.sh diff --git a/docs/development/current/main/phase67-mir-var-identity-survey.md b/docs/development/current/main/phase67-mir-var-identity-survey.md new file mode 100644 index 00000000..16170452 --- /dev/null +++ b/docs/development/current/main/phase67-mir-var-identity-survey.md @@ -0,0 +1,87 @@ +# Phase 67: MIR Var Identity Survey + Shadowing Probe + +## 目的 + +- MIR 側の「ブロックスコープ/シャドウイング」が現状どう実現されているかを確定する。 +- JoinIR/Normalized の Ownership/Relay を BindingId(束縛ID)化すべきか、MIR 側のスコープ機構で足りるか判断する。 + +## 調査対象(読んだ場所) + +- MIR ビルダー + - `src/mir/builder.rs`(`variable_map`, `build_variable_access`, `build_assignment`) + - `src/mir/builder/stmts.rs`(`build_block`, `build_local_statement`) + - `src/mir/builder/exprs.rs`(`ASTNode::Program` の lowering が `cf_block` に直結) +- JoinIR lowering + - `src/mir/join_ir/lowering/scope_manager.rs`(名前ベース lookup) +- AST + - `src/ast.rs`(`ASTNode::ScopeBox` の意図コメント) +- 仕様(doc) + - `docs/reference/language/variables-and-scope.md`(block scope + shadowing + assignment rule) + - `docs/quick-reference/syntax-cheatsheet.md`(未宣言代入はエラー、と書いてある) + +## 観測: 現状の MIR ビルダーは「名前→ValueId」1枚 + +### 1) “束縛”は BindingId ではなく name で追跡される + +- `MirBuilder` は `variable_map: BTreeMap` を持つ(`src/mir/builder.rs:90`)。 +- `build_variable_access` は `variable_map.get(name)` のみで解決し、無ければ即 `Undefined variable: ` エラー(`src/mir/builder.rs:533`)。 +- `build_assignment` は「既存束縛の有無」を見ず、`variable_map.insert(name, value_id)` を行う(`src/mir/builder.rs:574`)。 + +結論: MIR/SSA の識別子解決は “name ベース上書き” であり、現状は BindingId(束縛の同一性)を持っていない。 + +### 2) ブロックは変数束縛のスコープフレームを作っていない + +- `ASTNode::Program` は `cf_block` に入り、そのまま `build_block` を呼ぶ(`src/mir/builder/exprs.rs:20`, `src/mir/builder/control_flow/mod.rs:59`)。 +- `build_block` は statements を順に lowering するだけで、`variable_map` の退避/復元を行わない(`src/mir/builder/stmts.rs:168`)。 +- `local` は `build_local_statement` が `variable_map.insert(var_name, var_id)` を直で行う(`src/mir/builder/stmts.rs:281`)。 + +結論: 現状の MIR では「ブロックスコープ」は存在せず、`local` の再宣言は outer を恒久的に上書きする。 + +### 3) `ASTNode::ScopeBox` は “意味不変” のラッパーであり、束縛スコープではない + +- `ASTNode::ScopeBox` は “診断/マクロ可視性のための no-op スコープ” と明記されている(`src/ast.rs:593`)。 + +## 仕様(doc)の不一致 + +- `docs/reference/language/variables-and-scope.md` は: + - `local` は block-scoped(`docs/reference/language/variables-and-scope.md:9`) + - inner `local` は shadowing(`docs/reference/language/variables-and-scope.md:11`) + - 未宣言代入は “現在スコープに新規束縛を作る”(`docs/reference/language/variables-and-scope.md:22`) +- `docs/quick-reference/syntax-cheatsheet.md` は: + - “未宣言名への代入はエラー” と書いてある(`docs/quick-reference/syntax-cheatsheet.md:13`, `docs/quick-reference/syntax-cheatsheet.md:204`) + +この Phase 67 では「実装現状」を SSOT 化し、どちらの仕様に合わせて修正するかは Phase 68 以降で決める(ただし、少なくとも +shadowing の有無は言語中枢なので放置できない)。 + +## プローブ(VM スモーク追加) + +追加したスモーク: + +- `tools/smokes/v2/profiles/quick/core/phase215/scope_shadow_vm.sh` + - 観測: `local x=1 { local x=2 } return x` が `rc=2`(inner `local x` が outer を上書きして leak) +- `tools/smokes/v2/profiles/quick/core/phase215/scope_assign_creates_local_vm.sh` + - 観測: `{ y = 42 } return y` が `rc=42`(束縛が block 外へ leak) + +再現コマンド: + +- `bash tools/smokes/v2/profiles/quick/core/phase215/scope_shadow_vm.sh` +- `bash tools/smokes/v2/profiles/quick/core/phase215/scope_assign_creates_local_vm.sh` + +## 結論と選択肢(Phase 68 への分岐) + +結論(現状): + +- MIR ビルダーはブロックスコープ/シャドウイングを実装していない(関数スコープの `variable_map` 1枚)。 +- JoinIR lowering 側も `ScopeManager::lookup(name)` の名前ベースであり、BindingId を前提にしていない(`src/mir/join_ir/lowering/scope_manager.rs:58`)。 + +選択肢: + +- A) MIR にスコープフレームを入れてシャドウイングを MIR で解決 + - Phase 67 のプローブが “仕様どおり” に通るようになるまで、MIR ビルダー側で `variable_map` をスコープスタック化するのが + 直接的。 +- B) JoinIR/Ownership を BindingId(束縛ID)SSOT にして、名前は表示専用に降格 + - Ownership/Relay/ShapeGuard の解析精度は上がるが、言語全体の block scope は MIR 側で結局必要になる(JoinIR だけ直しても + Stage-3 の一般コードは救えない)。 + +Phase 67 の観測(de facto 実装)から、Phase 68 はまず A(MIR スコープフレーム)に進むのが安全。 +その上で、Ownership/Relay が shadowing を正確に扱う必要が出た箇所だけを Phase 69+ で BindingId 化するのが最小差分になる。 diff --git a/docs/reference/language/variables-and-scope.md b/docs/reference/language/variables-and-scope.md index b7a37817..46a78e67 100644 --- a/docs/reference/language/variables-and-scope.md +++ b/docs/reference/language/variables-and-scope.md @@ -21,7 +21,7 @@ Assignment to an identifier resolves as follows: 1) If a `local` declaration with the same name exists in the current block, update that binding. 2) Otherwise, search outward through enclosing blocks and update the first found binding. -3) If no binding exists in any enclosing scope, create a new binding in the current scope. +3) If no binding exists in any enclosing scope, it is an error (undeclared variable). Declare it with `local`. This matches intuitive block‑scoped semantics (Lua‑like), and differs from Python where inner blocks do not create a new scope (function scope), and assignment would create a local unless `nonlocal`/`global` is used. @@ -63,4 +63,3 @@ The above semantics are enforced consistently across: - LLVM harness/EXE: parity tests validate identical exit codes/behavior. See also: quick/integration smokes `scope_assign_vm.sh`, `vm_llvm_scope_assign.sh`. - diff --git a/src/ast.rs b/src/ast.rs index 52b8b0b5..c9b9dedc 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -590,8 +590,8 @@ pub enum ASTNode { span: Span, }, - /// ScopeBox(オプション): 診断/マクロ可視性のためのno-opスコープ。 - /// 正規化で注入され、MIRビルダがブロックとして処理(意味不変)。 + /// ScopeBox(オプション): 正規化で注入される明示的なレキシカルスコープ境界。 + /// MIR ビルダは `{ ... }` と同様にブロックとして処理する(local のシャドウイング/寿命を分離)。 ScopeBox { body: Vec, span: Span }, /// Outbox変数宣言: outbox x, y, z (static関数内専用) diff --git a/src/mir/builder.rs b/src/mir/builder.rs index f3903d1a..8e5c49cc 100644 --- a/src/mir/builder.rs +++ b/src/mir/builder.rs @@ -92,6 +92,12 @@ pub struct MirBuilder { /// Phase 25.1: HashMap → BTreeMap(PHI生成の決定性確保) pub(super) variable_map: BTreeMap, + /// Lexical scope stack for block-scoped `local` declarations. + /// + /// This tracks per-block shadowing so `local x` inside `{...}` restores the + /// outer binding when the block ends. + lexical_scope_stack: Vec, + /// Pending phi functions to be inserted #[allow(dead_code)] pub(super) pending_phis: Vec<(BasicBlockId, ValueId, String)>, @@ -266,6 +272,7 @@ impl MirBuilder { block_gen: BasicBlockIdGenerator::new(), compilation_context: None, // 箱理論: デフォルトは従来モード variable_map: BTreeMap::new(), // Phase 25.1: 決定性確保 + lexical_scope_stack: Vec::new(), pending_phis: Vec::new(), value_origin_newbox: BTreeMap::new(), // Phase 25.1: 決定性確保 user_defined_boxes: HashSet::new(), @@ -546,37 +553,45 @@ impl MirBuilder { if let Some(&value_id) = self.variable_map.get(&name) { Ok(value_id) } else { - // Enhance diagnostics using Using simple registry (Phase 1) - let mut msg = format!("Undefined variable: {}", name); - - // Stage-3 keyword diagnostic (local/flow/try/catch/throw) - if name == "local" && !crate::config::env::parser_stage3_enabled() { - msg.push_str("\nHint: 'local' is a Stage-3 keyword. Prefer NYASH_FEATURES=stage3 (legacy: NYASH_PARSER_STAGE3=1 / HAKO_PARSER_STAGE3=1 for Stage-B)."); - msg.push_str("\nFor AotPrep verification, use tools/hakorune_emit_mir.sh which sets these automatically."); - } else if (name == "flow" || name == "try" || name == "catch" || name == "throw") - && !crate::config::env::parser_stage3_enabled() - { - msg.push_str(&format!("\nHint: '{}' is a Stage-3 keyword. Prefer NYASH_FEATURES=stage3 (legacy: NYASH_PARSER_STAGE3=1 / HAKO_PARSER_STAGE3=1 for Stage-B).", name)); - } - - let suggest = crate::using::simple_registry::suggest_using_for_symbol(&name); - if !suggest.is_empty() { - msg.push_str("\nHint: symbol appears in using module(s): "); - msg.push_str(&suggest.join(", ")); - msg.push_str( - "\nConsider adding 'using [as Alias]' or check nyash.toml [using].", - ); - } - Err(msg) + Err(self.undefined_variable_message(&name)) } } + pub(in crate::mir::builder) fn undefined_variable_message(&self, name: &str) -> String { + // Enhance diagnostics using Using simple registry (Phase 1) + let mut msg = format!("Undefined variable: {}", name); + + // Stage-3 keyword diagnostic (local/flow/try/catch/throw) + if name == "local" && !crate::config::env::parser_stage3_enabled() { + msg.push_str("\nHint: 'local' is a Stage-3 keyword. Prefer NYASH_FEATURES=stage3 (legacy: NYASH_PARSER_STAGE3=1 / HAKO_PARSER_STAGE3=1 for Stage-B)."); + msg.push_str("\nFor AotPrep verification, use tools/hakorune_emit_mir.sh which sets these automatically."); + } else if (name == "flow" || name == "try" || name == "catch" || name == "throw") + && !crate::config::env::parser_stage3_enabled() + { + msg.push_str(&format!("\nHint: '{}' is a Stage-3 keyword. Prefer NYASH_FEATURES=stage3 (legacy: NYASH_PARSER_STAGE3=1 / HAKO_PARSER_STAGE3=1 for Stage-B).", name)); + } + + let suggest = crate::using::simple_registry::suggest_using_for_symbol(name); + if !suggest.is_empty() { + msg.push_str("\nHint: symbol appears in using module(s): "); + msg.push_str(&suggest.join(", ")); + msg.push_str("\nConsider adding 'using [as Alias]' or check nyash.toml [using]."); + } + + msg + } + /// Build assignment pub(super) fn build_assignment( &mut self, var_name: String, value: ASTNode, ) -> Result { + // SSOT (LANGUAGE_REFERENCE_2025 / syntax-cheatsheet): + // - Assignment to an undeclared name is an error. + // - Use `local name = ...` (or `local name; name = ...`) to declare. + vars::assignment_resolver::AssignmentResolverBox::ensure_declared(self, &var_name)?; + let value_id = self.build_expression(value)?; // Step 5-5-E: FIX variable map corruption bug diff --git a/src/mir/builder/exprs.rs b/src/mir/builder/exprs.rs index a04ea0b0..623d2195 100644 --- a/src/mir/builder/exprs.rs +++ b/src/mir/builder/exprs.rs @@ -21,6 +21,7 @@ impl super::MirBuilder { // Sequentially lower statements and return last value (or Void) self.cf_block(statements) } + ASTNode::ScopeBox { body, .. } => self.cf_block(body), ASTNode::Print { expression, .. } => self.build_print_statement(*expression), ASTNode::If { condition, diff --git a/src/mir/builder/stmts.rs b/src/mir/builder/stmts.rs index 36203b16..1d61ba1e 100644 --- a/src/mir/builder/stmts.rs +++ b/src/mir/builder/stmts.rs @@ -170,6 +170,8 @@ impl super::MirBuilder { // Scope hint for bare block (Program) let scope_id = self.current_block.map(|bb| bb.as_u32()).unwrap_or(0); self.hint_scope_enter(scope_id); + let _lex_scope = super::vars::lexical_scope::LexicalScopeGuard::new(self); + let mut last_value = None; let total = statements.len(); eprintln!("[DEBUG/build_block] Processing {} statements", total); @@ -336,7 +338,7 @@ impl super::MirBuilder { var_name, var_id ); } - self.variable_map.insert(var_name.clone(), var_id); + self.declare_local_in_current_scope(var_name, var_id)?; // SlotRegistry にもローカル変数スロットを登録しておくよ(観測専用) if let Some(reg) = self.current_slot_registry.as_mut() { let ty = self.value_types.get(&var_id).cloned(); diff --git a/src/mir/builder/vars/README.md b/src/mir/builder/vars/README.md new file mode 100644 index 00000000..25a8685a --- /dev/null +++ b/src/mir/builder/vars/README.md @@ -0,0 +1,26 @@ +# `mir::builder::vars`(変数/スコープ系の小箱) + +このディレクトリは「MIR ビルダ内の変数・スコープ」に関する小さな責務を分離するための層だよ。 + +## 責務(この層がやる) + +- **レキシカルスコープ**: `{...}` / `ScopeBox` の境界で `local` のシャドウイングを復元する。 +- **AST 走査ユーティリティ**: free vars 収集など、純粋な走査処理。 +- **代入の宣言ポリシー**: 未宣言名への代入を Fail-Fast にする(`AssignmentResolverBox`)。 + +## 非責務(この層がやらない) + +- JoinIR lowering 側の名前解決(`join_ir/lowering/*` の `ScopeManager` が担当)。 +- ループパターン/PHI/境界生成(`control_flow/joinir/*` が担当)。 +- 言語仕様の追加(この層は既存仕様の実装に限定)。 + +## スコープ/名前解決の境界(SSOT) + +同じ「名前」を扱っていても、層ごとに “解いている問題” が違うので混ぜない。 + +- **MIR(この層)**: `variable_map` + `LexicalScopeGuard` で「束縛の寿命・シャドウイング」を管理する(SSA 変換のため)。 +- **JoinIR lowering**: `src/mir/join_ir/lowering/scope_manager.rs` は JoinIR 内の `name → ValueId` を解決する。 + - `ExprLowerer` は **ScopeManager 経由のみ** で名前解決する(env を直参照しない)。 +- **解析箱**: `LoopConditionScopeBox` / `LoopBodyLocalEnv` は「禁止/許可」「スケジュール」などの補助情報で、束縛そのものではない。 + +この境界を跨ぐ “便利メソッド” を作るのは原則禁止(責務混線の温床)。 diff --git a/src/mir/builder/vars/assignment_resolver.rs b/src/mir/builder/vars/assignment_resolver.rs new file mode 100644 index 00000000..7509dc7d --- /dev/null +++ b/src/mir/builder/vars/assignment_resolver.rs @@ -0,0 +1,29 @@ +use super::super::MirBuilder; + +/// AssignmentResolverBox +/// +/// Responsibility: +/// - Enforce "explicit local declaration" policy for assignments. +/// - Produce consistent diagnostics shared with variable access errors. +pub(in crate::mir::builder) struct AssignmentResolverBox; + +impl AssignmentResolverBox { + pub(in crate::mir::builder) fn ensure_declared( + builder: &MirBuilder, + var_name: &str, + ) -> Result<(), String> { + // Compiler-generated temporaries are not part of the user variable namespace. + if var_name.starts_with("__pin$") { + return Ok(()); + } + + if builder.variable_map.contains_key(var_name) { + return Ok(()); + } + + let mut msg = builder.undefined_variable_message(var_name); + msg.push_str("\nHint: Nyash requires explicit local declaration. Use `local ` before assignment."); + Err(msg) + } +} + diff --git a/src/mir/builder/vars.rs b/src/mir/builder/vars/free_vars.rs similarity index 98% rename from src/mir/builder/vars.rs rename to src/mir/builder/vars/free_vars.rs index 50d428ba..8d1bc419 100644 --- a/src/mir/builder/vars.rs +++ b/src/mir/builder/vars/free_vars.rs @@ -4,7 +4,7 @@ use std::collections::HashSet; /// Collect free variables used in `node` into `used`, excluding names present in `locals`. /// `locals` is updated as new local declarations are encountered. #[allow(dead_code)] -pub(super) fn collect_free_vars( +pub(in crate::mir::builder) fn collect_free_vars( node: &ASTNode, used: &mut HashSet, locals: &mut HashSet, diff --git a/src/mir/builder/vars/lexical_scope.rs b/src/mir/builder/vars/lexical_scope.rs new file mode 100644 index 00000000..70598395 --- /dev/null +++ b/src/mir/builder/vars/lexical_scope.rs @@ -0,0 +1,75 @@ +use crate::mir::ValueId; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Debug, Default)] +pub(in crate::mir::builder) struct LexicalScopeFrame { + declared: BTreeSet, + restore: BTreeMap>, +} + +impl LexicalScopeFrame { + fn new() -> Self { + Self::default() + } +} + +pub(in crate::mir::builder) struct LexicalScopeGuard { + builder: *mut super::super::MirBuilder, +} + +impl LexicalScopeGuard { + pub(in crate::mir::builder) fn new(builder: &mut super::super::MirBuilder) -> Self { + builder.push_lexical_scope(); + Self { builder } + } +} + +impl Drop for LexicalScopeGuard { + fn drop(&mut self) { + // Safety: LexicalScopeGuard is created from a unique `&mut MirBuilder` and its lifetime + // is bounded by the surrounding lexical scope. Drop runs at most once. + unsafe { &mut *self.builder }.pop_lexical_scope(); + } +} + +impl super::super::MirBuilder { + pub(in crate::mir::builder) fn push_lexical_scope(&mut self) { + self.lexical_scope_stack.push(LexicalScopeFrame::new()); + } + + pub(in crate::mir::builder) fn pop_lexical_scope(&mut self) { + let frame = self + .lexical_scope_stack + .pop() + .expect("COMPILER BUG: pop_lexical_scope without push_lexical_scope"); + + for (name, previous) in frame.restore { + match previous { + Some(prev_id) => { + self.variable_map.insert(name, prev_id); + } + None => { + self.variable_map.remove(&name); + } + } + } + } + + pub(in crate::mir::builder) fn declare_local_in_current_scope( + &mut self, + name: &str, + value: ValueId, + ) -> Result<(), String> { + let Some(frame) = self.lexical_scope_stack.last_mut() else { + return Err("COMPILER BUG: local declaration outside lexical scope".to_string()); + }; + + if frame.declared.insert(name.to_string()) { + let previous = self.variable_map.get(name).copied(); + frame.restore.insert(name.to_string(), previous); + } + + self.variable_map.insert(name.to_string(), value); + Ok(()) + } +} diff --git a/src/mir/builder/vars/mod.rs b/src/mir/builder/vars/mod.rs new file mode 100644 index 00000000..5493eaaa --- /dev/null +++ b/src/mir/builder/vars/mod.rs @@ -0,0 +1,3 @@ +pub(super) mod assignment_resolver; +pub(super) mod free_vars; +pub(super) mod lexical_scope; diff --git a/src/mir/join_ir/lowering/README.md b/src/mir/join_ir/lowering/README.md index 72068a04..eccf1586 100644 --- a/src/mir/join_ir/lowering/README.md +++ b/src/mir/join_ir/lowering/README.md @@ -7,4 +7,13 @@ - ConditionEnv は「条件で参照する JoinIR ValueId だけ」を持つ。body-local を直接入れず、必要なら昇格+ScopeManager に解決を任せる。 - Fail-Fast 原則: Unsupported/NotFound は明示エラーにして、by-name ヒューリスティックや静かなフォールバックは禁止。 +## 名前解決の境界(SSOT) + +このディレクトリの `ScopeManager` は「JoinIR lowering の中で」名前を `ValueId` に解決するための箱だよ。 +同じ “名前” でも、MIR 側の束縛寿命とは問題が違うので混ぜない。 + +- **MIR(SSA/束縛寿命)**: `src/mir/builder/vars/*` が `{...}` のレキシカルスコープと `local` のシャドウイングを管理する。 +- **JoinIR lowering(この層)**: `ScopeManager` が `ConditionEnv/LoopBodyLocalEnv/CapturedEnv/CarrierInfo` を束ねて解決順序を固定する。 +- **解析箱**: `LoopConditionScopeBox` は「条件が参照して良いスコープか」を判定する箱で、名前解決そのものはしない。 + 詳しい境界ルールは `docs/development/current/main/phase238-exprlowerer-scope-boundaries.md` を参照してね。*** diff --git a/tools/smokes/v2/profiles/quick/core/phase215/scope_assign_creates_local_vm.sh b/tools/smokes/v2/profiles/quick/core/phase215/scope_assign_creates_local_vm.sh new file mode 100644 index 00000000..e374dedc --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/phase215/scope_assign_creates_local_vm.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/../../../../../../.." && pwd)" +source "$ROOT_DIR/tools/smokes/v2/lib/test_runner.sh" || true +source "$ROOT_DIR/tools/smokes/v2/lib/result_checker.sh" || true + +require_env || { echo "[SKIP] env not ready"; exit 0; } + +test_scope_assign_creates_local_vm() { + local code='static box Main { method main(args) { { if (1==1) { y = 42 } } return y } }' + local tmp + tmp=$(mktemp --suffix .hako) + printf '%s' "$code" > "$tmp" + + # Spec (LANGUAGE_REFERENCE_2025 / syntax-cheatsheet): + # - Undeclared assignment is an error (must use `local y`). + local out + out=$(NYASH_FEATURES=stage3 "$NYASH_BIN" --backend vm "$tmp" 2>&1) + local rc=$? + if [[ "$rc" -eq 0 ]]; then + echo "[FAIL] scope_assign_creates_local_vm: expected failure, got rc=0" + rm -f "$tmp" 2>/dev/null || true + return 1 + fi + if ! printf '%s' "$out" | grep -q "Undefined variable: y"; then + echo "[FAIL] scope_assign_creates_local_vm: missing 'Undefined variable: y' in output" + echo "$out" + rm -f "$tmp" 2>/dev/null || true + return 1 + fi + + echo "[PASS] scope_assign_creates_local_vm" + rm -f "$tmp" 2>/dev/null || true + return 0 +} + +run_test "scope_assign_creates_local_vm" test_scope_assign_creates_local_vm diff --git a/tools/smokes/v2/profiles/quick/core/phase215/scope_loop_body_local_vm.sh b/tools/smokes/v2/profiles/quick/core/phase215/scope_loop_body_local_vm.sh new file mode 100644 index 00000000..75d374ef --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/phase215/scope_loop_body_local_vm.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/../../../../../../.." && pwd)" +source "$ROOT_DIR/tools/smokes/v2/lib/test_runner.sh" || true +source "$ROOT_DIR/tools/smokes/v2/lib/result_checker.sh" || true + +require_env || { echo "[SKIP] env not ready"; exit 0; } + +test_scope_loop_body_local_vm() { + local code='static box Main { method main(args) { local i = 0 local sum = 0 loop(i < 10) { local y = 7 sum = sum + y if i == 1 { break } i = i + 1 } return y } }' + local tmp + tmp=$(mktemp --suffix .hako) + printf '%s' "$code" > "$tmp" + + # Spec: loop body is a block; `local y` must not leak outside the loop. + local out + out=$(NYASH_FEATURES=stage3 "$NYASH_BIN" --backend vm "$tmp" 2>&1) + local rc=$? + if [[ "$rc" -eq 0 ]]; then + echo "[FAIL] scope_loop_body_local_vm: expected failure, got rc=0" + rm -f "$tmp" 2>/dev/null || true + return 1 + fi + if ! printf '%s' "$out" | grep -q "Undefined variable: y"; then + echo "[FAIL] scope_loop_body_local_vm: missing 'Undefined variable: y' in output" + echo "$out" + rm -f "$tmp" 2>/dev/null || true + return 1 + fi + + echo "[PASS] scope_loop_body_local_vm" + rm -f "$tmp" 2>/dev/null || true + return 0 +} + +run_test "scope_loop_body_local_vm" test_scope_loop_body_local_vm diff --git a/tools/smokes/v2/profiles/quick/core/phase215/scope_shadow_vm.sh b/tools/smokes/v2/profiles/quick/core/phase215/scope_shadow_vm.sh new file mode 100644 index 00000000..28faebf4 --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/phase215/scope_shadow_vm.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/../../../../../../.." && pwd)" +source "$ROOT_DIR/tools/smokes/v2/lib/test_runner.sh" || true +source "$ROOT_DIR/tools/smokes/v2/lib/result_checker.sh" || true + +require_env || { echo "[SKIP] env not ready"; exit 0; } + +test_scope_shadow_vm() { + local code='static box Main { method main(args) { local x = 1 { local x = 2 } return x } }' + local tmp + tmp=$(mktemp --suffix .hako) + printf '%s' "$code" > "$tmp" + + # Spec (LANGUAGE_REFERENCE_2025 / syntax-cheatsheet): + # `local` is block-scoped and supports shadowing. + # The inner `local x` must not overwrite the outer binding. + local rc + NYASH_FEATURES=stage3 "$NYASH_BIN" --backend vm "$tmp" >/dev/null 2>&1 + rc=$? + if [[ "$rc" -ne 1 ]]; then + echo "[FAIL] scope_shadow_vm: vm rc=$rc (expected 1)" + rm -f "$tmp" 2>/dev/null || true + return 1 + fi + + echo "[PASS] scope_shadow_vm" + rm -f "$tmp" 2>/dev/null || true + return 0 +} + +run_test "scope_shadow_vm" test_scope_shadow_vm diff --git a/tools/smokes/v2/run.sh b/tools/smokes/v2/run.sh index 3bc9783c..9a5896a7 100644 --- a/tools/smokes/v2/run.sh +++ b/tools/smokes/v2/run.sh @@ -263,7 +263,9 @@ find_test_files() { test_files+=("$file") done < <(find "$profile_dir" -name "*.sh" -type f -print0) - printf '%s\n' "${test_files[@]}" + if [ ${#test_files[@]} -gt 0 ]; then + printf '%s\n' "${test_files[@]}" + fi } # 単一テスト実行