From 74ff1e7e9f76cee41178f14b51a3ec879566de64 Mon Sep 17 00:00:00 2001 From: Aaron Sinclair Date: Sat, 15 Nov 2025 03:32:38 -0800 Subject: [PATCH] Support multiple components in single file with scoped signals - Update generate_llvm_ir() to accept list of component dicts - Process each component with own signal/handler state - Scope signal globals with component name prefix (@signal_ComponentName_signalName) - Add _signal_global() helper for scoped signal names - Update tests for scoped signal names - All tests passing (30/30) --- .../llvm_codegen_signals.cpython-313.pyc | Bin 21423 -> 22703 bytes ritz/llvm_codegen_signals.py | 75 ++++++++++++------ ...n_conditional.cpython-313-pytest-8.4.2.pyc | Bin 7134 -> 7141 bytes ...ignal_codegen.cpython-313-pytest-8.4.2.pyc | Bin 13613 -> 13633 bytes tests/test_llvm_codegen_conditional.py | 4 +- tests/test_signal_codegen.py | 4 +- 6 files changed, 53 insertions(+), 30 deletions(-) diff --git a/ritz/__pycache__/llvm_codegen_signals.cpython-313.pyc b/ritz/__pycache__/llvm_codegen_signals.cpython-313.pyc index 435bdd9ff92abb3f97211fca4e634d64f5d10b99..ec28abc5ab7966fe84139c7620a6a60042e32a90 100644 GIT binary patch delta 5384 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 IDET!152?c%V*mgE delta 4050 zcmaJ@3v8QL75=XuzyJ6V=iwxd<0OCLyzDy7qX|jVW^J~vP20su`!`$H6g)Tno4RxC z?Am>WjTZJm6JuL@wvlZDIthtQ?1?21LZAR*>zELUmaW|{CMpmN7>JBjOqvkmoNFg- zTEs8;_}p{Px%WK2bMJlSHTKGPS?k9xmtDfIKJHC8w@K0;Xt45G3e3mb&b9^`HOoOu zk43Zkq@xyY^-7JBCi^7K)@#bEarx|5zR%7>FFTNXjX9a!x?=t~ykQEQ?lEZ=VXJ7? zgEEfL8doaPYBfc0cI1i#=RnRbI45!r!MTug3a%JAm*Cu5@j+bcpsmM5n`u_fh90`b zT#060Gj}>aSt2G&uQxm>YqoZqB%yh!&?x&q8f8MGTq|F*0R?5zJYvpsqxET=tPqnG zH%_i@UMVyxwJLNdzo1u0s;-e#3yHUoMV}Z|7e+N=GR;jh-a;fOr!*p7oU(xo!bBR2T8yxIPaqH-t7xD~oatC<%bQ-j=oI+aWxiX}4;ve&}n_AS!K1`R6@PS1ou%tBKpyOXu{B!%heaF*x;4H3J;<@M!ExJQ*yTc1gSR~FHnOv8w#ys7)fC zp$Jt~<#dv9KUr*mJuOx6QGP!P+9P8Us84;60VBoMxJka zrS*l@S6g1_SO^X*_%F`nk*W{lKwwO>XCCCLGo)s6BL3l_~b!mhFg&-%!0=O$GX z&7o$pX&#%>;hSX*cZteckH?b7bkXm#%GN@eW{cH9HQz^SVnesesW4b?QRUTS8P-Hr zpUxQODLoss+1lN+_1QkjXk686nK1A7;yoZHPcAbH4;7b+wk;mXs0r zW%*`z&GW8Tzc4>Czh$9Ld)K)ySM51x4v+(ppy`@QU0R@L+Y8G5Dx{BV1BXT3*J4ovW-?u+82msH}tPn&SnP4{Ba!Z^Bdl zV@w5St+)1?1gTulisUq71`T!}?}3fAZGlErT@>T``7xYkNA(*Ci8U zqqB(*P)g|ZT}hdWyb&hw!KbxuIY#qi;I2FB*g9V`?^>vfEICKv@wx{cH_vaHZ(pbz zTXOEnh3gY0)(%fKw1f$*{AME5^?VzVTZrUyY7uQ)NG$xhp}w2^Flek#iE zf_=5CMTz9A(uOM_1cLa-*TaTTYt`C1tsYWvPpE2(KnfMYD4tw7&_sz0g{RY55ej*3 zm@%jud~Sr!_6B%ARK%XkeGt0BFxkHi4_1jqMTxJ4-<_x&Vn51Fw1+G#0uOZ!4HJwo z;p!o-CS!;6q}m(_+PFsh+4DECesboTT=I>=m872HcOn;b6jBTB?h`ByAC3B9tkG&t z9R5Y_d);rd$_Va%16+m$TsEwMOJ{E@z6bXAcEEFZbv@HtA^v~f+s<6M<=z3-N?8@h zJP#8WAu@`@V4a3CnNDT0u~e4#!JfXmykjVzRS-sr$>|w&R?ast-wSW{)%4l&-AXZD z#fV|fWH~=dGKw&0&&PF~1k}cSAE^ByHV(V{o7hp9>2H=lL~&*pJlnqpD7jDiKe1wR z|8dh0+Y4P=n!IZrH&~RPq6PaPz2!O93Z+}S-1|`wv>VoB`h?E8279*FjFylR-D8F` zioPXMQ_<|H8J+i&NaiG(;SZCNC`8#+j2lipsZUb_CsK#g{2b{3{CMjw)(7%XoE<{< z8>}Z1*&`LXvqO){rPK`(F8ma(MWA!|CdXc~dxFT5aB8?&rYn|*;XA`&b`lnc_ih#! zi6ZiqcjOt;SesY=EXnAuS%nG-`XIWq&iN9uhGizkv)Gmo@9dH%Nzg!7ihL@eC*!+v zzu&prgl@0DZMix`i#50fTwh&*WR63pvU(JOJ(WGO_F+RIh$ll8EJqydA^3OX^^Sum+3i4Z<{oQUYJL&Q;~xxkA6hyYCh!ngMh?7<7>A?ZEK#$}7cx_PeWy!pI# zUU~YaD-sTueZ|)Hxxf_(|E{PGYu6Q@#aeg8X-DGC{q6QvhNss%xV9l str: +def generate_llvm_ir(ir: dict[str, Any] | list[dict[str, Any]]) -> str: """ Generate LLVM IR from Ritz IR with Signal support. Args: - ir: Component IR dictionary + ir: Component IR dictionary or list of component dictionaries Returns: LLVM IR source code with signals stored in WASM memory @@ -20,8 +20,9 @@ def generate_llvm_ir(ir: dict[str, Any]) -> str: class SignalLLVMIRGenerator: """Generates LLVM IR with reactive Signal support.""" - def __init__(self, ir: dict[str, Any]): - self.ir = ir + def __init__(self, ir: dict[str, Any] | list[dict[str, Any]]): + # Normalize to list for uniform processing + self.components = ir if isinstance(ir, list) else [ir] self.lines: list[str] = [] self.strings: dict[str, str] = {} # value -> global name self.signals: dict[str, dict[str, Any]] = {} # signal_name -> {initial, index} @@ -33,25 +34,45 @@ class SignalLLVMIRGenerator: # Track signal-to-DOM bindings for updates self.signal_dom_bindings: dict[str, list[str]] = {} # signal_name -> [dom_node_ids] + # Track current component for signal scoping + self.current_component = None + + def _signal_global(self, signal_name: str) -> str: + """Get the scoped global name for a signal.""" + if self.current_component: + return f"@signal_{self.current_component['name']}_{signal_name}" + return f"@signal_{signal_name}" + def generate(self) -> str: """Generate complete LLVM IR module with signals.""" - # First pass: collect signals and handlers - self._collect_signals_and_handlers() - - # Generate IR + # Emit shared header and function declarations once self._emit_header() - self._emit_signal_globals() self._emit_function_declarations() - self._emit_helper_functions() - self._emit_event_handlers() - self._emit_component(self.ir) + + # Process each component + for component in self.components: + # Set current component for signal scoping + self.current_component = component + + # Reset component-specific state + self.signals = {} + self.event_handlers = [] + + # Collect signals and handlers for this component + self._collect_signals_and_handlers(component) + + # Generate component-specific code + self._emit_signal_globals(component) + self._emit_helper_functions() + self._emit_event_handlers() + self._emit_component(component) # Prepend string constants return self._finalize_with_strings() - def _collect_signals_and_handlers(self) -> None: - """Collect all signals and event handlers from IR.""" - for stmt in self.ir["body"]: + def _collect_signals_and_handlers(self, component: dict[str, Any]) -> None: + """Collect all signals and event handlers from component IR.""" + for stmt in component["body"]: if stmt["type"] == "signal": signal_id = self.next_signal_id self.next_signal_id += 1 @@ -65,19 +86,21 @@ class SignalLLVMIRGenerator: def _emit_header(self) -> None: """Emit module header.""" self.lines.append("; Generated by Ritz Framework with Signals") - self.lines.append(f'; Component: {self.ir["name"]}') + component_names = [c["name"] for c in self.components] + self.lines.append(f'; Components: {", ".join(component_names)}') self.lines.append("") - def _emit_signal_globals(self) -> None: - """Emit global variables for signals.""" + def _emit_signal_globals(self, component: dict[str, Any]) -> None: + """Emit global variables for signals in this component.""" if not self.signals: return - self.lines.append("; Signal storage (reactive state in WASM memory)") + component_name = component["name"] + self.lines.append(f"; Signal storage for {component_name} (reactive state in WASM memory)") for signal_name, signal_info in self.signals.items(): - # Each signal is a global i32 + # Each signal is a global i32, scoped to component to avoid collisions initial_value = signal_info["initial"].get("value", 0) - self.lines.append(f"@signal_{signal_name} = global i32 {initial_value}") + self.lines.append(f"@signal_{component_name}_{signal_name} = global i32 {initial_value}") self.lines.append("") def _emit_function_declarations(self) -> None: @@ -105,7 +128,7 @@ class SignalLLVMIRGenerator: for signal_name in self.signals: self.lines.append(f"define i32 @get_{signal_name}() {{") self.lines.append("entry:") - self.lines.append(f" %val = load i32, i32* @signal_{signal_name}") + self.lines.append(f" %val = load i32, i32* {self._signal_global(signal_name)}") self.lines.append(" ret i32 %val") self.lines.append("}") self.lines.append("") @@ -114,7 +137,7 @@ class SignalLLVMIRGenerator: for signal_name in self.signals: self.lines.append(f"define void @set_{signal_name}(i32 %newval) {{") self.lines.append("entry:") - self.lines.append(f" store i32 %newval, i32* @signal_{signal_name}") + self.lines.append(f" store i32 %newval, i32* {self._signal_global(signal_name)}") self.lines.append(" ; TODO: Trigger DOM update callback") self.lines.append(" ret void") self.lines.append("}") @@ -144,7 +167,7 @@ class SignalLLVMIRGenerator: if signal_name in self.signals: # Load current value load_var = self._new_var() - self.lines.append(f" %{load_var} = load i32, i32* @signal_{signal_name}") + self.lines.append(f" %{load_var} = load i32, i32* {self._signal_global(signal_name)}") # Perform operation result_var = self._new_var() @@ -153,7 +176,7 @@ class SignalLLVMIRGenerator: self.lines.append(f" %{result_var} = add i32 %{load_var}, {value}") # Store back - self.lines.append(f" store i32 %{result_var}, i32* @signal_{signal_name}") + self.lines.append(f" store i32 %{result_var}, i32* {self._signal_global(signal_name)}") # TODO: Trigger DOM updates for this signal self.lines.append(f" ; TODO: Update DOM nodes bound to {signal_name}") @@ -239,7 +262,7 @@ class SignalLLVMIRGenerator: signal_name = expr["id"] if signal_name in self.signals: load_var = self._new_var() - self.lines.append(f" %{load_var} = load i32, i32* @signal_{signal_name}") + self.lines.append(f" %{load_var} = load i32, i32* {self._signal_global(signal_name)}") return load_var else: # Unknown variable diff --git a/tests/__pycache__/test_llvm_codegen_conditional.cpython-313-pytest-8.4.2.pyc b/tests/__pycache__/test_llvm_codegen_conditional.cpython-313-pytest-8.4.2.pyc index 64f1643e37599b77f9c0ff3b8b7dcc6d8c53b12a..b45fb961b6e877696627b18d3d5dee2f432d61c4 100644 GIT binary patch delta 140 zcmca-{?wfJGcPX}0}$*=k;ru0$lK4(uj){onVy%J6CaYFo}QDsc?tV-#%SXUEVdic rFLK*nXMv(iEVdW8jUe1BNNl(Qpx}mdIPW3~4=#L}#dh;E&L4sRre-sB delta 133 zcmaEAe$SluGcPX}0}zM>N@Qwoejw6sE7V5QI&?bagrt1*gFViS{EZA`R@ZV(Wuur#yV7`W7v znivme{s5bJk%Nc27cU-qH9b%@G1!s&mij z>=XF&A@3_)-g2IHkh-361jx_;chW(Q@3!dFS}mT8snn}KbWRfD&|kW$gv{&x?j75W z`cO(O6_spRPFL3ztw7}*erYd{jgk6*868u_k|M%P@N*p4>N2|dvDC%wx+|-y1*(~X zlU&fxg8pzwBgy!JFVs`?VWO=`j^+a5OhUehw##ub# z3!Om#DoW>o3qT6E2wY;7$OuWZr;!j@)?Y`Ogk<#EM3>dQz_*R=Y(7t~Lb$}99echZ z!u)H3hNs;|xivZ!wMeHB6$6d}lfXaC$4w}20V^Daee~Cq#QzwOx6|W&x4A6Kzv?X8 zig?+yTYNPrkxuKRnRk|j|8E_-v#)>j8rIiQxv?}i4X;*EHMy$f%B9=``EE(cmy{yS z>bZoCmy?x~elo=#Clhl~1e*Vpc~}@{x(pz1io_cN#hg*|T-&)yHNRdioTvBMUhm delta 951 zcmb7?zfV(96vw&nkO%E6Em)+l77A^VCk=vaQR^>|mS{qZ2o6;PDJeE3*5Y{>0*ku2 zFqpd#HNmwp4(2Tmn&{%>0BT~=!9T#&jmh(Utu+o9y?OcMd(ZveAMc#=e);9H_CwR! z8hFgkbQg?Q+6kxFv(nCh@OE;L4l?%C$l%KQXxuQ#YprQVgwU*QS4oJp)$abNPLc{I z4l%?=hwU}YVBt;>GRC_!EHze|_z-Gmz2VB5<`S*ih6Eeja>UoK_u-gkpdV-fLO@vF z^#!gCK@J1Q7)EQ=YP&`d9R+X|C4d;C;uv39U$|AGVsBd9 zTx3qm19~#!-%%o4`CUikt5W+uDUnC#tFte^Wqp}rTlxBI_m5dU{8@>}r}|rEiZ6Qn zyvrhgmwFOOBPU=#+y&(UIsiX#7zhAfpa=Up^!Kz$J=~eGGgkKOpRwaO z6&a&AU?*@4>J)&)(P>~Dm;f#Slk#C$7b*EH+$-j+H{l&2rmW3@#%A{=UamA{b2+*K zIf0;*c+*cv7yksgX26L6 diff --git a/tests/test_llvm_codegen_conditional.py b/tests/test_llvm_codegen_conditional.py index 188f924..f18bc1c 100644 --- a/tests/test_llvm_codegen_conditional.py +++ b/tests/test_llvm_codegen_conditional.py @@ -21,8 +21,8 @@ def Toggle(): ir = parse_component(source) llvm_ir = generate_llvm_ir(ir) - # Should have signal global - assert "@signal_visible = global i32 1" in llvm_ir + # Should have signal global (scoped to component) + assert "@signal_Toggle_visible = global i32 1" in llvm_ir # Should have conditional branch assert "br i1" in llvm_ir diff --git a/tests/test_signal_codegen.py b/tests/test_signal_codegen.py index 66fcfff..1c86975 100644 --- a/tests/test_signal_codegen.py +++ b/tests/test_signal_codegen.py @@ -55,8 +55,8 @@ def test_signal_with_fstring(): ir = parse_component(source) llvm_ir = generate_llvm_ir(ir) - # Should have signal storage - assert "@signal_count" in llvm_ir + # Should have signal storage (scoped to component) + assert "@signal_Counter_count" in llvm_ir # Should have getter function assert "get_count" in llvm_ir