Files
ritz/archive/framework.py
T
Aaron Sinclair 635d466874 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!
2025-11-15 01:50:40 -08:00

176 lines
6.2 KiB
Python

"""
Ritz Framework - Python Prototype
This runs in actual Python for debugging/prototyping!
"""
from contextlib import contextmanager
from typing import Generator, Any, List, Dict, Optional, Callable
import json
# Global context for building component trees
_context_stack = []
class Signal:
"""Reactive signal - simplified for Python prototype"""
def __init__(self, initial_value):
self.value = initial_value
self.subscribers = []
def __str__(self):
return str(self.value)
def __iadd__(self, other):
self.value += other
self._notify()
return self
def _notify(self):
for subscriber in self.subscribers:
subscriber()
def subscribe(self, callback):
self.subscribers.append(callback)
class VNode:
"""Virtual node for component tree"""
def __init__(self, tag: str, props: Dict = None, children: List = None):
self.tag = tag
self.props = props or {}
self.children = children or []
def to_html(self) -> str:
"""Render to HTML string"""
if self.children:
children_html = ''.join(
child.to_html() if isinstance(child, VNode) else str(child)
for child in self.children
)
attrs = ' '.join(f'{k}="{v}"' for k, v in self.props.items() if k != 'onclick')
attrs = ' ' + attrs if attrs else ''
return f'<{self.tag}{attrs}>{children_html}</{self.tag}>'
else:
attrs = ' '.join(f'{k}="{v}"' for k, v in self.props.items() if k != 'onclick')
attrs = ' ' + attrs if attrs else ''
return f'<{self.tag}{attrs} />'
def to_ir(self) -> Dict:
"""Convert to intermediate representation"""
# Filter out non-serializable props like onclick functions
serializable_props = {
k: v for k, v in self.props.items()
if not callable(v)
}
return {
'type': 'element',
'tag': self.tag,
'props': serializable_props,
'children': [
child.to_ir() if isinstance(child, VNode)
else {'type': 'text', 'value': str(child)}
for child in self.children
]
}
class Element:
"""Context manager for building elements"""
def __init__(self, tag: str, *children, **props):
self.tag = tag
self.props = props
self.children = list(children) if children else []
self.vnode = VNode(tag, props, self.children)
def __enter__(self):
_context_stack.append(self)
# Store the initial length so we know what was added in context
self._initial_children_len = len(self.children)
return self
def __exit__(self, *args):
_context_stack.pop()
# Don't rebuild vnode here - let the component decorator handle it
def component(func: Callable) -> Callable:
"""Decorator for components that use yield"""
def wrapper(*args, **kwargs):
gen = func(*args, **kwargs)
if hasattr(gen, '__next__'): # It's a generator
root_element = None
initial_stack_len = len(_context_stack)
# Track the first context that gets created
first_context = None
# Execute the generator to build the tree
for item in gen:
# Check if a new context was just created
if not first_context and len(_context_stack) > initial_stack_len:
first_context = _context_stack[initial_stack_len]
current_context = _context_stack[-1] if _context_stack else None
if current_context:
# We're inside a with statement, add to its children
if isinstance(item, Element):
current_context.children.append(item.vnode)
elif isinstance(item, VNode):
current_context.children.append(item)
elif isinstance(item, str):
current_context.children.append(item)
else:
# Try to convert to string
current_context.children.append(str(item))
else:
# No context, this should be the root
if isinstance(item, Element):
root_element = item.vnode
elif isinstance(item, VNode):
root_element = item
# Return the first context if it was created, otherwise root_element
if first_context:
first_context.vnode = VNode(
first_context.tag,
first_context.props,
first_context.children
)
return first_context.vnode
return root_element
else:
return func(*args, **kwargs)
return wrapper
# Factory functions for HTML elements
def _make_element(tag: str) -> Callable:
def element_factory(*children, **props):
if len(children) == 1 and isinstance(children[0], str):
# Shorthand: P('text') creates element with text
return Element(tag, children[0], **props)
return Element(tag, *children, **props)
return element_factory
# Generate standard HTML elements
for tag in ['main', 'div', 'p', 'button', 'label', 'h1', 'h2', 'h3',
'article', 'section', 'header', 'footer', 'span', 'strong',
'em', 'li', 'ul', 'ol', 'form', 'input', 'nav', 'time']:
globals()[tag.capitalize()] = _make_element(tag)
def signal(initial_value) -> Signal:
"""Create a reactive signal"""
return Signal(initial_value)
# Helper function for debugging
def render(component_func, *args, **kwargs):
"""Render a component to HTML and IR"""
result = component_func(*args, **kwargs)
if isinstance(result, VNode):
print("=== HTML Output ===")
print(result.to_html())
print("\n=== IR Output ===")
print(json.dumps(result.to_ir(), indent=2))
else:
print("Component did not return a VNode")
return result