Implement self-hosted debug console and @external decorator
This commit completes the self-hosting of Ritz's debug console and
introduces the @external decorator for elegant JavaScript bridge functions.
## Major Features
### 1. Self-Hosted Debug Console (DebugConsole.py)
- Entire debug console UI written in Ritz itself
- Signal-based state management for toggle functionality
- Displays runtime stats (memory, signals, DOM nodes, logs)
- Live reactive updates when signals change in main app
- Keyboard shortcut support (Ctrl+`)
### 2. @external Decorator for JavaScript Bridges
- Pythonic decorator syntax: @external or @bridge
- Parser automatically recognizes and tracks external functions
- Codegen emits proper LLVM declarations
- No more hacky string checks (startswith("js_"))
- Clean, declarative bridge function definitions
Example:
```python
@external
def js_toggle_debug_console():
pass
def toggle():
js_toggle_debug_console() # Clean call to JS bridge
```
### 3. Enhanced RitzLoader Debug Mode
- `new RitzLoader({ debug: true })` auto-loads DebugConsole.wasm
- Keyboard shortcut registration (Ctrl+` toggle)
- Runtime stats injection (memory, signals, DOM nodes)
- Automatic status element hiding when debug enabled
- Debug console isolated in separate WASM runtime
## Bug Fixes
- Fixed reactive text node scoping (prevent global name collisions)
- Fixed CSS specificity for debug console status (use #ritz-debug)
- Fixed signal cross-contamination between runtimes (scope to rootElement)
- Fixed parser to handle expression statements in function bodies
- Fixed conditional branch rendering (append children to wrapper divs)
## Infrastructure Improvements
- Added H4 and Pre elements to syntax.py
- Parser handles @external decorator on top-level functions
- Codegen pre-collects external functions before emitting declarations
- Runtime scopes querySelector to prevent cross-runtime interference
- Debug stats update reactively on every signal change
## Files Added/Modified
New files:
- examples/runtime/DebugConsole.py - Self-hosted debug UI
- examples/runtime/DebugConsole.wasm - Compiled debug console (1.8KB)
- PHASE4_CLEANUP.md - Next phase planning document
Modified:
- ritz/parser.py - @external decorator support
- ritz/llvm_codegen_signals.py - External function handling
- ritz/syntax.py - Added external/bridge decorators, H4, Pre
- examples/runtime/ritz-runtime.js - Debug mode, stats updates
- examples/runtime/styles.css - Merged debug styles
- examples/template.html - Debug mode enabled by default
- examples/highfive/HighFive.html - Simplified using template
## Known Issues (Addressed in PHASE4_CLEANUP.md)
1. Debug console shifts page RIGHT (should shift LEFT)
2. Toggle implementation un-Ritzy (calls JS to manipulate DOM)
- Should use reactive class binding + @event decorator
- Needs event object bridge for keyboard events
3. Missing test infrastructure
## Next Steps (Phase 4)
See PHASE4_CLEANUP.md for detailed plan:
- Implement reactive class binding (signal-driven classes)
- Add @event decorator for declarative event registration
- Build event object bridge (access event.key, event.ctrlKey)
- Create Python test mode (execute without browser)
- Add comprehensive test suite
- Update all examples to use new patterns
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
ddd2c03a9d
commit
ea7dadc5ae
20 files changed
+1324
-70
No files matched your search
@@ -77,6 +77,14 @@ class RitzRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -151,11 +159,16 @@ class RitzRuntime {
|
||||
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
|
||||
// For now, update any element with class matching signal name or data-signal attribute
|
||||
// IMPORTANT: Only search within this runtime's root element to avoid cross-contamination
|
||||
const selectors = [
|
||||
`[data-signal="${signalName}"]`,
|
||||
`.${signalName}`,
|
||||
@@ -163,7 +176,8 @@ class RitzRuntime {
|
||||
];
|
||||
|
||||
selectors.forEach(selector => {
|
||||
const elements = document.querySelectorAll(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;
|
||||
@@ -284,8 +298,10 @@ 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 = {}) {
|
||||
@@ -353,6 +369,11 @@ class RitzLoader {
|
||||
|
||||
this.onLoad(runtime, instance);
|
||||
|
||||
// Load debug console if debug mode is enabled
|
||||
if (this.debug) {
|
||||
await this.loadDebugConsole(componentName);
|
||||
}
|
||||
|
||||
return { runtime, instance };
|
||||
|
||||
} catch (error) {
|
||||
@@ -362,6 +383,149 @@ class RitzLoader {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Reference in new issue
Block a user