# 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! 🚀