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:
1 parent
ac7726df46
commit
e686903170
4 files changed
+272
-41
No files matched your search
@@ -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("")
|
||||
|
||||
|
||||
Reference in new issue
Block a user