mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Compile .length of computed tuple expressions (#2482)
This commit is contained in:
@@ -3234,6 +3234,22 @@ export function lowerGroupsProjection(
|
||||
|
||||
/** Field reads use the shared FieldTarget union. Native method reads select
|
||||
* an interned unbound callable; invocation supplies its receiver separately. */
|
||||
/** Whether a receiver is a binding or `this`, or a chain of property reads
|
||||
* from one whose every member is a plain property (no accessor), so that
|
||||
* reading it has no effect. */
|
||||
function plainReadChain(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
let current = node;
|
||||
while (ts.isPropertyAccessExpression(current)) {
|
||||
if (current.questionDotToken) return false;
|
||||
const member = lowerer.checker.getSymbolAtLocation(current.name);
|
||||
if (member === undefined || (member.flags & ts.SymbolFlags.Property) === 0) return false;
|
||||
if ((member.flags & (ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor)) !== 0)
|
||||
return false;
|
||||
current = current.expression;
|
||||
}
|
||||
return ts.isIdentifier(current) || current.kind === ts.SyntaxKind.ThisKeyword;
|
||||
}
|
||||
|
||||
export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null {
|
||||
// A checked receiver already owns its property table. An ambient
|
||||
// generic method signature must not divert its value into native
|
||||
@@ -3388,22 +3404,71 @@ export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressi
|
||||
}
|
||||
}
|
||||
// `t.length` on a tuple: the arity CONSTANT (tuples are fixed-shape —
|
||||
// the checker types it as the literal arity too). Folding discards the
|
||||
// receiver's evaluation, so only side-effect-free receivers fold;
|
||||
// anything else (a call result) binds to a const first.
|
||||
// the checker types it as the literal arity too). A binding or `this`,
|
||||
// or a chain of plain property reads from one, folds; any other receiver
|
||||
// (a call result, a ternary, an assertion, a chain through an accessor)
|
||||
// is evaluated first, for its side effects and exceptions, and its value
|
||||
// discarded.
|
||||
if (receiverIr?.kind === "record" && expr.name.text === "length") {
|
||||
const shape = lowerer.shapes.get(receiverIr.shapeId);
|
||||
if (shape?.tuple) {
|
||||
let root: ts.Expression = expr.expression;
|
||||
while (ts.isPropertyAccessExpression(root)) root = root.expression;
|
||||
if (!ts.isIdentifier(root) && root.kind !== ts.SyntaxKind.ThisKeyword) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
"'.length' of a computed tuple expression (the arity is a constant — bind the tuple to a const first)",
|
||||
);
|
||||
const arity: IrExpr = {
|
||||
kind: "numLit",
|
||||
value: shape.fields.length,
|
||||
type: F64,
|
||||
loc: locOf(expr),
|
||||
};
|
||||
if (plainReadChain(lowerer, expr.expression)) return arity;
|
||||
const receiver = checkerArray ?? lowerer.lowerExpr(expr.expression);
|
||||
const loc = locOf(expr);
|
||||
// An unchecked element read (`rows[i].length` with `rows: T[]`) answers
|
||||
// the element's optional value: a missing element is undefined, and
|
||||
// reading `length` from it throws Node's TypeError.
|
||||
if (receiver.type.kind === "union") {
|
||||
const unionId = receiver.type.unionId;
|
||||
const held = lowerer.declareHiddenLocal("%tupleLengthReceiver", receiver.type);
|
||||
let result: IrExpr = arity;
|
||||
for (const unit of ["undefined", "null"] as const) {
|
||||
const tag = lowerer.armTag(unionId, unit === "undefined" ? UNDEFINED_T : NULL_T);
|
||||
if (tag < 0) continue;
|
||||
result = {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
unionId,
|
||||
tag,
|
||||
negated: false,
|
||||
value: varRef(held.id, receiver.type, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: nodeThrowExpr(
|
||||
1,
|
||||
"",
|
||||
`Cannot read properties of ${unit} (reading 'length')`,
|
||||
F64,
|
||||
loc,
|
||||
),
|
||||
else_: result,
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: held.id, init: receiver, loc }],
|
||||
result,
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return { kind: "numLit", value: shape.fields.length, type: F64, loc: locOf(expr) };
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: receiver, loc: locOf(expr.expression) }],
|
||||
result: arity,
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/tuple-length-computed.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// `.length` of computed tuple expressions: call results, ternaries, `as`
|
||||
// assertions, `as const` literals, member reads (fields and accessors) and
|
||||
// element reads. The receiver is evaluated once, in order, for its side
|
||||
// effects and exceptions, and the length is the tuple's arity.
|
||||
|
||||
const log: string[] = [];
|
||||
|
||||
function pair(tag: string): [number, string] {
|
||||
log.push(`pair ${tag}`);
|
||||
return [1, tag];
|
||||
}
|
||||
|
||||
function triple(): [number, string, boolean] {
|
||||
log.push("triple");
|
||||
return [1, "x", true];
|
||||
}
|
||||
|
||||
function failing(): [number, number] {
|
||||
log.push("failing");
|
||||
throw new Error("no tuple");
|
||||
}
|
||||
|
||||
class Holder {
|
||||
plain: [string, string] = ["a", "b"];
|
||||
reads = 0;
|
||||
get viaAccessor(): [number, number, number] {
|
||||
this.reads++;
|
||||
log.push("accessor");
|
||||
return [this.reads, 2, 3];
|
||||
}
|
||||
ownLength(): number {
|
||||
return this.plain.length + this.viaAccessor.length;
|
||||
}
|
||||
}
|
||||
|
||||
const nested: [[number, number], [string, string, string]] = [
|
||||
[1, 2],
|
||||
["a", "b", "c"],
|
||||
];
|
||||
|
||||
let flip = 0;
|
||||
function pick(): boolean {
|
||||
flip++;
|
||||
return flip % 2 === 1;
|
||||
}
|
||||
|
||||
console.log(pair("call").length);
|
||||
console.log((pick() ? triple() : ([0, "", false] as [number, string, boolean])).length);
|
||||
console.log((pick() ? triple() : ([0, "", false] as [number, string, boolean])).length);
|
||||
console.log(([1, 2, 3] as const).length);
|
||||
console.log(([flip, "y"] as [number, string]).length);
|
||||
|
||||
const h = new Holder();
|
||||
console.log(h.plain.length, h.viaAccessor.length, h.viaAccessor.length, h.reads);
|
||||
console.log(h.ownLength(), h.reads);
|
||||
console.log(nested[0].length, nested[1].length, nested.length);
|
||||
|
||||
// Evaluation order inside one expression, and in a loop condition.
|
||||
const sum = pair("left").length + triple().length * 10 + pair("right").length * 100;
|
||||
console.log(sum);
|
||||
let rounds = 0;
|
||||
for (let i = 0; i < pair(`loop ${i}`).length; i++) rounds++;
|
||||
console.log(rounds);
|
||||
console.log(`${pair("template").length}:${typeof triple().length}`);
|
||||
|
||||
// An exception in the receiver propagates; no length is produced.
|
||||
try {
|
||||
console.log(failing().length);
|
||||
} catch (e) {
|
||||
console.log("caught", (e as Error).message);
|
||||
}
|
||||
|
||||
// An unchecked element read past the end is undefined, so its `length`
|
||||
// throws, also through a field holding the array.
|
||||
const rows: [number, string][] = [[1, "a"]];
|
||||
let missing = 5;
|
||||
try {
|
||||
console.log(rows[missing].length);
|
||||
} catch (e) {
|
||||
console.log("missing", String(e));
|
||||
}
|
||||
console.log("present", rows[0].length);
|
||||
const table: { rows: [number, number][] } = { rows: [] };
|
||||
try {
|
||||
console.log(table.rows[missing - 3].length);
|
||||
} catch (e) {
|
||||
console.log("missing field", String(e));
|
||||
}
|
||||
|
||||
console.log(log.join(" | "));
|
||||
@@ -24,12 +24,6 @@ for (const part of pair) {
|
||||
// An array's runtime length cannot determine a fixed tuple's positions.
|
||||
const copy = [...(["a", 1] as (string | number)[])] as [string, number];
|
||||
|
||||
// The arity constant folds only off side-effect-free receivers.
|
||||
function mk(): [string, number] {
|
||||
return ["z", 9];
|
||||
}
|
||||
console.log(mk().length);
|
||||
|
||||
// join on ref-element arrays: JS would recursively toString — dishonest.
|
||||
const recs: { id: number }[] = [{ id: 1 }];
|
||||
console.log(recs.join(","));
|
||||
|
||||
@@ -26,23 +26,16 @@ tuples.ts:25:15 - error SC1090: spreading a variable-length value into a fixed t
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
26 |
|
||||
|
||||
tuples.ts:31:13 - error SC1090: '.length' of a computed tuple expression (the arity is a constant — bind the tuple to a const first) is not supported yet
|
||||
tuples.ts:29:13 - error SC1090: '.join()' on arrays of this element type (number, string, boolean and Date arrays join — unions of those with undefined/null arms too, the units printing empty like JS) is not supported yet
|
||||
|
||||
30 | }
|
||||
31 | console.log(mk().length);
|
||||
| ^~~~~~~~~~~
|
||||
32 |
|
||||
|
||||
tuples.ts:35:13 - error SC1090: '.join()' on arrays of this element type (number, string, boolean and Date arrays join — unions of those with undefined/null arms too, the units printing empty like JS) is not supported yet
|
||||
|
||||
34 | const recs: { id: number }[] = [{ id: 1 }];
|
||||
35 | console.log(recs.join(","));
|
||||
28 | const recs: { id: number }[] = [{ id: 1 }];
|
||||
29 | console.log(recs.join(","));
|
||||
| ^~~~~~~~~~~~~~
|
||||
36 | const tuples: [string, string][] = [["k", "v"]];
|
||||
30 | const tuples: [string, string][] = [["k", "v"]];
|
||||
|
||||
tuples.ts:37:13 - error SC1090: '.join()' on arrays of this element type (number, string, boolean and Date arrays join — unions of those with undefined/null arms too, the units printing empty like JS) is not supported yet
|
||||
tuples.ts:31:13 - error SC1090: '.join()' on arrays of this element type (number, string, boolean and Date arrays join — unions of those with undefined/null arms too, the units printing empty like JS) is not supported yet
|
||||
|
||||
36 | const tuples: [string, string][] = [["k", "v"]];
|
||||
37 | console.log(tuples.join(";"));
|
||||
30 | const tuples: [string, string][] = [["k", "v"]];
|
||||
31 | console.log(tuples.join(";"));
|
||||
| ^~~~~~~~~~~~~~~~
|
||||
38 | // Map elements compile through the same REF storage as function elements.
|
||||
32 | // Map elements compile through the same REF storage as function elements.
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2500, "san": 3000 }
|
||||
Reference in New Issue
Block a user