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

+44 -8
View File
@@ -115,6 +115,22 @@ class RitzRuntime {
this.updateSignalDisplay(signalName, value);
}
}
// Re-evaluate all reactive conditionals
for (const [name, fn] of Object.entries(exports)) {
if (name.startsWith('reevaluate_conditional_') && typeof fn === 'function') {
fn();
this._log(`[Reactive] ${name}()`);
}
}
// Re-evaluate all reactive match statements
for (const [name, fn] of Object.entries(exports)) {
if (name.startsWith('reevaluate_match_') && typeof fn === 'function') {
fn();
this._log(`[Reactive] ${name}()`);
}
}
}
updateSignalDisplay(signalName, value) {
@@ -154,22 +170,33 @@ class RitzRuntime {
// Replace DOM node at marker position
const marker = this.nodes.get(markerId);
if (!marker) {
console.error(`Marker ${markerId} not found`);
console.error(`[replace_at_marker] Marker ${markerId} not found`);
return;
}
const parent = marker.parentNode;
if (!parent) {
console.error(`Marker has no parent`);
console.error(`[replace_at_marker] Marker ${markerId} has no parent`);
return;
}
// Remove old node if it exists (0 means no old node)
this._log(`[replace_at_marker] marker=${markerId}, old=${oldNodeId}, new=${newNodeId}`);
// Remove old node if it exists (0 means no old node to remove)
if (oldNodeId !== 0) {
const oldNode = this.nodes.get(oldNodeId);
if (oldNode && oldNode.parentNode) {
if (!oldNode) {
console.warn(`[replace_at_marker] Old node ${oldNodeId} not found in registry`);
} else if (!oldNode.parentNode) {
console.warn(`[replace_at_marker] Old node ${oldNodeId} not in DOM`);
} else if (oldNode.parentNode !== parent) {
console.warn(`[replace_at_marker] Old node ${oldNodeId} in wrong parent`);
oldNode.parentNode.removeChild(oldNode);
this._log(`Removed old node ${oldNodeId}`);
this._log(`[replace_at_marker] Removed ${oldNodeId} from wrong parent`);
} else {
// Remove from correct parent
parent.removeChild(oldNode);
this._log(`[replace_at_marker] Removed ${oldNodeId} from correct parent`);
}
}
@@ -177,11 +204,20 @@ class RitzRuntime {
if (newNodeId !== 0) {
const newNode = this.nodes.get(newNodeId);
if (!newNode) {
console.error(`New node ${newNodeId} not found`);
console.error(`[replace_at_marker] New node ${newNodeId} not found`);
return;
}
parent.insertBefore(newNode, marker.nextSibling);
this._log(`Inserted node ${newNodeId} at marker ${markerId}`);
// Insert right after the marker
// If newNode is already in DOM elsewhere, this will move it
const nextSibling = marker.nextSibling;
if (nextSibling) {
parent.insertBefore(newNode, nextSibling);
this._log(`[replace_at_marker] Inserted ${newNodeId} before sibling`);
} else {
parent.appendChild(newNode);
this._log(`[replace_at_marker] Appended ${newNodeId} (no sibling)`);
}
}
}