522 lines
14 KiB
Markdown
522 lines
14 KiB
Markdown
# Ritz Framework Research & Architecture
|
|
|
|
## Core Insight: Python-Executable, WASM-Compilable
|
|
|
|
The breakthrough: Our framework uses **valid Python syntax** that can execute in a Python interpreter for debugging/prototyping, but compiles to WebAssembly for production. Developers write Python, debug in Python, ship as WASM.
|
|
|
|
## The Yield-Based Component Model
|
|
|
|
### Pure Python Implementation (for prototyping/debugging)
|
|
|
|
```python
|
|
# This is ACTUAL Python that runs in CPython!
|
|
from ritz import component, signal, Main, P, Label, Button
|
|
|
|
@component
|
|
def HighFiveApp():
|
|
high_fives = signal(0)
|
|
|
|
def increment():
|
|
high_fives.value += 1
|
|
|
|
with Main():
|
|
yield P('Give me a high 5 bro!')
|
|
yield Label(f'High fives: {high_fives}')
|
|
yield Button('Click me', onclick=increment)
|
|
|
|
# Can execute in Python for debugging:
|
|
# python app.py -> generates HTML/IR for verification
|
|
```
|
|
|
|
### Complex Example with Mixed Content
|
|
|
|
```python
|
|
@component
|
|
def BlogPost(title, author, content):
|
|
with Article(class_='blog-post'):
|
|
with Header():
|
|
yield H1(title)
|
|
yield 'By ' # Raw strings!
|
|
yield Span(author, class_='author')
|
|
yield ' on '
|
|
yield Time(content.date)
|
|
|
|
with Section():
|
|
yield from content.paragraphs # Composition via yield from
|
|
|
|
for comment in content.comments:
|
|
yield Comment(comment.author, comment.text)
|
|
```
|
|
|
|
### Why This Architecture Works
|
|
|
|
1. **Debugging**: Run components in Python interpreter to see generated HTML/IR
|
|
2. **Type Checking**: Use Python's type hints for compile-time validation
|
|
3. **Testing**: Standard Python test frameworks work
|
|
4. **Compilation**: Parser only needs to handle subset of Python, not full language
|
|
|
|
## Compilation Pipeline
|
|
|
|
### 1. Parser (Python → AST) - Using Python's `ast` Module!
|
|
|
|
**BREAKTHROUGH**: Since our components are valid Python, we can use Python's own `ast` module to parse them! No custom parser needed!
|
|
|
|
```python
|
|
import ast
|
|
import inspect
|
|
|
|
def parse_component(func):
|
|
"""Parse a component function using Python's AST"""
|
|
source = inspect.getsource(func)
|
|
tree = ast.parse(source)
|
|
|
|
# Walk the AST and transform to our IR
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.FunctionDef):
|
|
# Found our component function
|
|
if has_component_decorator(node):
|
|
return transform_to_ritz_ast(node)
|
|
|
|
def transform_to_ritz_ast(node):
|
|
"""Transform Python AST to Ritz IR"""
|
|
component = {
|
|
'type': 'component',
|
|
'name': node.name,
|
|
'body': []
|
|
}
|
|
|
|
for stmt in node.body:
|
|
if isinstance(stmt, ast.With):
|
|
# with Element(): ...
|
|
component['body'].append(process_with_statement(stmt))
|
|
elif isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Yield):
|
|
# yield Element()
|
|
component['body'].append(process_yield(stmt.value))
|
|
|
|
return component
|
|
```
|
|
|
|
This gives us:
|
|
- **Zero parsing overhead** - Python does it for us
|
|
- **Perfect Python compatibility** - It IS Python
|
|
- **Error messages for free** - Python's parser handles syntax errors
|
|
- **Type hints support** - Can extract from AST
|
|
|
|
### 2. LLVM IR Generation
|
|
|
|
```cpp
|
|
// codegen.cpp
|
|
class RitzCodeGen {
|
|
llvm::LLVMContext context;
|
|
llvm::IRBuilder<> builder;
|
|
llvm::Module* module;
|
|
|
|
// Compile @component function to WASM
|
|
llvm::Function* compileComponent(ASTNode* node) {
|
|
auto func = llvm::Function::Create(
|
|
llvm::FunctionType::get(builder.getVoidTy(), false),
|
|
llvm::Function::ExternalLinkage,
|
|
node->component.name,
|
|
module
|
|
);
|
|
|
|
auto entry = llvm::BasicBlock::Create(context, "entry", func);
|
|
builder.SetInsertPoint(entry);
|
|
|
|
// Process yields - each becomes DOM operations
|
|
for (auto stmt : node->component.body) {
|
|
if (stmt->type == AST_YIELD) {
|
|
compileYield(stmt);
|
|
}
|
|
}
|
|
|
|
builder.CreateRetVoid();
|
|
return func;
|
|
}
|
|
|
|
void compileYield(ASTNode* node) {
|
|
// yield P('text') compiles to:
|
|
// 1. Create P element
|
|
// 2. Create text node
|
|
// 3. Append text to P
|
|
// 4. Append P to current parent
|
|
|
|
auto create_p = builder.CreateCall(
|
|
module->getFunction("dom_create_element"),
|
|
builder.getInt32(TAG_P)
|
|
);
|
|
|
|
// ... rest of DOM operations
|
|
}
|
|
};
|
|
```
|
|
|
|
### 3. Reactive Runtime (in WASM)
|
|
|
|
```c
|
|
// runtime.c - Compiles to WASM
|
|
typedef struct {
|
|
int32_t value;
|
|
uint32_t subscribers[MAX_SUBSCRIBERS];
|
|
uint32_t subscriber_count;
|
|
uint32_t dom_node_id; // Direct DOM node reference
|
|
} Signal;
|
|
|
|
Signal* create_signal(int32_t initial) {
|
|
Signal* sig = (Signal*)wasm_alloc(sizeof(Signal));
|
|
sig->value = initial;
|
|
sig->subscriber_count = 0;
|
|
return sig;
|
|
}
|
|
|
|
void signal_set(Signal* sig, int32_t value) {
|
|
sig->value = value;
|
|
|
|
// Direct DOM update - no virtual DOM!
|
|
if (sig->dom_node_id) {
|
|
dom_ops[op_count++] = OP_UPDATE_TEXT;
|
|
dom_ops[op_count++] = sig->dom_node_id;
|
|
dom_ops[op_count++] = (uint32_t)int_to_string(value);
|
|
}
|
|
|
|
// Notify subscribers
|
|
for (int i = 0; i < sig->subscriber_count; i++) {
|
|
call_effect(sig->subscribers[i]);
|
|
}
|
|
}
|
|
```
|
|
|
|
## DOM Bridge Architecture
|
|
|
|
### WASM Side - Accumulate Operations
|
|
|
|
```c
|
|
// Opcodes for DOM operations
|
|
enum DOMOp {
|
|
OP_CREATE_ELEMENT = 0x01,
|
|
OP_CREATE_TEXT = 0x02,
|
|
OP_SET_ATTRIBUTE = 0x03,
|
|
OP_APPEND_CHILD = 0x04,
|
|
OP_UPDATE_TEXT = 0x05,
|
|
OP_ADD_EVENT = 0x06,
|
|
OP_REMOVE = 0x07,
|
|
OP_BATCH_END = 0xFF
|
|
};
|
|
|
|
// Global operation buffer
|
|
uint32_t dom_ops[MAX_OPS];
|
|
uint32_t op_count = 0;
|
|
|
|
// Batch operations then flush
|
|
void flush_dom_ops() {
|
|
// Call JS with pointer to operation buffer
|
|
js_flush_dom_ops(dom_ops, op_count);
|
|
op_count = 0;
|
|
}
|
|
```
|
|
|
|
### JavaScript Side - Execute Operations
|
|
|
|
```javascript
|
|
// bridge.js
|
|
const nodeMap = new Map();
|
|
let nodeIdCounter = 1;
|
|
|
|
function flushDOMOps(ptr, len) {
|
|
const ops = new Uint32Array(wasmMemory.buffer, ptr, len);
|
|
const stack = [];
|
|
|
|
for (let i = 0; i < len;) {
|
|
switch(ops[i++]) {
|
|
case 0x01: { // CREATE_ELEMENT
|
|
const tagId = ops[i++];
|
|
const el = document.createElement(TAG_NAMES[tagId]);
|
|
const id = nodeIdCounter++;
|
|
nodeMap.set(id, el);
|
|
stack.push(id);
|
|
break;
|
|
}
|
|
|
|
case 0x05: { // UPDATE_TEXT
|
|
const nodeId = ops[i++];
|
|
const textPtr = ops[i++];
|
|
const text = readString(textPtr);
|
|
nodeMap.get(nodeId).textContent = text;
|
|
break;
|
|
}
|
|
|
|
// ... other operations
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Implementation Roadmap
|
|
|
|
### Phase 1: Python Prototype (Week 1)
|
|
- [x] Define component syntax with yield
|
|
- [ ] Build Python runtime for testing (framework.py)
|
|
- [ ] Create HTML generation from component tree
|
|
- [ ] Signal/reactive state in pure Python
|
|
|
|
### Phase 2: Parser (Week 2)
|
|
- [ ] Python subset grammar definition
|
|
- [ ] Parser using ANTLR or hand-written recursive descent
|
|
- [ ] AST generation from Python code
|
|
- [ ] Semantic analysis (type checking, reactive dependency tracking)
|
|
|
|
### Phase 3: WASM Compiler (Week 3-4)
|
|
- [ ] LLVM setup and IR generation
|
|
- [ ] Compile components to WASM functions
|
|
- [ ] Reactive runtime in C/Rust
|
|
- [ ] DOM operation batching
|
|
|
|
### Phase 4: JavaScript Bridge (Week 5)
|
|
- [ ] DOM operation interpreter in JS
|
|
- [ ] Event handler callbacks
|
|
- [ ] String passing optimization
|
|
- [ ] Memory management
|
|
|
|
### Phase 5: Optimization (Week 6)
|
|
- [ ] Template compilation (static vs dynamic parts)
|
|
- [ ] Signal-to-DOM direct mapping
|
|
- [ ] Batch operation compression
|
|
- [ ] Bundle size optimization
|
|
|
|
## Key Technical Decisions
|
|
|
|
### Why Yield?
|
|
- Natural Python syntax for building trees
|
|
- Composable via `yield from`
|
|
- Clear execution order
|
|
- No magic - just generators
|
|
|
|
### Why Compile vs Interpret?
|
|
- 10-100x performance improvement
|
|
- 5-10x smaller bundle (no Python runtime)
|
|
- Direct DOM updates (no virtual DOM)
|
|
- Type safety at compile time
|
|
|
|
### Why LLVM?
|
|
- Mature toolchain
|
|
- Excellent WASM backend
|
|
- Optimization passes
|
|
- Debugging support
|
|
|
|
## Performance Targets
|
|
|
|
Based on research of SolidJS, Leptos, and Dioxus:
|
|
|
|
- **Bundle size**: < 200KB compressed
|
|
- **Startup time**: < 500ms on slow 3G
|
|
- **Update latency**: Within 10% of vanilla JS
|
|
- **Memory usage**: 70% less than React
|
|
- **DOM mutations**: 99% fewer than virtual DOM
|
|
|
|
## Critical Implementation Details
|
|
|
|
### Signal Subscription Model
|
|
```c
|
|
// Each signal tracks its subscribers
|
|
// Each effect tracks its dependencies
|
|
// Bidirectional relationship for cleanup
|
|
|
|
typedef struct Effect {
|
|
void (*fn)(void);
|
|
Signal** dependencies;
|
|
uint32_t dep_count;
|
|
} Effect;
|
|
|
|
// Global context during effect execution
|
|
Effect* current_effect = NULL;
|
|
|
|
// When signal.get() called, register dependency
|
|
int32_t signal_get(Signal* sig) {
|
|
if (current_effect) {
|
|
add_dependency(current_effect, sig);
|
|
}
|
|
return sig->value;
|
|
}
|
|
```
|
|
|
|
### Memory Management
|
|
- Bump allocator for per-frame allocations
|
|
- Generation-based GC for long-lived objects
|
|
- Reference counting for shared resources
|
|
- No Rc/RefCell exposed to users
|
|
|
|
### String Handling
|
|
- String table in WASM memory
|
|
- Pass indices, not pointers
|
|
- Intern common strings
|
|
- UTF-8 throughout
|
|
|
|
## Revolutionary Debugging Story
|
|
|
|
### Python Debugging in WASM!
|
|
|
|
**THE INSIGHT**: We're not debugging JavaScript - we're debugging Python code running in WASM! This means we can:
|
|
|
|
```python
|
|
# This is what you see in DevTools
|
|
@component
|
|
def Counter():
|
|
count = signal(0) # <-- Breakpoint here shows Python source!
|
|
|
|
def increment():
|
|
count += 1 # <-- Step through Python, not JS!
|
|
print(f"Count: {count}") # <-- See Python values
|
|
```
|
|
|
|
### How It Works
|
|
|
|
1. **DWARF Debug Symbols**: LLVM generates standard DWARF symbols in WASM
|
|
2. **Source Maps on Steroids**: Map WASM instructions → Python source lines
|
|
3. **Symbol Translation**:
|
|
```
|
|
WASM offset 0x1234 → Counter.py:15:8 → "count += 1"
|
|
```
|
|
4. **Browser Integration**: Chrome DevTools already supports WASM debugging!
|
|
5. **Live Editing**: Recompile just the changed component, hot-reload the WASM module
|
|
|
|
### Debugging Options
|
|
|
|
```javascript
|
|
// In browser console
|
|
wasm.debug.setBreakpoint('Counter.py', 15)
|
|
wasm.debug.stepInto()
|
|
wasm.debug.inspect('count') // Shows Signal { value: 5, subscribers: [...] }
|
|
|
|
// Or even connect Python debugger!
|
|
# pdb remote debugging over WebSocket
|
|
import pdb; pdb.set_trace() # This could actually work!
|
|
```
|
|
|
|
## Modular Loading Architecture
|
|
|
|
### Progressive WASM Bundles
|
|
|
|
Instead of one monolithic WASM file, split into modules:
|
|
|
|
```
|
|
bootstrap.wasm (50KB)
|
|
├── Minimal runtime
|
|
├── Login component
|
|
└── Module loader
|
|
|
|
app.wasm (200KB) - Loaded after auth
|
|
├── Main application
|
|
├── Core components
|
|
└── Router
|
|
|
|
lazy/
|
|
├── admin.wasm (150KB) - Admin panel
|
|
├── analytics.wasm (100KB) - Analytics dashboard
|
|
├── reports.wasm (120KB) - Report generation
|
|
└── [feature].wasm - Feature-specific modules
|
|
```
|
|
|
|
### Security Through Architecture
|
|
|
|
```python
|
|
# bootstrap.py - Compiles to bootstrap.wasm
|
|
@component
|
|
def LoginPage():
|
|
with Main():
|
|
yield LoginForm()
|
|
# No app code here - not even downloaded!
|
|
|
|
# After successful auth
|
|
async def load_app():
|
|
# Server only sends this to authenticated users
|
|
app_module = await fetch('/wasm/app.wasm', {
|
|
'headers': {'Authorization': f'Bearer {token}'}
|
|
})
|
|
wasm.load_module(app_module)
|
|
```
|
|
|
|
### Implementation
|
|
|
|
```javascript
|
|
// module-loader.js
|
|
class RitzModuleLoader {
|
|
constructor() {
|
|
this.modules = new Map();
|
|
this.cache = new Cache('ritz-modules-v1');
|
|
}
|
|
|
|
async loadModule(name, url) {
|
|
// Check cache first
|
|
const cached = await this.cache.match(url);
|
|
if (cached) {
|
|
const wasm = await WebAssembly.instantiate(cached);
|
|
this.modules.set(name, wasm.instance);
|
|
return;
|
|
}
|
|
|
|
// Download and cache
|
|
const response = await fetch(url);
|
|
const bytes = await response.arrayBuffer();
|
|
|
|
// Cache for next time
|
|
await this.cache.put(url, new Response(bytes));
|
|
|
|
// Instantiate
|
|
const wasm = await WebAssembly.instantiate(bytes, {
|
|
env: this.getImports(name)
|
|
});
|
|
|
|
this.modules.set(name, wasm.instance);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Route-Based Code Splitting
|
|
|
|
```python
|
|
# router.py
|
|
@component
|
|
def Router():
|
|
current_route = signal(window.location.pathname)
|
|
|
|
with Main():
|
|
if current_route == '/admin':
|
|
# Lazy load admin module
|
|
yield LazyLoad('admin.wasm', AdminPanel)
|
|
elif current_route == '/reports':
|
|
# Only load if needed
|
|
yield LazyLoad('reports.wasm', ReportsView)
|
|
else:
|
|
# Main app is already loaded
|
|
yield Dashboard()
|
|
```
|
|
|
|
## Next Steps (Updated)
|
|
|
|
1. ✅ **Prototype**: Build framework.py with working signals and components
|
|
2. **AST Parser**: Use Python's `ast` module to parse components
|
|
3. **Compiler Pipeline**:
|
|
- Python AST → Ritz IR
|
|
- Ritz IR → LLVM IR
|
|
- LLVM IR → WASM with debug symbols
|
|
4. **Debugging Infrastructure**:
|
|
- Generate source maps
|
|
- Embed Python source in WASM
|
|
- Browser DevTools integration
|
|
5. **Module System**:
|
|
- Build tool for splitting modules
|
|
- Runtime module loader
|
|
- Caching strategy
|
|
6. **Test**: Port TodoMVC with lazy-loaded features
|
|
|
|
## References
|
|
|
|
- SolidJS: Fine-grained reactivity without virtual DOM
|
|
- Leptos: Rust signals with Copy + 'static
|
|
- Dioxus Sledgehammer: Batched DOM operations
|
|
- LLVM WebAssembly Backend: https://llvm.org/docs/WebAssemblyUsage.html
|
|
- Binaryen: WASM optimizer and code generator
|
|
|
|
## Core Innovation
|
|
|
|
**Python syntax → WASM performance**. Write and debug in Python, ship as WASM. No virtual DOM, direct signal-to-DOM updates, 99% fewer mutations than React. The future of web frameworks: compiled, reactive, Python-like. |