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("")