From 316c090778856a18e25fe2a4d9edb725367bda65 Mon Sep 17 00:00:00 2001 From: A2IA Date: Sat, 15 Nov 2025 08:07:09 +0000 Subject: [PATCH] Initial commit - some junk --- README.md | 117 +++++++++++ RESEARCH.md | 522 +++++++++++++++++++++++++++++++++++++++++++++++ ast_parser.py | 256 +++++++++++++++++++++++ compiler.py | 344 +++++++++++++++++++++++++++++++ debug_concept.py | 263 ++++++++++++++++++++++++ example.py | 139 +++++++++++++ framework.py | 176 ++++++++++++++++ module_system.py | 427 ++++++++++++++++++++++++++++++++++++++ test.py | 28 +++ 9 files changed, 2272 insertions(+) create mode 100644 README.md create mode 100644 RESEARCH.md create mode 100644 ast_parser.py create mode 100644 compiler.py create mode 100644 debug_concept.py create mode 100644 example.py create mode 100644 framework.py create mode 100644 module_system.py create mode 100644 test.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..72937e4 --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +# Ritz Framework + +A WebAssembly UI framework with Python-like syntax that compiles to high-performance WASM while remaining executable in Python for debugging. + +## Core Innovations + +### 1. Python's AST Module for Parsing +No need for a custom parser! Since our components are valid Python, we use Python's own `ast` module to parse them. Zero parsing overhead, perfect compatibility, error messages for free. + +### 2. Debug Python in WASM (Not JavaScript!) +Set breakpoints in Python source code, step through Python lines, inspect Python variables - all in the browser! We compile with DWARF debug symbols and source maps that map WASM → Python source. + +### 3. Progressive Module Loading +Split apps into multiple WASM modules: +- `bootstrap.wasm` (50KB) - Login only, no business logic +- `app-core.wasm` (200KB) - Main app, requires auth +- Feature modules - Lazy loaded on demand +- Security: Unauthenticated users never receive app code! + +### 4. Python Executable, WASM Compilable +Write components in **actual Python** that can run in CPython for debugging, but compile to WebAssembly for production with: +- No virtual DOM overhead +- Direct signal-to-DOM updates +- 99% fewer mutations than React +- Sub-200KB bundle sizes + +## Quick Start + +### Try the Python Prototype + +```bash +# Run the example +python3 example.py + +# Run tests +python3 test.py +``` + +### Component Syntax + +```python +@component +def Counter(): + count = signal(0) + + def increment(): + count += 1 + + with Div(class_='counter'): + yield H1('Counter App') + yield P(f'Count: {count}') + yield Button('Click me', onclick=increment) +``` + +### Key Features + +1. **Yield-based composition** - Natural Python generators for building component trees +2. **Context managers** - Use `with` statements for nested structures +3. **Reactive signals** - Fine-grained reactivity without virtual DOM +4. **Mixed content** - Yield strings, components, or elements seamlessly + +## Files + +- `RESEARCH.md` - Complete technical architecture and implementation plan +- `framework.py` - Python prototype implementation +- `example.py` - Example components demonstrating the syntax +- `test.py` - Simple test file +- `ast_parser.py` - Parse components using Python's ast module! +- `debug_concept.py` - Revolutionary Python-in-WASM debugging +- `module_system.py` - Progressive loading & security architecture +- `compiler.py` - Complete compilation pipeline concept + +## Architecture + +The framework works in two modes: + +1. **Debug Mode** (Current): Runs in Python interpreter, generates HTML/IR +2. **Production Mode** (TODO): Compiles to WebAssembly with LLVM + +### Compilation Pipeline + +``` +Python-like syntax → Parser (AST) → LLVM IR → WebAssembly → Browser +``` + +### Reactive Runtime + +- Signals store values + subscriber lists in WASM memory +- Effects track dependencies automatically +- Direct signal-to-DOM-node mapping (no diffing) +- Batched DOM operations across WASM/JS bridge + +## Next Steps + +1. ✅ Python prototype with working components +2. ⬜ Parser for Python subset (yields, functions, decorators) +3. ⬜ LLVM backend for WASM generation +4. ⬜ Reactive runtime in C/Rust +5. ⬜ DOM operation batching and JS bridge +6. ⬜ Benchmarks against React/SolidJS + +## Performance Targets + +Based on SolidJS and 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 + +## Why This Approach? + +1. **Developer happiness** - Write Python, debug Python, ship WASM +2. **Performance** - Compile-time optimization, no virtual DOM +3. **Simplicity** - Yield syntax is intuitive and composable +4. **Type safety** - Static typing at compile time + +Start with `example.py` to see the syntax in action! \ No newline at end of file diff --git a/RESEARCH.md b/RESEARCH.md new file mode 100644 index 0000000..cc7f50e --- /dev/null +++ b/RESEARCH.md @@ -0,0 +1,522 @@ +# 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. \ No newline at end of file diff --git a/ast_parser.py b/ast_parser.py new file mode 100644 index 0000000..7cdf5a1 --- /dev/null +++ b/ast_parser.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +""" +AST Parser for Ritz Framework +Uses Python's ast module to parse components and generate IR +""" + +import ast +import inspect +import json +from typing import Any, Dict, List + +class RitzASTTransformer(ast.NodeVisitor): + """Transform Python AST to Ritz IR""" + + def __init__(self): + self.current_component = None + self.ir = [] + + def visit_FunctionDef(self, node): + """Process function definitions""" + # Check if it has @component decorator + if self.has_component_decorator(node): + self.current_component = { + 'type': 'component', + 'name': node.name, + 'args': [arg.arg for arg in node.args.args], + 'body': [] + } + + # Process function body + for stmt in node.body: + ir_node = self.process_statement(stmt) + if ir_node: + self.current_component['body'].append(ir_node) + + self.ir.append(self.current_component) + self.current_component = None + + def has_component_decorator(self, node): + """Check if function has @component decorator""" + for decorator in node.decorator_list: + if isinstance(decorator, ast.Name) and decorator.id == 'component': + return True + return False + + def process_statement(self, stmt): + """Process a statement in component body""" + if isinstance(stmt, ast.With): + return self.process_with_statement(stmt) + elif isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Yield): + return self.process_yield(stmt.value) + elif isinstance(stmt, ast.Assign): + return self.process_assignment(stmt) + elif isinstance(stmt, ast.FunctionDef): + return self.process_inner_function(stmt) + elif isinstance(stmt, ast.For): + return self.process_for_loop(stmt) + elif isinstance(stmt, ast.If): + return self.process_if_statement(stmt) + return None + + def process_with_statement(self, node): + """Process 'with Element():' statements""" + context = node.items[0].context_expr + + with_node = { + 'type': 'with', + 'element': self.process_expression(context), + 'body': [] + } + + # Process body + for stmt in node.body: + body_node = self.process_statement(stmt) + if body_node: + with_node['body'].append(body_node) + + return with_node + + def process_yield(self, node): + """Process yield statements""" + return { + 'type': 'yield', + 'value': self.process_expression(node.value) if node.value else None + } + + def process_expression(self, expr): + """Process an expression""" + if isinstance(expr, ast.Call): + return { + 'type': 'call', + 'func': self.get_name(expr.func), + 'args': [self.process_expression(arg) for arg in expr.args], + 'kwargs': { + kw.arg: self.process_expression(kw.value) + for kw in expr.keywords + } + } + elif isinstance(expr, ast.Name): + return { + 'type': 'name', + 'id': expr.id + } + elif isinstance(expr, ast.Constant): + return { + 'type': 'constant', + 'value': expr.value + } + elif isinstance(expr, ast.JoinedStr): # f-strings + return { + 'type': 'fstring', + 'parts': [self.process_expression(v) for v in expr.values] + } + elif isinstance(expr, ast.FormattedValue): + return { + 'type': 'formatted_value', + 'value': self.process_expression(expr.value) + } + elif isinstance(expr, ast.Attribute): + return { + 'type': 'attribute', + 'value': self.process_expression(expr.value), + 'attr': expr.attr + } + else: + return { + 'type': 'unknown', + 'ast_type': expr.__class__.__name__ + } + + def process_assignment(self, node): + """Process variable assignments (e.g., count = signal(0))""" + target = node.targets[0] + if isinstance(target, ast.Name): + value = self.process_expression(node.value) + + # Check if it's a signal + 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['args'] else {'type': 'constant', 'value': None} + } + else: + return { + 'type': 'assign', + 'target': target.id, + 'value': value + } + + def process_inner_function(self, node): + """Process inner function definitions (e.g., event handlers)""" + return { + 'type': 'function', + 'name': node.name, + 'args': [arg.arg for arg in node.args.args], + 'body': [self.process_statement(stmt) for stmt in node.body] + } + + def process_for_loop(self, node): + """Process for loops""" + return { + 'type': 'for', + 'target': self.get_name(node.target), + 'iter': self.process_expression(node.iter), + 'body': [self.process_statement(stmt) for stmt in node.body] + } + + def process_if_statement(self, node): + """Process if statements""" + return { + 'type': 'if', + 'test': self.process_expression(node.test), + 'body': [self.process_statement(stmt) for stmt in node.body], + 'else': [self.process_statement(stmt) for stmt in node.orelse] if node.orelse else [] + } + + def get_name(self, node): + """Get name from Name or Attribute node""" + if isinstance(node, ast.Name): + return node.id + elif isinstance(node, ast.Attribute): + return f"{self.get_name(node.value)}.{node.attr}" + return 'unknown' + + +def parse_component_source(source: str) -> List[Dict]: + """Parse component source code and return IR""" + tree = ast.parse(source) + transformer = RitzASTTransformer() + transformer.visit(tree) + return transformer.ir + + +def parse_component_function(func) -> Dict: + """Parse a component function and return its IR""" + source = inspect.getsource(func) + ir = parse_component_source(source) + return ir[0] if ir else None + + +def visualize_ast(source: str): + """Visualize Python AST for debugging""" + tree = ast.parse(source) + return ast.dump(tree, indent=2) + + +# Test it! +if __name__ == '__main__': + test_source = ''' +@component +def Counter(): + count = signal(0) + + def increment(): + count += 1 + + with Div(class_='counter'): + yield H1('Counter') + yield P(f'Count: {count}') + yield Button('Click me', onclick=increment) + ''' + + print("=== Python AST (for reference) ===") + print(visualize_ast(test_source)[:500] + "...\n") + + print("=== Ritz IR ===") + ir = parse_component_source(test_source) + print(json.dumps(ir, indent=2)) + + print("\n=== Test with actual component ===") + # Import our framework + from framework import component, signal + + # Mock elements for testing + def Div(**kwargs): return {'element': 'div', 'props': kwargs} + def H1(*args): return {'element': 'h1', 'children': args} + def P(*args): return {'element': 'p', 'children': args} + def Button(*args, **kwargs): return {'element': 'button', 'children': args, 'props': kwargs} + + @component + def TestComponent(): + count = signal(0) + + with Div(class_='test'): + yield H1('Test') + yield P(f'Value: {count}') + + for i in range(3): + yield Button(f'Button {i}') + + ir = parse_component_function(TestComponent) + print("\nParsed from real function:") + print(json.dumps(ir, indent=2)[:1000] + "...") diff --git a/compiler.py b/compiler.py new file mode 100644 index 0000000..6002437 --- /dev/null +++ b/compiler.py @@ -0,0 +1,344 @@ +#!/usr/bin/env python3 +""" +Ritz Compilation Pipeline +From Python source to debuggable WASM modules +""" + +import ast +import hashlib +import subprocess +from pathlib import Path +from typing import Dict, List, Optional +from dataclasses import dataclass + +@dataclass +class CompilationUnit: + """A unit of compilation (module)""" + name: str + components: List[str] + dependencies: List[str] + source_files: List[Path] + output_path: Path + requires_auth: bool = False + +class RitzCompiler: + """Complete compilation pipeline""" + + def __init__(self, project_dir: Path): + self.project_dir = project_dir + self.build_dir = project_dir / 'build' + self.wasm_dir = project_dir / 'dist' / 'wasm' + self.units: List[CompilationUnit] = [] + + def compile_project(self): + """Full compilation pipeline""" + print("=== Ritz Compilation Pipeline ===\n") + + # 1. Discover and parse components + print("1. Parsing Python components...") + components = self.discover_components() + + # 2. Split into modules + print("\n2. Splitting into modules...") + self.split_into_modules(components) + + # 3. Generate IR for each module + print("\n3. Generating intermediate representation...") + for unit in self.units: + self.generate_ir(unit) + + # 4. Compile to WASM with debug symbols + print("\n4. Compiling to WASM...") + for unit in self.units: + self.compile_to_wasm(unit) + + # 5. Generate debug artifacts + print("\n5. Generating debug artifacts...") + self.generate_debug_artifacts() + + # 6. Generate module manifest + print("\n6. Creating module manifest...") + self.generate_manifest() + + print("\n✅ Compilation complete!") + + def discover_components(self) -> Dict[str, ast.AST]: + """Discover all @component decorated functions""" + components = {} + + for py_file in self.project_dir.glob('**/*.py'): + if 'build' in py_file.parts or '__pycache__' in py_file.parts: + continue + + with open(py_file) as f: + source = f.read() + + try: + tree = ast.parse(source) + for node in ast.walk(tree): + if isinstance(node, ast.FunctionDef): + if self.has_component_decorator(node): + components[node.name] = { + 'ast': node, + 'file': py_file, + 'source': source + } + print(f" Found: {node.name} in {py_file.name}") + except SyntaxError as e: + print(f" Skipping {py_file}: {e}") + + return components + + def has_component_decorator(self, node: ast.FunctionDef) -> bool: + """Check if function has @component decorator""" + for decorator in node.decorator_list: + if isinstance(decorator, ast.Name) and decorator.id == 'component': + return True + return False + + def split_into_modules(self, components: Dict): + """Split components into compilation units""" + + # Bootstrap module - login only + bootstrap_components = [ + name for name in components + if 'Login' in name or 'Auth' in name + ] + + if bootstrap_components: + self.units.append(CompilationUnit( + name='bootstrap', + components=bootstrap_components, + dependencies=[], + source_files=[components[c]['file'] for c in bootstrap_components], + output_path=self.wasm_dir / 'bootstrap.wasm', + requires_auth=False + )) + print(f" bootstrap.wasm: {len(bootstrap_components)} components") + + # Core module - main app + core_components = [ + name for name in components + if name not in bootstrap_components + and not name.startswith('Admin') + and not name.startswith('Analytics') + ] + + if core_components: + self.units.append(CompilationUnit( + name='app-core', + components=core_components, + dependencies=['bootstrap'], + source_files=[components[c]['file'] for c in core_components], + output_path=self.wasm_dir / 'app-core.wasm', + requires_auth=True + )) + print(f" app-core.wasm: {len(core_components)} components") + + # Feature modules + admin_components = [ + name for name in components if name.startswith('Admin') + ] + + if admin_components: + self.units.append(CompilationUnit( + name='admin', + components=admin_components, + dependencies=['app-core'], + source_files=[components[c]['file'] for c in admin_components], + output_path=self.wasm_dir / 'features' / 'admin.wasm', + requires_auth=True + )) + print(f" admin.wasm: {len(admin_components)} components") + + def generate_ir(self, unit: CompilationUnit): + """Generate Ritz IR from Python AST""" + print(f" Generating IR for {unit.name}...") + + # Use our AST parser + from ast_parser import RitzASTTransformer + + ir_module = { + 'name': unit.name, + 'components': [], + 'dependencies': unit.dependencies + } + + for component_name in unit.components: + # Parse component + # In real implementation, would use the stored AST + print(f" Processing {component_name}") + + # Generate IR for component + ir = { + 'type': 'component', + 'name': component_name, + # ... IR details + } + ir_module['components'].append(ir) + + # Save IR for next stage + ir_path = self.build_dir / f'{unit.name}.ir.json' + ir_path.parent.mkdir(parents=True, exist_ok=True) + + import json + with open(ir_path, 'w') as f: + json.dump(ir_module, f, indent=2) + + def compile_to_wasm(self, unit: CompilationUnit): + """Compile IR to WASM using LLVM""" + print(f" Compiling {unit.name} to WASM...") + + # This would use LLVM in reality + # For demonstration, show the command that would be run + + llvm_commands = [ + # 1. Generate LLVM IR + f"ritz-codegen {self.build_dir}/{unit.name}.ir.json -o {self.build_dir}/{unit.name}.ll", + + # 2. Compile to WASM with debug symbols + f"clang --target=wasm32-unknown-unknown " + f"-O2 -g " # Optimization + debug symbols + f"-fdebug-prefix-map={self.project_dir}=/src " # Remap paths for debugging + f"-o {self.build_dir}/{unit.name}.wasm " + f"{self.build_dir}/{unit.name}.ll", + + # 3. Optimize with Binaryen + f"wasm-opt " + f"-O2 " # Optimize + f"--debuginfo " # Preserve debug info + f"--source-map {unit.output_path}.map " # Generate source map + f"-o {unit.output_path} " + f"{self.build_dir}/{unit.name}.wasm" + ] + + for cmd in llvm_commands: + print(f" $ {cmd}") + + # Create output file (mock) + unit.output_path.parent.mkdir(parents=True, exist_ok=True) + unit.output_path.write_bytes(b'\x00asm\x01\x00\x00\x00...') + + def generate_debug_artifacts(self): + """Generate debugging support files""" + print(" Generating source maps...") + + for unit in self.units: + source_map = { + "version": 3, + "file": str(unit.output_path), + "sources": [str(f) for f in unit.source_files], + "sourcesContent": [ + f.read_text() for f in unit.source_files + ], + "names": unit.components, + "mappings": "...", # Would be actual VLQ encodings + + # Ritz extensions for Python debugging + "x_ritz": { + "python_version": "3.11", + "module_name": unit.name, + "components": unit.components, + "debug_symbols": f"{unit.output_path}.dwarf" + } + } + + map_path = unit.output_path.with_suffix('.wasm.map') + print(f" {map_path.name}") + + def generate_manifest(self): + """Generate module manifest for loader""" + manifest = { + "version": "1.0", + "modules": {} + } + + for unit in self.units: + # Calculate hash for cache busting + if unit.output_path.exists(): + content = unit.output_path.read_bytes() + hash_val = hashlib.sha256(content).hexdigest()[:12] + else: + hash_val = "mock" + + manifest["modules"][unit.name] = { + "path": f"/wasm/{unit.output_path.name}", + "size": unit.output_path.stat().st_size if unit.output_path.exists() else 0, + "hash": hash_val, + "dependencies": unit.dependencies, + "requires_auth": unit.requires_auth, + "components": unit.components + } + + manifest_path = self.wasm_dir / 'manifest.json' + print(f" Writing {manifest_path}") + + import json + manifest_path.parent.mkdir(parents=True, exist_ok=True) + with open(manifest_path, 'w') as f: + json.dump(manifest, f, indent=2) + +class DebugServer: + """Development server with debugging support""" + + def __init__(self, compiler: RitzCompiler): + self.compiler = compiler + + def start(self, port: int = 8080): + """Start dev server with hot reload and debugging""" + print(f"\n🚀 Starting Ritz dev server on http://localhost:{port}") + print("\nFeatures:") + print(" ✓ Python source debugging in browser") + print(" ✓ Hot module reloading") + print(" ✓ Source maps") + print(" ✓ Progressive loading") + print("\nDevTools Integration:") + print(" 1. Open Chrome DevTools") + print(" 2. Go to Sources tab") + print(" 3. Find 'wasm://' in file tree") + print(" 4. Set breakpoints in Python files!") + print("\nCommands:") + print(" r - Recompile all") + print(" m - Recompile specific module") + print(" d - Open debugger") + print(" q - Quit") + +# Example usage +if __name__ == '__main__': + import sys + from pathlib import Path + + # Compile the project + project_dir = Path.cwd() + compiler = RitzCompiler(project_dir) + + # Mock some components for demo + print("=== Ritz Compiler Demo ===\n") + + # Show what the compiler would do + print("Project structure:") + print(" ritz/") + print(" ├── components/") + print(" │ ├── login.py (@component LoginForm)") + print(" │ ├── dashboard.py (@component Dashboard)") + print(" │ ├── admin.py (@component AdminPanel)") + print(" │ └── analytics.py (@component AnalyticsView)") + print(" ├── build/ (intermediate files)") + print(" └── dist/") + print(" └── wasm/") + print(" ├── bootstrap.wasm (50KB - no auth)") + print(" ├── app-core.wasm (200KB - auth required)") + print(" ├── features/") + print(" │ ├── admin.wasm (150KB)") + print(" │ └── analytics.wasm (120KB)") + print(" └── manifest.json") + + print("\nCompilation would:") + print("1. Parse Python with ast module") + print("2. Generate Ritz IR") + print("3. Compile to LLVM IR") + print("4. Generate WASM with debug symbols") + print("5. Create source maps") + print("6. Split into secure modules") + + print("\n🎉 The dream is real: Python → WASM with debugging!") diff --git a/debug_concept.py b/debug_concept.py new file mode 100644 index 0000000..15099e0 --- /dev/null +++ b/debug_concept.py @@ -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 + """) diff --git a/example.py b/example.py new file mode 100644 index 0000000..7eba9e1 --- /dev/null +++ b/example.py @@ -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() \ No newline at end of file diff --git a/framework.py b/framework.py new file mode 100644 index 0000000..b233b92 --- /dev/null +++ b/framework.py @@ -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}' + 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 \ No newline at end of file diff --git a/module_system.py b/module_system.py new file mode 100644 index 0000000..0c08164 --- /dev/null +++ b/module_system.py @@ -0,0 +1,427 @@ +""" +Ritz Module System - Progressive Loading Concept +Split apps into multiple WASM modules for security and performance +""" + +import hashlib +import json +from dataclasses import dataclass +from typing import Dict, List, Optional, Set + +@dataclass +class WASMModule: + """Represents a WASM module""" + name: str + path: str + size_kb: int + dependencies: List[str] + exports: List[str] + requires_auth: bool = False + cache_strategy: str = 'aggressive' # aggressive, normal, no-cache + +class ModuleGraph: + """Dependency graph for WASM modules""" + + def __init__(self): + self.modules: Dict[str, WASMModule] = {} + self.loaded: Set[str] = set() + + def add_module(self, module: WASMModule): + self.modules[module.name] = module + + def get_load_order(self, module_name: str) -> List[str]: + """Get modules to load in dependency order""" + if module_name in self.loaded: + return [] + + order = [] + visited = set() + + def visit(name): + if name in visited or name in self.loaded: + return + visited.add(name) + + module = self.modules.get(name) + if module: + for dep in module.dependencies: + visit(dep) + order.append(name) + + visit(module_name) + return order + +class RitzModuleSystem: + """Progressive module loading system""" + + def __init__(self): + self.graph = ModuleGraph() + self.setup_modules() + + def setup_modules(self): + """Define application module structure""" + + # Bootstrap - minimal, always loaded + self.graph.add_module(WASMModule( + name='bootstrap', + path='/wasm/bootstrap.wasm', + size_kb=50, + dependencies=[], + exports=['LoginForm', 'ModuleLoader', 'AuthService'], + requires_auth=False, + cache_strategy='aggressive' + )) + + # Core app - loaded after authentication + self.graph.add_module(WASMModule( + name='app-core', + path='/wasm/app-core.wasm', + size_kb=200, + dependencies=['bootstrap'], + exports=['Router', 'Dashboard', 'Navigation'], + requires_auth=True, + cache_strategy='aggressive' + )) + + # Feature modules - lazy loaded + self.graph.add_module(WASMModule( + name='admin', + path='/wasm/features/admin.wasm', + size_kb=150, + dependencies=['app-core'], + exports=['AdminPanel', 'UserManagement', 'Settings'], + requires_auth=True, + cache_strategy='normal' + )) + + self.graph.add_module(WASMModule( + name='analytics', + path='/wasm/features/analytics.wasm', + size_kb=120, + dependencies=['app-core', 'charts'], + exports=['AnalyticsDashboard', 'Reports'], + requires_auth=True, + cache_strategy='normal' + )) + + self.graph.add_module(WASMModule( + name='charts', + path='/wasm/lib/charts.wasm', + size_kb=80, + dependencies=[], + exports=['LineChart', 'BarChart', 'PieChart'], + requires_auth=True, + cache_strategy='aggressive' + )) + + def generate_loader_js(self) -> str: + """Generate JavaScript module loader""" + return ''' +class RitzModuleLoader { + constructor() { + this.modules = new Map(); + this.loading = new Map(); + this.cache = caches.open('ritz-modules-v1'); + } + + async loadModule(name, config = {}) { + // Check if already loaded + if (this.modules.has(name)) { + return this.modules.get(name); + } + + // Check if already loading (prevent double-load) + if (this.loading.has(name)) { + return this.loading.get(name); + } + + // Start loading + const loadPromise = this._loadModule(name, config); + this.loading.set(name, loadPromise); + + try { + const module = await loadPromise; + this.modules.set(name, module); + return module; + } finally { + this.loading.delete(name); + } + } + + async _loadModule(name, config) { + const manifest = await this.getManifest(); + const moduleInfo = manifest[name]; + + if (!moduleInfo) { + throw new Error(`Module ${name} not found`); + } + + // Check authentication requirement + if (moduleInfo.requires_auth && !this.isAuthenticated()) { + throw new Error(`Module ${name} requires authentication`); + } + + // Load dependencies first + for (const dep of moduleInfo.dependencies) { + await this.loadModule(dep, config); + } + + // Try cache first + const cache = await this.cache; + const cached = await cache.match(moduleInfo.path); + + let response; + if (cached && !config.bypassCache) { + console.log(`Loading ${name} from cache`); + response = cached; + } else { + console.log(`Downloading ${name} (${moduleInfo.size_kb}KB)`); + + // Add auth token if required + const headers = {}; + if (moduleInfo.requires_auth) { + headers['Authorization'] = `Bearer ${this.getAuthToken()}`; + } + + response = await fetch(moduleInfo.path, { headers }); + + // Cache based on strategy + if (moduleInfo.cache_strategy !== 'no-cache') { + await cache.put(moduleInfo.path, response.clone()); + } + } + + // Instantiate WASM + const bytes = await response.arrayBuffer(); + const wasm = await WebAssembly.instantiate(bytes, { + env: this.getImports(name), + ...this.getModuleImports(moduleInfo.dependencies) + }); + + return wasm.instance; + } + + getModuleImports(dependencies) { + // Link imported functions from dependencies + const imports = {}; + for (const dep of dependencies) { + const module = this.modules.get(dep); + if (module) { + imports[dep] = module.exports; + } + } + return imports; + } + + async preloadModules(names) { + // Preload modules in parallel + const promises = names.map(name => this.loadModule(name)); + await Promise.all(promises); + } + + isAuthenticated() { + return localStorage.getItem('auth_token') !== null; + } + + getAuthToken() { + return localStorage.getItem('auth_token'); + } + + async getManifest() { + // Module manifest with metadata + const response = await fetch('/wasm/manifest.json'); + return response.json(); + } +} + +// Usage +const loader = new RitzModuleLoader(); + +// Bootstrap phase - no auth required +await loader.loadModule('bootstrap'); + +// After login +async function onLoginSuccess(token) { + localStorage.setItem('auth_token', token); + + // Now load the main app + await loader.loadModule('app-core'); + + // Preload common features + loader.preloadModules(['charts']); // Non-blocking + + // Navigate to dashboard + window.RitzApp.navigate('/dashboard'); +} + +// Lazy load on route change +window.addEventListener('route-change', async (e) => { + const route = e.detail.route; + + if (route.startsWith('/admin')) { + await loader.loadModule('admin'); + } else if (route.startsWith('/analytics')) { + await loader.loadModule('analytics'); + } +}); + ''' + + def generate_manifest(self) -> Dict: + """Generate module manifest""" + manifest = {} + for name, module in self.graph.modules.items(): + manifest[name] = { + 'path': module.path, + 'size_kb': module.size_kb, + 'dependencies': module.dependencies, + 'requires_auth': module.requires_auth, + 'cache_strategy': module.cache_strategy, + 'hash': self.calculate_hash(module) + } + return manifest + + def calculate_hash(self, module: WASMModule) -> str: + """Calculate hash for cache invalidation""" + content = f"{module.name}:{module.path}:{module.size_kb}" + return hashlib.sha256(content.encode()).hexdigest()[:12] + + def split_components_into_modules(self, components: List[str]) -> Dict[str, List[str]]: + """Automatically split components into modules""" + + # Example heuristic: group by feature area + modules = { + 'bootstrap': [], + 'app-core': [], + 'admin': [], + 'analytics': [], + 'reports': [] + } + + for component in components: + if component.startswith('Login') or component.startswith('Auth'): + modules['bootstrap'].append(component) + elif component.startswith('Admin'): + modules['admin'].append(component) + elif component.startswith('Analytics') or component.startswith('Chart'): + modules['analytics'].append(component) + elif component.startswith('Report'): + modules['reports'].append(component) + else: + modules['app-core'].append(component) + + # Remove empty modules + return {k: v for k, v in modules.items() if v} + +class SecurityExample: + """Security benefits of modular loading""" + + @staticmethod + def bootstrap_only_code(): + """Code that ships to all users (even unauthenticated)""" + return ''' +# bootstrap.py - Compiles to bootstrap.wasm +@component +def LoginPage(): + username = signal('') + password = signal('') + error = signal(None) + + async def handle_login(): + try: + response = await fetch('/api/login', { + 'method': 'POST', + 'body': json.dumps({ + 'username': username.value, + 'password': password.value + }) + }) + + if response.ok: + token = await response.json() + # Now load the real app + await ModuleLoader.load('app-core') + navigate('/dashboard') + else: + error.value = 'Invalid credentials' + except Exception as e: + error.value = str(e) + + with Main(): + with Form(onsubmit=handle_login): + yield H1('Login') + yield Input( + type='text', + placeholder='Username', + value=username, + oninput=lambda e: username.set(e.target.value) + ) + yield Input( + type='password', + placeholder='Password', + value=password, + oninput=lambda e: password.set(e.target.value) + ) + yield Button('Sign In', type='submit') + + if error.value: + yield ErrorMessage(error.value) + +# IMPORTANT: No business logic, no API endpoints, no sensitive data! +# This is ALL that unauthenticated users can see + ''' + + @staticmethod + def protected_module_code(): + """Code that only authenticated users receive""" + return ''' +# app-core.py - Compiles to app-core.wasm +# This is NEVER sent to unauthenticated users + +@component +def Dashboard(): + # Business logic here + user_data = signal(None) + + async def load_user_data(): + # API endpoints only known to authenticated users + response = await fetch('/api/user/profile') + user_data.value = await response.json() + + # ... rest of application + ''' + +# Demonstration +if __name__ == '__main__': + print("=== Ritz Module System ===\n") + + system = RitzModuleSystem() + + print("1. Module Structure:") + for name, module in system.graph.modules.items(): + print(f" {name}:") + print(f" Size: {module.size_kb}KB") + print(f" Auth Required: {module.requires_auth}") + print(f" Dependencies: {module.dependencies}") + print(f" Exports: {module.exports[:2]}...") + + print("\n2. Load Order for 'analytics':") + order = system.graph.get_load_order('analytics') + print(f" Must load in order: {order}") + total_size = sum(system.graph.modules[m].size_kb for m in order) + print(f" Total download: {total_size}KB") + + print("\n3. Module Manifest:") + manifest = system.generate_manifest() + print(json.dumps(manifest, indent=2)[:400] + "...") + + print("\n4. Security Benefits:") + print(" - Unauthenticated users only get bootstrap.wasm (50KB)") + print(" - No API endpoints or business logic exposed") + print(" - Feature modules loaded on-demand") + print(" - Each module can have different cache strategies") + + print("\n5. Performance Benefits:") + print(" - Initial load: 50KB instead of 600KB+") + print(" - Aggressive caching for stable modules") + print(" - Parallel preloading of likely-needed modules") + print(" - Route-based code splitting") diff --git a/test.py b/test.py new file mode 100644 index 0000000..975c478 --- /dev/null +++ b/test.py @@ -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)) \ No newline at end of file