mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Fix the X509 recursive-type crash and lower the refused array species forms (#2467)
This commit is contained in:
@@ -647,26 +647,27 @@ export function lowerWithArraySpecies(
|
||||
// instance is created (with that length for map, 0 otherwise), then
|
||||
// the method runs to that length and its elements move into the
|
||||
// instance.
|
||||
const bounded =
|
||||
result.kind === "call" &&
|
||||
result.args[0]?.kind === "varRef" &&
|
||||
result.args[0].localId === local.id
|
||||
? boundedArrayHofHelper(lowerer, result.callee)
|
||||
: null;
|
||||
if (bounded === null || result.kind !== "call")
|
||||
const stmts: IrStmt[] = [{ kind: "varDecl", localId: local.id, init: receiver, loc }];
|
||||
let bound: IrLocal | undefined;
|
||||
const rebound = rebuildSpeciesCall(result, local.id, (inner) => {
|
||||
const args: IrExpr[] = [receiverRef];
|
||||
for (const arg of inner.args.slice(1)) {
|
||||
const held = lowerer.declareHiddenLocal("%speciesArgument", arg.type);
|
||||
stmts.push({ kind: "varDecl", localId: held.id, init: arg, loc });
|
||||
args.push({ kind: "varRef", localId: held.id, type: arg.type, loc });
|
||||
}
|
||||
const length = lowerer.declareHiddenLocal("%speciesLength", F64);
|
||||
bound = length;
|
||||
args.push({ kind: "varRef", localId: length.id, type: F64, loc });
|
||||
const helper = boundedArrayHofHelper(lowerer, inner.callee);
|
||||
return helper === null ? null : { ...inner, callee: helper, args };
|
||||
});
|
||||
if (rebound === null || bound === undefined)
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
`'${access.name.text}' in this form when the program declares a class that extends Array`,
|
||||
);
|
||||
const stmts: IrStmt[] = [{ kind: "varDecl", localId: local.id, init: receiver, loc }];
|
||||
const args: IrExpr[] = [receiverRef];
|
||||
for (const arg of result.args.slice(1)) {
|
||||
const held = lowerer.declareHiddenLocal("%speciesArgument", arg.type);
|
||||
stmts.push({ kind: "varDecl", localId: held.id, init: arg, loc });
|
||||
args.push({ kind: "varRef", localId: held.id, type: arg.type, loc });
|
||||
}
|
||||
const bound = lowerer.declareHiddenLocal("%speciesLength", F64);
|
||||
stmts.push({
|
||||
kind: "varDecl",
|
||||
localId: bound.id,
|
||||
@@ -681,7 +682,6 @@ export function lowerWithArraySpecies(
|
||||
loc,
|
||||
});
|
||||
const boundRef: IrExpr = { kind: "varRef", localId: bound.id, type: F64, loc };
|
||||
args.push(boundRef);
|
||||
const instance = lowerer.declareHiddenLocal("%speciesInstance", receiver.type);
|
||||
const length: IrExpr = mode.resultLength
|
||||
? boundRef
|
||||
@@ -706,7 +706,7 @@ export function lowerWithArraySpecies(
|
||||
fn: "array.speciesFinish",
|
||||
args: [
|
||||
{ kind: "varRef", localId: instance.id, type: receiver.type, loc },
|
||||
{ ...result, callee: bounded, args },
|
||||
rebound,
|
||||
{ kind: "boolLit", value: mode.setLength, type: BOOL, loc },
|
||||
],
|
||||
type: result.type,
|
||||
@@ -736,6 +736,31 @@ export function lowerWithArraySpecies(
|
||||
};
|
||||
}
|
||||
|
||||
/** `e` with the `%arr.<method>` helper call over `receiverLocal` rebuilt by
|
||||
* `rebuild`, or null when `e` holds no such call. The helper call is the
|
||||
* one taking the receiver as its first argument; a filter whose callback
|
||||
* narrows converts the filtered array after the helper ran, so the call
|
||||
* can sit behind a wrapper (the checked-narrowing call), which this
|
||||
* rebuild leaves in place. */
|
||||
function rebuildSpeciesCall(
|
||||
e: IrExpr,
|
||||
receiverLocal: string,
|
||||
rebuild: (inner: IrExpr & { kind: "call" }) => IrExpr | null,
|
||||
): IrExpr | null {
|
||||
if (e.kind !== "call") return null;
|
||||
const first = e.args[0];
|
||||
if (first !== undefined && first.kind === "varRef" && first.localId === receiverLocal)
|
||||
return rebuild(e);
|
||||
for (let i = 0; i < e.args.length; i++) {
|
||||
const replaced = rebuildSpeciesCall(e.args[i]!, receiverLocal, rebuild);
|
||||
if (replaced === null) continue;
|
||||
const args = e.args.slice();
|
||||
args[i] = replaced;
|
||||
return { ...e, args };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `C.of(...items)` and `C.from(iterable)` on a class declared `extends
|
||||
* Array` that declares neither: Array.of / Array.from's elements, in an
|
||||
* instance the class constructs (`new C(items.length)` for of, `new C()`
|
||||
|
||||
@@ -62,6 +62,22 @@ export function refuseX509DynValues(lowerer: Lowerer, functions: readonly IrFunc
|
||||
)
|
||||
return;
|
||||
const seen = new Map<string, boolean>();
|
||||
// A type is recursive when it reaches itself without naming a shape or a
|
||||
// union: `type Rec = string | Rec[]` passes through the array, set, map
|
||||
// or promise case below. Those kinds have no id to memoize by, so a type
|
||||
// met again while it is still being visited answers false: what a cycle
|
||||
// adds to `holds` (a certificate exists somewhere in the type) is
|
||||
// reachability, and nothing outside the cycle makes it reachable.
|
||||
const visiting = new Set<IrType>();
|
||||
const descend = (t: IrType, visit: () => boolean): boolean => {
|
||||
if (visiting.has(t)) return false;
|
||||
visiting.add(t);
|
||||
try {
|
||||
return visit();
|
||||
} finally {
|
||||
visiting.delete(t);
|
||||
}
|
||||
};
|
||||
const holds = (t: IrType): boolean => {
|
||||
switch (t.kind) {
|
||||
case "record": {
|
||||
@@ -79,13 +95,24 @@ export function refuseX509DynValues(lowerer: Lowerer, functions: readonly IrFunc
|
||||
}
|
||||
case "array":
|
||||
case "set":
|
||||
return holds(t.elem);
|
||||
return descend(t, () => holds(t.elem));
|
||||
case "map":
|
||||
return holds(t.key) || holds(t.value);
|
||||
return descend(t, () => holds(t.key) || holds(t.value));
|
||||
case "promise":
|
||||
return holds(t.inner);
|
||||
case "union":
|
||||
return lowerer.unions.get(t.unionId)?.arms.some(holds) === true;
|
||||
return descend(t, () => holds(t.inner));
|
||||
case "union": {
|
||||
// Unions have an id, so a cycle that names one is answered from the
|
||||
// memo above; the identity guard covers a cycle that does not.
|
||||
const key = `union:${t.unionId}`;
|
||||
const known = seen.get(key);
|
||||
if (known !== undefined) return known;
|
||||
return descend(t, () => {
|
||||
seen.set(key, false);
|
||||
const result = lowerer.unions.get(t.unionId)?.arms.some(holds) === true;
|
||||
seen.set(key, result);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3382,10 +3382,13 @@ export function lowerTuplePredicateCall(
|
||||
* of the receiver's length when it starts. ArraySpeciesCreate reads the
|
||||
* receiver's length before the species constructor runs, so a constructor
|
||||
* that appends to the receiver does not extend the loop
|
||||
* (lowerWithArraySpecies). Null when `name` is no such helper. */
|
||||
* (lowerWithArraySpecies). Every helper that reads a length is one, the
|
||||
* flatMap whose callback returns a union included, whether or not a
|
||||
* pipeline can fuse it (arrHofInfo holds the fusable ones); forEach makes
|
||||
* no array, so it stays out. Null when `name` is no such helper. */
|
||||
export function boundedArrayHofHelper(lowerer: Lowerer, name: string): string | null {
|
||||
const info = lowerer.arrHofInfo.get(name);
|
||||
if (!info || info.method === "forEach") return null;
|
||||
if (!name.startsWith("%arr.")) return null;
|
||||
if (lowerer.arrHofInfo.get(name)?.method === "forEach") return null;
|
||||
const bounded = `${name}.bounded`;
|
||||
if (lowerer.liftedFnNamed(bounded)) return bounded;
|
||||
const fn = lowerer.liftedFnNamed(name);
|
||||
|
||||
@@ -4892,9 +4892,10 @@ const DYN_STRING_ONLY_METHODS = new Set([
|
||||
/** `Array.isArray(v)` — a real runtime test on `unknown` values (the checked-dynamic tree's
|
||||
* array kind: dyn arrays answer true, bytes/objects/scalars false — exactly
|
||||
* JS, Uint8Array included), a compile-time constant on statically-typed
|
||||
* ones (an `T[]` value IS an array, every other static kind is not; folded
|
||||
* only over side-effect-free reads, the `in`-operator discipline; unions
|
||||
* fence with the narrow-first hint). Null when the callee isn't THE
|
||||
* ones (an `T[]` value IS an array, every other static kind is not; a
|
||||
* computed argument is evaluated and its value discarded, the
|
||||
* `in`-operator discipline; unions fence with the narrow-first hint). Null
|
||||
* when the callee isn't THE
|
||||
* stdlib Array.isArray, so the chain keeps trying. */
|
||||
function lowerArrayIsArrayCall(
|
||||
lowerer: Lowerer,
|
||||
@@ -4964,14 +4965,23 @@ function lowerArrayIsArrayCall(
|
||||
);
|
||||
}
|
||||
if (arg.type.kind === "caught") return null;
|
||||
if (arg.kind === "varRef" || arg.kind === "recordGet" || arg.kind === "fieldGet") {
|
||||
return { kind: "boolLit", value: lowerer.isArrayValueType(arg.type), type: BOOL, loc };
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
"statically-decided Array.isArray on computed arguments (bind the value to a variable first)",
|
||||
);
|
||||
const folded: IrExpr = {
|
||||
kind: "boolLit",
|
||||
value: lowerer.isArrayValueType(arg.type),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
if (arg.kind === "varRef" || arg.kind === "recordGet" || arg.kind === "fieldGet") return folded;
|
||||
// A computed argument (`Array.isArray(make())`, `Array.isArray([1, 2])`)
|
||||
// still answers from its static type, so evaluate the argument for its
|
||||
// effects and discard the value.
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: arg, loc }],
|
||||
result: folded,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Predicate declarations currently being inlined — re-entrancy guard
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8420-x509-recursive-dyn-type.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8421-array-subclass-species-forms.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8422-array-isarray-computed.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// A recursive type that cycles through an array and a set, next to a
|
||||
// certificate: the fence that refuses an X509Certificate in an `unknown`
|
||||
// slot (x509.ts) walks the converted value's type, and a cycle through
|
||||
// anything but a record used to recurse until the stack ran out — a crash,
|
||||
// not a diagnostic. The types are named here the way a JSON-ish union is,
|
||||
// and the certificate is the one 7090 parses.
|
||||
import { X509Certificate } from "node:crypto";
|
||||
|
||||
// A self-signed certificate without alternative names (as in 7090).
|
||||
const PLAIN = `-----BEGIN CERTIFICATE-----
|
||||
MIIC8jCCAdoCAQcwDQYJKoZIhvcNAQELBQAwPjEaMBgGA1UEAwwRcGxhaW4uZXhh
|
||||
bXBsZS5vcmcxIDAeBgkqhkiG9w0BCQEWEXBsYWluQGV4YW1wbGUub3JnMCAXDTI2
|
||||
MTAyMjA5NDY0MVoYDzIxMjYwOTI4MDk0NjQxWjA+MRowGAYDVQQDDBFwbGFpbi5l
|
||||
eGFtcGxlLm9yZzEgMB4GCSqGSIb3DQEJARYRcGxhaW5AZXhhbXBsZS5vcmcwggEi
|
||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCq01NcNAy9Ng04mVuAFvmZUvsm
|
||||
rY7aOaMgtODmptulgZ2qw25E09RgGYCn1S+4IYDu9hH9X4s+28Ag7Khmj4nmeoXN
|
||||
of5095wche8DR2AAUElqyr/Nmgm2kTLdIJX4wl0eWXHIlmSPdOUNovA6JjGKH99b
|
||||
8W7a2TqY/TsSadpPSvvN2esr7Jr4KgW7MXPUOnwNbcBOVcQP9rtFrduAWz01q0HM
|
||||
02LboclnUnk/e58BXAj8vgdVojljDcc0bhbdz/Hh1/5gaaNILjJNOd4AgwZaCT02
|
||||
KojGz4Nb6MSgFmON2iMgAZjY1fXXkJ7wsfVPytjW73e7uV/RFuqSHQZenxupAgMB
|
||||
AAEwDQYJKoZIhvcNAQELBQADggEBAJX1DGuiSLoCKBDI3oCgAu7X86G01ovEgZJr
|
||||
8JrmGHW5q7ujjo53t46h8XPm3LJ8UV9/MZ+Yry43xB2txXYv3b3g/i+7JdTWLgUD
|
||||
INA5VWvVkEZfm0lBEpU1Agpy4lV5uUCKZTgP/3MY5hnJ0YhQIs4BOjbwueMRFshL
|
||||
yyHVb73MMbpWaZ7+T368seafa+ekeuY6gQly/HacuszjoYd6KKV3n8eDewenClCF
|
||||
HoOF1Y4I+CvmR+a6B3iBczJ+a4tceFIIWsZUIeqzvGHpM7G9YwlmbvGprfLgR8Nv
|
||||
igUib/D9tU3hdL6VWN5nMjdXV/8Le4nDnBjVBjYDkVbsnmT91AQ=
|
||||
-----END CERTIFICATE-----
|
||||
`;
|
||||
|
||||
type Rec = string | Rec[];
|
||||
type RecSet = number | Set<RecSet>;
|
||||
|
||||
const cert = new X509Certificate(PLAIN);
|
||||
const rec: Rec = ["a", ["b"]];
|
||||
const set: RecSet = new Set([1, 2]);
|
||||
|
||||
function describe(value: unknown): string {
|
||||
return typeof value;
|
||||
}
|
||||
|
||||
console.log(describe(rec), rec.length, describe(set), set.size);
|
||||
console.log(cert.subject);
|
||||
@@ -0,0 +1,32 @@
|
||||
// One class that extends Array sends every array method call in the program
|
||||
// through the species lowering, plain-array receivers included. Two callback
|
||||
// forms the species path used to refuse: a filter whose callback narrows the
|
||||
// element union (a type predicate, or a discriminant test tsc infers one
|
||||
// from), which wraps the filtered array in a checked-narrowing conversion,
|
||||
// and a flatMap whose callback returns a union holding an array arm.
|
||||
class Stack extends Array<number> {}
|
||||
|
||||
type Item = { kind: "a"; n: number } | { kind: "b"; s: string };
|
||||
const items: Item[] = [
|
||||
{ kind: "a", n: 1 },
|
||||
{ kind: "b", s: "x" },
|
||||
{ kind: "a", n: 2 },
|
||||
];
|
||||
const isA = (item: Item): item is { kind: "a"; n: number } => item.kind === "a";
|
||||
|
||||
const tagged = items.filter(isA);
|
||||
const inferred = items.filter((item) => item.kind === "b");
|
||||
const blocked = items.filter((item) => {
|
||||
return item.kind === "b";
|
||||
});
|
||||
const flattened = items.flatMap((item) => (item.kind === "a" ? [item.n] : []));
|
||||
const mixed = items.flatMap((item) => (item.kind === "a" ? [item.n] : undefined));
|
||||
console.log(tagged.length, tagged.map((item) => item.n).join(","));
|
||||
console.log(inferred.length, inferred.map((item) => item.s).join(","), blocked.length);
|
||||
console.log(flattened.join(","), mixed.length, mixed.join(","));
|
||||
|
||||
const stack = Stack.from([1, 2, 3]);
|
||||
const even = stack.filter((n) => n % 2 === 0);
|
||||
const doubled = stack.flatMap((n) => (n > 1 ? [n, n * 10] : []));
|
||||
console.log(even instanceof Stack, even.join(","), stack.length);
|
||||
console.log(doubled instanceof Stack, doubled.join(","), stack.length);
|
||||
@@ -0,0 +1,28 @@
|
||||
// `Array.isArray` over a computed argument: a call, an array literal, an
|
||||
// object literal. The answer comes from the argument's static type, so the
|
||||
// argument is evaluated for its effects and its value discarded. A union
|
||||
// argument keeps answering by its runtime tag, and side effects of the
|
||||
// argument survive the fold.
|
||||
function rows(): number[] {
|
||||
return [1, 2];
|
||||
}
|
||||
function label(): string {
|
||||
return "rows";
|
||||
}
|
||||
type Pair = [number, string];
|
||||
function pair(): Pair {
|
||||
return [3, "three"];
|
||||
}
|
||||
let evals = 0;
|
||||
function counted(): number[] {
|
||||
evals++;
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(Array.isArray(rows()), Array.isArray([1, 2]), Array.isArray({ a: 1 }));
|
||||
console.log(Array.isArray(label()), Array.isArray(pair()));
|
||||
console.log(Array.isArray(counted()), evals);
|
||||
const maybe: string | string[] = ["a"];
|
||||
const narrow: string | string[] = "a";
|
||||
console.log(Array.isArray(maybe), Array.isArray(narrow));
|
||||
console.log(Array.isArray(rows().map((n) => n * 2)));
|
||||
@@ -12,7 +12,7 @@ const g = globalThis;
|
||||
const nan = NaN; // lowers now — a non-finite numeric literal like Infinity
|
||||
const s5 = String(5);
|
||||
const n5 = Number.NaN; // lowers now, with the other Number constants
|
||||
const isArr = Array.isArray([1]);
|
||||
const isArr = Array.isArray([1]); // lowers now: a computed argument folds after it is evaluated
|
||||
const merged = Object.assign({}, { a: 1 }); // the empty-target literal-source shape lowers now (it IS the source literal)
|
||||
const aliasedAssign = Object.assign(merged, { b: 2 }); // aliased targets stay fenced (real mutation of a live object)
|
||||
const refl = Reflect.has({ a: 1 }, "a");
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
stdlib-fence.ts:15:15 - error SC1090: statically-decided Array.isArray on computed arguments (bind the value to a variable first) are not supported yet
|
||||
|
||||
14 | const n5 = Number.NaN; // lowers now, with the other Number constants
|
||||
15 | const isArr = Array.isArray([1]);
|
||||
| ^~~~~~~~~~~~~~~~~~
|
||||
16 | const merged = Object.assign({}, { a: 1 }); // the empty-target literal-source shape lowers now (it IS the source literal)
|
||||
|
||||
stdlib-fence.ts:17:23 - error SC2020: 'Object.assign' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
16 | const merged = Object.assign({}, { a: 1 }); // the empty-target literal-source shape lowers now (it IS the source literal)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2100, "san": 1600 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1850, "san": 1250 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2000, "san": 1375 }
|
||||
Reference in New Issue
Block a user