From 801833df8d62b8bee9bd9b0eb14c87ca083ec79a Mon Sep 17 00:00:00 2001 From: nyash-codex Date: Thu, 13 Nov 2025 18:55:14 +0900 Subject: [PATCH] fix(env): improve Environment::set scope resolution (partial) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed: - Environment::set now properly searches ancestor chain before creating new binding - Added exists_in_chain_locked() helper for explicit existence checking - Simple {} blocks now correctly update outer scope variables Verified Working: - local x = 10; { x = 42 }; print(x) → prints 42 ✅ Still Broken: - else blocks don't update outer scope variables - local x = 10; if flag { x = 99 } else { x = 42 }; print(x) → prints 10 ❌ Root Cause Identified: - Issue is in MIR Builder (compile-time), not Environment (runtime) - src/mir/builder/if_form.rs:108 resets variable_map before else block - PHI generation at merge doesn't use else_var_map_end correctly - MIR shows: phi [%32, bb1], [%1, bb2] where %1 is original value, not else value Next: Fix else block variable merging in if_form.rs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lang/src/compiler/entry/compiler_stageb.hako | 98 ++++++++++++++++++- src/environment.rs | 32 ++++-- .../parity/vm_llvm_scope_assign.sh | 17 ++++ .../quick/core/phase215/scope_assign_vm.sh | 29 ++++++ .../phase215/stageb_scope_extract_canary.sh | 22 +++++ 5 files changed, 187 insertions(+), 11 deletions(-) create mode 100644 tools/smokes/v2/profiles/integration/parity/vm_llvm_scope_assign.sh create mode 100644 tools/smokes/v2/profiles/quick/core/phase215/scope_assign_vm.sh create mode 100644 tools/smokes/v2/profiles/quick/core/phase215/stageb_scope_extract_canary.sh diff --git a/lang/src/compiler/entry/compiler_stageb.hako b/lang/src/compiler/entry/compiler_stageb.hako index 54e2f1e8..92c00598 100644 --- a/lang/src/compiler/entry/compiler_stageb.hako +++ b/lang/src/compiler/entry/compiler_stageb.hako @@ -102,6 +102,13 @@ static box Main { } } } + { + local dbg = env.get("HAKO_STAGEB_DEBUG") + if dbg != null && ("" + dbg) == "1" { + print("[DEBUG] Source length: " + s.length()) + print("[DEBUG] k0 (main position) = " + k0) + } + } if k0 >= 0 { // find '(' after k0 (skip inside strings) local k1 = -1 @@ -124,6 +131,12 @@ static box Main { j = j + 1 } } + { + local dbg = env.get("HAKO_STAGEB_DEBUG") + if dbg != null && ("" + dbg) == "1" { + print("[DEBUG] k1 ('(' position) = " + k1) + } + } if k1 >= 0 { // find ')' after k1 (skip inside strings) local k2 = -1 @@ -146,6 +159,12 @@ static box Main { j = j + 1 } } + { + local dbg = env.get("HAKO_STAGEB_DEBUG") + if dbg != null && ("" + dbg) == "1" { + print("[DEBUG] k2 (')' position) = " + k2) + } + } if k2 >= 0 { // Find opening '{' following ')' (skip inside strings) local k3 = -1 @@ -168,6 +187,12 @@ static box Main { j = j + 1 } } + { + local dbg = env.get("HAKO_STAGEB_DEBUG") + if dbg != null && ("" + dbg) == "1" { + print("[DEBUG] k3 ('{' position) = " + k3) + } + } if k3 >= 0 { // Balanced scan for matching '}' local depth = 0 @@ -175,6 +200,8 @@ static box Main { local n = s.length() local in_str = 0 local esc = 0 + local start_pos = -1 + local end_pos = -1 loop(i < n) { local ch = s.substring(i, i + 1) if in_str == 1 { @@ -194,16 +221,85 @@ static box Main { } i = i + 1 } + { + local dbg = env.get("HAKO_STAGEB_DEBUG") + if dbg != null && ("" + dbg) == "1" { + print("[DEBUG] After balanced scan: depth=" + depth + ", i=" + i) + } + } if depth == 0 { // inside of '{'..'}' - body_src = s.substring(k3 + 1, i - 1) + start_pos = k3 + 1 + end_pos = i - 1 + { + local dbg = env.get("HAKO_STAGEB_DEBUG") + if dbg != null && ("" + dbg) == "1" { + print("[DEBUG] Will extract substring(" + start_pos + ", " + end_pos + ")") + } + } + } + // Extract outside the nested blocks to avoid potential scoping issues + if start_pos >= 0 && end_pos >= start_pos { + body_src = s.substring(start_pos, end_pos) + { + local dbg = env.get("HAKO_STAGEB_DEBUG") + if dbg != null && ("" + dbg) == "1" { + if body_src == null { + print("[DEBUG] After extraction: body_src is NULL!") + } else { + print("[DEBUG] After extraction: body_src length = " + body_src.length()) + } + } + } + } + { + local dbg = env.get("HAKO_STAGEB_DEBUG") + if dbg != null && ("" + dbg) == "1" { + if body_src == null { print("[DEBUG] body_src is NULL after line 254") } else { print("[DEBUG] body_src OK after line 254, len=" + body_src.length()) } + } + } + } + { + local dbg = env.get("HAKO_STAGEB_DEBUG") + if dbg != null && ("" + dbg) == "1" { + if body_src == null { print("[DEBUG] body_src is NULL after line 256") } else { print("[DEBUG] body_src OK after line 256, len=" + body_src.length()) } } } } + { + local dbg = env.get("HAKO_STAGEB_DEBUG") + if dbg != null && ("" + dbg) == "1" { + if body_src == null { print("[DEBUG] body_src is NULL after line 268") } else { print("[DEBUG] body_src OK after line 268, len=" + body_src.length()) } + } + } + } + { + local dbg = env.get("HAKO_STAGEB_DEBUG") + if dbg != null && ("" + dbg) == "1" { + if body_src == null { print("[DEBUG] body_src is NULL after line 269") } else { print("[DEBUG] body_src OK after line 269, len=" + body_src.length()) } + } + } + } + { + local dbg = env.get("HAKO_STAGEB_DEBUG") + if dbg != null && ("" + dbg) == "1" { + if body_src == null { print("[DEBUG] body_src is NULL after line 270") } else { print("[DEBUG] body_src OK after line 270, len=" + body_src.length()) } } } } // Fallback: if extraction failed or produced empty, use full src + { + local dbg = env.get("HAKO_STAGEB_DEBUG") + if dbg != null && ("" + dbg) == "1" { + print("[DEBUG] Body extraction:") + if body_src == null { + print("[DEBUG] body_src = null") + } else { + print("[DEBUG] body_src length = " + ("" + body_src).length()) + print("[DEBUG] body_src = [" + body_src + "]") + } + } + } if body_src == null || ("" + body_src).length() == 0 { body_src = src } } diff --git a/src/environment.rs b/src/environment.rs index eac7c6c9..0a372d75 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -109,20 +109,32 @@ impl Environment { return Ok(()); } - // 親スコープで再帰的に検索・設定 + // 祖先いずれかに既存があれば「既存を更新」、無ければ「現在スコープに新規定義」 if let Some(parent) = &self.parent { - match parent.lock().unwrap().set(&name, value.clone_or_share()) { - Ok(()) => return Ok(()), - Err(EnvironmentError::UndefinedVariable { .. }) => { - // 親にもない場合は現在のスコープに新規定義 - } - Err(e) => return Err(e), + if Self::exists_in_chain_locked(&parent.lock().unwrap(), &name) { + // 祖先のどこかに存在 → そこで更新 + parent.lock().unwrap().set(&name, value) + } else { + // どこにも存在しない → 現在のスコープで定義 + self.bindings.lock().unwrap().insert(name, value); + Ok(()) } + } else { + // 親がない(グローバル)→ 現在で定義 + self.bindings.lock().unwrap().insert(name, value); + Ok(()) } + } - // 新規定義として現在のスコープに追加 - self.bindings.lock().unwrap().insert(name, value); - Ok(()) + /// 祖先(自分含む)に名前が存在するか(ロック下で使用) + fn exists_in_chain_locked(&self, name: &str) -> bool { + if self.bindings.lock().unwrap().contains_key(name) { + return true; + } + if let Some(parent) = &self.parent { + return parent.lock().unwrap().exists_in_chain_locked(name); + } + false } /// 変数が存在するかチェック diff --git a/tools/smokes/v2/profiles/integration/parity/vm_llvm_scope_assign.sh b/tools/smokes/v2/profiles/integration/parity/vm_llvm_scope_assign.sh new file mode 100644 index 00000000..53072170 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/parity/vm_llvm_scope_assign.sh @@ -0,0 +1,17 @@ +#!/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_vm_llvm_scope_assign() { + local code='static box Main { method main(args) { local x = 0 { if (1==1) { x = 42 } } return x } }' + # Stage-3 parse is required for `local` + NYASH_PARSER_STAGE3=1 check_parity -c "$code" "vm_llvm_scope_assign" +} + +run_test "vm_llvm_scope_assign" test_vm_llvm_scope_assign diff --git a/tools/smokes/v2/profiles/quick/core/phase215/scope_assign_vm.sh b/tools/smokes/v2/profiles/quick/core/phase215/scope_assign_vm.sh new file mode 100644 index 00000000..9824a57b --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/phase215/scope_assign_vm.sh @@ -0,0 +1,29 @@ +#!/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_vm() { + local code='static box Main { method main(args) { local x = 0 { if (1==1) { x = 42 } } return x } }' + local tmp + tmp=$(mktemp --suffix .hako) + printf '%s' "$code" > "$tmp" + # Quick: VM should reflect nested assignment + local rc + NYASH_PARSER_STAGE3=1 "$NYASH_BIN" --backend vm "$tmp" >/dev/null 2>&1 + rc=$? + if [[ "$rc" -ne 42 ]]; then + echo "[FAIL] scope_assign_vm: vm rc=$rc (expected 42)" + return 1 + fi + echo "[PASS] scope_assign_vm" + rm -f "$tmp" 2>/dev/null || true + return 0 +} + +run_test "scope_assign_vm" test_scope_assign_vm diff --git a/tools/smokes/v2/profiles/quick/core/phase215/stageb_scope_extract_canary.sh b/tools/smokes/v2/profiles/quick/core/phase215/stageb_scope_extract_canary.sh new file mode 100644 index 00000000..4c1d9e04 --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/phase215/stageb_scope_extract_canary.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../../../../../.." && pwd)" +BIN="$ROOT/target/release/hakorune" + +if [[ ! -x "$BIN" ]]; then + echo "[SKIP] hakorune not built"; exit 0 +fi + +# Source with nested assignment; Stage-B should extract body or at least output Program JSON +SRC='static box Main { method main(args) { local x = 0 { if (1==1) { x = 42 } } return x } }' + +out=$(NYASH_ENABLE_USING=1 HAKO_ENABLE_USING=1 "$BIN" --backend vm "$ROOT/lang/src/compiler/entry/compiler_stageb.hako" -- --source "$SRC" 2>/dev/null || true) + +echo "$out" | grep -q '"kind":"Program"' || { echo "[FAIL] stageb_scope_extract_canary: no Program JSON"; exit 1; } + +# Heuristic check: JSON contains at least a numeric literal 42 somewhere +echo "$out" | grep -q '42' || { echo "[FAIL] stageb_scope_extract_canary: literal 42 not found"; exit 1; } + +echo "[PASS] stageb_scope_extract_canary" +