Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 45 additions & 24 deletions packages/compiler/src/frontend/lowering/lower-exprs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7897,6 +7897,43 @@ function seqExprSafeStmt(s: IrStmt): boolean {
}
}

/** Run one supported `any` binary operation in the island. Checked-dynamic
* operands marshal in only after lowerBinary's identity-preserving native
* cases have had their chance (notably dyn strict equality). */
function lowerAnyBinaryInIsland(
lowerer: Lowerer,
expr: ts.BinaryExpression,
left: IrExpr,
right: IrExpr,
loc: SrcLoc,
): IrExpr {
const op = expr.operatorToken.kind;
const JS_BIN: Partial<Record<ts.SyntaxKind, IrJsOp>> = {
[ts.SyntaxKind.PlusToken]: "add",
[ts.SyntaxKind.MinusToken]: "sub",
[ts.SyntaxKind.AsteriskToken]: "mul",
[ts.SyntaxKind.SlashToken]: "div",
[ts.SyntaxKind.PercentToken]: "mod",
[ts.SyntaxKind.AsteriskAsteriskToken]: "pow",
[ts.SyntaxKind.LessThanToken]: "lt",
[ts.SyntaxKind.LessThanEqualsToken]: "le",
[ts.SyntaxKind.GreaterThanToken]: "gt",
[ts.SyntaxKind.GreaterThanEqualsToken]: "ge",
[ts.SyntaxKind.EqualsEqualsEqualsToken]: "eq",
[ts.SyntaxKind.ExclamationEqualsEqualsToken]: "neq",
};
const jop = JS_BIN[op];
if (jop === undefined) {
lowerer.unsupported("SC1090", expr, `operator '${ts.tokenToString(op) ?? ts.SyntaxKind[op]}' on 'any' values`);
}
const type = jsOpResultKind(jop) === "bool" ? BOOL : JSVAL;
return {
kind: "jsOp", op: jop,
args: [lowerer.jsvalIn(left, expr.left), lowerer.jsvalIn(right, expr.right)],
type, loc,
};
}

export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr {
const loc = locOf(expr);
const op = expr.operatorToken.kind;
Expand Down Expand Up @@ -8394,6 +8431,13 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
(left.type.kind === "dyn" && lowerer.anyOrigin(expr.left)) ||
(right.type.kind === "dyn" && lowerer.anyOrigin(expr.right))
) {
// `JSON.parse` and other checked-dynamic producers deliberately
// remain dyn when assigned to an `any` local: eagerly converting
// the binding would deep-copy its data and sever aliases. At an
// operator the dynamic build can cross only the operands and let
// the engine apply JS's exact coercion semantics. Static builds
// retain the SC2011 promise that --dynamic lifts this site.
if (lowerer.dynamic) return lowerAnyBinaryInIsland(lowerer, expr, left, right, loc);
lowerer.anyOpFence(`the '${ts.tokenToString(op) ?? ts.SyntaxKind[op]}' operator`, expr);
}
// tsc allows ===/!== on unknown (arithmetic/comparisons it rejects
Expand All @@ -8405,30 +8449,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
// computes. Comparisons come back as static bools; arithmetic stays
// an island value ('1 as any + "x"' is a string over there).
if (left.type.kind === "jsval" || right.type.kind === "jsval") {
const JS_BIN: Partial<Record<ts.SyntaxKind, IrJsOp>> = {
[ts.SyntaxKind.PlusToken]: "add",
[ts.SyntaxKind.MinusToken]: "sub",
[ts.SyntaxKind.AsteriskToken]: "mul",
[ts.SyntaxKind.SlashToken]: "div",
[ts.SyntaxKind.PercentToken]: "mod",
[ts.SyntaxKind.AsteriskAsteriskToken]: "pow",
[ts.SyntaxKind.LessThanToken]: "lt",
[ts.SyntaxKind.LessThanEqualsToken]: "le",
[ts.SyntaxKind.GreaterThanToken]: "gt",
[ts.SyntaxKind.GreaterThanEqualsToken]: "ge",
[ts.SyntaxKind.EqualsEqualsEqualsToken]: "eq",
[ts.SyntaxKind.ExclamationEqualsEqualsToken]: "neq",
};
const jop = JS_BIN[op];
if (jop === undefined) {
lowerer.unsupported("SC1090", expr, `operator '${ts.tokenToString(op) ?? ts.SyntaxKind[op]}' on 'any' values`);
}
const type = jsOpResultKind(jop) === "bool" ? BOOL : JSVAL;
return {
kind: "jsOp", op: jop,
args: [lowerer.jsvalIn(left, expr.left), lowerer.jsvalIn(right, expr.right)],
type, loc,
};
return lowerAnyBinaryInIsland(lowerer, expr, left, right, loc);
}
// An unchecked array read keeps its undefined arm in the IR even when
// the checker typed the binding as `string`. An optional string is not
Expand Down
6 changes: 6 additions & 0 deletions packages/compiler/test/ts7/baselines/order-parity.json
Original file line number Diff line number Diff line change
Expand Up @@ -6052,6 +6052,12 @@
],
"diags": []
},
"<repo>/tests/corpus/2856-dynamic-any-local-operators.ts": {
"order": [
"<repo>/tests/corpus/2856-dynamic-any-local-operators.ts"
],
"diags": []
},
"<repo>/tests/corpus/300-if-else.ts": {
"order": [
"<repo>/tests/corpus/300-if-else.ts"
Expand Down
30 changes: 30 additions & 0 deletions tests/corpus/2856-dynamic-any-local-operators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// @dynamic
// Checked-dynamic producers stay in dyn storage under an `any` spelling to
// preserve aliases, but JS-coercive operators cross their operands into the
// island. Strict equality remains native so object identity is not copied.
function answer(): number {
const value: any = JSON.parse("41");
return value + 1;
}

const left: any = JSON.parse("20");
const right: any = JSON.parse("22");
const sum: number = left + right;
console.log(answer(), sum);

const n: any = JSON.parse("9");
const difference: number = n - 4;
const product: number = n * 2;
const quotient: number = n / 3;
const remainder: number = n % 4;
const power: number = n ** 2;
console.log(difference, product, quotient, remainder, power);
console.log(n < 10, n <= 9, n > 8, n >= 10);

const text: any = JSON.parse('"left"');
const joined: string = text + "-right";
console.log(joined);

const object: any = JSON.parse('{"value":1}');
const objectText: string = object + "!";
console.log(object === object, object !== object, objectText);
12 changes: 12 additions & 0 deletions tests/harness/coverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ test("JS inference gaps land where 'any' lands: SC2011 static, island dynamic",
);
});

test("any-typed checked-dynamic locals honor the --dynamic coverage promise", () => {
const file = join(repoRoot, "tests/corpus/2856-dynamic-any-local-operators.ts");
const staticCoverage = analyze(file).coverage;
expect(staticCoverage.diagnostics.length).toBeGreaterThan(0);
expect(new Set(staticCoverage.diagnostics.map((d) => d.code))).toEqual(new Set(["SC2011"]));

const dynamicCoverage = analyze(file, { dynamic: true }).coverage;
expect(dynamicCoverage.diagnostics).toEqual([]);
expect(dynamicCoverage.stats.statementsFailed).toBe(0);
expect(dynamicCoverage.stats.statementsIsland).toBeGreaterThan(0);
});

test("npm package sites attribute per package", async () => {
// Every site of a package-declared value groups into one SC2013 line
// naming the package ("values from the 'mathkit' package ..."), inside
Expand Down
Loading