Root Cause: - Else blocks were not propagating variable assignments to outer scope - Bug 1 (if_form.rs): PHI materialization happened before variable_map reset, causing PHI nodes to be lost - Bug 2 (phi.rs): Variable merge didn't check if else branch modified variables Changes: - src/mir/builder/if_form.rs:93-127 - Reordered: reset variable_map BEFORE materializing PHI nodes - Now matches then-branch pattern (reset → materialize → execute) - Applied to both "else" and "no else" branches for consistency - src/mir/builder/phi.rs:137-154 - Added else_modified_var check to detect variable modifications - Use modified value from else_var_map_end_opt when available - Fall back to pre-if value only when truly not modified Test Results: ✅ Simple block: { x=42 } → 42 ✅ If block: if 1 { x=42 } → 42 ✅ Else block: if 0 { x=99 } else { x=42 } → 42 (FIXED!) ✅ Stage-B body extraction: "return 42" correctly extracted (was null) Impact: - Else block variable assignments now work correctly - Stage-B compiler body extraction restored - Selfhost builder path can now function - Foundation for Phase 21.x progress 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
3.3 KiB
Variables and Scope (Local/Block Semantics)
Status: Stable (Stage‑3 surface for local), default strong references.
This document defines the variable model used by Hakorune/Nyash and clarifies how locals interact with blocks, memory, and references across VMs (Rust VM, Hakorune VM, LLVM harness).
Local Variables
- Syntax:
local name = expr - Scope: Block‑scoped. The variable is visible from its declaration to the end of the lexical block.
- Redeclaration: Writing
local name = ...inside a nested block creates a new shadowing binding. Writingname = ...withoutlocalupdates the nearest existing binding in an enclosing scope. - Mutability: Locals are mutable unless future keywords specify otherwise (e.g.,
const). - Lifetime: The variable binding is dropped at block end; any referenced objects live as long as at least one strong reference exists elsewhere.
Notes:
- Stage‑3 gate: Parsing
localrequires Stage‑3 to be enabled (NYASH_PARSER_STAGE3=1or equivalent runner profile).
Assignment Resolution (Enclosing Scope Update)
Assignment to an identifier resolves as follows:
- If a
localdeclaration with the same name exists in the current block, update that binding. - Otherwise, search outward through enclosing blocks and update the first found binding.
- If no binding exists in any enclosing scope, create a new binding in the current scope.
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.
Reference Semantics (Strong/Weak)
- Default: Locals hold strong references to boxes/collections. Implementation uses reference counting (strong = ownership) with internal synchronization.
- Weak references: Use
WeakBoxto hold a non‑owning (weak) reference. Weak refs do not keep the object alive; they can be upgraded to strong at use sites. Intended for back‑pointers and cache‑like links to avoid cycles. - Typical guidance:
- Locals and return values: strong references.
- Object fields that create cycles (child→parent): weak references.
Example (nested block retains object via outer local):
local a = null
{
local b = new Box(a)
a = b // outer binding updated; a and b point to the same object
}
// leaving the block drops `b` (strong‑count ‑1), but `a` still keeps the object alive
Shadowing vs. Updating
- Shadowing:
local x = ...inside a block hides an outerxfor the remainder of the inner block. The outerxremains unchanged. - Updating:
x = ...withoutlocalupdates the nearest enclosingxbinding.
Prefer clarity: avoid accidental shadowing. If you intentionally shadow, consider naming or comments to clarify intent.
Const/Immutability (Future)
- A separate keyword (e.g.,
const) can introduce an immutable local. Semantics: same scoping aslocal, but re‑assignment is a compile error. This does not affect reference ownership (still strong by default).
Cross‑VM Consistency
The above semantics are enforced consistently across:
- Rust VM (MIR interpreter): scope updates propagate to enclosing locals.
- Hakorune VM/runner: same resolution rules.
- LLVM harness/EXE: parity tests validate identical exit codes/behavior.
See also: quick/integration smokes scope_assign_vm.sh, vm_llvm_scope_assign.sh.