MIR: lexical scoping + builder vars modules

This commit is contained in:
nyash-codex
2025-12-13 01:30:04 +09:00
parent 5c75506dcc
commit 1fae4f1648
16 changed files with 388 additions and 29 deletions

View File

@ -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関数内専用)

View File

@ -92,6 +92,12 @@ pub struct MirBuilder {
/// Phase 25.1: HashMap → BTreeMapPHI生成の決定性確保
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

View File

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

View File

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

View 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` は「禁止/許可」「スケジュール」などの補助情報で、束縛そのものではない。
この境界を跨ぐ “便利メソッド” を作るのは原則禁止(責務混線の温床)。

View 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)
}
}

View File

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

View 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(())
}
}

View File

@ -0,0 +1,3 @@
pub(super) mod assignment_resolver;
pub(super) mod free_vars;
pub(super) mod lexical_scope;

View File

@ -7,4 +7,13 @@
- ConditionEnv は「条件で参照する JoinIR ValueId だけ」を持つ。body-local を直接入れず、必要なら昇格ScopeManager に解決を任せる。
- Fail-Fast 原則: Unsupported/NotFound は明示エラーにして、by-name ヒューリスティックや静かなフォールバックは禁止。
## 名前解決の境界SSOT
このディレクトリの `ScopeManager` は「JoinIR lowering の中で」名前を `ValueId` に解決するための箱だよ。
同じ “名前” でも、MIR 側の束縛寿命とは問題が違うので混ぜない。
- **MIRSSA/束縛寿命)**: `src/mir/builder/vars/*``{...}` のレキシカルスコープと `local` のシャドウイングを管理する。
- **JoinIR loweringこの層**: `ScopeManager``ConditionEnv/LoopBodyLocalEnv/CapturedEnv/CarrierInfo` を束ねて解決順序を固定する。
- **解析箱**: `LoopConditionScopeBox` は「条件が参照して良いスコープか」を判定する箱で、名前解決そのものはしない。
詳しい境界ルールは `docs/development/current/main/phase238-exprlowerer-scope-boundaries.md` を参照してね。***