From ff73cdce662c2a50f7bfe8bcbaeb847d358b8439 Mon Sep 17 00:00:00 2001 From: Max B Date: Sun, 23 Aug 2026 21:32:47 +0200 Subject: [PATCH] fix(dynamic): lower globalThis, JSVAL property access, and "in" on JSVAL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three gaps where --dynamic mode expressions involving the global object or JSVAL-typed receivers produced SC errors or incorrect lowering. 1. **`globalThis` as JSVAL** (`lower-exprs.ts`): in --dynamic mode, referencing `globalThis` now returns a JSVAL via `jsOp/globalGet` instead of falling through to the SC1090 global-hints fence. Property accesses (`globalThis.window`, `globalThis.__vibeXxx`) then flow through as JSVAL member reads. Without this, feature-detection idioms like `"window" in globalThis` caused a compile error. 2. **Property access on a JSVAL receiver** (`lower-exprs.ts`): a property access whose receiver lowered to JSVAL (e.g. `(await import("excluded-module")).tools` — the import was stubbed to `Promise.resolve({})`) now emits a `jsOp/getProp` instead of falling through to the union-receiver paths and crashing or producing a spurious error. Property access on JSVAL is always valid — the value is an opaque engine handle. 3. **`"key" in jsval`** (`lower-exprs.ts`): the `in` operator on a JSVAL receiver (e.g. `"window" in globalThis` where `globalThis` is JSVAL in --dynamic mode) now emits a JS-engine property-existence check (`getProp(recv, key) !== undefined`) instead of falling through to the union/fence path. This correctly handles all feature-detection patterns at runtime. --- .../src/frontend/lowering/lower-exprs.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index b0f1f0973..36d2c0987 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -961,6 +961,12 @@ function lowerExprInner(L: Lowerer, expr: ts.Expression): IrExpr { const canonical = stdlibGlobalNameOf(L, expr) ?? expr.text; return { kind: "strLit", value: `[builtin ${canonical}]`, type: STRING, loc }; } + // In --dynamic mode, `globalThis` is available as a JSVAL via the + // engine's global object. Property accesses (globalThis.window, + // globalThis.__vibeXxx) then flow through as JSVAL member reads. + if (L.dynamic && expr.text === "globalThis") { + return { kind: "jsOp", op: "globalGet", name: "globalThis", args: [], type: JSVAL, loc }; + } // The families with a WHY: each hint states what makes the // surface genuinely non-static (or what to use instead). const globalHints: Record = { @@ -1844,6 +1850,13 @@ function lowerExprInner(L: Lowerer, expr: ts.Expression): IrExpr { expr, ); } + // The lowered receiver is JSVAL (e.g. `(await import("excluded-module")).tools` + // where the dynamic import was stubbed to Promise.resolve({}) by lowerOwnModuleImport, + // or any other case where a TypeScript-typed expression lowered to the JS engine island). + // Property access on JSVAL is a native engine getProp — always valid. + if (recvLowered.type.kind === "jsval") { + return { kind: "jsOp", op: "getProp", name: expr.name.text, args: [recvLowered], type: JSVAL, loc }; + } // The lowered receiver is a RECORD the checker spelled wider — // `s.match(re).groups.key` in a JS file: the checker says // `{ [key: string]: string } | undefined`, but the groups @@ -9292,6 +9305,16 @@ export function lowerBinary(L: Lowerer, expr: ts.BinaryExpression): IrExpr { if (recv.type.kind === "dyn") { return { kind: "dynHasKey", key, value: recv, type: BOOL, loc }; } + // `"key" in globalThis` (or any JSVAL receiver) — the globalThis object is a JSVAL in + // --dynamic mode. Emit a JS-engine property existence check: getProp(recv, key) !== undefined. + // This correctly handles `"window" in globalThis` (false on server, true on browser), + // `"document" in globalThis`, and similar feature-detection patterns. + if (recv.type.kind === "jsval") { + const prop: IrExpr = { kind: "jsOp", op: "getProp", name: key, args: [recv], type: JSVAL, loc }; + const undef: IrExpr = { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }; + const marshaledUndef: IrExpr = { kind: "jsMarshal", value: undef, type: JSVAL, loc }; + return { kind: "jsOp", op: "neq", args: [prop, marshaledUndef], type: BOOL, loc }; + } // `"k" in u` over a UNION whose arms are FIXED record shapes: every // arm answers membership STATICALLY (a declared non-optional field is // always present, an undeclared name never is), so the whole test