Parser: Support multiple components in one file, change args to params with defaults
This commit is contained in:
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.
Binary file not shown.
+7
-1
@@ -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
@@ -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
@@ -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__}
|
||||
|
||||
|
||||
Reference in new issue
Block a user