From ddd2c03a9d4bb6b3f933eeaa58baff45685d8629 Mon Sep 17 00:00:00 2001 From: Aaron Sinclair Date: Sat, 15 Nov 2025 15:46:36 -0800 Subject: [PATCH] Implement RitzMain() convention and Main.wasm standard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Convention: RitzMain() entry point** Every Ritz file now requires a `@component def RitzMain()` as the entry point. This provides a consistent, predictable entry point for all applications. **Universal Main.wasm output** Build system now always outputs Main.wasm instead of ComponentName.wasm. This means every example uses the exact same HTML boilerplate! **Changes:** **Codegen (llvm_codegen_signals.py):** - Check for RitzMain component at generation time - Error if RitzMain not found: "No RitzMain component found!" - Always call `render_RitzMain()` from main() entry point - Fix: Scope reactive text nodes to components (`@reactive_text_ComponentName_N_node`) - Prevents global name collisions when multiple components have reactive text **Build (build.py):** - Output Main.wasm in same directory as source file - Output Main.ll (LLVM IR) for inspection - Convention over configuration! **Loader (ritz-runtime.js):** - Auto-detect component name from WASM filename - `new RitzLoader().load('Main.wasm')` - no second parameter needed! - Component name used only for status messages **Template (examples/template.html):** - Minimal boilerplate - literally 25 lines - Just copy and change nothing! - Loads Main.wasm - works for every example **Styles (examples/runtime/styles.css):** - Shared styles for all examples - Clean, modern defaults - App container, buttons, inputs, typography **Example (HighFive):** ```python @component def HighFive(): # ... component logic @component def RitzMain(): """Entry point - required!""" yield HighFive() ``` **Result:** ✅ Every example: copy template.html, change nothing ✅ Every example: builds to Main.wasm ✅ Consistent entry point: RitzMain() ✅ No more component name boilerplate 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- examples/highfive/HighFive.html | 4 +- examples/highfive/HighFive.py | 6 ++ examples/highfive/Main.ll | 114 +++++++++++++++++++++++++++++++ examples/highfive/Main.wasm | Bin 0 -> 1090 bytes examples/runtime/ritz-runtime.js | 8 ++- examples/runtime/styles.css | 100 +++++++++++++++++++++++++++ examples/template.html | 44 +++--------- ritz/build.py | 5 +- ritz/llvm_codegen_signals.py | 32 +++++---- 9 files changed, 261 insertions(+), 52 deletions(-) create mode 100644 examples/highfive/Main.ll create mode 100755 examples/highfive/Main.wasm create mode 100644 examples/runtime/styles.css diff --git a/examples/highfive/HighFive.html b/examples/highfive/HighFive.html index 5a2d878..f806d29 100644 --- a/examples/highfive/HighFive.html +++ b/examples/highfive/HighFive.html @@ -86,10 +86,10 @@ ritzInstance.exports.increment() // Wait for DOM to be ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { - new RitzLoader().load('HighFive.wasm', 'HighFive'); + new RitzLoader().load('Main.wasm'); }); } else { - new RitzLoader().load('HighFive.wasm', 'HighFive'); + new RitzLoader().load('Main.wasm'); } diff --git a/examples/highfive/HighFive.py b/examples/highfive/HighFive.py index d9a2c03..2c39a87 100644 --- a/examples/highfive/HighFive.py +++ b/examples/highfive/HighFive.py @@ -24,3 +24,9 @@ def HighFive(): with Button(onclick=increment): yield "High Five!" + + +@component +def RitzMain(): + """Entry point - required for all Ritz applications.""" + yield HighFive() diff --git a/examples/highfive/Main.ll b/examples/highfive/Main.ll new file mode 100644 index 0000000..9352025 --- /dev/null +++ b/examples/highfive/Main.ll @@ -0,0 +1,114 @@ +; Generated by Ritz Framework with Signals +; Components: HighFive, RitzMain + +; String constants +@.str.0 = private unnamed_addr constant [13 x i8] c"High fives: \00" +@.str.1 = private unnamed_addr constant [4 x i8] c"div\00" +@.str.2 = private unnamed_addr constant [6 x i8] c"class\00" +@.str.3 = private unnamed_addr constant [14 x i8] c"high-five-app\00" +@.str.4 = private unnamed_addr constant [2 x i8] c"p\00" +@.str.5 = private unnamed_addr constant [25 x i8] c"Give me a high five! ✋\00" +@.str.6 = private unnamed_addr constant [6 x i8] c"count\00" +@.str.7 = private unnamed_addr constant [7 x i8] c"button\00" +@.str.8 = private unnamed_addr constant [6 x i8] c"click\00" +@.str.9 = private unnamed_addr constant [10 x i8] c"increment\00" +@.str.10 = private unnamed_addr constant [11 x i8] c"High Five!\00" + +; JavaScript bridge function declarations +declare i32 @js_create_element(i8*) +declare i32 @js_create_text(i8*) +declare i32 @js_create_text_with_int(i8*, i32) +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 void @js_update_text_with_int(i32, i8*, i32) +declare i32 @js_create_marker() +declare void @js_replace_at_marker(i32, i32, i32) + +; Signal storage for HighFive (reactive state in WASM memory) +@signal_HighFive_high_fives = global i32 0 + +; Reactive text node storage for HighFive +@reactive_text_HighFive_0_node = global i32 0 + +; Signal helper functions +define i32 @get_high_fives() { +entry: + %val = load i32, i32* @signal_HighFive_high_fives + ret i32 %val +} + +define void @set_high_fives(i32 %newval) { +entry: + store i32 %newval, i32* @signal_HighFive_high_fives + ; TODO: Trigger DOM update callback + ret void +} + +; Event handler: increment +define void @increment() { +entry: + %var0 = load i32, i32* @signal_HighFive_high_fives + %var1 = add i32 %var0, 1 + store i32 %var1, i32* @signal_HighFive_high_fives + %var2 = load i32, i32* @reactive_text_HighFive_0_node + %var3 = getelementptr inbounds [13 x i8], [13 x i8]* @.str.0, i32 0, i32 0 + %var4 = load i32, i32* @signal_HighFive_high_fives + call void @js_update_text_with_int(i32 %var2, i8* %var3, i32 %var4) + ret void +} + +; Component render function +define i32 @render_HighFive() { +entry: + %var6 = getelementptr inbounds [4 x i8], [4 x i8]* @.str.1, i32 0, i32 0 + %var5 = call i32 @js_create_element(i8* %var6) + %var7 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.2, i32 0, i32 0 + %var8 = getelementptr inbounds [14 x i8], [14 x i8]* @.str.3, i32 0, i32 0 + call void @js_set_attribute(i32 %var5, i8* %var7, i8* %var8) + %var10 = getelementptr inbounds [2 x i8], [2 x i8]* @.str.4, i32 0, i32 0 + %var9 = call i32 @js_create_element(i8* %var10) + %var11 = call i32 @js_create_text(i8* getelementptr inbounds ([25 x i8], [25 x i8]* @.str.5, i32 0, i32 0)) + call void @js_append_child(i32 %var9, i32 %var11) + call void @js_append_child(i32 %var5, i32 %var9) + %var13 = getelementptr inbounds [2 x i8], [2 x i8]* @.str.4, i32 0, i32 0 + %var12 = call i32 @js_create_element(i8* %var13) + %var14 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.2, i32 0, i32 0 + %var15 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.6, i32 0, i32 0 + call void @js_set_attribute(i32 %var12, i8* %var14, i8* %var15) + %var16 = getelementptr inbounds [13 x i8], [13 x i8]* @.str.0, i32 0, i32 0 + %var17 = load i32, i32* @signal_HighFive_high_fives + %var18 = call i32 @js_create_text_with_int(i8* %var16, i32 %var17) + store i32 %var18, i32* @reactive_text_HighFive_0_node + call void @js_append_child(i32 %var12, i32 %var18) + call void @js_append_child(i32 %var5, i32 %var12) + %var20 = getelementptr inbounds [7 x i8], [7 x i8]* @.str.7, i32 0, i32 0 + %var19 = call i32 @js_create_element(i8* %var20) + %var21 = getelementptr inbounds [6 x i8], [6 x i8]* @.str.8, i32 0, i32 0 + %var22 = getelementptr inbounds [10 x i8], [10 x i8]* @.str.9, i32 0, i32 0 + call void @js_bind_event(i32 %var19, i8* %var21, i8* %var22) + %var23 = call i32 @js_create_text(i8* getelementptr inbounds ([11 x i8], [11 x i8]* @.str.10, i32 0, i32 0)) + call void @js_append_child(i32 %var19, i32 %var23) + call void @js_append_child(i32 %var5, i32 %var19) + ret i32 %var5 +} + +; Reactive text node storage for RitzMain +@reactive_text_RitzMain_0_node = global i32 0 + +; Component render function +define i32 @render_RitzMain() { +entry: + %var24 = call i32 @render_HighFive() + ret i32 %var24 +} + +; Main entry point +define void @main() { +entry: + %root = call i32 @render_RitzMain() + call void @js_set_root(i32 %root) + ret void +} diff --git a/examples/highfive/Main.wasm b/examples/highfive/Main.wasm new file mode 100755 index 0000000000000000000000000000000000000000..1462bc41031302505e0dca5f8299561d3473276c GIT binary patch literal 1090 zcmZ8gJ&)5s5S{fp&L++!K7oXUgyc(w6j`F8pmPHViH1M0+1Q)d7k@*sC1r5+r zA!6+cT=!qY+l=;|?6qe|+k%w#SRryBPVuchKbsVZGG zN*en3ny$qRcY>hUY-ADhC`q#zdWUoO%Kx70=;{&fS){l_ZuObnySV!^tFlyJQ`RSz zO)QJ_C;`iPHfkaK#axV+UHsj}T^IkD^*~p5Z|v@2pnYO!pbNz9|FCX5xy;Mz+=64y z&#s*R{{@sQO1&zNrior)s-@g4#rlKgRz_9qN29WsNwG>yjs; zh%-s)5L2N9M}2C$oYzW3Yo3*7w1-TS>9?lN5$CI{T;TUDL@j9_$%zyjlQGokGOaT} zs%e}l4pB*oMJ7$^jtCiEkiG>j)5E%ti=kfl@DP_%LRw5rAta|$jp8(s7JfsH2W~@X z8}67#rX+mN!Vk=|!cRJd@H_LUXZr+s_5nl6YfZUiC}4EG-qb=_AAIYI{n;pdVc}cm zIX;A6Z)o|%wZR*?4hFC9x+OsmF*k|REktF5?z|pnl!>|q-L*hFBS6~&-VRk35M^n! z20dS2JLJSHJO4cL0e?>r^>gV9U+?#I<;4H_@g1xuN>ti$OT33oC2MCk$(ydNftgD^ zyH%8hs_?k9SOv?CD)9!-+(nbE12c~q(-sZzQqIsoONq9MWT4JBvYrj|CPNE~tdV~K DV0}J` literal 0 HcmV?d00001 diff --git a/examples/runtime/ritz-runtime.js b/examples/runtime/ritz-runtime.js index 07c5cbf..f0d97d2 100644 --- a/examples/runtime/ritz-runtime.js +++ b/examples/runtime/ritz-runtime.js @@ -288,8 +288,14 @@ class RitzLoader { this.onError = options.onError || ((err) => console.error(err)); } - async load(wasmPath, componentName, props = {}) { + async load(wasmPath, componentName = null, props = {}) { try { + // Auto-detect component name from wasm filename if not provided + if (!componentName) { + // Extract from path: "Main.wasm" → "Main", "path/to/Main.wasm" → "Main" + componentName = wasmPath.split('/').pop().replace('.wasm', ''); + } + this.setStatus('⏳ Creating runtime...'); const runtime = new RitzRuntime(this.rootElement); diff --git a/examples/runtime/styles.css b/examples/runtime/styles.css new file mode 100644 index 0000000..0e7528f --- /dev/null +++ b/examples/runtime/styles.css @@ -0,0 +1,100 @@ +/* Ritz Framework - Shared Example Styles */ + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + background: #f5f5f5; + color: #333; + line-height: 1.6; +} + +#app { + max-width: 800px; + margin: 50px auto; + padding: 40px; + background: white; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); +} + +#status { + position: fixed; + top: 20px; + right: 20px; + padding: 12px 20px; + background: #fff; + border: 1px solid #ddd; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + font-size: 14px; +} + +#status.success { + background: #d4edda; + border-color: #c3e6cb; + color: #155724; +} + +#status.error { + background: #f8d7da; + border-color: #f5c6cb; + color: #721c24; +} + +h1, h2, h3 { + margin-top: 0; + color: #222; +} + +p { + margin: 15px 0; +} + +button { + padding: 12px 24px; + font-size: 16px; + font-weight: 500; + background: #0066cc; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s; +} + +button:hover { + background: #0052a3; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + +button:active { + transform: translateY(0); + box-shadow: none; +} + +input, select, textarea { + padding: 10px; + font-size: 16px; + border: 1px solid #ddd; + border-radius: 4px; + font-family: inherit; +} + +input:focus, select:focus, textarea:focus { + outline: none; + border-color: #0066cc; + box-shadow: 0 0 0 3px rgba(0,102,204,0.1); +} + +ul { + padding-left: 20px; +} + +li { + margin: 8px 0; +} diff --git a/examples/template.html b/examples/template.html index b0c9b9a..7a67bb5 100644 --- a/examples/template.html +++ b/examples/template.html @@ -3,45 +3,23 @@ - {{TITLE}} - Ritz Framework - - + Ritz Example +
- +
- diff --git a/ritz/build.py b/ritz/build.py index f5876b6..9078044 100644 --- a/ritz/build.py +++ b/ritz/build.py @@ -39,7 +39,7 @@ def build_component(component_file: Path, output_file: Path, debug: bool = False print(" [3/4] Generating LLVM IR...") llvm_ir = generate_llvm_ir(ir) - # Save LLVM IR for inspection + # Save LLVM IR for inspection (always Main.ll) ll_file = output_file.with_suffix(".ll") ll_file.write_text(llvm_ir) print(f" Saved LLVM IR: {ll_file}") @@ -130,7 +130,8 @@ def main() -> None: if args.output: output_file = args.output else: - output_file = args.component.with_suffix(".wasm") + # Convention: always output Main.wasm in the same directory as the component + output_file = args.component.parent / "Main.wasm" # Ensure output directory exists output_file.parent.mkdir(parents=True, exist_ok=True) diff --git a/ritz/llvm_codegen_signals.py b/ritz/llvm_codegen_signals.py index 0ce8044..df3a9a0 100644 --- a/ritz/llvm_codegen_signals.py +++ b/ritz/llvm_codegen_signals.py @@ -109,9 +109,13 @@ class SignalLLVMIRGenerator: self._emit_conditional_functions() self._emit_component(component) - # Emit main entry point for the last (top-level) component - if self.components: - self._emit_main_entry(self.components[-1]["name"]) + # Emit main entry point - always looks for RitzMain component + ritz_main = next((c for c in self.components if c["name"] == "RitzMain"), None) + if not ritz_main: + raise ValueError( + "No RitzMain component found! Every Ritz file must have a @component def RitzMain() as entry point." + ) + self._emit_main_entry() # Prepend string constants return self._finalize_with_strings() @@ -425,15 +429,15 @@ class SignalLLVMIRGenerator: self.lines.append("") def _emit_reactive_text_globals(self) -> None: - """Emit global storage for reactive text node references.""" + """Emit global storage for reactive text node references (scoped to component).""" if not self.reactive_text_nodes: return - self.lines.append("; Reactive text node storage") + self.lines.append(f"; Reactive text node storage for {self.current_component}") for text_info in self.reactive_text_nodes: text_id = text_info["id"] - # Store text node reference so we can update its content later - self.lines.append(f"@reactive_text_{text_id}_node = global i32 0") + # Store text node reference so we can update its content later (scoped to component) + self.lines.append(f"@reactive_text_{self.current_component}_{text_id}_node = global i32 0") self.lines.append("") def _emit_function_declarations(self) -> None: @@ -629,9 +633,9 @@ class SignalLLVMIRGenerator: if not text_info: return # Text node not found - # Load the text node reference + # Load the text node reference (scoped to component) node_var = self._new_var() - self.lines.append(f" %{node_var} = load i32, i32* @reactive_text_{text_id}_node") + self.lines.append(f" %{node_var} = load i32, i32* @reactive_text_{self.current_component}_{text_id}_node") # Build the f-string text result = self._build_fstring_text(text_info["fstring"]) @@ -933,12 +937,12 @@ class SignalLLVMIRGenerator: self.lines.append("}") self.lines.append("") - def _emit_main_entry(self, component_name: str) -> None: - """Emit main entry point that renders top-level component and sets root.""" + def _emit_main_entry(self) -> None: + """Emit main entry point that renders RitzMain component and sets root.""" self.lines.append("; Main entry point") self.lines.append("define void @main() {") self.lines.append("entry:") - self.lines.append(f" %root = call i32 @render_{component_name}()") + self.lines.append(" %root = call i32 @render_RitzMain()") self.lines.append(" call void @js_set_root(i32 %root)") self.lines.append(" ret void") self.lines.append("}") @@ -1376,8 +1380,8 @@ class SignalLLVMIRGenerator: empty_ptr = self._emit_string_ptr("") self.lines.append(f" %{text_var} = call i32 @js_create_text(i8* {empty_ptr})") - # Store the text node reference for reactive updates - self.lines.append(f" store i32 %{text_var}, i32* @reactive_text_{text_id}_node") + # Store the text node reference for reactive updates (scoped to component) + self.lines.append(f" store i32 %{text_var}, i32* @reactive_text_{self.current_component}_{text_id}_node") return text_var else: