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
+51 -28

No files matched your search

+47 -24
View File
@@ -3,12 +3,12 @@
from typing import Any 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. Generate LLVM IR from Ritz IR with Signal support.
Args: Args:
ir: Component IR dictionary ir: Component IR dictionary or list of component dictionaries
Returns: Returns:
LLVM IR source code with signals stored in WASM memory 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: class SignalLLVMIRGenerator:
"""Generates LLVM IR with reactive Signal support.""" """Generates LLVM IR with reactive Signal support."""
def __init__(self, ir: dict[str, Any]): def __init__(self, ir: dict[str, Any] | list[dict[str, Any]]):
self.ir = ir # Normalize to list for uniform processing
self.components = ir if isinstance(ir, list) else [ir]
self.lines: list[str] = [] self.lines: list[str] = []
self.strings: dict[str, str] = {} # value -> global name self.strings: dict[str, str] = {} # value -> global name
self.signals: dict[str, dict[str, Any]] = {} # signal_name -> {initial, index} self.signals: dict[str, dict[str, Any]] = {} # signal_name -> {initial, index}
@@ -33,25 +34,45 @@ class SignalLLVMIRGenerator:
# Track signal-to-DOM bindings for updates # Track signal-to-DOM bindings for updates
self.signal_dom_bindings: dict[str, list[str]] = {} # signal_name -> [dom_node_ids] 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: def generate(self) -> str:
"""Generate complete LLVM IR module with signals.""" """Generate complete LLVM IR module with signals."""
# First pass: collect signals and handlers # Emit shared header and function declarations once
self._collect_signals_and_handlers()
# Generate IR
self._emit_header() self._emit_header()
self._emit_signal_globals()
self._emit_function_declarations() self._emit_function_declarations()
# 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_helper_functions()
self._emit_event_handlers() self._emit_event_handlers()
self._emit_component(self.ir) self._emit_component(component)
# Prepend string constants # Prepend string constants
return self._finalize_with_strings() return self._finalize_with_strings()
def _collect_signals_and_handlers(self) -> None: def _collect_signals_and_handlers(self, component: dict[str, Any]) -> None:
"""Collect all signals and event handlers from IR.""" """Collect all signals and event handlers from component IR."""
for stmt in self.ir["body"]: for stmt in component["body"]:
if stmt["type"] == "signal": if stmt["type"] == "signal":
signal_id = self.next_signal_id signal_id = self.next_signal_id
self.next_signal_id += 1 self.next_signal_id += 1
@@ -65,19 +86,21 @@ class SignalLLVMIRGenerator:
def _emit_header(self) -> None: def _emit_header(self) -> None:
"""Emit module header.""" """Emit module header."""
self.lines.append("; Generated by Ritz Framework with Signals") 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("") self.lines.append("")
def _emit_signal_globals(self) -> None: def _emit_signal_globals(self, component: dict[str, Any]) -> None:
"""Emit global variables for signals.""" """Emit global variables for signals in this component."""
if not self.signals: if not self.signals:
return 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(): 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) 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("") self.lines.append("")
def _emit_function_declarations(self) -> None: def _emit_function_declarations(self) -> None:
@@ -105,7 +128,7 @@ class SignalLLVMIRGenerator:
for signal_name in self.signals: for signal_name in self.signals:
self.lines.append(f"define i32 @get_{signal_name}() {{") self.lines.append(f"define i32 @get_{signal_name}() {{")
self.lines.append("entry:") 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(" ret i32 %val")
self.lines.append("}") self.lines.append("}")
self.lines.append("") self.lines.append("")
@@ -114,7 +137,7 @@ class SignalLLVMIRGenerator:
for signal_name in self.signals: for signal_name in self.signals:
self.lines.append(f"define void @set_{signal_name}(i32 %newval) {{") self.lines.append(f"define void @set_{signal_name}(i32 %newval) {{")
self.lines.append("entry:") 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(" ; TODO: Trigger DOM update callback")
self.lines.append(" ret void") self.lines.append(" ret void")
self.lines.append("}") self.lines.append("}")
@@ -144,7 +167,7 @@ class SignalLLVMIRGenerator:
if signal_name in self.signals: if signal_name in self.signals:
# Load current value # Load current value
load_var = self._new_var() 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 # Perform operation
result_var = self._new_var() result_var = self._new_var()
@@ -153,7 +176,7 @@ class SignalLLVMIRGenerator:
self.lines.append(f" %{result_var} = add i32 %{load_var}, {value}") self.lines.append(f" %{result_var} = add i32 %{load_var}, {value}")
# Store back # 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 # TODO: Trigger DOM updates for this signal
self.lines.append(f" ; TODO: Update DOM nodes bound to {signal_name}") self.lines.append(f" ; TODO: Update DOM nodes bound to {signal_name}")
@@ -239,7 +262,7 @@ class SignalLLVMIRGenerator:
signal_name = expr["id"] signal_name = expr["id"]
if signal_name in self.signals: if signal_name in self.signals:
load_var = self._new_var() 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 return load_var
else: else:
# Unknown variable # Unknown variable
+2 -2
View File
@@ -21,8 +21,8 @@ def Toggle():
ir = parse_component(source) ir = parse_component(source)
llvm_ir = generate_llvm_ir(ir) llvm_ir = generate_llvm_ir(ir)
# Should have signal global # Should have signal global (scoped to component)
assert "@signal_visible = global i32 1" in llvm_ir assert "@signal_Toggle_visible = global i32 1" in llvm_ir
# Should have conditional branch # Should have conditional branch
assert "br i1" in llvm_ir assert "br i1" in llvm_ir
+2 -2
View File
@@ -55,8 +55,8 @@ def test_signal_with_fstring():
ir = parse_component(source) ir = parse_component(source)
llvm_ir = generate_llvm_ir(ir) llvm_ir = generate_llvm_ir(ir)
# Should have signal storage # Should have signal storage (scoped to component)
assert "@signal_count" in llvm_ir assert "@signal_Counter_count" in llvm_ir
# Should have getter function # Should have getter function
assert "get_count" in llvm_ir assert "get_count" in llvm_ir