MIR: lexical scoping + builder vars modules
This commit is contained in:
@ -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<String, ValueId>` を持つ(`src/mir/builder.rs:90`)。
|
||||
- `build_variable_access` は `variable_map.get(name)` のみで解決し、無ければ即 `Undefined variable: <name>` エラー(`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 化するのが最小差分になる。
|
||||
@ -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`.
|
||||
|
||||
|
||||
@ -590,8 +590,8 @@ pub enum ASTNode {
|
||||
span: Span,
|
||||
},
|
||||
|
||||
/// ScopeBox(オプション): 診断/マクロ可視性のためのno-opスコープ。
|
||||
/// 正規化で注入され、MIRビルダがブロックとして処理(意味不変)。
|
||||
/// ScopeBox(オプション): 正規化で注入される明示的なレキシカルスコープ境界。
|
||||
/// MIR ビルダは `{ ... }` と同様にブロックとして処理する(local のシャドウイング/寿命を分離)。
|
||||
ScopeBox { body: Vec<ASTNode>, span: Span },
|
||||
|
||||
/// Outbox変数宣言: outbox x, y, z (static関数内専用)
|
||||
|
||||
@ -92,6 +92,12 @@ pub struct MirBuilder {
|
||||
/// Phase 25.1: HashMap → BTreeMap(PHI生成の決定性確保)
|
||||
pub(super) variable_map: BTreeMap<String, ValueId>,
|
||||
|
||||
/// 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<vars::lexical_scope::LexicalScopeFrame>,
|
||||
|
||||
/// 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 <module> [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 <module> [as Alias]' or check nyash.toml [using].");
|
||||
}
|
||||
|
||||
msg
|
||||
}
|
||||
|
||||
/// Build assignment
|
||||
pub(super) fn build_assignment(
|
||||
&mut self,
|
||||
var_name: String,
|
||||
value: ASTNode,
|
||||
) -> Result<ValueId, String> {
|
||||
// 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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
|
||||
26
src/mir/builder/vars/README.md
Normal file
26
src/mir/builder/vars/README.md
Normal file
@ -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` は「禁止/許可」「スケジュール」などの補助情報で、束縛そのものではない。
|
||||
|
||||
この境界を跨ぐ “便利メソッド” を作るのは原則禁止(責務混線の温床)。
|
||||
29
src/mir/builder/vars/assignment_resolver.rs
Normal file
29
src/mir/builder/vars/assignment_resolver.rs
Normal file
@ -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 <name>` before assignment.");
|
||||
Err(msg)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String>,
|
||||
locals: &mut HashSet<String>,
|
||||
75
src/mir/builder/vars/lexical_scope.rs
Normal file
75
src/mir/builder/vars/lexical_scope.rs
Normal file
@ -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<String>,
|
||||
restore: BTreeMap<String, Option<ValueId>>,
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
3
src/mir/builder/vars/mod.rs
Normal file
3
src/mir/builder/vars/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub(super) mod assignment_resolver;
|
||||
pub(super) mod free_vars;
|
||||
pub(super) mod lexical_scope;
|
||||
@ -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` を参照してね。***
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
}
|
||||
|
||||
# 単一テスト実行
|
||||
|
||||
Reference in New Issue
Block a user