mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Test a scalar optional left of ?? in JavaScript on the typed path
In JavaScript, `counts.get(key) ?? 0` on a Map the compiler types as Map<string, number> is typed `any` by the checker, so `??` was lowered on checked-dynamic values: the `number | undefined` read converted to a dynamic value, which allocated a heap union for every read, and the test ran on that value. When the left of such a `??` is a union of one number, boolean or string arm with undefined or null, and the default has the same type, the test and the default now run on the typed path, where the read needs no heap union, and only the result converts to a dynamic value. Other defaults keep the dynamic path. New corpus program: untyped-nullish-scalar-defaults.mjs.
This commit is contained in:
@@ -388,6 +388,20 @@ export function lowerNullishPair(
|
||||
lowerer.runtimeOptionalArithmeticTypes.set(expr, type);
|
||||
}
|
||||
if (type.kind === "dyn" && lowerer.dynConvertible(left.type)) {
|
||||
// `counts.get(k) ?? 0` in JavaScript: a scalar optional left and a
|
||||
// default of its value's type give a scalar, so the test and the default
|
||||
// run on the typed path and only the result converts. Converting the
|
||||
// optional left itself would allocate a union for every read.
|
||||
if (rest.length === 1 && primitive(rest[0]!) && rightType && typeEquals(rightType, rest[0]!)) {
|
||||
const typed: IrExpr = {
|
||||
kind: "nullish",
|
||||
left,
|
||||
right: lowerer.lowerExprExpecting(expr.right, rest[0]!),
|
||||
type: rest[0]!,
|
||||
loc,
|
||||
};
|
||||
return lowerer.coerceToExpected(typed, DYN);
|
||||
}
|
||||
return {
|
||||
kind: "nullish",
|
||||
left: lowerer.coerceToExpected(left, DYN),
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/untyped-nullish-scalar-defaults.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule, validateModule } from "../src/index.js";
|
||||
|
||||
test("`??` in JavaScript tests a scalar optional left on the typed path when the default has its type", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-untyped-nullish-"));
|
||||
try {
|
||||
const entry = join(dir, "main.mjs");
|
||||
writeFileSync(
|
||||
entry,
|
||||
[
|
||||
"const counts = new Map();",
|
||||
'for (const w of ["a", "b", "a"]) counts.set(w, (counts.get(w) ?? 0) + 1);',
|
||||
'console.log(counts.get("a") ?? 0, counts.get("z") ?? "none");',
|
||||
].join("\n"),
|
||||
);
|
||||
const output = join(dir, "main.ir.json");
|
||||
const lowered = await compile(entry, { outputKind: "ir", outDir: dir, outPath: output });
|
||||
if (!lowered.ok) throw new Error(JSON.stringify(lowered.diagnostics));
|
||||
const module = deserializeModule(readFileSync(output, "utf8"));
|
||||
expect(validateModule(module)).toEqual([]);
|
||||
const nullish: { type: string; left: string }[] = [];
|
||||
const visit = (node: unknown, parent: string | null): void => {
|
||||
if (node === null || typeof node !== "object") return;
|
||||
if (Array.isArray(node)) return node.forEach((child) => visit(child, parent));
|
||||
const n = node as { kind?: string; type?: { kind: string }; left?: { kind: string } };
|
||||
if (n.kind === "nullish")
|
||||
nullish.push({ type: `${parent}>${n.type!.kind}`, left: n.left!.kind });
|
||||
for (const [key, value] of Object.entries(node))
|
||||
if (key !== "loc") visit(value, n.kind ?? parent);
|
||||
};
|
||||
visit(module.functions, null);
|
||||
// The loop's `?? 0`, whose result is an untyped value, reads the Map's
|
||||
// number on the typed path and converts only the result. (The first
|
||||
// console.log argument is a number already.) `?? "none"` takes another
|
||||
// type, so its left converts first.
|
||||
expect(
|
||||
nullish.filter((n) => n.type === "dynFrom>f64" && n.left === "mapIntrinsic"),
|
||||
).toHaveLength(1);
|
||||
expect(nullish.filter((n) => n.type.endsWith(">dyn") && n.left === "dynFrom")).toHaveLength(1);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
// `??` in untyped JavaScript where the left is an optional number, boolean or
|
||||
// string and the default has the same type: Map reads, find(), a function that
|
||||
// may return undefined, and defaults of another type, null or an object.
|
||||
const words = ["alpha", "beta", "gamma", "beta", "delta", "beta", "alpha"];
|
||||
const counts = new Map();
|
||||
for (const word of words) counts.set(word, (counts.get(word) ?? 0) + 1);
|
||||
for (const [word, n] of counts) console.log(word, n);
|
||||
|
||||
const odd = new Map();
|
||||
odd.set("nan", NaN);
|
||||
odd.set("zero", -0);
|
||||
odd.set("one", 1);
|
||||
for (const key of ["nan", "zero", "one", "missing"]) {
|
||||
const value = odd.get(key) ?? 7;
|
||||
console.log(key, value, Object.is(value, -0), Number.isNaN(value), typeof value);
|
||||
}
|
||||
|
||||
const flags = new Map();
|
||||
flags.set("on", true);
|
||||
flags.set("off", false);
|
||||
for (const key of ["on", "off", "missing"]) console.log(key, flags.get(key) ?? true);
|
||||
|
||||
const names = new Map();
|
||||
names.set("a", "");
|
||||
names.set("b", "bee");
|
||||
for (const key of ["a", "b", "c"]) console.log(JSON.stringify(names.get(key) ?? "none"));
|
||||
|
||||
const values = [3, 8, 13, 21];
|
||||
console.log(values.find((v) => v > 10) ?? -1, values.find((v) => v > 100) ?? -1);
|
||||
|
||||
function lookup(table, key) {
|
||||
return key in table ? table[key] : undefined;
|
||||
}
|
||||
const table = { x: 1.5, y: 0 };
|
||||
console.log(lookup(table, "x") ?? 0, lookup(table, "y") ?? 9, lookup(table, "z") ?? 9);
|
||||
|
||||
// Defaults of another type keep the left's value or take the default as is.
|
||||
console.log(counts.get("beta") ?? "none", counts.get("omega") ?? "none");
|
||||
console.log(counts.get("omega") ?? null, counts.get("beta") ?? null);
|
||||
const fallback = { n: 0 };
|
||||
console.log(counts.get("omega") ?? fallback, counts.get("alpha") ?? fallback);
|
||||
|
||||
// The result is an ordinary value in later arithmetic and comparisons.
|
||||
let total = 0;
|
||||
for (const word of ["alpha", "beta", "omega"]) total += (counts.get(word) ?? 0) * 10;
|
||||
console.log(total, (counts.get("omega") ?? 0) === 0, (odd.get("nan") ?? 0) === (odd.get("nan") ?? 0));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3711, "san": 5463 }
|
||||
Reference in New Issue
Block a user