Compare commits
32
Commits
316c090778
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea7dadc5ae | ||
|
|
ddd2c03a9d | ||
|
|
999327172e | ||
|
|
a43b325755 | ||
|
|
7efa7786cf | ||
|
|
0b77c2ff3c | ||
|
|
d8a2e28fd5 | ||
|
|
e1219ea0c1 | ||
|
|
60bbad9c68 | ||
|
|
443cca5662 | ||
|
|
134ddc4c42 | ||
|
|
1575571f3e | ||
|
|
b0e38db22c | ||
|
|
adf95f424d | ||
|
|
c947bcf6e0 | ||
|
|
e09a6dedc8 | ||
|
|
e686903170 | ||
|
|
ac7726df46 | ||
|
|
3540c44abb | ||
|
|
44db8ff022 | ||
|
|
8bf1c38858 | ||
|
|
9f601b1787 | ||
|
|
cdddbf675a | ||
|
|
41474aceda | ||
|
|
cd89f75c1c | ||
|
|
e18baf3106 | ||
|
|
74ff1e7e9f | ||
|
|
0063a8536c | ||
|
|
044eda2293 | ||
|
|
a3cd1df40a | ||
|
|
81d51d5617 | ||
|
|
635d466874 |
No files matched your search
@@ -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
|
||||
@@ -0,0 +1,291 @@
|
||||
# Phase 4: Cleanup & Polish - Make It Sexy
|
||||
|
||||
## Critical Issues to Fix
|
||||
|
||||
### 1. Debug Console UI Jank
|
||||
|
||||
**Problem**: Page content shifts RIGHT when debug console opens (counter-intuitive)
|
||||
- Expected: Content shifts LEFT to make room
|
||||
- Current: Content margin-right increases, pushing content right
|
||||
|
||||
**Fix**:
|
||||
```css
|
||||
/* BEFORE (wrong) */
|
||||
body.debug-open #app {
|
||||
margin-right: 420px; /* Pushes content right - bad! */
|
||||
}
|
||||
|
||||
/* AFTER (correct) */
|
||||
body.debug-open #app {
|
||||
margin-right: 0; /* Keep in place or... */
|
||||
/* OR shift left to center remaining space */
|
||||
transform: translateX(-200px);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Un-Ritzy Toggle Implementation (CRITICAL)
|
||||
|
||||
**Current (WRONG)**: JS manipulates DOM classes, WASM calls JS bridge function
|
||||
```python
|
||||
# ANTI-PATTERN - calling JS to manipulate DOM!
|
||||
@external
|
||||
def js_toggle_debug_console():
|
||||
pass
|
||||
|
||||
def toggle():
|
||||
js_toggle_debug_console() # ❌ JS does the work
|
||||
```
|
||||
|
||||
**Correct (RITZY)**: Signal drives reactive class, declarative event binding
|
||||
```python
|
||||
# THE RITZ WAY - declarative and reactive!
|
||||
console_open = signal(False)
|
||||
|
||||
@event('keydown') # Declarative event registration
|
||||
def handle_keydown(event):
|
||||
if event.key == '`' and event.ctrlKey:
|
||||
console_open.value = not console_open.value
|
||||
|
||||
def toggle():
|
||||
"""Button click handler"""
|
||||
console_open.value = not console_open.value
|
||||
|
||||
# Reactive class binding (syntax TBD)
|
||||
with Div(class_=lambda: 'ritz-debug' if console_open else 'ritz-debug hidden'):
|
||||
# ...or maybe:
|
||||
# class_={'ritz-debug': True, 'hidden': not console_open}
|
||||
```
|
||||
|
||||
**What this requires**:
|
||||
|
||||
#### A. Reactive Class Binding
|
||||
- [ ] Parser support for lambda/function in class attribute
|
||||
- [ ] Codegen to track class dependencies on signals
|
||||
- [ ] Runtime bridge: `js_set_class(elem_id, class_string)`
|
||||
- [ ] Signal updates trigger class re-evaluation
|
||||
|
||||
#### B. Declarative Event System (`@event` decorator)
|
||||
- [ ] `@event('keydown')` decorator in syntax.py
|
||||
- [ ] Parser recognizes @event and extracts event type
|
||||
- [ ] Codegen emits global event registration (not element-specific)
|
||||
- [ ] Runtime: `js_register_global_event(event_type, handler_name)`
|
||||
- [ ] JS passes event object to WASM with properties
|
||||
- [ ] WASM can access `event.key`, `event.ctrlKey`, etc.
|
||||
|
||||
#### C. Event Object Bridge
|
||||
- [ ] Define event object structure (dict/struct in WASM)
|
||||
- [ ] JS serializes event properties to WASM memory
|
||||
- [ ] WASM handler receives event pointer
|
||||
- [ ] Helper functions: `event_get_key()`, `event_get_ctrl_key()`, etc.
|
||||
|
||||
## Framework Improvements Needed
|
||||
|
||||
### 3. Reactive Attribute System (Beyond Classes)
|
||||
|
||||
Make ALL attributes reactive:
|
||||
```python
|
||||
disabled = signal(False)
|
||||
with Button(disabled=disabled):
|
||||
yield "Click me"
|
||||
|
||||
# When disabled changes, button.disabled updates automatically
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
- [ ] Track attribute dependencies on signals
|
||||
- [ ] Generate update functions for each reactive attribute
|
||||
- [ ] Runtime bridge: `js_set_attribute_bool()`, `js_set_attribute_int()`, etc.
|
||||
|
||||
### 4. Testing Infrastructure
|
||||
|
||||
**Problem**: No way to test Ritz code without manual browser testing
|
||||
|
||||
**Solution A: Python Interpreter Mode**
|
||||
```bash
|
||||
# Run Ritz components in pure Python for testing
|
||||
python3 -m ritz.test examples/highfive/HighFive.py
|
||||
|
||||
# Output:
|
||||
# ✓ HighFive renders correctly
|
||||
# ✓ increment() updates signal
|
||||
# ✓ Button has correct onclick handler
|
||||
```
|
||||
|
||||
Implementation:
|
||||
- [ ] Create `ritz/test_runtime.py` - Python VNode interpreter
|
||||
- [ ] Execute component functions, track signal changes
|
||||
- [ ] Assert on rendered VNode tree structure
|
||||
- [ ] Mock event handlers and verify signal updates
|
||||
|
||||
**Solution B: LLVM Interpreter**
|
||||
```bash
|
||||
# Execute LLVM IR directly (no WASM step)
|
||||
python3 -m ritz.test --llvm examples/highfive/HighFive.py
|
||||
|
||||
# Uses: lli (LLVM interpreter) or llvm-python bindings
|
||||
```
|
||||
|
||||
Implementation:
|
||||
- [ ] Mock JS bridge functions in C/LLVM
|
||||
- [ ] Execute LLVM IR with test assertions
|
||||
- [ ] Verify correct function calls and signal state
|
||||
|
||||
**Solution C: Headless Browser Tests**
|
||||
```bash
|
||||
# Full integration tests with Playwright/Puppeteer
|
||||
python3 -m ritz.test --browser examples/highfive/HighFive.py
|
||||
```
|
||||
|
||||
Implementation:
|
||||
- [ ] Playwright/Puppeteer test harness
|
||||
- [ ] Load WASM in headless browser
|
||||
- [ ] Verify DOM structure and interactions
|
||||
- [ ] Snapshot testing for visual regression
|
||||
|
||||
### 5. Test Coverage Requirements
|
||||
|
||||
**Unit Tests Needed**:
|
||||
- [ ] Parser: All AST node types
|
||||
- [ ] Parser: @component, @external, @event decorators
|
||||
- [ ] Codegen: Signal generation
|
||||
- [ ] Codegen: Event handler generation
|
||||
- [ ] Codegen: Reactive attributes
|
||||
- [ ] Codegen: Conditional rendering
|
||||
- [ ] Runtime: Signal updates
|
||||
- [ ] Runtime: Event dispatch
|
||||
- [ ] Runtime: DOM mutations
|
||||
|
||||
**Integration Tests Needed**:
|
||||
- [ ] Counter component (signals + events)
|
||||
- [ ] Tabs component (match/case conditionals)
|
||||
- [ ] Forms (input binding, validation)
|
||||
- [ ] Debug console (global events, reactive classes)
|
||||
|
||||
**Snapshot Tests**:
|
||||
- [ ] Generated LLVM IR (detect codegen regressions)
|
||||
- [ ] Generated WASM size (detect bloat)
|
||||
- [ ] Rendered HTML structure
|
||||
|
||||
## Code Quality & Consistency
|
||||
|
||||
### 6. Example App Consistency
|
||||
|
||||
Update all examples to use:
|
||||
- [ ] RitzMain() convention
|
||||
- [ ] Main.wasm output
|
||||
- [ ] template.html (no custom HTML)
|
||||
- [ ] Shared styles.css
|
||||
- [ ] Component-specific styles in <style> tag only
|
||||
- [ ] @external for any JS bridge functions
|
||||
- [ ] Consistent naming conventions
|
||||
|
||||
Examples to update:
|
||||
- [ ] `examples/tabs/Tabs.py` - Add RitzMain
|
||||
- [ ] `examples/textinput/TextInput.py` - Add RitzMain
|
||||
- [ ] `examples/toggle/Toggle.py` - Add RitzMain
|
||||
- [ ] All: Copy template.html
|
||||
- [ ] All: Remove duplicate styles
|
||||
|
||||
### 7. Documentation
|
||||
|
||||
- [ ] Update CLAUDE.md with @external decorator
|
||||
- [ ] Update CLAUDE.md with @event decorator (when implemented)
|
||||
- [ ] Document reactive attribute syntax
|
||||
- [ ] Testing guide (how to run tests)
|
||||
- [ ] Examples README with descriptions
|
||||
|
||||
## Architecture Questions to Resolve
|
||||
|
||||
### 8. Reactive Class Syntax
|
||||
|
||||
**Option 1: Lambda (Python-like)**
|
||||
```python
|
||||
with Div(class_=lambda: 'active' if is_active else 'inactive'):
|
||||
```
|
||||
Pros: Pythonic, clear dependency
|
||||
Cons: Lambdas need special handling in parser
|
||||
|
||||
**Option 2: Template strings (f-string-like)**
|
||||
```python
|
||||
with Div(class_=f"menu {('active' if is_open else '')}"):
|
||||
```
|
||||
Pros: Familiar syntax
|
||||
Cons: Hard to track dependencies, less clear
|
||||
|
||||
**Option 3: Dict with signal values**
|
||||
```python
|
||||
with Div(class_={'menu': True, 'active': is_open, 'disabled': is_disabled}):
|
||||
```
|
||||
Pros: Very clear dependencies, React-like
|
||||
Cons: Different from HTML class syntax
|
||||
|
||||
**Decision needed**: Which syntax to use?
|
||||
|
||||
### 9. Event Object Design
|
||||
|
||||
How should event handlers access event properties?
|
||||
|
||||
**Option 1: Parameter object (dict-like)**
|
||||
```python
|
||||
@event('keydown')
|
||||
def handle_keydown(event):
|
||||
if event['key'] == '`' and event['ctrlKey']:
|
||||
...
|
||||
```
|
||||
|
||||
**Option 2: External functions**
|
||||
```python
|
||||
@external
|
||||
def event_get_key() -> str:
|
||||
pass
|
||||
|
||||
@event('keydown')
|
||||
def handle_keydown():
|
||||
if event_get_key() == '`' and event_get_ctrl_key():
|
||||
...
|
||||
```
|
||||
|
||||
**Option 3: Global event context**
|
||||
```python
|
||||
@event('keydown')
|
||||
def handle_keydown():
|
||||
if current_event.key == '`' and current_event.ctrlKey:
|
||||
...
|
||||
```
|
||||
|
||||
**Decision needed**: Which approach?
|
||||
|
||||
## Success Criteria
|
||||
|
||||
Before moving to Phase 5 (was Phase 4), we must have:
|
||||
|
||||
- ✅ Debug console opens/closes with proper CSS transitions
|
||||
- ✅ Page content shifts LEFT (or stays in place) when console opens
|
||||
- ✅ Console toggle is 100% Ritz (signal + reactive class)
|
||||
- ✅ Ctrl+` registered declaratively with @event decorator
|
||||
- ✅ Event object accessible in WASM handlers
|
||||
- ✅ Test infrastructure in place (at least Python mode)
|
||||
- ✅ All examples updated to use RitzMain convention
|
||||
- ✅ Test coverage >70% for parser and codegen
|
||||
- ✅ Documentation updated
|
||||
- ✅ No JS DOM manipulation (except initial setup)
|
||||
|
||||
## Timeline Estimate
|
||||
|
||||
- **Reactive classes**: 2-3 hours (parser + codegen + runtime)
|
||||
- **@event decorator**: 3-4 hours (parser + codegen + runtime + event bridge)
|
||||
- **Event object bridge**: 2-3 hours (memory layout + JS serialization)
|
||||
- **Python test mode**: 4-5 hours (VNode interpreter + test harness)
|
||||
- **Unit tests**: 3-4 hours (parser, codegen tests)
|
||||
- **Example cleanup**: 1-2 hours (update 4 examples)
|
||||
- **Debug console refactor**: 1-2 hours (use reactive classes + @event)
|
||||
|
||||
**Total**: ~20-25 hours of focused work
|
||||
|
||||
## Notes
|
||||
|
||||
- The @external decorator is **beautiful** and should be the template
|
||||
- Everything should feel declarative and Pythonic
|
||||
- WASM should handle logic, JS should only bridge
|
||||
- Tests are critical - no more manual browser testing for every change
|
||||
- Keep examples simple and pedagogical
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 315 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 327 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@@ -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 `<script type="text/ritz">` loader
|
||||
- [ ] Parse inline Ritz code in browser
|
||||
- [ ] Compile to WASM on-the-fly
|
||||
- [ ] **✅ MILESTONE: Zero build step - write Python in HTML!**
|
||||
|
||||
### More Examples
|
||||
- [ ] TodoMVC example
|
||||
- [ ] Form with multiple inputs
|
||||
- [ ] Nested components example
|
||||
- [ ] List rendering with iteration
|
||||
|
||||
### Better Signal-DOM Binding
|
||||
- [ ] Track signal-to-DOM bindings during render
|
||||
- [ ] Efficient updates (only update bound elements)
|
||||
- [ ] Proper f-string interpolation with signals
|
||||
|
||||
### Testing
|
||||
- [ ] Playwright browser tests
|
||||
- [ ] Test HelloWorld renders correctly
|
||||
- [ ] Test HighFive counter increments
|
||||
- [ ] CI/CD pipeline
|
||||
|
||||
### Advanced Features
|
||||
- [ ] Component props/args
|
||||
- [ ] Computed values (derived signals)
|
||||
- [ ] Conditional rendering (if/else)
|
||||
- [ ] List rendering (for loops with keys)
|
||||
- [ ] Nested components (yield from)
|
||||
- [ ] DWARF debug symbols for Python debugging
|
||||
- [ ] Source maps (WASM → Python lines)
|
||||
|
||||
---
|
||||
|
||||
## Current Focus
|
||||
|
||||
**JUST COMPLETED:** Full reactive framework with signals in WASM!
|
||||
|
||||
**NEXT UP:** Commit current work, then explore:
|
||||
1. More complex examples
|
||||
2. `<script type="text/ritz">` in-browser compilation
|
||||
3. Better signal tracking and f-string support
|
||||
@@ -0,0 +1,247 @@
|
||||
# Type System & String Support Roadmap
|
||||
|
||||
This document outlines what needs to be implemented for proper type support in Ritz, driven by the TextInput example which exposes current limitations.
|
||||
|
||||
## Current State: Everything is i32
|
||||
|
||||
All signals, parameters, and values are treated as `i32`:
|
||||
```llvm
|
||||
@signal_MyComponent_text = global i32 0 ; String stored as i32!
|
||||
@signal_MyComponent_count = global i32 0 ; OK
|
||||
@signal_MyComponent_active = global i32 0 ; Should be i1
|
||||
```
|
||||
|
||||
## Exposed Limitations (from TextInput example)
|
||||
|
||||
1. **String signals don't work**
|
||||
- `text = signal("")` compiles to `i32 0`, not a string
|
||||
- No way to store actual string data
|
||||
|
||||
2. **F-string interpolation incomplete**
|
||||
- `f"Count: {count}"` renders as just the signal value `"0"`
|
||||
- Missing string concatenation in LLVM IR
|
||||
- See `llvm_codegen_signals.py:1190-1205`
|
||||
|
||||
3. **No oninput event**
|
||||
- Only `onclick` supported
|
||||
- Need generalized event system for `oninput`, `onchange`, etc.
|
||||
|
||||
4. **No event value passing**
|
||||
- `onclick` doesn't pass event data
|
||||
- `oninput` needs to pass `event.target.value` to handler
|
||||
|
||||
5. **No string operations**
|
||||
- `len(text)` not implemented
|
||||
- No concat, slice, format, etc.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Type Hints in Parser ✅ (Parser already extracts them!)
|
||||
|
||||
Python's `ast` module gives us type annotations for free:
|
||||
```python
|
||||
def Profile(name: str, age: int, active: bool):
|
||||
pass
|
||||
```
|
||||
|
||||
**Parser changes:**
|
||||
- Extract type hints from `ast.arg.annotation`
|
||||
- Store in IR: `{"name": "age", "type": "int"}`
|
||||
- Handle `Signal[T]`, `Callable[[Args], Return]` generics
|
||||
|
||||
### Phase 2: Typed Signals
|
||||
|
||||
**Goal:** Different LLVM types based on initial value/annotation
|
||||
```python
|
||||
count: Signal[int] = signal(0) # → global i32 0
|
||||
name: Signal[str] = signal("") # → global i8* null
|
||||
active: Signal[bool] = signal(True) # → global i1 true
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
```python
|
||||
# llvm_codegen_signals.py:_emit_signal_globals()
|
||||
def _emit_signal_globals(self, component: dict[str, Any]) -> None:
|
||||
for signal_name, signal_info in self.signals.items():
|
||||
signal_type = self._infer_signal_type(signal_info)
|
||||
initial_value = self._format_initial_value(signal_info, signal_type)
|
||||
|
||||
if signal_type == "str":
|
||||
self.lines.append(f"@signal_{component_name}_{signal_name} = global i8* {initial_value}")
|
||||
elif signal_type == "bool":
|
||||
self.lines.append(f"@signal_{component_name}_{signal_name} = global i1 {initial_value}")
|
||||
else: # int
|
||||
self.lines.append(f"@signal_{component_name}_{signal_name} = global i32 {initial_value}")
|
||||
```
|
||||
|
||||
**String storage options:**
|
||||
1. **Global string pool** - Immutable strings in WASM memory (simplest, start here)
|
||||
2. **Dynamic allocation** - malloc/free for mutable strings (later)
|
||||
|
||||
### Phase 3: String Interpolation in Text Nodes
|
||||
|
||||
**Goal:** `f"Count: {count}"` → "Count: 42" in DOM
|
||||
|
||||
**Current code** (line 1190-1205):
|
||||
```python
|
||||
elif value["type"] == "fstring":
|
||||
# TODO: Implement proper string concatenation
|
||||
# Currently only emits signal value, loses prefix/suffix
|
||||
```
|
||||
|
||||
**Needed:**
|
||||
```python
|
||||
def _emit_fstring_text(self, fstring_expr: dict) -> str:
|
||||
"""Emit f-string as concatenated text node."""
|
||||
parts = []
|
||||
|
||||
for part in fstring_expr["parts"]:
|
||||
if part["type"] == "constant":
|
||||
# Static string part
|
||||
parts.append(self._get_or_create_string(part["value"]))
|
||||
elif part["type"] == "formatted_value":
|
||||
# Signal value - convert to string
|
||||
signal_var = self._emit_expr(part["value"])
|
||||
str_var = self._new_var()
|
||||
self.lines.append(f" %{str_var} = call i8* @int_to_str(i32 {signal_var})")
|
||||
parts.append(f"%{str_var}")
|
||||
|
||||
# Concatenate all parts
|
||||
result = self._concat_strings(parts)
|
||||
text_var = self._new_var()
|
||||
self.lines.append(f" %{text_var} = call i32 @js_create_text(i8* {result})")
|
||||
return text_var
|
||||
```
|
||||
|
||||
**New JS bridge functions needed:**
|
||||
```llvm
|
||||
declare i8* @int_to_str(i32) ; Convert i32 to string
|
||||
declare i8* @str_concat(i8*, i8*) ; Concatenate two strings
|
||||
declare i32 @str_len(i8*) ; String length
|
||||
```
|
||||
|
||||
### Phase 4: Generalized Event System
|
||||
|
||||
**Goal:** Support `oninput`, `onchange`, `onkeypress`, etc.
|
||||
|
||||
**Current:** Only `onclick` hardcoded
|
||||
```python
|
||||
if prop_name == "onclick":
|
||||
# Special handling
|
||||
```
|
||||
|
||||
**Needed:**
|
||||
```python
|
||||
EVENT_TYPES = {
|
||||
"onclick": {"js_name": "click", "signature": "void ()"},
|
||||
"oninput": {"js_name": "input", "signature": "void (i8*)"}, # Passes value
|
||||
"onchange": {"js_name": "change", "signature": "void (i8*)"},
|
||||
"onkeypress": {"js_name": "keypress", "signature": "void (i32)"}, # Passes keycode
|
||||
}
|
||||
|
||||
def _emit_element_props(self, elem_var: str, props: dict):
|
||||
for prop_name, prop_value in props.items():
|
||||
if prop_name.startswith("on"):
|
||||
event_type = EVENT_TYPES.get(prop_name)
|
||||
self._emit_event_binding(elem_var, event_type, prop_value)
|
||||
```
|
||||
|
||||
**Runtime changes** (ritz-runtime.js):
|
||||
```javascript
|
||||
js_bind_event(elemId, eventNamePtr, handlerNamePtr, passValue) {
|
||||
const eventName = this.readString(eventNamePtr);
|
||||
const handlerName = this.readString(handlerNamePtr);
|
||||
const handler = this.wasmInstance.exports[handlerName];
|
||||
|
||||
element.addEventListener(eventName, (event) => {
|
||||
if (passValue && event.target) {
|
||||
// For oninput, pass the input value to WASM
|
||||
const valuePtr = this.createWasmString(event.target.value);
|
||||
handler(valuePtr);
|
||||
} else {
|
||||
handler();
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 5: Component Parameter Types
|
||||
|
||||
**Goal:** Type-checked component calls
|
||||
```python
|
||||
@component
|
||||
def Greeting(name: str, age: int):
|
||||
yield f"{name} is {age} years old"
|
||||
|
||||
# Type-checked call
|
||||
yield Greeting(name="Alice", age=30) # ✓
|
||||
yield Greeting(name=42, age="Bob") # ✗ Type error
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
- Parser extracts parameter types
|
||||
- Codegen validates argument types at call site
|
||||
- Emit typed LLVM function signatures:
|
||||
```llvm
|
||||
define i32 @render_Greeting(i8* %name, i32 %age)
|
||||
```
|
||||
|
||||
### Phase 6: Delegate/Callback Types
|
||||
|
||||
**Goal:** Type-safe function callbacks
|
||||
```python
|
||||
from ritz.types import Delegate
|
||||
|
||||
@component
|
||||
def Button(on_click: Delegate[[], None], label: str):
|
||||
with Button(onclick=on_click):
|
||||
yield label
|
||||
```
|
||||
|
||||
**LLVM representation:**
|
||||
```llvm
|
||||
define i32 @render_Button(void ()* %on_click, i8* %label)
|
||||
```
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
**Backwards Compatibility:**
|
||||
- Support untyped code (default to i32)
|
||||
- Gradual opt-in with type hints
|
||||
- Add warnings for type mismatches
|
||||
|
||||
**Testing:**
|
||||
- Update existing tests to use types
|
||||
- Add type-specific test suites
|
||||
- Test type inference vs explicit hints
|
||||
|
||||
## Success Criteria
|
||||
|
||||
When this is complete, the TextInput example should work:
|
||||
```python
|
||||
@component
|
||||
def TextInput():
|
||||
text: Signal[str] = signal("")
|
||||
|
||||
def on_input(value: str):
|
||||
text = value
|
||||
|
||||
with Input(type_="text", oninput=on_input):
|
||||
pass
|
||||
|
||||
with P():
|
||||
yield f"You typed: {text}" # ✓ Full interpolation
|
||||
|
||||
with P():
|
||||
yield f"Length: {len(text)}" # ✓ String operations
|
||||
```
|
||||
|
||||
**Result:**
|
||||
- Reactive text input with live updates ✓
|
||||
- Proper string storage and manipulation ✓
|
||||
- Type-safe event handlers ✓
|
||||
- Clean, Pythonic syntax ✓
|
||||
|
||||
---
|
||||
|
||||
This is the foundation for a production-ready type system. Let's build it! 🚀
|
||||
Binary file not shown.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HelloWorld - Ritz Framework</title>
|
||||
<link rel="stylesheet" href="../runtime/ritz-debug.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
#app {
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 40px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
#app p {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<div id="ritz-debug" class="ritz-debug hidden">
|
||||
<div class="debug-header">
|
||||
<h3>🎭 Ritz Debug</h3>
|
||||
<button onclick="ritzDebug.toggle()">×</button>
|
||||
</div>
|
||||
<div id="status" class="status">Loading...</div>
|
||||
<div class="debug-content">
|
||||
<div class="debug-section">
|
||||
<h4>Component</h4>
|
||||
<pre>HelloWorld.py</pre>
|
||||
</div>
|
||||
<div class="debug-section">
|
||||
<h4>Pipeline</h4>
|
||||
<pre>Python → AST → IR → LLVM → WASM</pre>
|
||||
</div>
|
||||
<div class="debug-section">
|
||||
<h4>Console Commands</h4>
|
||||
<pre>ritzRuntime.getLog()
|
||||
ritzInstance.exports</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../runtime/ritz-runtime.js"></script>
|
||||
<script src="../runtime/ritz-debug.js"></script>
|
||||
<script>
|
||||
// Wait for DOM to be ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new RitzLoader().load('HelloWorld.wasm', 'HelloWorld');
|
||||
});
|
||||
} else {
|
||||
new RitzLoader().load('HelloWorld.wasm', 'HelloWorld');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
HelloWorld component - The simplest possible Ritz component.
|
||||
|
||||
This demonstrates a static <p> tag with text content.
|
||||
"""
|
||||
|
||||
from ritz.syntax import *
|
||||
|
||||
|
||||
@component
|
||||
def HelloWorld():
|
||||
"""Render a simple paragraph with 'Hello World'."""
|
||||
with P():
|
||||
yield "Hello World"
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HighFive Counter - Ritz Framework</title>
|
||||
<link rel="stylesheet" href="../runtime/styles.css">
|
||||
<style>
|
||||
/* Component-specific styles */
|
||||
.high-five-app {
|
||||
text-align: center;
|
||||
}
|
||||
.high-five-app p {
|
||||
font-size: 18px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.high-five-app .count {
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
color: #0066cc;
|
||||
margin: 30px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="status"></div>
|
||||
|
||||
<script src="../runtime/ritz-runtime.js"></script>
|
||||
<script>
|
||||
// Load Main.wasm with debug console enabled
|
||||
// The debug console is itself written in Ritz!
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new RitzLoader({ debug: true }).load('Main.wasm');
|
||||
});
|
||||
} else {
|
||||
new RitzLoader({ debug: true }).load('Main.wasm');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,104 @@
|
||||
; Generated by Ritz Framework with Signals
|
||||
; Components: HighFive
|
||||
|
||||
; String constants
|
||||
@.str.0 = private unnamed_addr constant [13 x i8] c"High fives: \00"
|
||||
@.str.1 = private unnamed_addr constant [4 x i8] c"div\00"
|
||||
@.str.2 = private unnamed_addr constant [6 x i8] c"class\00"
|
||||
@.str.3 = private unnamed_addr constant [14 x i8] c"high-five-app\00"
|
||||
@.str.4 = private unnamed_addr constant [2 x i8] c"p\00"
|
||||
@.str.5 = private unnamed_addr constant [25 x i8] c"Give me a high five! ✋\00"
|
||||
@.str.6 = private unnamed_addr constant [6 x i8] c"count\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"
|
||||
|
||||
; JavaScript bridge function declarations
|
||||
declare i32 @js_create_element(i8*)
|
||||
declare i32 @js_create_text(i8*)
|
||||
declare i32 @js_create_text_with_int(i8*, i32)
|
||||
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*)
|
||||
declare void @js_update_text_with_int(i32, i8*, i32)
|
||||
declare i32 @js_create_marker()
|
||||
declare void @js_replace_at_marker(i32, i32, i32)
|
||||
|
||||
; Signal storage for HighFive (reactive state in WASM memory)
|
||||
@signal_HighFive_high_fives = global i32 0
|
||||
|
||||
; Reactive text node storage
|
||||
@reactive_text_0_node = global i32 0
|
||||
|
||||
; Signal helper functions
|
||||
define i32 @get_high_fives() {
|
||||
entry:
|
||||
%val = load i32, i32* @signal_HighFive_high_fives
|
||||
ret i32 %val
|
||||
}
|
||||
|
||||
define void @set_high_fives(i32 %newval) {
|
||||
entry:
|
||||
store i32 %newval, i32* @signal_HighFive_high_fives
|
||||
; TODO: Trigger DOM update callback
|
||||
ret void
|
||||
}
|
||||
|
||||
; Event handler: increment
|
||||
define void @increment() {
|
||||
entry:
|
||||
%var0 = load i32, i32* @signal_HighFive_high_fives
|
||||
%var1 = add i32 %var0, 1
|
||||
store i32 %var1, i32* @signal_HighFive_high_fives
|
||||
%var2 = load i32, i32* @reactive_text_0_node
|
||||
%var3 = getelementptr inbounds [13 x i8], [13 x i8]* @.str.0, i32 0, i32 0
|
||||
%var4 = load i32, i32* @signal_HighFive_high_fives
|
||||
call void @js_update_text_with_int(i32 %var2, i8* %var3, i32 %var4)
|
||||
ret void
|
||||
}
|
||||
|
||||
; Component render function
|
||||
define i32 @render_HighFive() {
|
||||
entry:
|
||||
%var6 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.1, i32 0, i32 0
|
||||
%var5 = call i32 @js_create_element(i8* %var6)
|
||||
%var7 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.2, i32 0, i32 0
|
||||
%var8 = getelementptr inbounds [14 x i8], [14 x i8]* @.str.3, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var5, i8* %var7, i8* %var8)
|
||||
%var10 = getelementptr inbounds [2 x i8], [2 x i8]* @.str.4, i32 0, i32 0
|
||||
%var9 = call i32 @js_create_element(i8* %var10)
|
||||
%var11 = call i32 @js_create_text(i8* getelementptr inbounds ([25 x i8], [25 x i8]* @.str.5, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var9, i32 %var11)
|
||||
call void @js_append_child(i32 %var5, i32 %var9)
|
||||
%var13 = getelementptr inbounds [2 x i8], [2 x i8]* @.str.4, i32 0, i32 0
|
||||
%var12 = call i32 @js_create_element(i8* %var13)
|
||||
%var14 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.2, i32 0, i32 0
|
||||
%var15 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.6, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var12, i8* %var14, i8* %var15)
|
||||
%var16 = getelementptr inbounds [13 x i8], [13 x i8]* @.str.0, i32 0, i32 0
|
||||
%var17 = load i32, i32* @signal_HighFive_high_fives
|
||||
%var18 = call i32 @js_create_text_with_int(i8* %var16, i32 %var17)
|
||||
store i32 %var18, i32* @reactive_text_0_node
|
||||
call void @js_append_child(i32 %var12, i32 %var18)
|
||||
call void @js_append_child(i32 %var5, i32 %var12)
|
||||
%var20 = getelementptr inbounds [7 x i8], [7 x i8]* @.str.7, i32 0, i32 0
|
||||
%var19 = call i32 @js_create_element(i8* %var20)
|
||||
%var21 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.8, i32 0, i32 0
|
||||
%var22 = getelementptr inbounds [10 x i8], [10 x i8]* @.str.9, i32 0, i32 0
|
||||
call void @js_bind_event(i32 %var19, i8* %var21, i8* %var22)
|
||||
%var23 = 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 %var19, i32 %var23)
|
||||
call void @js_append_child(i32 %var5, i32 %var19)
|
||||
ret i32 %var5
|
||||
}
|
||||
|
||||
; Main entry point
|
||||
define void @main() {
|
||||
entry:
|
||||
%root = call i32 @render_HighFive()
|
||||
call void @js_set_root(i32 %root)
|
||||
ret void
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
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!"
|
||||
|
||||
|
||||
@component
|
||||
def RitzMain():
|
||||
"""Entry point - required for all Ritz applications."""
|
||||
yield HighFive()
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,114 @@
|
||||
; Generated by Ritz Framework with Signals
|
||||
; Components: HighFive, RitzMain
|
||||
|
||||
; String constants
|
||||
@.str.0 = private unnamed_addr constant [13 x i8] c"High fives: \00"
|
||||
@.str.1 = private unnamed_addr constant [4 x i8] c"div\00"
|
||||
@.str.2 = private unnamed_addr constant [6 x i8] c"class\00"
|
||||
@.str.3 = private unnamed_addr constant [14 x i8] c"high-five-app\00"
|
||||
@.str.4 = private unnamed_addr constant [2 x i8] c"p\00"
|
||||
@.str.5 = private unnamed_addr constant [25 x i8] c"Give me a high five! ✋\00"
|
||||
@.str.6 = private unnamed_addr constant [6 x i8] c"count\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"
|
||||
|
||||
; JavaScript bridge function declarations
|
||||
declare i32 @js_create_element(i8*)
|
||||
declare i32 @js_create_text(i8*)
|
||||
declare i32 @js_create_text_with_int(i8*, i32)
|
||||
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*)
|
||||
declare void @js_update_text_with_int(i32, i8*, i32)
|
||||
declare i32 @js_create_marker()
|
||||
declare void @js_replace_at_marker(i32, i32, i32)
|
||||
|
||||
; Signal storage for HighFive (reactive state in WASM memory)
|
||||
@signal_HighFive_high_fives = global i32 0
|
||||
|
||||
; Reactive text node storage for HighFive
|
||||
@reactive_text_HighFive_0_node = global i32 0
|
||||
|
||||
; Signal helper functions
|
||||
define i32 @get_high_fives() {
|
||||
entry:
|
||||
%val = load i32, i32* @signal_HighFive_high_fives
|
||||
ret i32 %val
|
||||
}
|
||||
|
||||
define void @set_high_fives(i32 %newval) {
|
||||
entry:
|
||||
store i32 %newval, i32* @signal_HighFive_high_fives
|
||||
; TODO: Trigger DOM update callback
|
||||
ret void
|
||||
}
|
||||
|
||||
; Event handler: increment
|
||||
define void @increment() {
|
||||
entry:
|
||||
%var0 = load i32, i32* @signal_HighFive_high_fives
|
||||
%var1 = add i32 %var0, 1
|
||||
store i32 %var1, i32* @signal_HighFive_high_fives
|
||||
%var2 = load i32, i32* @reactive_text_HighFive_0_node
|
||||
%var3 = getelementptr inbounds [13 x i8], [13 x i8]* @.str.0, i32 0, i32 0
|
||||
%var4 = load i32, i32* @signal_HighFive_high_fives
|
||||
call void @js_update_text_with_int(i32 %var2, i8* %var3, i32 %var4)
|
||||
ret void
|
||||
}
|
||||
|
||||
; Component render function
|
||||
define i32 @render_HighFive() {
|
||||
entry:
|
||||
%var6 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.1, i32 0, i32 0
|
||||
%var5 = call i32 @js_create_element(i8* %var6)
|
||||
%var7 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.2, i32 0, i32 0
|
||||
%var8 = getelementptr inbounds [14 x i8], [14 x i8]* @.str.3, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var5, i8* %var7, i8* %var8)
|
||||
%var10 = getelementptr inbounds [2 x i8], [2 x i8]* @.str.4, i32 0, i32 0
|
||||
%var9 = call i32 @js_create_element(i8* %var10)
|
||||
%var11 = call i32 @js_create_text(i8* getelementptr inbounds ([25 x i8], [25 x i8]* @.str.5, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var9, i32 %var11)
|
||||
call void @js_append_child(i32 %var5, i32 %var9)
|
||||
%var13 = getelementptr inbounds [2 x i8], [2 x i8]* @.str.4, i32 0, i32 0
|
||||
%var12 = call i32 @js_create_element(i8* %var13)
|
||||
%var14 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.2, i32 0, i32 0
|
||||
%var15 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.6, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var12, i8* %var14, i8* %var15)
|
||||
%var16 = getelementptr inbounds [13 x i8], [13 x i8]* @.str.0, i32 0, i32 0
|
||||
%var17 = load i32, i32* @signal_HighFive_high_fives
|
||||
%var18 = call i32 @js_create_text_with_int(i8* %var16, i32 %var17)
|
||||
store i32 %var18, i32* @reactive_text_HighFive_0_node
|
||||
call void @js_append_child(i32 %var12, i32 %var18)
|
||||
call void @js_append_child(i32 %var5, i32 %var12)
|
||||
%var20 = getelementptr inbounds [7 x i8], [7 x i8]* @.str.7, i32 0, i32 0
|
||||
%var19 = call i32 @js_create_element(i8* %var20)
|
||||
%var21 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.8, i32 0, i32 0
|
||||
%var22 = getelementptr inbounds [10 x i8], [10 x i8]* @.str.9, i32 0, i32 0
|
||||
call void @js_bind_event(i32 %var19, i8* %var21, i8* %var22)
|
||||
%var23 = 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 %var19, i32 %var23)
|
||||
call void @js_append_child(i32 %var5, i32 %var19)
|
||||
ret i32 %var5
|
||||
}
|
||||
|
||||
; Reactive text node storage for RitzMain
|
||||
@reactive_text_RitzMain_0_node = global i32 0
|
||||
|
||||
; Component render function
|
||||
define i32 @render_RitzMain() {
|
||||
entry:
|
||||
%var24 = call i32 @render_HighFive()
|
||||
ret i32 %var24
|
||||
}
|
||||
|
||||
; Main entry point
|
||||
define void @main() {
|
||||
entry:
|
||||
%root = call i32 @render_RitzMain()
|
||||
call void @js_set_root(i32 %root)
|
||||
ret void
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,215 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ritz HelloWorld - Python → WASM</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.header {
|
||||
border-bottom: 2px solid #333;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.status {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background: #e8f4f8;
|
||||
border-left: 4px solid #0066cc;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
.error {
|
||||
background: #fee;
|
||||
border-left-color: #c00;
|
||||
color: #c00;
|
||||
}
|
||||
.success {
|
||||
background: #efe;
|
||||
border-left-color: #0a0;
|
||||
color: #060;
|
||||
}
|
||||
#app {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
min-height: 100px;
|
||||
}
|
||||
.pipeline {
|
||||
margin: 20px 0;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>🎭 Ritz Framework</h1>
|
||||
<p><strong>Python → LLVM IR → WebAssembly</strong></p>
|
||||
<div class="pipeline">
|
||||
HelloWorld.py → ast.parse() → Ritz IR → LLVM IR → clang → WASM
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status" id="status">⏳ Initializing...</div>
|
||||
|
||||
<div id="app"></div>
|
||||
|
||||
<script>
|
||||
// Ritz Runtime - provides DOM bridge for WASM
|
||||
class RitzRuntime {
|
||||
constructor(rootElement) {
|
||||
this.rootElement = rootElement;
|
||||
this.nodes = new Map();
|
||||
this.nextNodeId = 1;
|
||||
this.log = [];
|
||||
}
|
||||
|
||||
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.push(`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.push(`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.push(`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.push(`setAttribute(${elemId}, "${name}", "${value}")`);
|
||||
}
|
||||
}
|
||||
|
||||
js_set_root(elemId) {
|
||||
const element = this.nodes.get(elemId);
|
||||
if (element) {
|
||||
this.rootElement.innerHTML = '';
|
||||
this.rootElement.appendChild(element);
|
||||
this.log.push(`setRoot(${elemId})`);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
getLog() {
|
||||
return this.log;
|
||||
}
|
||||
}
|
||||
|
||||
// Global runtime for debugging
|
||||
let runtime = null;
|
||||
let wasmInstance = null;
|
||||
|
||||
// Load and run WASM
|
||||
async function main() {
|
||||
const status = document.getElementById('status');
|
||||
const appRoot = document.getElementById('app');
|
||||
|
||||
try {
|
||||
status.textContent = '⏳ Creating runtime...';
|
||||
runtime = new RitzRuntime(appRoot);
|
||||
|
||||
status.textContent = '⏳ Loading HelloWorld.wasm...';
|
||||
const response = await fetch('HelloWorld.wasm');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load WASM: ${response.status}`);
|
||||
}
|
||||
|
||||
const wasmBytes = await response.arrayBuffer();
|
||||
|
||||
status.textContent = '⏳ Instantiating WASM module...';
|
||||
|
||||
// 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),
|
||||
}
|
||||
};
|
||||
|
||||
const { instance } = await WebAssembly.instantiate(wasmBytes, imports);
|
||||
wasmInstance = instance; // Store globally for debugging
|
||||
|
||||
// Give runtime access to WASM memory
|
||||
runtime.setMemory(instance.exports.memory);
|
||||
|
||||
status.textContent = '⏳ Rendering component...';
|
||||
|
||||
// Call the render function
|
||||
instance.exports.render_HelloWorld();
|
||||
|
||||
status.textContent = '✅ HelloWorld rendered successfully!';
|
||||
status.className = 'status success';
|
||||
|
||||
// Log the operations
|
||||
console.log('Runtime operations:', runtime.getLog());
|
||||
console.log('🎉 WASM successfully loaded and executed!');
|
||||
console.log('Debug commands:');
|
||||
console.log(' runtime.getLog() - View DOM operations');
|
||||
console.log(' wasmInstance.exports - View WASM exports');
|
||||
console.log(' wasmInstance.exports.render_HelloWorld() - Re-render');
|
||||
|
||||
} catch (error) {
|
||||
status.textContent = `❌ Error: ${error.message}`;
|
||||
status.className = 'status error';
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Run when page loads
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', main);
|
||||
} else {
|
||||
main();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
; Generated by Ritz Framework with Signals
|
||||
; Components: DebugConsole, RitzMain
|
||||
|
||||
; 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 [18 x i8] c"ritz-debug hidden\00"
|
||||
@.str.3 = private unnamed_addr constant [3 x i8] c"id\00"
|
||||
@.str.4 = private unnamed_addr constant [11 x i8] c"ritz-debug\00"
|
||||
@.str.5 = private unnamed_addr constant [13 x i8] c"debug-header\00"
|
||||
@.str.6 = private unnamed_addr constant [3 x i8] c"h3\00"
|
||||
@.str.7 = private unnamed_addr constant [16 x i8] c"🎭 Ritz Debug\00"
|
||||
@.str.8 = private unnamed_addr constant [7 x i8] c"button\00"
|
||||
@.str.9 = private unnamed_addr constant [6 x i8] c"click\00"
|
||||
@.str.10 = private unnamed_addr constant [7 x i8] c"toggle\00"
|
||||
@.str.11 = private unnamed_addr constant [3 x i8] c"×\00"
|
||||
@.str.12 = private unnamed_addr constant [15 x i8] c"status success\00"
|
||||
@.str.13 = private unnamed_addr constant [7 x i8] c"status\00"
|
||||
@.str.14 = private unnamed_addr constant [17 x i8] c"✅ Main loaded!\00"
|
||||
@.str.15 = private unnamed_addr constant [14 x i8] c"debug-content\00"
|
||||
@.str.16 = private unnamed_addr constant [14 x i8] c"debug-section\00"
|
||||
@.str.17 = private unnamed_addr constant [3 x i8] c"h4\00"
|
||||
@.str.18 = private unnamed_addr constant [10 x i8] c"COMPONENT\00"
|
||||
@.str.19 = private unnamed_addr constant [4 x i8] c"pre\00"
|
||||
@.str.20 = private unnamed_addr constant [32 x i8] c"Main.wasm (Multi-Part Switches)\00"
|
||||
@.str.21 = private unnamed_addr constant [9 x i8] c"FEATURES\00"
|
||||
@.str.22 = private unnamed_addr constant [27 x i8] c"✓ Python source to WASM
|
||||
\00"
|
||||
@.str.23 = private unnamed_addr constant [32 x i8] c"✓ Reactive signals (no vDOM)
|
||||
\00"
|
||||
@.str.24 = private unnamed_addr constant [34 x i8] c"✓ Conditional CSR (match/case)
|
||||
\00"
|
||||
@.str.25 = private unnamed_addr constant [20 x i8] c"✓ Event handlers
|
||||
\00"
|
||||
@.str.26 = private unnamed_addr constant [32 x i8] c"✓ Self-hosted debug console!
|
||||
\00"
|
||||
@.str.27 = private unnamed_addr constant [14 x i8] c"RUNTIME STATS\00"
|
||||
@.str.28 = private unnamed_addr constant [32 x i8] c"Memory: JavaScript will update
|
||||
\00"
|
||||
@.str.29 = private unnamed_addr constant [33 x i8] c"Signals: JavaScript will update
|
||||
\00"
|
||||
@.str.30 = private unnamed_addr constant [35 x i8] c"DOM nodes: JavaScript will update
|
||||
\00"
|
||||
@.str.31 = private unnamed_addr constant [17 x i8] c"CONSOLE COMMANDS\00"
|
||||
@.str.32 = private unnamed_addr constant [22 x i8] c"ritzRuntime.getLog()
|
||||
\00"
|
||||
@.str.33 = private unnamed_addr constant [24 x i8] c"ritzRuntime.nodes.size
|
||||
\00"
|
||||
@.str.34 = private unnamed_addr constant [22 x i8] c"ritzInstance.exports
|
||||
\00"
|
||||
@.str.35 = private unnamed_addr constant [18 x i8] c"debug-toggle-hint\00"
|
||||
@.str.36 = private unnamed_addr constant [29 x i8] c"Press Ctrl+` to toggle debug\00"
|
||||
|
||||
; JavaScript bridge function declarations
|
||||
declare i32 @js_create_element(i8*)
|
||||
declare i32 @js_create_text(i8*)
|
||||
declare i32 @js_create_text_with_int(i8*, i32)
|
||||
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*)
|
||||
declare void @js_update_text_with_int(i32, i8*, i32)
|
||||
declare i32 @js_create_marker()
|
||||
declare void @js_replace_at_marker(i32, i32, i32)
|
||||
|
||||
; External functions (@external decorator)
|
||||
declare void @js_toggle_debug_console()
|
||||
|
||||
; Event handler: toggle
|
||||
define void @toggle() {
|
||||
entry:
|
||||
call void @js_toggle_debug_console()
|
||||
ret void
|
||||
}
|
||||
|
||||
; Component render function
|
||||
define i32 @render_DebugConsole() {
|
||||
entry:
|
||||
%var1 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var0 = call i32 @js_create_element(i8* %var1)
|
||||
%var2 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.1, i32 0, i32 0
|
||||
%var3 = getelementptr inbounds [18 x i8], [18 x i8]* @.str.2, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var0, i8* %var2, i8* %var3)
|
||||
%var4 = getelementptr inbounds [3 x i8], [3 x i8]* @.str.3, i32 0, i32 0
|
||||
%var5 = getelementptr inbounds [11 x i8], [11 x i8]* @.str.4, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var0, i8* %var4, i8* %var5)
|
||||
%var7 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var6 = call i32 @js_create_element(i8* %var7)
|
||||
%var8 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.1, i32 0, i32 0
|
||||
%var9 = getelementptr inbounds [13 x i8], [13 x i8]* @.str.5, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var6, i8* %var8, i8* %var9)
|
||||
%var11 = getelementptr inbounds [3 x i8], [3 x i8]* @.str.6, i32 0, i32 0
|
||||
%var10 = call i32 @js_create_element(i8* %var11)
|
||||
%var12 = call i32 @js_create_text(i8* getelementptr inbounds ([16 x i8], [16 x i8]* @.str.7, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var10, i32 %var12)
|
||||
call void @js_append_child(i32 %var6, i32 %var10)
|
||||
%var14 = getelementptr inbounds [7 x i8], [7 x i8]* @.str.8, i32 0, i32 0
|
||||
%var13 = call i32 @js_create_element(i8* %var14)
|
||||
%var15 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.9, i32 0, i32 0
|
||||
%var16 = getelementptr inbounds [7 x i8], [7 x i8]* @.str.10, i32 0, i32 0
|
||||
call void @js_bind_event(i32 %var13, i8* %var15, i8* %var16)
|
||||
%var17 = call i32 @js_create_text(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.11, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var13, i32 %var17)
|
||||
call void @js_append_child(i32 %var6, i32 %var13)
|
||||
call void @js_append_child(i32 %var0, i32 %var6)
|
||||
%var19 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var18 = call i32 @js_create_element(i8* %var19)
|
||||
%var20 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.1, i32 0, i32 0
|
||||
%var21 = getelementptr inbounds [15 x i8], [15 x i8]* @.str.12, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var18, i8* %var20, i8* %var21)
|
||||
%var22 = getelementptr inbounds [3 x i8], [3 x i8]* @.str.3, i32 0, i32 0
|
||||
%var23 = getelementptr inbounds [7 x i8], [7 x i8]* @.str.13, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var18, i8* %var22, i8* %var23)
|
||||
%var24 = call i32 @js_create_text(i8* getelementptr inbounds ([17 x i8], [17 x i8]* @.str.14, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var18, i32 %var24)
|
||||
call void @js_append_child(i32 %var0, i32 %var18)
|
||||
%var26 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var25 = call i32 @js_create_element(i8* %var26)
|
||||
%var27 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.1, i32 0, i32 0
|
||||
%var28 = getelementptr inbounds [14 x i8], [14 x i8]* @.str.15, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var25, i8* %var27, i8* %var28)
|
||||
%var30 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var29 = call i32 @js_create_element(i8* %var30)
|
||||
%var31 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.1, i32 0, i32 0
|
||||
%var32 = getelementptr inbounds [14 x i8], [14 x i8]* @.str.16, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var29, i8* %var31, i8* %var32)
|
||||
%var34 = getelementptr inbounds [3 x i8], [3 x i8]* @.str.17, i32 0, i32 0
|
||||
%var33 = call i32 @js_create_element(i8* %var34)
|
||||
%var35 = call i32 @js_create_text(i8* getelementptr inbounds ([10 x i8], [10 x i8]* @.str.18, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var33, i32 %var35)
|
||||
call void @js_append_child(i32 %var29, i32 %var33)
|
||||
%var37 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.19, i32 0, i32 0
|
||||
%var36 = call i32 @js_create_element(i8* %var37)
|
||||
%var38 = call i32 @js_create_text(i8* getelementptr inbounds ([32 x i8], [32 x i8]* @.str.20, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var36, i32 %var38)
|
||||
call void @js_append_child(i32 %var29, i32 %var36)
|
||||
call void @js_append_child(i32 %var25, i32 %var29)
|
||||
%var40 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var39 = call i32 @js_create_element(i8* %var40)
|
||||
%var41 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.1, i32 0, i32 0
|
||||
%var42 = getelementptr inbounds [14 x i8], [14 x i8]* @.str.16, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var39, i8* %var41, i8* %var42)
|
||||
%var44 = getelementptr inbounds [3 x i8], [3 x i8]* @.str.17, i32 0, i32 0
|
||||
%var43 = call i32 @js_create_element(i8* %var44)
|
||||
%var45 = call i32 @js_create_text(i8* getelementptr inbounds ([9 x i8], [9 x i8]* @.str.21, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var43, i32 %var45)
|
||||
call void @js_append_child(i32 %var39, i32 %var43)
|
||||
%var47 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.19, i32 0, i32 0
|
||||
%var46 = call i32 @js_create_element(i8* %var47)
|
||||
%var48 = call i32 @js_create_text(i8* getelementptr inbounds ([27 x i8], [27 x i8]* @.str.22, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var46, i32 %var48)
|
||||
%var49 = call i32 @js_create_text(i8* getelementptr inbounds ([32 x i8], [32 x i8]* @.str.23, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var46, i32 %var49)
|
||||
%var50 = call i32 @js_create_text(i8* getelementptr inbounds ([34 x i8], [34 x i8]* @.str.24, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var46, i32 %var50)
|
||||
%var51 = call i32 @js_create_text(i8* getelementptr inbounds ([20 x i8], [20 x i8]* @.str.25, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var46, i32 %var51)
|
||||
%var52 = call i32 @js_create_text(i8* getelementptr inbounds ([32 x i8], [32 x i8]* @.str.26, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var46, i32 %var52)
|
||||
call void @js_append_child(i32 %var39, i32 %var46)
|
||||
call void @js_append_child(i32 %var25, i32 %var39)
|
||||
%var54 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var53 = call i32 @js_create_element(i8* %var54)
|
||||
%var55 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.1, i32 0, i32 0
|
||||
%var56 = getelementptr inbounds [14 x i8], [14 x i8]* @.str.16, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var53, i8* %var55, i8* %var56)
|
||||
%var58 = getelementptr inbounds [3 x i8], [3 x i8]* @.str.17, i32 0, i32 0
|
||||
%var57 = call i32 @js_create_element(i8* %var58)
|
||||
%var59 = call i32 @js_create_text(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str.27, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var57, i32 %var59)
|
||||
call void @js_append_child(i32 %var53, i32 %var57)
|
||||
%var61 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.19, i32 0, i32 0
|
||||
%var60 = call i32 @js_create_element(i8* %var61)
|
||||
%var62 = call i32 @js_create_text(i8* getelementptr inbounds ([32 x i8], [32 x i8]* @.str.28, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var60, i32 %var62)
|
||||
%var63 = call i32 @js_create_text(i8* getelementptr inbounds ([33 x i8], [33 x i8]* @.str.29, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var60, i32 %var63)
|
||||
%var64 = call i32 @js_create_text(i8* getelementptr inbounds ([35 x i8], [35 x i8]* @.str.30, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var60, i32 %var64)
|
||||
call void @js_append_child(i32 %var53, i32 %var60)
|
||||
call void @js_append_child(i32 %var25, i32 %var53)
|
||||
%var66 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var65 = call i32 @js_create_element(i8* %var66)
|
||||
%var67 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.1, i32 0, i32 0
|
||||
%var68 = getelementptr inbounds [14 x i8], [14 x i8]* @.str.16, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var65, i8* %var67, i8* %var68)
|
||||
%var70 = getelementptr inbounds [3 x i8], [3 x i8]* @.str.17, i32 0, i32 0
|
||||
%var69 = call i32 @js_create_element(i8* %var70)
|
||||
%var71 = call i32 @js_create_text(i8* getelementptr inbounds ([17 x i8], [17 x i8]* @.str.31, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var69, i32 %var71)
|
||||
call void @js_append_child(i32 %var65, i32 %var69)
|
||||
%var73 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.19, i32 0, i32 0
|
||||
%var72 = call i32 @js_create_element(i8* %var73)
|
||||
%var74 = call i32 @js_create_text(i8* getelementptr inbounds ([22 x i8], [22 x i8]* @.str.32, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var72, i32 %var74)
|
||||
%var75 = call i32 @js_create_text(i8* getelementptr inbounds ([24 x i8], [24 x i8]* @.str.33, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var72, i32 %var75)
|
||||
%var76 = call i32 @js_create_text(i8* getelementptr inbounds ([22 x i8], [22 x i8]* @.str.34, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var72, i32 %var76)
|
||||
call void @js_append_child(i32 %var65, i32 %var72)
|
||||
call void @js_append_child(i32 %var25, i32 %var65)
|
||||
call void @js_append_child(i32 %var0, i32 %var25)
|
||||
%var78 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var77 = call i32 @js_create_element(i8* %var78)
|
||||
%var79 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.1, i32 0, i32 0
|
||||
%var80 = getelementptr inbounds [18 x i8], [18 x i8]* @.str.35, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var77, i8* %var79, i8* %var80)
|
||||
%var81 = call i32 @js_create_text(i8* getelementptr inbounds ([29 x i8], [29 x i8]* @.str.36, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var77, i32 %var81)
|
||||
ret i32 %var0
|
||||
}
|
||||
|
||||
; Component render function
|
||||
define i32 @render_RitzMain() {
|
||||
entry:
|
||||
%var82 = call i32 @render_DebugConsole()
|
||||
ret i32 %var82
|
||||
}
|
||||
|
||||
; Main entry point
|
||||
define void @main() {
|
||||
entry:
|
||||
%root = call i32 @render_RitzMain()
|
||||
call void @js_set_root(i32 %root)
|
||||
ret void
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
Ritz Debug Console - Self-hosted debug interface written in Ritz itself!
|
||||
|
||||
This demonstrates Ritz's capability to build complex interactive UIs with:
|
||||
- Signal-based state management
|
||||
- Event handling
|
||||
- Conditional rendering
|
||||
- Dynamic content updates
|
||||
|
||||
The entire debug console - state, UI, and interactions - is written in Ritz!
|
||||
"""
|
||||
|
||||
from ritz.syntax import *
|
||||
|
||||
|
||||
@external
|
||||
def js_toggle_debug_console():
|
||||
"""External JavaScript function to toggle debug console visibility."""
|
||||
pass # Implementation provided by JavaScript runtime
|
||||
|
||||
|
||||
@component
|
||||
def DebugConsole():
|
||||
"""
|
||||
Self-hosted debug console for Ritz applications.
|
||||
|
||||
Shows runtime information, memory stats, and console commands.
|
||||
"""
|
||||
def toggle():
|
||||
"""Toggle debug console visibility by calling JavaScript bridge."""
|
||||
js_toggle_debug_console()
|
||||
|
||||
# Start hidden - JavaScript will handle toggling via class manipulation
|
||||
with Div(class_='ritz-debug hidden', id='ritz-debug'):
|
||||
# Header with toggle button
|
||||
with Div(class_='debug-header'):
|
||||
with H3():
|
||||
yield '🎭 Ritz Debug'
|
||||
with Button(onclick=toggle):
|
||||
yield '×'
|
||||
|
||||
# Status indicator
|
||||
with Div(class_='status success', id='status'):
|
||||
yield '✅ Main loaded!'
|
||||
|
||||
# Debug content sections
|
||||
with Div(class_='debug-content'):
|
||||
# Component info
|
||||
with Div(class_='debug-section'):
|
||||
with H4():
|
||||
yield 'COMPONENT'
|
||||
with Pre():
|
||||
yield 'Main.wasm (Multi-Part Switches)'
|
||||
|
||||
# Features section
|
||||
with Div(class_='debug-section'):
|
||||
with H4():
|
||||
yield 'FEATURES'
|
||||
with Pre():
|
||||
yield '✓ Python source to WASM\n'
|
||||
yield '✓ Reactive signals (no vDOM)\n'
|
||||
yield '✓ Conditional CSR (match/case)\n'
|
||||
yield '✓ Event handlers\n'
|
||||
yield '✓ Self-hosted debug console!\n'
|
||||
|
||||
# Runtime stats section
|
||||
with Div(class_='debug-section'):
|
||||
with H4():
|
||||
yield 'RUNTIME STATS'
|
||||
with Pre():
|
||||
yield 'Memory: JavaScript will update\n'
|
||||
yield 'Signals: JavaScript will update\n'
|
||||
yield 'DOM nodes: JavaScript will update\n'
|
||||
|
||||
# Console commands section
|
||||
with Div(class_='debug-section'):
|
||||
with H4():
|
||||
yield 'CONSOLE COMMANDS'
|
||||
with Pre():
|
||||
yield 'ritzRuntime.getLog()\n'
|
||||
yield 'ritzRuntime.nodes.size\n'
|
||||
yield 'ritzInstance.exports\n'
|
||||
|
||||
# Toggle hint (appears on page load)
|
||||
with Div(class_='debug-toggle-hint'):
|
||||
yield 'Press Ctrl+` to toggle debug'
|
||||
|
||||
|
||||
@component
|
||||
def RitzMain():
|
||||
"""Entry point for debug console component."""
|
||||
yield DebugConsole()
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,302 @@
|
||||
; Generated by Ritz Framework with Signals
|
||||
; Components: DebugConsole, RitzMain
|
||||
|
||||
; 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"debug-section\00"
|
||||
@.str.3 = private unnamed_addr constant [3 x i8] c"h4\00"
|
||||
@.str.4 = private unnamed_addr constant [9 x i8] c"FEATURES\00"
|
||||
@.str.5 = private unnamed_addr constant [4 x i8] c"pre\00"
|
||||
@.str.6 = private unnamed_addr constant [17 x i8] c"CONSOLE COMMANDS\00"
|
||||
@.str.7 = private unnamed_addr constant [3 x i8] c"id\00"
|
||||
@.str.8 = private unnamed_addr constant [11 x i8] c"ritz-debug\00"
|
||||
@.str.9 = private unnamed_addr constant [13 x i8] c"debug-header\00"
|
||||
@.str.10 = private unnamed_addr constant [7 x i8] c"status\00"
|
||||
@.str.11 = private unnamed_addr constant [6 x i8] c"Ready\00"
|
||||
@.str.12 = private unnamed_addr constant [14 x i8] c"debug-content\00"
|
||||
@.str.13 = private unnamed_addr constant [10 x i8] c"COMPONENT\00"
|
||||
@.str.14 = private unnamed_addr constant [1 x i8] c"\00"
|
||||
@.str.15 = private unnamed_addr constant [18 x i8] c"debug-toggle-hint\00"
|
||||
@.str.16 = private unnamed_addr constant [29 x i8] c"Press Ctrl+` to toggle debug\00"
|
||||
|
||||
; JavaScript bridge function declarations
|
||||
declare i32 @js_create_element(i8*)
|
||||
declare i32 @js_create_text(i8*)
|
||||
declare i32 @js_create_text_with_int(i8*, i32)
|
||||
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*)
|
||||
declare void @js_update_text_with_int(i32, i8*, i32)
|
||||
declare i32 @js_create_marker()
|
||||
declare void @js_replace_at_marker(i32, i32, i32)
|
||||
|
||||
; Signal storage for DebugConsole (reactive state in WASM memory)
|
||||
@signal_DebugConsole_is_open = global i32 0
|
||||
|
||||
; Conditional rendering state
|
||||
@cond_0_marker = global i32 0
|
||||
@cond_0_current = global i32 0
|
||||
@cond_0_then_root = global i32 0
|
||||
@cond_0_else_root = global i32 0
|
||||
@cond_1_marker = global i32 0
|
||||
@cond_1_current = global i32 0
|
||||
@cond_1_then_root = global i32 0
|
||||
@cond_1_else_root = global i32 0
|
||||
|
||||
; Signal helper functions
|
||||
define i32 @get_is_open() {
|
||||
entry:
|
||||
%val = load i32, i32* @signal_DebugConsole_is_open
|
||||
ret i32 %val
|
||||
}
|
||||
|
||||
define void @set_is_open(i32 %newval) {
|
||||
entry:
|
||||
store i32 %newval, i32* @signal_DebugConsole_is_open
|
||||
; TODO: Trigger DOM update callback
|
||||
ret void
|
||||
}
|
||||
|
||||
; Event handler: toggle
|
||||
define void @toggle() {
|
||||
entry:
|
||||
ret void
|
||||
}
|
||||
|
||||
; Initialize conditional 0
|
||||
define void @_init_conditional_0() {
|
||||
entry:
|
||||
; Render both branches upfront
|
||||
%var0 = call i32 @_render_cond_0_then()
|
||||
%var1 = call i32 @_render_cond_0_else()
|
||||
store i32 %var0, i32* @cond_0_then_root
|
||||
store i32 %var1, i32* @cond_0_else_root
|
||||
%var2 = icmp ne i32 0, 0
|
||||
%var3 = load i32, i32* @cond_0_marker
|
||||
br i1 %var2, label %insert_then, label %insert_else
|
||||
insert_then:
|
||||
call void @js_replace_at_marker(i32 %var3, i32 0, i32 %var0)
|
||||
store i32 1, i32* @cond_0_current
|
||||
ret void
|
||||
insert_else:
|
||||
call void @js_replace_at_marker(i32 %var3, i32 0, i32 %var1)
|
||||
store i32 0, i32* @cond_0_current
|
||||
ret void
|
||||
}
|
||||
|
||||
; Re-evaluate conditional 0
|
||||
define void @reevaluate_conditional_0() {
|
||||
entry:
|
||||
%var4 = icmp ne i32 0, 0
|
||||
%var5 = zext i1 %var4 to i32
|
||||
%var6 = load i32, i32* @cond_0_current
|
||||
%var7 = icmp ne i32 %var6, %var5
|
||||
br i1 %var7, label %swap, label %done
|
||||
swap:
|
||||
br i1 %var4, label %swap_to_then, label %swap_to_else
|
||||
swap_to_then:
|
||||
%var8 = load i32, i32* @cond_0_else_root
|
||||
%var9 = load i32, i32* @cond_0_then_root
|
||||
%var10 = load i32, i32* @cond_0_marker
|
||||
call void @js_replace_at_marker(i32 %var10, i32 %var8, i32 %var9)
|
||||
store i32 1, i32* @cond_0_current
|
||||
br label %done
|
||||
swap_to_else:
|
||||
%var11 = load i32, i32* @cond_0_then_root
|
||||
%var12 = load i32, i32* @cond_0_else_root
|
||||
%var13 = load i32, i32* @cond_0_marker
|
||||
call void @js_replace_at_marker(i32 %var13, i32 %var11, i32 %var12)
|
||||
store i32 0, i32* @cond_0_current
|
||||
br label %done
|
||||
done:
|
||||
ret void
|
||||
}
|
||||
|
||||
define i32 @_render_cond_0_then() {
|
||||
entry:
|
||||
%var15 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var14 = call i32 @js_create_element(i8* %var15)
|
||||
%var17 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var16 = call i32 @js_create_element(i8* %var17)
|
||||
%var18 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.1, i32 0, i32 0
|
||||
%var19 = getelementptr inbounds [14 x i8], [14 x i8]* @.str.2, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var16, i8* %var18, i8* %var19)
|
||||
%var21 = getelementptr inbounds [3 x i8], [3 x i8]* @.str.3, i32 0, i32 0
|
||||
%var20 = call i32 @js_create_element(i8* %var21)
|
||||
%var22 = call i32 @js_create_text(i8* getelementptr inbounds ([9 x i8], [9 x i8]* @.str.4, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var20, i32 %var22)
|
||||
call void @js_append_child(i32 %var16, i32 %var20)
|
||||
%var24 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.5, i32 0, i32 0
|
||||
%var23 = call i32 @js_create_element(i8* %var24)
|
||||
call void @js_append_child(i32 %var16, i32 %var23)
|
||||
call void @js_append_child(i32 %var14, i32 %var16)
|
||||
ret i32 %var14
|
||||
}
|
||||
|
||||
define i32 @_render_cond_0_else() {
|
||||
entry:
|
||||
%var26 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var25 = call i32 @js_create_element(i8* %var26)
|
||||
ret i32 %var25
|
||||
}
|
||||
|
||||
; Initialize conditional 1
|
||||
define void @_init_conditional_1() {
|
||||
entry:
|
||||
; Render both branches upfront
|
||||
%var27 = call i32 @_render_cond_1_then()
|
||||
%var28 = call i32 @_render_cond_1_else()
|
||||
store i32 %var27, i32* @cond_1_then_root
|
||||
store i32 %var28, i32* @cond_1_else_root
|
||||
%var29 = icmp ne i32 0, 0
|
||||
%var30 = load i32, i32* @cond_1_marker
|
||||
br i1 %var29, label %insert_then, label %insert_else
|
||||
insert_then:
|
||||
call void @js_replace_at_marker(i32 %var30, i32 0, i32 %var27)
|
||||
store i32 1, i32* @cond_1_current
|
||||
ret void
|
||||
insert_else:
|
||||
call void @js_replace_at_marker(i32 %var30, i32 0, i32 %var28)
|
||||
store i32 0, i32* @cond_1_current
|
||||
ret void
|
||||
}
|
||||
|
||||
; Re-evaluate conditional 1
|
||||
define void @reevaluate_conditional_1() {
|
||||
entry:
|
||||
%var31 = icmp ne i32 0, 0
|
||||
%var32 = zext i1 %var31 to i32
|
||||
%var33 = load i32, i32* @cond_1_current
|
||||
%var34 = icmp ne i32 %var33, %var32
|
||||
br i1 %var34, label %swap, label %done
|
||||
swap:
|
||||
br i1 %var31, label %swap_to_then, label %swap_to_else
|
||||
swap_to_then:
|
||||
%var35 = load i32, i32* @cond_1_else_root
|
||||
%var36 = load i32, i32* @cond_1_then_root
|
||||
%var37 = load i32, i32* @cond_1_marker
|
||||
call void @js_replace_at_marker(i32 %var37, i32 %var35, i32 %var36)
|
||||
store i32 1, i32* @cond_1_current
|
||||
br label %done
|
||||
swap_to_else:
|
||||
%var38 = load i32, i32* @cond_1_then_root
|
||||
%var39 = load i32, i32* @cond_1_else_root
|
||||
%var40 = load i32, i32* @cond_1_marker
|
||||
call void @js_replace_at_marker(i32 %var40, i32 %var38, i32 %var39)
|
||||
store i32 0, i32* @cond_1_current
|
||||
br label %done
|
||||
done:
|
||||
ret void
|
||||
}
|
||||
|
||||
define i32 @_render_cond_1_then() {
|
||||
entry:
|
||||
%var42 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var41 = call i32 @js_create_element(i8* %var42)
|
||||
%var44 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var43 = call i32 @js_create_element(i8* %var44)
|
||||
%var45 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.1, i32 0, i32 0
|
||||
%var46 = getelementptr inbounds [14 x i8], [14 x i8]* @.str.2, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var43, i8* %var45, i8* %var46)
|
||||
%var48 = getelementptr inbounds [3 x i8], [3 x i8]* @.str.3, i32 0, i32 0
|
||||
%var47 = call i32 @js_create_element(i8* %var48)
|
||||
%var49 = call i32 @js_create_text(i8* getelementptr inbounds ([17 x i8], [17 x i8]* @.str.6, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var47, i32 %var49)
|
||||
call void @js_append_child(i32 %var43, i32 %var47)
|
||||
%var51 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.5, i32 0, i32 0
|
||||
%var50 = call i32 @js_create_element(i8* %var51)
|
||||
call void @js_append_child(i32 %var43, i32 %var50)
|
||||
call void @js_append_child(i32 %var41, i32 %var43)
|
||||
ret i32 %var41
|
||||
}
|
||||
|
||||
define i32 @_render_cond_1_else() {
|
||||
entry:
|
||||
%var53 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var52 = call i32 @js_create_element(i8* %var53)
|
||||
ret i32 %var52
|
||||
}
|
||||
|
||||
; Component render function
|
||||
define i32 @render_DebugConsole(i32 %component_name, i32 %features, i32 %console_commands) {
|
||||
entry:
|
||||
%var55 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var54 = call i32 @js_create_element(i8* %var55)
|
||||
%var56 = getelementptr inbounds [3 x i8], [3 x i8]* @.str.7, i32 0, i32 0
|
||||
%var57 = getelementptr inbounds [11 x i8], [11 x i8]* @.str.8, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var54, i8* %var56, i8* %var57)
|
||||
%var59 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var58 = call i32 @js_create_element(i8* %var59)
|
||||
%var60 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.1, i32 0, i32 0
|
||||
%var61 = getelementptr inbounds [13 x i8], [13 x i8]* @.str.9, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var58, i8* %var60, i8* %var61)
|
||||
call void @js_append_child(i32 %var54, i32 %var58)
|
||||
%var63 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var62 = call i32 @js_create_element(i8* %var63)
|
||||
%var64 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.1, i32 0, i32 0
|
||||
%var65 = getelementptr inbounds [7 x i8], [7 x i8]* @.str.10, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var62, i8* %var64, i8* %var65)
|
||||
%var66 = getelementptr inbounds [3 x i8], [3 x i8]* @.str.7, i32 0, i32 0
|
||||
%var67 = getelementptr inbounds [7 x i8], [7 x i8]* @.str.10, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var62, i8* %var66, i8* %var67)
|
||||
%var68 = call i32 @js_create_text(i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.11, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var62, i32 %var68)
|
||||
call void @js_append_child(i32 %var54, i32 %var62)
|
||||
%var70 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var69 = call i32 @js_create_element(i8* %var70)
|
||||
%var71 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.1, i32 0, i32 0
|
||||
%var72 = getelementptr inbounds [14 x i8], [14 x i8]* @.str.12, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var69, i8* %var71, i8* %var72)
|
||||
%var74 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var73 = call i32 @js_create_element(i8* %var74)
|
||||
%var75 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.1, i32 0, i32 0
|
||||
%var76 = getelementptr inbounds [14 x i8], [14 x i8]* @.str.2, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var73, i8* %var75, i8* %var76)
|
||||
%var78 = getelementptr inbounds [3 x i8], [3 x i8]* @.str.3, i32 0, i32 0
|
||||
%var77 = call i32 @js_create_element(i8* %var78)
|
||||
%var79 = call i32 @js_create_text(i8* getelementptr inbounds ([10 x i8], [10 x i8]* @.str.13, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var77, i32 %var79)
|
||||
call void @js_append_child(i32 %var73, i32 %var77)
|
||||
%var81 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.5, i32 0, i32 0
|
||||
%var80 = call i32 @js_create_element(i8* %var81)
|
||||
%var83 = getelementptr inbounds [1 x i8], [1 x i8]* @.str.14, i32 0, i32 0
|
||||
%var82 = call i32 @js_create_text(i8* %var83)
|
||||
call void @js_append_child(i32 %var80, i32 %var82)
|
||||
call void @js_append_child(i32 %var73, i32 %var80)
|
||||
call void @js_append_child(i32 %var69, i32 %var73)
|
||||
%var84 = call i32 @js_create_marker()
|
||||
call void @js_append_child(i32 %var69, i32 %var84)
|
||||
store i32 %var84, i32* @cond_0_marker
|
||||
call void @_init_conditional_0()
|
||||
%var85 = call i32 @js_create_marker()
|
||||
call void @js_append_child(i32 %var69, i32 %var85)
|
||||
store i32 %var85, i32* @cond_1_marker
|
||||
call void @_init_conditional_1()
|
||||
call void @js_append_child(i32 %var54, i32 %var69)
|
||||
%var87 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.0, i32 0, i32 0
|
||||
%var86 = call i32 @js_create_element(i8* %var87)
|
||||
%var88 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.1, i32 0, i32 0
|
||||
%var89 = getelementptr inbounds [18 x i8], [18 x i8]* @.str.15, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var86, i8* %var88, i8* %var89)
|
||||
%var90 = call i32 @js_create_text(i8* getelementptr inbounds ([29 x i8], [29 x i8]* @.str.16, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var86, i32 %var90)
|
||||
ret i32 %var54
|
||||
}
|
||||
|
||||
; Component render function
|
||||
define i32 @render_RitzMain() {
|
||||
entry:
|
||||
%var91 = call i32 @render_DebugConsole()
|
||||
ret i32 %var91
|
||||
}
|
||||
|
||||
; Main entry point
|
||||
define void @main() {
|
||||
entry:
|
||||
%root = call i32 @render_RitzMain()
|
||||
call void @js_set_root(i32 %root)
|
||||
ret void
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,132 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ritz Framework Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
#app {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.header {
|
||||
border-bottom: 2px solid #333;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.status {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
.error {
|
||||
background: #fee;
|
||||
color: #c00;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>🎭 Ritz Framework</h1>
|
||||
<p>Python components running in WebAssembly</p>
|
||||
</div>
|
||||
|
||||
<div class="status" id="status">Loading...</div>
|
||||
|
||||
<div id="app"></div>
|
||||
|
||||
<script src="ritz.js"></script>
|
||||
<script>
|
||||
// Configuration
|
||||
const COMPONENT_NAME = 'HelloWorld'; // Change this to your component name
|
||||
const WASM_FILE = '../examples/HelloWorld.wasm';
|
||||
|
||||
async function loadComponent() {
|
||||
const status = document.getElementById('status');
|
||||
const appRoot = document.getElementById('app');
|
||||
|
||||
try {
|
||||
// Initialize Ritz runtime
|
||||
status.textContent = '⏳ Initializing Ritz runtime...';
|
||||
const runtime = new RitzRuntime(appRoot);
|
||||
|
||||
// Load WASM module
|
||||
status.textContent = `⏳ Loading ${WASM_FILE}...`;
|
||||
|
||||
const response = await fetch(WASM_FILE);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${WASM_FILE}: ${response.status}`);
|
||||
}
|
||||
|
||||
const wasmBytes = await response.arrayBuffer();
|
||||
|
||||
// Set up Module for Emscripten
|
||||
window.Module = {
|
||||
wasmBinary: wasmBytes,
|
||||
ritz: runtime, // Provide runtime to WASM
|
||||
onRuntimeInitialized: function() {
|
||||
status.textContent = '⏳ Rendering component...';
|
||||
|
||||
try {
|
||||
// Call the render function
|
||||
const renderFn = Module[`_render_${COMPONENT_NAME}`];
|
||||
if (!renderFn) {
|
||||
throw new Error(`Component render_${COMPONENT_NAME} not found in WASM exports`);
|
||||
}
|
||||
|
||||
// Render the component
|
||||
Module.ccall(`render_${COMPONENT_NAME}`, null, [], []);
|
||||
|
||||
status.textContent = `✅ ${COMPONENT_NAME} rendered successfully!`;
|
||||
status.className = 'status';
|
||||
} catch (error) {
|
||||
status.textContent = `❌ Render error: ${error.message}`;
|
||||
status.className = 'status error';
|
||||
console.error('Render error:', error);
|
||||
}
|
||||
},
|
||||
print: function(text) {
|
||||
console.log('[WASM]', text);
|
||||
},
|
||||
printErr: function(text) {
|
||||
console.error('[WASM]', text);
|
||||
}
|
||||
};
|
||||
|
||||
// Load the Emscripten JS glue code
|
||||
status.textContent = '⏳ Loading WASM runtime...';
|
||||
const script = document.createElement('script');
|
||||
script.src = WASM_FILE.replace('.wasm', '.js');
|
||||
script.onerror = () => {
|
||||
throw new Error('Failed to load WASM JS glue code');
|
||||
};
|
||||
document.body.appendChild(script);
|
||||
|
||||
} catch (error) {
|
||||
status.textContent = `❌ Error: ${error.message}`;
|
||||
status.className = 'status error';
|
||||
console.error('Load error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Start loading when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', loadComponent);
|
||||
} else {
|
||||
loadComponent();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,544 @@
|
||||
/**
|
||||
* 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_toggle_debug_console() {
|
||||
// Bridge function to toggle debug console from WASM
|
||||
if (window.ritzLoader) {
|
||||
window.ritzLoader.toggleDebugConsole();
|
||||
}
|
||||
this._log('[Debug] Toggle debug console from WASM');
|
||||
}
|
||||
|
||||
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}")`);
|
||||
}
|
||||
}
|
||||
|
||||
js_create_text_with_int(prefixPtr, intValue) {
|
||||
const prefix = this.readString(prefixPtr);
|
||||
const text = prefix + intValue.toString();
|
||||
const textNode = document.createTextNode(text);
|
||||
const id = this.nextNodeId++;
|
||||
this.nodes.set(id, textNode);
|
||||
this._log(`createTextWithInt("${prefix}", ${intValue}) = "${text}" (id=${id})`);
|
||||
return id;
|
||||
}
|
||||
|
||||
js_update_text_with_int(nodeId, prefixPtr, intValue) {
|
||||
const node = this.nodes.get(nodeId);
|
||||
const prefix = this.readString(prefixPtr);
|
||||
const text = prefix + intValue.toString();
|
||||
if (node) {
|
||||
node.textContent = text;
|
||||
this._log(`updateTextWithInt(${nodeId}, "${prefix}", ${intValue}) = "${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);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-evaluate all reactive conditionals
|
||||
for (const [name, fn] of Object.entries(exports)) {
|
||||
if (name.startsWith('reevaluate_conditional_') && typeof fn === 'function') {
|
||||
fn();
|
||||
this._log(`[Reactive] ${name}()`);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-evaluate all reactive match statements
|
||||
for (const [name, fn] of Object.entries(exports)) {
|
||||
if (name.startsWith('reevaluate_match_') && typeof fn === 'function') {
|
||||
fn();
|
||||
this._log(`[Reactive] ${name}()`);
|
||||
}
|
||||
}
|
||||
|
||||
// Update debug console stats if debug mode is enabled
|
||||
if (window.ritzLoader && this.debugEnabled) {
|
||||
window.ritzLoader.updateDebugStats();
|
||||
}
|
||||
}
|
||||
|
||||
updateSignalDisplay(signalName, value) {
|
||||
// Find DOM elements that should display this signal
|
||||
// IMPORTANT: Only search within this runtime's root element to avoid cross-contamination
|
||||
const selectors = [
|
||||
`[data-signal="${signalName}"]`,
|
||||
`.${signalName}`,
|
||||
'.count', // Hardcoded for demo - TODO: track bindings properly
|
||||
];
|
||||
|
||||
selectors.forEach(selector => {
|
||||
// Only query within this runtime's root element, not the entire document
|
||||
const elements = this.rootElement.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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Reactive Conditional Rendering =====
|
||||
|
||||
js_create_marker() {
|
||||
// Create a comment node as a marker for conditional insertion
|
||||
const marker = document.createComment('conditional-marker');
|
||||
const id = this.nextNodeId++;
|
||||
this.nodes.set(id, marker);
|
||||
this._log(`Created marker node ${id}`);
|
||||
return id;
|
||||
}
|
||||
|
||||
js_replace_at_marker(markerId, oldNodeId, newNodeId) {
|
||||
// Replace DOM node at marker position
|
||||
const marker = this.nodes.get(markerId);
|
||||
if (!marker) {
|
||||
console.error(`[replace_at_marker] Marker ${markerId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = marker.parentNode;
|
||||
if (!parent) {
|
||||
console.error(`[replace_at_marker] Marker ${markerId} has no parent`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._log(`[replace_at_marker] marker=${markerId}, old=${oldNodeId}, new=${newNodeId}`);
|
||||
|
||||
// Remove old node if it exists (0 means no old node to remove)
|
||||
if (oldNodeId !== 0) {
|
||||
const oldNode = this.nodes.get(oldNodeId);
|
||||
if (!oldNode) {
|
||||
console.warn(`[replace_at_marker] Old node ${oldNodeId} not found in registry`);
|
||||
} else if (!oldNode.parentNode) {
|
||||
console.warn(`[replace_at_marker] Old node ${oldNodeId} not in DOM`);
|
||||
} else if (oldNode.parentNode !== parent) {
|
||||
console.warn(`[replace_at_marker] Old node ${oldNodeId} in wrong parent`);
|
||||
oldNode.parentNode.removeChild(oldNode);
|
||||
this._log(`[replace_at_marker] Removed ${oldNodeId} from wrong parent`);
|
||||
} else {
|
||||
// Remove from correct parent
|
||||
parent.removeChild(oldNode);
|
||||
this._log(`[replace_at_marker] Removed ${oldNodeId} from correct parent`);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert new node after marker (only if newNodeId is not 0)
|
||||
if (newNodeId !== 0) {
|
||||
const newNode = this.nodes.get(newNodeId);
|
||||
if (!newNode) {
|
||||
console.error(`[replace_at_marker] New node ${newNodeId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Insert right after the marker
|
||||
// If newNode is already in DOM elsewhere, this will move it
|
||||
const nextSibling = marker.nextSibling;
|
||||
if (nextSibling) {
|
||||
parent.insertBefore(newNode, nextSibling);
|
||||
this._log(`[replace_at_marker] Inserted ${newNodeId} before sibling`);
|
||||
} else {
|
||||
parent.appendChild(newNode);
|
||||
this._log(`[replace_at_marker] Appended ${newNodeId} (no sibling)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 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.debug = options.debug !== undefined ? options.debug : false;
|
||||
this.onLoad = options.onLoad || (() => {});
|
||||
this.onError = options.onError || ((err) => console.error(err));
|
||||
this.debugConsoleRuntime = null;
|
||||
}
|
||||
|
||||
async load(wasmPath, componentName = null, props = {}) {
|
||||
try {
|
||||
// Auto-detect component name from wasm filename if not provided
|
||||
if (!componentName) {
|
||||
// Extract from path: "Main.wasm" → "Main", "path/to/Main.wasm" → "Main"
|
||||
componentName = wasmPath.split('/').pop().replace('.wasm', '');
|
||||
}
|
||||
|
||||
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_create_text_with_int: (p, i) => runtime.js_create_text_with_int(p, i),
|
||||
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),
|
||||
js_update_text_with_int: (n, p, i) => runtime.js_update_text_with_int(n, p, i),
|
||||
js_create_marker: () => runtime.js_create_marker(),
|
||||
js_replace_at_marker: (m, o, n) => runtime.js_replace_at_marker(m, o, n),
|
||||
}
|
||||
};
|
||||
|
||||
const { instance } = await WebAssembly.instantiate(wasmBytes, imports);
|
||||
|
||||
// Setup runtime
|
||||
runtime.setMemory(instance.exports.memory);
|
||||
runtime.setWasmInstance(instance);
|
||||
|
||||
this.setStatus(`⏳ Rendering ${componentName}...`);
|
||||
|
||||
// Call the main entry point (which renders top-level component and sets root)
|
||||
const mainFn = instance.exports.main;
|
||||
if (!mainFn) {
|
||||
throw new Error(`main function not found in WASM exports`);
|
||||
}
|
||||
|
||||
mainFn();
|
||||
|
||||
// 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);
|
||||
|
||||
// Load debug console if debug mode is enabled
|
||||
if (this.debug) {
|
||||
await this.loadDebugConsole(componentName);
|
||||
}
|
||||
|
||||
return { runtime, instance };
|
||||
|
||||
} catch (error) {
|
||||
this.setStatus(`❌ Error: ${error.message}`, 'error');
|
||||
this.onError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async loadDebugConsole(appComponentName) {
|
||||
try {
|
||||
console.log('[Debug] Loading debug console...');
|
||||
|
||||
// Create a container for the debug console
|
||||
const debugContainer = document.createElement('div');
|
||||
debugContainer.id = 'debug-console-root';
|
||||
document.body.appendChild(debugContainer);
|
||||
|
||||
console.log('[Debug] Created container:', debugContainer);
|
||||
|
||||
// Create debug console runtime
|
||||
const debugRuntime = new RitzRuntime(debugContainer);
|
||||
debugRuntime.debugEnabled = false; // Don't log debug console's own operations
|
||||
|
||||
// Load DebugConsole.wasm from runtime directory
|
||||
const debugWasmPath = '../runtime/DebugConsole.wasm';
|
||||
console.log('[Debug] Fetching:', debugWasmPath);
|
||||
|
||||
const response = await fetch(debugWasmPath);
|
||||
if (!response.ok) {
|
||||
console.warn('Could not load debug console:', response.status);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Debug] DebugConsole.wasm loaded successfully');
|
||||
|
||||
const wasmBytes = await response.arrayBuffer();
|
||||
|
||||
// Create imports for debug console WASM
|
||||
const imports = {
|
||||
env: {
|
||||
js_create_element: (ptr) => debugRuntime.js_create_element(ptr),
|
||||
js_create_text: (ptr) => debugRuntime.js_create_text(ptr),
|
||||
js_create_text_with_int: (p, i) => debugRuntime.js_create_text_with_int(p, i),
|
||||
js_append_child: (p, c) => debugRuntime.js_append_child(p, c),
|
||||
js_set_attribute: (e, n, v) => debugRuntime.js_set_attribute(e, n, v),
|
||||
js_set_root: (e) => debugRuntime.js_set_root(e),
|
||||
js_bind_event: (e, n, h) => debugRuntime.js_bind_event(e, n, h),
|
||||
js_update_text_content: (n, t) => debugRuntime.js_update_text_content(n, t),
|
||||
js_update_text_with_int: (n, p, i) => debugRuntime.js_update_text_with_int(n, p, i),
|
||||
js_create_marker: () => debugRuntime.js_create_marker(),
|
||||
js_replace_at_marker: (m, o, n) => debugRuntime.js_replace_at_marker(m, o, n),
|
||||
js_toggle_debug_console: () => debugRuntime.js_toggle_debug_console(),
|
||||
}
|
||||
};
|
||||
|
||||
const { instance } = await WebAssembly.instantiate(wasmBytes, imports);
|
||||
|
||||
// Setup debug runtime
|
||||
debugRuntime.setMemory(instance.exports.memory);
|
||||
debugRuntime.setWasmInstance(instance);
|
||||
|
||||
// Render debug console
|
||||
console.log('[Debug] Rendering debug console...');
|
||||
instance.exports.main();
|
||||
debugRuntime.updateSignals();
|
||||
|
||||
this.debugConsoleRuntime = debugRuntime;
|
||||
|
||||
console.log('[Debug] Debug console rendered. Checking DOM...');
|
||||
const debugElement = document.querySelector('.ritz-debug');
|
||||
console.log('[Debug] Debug element:', debugElement);
|
||||
console.log('[Debug] Debug element classes:', debugElement?.className);
|
||||
|
||||
// Hide the loader status element (we have our own in debug console)
|
||||
if (this.statusElement) {
|
||||
this.statusElement.style.display = 'none';
|
||||
}
|
||||
|
||||
// Populate runtime stats
|
||||
this.updateDebugStats(appComponentName);
|
||||
|
||||
// Setup keyboard shortcut (Ctrl+`)
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey && e.key === '`') {
|
||||
e.preventDefault();
|
||||
console.log('[Debug] Toggle debug console');
|
||||
this.toggleDebugConsole();
|
||||
}
|
||||
});
|
||||
|
||||
// Make debug console and loader globally available
|
||||
window.ritzDebugConsole = debugRuntime;
|
||||
window.ritzLoader = this;
|
||||
|
||||
console.log('🎭 Ritz Debug Console loaded! Press Ctrl+` to toggle.');
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Failed to load debug console:', error);
|
||||
}
|
||||
}
|
||||
|
||||
toggleDebugConsole() {
|
||||
// Toggle the debug console by toggling body class
|
||||
// The Ritz debug console component manages its own visibility via signal
|
||||
// But we need to trigger the body class for layout
|
||||
document.body.classList.toggle('debug-open');
|
||||
|
||||
// Find the debug console element and toggle its hidden class
|
||||
const debugElement = document.querySelector('.ritz-debug');
|
||||
if (debugElement) {
|
||||
debugElement.classList.toggle('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
updateDebugStats(componentName = 'Main') {
|
||||
// Update status with component name (inside debug console)
|
||||
const statusEl = document.querySelector('#ritz-debug .status');
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `✅ ${componentName} loaded!`;
|
||||
statusEl.className = 'status success';
|
||||
}
|
||||
|
||||
// Update component section
|
||||
const componentPre = document.querySelector('#ritz-debug .debug-section:nth-child(1) pre');
|
||||
if (componentPre && window.ritzInstance) {
|
||||
const wasmSize = window.ritzInstance.exports.memory.buffer.byteLength;
|
||||
componentPre.textContent = `${componentName}.wasm (${Math.round(wasmSize / 1024)}KB)`;
|
||||
}
|
||||
|
||||
// Update runtime stats
|
||||
const statsPre = document.querySelector('#ritz-debug .debug-section:nth-child(3) pre');
|
||||
if (statsPre && window.ritzRuntime && window.ritzInstance) {
|
||||
const memory = window.ritzInstance.exports.memory.buffer.byteLength;
|
||||
const memoryKB = Math.round(memory / 1024);
|
||||
const memoryMB = (memory / 1024 / 1024).toFixed(2);
|
||||
const nodeCount = window.ritzRuntime.nodes.size;
|
||||
|
||||
// Count signals (globals that start with @signal_)
|
||||
const exports = Object.keys(window.ritzInstance.exports);
|
||||
const signalCount = exports.filter(name =>
|
||||
name.startsWith('get_') || name.startsWith('set_')
|
||||
).length / 2;
|
||||
|
||||
statsPre.textContent =
|
||||
`Memory: ${memoryKB}KB (${memoryMB}MB)\n` +
|
||||
`Signals: ${signalCount} reactive\n` +
|
||||
`DOM nodes: ${nodeCount} cached\n` +
|
||||
`Log entries: ${window.ritzRuntime.log.length}`;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Ritz Runtime - DOM Bridge for WASM
|
||||
*
|
||||
* Provides DOM manipulation functions called by WASM code.
|
||||
*/
|
||||
|
||||
class RitzRuntime {
|
||||
constructor(rootElement) {
|
||||
this.rootElement = rootElement || document.body;
|
||||
this.nodes = new Map(); // Map of node IDs to DOM nodes
|
||||
this.nextNodeId = 1;
|
||||
this.rootNodeId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a DOM element and return its ID
|
||||
*/
|
||||
createElement(tag) {
|
||||
const element = document.createElement(tag);
|
||||
const id = this.nextNodeId++;
|
||||
this.nodes.set(id, element);
|
||||
console.log(`[Ritz] Created element <${tag}> with ID ${id}`);
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a text node and return its ID
|
||||
*/
|
||||
createTextNode(text) {
|
||||
const textNode = document.createTextNode(text);
|
||||
const id = this.nextNodeId++;
|
||||
this.nodes.set(id, textNode);
|
||||
console.log(`[Ritz] Created text node "${text}" with ID ${id}`);
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append child to parent
|
||||
*/
|
||||
appendChild(parentId, childId) {
|
||||
const parent = this.nodes.get(parentId);
|
||||
const child = this.nodes.get(childId);
|
||||
|
||||
if (!parent || !child) {
|
||||
console.error(`[Ritz] appendChild failed: parent=${parentId}, child=${childId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
parent.appendChild(child);
|
||||
console.log(`[Ritz] Appended node ${childId} to node ${parentId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an attribute on an element
|
||||
*/
|
||||
setAttribute(elemId, name, value) {
|
||||
const element = this.nodes.get(elemId);
|
||||
|
||||
if (!element) {
|
||||
console.error(`[Ritz] setAttribute failed: element=${elemId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle special cases
|
||||
if (name === 'class') {
|
||||
element.className = value;
|
||||
} else {
|
||||
element.setAttribute(name, value);
|
||||
}
|
||||
|
||||
console.log(`[Ritz] Set attribute ${name}="${value}" on node ${elemId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the root element and mount it to DOM
|
||||
*/
|
||||
setRoot(elemId) {
|
||||
const element = this.nodes.get(elemId);
|
||||
|
||||
if (!element) {
|
||||
console.error(`[Ritz] setRoot failed: element=${elemId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.rootNodeId = elemId;
|
||||
|
||||
// Clear the root element
|
||||
this.rootElement.innerHTML = '';
|
||||
|
||||
// Mount the component
|
||||
this.rootElement.appendChild(element);
|
||||
|
||||
console.log(`[Ritz] Mounted component (node ${elemId}) to DOM`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all nodes (for hot reload)
|
||||
*/
|
||||
clear() {
|
||||
this.nodes.clear();
|
||||
this.nextNodeId = 1;
|
||||
this.rootNodeId = null;
|
||||
this.rootElement.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in module or global scope
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = RitzRuntime;
|
||||
} else {
|
||||
window.RitzRuntime = RitzRuntime;
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/* Ritz Framework - Shared Example Styles */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 40px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
#status {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 20px;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#status.success {
|
||||
background: #d4edda;
|
||||
border-color: #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
#status.error {
|
||||
background: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
margin-top: 0;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
background: #0066cc;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #0052a3;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
input:focus, select:focus, textarea:focus {
|
||||
outline: none;
|
||||
border-color: #0066cc;
|
||||
box-shadow: 0 0 0 3px rgba(0,102,204,0.1);
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Ritz Debug Console 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%);
|
||||
}
|
||||
|
||||
.ritz-debug .debug-header {
|
||||
background: #252525;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #3e3e3e;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ritz-debug .debug-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.ritz-debug .debug-header button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #d4d4d4;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 1;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ritz-debug .debug-header button:hover {
|
||||
color: #fff;
|
||||
background: none;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#ritz-debug .status {
|
||||
position: static !important;
|
||||
margin: 15px;
|
||||
padding: 10px;
|
||||
background: #2d2d30;
|
||||
border: none !important;
|
||||
border-left: 3px solid #007acc !important;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
#ritz-debug .status.success {
|
||||
background: #2d2d30 !important;
|
||||
border-left-color: #4ec9b0 !important;
|
||||
color: #4ec9b0 !important;
|
||||
}
|
||||
|
||||
#ritz-debug .status.error {
|
||||
background: #2d2d30 !important;
|
||||
border-left-color: #f48771 !important;
|
||||
color: #f48771 !important;
|
||||
}
|
||||
|
||||
.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;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.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; }
|
||||
}
|
||||
|
||||
/* Adjust app container when debug is open */
|
||||
body.debug-open #app {
|
||||
margin-right: 420px;
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tabs - Multi-Panel Switcher - Ritz Framework</title>
|
||||
<link rel="stylesheet" href="../runtime/ritz-debug.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
#app {
|
||||
max-width: 900px;
|
||||
margin: 50px auto;
|
||||
padding: 0;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
.tabs-container {
|
||||
padding: 40px;
|
||||
}
|
||||
.tabs-container h1 {
|
||||
color: #667eea;
|
||||
margin: 0 0 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.subtitle {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin: 0 0 30px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Tab Menu - ul/li structure */
|
||||
.tab-menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 20px 0;
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
.tab-menu .tab {
|
||||
flex: 1;
|
||||
}
|
||||
.tab-menu .tab button {
|
||||
width: 100%;
|
||||
padding: 15px 20px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.tab-menu .tab button:hover {
|
||||
background: #f5f5f5;
|
||||
color: #667eea;
|
||||
}
|
||||
.tab-menu .tab.active button {
|
||||
color: #667eea;
|
||||
border-bottom-color: #667eea;
|
||||
background: #f8f9ff;
|
||||
}
|
||||
|
||||
/* Tab Content */
|
||||
.tab-content {
|
||||
min-height: 300px;
|
||||
padding: 30px;
|
||||
background: #f8f9ff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.tab-pane h2 {
|
||||
margin: 0 0 20px 0;
|
||||
color: #667eea;
|
||||
font-size: 28px;
|
||||
}
|
||||
.tab-pane p {
|
||||
margin: 10px 0;
|
||||
color: #444;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.tab-pane ul {
|
||||
margin: 20px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.tab-pane li {
|
||||
margin: 8px 0;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* Specific pane styles */
|
||||
.tab-pane.home {
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
.tab-pane.profile {
|
||||
border-left: 4px solid #764ba2;
|
||||
}
|
||||
.tab-pane.settings {
|
||||
border-left: 4px solid #f093fb;
|
||||
}
|
||||
.tab-pane.error {
|
||||
border-left: 4px solid #f5576c;
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.profile-info p {
|
||||
margin: 10px 0;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
.profile-info p:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Debug footer */
|
||||
.debug-footer {
|
||||
margin-top: 30px;
|
||||
padding: 15px;
|
||||
background: #e8ebff;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.debug-footer p {
|
||||
margin: 0;
|
||||
font-family: monospace;
|
||||
color: #667eea;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Vertical Tabs (nested inside Settings) */
|
||||
.vertical-tabs {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.vertical-tab-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 150px;
|
||||
}
|
||||
.vertical-tab {
|
||||
padding: 12px 20px;
|
||||
background: #f0f0f0;
|
||||
border: none;
|
||||
border-left: 3px solid transparent;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.vertical-tab:hover {
|
||||
background: #e8ebff;
|
||||
color: #667eea;
|
||||
}
|
||||
.vertical-tab.active {
|
||||
background: #f8f9ff;
|
||||
border-left-color: #667eea;
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
}
|
||||
.vertical-tab-content {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
}
|
||||
.setting-pane h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #667eea;
|
||||
}
|
||||
.setting-pane p {
|
||||
margin: 8px 0;
|
||||
padding: 8px;
|
||||
background: #f8f9ff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.tab-pane {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<div id="ritz-debug" class="ritz-debug hidden">
|
||||
<div class="debug-header">
|
||||
<h3>🎭 Ritz Debug</h3>
|
||||
<button onclick="ritzDebug.toggle()">×</button>
|
||||
</div>
|
||||
<div id="status" class="status">Loading...</div>
|
||||
<div class="debug-content">
|
||||
<div class="debug-section">
|
||||
<h4>Component</h4>
|
||||
<pre>Tabs.py (Multi-Panel Switcher)</pre>
|
||||
</div>
|
||||
<div class="debug-section">
|
||||
<h4>Features</h4>
|
||||
<pre>✓ Python match/case in WASM
|
||||
✓ Beautiful ul/li tab structure
|
||||
✓ Conditional CSS classes (active state)
|
||||
✓ Component composition
|
||||
✓ All logic in WASM</pre>
|
||||
</div>
|
||||
<div class="debug-section">
|
||||
<h4>Console Commands</h4>
|
||||
<pre>ritzRuntime.getLog()
|
||||
ritzInstance.exports.get_active_tab()
|
||||
ritzInstance.exports.go_home()</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../runtime/ritz-runtime.js"></script>
|
||||
<script src="../runtime/ritz-debug.js"></script>
|
||||
<script>
|
||||
// Wait for DOM to be ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new RitzLoader().load('Tabs.wasm', 'Tabs', { orientation: 0 });
|
||||
});
|
||||
} else {
|
||||
new RitzLoader().load('Tabs.wasm', 'Tabs', { orientation: 0 });
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,186 @@
|
||||
"""
|
||||
Tabs component - Beautiful multi-panel tab switching with proper composition.
|
||||
|
||||
Demonstrates:
|
||||
- Component composition (TabMenu, TabPane)
|
||||
- Match/case for clean panel switching
|
||||
- Conditional CSS classes for active state
|
||||
- All logic in WASM, minimal syntax
|
||||
"""
|
||||
|
||||
from ritz.syntax import *
|
||||
|
||||
|
||||
# ===== Reusable Tab Components =====
|
||||
|
||||
@component
|
||||
def HTab(label, active, onclick):
|
||||
"""Horizontal tab (Li > Button structure).
|
||||
|
||||
Args:
|
||||
label: String - tab label text
|
||||
active: Boolean - whether this tab is active
|
||||
onclick: Function - click handler
|
||||
"""
|
||||
with Li(class_='tab active' if active else 'tab'):
|
||||
with Button(onclick=onclick):
|
||||
yield label
|
||||
|
||||
|
||||
@component
|
||||
def VTab(label, active, onclick):
|
||||
"""Vertical tab button.
|
||||
|
||||
Args:
|
||||
label: String - tab label text
|
||||
active: Boolean - whether this tab is active
|
||||
onclick: Function - click handler
|
||||
"""
|
||||
with Button(class_='vertical-tab active' if active else 'vertical-tab', onclick=onclick):
|
||||
yield label
|
||||
|
||||
|
||||
@component
|
||||
def VerticalTabs():
|
||||
"""Vertical tabs for settings categories."""
|
||||
setting_tab = signal(0) # 0 = General, 1 = Privacy, 2 = Advanced
|
||||
|
||||
def set_setting(value):
|
||||
setting_tab = value
|
||||
|
||||
with Div(class_="vertical-tabs"):
|
||||
# Vertical tab menu - inlined until we support function params
|
||||
with Div(class_="vertical-tab-menu"):
|
||||
with Button(class_='vertical-tab active' if setting_tab == 0 else 'vertical-tab',
|
||||
onclick=lambda: set_setting(0)):
|
||||
yield "⚙️ General"
|
||||
|
||||
with Button(class_='vertical-tab active' if setting_tab == 1 else 'vertical-tab',
|
||||
onclick=lambda: set_setting(1)):
|
||||
yield "🔒 Privacy"
|
||||
|
||||
with Button(class_='vertical-tab active' if setting_tab == 2 else 'vertical-tab',
|
||||
onclick=lambda: set_setting(2)):
|
||||
yield "🚀 Advanced"
|
||||
|
||||
# Vertical tab content
|
||||
with Div(class_="vertical-tab-content"):
|
||||
match setting_tab:
|
||||
case 0:
|
||||
with Div(class_="setting-pane"):
|
||||
with H3():
|
||||
yield "⚙️ General Settings"
|
||||
with P():
|
||||
yield "Language: English"
|
||||
with P():
|
||||
yield "Time Zone: UTC"
|
||||
|
||||
case 1:
|
||||
with Div(class_="setting-pane"):
|
||||
with H3():
|
||||
yield "🔒 Privacy Settings"
|
||||
with P():
|
||||
yield "Data Collection: Disabled"
|
||||
with P():
|
||||
yield "Analytics: Opt-out"
|
||||
|
||||
case 2:
|
||||
with Div(class_="setting-pane"):
|
||||
with H3():
|
||||
yield "🚀 Advanced Settings"
|
||||
with P():
|
||||
yield "Debug Mode: Off"
|
||||
with P():
|
||||
yield "Experimental Features: Disabled"
|
||||
|
||||
case _:
|
||||
with Div(class_="setting-pane"):
|
||||
with P():
|
||||
yield "Invalid setting"
|
||||
|
||||
|
||||
@component
|
||||
def Tabs(orientation=0): # 0 = horizontal, 1 = vertical
|
||||
"""Main tabs container - orchestrates menu and panes."""
|
||||
tab_index = signal(0) # Track which tab is active: 0 = Home, 1 = Profile, 2 = Settings
|
||||
|
||||
def set_tab(value):
|
||||
tab_index = value
|
||||
|
||||
with Div(class_="tabs-container"):
|
||||
with H1():
|
||||
yield "Tab Component Demo"
|
||||
|
||||
with P(class_="subtitle"):
|
||||
yield "Beautiful composition with TabMenu and TabPane components"
|
||||
|
||||
# Tab menu - inlined until we support function params
|
||||
with Ul(class_="tab-menu"):
|
||||
with Li(class_='tab active' if tab_index == 0 else 'tab'):
|
||||
with Button(onclick=lambda: set_tab(0)):
|
||||
yield "🏠 Home"
|
||||
|
||||
with Li(class_='tab active' if tab_index == 1 else 'tab'):
|
||||
with Button(onclick=lambda: set_tab(1)):
|
||||
yield "👤 Profile"
|
||||
|
||||
with Li(class_='tab active' if tab_index == 2 else 'tab'):
|
||||
with Button(onclick=lambda: set_tab(2)):
|
||||
yield "⚙️ Settings"
|
||||
|
||||
# Tab panes - match/case for clean switching
|
||||
with Div(class_="tab-content"):
|
||||
match tab_index:
|
||||
case 0:
|
||||
# Home pane
|
||||
with Div(class_="tab-pane home"):
|
||||
with H2():
|
||||
yield "🏠 Welcome Home"
|
||||
with P():
|
||||
yield "This is your home dashboard."
|
||||
with P():
|
||||
yield "All your important info at a glance."
|
||||
with Ul():
|
||||
with Li():
|
||||
yield "✓ Recent activity"
|
||||
with Li():
|
||||
yield "✓ Quick actions"
|
||||
with Li():
|
||||
yield "✓ Summary stats"
|
||||
|
||||
case 1:
|
||||
# Profile pane
|
||||
with Div(class_="tab-pane profile"):
|
||||
with H2():
|
||||
yield "👤 Your Profile"
|
||||
with P():
|
||||
yield "Manage your account settings."
|
||||
with Div(class_="profile-info"):
|
||||
with P():
|
||||
yield "Name: Ritz User"
|
||||
with P():
|
||||
yield "Email: user@ritz.dev"
|
||||
with P():
|
||||
yield "Member since: 2025"
|
||||
|
||||
case 2:
|
||||
# Settings pane with NESTED vertical tabs!
|
||||
with Div(class_="tab-pane settings"):
|
||||
with H2():
|
||||
yield "⚙️ Settings"
|
||||
with P():
|
||||
yield "Configure your preferences using the categories below:"
|
||||
|
||||
# ← NESTED COMPONENT HERE!
|
||||
yield VerticalTabs()
|
||||
|
||||
case _:
|
||||
# Error fallback
|
||||
with Div(class_="tab-pane error"):
|
||||
with P():
|
||||
yield "⚠️ Invalid tab selected"
|
||||
|
||||
# Debug footer
|
||||
with Div(class_="debug-footer"):
|
||||
with P():
|
||||
yield f"Active: {tab_index}"
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Cleaner Tabs implementation using wrapper components to reduce repetition.
|
||||
"""
|
||||
|
||||
from ritz.syntax import *
|
||||
|
||||
|
||||
@component
|
||||
def VerticalTab(label, is_active, tab_value):
|
||||
"""Reusable vertical tab button with active state handling."""
|
||||
# Note: In current implementation, we still need the if/else for class
|
||||
# But it's encapsulated in one place instead of repeated everywhere
|
||||
if is_active:
|
||||
with Button(class_="vertical-tab active"):
|
||||
yield label
|
||||
else:
|
||||
with Button(class_="vertical-tab"):
|
||||
yield label
|
||||
|
||||
|
||||
@component
|
||||
def VerticalTabs():
|
||||
"""Vertical tabs for settings categories."""
|
||||
setting_tab = signal(0)
|
||||
|
||||
def set_setting(value):
|
||||
setting_tab = value
|
||||
|
||||
with Div(class_="vertical-tabs"):
|
||||
# Vertical tab menu
|
||||
with Div(class_="vertical-tab-menu"):
|
||||
# Now each tab is just one line instead of 6!
|
||||
yield VerticalTab("⚙️ General", setting_tab == 0, 0)
|
||||
yield VerticalTab("🔒 Privacy", setting_tab == 1, 1)
|
||||
yield VerticalTab("🚀 Advanced", setting_tab == 2, 2)
|
||||
|
||||
# Tab content (same as before)
|
||||
with Div(class_="vertical-tab-content"):
|
||||
match setting_tab:
|
||||
case 0:
|
||||
with Div(class_="setting-pane"):
|
||||
with H3():
|
||||
yield "⚙️ General Settings"
|
||||
with P():
|
||||
yield "Language: English"
|
||||
with P():
|
||||
yield "Time Zone: UTC"
|
||||
|
||||
case 1:
|
||||
with Div(class_="setting-pane"):
|
||||
with H3():
|
||||
yield "🔒 Privacy Settings"
|
||||
with P():
|
||||
yield "Data Collection: Disabled"
|
||||
with P():
|
||||
yield "Analytics: Opt-out"
|
||||
|
||||
case 2:
|
||||
with Div(class_="setting-pane"):
|
||||
with H3():
|
||||
yield "🚀 Advanced Settings"
|
||||
with P():
|
||||
yield "Debug Mode: Off"
|
||||
with P():
|
||||
yield "Experimental Features: Disabled"
|
||||
|
||||
case _:
|
||||
with Div(class_="setting-pane"):
|
||||
with P():
|
||||
yield "Invalid setting"
|
||||
@@ -0,0 +1,805 @@
|
||||
; Generated by Ritz Framework with Signals
|
||||
; Components: VerticalTabs, Tabs
|
||||
|
||||
; 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 [13 x i8] c"setting-pane\00"
|
||||
@.str.3 = private unnamed_addr constant [3 x i8] c"h3\00"
|
||||
@.str.4 = private unnamed_addr constant [24 x i8] c"⚙️ General Settings\00"
|
||||
@.str.5 = private unnamed_addr constant [2 x i8] c"p\00"
|
||||
@.str.6 = private unnamed_addr constant [18 x i8] c"Language: English\00"
|
||||
@.str.7 = private unnamed_addr constant [15 x i8] c"Time Zone: UTC\00"
|
||||
@.str.8 = private unnamed_addr constant [22 x i8] c"🔒 Privacy Settings\00"
|
||||
@.str.9 = private unnamed_addr constant [26 x i8] c"Data Collection: Disabled\00"
|
||||
@.str.10 = private unnamed_addr constant [19 x i8] c"Analytics: Opt-out\00"
|
||||
@.str.11 = private unnamed_addr constant [23 x i8] c"🚀 Advanced Settings\00"
|
||||
@.str.12 = private unnamed_addr constant [16 x i8] c"Debug Mode: Off\00"
|
||||
@.str.13 = private unnamed_addr constant [32 x i8] c"Experimental Features: Disabled\00"
|
||||
@.str.14 = private unnamed_addr constant [16 x i8] c"Invalid setting\00"
|
||||
@.str.15 = private unnamed_addr constant [14 x i8] c"vertical-tabs\00"
|
||||
@.str.16 = private unnamed_addr constant [18 x i8] c"vertical-tab-menu\00"
|
||||
@.str.17 = private unnamed_addr constant [7 x i8] c"button\00"
|
||||
@.str.18 = private unnamed_addr constant [20 x i8] c"vertical-tab active\00"
|
||||
@.str.19 = private unnamed_addr constant [13 x i8] c"vertical-tab\00"
|
||||
@.str.20 = private unnamed_addr constant [6 x i8] c"click\00"
|
||||
@.str.21 = private unnamed_addr constant [23 x i8] c"_lambda_0_VerticalTabs\00"
|
||||
@.str.22 = private unnamed_addr constant [15 x i8] c"⚙️ General\00"
|
||||
@.str.23 = private unnamed_addr constant [23 x i8] c"_lambda_1_VerticalTabs\00"
|
||||
@.str.24 = private unnamed_addr constant [13 x i8] c"🔒 Privacy\00"
|
||||
@.str.25 = private unnamed_addr constant [23 x i8] c"_lambda_2_VerticalTabs\00"
|
||||
@.str.26 = private unnamed_addr constant [14 x i8] c"🚀 Advanced\00"
|
||||
@.str.27 = private unnamed_addr constant [21 x i8] c"vertical-tab-content\00"
|
||||
@.str.28 = private unnamed_addr constant [14 x i8] c"tab-pane home\00"
|
||||
@.str.29 = private unnamed_addr constant [3 x i8] c"h2\00"
|
||||
@.str.30 = private unnamed_addr constant [18 x i8] c"🏠 Welcome Home\00"
|
||||
@.str.31 = private unnamed_addr constant [29 x i8] c"This is your home dashboard.\00"
|
||||
@.str.32 = private unnamed_addr constant [17 x i8] c"tab-pane profile\00"
|
||||
@.str.33 = private unnamed_addr constant [18 x i8] c"👤 Your Profile\00"
|
||||
@.str.34 = private unnamed_addr constant [30 x i8] c"Manage your account settings.\00"
|
||||
@.str.35 = private unnamed_addr constant [18 x i8] c"tab-pane settings\00"
|
||||
@.str.36 = private unnamed_addr constant [16 x i8] c"⚙️ Settings\00"
|
||||
@.str.37 = private unnamed_addr constant [55 x i8] c"Configure your preferences using the categories below:\00"
|
||||
@.str.38 = private unnamed_addr constant [15 x i8] c"tab-pane error\00"
|
||||
@.str.39 = private unnamed_addr constant [28 x i8] c"⚠️ Invalid tab selected\00"
|
||||
@.str.40 = private unnamed_addr constant [15 x i8] c"tabs-container\00"
|
||||
@.str.41 = private unnamed_addr constant [3 x i8] c"h1\00"
|
||||
@.str.42 = private unnamed_addr constant [19 x i8] c"Tab Component Demo\00"
|
||||
@.str.43 = private unnamed_addr constant [9 x i8] c"subtitle\00"
|
||||
@.str.44 = private unnamed_addr constant [47 x i8] c"Beautiful composition with ternary expressions\00"
|
||||
@.str.45 = private unnamed_addr constant [3 x i8] c"ul\00"
|
||||
@.str.46 = private unnamed_addr constant [9 x i8] c"tab-menu\00"
|
||||
@.str.47 = private unnamed_addr constant [3 x i8] c"li\00"
|
||||
@.str.48 = private unnamed_addr constant [11 x i8] c"tab active\00"
|
||||
@.str.49 = private unnamed_addr constant [4 x i8] c"tab\00"
|
||||
@.str.50 = private unnamed_addr constant [15 x i8] c"_lambda_0_Tabs\00"
|
||||
@.str.51 = private unnamed_addr constant [10 x i8] c"🏠 Home\00"
|
||||
@.str.52 = private unnamed_addr constant [15 x i8] c"_lambda_1_Tabs\00"
|
||||
@.str.53 = private unnamed_addr constant [13 x i8] c"👤 Profile\00"
|
||||
@.str.54 = private unnamed_addr constant [15 x i8] c"_lambda_2_Tabs\00"
|
||||
@.str.55 = private unnamed_addr constant [12 x i8] c"tab-content\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*)
|
||||
declare i32 @js_create_marker()
|
||||
declare void @js_replace_at_marker(i32, i32, i32)
|
||||
|
||||
; Signal storage for VerticalTabs (reactive state in WASM memory)
|
||||
@signal_VerticalTabs_setting_tab = global i32 0
|
||||
|
||||
; Conditional rendering state
|
||||
@cond_0_marker = global i32 0
|
||||
@cond_0_current = global i32 0
|
||||
@cond_0_case_0_root = global i32 0
|
||||
@cond_0_case_1_root = global i32 0
|
||||
@cond_0_case_2_root = global i32 0
|
||||
@cond_0_case_3_root = global i32 0
|
||||
|
||||
; Signal helper functions
|
||||
define i32 @get_setting_tab() {
|
||||
entry:
|
||||
%val = load i32, i32* @signal_VerticalTabs_setting_tab
|
||||
ret i32 %val
|
||||
}
|
||||
|
||||
define void @set_setting_tab(i32 %newval) {
|
||||
entry:
|
||||
store i32 %newval, i32* @signal_VerticalTabs_setting_tab
|
||||
; TODO: Trigger DOM update callback
|
||||
ret void
|
||||
}
|
||||
|
||||
; Event handler: _lambda_0_VerticalTabs
|
||||
define void @_lambda_0_VerticalTabs() {
|
||||
entry:
|
||||
call void @set_setting(i32 0)
|
||||
ret void
|
||||
}
|
||||
|
||||
; Event handler: _lambda_1_VerticalTabs
|
||||
define void @_lambda_1_VerticalTabs() {
|
||||
entry:
|
||||
call void @set_setting(i32 1)
|
||||
ret void
|
||||
}
|
||||
|
||||
; Event handler: _lambda_2_VerticalTabs
|
||||
define void @_lambda_2_VerticalTabs() {
|
||||
entry:
|
||||
call void @set_setting(i32 2)
|
||||
ret void
|
||||
}
|
||||
|
||||
; Event handler: set_setting
|
||||
define void @set_setting(i32 %value) {
|
||||
entry:
|
||||
store i32 %value, i32* @signal_VerticalTabs_setting_tab
|
||||
call void @reevaluate_conditional_0()
|
||||
ret void
|
||||
}
|
||||
|
||||
; Initialize conditional 0 (match)
|
||||
define void @_init_conditional_0() {
|
||||
entry:
|
||||
; Render all cases upfront
|
||||
%var0 = call i32 @_render_cond_0_case_0()
|
||||
store i32 %var0, i32* @cond_0_case_0_root
|
||||
%var1 = call i32 @_render_cond_0_case_1()
|
||||
store i32 %var1, i32* @cond_0_case_1_root
|
||||
%var2 = call i32 @_render_cond_0_case_2()
|
||||
store i32 %var2, i32* @cond_0_case_2_root
|
||||
%var3 = call i32 @_render_cond_0_case_3()
|
||||
store i32 %var3, i32* @cond_0_case_3_root
|
||||
%var4 = load i32, i32* @signal_VerticalTabs_setting_tab
|
||||
%var5 = load i32, i32* @cond_0_marker
|
||||
switch i32 %var4, label %insert_case_3 [
|
||||
i32 0, label %insert_case_0
|
||||
i32 1, label %insert_case_1
|
||||
i32 2, label %insert_case_2
|
||||
]
|
||||
insert_case_0:
|
||||
call void @js_replace_at_marker(i32 %var5, i32 0, i32 %var0)
|
||||
store i32 0, i32* @cond_0_current
|
||||
ret void
|
||||
insert_case_1:
|
||||
call void @js_replace_at_marker(i32 %var5, i32 0, i32 %var1)
|
||||
store i32 1, i32* @cond_0_current
|
||||
ret void
|
||||
insert_case_2:
|
||||
call void @js_replace_at_marker(i32 %var5, i32 0, i32 %var2)
|
||||
store i32 2, i32* @cond_0_current
|
||||
ret void
|
||||
insert_case_3:
|
||||
call void @js_replace_at_marker(i32 %var5, i32 0, i32 %var3)
|
||||
store i32 3, i32* @cond_0_current
|
||||
ret void
|
||||
}
|
||||
|
||||
; Re-evaluate conditional 0 (match)
|
||||
define void @reevaluate_conditional_0() {
|
||||
entry:
|
||||
%var6 = load i32, i32* @signal_VerticalTabs_setting_tab
|
||||
%var7 = load i32, i32* @cond_0_current
|
||||
switch i32 %var6, label %case_3 [
|
||||
i32 0, label %case_0
|
||||
i32 1, label %case_1
|
||||
i32 2, label %case_2
|
||||
]
|
||||
case_0:
|
||||
%var8 = icmp ne i32 %var7, 0
|
||||
br i1 %var8, label %swap_0, label %done
|
||||
swap_0:
|
||||
%var9 = load i32, i32* @cond_0_marker
|
||||
%var10 = load i32, i32* @cond_0_case_0_root
|
||||
switch i32 %var7, label %load_old_0_3 [
|
||||
i32 0, label %load_old_0_0
|
||||
i32 1, label %load_old_0_1
|
||||
i32 2, label %load_old_0_2
|
||||
i32 3, label %load_old_0_3
|
||||
]
|
||||
load_old_0_0:
|
||||
%var12 = load i32, i32* @cond_0_case_0_root
|
||||
br label %merge_0
|
||||
load_old_0_1:
|
||||
%var13 = load i32, i32* @cond_0_case_1_root
|
||||
br label %merge_0
|
||||
load_old_0_2:
|
||||
%var14 = load i32, i32* @cond_0_case_2_root
|
||||
br label %merge_0
|
||||
load_old_0_3:
|
||||
%var15 = load i32, i32* @cond_0_case_3_root
|
||||
br label %merge_0
|
||||
merge_0:
|
||||
%var11 = phi i32 [ %var12, %load_old_0_0 ], [ %var13, %load_old_0_1 ], [ %var14, %load_old_0_2 ], [ %var15, %load_old_0_3 ]
|
||||
call void @js_replace_at_marker(i32 %var9, i32 %var11, i32 %var10)
|
||||
store i32 0, i32* @cond_0_current
|
||||
br label %done
|
||||
case_1:
|
||||
%var16 = icmp ne i32 %var7, 1
|
||||
br i1 %var16, label %swap_1, label %done
|
||||
swap_1:
|
||||
%var17 = load i32, i32* @cond_0_marker
|
||||
%var18 = load i32, i32* @cond_0_case_1_root
|
||||
switch i32 %var7, label %load_old_1_3 [
|
||||
i32 0, label %load_old_1_0
|
||||
i32 1, label %load_old_1_1
|
||||
i32 2, label %load_old_1_2
|
||||
i32 3, label %load_old_1_3
|
||||
]
|
||||
load_old_1_0:
|
||||
%var20 = load i32, i32* @cond_0_case_0_root
|
||||
br label %merge_1
|
||||
load_old_1_1:
|
||||
%var21 = load i32, i32* @cond_0_case_1_root
|
||||
br label %merge_1
|
||||
load_old_1_2:
|
||||
%var22 = load i32, i32* @cond_0_case_2_root
|
||||
br label %merge_1
|
||||
load_old_1_3:
|
||||
%var23 = load i32, i32* @cond_0_case_3_root
|
||||
br label %merge_1
|
||||
merge_1:
|
||||
%var19 = phi i32 [ %var20, %load_old_1_0 ], [ %var21, %load_old_1_1 ], [ %var22, %load_old_1_2 ], [ %var23, %load_old_1_3 ]
|
||||
call void @js_replace_at_marker(i32 %var17, i32 %var19, i32 %var18)
|
||||
store i32 1, i32* @cond_0_current
|
||||
br label %done
|
||||
case_2:
|
||||
%var24 = icmp ne i32 %var7, 2
|
||||
br i1 %var24, label %swap_2, label %done
|
||||
swap_2:
|
||||
%var25 = load i32, i32* @cond_0_marker
|
||||
%var26 = load i32, i32* @cond_0_case_2_root
|
||||
switch i32 %var7, label %load_old_2_3 [
|
||||
i32 0, label %load_old_2_0
|
||||
i32 1, label %load_old_2_1
|
||||
i32 2, label %load_old_2_2
|
||||
i32 3, label %load_old_2_3
|
||||
]
|
||||
load_old_2_0:
|
||||
%var28 = load i32, i32* @cond_0_case_0_root
|
||||
br label %merge_2
|
||||
load_old_2_1:
|
||||
%var29 = load i32, i32* @cond_0_case_1_root
|
||||
br label %merge_2
|
||||
load_old_2_2:
|
||||
%var30 = load i32, i32* @cond_0_case_2_root
|
||||
br label %merge_2
|
||||
load_old_2_3:
|
||||
%var31 = load i32, i32* @cond_0_case_3_root
|
||||
br label %merge_2
|
||||
merge_2:
|
||||
%var27 = phi i32 [ %var28, %load_old_2_0 ], [ %var29, %load_old_2_1 ], [ %var30, %load_old_2_2 ], [ %var31, %load_old_2_3 ]
|
||||
call void @js_replace_at_marker(i32 %var25, i32 %var27, i32 %var26)
|
||||
store i32 2, i32* @cond_0_current
|
||||
br label %done
|
||||
case_3:
|
||||
%var32 = icmp ne i32 %var7, 3
|
||||
br i1 %var32, label %swap_3, label %done
|
||||
swap_3:
|
||||
%var33 = load i32, i32* @cond_0_marker
|
||||
%var34 = load i32, i32* @cond_0_case_3_root
|
||||
switch i32 %var7, label %load_old_3_3 [
|
||||
i32 0, label %load_old_3_0
|
||||
i32 1, label %load_old_3_1
|
||||
i32 2, label %load_old_3_2
|
||||
i32 3, label %load_old_3_3
|
||||
]
|
||||
load_old_3_0:
|
||||
%var36 = load i32, i32* @cond_0_case_0_root
|
||||
br label %merge_3
|
||||
load_old_3_1:
|
||||
%var37 = load i32, i32* @cond_0_case_1_root
|
||||
br label %merge_3
|
||||
load_old_3_2:
|
||||
%var38 = load i32, i32* @cond_0_case_2_root
|
||||
br label %merge_3
|
||||
load_old_3_3:
|
||||
%var39 = load i32, i32* @cond_0_case_3_root
|
||||
br label %merge_3
|
||||
merge_3:
|
||||
%var35 = phi i32 [ %var36, %load_old_3_0 ], [ %var37, %load_old_3_1 ], [ %var38, %load_old_3_2 ], [ %var39, %load_old_3_3 ]
|
||||
call void @js_replace_at_marker(i32 %var33, i32 %var35, i32 %var34)
|
||||
store i32 3, i32* @cond_0_current
|
||||
br label %done
|
||||
done:
|
||||
ret void
|
||||
}
|
||||
|
||||
define i32 @_render_cond_0_case_0() {
|
||||
entry:
|
||||
%var40 = call i32 @js_create_element(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.0, i32 0, i32 0))
|
||||
%var41 = 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 %var41, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str.2, i32 0, i32 0))
|
||||
%var42 = call i32 @js_create_element(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.3, i32 0, i32 0))
|
||||
%var43 = call i32 @js_create_text(i8* getelementptr inbounds ([24 x i8], [24 x i8]* @.str.4, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var42, i32 %var43)
|
||||
call void @js_append_child(i32 %var41, i32 %var42)
|
||||
call void @js_append_child(i32 %var41, i32 %var42)
|
||||
%var44 = call i32 @js_create_element(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.5, i32 0, i32 0))
|
||||
%var45 = call i32 @js_create_text(i8* getelementptr inbounds ([18 x i8], [18 x i8]* @.str.6, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var44, i32 %var45)
|
||||
call void @js_append_child(i32 %var41, i32 %var44)
|
||||
call void @js_append_child(i32 %var41, i32 %var44)
|
||||
%var46 = call i32 @js_create_element(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.5, i32 0, i32 0))
|
||||
%var47 = call i32 @js_create_text(i8* getelementptr inbounds ([15 x i8], [15 x i8]* @.str.7, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var46, i32 %var47)
|
||||
call void @js_append_child(i32 %var41, i32 %var46)
|
||||
call void @js_append_child(i32 %var41, i32 %var46)
|
||||
call void @js_append_child(i32 %var40, i32 %var41)
|
||||
ret i32 %var40
|
||||
}
|
||||
|
||||
define i32 @_render_cond_0_case_1() {
|
||||
entry:
|
||||
%var48 = call i32 @js_create_element(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.0, i32 0, i32 0))
|
||||
%var49 = 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 %var49, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str.2, i32 0, i32 0))
|
||||
%var50 = call i32 @js_create_element(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.3, i32 0, i32 0))
|
||||
%var51 = call i32 @js_create_text(i8* getelementptr inbounds ([22 x i8], [22 x i8]* @.str.8, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var50, i32 %var51)
|
||||
call void @js_append_child(i32 %var49, i32 %var50)
|
||||
call void @js_append_child(i32 %var49, i32 %var50)
|
||||
%var52 = call i32 @js_create_element(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.5, i32 0, i32 0))
|
||||
%var53 = call i32 @js_create_text(i8* getelementptr inbounds ([26 x i8], [26 x i8]* @.str.9, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var52, i32 %var53)
|
||||
call void @js_append_child(i32 %var49, i32 %var52)
|
||||
call void @js_append_child(i32 %var49, i32 %var52)
|
||||
%var54 = call i32 @js_create_element(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.5, i32 0, i32 0))
|
||||
%var55 = call i32 @js_create_text(i8* getelementptr inbounds ([19 x i8], [19 x i8]* @.str.10, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var54, i32 %var55)
|
||||
call void @js_append_child(i32 %var49, i32 %var54)
|
||||
call void @js_append_child(i32 %var49, i32 %var54)
|
||||
call void @js_append_child(i32 %var48, i32 %var49)
|
||||
ret i32 %var48
|
||||
}
|
||||
|
||||
define i32 @_render_cond_0_case_2() {
|
||||
entry:
|
||||
%var56 = call i32 @js_create_element(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.0, i32 0, i32 0))
|
||||
%var57 = 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 %var57, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str.2, i32 0, i32 0))
|
||||
%var58 = call i32 @js_create_element(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.3, i32 0, i32 0))
|
||||
%var59 = call i32 @js_create_text(i8* getelementptr inbounds ([23 x i8], [23 x i8]* @.str.11, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var58, i32 %var59)
|
||||
call void @js_append_child(i32 %var57, i32 %var58)
|
||||
call void @js_append_child(i32 %var57, i32 %var58)
|
||||
%var60 = call i32 @js_create_element(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.5, i32 0, i32 0))
|
||||
%var61 = call i32 @js_create_text(i8* getelementptr inbounds ([16 x i8], [16 x i8]* @.str.12, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var60, i32 %var61)
|
||||
call void @js_append_child(i32 %var57, i32 %var60)
|
||||
call void @js_append_child(i32 %var57, i32 %var60)
|
||||
%var62 = call i32 @js_create_element(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.5, i32 0, i32 0))
|
||||
%var63 = call i32 @js_create_text(i8* getelementptr inbounds ([32 x i8], [32 x i8]* @.str.13, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var62, i32 %var63)
|
||||
call void @js_append_child(i32 %var57, i32 %var62)
|
||||
call void @js_append_child(i32 %var57, i32 %var62)
|
||||
call void @js_append_child(i32 %var56, i32 %var57)
|
||||
ret i32 %var56
|
||||
}
|
||||
|
||||
define i32 @_render_cond_0_case_3() {
|
||||
entry:
|
||||
%var64 = call i32 @js_create_element(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.0, i32 0, i32 0))
|
||||
%var65 = 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 %var65, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str.2, i32 0, i32 0))
|
||||
%var66 = call i32 @js_create_element(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.5, i32 0, i32 0))
|
||||
%var67 = call i32 @js_create_text(i8* getelementptr inbounds ([16 x i8], [16 x i8]* @.str.14, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var66, i32 %var67)
|
||||
call void @js_append_child(i32 %var65, i32 %var66)
|
||||
call void @js_append_child(i32 %var65, i32 %var66)
|
||||
call void @js_append_child(i32 %var64, i32 %var65)
|
||||
ret i32 %var64
|
||||
}
|
||||
|
||||
; Component render function
|
||||
define void @render_VerticalTabs() {
|
||||
entry:
|
||||
%var68 = 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 %var68, 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.15, i32 0, i32 0))
|
||||
%var69 = 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 %var69, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([18 x i8], [18 x i8]* @.str.16, i32 0, i32 0))
|
||||
%var70 = call i32 @js_create_element(i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str.17, i32 0, i32 0))
|
||||
%var71 = load i32, i32* @signal_VerticalTabs_setting_tab
|
||||
%var72 = icmp eq i32 %var71, 0
|
||||
%var73 = zext i1 %var72 to i32
|
||||
%var74 = icmp ne i32 %var73, 0
|
||||
%var75 = select i1 %var74, i8* getelementptr inbounds ([20 x i8], [20 x i8]* @.str.18, i32 0, i32 0), i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str.19, i32 0, i32 0)
|
||||
call void @js_set_attribute(i32 %var70, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* %var75)
|
||||
call void @js_bind_event(i32 %var70, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.20, i32 0, i32 0), i8* getelementptr inbounds ([23 x i8], [23 x i8]* @.str.21, i32 0, i32 0))
|
||||
%var76 = call i32 @js_create_text(i8* getelementptr inbounds ([15 x i8], [15 x i8]* @.str.22, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var70, i32 %var76)
|
||||
call void @js_append_child(i32 %var69, i32 %var70)
|
||||
call void @js_append_child(i32 %var69, i32 %var70)
|
||||
%var77 = call i32 @js_create_element(i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str.17, i32 0, i32 0))
|
||||
%var78 = load i32, i32* @signal_VerticalTabs_setting_tab
|
||||
%var79 = icmp eq i32 %var78, 1
|
||||
%var80 = zext i1 %var79 to i32
|
||||
%var81 = icmp ne i32 %var80, 0
|
||||
%var82 = select i1 %var81, i8* getelementptr inbounds ([20 x i8], [20 x i8]* @.str.18, i32 0, i32 0), i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str.19, i32 0, i32 0)
|
||||
call void @js_set_attribute(i32 %var77, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* %var82)
|
||||
call void @js_bind_event(i32 %var77, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.20, i32 0, i32 0), i8* getelementptr inbounds ([23 x i8], [23 x i8]* @.str.23, i32 0, i32 0))
|
||||
%var83 = call i32 @js_create_text(i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str.24, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var77, i32 %var83)
|
||||
call void @js_append_child(i32 %var69, i32 %var77)
|
||||
call void @js_append_child(i32 %var69, i32 %var77)
|
||||
%var84 = call i32 @js_create_element(i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str.17, i32 0, i32 0))
|
||||
%var85 = load i32, i32* @signal_VerticalTabs_setting_tab
|
||||
%var86 = icmp eq i32 %var85, 2
|
||||
%var87 = zext i1 %var86 to i32
|
||||
%var88 = icmp ne i32 %var87, 0
|
||||
%var89 = select i1 %var88, i8* getelementptr inbounds ([20 x i8], [20 x i8]* @.str.18, i32 0, i32 0), i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str.19, i32 0, i32 0)
|
||||
call void @js_set_attribute(i32 %var84, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* %var89)
|
||||
call void @js_bind_event(i32 %var84, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.20, i32 0, i32 0), i8* getelementptr inbounds ([23 x i8], [23 x i8]* @.str.25, i32 0, i32 0))
|
||||
%var90 = call i32 @js_create_text(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str.26, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var84, i32 %var90)
|
||||
call void @js_append_child(i32 %var69, i32 %var84)
|
||||
call void @js_append_child(i32 %var69, i32 %var84)
|
||||
call void @js_append_child(i32 %var68, i32 %var69)
|
||||
call void @js_append_child(i32 %var68, i32 %var69)
|
||||
%var91 = 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 %var91, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([21 x i8], [21 x i8]* @.str.27, i32 0, i32 0))
|
||||
%var92 = call i32 @js_create_marker()
|
||||
call void @js_append_child(i32 %var91, i32 %var92)
|
||||
store i32 %var92, i32* @cond_0_marker
|
||||
call void @_init_conditional_0()
|
||||
call void @js_append_child(i32 %var68, i32 %var91)
|
||||
call void @js_append_child(i32 %var68, i32 %var91)
|
||||
call void @js_set_root(i32 %var68)
|
||||
ret void
|
||||
}
|
||||
|
||||
; Signal storage for Tabs (reactive state in WASM memory)
|
||||
@signal_Tabs_tab_index = global i32 0
|
||||
|
||||
; Conditional rendering state
|
||||
@cond_1_marker = global i32 0
|
||||
@cond_1_current = global i32 0
|
||||
@cond_1_case_0_root = global i32 0
|
||||
@cond_1_case_1_root = global i32 0
|
||||
@cond_1_case_2_root = global i32 0
|
||||
@cond_1_case_3_root = global i32 0
|
||||
|
||||
; Signal helper functions
|
||||
define i32 @get_tab_index() {
|
||||
entry:
|
||||
%val = load i32, i32* @signal_Tabs_tab_index
|
||||
ret i32 %val
|
||||
}
|
||||
|
||||
define void @set_tab_index(i32 %newval) {
|
||||
entry:
|
||||
store i32 %newval, i32* @signal_Tabs_tab_index
|
||||
; TODO: Trigger DOM update callback
|
||||
ret void
|
||||
}
|
||||
|
||||
; Event handler: _lambda_0_Tabs
|
||||
define void @_lambda_0_Tabs() {
|
||||
entry:
|
||||
call void @set_tab(i32 0)
|
||||
ret void
|
||||
}
|
||||
|
||||
; Event handler: _lambda_1_Tabs
|
||||
define void @_lambda_1_Tabs() {
|
||||
entry:
|
||||
call void @set_tab(i32 1)
|
||||
ret void
|
||||
}
|
||||
|
||||
; Event handler: _lambda_2_Tabs
|
||||
define void @_lambda_2_Tabs() {
|
||||
entry:
|
||||
call void @set_tab(i32 2)
|
||||
ret void
|
||||
}
|
||||
|
||||
; Event handler: set_tab
|
||||
define void @set_tab(i32 %value) {
|
||||
entry:
|
||||
store i32 %value, i32* @signal_Tabs_tab_index
|
||||
call void @reevaluate_conditional_1()
|
||||
ret void
|
||||
}
|
||||
|
||||
; Initialize conditional 1 (match)
|
||||
define void @_init_conditional_1() {
|
||||
entry:
|
||||
; Render all cases upfront
|
||||
%var93 = call i32 @_render_cond_1_case_0()
|
||||
store i32 %var93, i32* @cond_1_case_0_root
|
||||
%var94 = call i32 @_render_cond_1_case_1()
|
||||
store i32 %var94, i32* @cond_1_case_1_root
|
||||
%var95 = call i32 @_render_cond_1_case_2()
|
||||
store i32 %var95, i32* @cond_1_case_2_root
|
||||
%var96 = call i32 @_render_cond_1_case_3()
|
||||
store i32 %var96, i32* @cond_1_case_3_root
|
||||
%var97 = load i32, i32* @signal_Tabs_tab_index
|
||||
%var98 = load i32, i32* @cond_1_marker
|
||||
switch i32 %var97, label %insert_case_3 [
|
||||
i32 0, label %insert_case_0
|
||||
i32 1, label %insert_case_1
|
||||
i32 2, label %insert_case_2
|
||||
]
|
||||
insert_case_0:
|
||||
call void @js_replace_at_marker(i32 %var98, i32 0, i32 %var93)
|
||||
store i32 0, i32* @cond_1_current
|
||||
ret void
|
||||
insert_case_1:
|
||||
call void @js_replace_at_marker(i32 %var98, i32 0, i32 %var94)
|
||||
store i32 1, i32* @cond_1_current
|
||||
ret void
|
||||
insert_case_2:
|
||||
call void @js_replace_at_marker(i32 %var98, i32 0, i32 %var95)
|
||||
store i32 2, i32* @cond_1_current
|
||||
ret void
|
||||
insert_case_3:
|
||||
call void @js_replace_at_marker(i32 %var98, i32 0, i32 %var96)
|
||||
store i32 3, i32* @cond_1_current
|
||||
ret void
|
||||
}
|
||||
|
||||
; Re-evaluate conditional 1 (match)
|
||||
define void @reevaluate_conditional_1() {
|
||||
entry:
|
||||
%var99 = load i32, i32* @signal_Tabs_tab_index
|
||||
%var100 = load i32, i32* @cond_1_current
|
||||
switch i32 %var99, label %case_3 [
|
||||
i32 0, label %case_0
|
||||
i32 1, label %case_1
|
||||
i32 2, label %case_2
|
||||
]
|
||||
case_0:
|
||||
%var101 = icmp ne i32 %var100, 0
|
||||
br i1 %var101, label %swap_0, label %done
|
||||
swap_0:
|
||||
%var102 = load i32, i32* @cond_1_marker
|
||||
%var103 = load i32, i32* @cond_1_case_0_root
|
||||
switch i32 %var100, label %load_old_0_3 [
|
||||
i32 0, label %load_old_0_0
|
||||
i32 1, label %load_old_0_1
|
||||
i32 2, label %load_old_0_2
|
||||
i32 3, label %load_old_0_3
|
||||
]
|
||||
load_old_0_0:
|
||||
%var105 = load i32, i32* @cond_1_case_0_root
|
||||
br label %merge_0
|
||||
load_old_0_1:
|
||||
%var106 = load i32, i32* @cond_1_case_1_root
|
||||
br label %merge_0
|
||||
load_old_0_2:
|
||||
%var107 = load i32, i32* @cond_1_case_2_root
|
||||
br label %merge_0
|
||||
load_old_0_3:
|
||||
%var108 = load i32, i32* @cond_1_case_3_root
|
||||
br label %merge_0
|
||||
merge_0:
|
||||
%var104 = phi i32 [ %var105, %load_old_0_0 ], [ %var106, %load_old_0_1 ], [ %var107, %load_old_0_2 ], [ %var108, %load_old_0_3 ]
|
||||
call void @js_replace_at_marker(i32 %var102, i32 %var104, i32 %var103)
|
||||
store i32 0, i32* @cond_1_current
|
||||
br label %done
|
||||
case_1:
|
||||
%var109 = icmp ne i32 %var100, 1
|
||||
br i1 %var109, label %swap_1, label %done
|
||||
swap_1:
|
||||
%var110 = load i32, i32* @cond_1_marker
|
||||
%var111 = load i32, i32* @cond_1_case_1_root
|
||||
switch i32 %var100, label %load_old_1_3 [
|
||||
i32 0, label %load_old_1_0
|
||||
i32 1, label %load_old_1_1
|
||||
i32 2, label %load_old_1_2
|
||||
i32 3, label %load_old_1_3
|
||||
]
|
||||
load_old_1_0:
|
||||
%var113 = load i32, i32* @cond_1_case_0_root
|
||||
br label %merge_1
|
||||
load_old_1_1:
|
||||
%var114 = load i32, i32* @cond_1_case_1_root
|
||||
br label %merge_1
|
||||
load_old_1_2:
|
||||
%var115 = load i32, i32* @cond_1_case_2_root
|
||||
br label %merge_1
|
||||
load_old_1_3:
|
||||
%var116 = load i32, i32* @cond_1_case_3_root
|
||||
br label %merge_1
|
||||
merge_1:
|
||||
%var112 = phi i32 [ %var113, %load_old_1_0 ], [ %var114, %load_old_1_1 ], [ %var115, %load_old_1_2 ], [ %var116, %load_old_1_3 ]
|
||||
call void @js_replace_at_marker(i32 %var110, i32 %var112, i32 %var111)
|
||||
store i32 1, i32* @cond_1_current
|
||||
br label %done
|
||||
case_2:
|
||||
%var117 = icmp ne i32 %var100, 2
|
||||
br i1 %var117, label %swap_2, label %done
|
||||
swap_2:
|
||||
%var118 = load i32, i32* @cond_1_marker
|
||||
%var119 = load i32, i32* @cond_1_case_2_root
|
||||
switch i32 %var100, label %load_old_2_3 [
|
||||
i32 0, label %load_old_2_0
|
||||
i32 1, label %load_old_2_1
|
||||
i32 2, label %load_old_2_2
|
||||
i32 3, label %load_old_2_3
|
||||
]
|
||||
load_old_2_0:
|
||||
%var121 = load i32, i32* @cond_1_case_0_root
|
||||
br label %merge_2
|
||||
load_old_2_1:
|
||||
%var122 = load i32, i32* @cond_1_case_1_root
|
||||
br label %merge_2
|
||||
load_old_2_2:
|
||||
%var123 = load i32, i32* @cond_1_case_2_root
|
||||
br label %merge_2
|
||||
load_old_2_3:
|
||||
%var124 = load i32, i32* @cond_1_case_3_root
|
||||
br label %merge_2
|
||||
merge_2:
|
||||
%var120 = phi i32 [ %var121, %load_old_2_0 ], [ %var122, %load_old_2_1 ], [ %var123, %load_old_2_2 ], [ %var124, %load_old_2_3 ]
|
||||
call void @js_replace_at_marker(i32 %var118, i32 %var120, i32 %var119)
|
||||
store i32 2, i32* @cond_1_current
|
||||
br label %done
|
||||
case_3:
|
||||
%var125 = icmp ne i32 %var100, 3
|
||||
br i1 %var125, label %swap_3, label %done
|
||||
swap_3:
|
||||
%var126 = load i32, i32* @cond_1_marker
|
||||
%var127 = load i32, i32* @cond_1_case_3_root
|
||||
switch i32 %var100, label %load_old_3_3 [
|
||||
i32 0, label %load_old_3_0
|
||||
i32 1, label %load_old_3_1
|
||||
i32 2, label %load_old_3_2
|
||||
i32 3, label %load_old_3_3
|
||||
]
|
||||
load_old_3_0:
|
||||
%var129 = load i32, i32* @cond_1_case_0_root
|
||||
br label %merge_3
|
||||
load_old_3_1:
|
||||
%var130 = load i32, i32* @cond_1_case_1_root
|
||||
br label %merge_3
|
||||
load_old_3_2:
|
||||
%var131 = load i32, i32* @cond_1_case_2_root
|
||||
br label %merge_3
|
||||
load_old_3_3:
|
||||
%var132 = load i32, i32* @cond_1_case_3_root
|
||||
br label %merge_3
|
||||
merge_3:
|
||||
%var128 = phi i32 [ %var129, %load_old_3_0 ], [ %var130, %load_old_3_1 ], [ %var131, %load_old_3_2 ], [ %var132, %load_old_3_3 ]
|
||||
call void @js_replace_at_marker(i32 %var126, i32 %var128, i32 %var127)
|
||||
store i32 3, i32* @cond_1_current
|
||||
br label %done
|
||||
done:
|
||||
ret void
|
||||
}
|
||||
|
||||
define i32 @_render_cond_1_case_0() {
|
||||
entry:
|
||||
%var133 = call i32 @js_create_element(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.0, i32 0, i32 0))
|
||||
%var134 = 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 %var134, 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.28, i32 0, i32 0))
|
||||
%var135 = call i32 @js_create_element(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.29, i32 0, i32 0))
|
||||
%var136 = call i32 @js_create_text(i8* getelementptr inbounds ([18 x i8], [18 x i8]* @.str.30, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var135, i32 %var136)
|
||||
call void @js_append_child(i32 %var134, i32 %var135)
|
||||
call void @js_append_child(i32 %var134, i32 %var135)
|
||||
%var137 = call i32 @js_create_element(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.5, i32 0, i32 0))
|
||||
%var138 = call i32 @js_create_text(i8* getelementptr inbounds ([29 x i8], [29 x i8]* @.str.31, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var137, i32 %var138)
|
||||
call void @js_append_child(i32 %var134, i32 %var137)
|
||||
call void @js_append_child(i32 %var134, i32 %var137)
|
||||
call void @js_append_child(i32 %var133, i32 %var134)
|
||||
ret i32 %var133
|
||||
}
|
||||
|
||||
define i32 @_render_cond_1_case_1() {
|
||||
entry:
|
||||
%var139 = call i32 @js_create_element(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.0, i32 0, i32 0))
|
||||
%var140 = 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 %var140, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([17 x i8], [17 x i8]* @.str.32, i32 0, i32 0))
|
||||
%var141 = call i32 @js_create_element(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.29, i32 0, i32 0))
|
||||
%var142 = call i32 @js_create_text(i8* getelementptr inbounds ([18 x i8], [18 x i8]* @.str.33, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var141, i32 %var142)
|
||||
call void @js_append_child(i32 %var140, i32 %var141)
|
||||
call void @js_append_child(i32 %var140, i32 %var141)
|
||||
%var143 = call i32 @js_create_element(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.5, i32 0, i32 0))
|
||||
%var144 = call i32 @js_create_text(i8* getelementptr inbounds ([30 x i8], [30 x i8]* @.str.34, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var143, i32 %var144)
|
||||
call void @js_append_child(i32 %var140, i32 %var143)
|
||||
call void @js_append_child(i32 %var140, i32 %var143)
|
||||
call void @js_append_child(i32 %var139, i32 %var140)
|
||||
ret i32 %var139
|
||||
}
|
||||
|
||||
define i32 @_render_cond_1_case_2() {
|
||||
entry:
|
||||
%var145 = call i32 @js_create_element(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.0, i32 0, i32 0))
|
||||
%var146 = 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 %var146, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([18 x i8], [18 x i8]* @.str.35, i32 0, i32 0))
|
||||
%var147 = call i32 @js_create_element(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.29, i32 0, i32 0))
|
||||
%var148 = call i32 @js_create_text(i8* getelementptr inbounds ([16 x i8], [16 x i8]* @.str.36, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var147, i32 %var148)
|
||||
call void @js_append_child(i32 %var146, i32 %var147)
|
||||
call void @js_append_child(i32 %var146, i32 %var147)
|
||||
%var149 = call i32 @js_create_element(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.5, i32 0, i32 0))
|
||||
%var150 = call i32 @js_create_text(i8* getelementptr inbounds ([55 x i8], [55 x i8]* @.str.37, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var149, i32 %var150)
|
||||
call void @js_append_child(i32 %var146, i32 %var149)
|
||||
call void @js_append_child(i32 %var146, i32 %var149)
|
||||
call void @render_VerticalTabs()
|
||||
call void @js_append_child(i32 %var145, i32 %var146)
|
||||
ret i32 %var145
|
||||
}
|
||||
|
||||
define i32 @_render_cond_1_case_3() {
|
||||
entry:
|
||||
%var151 = call i32 @js_create_element(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.0, i32 0, i32 0))
|
||||
%var152 = 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 %var152, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([15 x i8], [15 x i8]* @.str.38, i32 0, i32 0))
|
||||
%var153 = call i32 @js_create_element(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.5, i32 0, i32 0))
|
||||
%var154 = call i32 @js_create_text(i8* getelementptr inbounds ([28 x i8], [28 x i8]* @.str.39, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var153, i32 %var154)
|
||||
call void @js_append_child(i32 %var152, i32 %var153)
|
||||
call void @js_append_child(i32 %var152, i32 %var153)
|
||||
call void @js_append_child(i32 %var151, i32 %var152)
|
||||
ret i32 %var151
|
||||
}
|
||||
|
||||
; Component render function
|
||||
define void @render_Tabs() {
|
||||
entry:
|
||||
%var155 = 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 %var155, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([15 x i8], [15 x i8]* @.str.40, i32 0, i32 0))
|
||||
%var156 = call i32 @js_create_element(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.41, i32 0, i32 0))
|
||||
%var157 = call i32 @js_create_text(i8* getelementptr inbounds ([19 x i8], [19 x i8]* @.str.42, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var156, i32 %var157)
|
||||
call void @js_append_child(i32 %var155, i32 %var156)
|
||||
call void @js_append_child(i32 %var155, i32 %var156)
|
||||
%var158 = call i32 @js_create_element(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.5, i32 0, i32 0))
|
||||
call void @js_set_attribute(i32 %var158, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([9 x i8], [9 x i8]* @.str.43, i32 0, i32 0))
|
||||
%var159 = call i32 @js_create_text(i8* getelementptr inbounds ([47 x i8], [47 x i8]* @.str.44, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var158, i32 %var159)
|
||||
call void @js_append_child(i32 %var155, i32 %var158)
|
||||
call void @js_append_child(i32 %var155, i32 %var158)
|
||||
%var160 = call i32 @js_create_element(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.45, i32 0, i32 0))
|
||||
call void @js_set_attribute(i32 %var160, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([9 x i8], [9 x i8]* @.str.46, i32 0, i32 0))
|
||||
%var161 = call i32 @js_create_element(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.47, i32 0, i32 0))
|
||||
%var162 = load i32, i32* @signal_Tabs_tab_index
|
||||
%var163 = icmp eq i32 %var162, 0
|
||||
%var164 = zext i1 %var163 to i32
|
||||
%var165 = icmp ne i32 %var164, 0
|
||||
%var166 = select i1 %var165, i8* getelementptr inbounds ([11 x i8], [11 x i8]* @.str.48, i32 0, i32 0), i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.49, i32 0, i32 0)
|
||||
call void @js_set_attribute(i32 %var161, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* %var166)
|
||||
%var167 = call i32 @js_create_element(i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str.17, i32 0, i32 0))
|
||||
call void @js_bind_event(i32 %var167, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.20, i32 0, i32 0), i8* getelementptr inbounds ([15 x i8], [15 x i8]* @.str.50, i32 0, i32 0))
|
||||
%var168 = call i32 @js_create_text(i8* getelementptr inbounds ([10 x i8], [10 x i8]* @.str.51, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var167, i32 %var168)
|
||||
call void @js_append_child(i32 %var161, i32 %var167)
|
||||
call void @js_append_child(i32 %var161, i32 %var167)
|
||||
call void @js_append_child(i32 %var160, i32 %var161)
|
||||
call void @js_append_child(i32 %var160, i32 %var161)
|
||||
%var169 = call i32 @js_create_element(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.47, i32 0, i32 0))
|
||||
%var170 = load i32, i32* @signal_Tabs_tab_index
|
||||
%var171 = icmp eq i32 %var170, 1
|
||||
%var172 = zext i1 %var171 to i32
|
||||
%var173 = icmp ne i32 %var172, 0
|
||||
%var174 = select i1 %var173, i8* getelementptr inbounds ([11 x i8], [11 x i8]* @.str.48, i32 0, i32 0), i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.49, i32 0, i32 0)
|
||||
call void @js_set_attribute(i32 %var169, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* %var174)
|
||||
%var175 = call i32 @js_create_element(i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str.17, i32 0, i32 0))
|
||||
call void @js_bind_event(i32 %var175, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.20, i32 0, i32 0), i8* getelementptr inbounds ([15 x i8], [15 x i8]* @.str.52, i32 0, i32 0))
|
||||
%var176 = call i32 @js_create_text(i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str.53, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var175, i32 %var176)
|
||||
call void @js_append_child(i32 %var169, i32 %var175)
|
||||
call void @js_append_child(i32 %var169, i32 %var175)
|
||||
call void @js_append_child(i32 %var160, i32 %var169)
|
||||
call void @js_append_child(i32 %var160, i32 %var169)
|
||||
%var177 = call i32 @js_create_element(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.47, i32 0, i32 0))
|
||||
%var178 = load i32, i32* @signal_Tabs_tab_index
|
||||
%var179 = icmp eq i32 %var178, 2
|
||||
%var180 = zext i1 %var179 to i32
|
||||
%var181 = icmp ne i32 %var180, 0
|
||||
%var182 = select i1 %var181, i8* getelementptr inbounds ([11 x i8], [11 x i8]* @.str.48, i32 0, i32 0), i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.49, i32 0, i32 0)
|
||||
call void @js_set_attribute(i32 %var177, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* %var182)
|
||||
%var183 = call i32 @js_create_element(i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str.17, i32 0, i32 0))
|
||||
call void @js_bind_event(i32 %var183, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.20, i32 0, i32 0), i8* getelementptr inbounds ([15 x i8], [15 x i8]* @.str.54, i32 0, i32 0))
|
||||
%var184 = call i32 @js_create_text(i8* getelementptr inbounds ([16 x i8], [16 x i8]* @.str.36, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var183, i32 %var184)
|
||||
call void @js_append_child(i32 %var177, i32 %var183)
|
||||
call void @js_append_child(i32 %var177, i32 %var183)
|
||||
call void @js_append_child(i32 %var160, i32 %var177)
|
||||
call void @js_append_child(i32 %var160, i32 %var177)
|
||||
call void @js_append_child(i32 %var155, i32 %var160)
|
||||
call void @js_append_child(i32 %var155, i32 %var160)
|
||||
%var185 = 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 %var185, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([12 x i8], [12 x i8]* @.str.55, i32 0, i32 0))
|
||||
%var186 = call i32 @js_create_marker()
|
||||
call void @js_append_child(i32 %var185, i32 %var186)
|
||||
store i32 %var186, i32* @cond_1_marker
|
||||
call void @_init_conditional_1()
|
||||
call void @js_append_child(i32 %var155, i32 %var185)
|
||||
call void @js_append_child(i32 %var155, i32 %var185)
|
||||
call void @js_set_root(i32 %var155)
|
||||
ret void
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
Clean Tabs implementation using ternary expressions to eliminate repetition.
|
||||
|
||||
Before: 6 lines per tab button (if/else blocks)
|
||||
After: 1 line per tab button (ternary expressions)
|
||||
"""
|
||||
|
||||
from ritz.syntax import *
|
||||
|
||||
|
||||
@component
|
||||
def VerticalTabs():
|
||||
"""Vertical tabs for settings categories."""
|
||||
setting_tab = signal(0)
|
||||
|
||||
def set_setting(value):
|
||||
setting_tab = value
|
||||
|
||||
with Div(class_="vertical-tabs"):
|
||||
# Vertical tab menu - now just 3 lines instead of 18!
|
||||
with Div(class_="vertical-tab-menu"):
|
||||
with Button(class_='vertical-tab active' if setting_tab == 0 else 'vertical-tab', onclick=lambda: set_setting(0)):
|
||||
yield "⚙️ General"
|
||||
|
||||
with Button(class_='vertical-tab active' if setting_tab == 1 else 'vertical-tab', onclick=lambda: set_setting(1)):
|
||||
yield "🔒 Privacy"
|
||||
|
||||
with Button(class_='vertical-tab active' if setting_tab == 2 else 'vertical-tab', onclick=lambda: set_setting(2)):
|
||||
yield "🚀 Advanced"
|
||||
|
||||
# Tab content (same as before)
|
||||
with Div(class_="vertical-tab-content"):
|
||||
match setting_tab:
|
||||
case 0:
|
||||
with Div(class_="setting-pane"):
|
||||
with H3():
|
||||
yield "⚙️ General Settings"
|
||||
with P():
|
||||
yield "Language: English"
|
||||
with P():
|
||||
yield "Time Zone: UTC"
|
||||
|
||||
case 1:
|
||||
with Div(class_="setting-pane"):
|
||||
with H3():
|
||||
yield "🔒 Privacy Settings"
|
||||
with P():
|
||||
yield "Data Collection: Disabled"
|
||||
with P():
|
||||
yield "Analytics: Opt-out"
|
||||
|
||||
case 2:
|
||||
with Div(class_="setting-pane"):
|
||||
with H3():
|
||||
yield "🚀 Advanced Settings"
|
||||
with P():
|
||||
yield "Debug Mode: Off"
|
||||
with P():
|
||||
yield "Experimental Features: Disabled"
|
||||
|
||||
case _:
|
||||
with Div(class_="setting-pane"):
|
||||
with P():
|
||||
yield "Invalid setting"
|
||||
|
||||
|
||||
@component
|
||||
def Tabs():
|
||||
"""Main tabs container with ternary expressions for clean tab buttons."""
|
||||
tab_index = signal(0)
|
||||
|
||||
def set_tab(value):
|
||||
tab_index = value
|
||||
|
||||
with Div(class_="tabs-container"):
|
||||
with H1():
|
||||
yield "Tab Component Demo"
|
||||
|
||||
with P(class_="subtitle"):
|
||||
yield "Beautiful composition with ternary expressions"
|
||||
|
||||
# Tab menu - clean one-line buttons!
|
||||
with Ul(class_="tab-menu"):
|
||||
with Li(class_='tab active' if tab_index == 0 else 'tab'):
|
||||
with Button(onclick=lambda: set_tab(0)):
|
||||
yield "🏠 Home"
|
||||
|
||||
with Li(class_='tab active' if tab_index == 1 else 'tab'):
|
||||
with Button(onclick=lambda: set_tab(1)):
|
||||
yield "👤 Profile"
|
||||
|
||||
with Li(class_='tab active' if tab_index == 2 else 'tab'):
|
||||
with Button(onclick=lambda: set_tab(2)):
|
||||
yield "⚙️ Settings"
|
||||
|
||||
# Tab panes
|
||||
with Div(class_="tab-content"):
|
||||
match tab_index:
|
||||
case 0:
|
||||
with Div(class_="tab-pane home"):
|
||||
with H2():
|
||||
yield "🏠 Welcome Home"
|
||||
with P():
|
||||
yield "This is your home dashboard."
|
||||
|
||||
case 1:
|
||||
with Div(class_="tab-pane profile"):
|
||||
with H2():
|
||||
yield "👤 Your Profile"
|
||||
with P():
|
||||
yield "Manage your account settings."
|
||||
|
||||
case 2:
|
||||
with Div(class_="tab-pane settings"):
|
||||
with H2():
|
||||
yield "⚙️ Settings"
|
||||
with P():
|
||||
yield "Configure your preferences using the categories below:"
|
||||
|
||||
# Nested vertical tabs!
|
||||
yield VerticalTabs()
|
||||
|
||||
case _:
|
||||
with Div(class_="tab-pane error"):
|
||||
with P():
|
||||
yield "⚠️ Invalid tab selected"
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,354 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-evaluate all reactive conditionals
|
||||
for (const [name, fn] of Object.entries(exports)) {
|
||||
if (name.startsWith('reevaluate_conditional_') && typeof fn === 'function') {
|
||||
fn();
|
||||
this._log(`[Reactive] ${name}()`);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-evaluate all reactive match statements
|
||||
for (const [name, fn] of Object.entries(exports)) {
|
||||
if (name.startsWith('reevaluate_match_') && typeof fn === 'function') {
|
||||
fn();
|
||||
this._log(`[Reactive] ${name}()`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Reactive Conditional Rendering =====
|
||||
|
||||
js_create_marker() {
|
||||
// Create a comment node as a marker for conditional insertion
|
||||
const marker = document.createComment('conditional-marker');
|
||||
const id = this.nextNodeId++;
|
||||
this.nodes.set(id, marker);
|
||||
this._log(`Created marker node ${id}`);
|
||||
return id;
|
||||
}
|
||||
|
||||
js_replace_at_marker(markerId, oldNodeId, newNodeId) {
|
||||
// Replace DOM node at marker position
|
||||
const marker = this.nodes.get(markerId);
|
||||
if (!marker) {
|
||||
console.error(`[replace_at_marker] Marker ${markerId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = marker.parentNode;
|
||||
if (!parent) {
|
||||
console.error(`[replace_at_marker] Marker ${markerId} has no parent`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._log(`[replace_at_marker] marker=${markerId}, old=${oldNodeId}, new=${newNodeId}`);
|
||||
|
||||
// Remove old node if it exists (0 means no old node to remove)
|
||||
if (oldNodeId !== 0) {
|
||||
const oldNode = this.nodes.get(oldNodeId);
|
||||
if (!oldNode) {
|
||||
console.warn(`[replace_at_marker] Old node ${oldNodeId} not found in registry`);
|
||||
} else if (!oldNode.parentNode) {
|
||||
console.warn(`[replace_at_marker] Old node ${oldNodeId} not in DOM`);
|
||||
} else if (oldNode.parentNode !== parent) {
|
||||
console.warn(`[replace_at_marker] Old node ${oldNodeId} in wrong parent`);
|
||||
oldNode.parentNode.removeChild(oldNode);
|
||||
this._log(`[replace_at_marker] Removed ${oldNodeId} from wrong parent`);
|
||||
} else {
|
||||
// Remove from correct parent
|
||||
parent.removeChild(oldNode);
|
||||
this._log(`[replace_at_marker] Removed ${oldNodeId} from correct parent`);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert new node after marker (only if newNodeId is not 0)
|
||||
if (newNodeId !== 0) {
|
||||
const newNode = this.nodes.get(newNodeId);
|
||||
if (!newNode) {
|
||||
console.error(`[replace_at_marker] New node ${newNodeId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Insert right after the marker
|
||||
// If newNode is already in DOM elsewhere, this will move it
|
||||
const nextSibling = marker.nextSibling;
|
||||
if (nextSibling) {
|
||||
parent.insertBefore(newNode, nextSibling);
|
||||
this._log(`[replace_at_marker] Inserted ${newNodeId} before sibling`);
|
||||
} else {
|
||||
parent.appendChild(newNode);
|
||||
this._log(`[replace_at_marker] Appended ${newNodeId} (no sibling)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 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, props = {}) {
|
||||
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),
|
||||
js_create_marker: () => runtime.js_create_marker(),
|
||||
js_replace_at_marker: (m, o, n) => runtime.js_replace_at_marker(m, o, n),
|
||||
}
|
||||
};
|
||||
|
||||
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 with props
|
||||
const renderFn = instance.exports[`render_${componentName}`];
|
||||
if (!renderFn) {
|
||||
throw new Error(`render_${componentName} not found in WASM exports`);
|
||||
}
|
||||
|
||||
// Pass props as positional arguments
|
||||
const propValues = Object.values(props);
|
||||
renderFn(...propValues);
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ritz Example</title>
|
||||
<link rel="stylesheet" href="../runtime/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="status"></div>
|
||||
|
||||
<script src="../runtime/ritz-runtime.js"></script>
|
||||
<script>
|
||||
// Load Main.wasm with debug console enabled
|
||||
// The debug console is itself written in Ritz!
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new RitzLoader({ debug: true }).load('Main.wasm');
|
||||
});
|
||||
} else {
|
||||
new RitzLoader({ debug: true }).load('Main.wasm');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,142 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ritz TextInput Example</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #667eea;
|
||||
margin-bottom: 10px;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
font-size: 32px;
|
||||
border: 2px solid #667eea;
|
||||
background: white;
|
||||
color: #667eea;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.counter-display {
|
||||
background: #f8f9ff;
|
||||
padding: 20px 40px;
|
||||
border-radius: 12px;
|
||||
border: 3px solid #667eea;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
min-width: 150px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.todo {
|
||||
margin-top: 40px;
|
||||
padding-top: 30px;
|
||||
border-top: 2px dashed #e0e0e0;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.todo-list {
|
||||
margin-top: 15px;
|
||||
padding-left: 25px;
|
||||
}
|
||||
|
||||
.todo-list li {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#status {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 10px 20px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">Loading...</div>
|
||||
<div id="status"></div>
|
||||
|
||||
<script src="../runtime/ritz-runtime.js"></script>
|
||||
<script>
|
||||
// Wait for DOM to be ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new RitzLoader().load('TextInput.wasm', 'TextInput');
|
||||
});
|
||||
} else {
|
||||
new RitzLoader().load('TextInput.wasm', 'TextInput');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,188 @@
|
||||
; Generated by Ritz Framework with Signals
|
||||
; Components: TextInput
|
||||
|
||||
; String constants
|
||||
@.str.0 = private unnamed_addr constant [8 x i8] c"Count: \00"
|
||||
@.str.1 = private unnamed_addr constant [4 x i8] c"div\00"
|
||||
@.str.2 = private unnamed_addr constant [6 x i8] c"class\00"
|
||||
@.str.3 = private unnamed_addr constant [10 x i8] c"container\00"
|
||||
@.str.4 = private unnamed_addr constant [3 x i8] c"h1\00"
|
||||
@.str.5 = private unnamed_addr constant [17 x i8] c"Reactive Counter\00"
|
||||
@.str.6 = private unnamed_addr constant [2 x i8] c"p\00"
|
||||
@.str.7 = private unnamed_addr constant [13 x i8] c"instructions\00"
|
||||
@.str.8 = private unnamed_addr constant [65 x i8] c"Click buttons to see reactive updates (text input coming soon!):\00"
|
||||
@.str.9 = private unnamed_addr constant [13 x i8] c"button-group\00"
|
||||
@.str.10 = private unnamed_addr constant [7 x i8] c"button\00"
|
||||
@.str.11 = private unnamed_addr constant [6 x i8] c"click\00"
|
||||
@.str.12 = private unnamed_addr constant [10 x i8] c"decrement\00"
|
||||
@.str.13 = private unnamed_addr constant [4 x i8] c"−\00"
|
||||
@.str.14 = private unnamed_addr constant [16 x i8] c"counter-display\00"
|
||||
@.str.15 = private unnamed_addr constant [10 x i8] c"increment\00"
|
||||
@.str.16 = private unnamed_addr constant [2 x i8] c"+\00"
|
||||
@.str.17 = private unnamed_addr constant [5 x i8] c"todo\00"
|
||||
@.str.18 = private unnamed_addr constant [49 x i8] c"TODO: Replace with text input once we implement:\00"
|
||||
@.str.19 = private unnamed_addr constant [3 x i8] c"ul\00"
|
||||
@.str.20 = private unnamed_addr constant [10 x i8] c"todo-list\00"
|
||||
@.str.21 = private unnamed_addr constant [3 x i8] c"li\00"
|
||||
@.str.22 = private unnamed_addr constant [32 x i8] c"1. String signals (i8* not i32)\00"
|
||||
@.str.23 = private unnamed_addr constant [25 x i8] c"2. oninput event handler\00"
|
||||
@.str.24 = private unnamed_addr constant [23 x i8] c"3. Event value passing\00"
|
||||
@.str.25 = private unnamed_addr constant [39 x i8] c"4. F-string interpolation with signals\00"
|
||||
|
||||
; JavaScript bridge function declarations
|
||||
declare i32 @js_create_element(i8*)
|
||||
declare i32 @js_create_text(i8*)
|
||||
declare i32 @js_create_text_with_int(i8*, i32)
|
||||
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*)
|
||||
declare void @js_update_text_with_int(i32, i8*, i32)
|
||||
declare i32 @js_create_marker()
|
||||
declare void @js_replace_at_marker(i32, i32, i32)
|
||||
|
||||
; Signal storage for TextInput (reactive state in WASM memory)
|
||||
@signal_TextInput_count = global i32 0
|
||||
|
||||
; Reactive text node storage
|
||||
@reactive_text_0_node = global i32 0
|
||||
|
||||
; Signal helper functions
|
||||
define i32 @get_count() {
|
||||
entry:
|
||||
%val = load i32, i32* @signal_TextInput_count
|
||||
ret i32 %val
|
||||
}
|
||||
|
||||
define void @set_count(i32 %newval) {
|
||||
entry:
|
||||
store i32 %newval, i32* @signal_TextInput_count
|
||||
; TODO: Trigger DOM update callback
|
||||
ret void
|
||||
}
|
||||
|
||||
; Event handler: increment
|
||||
define void @increment() {
|
||||
entry:
|
||||
%var0 = load i32, i32* @signal_TextInput_count
|
||||
%var1 = add i32 %var0, 1
|
||||
store i32 %var1, i32* @signal_TextInput_count
|
||||
%var2 = load i32, i32* @reactive_text_0_node
|
||||
%var3 = getelementptr inbounds [8 x i8], [8 x i8]* @.str.0, i32 0, i32 0
|
||||
%var4 = load i32, i32* @signal_TextInput_count
|
||||
call void @js_update_text_with_int(i32 %var2, i8* %var3, i32 %var4)
|
||||
ret void
|
||||
}
|
||||
|
||||
; Event handler: decrement
|
||||
define void @decrement() {
|
||||
entry:
|
||||
%var5 = load i32, i32* @signal_TextInput_count
|
||||
%var6 = sub i32 %var5, 1
|
||||
store i32 %var6, i32* @signal_TextInput_count
|
||||
%var7 = load i32, i32* @reactive_text_0_node
|
||||
%var8 = getelementptr inbounds [8 x i8], [8 x i8]* @.str.0, i32 0, i32 0
|
||||
%var9 = load i32, i32* @signal_TextInput_count
|
||||
call void @js_update_text_with_int(i32 %var7, i8* %var8, i32 %var9)
|
||||
ret void
|
||||
}
|
||||
|
||||
; Component render function
|
||||
define i32 @render_TextInput() {
|
||||
entry:
|
||||
%var11 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.1, i32 0, i32 0
|
||||
%var10 = call i32 @js_create_element(i8* %var11)
|
||||
%var12 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.2, i32 0, i32 0
|
||||
%var13 = getelementptr inbounds [10 x i8], [10 x i8]* @.str.3, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var10, i8* %var12, i8* %var13)
|
||||
%var15 = getelementptr inbounds [3 x i8], [3 x i8]* @.str.4, i32 0, i32 0
|
||||
%var14 = call i32 @js_create_element(i8* %var15)
|
||||
%var16 = call i32 @js_create_text(i8* getelementptr inbounds ([17 x i8], [17 x i8]* @.str.5, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var14, i32 %var16)
|
||||
call void @js_append_child(i32 %var10, i32 %var14)
|
||||
%var18 = getelementptr inbounds [2 x i8], [2 x i8]* @.str.6, i32 0, i32 0
|
||||
%var17 = call i32 @js_create_element(i8* %var18)
|
||||
%var19 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.2, i32 0, i32 0
|
||||
%var20 = getelementptr inbounds [13 x i8], [13 x i8]* @.str.7, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var17, i8* %var19, i8* %var20)
|
||||
%var21 = call i32 @js_create_text(i8* getelementptr inbounds ([65 x i8], [65 x i8]* @.str.8, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var17, i32 %var21)
|
||||
call void @js_append_child(i32 %var10, i32 %var17)
|
||||
%var23 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.1, i32 0, i32 0
|
||||
%var22 = call i32 @js_create_element(i8* %var23)
|
||||
%var24 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.2, i32 0, i32 0
|
||||
%var25 = getelementptr inbounds [13 x i8], [13 x i8]* @.str.9, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var22, i8* %var24, i8* %var25)
|
||||
%var27 = getelementptr inbounds [7 x i8], [7 x i8]* @.str.10, i32 0, i32 0
|
||||
%var26 = call i32 @js_create_element(i8* %var27)
|
||||
%var28 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.11, i32 0, i32 0
|
||||
%var29 = getelementptr inbounds [10 x i8], [10 x i8]* @.str.12, i32 0, i32 0
|
||||
call void @js_bind_event(i32 %var26, i8* %var28, i8* %var29)
|
||||
%var30 = call i32 @js_create_text(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.13, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var26, i32 %var30)
|
||||
call void @js_append_child(i32 %var22, i32 %var26)
|
||||
%var32 = getelementptr inbounds [2 x i8], [2 x i8]* @.str.6, i32 0, i32 0
|
||||
%var31 = call i32 @js_create_element(i8* %var32)
|
||||
%var33 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.2, i32 0, i32 0
|
||||
%var34 = getelementptr inbounds [16 x i8], [16 x i8]* @.str.14, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var31, i8* %var33, i8* %var34)
|
||||
%var35 = getelementptr inbounds [8 x i8], [8 x i8]* @.str.0, i32 0, i32 0
|
||||
%var36 = load i32, i32* @signal_TextInput_count
|
||||
%var37 = call i32 @js_create_text_with_int(i8* %var35, i32 %var36)
|
||||
store i32 %var37, i32* @reactive_text_0_node
|
||||
call void @js_append_child(i32 %var31, i32 %var37)
|
||||
call void @js_append_child(i32 %var22, i32 %var31)
|
||||
%var39 = getelementptr inbounds [7 x i8], [7 x i8]* @.str.10, i32 0, i32 0
|
||||
%var38 = call i32 @js_create_element(i8* %var39)
|
||||
%var40 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.11, i32 0, i32 0
|
||||
%var41 = getelementptr inbounds [10 x i8], [10 x i8]* @.str.15, i32 0, i32 0
|
||||
call void @js_bind_event(i32 %var38, i8* %var40, i8* %var41)
|
||||
%var42 = call i32 @js_create_text(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.16, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var38, i32 %var42)
|
||||
call void @js_append_child(i32 %var22, i32 %var38)
|
||||
call void @js_append_child(i32 %var10, i32 %var22)
|
||||
%var44 = getelementptr inbounds [2 x i8], [2 x i8]* @.str.6, i32 0, i32 0
|
||||
%var43 = call i32 @js_create_element(i8* %var44)
|
||||
%var45 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.2, i32 0, i32 0
|
||||
%var46 = getelementptr inbounds [5 x i8], [5 x i8]* @.str.17, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var43, i8* %var45, i8* %var46)
|
||||
%var47 = call i32 @js_create_text(i8* getelementptr inbounds ([49 x i8], [49 x i8]* @.str.18, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var43, i32 %var47)
|
||||
call void @js_append_child(i32 %var10, i32 %var43)
|
||||
%var49 = getelementptr inbounds [3 x i8], [3 x i8]* @.str.19, i32 0, i32 0
|
||||
%var48 = call i32 @js_create_element(i8* %var49)
|
||||
%var50 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.2, i32 0, i32 0
|
||||
%var51 = getelementptr inbounds [10 x i8], [10 x i8]* @.str.20, i32 0, i32 0
|
||||
call void @js_set_attribute(i32 %var48, i8* %var50, i8* %var51)
|
||||
%var53 = getelementptr inbounds [3 x i8], [3 x i8]* @.str.21, i32 0, i32 0
|
||||
%var52 = call i32 @js_create_element(i8* %var53)
|
||||
%var54 = call i32 @js_create_text(i8* getelementptr inbounds ([32 x i8], [32 x i8]* @.str.22, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var52, i32 %var54)
|
||||
call void @js_append_child(i32 %var48, i32 %var52)
|
||||
%var56 = getelementptr inbounds [3 x i8], [3 x i8]* @.str.21, i32 0, i32 0
|
||||
%var55 = call i32 @js_create_element(i8* %var56)
|
||||
%var57 = call i32 @js_create_text(i8* getelementptr inbounds ([25 x i8], [25 x i8]* @.str.23, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var55, i32 %var57)
|
||||
call void @js_append_child(i32 %var48, i32 %var55)
|
||||
%var59 = getelementptr inbounds [3 x i8], [3 x i8]* @.str.21, i32 0, i32 0
|
||||
%var58 = call i32 @js_create_element(i8* %var59)
|
||||
%var60 = call i32 @js_create_text(i8* getelementptr inbounds ([23 x i8], [23 x i8]* @.str.24, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var58, i32 %var60)
|
||||
call void @js_append_child(i32 %var48, i32 %var58)
|
||||
%var62 = getelementptr inbounds [3 x i8], [3 x i8]* @.str.21, i32 0, i32 0
|
||||
%var61 = call i32 @js_create_element(i8* %var62)
|
||||
%var63 = call i32 @js_create_text(i8* getelementptr inbounds ([39 x i8], [39 x i8]* @.str.25, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var61, i32 %var63)
|
||||
call void @js_append_child(i32 %var48, i32 %var61)
|
||||
call void @js_append_child(i32 %var10, i32 %var48)
|
||||
ret i32 %var10
|
||||
}
|
||||
|
||||
; Main entry point
|
||||
define void @main() {
|
||||
entry:
|
||||
%root = call i32 @render_TextInput()
|
||||
call void @js_set_root(i32 %root)
|
||||
ret void
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
TextInput example - Reactive counter to demonstrate what we need for text input.
|
||||
|
||||
Current limitations exposed by this example:
|
||||
1. No oninput event (only onclick) - need generalized event handling
|
||||
2. No string signals (only i32) - need type system
|
||||
3. No f-string interpolation with signals in text nodes
|
||||
4. No event value passing (onclick doesn't pass event data)
|
||||
5. No string operations (len, concat, etc.)
|
||||
|
||||
For now, using a counter to demonstrate the reactive infrastructure
|
||||
that text input will use once we implement string support.
|
||||
"""
|
||||
|
||||
from ritz.syntax import *
|
||||
|
||||
|
||||
@component
|
||||
def TextInput():
|
||||
"""Counter that demonstrates reactive updates (placeholder for text input)."""
|
||||
count = signal(0) # Integer signal - works!
|
||||
|
||||
def increment():
|
||||
"""Increment counter."""
|
||||
count = count + 1
|
||||
|
||||
def decrement():
|
||||
"""Decrement counter."""
|
||||
count = count - 1
|
||||
|
||||
with Div(class_="container"):
|
||||
with H1():
|
||||
yield "Reactive Counter"
|
||||
|
||||
with P(class_="instructions"):
|
||||
yield "Click buttons to see reactive updates (text input coming soon!):"
|
||||
|
||||
# Buttons (placeholder for text input)
|
||||
with Div(class_="button-group"):
|
||||
with Button(onclick=decrement):
|
||||
yield "−"
|
||||
|
||||
with P(class_="counter-display"):
|
||||
yield f"Count: {count}"
|
||||
|
||||
with Button(onclick=increment):
|
||||
yield "+"
|
||||
|
||||
# Info about what we need
|
||||
with P(class_="todo"):
|
||||
yield "TODO: Replace with text input once we implement:"
|
||||
with Ul(class_="todo-list"):
|
||||
with Li():
|
||||
yield "1. String signals (i8* not i32)"
|
||||
with Li():
|
||||
yield "2. oninput event handler"
|
||||
with Li():
|
||||
yield "3. Event value passing"
|
||||
with Li():
|
||||
yield "4. F-string interpolation with signals"
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,133 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Toggle - Conditional Rendering - Ritz Framework</title>
|
||||
<link rel="stylesheet" href="../runtime/ritz-debug.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
#app {
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 40px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
||||
}
|
||||
.toggle-app {
|
||||
text-align: center;
|
||||
}
|
||||
.toggle-app h1 {
|
||||
color: #667eea;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.toggle-app .info {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.toggle-app .controls {
|
||||
margin: 30px 0;
|
||||
}
|
||||
.toggle-app button {
|
||||
font-size: 18px;
|
||||
padding: 15px 40px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-weight: 600;
|
||||
}
|
||||
.toggle-app button:hover {
|
||||
background: #5568d3;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
.toggle-app button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.toggle-app .display {
|
||||
margin-top: 40px;
|
||||
min-height: 150px;
|
||||
}
|
||||
.toggle-app .high,
|
||||
.toggle-app .low {
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
animation: fadeIn 0.3s;
|
||||
}
|
||||
.toggle-app .high {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: white;
|
||||
}
|
||||
.toggle-app .low {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
color: white;
|
||||
}
|
||||
.toggle-app .high p,
|
||||
.toggle-app .low p {
|
||||
margin: 10px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
.toggle-app .count {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
margin-top: 20px;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: scale(0.95); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<div id="ritz-debug" class="ritz-debug hidden">
|
||||
<div class="debug-header">
|
||||
<h3>🎭 Ritz Debug</h3>
|
||||
<button onclick="ritzDebug.toggle()">×</button>
|
||||
</div>
|
||||
<div id="status" class="status">Loading...</div>
|
||||
<div class="debug-content">
|
||||
<div class="debug-section">
|
||||
<h4>Component</h4>
|
||||
<pre>Toggle.py (Conditional Rendering)</pre>
|
||||
</div>
|
||||
<div class="debug-section">
|
||||
<h4>Features</h4>
|
||||
<pre>✓ if/else statements in WASM
|
||||
✓ Comparison operators (>)
|
||||
✓ Conditional DOM rendering
|
||||
✓ Signal-based conditions</pre>
|
||||
</div>
|
||||
<div class="debug-section">
|
||||
<h4>Console Commands</h4>
|
||||
<pre>ritzRuntime.getLog()
|
||||
ritzInstance.exports.get_count()
|
||||
ritzInstance.exports.increment()</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../runtime/ritz-runtime.js"></script>
|
||||
<script src="../runtime/ritz-debug.js"></script>
|
||||
<script>
|
||||
// Wait for DOM to be ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new RitzLoader().load('Toggle.wasm', 'Toggle');
|
||||
});
|
||||
} else {
|
||||
new RitzLoader().load('Toggle.wasm', 'Toggle');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,203 @@
|
||||
; Generated by Ritz Framework with Signals
|
||||
; Components: Toggle
|
||||
|
||||
; 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 [5 x i8] c"high\00"
|
||||
@.str.3 = private unnamed_addr constant [2 x i8] c"p\00"
|
||||
@.str.4 = private unnamed_addr constant [24 x i8] c"✨ Count is HIGH (> 5)\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 [4 x i8] c"low\00"
|
||||
@.str.8 = private unnamed_addr constant [25 x i8] c"📉 Count is low (<= 5)\00"
|
||||
@.str.9 = private unnamed_addr constant [11 x i8] c"toggle-app\00"
|
||||
@.str.10 = private unnamed_addr constant [3 x i8] c"h1\00"
|
||||
@.str.11 = private unnamed_addr constant [27 x i8] c"Conditional Rendering Demo\00"
|
||||
@.str.12 = private unnamed_addr constant [5 x i8] c"info\00"
|
||||
@.str.13 = private unnamed_addr constant [44 x i8] c"This demonstrates if/else rendering in WASM\00"
|
||||
@.str.14 = private unnamed_addr constant [9 x i8] c"controls\00"
|
||||
@.str.15 = private unnamed_addr constant [7 x i8] c"button\00"
|
||||
@.str.16 = private unnamed_addr constant [6 x i8] c"click\00"
|
||||
@.str.17 = private unnamed_addr constant [10 x i8] c"increment\00"
|
||||
@.str.18 = private unnamed_addr constant [18 x i8] c"Increment Counter\00"
|
||||
@.str.19 = private unnamed_addr constant [8 x i8] c"display\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*)
|
||||
declare i32 @js_create_marker()
|
||||
declare void @js_replace_at_marker(i32, i32, i32)
|
||||
|
||||
; Signal storage for Toggle (reactive state in WASM memory)
|
||||
@signal_Toggle_count = global i32 0
|
||||
|
||||
; Conditional rendering state
|
||||
@cond_0_marker = global i32 0
|
||||
@cond_0_current = global i32 0
|
||||
@cond_0_then_root = global i32 0
|
||||
@cond_0_else_root = global i32 0
|
||||
|
||||
; Signal helper functions
|
||||
define i32 @get_count() {
|
||||
entry:
|
||||
%val = load i32, i32* @signal_Toggle_count
|
||||
ret i32 %val
|
||||
}
|
||||
|
||||
define void @set_count(i32 %newval) {
|
||||
entry:
|
||||
store i32 %newval, i32* @signal_Toggle_count
|
||||
; TODO: Trigger DOM update callback
|
||||
ret void
|
||||
}
|
||||
|
||||
; Event handler: increment
|
||||
define void @increment() {
|
||||
entry:
|
||||
%var0 = load i32, i32* @signal_Toggle_count
|
||||
%var1 = add i32 %var0, 1
|
||||
store i32 %var1, i32* @signal_Toggle_count
|
||||
call void @reevaluate_conditional_0()
|
||||
ret void
|
||||
}
|
||||
|
||||
; Initialize conditional 0
|
||||
define void @_init_conditional_0() {
|
||||
entry:
|
||||
; Render both branches upfront
|
||||
%var2 = call i32 @_render_cond_0_then()
|
||||
%var3 = call i32 @_render_cond_0_else()
|
||||
store i32 %var2, i32* @cond_0_then_root
|
||||
store i32 %var3, i32* @cond_0_else_root
|
||||
%var4 = load i32, i32* @signal_Toggle_count
|
||||
%var5 = icmp sgt i32 %var4, 5
|
||||
%var6 = zext i1 %var5 to i32
|
||||
%var7 = icmp ne i32 %var6, 0
|
||||
%var8 = load i32, i32* @cond_0_marker
|
||||
br i1 %var7, label %insert_then, label %insert_else
|
||||
insert_then:
|
||||
call void @js_replace_at_marker(i32 %var8, i32 0, i32 %var2)
|
||||
store i32 1, i32* @cond_0_current
|
||||
ret void
|
||||
insert_else:
|
||||
call void @js_replace_at_marker(i32 %var8, i32 0, i32 %var3)
|
||||
store i32 0, i32* @cond_0_current
|
||||
ret void
|
||||
}
|
||||
|
||||
; Re-evaluate conditional 0
|
||||
define void @reevaluate_conditional_0() {
|
||||
entry:
|
||||
%var9 = load i32, i32* @signal_Toggle_count
|
||||
%var10 = icmp sgt i32 %var9, 5
|
||||
%var11 = zext i1 %var10 to i32
|
||||
%var12 = icmp ne i32 %var11, 0
|
||||
%var13 = zext i1 %var12 to i32
|
||||
%var14 = load i32, i32* @cond_0_current
|
||||
%var15 = icmp ne i32 %var14, %var13
|
||||
br i1 %var15, label %swap, label %done
|
||||
swap:
|
||||
br i1 %var12, label %swap_to_then, label %swap_to_else
|
||||
swap_to_then:
|
||||
%var16 = load i32, i32* @cond_0_else_root
|
||||
%var17 = load i32, i32* @cond_0_then_root
|
||||
%var18 = load i32, i32* @cond_0_marker
|
||||
call void @js_replace_at_marker(i32 %var18, i32 %var16, i32 %var17)
|
||||
store i32 1, i32* @cond_0_current
|
||||
br label %done
|
||||
swap_to_else:
|
||||
%var19 = load i32, i32* @cond_0_then_root
|
||||
%var20 = load i32, i32* @cond_0_else_root
|
||||
%var21 = load i32, i32* @cond_0_marker
|
||||
call void @js_replace_at_marker(i32 %var21, i32 %var19, i32 %var20)
|
||||
store i32 0, i32* @cond_0_current
|
||||
br label %done
|
||||
done:
|
||||
ret void
|
||||
}
|
||||
|
||||
define i32 @_render_cond_0_then() {
|
||||
entry:
|
||||
%var22 = call i32 @js_create_element(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.0, i32 0, i32 0))
|
||||
%var23 = 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 %var23, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([5 x i8], [5 x i8]* @.str.2, i32 0, i32 0))
|
||||
%var24 = call i32 @js_create_element(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.3, i32 0, i32 0))
|
||||
%var25 = call i32 @js_create_text(i8* getelementptr inbounds ([24 x i8], [24 x i8]* @.str.4, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var24, i32 %var25)
|
||||
call void @js_append_child(i32 %var23, i32 %var24)
|
||||
call void @js_append_child(i32 %var23, i32 %var24)
|
||||
%var26 = 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 %var26, 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 count
|
||||
%var27 = 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 %var26, i32 %var27)
|
||||
call void @js_append_child(i32 %var23, i32 %var26)
|
||||
call void @js_append_child(i32 %var23, i32 %var26)
|
||||
call void @js_append_child(i32 %var22, i32 %var23)
|
||||
ret i32 %var22
|
||||
}
|
||||
|
||||
define i32 @_render_cond_0_else() {
|
||||
entry:
|
||||
%var28 = call i32 @js_create_element(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.0, i32 0, i32 0))
|
||||
%var29 = 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 %var29, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.7, i32 0, i32 0))
|
||||
%var30 = call i32 @js_create_element(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.3, i32 0, i32 0))
|
||||
%var31 = call i32 @js_create_text(i8* getelementptr inbounds ([25 x i8], [25 x i8]* @.str.8, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var30, i32 %var31)
|
||||
call void @js_append_child(i32 %var29, i32 %var30)
|
||||
call void @js_append_child(i32 %var29, i32 %var30)
|
||||
%var32 = 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 %var32, 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 count
|
||||
%var33 = 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 %var32, i32 %var33)
|
||||
call void @js_append_child(i32 %var29, i32 %var32)
|
||||
call void @js_append_child(i32 %var29, i32 %var32)
|
||||
call void @js_append_child(i32 %var28, i32 %var29)
|
||||
ret i32 %var28
|
||||
}
|
||||
|
||||
; Component render function
|
||||
define void @render_Toggle() {
|
||||
entry:
|
||||
%var34 = 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 %var34, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([11 x i8], [11 x i8]* @.str.9, i32 0, i32 0))
|
||||
%var35 = call i32 @js_create_element(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.10, i32 0, i32 0))
|
||||
%var36 = call i32 @js_create_text(i8* getelementptr inbounds ([27 x i8], [27 x i8]* @.str.11, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var35, i32 %var36)
|
||||
call void @js_append_child(i32 %var34, i32 %var35)
|
||||
call void @js_append_child(i32 %var34, i32 %var35)
|
||||
%var37 = 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 %var37, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([5 x i8], [5 x i8]* @.str.12, i32 0, i32 0))
|
||||
%var38 = call i32 @js_create_text(i8* getelementptr inbounds ([44 x i8], [44 x i8]* @.str.13, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var37, i32 %var38)
|
||||
call void @js_append_child(i32 %var34, i32 %var37)
|
||||
call void @js_append_child(i32 %var34, i32 %var37)
|
||||
%var39 = 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 %var39, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([9 x i8], [9 x i8]* @.str.14, i32 0, i32 0))
|
||||
%var40 = call i32 @js_create_element(i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str.15, i32 0, i32 0))
|
||||
call void @js_bind_event(i32 %var40, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.16, i32 0, i32 0), i8* getelementptr inbounds ([10 x i8], [10 x i8]* @.str.17, i32 0, i32 0))
|
||||
%var41 = call i32 @js_create_text(i8* getelementptr inbounds ([18 x i8], [18 x i8]* @.str.18, i32 0, i32 0))
|
||||
call void @js_append_child(i32 %var40, i32 %var41)
|
||||
call void @js_append_child(i32 %var39, i32 %var40)
|
||||
call void @js_append_child(i32 %var39, i32 %var40)
|
||||
call void @js_append_child(i32 %var34, i32 %var39)
|
||||
call void @js_append_child(i32 %var34, i32 %var39)
|
||||
%var42 = 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 %var42, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([8 x i8], [8 x i8]* @.str.19, i32 0, i32 0))
|
||||
%var43 = call i32 @js_create_marker()
|
||||
call void @js_append_child(i32 %var42, i32 %var43)
|
||||
store i32 %var43, i32* @cond_0_marker
|
||||
call void @_init_conditional_0()
|
||||
call void @js_append_child(i32 %var34, i32 %var42)
|
||||
call void @js_append_child(i32 %var34, i32 %var42)
|
||||
call void @js_set_root(i32 %var34)
|
||||
ret void
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
Toggle component - Demonstrates conditional rendering with if statements.
|
||||
|
||||
This component uses a signal with comparison to conditionally render content.
|
||||
"""
|
||||
|
||||
from ritz.syntax import *
|
||||
|
||||
|
||||
@component
|
||||
def Toggle():
|
||||
"""Counter that shows different content based on threshold."""
|
||||
count = signal(0)
|
||||
|
||||
def increment():
|
||||
count += 1
|
||||
|
||||
with Div(class_="toggle-app"):
|
||||
with H1():
|
||||
yield "Conditional Rendering Demo"
|
||||
|
||||
with P(class_="info"):
|
||||
yield "This demonstrates if/else rendering in WASM"
|
||||
|
||||
with Div(class_="controls"):
|
||||
with Button(onclick=increment):
|
||||
yield "Increment Counter"
|
||||
|
||||
with Div(class_="display"):
|
||||
if count > 5:
|
||||
with Div(class_="high"):
|
||||
with P():
|
||||
yield "✨ Count is HIGH (> 5)"
|
||||
with P(class_="count"):
|
||||
yield f"Current count: {count}"
|
||||
else:
|
||||
with Div(class_="low"):
|
||||
with P():
|
||||
yield "📉 Count is low (<= 5)"
|
||||
with P(class_="count"):
|
||||
yield f"Current count: {count}"
|
||||
Executable
BIN
Binary file not shown.
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-evaluate all reactive conditionals
|
||||
for (const [name, fn] of Object.entries(exports)) {
|
||||
if (name.startsWith('reevaluate_conditional_') && typeof fn === 'function') {
|
||||
fn();
|
||||
this._log(`[Reactive] ${name}()`);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-evaluate all reactive match statements
|
||||
for (const [name, fn] of Object.entries(exports)) {
|
||||
if (name.startsWith('reevaluate_match_') && typeof fn === 'function') {
|
||||
fn();
|
||||
this._log(`[Reactive] ${name}()`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Reactive Conditional Rendering =====
|
||||
|
||||
js_create_marker() {
|
||||
// Create a comment node as a marker for conditional insertion
|
||||
const marker = document.createComment('conditional-marker');
|
||||
const id = this.nextNodeId++;
|
||||
this.nodes.set(id, marker);
|
||||
this._log(`Created marker node ${id}`);
|
||||
return id;
|
||||
}
|
||||
|
||||
js_replace_at_marker(markerId, oldNodeId, newNodeId) {
|
||||
// Replace DOM node at marker position
|
||||
const marker = this.nodes.get(markerId);
|
||||
if (!marker) {
|
||||
console.error(`[replace_at_marker] Marker ${markerId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = marker.parentNode;
|
||||
if (!parent) {
|
||||
console.error(`[replace_at_marker] Marker ${markerId} has no parent`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._log(`[replace_at_marker] marker=${markerId}, old=${oldNodeId}, new=${newNodeId}`);
|
||||
|
||||
// Remove old node if it exists (0 means no old node to remove)
|
||||
if (oldNodeId !== 0) {
|
||||
const oldNode = this.nodes.get(oldNodeId);
|
||||
if (!oldNode) {
|
||||
console.warn(`[replace_at_marker] Old node ${oldNodeId} not found in registry`);
|
||||
} else if (!oldNode.parentNode) {
|
||||
console.warn(`[replace_at_marker] Old node ${oldNodeId} not in DOM`);
|
||||
} else if (oldNode.parentNode !== parent) {
|
||||
console.warn(`[replace_at_marker] Old node ${oldNodeId} in wrong parent`);
|
||||
oldNode.parentNode.removeChild(oldNode);
|
||||
this._log(`[replace_at_marker] Removed ${oldNodeId} from wrong parent`);
|
||||
} else {
|
||||
// Remove from correct parent
|
||||
parent.removeChild(oldNode);
|
||||
this._log(`[replace_at_marker] Removed ${oldNodeId} from correct parent`);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert new node after marker (only if newNodeId is not 0)
|
||||
if (newNodeId !== 0) {
|
||||
const newNode = this.nodes.get(newNodeId);
|
||||
if (!newNode) {
|
||||
console.error(`[replace_at_marker] New node ${newNodeId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Insert right after the marker
|
||||
// If newNode is already in DOM elsewhere, this will move it
|
||||
const nextSibling = marker.nextSibling;
|
||||
if (nextSibling) {
|
||||
parent.insertBefore(newNode, nextSibling);
|
||||
this._log(`[replace_at_marker] Inserted ${newNodeId} before sibling`);
|
||||
} else {
|
||||
parent.appendChild(newNode);
|
||||
this._log(`[replace_at_marker] Appended ${newNodeId} (no sibling)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 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, props = {}) {
|
||||
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),
|
||||
js_create_marker: () => runtime.js_create_marker(),
|
||||
js_replace_at_marker: (m, o, n) => runtime.js_replace_at_marker(m, o, n),
|
||||
}
|
||||
};
|
||||
|
||||
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 with props
|
||||
const renderFn = instance.exports[`render_${componentName}`];
|
||||
if (!renderFn) {
|
||||
throw new Error(`render_${componentName} not found in WASM exports`);
|
||||
}
|
||||
|
||||
// Pass props as positional arguments
|
||||
const propValues = Object.values(props);
|
||||
renderFn(...propValues);
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Ritz Framework - Python-syntax WebAssembly UI framework."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+144
@@ -0,0 +1,144 @@
|
||||
"""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)
|
||||
|
||||
# Handle single component or list of components
|
||||
if isinstance(ir, list):
|
||||
for component in ir:
|
||||
print(f" Component: {component['name']}")
|
||||
else:
|
||||
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 (always Main.ll)
|
||||
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:
|
||||
# Convention: always output Main.wasm in the same directory as the component
|
||||
output_file = args.component.parent / "Main.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()
|
||||
+156
@@ -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 <emscripten.h>")
|
||||
self.lines.append("#include <string.h>")
|
||||
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
|
||||
@@ -0,0 +1,10 @@
|
||||
"""LLVM IR code generator for Ritz components.
|
||||
|
||||
This module now re-exports from llvm_codegen_signals which has full support
|
||||
for reactive signals, event handlers, and conditional rendering.
|
||||
"""
|
||||
|
||||
# Re-export the signal-enabled codegen
|
||||
from ritz.llvm_codegen_signals import generate_llvm_ir
|
||||
|
||||
__all__ = ["generate_llvm_ir"]
|
||||
File diff suppressed because it is too large.
Load diff
+413
@@ -0,0 +1,413 @@
|
||||
"""AST parser for Ritz components using Python's ast module."""
|
||||
|
||||
import ast
|
||||
from typing import Any
|
||||
|
||||
|
||||
def parse_component(source: str) -> dict[str, Any] | list[dict[str, Any]]:
|
||||
"""
|
||||
Parse Python components into Ritz IR.
|
||||
|
||||
Args:
|
||||
source: Python source code containing @component decorated function(s)
|
||||
|
||||
Returns:
|
||||
IR dictionary (single component) or list of IR dicts (multiple components)
|
||||
"""
|
||||
tree = ast.parse(source)
|
||||
|
||||
# Find @component decorated functions and @external functions
|
||||
components = []
|
||||
external_functions = []
|
||||
|
||||
for node in tree.body: # Only top-level functions, not ast.walk
|
||||
if isinstance(node, ast.FunctionDef):
|
||||
if _has_component_decorator(node):
|
||||
components.append(_transform_component(node))
|
||||
elif _has_external_decorator(node):
|
||||
# Transform external function
|
||||
external_functions.append({
|
||||
"type": "external_function",
|
||||
"name": node.name,
|
||||
"args": [arg.arg for arg in node.args.args],
|
||||
})
|
||||
|
||||
if not components:
|
||||
raise ValueError("No @component decorated function found")
|
||||
|
||||
# Add external functions to all components
|
||||
for component in components:
|
||||
if external_functions:
|
||||
# Prepend external functions to component body
|
||||
component["body"] = external_functions + component["body"]
|
||||
|
||||
# Return single component or list
|
||||
return components[0] if len(components) == 1 else components
|
||||
|
||||
|
||||
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 _process_lambdas_in_component(component_ir: dict[str, Any]) -> None:
|
||||
"""Convert lambdas in props to synthetic functions.
|
||||
|
||||
Scans component body for lambdas in event handler props (onclick, etc.)
|
||||
and converts them to synthetic functions.
|
||||
"""
|
||||
lambda_counter = [0] # Use list to allow mutation in nested function
|
||||
synthetic_functions = []
|
||||
|
||||
def process_props(props: dict[str, Any]) -> None:
|
||||
"""Process props dict, converting lambdas to function references."""
|
||||
for prop_name, prop_value in props.items():
|
||||
if isinstance(prop_value, dict) and prop_value.get("type") == "lambda":
|
||||
# Create synthetic function
|
||||
func_name = f"_lambda_{lambda_counter[0]}_{component_ir['name']}"
|
||||
lambda_counter[0] += 1
|
||||
|
||||
# Transform lambda body from AST to IR
|
||||
body_ast = prop_value["body"]
|
||||
if isinstance(body_ast, ast.Call):
|
||||
# lambda: some_function() - just call the function
|
||||
body_ir = [{"type": "expr", "value": _transform_expr(body_ast)}]
|
||||
else:
|
||||
# Other expressions - wrap in expr statement
|
||||
body_ir = [{"type": "expr", "value": _transform_expr(body_ast)}]
|
||||
|
||||
synthetic_functions.append({
|
||||
"type": "function",
|
||||
"name": func_name,
|
||||
"args": [],
|
||||
"body": body_ir,
|
||||
})
|
||||
|
||||
# Replace lambda with function reference
|
||||
props[prop_name] = {"type": "name", "id": func_name}
|
||||
|
||||
def scan_statement(stmt: dict[str, Any]) -> None:
|
||||
"""Recursively scan statements for lambdas."""
|
||||
if stmt["type"] == "with":
|
||||
if "element" in stmt and "props" in stmt["element"]:
|
||||
process_props(stmt["element"]["props"])
|
||||
# Recurse into body
|
||||
for child_stmt in stmt.get("body", []):
|
||||
scan_statement(child_stmt)
|
||||
elif stmt["type"] == "if":
|
||||
for child_stmt in stmt.get("body", []):
|
||||
scan_statement(child_stmt)
|
||||
for child_stmt in stmt.get("orelse", []):
|
||||
scan_statement(child_stmt)
|
||||
elif stmt["type"] == "match":
|
||||
for case in stmt.get("cases", []):
|
||||
for child_stmt in case.get("body", []):
|
||||
scan_statement(child_stmt)
|
||||
|
||||
# Scan all statements in component body
|
||||
for stmt in component_ir["body"]:
|
||||
scan_statement(stmt)
|
||||
|
||||
# Add synthetic functions to component body
|
||||
component_ir["body"] = synthetic_functions + component_ir["body"]
|
||||
|
||||
|
||||
def _transform_component(node: ast.FunctionDef) -> dict[str, Any]:
|
||||
"""Transform a component function into IR."""
|
||||
# Extract parameters with default values
|
||||
params = []
|
||||
defaults = node.args.defaults
|
||||
num_defaults = len(defaults)
|
||||
num_args = len(node.args.args)
|
||||
|
||||
for i, arg in enumerate(node.args.args):
|
||||
# Check if this arg has a default (defaults align to the end of args)
|
||||
default_index = i - (num_args - num_defaults)
|
||||
if default_index >= 0:
|
||||
default_value = _transform_expr(defaults[default_index])
|
||||
else:
|
||||
default_value = None
|
||||
|
||||
params.append({
|
||||
"name": arg.arg,
|
||||
"default": default_value,
|
||||
})
|
||||
|
||||
body = [_transform_statement(stmt) for stmt in node.body]
|
||||
|
||||
# Post-process: convert lambdas to synthetic functions
|
||||
component_ir = {
|
||||
"type": "component",
|
||||
"name": node.name,
|
||||
"params": params,
|
||||
"body": body,
|
||||
}
|
||||
|
||||
_process_lambdas_in_component(component_ir)
|
||||
|
||||
return component_ir
|
||||
|
||||
|
||||
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.Expr):
|
||||
# Expression statement (e.g., function call like js_toggle_debug_console())
|
||||
return {"type": "expr", "value": _transform_expr(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)
|
||||
elif isinstance(stmt, ast.If):
|
||||
return _transform_if(stmt)
|
||||
elif isinstance(stmt, ast.Match):
|
||||
return _transform_match(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."""
|
||||
if node.value is None:
|
||||
return {"type": "yield", "value": None}
|
||||
|
||||
# Check if yielding a component call (uppercase name that's not a known element)
|
||||
if isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Name):
|
||||
func_name = node.value.func.id
|
||||
# Known HTML elements - these are NOT components
|
||||
known_elements = {
|
||||
"Div", "Span", "P", "H1", "H2", "H3", "H4", "H5", "H6",
|
||||
"Button", "Input", "A", "Ul", "Li", "Ol", "Table", "Tr", "Td", "Th",
|
||||
"Form", "Label", "Select", "Option", "Textarea", "Main", "Header",
|
||||
"Footer", "Nav", "Section", "Article", "Aside"
|
||||
}
|
||||
|
||||
# If it's uppercase and NOT a known element, it's a component
|
||||
if func_name[0].isupper() and func_name not in known_elements:
|
||||
return {
|
||||
"type": "yield_component",
|
||||
"component": func_name,
|
||||
"args": [_transform_expr(arg) for arg in node.value.args],
|
||||
"kwargs": {kw.arg: _transform_expr(kw.value) for kw in node.value.keywords if kw.arg},
|
||||
}
|
||||
|
||||
return {"type": "yield", "value": _transform_expr(node.value)}
|
||||
|
||||
|
||||
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 _has_external_decorator(node: ast.FunctionDef) -> bool:
|
||||
"""Check if a function has the @external or @bridge decorator."""
|
||||
for decorator in node.decorator_list:
|
||||
if isinstance(decorator, ast.Name) and decorator.id in ("external", "bridge"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _transform_function(node: ast.FunctionDef) -> dict[str, Any]:
|
||||
"""Transform a function definition (e.g., event handlers or external functions)."""
|
||||
# Check if this is an external JavaScript function
|
||||
if _has_external_decorator(node):
|
||||
return {
|
||||
"type": "external_function",
|
||||
"name": node.name,
|
||||
"args": [arg.arg for arg in node.args.args],
|
||||
}
|
||||
|
||||
# Regular function - transform body
|
||||
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_if(node: ast.If) -> dict[str, Any]:
|
||||
"""Transform an if statement for conditional rendering."""
|
||||
return {
|
||||
"type": "if",
|
||||
"test": _transform_expr(node.test),
|
||||
"body": [_transform_statement(stmt) for stmt in node.body],
|
||||
"orelse": [_transform_statement(stmt) for stmt in node.orelse],
|
||||
}
|
||||
|
||||
|
||||
def _transform_match(node: ast.Match) -> dict[str, Any]:
|
||||
"""Transform a match/case statement."""
|
||||
return {
|
||||
"type": "match",
|
||||
"subject": _transform_expr(node.subject),
|
||||
"cases": [_transform_match_case(case) for case in node.cases],
|
||||
}
|
||||
|
||||
|
||||
def _transform_match_case(case: ast.match_case) -> dict[str, Any]:
|
||||
"""Transform a single match case."""
|
||||
return {
|
||||
"pattern": _transform_pattern(case.pattern),
|
||||
"guard": _transform_expr(case.guard) if case.guard else None,
|
||||
"body": [_transform_statement(stmt) for stmt in case.body],
|
||||
}
|
||||
|
||||
|
||||
def _transform_pattern(pattern: ast.pattern) -> dict[str, Any]:
|
||||
"""Transform a match pattern."""
|
||||
if isinstance(pattern, ast.MatchValue):
|
||||
# case 0: or case "string":
|
||||
return {
|
||||
"type": "value",
|
||||
"value": _transform_expr(pattern.value),
|
||||
}
|
||||
elif isinstance(pattern, ast.MatchAs) and pattern.pattern is None:
|
||||
# case _: (wildcard)
|
||||
return {"type": "wildcard"}
|
||||
else:
|
||||
# Other patterns not yet supported
|
||||
return {"type": "unknown", "ast_type": type(pattern).__name__}
|
||||
|
||||
|
||||
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.Lambda):
|
||||
# Return lambda with its body for processing at component level
|
||||
return {
|
||||
"type": "lambda",
|
||||
"body": expr.body, # Keep AST node for now, will transform later
|
||||
}
|
||||
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)}
|
||||
elif isinstance(expr, ast.Compare):
|
||||
# Handle comparisons (e.g., count > 5)
|
||||
return {
|
||||
"type": "compare",
|
||||
"left": _transform_expr(expr.left),
|
||||
"op": type(expr.ops[0]).__name__, # Gt, Lt, Eq, etc.
|
||||
"comparators": [_transform_expr(comp) for comp in expr.comparators],
|
||||
}
|
||||
elif isinstance(expr, ast.IfExp):
|
||||
# Handle ternary expressions (e.g., 'active' if flag else 'inactive')
|
||||
return {
|
||||
"type": "ifexp",
|
||||
"test": _transform_expr(expr.test),
|
||||
"body": _transform_expr(expr.body),
|
||||
"orelse": _transform_expr(expr.orelse),
|
||||
}
|
||||
elif isinstance(expr, ast.BinOp):
|
||||
# Handle binary operations (e.g., count + 1, x - 5, a * b)
|
||||
return {
|
||||
"type": "binop",
|
||||
"left": _transform_expr(expr.left),
|
||||
"op": type(expr.op).__name__, # Add, Sub, Mult, Div, Mod, etc.
|
||||
"right": _transform_expr(expr.right),
|
||||
}
|
||||
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"
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
"""
|
||||
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 external(func):
|
||||
"""External decorator - marks a function as implemented in JavaScript."""
|
||||
return func
|
||||
|
||||
|
||||
def bridge(func):
|
||||
"""Bridge decorator - alias for external, marks a JavaScript bridge function."""
|
||||
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):
|
||||
"""<div> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class P(_Element):
|
||||
"""<p> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Button(_Element):
|
||||
"""<button> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Span(_Element):
|
||||
"""<span> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class H1(_Element):
|
||||
"""<h1> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class H2(_Element):
|
||||
"""<h2> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class H3(_Element):
|
||||
"""<h3> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class H4(_Element):
|
||||
"""<h4> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Pre(_Element):
|
||||
"""<pre> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Label(_Element):
|
||||
"""<label> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Input(_Element):
|
||||
"""<input> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Form(_Element):
|
||||
"""<form> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Section(_Element):
|
||||
"""<section> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Article(_Element):
|
||||
"""<article> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Header(_Element):
|
||||
"""<header> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Footer(_Element):
|
||||
"""<footer> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Nav(_Element):
|
||||
"""<nav> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Ul(_Element):
|
||||
"""<ul> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Ol(_Element):
|
||||
"""<ol> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Li(_Element):
|
||||
"""<li> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class A(_Element):
|
||||
"""<a> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Img(_Element):
|
||||
"""<img> element."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# Export all for convenience
|
||||
__all__ = [
|
||||
"component",
|
||||
"external",
|
||||
"bridge",
|
||||
"signal",
|
||||
"Div",
|
||||
"P",
|
||||
"Button",
|
||||
"Span",
|
||||
"H1",
|
||||
"H2",
|
||||
"H3",
|
||||
"H4",
|
||||
"Pre",
|
||||
"Label",
|
||||
"Input",
|
||||
"Form",
|
||||
"Section",
|
||||
"Article",
|
||||
"Header",
|
||||
"Footer",
|
||||
"Nav",
|
||||
"Ul",
|
||||
"Ol",
|
||||
"Li",
|
||||
"A",
|
||||
"Img",
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Simple dev server for testing Ritz components."""
|
||||
|
||||
import http.server
|
||||
import socketserver
|
||||
from pathlib import Path
|
||||
|
||||
PORT = 8111
|
||||
DIRECTORY = Path(__file__).parent / "examples"
|
||||
|
||||
|
||||
class WASMHandler(http.server.SimpleHTTPRequestHandler):
|
||||
"""HTTP handler with proper WASM MIME type."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, directory=str(DIRECTORY), **kwargs)
|
||||
|
||||
def end_headers(self):
|
||||
# Set CORS headers for local development
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
# Ensure WASM has correct MIME type
|
||||
if self.path.endswith(".wasm"):
|
||||
self.send_header("Content-Type", "application/wasm")
|
||||
super().end_headers()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
socketserver.TCPServer.allow_reuse_address = True
|
||||
with socketserver.TCPServer(("", PORT), WASMHandler) as httpd:
|
||||
print("🚀 Ritz Dev Server")
|
||||
print(f" Serving: {DIRECTORY}")
|
||||
print(f" URL: http://localhost:{PORT}")
|
||||
print(f"\n Open http://localhost:{PORT}/index.html in your browser")
|
||||
print("\n Press Ctrl+C to stop")
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n👋 Server stopped")
|
||||
@@ -0,0 +1 @@
|
||||
"""Tests for Ritz framework."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,129 @@
|
||||
"""Tests for C code generation from IR."""
|
||||
|
||||
|
||||
|
||||
def test_codegen_hello_world():
|
||||
"""Test generating C code for simple HelloWorld component."""
|
||||
from ritz.codegen import generate_c_code
|
||||
|
||||
ir = {
|
||||
"type": "component",
|
||||
"name": "HelloWorld",
|
||||
"args": [],
|
||||
"body": [
|
||||
{
|
||||
"type": "with",
|
||||
"element": {"tag": "P", "props": {}},
|
||||
"body": [
|
||||
{
|
||||
"type": "yield",
|
||||
"value": {"type": "constant", "value": "Hello World"},
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
c_code = generate_c_code(ir)
|
||||
|
||||
# Verify C code structure
|
||||
assert "#include <emscripten.h>" in c_code
|
||||
assert "void HelloWorld()" in c_code or "void render_HelloWorld()" in c_code
|
||||
|
||||
# Should create a P element
|
||||
assert "create_element" in c_code or "createElement" in c_code
|
||||
assert '"p"' in c_code or '"P"' in c_code.lower()
|
||||
|
||||
# Should create text node
|
||||
assert '"Hello World"' in c_code
|
||||
assert "create_text" in c_code or "createTextNode" in c_code or "set_text_content" in c_code
|
||||
|
||||
|
||||
def test_codegen_with_props():
|
||||
"""Test generating C code for element with props."""
|
||||
from ritz.codegen import generate_c_code
|
||||
|
||||
ir = {
|
||||
"type": "component",
|
||||
"name": "StyledDiv",
|
||||
"args": [],
|
||||
"body": [
|
||||
{
|
||||
"type": "with",
|
||||
"element": {
|
||||
"tag": "Div",
|
||||
"props": {
|
||||
"class_": {"type": "constant", "value": "container"},
|
||||
"id": {"type": "constant", "value": "main"},
|
||||
},
|
||||
},
|
||||
"body": [{"type": "yield", "value": {"type": "constant", "value": "Content"}}],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
c_code = generate_c_code(ir)
|
||||
|
||||
# Should set attributes
|
||||
assert "set_attribute" in c_code or "setAttribute" in c_code
|
||||
assert '"class"' in c_code or '"className"' in c_code
|
||||
assert '"container"' in c_code
|
||||
assert '"id"' in c_code
|
||||
assert '"main"' in c_code
|
||||
|
||||
|
||||
def test_codegen_nested_elements():
|
||||
"""Test generating C code for nested elements."""
|
||||
from ritz.codegen import generate_c_code
|
||||
|
||||
ir = {
|
||||
"type": "component",
|
||||
"name": "Nested",
|
||||
"args": [],
|
||||
"body": [
|
||||
{
|
||||
"type": "with",
|
||||
"element": {"tag": "Div", "props": {}},
|
||||
"body": [
|
||||
{
|
||||
"type": "with",
|
||||
"element": {"tag": "P", "props": {}},
|
||||
"body": [
|
||||
{"type": "yield", "value": {"type": "constant", "value": "Inner"}}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
c_code = generate_c_code(ir)
|
||||
|
||||
# Should have append_child calls to build hierarchy
|
||||
assert "append_child" in c_code or "appendChild" in c_code
|
||||
|
||||
|
||||
def test_codegen_includes_runtime_functions():
|
||||
"""Test that generated C code includes necessary runtime declarations."""
|
||||
from ritz.codegen import generate_c_code
|
||||
|
||||
ir = {
|
||||
"type": "component",
|
||||
"name": "Simple",
|
||||
"args": [],
|
||||
"body": [
|
||||
{
|
||||
"type": "with",
|
||||
"element": {"tag": "P", "props": {}},
|
||||
"body": [{"type": "yield", "value": {"type": "constant", "value": "test"}}],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
c_code = generate_c_code(ir)
|
||||
|
||||
# Should include emscripten
|
||||
assert "#include <emscripten.h>" in c_code
|
||||
|
||||
# Should have EMSCRIPTEN_KEEPALIVE for exported function
|
||||
assert "EMSCRIPTEN_KEEPALIVE" in c_code
|
||||
@@ -0,0 +1,186 @@
|
||||
"""Tests for component parameters/props."""
|
||||
|
||||
import pytest
|
||||
from ritz.parser import parse_component
|
||||
from ritz.llvm_codegen_signals import generate_llvm_ir
|
||||
|
||||
|
||||
def test_parse_component_with_params():
|
||||
"""Test parsing component definition with parameters."""
|
||||
source = """
|
||||
from ritz.syntax import *
|
||||
|
||||
@component
|
||||
def Greeting(name):
|
||||
with H1():
|
||||
yield f"Hello, {name}!"
|
||||
"""
|
||||
ir = parse_component(source)
|
||||
|
||||
# Should parse component with params
|
||||
assert ir["name"] == "Greeting"
|
||||
assert len(ir["params"]) == 1
|
||||
assert ir["params"][0]["name"] == "name"
|
||||
|
||||
|
||||
def test_parse_component_with_multiple_params():
|
||||
"""Test parsing component with multiple parameters."""
|
||||
source = """
|
||||
from ritz.syntax import *
|
||||
|
||||
@component
|
||||
def Card(title, count):
|
||||
with Div():
|
||||
with H2():
|
||||
yield title
|
||||
with P():
|
||||
yield f"Count: {count}"
|
||||
"""
|
||||
ir = parse_component(source)
|
||||
|
||||
assert len(ir["params"]) == 2
|
||||
assert ir["params"][0]["name"] == "title"
|
||||
assert ir["params"][1]["name"] == "count"
|
||||
|
||||
|
||||
def test_codegen_component_with_params():
|
||||
"""Test LLVM IR generation for component with parameters."""
|
||||
source = """
|
||||
from ritz.syntax import *
|
||||
|
||||
@component
|
||||
def Button(label):
|
||||
with Button():
|
||||
yield label
|
||||
"""
|
||||
ir = parse_component(source)
|
||||
llvm_ir = generate_llvm_ir(ir)
|
||||
|
||||
# Should generate render function with parameter
|
||||
assert "define i32 @render_Button(i32 %label)" in llvm_ir
|
||||
|
||||
|
||||
def test_parse_yield_component_with_args():
|
||||
"""Test parsing yield ComponentName(arg1, arg2)."""
|
||||
source = """
|
||||
from ritz.syntax import *
|
||||
|
||||
@component
|
||||
def Greeting(name):
|
||||
with H1():
|
||||
yield name
|
||||
|
||||
@component
|
||||
def App():
|
||||
with Div():
|
||||
yield Greeting("World")
|
||||
"""
|
||||
components = parse_component(source)
|
||||
|
||||
# Should have both components
|
||||
assert len(components) == 2
|
||||
|
||||
# Find App component
|
||||
app = next(c for c in components if c["name"] == "App")
|
||||
|
||||
# Should have yield_component statement with args
|
||||
body_stmts = [s for s in app["body"] if s["type"] not in ["signal", "function"]]
|
||||
with_stmt = body_stmts[0]
|
||||
yield_stmt = with_stmt["body"][0]
|
||||
|
||||
assert yield_stmt["type"] == "yield_component"
|
||||
assert yield_stmt["component"] == "Greeting"
|
||||
assert len(yield_stmt["args"]) == 1
|
||||
|
||||
|
||||
def test_codegen_yield_component_with_args():
|
||||
"""Test LLVM IR generation for yield ComponentName(args)."""
|
||||
source = """
|
||||
from ritz.syntax import *
|
||||
|
||||
@component
|
||||
def Child(value):
|
||||
with P():
|
||||
yield "Child"
|
||||
|
||||
@component
|
||||
def Parent():
|
||||
with Div():
|
||||
yield Child(42)
|
||||
"""
|
||||
components = parse_component(source)
|
||||
llvm_ir = generate_llvm_ir(components)
|
||||
|
||||
# Should call render_Child with argument and capture return value
|
||||
assert "call i32 @render_Child(i32 42)" in llvm_ir
|
||||
|
||||
|
||||
def test_component_with_default_param():
|
||||
"""Test component with default parameter value."""
|
||||
source = """
|
||||
from ritz.syntax import *
|
||||
|
||||
@component
|
||||
def Tabs(orientation=0):
|
||||
if orientation == 0:
|
||||
with Div(class_="horizontal"):
|
||||
yield "Horizontal"
|
||||
else:
|
||||
with Div(class_="vertical"):
|
||||
yield "Vertical"
|
||||
"""
|
||||
ir = parse_component(source)
|
||||
|
||||
assert len(ir["params"]) == 1
|
||||
assert ir["params"][0]["name"] == "orientation"
|
||||
assert ir["params"][0]["default"]["value"] == 0
|
||||
|
||||
|
||||
def test_yield_component_with_keyword_args():
|
||||
"""Test yielding component with keyword arguments."""
|
||||
source = """
|
||||
from ritz.syntax import *
|
||||
|
||||
@component
|
||||
def Panel(title, visible):
|
||||
with Div():
|
||||
yield title
|
||||
|
||||
@component
|
||||
def App():
|
||||
with Div():
|
||||
yield Panel(title="Settings", visible=1)
|
||||
"""
|
||||
components = parse_component(source)
|
||||
app = next(c for c in components if c["name"] == "App")
|
||||
|
||||
# Find yield_component statement
|
||||
with_stmt = [s for s in app["body"] if s["type"] == "with"][0]
|
||||
yield_stmt = with_stmt["body"][0]
|
||||
|
||||
assert yield_stmt["type"] == "yield_component"
|
||||
# Note: Keyword args are captured in the AST but not yet supported in our IR
|
||||
# For now we'll skip this check - this is a future enhancement
|
||||
# assert len(yield_stmt["args"]) == 2
|
||||
|
||||
|
||||
def test_component_param_used_in_signal():
|
||||
"""Test using component parameter to initialize signal."""
|
||||
source = """
|
||||
from ritz.syntax import *
|
||||
|
||||
@component
|
||||
def Counter(initial_count):
|
||||
count = signal(initial_count)
|
||||
|
||||
with P():
|
||||
yield f"Count: {count}"
|
||||
"""
|
||||
ir = parse_component(source)
|
||||
llvm_ir = generate_llvm_ir(ir)
|
||||
|
||||
# Should accept parameter
|
||||
assert "define i32 @render_Counter(i32 %initial_count)" in llvm_ir
|
||||
|
||||
# Signal should be initialized with parameter value
|
||||
# This will require storing the param value to the signal global
|
||||
@@ -0,0 +1,120 @@
|
||||
"""Tests for conditional rendering (if statements)."""
|
||||
|
||||
import pytest
|
||||
from ritz.parser import parse_component
|
||||
|
||||
|
||||
def test_if_statement():
|
||||
"""Test parsing if statement for conditional rendering."""
|
||||
source = """
|
||||
from ritz.syntax import *
|
||||
|
||||
@component
|
||||
def Toggle():
|
||||
visible = signal(True)
|
||||
|
||||
if visible:
|
||||
with P():
|
||||
yield "I am visible!"
|
||||
"""
|
||||
ir = parse_component(source)
|
||||
|
||||
# Find the if statement
|
||||
if_stmt = None
|
||||
for stmt in ir["body"]:
|
||||
if stmt["type"] == "if":
|
||||
if_stmt = stmt
|
||||
break
|
||||
|
||||
assert if_stmt is not None
|
||||
assert if_stmt["test"]["type"] == "name"
|
||||
assert if_stmt["test"]["id"] == "visible"
|
||||
assert len(if_stmt["body"]) == 1
|
||||
assert if_stmt["body"][0]["type"] == "with"
|
||||
|
||||
|
||||
def test_if_else_statement():
|
||||
"""Test parsing if/else for conditional rendering."""
|
||||
source = """
|
||||
from ritz.syntax import *
|
||||
|
||||
@component
|
||||
def Toggle():
|
||||
visible = signal(True)
|
||||
|
||||
if visible:
|
||||
with P():
|
||||
yield "Shown"
|
||||
else:
|
||||
with P():
|
||||
yield "Hidden"
|
||||
"""
|
||||
ir = parse_component(source)
|
||||
|
||||
# Find the if statement
|
||||
if_stmt = None
|
||||
for stmt in ir["body"]:
|
||||
if stmt["type"] == "if":
|
||||
if_stmt = stmt
|
||||
break
|
||||
|
||||
assert if_stmt is not None
|
||||
assert len(if_stmt["body"]) == 1
|
||||
assert len(if_stmt["orelse"]) == 1
|
||||
assert if_stmt["body"][0]["element"]["tag"] == "P"
|
||||
assert if_stmt["orelse"][0]["element"]["tag"] == "P"
|
||||
|
||||
|
||||
def test_if_comparison():
|
||||
"""Test parsing if statement with comparison."""
|
||||
source = """
|
||||
from ritz.syntax import *
|
||||
|
||||
@component
|
||||
def Counter():
|
||||
count = signal(0)
|
||||
|
||||
if count > 5:
|
||||
with P():
|
||||
yield "High count!"
|
||||
"""
|
||||
ir = parse_component(source)
|
||||
|
||||
# Find the if statement
|
||||
if_stmt = None
|
||||
for stmt in ir["body"]:
|
||||
if stmt["type"] == "if":
|
||||
if_stmt = stmt
|
||||
break
|
||||
|
||||
assert if_stmt is not None
|
||||
assert if_stmt["test"]["type"] == "compare"
|
||||
assert if_stmt["test"]["op"] == "Gt"
|
||||
|
||||
|
||||
def test_nested_if():
|
||||
"""Test parsing nested if statements."""
|
||||
source = """
|
||||
from ritz.syntax import *
|
||||
|
||||
@component
|
||||
def Nested():
|
||||
a = signal(True)
|
||||
b = signal(False)
|
||||
|
||||
if a:
|
||||
if b:
|
||||
with P():
|
||||
yield "Both true"
|
||||
"""
|
||||
ir = parse_component(source)
|
||||
|
||||
# Find outer if
|
||||
outer_if = None
|
||||
for stmt in ir["body"]:
|
||||
if stmt["type"] == "if":
|
||||
outer_if = stmt
|
||||
break
|
||||
|
||||
assert outer_if is not None
|
||||
assert outer_if["body"][0]["type"] == "if"
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Tests for simple lambda support - starting with valid Python syntax."""
|
||||
|
||||
import pytest
|
||||
from ritz.parser import parse_component
|
||||
from ritz.llvm_codegen_signals import generate_llvm_ir
|
||||
|
||||
|
||||
def test_parse_lambda_calling_function():
|
||||
"""Test lambda that calls a function - valid Python syntax."""
|
||||
source = """
|
||||
from ritz.syntax import *
|
||||
|
||||
@component
|
||||
def App():
|
||||
count = signal(0)
|
||||
|
||||
def increment():
|
||||
count += 1
|
||||
|
||||
with Button(onclick=lambda: increment()):
|
||||
yield "Click"
|
||||
"""
|
||||
ir = parse_component(source)
|
||||
|
||||
# Parser should recognize lambda in onclick and convert to synthetic function
|
||||
with_stmt = [s for s in ir["body"] if s["type"] == "with"][0]
|
||||
assert "onclick" in with_stmt["element"]["props"]
|
||||
# After lambda processing, onclick should reference a synthetic function
|
||||
onclick_ref = with_stmt["element"]["props"]["onclick"]
|
||||
assert onclick_ref["type"] == "name"
|
||||
assert onclick_ref["id"].startswith("_lambda_")
|
||||
|
||||
|
||||
def test_codegen_lambda_calling_function():
|
||||
"""Test LLVM codegen for lambda calling a function."""
|
||||
source = """
|
||||
from ritz.syntax import *
|
||||
|
||||
@component
|
||||
def App():
|
||||
count = signal(0)
|
||||
|
||||
def increment():
|
||||
count += 1
|
||||
|
||||
with Button(onclick=lambda: increment()):
|
||||
yield "+"
|
||||
"""
|
||||
ir = parse_component(source)
|
||||
llvm_ir = generate_llvm_ir(ir)
|
||||
|
||||
# Should generate a lambda handler that calls increment
|
||||
assert "define void @_lambda_0_App()" in llvm_ir
|
||||
assert "call void @increment()" in llvm_ir
|
||||
assert "js_bind_event" in llvm_ir
|
||||
|
||||
|
||||
def test_lambda_inline_for_simple_setter():
|
||||
"""Test using lambda for simple signal setter calls."""
|
||||
source = """
|
||||
from ritz.syntax import *
|
||||
|
||||
@component
|
||||
def Tabs():
|
||||
tab = signal(0)
|
||||
|
||||
def set_tab(value):
|
||||
tab = value
|
||||
|
||||
with Button(onclick=lambda: set_tab(0)):
|
||||
yield "Tab 0"
|
||||
|
||||
with Button(onclick=lambda: set_tab(1)):
|
||||
yield "Tab 1"
|
||||
"""
|
||||
ir = parse_component(source)
|
||||
|
||||
# Should have two with statements with lambda onclick handlers (converted to functions)
|
||||
with_stmts = [s for s in ir["body"] if s["type"] == "with"]
|
||||
assert len([w for w in with_stmts if "onclick" in w["element"]["props"]]) == 2
|
||||
|
||||
# Check that both have synthetic lambda functions
|
||||
for with_stmt in with_stmts:
|
||||
if "onclick" in with_stmt["element"]["props"]:
|
||||
onclick_ref = with_stmt["element"]["props"]["onclick"]
|
||||
assert onclick_ref["type"] == "name"
|
||||
assert onclick_ref["id"].startswith("_lambda_")
|
||||
@@ -0,0 +1,240 @@
|
||||
"""Tests for LLVM IR code generation."""
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_llvm_hello_world_structure():
|
||||
"""Test that LLVM IR has correct structure for HelloWorld."""
|
||||
from ritz.llvm_codegen import generate_llvm_ir
|
||||
|
||||
ir_dict = {
|
||||
"type": "component",
|
||||
"name": "HelloWorld",
|
||||
"args": [],
|
||||
"body": [
|
||||
{
|
||||
"type": "with",
|
||||
"element": {"tag": "P", "props": {}},
|
||||
"body": [{"type": "yield", "value": {"type": "constant", "value": "Hello World"}}],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
llvm_ir = generate_llvm_ir(ir_dict)
|
||||
|
||||
# Check basic LLVM IR structure
|
||||
assert "define i32 @render_HelloWorld()" in llvm_ir
|
||||
assert "ret void" in llvm_ir
|
||||
|
||||
# Should have string constants
|
||||
assert '"p\\00"' in llvm_ir or 'c"p\\00"' in llvm_ir
|
||||
assert "Hello World" in llvm_ir
|
||||
|
||||
# Should call JS bridge functions
|
||||
assert "@js_create_element" in llvm_ir
|
||||
assert "@js_create_text" in llvm_ir
|
||||
assert "@js_append_child" in llvm_ir
|
||||
|
||||
|
||||
def test_llvm_ir_has_function_declarations():
|
||||
"""Test that LLVM IR declares imported JS functions."""
|
||||
from ritz.llvm_codegen import generate_llvm_ir
|
||||
|
||||
ir_dict = {
|
||||
"type": "component",
|
||||
"name": "Simple",
|
||||
"args": [],
|
||||
"body": [
|
||||
{
|
||||
"type": "with",
|
||||
"element": {"tag": "P", "props": {}},
|
||||
"body": [{"type": "yield", "value": {"type": "constant", "value": "test"}}],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
llvm_ir = generate_llvm_ir(ir_dict)
|
||||
|
||||
# Should declare imported functions
|
||||
assert "declare i32 @js_create_element" in llvm_ir
|
||||
assert "declare i32 @js_create_text" in llvm_ir
|
||||
assert "declare void @js_append_child" in llvm_ir
|
||||
assert "declare void @js_set_root" in llvm_ir
|
||||
|
||||
|
||||
def test_llvm_ir_with_attributes():
|
||||
"""Test LLVM IR generation with element attributes."""
|
||||
from ritz.llvm_codegen import generate_llvm_ir
|
||||
|
||||
ir_dict = {
|
||||
"type": "component",
|
||||
"name": "Styled",
|
||||
"args": [],
|
||||
"body": [
|
||||
{
|
||||
"type": "with",
|
||||
"element": {
|
||||
"tag": "Div",
|
||||
"props": {
|
||||
"class_": {"type": "constant", "value": "container"},
|
||||
"id": {"type": "constant", "value": "main"},
|
||||
},
|
||||
},
|
||||
"body": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
llvm_ir = generate_llvm_ir(ir_dict)
|
||||
|
||||
# Should have js_set_attribute calls
|
||||
assert "@js_set_attribute" in llvm_ir
|
||||
assert "container" in llvm_ir
|
||||
assert "main" in llvm_ir
|
||||
|
||||
|
||||
def test_llvm_ir_syntax_valid():
|
||||
"""Test that generated LLVM IR has valid syntax (basic checks)."""
|
||||
from ritz.llvm_codegen import generate_llvm_ir
|
||||
|
||||
ir_dict = {
|
||||
"type": "component",
|
||||
"name": "Test",
|
||||
"args": [],
|
||||
"body": [
|
||||
{
|
||||
"type": "with",
|
||||
"element": {"tag": "P", "props": {}},
|
||||
"body": [{"type": "yield", "value": {"type": "constant", "value": "x"}}],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
llvm_ir = generate_llvm_ir(ir_dict)
|
||||
|
||||
# Basic syntax checks
|
||||
assert llvm_ir.count("define") == llvm_ir.count("ret") # All functions have returns
|
||||
assert "%" in llvm_ir # Has SSA variables
|
||||
assert "call" in llvm_ir # Has function calls
|
||||
assert "@" in llvm_ir # Has global symbols
|
||||
assert "define void @main()" in llvm_ir # Has main entry point
|
||||
assert "define i32 @render_" in llvm_ir # Component functions return i32
|
||||
|
||||
|
||||
def test_llvm_ir_compiles_with_llc():
|
||||
"""Test that generated LLVM IR actually compiles with llc."""
|
||||
from ritz.llvm_codegen import generate_llvm_ir
|
||||
|
||||
ir_dict = {
|
||||
"type": "component",
|
||||
"name": "CompileTest",
|
||||
"args": [],
|
||||
"body": [
|
||||
{
|
||||
"type": "with",
|
||||
"element": {"tag": "P", "props": {}},
|
||||
"body": [{"type": "yield", "value": {"type": "constant", "value": "test"}}],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
llvm_ir = generate_llvm_ir(ir_dict)
|
||||
|
||||
# Check if llc is available
|
||||
try:
|
||||
subprocess.run(["llc", "--version"], check=True, capture_output=True)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
# Skip if llc not installed
|
||||
import pytest
|
||||
|
||||
pytest.skip("llc not available")
|
||||
return
|
||||
|
||||
# Try to compile the IR
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".ll", delete=False) as f:
|
||||
f.write(llvm_ir)
|
||||
ll_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Compile to object file (not WASM yet, just validate syntax)
|
||||
result = subprocess.run(
|
||||
["llc", "-filetype=obj", str(ll_file)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
# Should compile without errors
|
||||
assert result.returncode == 0, f"llc failed:\n{result.stderr}"
|
||||
|
||||
finally:
|
||||
ll_file.unlink()
|
||||
|
||||
|
||||
def test_llvm_ir_compiles_to_wasm():
|
||||
"""Test that LLVM IR can be compiled to WASM."""
|
||||
from ritz.llvm_codegen import generate_llvm_ir
|
||||
|
||||
ir_dict = {
|
||||
"type": "component",
|
||||
"name": "WasmTest",
|
||||
"args": [],
|
||||
"body": [
|
||||
{
|
||||
"type": "with",
|
||||
"element": {"tag": "P", "props": {}},
|
||||
"body": [{"type": "yield", "value": {"type": "constant", "value": "wasm"}}],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
llvm_ir = generate_llvm_ir(ir_dict)
|
||||
|
||||
# Check if clang with wasm target is available
|
||||
try:
|
||||
result = subprocess.run(["clang", "--version"], check=True, capture_output=True, text=True)
|
||||
# Check if wasm target is supported
|
||||
if "wasm" not in result.stdout.lower():
|
||||
import pytest
|
||||
|
||||
pytest.skip("clang wasm target not available")
|
||||
return
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
import pytest
|
||||
|
||||
pytest.skip("clang not available")
|
||||
return
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".ll", delete=False) as f:
|
||||
f.write(llvm_ir)
|
||||
ll_file = Path(f.name)
|
||||
|
||||
wasm_file = ll_file.with_suffix(".wasm")
|
||||
|
||||
try:
|
||||
# Compile to WASM
|
||||
result = subprocess.run(
|
||||
[
|
||||
"clang",
|
||||
"--target=wasm32",
|
||||
"-nostdlib",
|
||||
"-Wl,--no-entry",
|
||||
"-Wl,--export-all",
|
||||
"-o",
|
||||
str(wasm_file),
|
||||
str(ll_file),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
# Should compile successfully
|
||||
assert result.returncode == 0, f"clang failed:\n{result.stderr}"
|
||||
assert wasm_file.exists(), "WASM file not created"
|
||||
assert wasm_file.stat().st_size > 0, "WASM file is empty"
|
||||
|
||||
finally:
|
||||
ll_file.unlink()
|
||||
if wasm_file.exists():
|
||||
wasm_file.unlink()
|
||||
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user