mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Version string scans over the UTF-16 units of strings that are not ASCII
`for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 32) n++` over a text with any non-ASCII character failed the loop version's ASCII guard and ran the checked loop. Each read tested the ASCII bit, compared the string with the runtime's published scan receiver, tested the bounds, loaded the units pointer, and kept the opaque fallback. The guard now also accepts a version's one string receiver when it is not ASCII. One runtime read publishes the string as the scan receiver (ScrStrScan), whose UTF-16 units the runtime holds once a scan has read it enough. The guard then tests the accesses against the published unit count, and a third copy of the loop reads each unit with one 16-bit load from the units pointer taken in the guard. The guard takes that path only while the runtime publishes the receiver itself, so the length it took is the receiver's UTF-16 length on both fast paths. Both fast copies read it for the receiver's `length`, which a while loop's condition otherwise asks the runtime for on every iteration. The runtime drops a string's units when the string is released or appended to in place, or when its index entry is evicted because another non-ASCII string is indexed. The receiver is a binding the loop does not write, so the first two cannot happen while the loop runs. A units copy is emitted only for a loop that uses unit-safe operations (string-unit-versions.ts): numbers, locals, records and class fields, typed arrays, number arrays, Math, the receiver's charCodeAt and length, and calls of functions made of the same operations that read no string. Tests of `s.charAt(i)` against one-unit literals now become charCodeAt compares in the IR pass (char-unit-locals.ts), so a version sees one kind of read.
This commit is contained in:
@@ -580,7 +580,9 @@ test("the guard tests a string's ASCII bit and the fast copy reads bytes", () =>
|
||||
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
|
||||
const guard = body.slice(0, body.search(/^bytes\.version\.slow\d*:/m));
|
||||
expect(guard).toMatch(/load i32, ptr %\S+\n\s+%\S+ = icmp slt i32 %\S+, 0/);
|
||||
const fast = body.slice(body.lastIndexOf("bytes.version.fast"));
|
||||
// The byte copy, up to the units copy that follows it.
|
||||
const units = body.search(/^bytes\.version\.fast\.units\d+:/m);
|
||||
const fast = body.slice(body.search(/^bytes\.version\.fast\d+:/m), units < 0 ? undefined : units);
|
||||
expect(fast).toMatch(/load i8, ptr %\S+\n\s+%\S+ = uitofp i8/);
|
||||
expect(fast).not.toContain("@sc_str_char_code_at_int");
|
||||
// The checked copy keeps the inline helper and its fallback.
|
||||
|
||||
@@ -12,7 +12,13 @@ import {
|
||||
} from "./counted-loops.js";
|
||||
import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { emitStringAsciiFlag, emitStringByteLength } from "./string-reads.js";
|
||||
import {
|
||||
emitStringAsciiFlag,
|
||||
emitStringByteLength,
|
||||
STRING_CHAR_CODE_AT_DECL,
|
||||
stringScanType,
|
||||
} from "./string-reads.js";
|
||||
import { unitSafeLoop } from "./string-unit-versions.js";
|
||||
|
||||
type Node = IrExpr | IrStmt;
|
||||
type VarRef = IrExpr & { kind: "varRef" };
|
||||
@@ -73,6 +79,14 @@ export interface BytesLoopVersion {
|
||||
receivers: { ref: VarRef; global: boolean; string: boolean }[];
|
||||
/** Receivers the guard requires to have no shared storage. */
|
||||
ownedReceivers: number[];
|
||||
/** The string reads among `bounds`. */
|
||||
stringReads: ReadonlySet<Node>;
|
||||
/** The version's one string receiver, when the loop may also run over
|
||||
* its UTF-16 units if it is not ASCII (string-unit-versions.ts). */
|
||||
unitsReceiver: number | null;
|
||||
/** The units receiver's `length` reads in the loop. Both fast copies
|
||||
* read the length the guard took (its UTF-16 length) in their place. */
|
||||
unitLengths: ReadonlySet<IrExpr>;
|
||||
fixed: FixedCheck[];
|
||||
affine: AffineCheck[];
|
||||
/** While loops: accesses at a counter below a loop-invariant bound. */
|
||||
@@ -114,6 +128,9 @@ export interface BytesLoopFacts {
|
||||
/** String bindings held as a slice of another string (string-slices.ts):
|
||||
* they have no string header for the guard to read. */
|
||||
scalarStrings: ReadonlySet<string>;
|
||||
/** The functions a loop over a string's UTF-16 units may call; absent
|
||||
* or null where emitted code does not read the runtime's scan receiver. */
|
||||
unitSafeFunctions?: ReadonlySet<string> | null;
|
||||
}
|
||||
|
||||
const MAX_COST = 400;
|
||||
@@ -670,6 +687,19 @@ export function matchBytesLoopVersion(
|
||||
// Renumber the receivers the kept checks use.
|
||||
const order = [...used].sort((a, b) => a - b);
|
||||
const renumber = new Map(order.map((old, index) => [old, index]));
|
||||
const stringReads = new Set<Node>();
|
||||
for (const group of [...fixedKept, ...affineKept])
|
||||
if (receivers[group.check.receiver]!.string)
|
||||
for (const node of group.nodes) stringReads.add(node);
|
||||
for (const group of boundedKept)
|
||||
if (receivers[group.check.receiver]!.string)
|
||||
for (const { node } of group.nodes) stringReads.add(node);
|
||||
const strings = order.filter((index) => receivers[index]!.string);
|
||||
const unitLengths =
|
||||
facts.unitSafeFunctions != null && strings.length === 1
|
||||
? unitSafeLoop(loop, receivers[strings[0]!]!.ref.localId, facts.unitSafeFunctions)
|
||||
: null;
|
||||
const unitsReceiver = unitLengths !== null ? renumber.get(strings[0]!)! : null;
|
||||
const needsBound =
|
||||
affineKept.length > 0 &&
|
||||
counted !== null &&
|
||||
@@ -679,6 +709,9 @@ export function matchBytesLoopVersion(
|
||||
owned: ownedNodes,
|
||||
receivers: order.map((index) => receivers[index]!),
|
||||
ownedReceivers: ownedKept.map(([receiver]) => renumber.get(receiver)!),
|
||||
stringReads,
|
||||
unitsReceiver,
|
||||
unitLengths: unitLengths ?? new Set<IrExpr>(),
|
||||
fixed: fixedKept.map((g) => ({
|
||||
receiver: renumber.get(g.check.receiver)!,
|
||||
end: g.check.end,
|
||||
@@ -1018,13 +1051,22 @@ function findWhileCounterAccesses(
|
||||
/** Branches to `fast` when every access the version proves is in bounds for
|
||||
* every iteration and every view it proves thread-owned has no shared
|
||||
* storage, and to `slow` otherwise (including a counted loop whose
|
||||
* integer form is not established, or a module receiver not yet set). */
|
||||
* integer form is not established, or a module receiver not yet set).
|
||||
*
|
||||
* With `unitsFast`, the version's units receiver may instead be a string
|
||||
* that is not ASCII: the guard has the runtime publish it as the scan
|
||||
* receiver (one read), tests the same accesses against its UTF-16 length,
|
||||
* and branches to `unitsFast`. It returns the units pointer that copy
|
||||
* reads, which stays valid while the loop runs (string-unit-versions.ts),
|
||||
* and for a version with a units receiver, the receiver's UTF-16 length
|
||||
* on both fast paths (an i64). */
|
||||
export function emitBytesLoopVersionGuard(
|
||||
host: LlvmEmitterContext,
|
||||
version: BytesLoopVersion,
|
||||
fast: string,
|
||||
slow: string,
|
||||
): void {
|
||||
unitsFast?: string,
|
||||
): { units: string | null; length: string | null } | null {
|
||||
const B = host.B;
|
||||
const next = (cond: string): void => {
|
||||
if (cond === "true") return;
|
||||
@@ -1043,19 +1085,36 @@ export function emitBytesLoopVersionGuard(
|
||||
}
|
||||
return value;
|
||||
});
|
||||
const lengths = receivers.map((receiver, index) => {
|
||||
const len = version.receivers[index]!.string
|
||||
? emitStringByteLength(host, receiver)
|
||||
: emitBytesHeaderLength(host, receiver);
|
||||
const unitsIndex = unitsFast !== undefined ? version.unitsReceiver : null;
|
||||
const units: { read: { pointer: string; taken: string; published: string } | null } = {
|
||||
read: null,
|
||||
};
|
||||
const widen = (len: string): string => {
|
||||
if (host.sizeType === "i64") return len;
|
||||
const wide = B.tmp();
|
||||
B.line(`${wide} = zext ${host.sizeType} ${len} to i64`);
|
||||
return wide;
|
||||
};
|
||||
const lengths = receivers.map((receiver, index) => {
|
||||
if (index === unitsIndex) {
|
||||
const read = emitUnitsLength(host, receiver);
|
||||
units.read = read;
|
||||
return read.length;
|
||||
}
|
||||
return widen(
|
||||
version.receivers[index]!.string
|
||||
? emitStringByteLength(host, receiver)
|
||||
: emitBytesHeaderLength(host, receiver),
|
||||
);
|
||||
});
|
||||
const conditions: string[] = [];
|
||||
// The published count is the receiver's UTF-16 length only while the
|
||||
// runtime publishes the receiver itself.
|
||||
if (units.read) conditions.push(units.read.published);
|
||||
// A string's byte length is its UTF-16 length when it is ASCII.
|
||||
version.receivers.forEach(({ string }, index) => {
|
||||
if (string) conditions.push(emitStringAsciiFlag(host, receivers[index]!));
|
||||
if (string && index !== unitsIndex)
|
||||
conditions.push(emitStringAsciiFlag(host, receivers[index]!));
|
||||
});
|
||||
for (const index of version.ownedReceivers) {
|
||||
const owned = B.tmp();
|
||||
@@ -1158,7 +1217,7 @@ export function emitBytesLoopVersionGuard(
|
||||
// Unreachable: every term has a proven range. Stay checked.
|
||||
B.br(slow);
|
||||
B.startBlock(B.newLabel("bytes.version.dead"));
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
const wide = widenInteger(host, integer);
|
||||
const sum = B.tmp();
|
||||
@@ -1215,8 +1274,70 @@ export function emitBytesLoopVersionGuard(
|
||||
valid = all;
|
||||
}
|
||||
}
|
||||
if (valid === "true") B.br(fast);
|
||||
else B.condBr(valid, fast, slow);
|
||||
const length = version.unitsReceiver !== null ? lengths[version.unitsReceiver]! : null;
|
||||
const taken = units.read;
|
||||
const target = taken ? B.newLabel("bytes.version.units") : fast;
|
||||
if (valid === "true") B.br(target);
|
||||
else B.condBr(valid, target, slow);
|
||||
if (!taken) return { units: null, length };
|
||||
B.startBlock(target);
|
||||
B.condBr(taken.taken, unitsFast!, fast);
|
||||
return { units: taken.pointer, length };
|
||||
}
|
||||
|
||||
/** The length of a version's units receiver `s` in UTF-16 units: its byte
|
||||
* length when it is ASCII; otherwise, after one runtime read publishes it
|
||||
* as the scan receiver, the published unit count (0 when the runtime does
|
||||
* not hold its units). Also the units pointer, whether the loop is to
|
||||
* read it, and whether the published receiver is `s`. */
|
||||
function emitUnitsLength(
|
||||
host: LlvmEmitterContext,
|
||||
s: string,
|
||||
): { length: string; pointer: string; taken: string; published: string } {
|
||||
const B = host.B;
|
||||
const sz = host.sizeType;
|
||||
const type = stringScanType(sz);
|
||||
const ascii = emitStringAsciiFlag(host, s);
|
||||
const bytes = emitStringByteLength(host, s);
|
||||
const from = B.currentLabel();
|
||||
const publish = B.newLabel("bytes.version.publish"),
|
||||
join = B.newLabel("bytes.version.length");
|
||||
B.condBr(ascii, join, publish);
|
||||
B.startBlock(publish);
|
||||
host.declare(STRING_CHAR_CODE_AT_DECL);
|
||||
const read = B.tmp(),
|
||||
receiver = B.tmp(),
|
||||
same = B.tmp(),
|
||||
pointerSlot = B.tmp(),
|
||||
pointer = B.tmp(),
|
||||
countSlot = B.tmp(),
|
||||
count = B.tmp(),
|
||||
published = B.tmp();
|
||||
B.line(`${read} = call double @scr_str_char_code_at(ptr ${s}, double 0.0)`);
|
||||
B.line(`${receiver} = load ptr, ptr @scr_str_scan`);
|
||||
B.line(`${same} = icmp eq ptr ${receiver}, ${s}`);
|
||||
B.line(`${pointerSlot} = getelementptr inbounds ${type}, ptr @scr_str_scan, i32 0, i32 1`);
|
||||
B.line(`${pointer} = load ptr, ptr ${pointerSlot}`);
|
||||
B.line(`${countSlot} = getelementptr inbounds ${type}, ptr @scr_str_scan, i32 0, i32 2`);
|
||||
B.line(`${count} = load ${sz}, ptr ${countSlot}`);
|
||||
B.line(`${published} = select i1 ${same}, ${sz} ${count}, ${sz} 0`);
|
||||
const end = B.currentLabel();
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const length = B.tmp(),
|
||||
units = B.tmp(),
|
||||
taken = B.tmp();
|
||||
B.line(`${length} = phi ${sz} [ ${bytes}, %${from} ], [ ${published}, %${end} ]`);
|
||||
B.line(`${units} = phi ptr [ null, %${from} ], [ ${pointer}, %${end} ]`);
|
||||
B.line(`${taken} = phi i1 [ false, %${from} ], [ true, %${end} ]`);
|
||||
const exact = B.tmp();
|
||||
B.line(`${exact} = phi i1 [ true, %${from} ], [ ${same}, %${end} ]`);
|
||||
let wide = length;
|
||||
if (sz !== "i64") {
|
||||
wide = B.tmp();
|
||||
B.line(`${wide} = zext ${sz} ${length} to i64`);
|
||||
}
|
||||
return { length: wide, pointer: units, taken, published: exact };
|
||||
}
|
||||
|
||||
function scaleCounter(host: LlvmEmitterContext, value: string, scale: number): string {
|
||||
|
||||
@@ -61,7 +61,7 @@ import { hasLoneSurrogate, wtf8Bytes } from "../literals.js";
|
||||
import { undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { NULLABLE_ABSENT, NULLABLE_NULL } from "./nullable-unions.js";
|
||||
import { literalEqualityDefinition } from "./string-literal-equality.js";
|
||||
import { STRING_READ_HELPERS, stringScanDecl } from "./string-reads.js";
|
||||
import { STRING_READ_HELPERS, stringScanDecl, stringScanInline } from "./string-reads.js";
|
||||
import { emitLibraryIdentityLines } from "../library-identity-markers.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { targetCpuAttribute } from "../target-cpu.js";
|
||||
@@ -1586,9 +1586,7 @@ export function helperDefs(emitter: LlEmitter): string[] {
|
||||
// A scan's receiver reads inline too (ScrStrScan): from ordinary global
|
||||
// storage, or, in worker executables, from the thread's static TLS block,
|
||||
// one thread-pointer-relative load (see executableTls).
|
||||
const scanInline =
|
||||
emitter.executableTls ||
|
||||
(emitter.mod.workers !== true && emitter.mod.lib?.threadInstances !== true);
|
||||
const scanInline = stringScanInline(emitter);
|
||||
let scanDeclared = false;
|
||||
for (const helper of STRING_READ_HELPERS) {
|
||||
if (!emitter.decls.has(helper.decl)) continue;
|
||||
|
||||
@@ -2082,28 +2082,56 @@ function emitVersionedLoop(
|
||||
const fast = B.newLabel("bytes.version.fast"),
|
||||
slow = B.newLabel("bytes.version.slow"),
|
||||
done = B.newLabel("bytes.version.done");
|
||||
emitBytesLoopVersionGuard(emitter, version, fast, slow);
|
||||
const unitsFast =
|
||||
version.unitsReceiver !== null ? B.newLabel("bytes.version.fast.units") : undefined;
|
||||
const guard = emitBytesLoopVersionGuard(emitter, version, fast, slow, unitsFast);
|
||||
const units = guard?.units ?? null;
|
||||
const originalBounds = emitter.bytesBounds,
|
||||
originalOwned = emitter.bytesOwned,
|
||||
originalRanges = emitter.integerRanges;
|
||||
originalRanges = emitter.integerRanges,
|
||||
originalUnitReads = emitter.stringUnitReads,
|
||||
originalLengthReads = emitter.stringLengthReads;
|
||||
LOOPS.of(emitter).bytesLoopVersioning.add(s);
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]);
|
||||
if (version.owned.size > 0) emitter.bytesOwned = new Set([...originalOwned, ...version.owned]);
|
||||
if (version.ranges.size > 0)
|
||||
emitter.integerRanges = new Map([...originalRanges, ...version.ranges]);
|
||||
if (version.counted?.guarded || version.paired?.loop.guarded)
|
||||
LOOPS.of(emitter).provenCountedGuards.add(s);
|
||||
B.startBlock(fast);
|
||||
emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]);
|
||||
// Both fast copies read the string's length the guard took.
|
||||
const length = guard?.length;
|
||||
if (length != null && version.unitLengths.size > 0)
|
||||
emitter.stringLengthReads = new Map([
|
||||
...originalLengthReads,
|
||||
...[...version.unitLengths].map((node) => [node, length] as const),
|
||||
]);
|
||||
emitter.emitStmt(s);
|
||||
B.br(done);
|
||||
if (unitsFast !== undefined && units !== null) {
|
||||
// The string reads take the receiver's UTF-16 units.
|
||||
B.startBlock(unitsFast);
|
||||
emitter.bytesBounds = new Set([
|
||||
...originalBounds,
|
||||
...[...version.bounds].filter((node) => !version.stringReads.has(node)),
|
||||
]);
|
||||
emitter.stringUnitReads = new Map([
|
||||
...originalUnitReads,
|
||||
...[...version.stringReads].map((node) => [node, units] as const),
|
||||
]);
|
||||
emitter.emitStmt(s);
|
||||
B.br(done);
|
||||
}
|
||||
} finally {
|
||||
emitter.bytesBounds = originalBounds;
|
||||
emitter.bytesOwned = originalOwned;
|
||||
emitter.integerRanges = originalRanges;
|
||||
emitter.stringUnitReads = originalUnitReads;
|
||||
emitter.stringLengthReads = originalLengthReads;
|
||||
LOOPS.of(emitter).provenCountedGuards.delete(s);
|
||||
LOOPS.of(emitter).bytesLoopVersioning.delete(s);
|
||||
}
|
||||
|
||||
@@ -104,6 +104,8 @@ import { dropUnwrittenPropertyBags } from "../../ir/property-bags.js";
|
||||
import { freezeInitOnlyGlobals } from "../../ir/init-only-globals.js";
|
||||
import { hoistFinalFieldReads } from "../../ir/field-values.js";
|
||||
import { holdCharLocalsAsUnits } from "../../ir/char-unit-locals.js";
|
||||
import { unitSafeFunctions } from "./string-unit-versions.js";
|
||||
import { stringScanInline } from "./string-reads.js";
|
||||
import {
|
||||
findInt32Abis,
|
||||
INT32_ABI_SUFFIX,
|
||||
@@ -1477,6 +1479,12 @@ export class LlEmitter {
|
||||
* (one-unit tests and switches, charAtUnitRead), with the charAt each
|
||||
* stands for: a loop version's proof for the charAt covers its read. */
|
||||
readonly charAtUnitReads = new Map<IrExpr, IrExpr>();
|
||||
/** String reads a loop version proved in bounds of a string's UTF-16
|
||||
* units, with the units pointer to read (string-unit-versions.ts). */
|
||||
stringUnitReads: ReadonlyMap<IrExpr | IrStmt, string> = new Map();
|
||||
/** `length` reads of a loop version's string, with the i64 UTF-16
|
||||
* length its guard took (bytes-loop-versions.ts). */
|
||||
stringLengthReads: ReadonlyMap<IrExpr, string> = new Map();
|
||||
/** Worker programs: typed-array accesses whose views an enclosing version
|
||||
* guard proved thread-owned. */
|
||||
bytesOwned: ReadonlySet<IrExpr | IrStmt> = new Set();
|
||||
@@ -4305,11 +4313,21 @@ export class LlEmitter {
|
||||
workers: this.mod.workers === true,
|
||||
owned: this.bytesOwned,
|
||||
scalarStrings: new Set(this.stringSlices.keys()),
|
||||
unitSafeFunctions: this.unitSafeFunctions(),
|
||||
},
|
||||
paired,
|
||||
);
|
||||
}
|
||||
|
||||
private unitSafe: ReadonlySet<string> | null | undefined;
|
||||
/** The functions a loop over a string's UTF-16 units may call, or null
|
||||
* where emitted reads do not use the runtime's scan receiver. */
|
||||
private unitSafeFunctions(): ReadonlySet<string> | null {
|
||||
if (this.unitSafe === undefined)
|
||||
this.unitSafe = stringScanInline(this) ? unitSafeFunctions(this.mod) : null;
|
||||
return this.unitSafe;
|
||||
}
|
||||
|
||||
/** The alias tag of a whole-slot load of a binding (module globals only). */
|
||||
slotAlias(id: string): string {
|
||||
const binding = this.binding(id);
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
STRING_CHAR_CODE_AT_INT_MARKER,
|
||||
emitAsciiChar,
|
||||
emitAsciiCharCode,
|
||||
emitUnitCharCode,
|
||||
STRING_UTF16_LEN_DECL,
|
||||
} from "./string-reads.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
@@ -322,6 +323,15 @@ export function charAtUnitRead(
|
||||
return read;
|
||||
}
|
||||
|
||||
/** The UTF-16 units an enclosing loop version proved the index of `e`
|
||||
* below the count of (string-unit-versions.ts). */
|
||||
function provenUnitsRead(host: LlvmEmitterContext, e: IrExpr): string | null {
|
||||
const units = host.stringUnitReads.get(e);
|
||||
if (units !== undefined) return units;
|
||||
const origin = host.charAtUnitReads.get(e);
|
||||
return origin !== undefined ? (host.stringUnitReads.get(origin) ?? null) : null;
|
||||
}
|
||||
|
||||
/** An enclosing loop version proved the string of `e` ASCII and its index
|
||||
* below the length (bytes-loop-versions.ts). */
|
||||
function provenStringRead(host: LlvmEmitterContext, e: IrExpr): boolean {
|
||||
@@ -364,9 +374,14 @@ export function emitStrIntrinsic(
|
||||
const method = e.method;
|
||||
switch (method) {
|
||||
case "length": {
|
||||
const t = B.tmp();
|
||||
const length = host.stringLengthReads.get(e);
|
||||
if (length !== undefined) {
|
||||
B.line(`${t} = sitofp i64 ${length} to double`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
// sc_str_utf16_len (string-reads.ts) answers proven-ASCII strings inline.
|
||||
host.declare(STRING_UTF16_LEN_DECL);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call double @sc_str_utf16_len(ptr ${r.name})`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
@@ -376,6 +391,8 @@ export function emitStrIntrinsic(
|
||||
const index = e.args[0] ? exactInteger(host, args[0]!, e.args[0]) : null;
|
||||
if (index) {
|
||||
const wide = widenInteger(host, index);
|
||||
const units = provenUnitsRead(host, e);
|
||||
if (units !== null) return { name: emitUnitCharCode(host, units, wide), type: e.type };
|
||||
if (provenStringRead(host, e))
|
||||
return { name: emitAsciiCharCode(host, r.name, wide), type: e.type };
|
||||
host.declare(STRING_CHAR_CODE_AT_INT_MARKER);
|
||||
|
||||
@@ -75,6 +75,19 @@ export function emitAsciiCharCode(host: LlvmEmitterContext, s: string, index: st
|
||||
return code;
|
||||
}
|
||||
|
||||
/** Unit `index` (an i64, below their count) of a string's UTF-16 `units`
|
||||
* (ScrStrScan): one 16-bit load, as a double. */
|
||||
export function emitUnitCharCode(host: LlvmEmitterContext, units: string, index: string): string {
|
||||
const B = host.B;
|
||||
const pointer = B.tmp(),
|
||||
unit = B.tmp(),
|
||||
code = B.tmp();
|
||||
B.line(`${pointer} = getelementptr inbounds i16, ptr ${units}, i64 ${index}`);
|
||||
B.line(`${unit} = load i16, ptr ${pointer}`);
|
||||
B.line(`${code} = uitofp i16 ${unit} to double`);
|
||||
return code;
|
||||
}
|
||||
|
||||
/** The one-character string of unit `index` (an i64) of a proven-ASCII
|
||||
* string `s` whose length covers it: the immortal `@sc_ascii_chars` entry
|
||||
* for its byte, as the inline charAt helpers answer. */
|
||||
@@ -114,6 +127,15 @@ function loadLength(sz: string): string[] {
|
||||
];
|
||||
}
|
||||
|
||||
/** Whether emitted reads use the runtime's scan receiver: from ordinary
|
||||
* global storage, or, in worker executables, from the thread's static TLS
|
||||
* block (executableTls). */
|
||||
export function stringScanInline(host: LlvmEmitterContext): boolean {
|
||||
return (
|
||||
host.executableTls || (host.mod.workers !== true && host.mod.lib?.threadInstances !== true)
|
||||
);
|
||||
}
|
||||
|
||||
/** The runtime's ScrStrScan record and the declaration naming it. */
|
||||
export function stringScanType(sz: string): string {
|
||||
return `{ ptr, ptr, ${sz} }`;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../../index.js";
|
||||
import type { IrModule, IrStmt } from "../../ir/ir.js";
|
||||
import { unitSafeFunctions, unitSafeLoop } from "./string-unit-versions.js";
|
||||
|
||||
async function lower(source: string): Promise<IrModule> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-unit-versions-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
return deserializeModule(await readFile(outPath, "utf8"));
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const loopOf = (mod: IrModule, name: string): IrStmt & { kind: "for" } =>
|
||||
mod.functions.find((f) => f.name === name)!.body.find((s) => s.kind === "for") as IrStmt & {
|
||||
kind: "for";
|
||||
};
|
||||
|
||||
test("loops over a string's units call only functions that read no string", async () => {
|
||||
const mod = await lower(`
|
||||
function lower(c: number): boolean {
|
||||
return c >= 97 && c <= 122;
|
||||
}
|
||||
function word(c: number): boolean {
|
||||
return lower(c) || (c >= 48 && c <= 57) || Math.abs(c - 200) < 50;
|
||||
}
|
||||
function codeOf(t: string, i: number): number {
|
||||
return t.charCodeAt(i);
|
||||
}
|
||||
function viaCodeOf(t: string): number {
|
||||
return codeOf(t, 0);
|
||||
}
|
||||
function words(s: string): number {
|
||||
let n = 0;
|
||||
const starts = [0];
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
if (word(s.charCodeAt(i))) n++;
|
||||
else starts.push(i);
|
||||
}
|
||||
return n + starts.length;
|
||||
}
|
||||
function pairs(s: string, t: string): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < s.length; i++) n += s.charCodeAt(i) + codeOf(t, i);
|
||||
return n;
|
||||
}
|
||||
function both(s: string, t: string): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < s.length; i++) n += s.charCodeAt(i) + t.charCodeAt(i);
|
||||
return n;
|
||||
}
|
||||
function concatenates(s: string): string {
|
||||
let out = "";
|
||||
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 32) out += "-";
|
||||
return out;
|
||||
}
|
||||
console.log(words("ab c"), pairs("ab", "cd"), both("ab", "cd"), concatenates("a b"), viaCodeOf("x"));
|
||||
`);
|
||||
const safe = unitSafeFunctions(mod);
|
||||
expect(safe.has("lower")).toBe(true);
|
||||
expect(safe.has("word")).toBe(true);
|
||||
// A string read, directly or through a call, makes a function unsafe.
|
||||
expect(safe.has("codeOf")).toBe(false);
|
||||
expect(safe.has("viaCodeOf")).toBe(false);
|
||||
|
||||
const param = (fn: string, index = 0): string =>
|
||||
mod.functions.find((f) => f.name === fn)!.params[index]!.localId;
|
||||
// A unit-safe loop answers its receiver's length reads: here the
|
||||
// condition's `s.length`.
|
||||
const lengths = unitSafeLoop(loopOf(mod, "words"), param("words"), safe);
|
||||
expect(lengths?.size).toBe(1);
|
||||
expect([...lengths!].every((e) => e.kind === "strIntrinsic" && e.method === "length")).toBe(true);
|
||||
expect(unitSafeLoop(loopOf(mod, "pairs"), param("pairs"), safe)).toBeNull();
|
||||
expect(unitSafeLoop(loopOf(mod, "both"), param("both"), safe)).toBeNull();
|
||||
expect(unitSafeLoop(loopOf(mod, "concatenates"), param("concatenates"), safe)).toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
import type { IrExpr, IrModule, IrStmt } from "../../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
|
||||
/* Loop versions over the UTF-16 units of a string that is not ASCII.
|
||||
*
|
||||
* The runtime publishes the receiver of an indexed scan with its UTF-16
|
||||
* units (ScrStrScan, string-reads.ts), and drops them when the string is
|
||||
* released or appended to in place, or when its index entry is evicted:
|
||||
* the runtime indexes another non-ASCII string. A loop version's string
|
||||
* receiver is a binding the loop does not write, which keeps it alive and
|
||||
* unappended for the whole loop. Its fast copy over units therefore only
|
||||
* needs a body that indexes no other string: the operations below, and
|
||||
* calls of functions made of the same operations without string reads
|
||||
* (`unitSafeFunctions`). */
|
||||
|
||||
const ARRAY_METHODS = new Set(["push", "pop", "length"]);
|
||||
|
||||
/** Whether `e` (or statement `s`) uses only operations that index no
|
||||
* string: numbers, booleans, locals, records and class fields, typed
|
||||
* arrays, number arrays and Math. `reads` admits the string reads a
|
||||
* version covers; `calls` the callees. */
|
||||
function unitSafe(
|
||||
node: { e: IrExpr } | { s: IrStmt },
|
||||
reads: (e: IrExpr & { kind: "strIntrinsic" }) => boolean,
|
||||
calls: (callee: string) => boolean,
|
||||
): boolean {
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
case "numLit":
|
||||
case "boolLit":
|
||||
case "bin":
|
||||
case "unary":
|
||||
case "logical":
|
||||
case "ternary":
|
||||
case "toBool":
|
||||
case "seqExpr":
|
||||
case "assignExpr":
|
||||
case "incDec":
|
||||
case "recordGet":
|
||||
case "fieldGet":
|
||||
case "bytesIntrinsic":
|
||||
break;
|
||||
case "arrayGet":
|
||||
if (e.type.kind !== "f64" && e.type.kind !== "bool") return false;
|
||||
break;
|
||||
case "arrIntrinsic": {
|
||||
// Growing or shrinking an array of numbers allocates; it reads no
|
||||
// string.
|
||||
const array = e.receiver.type;
|
||||
if (
|
||||
!ARRAY_METHODS.has(e.method) ||
|
||||
array.kind !== "array" ||
|
||||
(array.elem.kind !== "f64" && array.elem.kind !== "bool")
|
||||
)
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
case "libCall":
|
||||
if (!e.fn.startsWith("math.")) return false;
|
||||
break;
|
||||
case "call":
|
||||
if (!calls(e.callee)) return false;
|
||||
break;
|
||||
case "strIntrinsic":
|
||||
if (!reads(e)) return false;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
switch (s.kind) {
|
||||
case "varDecl":
|
||||
case "assign":
|
||||
case "exprStmt":
|
||||
case "if":
|
||||
case "block":
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "for":
|
||||
case "break":
|
||||
case "continue":
|
||||
case "return":
|
||||
case "recordSet":
|
||||
case "fieldSet":
|
||||
case "bytesSet":
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
return "e" in node ? expr(node.e) : stmt(node.s);
|
||||
}
|
||||
|
||||
/** The module's functions a units version may call: synchronous
|
||||
* functions without captures whose bodies use only unit-safe operations,
|
||||
* read no string, and call only such functions. */
|
||||
export function unitSafeFunctions(mod: IrModule): ReadonlySet<string> {
|
||||
const candidates = new Map(
|
||||
mod.functions
|
||||
.filter((fn) => !fn.async && !fn.generator && !(fn.captures?.length ?? 0))
|
||||
.map((fn) => [fn.name, fn]),
|
||||
);
|
||||
const safe = new Set(candidates.keys());
|
||||
for (let settled = false; !settled;) {
|
||||
settled = true;
|
||||
for (const name of safe) {
|
||||
const fn = candidates.get(name)!;
|
||||
const ok = fn.body.every((s) =>
|
||||
unitSafe(
|
||||
{ s },
|
||||
() => false,
|
||||
(callee) => safe.has(callee),
|
||||
),
|
||||
);
|
||||
if (!ok) {
|
||||
safe.delete(name);
|
||||
settled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return safe;
|
||||
}
|
||||
|
||||
/** The `length` reads of the binding `receiver` in a loop whose every
|
||||
* part is unit-safe, reading only charCodeAt and length of `receiver` and
|
||||
* calling only `safe` functions; null for any other loop. */
|
||||
export function unitSafeLoop(
|
||||
loop: IrStmt & { kind: "for" | "while" },
|
||||
receiver: string,
|
||||
safe: ReadonlySet<string>,
|
||||
): ReadonlySet<IrExpr> | null {
|
||||
const lengths = new Set<IrExpr>();
|
||||
const reads = (e: IrExpr & { kind: "strIntrinsic" }): boolean => {
|
||||
if (e.receiver.kind !== "varRef" || e.receiver.localId !== receiver) return false;
|
||||
if (e.method === "length") lengths.add(e);
|
||||
return e.method === "charCodeAt" || e.method === "length";
|
||||
};
|
||||
const calls = (callee: string): boolean => safe.has(callee);
|
||||
return unitSafe({ s: loop }, reads, calls) ? lengths : null;
|
||||
}
|
||||
@@ -96,3 +96,26 @@ console.log(escapes("a b", 1), rebound(" x", 0), wide("ab", 0), against("a", "b
|
||||
expect(fn(after, name)).toBe(fn(before, name));
|
||||
}
|
||||
});
|
||||
|
||||
test("charAt tests in place compare charCodeAt", async () => {
|
||||
const mod = holdCharLocalsAsUnits(
|
||||
await lower(`
|
||||
function tests(s: string, i: number): number {
|
||||
let n = 0;
|
||||
if (s.charAt(i) === " ") n += 1;
|
||||
if ("a" <= s.charAt(i) && s.charAt(i) < "n") n += 2;
|
||||
if (s.charAt(i) === "ab") n += 4;
|
||||
return n;
|
||||
}
|
||||
console.log(tests("a b", 1), tests("abc", 0), tests("", 2));
|
||||
`),
|
||||
);
|
||||
// The two-unit literal keeps its string comparison.
|
||||
expect(reads(fn(mod, "tests"))).toEqual([
|
||||
"charCodeAt",
|
||||
"charCodeAt",
|
||||
"charCodeAt",
|
||||
"strEq",
|
||||
"charAt",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -12,6 +12,8 @@ import { mapExprChildren, mapStmtChildren } from "./traverse.js";
|
||||
type CharAt = IrExpr & { kind: "strIntrinsic" };
|
||||
type UnitUse = IrExpr & { kind: "strEq" | "strCmp" };
|
||||
|
||||
const FLIPPED = { "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as const;
|
||||
|
||||
/** Character locals held as their code unit.
|
||||
*
|
||||
* `const c = s[i]` (or `s.charAt(i)`) followed only by tests such as
|
||||
@@ -28,7 +30,8 @@ type UnitUse = IrExpr & { kind: "strEq" | "strCmp" };
|
||||
*
|
||||
* Only immutable unboxed string locals qualify, with one declaration whose
|
||||
* initializer is a one-argument charAt, and with no other mention of the
|
||||
* local. */
|
||||
* local. The same tests of `s.charAt(i)` in place compare
|
||||
* `s.charCodeAt(i)`, so a loop version sees one kind of read. */
|
||||
export function holdCharLocalsAsUnits(mod: IrModule): IrModule {
|
||||
let changed = false;
|
||||
const functions = mod.functions.map((fn) => {
|
||||
@@ -43,7 +46,6 @@ function rewriteFunction(fn: IrFunction): IrFunction {
|
||||
const candidates = new Set(
|
||||
fn.locals.filter((l) => !l.mutable && !l.boxed && l.type.kind === "string").map((l) => l.id),
|
||||
);
|
||||
if (candidates.size === 0) return fn;
|
||||
// Every mention of a candidate, in any field, against the mentions the
|
||||
// rewrite understands.
|
||||
const mentions = new Map<string, number>();
|
||||
@@ -67,7 +69,7 @@ function rewriteFunction(fn: IrFunction): IrFunction {
|
||||
for (const key in node) if (key !== "loc") count(node[key]);
|
||||
}
|
||||
};
|
||||
count(fn.body);
|
||||
if (candidates.size > 0) count(fn.body);
|
||||
const held = new Set<string>();
|
||||
for (const id of candidates)
|
||||
if (
|
||||
@@ -76,7 +78,7 @@ function rewriteFunction(fn: IrFunction): IrFunction {
|
||||
mentions.get(id) === understood.get(id)
|
||||
)
|
||||
held.add(id);
|
||||
if (held.size === 0) return fn;
|
||||
let changed = false;
|
||||
|
||||
const unitRef = (id: string, loc: IrExpr["loc"]): IrExpr => ({
|
||||
kind: "varRef",
|
||||
@@ -88,38 +90,28 @@ function rewriteFunction(fn: IrFunction): IrFunction {
|
||||
if (e.kind === "strEq" || e.kind === "strCmp") {
|
||||
const use = unitUse(e);
|
||||
if (use && held.has(use.id)) {
|
||||
const code: IrExpr = { kind: "numLit", value: use.code, type: F64, loc: e.loc };
|
||||
const c = unitRef(use.id, e.loc);
|
||||
if (e.kind === "strEq")
|
||||
return {
|
||||
kind: "bin",
|
||||
op: e.negated ? "!==" : "===",
|
||||
left: c,
|
||||
right: code,
|
||||
type: BOOL,
|
||||
loc: e.loc,
|
||||
};
|
||||
// The comparison with the local on the left.
|
||||
const op = use.left
|
||||
? e.op
|
||||
: ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as const)[e.op];
|
||||
if (op === ">" || op === ">=")
|
||||
return { kind: "bin", op, left: c, right: code, type: BOOL, loc: e.loc };
|
||||
const negation: IrExpr = {
|
||||
kind: "bin",
|
||||
op: op === "<" ? ">=" : ">",
|
||||
left: c,
|
||||
right: code,
|
||||
type: BOOL,
|
||||
loc: e.loc,
|
||||
changed = true;
|
||||
return charUnitTest(e, unitRef(use.id, e.loc), use.code, use.left);
|
||||
}
|
||||
// `s.charAt(i)` in place: its unit read, `s.charCodeAt(i)`.
|
||||
const direct = charAtTest(e);
|
||||
if (direct) {
|
||||
changed = true;
|
||||
const read: IrExpr = {
|
||||
...direct.charAt,
|
||||
method: "charCodeAt",
|
||||
receiver: rewriteExpr(direct.charAt.receiver),
|
||||
args: direct.charAt.args.map(rewriteExpr),
|
||||
type: F64,
|
||||
};
|
||||
return { kind: "unary", op: "!", operand: negation, type: BOOL, loc: e.loc };
|
||||
return charUnitTest(e, read, direct.code, direct.left);
|
||||
}
|
||||
}
|
||||
return mapExprChildren(e, rewriteExpr, rewriteStmt);
|
||||
};
|
||||
const rewriteStmt = (s: IrStmt): IrStmt => {
|
||||
if (s.kind === "varDecl" && held.has(s.localId) && s.init?.kind === "strIntrinsic")
|
||||
if (s.kind === "varDecl" && held.has(s.localId) && s.init?.kind === "strIntrinsic") {
|
||||
changed = true;
|
||||
return {
|
||||
...s,
|
||||
init: {
|
||||
@@ -130,7 +122,9 @@ function rewriteFunction(fn: IrFunction): IrFunction {
|
||||
type: F64,
|
||||
},
|
||||
};
|
||||
if (s.kind === "switch" && s.disc.kind === "varRef" && held.has(s.disc.localId))
|
||||
}
|
||||
if (s.kind === "switch" && s.disc.kind === "varRef" && held.has(s.disc.localId)) {
|
||||
changed = true;
|
||||
return {
|
||||
...s,
|
||||
disc: unitRef(s.disc.localId, s.disc.loc),
|
||||
@@ -143,11 +137,14 @@ function rewriteFunction(fn: IrFunction): IrFunction {
|
||||
body: c.body.map(rewriteStmt),
|
||||
})),
|
||||
};
|
||||
}
|
||||
return mapStmtChildren(s, rewriteExpr, rewriteStmt);
|
||||
};
|
||||
const body = fn.body.map(rewriteStmt);
|
||||
if (!changed) return fn;
|
||||
return {
|
||||
...fn,
|
||||
body: fn.body.map(rewriteStmt),
|
||||
body,
|
||||
locals: fn.locals.map((l) => (held.has(l.id) ? unitLocal(l) : l)),
|
||||
};
|
||||
}
|
||||
@@ -179,6 +176,45 @@ function understoodMention(node: Record<string, unknown>): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A one-unit test of a string that reads one character or "" (charAt),
|
||||
* on its unit `unit`, which reads NaN for "": an equality, or an ordered
|
||||
* comparison with the unit on the left when `unitLeft`. "" is below every
|
||||
* one-unit literal, and NaN fails every ordered comparison, so `c < "x"`
|
||||
* becomes `!(c >= 120)`. */
|
||||
function charUnitTest(e: UnitUse, unit: IrExpr, code: number, unitLeft: boolean): IrExpr {
|
||||
const literal: IrExpr = { kind: "numLit", value: code, type: F64, loc: e.loc };
|
||||
if (e.kind === "strEq")
|
||||
return {
|
||||
kind: "bin",
|
||||
op: e.negated ? "!==" : "===",
|
||||
left: unit,
|
||||
right: literal,
|
||||
type: BOOL,
|
||||
loc: e.loc,
|
||||
};
|
||||
const op = unitLeft ? e.op : FLIPPED[e.op];
|
||||
if (op === ">" || op === ">=")
|
||||
return { kind: "bin", op, left: unit, right: literal, type: BOOL, loc: e.loc };
|
||||
const negation: IrExpr = {
|
||||
kind: "bin",
|
||||
op: op === "<" ? ">=" : ">",
|
||||
left: unit,
|
||||
right: literal,
|
||||
type: BOOL,
|
||||
loc: e.loc,
|
||||
};
|
||||
return { kind: "unary", op: "!", operand: negation, type: BOOL, loc: e.loc };
|
||||
}
|
||||
|
||||
/** A test between `s.charAt(i)` and a literal of one code unit. */
|
||||
function charAtTest(e: UnitUse): { charAt: CharAt; code: number; left: boolean } | null {
|
||||
const literalRight = e.right.kind === "strLit";
|
||||
const literal = literalRight ? e.right : e.left;
|
||||
const other = literalRight ? e.left : e.right;
|
||||
if (literal.kind !== "strLit" || literal.value.length !== 1 || !isCharAt(other)) return null;
|
||||
return { charAt: other, code: literal.value.charCodeAt(0), left: literalRight };
|
||||
}
|
||||
|
||||
function isCharAt(e: IrExpr): e is CharAt {
|
||||
return e.kind === "strIntrinsic" && e.method === "charAt" && e.args.length === 1;
|
||||
}
|
||||
|
||||
@@ -43,8 +43,12 @@ console.log(spaces("a b c"), letters("abC1z"), chars("xyz").join("-"), chars("é
|
||||
ll.match(
|
||||
new RegExp(`define internal [^\\n]+ @sc_bf_${name}\\([^\\n]*\\) #0 \\{[\\s\\S]*?\\n\\}`),
|
||||
)![0];
|
||||
// The fast copy follows the checked one.
|
||||
const fast = (fn: string): string => fn.slice(fn.lastIndexOf("bytes.version.fast"));
|
||||
// The byte copy follows the checked one, and the units copy (for
|
||||
// strings that are not ASCII) follows it.
|
||||
const fast = (fn: string): string => {
|
||||
const units = fn.search(/^bytes\.version\.fast\.units\d+:/m);
|
||||
return fn.slice(fn.search(/^bytes\.version\.fast\d+:/m), units < 0 ? undefined : units);
|
||||
};
|
||||
|
||||
// `s[i] === " "` compares the byte; the checked copy keeps the helper.
|
||||
const spaces = body("spaces");
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../src/index.js";
|
||||
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
|
||||
|
||||
test("scans of strings that are not ASCII read their UTF-16 units", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-unit-loops-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
await writeFile(
|
||||
entry,
|
||||
`
|
||||
function spaces(s: string): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 32) n++;
|
||||
return n;
|
||||
}
|
||||
function skip(s: string): number {
|
||||
let pos = 0;
|
||||
while (pos < s.length && s.charCodeAt(pos) !== 32) pos++;
|
||||
return pos;
|
||||
}
|
||||
function pairs(s: string, t: string): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === t.charCodeAt(i % t.length)) n++;
|
||||
return n;
|
||||
}
|
||||
let text = "";
|
||||
for (let k = 0; k < 400; k++) text += k % 3 === 0 ? "café " : "naïve façade ";
|
||||
const other = text.replace(/a/g, "ä");
|
||||
let total = 0;
|
||||
for (let pass = 0; pass < 4; pass++)
|
||||
total += spaces(text) + spaces(other) + pairs(text, other) + skip(text);
|
||||
console.log(total, spaces("plain ascii text"), spaces(""));
|
||||
`,
|
||||
);
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
const lowered = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
||||
if (!lowered.ok)
|
||||
throw new Error(lowered.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8")));
|
||||
const body = (name: string): string =>
|
||||
ll.match(
|
||||
new RegExp(
|
||||
`define internal [^\\n]+ @sc_bf_${name}(?:\\.i32)?\\([^\\n]*\\) #0 \\{[\\s\\S]*?\\n\\}`,
|
||||
),
|
||||
)![0];
|
||||
|
||||
const spaces = body("spaces");
|
||||
// A string that is not ASCII is published as the scan receiver once,
|
||||
// before the loop, and its copy loads units.
|
||||
expect(spaces).toMatch(/call double @scr_str_char_code_at\(ptr %\S+, double 0\.0\)/);
|
||||
// The copies follow the checked one: bytes, then units.
|
||||
const bytesAt = spaces.search(/^bytes\.version\.fast\d+:/m);
|
||||
const unitsAt = spaces.search(/^bytes\.version\.fast\.units\d+:/m);
|
||||
expect(bytesAt).toBeGreaterThan(0);
|
||||
expect(unitsAt).toBeGreaterThan(bytesAt);
|
||||
const units = spaces.slice(unitsAt);
|
||||
expect(units).toMatch(/load i16, ptr %\S+\n\s+%\S+ = uitofp i16/);
|
||||
expect(units).not.toContain("@sc_str_char_code_at_int");
|
||||
// An ASCII string still reads bytes.
|
||||
const bytes = spaces.slice(bytesAt, unitsAt);
|
||||
expect(bytes).toMatch(/load i8, ptr %\S+\n\s+%\S+ = uitofp i8/);
|
||||
expect(bytes).not.toContain("@sc_str_char_code_at_int");
|
||||
// A while loop over a counter below the length reads units too.
|
||||
const skip = body("skip");
|
||||
expect(skip).toMatch(/^bytes\.version\.fast\.units\d+:/m);
|
||||
// Its fast copies read the length the guard took; the checked copy
|
||||
// still asks for it.
|
||||
const skipFastAt = skip.search(/^bytes\.version\.fast\d+:/m);
|
||||
expect(skip.slice(0, skipFastAt)).toContain("@sc_str_utf16_len");
|
||||
expect(skip.slice(skipFastAt)).not.toContain("@sc_str_utf16_len");
|
||||
// Two strings cannot both be the scan receiver.
|
||||
expect(body("pairs")).not.toContain("bytes.version.fast.units");
|
||||
|
||||
const exe = join(dir, "main");
|
||||
const built = await compile(entry, { outputKind: "exe", outDir: dir, outPath: exe });
|
||||
if (!built.ok) throw new Error(JSON.stringify(built.diagnostics));
|
||||
let text = "";
|
||||
for (let k = 0; k < 400; k++) text += k % 3 === 0 ? "café " : "naïve façade ";
|
||||
const other = text.replace(/a/g, "ä");
|
||||
const count = (s: string): number => s.split(" ").length - 1;
|
||||
let pairs = 0;
|
||||
for (let i = 0; i < text.length; i++)
|
||||
if (text.charCodeAt(i) === other.charCodeAt(i % other.length)) pairs++;
|
||||
const total = 4 * (count(text) + count(other) + pairs + text.indexOf(" "));
|
||||
expect(execFileSync(exe, { encoding: "utf8" })).toBe(`${total} 2 0\n`);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7305-string-unit-loop-versions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
// charCodeAt scans of strings that are not ASCII, repeated so the runtime
|
||||
// holds their UTF-16 units: counts, hashes, neighbor reads, descending
|
||||
// loops, reads past the end, line starts pushed to a number array, helper
|
||||
// functions that read no string, a helper that reads another string, a
|
||||
// second non-ASCII string read in the same loop, two texts scanned in
|
||||
// turn, a short text, a text appended to between scans, surrogate pairs
|
||||
// and lone surrogates, ASCII texts beside them, and while loops over a
|
||||
// counter below the length: word and identifier scans, peeks past the
|
||||
// counter, and a lexer class whose position is a field. Loops also read
|
||||
// the length in their bodies, and loops over a fresh text that no scan
|
||||
// has published run from its end and over its last units.
|
||||
|
||||
function isWordUnit(c: number): boolean {
|
||||
return (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || c > 127;
|
||||
}
|
||||
|
||||
function isWordOrDigit(c: number): boolean {
|
||||
return isWordUnit(c) || (c >= 48 && c <= 57);
|
||||
}
|
||||
|
||||
function spaces(s: string): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 32) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
function hash(s: string): number {
|
||||
let h = 7;
|
||||
for (let i = 0; i < s.length; i++) h = (Math.imul(h, 31) + s.charCodeAt(i)) | 0;
|
||||
return h;
|
||||
}
|
||||
|
||||
function rises(s: string): number {
|
||||
let n = 0;
|
||||
for (let i = 1; i < s.length - 1; i++)
|
||||
if (s.charCodeAt(i) > s.charCodeAt(i - 1) && s.charCodeAt(i) >= s.charCodeAt(i + 1)) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
function backwards(s: string): number {
|
||||
let h = 0;
|
||||
for (let i = s.length - 1; i >= 0; i -= 3) h = (h * 7 + s.charCodeAt(i)) % 1000003;
|
||||
return h;
|
||||
}
|
||||
|
||||
function pastEnd(s: string): string {
|
||||
let sum = 0;
|
||||
for (let i = 0; i <= s.length; i++) sum += s.charCodeAt(i);
|
||||
return String(sum);
|
||||
}
|
||||
|
||||
function lineStarts(s: string): number[] {
|
||||
const starts = [0];
|
||||
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 10) starts.push(i + 1);
|
||||
return starts;
|
||||
}
|
||||
|
||||
function wordUnits(s: string): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < s.length; i++) if (isWordOrDigit(s.charCodeAt(i))) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
function codeOf(t: string, i: number): number {
|
||||
return t.charCodeAt(i % t.length);
|
||||
}
|
||||
|
||||
function mixed(s: string, t: string): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < s.length; i++) n = (n + s.charCodeAt(i) * codeOf(t, i)) % 1000003;
|
||||
return n;
|
||||
}
|
||||
|
||||
function sameAt(a: string, b: string): number {
|
||||
let n = 0;
|
||||
const end = Math.min(a.length, b.length);
|
||||
for (let i = 0; i < end; i++) if (a.charCodeAt(i) === b.charCodeAt(i)) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
function surrogates(s: string): string {
|
||||
let high = 0,
|
||||
low = 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const c = s.charCodeAt(i);
|
||||
if (c >= 0xd800 && c < 0xdc00) high++;
|
||||
else if (c >= 0xdc00 && c < 0xe000) low++;
|
||||
}
|
||||
return `${high}/${low}`;
|
||||
}
|
||||
|
||||
function wordCount(s: string): number {
|
||||
let pos = 0,
|
||||
n = 0;
|
||||
const end = s.length;
|
||||
while (pos < end) {
|
||||
while (pos < end && s.charCodeAt(pos) === 32) pos++;
|
||||
if (pos >= end) break;
|
||||
n++;
|
||||
while (pos < end && s.charCodeAt(pos) !== 32) pos++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function wordUnitRuns(s: string): number {
|
||||
let pos = 0,
|
||||
total = 0;
|
||||
while (pos < s.length) {
|
||||
if (isWordUnit(s.charCodeAt(pos))) {
|
||||
const start = pos;
|
||||
while (pos < s.length && isWordOrDigit(s.charCodeAt(pos))) pos++;
|
||||
total += pos - start;
|
||||
} else pos = (pos + 1) | 0;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
function doubled(s: string): number {
|
||||
let pos = 0,
|
||||
n = 0;
|
||||
while (pos < s.length) {
|
||||
if (s.charCodeAt(pos) === s.charCodeAt(pos + 1)) n++;
|
||||
pos += 1;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
class Scanner {
|
||||
pos = 0;
|
||||
src: string;
|
||||
constructor(src: string) {
|
||||
this.src = src;
|
||||
}
|
||||
line(): number {
|
||||
const src = this.src;
|
||||
const start = this.pos;
|
||||
let pos = start;
|
||||
while (pos < src.length && src.charCodeAt(pos) !== 10) pos++;
|
||||
this.pos = pos + 1;
|
||||
return pos - start;
|
||||
}
|
||||
}
|
||||
|
||||
function lineLengths(s: string): number {
|
||||
const scanner = new Scanner(s);
|
||||
let total = 0,
|
||||
lines = 0;
|
||||
while (scanner.pos < s.length) {
|
||||
total = (total * 31 + scanner.line()) % 1000003;
|
||||
lines++;
|
||||
}
|
||||
return total * 1000 + lines;
|
||||
}
|
||||
|
||||
function remaining(s: string): number {
|
||||
let pos = 0;
|
||||
let sum = 0;
|
||||
while (pos < s.length) {
|
||||
if (s.charCodeAt(pos) === 32) sum += s.length - pos;
|
||||
pos++;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
function lastLineLength(s: string): number {
|
||||
let last = s.length;
|
||||
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 10) last = s.length - i - 1;
|
||||
return last;
|
||||
}
|
||||
|
||||
function from(s: string, start: number): number {
|
||||
let pos = start;
|
||||
let sum = 0;
|
||||
while (pos < s.length) {
|
||||
sum = sum * 3 + s.charCodeAt(pos);
|
||||
pos++;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
const words = ["Größe", "naïve", "façade", "東京", "ok", "✓", "mañana", "plain", "😀", "end\n"];
|
||||
function text(count: number, seed: number): string {
|
||||
let out = "";
|
||||
for (let k = 0; k < count; k++) out += words[(k * 7 + seed) % words.length] + (k % 5 === 0 ? " " : " ");
|
||||
return out;
|
||||
}
|
||||
|
||||
const big = text(3000, 1);
|
||||
const other = text(2000, 4);
|
||||
const ascii = big.replace(/[^\x00-\x7f]/g, "x");
|
||||
const short = "café ✓ 東京";
|
||||
const lone = "a\uD800b\uDFFFc " + big.slice(0, 500) + "\uD83D";
|
||||
for (let pass = 0; pass < 3; pass++) {
|
||||
console.log(
|
||||
pass,
|
||||
spaces(big),
|
||||
hash(big),
|
||||
rises(big),
|
||||
backwards(big),
|
||||
pastEnd(big),
|
||||
lineStarts(big).length,
|
||||
wordUnits(big),
|
||||
);
|
||||
console.log(" ", spaces(other), hash(other), spaces(big), hash(other), wordUnits(other));
|
||||
console.log(" ", mixed(big, other), sameAt(big, other), surrogates(big), surrogates(lone));
|
||||
console.log(" ", spaces(ascii), hash(ascii), spaces(short), hash(short), hash(lone), rises(lone));
|
||||
console.log(" ", wordCount(big), wordUnitRuns(big), doubled(big), lineLengths(big));
|
||||
console.log(" ", wordCount(ascii), wordUnitRuns(other), doubled(lone), lineLengths(lone), wordCount(""));
|
||||
const fresh = text(40 + pass, pass) + "ü";
|
||||
console.log(" ", remaining(big), remaining(ascii), lastLineLength(big), lastLineLength(other));
|
||||
console.log(" ", from(fresh, fresh.length), from(fresh, fresh.length - 3), from(big, big.length - 4));
|
||||
}
|
||||
|
||||
let grown = text(1500, 2);
|
||||
for (let round = 0; round < 3; round++) {
|
||||
console.log("grown", round, grown.length, spaces(grown), hash(grown), hash(grown));
|
||||
grown += text(200, round) + "Ünïcödé";
|
||||
}
|
||||
console.log("empty", spaces(""), hash(""), pastEnd(""), lineStarts("").length);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 10500, "san": 14500 }
|
||||
Reference in New Issue
Block a user