Files
ritz/TYPE_SYSTEM_ROADMAP.md
Aaron SinclairandClaude 443cca5662 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>
2025-11-15 11:53:56 -08:00

7.2 KiB

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:

@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:

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

count: Signal[int] = signal(0)      # → global i32 0
name: Signal[str] = signal("")      # → global i8* null
active: Signal[bool] = signal(True) # → global i1 true

Implementation:

# 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):

elif value["type"] == "fstring":
    # TODO: Implement proper string concatenation
    # Currently only emits signal value, loses prefix/suffix

Needed:

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:

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

if prop_name == "onclick":
    # Special handling

Needed:

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):

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

@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:
    define i32 @render_Greeting(i8* %name, i32 %age)
    

Phase 6: Delegate/Callback Types

Goal: Type-safe function callbacks

from ritz.types import Delegate

@component
def Button(on_click: Delegate[[], None], label: str):
    with Button(onclick=on_click):
        yield label

LLVM representation:

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:

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