From 8bf1c388584fe04a3356954677dbefcfbfc3ca5e Mon Sep 17 00:00:00 2001 From: Aaron Sinclair Date: Sat, 15 Nov 2025 03:51:01 -0800 Subject: [PATCH] Add reactive conditional infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Collect conditionals during IR scan (if/match statements) - Emit conditional state globals (marker, current, branch roots) - Generate init functions that pre-render all branches - Generate reevaluate functions that swap DOM on condition change - Add JS function declarations for markers and swapping Not yet integrated with actual rendering - still emitting static if/match. Next: modify _emit_if() to use reactive approach. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../llvm_codegen_signals.cpython-313.pyc | Bin 26697 -> 26748 bytes ritz/llvm_codegen_signals.py | 217 +++++++++++++++++- 2 files changed, 216 insertions(+), 1 deletion(-) diff --git a/ritz/__pycache__/llvm_codegen_signals.cpython-313.pyc b/ritz/__pycache__/llvm_codegen_signals.cpython-313.pyc index 636165745eb532a060f47836f8aeeb6ef8a61df2..ab2f55db640e21bfcbbee26562db72859ed6bc2b 100644 GIT binary patch delta 2409 zcmZ8iYiv|i5a#UT?zX#qKwDbq>-MoM+wMvqwos(4Eosx1uoMBI@>uTfo^G$+-d)db zDQybH5EX;*hsVU2NDPT0ibmxoB*w&G!qZ43Ml`vas4*rg#t=ZF(x5ZvmKL}_X3w5; zX3oqv-^^UROlB^V+#5ML*$d$7Oq8grhd#=E*h#+UYn@+~ZnAAzpyl8Zeu)JEdzjf^ z)f>p=71GIKvq;q=v7{bXGip*-v`CaGdTfMWmikDBFL7y}uiaHY`sxljuDGlDA=g(@ z1!%J>zT4eHLdG%oDI2NaJ3XDGnIG{SCrw7LH$}4B;F4)mn)tPx8Yze; z&HNd6F>lB%BVl8G?oC2!d3Szw!5TEZ2roFEOsaao&MNqLzK_)LBl&)^-8h>cvcc6G zi`u8f1I|5)HbzA}tOj*`ASQF=b~&LXqlzZ0%}p}vhRuMJ72$;f6!?tkR5NrmZQ2tw z6Ofn)stNaVW69A}Qm1+*qAR16RpF*5ttJp0RUb~8IjXMm4+=`95FXX>jlE_3kAhqh zHEtJFJL>Sc@H@8orvgNnPZKzh8K#NVqh_Pfd^5Xo zo30T>k86~fS;J#`%!;@j4$=+~GJKZ5EATExTFx(2=a5(VH`V7zJHOMo#5h$`W?$Tj z=aP~}m`o|7FhG^{@n4rskuFaC!TF#}cSNV-kv$3vxGdE1IpEVtzpuCtmT4CVd>KCL z@ge@FzlpqUR4qT~SXwJyN-0bkO;0*8zdqK57P|ST!Oju{AZ#IkJ_T%SB_2M)i$eaj zbC>`d25?(Iy%tx9iriNXIuwM!-wTyX4XEN1$+#NI zA{ULnLLb^lD?ipSQ6}glaJ)Bt$vKVexi@|{lt$_x3P0b{)rFb+*DK3WCTmKRYO*g3 zC=KJw6vZ5Sh5M#mqdG&Q0naRyyuWpvO!LoLYslZmFReRl{mALbLRpR}nkMf_s&V;I zM)f#lkxAR~K$s1pI@B4O7n#5~({>PZ`0s~5X1xgXRzn_I|%stgdfvLQkgG3DKjskO77 zyvU=SZPIIKvyUI`Y?cn7auqryjVhVgNS!g$neFtWR}4AAR-;f*L_wIO!E{5L1Kf3F zW~gU3iSd_u76U)u>ZxuK7|+9tOa%(&6KZ;?CfF>R7dwL& z&+=C`43Iwl!-l8H6nO3^+^`K{QA}) zd6!pidu$E17|5`wDdEl+c=-WLFX0|G;W@VAMjwBFTZQ*0Seg!~>I`&(Z?`o{`*6cb z?}SRV_(h{(Xq^pQp4xt`eH9w9u|m`@#`P?J(RH zaFJd#W|HlblDt<|SM3D5f|nB2W63zZ-*j{$LnBahiOk6TrZ2&OD&rlyJjBTdcAZ<1 z0i#T~NmvQ2!K!GtZs>cKXZQAnVJO%#_iS7A7_rD58_a@ebmf3Zw2RS{ro<={2#TKd zI@%XKOQL%-P|pmz*epd&?<`j|Ri|lDnd~b6IojxZ11^~kv5jL0Dtz{{8f_yU2L(Wb zu_Ch27>-qvMP4zHSt%S^Go+lq9G{#PdB#4$7NV#`A*PlaS2*Qa9SR(hV%{;FUTg)5 zAr#m@1!dVZuCU9n11LnRo5B^cfC(;%&;_A{lSL7Jn69=LI_vobTH%D>YeC~GJwtjV z0x?KXB~UC(xaWc$t4M^S9<+%whGpS_7gq>|>0UFMjE`xwgL(MPL~q_^H}PLzfKpug EAC(|gw*UYD delta 2314 zcmZ8iZ){Ul6z8^muie^pgAF#JD{m{Mds_!|z+n6vgRlW=M-XHLSohXGx@!AcZwng( zA;v$LkRaq}Vl*-OfgnT^@g;olgCFEy3`B@xjL(>0!Ur`5i3m|N>N)q74e5tp?>pz* z^Z%Ut_ciwUHRiulRaH3!fBtks#u_-|UnH?FV6Al3m!6_J;@p8OwF=thkKvxYo@Jri zoppZgX@axvFXcvLas`fh*0DD8y63EeHNp{ZH;ch#@B2~=xpB~T%`lJpo@SLx@zZi- zQal8=s#@*4F$nrYavO0LLQ_jU4Eh7?33I>ycg8|csA;ZUP9~<2fa{rj#!#KS5l+;E znF^Z*18}=pa*ya*O6S|nJ2mYNywp^?@{lkk?bWh-bP)h=CM|Oi#{J4lC7sO=Ygr|; zutVWJNK_?0odjwL$e<-&D{_`Y_Q93@sO1{Hvrb$&Kvs&&n-Fqw;=a!i_1nNVM3$WS;{g117Up_Apz zlMOM6^}x4{FE)uR%q3M$v_!;Lmn*9?6OTtM$E< zk@B_T8{i6EWGf)r@tAot6mYV|z#?_54`xITu+;#O=wv*WCt>KLiM<+E-FBYI686-M zNVvX_jJnH4C+rSDprwPIF#B6xbj=Kr6LLZ0TCO-QQANXiHPUzw{1WYMpxEPf@Qd-N zI(R2Bm%-ZhNc}`Qu>Ko}EV8@?UT6>0h>}yhd?UfL@L_utL$cZ%<@rdk!c^GGiEg-L zhUR_bz(B7r*cT9YMZzAglQDU%Skkb)Fl9fQuf=p18-lxwdwQru_kD^o%$2M*tY?*Q z9CM{wOrvvA(&^24_Cn200x+=0u zAr+kn6FGezKdd&2sb*hB&!!R+kp<10U4Ob}QT8m)XeOIVYCOfMGUF~WKrg``t3n%I zMuMH^SMa2ICjGV~t{G{aA0`bIy0Hhx2E(_LEn*TuR6htJ)!oL9z_IQw`AyQ<2Uoin z%7^H36Zt3Sv{G`U#cW+&DMiRDPA$N@2!;qW1dA=OJjuL)i8e~8V#m6nhGC?SdvN+Q z$y`Ahm&N#Fv%pvDniq>KPbQcq&2kn?+|E4ZTdlMmJc(zn5Tv2Cx0Q{+*4{ZR1&1~@ zN%~lU?=s)*?RCpTC^5bq&TOj(XFQBuug2%he8gSaNBlHFAEe{&u|-hPAFInD;irG` zr}``@n;+FV*CF1oY!Qpemq4#FMmka2SJ3%-l(l3|qyqnhGzADmt2qn#f?7LSb<3w` zMbn8r#m|x8dHAe&8la%GPx4l?n|8IJ7 zPBeWz+}c^k?wihCZWbYB7TbXhGNxpf?n1qXi6ZMpGM~~XR6}eq$gbcc6k$#n8@xEb zh*Dg5!gvFo@ghx}<-y%%8-I8k=s2bUPkHrKn8y?UIB?Y;8(y+e)D-8wkkyhp7s(QP z>218pbomHE!Cug%NuXu6!EM)Q-a3M8*NK}(n2A@h0BoObBhfef>bCrNzJ>E({?-HL! zFhD?CQ#2+&NGDo2oTl2Iu>EwRT657cB5Kh#!oTSW_*7r!oF;X^UwTl&|JyBQaQAr@ vmGIfLWSj*Y2`=1|kz$X6aMVjW-2@)+k2J7Kh>Y}BZ}zarZ&L`l4|)FqgMl)o diff --git a/ritz/llvm_codegen_signals.py b/ritz/llvm_codegen_signals.py index b4c2a3b..9ff8389 100644 --- a/ritz/llvm_codegen_signals.py +++ b/ritz/llvm_codegen_signals.py @@ -27,9 +27,12 @@ class SignalLLVMIRGenerator: self.strings: dict[str, str] = {} # value -> global name self.signals: dict[str, dict[str, Any]] = {} # signal_name -> {initial, index} self.event_handlers: list[dict[str, Any]] = [] # List of event handler functions + self.conditionals: list[dict[str, Any]] = [] # List of conditional branches (if/match) + self.conditional_map: dict[int, int] = {} # stmt id() -> conditional id self.next_string_id = 0 self.next_var_id = 0 self.next_signal_id = 0 + self.next_conditional_id = 0 # Track signal-to-DOM bindings for updates self.signal_dom_bindings: dict[str, list[str]] = {} # signal_name -> [dom_node_ids] @@ -62,14 +65,19 @@ class SignalLLVMIRGenerator: # Reset component-specific state self.signals = {} self.event_handlers = [] + self.conditionals = [] + self.conditional_map = {} - # Collect signals and handlers for this component + # Collect signals, handlers, and conditionals for this component self._collect_signals_and_handlers(component) + self._collect_conditionals(component) # Generate component-specific code self._emit_signal_globals(component) + self._emit_conditional_globals() self._emit_helper_functions() self._emit_event_handlers() + self._emit_conditional_functions() self._emit_component(component) # Prepend string constants @@ -88,6 +96,43 @@ class SignalLLVMIRGenerator: elif stmt["type"] == "function": self.event_handlers.append(stmt) + def _collect_conditionals(self, component: dict[str, Any]) -> None: + """Scan IR tree for if/match statements and assign IDs.""" + + def scan_statements(stmts: list[dict[str, Any]]): + for stmt in stmts: + if stmt["type"] == "if": + cond_id = self.next_conditional_id + self.next_conditional_id += 1 + self.conditional_map[id(stmt)] = cond_id + self.conditionals.append({ + "id": cond_id, + "type": "if", + "test": stmt["test"], + "then_body": stmt["body"], + "else_body": stmt.get("orelse", []), + }) + # Recursively scan nested statements + scan_statements(stmt["body"]) + scan_statements(stmt.get("orelse", [])) + elif stmt["type"] == "match": + cond_id = self.next_conditional_id + self.next_conditional_id += 1 + self.conditional_map[id(stmt)] = cond_id + self.conditionals.append({ + "id": cond_id, + "type": "match", + "subject": stmt["subject"], + "cases": stmt["cases"], + }) + # Recursively scan nested statements in each case + for case in stmt["cases"]: + scan_statements(case.get("body", [])) + elif stmt["type"] == "with": + scan_statements(stmt.get("body", [])) + + scan_statements(component["body"]) + def _emit_header(self) -> None: """Emit module header.""" self.lines.append("; Generated by Ritz Framework with Signals") @@ -108,6 +153,31 @@ class SignalLLVMIRGenerator: self.lines.append(f"@signal_{component_name}_{signal_name} = global i32 {initial_value}") self.lines.append("") + def _emit_conditional_globals(self) -> None: + """Emit global state for reactive conditionals.""" + if not self.conditionals: + return + + self.lines.append("; Conditional rendering state") + for cond_info in self.conditionals: + cond_id = cond_info["id"] + cond_type = cond_info["type"] + + # Common state for all conditionals + self.lines.append(f"@cond_{cond_id}_marker = global i32 0") + self.lines.append(f"@cond_{cond_id}_current = global i32 0") + + if cond_type == "if": + # Binary conditional: then_root, else_root + self.lines.append(f"@cond_{cond_id}_then_root = global i32 0") + self.lines.append(f"@cond_{cond_id}_else_root = global i32 0") + elif cond_type == "match": + # Multi-way conditional: one root per case + num_cases = len(cond_info["cases"]) + for case_idx in range(num_cases): + self.lines.append(f"@cond_{cond_id}_case_{case_idx}_root = global i32 0") + self.lines.append("") + def _emit_function_declarations(self) -> None: """Emit JS bridge function declarations.""" self.lines.append("; JavaScript bridge function declarations") @@ -120,6 +190,8 @@ class SignalLLVMIRGenerator: "declare void @js_bind_event(i32, i8*, i8*)" ) # elem, event_name, handler_name self.lines.append("declare void @js_update_text_content(i32, i8*)") # Update DOM node text + self.lines.append("declare i32 @js_create_marker()") # Create invisible marker element + self.lines.append("declare void @js_replace_at_marker(i32, i32, i32)") # marker, old, new self.lines.append("") def _emit_helper_functions(self) -> None: @@ -198,6 +270,149 @@ class SignalLLVMIRGenerator: # TODO: Trigger DOM updates for this signal self.lines.append(f" ; TODO: Update DOM nodes bound to {signal_name}") + def _emit_conditional_functions(self) -> None: + """Emit init and reevaluate functions for each conditional.""" + for cond_info in self.conditionals: + if cond_info["type"] == "if": + self._emit_if_reactive(cond_info) + elif cond_info["type"] == "match": + self._emit_match_reactive(cond_info) + + def _emit_if_reactive(self, cond_info: dict[str, Any]) -> None: + """Emit reactive init and reevaluate functions for if/else.""" + cond_id = cond_info["id"] + + # Emit init function + self.lines.append(f"; Initialize conditional {cond_id}") + self.lines.append(f"define void @_init_conditional_{cond_id}() {{") + self.lines.append("entry:") + + # Render both branches and store their roots + self.lines.append(" ; Render both branches upfront") + then_root_var = self._new_var() + else_root_var = self._new_var() + + self.lines.append(f" %{then_root_var} = call i32 @_render_cond_{cond_id}_then()") + self.lines.append(f" %{else_root_var} = call i32 @_render_cond_{cond_id}_else()") + + self.lines.append(f" store i32 %{then_root_var}, i32* @cond_{cond_id}_then_root") + self.lines.append(f" store i32 %{else_root_var}, i32* @cond_{cond_id}_else_root") + + # Evaluate condition and insert the active branch at marker + test_var = self._emit_expr(cond_info["test"]) + cond_var = self._new_var() + self.lines.append(f" %{cond_var} = icmp ne i32 %{test_var}, 0") + + # Load marker + marker_var = self._new_var() + self.lines.append(f" %{marker_var} = load i32, i32* @cond_{cond_id}_marker") + + # Branch to insert correct element + self.lines.append(f" br i1 %{cond_var}, label %insert_then, label %insert_else") + + self.lines.append("insert_then:") + self.lines.append(f" call void @js_replace_at_marker(i32 %{marker_var}, i32 0, i32 %{then_root_var})") + self.lines.append(f" store i32 1, i32* @cond_{cond_id}_current") + self.lines.append(" ret void") + + self.lines.append("insert_else:") + self.lines.append(f" call void @js_replace_at_marker(i32 %{marker_var}, i32 0, i32 %{else_root_var})") + self.lines.append(f" store i32 0, i32* @cond_{cond_id}_current") + self.lines.append(" ret void") + + self.lines.append("}") + self.lines.append("") + + # Emit reevaluate function + self.lines.append(f"; Re-evaluate conditional {cond_id}") + self.lines.append(f"define void @reevaluate_conditional_{cond_id}() {{") + self.lines.append("entry:") + + # Re-evaluate condition + test_var2 = self._emit_expr(cond_info["test"]) + cond_var2 = self._new_var() + self.lines.append(f" %{cond_var2} = icmp ne i32 %{test_var2}, 0") + + # Convert to i32 + cond_i32 = self._new_var() + self.lines.append(f" %{cond_i32} = zext i1 %{cond_var2} to i32") + + # Load current state + current_var = self._new_var() + self.lines.append(f" %{current_var} = load i32, i32* @cond_{cond_id}_current") + + # Check if changed + changed_var = self._new_var() + self.lines.append(f" %{changed_var} = icmp ne i32 %{current_var}, %{cond_i32}") + self.lines.append(f" br i1 %{changed_var}, label %swap, label %done") + + # Swap branch + self.lines.append("swap:") + self.lines.append(f" br i1 %{cond_var2}, label %swap_to_then, label %swap_to_else") + + # Swap to then + self.lines.append("swap_to_then:") + old_var = self._new_var() + new_var = self._new_var() + marker_var2 = self._new_var() + self.lines.append(f" %{old_var} = load i32, i32* @cond_{cond_id}_else_root") + self.lines.append(f" %{new_var} = load i32, i32* @cond_{cond_id}_then_root") + self.lines.append(f" %{marker_var2} = load i32, i32* @cond_{cond_id}_marker") + self.lines.append(f" call void @js_replace_at_marker(i32 %{marker_var2}, i32 %{old_var}, i32 %{new_var})") + self.lines.append(f" store i32 1, i32* @cond_{cond_id}_current") + self.lines.append(" br label %done") + + # Swap to else + self.lines.append("swap_to_else:") + old_var2 = self._new_var() + new_var2 = self._new_var() + marker_var3 = self._new_var() + self.lines.append(f" %{old_var2} = load i32, i32* @cond_{cond_id}_then_root") + self.lines.append(f" %{new_var2} = load i32, i32* @cond_{cond_id}_else_root") + self.lines.append(f" %{marker_var3} = load i32, i32* @cond_{cond_id}_marker") + self.lines.append(f" call void @js_replace_at_marker(i32 %{marker_var3}, i32 %{old_var2}, i32 %{new_var2})") + self.lines.append(f" store i32 0, i32* @cond_{cond_id}_current") + self.lines.append(" br label %done") + + self.lines.append("done:") + self.lines.append(" ret void") + self.lines.append("}") + self.lines.append("") + + # Emit branch render functions + self._emit_branch_render_function(cond_id, "then", cond_info["then_body"]) + self._emit_branch_render_function(cond_id, "else", cond_info["else_body"]) + + def _emit_match_reactive(self, cond_info: dict[str, Any]) -> None: + """Emit reactive init and reevaluate functions for match/case.""" + # TODO: Implement match reactive rendering + pass + + def _emit_branch_render_function(self, cond_id: int, branch_name: str, body: list[dict[str, Any]]) -> None: + """Emit a function that renders a branch and returns its root element.""" + self.lines.append(f"define i32 @_render_cond_{cond_id}_{branch_name}() {{") + self.lines.append("entry:") + + # Render branch body + root_var = None + for stmt in body: + var = self._emit_statement(stmt) + if root_var is None and var: + root_var = var + + # Return root element + if root_var: + self.lines.append(f" ret i32 %{root_var}") + else: + # Empty branch - create empty div + empty_var = self._new_var() + div_str = self._get_or_create_string("div") + self.lines.append(f" %{empty_var} = call i32 @js_create_element(i8* getelementptr inbounds ({self._string_type('div')}, {self._string_type('div')}* {div_str}, i32 0, i32 0))") + self.lines.append(f" ret i32 %{empty_var}") + + self.lines.append("}") + self.lines.append("") + def _emit_component(self, ir: dict[str, Any]) -> None: """Emit component render function.""" component_name = ir["name"]