Fix reactive conditional rendering with proper branch swapping

Implements efficient reactive conditionals that pre-render branches once
and swap DOM nodes without recreation, enabling fast switching with
thousands of elements.

Key Fixes:
- Runtime now calls reevaluate_conditional_*() on signal changes
- Match/case swap logic uses phi nodes to load correct old case
- Previously hardcoded case_0 as old value, causing accumulation
- Branch render functions now wrap all content in container div

Implementation:
- Pre-render both if/else branches at init
- Store roots in globals (@cond_N_then_root, @cond_N_else_root)
- On signal change, swap DOM nodes via js_replace_at_marker()
- Match statements use switch/phi pattern to select old case

Tests:
- Added test_reactive_conditionals.py (5 new tests)
- Updated test_llvm_codegen_conditional.py for reactive infrastructure
- All 35 tests passing

Performance: No re-rendering on switches, perfect for dynamic content.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Aaron SinclairandClaude committed 2025-11-15 10:32:40 -08:00
1 parent ac7726df46
commit e686903170
4 files changed
+272 -41

No files matched your search

+48 -25
View File
@@ -522,21 +522,41 @@ class SignalLLVMIRGenerator:
# Swap to this case
self.lines.append(f"swap_{case_idx}:")
old_var = self._new_var()
new_var = self._new_var()
marker_var2 = self._new_var()
old_case_var = self._new_var()
# Load the old case root based on current state
# We need to use a phi or switch to load the right old root
# For simplicity, just create a minimal subtree swap
# Load marker and new case root
marker_var2 = self._new_var()
new_var = self._new_var()
self.lines.append(f" %{marker_var2} = load i32, i32* @cond_{cond_id}_marker")
self.lines.append(f" %{new_var} = load i32, i32* @cond_{cond_id}_case_{case_idx}_root")
# Approximate: load old root based on current value
# This is simplified - ideally we'd use a switch or phi
self.lines.append(f" %{old_var} = load i32, i32* @cond_{cond_id}_case_0_root")
# Use switch to load the correct old case root based on current value
old_var = self._new_var()
switch_labels = [f"load_old_{case_idx}_{i}" for i in range(num_cases)]
if default_idx is not None:
self.lines.append(f" switch i32 %{current_var}, label %{switch_labels[default_idx]} [")
else:
self.lines.append(f" switch i32 %{current_var}, label %{switch_labels[0]} [")
for i in range(num_cases):
self.lines.append(f" i32 {i}, label %{switch_labels[i]}")
self.lines.append(" ]")
# Emit load blocks and phi merge
phi_inputs = []
for i in range(num_cases):
self.lines.append(f"{switch_labels[i]}:")
tmp_var = self._new_var()
self.lines.append(f" %{tmp_var} = load i32, i32* @cond_{cond_id}_case_{i}_root")
phi_inputs.append((tmp_var, switch_labels[i]))
self.lines.append(f" br label %merge_{case_idx}")
# Merge point with phi to get old_var
self.lines.append(f"merge_{case_idx}:")
phi_operands = ", ".join([f"[ %{var}, %{label} ]" for var, label in phi_inputs])
self.lines.append(f" %{old_var} = phi i32 {phi_operands}")
# Now do the actual replacement
self.lines.append(f" call void @js_replace_at_marker(i32 %{marker_var2}, i32 %{old_var}, i32 %{new_var})")
self.lines.append(f" store i32 {case_idx}, i32* @cond_{cond_id}_current")
self.lines.append(" br label %done")
@@ -551,27 +571,30 @@ class SignalLLVMIRGenerator:
self._emit_branch_render_function(cond_id, f"case_{case_idx}", case.get("body", []))
def _emit_branch_render_function(self, cond_id: int, branch_name: str, body: list[dict[str, Any]]) -> None:
"""Emit a function that renders a branch and returns its root element."""
"""Emit a function that renders a branch and returns its root element.
Wraps all branch content in a container div to handle multiple elements.
"""
self.lines.append(f"define i32 @_render_cond_{cond_id}_{branch_name}() {{")
self.lines.append("entry:")
# Render branch body
root_var = None
# Create wrapper div for the branch content
wrapper_var = self._new_var()
div_str = self._get_or_create_string("div")
self.lines.append(f" %{wrapper_var} = call i32 @js_create_element(i8* getelementptr inbounds ({self._string_type('div')}, {self._string_type('div')}* {div_str}, i32 0, i32 0))")
# Push wrapper onto parent stack so child elements append to it
self.parent_element_stack.append(wrapper_var)
# Render all statements in the branch body
for stmt in body:
var = self._emit_statement(stmt)
if root_var is None and var:
root_var = var
self._emit_statement(stmt)
# Return root element
if root_var:
self.lines.append(f" ret i32 %{root_var}")
else:
# Empty branch - create empty div
empty_var = self._new_var()
div_str = self._get_or_create_string("div")
self.lines.append(f" %{empty_var} = call i32 @js_create_element(i8* getelementptr inbounds ({self._string_type('div')}, {self._string_type('div')}* {div_str}, i32 0, i32 0))")
self.lines.append(f" ret i32 %{empty_var}")
# Pop wrapper from parent stack
self.parent_element_stack.pop()
# Return the wrapper element
self.lines.append(f" ret i32 %{wrapper_var}")
self.lines.append("}")
self.lines.append("")