Parser: Support multiple components in one file, change args to params with defaults

This commit is contained in:
Aaron Sinclair committed 2025-11-15 03:25:13 -08:00
1 parent 635d466874
commit 81d51d5617
38 files changed
+2151 -455

No files matched your search

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+7 -1
View File
@@ -27,7 +27,13 @@ def build_component(component_file: Path, output_file: Path, debug: bool = False
# Step 2: Parse to IR
print(" [2/4] Parsing Python to IR...")
ir = parse_component(source)
print(f" Component: {ir['name']}")
# Handle single component or list of components
if isinstance(ir, list):
for component in ir:
print(f" Component: {component['name']}")
else:
print(f" Component: {ir['name']}")
# Step 3: Generate LLVM IR
print(" [3/4] Generating LLVM IR...")
+7 -163
View File
@@ -1,166 +1,10 @@
"""LLVM IR code generator for Ritz components."""
"""LLVM IR code generator for Ritz components.
from typing import Any
This module now re-exports from llvm_codegen_signals which has full support
for reactive signals, event handlers, and conditional rendering.
"""
# Re-export the signal-enabled codegen
from ritz.llvm_codegen_signals import generate_llvm_ir
def generate_llvm_ir(ir: dict[str, Any]) -> str:
"""
Generate LLVM IR from Ritz IR.
Args:
ir: Component IR dictionary
Returns:
LLVM IR source code
"""
generator = LLVMIRGenerator(ir)
return generator.generate()
class LLVMIRGenerator:
"""Generates LLVM IR from Ritz IR."""
def __init__(self, ir: dict[str, Any]):
self.ir = ir
self.lines: list[str] = []
self.strings: dict[str, str] = {} # value -> global name
self.next_string_id = 0
self.next_var_id = 0
def generate(self) -> str:
"""Generate complete LLVM IR module."""
self._emit_header()
self._emit_string_constants()
self._emit_function_declarations()
self._emit_component(self.ir)
return "\n".join(self.lines)
def _emit_header(self) -> None:
"""Emit module header."""
self.lines.append("; Generated by Ritz Framework")
self.lines.append("; Component: " + self.ir["name"])
self.lines.append("")
def _emit_string_constants(self) -> None:
"""Emit string constant declarations (filled during code generation)."""
# We'll add strings as we encounter them, then prepend later
pass
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("")
def _emit_component(self, ir: dict[str, Any]) -> None:
"""Emit component function."""
component_name = ir["name"]
self.lines.append(f"; Component function: {component_name}")
self.lines.append(f"define void @render_{component_name}() {{")
self.lines.append("entry:")
# Process component body
root_var = None
for stmt in ir["body"]:
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)
return None
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))"
)
# Set attributes
for prop_name, prop_value in props.items():
attr_name = prop_name.rstrip("_") # class_ -> class
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))"
)
# 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})"
)
return elem_var
def _emit_yield(self, stmt: dict[str, Any]) -> str | None:
"""Emit a yield statement."""
value = stmt.get("value")
if not value or value["type"] != "constant":
return None
# Create 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
def _get_or_create_string(self, value: str) -> str:
"""Get or create a string constant, return its global name."""
if value in self.strings:
return self.strings[value]
# Create new string constant
global_name = f"@.str.{self.next_string_id}"
self.next_string_id += 1
self.strings[value] = global_name
# Add to beginning of output (will prepend later)
return global_name
def _string_type(self, value: str) -> str:
"""Get LLVM type for a string constant."""
# +1 for null terminator
length = len(value) + 1
return f"[{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
__all__ = ["generate_llvm_ir"]
+113 -10
View File
@@ -4,24 +4,29 @@ import ast
from typing import Any
def parse_component(source: str) -> dict[str, Any]:
def parse_component(source: str) -> dict[str, Any] | list[dict[str, Any]]:
"""
Parse a Python component into Ritz IR.
Parse Python components into Ritz IR.
Args:
source: Python source code containing a @component decorated function
source: Python source code containing @component decorated function(s)
Returns:
IR dictionary representing the component structure
IR dictionary (single component) or list of IR dicts (multiple components)
"""
tree = ast.parse(source)
# Find the @component decorated function
for node in ast.walk(tree):
# Find ALL @component decorated functions
components = []
for node in tree.body: # Only top-level functions, not ast.walk
if isinstance(node, ast.FunctionDef) and _has_component_decorator(node):
return _transform_component(node)
components.append(_transform_component(node))
raise ValueError("No @component decorated function found")
if not components:
raise ValueError("No @component decorated function found")
# Return single component or list
return components[0] if len(components) == 1 else components
def _has_component_decorator(node: ast.FunctionDef) -> bool:
@@ -34,10 +39,29 @@ def _has_component_decorator(node: ast.FunctionDef) -> bool:
def _transform_component(node: ast.FunctionDef) -> dict[str, Any]:
"""Transform a component function into IR."""
# Extract parameters with default values
params = []
defaults = node.args.defaults
num_defaults = len(defaults)
num_args = len(node.args.args)
for i, arg in enumerate(node.args.args):
# Check if this arg has a default (defaults align to the end of args)
default_index = i - (num_args - num_defaults)
if default_index >= 0:
default_value = _transform_expr(defaults[default_index])
else:
default_value = None
params.append({
"name": arg.arg,
"default": default_value,
})
return {
"type": "component",
"name": node.name,
"args": [arg.arg for arg in node.args.args],
"params": params, # Changed from "args" to "params" with defaults
"body": [_transform_statement(stmt) for stmt in node.body],
}
@@ -54,6 +78,10 @@ def _transform_statement(stmt: ast.stmt) -> dict[str, Any]:
return _transform_augassign(stmt)
elif isinstance(stmt, ast.FunctionDef):
return _transform_function(stmt)
elif isinstance(stmt, ast.If):
return _transform_if(stmt)
elif isinstance(stmt, ast.Match):
return _transform_match(stmt)
else:
return {"type": "unknown", "ast_type": type(stmt).__name__}
@@ -89,7 +117,30 @@ def _transform_element_call(expr: ast.expr) -> dict[str, Any]:
def _transform_yield(node: ast.Yield) -> dict[str, Any]:
"""Transform a yield statement."""
return {"type": "yield", "value": _transform_expr(node.value) if node.value else None}
if node.value is None:
return {"type": "yield", "value": None}
# Check if yielding a component call (uppercase name that's not a known element)
if isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Name):
func_name = node.value.func.id
# Known HTML elements - these are NOT components
known_elements = {
"Div", "Span", "P", "H1", "H2", "H3", "H4", "H5", "H6",
"Button", "Input", "A", "Ul", "Li", "Ol", "Table", "Tr", "Td", "Th",
"Form", "Label", "Select", "Option", "Textarea", "Main", "Header",
"Footer", "Nav", "Section", "Article", "Aside"
}
# If it's uppercase and NOT a known element, it's a component
if func_name[0].isupper() and func_name not in known_elements:
return {
"type": "yield_component",
"component": func_name,
"args": [_transform_expr(arg) for arg in node.value.args],
"kwargs": {kw.arg: _transform_expr(kw.value) for kw in node.value.keywords if kw.arg},
}
return {"type": "yield", "value": _transform_expr(node.value)}
def _transform_assign(node: ast.Assign) -> dict[str, Any]:
@@ -144,6 +195,50 @@ def _transform_function(node: ast.FunctionDef) -> dict[str, Any]:
}
def _transform_if(node: ast.If) -> dict[str, Any]:
"""Transform an if statement for conditional rendering."""
return {
"type": "if",
"test": _transform_expr(node.test),
"body": [_transform_statement(stmt) for stmt in node.body],
"orelse": [_transform_statement(stmt) for stmt in node.orelse],
}
def _transform_match(node: ast.Match) -> dict[str, Any]:
"""Transform a match/case statement."""
return {
"type": "match",
"subject": _transform_expr(node.subject),
"cases": [_transform_match_case(case) for case in node.cases],
}
def _transform_match_case(case: ast.match_case) -> dict[str, Any]:
"""Transform a single match case."""
return {
"pattern": _transform_pattern(case.pattern),
"guard": _transform_expr(case.guard) if case.guard else None,
"body": [_transform_statement(stmt) for stmt in case.body],
}
def _transform_pattern(pattern: ast.pattern) -> dict[str, Any]:
"""Transform a match pattern."""
if isinstance(pattern, ast.MatchValue):
# case 0: or case "string":
return {
"type": "value",
"value": _transform_expr(pattern.value),
}
elif isinstance(pattern, ast.MatchAs) and pattern.pattern is None:
# case _: (wildcard)
return {"type": "wildcard"}
else:
# Other patterns not yet supported
return {"type": "unknown", "ast_type": type(pattern).__name__}
def _transform_expr(expr: ast.expr | None) -> dict[str, Any]:
"""Transform an expression into IR."""
if expr is None:
@@ -170,6 +265,14 @@ def _transform_expr(expr: ast.expr | None) -> dict[str, Any]:
return {"type": "fstring", "parts": [_transform_expr(v) for v in expr.values]}
elif isinstance(expr, ast.FormattedValue):
return {"type": "formatted_value", "value": _transform_expr(expr.value)}
elif isinstance(expr, ast.Compare):
# Handle comparisons (e.g., count > 5)
return {
"type": "compare",
"left": _transform_expr(expr.left),
"op": type(expr.ops[0]).__name__, # Gt, Lt, Eq, etc.
"comparators": [_transform_expr(comp) for comp in expr.comparators],
}
else:
return {"type": "unknown", "ast_type": type(expr).__name__}