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:
1 parent
316c090778
commit
635d466874
54 files changed
+4393
No files matched your search
@@ -0,0 +1,263 @@
|
||||
"""
|
||||
Ritz Debugging Infrastructure Concept
|
||||
Shows how we can debug Python code running in WASM
|
||||
"""
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
@dataclass
|
||||
class DebugSymbol:
|
||||
"""Maps WASM offset to Python source location"""
|
||||
wasm_offset: int
|
||||
file: str
|
||||
line: int
|
||||
column: int
|
||||
function: str
|
||||
source_text: str
|
||||
|
||||
class WASMDebugger:
|
||||
"""Debugging infrastructure for Python-in-WASM"""
|
||||
|
||||
def __init__(self):
|
||||
self.symbols: Dict[int, DebugSymbol] = {}
|
||||
self.breakpoints: List[DebugSymbol] = []
|
||||
self.source_files: Dict[str, List[str]] = {}
|
||||
|
||||
def generate_debug_info(self, component_ir, wasm_module):
|
||||
"""Generate debug symbols during compilation"""
|
||||
# This would be done by LLVM during compilation
|
||||
# Here's a conceptual example:
|
||||
|
||||
debug_info = {
|
||||
'format': 'ritz-debug-v1',
|
||||
'source_language': 'python',
|
||||
'symbols': []
|
||||
}
|
||||
|
||||
# Map each IR node to WASM offsets
|
||||
for node in component_ir['body']:
|
||||
if node['type'] == 'yield':
|
||||
symbol = {
|
||||
'wasm_offset': '0x1234', # Would be actual WASM offset
|
||||
'source': {
|
||||
'file': 'component.py',
|
||||
'line': node.get('line', 0),
|
||||
'column': node.get('column', 0),
|
||||
'text': 'yield Element()'
|
||||
}
|
||||
}
|
||||
debug_info['symbols'].append(symbol)
|
||||
|
||||
return debug_info
|
||||
|
||||
def set_breakpoint(self, file: str, line: int):
|
||||
"""Set a breakpoint at Python source location"""
|
||||
# Find WASM offset for this source location
|
||||
for offset, symbol in self.symbols.items():
|
||||
if symbol.file == file and symbol.line == line:
|
||||
print(f"Breakpoint set at {file}:{line} (WASM offset: {hex(offset)})")
|
||||
self.breakpoints.append(symbol)
|
||||
|
||||
# In real implementation, this would:
|
||||
# 1. Communicate with browser DevTools
|
||||
# 2. Set WASM breakpoint at the offset
|
||||
# 3. Map back to Python source when hit
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def source_map_example(self):
|
||||
"""Example of source map for WASM→Python mapping"""
|
||||
source_map = {
|
||||
"version": 3,
|
||||
"sources": ["counter.py", "framework.py"],
|
||||
"names": ["count", "increment", "signal"],
|
||||
"mappings": "AAAA,SAAS;AACT,YAAY", # Base64 VLQ encoded
|
||||
"file": "app.wasm",
|
||||
|
||||
# Extensions for Python debugging
|
||||
"x_ritz_debug": {
|
||||
"python_version": "3.11",
|
||||
"symbols": [
|
||||
{
|
||||
"wasm_offset": "0x1000",
|
||||
"python_location": "counter.py:5:8",
|
||||
"ast_node": "ast.Assign",
|
||||
"scope_vars": ["count", "increment"]
|
||||
}
|
||||
],
|
||||
|
||||
# Embedded Python source for debugging!
|
||||
"embedded_sources": {
|
||||
"counter.py": """
|
||||
@component
|
||||
def Counter():
|
||||
count = signal(0) # Line 3
|
||||
|
||||
def increment(): # Line 5
|
||||
count += 1 # Line 6 - Breakpoint here!
|
||||
|
||||
with Div():
|
||||
yield Button('Click', onclick=increment)
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return source_map
|
||||
|
||||
class LiveEditingSupport:
|
||||
"""Live editing Python components in the browser"""
|
||||
|
||||
def __init__(self, debugger: WASMDebugger):
|
||||
self.debugger = debugger
|
||||
self.module_cache = {}
|
||||
|
||||
def edit_component(self, component_name: str, new_source: str):
|
||||
"""Live edit a component"""
|
||||
print(f"Live editing {component_name}")
|
||||
|
||||
# 1. Parse the new Python source
|
||||
from ast_parser import parse_component_source
|
||||
ir = parse_component_source(new_source)
|
||||
|
||||
# 2. Compile just this component to WASM
|
||||
wasm_module = self.compile_component(ir)
|
||||
|
||||
# 3. Hot reload in browser
|
||||
self.hot_reload(component_name, wasm_module)
|
||||
|
||||
return {
|
||||
'status': 'reloaded',
|
||||
'component': component_name,
|
||||
'size': len(wasm_module)
|
||||
}
|
||||
|
||||
def compile_component(self, ir):
|
||||
"""Compile single component to WASM module"""
|
||||
# In reality, this would use LLVM
|
||||
# For now, mock it
|
||||
return b'\x00asm\x01\x00\x00\x00...'
|
||||
|
||||
def hot_reload(self, name: str, wasm_bytes: bytes):
|
||||
"""Hot reload a WASM module in browser"""
|
||||
js_code = f"""
|
||||
// Hot reload component
|
||||
const oldModule = RitzModules.get('{name}');
|
||||
const newModule = await WebAssembly.instantiate({list(wasm_bytes)});
|
||||
|
||||
// Preserve state
|
||||
const state = oldModule.exports.get_state();
|
||||
newModule.exports.set_state(state);
|
||||
|
||||
// Swap modules
|
||||
RitzModules.set('{name}', newModule);
|
||||
|
||||
// Re-render affected components
|
||||
RitzRenderer.rerender('{name}');
|
||||
"""
|
||||
print(f"Hot reload JS:\n{js_code}")
|
||||
|
||||
class BrowserDevToolsIntegration:
|
||||
"""Integration with Chrome/Firefox DevTools"""
|
||||
|
||||
@staticmethod
|
||||
def generate_dwarf_symbols():
|
||||
"""Generate DWARF debug symbols for WASM"""
|
||||
# DWARF is the standard debug format
|
||||
# LLVM can generate this automatically
|
||||
|
||||
dwarf = {
|
||||
'.debug_info': {
|
||||
'compilation_unit': {
|
||||
'language': 'DW_LANG_Python',
|
||||
'producer': 'Ritz Compiler v0.1',
|
||||
'source_file': 'app.py'
|
||||
}
|
||||
},
|
||||
'.debug_line': {
|
||||
# Line number program
|
||||
# Maps WASM addresses to source lines
|
||||
'line_table': [
|
||||
{'address': 0x1000, 'file': 1, 'line': 5, 'column': 8},
|
||||
{'address': 0x1010, 'file': 1, 'line': 6, 'column': 4},
|
||||
]
|
||||
},
|
||||
'.debug_abbrev': {
|
||||
# Abbreviation table
|
||||
}
|
||||
}
|
||||
|
||||
return dwarf
|
||||
|
||||
@staticmethod
|
||||
def devtools_protocol_example():
|
||||
"""Example of Chrome DevTools Protocol for WASM debugging"""
|
||||
return {
|
||||
'method': 'Debugger.setBreakpoint',
|
||||
'params': {
|
||||
'location': {
|
||||
'scriptId': 'wasm://app.wasm/counter.py',
|
||||
'lineNumber': 6,
|
||||
'columnNumber': 8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Demonstration
|
||||
if __name__ == '__main__':
|
||||
print("=== Ritz Debugging Infrastructure ===\n")
|
||||
|
||||
debugger = WASMDebugger()
|
||||
|
||||
print("1. Source Map Example:")
|
||||
print(json.dumps(debugger.source_map_example(), indent=2)[:500] + "...\n")
|
||||
|
||||
print("2. Setting Breakpoint:")
|
||||
# Simulate adding debug symbols
|
||||
debugger.symbols[0x1234] = DebugSymbol(
|
||||
wasm_offset=0x1234,
|
||||
file='counter.py',
|
||||
line=6,
|
||||
column=8,
|
||||
function='increment',
|
||||
source_text='count += 1'
|
||||
)
|
||||
debugger.set_breakpoint('counter.py', 6)
|
||||
|
||||
print("\n3. Live Editing:")
|
||||
editor = LiveEditingSupport(debugger)
|
||||
result = editor.edit_component('Counter', '''
|
||||
@component
|
||||
def Counter():
|
||||
count = signal(0)
|
||||
|
||||
def increment():
|
||||
count += 2 # Changed from += 1
|
||||
print(f"New count: {count}")
|
||||
|
||||
with Div():
|
||||
yield Button('Click', onclick=increment)
|
||||
''')
|
||||
print(f"Result: {result}")
|
||||
|
||||
print("\n4. DevTools Integration:")
|
||||
print(json.dumps(BrowserDevToolsIntegration.devtools_protocol_example(), indent=2))
|
||||
|
||||
print("\n=== The Dream ===")
|
||||
print("""
|
||||
In Chrome DevTools:
|
||||
- Set breakpoints in Python source (not WASM or JS!)
|
||||
- Step through Python code line by line
|
||||
- Inspect Python variables and objects
|
||||
- Live edit Python and see changes immediately
|
||||
- Full stack traces in Python
|
||||
|
||||
This is possible because:
|
||||
1. Our Python compiles to WASM with debug symbols
|
||||
2. WASM has source map support
|
||||
3. We embed Python source in the WASM module
|
||||
4. Browser DevTools understand DWARF/source maps
|
||||
""")
|
||||
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example Ritz Application
|
||||
This actually runs in Python!
|
||||
"""
|
||||
|
||||
from framework import (
|
||||
component, signal, render,
|
||||
Main, Div, P, Button, Label, H1, H2,
|
||||
Article, Header, Section, Footer, Span, Strong, Em, Time
|
||||
)
|
||||
|
||||
@component
|
||||
def HighFiveApp():
|
||||
"""Simple counter example"""
|
||||
high_fives = signal(0)
|
||||
|
||||
def increment():
|
||||
high_fives += 1
|
||||
print(f"High fives incremented to: {high_fives.value}")
|
||||
|
||||
with Main():
|
||||
yield H1('High Five Counter')
|
||||
yield P('Give me a high 5 bro!')
|
||||
yield Label(f'High fives: {high_fives}')
|
||||
yield Button('Click me', onclick=increment)
|
||||
|
||||
@component
|
||||
def Comment(author: str, text: str, replies=None):
|
||||
"""Nested comment component"""
|
||||
with Div(class_='comment'):
|
||||
with Div(class_='comment-header'):
|
||||
yield Strong(author)
|
||||
yield ' says:'
|
||||
|
||||
with Div(class_='comment-body'):
|
||||
yield text
|
||||
|
||||
if replies:
|
||||
with Div(class_='replies'):
|
||||
for reply in replies:
|
||||
yield reply
|
||||
|
||||
@component
|
||||
def BlogPost(title: str, author: str, content: str):
|
||||
"""Complex component with mixed content"""
|
||||
with Article(class_='blog-post'):
|
||||
with Header():
|
||||
yield H1(title)
|
||||
yield 'By ' # Raw string!
|
||||
yield Span(author, class_='author')
|
||||
yield ' on '
|
||||
yield Time('2024-01-15')
|
||||
|
||||
with Section(class_='content'):
|
||||
yield P(content)
|
||||
|
||||
yield H2('Comments')
|
||||
|
||||
# Nested components with yield
|
||||
yield Comment(
|
||||
'Alice',
|
||||
'Great post! This yield syntax is brilliant.',
|
||||
replies=[
|
||||
Comment('Bob', 'I agree! So Pythonic.'),
|
||||
Comment('Charlie', 'Can\'t wait to see the WASM output.')
|
||||
]
|
||||
)
|
||||
|
||||
yield Comment(
|
||||
'Dave',
|
||||
'How does this compare to React performance-wise?'
|
||||
)
|
||||
|
||||
@component
|
||||
def TodoItem(text: str, completed: signal):
|
||||
"""Todo item with reactive state"""
|
||||
def toggle():
|
||||
completed.value = not completed.value
|
||||
print(f"Todo '{text}' toggled to: {completed.value}")
|
||||
|
||||
with Div(class_='todo-item'):
|
||||
checkbox_props = {'type': 'checkbox', 'onclick': toggle}
|
||||
if completed.value:
|
||||
checkbox_props['checked'] = 'checked'
|
||||
|
||||
# Note: In real implementation, Input would be a special element
|
||||
# For now we just demonstrate the pattern
|
||||
yield Div(f'[{"✓" if completed.value else " "}] {text}',
|
||||
class_='todo-checkbox',
|
||||
onclick=toggle)
|
||||
|
||||
@component
|
||||
def TodoApp():
|
||||
"""Todo list with multiple signals"""
|
||||
todos = [
|
||||
('Build parser', signal(False)),
|
||||
('Implement WASM compiler', signal(False)),
|
||||
('Create reactive runtime', signal(False)),
|
||||
('Optimize DOM bridge', signal(False))
|
||||
]
|
||||
|
||||
with Div(class_='todo-app'):
|
||||
yield H1('Ritz Framework TODO')
|
||||
|
||||
for text, completed in todos:
|
||||
yield TodoItem(text, completed)
|
||||
|
||||
with Footer():
|
||||
completed_count = sum(1 for _, c in todos if c.value)
|
||||
yield P(f'Completed: {completed_count}/{len(todos)}')
|
||||
|
||||
def main():
|
||||
"""Run examples and show output"""
|
||||
print("=" * 60)
|
||||
print("RITZ FRAMEWORK - Python Prototype")
|
||||
print("=" * 60)
|
||||
|
||||
print("\n### Example 1: High Five Counter ###")
|
||||
render(HighFiveApp)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("\n### Example 2: Blog Post with Comments ###")
|
||||
render(BlogPost,
|
||||
title="Introducing Ritz Framework",
|
||||
author="Aaron",
|
||||
content="A new approach to web frameworks: Python syntax that compiles to WASM!")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("\n### Example 3: Todo App ###")
|
||||
render(TodoApp)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("\nThis is actual Python running in CPython!")
|
||||
print("In production, this would compile to WebAssembly.")
|
||||
print("The same code runs in both environments!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
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
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple test to debug Ritz framework
|
||||
"""
|
||||
|
||||
from framework import (
|
||||
component, signal,
|
||||
Main, Div, P, Button, H1
|
||||
)
|
||||
|
||||
@component
|
||||
def SimpleTest():
|
||||
"""Simplest possible test"""
|
||||
with Main():
|
||||
yield P('Hello World')
|
||||
|
||||
# Test it
|
||||
import json
|
||||
|
||||
result = SimpleTest()
|
||||
print("Result type:", type(result))
|
||||
print("Result:", result)
|
||||
|
||||
if result:
|
||||
print("\n=== HTML ===")
|
||||
print(result.to_html())
|
||||
print("\n=== IR ===")
|
||||
print(json.dumps(result.to_ir(), indent=2))
|
||||
Reference in new issue
Block a user