mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-25 06:24:27 +08:00
Index strings whose binding still stores a union
`const w = words[i]; w[w.length - 1]` and the narrowed form
`typeof t === "string" ? t[t.length - 1] : ...` were refused with SC1090
("string indexing with this index/result shape"), in `if` statements as well
as in conditional expressions. A binding read from an array keeps an
undefined arm in its storage, and a typeof guard narrows only the checker's
type, so the receiver reached the string-index lowering as the storage
union rather than a string.
The refused path now captures the union and the index and reads the string
arm. A missing value performs the checked-dynamic keyed read on the unit,
which throws Node's "Cannot read properties of undefined (reading '<key>')"
with the evaluated key; a value arm the checker excluded fails the checked
extraction. Receivers that compiled before take the paths they took before.
This is path-to-regexp's tokensToRegexp shape.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
56269bdad8
commit
07b44cd4b1
@@ -130,7 +130,7 @@ import {
|
||||
lowerSafeBytesRead,
|
||||
} from "./array-values.js";
|
||||
import { strCharsCall } from "./containers/array-construction.js";
|
||||
import { lowerOptionalStringIndex } from "./string-index.js";
|
||||
import { lowerOptionalStringIndex, lowerUnionStringIndex } from "./string-index.js";
|
||||
import { tryLowerIndexedComparison } from "./indexed-comparison.js";
|
||||
import { npmStaticPackageOfPath } from "../npm-static.js";
|
||||
import { unsupportedModuleFeatureOf } from "../builtin-modules.js";
|
||||
@@ -6879,6 +6879,19 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
|
||||
locOf(expr),
|
||||
);
|
||||
}
|
||||
// The checker narrowed the receiver to string, but its stored value
|
||||
// is still the union (an array-derived binding, a typeof guard).
|
||||
if (index.type.kind === "f64") {
|
||||
const read = lowerUnionStringIndex(
|
||||
lowerer,
|
||||
recv,
|
||||
index,
|
||||
expr.argumentExpression,
|
||||
lowerer.withUndefinedArm(STRING),
|
||||
locOf(expr),
|
||||
);
|
||||
if (read) return read;
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import { BOOL, F64, STRING, type IrExpr, type IrType, type SrcLoc } from "../../ir/ir.js";
|
||||
import type * as ts from "../ts7/adapter.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
F64,
|
||||
STRING,
|
||||
isUnitType,
|
||||
type IrExpr,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import { numLit, varRef } from "../../ir/build.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
@@ -66,3 +77,77 @@ export function lowerOptionalStringIndex(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A number-keyed read of a receiver the checker narrowed to string whose
|
||||
* stored value is still a union: an array-derived binding keeps its
|
||||
* undefined arm (`const t = tokens[i]`), and a `typeof t === "string"`
|
||||
* guard narrows the checker's type without retagging the storage. Both
|
||||
* operands are captured first, as for a plain string receiver. A unit arm
|
||||
* performs the checked-dynamic keyed read on that unit, which throws Node's
|
||||
* "Cannot read properties of undefined (reading '<key>')"; another value
|
||||
* arm, which the checker has excluded, fails the checked extraction; the
|
||||
* string arm reads like any string receiver. Null when the union has no
|
||||
* string arm. */
|
||||
export function lowerUnionStringIndex(
|
||||
lowerer: Lowerer,
|
||||
receiver: IrExpr,
|
||||
index: IrExpr,
|
||||
keyNode: ts.Node,
|
||||
type: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (receiver.type.kind !== "union") return null;
|
||||
const unionType = receiver.type;
|
||||
const unionId = unionType.unionId;
|
||||
const def = lowerer.unions.get(unionId);
|
||||
const stringTag = lowerer.armTag(unionId, STRING);
|
||||
if (!def || stringTag < 0) return null;
|
||||
const receiverLocal = lowerer.declareHiddenLocal("%indexedUnion", unionType);
|
||||
const indexLocal = lowerer.declareHiddenLocal("%unionStringIndex", F64);
|
||||
const stored = (): IrExpr => varRef(receiverLocal.id, unionType, loc);
|
||||
const key = (): IrExpr => varRef(indexLocal.id, F64, loc);
|
||||
const stmts: IrStmt[] = [
|
||||
{ kind: "varDecl", localId: receiverLocal.id, init: receiver, loc },
|
||||
{ kind: "varDecl", localId: indexLocal.id, init: index, loc },
|
||||
];
|
||||
let text: IrExpr;
|
||||
if (def.arms.every((arm, tag) => tag === stringTag || isUnitType(arm))) {
|
||||
text = { kind: "unionNarrow", unionId, tag: stringTag, value: stored(), type: STRING, loc };
|
||||
} else {
|
||||
const helper = lowerer.narrowedArmHelper(unionId, STRING, loc);
|
||||
if (helper === null) return null;
|
||||
text = lowerer.checkedNarrowCall(helper, stored(), STRING, loc);
|
||||
}
|
||||
let result = lowerOptionalStringIndex(lowerer, text, key(), type, loc);
|
||||
for (let tag = def.arms.length - 1; tag >= 0; tag--) {
|
||||
const arm = def.arms[tag]!;
|
||||
if (!isUnitType(arm)) continue;
|
||||
const unit = arm.kind === "nullT" ? "null" : "undefined";
|
||||
const thrown: IrExpr = {
|
||||
kind: "dynCheck",
|
||||
value: {
|
||||
kind: "dynKeyGet",
|
||||
value: {
|
||||
kind: "dynFrom",
|
||||
value: { kind: "unitLit", unit, type: arm, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
key: lowerer.ensureString(key(), keyNode),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
result = {
|
||||
kind: "ternary",
|
||||
cond: { kind: "unionIsTag", unionId, tag, negated: false, value: stored(), type: BOOL, loc },
|
||||
then: lowerer.coerceToExpected(thrown, type),
|
||||
else_: result,
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return { kind: "seqExpr", stmts, result, type, loc };
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6043-string-index-narrowed-union.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Indexing a string whose binding was read from an array or narrowed from a
|
||||
// union with typeof. The receiver's stored value keeps its other arms, so an
|
||||
// out-of-bounds array read throws Node's TypeError at the index, naming the
|
||||
// key.
|
||||
interface Key {
|
||||
name: string;
|
||||
}
|
||||
type Token = string | Key;
|
||||
|
||||
// path-to-regexp's tokensToRegexp shape.
|
||||
function endDelimited(tokens: Token[], delimiters: string): boolean {
|
||||
const endToken = tokens[tokens.length - 1];
|
||||
return typeof endToken === "string"
|
||||
? delimiters.indexOf(endToken[endToken.length - 1]) > -1
|
||||
: endToken === undefined;
|
||||
}
|
||||
console.log(
|
||||
endDelimited(["a/", { name: "x" }], "/"),
|
||||
endDelimited(["a/"], "/"),
|
||||
endDelimited([], "/"),
|
||||
endDelimited(["ab"], "/"),
|
||||
);
|
||||
|
||||
function lastChar(tokens: Token[]): string {
|
||||
const t = tokens[0];
|
||||
if (typeof t === "string") return t[t.length - 1];
|
||||
return "-";
|
||||
}
|
||||
console.log(lastChar(["xyz"]), lastChar([{ name: "k" }]));
|
||||
|
||||
function lastLetter(words: string[], i: number): string {
|
||||
const w = words[i];
|
||||
return w[w.length - 1];
|
||||
}
|
||||
console.log(lastLetter(["ab", "cd"], 1));
|
||||
|
||||
function at(words: string[], i: number, j: number): string | undefined {
|
||||
const w = words[i];
|
||||
return w[j];
|
||||
}
|
||||
console.log(at(["abc"], 0, 1), at(["abc"], 0, 7), at(["abc"], 0, 1.5), at(["abc"], 0, -1));
|
||||
for (const j of [2, -0.5, 1e21]) {
|
||||
try {
|
||||
at(["abc"], 3, j);
|
||||
} catch (e) {
|
||||
console.log(e instanceof TypeError, (e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
let calls = 0;
|
||||
function key(): number {
|
||||
calls++;
|
||||
return 0;
|
||||
}
|
||||
function viaCall(words: string[]): string {
|
||||
const w = words[0];
|
||||
return w[key()];
|
||||
}
|
||||
console.log(viaCall(["q"]), calls);
|
||||
Reference in New Issue
Block a user