Skip to content

Commit 11c5a06

Browse files
authored
Recursive wires (#132)
* Phase 1 and 2 * Stage 4 * Parser * Security audit fixes * 6B * Compiler * Cleanup * Final cleanup * Add new internal wire structure with recursive expressions
1 parent 15ca618 commit 11c5a06

35 files changed

Lines changed: 4623 additions & 3607 deletions

.changeset/funky-jars-accept.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@stackables/bridge-compiler": minor
3+
"@stackables/bridge-graphql": minor
4+
"@stackables/bridge-parser": minor
5+
"@stackables/bridge-core": minor
6+
"@stackables/bridge": minor
7+
---
8+
9+
New internal wire structure with recursive expressions

packages/bridge-compiler/src/bridge-asserts.ts

Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ import {
22
SELF_MODULE,
33
type Bridge,
44
type NodeRef,
5+
type Wire,
56
} from "@stackables/bridge-core";
67

8+
const isPull = (w: Wire): boolean => w.sources[0]?.expr.type === "ref";
9+
const wRef = (w: Wire): NodeRef => (w.sources[0].expr as { ref: NodeRef }).ref;
10+
711
export class BridgeCompilerIncompatibleError extends Error {
812
constructor(
913
public readonly operation: string,
@@ -56,20 +60,22 @@ export function assertBridgeCompilerCompatible(
5660
): void {
5761
const op = `${bridge.type}.${bridge.field}`;
5862

63+
const wires: Wire[] = bridge.wires;
64+
5965
// Pipe-handle trunk keys — block-scoped aliases inside array maps
6066
// reference these; the compiler handles them correctly.
6167
const pipeTrunkKeys = new Set((bridge.pipeHandles ?? []).map((ph) => ph.key));
6268

63-
for (const w of bridge.wires) {
69+
for (const w of wires) {
6470
// User-level alias (Shadow) wires: compiler has TDZ ordering bugs.
6571
// Block-scoped aliases inside array maps wire FROM a pipe-handle tool
6672
// instance (key is in pipeTrunkKeys) and are handled correctly.
6773
if (w.to.module === "__local" && w.to.type === "Shadow") {
68-
if (!("from" in w)) continue;
74+
if (!isPull(w)) continue;
6975
const fromKey =
70-
w.from.instance != null
71-
? `${w.from.module}:${w.from.type}:${w.from.field}:${w.from.instance}`
72-
: `${w.from.module}:${w.from.type}:${w.from.field}`;
76+
wRef(w).instance != null
77+
? `${wRef(w).module}:${wRef(w).type}:${wRef(w).field}:${wRef(w).instance}`
78+
: `${wRef(w).module}:${wRef(w).type}:${wRef(w).field}`;
7379
if (!pipeTrunkKeys.has(fromKey)) {
7480
throw new BridgeCompilerIncompatibleError(
7581
op,
@@ -79,16 +85,12 @@ export function assertBridgeCompilerCompatible(
7985
continue;
8086
}
8187

82-
if (!("from" in w)) continue;
88+
if (!isPull(w)) continue;
8389

8490
// Catch fallback on pipe wires (expression results) — the catch must
8591
// propagate to the upstream tool, not the internal operator; codegen
8692
// does not handle this yet.
87-
if (
88-
"pipe" in w &&
89-
w.pipe &&
90-
("catchFallback" in w || "catchFallbackRef" in w || "catchControl" in w)
91-
) {
93+
if (w.pipe && w.catch) {
9294
throw new BridgeCompilerIncompatibleError(
9395
op,
9496
"Catch fallback on expression (pipe) wires is not yet supported by the compiler.",
@@ -97,13 +99,11 @@ export function assertBridgeCompilerCompatible(
9799

98100
// Catch fallback that references a pipe handle — the compiler eagerly
99101
// calls all tools in the catch branch even when the main wire succeeds.
100-
if ("catchFallbackRef" in w && w.catchFallbackRef) {
101-
const ref = w.catchFallbackRef as NodeRef;
102+
if (w.catch && "ref" in w.catch) {
103+
const ref = w.catch.ref;
102104
if (ref.instance != null) {
103105
const refKey = `${ref.module}:${ref.type}:${ref.field}:${ref.instance}`;
104-
if (
105-
bridge.pipeHandles?.some((ph) => ph.key === refKey)
106-
) {
106+
if (bridge.pipeHandles?.some((ph) => ph.key === refKey)) {
107107
throw new BridgeCompilerIncompatibleError(
108108
op,
109109
"Catch fallback referencing a pipe expression is not yet supported by the compiler.",
@@ -115,16 +115,12 @@ export function assertBridgeCompilerCompatible(
115115
// Catch fallback on wires whose source tool has tool-backed input
116116
// dependencies — the compiler only catch-guards the direct source
117117
// tool, not its transitive dependency chain.
118-
if (
119-
("catchFallback" in w || "catchFallbackRef" in w || "catchControl" in w) &&
120-
"from" in w &&
121-
isToolRef(w.from, bridge)
122-
) {
123-
const sourceTrunk = `${w.from.module}:${w.from.type}:${w.from.field}`;
124-
for (const iw of bridge.wires) {
125-
if (!("from" in iw)) continue;
118+
if (w.catch && isToolRef(wRef(w), bridge)) {
119+
const sourceTrunk = `${wRef(w).module}:${wRef(w).type}:${wRef(w).field}`;
120+
for (const iw of wires) {
121+
if (!isPull(iw)) continue;
126122
const iwDest = `${iw.to.module}:${iw.to.type}:${iw.to.field}`;
127-
if (iwDest === sourceTrunk && isToolRef(iw.from, bridge)) {
123+
if (iwDest === sourceTrunk && isToolRef(wRef(iw), bridge)) {
128124
throw new BridgeCompilerIncompatibleError(
129125
op,
130126
"Catch fallback on wires with tool chain dependencies is not yet supported by the compiler.",
@@ -136,30 +132,28 @@ export function assertBridgeCompilerCompatible(
136132
// Fallback chains (|| / ??) with tool-backed refs — compiler eagerly
137133
// calls all tools via Promise.all, so short-circuit semantics are lost
138134
// and tool side effects fire unconditionally.
139-
if (w.fallbacks) {
140-
for (const fb of w.fallbacks) {
141-
if (fb.ref && isToolRef(fb.ref, bridge)) {
142-
throw new BridgeCompilerIncompatibleError(
143-
op,
144-
"Fallback chains (|| / ??) with tool-backed sources are not yet supported by the compiler.",
145-
);
146-
}
135+
for (const src of w.sources.slice(1)) {
136+
if (src.expr.type === "ref" && isToolRef(src.expr.ref, bridge)) {
137+
throw new BridgeCompilerIncompatibleError(
138+
op,
139+
"Fallback chains (|| / ??) with tool-backed sources are not yet supported by the compiler.",
140+
);
147141
}
148142
}
149143
}
150144

151145
// Same-cost overdefinition sourced only from tools can diverge from runtime
152146
// tracing/error behavior in current AOT codegen; compile must downgrade.
153147
const toolOnlyOverdefs = new Map<string, number>();
154-
for (const w of bridge.wires) {
148+
for (const w of wires) {
155149
if (
156150
w.to.module !== SELF_MODULE ||
157151
w.to.type !== bridge.type ||
158152
w.to.field !== bridge.field
159153
) {
160154
continue;
161155
}
162-
if (!("from" in w) || !isToolRef(w.from, bridge)) {
156+
if (!isPull(w) || !isToolRef(wRef(w), bridge)) {
163157
continue;
164158
}
165159

@@ -196,8 +190,8 @@ export function assertBridgeCompilerCompatible(
196190
);
197191
}
198192

199-
for (const w of bridge.wires) {
200-
if (!("from" in w) || w.to.path.length === 0) continue;
193+
for (const w of wires) {
194+
if (!isPull(w) || w.to.path.length === 0) continue;
201195
// Build the full key for this wire target
202196
const fullKey =
203197
w.to.instance != null

0 commit comments

Comments
 (0)