"""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)