diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..890d7f9 Binary files /dev/null and b/.coverage differ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..95a3594 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,163 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Ritz is a WebAssembly UI framework with Python-like syntax that compiles to high-performance WASM while remaining executable in Python for debugging. The framework uses **valid Python syntax** that can execute in CPython for prototyping/debugging, but is designed to compile to WebAssembly for production. + +**Current Status**: Python prototype stage. The WASM compilation pipeline is conceptual/architectural. + +## Development Commands + +### Running the Prototype +```bash +# Run example application showing various components +python3 example.py + +# Run simple test +python3 test.py + +# Test AST parser +python3 ast_parser.py + +# View module system concept +python3 module_system.py + +# View compilation pipeline concept +python3 compiler.py + +# View debugging concept +python3 debug_concept.py +``` + +## Core Architecture + +### Component Model +Components are defined using the `@component` decorator with Python generator syntax (yield-based): + +```python +@component +def Counter(): + count = signal(0) # Reactive state + + def increment(): + count += 1 + + with Div(class_='counter'): # Context managers for element nesting + yield H1('Counter') # Yield elements/strings/components + yield P(f'Count: {count}') + yield Button('Click me', onclick=increment) +``` + +Key patterns: +- **Generators**: Components are generator functions that yield children +- **Context managers**: Use `with Element():` for nested structures +- **Signals**: Reactive state via `signal(initial_value)` +- **Mixed content**: Can yield strings, elements, or other components + +### File Structure + +**Core Framework Files:** +- `framework.py` - Python prototype runtime (Signal, VNode, Element, component decorator) +- `ast_parser.py` - Parses Python components using Python's `ast` module to generate IR +- `compiler.py` - Complete compilation pipeline concept (Python → IR → LLVM → WASM) +- `module_system.py` - Progressive module loading architecture for code splitting + +**Conceptual/Research:** +- `debug_concept.py` - Python-in-WASM debugging architecture (DWARF symbols, source maps) +- `RESEARCH.md` - Complete technical architecture and implementation roadmap + +**Examples:** +- `example.py` - Demonstrates component syntax with various examples +- `test.py` - Simple test for debugging framework behavior + +### Compilation Pipeline (Conceptual) + +The planned compilation flow: +1. **Parser**: Uses Python's `ast` module to parse components (no custom parser needed!) +2. **IR Generation**: Transform Python AST → Ritz intermediate representation +3. **LLVM Codegen**: Ritz IR → LLVM IR +4. **WASM Output**: LLVM IR → WebAssembly with debug symbols (DWARF) +5. **Module Splitting**: Split into multiple WASM modules (bootstrap, core, features) + +### Reactive System + +**Signals** are the primitive reactive unit: +- Store a value and list of subscribers +- When value changes, notify all subscribers +- In Python prototype: simple Python class +- In WASM target: efficient memory layout with direct DOM node references + +**Key insight**: No virtual DOM. Signals map directly to DOM nodes for updates. + +### Module System Architecture + +Apps split into multiple WASM modules for security and performance: +- `bootstrap.wasm` (~50KB) - Login only, no auth required, no business logic +- `app-core.wasm` (~200KB) - Main app, requires authentication +- `features/*.wasm` - Lazy-loaded feature modules + +**Security benefit**: Unauthenticated users never receive app code containing API endpoints or business logic. + +## Important Implementation Details + +### AST Parsing +The framework uses **Python's own `ast` module** to parse components (not a custom parser). This provides: +- Zero parsing overhead +- Perfect Python compatibility +- Automatic syntax error messages +- Type hints support + +See `ast_parser.py` for the `RitzASTTransformer` that converts Python AST to Ritz IR. + +### VNode Structure +Internal representation during Python execution: +- `VNode(tag, props, children)` - Virtual DOM node +- `to_html()` - Render to HTML string +- `to_ir()` - Convert to intermediate representation for compilation + +### Context Stack +`_context_stack` is a global list tracking nested `with Element():` blocks. The `@component` decorator manages this stack to build the component tree. + +## Debugging Philosophy + +**Revolutionary concept**: Debug Python code running in WASM, not JavaScript! +- DWARF debug symbols embedded in WASM +- Source maps linking WASM instructions → Python source lines +- Chrome DevTools can set breakpoints in Python files +- Inspect Python variables, not mangled JS + +See `debug_concept.py` and RESEARCH.md sections on debugging. + +## Design Principles + +1. **Python-executable, WASM-compilable**: The same code runs in both environments +2. **Yield-based composition**: Natural Python syntax for building component trees +3. **Fine-grained reactivity**: Signals update DOM directly, no virtual DOM diffing +4. **Progressive loading**: Split code into secure, cacheable modules +5. **Developer experience**: Debug in Python, ship as WASM + +## Performance Targets + +Based on SolidJS/Leptos research: +- Bundle size: < 200KB compressed +- Startup time: < 500ms on slow 3G +- Update performance: Within 10% of vanilla JS +- Memory usage: 70% less than React +- DOM mutations: 99% fewer than virtual DOM frameworks + +## When Modifying Code + +1. **Python prototype** (`framework.py`): Currently functional, can be tested with Python +2. **Compilation pipeline** (`compiler.py`, `ast_parser.py`): Architectural/conceptual code that demonstrates intended design +3. **Module system** (`module_system.py`): Demonstrates progressive loading strategy +4. Components must be valid Python and use generator syntax with yield +5. All examples should run with `python3 example.py` + +## Key Technical Decisions + +- **Why yield?** Natural Python syntax, composable, clear execution order +- **Why compile vs interpret?** 10-100x performance, smaller bundle, type safety +- **Why LLVM?** Mature toolchain, excellent WASM backend, optimization passes +- **Why Python's ast module?** No custom parser needed, perfect compatibility diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..77df7b6 --- /dev/null +++ b/TODO.md @@ -0,0 +1,108 @@ +# Ritz Framework TODO + +## ✅ COMPLETED: Phase 1 & 2 - Working Reactive Framework! + +### Phase 1: Hello World (Static Content) ✅ +- [x] Set up project structure (pyproject.toml, ruff, black, pytest) +- [x] Write AST parser tests using Python's `ast` module +- [x] Implement AST parser (HelloWorld, props, nesting, augassign) +- [x] All parser tests passing (4/4) +- [x] Write LLVM IR codegen tests +- [x] **Implement LLVM IR code generator** (NOT C - direct LLVM!) +- [x] All codegen tests passing (4/4) +- [x] Create JavaScript DOM bridge/runtime (ritz-runtime.js) +- [x] Create build script (Python → AST → IR → LLVM → clang → WASM) +- [x] Write HelloWorld.py component +- [x] **✅ MILESTONE: HelloWorld.wasm renders in browser!** + +### Phase 2: Reactive Signals ✅ +- [x] Parse signal declarations and augmented assignment (+=) +- [x] Parse event handlers (onclick) +- [x] Parse f-strings with signal references +- [x] **Implement signals in WASM memory** (`@signal_name = global i32`) +- [x] Generate signal getter/setter functions +- [x] Generate exported event handler functions +- [x] Bind events in JS runtime to WASM functions +- [x] Update DOM when signals change +- [x] Write HighFive.py counter component +- [x] **✅ MILESTONE: Reactive counter working!** + +### Phase 3: Developer Experience ✅ +- [x] Clean runtime architecture (RitzRuntime + RitzLoader classes) +- [x] Debug bar with Ctrl+` toggle +- [x] Clean HTML examples with minimal boilerplate +- [x] Dev server with port reuse +- [x] Run black and ruff formatters +- [x] `from ritz.syntax import *` for clean component syntax + +--- + +## 🚀 Current State + +**WORKING:** +- Python → AST → Ritz IR → LLVM IR → WASM pipeline +- Reactive signals stored in WASM memory +- Event handlers exported from WASM +- Clean runtime with debug tools +- Two working examples (HelloWorld, HighFive) + +**Architecture:** +``` +Python Component + ↓ ast.parse() +Ritz IR + ↓ LLVM IR Generator (llvm_codegen_signals.py) +LLVM IR (.ll file) + ↓ clang --target=wasm32 +WebAssembly + ↓ RitzRuntime (JavaScript) +Live Reactive UI +``` + +--- + +## Phase 4: Next Steps + +### In-Browser Compilation (THE BIG ONE) +- [ ] Research: Compile Ritz compiler to WASM (Pyodide vs Rust rewrite) +- [ ] Implement ` + + + + diff --git a/examples/HelloWorld.ll b/examples/HelloWorld.ll new file mode 100644 index 0000000..e163c05 --- /dev/null +++ b/examples/HelloWorld.ll @@ -0,0 +1,25 @@ +; Generated by Ritz Framework with Signals +; Component: HelloWorld + +; String constants +@.str.0 = private unnamed_addr constant [2 x i8] c"p\00" +@.str.1 = private unnamed_addr constant [12 x i8] c"Hello World\00" + +; JavaScript bridge function declarations +declare i32 @js_create_element(i8*) +declare i32 @js_create_text(i8*) +declare void @js_append_child(i32, i32) +declare void @js_set_attribute(i32, i8*, i8*) +declare void @js_set_root(i32) +declare void @js_bind_event(i32, i8*, i8*) +declare void @js_update_text_content(i32, i8*) + +; Component render function +define void @render_HelloWorld() { +entry: + %var0 = call i32 @js_create_element(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.0, i32 0, i32 0)) + %var1 = call i32 @js_create_text(i8* getelementptr inbounds ([12 x i8], [12 x i8]* @.str.1, i32 0, i32 0)) + call void @js_append_child(i32 %var0, i32 %var1) + call void @js_set_root(i32 %var0) + ret void +} diff --git a/examples/HelloWorld.py b/examples/HelloWorld.py new file mode 100644 index 0000000..978a7b4 --- /dev/null +++ b/examples/HelloWorld.py @@ -0,0 +1,14 @@ +""" +HelloWorld component - The simplest possible Ritz component. + +This demonstrates a static

