Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 168 additions & 1 deletion packages/bridge-compiler/src/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,14 +575,15 @@ class CodegenContext {
// When requestedFields is provided, drop output wires for fields that
// weren't requested. Kahn's algorithm will then naturally eliminate
// tools that only feed into those dropped wires.
const outputWires = this.requestedFields
const filteredOutputWires = this.requestedFields
? allOutputWires.filter((w) => {
// Root wires (path length 0) and element wires are always included
if (w.to.path.length === 0) return true;
const fieldPath = w.to.path.join(".");
return matchesRequestedFields(fieldPath, this.requestedFields);
})
: allOutputWires;
const outputWires = this.reorderOverdefinedOutputWires(filteredOutputWires);

// Ensure force-only tools (no wires targeting them from output) are
// still included in the tool map for scheduling
Expand Down Expand Up @@ -2222,6 +2223,172 @@ class CodegenContext {
return `{\n${entries.join(",\n")},\n${innerPad}}`;
}

private reorderOverdefinedOutputWires(outputWires: Wire[]): Wire[] {
if (outputWires.length < 2) return outputWires;

const groups = new Map<string, Wire[]>();
for (const wire of outputWires) {
const pathKey = wire.to.path.join(".");
const group = groups.get(pathKey) ?? [];
group.push(wire);
groups.set(pathKey, group);
}

const emitted = new Set<string>();
const reordered: Wire[] = [];
let changed = false;

for (const wire of outputWires) {
const pathKey = wire.to.path.join(".");
if (emitted.has(pathKey)) continue;
emitted.add(pathKey);

const group = groups.get(pathKey)!;
if (group.length < 2) {
reordered.push(...group);
continue;
}

const ranked = group.map((candidate, index) => ({
wire: candidate,
index,
cost: this.classifyOverdefinitionWire(candidate),
}));
ranked.sort((left, right) => {
if (left.cost !== right.cost) {
changed = true;
return left.cost - right.cost;
}
return left.index - right.index;
});
reordered.push(...ranked.map((entry) => entry.wire));
}

return changed ? reordered : outputWires;
}

private classifyOverdefinitionWire(
wire: Wire,
visited = new Set<string>(),
): number {
return this.canResolveWireCheaply(wire, visited) ? 0 : 1;
}

private canResolveWireCheaply(
wire: Wire,
visited = new Set<string>(),
): boolean {
if ("value" in wire) return true;

if ("from" in wire) {
if (!this.refIsZeroCost(wire.from, visited)) return false;
for (const fallback of wire.fallbacks ?? []) {
if (fallback.ref && !this.refIsZeroCost(fallback.ref, visited)) {
return false;
}
}
if (
wire.catchFallbackRef &&
!this.refIsZeroCost(wire.catchFallbackRef, visited)
) {
return false;
}
return true;
}

if ("cond" in wire) {
if (!this.refIsZeroCost(wire.cond, visited)) return false;
if (wire.thenRef && !this.refIsZeroCost(wire.thenRef, visited))
return false;
if (wire.elseRef && !this.refIsZeroCost(wire.elseRef, visited))
return false;
for (const fallback of wire.fallbacks ?? []) {
if (fallback.ref && !this.refIsZeroCost(fallback.ref, visited)) {
return false;
}
}
if (
wire.catchFallbackRef &&
!this.refIsZeroCost(wire.catchFallbackRef, visited)
) {
return false;
}
return true;
}

if ("condAnd" in wire) {
if (!this.refIsZeroCost(wire.condAnd.leftRef, visited)) return false;
if (
wire.condAnd.rightRef &&
!this.refIsZeroCost(wire.condAnd.rightRef, visited)
) {
return false;
}
for (const fallback of wire.fallbacks ?? []) {
if (fallback.ref && !this.refIsZeroCost(fallback.ref, visited)) {
return false;
}
}
if (
wire.catchFallbackRef &&
!this.refIsZeroCost(wire.catchFallbackRef, visited)
) {
return false;
}
return true;
}

if ("condOr" in wire) {
if (!this.refIsZeroCost(wire.condOr.leftRef, visited)) return false;
if (
wire.condOr.rightRef &&
!this.refIsZeroCost(wire.condOr.rightRef, visited)
) {
return false;
}
for (const fallback of wire.fallbacks ?? []) {
if (fallback.ref && !this.refIsZeroCost(fallback.ref, visited)) {
return false;
}
}
if (
wire.catchFallbackRef &&
!this.refIsZeroCost(wire.catchFallbackRef, visited)
) {
return false;
}
return true;
}

return false;
}

private refIsZeroCost(ref: NodeRef, visited = new Set<string>()): boolean {
if (ref.element) return true;
if (
ref.module === SELF_MODULE &&
((ref.type === this.bridge.type && ref.field === this.bridge.field) ||
(ref.type === "Context" && ref.field === "context") ||
(ref.type === "Const" && ref.field === "const"))
) {
return true;
}
if (ref.module.startsWith("__define_")) return false;

const key = refTrunkKey(ref);
if (visited.has(key)) return false;
visited.add(key);

if (ref.module === "__local") {
const incoming = this.bridge.wires.filter(
(wire) => refTrunkKey(wire.to) === key,
);
return incoming.some((wire) => this.canResolveWireCheaply(wire, visited));
}

return false;
}

/**
* Build the body of a `.map()` callback from element wires.
*
Expand Down
49 changes: 45 additions & 4 deletions packages/bridge-compiler/test/codegen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1399,7 +1399,7 @@ bridge Query.test {
assert.deepStrictEqual(callLog, ["expensiveApi"], "tool should be called");
});

test("tool before input — tool always called (primary position)", async () => {
test("tool before input — zero-cost input still wins", async () => {
const callLog: string[] = [];
const result = await compileAndRun(
`version 1.5
Expand All @@ -1420,9 +1420,8 @@ bridge Query.test {
},
},
);
// Tool is first (primary) → always called → wins
assert.equal(result.label, "from-api");
assert.deepStrictEqual(callLog, ["api"]);
assert.equal(result.label, "from-input");
assert.deepStrictEqual(callLog, []);
});

test("two tools — second skipped when first resolves non-null", async () => {
Expand Down Expand Up @@ -1532,6 +1531,48 @@ bridge Query.test {
assert.deepStrictEqual(aotNoCache.data, rtNoCache.data);
});

test("overdefinition parity — zero-cost sources win before tool calls", async () => {
const bridgeText = `version 1.5
bridge Query.test {
with expensiveApi
with input as i
with output as o

o.val <- expensiveApi.data
o.val <- i.cached
}`;
const document = parseBridgeFormat(bridgeText);
const callLog: string[] = [];
const tools = {
expensiveApi: () => {
callLog.push("expensiveApi");
return { data: "expensive" };
},
};

const rtWithCache = await executeBridge({
document,
operation: "Query.test",
input: { cached: "hit" },
tools,
});
const rtLog = [...callLog];
callLog.length = 0;

const aotWithCache = await executeAot({
document,
operation: "Query.test",
input: { cached: "hit" },
tools,
});
const aotLog = [...callLog];

assert.deepStrictEqual(rtWithCache.data, { val: "hit" });
assert.deepStrictEqual(aotWithCache.data, { val: "hit" });
assert.deepStrictEqual(rtLog, []);
assert.deepStrictEqual(aotLog, []);
});

test("constant overdefinition parity — first constant remains terminal", async () => {
const document: BridgeDocument = {
instructions: [
Expand Down
Loading
Loading