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

+871 -633
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+126
View File
@@ -0,0 +1,126 @@
"""
Clean Tabs implementation using ternary expressions to eliminate repetition.
Before: 6 lines per tab button (if/else blocks)
After: 1 line per tab button (ternary expressions)
"""
from ritz.syntax import *
@component
def VerticalTabs():
"""Vertical tabs for settings categories."""
setting_tab = signal(0)
def set_setting(value):
setting_tab = value
with Div(class_="vertical-tabs"):
# Vertical tab menu - now just 3 lines instead of 18!
with Div(class_="vertical-tab-menu"):
with Button(class_='vertical-tab active' if setting_tab == 0 else 'vertical-tab', onclick=lambda: set_setting(0)):
yield "⚙️ General"
with Button(class_='vertical-tab active' if setting_tab == 1 else 'vertical-tab', onclick=lambda: set_setting(1)):
yield "🔒 Privacy"
with Button(class_='vertical-tab active' if setting_tab == 2 else 'vertical-tab', onclick=lambda: set_setting(2)):
yield "🚀 Advanced"
# Tab content (same as before)
with Div(class_="vertical-tab-content"):
match setting_tab:
case 0:
with Div(class_="setting-pane"):
with H3():
yield "⚙️ General Settings"
with P():
yield "Language: English"
with P():
yield "Time Zone: UTC"
case 1:
with Div(class_="setting-pane"):
with H3():
yield "🔒 Privacy Settings"
with P():
yield "Data Collection: Disabled"
with P():
yield "Analytics: Opt-out"
case 2:
with Div(class_="setting-pane"):
with H3():
yield "🚀 Advanced Settings"
with P():
yield "Debug Mode: Off"
with P():
yield "Experimental Features: Disabled"
case _:
with Div(class_="setting-pane"):
with P():
yield "Invalid setting"
@component
def Tabs():
"""Main tabs container with ternary expressions for clean tab buttons."""
tab_index = signal(0)
def set_tab(value):
tab_index = value
with Div(class_="tabs-container"):
with H1():
yield "Tab Component Demo"
with P(class_="subtitle"):
yield "Beautiful composition with ternary expressions"
# Tab menu - clean one-line buttons!
with Ul(class_="tab-menu"):
with Li(class_='tab active' if tab_index == 0 else 'tab'):
with Button(onclick=lambda: set_tab(0)):
yield "🏠 Home"
with Li(class_='tab active' if tab_index == 1 else 'tab'):
with Button(onclick=lambda: set_tab(1)):
yield "👤 Profile"
with Li(class_='tab active' if tab_index == 2 else 'tab'):
with Button(onclick=lambda: set_tab(2)):
yield "⚙️ Settings"
# Tab panes
with Div(class_="tab-content"):
match tab_index:
case 0:
with Div(class_="tab-pane home"):
with H2():
yield "🏠 Welcome Home"
with P():
yield "This is your home dashboard."
case 1:
with Div(class_="tab-pane profile"):
with H2():
yield "👤 Your Profile"
with P():
yield "Manage your account settings."
case 2:
with Div(class_="tab-pane settings"):
with H2():
yield "⚙️ Settings"
with P():
yield "Configure your preferences using the categories below:"
# Nested vertical tabs!
yield VerticalTabs()
case _:
with Div(class_="tab-pane error"):
with P():
yield "⚠️ Invalid tab selected"
+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__}
+78
View File
@@ -0,0 +1,78 @@
"""Tests for ternary expression support in props."""
import pytest
from ritz.parser import parse_component
from ritz.llvm_codegen_signals import generate_llvm_ir
def test_parse_ternary_in_prop():
"""Test parsing ternary expression in element prop."""
source = """
from ritz.syntax import *
@component
def Toggle():
active = signal(1)
with Button(class_='btn active' if active else 'btn'):
yield "Click"
"""
ir = parse_component(source)
# Find the with statement
with_stmt = [s for s in ir["body"] if s["type"] == "with"][0]
# Check that class_ prop contains a ternary (ifexp)
class_prop = with_stmt["element"]["props"]["class_"]
assert class_prop["type"] == "ifexp"
assert class_prop["test"]["id"] == "active"
assert class_prop["body"]["value"] == "btn active"
assert class_prop["orelse"]["value"] == "btn"
def test_codegen_ternary_in_prop():
"""Test LLVM codegen for ternary expression in prop."""
source = """
from ritz.syntax import *
@component
def Toggle():
active = signal(1)
with Button(class_='btn active' if active else 'btn'):
yield "Click"
"""
ir = parse_component(source)
llvm_ir = generate_llvm_ir(ir)
# Should generate code to evaluate the condition
assert "icmp" in llvm_ir # Comparison for active signal
# Should select between the two string values
# This will likely use select instruction or branching
assert "btn active" in llvm_ir
assert "btn" in llvm_ir
def test_ternary_with_comparison():
"""Test ternary with comparison expression."""
source = """
from ritz.syntax import *
@component
def Counter():
count = signal(0)
with Div(class_='high' if count > 5 else 'low'):
yield "Value"
"""
ir = parse_component(source)
# Find the with statement
with_stmt = [s for s in ir["body"] if s["type"] == "with"][0]
# Check ternary structure
class_prop = with_stmt["element"]["props"]["class_"]
assert class_prop["type"] == "ifexp"
assert class_prop["test"]["type"] == "compare"
assert class_prop["test"]["op"] == "Gt"
+82
View File
@@ -0,0 +1,82 @@
"""Tests for wrapper component pattern to reduce repetition."""
import pytest
from ritz.parser import parse_component
from ritz.llvm_codegen_signals import generate_llvm_ir
def test_tab_button_wrapper_component():
"""Test wrapper component pattern for reducing tab button repetition."""
source = """
from ritz.syntax import *
@component
def TabButton(label, index, active, onclick):
\"\"\"Wrapper component for tab buttons with active state.\"\"\"
if active:
with Button(class_="tab active", onclick=onclick):
yield label
else:
with Button(class_="tab", onclick=onclick):
yield label
@component
def SimpleTabs():
tab_index = signal(0)
def set_tab(value):
tab_index = value
with Div(class_="tab-menu"):
yield TabButton("Home", 0, tab_index == 0, lambda: set_tab(0))
yield TabButton("Profile", 1, tab_index == 1, lambda: set_tab(1))
yield TabButton("Settings", 2, tab_index == 2, lambda: set_tab(2))
"""
ir = parse_component(source)
# Should parse both components
assert isinstance(ir, list)
assert len(ir) == 2
assert ir[0]["name"] == "TabButton"
assert ir[1]["name"] == "SimpleTabs"
# TabButton should have parameters
assert len(ir[0]["params"]) == 4
assert ir[0]["params"][0]["name"] == "label"
assert ir[0]["params"][1]["name"] == "index"
assert ir[0]["params"][2]["name"] == "active"
assert ir[0]["params"][3]["name"] == "onclick"
def test_tab_button_wrapper_codegen():
"""Test LLVM codegen for wrapper component pattern."""
source = """
from ritz.syntax import *
@component
def TabButton(label, active, onclick):
if active:
with Button(class_="tab active", onclick=onclick):
yield label
else:
with Button(class_="tab", onclick=onclick):
yield label
@component
def SimpleTabs():
tab_index = signal(0)
def set_tab(value):
tab_index = value
with Div():
yield TabButton("Home", tab_index == 0, lambda: set_tab(0))
"""
ir = parse_component(source)
llvm_ir = generate_llvm_ir(ir)
# Should generate render function for TabButton with parameters
assert "define void @render_TabButton(" in llvm_ir
# Should call TabButton from SimpleTabs with arguments
assert "call void @render_TabButton(" in llvm_ir