Implement reactive attributes for minimal DOM updates
Features: - Track attributes that depend on signals (ternary expressions) - Store element references in globals for reactive attributes - Re-evaluate and update only changed attributes when signals change - Similar to reactive conditionals but for attributes Performance: - Minimal DOM updates - only changed attributes are updated - No full re-renders, no virtual DOM diffing - Direct attribute updates via js_set_attribute Example: class_='tab active' if tab_index == 0 else 'tab' When tab_index changes, only the class attribute updates. Implementation: - Collection phase: scan for attributes with signal dependencies - Render phase: store element references to globals - Signal update: re-evaluate expression and update attribute All tests passing (55 passed, 2 skipped) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
b0e38db22c
commit
1575571f3e
6 files changed
+761
-474
No files matched your search
Binary file not shown.
@@ -40,6 +40,13 @@ class SignalLLVMIRGenerator:
|
||||
# Track signal-to-conditional dependencies for reactivity
|
||||
self.signal_conditional_deps: dict[str, list[int]] = {} # signal_name -> [conditional_ids]
|
||||
|
||||
# Track reactive attributes (attributes that depend on signals)
|
||||
self.reactive_attributes: list[dict[str, Any]] = [] # List of reactive attribute metadata
|
||||
self.reactive_attr_map: dict[int, dict[str, Any]] = {} # attr_id -> attr_info
|
||||
self.next_reactive_attr_id = 0
|
||||
self.signal_attribute_deps: dict[str, list[int]] = {} # signal_name -> [attr_ids]
|
||||
self.stmt_reactive_attrs: dict[int, list[int]] = {} # stmt id() -> [attr_ids]
|
||||
|
||||
# Track current component for signal scoping
|
||||
self.current_component = None
|
||||
|
||||
@@ -74,14 +81,20 @@ class SignalLLVMIRGenerator:
|
||||
self.conditionals = []
|
||||
self.conditional_map = {}
|
||||
self.signal_conditional_deps = {}
|
||||
self.reactive_attributes = []
|
||||
self.reactive_attr_map = {}
|
||||
self.signal_attribute_deps = {}
|
||||
self.stmt_reactive_attrs = {}
|
||||
|
||||
# Collect signals, handlers, and conditionals for this component
|
||||
# Collect signals, handlers, conditionals, and reactive attributes for this component
|
||||
self._collect_signals_and_handlers(component)
|
||||
self._collect_conditionals(component)
|
||||
self._collect_reactive_attributes(component)
|
||||
|
||||
# Generate component-specific code
|
||||
self._emit_signal_globals(component)
|
||||
self._emit_conditional_globals()
|
||||
self._emit_reactive_attribute_globals()
|
||||
self._emit_helper_functions()
|
||||
self._emit_event_handlers()
|
||||
self._emit_conditional_functions()
|
||||
@@ -176,12 +189,76 @@ class SignalLLVMIRGenerator:
|
||||
signals.extend(self._extract_signals_from_expr(expr["left"]))
|
||||
for comparator in expr.get("comparators", []):
|
||||
signals.extend(self._extract_signals_from_expr(comparator))
|
||||
elif expr["type"] == "ifexp":
|
||||
# Ternary expression - check test, body, and orelse
|
||||
signals.extend(self._extract_signals_from_expr(expr["test"]))
|
||||
signals.extend(self._extract_signals_from_expr(expr["body"]))
|
||||
signals.extend(self._extract_signals_from_expr(expr["orelse"]))
|
||||
elif expr["type"] == "constant":
|
||||
# Constants don't reference signals
|
||||
pass
|
||||
|
||||
return signals
|
||||
|
||||
def _collect_reactive_attributes(self, component: dict[str, Any]) -> None:
|
||||
"""Scan IR tree for attributes that depend on signals."""
|
||||
|
||||
def scan_statements(stmts: list[dict[str, Any]]):
|
||||
for stmt in stmts:
|
||||
if stmt["type"] == "with":
|
||||
# Check if any props have signal dependencies
|
||||
element = stmt["element"]
|
||||
props = element.get("props", {})
|
||||
|
||||
for prop_name, prop_value in props.items():
|
||||
# Skip event handlers
|
||||
if prop_name == "onclick":
|
||||
continue
|
||||
|
||||
# Check if this prop depends on signals
|
||||
signals = self._extract_signals_from_expr(prop_value)
|
||||
|
||||
if signals:
|
||||
# This attribute is reactive!
|
||||
attr_id = self.next_reactive_attr_id
|
||||
self.next_reactive_attr_id += 1
|
||||
|
||||
attr_name = prop_name.rstrip("_") # Remove trailing underscore
|
||||
stmt_id = id(stmt)
|
||||
|
||||
attr_info = {
|
||||
"id": attr_id,
|
||||
"stmt_id": stmt_id, # Use Python id() to identify statement
|
||||
"attr_name": attr_name,
|
||||
"expression": prop_value,
|
||||
"signals": signals,
|
||||
}
|
||||
|
||||
self.reactive_attributes.append(attr_info)
|
||||
self.reactive_attr_map[attr_id] = attr_info
|
||||
|
||||
# Build stmt -> attr map for lookup during render
|
||||
if stmt_id not in self.stmt_reactive_attrs:
|
||||
self.stmt_reactive_attrs[stmt_id] = []
|
||||
self.stmt_reactive_attrs[stmt_id].append(attr_id)
|
||||
|
||||
# Register this attribute as dependent on these signals
|
||||
for signal_name in signals:
|
||||
if signal_name not in self.signal_attribute_deps:
|
||||
self.signal_attribute_deps[signal_name] = []
|
||||
self.signal_attribute_deps[signal_name].append(attr_id)
|
||||
|
||||
# Recursively scan nested statements
|
||||
scan_statements(stmt.get("body", []))
|
||||
elif stmt["type"] == "if":
|
||||
scan_statements(stmt["body"])
|
||||
scan_statements(stmt.get("orelse", []))
|
||||
elif stmt["type"] == "match":
|
||||
for case in stmt["cases"]:
|
||||
scan_statements(case.get("body", []))
|
||||
|
||||
scan_statements(component["body"])
|
||||
|
||||
def _emit_header(self) -> None:
|
||||
"""Emit module header."""
|
||||
self.lines.append("; Generated by Ritz Framework with Signals")
|
||||
@@ -227,6 +304,18 @@ class SignalLLVMIRGenerator:
|
||||
self.lines.append(f"@cond_{cond_id}_case_{case_idx}_root = global i32 0")
|
||||
self.lines.append("")
|
||||
|
||||
def _emit_reactive_attribute_globals(self) -> None:
|
||||
"""Emit global storage for reactive attribute element references."""
|
||||
if not self.reactive_attributes:
|
||||
return
|
||||
|
||||
self.lines.append("; Reactive attribute element storage")
|
||||
for attr_info in self.reactive_attributes:
|
||||
attr_id = attr_info["id"]
|
||||
# Store element reference so we can update its attribute later
|
||||
self.lines.append(f"@reactive_attr_{attr_id}_elem = global i32 0")
|
||||
self.lines.append("")
|
||||
|
||||
def _emit_function_declarations(self) -> None:
|
||||
"""Emit JS bridge function declarations."""
|
||||
self.lines.append("; JavaScript bridge function declarations")
|
||||
@@ -341,6 +430,11 @@ class SignalLLVMIRGenerator:
|
||||
if signal_name in self.signal_conditional_deps:
|
||||
for cond_id in self.signal_conditional_deps[signal_name]:
|
||||
self.lines.append(f" call void @reevaluate_conditional_{cond_id}()")
|
||||
|
||||
# Trigger reactive updates for attributes that depend on this signal
|
||||
if signal_name in self.signal_attribute_deps:
|
||||
for attr_id in self.signal_attribute_deps[signal_name]:
|
||||
self._emit_update_reactive_attribute(attr_id)
|
||||
elif stmt["type"] == "augassign":
|
||||
# Handle signal updates (count += 1)
|
||||
signal_name = stmt["target"]
|
||||
@@ -363,6 +457,36 @@ class SignalLLVMIRGenerator:
|
||||
for cond_id in self.signal_conditional_deps[signal_name]:
|
||||
self.lines.append(f" call void @reevaluate_conditional_{cond_id}()")
|
||||
|
||||
# Trigger reactive updates for attributes that depend on this signal
|
||||
if signal_name in self.signal_attribute_deps:
|
||||
for attr_id in self.signal_attribute_deps[signal_name]:
|
||||
self._emit_update_reactive_attribute(attr_id)
|
||||
|
||||
def _emit_update_reactive_attribute(self, attr_id: int) -> None:
|
||||
"""Emit inline code to update a reactive attribute."""
|
||||
# Look up attribute by ID
|
||||
attr_info = self.reactive_attr_map.get(attr_id)
|
||||
if not attr_info:
|
||||
return # Attribute not found
|
||||
attr_name = attr_info["attr_name"]
|
||||
expression = attr_info["expression"]
|
||||
|
||||
# Load the element reference
|
||||
elem_var = self._new_var()
|
||||
self.lines.append(f" %{elem_var} = load i32, i32* @reactive_attr_{attr_id}_elem")
|
||||
|
||||
# Re-evaluate the attribute expression
|
||||
if expression["type"] == "ifexp":
|
||||
value_ptr = self._emit_string_ifexp(expression)
|
||||
# Set the attribute
|
||||
name_str = self._get_or_create_string(attr_name)
|
||||
self.lines.append(
|
||||
f" call void @js_set_attribute(i32 %{elem_var}, "
|
||||
f"i8* getelementptr inbounds ({self._string_type(attr_name)}, {self._string_type(attr_name)}* {name_str}, i32 0, i32 0), "
|
||||
f"i8* %{value_ptr})"
|
||||
)
|
||||
# Could add support for other expression types here
|
||||
|
||||
def _emit_conditional_functions(self) -> None:
|
||||
"""Emit init and reevaluate functions for each conditional."""
|
||||
for cond_info in self.conditionals:
|
||||
@@ -1021,6 +1145,12 @@ class SignalLLVMIRGenerator:
|
||||
f"i8* getelementptr inbounds ({self._string_type(onclick_handler)}, {self._string_type(onclick_handler)}* {handler_str}, i32 0, i32 0))"
|
||||
)
|
||||
|
||||
# Store element reference if this element has reactive attributes
|
||||
stmt_id = id(stmt)
|
||||
if stmt_id in self.stmt_reactive_attrs:
|
||||
for attr_id in self.stmt_reactive_attrs[stmt_id]:
|
||||
self.lines.append(f" store i32 %{elem_var}, i32* @reactive_attr_{attr_id}_elem")
|
||||
|
||||
# Push this element as the parent for children
|
||||
self.parent_element_stack.append(elem_var)
|
||||
|
||||
|
||||
Reference in new issue
Block a user