@@ -71,6 +71,24 @@ function jsStr(s: string): string {
7171 return "'" + s . replace ( / \\ / g, "\\\\" ) . replace ( / ' / g, "\\'" ) + "'" ;
7272}
7373
74+ /**
75+ * Build a tool function lookup expression.
76+ * For dotted names like "vendor.sub.api", generates both a nested optional
77+ * chain (`tools?.["vendor"]?.["sub"]?.["api"]`) and a flat-key fallback
78+ * (`tools?.["vendor.sub.api"]`) so that either tools shape is accepted.
79+ * Single-segment names produce a plain bracket access.
80+ */
81+ function buildToolLookupExpr ( fnName : string ) : string {
82+ const segments = fnName . split ( "." ) ;
83+ if ( segments . length <= 1 ) return `tools[${ jsStr ( fnName ) } ]` ;
84+ // Use double-quoted bracket notation: tools?.["a"]?.["b"]
85+ const dqSeg = ( s : string ) =>
86+ `["${ s . replace ( / \\ / g, "\\\\" ) . replace ( / " / g, '\\"' ) } "]` ;
87+ const nested = `tools?.${ segments . map ( dqSeg ) . join ( "?." ) } ` ;
88+ const flat = `tools?.${ dqSeg ( fnName ) } ` ;
89+ return `(${ nested } ?? ${ flat } )` ;
90+ }
91+
7492/** Emit a JS object literal for a SourceLocation. */
7593function jsLoc ( loc : {
7694 startLine : number ;
@@ -437,6 +455,13 @@ class CodegenContext {
437455 this . emit ( "let __output = {};" ) ;
438456 this . emit ( "" ) ;
439457
458+ // If the bridge body has no output wires (recursively), emit a runtime throw.
459+ if ( ! this . hasAnyOutputWires ( this . bridge . body ) ) {
460+ this . emit (
461+ `throw new Error("Bridge '${ this . bridge . type } .${ this . bridge . field } ' has no output wires.");` ,
462+ ) ;
463+ }
464+
440465 // Compile the bridge body
441466 this . compileBody ( this . bridge . body , rootScope , "__output" ) ;
442467
@@ -938,7 +963,7 @@ class CodegenContext {
938963 // Emit tool function lookup — resolve fn through ToolDef extends chain
939964 const toolDef = this . resolveToolDef ( h . name ) ;
940965 const fnName = toolDef ?. fn ?? h . name ;
941- this . emit ( `const ${ toolFnVar } = tools[ ${ jsStr ( fnName ) } ] ;` ) ;
966+ this . emit ( `const ${ toolFnVar } = ${ buildToolLookupExpr ( fnName ) } ;` ) ;
942967 break ;
943968 }
944969 case "define" :
@@ -1141,7 +1166,7 @@ class CodegenContext {
11411166 // Resolve fn through ToolDef extends chain
11421167 const innerToolDef = this . resolveToolDef ( h . name ) ;
11431168 const fnName = innerToolDef ?. fn ?? h . name ;
1144- this . emit ( `const ${ toolFnVar } = tools[ ${ jsStr ( fnName ) } ] ;` ) ;
1169+ this . emit ( `const ${ toolFnVar } = ${ buildToolLookupExpr ( fnName ) } ;` ) ;
11451170 break ;
11461171 }
11471172 case "define" :
@@ -2085,6 +2110,26 @@ class CodegenContext {
20852110 return undefined ;
20862111 }
20872112
2113+ /** Recursively check if any output wire or spread exists in the body tree. */
2114+ private hasAnyOutputWires ( body : Statement [ ] ) : boolean {
2115+ for ( const stmt of body ) {
2116+ if ( stmt . kind === "wire" ) {
2117+ if (
2118+ stmt . target . module === SELF_MODULE &&
2119+ stmt . target . type === this . bridge . type &&
2120+ stmt . target . field === this . bridge . field
2121+ ) {
2122+ return true ;
2123+ }
2124+ } else if ( stmt . kind === "spread" ) {
2125+ return true ;
2126+ } else if ( stmt . kind === "scope" ) {
2127+ if ( this . hasAnyOutputWires ( stmt . body ) ) return true ;
2128+ }
2129+ }
2130+ return false ;
2131+ }
2132+
20882133 /**
20892134 * Group output-targeting wire statements by their target path key.
20902135 * Only groups wires that target the bridge's own output (skips tool input wires).
@@ -2219,9 +2264,10 @@ class CodegenContext {
22192264 }
22202265
22212266 /**
2222- * Emit a runtime-sorted overdefinition block for wires where all static
2223- * costs are equal. Uses tool metadata (`bridge.cost`, `bridge.sync`) to
2224- * determine cost at runtime and sort the evaluation order.
2267+ * Throw a compile-time error for overdefined wires where all static costs
2268+ * are equal and greater than zero (i.e. all sources are tool-backed with
2269+ * the same priority). The AOT compiler cannot statically determine which
2270+ * tool should win, so this configuration is rejected as incompatible.
22252271 */
22262272 private compileRuntimeSortedOverdef (
22272273 ranked : { wire : WireStatement ; index : number ; cost : number } [ ] ,
@@ -2294,7 +2340,7 @@ class CodegenContext {
22942340 }
22952341
22962342 /**
2297- * Compute a JS expression that evaluates to the runtime cost of an expression.
2343+ * Compute a runtime cost expression for a source expression.
22982344 * For tool refs, checks `tools[name].bridge?.cost ?? (tools[name].bridge?.sync ? 1 : 2)`.
22992345 * For non-tool expressions, returns a literal number.
23002346 */
@@ -2498,6 +2544,13 @@ class CodegenContext {
24982544
24992545 // ── Source chain compilation ──────────────────────────────────────────
25002546
2547+ /**
2548+ * Throw BridgeCompilerIncompatibleError if any source in the chain uses a
2549+ * falsy gate (||) with a tool-backed ref as the fallback. Tool-backed
2550+ * falsy fallbacks are unsupported because they may trigger the secondary
2551+ * tool call even for valid falsy values (0, "", false). Use ?? (nullish)
2552+ * or split into overdefined wires instead.
2553+ */
25012554 private compileSourceChain (
25022555 sources : WireSourceEntry [ ] ,
25032556 wireCatch : WireCatch | undefined ,
0 commit comments