mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-24 06:14:01 +08:00
Compare strings against literals inline
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4920-string-literal-switch.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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(","));
|
||||
Reference in New Issue
Block a user