diff --git a/packages/compiler/src/frontend/lowering/lower-containers.ts b/packages/compiler/src/frontend/lowering/lower-containers.ts index 5eb3008ca..54a641171 100644 --- a/packages/compiler/src/frontend/lowering/lower-containers.ts +++ b/packages/compiler/src/frontend/lowering/lower-containers.ts @@ -4,7 +4,7 @@ * method surfaces. */ import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; -import { BOOL, BYTES_U8, CAUGHT, DYN, F64, IrBytesElem, IrBytesIntrinsicMethod, IrExpr, IrFunction, IrLocal, IrMapIntrinsicMethod, IrParam, IrRecordShape, IrSetIntrinsicMethod, IrStmt, IrType, JSVAL, STRING, SrcLoc, UNDEFINED_T, VOID, arrayOf, bytesOf, funcOf, isRefCounted, isSupportedIndexValue, isUnitType, typeEquals } from "../../ir/nodes.js"; +import { BOOL, BYTES_U8, CAUGHT, DYN, F64, IrBytesElem, IrBytesIntrinsicMethod, IrExpr, IrFunction, IrLocal, IrMapIntrinsicMethod, IrParam, IrRecordShape, IrSetIntrinsicMethod, IrStmt, IrType, JSVAL, STRING, SrcLoc, UNDEFINED_T, VOID, arrayOf, bytesOf, funcOf, isRefCounted, isSupportedArrayElem, isSupportedIndexValue, isUnitType, typeEquals } from "../../ir/nodes.js"; import { ARRAY_METHODS, MAP_METHODS, SET_COMBINE_METHODS, SET_METHODS, STR_METHODS } from "./surfaces.js"; import { droppableStatic, isRequireMainFilename, lowerDynObjectLiteral, probeLower, pureReemittable } from "./lower-exprs.js"; import { forOfVarTarget, lowerDestructuringAssign } from "./lower-stmts.js"; @@ -105,6 +105,27 @@ function lowerSplitLimitArg(L: Lowerer, node: ts.Expression | undefined, loc: Sr return node ? lowerOptionalDefaultArg(L, node, F64, defaultValue) : defaultValue; } +/** Callback-driven array producers bypass mapType's ordinary T[] mapping: + * the callback is lowered first, then the helper's result array is built + * directly from its IR return type. Fence element kinds ScrArr cannot hold + * before constructing that array type. */ +function fenceProducedArrayElem(L: Lowerer, node: ts.Node, producer: string, elem: IrType): void { + if (elem.kind === "dyn") { + L.unsupported( + "SC1090", + node, + `${producer} with a callback returning 'unknown'-typed values (the result array has no static element type — annotate the callback's return)`, + ); + } + if (!isSupportedArrayElem(elem)) { + L.unsupported( + "SC1090", + node, + `${producer} with a callback returning '${L.fmt(elem)}' values (arrays of this element kind have no representation — store the values individually)`, + ); + } +} + /** Ambient array method calls. `push`/`unshift`/`pop`/`reverse`/ * `indexOf`/`includes`/`join` * lower to arrIntrinsic; the HOF family — `map`/`filter`/`forEach`/ @@ -631,38 +652,7 @@ function lowerSplitLimitArg(L: Lowerer, node: ts.Expression | undefined, loc: Sr // story yet). L.badType(call, L.typeOf(call)); } - if ( - method === "map" && - (fnRet.kind === "map" || fnRet.kind === "set" || fnRet.kind === "url" || - fnRet.kind === "searchParams" || fnRet.kind === "generator" || fnRet.kind === "caught" || - fnRet.kind === "stats" || fnRet.kind === "fileHandle" || fnRet.kind === "spawnRes" || fnRet.kind === "netSocket" || - fnRet.kind === "dgramSocket" || fnRet.kind === "testCtx" || fnRet.kind === "httpReq" || - fnRet.kind === "httpRes" || fnRet.kind === "httpClientReq" || fnRet.kind === "secureCtx" || - fnRet.kind === "fsWatcher" || fnRet.kind === "childStream" || fnRet.kind === "procStream" || - isUnitType(fnRet)) - ) { - // The result would be an array of an element kind ScrArr has no - // home for (mapTypeOf's own array exclusions) — the callback return - // type bypasses that gate, so it is enforced here: a named fence, - // never a mistyped array into the backends. - L.unsupported( - "SC1090", - call, - `'.map()' with a callback returning '${L.fmt(fnRet)}' values (arrays of this element kind have no representation — store the values individually)`, - ); - } - if (method === "map" && fnRet.kind === "dyn") { - // A checked-dynamic callback return would make the result a - // dyn-element STATIC array, which has no backend representation - // (dynFallbackType's rule: an unmappable element makes the WHOLE - // value dyn — but map's fresh array is built here, not by a - // declaration, so the honest answer is the fence). - L.unsupported( - "SC1090", - call, - "'.map()' with a callback returning 'unknown'-typed values (the result array has no static element type — annotate the callback's return)", - ); - } + if (method === "map") fenceProducedArrayElem(L, call, "'.map()'", fnRet); // JS applies ToBoolean to whatever the predicate answers, so a non-bool // result is not an error — the filter loop wraps the call in the same // toBool an `if` statement would apply. The island/dyn shapes, whose @@ -2537,6 +2527,7 @@ function filterCond(call: IrExpr, fnRet: IrType, loc: SrcLoc): IrExpr { const fnT = fnArg.type as IrType & { kind: "func" }; const fnRet = fnT.ret; if (fnRet.kind === "void" || fnRet.kind === "func") L.badType(call, L.typeOf(call)); + fenceProducedArrayElem(L, call, "'Array.from({ length }, mapper)'", fnRet); const arity = fnT.params.length; const key = `fromLen:${typeKey(fnRet)}:${arity}`; let helper = L.arrHofHelpers.get(key); diff --git a/packages/compiler/src/frontend/types.ts b/packages/compiler/src/frontend/types.ts index 850dcd16a..754066c57 100644 --- a/packages/compiler/src/frontend/types.ts +++ b/packages/compiler/src/frontend/types.ts @@ -1,6 +1,6 @@ import * as ts from "./ts7/adapter.js"; import type { IrRecordShape, IrType, IrUnionDef } from "../ir/nodes.js"; -import { arrayOf, BOOL, bytesOf, canConvertToDyn, CHILD_T, DATE_T, DYN, F64, funcOf, isSupportedIndexValue, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, JSVAL, mapOf, NULL_T, PROCSTREAM_T, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, setOf, STRING, SYMBOL_T, typeEquals, typeKey, UNDEFINED_T, VOID } from "../ir/nodes.js"; +import { arrayOf, BOOL, bytesOf, canConvertToDyn, CHILD_T, DATE_T, DYN, F64, funcOf, isSupportedArrayElem, isSupportedIndexValue, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, JSVAL, mapOf, NULL_T, PROCSTREAM_T, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, setOf, STRING, SYMBOL_T, typeEquals, typeKey, UNDEFINED_T, VOID } from "../ir/nodes.js"; import { isJsSourceFile, isNodeTypesPath } from "./program.js"; import { accessorSlotProp } from "../ir/nodes.js"; @@ -1118,37 +1118,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // the messages-building pattern (`const content: any[] = []`) — which // keeps its static array-of-handles representation. if (elem?.kind === "jsval" && !(elemTs.flags & ts.TypeFlags.Any)) return JSVAL; - // RegExp elements ride REF (scr_regex_retain_v/release_v, no trace — - // a regex holds only its bytecode and source): the derived-pattern - // idiom `[bases].map(ps => new RegExp(...))` builds real regex - // arrays, elements flow into the regex intrinsics unchanged, and - // indexOf/includes/=== are the REF kind's pointer identity — exactly - // JS object identity for RegExp values. - if ( - !elem || - elem.kind === "void" || - elem.kind === "date" || - elem.kind === "map" || - elem.kind === "set" || - elem.kind === "url" || - elem.kind === "searchParams" || - elem.kind === "stats" || - elem.kind === "fileHandle" || - elem.kind === "spawnRes" || - elem.kind === "netSocket" || - elem.kind === "dgramSocket" || - elem.kind === "testCtx" || - elem.kind === "httpReq" || - elem.kind === "httpRes" || - elem.kind === "httpClientReq" || - elem.kind === "secureCtx" || - elem.kind === "fsWatcher" || - elem.kind === "childStream" || - elem.kind === "procStream" || - elem.kind === "generator" - ) { - return null; // ScrArr has no closure/map/regex/url element kinds yet - } + if (!elem) return null; // A dyn ELEMENT makes the WHOLE array the checked-dynamic value: // `unknown[]`, `object[]`, and the collapsed `(string | object)[]` // (the plugins-slot shape) — the checked-dynamic tree has real arrays, so length/ @@ -1158,6 +1128,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // the mapping itself; construction sites build dynArrLit (the checked-dynamic tree // array literal) and typed sources convert per element at the slot. if (elem.kind === "dyn") return DYN; + // The shared predicate is the runtime/backend storage contract. In + // particular, valid standalone values such as Map/Set/Date and opaque + // handles do not automatically have an array element representation. + if (!isSupportedArrayElem(elem)) return null; // ChildProcess[] (the running-apps list) and Server[] (the [...set] // drain of the auxiliary-server registries): handles are ordinary // refcounted REF elements (their `_v` adapters, no trace — both drop diff --git a/packages/compiler/src/ir/nodes.ts b/packages/compiler/src/ir/nodes.ts index ca48a7ac0..784804e37 100644 --- a/packages/compiler/src/ir/nodes.ts +++ b/packages/compiler/src/ir/nodes.ts @@ -369,6 +369,35 @@ export function isUnitType(t: IrType): boolean { return t.kind === "undefinedT" || t.kind === "nullT"; } +/** Element kinds with a real ScrArr storage/RC representation in BOTH + * backends. Array-producing lowerings can learn their result element from + * a callback rather than through mapType's ordinary T[] gate, so they must + * share this predicate instead of reconstructing an array around an + * otherwise-valid standalone type (Map, Set, dyn, opaque handles, ...). */ +export function isSupportedArrayElem(t: IrType): boolean { + switch (t.kind) { + case "f64": + case "bool": + case "string": + case "array": + case "bytes": + case "record": + case "object": + case "union": + case "func": + case "promise": + case "jsval": + case "regex": + case "child": + case "netServer": + case "symbol": + case "classval": + return true; + default: + return false; + } +} + export function arrayOf(elem: IrType): IrType { return { kind: "array", elem }; } diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 73c43a003..546de47fe 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -18,7 +18,7 @@ import type { IrUnionDef, SrcLoc, } from "./nodes.js"; -import { arrayOf, BOOL, BYTES_U8, bytesOf, canAdaptDynFuncTo, canConvertToDyn, canExitIslandToType, canMarshalIntoIsland, canMarshalTypedFuncIntoIsland, CHILD_T, CHILDSTREAM_T, DATE_T, DGRAMSOCK_T, DYN, DYN_HANDLE_KINDS, F64, ffiClassType, ffiSourceParamTypes, FILEHANDLE_T, FSWATCHER_T, HTTP2SESSION_T, HTTP2STREAM_T, HTTPCLIENTREQ_T, HTTPREQ_T, HTTPRES_T, islandPromisePayloadTag, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isJsonSafeType, isRefCounted, isSupportedIndexValue, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, jsOpResultKind, JSVAL, NETSERVER_T, NETSOCKET_T, PROCSTREAM_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, SEARCH_PARAMS_T, SECURECTX_T, shapeHasAccessorSlots, SPAWNRES_T, STATS_T, STRING, SYMBOL_T, TESTCTX_T, typeEquals, typeKey, unionFuncSetArmsOk, URL_T, VOID } from "./nodes.js"; +import { arrayOf, BOOL, BYTES_U8, bytesOf, canAdaptDynFuncTo, canConvertToDyn, canExitIslandToType, canMarshalIntoIsland, canMarshalTypedFuncIntoIsland, CHILD_T, CHILDSTREAM_T, DATE_T, DGRAMSOCK_T, DYN, DYN_HANDLE_KINDS, F64, ffiClassType, ffiSourceParamTypes, FILEHANDLE_T, FSWATCHER_T, HTTP2SESSION_T, HTTP2STREAM_T, HTTPCLIENTREQ_T, HTTPREQ_T, HTTPRES_T, islandPromisePayloadTag, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isJsonSafeType, isRefCounted, isSupportedArrayElem, isSupportedIndexValue, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, jsOpResultKind, JSVAL, NETSERVER_T, NETSOCKET_T, PROCSTREAM_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, SEARCH_PARAMS_T, SECURECTX_T, shapeHasAccessorSlots, SPAWNRES_T, STATS_T, STRING, SYMBOL_T, TESTCTX_T, typeEquals, typeKey, unionFuncSetArmsOk, URL_T, VOID } from "./nodes.js"; /** Per-method signature for strIntrinsic: `argTypes` lists every argument * position (optional ones included); `minArgs` is how many may be omitted @@ -2314,6 +2314,9 @@ function validateFunction( break; } const elem = e.type.elem; + if (!isSupportedArrayElem(elem)) { + err(`arrayLit with unsupported ${elem.kind} elements (frontend must fence)`, e.loc); + } const spreadSet = new Set(e.spreads ?? []); for (const i of spreadSet) { if (!Number.isInteger(i) || i < 0 || i >= e.elems.length) { @@ -2338,6 +2341,9 @@ function validateFunction( if (!isRefCounted(e.type.elem)) { err(`arrayNewLen with non-refcounted ${e.type.elem.kind} elements (no absent value)`, e.loc); } + if (!isSupportedArrayElem(e.type.elem)) { + err(`arrayNewLen with unsupported ${e.type.elem.kind} elements (frontend must fence)`, e.loc); + } checkExpr(e.length); expectType(e.length, F64, "arrayNewLen length"); break; diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index db1bac6f5..351a50652 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -220,6 +220,25 @@ console.log(two(1, 2, loud())); ); }); + test("Array.from fences an unsupported callback result before emission (JS lane)", async () => { + // Callback-driven producers learn their result element from the + // lowered callback, bypassing mapType's ordinary T[] gate. An empty + // Set in JS maps to dyn here; the frontend must replace the statement + // with the normal JS runtime fence, never send dyn[] to either emitter. + const r = await compileAndRun( + "array-from-length-elements", + `const arr = Array.from({ length: 2 }, () => new Set()); +console.log(arr.length); +`, + "js", + ); + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe(""); + expect(r.stderr).toMatch( + /^Uncaught Error: 'Array\.from\(\{ length \}, mapper\)' with a callback returning 'unknown'-typed values .* \[SC1090 at .*array-from-length-elements\.js:1\]\n$/, + ); + }); + test("extending a property-assigned class ABOVE its assignment is a named deferred fence (JS lane)", async () => { // The property-assigned class-expression family (corpus 2032/2033 // pins what lowers): `exports.O = class extends exports.I {}` with