Support multiple components in single file with scoped signals

- Update generate_llvm_ir() to accept list of component dicts
- Process each component with own signal/handler state
- Scope signal globals with component name prefix (@signal_ComponentName_signalName)
- Add _signal_global() helper for scoped signal names
- Update tests for scoped signal names
- All tests passing (30/30)
This commit is contained in:
Aaron Sinclair committed 2025-11-15 03:32:38 -08:00
1 parent 0063a8536c
commit 74ff1e7e9f
6 files changed
+53 -30

No files matched your search

+49 -26
View File
@@ -3,12 +3,12 @@
from typing import Any
def generate_llvm_ir(ir: dict[str, Any]) -> str:
def generate_llvm_ir(ir: dict[str, Any] | list[dict[str, Any]]) -> str:
"""
Generate LLVM IR from Ritz IR with Signal support.
Args:
ir: Component IR dictionary
ir: Component IR dictionary or list of component dictionaries
Returns:
LLVM IR source code with signals stored in WASM memory
@@ -20,8 +20,9 @@ def generate_llvm_ir(ir: dict[str, Any]) -> str:
class SignalLLVMIRGenerator:
"""Generates LLVM IR with reactive Signal support."""
def __init__(self, ir: dict[str, Any]):
self.ir = ir
def __init__(self, ir: dict[str, Any] | list[dict[str, Any]]):
# Normalize to list for uniform processing
self.components = ir if isinstance(ir, list) else [ir]
self.lines: list[str] = []
self.strings: dict[str, str] = {} # value -> global name
self.signals: dict[str, dict[str, Any]] = {} # signal_name -> {initial, index}
@@ -33,25 +34,45 @@ class SignalLLVMIRGenerator:
# Track signal-to-DOM bindings for updates
self.signal_dom_bindings: dict[str, list[str]] = {} # signal_name -> [dom_node_ids]
# Track current component for signal scoping
self.current_component = None
def _signal_global(self, signal_name: str) -> str:
"""Get the scoped global name for a signal."""
if self.current_component:
return f"@signal_{self.current_component['name']}_{signal_name}"
return f"@signal_{signal_name}"
def generate(self) -> str:
"""Generate complete LLVM IR module with signals."""
# First pass: collect signals and handlers
self._collect_signals_and_handlers()
# Generate IR
# Emit shared header and function declarations once
self._emit_header()
self._emit_signal_globals()
self._emit_function_declarations()
self._emit_helper_functions()
self._emit_event_handlers()
self._emit_component(self.ir)
# Process each component
for component in self.components:
# Set current component for signal scoping
self.current_component = component
# Reset component-specific state
self.signals = {}
self.event_handlers = []
# Collect signals and handlers for this component
self._collect_signals_and_handlers(component)
# Generate component-specific code
self._emit_signal_globals(component)
self._emit_helper_functions()
self._emit_event_handlers()
self._emit_component(component)
# Prepend string constants
return self._finalize_with_strings()
def _collect_signals_and_handlers(self) -> None:
"""Collect all signals and event handlers from IR."""
for stmt in self.ir["body"]:
def _collect_signals_and_handlers(self, component: dict[str, Any]) -> None:
"""Collect all signals and event handlers from component IR."""
for stmt in component["body"]:
if stmt["type"] == "signal":
signal_id = self.next_signal_id
self.next_signal_id += 1
@@ -65,19 +86,21 @@ class SignalLLVMIRGenerator:
def _emit_header(self) -> None:
"""Emit module header."""
self.lines.append("; Generated by Ritz Framework with Signals")
self.lines.append(f'; Component: {self.ir["name"]}')
component_names = [c["name"] for c in self.components]
self.lines.append(f'; Components: {", ".join(component_names)}')
self.lines.append("")
def _emit_signal_globals(self) -> None:
"""Emit global variables for signals."""
def _emit_signal_globals(self, component: dict[str, Any]) -> None:
"""Emit global variables for signals in this component."""
if not self.signals:
return
self.lines.append("; Signal storage (reactive state in WASM memory)")
component_name = component["name"]
self.lines.append(f"; Signal storage for {component_name} (reactive state in WASM memory)")
for signal_name, signal_info in self.signals.items():
# Each signal is a global i32
# Each signal is a global i32, scoped to component to avoid collisions
initial_value = signal_info["initial"].get("value", 0)
self.lines.append(f"@signal_{signal_name} = global i32 {initial_value}")
self.lines.append(f"@signal_{component_name}_{signal_name} = global i32 {initial_value}")
self.lines.append("")
def _emit_function_declarations(self) -> None:
@@ -105,7 +128,7 @@ class SignalLLVMIRGenerator:
for signal_name in self.signals:
self.lines.append(f"define i32 @get_{signal_name}() {{")
self.lines.append("entry:")
self.lines.append(f" %val = load i32, i32* @signal_{signal_name}")
self.lines.append(f" %val = load i32, i32* {self._signal_global(signal_name)}")
self.lines.append(" ret i32 %val")
self.lines.append("}")
self.lines.append("")
@@ -114,7 +137,7 @@ class SignalLLVMIRGenerator:
for signal_name in self.signals:
self.lines.append(f"define void @set_{signal_name}(i32 %newval) {{")
self.lines.append("entry:")
self.lines.append(f" store i32 %newval, i32* @signal_{signal_name}")
self.lines.append(f" store i32 %newval, i32* {self._signal_global(signal_name)}")
self.lines.append(" ; TODO: Trigger DOM update callback")
self.lines.append(" ret void")
self.lines.append("}")
@@ -144,7 +167,7 @@ class SignalLLVMIRGenerator:
if signal_name in self.signals:
# Load current value
load_var = self._new_var()
self.lines.append(f" %{load_var} = load i32, i32* @signal_{signal_name}")
self.lines.append(f" %{load_var} = load i32, i32* {self._signal_global(signal_name)}")
# Perform operation
result_var = self._new_var()
@@ -153,7 +176,7 @@ class SignalLLVMIRGenerator:
self.lines.append(f" %{result_var} = add i32 %{load_var}, {value}")
# Store back
self.lines.append(f" store i32 %{result_var}, i32* @signal_{signal_name}")
self.lines.append(f" store i32 %{result_var}, i32* {self._signal_global(signal_name)}")
# TODO: Trigger DOM updates for this signal
self.lines.append(f" ; TODO: Update DOM nodes bound to {signal_name}")
@@ -239,7 +262,7 @@ class SignalLLVMIRGenerator:
signal_name = expr["id"]
if signal_name in self.signals:
load_var = self._new_var()
self.lines.append(f" %{load_var} = load i32, i32* @signal_{signal_name}")
self.lines.append(f" %{load_var} = load i32, i32* {self._signal_global(signal_name)}")
return load_var
else:
# Unknown variable