Fence unsupported Array.from elements

- Share the backend-supported array-element contract across type mapping, callback producers, and IR validation
- Diagnose or defer unsupported Array.from mapper results before emission
- Cover the JavaScript runtime-fence path that previously crashed the C emitter

Co-authored-by: xXCrash2BomberXx <91439080+xXCrash2BomberXx@users.noreply.github.com>
This commit is contained in:
Chris Tate
2026-08-20 17:38:21 -05:00
co-authored by xXCrash2BomberXx
parent d7b4480e47
commit cb8f16b66f
5 changed files with 85 additions and 66 deletions
@@ -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);
+6 -32
View File
@@ -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
+29
View File
@@ -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 };
}
+7 -1
View File
@@ -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;
+19
View File
@@ -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