tag with text content. +""" + +from ritz.syntax import * + + +@component +def HelloWorld(): + """Render a simple paragraph with 'Hello World'.""" + with P(): + yield "Hello World" diff --git a/examples/HelloWorld.wasm b/examples/HelloWorld.wasm new file mode 100755 index 0000000..f89133f Binary files /dev/null and b/examples/HelloWorld.wasm differ diff --git a/examples/HighFive.html b/examples/HighFive.html new file mode 100644 index 0000000..ed82544 --- /dev/null +++ b/examples/HighFive.html @@ -0,0 +1,96 @@ + + + + + + HighFive Counter - Ritz Framework + + + + +

+ + + + + + + + diff --git a/examples/HighFive.ll b/examples/HighFive.ll new file mode 100644 index 0000000..7262a39 --- /dev/null +++ b/examples/HighFive.ll @@ -0,0 +1,75 @@ +; Generated by Ritz Framework with Signals +; Component: HighFive + +; String constants +@.str.0 = private unnamed_addr constant [4 x i8] c"div\00" +@.str.1 = private unnamed_addr constant [6 x i8] c"class\00" +@.str.2 = private unnamed_addr constant [14 x i8] c"high-five-app\00" +@.str.3 = private unnamed_addr constant [2 x i8] c"p\00" +@.str.4 = private unnamed_addr constant [25 x i8] c"Give me a high five! ✋\00" +@.str.5 = private unnamed_addr constant [6 x i8] c"count\00" +@.str.6 = private unnamed_addr constant [2 x i8] c"0\00" +@.str.7 = private unnamed_addr constant [7 x i8] c"button\00" +@.str.8 = private unnamed_addr constant [6 x i8] c"click\00" +@.str.9 = private unnamed_addr constant [10 x i8] c"increment\00" +@.str.10 = private unnamed_addr constant [11 x i8] c"High Five!\00" + +; Signal storage (reactive state in WASM memory) +@signal_high_fives = global i32 0 + +; JavaScript bridge function declarations +declare i32 @js_create_element(i8*) +declare i32 @js_create_text(i8*) +declare void @js_append_child(i32, i32) +declare void @js_set_attribute(i32, i8*, i8*) +declare void @js_set_root(i32) +declare void @js_bind_event(i32, i8*, i8*) +declare void @js_update_text_content(i32, i8*) + +; Signal helper functions +define i32 @get_high_fives() { +entry: + %val = load i32, i32* @signal_high_fives + ret i32 %val +} + +define void @set_high_fives(i32 %newval) { +entry: + store i32 %newval, i32* @signal_high_fives + ; TODO: Trigger DOM update callback + ret void +} + +; Event handler: increment +define void @increment() { +entry: + %var0 = load i32, i32* @signal_high_fives + %var1 = add i32 %var0, 1 + store i32 %var1, i32* @signal_high_fives + ; TODO: Update DOM nodes bound to high_fives + ret void +} + +; Component render function +define void @render_HighFive() { +entry: + %var2 = call i32 @js_create_element(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.0, i32 0, i32 0)) + call void @js_set_attribute(i32 %var2, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str.2, i32 0, i32 0)) + %var3 = call i32 @js_create_element(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.3, i32 0, i32 0)) + %var4 = call i32 @js_create_text(i8* getelementptr inbounds ([25 x i8], [25 x i8]* @.str.4, i32 0, i32 0)) + call void @js_append_child(i32 %var3, i32 %var4) + call void @js_append_child(i32 %var2, i32 %var3) + %var5 = call i32 @js_create_element(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.3, i32 0, i32 0)) + call void @js_set_attribute(i32 %var5, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.5, i32 0, i32 0)) + ; TODO: Format f-string with signal high_fives + %var6 = call i32 @js_create_text(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.6, i32 0, i32 0)) + call void @js_append_child(i32 %var5, i32 %var6) + call void @js_append_child(i32 %var2, i32 %var5) + %var7 = call i32 @js_create_element(i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str.7, i32 0, i32 0)) + call void @js_bind_event(i32 %var7, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.8, i32 0, i32 0), i8* getelementptr inbounds ([10 x i8], [10 x i8]* @.str.9, i32 0, i32 0)) + %var8 = call i32 @js_create_text(i8* getelementptr inbounds ([11 x i8], [11 x i8]* @.str.10, i32 0, i32 0)) + call void @js_append_child(i32 %var7, i32 %var8) + call void @js_append_child(i32 %var2, i32 %var7) + call void @js_set_root(i32 %var2) + ret void +} diff --git a/examples/HighFive.py b/examples/HighFive.py new file mode 100644 index 0000000..d9a2c03 --- /dev/null +++ b/examples/HighFive.py @@ -0,0 +1,26 @@ +""" +HighFive counter component - Demonstrates reactive signals and event handlers. + +This component uses a signal to track state and an onclick handler to update it. +""" + +from ritz.syntax import * + + +@component +def HighFive(): + """A counter that tracks high fives.""" + high_fives = signal(0) + + def increment(): + high_fives += 1 + + with Div(class_="high-five-app"): + with P(): + yield "Give me a high five! ✋" + + with P(class_="count"): + yield f"High fives: {high_fives}" + + with Button(onclick=increment): + yield "High Five!" diff --git a/examples/HighFive.wasm b/examples/HighFive.wasm new file mode 100755 index 0000000..d79aeac Binary files /dev/null and b/examples/HighFive.wasm differ diff --git a/examples/highfive.html b/examples/highfive.html new file mode 100644 index 0000000..86bd6dd --- /dev/null +++ b/examples/highfive.html @@ -0,0 +1,314 @@ + + + + + + HighFive Counter - Reactive Signals in WASM + + + +
+

🎭 Ritz Framework - HighFive Counter

+

Reactive Signals in WebAssembly

+
+✨ Signal stored in WASM memory
+✨ Event handler exported from WASM
+✨ Click → WASM → Signal update → DOM update
+
+Pipeline: HighFive.py → LLVM IR → clang → WASM +
+
+ +
⏳ Initializing...
+ +
+ + + + diff --git a/examples/index.html b/examples/index.html new file mode 100644 index 0000000..39faff0 --- /dev/null +++ b/examples/index.html @@ -0,0 +1,215 @@ + + + + + + Ritz HelloWorld - Python → WASM + + + +
+

🎭 Ritz Framework

+

Python → LLVM IR → WebAssembly

+
+HelloWorld.py → ast.parse() → Ritz IR → LLVM IR → clang → WASM +
+
+ +
⏳ Initializing...
+ +
+ + + + diff --git a/examples/ritz-debug.css b/examples/ritz-debug.css new file mode 100644 index 0000000..2449b0b --- /dev/null +++ b/examples/ritz-debug.css @@ -0,0 +1,131 @@ +/* Ritz Debug Bar Styles */ + +.ritz-debug { + position: fixed; + top: 0; + right: 0; + width: 400px; + height: 100vh; + background: #1e1e1e; + color: #d4d4d4; + font-family: 'Consolas', 'Monaco', monospace; + font-size: 13px; + box-shadow: -2px 0 10px rgba(0,0,0,0.3); + transition: transform 0.3s ease; + z-index: 9999; + overflow-y: auto; +} + +.ritz-debug.hidden { + transform: translateX(100%); +} + +.debug-header { + background: #252525; + padding: 15px; + border-bottom: 1px solid #3e3e3e; + display: flex; + justify-content: space-between; + align-items: center; +} + +.debug-header h3 { + margin: 0; + font-size: 16px; + font-weight: 600; +} + +.debug-header button { + background: none; + border: none; + color: #d4d4d4; + font-size: 24px; + cursor: pointer; + padding: 0; + width: 30px; + height: 30px; + line-height: 1; +} + +.debug-header button:hover { + color: #fff; +} + +.status { + margin: 15px; + padding: 10px; + background: #2d2d30; + border-left: 3px solid #007acc; + border-radius: 3px; + font-size: 12px; +} + +.status.success { + border-left-color: #4ec9b0; +} + +.status.error { + border-left-color: #f48771; + color: #f48771; +} + +.debug-content { + padding: 15px; +} + +.debug-section { + margin-bottom: 20px; +} + +.debug-section h4 { + margin: 0 0 10px 0; + font-size: 12px; + text-transform: uppercase; + color: #858585; + font-weight: 600; +} + +.debug-section pre { + background: #252525; + padding: 10px; + border-radius: 3px; + overflow-x: auto; + margin: 0; + font-size: 11px; + line-height: 1.6; +} + +/* Toggle hint */ +.debug-toggle-hint { + position: fixed; + bottom: 20px; + right: 20px; + background: #252525; + color: #d4d4d4; + padding: 8px 12px; + border-radius: 4px; + font-family: 'Consolas', 'Monaco', monospace; + font-size: 11px; + box-shadow: 0 2px 8px rgba(0,0,0,0.3); + opacity: 0; + animation: fadeInOut 3s ease-in-out; + pointer-events: none; + z-index: 10000; +} + +@keyframes fadeInOut { + 0%, 100% { opacity: 0; } + 10%, 90% { opacity: 1; } +} + +/* App container default styles */ +#app { + max-width: 1200px; + margin: 0 auto; + padding: 20px; + transition: margin-right 0.3s ease; +} + +body.debug-open #app { + margin-right: 400px; +} diff --git a/examples/ritz-debug.js b/examples/ritz-debug.js new file mode 100644 index 0000000..a214f67 --- /dev/null +++ b/examples/ritz-debug.js @@ -0,0 +1,83 @@ +/** + * Ritz Debug Bar Controller + */ + +class RitzDebug { + constructor() { + this.debugBar = document.getElementById('ritz-debug'); + this.isOpen = false; + + // Setup keyboard shortcut (Ctrl+`) + document.addEventListener('keydown', (e) => { + if (e.ctrlKey && e.key === '`') { + e.preventDefault(); + this.toggle(); + } + }); + + // Show hint on first load + this.showHint(); + } + + toggle() { + this.isOpen = !this.isOpen; + + if (this.debugBar) { + if (this.isOpen) { + this.debugBar.classList.remove('hidden'); + document.body.classList.add('debug-open'); + } else { + this.debugBar.classList.add('hidden'); + document.body.classList.remove('debug-open'); + } + } + } + + open() { + this.isOpen = true; + if (this.debugBar) { + this.debugBar.classList.remove('hidden'); + document.body.classList.add('debug-open'); + } + } + + close() { + this.isOpen = false; + if (this.debugBar) { + this.debugBar.classList.add('hidden'); + document.body.classList.remove('debug-open'); + } + } + + showHint() { + // Show toggle hint for 3 seconds + const hint = document.createElement('div'); + hint.className = 'debug-toggle-hint'; + hint.textContent = 'Press Ctrl+` to toggle debug'; + document.body.appendChild(hint); + + setTimeout(() => { + hint.remove(); + }, 3000); + } + + log(message, type = 'info') { + const status = document.getElementById('status'); + if (status) { + status.textContent = message; + status.className = 'status ' + type; + } + console.log(`[Ritz] ${message}`); + } +} + +// Initialize debug controller +const ritzDebug = new RitzDebug(); + +// Export +if (typeof module !== 'undefined' && module.exports) { + module.exports = RitzDebug; +} else { + window.RitzDebug = RitzDebug; + window.ritzDebug = ritzDebug; +} diff --git a/examples/ritz-runtime.js b/examples/ritz-runtime.js new file mode 100644 index 0000000..12299ce --- /dev/null +++ b/examples/ritz-runtime.js @@ -0,0 +1,267 @@ +/** + * Ritz Runtime - WebAssembly DOM Bridge + * + * Provides the runtime environment for Ritz components compiled to WASM. + */ + +class RitzRuntime { + constructor(rootElement) { + this.rootElement = rootElement || document.getElementById('app'); + this.nodes = new Map(); + this.nextNodeId = 1; + this.log = []; + this.wasmInstance = null; + this.debugEnabled = true; + } + + // ===== DOM Manipulation Bridge ===== + + js_create_element(tagPtr) { + const tag = this.readString(tagPtr); + const element = document.createElement(tag); + const id = this.nextNodeId++; + this.nodes.set(id, element); + this._log(`createElement("${tag}") = ${id}`); + return id; + } + + js_create_text(textPtr) { + const text = this.readString(textPtr); + const textNode = document.createTextNode(text); + const id = this.nextNodeId++; + this.nodes.set(id, textNode); + this._log(`createTextNode("${text}") = ${id}`); + return id; + } + + js_append_child(parentId, childId) { + const parent = this.nodes.get(parentId); + const child = this.nodes.get(childId); + if (parent && child) { + parent.appendChild(child); + this._log(`appendChild(${parentId}, ${childId})`); + } + } + + js_set_attribute(elemId, namePtr, valuePtr) { + const element = this.nodes.get(elemId); + const name = this.readString(namePtr); + const value = this.readString(valuePtr); + if (element) { + if (name === 'class') { + element.className = value; + } else { + element.setAttribute(name, value); + } + this._log(`setAttribute(${elemId}, "${name}", "${value}")`); + } + } + + js_bind_event(elemId, eventNamePtr, handlerNamePtr) { + const element = this.nodes.get(elemId); + const eventName = this.readString(eventNamePtr); + const handlerName = this.readString(handlerNamePtr); + + if (element && this.wasmInstance) { + const handler = this.wasmInstance.exports[handlerName]; + if (handler) { + element.addEventListener(eventName, () => { + this._log(`[Event] ${eventName} → ${handlerName}()`); + handler(); + this.updateSignals(); + }); + this._log(`bindEvent(${elemId}, "${eventName}", "${handlerName}")`); + } else { + console.error(`Handler ${handlerName} not found in WASM exports`); + } + } + } + + js_set_root(elemId) { + const element = this.nodes.get(elemId); + if (element) { + this.rootElement.innerHTML = ''; + this.rootElement.appendChild(element); + this._log(`setRoot(${elemId})`); + } + } + + js_update_text_content(nodeId, textPtr) { + const node = this.nodes.get(nodeId); + const text = this.readString(textPtr); + if (node) { + node.textContent = text; + this._log(`updateTextContent(${nodeId}, "${text}")`); + } + } + + // ===== Signal Management ===== + + updateSignals() { + // Discover and update all signals + if (!this.wasmInstance) return; + + const exports = this.wasmInstance.exports; + + // Find all get_* functions (signal getters) + for (const [name, fn] of Object.entries(exports)) { + if (name.startsWith('get_') && typeof fn === 'function') { + const signalName = name.substring(4); // Remove 'get_' prefix + const value = fn(); + this._log(`[Signal] ${signalName} = ${value}`); + + // Update DOM elements bound to this signal + // TODO: Track signal-to-DOM bindings during render for efficiency + this.updateSignalDisplay(signalName, value); + } + } + } + + updateSignalDisplay(signalName, value) { + // Find DOM elements that should display this signal + // For now, update any element with class matching signal name or data-signal attribute + const selectors = [ + `[data-signal="${signalName}"]`, + `.${signalName}`, + '.count', // Hardcoded for demo - TODO: track bindings properly + ]; + + selectors.forEach(selector => { + const elements = document.querySelectorAll(selector); + elements.forEach(el => { + // Check if text contains the signal name or looks like a counter + const text = el.textContent; + if (text.includes(signalName) || text.match(/\d+/)) { + // Replace the number in the text + el.textContent = text.replace(/\d+/, value); + } + }); + }); + } + + // ===== Memory & String Utilities ===== + + readString(ptr) { + if (!this.memory) return ''; + const bytes = new Uint8Array(this.memory.buffer, ptr); + let length = 0; + while (bytes[length] !== 0) length++; + return new TextDecoder().decode(bytes.slice(0, length)); + } + + setMemory(memory) { + this.memory = memory; + } + + setWasmInstance(instance) { + this.wasmInstance = instance; + } + + // ===== Logging & Debug ===== + + _log(message) { + if (this.debugEnabled) { + this.log.push(message); + } + } + + getLog() { + return this.log; + } + + clearLog() { + this.log = []; + } +} + + +/** + * RitzLoader - Simplified WASM loading + */ +class RitzLoader { + constructor(options = {}) { + this.rootElement = options.rootElement || document.getElementById('app'); + this.statusElement = options.statusElement || document.getElementById('status'); + this.onLoad = options.onLoad || (() => {}); + this.onError = options.onError || ((err) => console.error(err)); + } + + async load(wasmPath, componentName) { + try { + this.setStatus('⏳ Creating runtime...'); + const runtime = new RitzRuntime(this.rootElement); + + this.setStatus(`⏳ Loading ${wasmPath}...`); + const response = await fetch(wasmPath); + if (!response.ok) { + throw new Error(`Failed to load ${wasmPath}: ${response.status}`); + } + + const wasmBytes = await response.arrayBuffer(); + + this.setStatus('⏳ Instantiating WASM...'); + + // Create imports for WASM + const imports = { + env: { + js_create_element: (ptr) => runtime.js_create_element(ptr), + js_create_text: (ptr) => runtime.js_create_text(ptr), + js_append_child: (p, c) => runtime.js_append_child(p, c), + js_set_attribute: (e, n, v) => runtime.js_set_attribute(e, n, v), + js_set_root: (e) => runtime.js_set_root(e), + js_bind_event: (e, n, h) => runtime.js_bind_event(e, n, h), + js_update_text_content: (n, t) => runtime.js_update_text_content(n, t), + } + }; + + const { instance } = await WebAssembly.instantiate(wasmBytes, imports); + + // Setup runtime + runtime.setMemory(instance.exports.memory); + runtime.setWasmInstance(instance); + + this.setStatus(`⏳ Rendering ${componentName}...`); + + // Call the render function + const renderFn = instance.exports[`render_${componentName}`]; + if (!renderFn) { + throw new Error(`render_${componentName} not found in WASM exports`); + } + renderFn(); + + // Update signals + runtime.updateSignals(); + + this.setStatus(`✅ ${componentName} loaded!`, 'success'); + + // Make runtime globally available for debugging + window.ritzRuntime = runtime; + window.ritzInstance = instance; + + this.onLoad(runtime, instance); + + return { runtime, instance }; + + } catch (error) { + this.setStatus(`❌ Error: ${error.message}`, 'error'); + this.onError(error); + throw error; + } + } + + setStatus(message, type = '') { + if (this.statusElement) { + this.statusElement.textContent = message; + this.statusElement.className = 'status ' + type; + } + } +} + + +// Export for use in modules or global scope +if (typeof module !== 'undefined' && module.exports) { + module.exports = { RitzRuntime, RitzLoader }; +} else { + window.RitzRuntime = RitzRuntime; + window.RitzLoader = RitzLoader; +} diff --git a/examples/template.html b/examples/template.html new file mode 100644 index 0000000..b0c9b9a --- /dev/null +++ b/examples/template.html @@ -0,0 +1,47 @@ + + + + + + {{TITLE}} - Ritz Framework + + + + +
+ + + + + + + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..352fc6d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,89 @@ +[project] +name = "ritz" +version = "0.1.0" +description = "A WebAssembly UI framework with Python-like syntax" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [] + +[project.optional-dependencies] +dev = [ + "pytest>=8.3.0", + "pytest-cov>=6.0.0", + "pytest-watch>=4.2.0", + "black>=24.10.0", + "ruff>=0.8.0", + "mypy>=1.13.0", + "playwright>=1.48.0", + "pytest-playwright>=0.6.0", +] + +[build-system] +requires = ["setuptools>=75.0.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 100 +target-version = ['py311'] +include = '\.pyi?$' +extend-exclude = ''' +/( + # directories + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | build + | dist +)/ +''' + +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "C901", # too complex +] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] + +[tool.pytest.ini_options] +minversion = "8.0" +addopts = "-ra -q --strict-markers" +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] + +[tool.mypy] +python_version = "3.11" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_no_return = true +strict_equality = true + +[[tool.mypy.overrides]] +module = "tests.*" +disallow_untyped_defs = false diff --git a/ritz/__init__.py b/ritz/__init__.py new file mode 100644 index 0000000..e4d3fe6 --- /dev/null +++ b/ritz/__init__.py @@ -0,0 +1,3 @@ +"""Ritz Framework - Python-syntax WebAssembly UI framework.""" + +__version__ = "0.1.0" diff --git a/ritz/__pycache__/__init__.cpython-313.pyc b/ritz/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..52d3424 Binary files /dev/null and b/ritz/__pycache__/__init__.cpython-313.pyc differ diff --git a/ritz/__pycache__/build.cpython-313.pyc b/ritz/__pycache__/build.cpython-313.pyc new file mode 100644 index 0000000..81266c3 Binary files /dev/null and b/ritz/__pycache__/build.cpython-313.pyc differ diff --git a/ritz/__pycache__/codegen.cpython-313.pyc b/ritz/__pycache__/codegen.cpython-313.pyc new file mode 100644 index 0000000..5df4c65 Binary files /dev/null and b/ritz/__pycache__/codegen.cpython-313.pyc differ diff --git a/ritz/__pycache__/llvm_codegen.cpython-313.pyc b/ritz/__pycache__/llvm_codegen.cpython-313.pyc new file mode 100644 index 0000000..6ae912a Binary files /dev/null and b/ritz/__pycache__/llvm_codegen.cpython-313.pyc differ diff --git a/ritz/__pycache__/llvm_codegen_signals.cpython-313.pyc b/ritz/__pycache__/llvm_codegen_signals.cpython-313.pyc new file mode 100644 index 0000000..2c41297 Binary files /dev/null and b/ritz/__pycache__/llvm_codegen_signals.cpython-313.pyc differ diff --git a/ritz/__pycache__/parser.cpython-313.pyc b/ritz/__pycache__/parser.cpython-313.pyc new file mode 100644 index 0000000..c6fbe5a Binary files /dev/null and b/ritz/__pycache__/parser.cpython-313.pyc differ diff --git a/ritz/build.py b/ritz/build.py new file mode 100644 index 0000000..752e46f --- /dev/null +++ b/ritz/build.py @@ -0,0 +1,137 @@ +"""Build script for compiling Python components to WASM.""" + +import argparse +import subprocess +import sys +from pathlib import Path + +from ritz.llvm_codegen_signals import generate_llvm_ir +from ritz.parser import parse_component + + +def build_component(component_file: Path, output_file: Path, debug: bool = False) -> None: + """ + Build a Python component to WASM. + + Args: + component_file: Path to Python component file + output_file: Path for output WASM file + debug: Whether to include debug symbols + """ + print(f"[Ritz Build] Compiling {component_file} → {output_file}") + + # Step 1: Read component source + print(" [1/4] Reading component...") + source = component_file.read_text() + + # Step 2: Parse to IR + print(" [2/4] Parsing Python to IR...") + ir = parse_component(source) + print(f" Component: {ir['name']}") + + # Step 3: Generate LLVM IR + print(" [3/4] Generating LLVM IR...") + llvm_ir = generate_llvm_ir(ir) + + # Save LLVM IR for inspection + ll_file = output_file.with_suffix(".ll") + ll_file.write_text(llvm_ir) + print(f" Saved LLVM IR: {ll_file}") + + # Step 4: Compile to WASM with clang + print(" [4/4] Compiling to WASM...") + compile_to_wasm(ll_file, output_file, debug=debug) + + print(f"✅ Build complete: {output_file}") + print(f" Size: {output_file.stat().st_size} bytes") + + +def compile_to_wasm(ll_file: Path, output_file: Path, debug: bool = False) -> None: + """ + Compile LLVM IR to WASM using clang. + + Args: + ll_file: Path to LLVM IR source file + output_file: Path for output WASM file + debug: Whether to include debug symbols (-g) + """ + # Check if clang is available + try: + subprocess.run( + ["clang", "--version"], + check=True, + capture_output=True, + text=True, + ) + except (subprocess.CalledProcessError, FileNotFoundError): + print("ERROR: clang not found. Install LLVM/clang") + print(" Ubuntu/Debian: apt install clang lld") + print(" macOS: brew install llvm") + sys.exit(1) + + # Build clang command + cmd = [ + "clang", + "--target=wasm32", + "-nostdlib", # No standard library + "-Wl,--no-entry", # No _start entry point + "-Wl,--export-all", # Export all functions + "-Wl,--allow-undefined", # Allow undefined JS imports + "-O2", # Optimization + "-o", + str(output_file), + str(ll_file), + ] + + # Add debug symbols if requested + if debug: + cmd.extend(["-g", "-Wl,--export-dynamic"]) + print(" Debug symbols: enabled (DWARF in WASM)") + + # Run clang + try: + result = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + ) + if result.stderr: + # Filter out common warnings + stderr = result.stderr + if "warning" in stderr.lower() and len(stderr) < 500: + print(f" Warnings: {stderr}") + except subprocess.CalledProcessError as e: + print("ERROR: Compilation failed") + print(f" Command: {' '.join(cmd)}") + print(f" stdout: {e.stdout}") + print(f" stderr: {e.stderr}") + sys.exit(1) + + +def main() -> None: + """CLI entry point.""" + parser = argparse.ArgumentParser(description="Build Ritz components to WASM") + parser.add_argument("component", type=Path, help="Python component file") + parser.add_argument( + "-o", "--output", type=Path, help="Output WASM file (default: component.wasm)" + ) + parser.add_argument("-g", "--debug", action="store_true", help="Include debug symbols") + + args = parser.parse_args() + + # Determine output file + if args.output: + output_file = args.output + else: + output_file = args.component.with_suffix(".wasm") + + # Ensure output directory exists + output_file.parent.mkdir(parents=True, exist_ok=True) + + # Build the component + build_component(args.component, output_file, debug=args.debug) + + +if __name__ == "__main__": + main() diff --git a/ritz/codegen.py b/ritz/codegen.py new file mode 100644 index 0000000..afee1a2 --- /dev/null +++ b/ritz/codegen.py @@ -0,0 +1,156 @@ +"""C code generator for Ritz IR.""" + +from typing import Any + + +def generate_c_code(ir: dict[str, Any]) -> str: + """ + Generate C code from Ritz IR. + + Args: + ir: Component IR dictionary + + Returns: + C source code that can be compiled with emscripten + """ + generator = CCodeGenerator(ir) + return generator.generate() + + +class CCodeGenerator: + """Generates C code from Ritz IR.""" + + def __init__(self, ir: dict[str, Any]): + self.ir = ir + self.indent_level = 0 + self.lines: list[str] = [] + self.next_var_id = 0 + + def generate(self) -> str: + """Generate complete C source code.""" + self._emit_header() + self._emit_js_imports() + self._emit_component(self.ir) + return "\n".join(self.lines) + + def _emit_header(self) -> None: + """Emit C header includes.""" + self.lines.append("#include ") + self.lines.append("#include ") + self.lines.append("") + + def _emit_js_imports(self) -> None: + """Emit JavaScript function imports.""" + self.lines.append("// JavaScript DOM manipulation functions (imported)") + self.lines.append("EM_JS(int, js_create_element, (const char* tag), {") + self.lines.append(" return Module.ritz.createElement(UTF8ToString(tag));") + self.lines.append("});") + self.lines.append("") + self.lines.append("EM_JS(int, js_create_text, (const char* text), {") + self.lines.append(" return Module.ritz.createTextNode(UTF8ToString(text));") + self.lines.append("});") + self.lines.append("") + self.lines.append("EM_JS(void, js_append_child, (int parent, int child), {") + self.lines.append(" Module.ritz.appendChild(parent, child);") + self.lines.append("});") + self.lines.append("") + self.lines.append( + "EM_JS(void, js_set_attribute, (int elem, const char* name, const char* value), {" + ) + self.lines.append( + " Module.ritz.setAttribute(elem, UTF8ToString(name), UTF8ToString(value));" + ) + self.lines.append("});") + self.lines.append("") + self.lines.append("EM_JS(void, js_set_root, (int elem), {") + self.lines.append(" Module.ritz.setRoot(elem);") + self.lines.append("});") + self.lines.append("") + + def _emit_component(self, ir: dict[str, Any]) -> None: + """Emit a component function.""" + component_name = ir["name"] + self.lines.append(f"// Component: {component_name}") + self.lines.append("EMSCRIPTEN_KEEPALIVE") + self.lines.append(f"void render_{component_name}() {{") + self.indent_level += 1 + + # Process component body + root_var = None + for stmt in ir["body"]: + var = self._emit_statement(stmt) + if root_var is None and var: + root_var = var + + # Set the root element + if root_var: + self._emit_line(f"js_set_root({root_var});") + + self.indent_level -= 1 + self.lines.append("}") + self.lines.append("") + + def _emit_statement(self, stmt: dict[str, Any]) -> str | None: + """ + Emit a statement and return the variable name if it creates an element. + + Returns: + Variable name of created element, or None + """ + if stmt["type"] == "with": + return self._emit_with(stmt) + elif stmt["type"] == "yield": + return self._emit_yield(stmt) + return None + + def _emit_with(self, stmt: dict[str, Any]) -> str: + """Emit a 'with Element()' block.""" + element_info = stmt["element"] + tag = element_info["tag"].lower() + props = element_info["props"] + + # Create the element + elem_var = self._new_var("elem") + self._emit_line(f'int {elem_var} = js_create_element("{tag}");') + + # Set attributes + for prop_name, prop_value in props.items(): + # Convert class_ to class + attr_name = prop_name.rstrip("_") + if prop_value["type"] == "constant": + value = prop_value["value"] + self._emit_line(f'js_set_attribute({elem_var}, "{attr_name}", "{value}");') + + # Process children + for child_stmt in stmt["body"]: + child_var = self._emit_statement(child_stmt) + if child_var: + self._emit_line(f"js_append_child({elem_var}, {child_var});") + + return elem_var + + def _emit_yield(self, stmt: dict[str, Any]) -> str | None: + """Emit a yield statement.""" + value = stmt.get("value") + if not value: + return None + + if value["type"] == "constant": + # Create text node + text_var = self._new_var("text") + text = value["value"] + self._emit_line(f'int {text_var} = js_create_text("{text}");') + return text_var + + return None + + def _emit_line(self, code: str) -> None: + """Emit a line of code with proper indentation.""" + indent = " " * self.indent_level + self.lines.append(f"{indent}{code}") + + def _new_var(self, prefix: str = "var") -> str: + """Generate a new variable name.""" + var_name = f"{prefix}{self.next_var_id}" + self.next_var_id += 1 + return var_name diff --git a/ritz/llvm_codegen.py b/ritz/llvm_codegen.py new file mode 100644 index 0000000..f32a297 --- /dev/null +++ b/ritz/llvm_codegen.py @@ -0,0 +1,166 @@ +"""LLVM IR code generator for Ritz components.""" + +from typing import Any + + +def generate_llvm_ir(ir: dict[str, Any]) -> str: + """ + Generate LLVM IR from Ritz IR. + + Args: + ir: Component IR dictionary + + Returns: + LLVM IR source code + """ + generator = LLVMIRGenerator(ir) + return generator.generate() + + +class LLVMIRGenerator: + """Generates LLVM IR from Ritz IR.""" + + def __init__(self, ir: dict[str, Any]): + self.ir = ir + self.lines: list[str] = [] + self.strings: dict[str, str] = {} # value -> global name + self.next_string_id = 0 + self.next_var_id = 0 + + def generate(self) -> str: + """Generate complete LLVM IR module.""" + self._emit_header() + self._emit_string_constants() + self._emit_function_declarations() + self._emit_component(self.ir) + return "\n".join(self.lines) + + def _emit_header(self) -> None: + """Emit module header.""" + self.lines.append("; Generated by Ritz Framework") + self.lines.append("; Component: " + self.ir["name"]) + self.lines.append("") + + def _emit_string_constants(self) -> None: + """Emit string constant declarations (filled during code generation).""" + # We'll add strings as we encounter them, then prepend later + pass + + def _emit_function_declarations(self) -> None: + """Emit JS bridge function declarations.""" + self.lines.append("; JavaScript bridge function declarations") + self.lines.append("declare i32 @js_create_element(i8*)") + self.lines.append("declare i32 @js_create_text(i8*)") + self.lines.append("declare void @js_append_child(i32, i32)") + self.lines.append("declare void @js_set_attribute(i32, i8*, i8*)") + self.lines.append("declare void @js_set_root(i32)") + self.lines.append("") + + def _emit_component(self, ir: dict[str, Any]) -> None: + """Emit component function.""" + component_name = ir["name"] + self.lines.append(f"; Component function: {component_name}") + self.lines.append(f"define void @render_{component_name}() {{") + self.lines.append("entry:") + + # Process component body + root_var = None + for stmt in ir["body"]: + var = self._emit_statement(stmt) + if root_var is None and var: + root_var = var + + # Set root element + if root_var: + self.lines.append(f" call void @js_set_root(i32 %{root_var})") + + self.lines.append(" ret void") + self.lines.append("}") + self.lines.append("") + + def _emit_statement(self, stmt: dict[str, Any]) -> str | None: + """Emit a statement, return variable name if it creates an element.""" + if stmt["type"] == "with": + return self._emit_with(stmt) + elif stmt["type"] == "yield": + return self._emit_yield(stmt) + return None + + def _emit_with(self, stmt: dict[str, Any]) -> str: + """Emit a 'with Element()' block.""" + element_info = stmt["element"] + tag = element_info["tag"].lower() + props = element_info["props"] + + # Create element + elem_var = self._new_var() + tag_str = self._get_or_create_string(tag) + self.lines.append( + f" %{elem_var} = call i32 @js_create_element(i8* getelementptr inbounds ({self._string_type(tag)}, {self._string_type(tag)}* {tag_str}, i32 0, i32 0))" + ) + + # Set attributes + for prop_name, prop_value in props.items(): + attr_name = prop_name.rstrip("_") # class_ -> class + if prop_value["type"] == "constant": + value = str(prop_value["value"]) + name_str = self._get_or_create_string(attr_name) + value_str = self._get_or_create_string(value) + + 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* getelementptr inbounds ({self._string_type(value)}, {self._string_type(value)}* {value_str}, i32 0, i32 0))" + ) + + # Process children + for child_stmt in stmt["body"]: + child_var = self._emit_statement(child_stmt) + if child_var: + self.lines.append( + f" call void @js_append_child(i32 %{elem_var}, i32 %{child_var})" + ) + + return elem_var + + def _emit_yield(self, stmt: dict[str, Any]) -> str | None: + """Emit a yield statement.""" + value = stmt.get("value") + if not value or value["type"] != "constant": + return None + + # Create text node + text = str(value["value"]) + text_str = self._get_or_create_string(text) + text_var = self._new_var() + + self.lines.append( + f" %{text_var} = call i32 @js_create_text(i8* getelementptr inbounds ({self._string_type(text)}, {self._string_type(text)}* {text_str}, i32 0, i32 0))" + ) + + return text_var + + def _get_or_create_string(self, value: str) -> str: + """Get or create a string constant, return its global name.""" + if value in self.strings: + return self.strings[value] + + # Create new string constant + global_name = f"@.str.{self.next_string_id}" + self.next_string_id += 1 + self.strings[value] = global_name + + # Add to beginning of output (will prepend later) + return global_name + + def _string_type(self, value: str) -> str: + """Get LLVM type for a string constant.""" + # +1 for null terminator + length = len(value) + 1 + return f"[{length} x i8]" + + def _new_var(self) -> str: + """Generate a new SSA variable name.""" + var_name = f"var{self.next_var_id}" + self.next_var_id += 1 + return var_name diff --git a/ritz/llvm_codegen_signals.py b/ritz/llvm_codegen_signals.py new file mode 100644 index 0000000..35e7451 --- /dev/null +++ b/ritz/llvm_codegen_signals.py @@ -0,0 +1,341 @@ +"""LLVM IR code generator with Signal support for Ritz components.""" + +from typing import Any + + +def generate_llvm_ir(ir: dict[str, Any]) -> str: + """ + Generate LLVM IR from Ritz IR with Signal support. + + Args: + ir: Component IR dictionary + + Returns: + LLVM IR source code with signals stored in WASM memory + """ + generator = SignalLLVMIRGenerator(ir) + return generator.generate() + + +class SignalLLVMIRGenerator: + """Generates LLVM IR with reactive Signal support.""" + + def __init__(self, ir: dict[str, Any]): + self.ir = ir + self.lines: list[str] = [] + self.strings: dict[str, str] = {} # value -> global name + self.signals: dict[str, dict[str, Any]] = {} # signal_name -> {initial, index} + self.event_handlers: list[dict[str, Any]] = [] # List of event handler functions + self.next_string_id = 0 + self.next_var_id = 0 + self.next_signal_id = 0 + + # Track signal-to-DOM bindings for updates + self.signal_dom_bindings: dict[str, list[str]] = {} # signal_name -> [dom_node_ids] + + def generate(self) -> str: + """Generate complete LLVM IR module with signals.""" + # First pass: collect signals and handlers + self._collect_signals_and_handlers() + + # Generate IR + self._emit_header() + self._emit_signal_globals() + self._emit_function_declarations() + self._emit_helper_functions() + self._emit_event_handlers() + self._emit_component(self.ir) + + # Prepend string constants + return self._finalize_with_strings() + + def _collect_signals_and_handlers(self) -> None: + """Collect all signals and event handlers from IR.""" + for stmt in self.ir["body"]: + if stmt["type"] == "signal": + signal_id = self.next_signal_id + self.next_signal_id += 1 + self.signals[stmt["name"]] = { + "initial": stmt["initial"], + "id": signal_id, + } + elif stmt["type"] == "function": + self.event_handlers.append(stmt) + + def _emit_header(self) -> None: + """Emit module header.""" + self.lines.append("; Generated by Ritz Framework with Signals") + self.lines.append(f'; Component: {self.ir["name"]}') + self.lines.append("") + + def _emit_signal_globals(self) -> None: + """Emit global variables for signals.""" + if not self.signals: + return + + self.lines.append("; Signal storage (reactive state in WASM memory)") + for signal_name, signal_info in self.signals.items(): + # Each signal is a global i32 + initial_value = signal_info["initial"].get("value", 0) + self.lines.append(f"@signal_{signal_name} = global i32 {initial_value}") + self.lines.append("") + + def _emit_function_declarations(self) -> None: + """Emit JS bridge function declarations.""" + self.lines.append("; JavaScript bridge function declarations") + self.lines.append("declare i32 @js_create_element(i8*)") + self.lines.append("declare i32 @js_create_text(i8*)") + self.lines.append("declare void @js_append_child(i32, i32)") + self.lines.append("declare void @js_set_attribute(i32, i8*, i8*)") + self.lines.append("declare void @js_set_root(i32)") + self.lines.append( + "declare void @js_bind_event(i32, i8*, i8*)" + ) # elem, event_name, handler_name + self.lines.append("declare void @js_update_text_content(i32, i8*)") # Update DOM node text + self.lines.append("") + + def _emit_helper_functions(self) -> None: + """Emit helper functions for signal operations.""" + if not self.signals: + return + + self.lines.append("; Signal helper functions") + + # Emit getter for each signal (exported for JS to call) + for signal_name in self.signals: + self.lines.append(f"define i32 @get_{signal_name}() {{") + self.lines.append("entry:") + self.lines.append(f" %val = load i32, i32* @signal_{signal_name}") + self.lines.append(" ret i32 %val") + self.lines.append("}") + self.lines.append("") + + # Emit setter for each signal (exported for JS to call) + for signal_name in self.signals: + self.lines.append(f"define void @set_{signal_name}(i32 %newval) {{") + self.lines.append("entry:") + self.lines.append(f" store i32 %newval, i32* @signal_{signal_name}") + self.lines.append(" ; TODO: Trigger DOM update callback") + self.lines.append(" ret void") + self.lines.append("}") + self.lines.append("") + + def _emit_event_handlers(self) -> None: + """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}() {{") + self.lines.append("entry:") + + # Emit handler body + for stmt in handler["body"]: + self._emit_handler_statement(stmt) + + self.lines.append(" ret void") + self.lines.append("}") + self.lines.append("") + + def _emit_handler_statement(self, stmt: dict[str, Any]) -> None: + """Emit a statement inside an event handler.""" + if stmt["type"] == "augassign": + # Handle signal updates (count += 1) + signal_name = stmt["target"] + if signal_name in self.signals: + # Load current value + load_var = self._new_var() + self.lines.append(f" %{load_var} = load i32, i32* @signal_{signal_name}") + + # Perform operation + result_var = self._new_var() + if stmt["op"] == "Add": + value = stmt["value"]["value"] + self.lines.append(f" %{result_var} = add i32 %{load_var}, {value}") + + # Store back + self.lines.append(f" store i32 %{result_var}, i32* @signal_{signal_name}") + + # TODO: Trigger DOM updates for this signal + self.lines.append(f" ; TODO: Update DOM nodes bound to {signal_name}") + + def _emit_component(self, ir: dict[str, Any]) -> None: + """Emit component render function.""" + component_name = ir["name"] + self.lines.append("; Component render function") + self.lines.append(f"define void @render_{component_name}() {{") + self.lines.append("entry:") + + # Process component body (skip signals and functions, already handled) + root_var = None + for stmt in ir["body"]: + if stmt["type"] in ["signal", "function"]: + continue # Already processed + var = self._emit_statement(stmt) + if root_var is None and var: + root_var = var + + # Set root element + if root_var: + self.lines.append(f" call void @js_set_root(i32 %{root_var})") + + self.lines.append(" ret void") + self.lines.append("}") + self.lines.append("") + + def _emit_statement(self, stmt: dict[str, Any]) -> str | None: + """Emit a statement, return variable name if it creates an element.""" + if stmt["type"] == "with": + return self._emit_with(stmt) + elif stmt["type"] == "yield": + return self._emit_yield(stmt) + return None + + def _emit_with(self, stmt: dict[str, Any]) -> str: + """Emit a 'with Element()' block.""" + element_info = stmt["element"] + tag = element_info["tag"].lower() + props = element_info["props"] + + # Create element + elem_var = self._new_var() + tag_str = self._get_or_create_string(tag) + self.lines.append( + f" %{elem_var} = call i32 @js_create_element(i8* getelementptr inbounds ({self._string_type(tag)}, {self._string_type(tag)}* {tag_str}, i32 0, i32 0))" + ) + + # Handle onclick prop specially + onclick_handler = None + for prop_name, prop_value in props.items(): + if prop_name == "onclick": + # Bind event handler + if prop_value["type"] == "name": + onclick_handler = prop_value["id"] + else: + # Regular attribute + attr_name = prop_name.rstrip("_") + if prop_value["type"] == "constant": + value = str(prop_value["value"]) + name_str = self._get_or_create_string(attr_name) + value_str = self._get_or_create_string(value) + 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* getelementptr inbounds ({self._string_type(value)}, {self._string_type(value)}* {value_str}, i32 0, i32 0))" + ) + + # Bind onclick if present + if onclick_handler: + event_str = self._get_or_create_string("click") + handler_str = self._get_or_create_string(onclick_handler) + self.lines.append( + f" call void @js_bind_event(i32 %{elem_var}, " + f"i8* getelementptr inbounds ({self._string_type('click')}, {self._string_type('click')}* {event_str}, i32 0, i32 0), " + f"i8* getelementptr inbounds ({self._string_type(onclick_handler)}, {self._string_type(onclick_handler)}* {handler_str}, i32 0, i32 0))" + ) + + # Process children + for child_stmt in stmt["body"]: + child_var = self._emit_statement(child_stmt) + if child_var: + self.lines.append( + f" call void @js_append_child(i32 %{elem_var}, i32 %{child_var})" + ) + + return elem_var + + def _emit_yield(self, stmt: dict[str, Any]) -> str | None: + """Emit a yield statement.""" + value = stmt.get("value") + if not value: + return None + + if value["type"] == "constant": + # Simple text node + text = str(value["value"]) + text_str = self._get_or_create_string(text) + text_var = self._new_var() + self.lines.append( + f" %{text_var} = call i32 @js_create_text(i8* getelementptr inbounds ({self._string_type(text)}, {self._string_type(text)}* {text_str}, i32 0, i32 0))" + ) + return text_var + + elif value["type"] == "fstring": + # Handle f-strings with signal references + # For now, create text with signal value + # TODO: Implement proper string concatenation + parts = value["parts"] + + # Find signal references + for part in parts: + if part["type"] == "formatted_value": + if part["value"]["type"] == "name": + signal_name = part["value"]["id"] + if signal_name in self.signals: + # Load signal value and create text node + # This is simplified - real impl would concat strings + text_var = self._new_var() + self.lines.append( + f" ; TODO: Format f-string with signal {signal_name}" + ) + # For now, just create placeholder text + placeholder = self._get_or_create_string("0") + self.lines.append( + f" %{text_var} = call i32 @js_create_text(i8* getelementptr inbounds ([2 x i8], [2 x i8]* {placeholder}, i32 0, i32 0))" + ) + return text_var + + # Fallback: create empty text + empty_str = self._get_or_create_string("") + text_var = self._new_var() + self.lines.append( + f" %{text_var} = call i32 @js_create_text(i8* getelementptr inbounds ({self._string_type('')}, {self._string_type('')}* {empty_str}, i32 0, i32 0))" + ) + return text_var + + return None + + def _get_or_create_string(self, value: str) -> str: + """Get or create a string constant.""" + if value in self.strings: + return self.strings[value] + global_name = f"@.str.{self.next_string_id}" + self.next_string_id += 1 + self.strings[value] = global_name + return global_name + + def _string_type(self, value: str) -> str: + """Get LLVM type for a string (account for UTF-8 byte length).""" + # Get actual byte length of UTF-8 encoded string + byte_length = len(value.encode("utf-8")) + 1 # +1 for null terminator + return f"[{byte_length} x i8]" + + def _new_var(self) -> str: + """Generate a new SSA variable name.""" + var_name = f"var{self.next_var_id}" + self.next_var_id += 1 + return var_name + + def _finalize_with_strings(self) -> str: + """Prepend string constants and return final IR.""" + string_lines = ["; String constants"] + for value, global_name in self.strings.items(): + escaped = value.replace("\\", "\\\\").replace('"', '\\"') + str_type = self._string_type(value) + string_lines.append( + f'{global_name} = private unnamed_addr constant {str_type} c"{escaped}\\00"' + ) + string_lines.append("") + + # Find insertion point (after header, before declarations) + result_lines = [] + header_end = 0 + for i, line in enumerate(self.lines): + if "JavaScript bridge" in line or "Signal storage" in line: + header_end = i + break + + result_lines.extend(self.lines[:header_end]) + result_lines.extend(string_lines) + result_lines.extend(self.lines[header_end:]) + + return "\n".join(result_lines) diff --git a/ritz/parser.py b/ritz/parser.py new file mode 100644 index 0000000..b9a45ae --- /dev/null +++ b/ritz/parser.py @@ -0,0 +1,183 @@ +"""AST parser for Ritz components using Python's ast module.""" + +import ast +from typing import Any + + +def parse_component(source: str) -> dict[str, Any]: + """ + Parse a Python component into Ritz IR. + + Args: + source: Python source code containing a @component decorated function + + Returns: + IR dictionary representing the component structure + """ + tree = ast.parse(source) + + # Find the @component decorated function + for node in ast.walk(tree): + if isinstance(node, ast.FunctionDef) and _has_component_decorator(node): + return _transform_component(node) + + raise ValueError("No @component decorated function found") + + +def _has_component_decorator(node: ast.FunctionDef) -> bool: + """Check if a function has the @component decorator.""" + for decorator in node.decorator_list: + if isinstance(decorator, ast.Name) and decorator.id == "component": + return True + return False + + +def _transform_component(node: ast.FunctionDef) -> dict[str, Any]: + """Transform a component function into IR.""" + return { + "type": "component", + "name": node.name, + "args": [arg.arg for arg in node.args.args], + "body": [_transform_statement(stmt) for stmt in node.body], + } + + +def _transform_statement(stmt: ast.stmt) -> dict[str, Any]: + """Transform a statement into IR.""" + if isinstance(stmt, ast.With): + return _transform_with(stmt) + elif isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Yield): + return _transform_yield(stmt.value) + elif isinstance(stmt, ast.Assign): + return _transform_assign(stmt) + elif isinstance(stmt, ast.AugAssign): + return _transform_augassign(stmt) + elif isinstance(stmt, ast.FunctionDef): + return _transform_function(stmt) + else: + return {"type": "unknown", "ast_type": type(stmt).__name__} + + +def _transform_with(node: ast.With) -> dict[str, Any]: + """Transform a 'with Element():' statement.""" + # Get the context expression (the Element() call) + context_expr = node.items[0].context_expr + + return { + "type": "with", + "element": _transform_element_call(context_expr), + "body": [_transform_statement(stmt) for stmt in node.body], + } + + +def _transform_element_call(expr: ast.expr) -> dict[str, Any]: + """Transform an Element() call into IR.""" + if not isinstance(expr, ast.Call): + return {"tag": "Unknown", "props": {}} + + # Get the element tag name + tag = _get_name(expr.func) + + # Get props from keyword arguments + props = {} + for keyword in expr.keywords: + if keyword.arg: + props[keyword.arg] = _transform_expr(keyword.value) + + return {"tag": tag, "props": props} + + +def _transform_yield(node: ast.Yield) -> dict[str, Any]: + """Transform a yield statement.""" + return {"type": "yield", "value": _transform_expr(node.value) if node.value else None} + + +def _transform_assign(node: ast.Assign) -> dict[str, Any]: + """Transform an assignment statement (e.g., count = signal(0)).""" + target = node.targets[0] + if isinstance(target, ast.Name): + value = _transform_expr(node.value) + + # Check if it's a signal assignment + if ( + isinstance(value, dict) + and value.get("type") == "call" + and value.get("func") == "signal" + ): + return { + "type": "signal", + "name": target.id, + "initial": ( + value["args"][0] if value.get("args") else {"type": "constant", "value": None} + ), + } + + return {"type": "assign", "target": target.id, "value": value} + + return {"type": "assign", "target": "unknown", "value": _transform_expr(node.value)} + + +def _transform_augassign(node: ast.AugAssign) -> dict[str, Any]: + """Transform an augmented assignment (e.g., count += 1).""" + if isinstance(node.target, ast.Name): + return { + "type": "augassign", + "target": node.target.id, + "op": type(node.op).__name__, # Add, Sub, Mult, etc. + "value": _transform_expr(node.value), + } + return { + "type": "augassign", + "target": "unknown", + "op": type(node.op).__name__, + "value": _transform_expr(node.value), + } + + +def _transform_function(node: ast.FunctionDef) -> dict[str, Any]: + """Transform a function definition (e.g., event handlers).""" + return { + "type": "function", + "name": node.name, + "args": [arg.arg for arg in node.args.args], + "body": [_transform_statement(stmt) for stmt in node.body], + } + + +def _transform_expr(expr: ast.expr | None) -> dict[str, Any]: + """Transform an expression into IR.""" + if expr is None: + return {"type": "constant", "value": None} + + if isinstance(expr, ast.Constant): + return {"type": "constant", "value": expr.value} + elif isinstance(expr, ast.Name): + return {"type": "name", "id": expr.id} + elif isinstance(expr, ast.Call): + return { + "type": "call", + "func": _get_name(expr.func), + "args": [_transform_expr(arg) for arg in expr.args], + "kwargs": {kw.arg: _transform_expr(kw.value) for kw in expr.keywords if kw.arg}, + } + elif isinstance(expr, ast.Attribute): + return { + "type": "attribute", + "value": _transform_expr(expr.value), + "attr": expr.attr, + } + elif isinstance(expr, ast.JoinedStr): # f-strings + return {"type": "fstring", "parts": [_transform_expr(v) for v in expr.values]} + elif isinstance(expr, ast.FormattedValue): + return {"type": "formatted_value", "value": _transform_expr(expr.value)} + else: + return {"type": "unknown", "ast_type": type(expr).__name__} + + +def _get_name(node: ast.expr) -> str: + """Extract name from a Name or Attribute node.""" + if isinstance(node, ast.Name): + return node.id + elif isinstance(node, ast.Attribute): + return f"{_get_name(node.value)}.{node.attr}" + return "Unknown" diff --git a/ritz/syntax.py b/ritz/syntax.py new file mode 100644 index 0000000..627363d --- /dev/null +++ b/ritz/syntax.py @@ -0,0 +1,179 @@ +""" +Ritz syntax placeholders for writing components. + +These are runtime placeholders that allow components to be valid Python. +The compiler ignores imports and uses AST parsing instead. +""" + + +def component(func): + """Component decorator - marks a function as a Ritz component.""" + return func + + +def signal(initial_value): + """Create a reactive signal with an initial value.""" + pass + + +# HTML Elements - Context managers for building DOM +class _Element: + """Base element class.""" + + def __init__(self, *children, **props): + self.children = children + self.props = props + + def __enter__(self): + return self + + def __exit__(self, *args): + pass + + +# Standard HTML elements +class Div(_Element): + """
element.""" + + pass + + +class P(_Element): + """

element.""" + + pass + + +class Button(_Element): + """