Files
ritz/ritz/llvm_codegen_signals.py
T
Aaron SinclairandClaude 3540c44abb Implement reactive match/case rendering
- Implemented _emit_match_reactive() to generate init/reevaluate functions
- Modified _emit_match() to create markers and call init functions
- Init function pre-renders all cases and uses switch to insert active one
- Reevaluate function checks if subject changed and swaps DOM

Match init function:
- Calls _render_cond_X_case_Y() for each case
- Stores all case roots in globals
- Evaluates subject with switch instruction
- Inserts matching case at marker position

Fallback to _emit_match_static() if not collected.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-15 03:55:26 -08:00

936 lines
39 KiB
Python

"""LLVM IR code generator with Signal support for Ritz components."""
from typing import Any
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 or list of component dictionaries
Returns:
LLVM IR source code with signals stored in WASM memory
"""
generator = SignalLLVMIRGenerator(ir)
return generator.generate()
class SignalLLVMIRGenerator:
"""Generates LLVM IR with reactive Signal support."""
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}
self.event_handlers: list[dict[str, Any]] = [] # List of event handler functions
self.conditionals: list[dict[str, Any]] = [] # List of conditional branches (if/match)
self.conditional_map: dict[int, int] = {} # stmt id() -> conditional id
self.next_string_id = 0
self.next_var_id = 0
self.next_signal_id = 0
self.next_conditional_id = 0
# 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
# Track parent element for appending children in if/match branches
self.parent_element_stack: list[str] = []
# Track if we're inside a conditional branch
self.in_conditional_branch = False
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."""
# Emit shared header and function declarations once
self._emit_header()
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 = []
self.conditionals = []
self.conditional_map = {}
# Collect signals, handlers, and conditionals for this component
self._collect_signals_and_handlers(component)
self._collect_conditionals(component)
# Generate component-specific code
self._emit_signal_globals(component)
self._emit_conditional_globals()
self._emit_helper_functions()
self._emit_event_handlers()
self._emit_conditional_functions()
self._emit_component(component)
# Prepend string constants
return self._finalize_with_strings()
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
self.signals[stmt["name"]] = {
"initial": stmt["initial"],
"id": signal_id,
}
elif stmt["type"] == "function":
self.event_handlers.append(stmt)
def _collect_conditionals(self, component: dict[str, Any]) -> None:
"""Scan IR tree for if/match statements and assign IDs."""
def scan_statements(stmts: list[dict[str, Any]]):
for stmt in stmts:
if stmt["type"] == "if":
cond_id = self.next_conditional_id
self.next_conditional_id += 1
self.conditional_map[id(stmt)] = cond_id
self.conditionals.append({
"id": cond_id,
"type": "if",
"test": stmt["test"],
"then_body": stmt["body"],
"else_body": stmt.get("orelse", []),
})
# Recursively scan nested statements
scan_statements(stmt["body"])
scan_statements(stmt.get("orelse", []))
elif stmt["type"] == "match":
cond_id = self.next_conditional_id
self.next_conditional_id += 1
self.conditional_map[id(stmt)] = cond_id
self.conditionals.append({
"id": cond_id,
"type": "match",
"subject": stmt["subject"],
"cases": stmt["cases"],
})
# Recursively scan nested statements in each case
for case in stmt["cases"]:
scan_statements(case.get("body", []))
elif stmt["type"] == "with":
scan_statements(stmt.get("body", []))
scan_statements(component["body"])
def _emit_header(self) -> None:
"""Emit module header."""
self.lines.append("; Generated by Ritz Framework with Signals")
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, component: dict[str, Any]) -> None:
"""Emit global variables for signals in this component."""
if not self.signals:
return
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, scoped to component to avoid collisions
initial_value = signal_info["initial"].get("value", 0)
self.lines.append(f"@signal_{component_name}_{signal_name} = global i32 {initial_value}")
self.lines.append("")
def _emit_conditional_globals(self) -> None:
"""Emit global state for reactive conditionals."""
if not self.conditionals:
return
self.lines.append("; Conditional rendering state")
for cond_info in self.conditionals:
cond_id = cond_info["id"]
cond_type = cond_info["type"]
# Common state for all conditionals
self.lines.append(f"@cond_{cond_id}_marker = global i32 0")
self.lines.append(f"@cond_{cond_id}_current = global i32 0")
if cond_type == "if":
# Binary conditional: then_root, else_root
self.lines.append(f"@cond_{cond_id}_then_root = global i32 0")
self.lines.append(f"@cond_{cond_id}_else_root = global i32 0")
elif cond_type == "match":
# Multi-way conditional: one root per case
num_cases = len(cond_info["cases"])
for case_idx in range(num_cases):
self.lines.append(f"@cond_{cond_id}_case_{case_idx}_root = global i32 0")
self.lines.append("")
def _emit_function_declarations(self) -> None:
"""Emit JS bridge function declarations."""
self.lines.append("; JavaScript bridge function declarations")
self.lines.append("declare i32 @js_create_element(i8*)")
self.lines.append("declare i32 @js_create_text(i8*)")
self.lines.append("declare void @js_append_child(i32, i32)")
self.lines.append("declare void @js_set_attribute(i32, i8*, i8*)")
self.lines.append("declare void @js_set_root(i32)")
self.lines.append(
"declare void @js_bind_event(i32, i8*, i8*)"
) # elem, event_name, handler_name
self.lines.append("declare void @js_update_text_content(i32, i8*)") # Update DOM node text
self.lines.append("declare i32 @js_create_marker()") # Create invisible marker element
self.lines.append("declare void @js_replace_at_marker(i32, i32, i32)") # marker, old, new
self.lines.append("")
def _emit_helper_functions(self) -> None:
"""Emit helper functions for signal operations."""
if not self.signals:
return
self.lines.append("; Signal helper functions")
# Emit getter for each signal (exported for JS to call)
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* {self._signal_global(signal_name)}")
self.lines.append(" ret i32 %val")
self.lines.append("}")
self.lines.append("")
# Emit setter for each signal (exported for JS to call)
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* {self._signal_global(signal_name)}")
self.lines.append(" ; TODO: Trigger DOM update callback")
self.lines.append(" ret void")
self.lines.append("}")
self.lines.append("")
def _emit_event_handlers(self) -> None:
"""Emit event handler functions (exported for JS to call)."""
for handler in self.event_handlers:
func_name = handler["name"]
self.lines.append(f"; Event handler: {func_name}")
self.lines.append(f"define void @{func_name}() {{")
self.lines.append("entry:")
# Emit handler body
for stmt in handler["body"]:
self._emit_handler_statement(stmt)
self.lines.append(" ret void")
self.lines.append("}")
self.lines.append("")
def _emit_handler_statement(self, stmt: dict[str, Any]) -> None:
"""Emit a statement inside an event handler."""
if stmt["type"] == "assign":
# Handle direct signal assignment (x = 5)
signal_name = stmt["target"]
if signal_name in self.signals:
# Evaluate the value expression
value_var = self._emit_expr(stmt["value"])
# Store the value
self.lines.append(f" store i32 {value_var}, i32* {self._signal_global(signal_name)}")
# TODO: Trigger DOM updates
self.lines.append(f" ; TODO: Update DOM nodes bound to {signal_name}")
elif stmt["type"] == "augassign":
# Handle signal updates (count += 1)
signal_name = stmt["target"]
if signal_name in self.signals:
# Load current value
load_var = self._new_var()
self.lines.append(f" %{load_var} = load i32, i32* {self._signal_global(signal_name)}")
# Perform operation
result_var = self._new_var()
if stmt["op"] == "Add":
value = stmt["value"]["value"]
self.lines.append(f" %{result_var} = add i32 %{load_var}, {value}")
# Store back
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}")
def _emit_conditional_functions(self) -> None:
"""Emit init and reevaluate functions for each conditional."""
for cond_info in self.conditionals:
if cond_info["type"] == "if":
self._emit_if_reactive(cond_info)
elif cond_info["type"] == "match":
self._emit_match_reactive(cond_info)
def _emit_if_reactive(self, cond_info: dict[str, Any]) -> None:
"""Emit reactive init and reevaluate functions for if/else."""
cond_id = cond_info["id"]
# Emit init function
self.lines.append(f"; Initialize conditional {cond_id}")
self.lines.append(f"define void @_init_conditional_{cond_id}() {{")
self.lines.append("entry:")
# Render both branches and store their roots
self.lines.append(" ; Render both branches upfront")
then_root_var = self._new_var()
else_root_var = self._new_var()
self.lines.append(f" %{then_root_var} = call i32 @_render_cond_{cond_id}_then()")
self.lines.append(f" %{else_root_var} = call i32 @_render_cond_{cond_id}_else()")
self.lines.append(f" store i32 %{then_root_var}, i32* @cond_{cond_id}_then_root")
self.lines.append(f" store i32 %{else_root_var}, i32* @cond_{cond_id}_else_root")
# Evaluate condition and insert the active branch at marker
test_var = self._emit_expr(cond_info["test"])
cond_var = self._new_var()
self.lines.append(f" %{cond_var} = icmp ne i32 %{test_var}, 0")
# Load marker
marker_var = self._new_var()
self.lines.append(f" %{marker_var} = load i32, i32* @cond_{cond_id}_marker")
# Branch to insert correct element
self.lines.append(f" br i1 %{cond_var}, label %insert_then, label %insert_else")
self.lines.append("insert_then:")
self.lines.append(f" call void @js_replace_at_marker(i32 %{marker_var}, i32 0, i32 %{then_root_var})")
self.lines.append(f" store i32 1, i32* @cond_{cond_id}_current")
self.lines.append(" ret void")
self.lines.append("insert_else:")
self.lines.append(f" call void @js_replace_at_marker(i32 %{marker_var}, i32 0, i32 %{else_root_var})")
self.lines.append(f" store i32 0, i32* @cond_{cond_id}_current")
self.lines.append(" ret void")
self.lines.append("}")
self.lines.append("")
# Emit reevaluate function
self.lines.append(f"; Re-evaluate conditional {cond_id}")
self.lines.append(f"define void @reevaluate_conditional_{cond_id}() {{")
self.lines.append("entry:")
# Re-evaluate condition
test_var2 = self._emit_expr(cond_info["test"])
cond_var2 = self._new_var()
self.lines.append(f" %{cond_var2} = icmp ne i32 %{test_var2}, 0")
# Convert to i32
cond_i32 = self._new_var()
self.lines.append(f" %{cond_i32} = zext i1 %{cond_var2} to i32")
# Load current state
current_var = self._new_var()
self.lines.append(f" %{current_var} = load i32, i32* @cond_{cond_id}_current")
# Check if changed
changed_var = self._new_var()
self.lines.append(f" %{changed_var} = icmp ne i32 %{current_var}, %{cond_i32}")
self.lines.append(f" br i1 %{changed_var}, label %swap, label %done")
# Swap branch
self.lines.append("swap:")
self.lines.append(f" br i1 %{cond_var2}, label %swap_to_then, label %swap_to_else")
# Swap to then
self.lines.append("swap_to_then:")
old_var = self._new_var()
new_var = self._new_var()
marker_var2 = self._new_var()
self.lines.append(f" %{old_var} = load i32, i32* @cond_{cond_id}_else_root")
self.lines.append(f" %{new_var} = load i32, i32* @cond_{cond_id}_then_root")
self.lines.append(f" %{marker_var2} = load i32, i32* @cond_{cond_id}_marker")
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 1, i32* @cond_{cond_id}_current")
self.lines.append(" br label %done")
# Swap to else
self.lines.append("swap_to_else:")
old_var2 = self._new_var()
new_var2 = self._new_var()
marker_var3 = self._new_var()
self.lines.append(f" %{old_var2} = load i32, i32* @cond_{cond_id}_then_root")
self.lines.append(f" %{new_var2} = load i32, i32* @cond_{cond_id}_else_root")
self.lines.append(f" %{marker_var3} = load i32, i32* @cond_{cond_id}_marker")
self.lines.append(f" call void @js_replace_at_marker(i32 %{marker_var3}, i32 %{old_var2}, i32 %{new_var2})")
self.lines.append(f" store i32 0, i32* @cond_{cond_id}_current")
self.lines.append(" br label %done")
self.lines.append("done:")
self.lines.append(" ret void")
self.lines.append("}")
self.lines.append("")
# Emit branch render functions
self._emit_branch_render_function(cond_id, "then", cond_info["then_body"])
self._emit_branch_render_function(cond_id, "else", cond_info["else_body"])
def _emit_match_reactive(self, cond_info: dict[str, Any]) -> None:
"""Emit reactive init and reevaluate functions for match/case."""
cond_id = cond_info["id"]
cases = cond_info["cases"]
num_cases = len(cases)
# Emit init function
self.lines.append(f"; Initialize conditional {cond_id} (match)")
self.lines.append(f"define void @_init_conditional_{cond_id}() {{")
self.lines.append("entry:")
# Render all case branches and store their roots
self.lines.append(" ; Render all cases upfront")
case_root_vars = []
for case_idx in range(num_cases):
root_var = self._new_var()
case_root_vars.append(root_var)
self.lines.append(f" %{root_var} = call i32 @_render_cond_{cond_id}_case_{case_idx}()")
self.lines.append(f" store i32 %{root_var}, i32* @cond_{cond_id}_case_{case_idx}_root")
# Evaluate subject and insert matching case at marker
subject_var = self._emit_expr(cond_info["subject"])
marker_var = self._new_var()
self.lines.append(f" %{marker_var} = load i32, i32* @cond_{cond_id}_marker")
# Build switch to insert the correct case
# Find default case (wildcard)
default_idx = None
for i, case in enumerate(cases):
if case["pattern"]["type"] == "wildcard":
default_idx = i
break
# Build switch instruction
insert_labels = [f"insert_case_{i}" for i in range(num_cases)]
if default_idx is not None:
self.lines.append(f" switch i32 %{subject_var}, label %{insert_labels[default_idx]} [")
else:
self.lines.append(f" switch i32 %{subject_var}, label %insert_case_0 [")
for i, case in enumerate(cases):
pattern = case["pattern"]
if pattern["type"] == "value":
value = pattern["value"]["value"]
self.lines.append(f" i32 {value}, label %{insert_labels[i]}")
self.lines.append(" ]")
# Emit insert blocks for each case
for case_idx in range(num_cases):
self.lines.append(f"{insert_labels[case_idx]}:")
self.lines.append(f" call void @js_replace_at_marker(i32 %{marker_var}, i32 0, i32 %{case_root_vars[case_idx]})")
self.lines.append(f" store i32 {case_idx}, i32* @cond_{cond_id}_current")
self.lines.append(" ret void")
self.lines.append("}")
self.lines.append("")
# Emit reevaluate function
self.lines.append(f"; Re-evaluate conditional {cond_id} (match)")
self.lines.append(f"define void @reevaluate_conditional_{cond_id}() {{")
self.lines.append("entry:")
# Re-evaluate subject
subject_var2 = self._emit_expr(cond_info["subject"])
# Load current state
current_var = self._new_var()
self.lines.append(f" %{current_var} = load i32, i32* @cond_{cond_id}_current")
# Build switch to determine which case should be active
case_labels = [f"case_{i}" for i in range(num_cases)]
if default_idx is not None:
self.lines.append(f" switch i32 %{subject_var2}, label %{case_labels[default_idx]} [")
else:
self.lines.append(f" switch i32 %{subject_var2}, label %{case_labels[0]} [")
for i, case in enumerate(cases):
pattern = case["pattern"]
if pattern["type"] == "value":
value = pattern["value"]["value"]
self.lines.append(f" i32 {value}, label %{case_labels[i]}")
self.lines.append(" ]")
# Emit check blocks for each case
for case_idx in range(num_cases):
self.lines.append(f"{case_labels[case_idx]}:")
# Check if this is already the current case
changed_var = self._new_var()
self.lines.append(f" %{changed_var} = icmp ne i32 %{current_var}, {case_idx}")
self.lines.append(f" br i1 %{changed_var}, label %swap_{case_idx}, label %done")
# 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
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")
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")
self.lines.append("done:")
self.lines.append(" ret void")
self.lines.append("}")
self.lines.append("")
# Emit branch render functions for each case
for case_idx, case in enumerate(cases):
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."""
self.lines.append(f"define i32 @_render_cond_{cond_id}_{branch_name}() {{")
self.lines.append("entry:")
# Render branch body
root_var = None
for stmt in body:
var = self._emit_statement(stmt)
if root_var is None and var:
root_var = var
# 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}")
self.lines.append("}")
self.lines.append("")
def _emit_component(self, ir: dict[str, Any]) -> None:
"""Emit component render function."""
component_name = ir["name"]
# Build parameter list
params = ir.get("params", [])
param_list = []
for param in params:
# For now, all params are i32
param_list.append(f"i32 %{param['name']}")
param_str = ", ".join(param_list) if param_list else ""
self.lines.append("; Component render function")
self.lines.append(f"define void @render_{component_name}({param_str}) {{")
self.lines.append("entry:")
# Process component body (skip signals and functions, already handled)
root_var = None
for stmt in ir["body"]:
if stmt["type"] in ["signal", "function"]:
continue # Already processed
var = self._emit_statement(stmt)
if root_var is None and var:
root_var = var
# Set root element
if root_var:
self.lines.append(f" call void @js_set_root(i32 %{root_var})")
self.lines.append(" ret void")
self.lines.append("}")
self.lines.append("")
def _emit_statement(self, stmt: dict[str, Any]) -> str | None:
"""Emit a statement, return variable name if it creates an element."""
if stmt["type"] == "with":
return self._emit_with(stmt)
elif stmt["type"] == "yield":
return self._emit_yield(stmt)
elif stmt["type"] == "yield_component":
return self._emit_yield_component(stmt)
elif stmt["type"] == "if":
return self._emit_if(stmt)
elif stmt["type"] == "match":
return self._emit_match(stmt)
return None
def _emit_if(self, stmt: dict[str, Any]) -> str | None:
"""Emit an if statement with reactive rendering."""
# Get conditional ID from our collected conditionals
cond_id = self.conditional_map.get(id(stmt))
if cond_id is None:
# Fallback to static rendering if not collected (shouldn't happen)
return self._emit_if_static(stmt)
# Create marker node
marker_var = self._new_var()
self.lines.append(f" %{marker_var} = call i32 @js_create_marker()")
# Append marker to parent if we have one
if self.parent_element_stack:
parent_var = self.parent_element_stack[-1]
self.lines.append(f" call void @js_append_child(i32 %{parent_var}, i32 %{marker_var})")
# Store marker ID in global
self.lines.append(f" store i32 %{marker_var}, i32* @cond_{cond_id}_marker")
# Call init function to render and insert active branch
self.lines.append(f" call void @_init_conditional_{cond_id}()")
return None
def _emit_if_static(self, stmt: dict[str, Any]) -> str | None:
"""Emit an if statement with static (non-reactive) rendering."""
# Generate unique labels for this if statement
if_id = self._new_var()
then_label = f"if.then.{if_id}"
else_label = f"if.else.{if_id}"
end_label = f"if.end.{if_id}"
# Evaluate condition
test_var = self._emit_expr(stmt["test"])
# Convert to i1 (boolean)
cond_var = self._new_var()
self.lines.append(f" %{cond_var} = icmp ne i32 %{test_var}, 0")
# Branch based on whether there's an else
has_else = stmt.get("orelse") and len(stmt["orelse"]) > 0
if has_else:
self.lines.append(f" br i1 %{cond_var}, label %{then_label}, label %{else_label}")
else:
self.lines.append(f" br i1 %{cond_var}, label %{then_label}, label %{end_label}")
# Emit then block
self.lines.append(f"{then_label}:")
for then_stmt in stmt["body"]:
self._emit_statement(then_stmt)
self.lines.append(f" br label %{end_label}")
# Emit else block if present
if has_else:
self.lines.append(f"{else_label}:")
for else_stmt in stmt["orelse"]:
self._emit_statement(else_stmt)
self.lines.append(f" br label %{end_label}")
# Emit merge block
self.lines.append(f"{end_label}:")
return None
def _emit_yield_component(self, stmt: dict[str, Any]) -> str | None:
"""Emit a component instantiation (yield SomeComponent(...))."""
component_name = stmt["component"]
# For now, just call the render function
# TODO: Pass arguments when component parameters are supported
self.lines.append(f" call void @render_{component_name}()")
return None
def _emit_match(self, stmt: dict[str, Any]) -> str | None:
"""Emit a match/case statement with reactive rendering."""
# Get conditional ID from our collected conditionals
cond_id = self.conditional_map.get(id(stmt))
if cond_id is None:
# Fallback to static rendering if not collected (shouldn't happen)
return self._emit_match_static(stmt)
# Create marker node
marker_var = self._new_var()
self.lines.append(f" %{marker_var} = call i32 @js_create_marker()")
# Append marker to parent if we have one
if self.parent_element_stack:
parent_var = self.parent_element_stack[-1]
self.lines.append(f" call void @js_append_child(i32 %{parent_var}, i32 %{marker_var})")
# Store marker ID in global
self.lines.append(f" store i32 %{marker_var}, i32* @cond_{cond_id}_marker")
# Call init function to render and insert active branch
self.lines.append(f" call void @_init_conditional_{cond_id}()")
return None
def _emit_match_static(self, stmt: dict[str, Any]) -> str | None:
"""Emit a match/case statement with static (non-reactive) rendering."""
# Generate unique labels
match_id = self._new_var()
# Evaluate subject expression
subject_var = self._emit_expr(stmt["subject"])
# Create labels for each case and the end
case_labels = [f"match.case{i}.{match_id}" for i in range(len(stmt["cases"]))]
end_label = f"match.end.{match_id}"
default_label = f"match.default.{match_id}"
# Build switch instruction
# Find wildcard case (case _:) to use as default
default_case_idx = None
switch_cases = []
for i, case in enumerate(stmt["cases"]):
pattern = case["pattern"]
if pattern["type"] == "wildcard":
default_case_idx = i
elif pattern["type"] == "value":
# Extract the constant value
value = pattern["value"]["value"]
switch_cases.append(f"i32 {value}, label %{case_labels[i]}")
# Emit switch
if default_case_idx is not None:
self.lines.append(f" switch i32 %{subject_var}, label %{case_labels[default_case_idx]} [")
else:
self.lines.append(f" switch i32 %{subject_var}, label %{default_label} [")
for case_spec in switch_cases:
self.lines.append(f" {case_spec}")
self.lines.append(" ]")
# Emit each case body
for i, case in enumerate(stmt["cases"]):
self.lines.append(f"{case_labels[i]}:")
for case_stmt in case["body"]:
self._emit_statement(case_stmt)
self.lines.append(f" br label %{end_label}")
# Emit default case if needed (no wildcard)
if default_case_idx is None:
self.lines.append(f"{default_label}:")
self.lines.append(f" br label %{end_label}")
# Emit end label
self.lines.append(f"{end_label}:")
return None
def _emit_expr(self, expr: dict[str, Any]) -> str:
"""Emit an expression and return the result variable name."""
if expr["type"] == "name":
# Load signal value
signal_name = expr["id"]
if signal_name in self.signals:
load_var = self._new_var()
self.lines.append(f" %{load_var} = load i32, i32* {self._signal_global(signal_name)}")
return load_var
else:
# Unknown variable
return "0"
elif expr["type"] == "constant":
# Return constant inline
return str(expr["value"])
elif expr["type"] == "compare":
# Handle comparison (e.g., count > 5)
left_var = self._emit_expr(expr["left"])
right_var = self._emit_expr(expr["comparators"][0])
# Map Python comparison ops to LLVM icmp predicates
op_map = {
"Gt": "sgt", # signed greater than
"Lt": "slt", # signed less than
"GtE": "sge", # signed greater or equal
"LtE": "sle", # signed less or equal
"Eq": "eq", # equal
"NotEq": "ne", # not equal
}
icmp_op = op_map.get(expr["op"], "eq")
result_var = self._new_var()
self.lines.append(f" %{result_var} = icmp {icmp_op} i32 %{left_var}, {right_var}")
# Convert i1 to i32 for compatibility
result_i32 = self._new_var()
self.lines.append(f" %{result_i32} = zext i1 %{result_var} to i32")
return result_i32
else:
# Unknown expression type
return "0"
def _emit_with(self, stmt: dict[str, Any]) -> str:
"""Emit a 'with Element()' block."""
element_info = stmt["element"]
tag = element_info["tag"].lower()
props = element_info["props"]
# Create element
elem_var = self._new_var()
tag_str = self._get_or_create_string(tag)
self.lines.append(
f" %{elem_var} = call i32 @js_create_element(i8* getelementptr inbounds ({self._string_type(tag)}, {self._string_type(tag)}* {tag_str}, i32 0, i32 0))"
)
# Handle onclick prop specially
onclick_handler = None
for prop_name, prop_value in props.items():
if prop_name == "onclick":
# Bind event handler
if prop_value["type"] == "name":
onclick_handler = prop_value["id"]
else:
# Regular attribute
attr_name = prop_name.rstrip("_")
if prop_value["type"] == "constant":
value = str(prop_value["value"])
name_str = self._get_or_create_string(attr_name)
value_str = self._get_or_create_string(value)
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* getelementptr inbounds ({self._string_type(value)}, {self._string_type(value)}* {value_str}, i32 0, i32 0))"
)
# Bind onclick if present
if onclick_handler:
event_str = self._get_or_create_string("click")
handler_str = self._get_or_create_string(onclick_handler)
self.lines.append(
f" call void @js_bind_event(i32 %{elem_var}, "
f"i8* getelementptr inbounds ({self._string_type('click')}, {self._string_type('click')}* {event_str}, i32 0, i32 0), "
f"i8* getelementptr inbounds ({self._string_type(onclick_handler)}, {self._string_type(onclick_handler)}* {handler_str}, i32 0, i32 0))"
)
# Push this element as the parent for children
self.parent_element_stack.append(elem_var)
# Process children
for child_stmt in stmt["body"]:
child_var = self._emit_statement(child_stmt)
if child_var:
self.lines.append(
f" call void @js_append_child(i32 %{elem_var}, i32 %{child_var})"
)
# Pop parent
self.parent_element_stack.pop()
# If we're inside an if/match branch (have a grandparent), append to it
if self.parent_element_stack:
parent_var = self.parent_element_stack[-1]
self.lines.append(f" call void @js_append_child(i32 %{parent_var}, i32 %{elem_var})")
return elem_var
def _emit_yield(self, stmt: dict[str, Any]) -> str | None:
"""Emit a yield statement."""
value = stmt.get("value")
if not value:
return None
if value["type"] == "constant":
# Simple text node
text = str(value["value"])
text_str = self._get_or_create_string(text)
text_var = self._new_var()
self.lines.append(
f" %{text_var} = call i32 @js_create_text(i8* getelementptr inbounds ({self._string_type(text)}, {self._string_type(text)}* {text_str}, i32 0, i32 0))"
)
return text_var
elif value["type"] == "fstring":
# Handle f-strings with signal references
# For now, create text with signal value
# TODO: Implement proper string concatenation
parts = value["parts"]
# Find signal references
for part in parts:
if part["type"] == "formatted_value":
if part["value"]["type"] == "name":
signal_name = part["value"]["id"]
if signal_name in self.signals:
# Load signal value and create text node
# This is simplified - real impl would concat strings
text_var = self._new_var()
self.lines.append(
f" ; TODO: Format f-string with signal {signal_name}"
)
# For now, just create placeholder text
placeholder = self._get_or_create_string("0")
self.lines.append(
f" %{text_var} = call i32 @js_create_text(i8* getelementptr inbounds ([2 x i8], [2 x i8]* {placeholder}, i32 0, i32 0))"
)
return text_var
# Fallback: create empty text
empty_str = self._get_or_create_string("")
text_var = self._new_var()
self.lines.append(
f" %{text_var} = call i32 @js_create_text(i8* getelementptr inbounds ({self._string_type('')}, {self._string_type('')}* {empty_str}, i32 0, i32 0))"
)
return text_var
return None
def _get_or_create_string(self, value: str) -> str:
"""Get or create a string constant."""
if value in self.strings:
return self.strings[value]
global_name = f"@.str.{self.next_string_id}"
self.next_string_id += 1
self.strings[value] = global_name
return global_name
def _string_type(self, value: str) -> str:
"""Get LLVM type for a string (account for UTF-8 byte length)."""
# Get actual byte length of UTF-8 encoded string
byte_length = len(value.encode("utf-8")) + 1 # +1 for null terminator
return f"[{byte_length} x i8]"
def _new_var(self) -> str:
"""Generate a new SSA variable name."""
var_name = f"var{self.next_var_id}"
self.next_var_id += 1
return var_name
def _finalize_with_strings(self) -> str:
"""Prepend string constants and return final IR."""
string_lines = ["; String constants"]
for value, global_name in self.strings.items():
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
str_type = self._string_type(value)
string_lines.append(
f'{global_name} = private unnamed_addr constant {str_type} c"{escaped}\\00"'
)
string_lines.append("")
# Find insertion point (after header, before declarations)
result_lines = []
header_end = 0
for i, line in enumerate(self.lines):
if "JavaScript bridge" in line or "Signal storage" in line:
header_end = i
break
result_lines.extend(self.lines[:header_end])
result_lines.extend(string_lines)
result_lines.extend(self.lines[header_end:])
return "\n".join(result_lines)