Add TextInput example exposing type system limitations + roadmap
Created a new example that demonstrates what we need for proper string/type support:
**TextInput Example (currently a counter):**
- Initially tried to build reactive text input
- Exposed 5 critical limitations:
1. String signals stored as i32 (should be i8*)
2. F-string interpolation incomplete (f"Count: {count}" → "0")
3. No oninput event (only onclick supported)
4. No event value passing (can't get input.value)
5. No string operations (len, concat, etc.)
**Quick Fix Applied:**
- Added type coercion in _emit_signal_globals()
- String signals default to i32 0 (temporary)
- Bool signals convert True/False to 1/0
- Example now uses counter to demonstrate reactive updates
**TYPE_SYSTEM_ROADMAP.md:**
Complete implementation plan for proper type support:
- Phase 1: Type hints extraction (already works!)
- Phase 2: Typed signals (i32, i8*, i1)
- Phase 3: String interpolation in text nodes
- Phase 4: Generalized event system
- Phase 5: Component parameter type checking
- Phase 6: Delegate/callback types
This example will drive the type system implementation. Once complete,
text input will work with proper string signals and reactive updates.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
134ddc4c42
commit
443cca5662
7 files changed
+579
No files matched your search
@@ -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! 🚀
|
||||
Reference in new issue
Block a user