Implement Python → LLVM IR → WASM reactive framework

MAJOR ACHIEVEMENT: Full working reactive framework!

## What's Working

✅ Python → AST → Ritz IR → LLVM IR → WASM pipeline
✅ Reactive signals stored in WASM memory (@signal_name = global i32)
✅ Event handlers exported from WASM and callable from JS
✅ Clean runtime with RitzLoader and debug tools
✅ Two working examples: HelloWorld and HighFive counter

## Core Components

### Parser (ritz/parser.py)
- Uses Python's ast module to parse components
- Handles @component decorator, signals, event handlers
- Supports yield, with statements, f-strings, augmented assignment (+=)
- Clean syntax: from ritz.syntax import *

### LLVM IR Code Generator (ritz/llvm_codegen_signals.py)
- Generates LLVM IR directly (not C!)
- Signals as global variables in WASM memory
- Exports getter/setter functions for each signal
- Exports event handler functions for JS to call
- UTF-8 string handling

### Build System (ritz/build.py)
- Python → AST → IR → LLVM IR → clang --target=wasm32 → WASM
- Saves intermediate .ll files for inspection
- Debug symbols support (-g flag)

### Runtime (runtime/ritz-runtime.js)
- RitzRuntime class: DOM bridge for WASM
- RitzLoader class: Simplified component loading
- Debug bar with Ctrl+` toggle
- Signal updates trigger DOM updates

## Examples

### HelloWorld.py
Simple static content - renders <p>Hello World</p>

### HighFive.py
Reactive counter with:
- Signal in WASM memory (high_fives = signal(0))
- Event handler (increment function)
- DOM updates on signal change

## Test Coverage

- 4 parser tests (all passing)
- 4 LLVM codegen tests (all passing)
- 4 signal parsing tests (all passing)
- Code formatted with black and ruff

## Next Steps

- <script type="text/ritz"> in-browser compilation
- Better signal-to-DOM binding tracking
- More complex examples (TodoMVC, forms, lists)
- Playwright browser tests

---

This is a genuinely innovative framework: write Python, compile to WASM,
run with reactive signals in WASM memory. No virtual DOM, no transpilation,
just compiled Python with fine-grained reactivity.

🎉 Python → WASM → Reactive UI is REAL!
This commit is contained in:
Aaron Sinclair committed 2025-11-15 01:50:40 -08:00
1 parent 316c090778
commit 635d466874
54 files changed
+4393

No files matched your search

+238
View File
@@ -0,0 +1,238 @@
"""Tests for LLVM IR code generation."""
import subprocess
import tempfile
from pathlib import Path
def test_llvm_hello_world_structure():
"""Test that LLVM IR has correct structure for HelloWorld."""
from ritz.llvm_codegen import generate_llvm_ir
ir_dict = {
"type": "component",
"name": "HelloWorld",
"args": [],
"body": [
{
"type": "with",
"element": {"tag": "P", "props": {}},
"body": [{"type": "yield", "value": {"type": "constant", "value": "Hello World"}}],
}
],
}
llvm_ir = generate_llvm_ir(ir_dict)
# Check basic LLVM IR structure
assert "define void @render_HelloWorld()" in llvm_ir
assert "ret void" in llvm_ir
# Should have string constants
assert '"p\\00"' in llvm_ir or 'c"p\\00"' in llvm_ir
assert "Hello World" in llvm_ir
# Should call JS bridge functions
assert "@js_create_element" in llvm_ir
assert "@js_create_text" in llvm_ir
assert "@js_append_child" in llvm_ir
def test_llvm_ir_has_function_declarations():
"""Test that LLVM IR declares imported JS functions."""
from ritz.llvm_codegen import generate_llvm_ir
ir_dict = {
"type": "component",
"name": "Simple",
"args": [],
"body": [
{
"type": "with",
"element": {"tag": "P", "props": {}},
"body": [{"type": "yield", "value": {"type": "constant", "value": "test"}}],
}
],
}
llvm_ir = generate_llvm_ir(ir_dict)
# Should declare imported functions
assert "declare i32 @js_create_element" in llvm_ir
assert "declare i32 @js_create_text" in llvm_ir
assert "declare void @js_append_child" in llvm_ir
assert "declare void @js_set_root" in llvm_ir
def test_llvm_ir_with_attributes():
"""Test LLVM IR generation with element attributes."""
from ritz.llvm_codegen import generate_llvm_ir
ir_dict = {
"type": "component",
"name": "Styled",
"args": [],
"body": [
{
"type": "with",
"element": {
"tag": "Div",
"props": {
"class_": {"type": "constant", "value": "container"},
"id": {"type": "constant", "value": "main"},
},
},
"body": [],
}
],
}
llvm_ir = generate_llvm_ir(ir_dict)
# Should have js_set_attribute calls
assert "@js_set_attribute" in llvm_ir
assert "container" in llvm_ir
assert "main" in llvm_ir
def test_llvm_ir_syntax_valid():
"""Test that generated LLVM IR has valid syntax (basic checks)."""
from ritz.llvm_codegen import generate_llvm_ir
ir_dict = {
"type": "component",
"name": "Test",
"args": [],
"body": [
{
"type": "with",
"element": {"tag": "P", "props": {}},
"body": [{"type": "yield", "value": {"type": "constant", "value": "x"}}],
}
],
}
llvm_ir = generate_llvm_ir(ir_dict)
# Basic syntax checks
assert llvm_ir.count("define") == llvm_ir.count("ret void") # All functions return
assert "%" in llvm_ir # Has SSA variables
assert "call" in llvm_ir # Has function calls
assert "@" in llvm_ir # Has global symbols
def test_llvm_ir_compiles_with_llc():
"""Test that generated LLVM IR actually compiles with llc."""
from ritz.llvm_codegen import generate_llvm_ir
ir_dict = {
"type": "component",
"name": "CompileTest",
"args": [],
"body": [
{
"type": "with",
"element": {"tag": "P", "props": {}},
"body": [{"type": "yield", "value": {"type": "constant", "value": "test"}}],
}
],
}
llvm_ir = generate_llvm_ir(ir_dict)
# Check if llc is available
try:
subprocess.run(["llc", "--version"], check=True, capture_output=True)
except (subprocess.CalledProcessError, FileNotFoundError):
# Skip if llc not installed
import pytest
pytest.skip("llc not available")
return
# Try to compile the IR
with tempfile.NamedTemporaryFile(mode="w", suffix=".ll", delete=False) as f:
f.write(llvm_ir)
ll_file = Path(f.name)
try:
# Compile to object file (not WASM yet, just validate syntax)
result = subprocess.run(
["llc", "-filetype=obj", str(ll_file)],
capture_output=True,
text=True,
)
# Should compile without errors
assert result.returncode == 0, f"llc failed:\n{result.stderr}"
finally:
ll_file.unlink()
def test_llvm_ir_compiles_to_wasm():
"""Test that LLVM IR can be compiled to WASM."""
from ritz.llvm_codegen import generate_llvm_ir
ir_dict = {
"type": "component",
"name": "WasmTest",
"args": [],
"body": [
{
"type": "with",
"element": {"tag": "P", "props": {}},
"body": [{"type": "yield", "value": {"type": "constant", "value": "wasm"}}],
}
],
}
llvm_ir = generate_llvm_ir(ir_dict)
# Check if clang with wasm target is available
try:
result = subprocess.run(["clang", "--version"], check=True, capture_output=True, text=True)
# Check if wasm target is supported
if "wasm" not in result.stdout.lower():
import pytest
pytest.skip("clang wasm target not available")
return
except (subprocess.CalledProcessError, FileNotFoundError):
import pytest
pytest.skip("clang not available")
return
with tempfile.NamedTemporaryFile(mode="w", suffix=".ll", delete=False) as f:
f.write(llvm_ir)
ll_file = Path(f.name)
wasm_file = ll_file.with_suffix(".wasm")
try:
# Compile to WASM
result = subprocess.run(
[
"clang",
"--target=wasm32",
"-nostdlib",
"-Wl,--no-entry",
"-Wl,--export-all",
"-o",
str(wasm_file),
str(ll_file),
],
capture_output=True,
text=True,
)
# Should compile successfully
assert result.returncode == 0, f"clang failed:\n{result.stderr}"
assert wasm_file.exists(), "WASM file not created"
assert wasm_file.stat().st_size > 0, "WASM file is empty"
finally:
ll_file.unlink()
if wasm_file.exists():
wasm_file.unlink()