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>
29 lines
1.8 KiB
Markdown
29 lines
1.8 KiB
Markdown
# Nyash Invariants (Spec)
|
||
|
||
This document lists non‑negotiable invariants the implementation preserves across backends. They’re used as a contract for testing and design.
|
||
|
||
Core
|
||
- PHI hygiene
|
||
- No empty PHI nodes are emitted in LLVM IR (temporary safety valve exists behind `NYASH_LLVM_SANITIZE_EMPTY_PHI=1`).
|
||
- All PHIs appear at the beginning of a basic block.
|
||
- Match/Peek
|
||
- A match scrutinee is evaluated exactly once, bound to an internal gensym.
|
||
- Guard conditions are logically conjoined with the arm predicate; fallthrough reaches the next arm.
|
||
- Exceptions
|
||
- Exceptions follow “scope first” semantics. Postfix `catch/cleanup` normalize to a single `TryCatch` block around the immediately‑preceding expression.
|
||
- LoopForm
|
||
- Loop bodies may be normalized where safe to a stable order: non‑assign statements then assign statements. No semantic reorder is performed when a non‑assign appears after any assign.
|
||
- Carrier analysis emits observation hints only (zero runtime cost).
|
||
- Break/continue lowering is unified via LoopBuilder; nested bare blocks inside loops are handled consistently (Program nodes recurse into loop‑aware lowering).
|
||
- Scope
|
||
- Enter/Leave scope events are observable through MIR hints; they do not affect program semantics.
|
||
- Block‑scoped locals: `local x = ...` declares a binding limited to the lexical block. Assignment without `local` updates the nearest enclosing binding; redeclaration with `local` shadows the outer variable. This is Lua‑like and differs from Python's block (no) scope.
|
||
|
||
Observability
|
||
- MIR hints can be traced via `NYASH_MIR_HINTS` (pipe style): `trace|scope|join|loop|phi` or `jsonl=path|loop`.
|
||
- Golden/Smoke tests cover representative invariants.
|
||
|
||
Backends
|
||
- PyVM and LLVM share semantics; PyVM prioritizes clarity over performance.
|
||
- Cranelift/WASM routes inherit the same invariants when enabled.
|