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!
264 lines
8.2 KiB
Python
264 lines
8.2 KiB
Python
"""
|
|
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
|
|
""")
|