mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
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:
co-authored by
xXCrash2BomberXx
parent
d7b4480e47
commit
cb8f16b66f
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user