Compare strings against literals inline

This commit is contained in:
Chris Tate
2026-10-13 09:42:05 -05:00
parent 725812afea
commit 443f5ebc9b
11 changed files with 503 additions and 38 deletions
+34 -6
View File
@@ -306,6 +306,7 @@ import {
vAdapters,
} from "./shapes.js";
import type { ExprOf, LibCallExpr, LlStreamTypedRefAdapter, LlValue } from "./expr-context.js";
import { INLINE_LITERAL_BYTES, literalEqualityDefinition } from "./string-literal-equality.js";
export { LlvmUnsupportedError } from "./unsupported.js";
@@ -527,6 +528,8 @@ export class LlEmitter {
/** Interned string literals: UTF-8 text → { symbol, byte length } —
* first-use order, the runtime ABI’s determinism discipline. */
private readonly literals = new Map<string, { sym: string; len: number }>();
/** Literal equality helpers: literal text → helper symbol (literalEquals). */
private readonly literalEqualities = new Map<string, string>();
/** Symbols of interned constant data, named by content (contentSymbol). */
private readonly contentSymbols = new Set<string>();
/** Interned unit-armed union instances: "unionId:tag" → symbol — one
@@ -2821,6 +2824,18 @@ export class LlEmitter {
``,
);
}
for (const [text, name] of this.literalEqualities) {
defs.push(
...literalEqualityDefinition(
name,
Buffer.from(text, "utf8"),
this.internLiteral(text),
this.sizeType,
FN_ATTRS,
),
``,
);
}
// String length and charCodeAt read proven-ASCII strings inline: bit 31
// of the capacity word (SCR_STR_ASCII_BIT, the low half's sign bit on
// little-endian targets) makes UTF-16 units bytes. Unproven strings and
@@ -3637,6 +3652,20 @@ export class LlEmitter {
return name;
}
/** The inline equality test against one string literal (an
* `i1 (ptr)` helper LLVM inlines at each site), by literal symbol. */
literalEquals(text: string): string {
const literal = this.internLiteral(text);
const name = `@sc_streq_${literal.slice("@sc_lit_".length)}`;
if (!this.literalEqualities.has(text)) {
this.literalEqualities.set(text, name);
if (Buffer.byteLength(text, "utf8") > INLINE_LITERAL_BYTES) {
this.declare(`declare zeroext i1 @scr_str_eq(ptr, ptr)`);
}
}
return name;
}
/** Interned NUL-terminated C-string constant (the scr_jb_puts /
* stringify-indent currency) — `@`-ref, first-use order. */
cstr(text: string): string {
@@ -6315,7 +6344,6 @@ export class LlEmitter {
break;
}
const join = B.newLabel("rks.j");
this.declare(`declare zeroext i1 @scr_str_eq(ptr, ptr)`);
if (iv.kind === "dyn" && shape.indexValue) {
// A dyn-valued shape (the C recordKeySetHelper's dyn arm,
// inline): declared keys VALIDATE the dyn value against the
@@ -6325,10 +6353,9 @@ export class LlEmitter {
// insert the dyn value into the overflow map as-is.
this.declare(`declare void @scr_dyn_release(ptr)`);
for (const f of shape.fields) {
const lit = this.internLiteral(f.name);
const hit = B.tmp();
B.line(
`${hit} = call zeroext i1 @sc_str_eq(ptr ${key.name}, ptr ${lit}) ; ${llvmCommentText(f.name)}`,
`${hit} = call zeroext i1 ${this.literalEquals(f.name)}(ptr ${key.name}) ; ${llvmCommentText(f.name)}`,
);
const lh = B.newLabel("rks.h");
const ln = B.newLabel("rks.n");
@@ -6382,10 +6409,9 @@ export class LlEmitter {
}
for (const f of shape.fields) {
// typeEquals(f.type, iv) — the frontend fences everything else.
const lit = this.internLiteral(f.name);
const hit = B.tmp();
B.line(
`${hit} = call zeroext i1 @sc_str_eq(ptr ${key.name}, ptr ${lit}) ; ${llvmCommentText(f.name)}`,
`${hit} = call zeroext i1 ${this.literalEquals(f.name)}(ptr ${key.name}) ; ${llvmCommentText(f.name)}`,
);
const lh = B.newLabel("rks.h");
const ln = B.newLabel("rks.n");
@@ -7258,7 +7284,9 @@ export class LlEmitter {
this.frames.push([]);
const t = this.emitExpr(c.test);
const hit = B.tmp();
if (c.test.type.kind === "string") {
if (c.test.kind === "strLit") {
B.line(`${hit} = call zeroext i1 ${this.literalEquals(c.test.value)}(ptr ${disc.name})`);
} else if (c.test.type.kind === "string") {
this.declare(`declare zeroext i1 @scr_str_eq(ptr, ptr)`);
B.line(`${hit} = call zeroext i1 @sc_str_eq(ptr ${disc.name}, ptr ${t.name})`);
} else if (c.test.type.kind === "bool") {
@@ -792,9 +792,16 @@ export function emitDynamicExpr(
B.line(`${go} = and i1 ${notSame}, ${present}`);
B.condBr(go, bytes, join);
B.startBlock(bytes);
host.declare(`declare zeroext i1 @scr_str_eq(ptr, ptr)`);
const eqBytes = B.tmp();
B.line(`${eqBytes} = call zeroext i1 @sc_str_eq(ptr ${l.name}, ptr ${r.name})`);
const literal = wrappedStringLiteral(e.right) ?? wrappedStringLiteral(e.left);
if (literal !== null) {
// Both hold strings here; the literal's length and bytes compare inline.
const other = wrappedStringLiteral(e.right) !== null ? l : r;
B.line(`${eqBytes} = call zeroext i1 ${host.literalEquals(literal)}(ptr ${other.name})`);
} else {
host.declare(`declare zeroext i1 @scr_str_eq(ptr, ptr)`);
B.line(`${eqBytes} = call zeroext i1 @sc_str_eq(ptr ${l.name}, ptr ${r.name})`);
}
B.line(`store i1 ${eqBytes}, ptr ${slot}`);
B.br(join);
B.startBlock(join);
@@ -846,11 +853,19 @@ export function emitDynamicExpr(
break;
}
case "string": {
host.declare(`declare zeroext i1 @scr_str_eq(ptr, ptr)`);
const a = host.unionPeek(l.name, def.id);
const b = host.unionPeek(r.name, def.id);
const t = B.tmp();
B.line(`${t} = call zeroext i1 @sc_str_eq(ptr ${a}, ptr ${b})`);
const right = wrappedStringLiteral(e.right);
const left = wrappedStringLiteral(e.left);
if (right !== null) {
B.line(`${t} = call zeroext i1 ${host.literalEquals(right)}(ptr ${a})`);
} else if (left !== null) {
B.line(`${t} = call zeroext i1 ${host.literalEquals(left)}(ptr ${b})`);
} else {
host.declare(`declare zeroext i1 @scr_str_eq(ptr, ptr)`);
B.line(`${t} = call zeroext i1 @sc_str_eq(ptr ${a}, ptr ${b})`);
}
B.line(`store i1 ${t}, ptr ${slot}`);
break;
}
@@ -1181,3 +1196,8 @@ function emitUnionEqWrappedScalar(
B.line(`${negated} = xor i1 ${result}, true`);
return { name: negated, type: e.type };
}
/** The text of a string literal wrapped into a union, or null. */
function wrappedStringLiteral(e: IrExpr): string | null {
return e.kind === "unionWrap" && e.value.kind === "strLit" ? e.value.value : null;
}
@@ -547,9 +547,22 @@ export function emitStringExpr(
}
case "strEq": {
const [l, r] = emitStringInputs(host, [e.left, e.right]);
host.declare(`declare zeroext i1 @scr_str_eq(ptr, ptr)`);
const eq = B.tmp();
B.line(`${eq} = call zeroext i1 @sc_str_eq(ptr ${l!.name}, ptr ${r!.name})`);
// A literal side compares its length and bytes inline.
const literal =
e.right.kind === "strLit"
? { text: e.right.value, other: l! }
: e.left.kind === "strLit"
? { text: e.left.value, other: r! }
: null;
if (literal !== null) {
B.line(
`${eq} = call zeroext i1 ${host.literalEquals(literal.text)}(ptr ${literal.other.name})`,
);
} else {
host.declare(`declare zeroext i1 @scr_str_eq(ptr, ptr)`);
B.line(`${eq} = call zeroext i1 @sc_str_eq(ptr ${l!.name}, ptr ${r!.name})`);
}
if (!e.negated) return { name: eq, type: e.type };
const t = B.tmp();
B.line(`${t} = xor i1 ${eq}, true`);
@@ -353,12 +353,10 @@ export function keyedRecordReadInto(
// convert through the per-type toDyn walker (borrowed read → fresh
// +1 tree), a dyn-valued overflow hit passes through (+1 from the
// map get), and a miss is JS's undefined — the dyn singleton.
host.declare(`declare zeroext i1 @scr_str_eq(ptr, ptr)`);
for (const f of overflowOnly ? [] : shape.fields) {
const lit = host.internLiteral(f.name);
const hit = B.tmp();
B.line(
`${hit} = call zeroext i1 @sc_str_eq(ptr ${keyName}, ptr ${lit}) ; ${llvmCommentText(f.name)}`,
`${hit} = call zeroext i1 ${host.literalEquals(f.name)}(ptr ${keyName}) ; ${llvmCommentText(f.name)}`,
);
const lh = B.newLabel("rkg.h");
const ln = B.newLabel("rkg.n");
@@ -474,12 +472,10 @@ export function keyedRecordReadInto(
const payload = owned ? expr : host.retainValue(expr, vt);
return host.unionNewOwned(def.id, tag, { name: payload, type: vt });
};
host.declare(`declare zeroext i1 @scr_str_eq(ptr, ptr)`);
for (const f of overflowOnly ? [] : shape.fields) {
const lit = host.internLiteral(f.name);
const hit = B.tmp();
B.line(
`${hit} = call zeroext i1 @sc_str_eq(ptr ${keyName}, ptr ${lit}) ; ${llvmCommentText(f.name)}`,
`${hit} = call zeroext i1 ${host.literalEquals(f.name)}(ptr ${keyName}) ; ${llvmCommentText(f.name)}`,
);
const lh = B.newLabel("rkg.h");
const ln = B.newLabel("rkg.n");
@@ -291,7 +291,7 @@ test("strict equality against a wrapped literal borrows the local and stack-boxe
expect(facts(mod).has("item")).toBe(true);
for (const bits of [32, 64] as const) {
const ir = body(mod, bits);
expect(ir).toContain("@sc_str_eq");
expect(ir).toMatch(/call zeroext i1 @sc_streq_\d+\(ptr /);
expect(ir).not.toMatch(/@scr_union_(?:new|retain|release)/);
}
});
@@ -452,10 +452,10 @@ test("string comparisons decide identity, lengths and leading bytes before the r
type: BOOL,
loc,
});
const same = fn("same", [], {
const same = fn("same", ["a", "b"], {
kind: "strEq",
left: str("a"),
right: str("b"),
left: { kind: "varRef", localId: "a", type: STRING, loc },
right: { kind: "varRef", localId: "b", type: STRING, loc },
negated: false,
type: BOOL,
loc,
@@ -0,0 +1,92 @@
/** Equality against a string literal, decided inline. Strings compare as
* their UTF-8 bytes (scr_str_eq), so a literal's equality test is its byte
* length plus a few little-endian word compares against constants: no
* runtime call and no read of the literal's own storage. */
/** Literals up to this many bytes compare inline; longer ones keep the
* length test and call the runtime for the bytes. */
export const INLINE_LITERAL_BYTES = 32;
export interface LiteralWord {
readonly offset: number;
readonly bits: number;
/** The word's little-endian value, signed as LLVM prints an iN constant. */
readonly value: string;
}
/** Little-endian value of bytes[offset, offset + size) as a signed iN. */
export function littleEndianWord(bytes: Uint8Array, offset: number, size: number): string {
let value = 0n;
for (let index = size - 1; index >= 0; index--) {
value = (value << 8n) | BigInt(bytes[offset + index]!);
}
return BigInt.asIntN(size * 8, value).toString();
}
/** The literal's bytes as loads of at most eight bytes each. */
export function literalWords(bytes: Uint8Array): LiteralWord[] {
const words: LiteralWord[] = [];
for (let offset = 0; offset < bytes.length; offset += 8) {
const size = Math.min(8, bytes.length - offset);
words.push({
offset,
bits: size * 8,
value: littleEndianWord(bytes, offset, size),
});
}
return words;
}
/** An internal function `i1 (ptr)` testing a string for equality with one
* literal: the byte length first, then the bytes, which are read only once
* the length proves they exist. `literal` is the interned ScrStr, used by
* the runtime fallback for literals longer than INLINE_LITERAL_BYTES. */
export function literalEqualityDefinition(
name: string,
bytes: Uint8Array,
literal: string,
sizeType: "i32" | "i64",
attrs: string,
): string[] {
const lines = [
`define internal zeroext i1 ${name}(ptr %s) ${attrs} {`,
`entry:`,
` %len.p = getelementptr inbounds %ScrStr, ptr %s, i32 0, i32 1`,
` %len = load ${sizeType}, ptr %len.p`,
` %samelen = icmp eq ${sizeType} %len, ${bytes.length}`,
];
if (bytes.length === 0) {
lines.push(` ret i1 %samelen`, `}`);
return lines;
}
lines.push(` br i1 %samelen, label %bytes, label %no`, `bytes:`);
if (bytes.length > INLINE_LITERAL_BYTES) {
lines.push(` %eq = call zeroext i1 @scr_str_eq(ptr %s, ptr ${literal})`, ` ret i1 %eq`);
} else {
lines.push(` %data = getelementptr inbounds %ScrStr, ptr %s, i32 1`);
let any = "";
literalWords(bytes).forEach((word, index) => {
const pointer = index === 0 ? "%data" : `%p${index}`;
if (index > 0) {
lines.push(` ${pointer} = getelementptr inbounds i8, ptr %data, i32 ${word.offset}`);
}
const type = `i${word.bits}`;
lines.push(` %w${index} = load ${type}, ptr ${pointer}, align 1`);
lines.push(` %x${index} = xor ${type} %w${index}, ${word.value}`);
let wide = `%x${index}`;
if (word.bits < 64) {
wide = `%z${index}`;
lines.push(` ${wide} = zext ${type} %x${index} to i64`);
}
if (any === "") {
any = wide;
} else {
lines.push(` %o${index} = or i64 ${any}, ${wide}`);
any = `%o${index}`;
}
});
lines.push(` %eq = icmp eq i64 ${any}, 0`, ` ret i1 %eq`);
}
lines.push(`no:`, ` ret i1 false`, `}`);
return lines;
}
@@ -1,5 +1,5 @@
import { expect, test } from "vitest";
import { F64, STRING, VOID, type IrExpr, type IrModule, type IrType } from "../../ir/ir.js";
import { BOOL, F64, STRING, VOID, type IrExpr, type IrModule, type IrType } from "../../ir/ir.js";
import { emitLlvmModule } from "./emitter.js";
const loc = { file: "switch.ts", start: 0, end: 0 };
@@ -96,15 +96,88 @@ test("noninteger literals and computed tests retain source-order comparisons", (
}
});
test("string dispatch uses target-width byte lengths and checks equality inside each group", () => {
test("string dispatch selects by byte length, then by the bytes as one word", () => {
for (const bits of [32, 64] as const) {
const body = emit(STRING, ["", "a", "é", "😀", "tail", "é"].map(string), bits);
expect(body).toContain(`load i${bits}, ptr`);
expect(body).toContain(`switch i${bits}`);
expect(body).not.toContain("_str_utf16_len");
expect(body.match(/call zeroext i1 @sc_str_eq/g)).toHaveLength(5);
const dispatch = body.split("\n").find((line) => line.includes(`switch i${bits}`))!;
// Every literal fits a word: no comparison calls at all.
expect(body).not.toContain("@sc_str_eq");
expect(body).not.toContain("@sc_streq_");
const lines = body.split("\n");
const dispatch = lines.find((line) => line.includes(`switch i${bits}`))!;
expect(dispatch).toContain(`i${bits} 2, label`);
expect(dispatch.match(new RegExp(`i${bits} 4, label`, "g"))).toHaveLength(1);
// "a" = 0x61; "é" = c3 a9; "😀" = f0 9f 98 80 and "tail" share one word switch.
expect(lines.find((line) => line.includes("switch i8 "))).toContain("i8 97, label");
expect(lines.find((line) => line.includes("switch i16 "))).toContain("i16 -22077, label");
const words = lines.find(
(line) => line.includes("switch i32 ") && line.includes("i32 1818845556"),
);
expect(words).toContain("i32 -2137481232, label");
}
});
test("duplicate string labels keep the first destination", () => {
const body = emit(STRING, ["on", "off", "on"].map(string));
const words = body.split("\n").find((line) => line.includes("switch i16 "))!;
expect(words.match(/i16 28271, label/g)).toHaveLength(1);
expect(words).toContain("label %sw.c1 ");
expect(words).not.toContain("label %sw.c3 ");
});
test("long string labels select by their first word and confirm the rest inline", () => {
const long = "a-literal-longer-than-thirty-two-bytes-total";
const body = emit(STRING, ["functions", "functionz", "instanceof", long].map(string));
const lines = body.split("\n");
// "function" is the shared first word of both nine-byte labels.
const prefix = lines.find((line) => line.includes("switch i64 ") && line.includes("sw.prefix"))!;
expect(prefix.match(/i64 7957695015192261990, label/g)).toHaveLength(1);
expect(body.match(/call zeroext i1 @sc_streq_/g)).toHaveLength(4);
expect(body).not.toContain("@sc_str_eq");
const mod = emitLlvmModuleFor(["functions", long]);
const helper = (name: string) =>
new RegExp(`^define internal [^\\n]*@sc_streq_${name}\\([^]*?^}`, "m");
const short = helper("\\d+").exec(mod)![0];
expect(short).toContain("icmp eq i64 %len, 9");
expect(short).toContain("load i64, ptr %data, align 1");
expect(short).toContain("load i8, ptr %p1, align 1");
expect(short).not.toContain("@scr_str_eq");
expect(mod).toMatch(
/icmp eq i64 %len, 44\n br i1 %samelen, label %bytes, label %no\nbytes:\n %eq = call zeroext i1 @scr_str_eq/,
);
});
function emitLlvmModuleFor(labels: string[]): string {
const value: IrExpr = { kind: "varRef", localId: "value", type: STRING, loc };
const mod: IrModule = {
irVersion: 16,
sourceFile: loc.file,
entry: "main",
functions: [
{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc },
...labels.map((label, index) => ({
name: `equals${index}`,
params: [{ name: "value", localId: "value", type: STRING }],
returnType: BOOL,
locals: [{ id: "value", name: "value", type: STRING, mutable: false }],
loc,
body: [
{
kind: "return" as const,
value: {
kind: "strEq" as const,
left: value,
right: string(label),
negated: false,
type: BOOL,
loc,
},
loc,
},
],
})),
],
};
return emitLlvmModule(mod);
}
@@ -2,6 +2,7 @@ import type { IrExpr, IrStmt } from "../../ir/ir.js";
import { withinInt32 } from "../../ir/integer-ranges.js";
import { exactInteger, widenInteger } from "./integer-values.js";
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
import { littleEndianWord } from "./string-literal-equality.js";
type SwitchCase = Extract<IrStmt, { kind: "switch" }>["cases"][number];
@@ -107,32 +108,79 @@ function emitStringSwitch(
if (group === undefined) groups.set(length, (group = []));
group.push({ value: test.value, label: labels[index]! });
}
if (seen.size < 4 || groups.size < 2) return false;
if (seen.size < 2) return false;
// Strings are equal exactly when their UTF-8 bytes are (scr_str_eq): the
// byte length selects a group, and a group of literals up to eight bytes
// long selects by the bytes as one little-endian word.
const B = host.B;
const pointer = B.tmp();
const length = B.tmp();
B.line(`${pointer} = getelementptr inbounds ${host.sizeType}, ptr ${disc.name}, i32 1`);
B.line(`${length} = load ${host.sizeType}, ptr ${pointer}`);
const groupLabels = new Map<number, string>();
for (const size of groups.keys()) groupLabels.set(size, B.newLabel("sw.length"));
for (const [size, group] of groups) {
// The empty string is the only literal of length zero.
groupLabels.set(size, size === 0 ? group[0]!.label : B.newLabel("sw.length"));
}
B.terminate(
`switch ${host.sizeType} ${length}, label %${fallback} [ ${[...groupLabels]
.map(([size, label]) => `${host.sizeType} ${size}, label %${label}`)
.join(" ")} ]`,
);
host.declare("declare zeroext i1 @scr_str_eq(ptr, ptr)");
for (const [size, group] of groups) {
if (size === 0) continue;
B.startBlock(groupLabels.get(size)!);
for (let index = 0; index < group.length; index++) {
const item = group[index]!;
const literal = host.internLiteral(item.value);
const hit = B.tmp();
const next = index + 1 < group.length ? B.newLabel("sw.equal") : fallback;
B.line(`${hit} = call zeroext i1 @sc_str_eq(ptr ${disc.name}, ptr ${literal})`);
B.condBr(hit, item.label, next);
if (next !== fallback) B.startBlock(next);
if (size <= 8) {
const type = `i${size * 8}`;
const data = B.tmp();
const word = B.tmp();
B.line(`${data} = getelementptr inbounds %ScrStr, ptr ${disc.name}, i32 1`);
B.line(`${word} = load ${type}, ptr ${data}, align 1`);
// Distinct texts can share bytes (lone surrogates encode as U+FFFD);
// the first case keeps the word, as the comparison chain would.
const targets = new Map<string, string>();
for (const item of group) {
const value = littleEndianWord(Buffer.from(item.value, "utf8"), 0, size);
if (!targets.has(value)) targets.set(value, item.label);
}
B.terminate(
`switch ${type} ${word}, label %${fallback} [ ${[...targets]
.map(([value, label]) => `${type} ${value}, label %${label}`)
.join(" ")} ]`,
);
continue;
}
// Longer literals select by their first eight bytes, then confirm the
// whole literal (the helper's repeated loads fold into this block's).
const data = B.tmp();
const word = B.tmp();
B.line(`${data} = getelementptr inbounds %ScrStr, ptr ${disc.name}, i32 1`);
B.line(`${word} = load i64, ptr ${data}, align 1`);
const prefixes = new Map<string, { value: string; label: string }[]>();
for (const item of group) {
const prefix = littleEndianWord(Buffer.from(item.value, "utf8"), 0, 8);
let same = prefixes.get(prefix);
if (same === undefined) prefixes.set(prefix, (same = []));
same.push(item);
}
const prefixLabels = [...prefixes.keys()].map(() => B.newLabel("sw.prefix"));
B.terminate(
`switch i64 ${word}, label %${fallback} [ ${[...prefixes.keys()]
.map((prefix, index) => `i64 ${prefix}, label %${prefixLabels[index]!}`)
.join(" ")} ]`,
);
[...prefixes.values()].forEach((same, prefixIndex) => {
B.startBlock(prefixLabels[prefixIndex]!);
for (let index = 0; index < same.length; index++) {
const item = same[index]!;
const hit = B.tmp();
const next = index + 1 < same.length ? B.newLabel("sw.equal") : fallback;
B.line(`${hit} = call zeroext i1 ${host.literalEquals(item.value)}(ptr ${disc.name})`);
B.condBr(hit, item.label, next);
if (next !== fallback) B.startBlock(next);
}
});
}
return true;
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/4920-string-literal-switch.ts"
],
"diags": []
}
+189
View File
@@ -0,0 +1,189 @@
// String switches and literal equality: length and byte dispatch must keep
// strict equality, first-match duplicates, fallthrough, default placement,
// and every length (empty, short, word-sized, prefix-sharing, long).
function classify(word: string): string {
switch (word) {
case "":
return "empty";
case "a":
return "a";
case "b":
case "c":
return "b-or-c";
case "if":
return "kw-if";
case "in":
return "kw-in";
case "let":
return "kw-let";
case "a":
return "duplicate a (unreachable)";
case "else":
return "kw-else";
case "while":
return "kw-while";
case "return":
return "kw-return";
case "default":
return "kw-default";
case "function":
return "kw-function";
case "functions":
return "plural";
case "functionz":
return "z-suffix";
case "continue":
return "kw-continue";
case "instanceof":
return "kw-instanceof";
case "café":
return "accent";
case "日本":
return "japanese";
case "🙂":
return "emoji";
case "a-very-long-literal-that-is-longer-than-thirty-two-bytes":
return "long";
case "exactly-thirty-two-bytes-long!!!":
return "thirty-two";
default:
return "other:" + word.length;
}
}
function fallthrough(op: string): string[] {
const out: string[] = [];
switch (op) {
case "+":
out.push("plus");
case "-":
out.push("minus");
break;
default:
out.push("default");
case "*":
out.push("times");
case "/":
out.push("divide");
break;
case "%":
out.push("mod");
}
return out;
}
function twoCases(s: string): number {
switch (s) {
case "on":
return 1;
case "off":
return 0;
}
return -1;
}
let evaluations = 0;
function next(value: string): string {
evaluations++;
return value;
}
const words = [
"",
"a",
"b",
"c",
"d",
"if",
"in",
"it",
"let",
"lex",
"else",
"elsE",
"while",
"return",
"returns",
"default",
"function",
"functions",
"functionz",
"functionx",
"continue",
"instanceof",
"instanceOf",
"café",
"cafe",
"日本",
"日",
"🙂",
"🙃",
"a-very-long-literal-that-is-longer-than-thirty-two-bytes",
"a-very-long-literal-that-is-longer-than-thirty-two-bytez",
"exactly-thirty-two-bytes-long!!!",
"exactly-thirty-two-bytes-long!!?",
];
for (const word of words) {
// Built at run time, so no case shares the literal's identity.
const built = ("<" + word + ">").slice(1, word.length + 1);
console.log(JSON.stringify(word), classify(word), classify(built));
}
for (const op of ["+", "-", "*", "/", "%", "^", "", "++"]) {
console.log(JSON.stringify(op), fallthrough(op).join(","), twoCases(op + "n"));
}
console.log(twoCases("on"), twoCases("off"), twoCases("of"), twoCases("of" + "f"));
switch (next("let")) {
case "if":
console.log("if");
break;
case "let":
console.log("let", evaluations);
break;
case "else":
console.log("else");
break;
}
// Literal equality on either side, negated, in loops and on record keys.
let hits = 0;
const kinds = ["num", "ident", "op", "lparen", "rparen", "comma", "end", "identifier"];
for (let round = 0; round < 3; round++) {
for (const kind of kinds) {
const k = kind.slice(0);
if (k === "ident") hits += 1;
if ("identifier" === k) hits += 10;
if (k !== "op") hits += 100;
if (k === "") hits += 1000;
}
}
console.log("hits", hits);
interface Totals {
department: number;
salary: number;
"x-long-field-name-beyond-a-word": number;
}
const totals: Totals = { department: 1, salary: 2, "x-long-field-name-beyond-a-word": 3 };
const keys: (keyof Totals)[] = ["salary", "department", "x-long-field-name-beyond-a-word"];
let sum = 0;
for (const key of keys) {
sum = sum * 10 + totals[key];
totals[key] = totals[key] + 1;
}
console.log("record keys", sum, JSON.stringify(totals));
// Optional and mixed unions compare their string arm against the literal.
function isAbc(x: string | undefined): boolean {
return x === "abc";
}
function notLong(x: string | number): boolean {
return x !== "a-literal-longer-than-thirty-two-bytes-in-total";
}
const maybe: (string | undefined)[] = [undefined, "abc", "ab" + "c", "abd", "", "abcd"];
console.log(maybe.map(isAbc).join(","), maybe.map((x) => "abc" === x).join(","));
const mixed: (string | number)[] = [1, "a-literal-longer-than-thirty-two-bytes-in-total", "a-literal-longer-than-thirty-two-bytes-in-totaL", 0];
console.log(mixed.map(notLong).join(","));