- 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>
936 lines
39 KiB
Python
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)
|