Fix lambda event handlers & add ternary expression support

Fixes:
- Lambda functions now correctly pass arguments to handlers
- Function parameters properly emitted in LLVM IR
- Event handlers defined with parameter lists

New feature - Ternary expressions:
- Support Python ternary syntax (x if cond else y) in props
- Eliminates repetitive if/else blocks for active states
- Before: 6 lines per tab button
- After: 1 line with ternary expression

Example:
  class_='tab active' if active else 'tab'

All tests passing (51 passed, 2 skipped)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Aaron SinclairandClaude committed 2025-11-15 11:07:58 -08:00
1 parent c947bcf6e0
commit adf95f424d
7 files changed
+1282 -665

No files matched your search

+117 -32
View File
@@ -45,6 +45,9 @@ class SignalLLVMIRGenerator:
# Track parent element for appending children in if/match branches
self.parent_element_stack: list[str] = []
# Track current function parameters for parameter reference handling
self.current_function_params: set[str] = set()
# Track if we're inside a conditional branch
self.in_conditional_branch = False
@@ -266,8 +269,21 @@ class SignalLLVMIRGenerator:
"""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}() {{")
# Build parameter list (all params are i32 for now)
args = handler.get("args", [])
# Track current function parameters for _emit_expr
self.current_function_params = set(args)
if args:
param_list = ", ".join([f"i32 %{arg}" for arg in args])
self.lines.append(f"; Event handler: {func_name}")
self.lines.append(f"define void @{func_name}({param_list}) {{")
else:
self.lines.append(f"; Event handler: {func_name}")
self.lines.append(f"define void @{func_name}() {{")
self.lines.append("entry:")
# Emit handler body
@@ -278,18 +294,32 @@ class SignalLLVMIRGenerator:
self.lines.append("}")
self.lines.append("")
# Clear function params
self.current_function_params = set()
def _emit_handler_statement(self, stmt: dict[str, Any]) -> None:
"""Emit a statement inside an event handler."""
if stmt["type"] == "expr":
# Expression statement (e.g., from lambda body calling a function)
expr = stmt["value"]
if expr["type"] == "call":
# Function call - emit it
# Function call - emit it with arguments
func_name = expr["func"]
# Check if it's an event handler function
if any(h["name"] == func_name for h in self.event_handlers):
# Call the handler function (functions are emitted without component suffix)
self.lines.append(f" call void @{func_name}()")
# Emit arguments if any
args = expr.get("args", [])
arg_values = []
for arg in args:
arg_var = self._emit_expr(arg)
arg_values.append(f"i32 {arg_var}")
# Call the handler function with arguments
if arg_values:
arg_list = ", ".join(arg_values)
self.lines.append(f" call void @{func_name}({arg_list})")
else:
self.lines.append(f" call void @{func_name}()")
else:
# Unknown function - skip or error
pass
@@ -360,7 +390,7 @@ class SignalLLVMIRGenerator:
# 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")
self.lines.append(f" %{cond_var} = icmp ne i32 {test_var}, 0")
# Load marker
marker_var = self._new_var()
@@ -390,7 +420,7 @@ class SignalLLVMIRGenerator:
# 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")
self.lines.append(f" %{cond_var2} = icmp ne i32 {test_var2}, 0")
# Convert to i32
cond_i32 = self._new_var()
@@ -478,9 +508,9 @@ class SignalLLVMIRGenerator:
# 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]} [")
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 [")
self.lines.append(f" switch i32 {subject_var}, label %insert_case_0 [")
for i, case in enumerate(cases):
pattern = case["pattern"]
@@ -514,9 +544,9 @@ class SignalLLVMIRGenerator:
# 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]} [")
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]} [")
self.lines.append(f" switch i32 {subject_var2}, label %{case_labels[0]} [")
for i, case in enumerate(cases):
pattern = case["pattern"]
@@ -697,7 +727,7 @@ class SignalLLVMIRGenerator:
# Convert to i1 (boolean)
cond_var = self._new_var()
self.lines.append(f" %{cond_var} = icmp ne i32 %{test_var}, 0")
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
@@ -733,12 +763,9 @@ class SignalLLVMIRGenerator:
arg_values = []
for arg in args:
# For constants, emit directly; for expressions, emit as variable
if arg["type"] == "constant":
arg_values.append(f"i32 {arg['value']}")
else:
arg_var = self._emit_expr(arg)
arg_values.append(f"i32 %{arg_var}")
# Emit argument expression
arg_var = self._emit_expr(arg)
arg_values.append(f"i32 {arg_var}")
# Build argument list and call
if arg_values:
@@ -803,9 +830,9 @@ class SignalLLVMIRGenerator:
# Emit switch
if default_case_idx is not None:
self.lines.append(f" switch i32 %{subject_var}, label %{case_labels[default_case_idx]} [")
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} [")
self.lines.append(f" switch i32 {subject_var}, label %{default_label} [")
for case_spec in switch_cases:
self.lines.append(f" {case_spec}")
@@ -828,20 +855,52 @@ class SignalLLVMIRGenerator:
return None
def _emit_string_ifexp(self, expr: dict[str, Any]) -> str:
"""Emit a ternary expression that returns a string pointer (i8*)."""
# Evaluate the condition
test_val = self._emit_expr(expr["test"])
# Convert to i1 if it's i32
cond_var = self._new_var()
self.lines.append(f" %{cond_var} = icmp ne i32 {test_val}, 0")
# Get string constants for both branches
true_str = str(expr["body"]["value"])
false_str = str(expr["orelse"]["value"])
true_str_global = self._get_or_create_string(true_str)
false_str_global = self._get_or_create_string(false_str)
# Use select with inline getelementptr
result_var = self._new_var()
self.lines.append(
f" %{result_var} = select i1 %{cond_var}, "
f"i8* getelementptr inbounds ({self._string_type(true_str)}, {self._string_type(true_str)}* {true_str_global}, i32 0, i32 0), "
f"i8* getelementptr inbounds ({self._string_type(false_str)}, {self._string_type(false_str)}* {false_str_global}, i32 0, i32 0)"
)
return result_var
def _emit_expr(self, expr: dict[str, Any]) -> str:
"""Emit an expression and return the result variable name."""
"""Emit an expression and return the complete LLVM IR value (with % prefix for vars)."""
if expr["type"] == "name":
# Load signal value
signal_name = expr["id"]
if signal_name in self.signals:
var_name = expr["id"]
# Check if it's a function parameter
if var_name in self.current_function_params:
# Return parameter with % prefix
return f"%{var_name}"
# Check if it's a signal
if var_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"
self.lines.append(f" %{load_var} = load i32, i32* {self._signal_global(var_name)}")
return f"%{load_var}"
# Unknown variable - return constant 0
return "0"
elif expr["type"] == "constant":
# Return constant inline
# Return constant inline (no % prefix)
return str(expr["value"])
elif expr["type"] == "compare":
# Handle comparison (e.g., count > 5)
@@ -860,12 +919,29 @@ class SignalLLVMIRGenerator:
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}")
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
return f"%{result_i32}"
elif expr["type"] == "ifexp":
# Handle ternary expressions (e.g., 'active' if flag else 'inactive')
# Evaluate the condition
test_val = self._emit_expr(expr["test"])
# Convert to i1 if it's i32
cond_var = self._new_var()
self.lines.append(f" %{cond_var} = icmp ne i32 {test_val}, 0")
# Evaluate both branches
true_val = self._emit_expr(expr["body"])
false_val = self._emit_expr(expr["orelse"])
# Use select instruction to choose between them
result_var = self._new_var()
self.lines.append(f" %{result_var} = select i1 %{cond_var}, i32 {true_val}, i32 {false_val}")
return f"%{result_var}"
else:
# Unknown expression type
return "0"
@@ -902,6 +978,15 @@ class SignalLLVMIRGenerator:
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))"
)
elif prop_value["type"] == "ifexp":
# Handle ternary expression for attribute value
value_ptr = self._emit_string_ifexp(prop_value)
name_str = self._get_or_create_string(attr_name)
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* %{value_ptr})"
)
# Bind onclick if present
if onclick_handler:
+8
View File
@@ -348,6 +348,14 @@ def _transform_expr(expr: ast.expr | None) -> dict[str, Any]:
"op": type(expr.ops[0]).__name__, # Gt, Lt, Eq, etc.
"comparators": [_transform_expr(comp) for comp in expr.comparators],
}
elif isinstance(expr, ast.IfExp):
# Handle ternary expressions (e.g., 'active' if flag else 'inactive')
return {
"type": "ifexp",
"test": _transform_expr(expr.test),
"body": _transform_expr(expr.body),
"orelse": _transform_expr(expr.orelse),
}
else:
return {"type": "unknown", "ast_type": type(expr).__name__}