diff --git a/TYPE_SYSTEM_ROADMAP.md b/TYPE_SYSTEM_ROADMAP.md
new file mode 100644
index 0000000..1d2786c
--- /dev/null
+++ b/TYPE_SYSTEM_ROADMAP.md
@@ -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! 🚀
diff --git a/examples/textinput/TextInput.html b/examples/textinput/TextInput.html
new file mode 100644
index 0000000..5f896c0
--- /dev/null
+++ b/examples/textinput/TextInput.html
@@ -0,0 +1,123 @@
+
+
+
+
+
+ Ritz TextInput Example
+
+
+
+ Loading...
+
+
+
+
+
diff --git a/examples/textinput/TextInput.ll b/examples/textinput/TextInput.ll
new file mode 100644
index 0000000..833c0da
--- /dev/null
+++ b/examples/textinput/TextInput.ll
@@ -0,0 +1,140 @@
+; Generated by Ritz Framework with Signals
+; Components: TextInput
+
+; 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 [10 x i8] c"container\00"
+@.str.3 = private unnamed_addr constant [3 x i8] c"h1\00"
+@.str.4 = private unnamed_addr constant [17 x i8] c"Reactive Counter\00"
+@.str.5 = private unnamed_addr constant [2 x i8] c"p\00"
+@.str.6 = private unnamed_addr constant [13 x i8] c"instructions\00"
+@.str.7 = private unnamed_addr constant [65 x i8] c"Click buttons to see reactive updates (text input coming soon!):\00"
+@.str.8 = private unnamed_addr constant [13 x i8] c"button-group\00"
+@.str.9 = private unnamed_addr constant [7 x i8] c"button\00"
+@.str.10 = private unnamed_addr constant [6 x i8] c"click\00"
+@.str.11 = private unnamed_addr constant [10 x i8] c"decrement\00"
+@.str.12 = private unnamed_addr constant [4 x i8] c"−\00"
+@.str.13 = private unnamed_addr constant [16 x i8] c"counter-display\00"
+@.str.14 = private unnamed_addr constant [2 x i8] c"0\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 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 TextInput (reactive state in WASM memory)
+@signal_TextInput_count = 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:
+ store i32 0, i32* @signal_TextInput_count
+ ret void
+}
+
+; Event handler: decrement
+define void @decrement() {
+entry:
+ store i32 0, i32* @signal_TextInput_count
+ ret void
+}
+
+; Component render function
+define i32 @render_TextInput() {
+entry:
+ %var0 = 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 %var0, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([10 x i8], [10 x i8]* @.str.2, i32 0, i32 0))
+ %var1 = call i32 @js_create_element(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.3, i32 0, i32 0))
+ %var2 = call i32 @js_create_text(i8* getelementptr inbounds ([17 x i8], [17 x i8]* @.str.4, i32 0, i32 0))
+ call void @js_append_child(i32 %var1, i32 %var2)
+ call void @js_append_child(i32 %var0, i32 %var1)
+ %var3 = 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 %var3, 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.6, i32 0, i32 0))
+ %var4 = call i32 @js_create_text(i8* getelementptr inbounds ([65 x i8], [65 x i8]* @.str.7, i32 0, i32 0))
+ call void @js_append_child(i32 %var3, i32 %var4)
+ call void @js_append_child(i32 %var0, i32 %var3)
+ %var5 = 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 %var5, 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.8, i32 0, i32 0))
+ %var6 = call i32 @js_create_element(i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str.9, i32 0, i32 0))
+ call void @js_bind_event(i32 %var6, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.10, i32 0, i32 0), i8* getelementptr inbounds ([10 x i8], [10 x i8]* @.str.11, i32 0, i32 0))
+ %var7 = call i32 @js_create_text(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.12, i32 0, i32 0))
+ call void @js_append_child(i32 %var6, i32 %var7)
+ call void @js_append_child(i32 %var5, i32 %var6)
+ %var8 = 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 %var8, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([16 x i8], [16 x i8]* @.str.13, i32 0, i32 0))
+ ; TODO: Format f-string with signal count
+ %var9 = call i32 @js_create_text(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str.14, i32 0, i32 0))
+ call void @js_append_child(i32 %var8, i32 %var9)
+ call void @js_append_child(i32 %var5, i32 %var8)
+ %var10 = call i32 @js_create_element(i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str.9, i32 0, i32 0))
+ call void @js_bind_event(i32 %var10, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.10, i32 0, i32 0), i8* getelementptr inbounds ([10 x i8], [10 x i8]* @.str.15, i32 0, i32 0))
+ %var11 = 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 %var10, i32 %var11)
+ call void @js_append_child(i32 %var5, i32 %var10)
+ call void @js_append_child(i32 %var0, i32 %var5)
+ %var12 = 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 %var12, 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.17, i32 0, i32 0))
+ %var13 = 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 %var12, i32 %var13)
+ call void @js_append_child(i32 %var0, i32 %var12)
+ %var14 = call i32 @js_create_element(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.19, i32 0, i32 0))
+ call void @js_set_attribute(i32 %var14, i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str.1, i32 0, i32 0), i8* getelementptr inbounds ([10 x i8], [10 x i8]* @.str.20, i32 0, i32 0))
+ %var15 = call i32 @js_create_element(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.21, i32 0, i32 0))
+ %var16 = 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 %var15, i32 %var16)
+ call void @js_append_child(i32 %var14, i32 %var15)
+ %var17 = call i32 @js_create_element(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.21, i32 0, i32 0))
+ %var18 = 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 %var17, i32 %var18)
+ call void @js_append_child(i32 %var14, i32 %var17)
+ %var19 = call i32 @js_create_element(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.21, i32 0, i32 0))
+ %var20 = 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 %var19, i32 %var20)
+ call void @js_append_child(i32 %var14, i32 %var19)
+ %var21 = call i32 @js_create_element(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.21, i32 0, i32 0))
+ %var22 = 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 %var21, i32 %var22)
+ call void @js_append_child(i32 %var14, i32 %var21)
+ call void @js_append_child(i32 %var0, i32 %var14)
+ ret i32 %var0
+}
+
+; Main entry point
+define void @main() {
+entry:
+ %root = call i32 @render_TextInput()
+ call void @js_set_root(i32 %root)
+ ret void
+}
diff --git a/examples/textinput/TextInput.py b/examples/textinput/TextInput.py
new file mode 100644
index 0000000..226541f
--- /dev/null
+++ b/examples/textinput/TextInput.py
@@ -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"
diff --git a/examples/textinput/TextInput.wasm b/examples/textinput/TextInput.wasm
new file mode 100755
index 0000000..c8d021a
Binary files /dev/null and b/examples/textinput/TextInput.wasm differ
diff --git a/ritz/__pycache__/llvm_codegen_signals.cpython-313.pyc b/ritz/__pycache__/llvm_codegen_signals.cpython-313.pyc
index 4a0cd6d..dca0fc5 100644
Binary files a/ritz/__pycache__/llvm_codegen_signals.cpython-313.pyc and b/ritz/__pycache__/llvm_codegen_signals.cpython-313.pyc differ
diff --git a/ritz/llvm_codegen_signals.py b/ritz/llvm_codegen_signals.py
index 132f6b3..5730b1c 100644
--- a/ritz/llvm_codegen_signals.py
+++ b/ritz/llvm_codegen_signals.py
@@ -276,6 +276,15 @@ class SignalLLVMIRGenerator:
for signal_name, signal_info in self.signals.items():
# Each signal is a global i32, scoped to component to avoid collisions
initial_value = signal_info["initial"].get("value", 0)
+
+ # TODO: Proper type system! For now, coerce to i32
+ # Strings should be i8*, bools should be i1, etc.
+ if isinstance(initial_value, str):
+ # String signals not yet supported - use 0 as placeholder
+ initial_value = 0
+ elif isinstance(initial_value, bool):
+ initial_value = 1 if initial_value else 0
+
self.lines.append(f"@signal_{component_name}_{signal_name} = global i32 {initial_value}")
self.lines.append("")