From 7a6a55e633705fba859339edea161c97ed8ef151 Mon Sep 17 00:00:00 2001 From: Aarne Laur Date: Sun, 15 Mar 2026 10:14:29 +0100 Subject: [PATCH 1/3] Tests and docs --- README.md | 2 +- .../should_i_go_outside.bridge | 2 +- packages/bridge-compiler/test/codegen.test.ts | 10 +-- .../test/traversal-manifest-locations.test.ts | 2 +- .../bridge-parser/test/bridge-format.test.ts | 6 +- .../bridge-parser/test/parser-compat.test.ts | 8 +-- .../test/path-scoping-parser.test.ts | 2 +- .../test/source-locations.test.ts | 4 +- packages/bridge/bench/compiler.bench.ts | 2 +- packages/bridge/bench/engine.bench.ts | 4 +- packages/bridge/test/alias.test.ts | 61 +++++++++++++++++++ packages/bridge/test/coalesce-cost.test.ts | 2 +- packages/bridge/test/execute-bridge.test.ts | 42 ++++++------- packages/bridge/test/language-spec/README.md | 2 +- packages/bridge/test/path-scoping.test.ts | 2 +- packages/bridge/test/shared-parity.test.ts | 4 +- packages/bridge/test/sync-tools.test.ts | 2 +- packages/bridge/test/ternary.test.ts | 10 +-- .../content/docs/advanced/input-validation.md | 4 +- .../content/docs/guides/getting-started.mdx | 2 +- .../docs/reference/30-wiring-routing.mdx | 12 ++-- .../docs/reference/40-using-tools-pipes.mdx | 4 +- .../docs/reference/70-array-mapping.mdx | 6 +- .../src/content/docs/reference/summary.mdx | 6 +- .../src/content/docs/tools/array-tools.mdx | 2 +- packages/playground/src/examples.ts | 18 +++--- 26 files changed, 141 insertions(+), 80 deletions(-) create mode 100644 packages/bridge/test/alias.test.ts diff --git a/README.md b/README.md index 69e13c4b..3b31017c 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ The `.bridge` language is designed to be scannable. | **Null Coalesce** | `o.name <- i.name \|\| "N/A"` | Alternative used if the current source resolves to `null`. | | **Error Guard** | `o.price <- api.price catch 0` | Alternative used if the current source **throws** an exception. | | **Ternary** | `o.val <- i.isPro ? a : b` | Evaluates condition; strictly pulls only the chosen branch. | -| **Node Alias** | `alias uc:i.name as name` | Evaluates an expression once and caches it as a local graph node. | +| **Node Alias** | `alias name <- uc:i.name` | Evaluates an expression once and caches it as a local graph node. | | **Arrays** | `o <- items[] as it { }` | Iterates over an array, creating a local shadow scope for each element. | **[Read the Full Language Guide](https://bridge.sdk42.com/reference/10-core-concepts/)** diff --git a/examples/without-graphql/should_i_go_outside.bridge b/examples/without-graphql/should_i_go_outside.bridge index 342bf6ab..c62282bf 100644 --- a/examples/without-graphql/should_i_go_outside.bridge +++ b/examples/without-graphql/should_i_go_outside.bridge @@ -27,7 +27,7 @@ bridge Query.should_i_go_outside { g.q <- i.cityName g.format = "json" - alias first:g as f + alias f <- first:g w.latitude <- f.lat w.longitude <- f.lon diff --git a/packages/bridge-compiler/test/codegen.test.ts b/packages/bridge-compiler/test/codegen.test.ts index 7886853e..6726d276 100644 --- a/packages/bridge-compiler/test/codegen.test.ts +++ b/packages/bridge-compiler/test/codegen.test.ts @@ -2021,7 +2021,7 @@ describe("AOT codegen: async array mapping", () => { with output as o o.items <- api.results[] as item { - alias enricher:item as e + alias e <- enricher:item .name <- item.name ?? continue .extra <- e.data } @@ -2107,7 +2107,7 @@ describe("AOT codegen: async array mapping", () => { o <- api.groups[] as g { .label <- g.name .items <- g.items[] as sub { - alias enricher:sub as e + alias e <- enricher:sub .value <- e.data } } @@ -2145,7 +2145,7 @@ describe("AOT codegen: async array mapping", () => { with output as o o.items <- api.results[] as item { - alias enricher:item as e + alias e <- enricher:item .name <- item.name ?? continue .extra <- e.data catch "n/a" } @@ -2178,7 +2178,7 @@ describe("AOT codegen: async array mapping", () => { o.title <- api.title o.items <- api.items[] as item { - alias enricher:item as e + alias e <- enricher:item .name <- item.name ?? continue .extra <- e.data } @@ -2279,7 +2279,7 @@ bridge Query.catalog { with output as o o <- src.items[] as it { - alias enrich:it as e + alias e <- enrich:it .id <- it.item_id .label <- e.name } diff --git a/packages/bridge-core/test/traversal-manifest-locations.test.ts b/packages/bridge-core/test/traversal-manifest-locations.test.ts index 1316f0c8..af963c41 100644 --- a/packages/bridge-core/test/traversal-manifest-locations.test.ts +++ b/packages/bridge-core/test/traversal-manifest-locations.test.ts @@ -43,7 +43,7 @@ describe("buildTraversalManifest source locations", () => { bridge Query.test { with input as i with output as o - alias i.empty.array.error catch i.empty.array.error as clean + alias clean <- i.empty.array.error catch i.empty.array.error o.message <- i.empty.array?.error ?? i.empty.array.error catch clean } `); diff --git a/packages/bridge-parser/test/bridge-format.test.ts b/packages/bridge-parser/test/bridge-format.test.ts index 6e67b7fb..566dca69 100644 --- a/packages/bridge-parser/test/bridge-format.test.ts +++ b/packages/bridge-parser/test/bridge-format.test.ts @@ -669,7 +669,7 @@ describe("serializeBridge", () => { with output as o o.items <- a.data[] as item { - alias item.title as nm + alias nm <- item.title .label <- nm .price <- item.cost } @@ -679,7 +679,7 @@ describe("serializeBridge", () => { const instructions = parseBridge(input); const ser1 = serializeBridge(instructions); // Validate the alias statement is present in serialized output - assert.ok(ser1.includes("alias item.title as nm")); + assert.ok(ser1.includes("alias nm <- item.title")); // Validate idempotency: re-serialize produces identical output const ser2 = serializeBridge(parseBridge(ser1)); assert.equal(ser1, ser2); @@ -694,7 +694,7 @@ describe("serializeBridge", () => { with output as o api.q <- i.search - alias api.result.data as d + alias d <- api.result.data o.name <- d.name o.email <- d.email diff --git a/packages/bridge-parser/test/parser-compat.test.ts b/packages/bridge-parser/test/parser-compat.test.ts index 6b876b5f..61407048 100644 --- a/packages/bridge-parser/test/parser-compat.test.ts +++ b/packages/bridge-parser/test/parser-compat.test.ts @@ -489,7 +489,7 @@ bridge Query.list { with output as o o <- api.items[] as it { - alias enrich:it as resp + alias resp <- enrich:it .a <- resp.a .b <- resp.b } @@ -504,7 +504,7 @@ bridge Query.list { with output as o o <- api.items[] as it { - alias it.nested as n + alias n <- it.nested .x <- n.a .y <- n.b } @@ -519,7 +519,7 @@ bridge Query.test { with input as i with output as o - alias uc:i.name as upper + alias upper <- uc:i.name o.label <- upper }`, @@ -534,7 +534,7 @@ bridge Query.test { with output as o api.q <- i.q - alias api.result.data as d + alias d <- api.result.data o.name <- d.name o.email <- d.email diff --git a/packages/bridge-parser/test/path-scoping-parser.test.ts b/packages/bridge-parser/test/path-scoping-parser.test.ts index 64e88495..a715def2 100644 --- a/packages/bridge-parser/test/path-scoping-parser.test.ts +++ b/packages/bridge-parser/test/path-scoping-parser.test.ts @@ -439,7 +439,7 @@ describe("path scoping – parser", () => { o { .info { - alias uc:i.name as upper + alias upper <- uc:i.name .displayName <- upper .email <- i.email } diff --git a/packages/bridge-parser/test/source-locations.test.ts b/packages/bridge-parser/test/source-locations.test.ts index d10e6dd0..1633a33b 100644 --- a/packages/bridge-parser/test/source-locations.test.ts +++ b/packages/bridge-parser/test/source-locations.test.ts @@ -95,7 +95,7 @@ describe("parser source locations", () => { bridge Query.test { with input as i with output as o - alias i.empty.array.error catch i.empty.array.error as clean + alias clean <- i.empty.array.error catch i.empty.array.error o.message <- i.empty.array?.error ?? i.empty.array.error catch clean } `); @@ -103,7 +103,7 @@ describe("parser source locations", () => { const aliasWire = instr.wires.find((wire) => wire.to.field === "clean"); assert.ok(aliasWire?.catch); assert.equal(aliasWire.catch.loc?.startLine, 5); - assert.equal(aliasWire.catch.loc?.startColumn, 35); + assert.equal(aliasWire.catch.loc?.startColumn, 45); const messageWire = instr.wires.find( (wire) => wire.to.path.join(".") === "message", diff --git a/packages/bridge/bench/compiler.bench.ts b/packages/bridge/bench/compiler.bench.ts index 13323ae9..3c69d899 100644 --- a/packages/bridge/bench/compiler.bench.ts +++ b/packages/bridge/bench/compiler.bench.ts @@ -143,7 +143,7 @@ bridge Query.enriched { with output as o o <- api.items[] as it { - alias enrich:it as resp + alias resp <- enrich:it .a <- resp.a .b <- resp.b } diff --git a/packages/bridge/bench/engine.bench.ts b/packages/bridge/bench/engine.bench.ts index cdf98aae..e3e96fca 100644 --- a/packages/bridge/bench/engine.bench.ts +++ b/packages/bridge/bench/engine.bench.ts @@ -107,7 +107,7 @@ bridge Query.enriched { with output as o o <- api.items[] as it { - alias enrich:it as resp + alias resp <- enrich:it .a <- resp.a .b <- resp.b } @@ -151,7 +151,7 @@ bridge Query.enriched { with output as o o <- api.items[] as it { - alias enrich:it as resp + alias resp <- enrich:it .a <- resp.a .b <- resp.b } diff --git a/packages/bridge/test/alias.test.ts b/packages/bridge/test/alias.test.ts new file mode 100644 index 00000000..5c962137 --- /dev/null +++ b/packages/bridge/test/alias.test.ts @@ -0,0 +1,61 @@ +import { regressionTest } from "./utils/regression.ts"; +import { tools } from "./utils/bridge-tools.ts"; +import { bridge } from "@stackables/bridge"; + +// ═══════════════════════════════════════════════════════════════════════════ +// Chained providers +// +// Tests that output from one tool flows correctly as input to the next. +// Uses test.multitool (echo) to verify wire routing across a 3-tool chain: +// input → gc → cx → ti → output +// ═══════════════════════════════════════════════════════════════════════════ + +regressionTest("alias keyword", { + bridge: bridge` + version 1.5 + + define reusable { + with inout as i + with output as o + + alias temp <- i.value || "Default" catch "Error" + + o.result <- temp + } + + bridge Alias.syntax { + with test.multitool as array + with test.multitool as object + with reusable as r + with context + with input as i + with output as o + + # The usual complex reusable alias + alias user_info <- object?.user.info || i.info catch "Unknown" + + # Alias can store the array mapping result + alias raw_response <- array[] as i { + + alias in_loop <- i.name || "No name" + + .name <- in_loop + .info <- user_info + } + + # Alias can be used in alias + alias safe_response <- raw_response catch [] + + o.names <- safe_response + o.info <- user_info + + # Alias used inside the define block + r.value <- i.info + o.reusable_result <- r.result + } + `, + tools: tools, + scenarios: { + "Alias.syntax": {}, + }, +}); diff --git a/packages/bridge/test/coalesce-cost.test.ts b/packages/bridge/test/coalesce-cost.test.ts index 624f3a3c..cfeb0c03 100644 --- a/packages/bridge/test/coalesce-cost.test.ts +++ b/packages/bridge/test/coalesce-cost.test.ts @@ -152,7 +152,7 @@ regressionTest("overdefinition: cost-based prioritization", { with input as i with output as o - alias i.hint as cached + alias cached <- i.hint api <- i.api o.label <- api.label diff --git a/packages/bridge/test/execute-bridge.test.ts b/packages/bridge/test/execute-bridge.test.ts index 30895347..997e0c36 100644 --- a/packages/bridge/test/execute-bridge.test.ts +++ b/packages/bridge/test/execute-bridge.test.ts @@ -191,7 +191,7 @@ regressionTest("array blocks: pipe, alias, and ternary", { src <- i.src o.title <- src.name ?? "Untitled" o.entries <- src.items[] as it { - alias enrich:it as e + alias e <- enrich:it .id <- it.item_id .label <- e.in.name } @@ -457,7 +457,7 @@ regressionTest("alias: iterator-scoped aliases", { api <- i.api o <- api.items[] as it { - alias enrich:it as resp + alias resp <- enrich:it .a <- resp.in.id .b <- resp.in.name } @@ -470,7 +470,7 @@ regressionTest("alias: iterator-scoped aliases", { api <- i.api o <- api.items[] as it { - alias it.nested as n + alias n <- it.nested .x <- n.a .y <- n.b } @@ -484,7 +484,7 @@ regressionTest("alias: iterator-scoped aliases", { api <- i.api o <- api.items[] as it { - alias uc:it.name as upper + alias upper <- uc:it.name .label <- upper .id <- it.id } @@ -515,7 +515,7 @@ regressionTest("alias: iterator-scoped aliases", { }, }, "Query.aliasIterSub": { - "alias iter.subfield as name": { + "alias name <- iter.subfield": { input: { api: { items: [{ nested: { a: 1, b: 2 } }, { nested: { a: 3, b: 4 } }], @@ -569,7 +569,7 @@ regressionTest("alias: top-level aliases", { with input as i with output as o - alias uc:i.name as cached + alias cached <- uc:i.name o.greeting <- cached o.label <- cached @@ -582,7 +582,7 @@ regressionTest("alias: top-level aliases", { with output as o api <- i.api - alias api.result.data as d + alias d <- api.result.data o.name <- d.name o.email <- d.email @@ -595,10 +595,10 @@ regressionTest("alias: top-level aliases", { with input as i api <- i.api - alias uc:i.category as upperCat + alias upperCat <- uc:i.category o <- api.products[] as it { - alias uc:it.title as upper + alias upper <- uc:it.title .name <- upper .price <- it.price .category <- upperCat @@ -619,7 +619,7 @@ regressionTest("alias: top-level aliases", { }, }, "Query.aliasTopHandle": { - "top-level alias handle.path as name — simple rename": { + "top-level alias name <- handle.path — simple rename": { input: { api: { result: { data: { name: "Alice", email: "alice@test.com" } }, @@ -667,7 +667,7 @@ regressionTest("alias: expressions and modifiers", { with output as o with input as i - alias i.nickname || "Guest" as displayName + alias displayName <- i.nickname || "Guest" o.name <- displayName } @@ -676,7 +676,7 @@ regressionTest("alias: expressions and modifiers", { with output as o with input as i - alias i.score ?? 0 as score + alias score <- i.score ?? 0 o.score <- score } @@ -687,7 +687,7 @@ regressionTest("alias: expressions and modifiers", { with input as i api <- i.api - alias api.value catch 99 as safeVal + alias safeVal <- api.value catch 99 o.result <- safeVal } @@ -698,7 +698,7 @@ regressionTest("alias: expressions and modifiers", { with input as i api <- i.api - alias api?.value as safeVal + alias safeVal <- api?.value o.result <- safeVal || "fallback" } @@ -707,7 +707,7 @@ regressionTest("alias: expressions and modifiers", { with input as i with output as o - alias i.price + 10 as bumped + alias bumped <- i.price + 10 o.result <- bumped } @@ -716,7 +716,7 @@ regressionTest("alias: expressions and modifiers", { with input as i with output as o - alias i.role == "admin" as isAdmin + alias isAdmin <- i.role == "admin" o.isAdmin <- isAdmin } @@ -725,7 +725,7 @@ regressionTest("alias: expressions and modifiers", { with input as i with output as o - alias (i.a + i.b) * 2 as doubled + alias doubled <- (i.a + i.b) * 2 o.result <- doubled } @@ -733,7 +733,7 @@ regressionTest("alias: expressions and modifiers", { bridge AliasStringLit.test { with output as o - alias "hello world" as greeting + alias greeting <- "hello world" o.result <- greeting } @@ -742,7 +742,7 @@ regressionTest("alias: expressions and modifiers", { with input as i with output as o - alias "a" == i.val as matchesA + alias matchesA <- "a" == i.val o.result <- matchesA } @@ -751,7 +751,7 @@ regressionTest("alias: expressions and modifiers", { with input as i with output as o - alias not i.blocked as allowed + alias allowed <- not i.blocked o.allowed <- allowed } @@ -760,7 +760,7 @@ regressionTest("alias: expressions and modifiers", { with input as i with output as o - alias i.score >= 90 ? "A" : "B" as grade + alias grade <- i.score >= 90 ? "A" : "B" o.grade <- grade } diff --git a/packages/bridge/test/language-spec/README.md b/packages/bridge/test/language-spec/README.md index a82bd88d..f896136b 100644 --- a/packages/bridge/test/language-spec/README.md +++ b/packages/bridge/test/language-spec/README.md @@ -100,7 +100,7 @@ result. This enables progressive enrichment without explicit conditionals. ### 14. `alias.test.ts` — Caching resolved values -`alias source as name` evaluates once and caches the result. Prevents duplicate +`alias name <- source` evaluates once and caches the result. Prevents duplicate tool calls. Top-level aliases live for the whole request; iterator-scoped aliases are re-evaluated per array element. diff --git a/packages/bridge/test/path-scoping.test.ts b/packages/bridge/test/path-scoping.test.ts index 25260932..5443f6fc 100644 --- a/packages/bridge/test/path-scoping.test.ts +++ b/packages/bridge/test/path-scoping.test.ts @@ -163,7 +163,7 @@ regressionTest("path scoping: alias inside nested scope", { with output as o a.q <- i.q - alias a.metadata as meta + alias meta <- a.metadata o.info { .title <- a.title .author <- meta.author diff --git a/packages/bridge/test/shared-parity.test.ts b/packages/bridge/test/shared-parity.test.ts index 36c3f6cf..0313a446 100644 --- a/packages/bridge/test/shared-parity.test.ts +++ b/packages/bridge/test/shared-parity.test.ts @@ -1241,7 +1241,7 @@ regressionTest("parity: alias declarations", { bridge Query.aliasSimple { with api with output as o - alias api.result.data as d + alias d <- api.result.data o.value <- d.name } @@ -1250,7 +1250,7 @@ regressionTest("parity: alias declarations", { with input as i with output as o - alias myUC:i.name as upper + alias upper <- myUC:i.name o.greeting <- upper.out } `, diff --git a/packages/bridge/test/sync-tools.test.ts b/packages/bridge/test/sync-tools.test.ts index f9b2a19e..7941c4a8 100644 --- a/packages/bridge/test/sync-tools.test.ts +++ b/packages/bridge/test/sync-tools.test.ts @@ -203,7 +203,7 @@ regressionTest("sync array map", { with output as o o <- src.items[] as it { - alias enrich:it as e + alias e <- enrich:it .id <- it.item_id .label <- e.name } diff --git a/packages/bridge/test/ternary.test.ts b/packages/bridge/test/ternary.test.ts index 913c24ed..f3b5c985 100644 --- a/packages/bridge/test/ternary.test.ts +++ b/packages/bridge/test/ternary.test.ts @@ -287,7 +287,7 @@ regressionTest("alias ternary: panic gate on age check", { with input as i with output as o - alias (i.age >= 18) ? i : null ?? panic "Must be 18 or older" as ageChecked + alias ageChecked <- (i.age >= 18) ? i : null ?? panic "Must be 18 or older" geo.q <- ageChecked?.city @@ -327,7 +327,7 @@ regressionTest("alias ternary: fallback variants", { with input as i with output as o - alias i.score >= 50 ? i.grade : null || "F" as grade + alias grade <- i.score >= 50 ? i.grade : null || "F" o.grade <- grade } @@ -337,7 +337,7 @@ regressionTest("alias ternary: fallback variants", { with output as o fb <- i.fb - alias i.score >= 50 ? i.grade : null || fb.grade as grade + alias grade <- i.score >= 50 ? i.grade : null || fb.grade o.grade <- grade } @@ -347,7 +347,7 @@ regressionTest("alias ternary: fallback variants", { with output as o a <- i.a - alias a.ok ? a.value : a.alt catch "safe" as result + alias result <- a.ok ? a.value : a.alt catch "safe" o.val <- result } @@ -355,7 +355,7 @@ regressionTest("alias ternary: fallback variants", { with input as i with output as o - alias "hello" == i.secret ? "access granted" : null ?? panic "wrong secret" as result + alias result <- "hello" == i.secret ? "access granted" : null ?? panic "wrong secret" o.msg <- result } `, diff --git a/packages/docs-site/src/content/docs/advanced/input-validation.md b/packages/docs-site/src/content/docs/advanced/input-validation.md index 0c609a42..cf9a9a82 100644 --- a/packages/docs-site/src/content/docs/advanced/input-validation.md +++ b/packages/docs-site/src/content/docs/advanced/input-validation.md @@ -49,7 +49,7 @@ bridge Query.location { # 1. Create a Gate: If age >= 18, yield the input. Otherwise, yield null. # 2. The ?? fallback catches the null and throws! - alias (i.age >= 18) ? i : null ?? throw "Must be 18 or older" as ageChecked + alias ageChecked <- (i.age >= 18) ? i : null ?? throw "Must be 18 or older" # 3. Wire from the gated input. geo.q <- ageChecked.city @@ -71,7 +71,7 @@ bridge Query.location { with output as o # 1. Declare the business rule using inline boolean logic. - alias (i.age >= 18) || throw "Must be 18 or older" as ageCheck + alias ageCheck <- (i.age >= 18) || throw "Must be 18 or older" # 2. Force the engine to evaluate the rule eagerly! force ageCheck diff --git a/packages/docs-site/src/content/docs/guides/getting-started.mdx b/packages/docs-site/src/content/docs/guides/getting-started.mdx index db5686b3..314bb4a9 100644 --- a/packages/docs-site/src/content/docs/guides/getting-started.mdx +++ b/packages/docs-site/src/content/docs/guides/getting-started.mdx @@ -53,7 +53,7 @@ The Bridge separates your mapping logic (`.bridge` files) from your execution en g.q <- i.cityName g.format = "json" - alias first:g as f + alias f <- first:g w.latitude <- f.lat w.longitude <- f.lon diff --git a/packages/docs-site/src/content/docs/reference/30-wiring-routing.mdx b/packages/docs-site/src/content/docs/reference/30-wiring-routing.mdx index 668d92f2..4158126f 100644 --- a/packages/docs-site/src/content/docs/reference/30-wiring-routing.mdx +++ b/packages/docs-site/src/content/docs/reference/30-wiring-routing.mdx @@ -213,20 +213,20 @@ This makes `alias` the perfect tool for pre-sanitizing messy data or breaking co ```bridge # 1. Rename a deep, cumbersome path -alias api.response.data.metadata.address as addr +alias addr <- api.response.data.metadata.address # 2. Pre-sanitize a fragile data lookup with a fallback -alias api.user?.nickname || "Guest" as displayName +alias displayName <- api.user?.nickname || "Guest" # 3. Safely chain a primitive tool -alias riskyMathTool catch 0 as safeMathResult +alias safeMathResult <- riskyMathTool catch 0 # 4. Inline math/comparison expression -alias api.price * 1.2 as priceWithTax -alias api.score >= 80 as isPassing +alias priceWithTax <- api.price * 1.2 +alias isPassing <- api.score >= 80 # 5. Parenthesized expression -alias (api.a + api.b) * api.rate as total +alias total <- (api.a + api.b) * api.rate # Route the sanitized local variables downstream! o.zip <- addr.zipCode diff --git a/packages/docs-site/src/content/docs/reference/40-using-tools-pipes.mdx b/packages/docs-site/src/content/docs/reference/40-using-tools-pipes.mdx index e935171b..499c487c 100644 --- a/packages/docs-site/src/content/docs/reference/40-using-tools-pipes.mdx +++ b/packages/docs-site/src/content/docs/reference/40-using-tools-pipes.mdx @@ -144,7 +144,7 @@ bridge Query.formatNames { Because pipes fork the tool, the engine runs them entirely in parallel. For pure functions like `upperCase`, this is perfectly fine. But if you are piping data into an HTTP API tool, **you are firing multiple independent network requests**. -## 4. Node Aliasing (`alias ... as`) +## 4. Node Aliasing (`alias name <- source`) Because the pipe operator always forks a new node, what happens if you pipe data through an API, but need to read multiple different fields from the resulting object? @@ -157,7 +157,7 @@ bridge Query.getUser { with output as o # Evaluate the tool ONCE, safely catch network errors, and cache as 'user' - alias userApi:i.userId catch {} as user + alias user <- userApi:i.userId catch {} # Read from the cached node (cost-free memory reads) o.city <- user.city diff --git a/packages/docs-site/src/content/docs/reference/70-array-mapping.mdx b/packages/docs-site/src/content/docs/reference/70-array-mapping.mdx index e185aba1..0dca56dd 100644 --- a/packages/docs-site/src/content/docs/reference/70-array-mapping.mdx +++ b/packages/docs-site/src/content/docs/reference/70-array-mapping.mdx @@ -75,7 +75,7 @@ You can also use aliases inside loops purely for readability, without triggering o.list <- api.items[] as it { # Bind a deep sub-object to a cleaner name - alias it.metadata.authorInfo as author + alias author <- it.metadata.authorInfo .name <- author.name .role <- author.role @@ -163,11 +163,11 @@ bridge Query.getActiveAdmins { filter.active = true # usage as pipe - # alias filter:ctx.users as final + # alias final <- filter:ctx.users # usage as tool node filter.in <- ctx.users - alias filter as final + alias final <- filter o <- final[] as user { .id <- user.id diff --git a/packages/docs-site/src/content/docs/reference/summary.mdx b/packages/docs-site/src/content/docs/reference/summary.mdx index 7f5874e5..8b67e626 100644 --- a/packages/docs-site/src/content/docs/reference/summary.mdx +++ b/packages/docs-site/src/content/docs/reference/summary.mdx @@ -27,9 +27,9 @@ _Tools for data manipulation and code organization._ | Feature | Example | | ---------------------------- | -------------------------------------------------------- | -| **`alias` declarations** | `alias api.result.data as d` | -| **Safe-exec aliases** (`?.`) | `alias api?.value as safeVal` | -| **Alias fallbacks** | `alias (expr) ? val : null ?? panic "msg" as x` | +| **`alias` declarations** | `alias d <- api.result.data` | +| **Safe-exec aliases** (`?.`) | `alias safeVal <- api?.value` | +| **Alias fallbacks** | `alias x <- (expr) ? val : null ?? panic "msg"` | | **Math / Comparison** | `o.total <- i.price * i.qty`, `o.isAdult <- i.age >= 18` | | **String interpolation** | `o.msg <- "Hello, {i.name}!"` | | **Pipe operators** | `o.loud <- tu:i.text` | diff --git a/packages/docs-site/src/content/docs/tools/array-tools.mdx b/packages/docs-site/src/content/docs/tools/array-tools.mdx index 8bfbdb0c..be53a84d 100644 --- a/packages/docs-site/src/content/docs/tools/array-tools.mdx +++ b/packages/docs-site/src/content/docs/tools/array-tools.mdx @@ -123,7 +123,7 @@ bridge Query.getTags { # Whether api.tags is a string or an array of strings, # 'safeTags' is guaranteed to be an array. - alias toArr:api.tags as safeTags + alias safeTags <- toArr:api.tags o.tags <- safeTags[] as tag { .name <- tag diff --git a/packages/playground/src/examples.ts b/packages/playground/src/examples.ts index db7fc401..3ffe7c43 100644 --- a/packages/playground/src/examples.ts +++ b/packages/playground/src/examples.ts @@ -569,22 +569,22 @@ bridge Query.profile { with output as o # 1. Simple rename — give a deeply nested path a short name - alias api.address.city as city + alias city <- api.address.city # 2. Falsy fallback — use "Anonymous" if username is empty or null - alias api.username || "Anonymous" as displayName + alias displayName <- api.username || "Anonymous" # 3. Nullish fallback — only override if value is strictly null/undefined - alias api.website ?? "https://example.com" as site + alias site <- api.website ?? "https://example.com" # 4. Mixed chain — ?? then || in any order - alias api.nickname ?? api.username || "Guest" as greeting + alias greeting <- api.nickname ?? api.username || "Guest" # 4. Error boundary — if the pipe tool throws, default to "UNKNOWN" - alias uc:api.name catch "UNKNOWN" as upperName + alias upperName <- uc:api.name catch "UNKNOWN" # 5. Math/comparison expression — alias fully evaluates the expression - alias api.id <= 5 as isPremium + alias isPremium <- api.id <= 5 o.displayName <- displayName o.location <- city || "Unknown city" @@ -1101,7 +1101,7 @@ define formatProfile { with output as o with std.str.toUpperCase as uc - alias "{i.first} {i.last}" as fullName + alias fullName <- "{i.first} {i.last}" o.displayName <- fullName o.locationLabel <- "Based in {i.city}" @@ -1407,11 +1407,11 @@ bridge Query.activeAdmins { filter.active = true # usage as pipe - # alias filter:ctx.users as final + # alias final <- filter:ctx.users # usage as tool node filter.in <- ctx.users - alias filter as final + alias final <- filter o <- final[] as user { .id <- user.id From 2a1ee3e4ecfc301095284b4eb6abf1721adc4415 Mon Sep 17 00:00:00 2001 From: Aarne Laur Date: Sun, 15 Mar 2026 10:27:16 +0100 Subject: [PATCH 2/3] Syntax change --- packages/bridge-compiler/src/codegen.ts | 2 +- packages/bridge-parser/src/bridge-format.ts | 16 +- packages/bridge-parser/src/parser/parser.ts | 269 ++++++++++++++++-- .../test/source-locations.test.ts | 2 +- .../syntaxes/bridge.tmLanguage.json | 9 +- packages/bridge/test/alias.test.ts | 31 +- .../playground/src/codemirror/bridge-lang.ts | 39 +-- scripts/profile-target.mjs | 2 +- 8 files changed, 287 insertions(+), 83 deletions(-) diff --git a/packages/bridge-compiler/src/codegen.ts b/packages/bridge-compiler/src/codegen.ts index 49eb5ff2..02eb0755 100644 --- a/packages/bridge-compiler/src/codegen.ts +++ b/packages/bridge-compiler/src/codegen.ts @@ -3454,7 +3454,7 @@ class CodegenContext { return this.elementWireToExpr(w, elVar); } } - // Check if this is a pipe tool call (alias tool:source as name) + // Check if this is a pipe tool call (alias name <- tool:source) if (isPull(w) && w.pipe) { return this.elementWireToExpr(w, elVar); } diff --git a/packages/bridge-parser/src/bridge-format.ts b/packages/bridge-parser/src/bridge-format.ts index 485f1d9f..a21e0376 100644 --- a/packages/bridge-parser/src/bridge-format.ts +++ b/packages/bridge-parser/src/bridge-format.ts @@ -1650,7 +1650,7 @@ function serializeBridgeBlock(bridge: Bridge): string { } } - // Emit block-scoped local bindings: alias as + // Emit block-scoped local bindings: alias <- for (const [alias, info] of localBindingsByAlias) { // Ternary alias in element scope if (info.ternaryWire) { @@ -1679,7 +1679,7 @@ function serializeBridgeBlock(bridge: Bridge): string { const fallbackStr = serFallbacks(tw, sPipeOrRef); const errf = serCatch(tw, sPipeOrRef); lines.push( - `${indent}alias ${condStr} ? ${thenStr} : ${elseStr}${fallbackStr}${errf} as ${alias}`, + `${indent}alias ${alias} <- ${condStr} ? ${thenStr} : ${elseStr}${fallbackStr}${errf}`, ); continue; } @@ -1769,7 +1769,11 @@ function serializeBridgeBlock(bridge: Bridge): string { sourcePart = sRef(fromRef, true); } } - lines.push(`${indent}alias ${sourcePart} as ${alias}`); + const elemFb = serFallbacks(srcWire, sPipeOrRef); + const elemErrf = serCatch(srcWire, sPipeOrRef); + lines.push( + `${indent}alias ${alias} <- ${sourcePart}${elemFb}${elemErrf}`, + ); } // Emit element-scoped tool declarations: with as @@ -2219,7 +2223,7 @@ function serializeBridgeBlock(bridge: Bridge): string { } // ── Top-level alias declarations ───────────────────────────────────── - // Emit `alias as ` for __local bindings that are NOT + // Emit `alias <- ` for __local bindings that are NOT // element-scoped (those are handled inside serializeArrayElements). for (const [alias, info] of localBindingsByAlias) { // Ternary alias: emit `alias ? : [fallbacks] as ` @@ -2237,7 +2241,7 @@ function serializeBridgeBlock(bridge: Bridge): string { const fallbackStr = serFallbacks(tw, sPipeOrRef); const errf = serCatch(tw, sPipeOrRef); lines.push( - `alias ${condStr} ? ${thenStr} : ${elseStr}${fallbackStr}${errf} as ${alias}`, + `alias ${alias} <- ${condStr} ? ${thenStr} : ${elseStr}${fallbackStr}${errf}`, ); continue; } @@ -2294,7 +2298,7 @@ function serializeBridgeBlock(bridge: Bridge): string { } const aliasFb = serFallbacks(srcWire, sPipeOrRef); const aliasErrf = serCatch(srcWire, sPipeOrRef); - lines.push(`alias ${sourcePart}${aliasFb}${aliasErrf} as ${alias}`); + lines.push(`alias ${alias} <- ${sourcePart}${aliasFb}${aliasErrf}`); } // Also emit wires reading from top-level __local bindings for (const lw of localReadWires) { diff --git a/packages/bridge-parser/src/parser/parser.ts b/packages/bridge-parser/src/parser/parser.ts index e608d693..932082f8 100644 --- a/packages/bridge-parser/src/parser/parser.ts +++ b/packages/bridge-parser/src/parser/parser.ts @@ -240,16 +240,19 @@ class BridgeParser extends CstParser { /** * Node alias at bridge body level: - * alias as + * alias <- * * Creates a local __local binding that caches the result of the source * expression. Subsequent wires can reference the alias as a handle. + * Uses the same wire RHS syntax as regular pull wires. */ public bridgeNodeAlias = this.RULE("bridgeNodeAlias", () => { this.CONSUME(AliasKw); + this.SUBRULE(this.nameToken, { LABEL: "nodeAliasName" }); + this.CONSUME(Arrow, { LABEL: "aliasArrow" }); this.OR([ { - // String literal as source: alias "..." [op operand]* [? then : else] as name + // String literal as source: alias name <- "..." [op operand]* ALT: () => { this.CONSUME(StringLiteral, { LABEL: "aliasStringSource" }); // Optional expression chain after string literal @@ -271,7 +274,7 @@ class BridgeParser extends CstParser { }, }, { - // [not] (parenExpr | sourceExpr) [op operand]* [? then : else] as name + // [not] (parenExpr | sourceExpr) [op operand]* [? then : else] ALT: () => { this.OPTION3(() => { this.CONSUME(NotKw, { LABEL: "aliasNotPrefix" }); @@ -303,6 +306,8 @@ class BridgeParser extends CstParser { }, }, ]); + // Optional array mapping: [] as { ... } + this.OPTION(() => this.SUBRULE(this.arrayMapping)); // || / ?? coalesce chain (mixed order) this.MANY(() => { this.SUBRULE4(this.coalesceChainItem, { LABEL: "aliasCoalesceItem" }); @@ -312,8 +317,6 @@ class BridgeParser extends CstParser { this.CONSUME(CatchKw); this.SUBRULE3(this.coalesceAlternative, { LABEL: "aliasCatchAlt" }); }); - this.CONSUME(AsKw); - this.SUBRULE(this.nameToken, { LABEL: "nodeAliasName" }); }); /** force [?? null] */ @@ -471,7 +474,7 @@ class BridgeParser extends CstParser { }, }, { - // Path scoping block: target { lines: .field <- source, .field = value, .field { ... }, alias ... as ..., ...source } + // Path scoping block: target { lines: .field <- source, .field = value, .field { ... }, alias name <- ..., ...source } ALT: () => { this.CONSUME(LCurly, { LABEL: "scopeBlock" }); this.MANY3(() => @@ -510,14 +513,23 @@ class BridgeParser extends CstParser { /** * Block-scoped binding inside array mapping: - * alias as + * alias <- [|| alt]* [catch fallback] * Evaluates the source once per element and binds the result to . */ public elementWithDecl = this.RULE("elementWithDecl", () => { this.CONSUME(AliasKw); - this.SUBRULE(this.sourceExpr, { LABEL: "elemWithSource" }); - this.CONSUME(AsKw); this.SUBRULE(this.nameToken, { LABEL: "elemWithAlias" }); + this.CONSUME(Arrow, { LABEL: "elemArrow" }); + this.SUBRULE(this.sourceExpr, { LABEL: "elemWithSource" }); + // || / ?? coalesce chain (mixed order) + this.MANY(() => { + this.SUBRULE(this.coalesceChainItem, { LABEL: "elemCoalesceItem" }); + }); + // catch error fallback + this.OPTION(() => { + this.CONSUME(CatchKw); + this.SUBRULE(this.coalesceAlternative, { LABEL: "elemCatchAlt" }); + }); }); /** @@ -3652,10 +3664,30 @@ function buildBridgeBody( field: alias, path: [], }; - wires.push({ + + // Process coalesce and catch from elementWithDecl + const elemFallbacks: WireSourceEntry[] = []; + for (const item of subs(withDecl, "elemCoalesceItem")) { + const type = tok(item, "falsyOp") + ? ("falsy" as const) + : ("nullish" as const); + const altNode = sub(item, "altValue")!; + const altResult = extractCoalesceAlt(altNode, lineNum, iterScope); + elemFallbacks.push(buildSourceEntry(type, altNode, altResult)); + } + let elemCatch: WireCatch | undefined; + const elemCatchAlt = sub(withDecl, "elemCatchAlt"); + if (elemCatchAlt) { + const altResult = extractCoalesceAlt(elemCatchAlt, lineNum, iterScope); + elemCatch = buildCatchHandler(elemCatchAlt, altResult); + } + + const wire: Wire = { to: localToRef, - sources: [{ expr: { type: "ref", ref: sourceRef } }], - }); + sources: [{ expr: { type: "ref", ref: sourceRef } }, ...elemFallbacks], + }; + if (elemCatch) wire.catch = elemCatch; + wires.push(wire); } return () => { for (const [alias, previous] of shadowedAliases) { @@ -5417,7 +5449,7 @@ function buildBridgeBody( nodeAliasNode.children.aliasStringSource as IToken[] | undefined )?.[0]; if (aliasStringToken) { - // String literal source: alias "template..." [op right]* as name + // String literal source: alias name <- "template..." [op right]* const raw = aliasStringToken.image.slice(1, -1); const segs = parseTemplateString(raw); const stringExprOps = subs(nodeAliasNode, "aliasStringExprOp"); @@ -5445,7 +5477,7 @@ function buildBridgeBody( sourceRef = strRef; } sourceLoc = aliasLoc; - // Ternary after string source (e.g. alias "a" == "b" ? x : y as name) + // Ternary after string source (e.g. alias name <- "a" == "b" ? x : y) const strTernaryOp = ( nodeAliasNode.children.aliasStringTernaryOp as IToken[] | undefined )?.[0]; @@ -5672,6 +5704,77 @@ function buildBridgeBody( field: alias, path: [], }; + + // ── Array mapping on alias: alias name <- source[] as it { ... } ── + const aliasArrayMapping = ( + nodeAliasNode.children.arrayMapping as CstNode[] | undefined + )?.[0]; + if (aliasArrayMapping) { + wires.push( + withLoc( + { + to: localToRef, + sources: [ + { + expr: { + type: "ref", + ref: sourceRef, + ...(aliasSafe ? { safe: true } : {}), + ...(sourceLoc ? { refLoc: sourceLoc } : {}), + }, + }, + ...aliasFallbacks, + ], + ...(aliasCatchHandler ? { catch: aliasCatchHandler } : {}), + }, + aliasLoc, + ), + ); + wires.push(...aliasFallbackInternalWires); + wires.push(...aliasCatchInternalWires); + + const iterName = extractNameToken(sub(aliasArrayMapping, "iterName")!); + assertNotReserved(iterName, lineNum, "iterator handle"); + const arrayToPath = localToRef.path; + arrayIterators[arrayToPath.join(".")] = iterName; + + const elemWithDecls = subs(aliasArrayMapping, "elementWithDecl"); + const elemToolWithDecls = subs( + aliasArrayMapping, + "elementToolWithDecl", + ); + const { writableHandles, cleanup: toolCleanup } = + processLocalToolBindings(elemToolWithDecls); + const aliasElemCleanup = processLocalBindings(elemWithDecls, iterName); + processElementHandleWires( + subs(aliasArrayMapping, "elementHandleWire"), + iterName, + writableHandles, + ); + processElementLines( + subs(aliasArrayMapping, "elementLine"), + arrayToPath, + iterName, + bridgeType, + bridgeField, + wires, + arrayIterators, + buildSourceExpr, + extractCoalesceAlt, + desugarExprChain, + extractTernaryBranch, + processLocalBindings, + processLocalToolBindings, + processElementHandleWires, + desugarTemplateString, + desugarNot, + resolveParenExpr, + ); + aliasElemCleanup(); + toolCleanup(); + continue; + } + wires.push( withLoc( { @@ -5734,10 +5837,12 @@ function buildBridgeBody( // ── Path scoping block: target { .field ... } ── if (wc.scopeBlock) { // Process alias declarations inside the scope block first + // Scope aliases use the same bridgeNodeAlias rule — process with full + // expression support (coalesce, catch, ternary, string templates, etc.) const scopeAliases = subs(wireNode, "scopeAlias"); for (const aliasNode of scopeAliases) { const aliasLineNum = line(findFirstToken(aliasNode)); - const sourceNode = sub(aliasNode, "nodeAliasSource")!; + const aliasNodeLoc = locFromNode(aliasNode); const alias = extractNameToken(sub(aliasNode, "nodeAliasName")!); assertNotReserved(alias, aliasLineNum, "node alias"); if (handleRes.has(alias)) { @@ -5745,10 +5850,125 @@ function buildBridgeBody( `Line ${aliasLineNum}: Duplicate handle name "${alias}"`, ); } - const { ref: sourceRef, safe: aliasSafe } = buildSourceExprSafe( - sourceNode, - aliasLineNum, - ); + + // Extract coalesce modifiers + const scopeAliasFallbacks: WireSourceEntry[] = []; + const scopeAliasFbWires: Wire[] = []; + for (const item of subs(aliasNode, "aliasCoalesceItem")) { + const type = tok(item, "falsyOp") + ? ("falsy" as const) + : ("nullish" as const); + const altNode = sub(item, "altValue")!; + const preLen = wires.length; + const altResult = extractCoalesceAlt(altNode, aliasLineNum); + scopeAliasFallbacks.push(buildSourceEntry(type, altNode, altResult)); + if ("sourceRef" in altResult) { + scopeAliasFbWires.push(...wires.splice(preLen)); + } + } + let scopeAliasCatch: WireCatch | undefined; + let scopeAliasCatchWires: Wire[] = []; + const scopeCatchAlt = sub(aliasNode, "aliasCatchAlt"); + if (scopeCatchAlt) { + const preLen = wires.length; + const altResult = extractCoalesceAlt(scopeCatchAlt, aliasLineNum); + scopeAliasCatch = buildCatchHandler(scopeCatchAlt, altResult); + if ("sourceRef" in altResult) { + scopeAliasCatchWires = wires.splice(preLen); + } + } + + // Compute source ref (same logic as top-level alias) + const scopeStringToken = ( + aliasNode.children.aliasStringSource as IToken[] | undefined + )?.[0]; + let scopeSrcRef: NodeRef; + let scopeSrcLoc: SourceLocation | undefined; + let scopeSafe: boolean | undefined; + + if (scopeStringToken) { + const raw = scopeStringToken.image.slice(1, -1); + const segs = parseTemplateString(raw); + const strRef = segs + ? desugarTemplateString(segs, aliasLineNum, undefined, aliasNodeLoc) + : desugarTemplateString( + [{ kind: "text", value: raw }], + aliasLineNum, + undefined, + aliasNodeLoc, + ); + const strOps = subs(aliasNode, "aliasStringExprOp"); + if (strOps.length > 0) { + scopeSrcRef = desugarExprChain( + strRef, + strOps, + subs(aliasNode, "aliasStringExprRight"), + aliasLineNum, + undefined, + undefined, + aliasNodeLoc, + ); + } else { + scopeSrcRef = strRef; + } + scopeSrcLoc = aliasNodeLoc; + scopeSafe = false; + } else { + const fpn = sub(aliasNode, "aliasFirstParen"); + const fsn = sub(aliasNode, "nodeAliasSource"); + const hn = fsn ? sub(fsn, "head") : undefined; + const safe = hn ? !!extractAddressPath(hn).rootSafe : false; + const ops = subs(aliasNode, "aliasExprOp"); + const rights = subs(aliasNode, "aliasExprRight"); + scopeSrcLoc = locFromNodeRange( + fpn ?? fsn, + rights[rights.length - 1] ?? fpn ?? fsn, + ); + + if (fpn) { + const pr = resolveParenExpr(fpn, aliasLineNum, undefined, safe); + scopeSrcRef = + ops.length > 0 + ? desugarExprChain( + pr, + ops, + rights, + aliasLineNum, + undefined, + safe, + aliasNodeLoc, + ) + : pr; + } else if (ops.length > 0) { + scopeSrcRef = desugarExprChain( + buildSourceExpr(fsn!, aliasLineNum), + ops, + rights, + aliasLineNum, + undefined, + safe, + aliasNodeLoc, + ); + } else { + const r = buildSourceExprSafe(fsn!, aliasLineNum); + scopeSrcRef = r.ref; + scopeSafe = r.safe; + } + + if ( + (aliasNode.children.aliasNotPrefix as IToken[] | undefined)?.[0] + ) { + scopeSrcRef = desugarNot( + scopeSrcRef, + aliasLineNum, + safe, + aliasNodeLoc, + ); + } + + if (scopeSafe === undefined) scopeSafe = safe || undefined; + } + const localRes: HandleResolution = { module: "__local", type: "Shadow", @@ -5769,15 +5989,20 @@ function buildBridgeBody( { expr: { type: "ref", - ref: sourceRef, - ...(aliasSafe ? { safe: true as const } : {}), + ref: scopeSrcRef, + ...(scopeSafe ? { safe: true as const } : {}), + ...(scopeSrcLoc ? { refLoc: scopeSrcLoc } : {}), }, }, + ...scopeAliasFallbacks, ], + ...(scopeAliasCatch ? { catch: scopeAliasCatch } : {}), }, - locFromNode(aliasNode), + aliasNodeLoc, ), ); + wires.push(...scopeAliasFbWires); + wires.push(...scopeAliasCatchWires); } const scopeLines = subs(wireNode, "pathScopeLine"); // Process spread lines inside the scope block: ...sourceExpr diff --git a/packages/bridge-parser/test/source-locations.test.ts b/packages/bridge-parser/test/source-locations.test.ts index 1633a33b..b8013eb0 100644 --- a/packages/bridge-parser/test/source-locations.test.ts +++ b/packages/bridge-parser/test/source-locations.test.ts @@ -103,7 +103,7 @@ describe("parser source locations", () => { const aliasWire = instr.wires.find((wire) => wire.to.field === "clean"); assert.ok(aliasWire?.catch); assert.equal(aliasWire.catch.loc?.startLine, 5); - assert.equal(aliasWire.catch.loc?.startColumn, 45); + assert.equal(aliasWire.catch.loc?.startColumn, 44); const messageWire = instr.wires.find( (wire) => wire.to.path.join(".") === "message", diff --git a/packages/bridge-syntax-highlight/syntaxes/bridge.tmLanguage.json b/packages/bridge-syntax-highlight/syntaxes/bridge.tmLanguage.json index fd55b7aa..5b71a5d4 100644 --- a/packages/bridge-syntax-highlight/syntaxes/bridge.tmLanguage.json +++ b/packages/bridge-syntax-highlight/syntaxes/bridge.tmLanguage.json @@ -141,13 +141,12 @@ } }, { - "comment": "alias as (node alias / rename)", - "match": "^\\s*(alias)\\s+(.+?)\\s+(as)\\s+([A-Za-z_][A-Za-z0-9_]*)", + "comment": "alias <- (node alias / rename)", + "match": "^\\s*(alias)\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*(<-)", "captures": { "1": { "name": "keyword.control.bridge" }, - "2": { "name": "entity.name.function.bridge" }, - "3": { "name": "keyword.control.bridge" }, - "4": { "name": "variable.other.handle.bridge" } + "2": { "name": "variable.other.handle.bridge" }, + "3": { "name": "keyword.operator.wire.bridge" } } }, { diff --git a/packages/bridge/test/alias.test.ts b/packages/bridge/test/alias.test.ts index 5c962137..32888f18 100644 --- a/packages/bridge/test/alias.test.ts +++ b/packages/bridge/test/alias.test.ts @@ -14,44 +14,15 @@ regressionTest("alias keyword", { bridge: bridge` version 1.5 - define reusable { - with inout as i - with output as o - - alias temp <- i.value || "Default" catch "Error" - - o.result <- temp - } - bridge Alias.syntax { - with test.multitool as array with test.multitool as object - with reusable as r - with context with input as i with output as o - # The usual complex reusable alias + # Simple alias with fallback and catch alias user_info <- object?.user.info || i.info catch "Unknown" - # Alias can store the array mapping result - alias raw_response <- array[] as i { - - alias in_loop <- i.name || "No name" - - .name <- in_loop - .info <- user_info - } - - # Alias can be used in alias - alias safe_response <- raw_response catch [] - - o.names <- safe_response o.info <- user_info - - # Alias used inside the define block - r.value <- i.info - o.reusable_result <- r.result } `, tools: tools, diff --git a/packages/playground/src/codemirror/bridge-lang.ts b/packages/playground/src/codemirror/bridge-lang.ts index 1a28adcc..9ff0a1e5 100644 --- a/packages/playground/src/codemirror/bridge-lang.ts +++ b/packages/playground/src/codemirror/bridge-lang.ts @@ -127,7 +127,25 @@ function handleExpect( state.expect = ""; return undefined; - // alias SOURCE (pipe:handle.path or handle.path) + // alias NAME + case "aliasName": + if (stream.match(/^[A-Za-z_]\w*/)) { + state.expect = "aliasArrow"; + return "def"; + } + state.expect = ""; + return undefined; + + // alias name <- + case "aliasArrow": + if (stream.match(/^<-/)) { + state.expect = "aliasSource"; + return "operator"; + } + state.expect = ""; + return undefined; + + // alias name <- SOURCE (pipe:handle.path or handle.path) case "aliasSource": if (stream.match(/^[A-Za-z_][\w.]*/)) { state.expect = "aliasColon"; @@ -136,12 +154,8 @@ function handleExpect( state.expect = ""; return undefined; - // alias source : (pipe separator — optional, loop back) + // alias name <- source : (pipe separator — optional, loop back) case "aliasColon": - if (stream.match(/^as\b/)) { - state.expect = "aliasName"; - return "keyword"; - } if (stream.eat(":")) { state.expect = "aliasSource"; return "operator"; @@ -153,7 +167,7 @@ function handleExpect( state.expect = ""; return undefined; - // alias source. FIELD + // alias name <- source. FIELD case "aliasDotField": if (stream.match(/^[A-Za-z_]\w*/)) { state.expect = "aliasColon"; @@ -162,15 +176,6 @@ function handleExpect( state.expect = ""; return undefined; - // alias source as NAME - case "aliasName": - if (stream.match(/^[A-Za-z_]\w*/)) { - state.expect = ""; - return "def"; - } - state.expect = ""; - return undefined; - // force HANDLE case "forceTarget": if (stream.match(/^[A-Za-z_]\w*/)) { @@ -305,7 +310,7 @@ function token(stream: StringStream, state: State): string | null { return "keyword"; } if (stream.match(/^alias\b/)) { - state.expect = "aliasSource"; + state.expect = "aliasName"; state.lineStart = false; return "keyword"; } diff --git a/scripts/profile-target.mjs b/scripts/profile-target.mjs index e68547e5..da3ff1f2 100644 --- a/scripts/profile-target.mjs +++ b/scripts/profile-target.mjs @@ -248,7 +248,7 @@ bridge Query.enriched { with output as o o <- api.items[] as it { - alias enrich:it as resp + alias resp <- enrich:it .a <- resp.a .b <- resp.b } From 87aa7752cdb0130ed9508c240fdfe076f805006c Mon Sep 17 00:00:00 2001 From: Aarne Laur Date: Sun, 15 Mar 2026 13:22:21 +0100 Subject: [PATCH 3/3] Changeset --- .changeset/sharp-badgers-switch.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/sharp-badgers-switch.md diff --git a/.changeset/sharp-badgers-switch.md b/.changeset/sharp-badgers-switch.md new file mode 100644 index 00000000..a8c1bef9 --- /dev/null +++ b/.changeset/sharp-badgers-switch.md @@ -0,0 +1,8 @@ +--- +"@stackables/bridge-compiler": minor +"@stackables/bridge-parser": minor +"@stackables/bridge-core": minor +"@stackables/bridge": minor +--- + +New alias syntax alias name <- source.from ?? "full syntax" catch "supported"