mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Inline destructured literal results and read ASCII slice ranges inline
- A function whose whole body is `return [a, b]` or `return { a, b }`,
with number, boolean or string fields, is now inlined where its result
is only destructured (`const [head, tail] = split(text, at)`). The
parameters are declared from the arguments, then each field straight
into its destructured binding, in the literal's order; an unread field
still evaluates into a binding of its own. No record is built, and a
slice field read only through its length or char codes becomes a
scalar slice that copies nothing. Arguments may own their value, but
as for numeric records nothing inside an argument may hold a
temporary of its own.
- A scalar slice of a proven-ASCII string computes its range inline:
the bounds clamp over the byte length exactly as scr_str_slice_range
does, and no boundary can split a surrogate pair. Other strings keep
the runtime call, which gains the same ASCII fast path.
- value-records' tuples section, `[head, tail] = split(...)` read only
through lengths, goes from two heap slices per step to none.
This commit is contained in:
@@ -68,7 +68,7 @@ import {
|
||||
type StoredSplitSnapshot,
|
||||
type StringSplit,
|
||||
} from "./split-loops.js";
|
||||
import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
|
||||
import { scalarizeLiteralResults, scalarizeNumericRecords } from "../../ir/scalar-records.js";
|
||||
import { addRecordResultEntries, resultEntryName } from "./record-results.js";
|
||||
import { specializeCallbackCalls } from "../../ir/callback-specialization.js";
|
||||
import {
|
||||
@@ -578,27 +578,29 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
? selectInt32Arrays(
|
||||
versionParameters(
|
||||
addRecordResultEntries(
|
||||
scalarizeNumericRecords(
|
||||
specializeCallbackCalls(
|
||||
specializeNumericCalls(
|
||||
inlineExpressionFunctions(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(
|
||||
dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)),
|
||||
scalarizeLiteralResults(
|
||||
scalarizeNumericRecords(
|
||||
specializeCallbackCalls(
|
||||
specializeNumericCalls(
|
||||
inlineExpressionFunctions(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(
|
||||
dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -286,8 +286,9 @@ function bump(s: State): State { return { ...s, count: s.count + 1 }; }
|
||||
export function tokens(n: number, w: string): number {
|
||||
const t = { kind: "id", text: w, pos: n };
|
||||
const u = make("num", w, n);
|
||||
const [q, r] = divmod(n, 7);
|
||||
return t.pos + width(u) + width({ kind: w, text: w, pos: 0 }) + q + r;
|
||||
// Read by index: a destructured literal result is inlined instead.
|
||||
const qr = divmod(n, 7);
|
||||
return t.pos + width(u) + width({ kind: w, text: w, pos: 0 }) + qr[0] + qr[1];
|
||||
}
|
||||
export function states(n: number, w: string): number {
|
||||
let s: State = { name: w, count: 0 };
|
||||
|
||||
@@ -8,6 +8,7 @@ import { everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse
|
||||
import { F64_INF, f64Lit, sizeToDouble } from "./common.js";
|
||||
import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { emitStringAsciiFlag, emitStringByteLength } from "./string-reads.js";
|
||||
|
||||
type Slice = IrExpr & { kind: "strIntrinsic" };
|
||||
|
||||
@@ -66,6 +67,58 @@ export function findScalarStringSlices(
|
||||
return slices;
|
||||
}
|
||||
|
||||
/** scr_slice_unit: a slice()/substring() bound `v` over `length` units. A
|
||||
* non-negative bound clamps to the length; a negative one counts from the
|
||||
* end for slice (truncated toward zero) and is 0 for substring, as is NaN. */
|
||||
function emitSliceUnit(
|
||||
host: LlvmEmitterContext,
|
||||
v: string,
|
||||
length: string,
|
||||
substring: boolean,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const size = host.sizeType;
|
||||
const lengthNumber = B.tmp(),
|
||||
nonNegative = B.tmp(),
|
||||
past = B.tmp(),
|
||||
whole = B.tmp(),
|
||||
clamped = B.tmp(),
|
||||
result = B.tmp();
|
||||
B.line(`${lengthNumber} = ${sizeToDouble(size)} ${size} ${length} to double`);
|
||||
B.line(`${nonNegative} = fcmp oge double ${v}, 0.0`);
|
||||
B.line(`${past} = fcmp oge double ${v}, ${lengthNumber}`);
|
||||
// Frozen: the conversion is only used where the bound is in range.
|
||||
const raw = B.tmp();
|
||||
B.line(`${raw} = fptosi double ${v} to i64`);
|
||||
B.line(`${whole} = freeze i64 ${raw}`);
|
||||
let unit = whole;
|
||||
if (size !== "i64") {
|
||||
unit = B.tmp();
|
||||
B.line(`${unit} = trunc i64 ${whole} to ${size}`);
|
||||
}
|
||||
B.line(`${clamped} = select i1 ${past}, ${size} ${length}, ${size} ${unit}`);
|
||||
if (substring) {
|
||||
B.line(`${result} = select i1 ${nonNegative}, ${size} ${clamped}, ${size} 0`);
|
||||
return result;
|
||||
}
|
||||
// Negative: (-length, -1] counts back from the end; (-1, 0) truncates to
|
||||
// 0, and at or below -length, and NaN, give 0.
|
||||
const negLength = B.tmp(),
|
||||
inside = B.tmp(),
|
||||
zero = B.tmp(),
|
||||
back = B.tmp(),
|
||||
counted = B.tmp(),
|
||||
negative = B.tmp();
|
||||
B.line(`${negLength} = fneg double ${lengthNumber}`);
|
||||
B.line(`${inside} = fcmp ogt double ${v}, ${negLength}`);
|
||||
B.line(`${zero} = icmp eq ${size} ${unit}, 0`);
|
||||
B.line(`${back} = add ${size} ${length}, ${unit}`);
|
||||
B.line(`${counted} = select i1 ${zero}, ${size} 0, ${size} ${back}`);
|
||||
B.line(`${negative} = select i1 ${inside}, ${size} ${counted}, ${size} 0`);
|
||||
B.line(`${result} = select i1 ${nonNegative}, ${size} ${clamped}, ${size} ${negative}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
export interface StringSliceSnapshot {
|
||||
source: string;
|
||||
range: string;
|
||||
@@ -87,10 +140,55 @@ export function emitStringSliceSnapshot(
|
||||
`${range} = alloca { ${host.sizeType}, ${host.sizeType}, i32 }`,
|
||||
);
|
||||
B.line(`store ptr ${source.name}, ptr ${sourceSlot}`);
|
||||
const start = args[0]?.name ?? f64Lit(0),
|
||||
end = args[1]?.name ?? F64_INF;
|
||||
const substring = slice.method === "substring";
|
||||
// A proven-ASCII receiver's units are its bytes, so the range is the
|
||||
// runtime's clamped bounds over its byte length, and no boundary splits a
|
||||
// surrogate pair (scr_str_slice_range); other strings ask the runtime.
|
||||
const ascii = B.newLabel("slice.range.ascii"),
|
||||
runtime = B.newLabel("slice.range.runtime"),
|
||||
done = B.newLabel("slice.range.done");
|
||||
B.condBr(emitStringAsciiFlag(host, source.name), ascii, runtime);
|
||||
B.startBlock(ascii);
|
||||
const size = host.sizeType;
|
||||
const length = emitStringByteLength(host, source.name);
|
||||
let from = emitSliceUnit(host, start, length, substring),
|
||||
to = emitSliceUnit(host, end, length, substring);
|
||||
if (substring) {
|
||||
const swap = B.tmp(),
|
||||
low = B.tmp(),
|
||||
high = B.tmp();
|
||||
B.line(`${swap} = icmp ugt ${size} ${from}, ${to}`);
|
||||
B.line(`${low} = select i1 ${swap}, ${size} ${to}, ${size} ${from}`);
|
||||
B.line(`${high} = select i1 ${swap}, ${size} ${from}, ${size} ${to}`);
|
||||
from = low;
|
||||
to = high;
|
||||
}
|
||||
const nonEmpty = B.tmp(),
|
||||
difference = B.tmp(),
|
||||
units = B.tmp(),
|
||||
startPtr = B.tmp(),
|
||||
lengthPtr = B.tmp(),
|
||||
splitPtr = B.tmp();
|
||||
B.line(`${nonEmpty} = icmp ugt ${size} ${to}, ${from}`);
|
||||
B.line(`${difference} = sub ${size} ${to}, ${from}`);
|
||||
B.line(`${units} = select i1 ${nonEmpty}, ${size} ${difference}, ${size} 0`);
|
||||
const rangeType = `{ ${size}, ${size}, i32 }`;
|
||||
B.line(`${startPtr} = getelementptr inbounds ${rangeType}, ptr ${range}, i32 0, i32 0`);
|
||||
B.line(`store ${size} ${from}, ptr ${startPtr}`);
|
||||
B.line(`${lengthPtr} = getelementptr inbounds ${rangeType}, ptr ${range}, i32 0, i32 1`);
|
||||
B.line(`store ${size} ${units}, ptr ${lengthPtr}`);
|
||||
B.line(`${splitPtr} = getelementptr inbounds ${rangeType}, ptr ${range}, i32 0, i32 2`);
|
||||
B.line(`store i32 0, ptr ${splitPtr}`);
|
||||
B.br(done);
|
||||
B.startBlock(runtime);
|
||||
host.declare("declare void @scr_str_slice_range(ptr, double, double, i1 zeroext, ptr)");
|
||||
B.line(
|
||||
`call void @scr_str_slice_range(ptr ${source.name}, double ${args[0]?.name ?? f64Lit(0)}, double ${args[1]?.name ?? F64_INF}, i1 ${slice.method === "substring" ? "true" : "false"}, ptr ${range})`,
|
||||
`call void @scr_str_slice_range(ptr ${source.name}, double ${start}, double ${end}, i1 ${substring ? "true" : "false"}, ptr ${range})`,
|
||||
);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
host.toFrame(source);
|
||||
return { source: sourceSlot, range };
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
} from "./ir.js";
|
||||
import { scalarizeNumericRecords } from "./scalar-records.js";
|
||||
import { scalarizeLiteralResults, scalarizeNumericRecords } from "./scalar-records.js";
|
||||
import { validateModule } from "./validate.js";
|
||||
|
||||
const loc = { file: "scalar-records.ts", start: 0, end: 0 };
|
||||
@@ -284,3 +284,161 @@ test("bounds expansion in callers with many eligible calls", () => {
|
||||
expect(remaining).toBeGreaterThan(0);
|
||||
expect(remaining).toBeLessThan(300);
|
||||
});
|
||||
|
||||
const tuple: IrType = { kind: "record", shapeId: "t0" };
|
||||
const strRef = (localId: string): IrExpr => ref(localId, STRING);
|
||||
const element = (localId: string, field: string, type: IrType): IrStmt => ({
|
||||
kind: "varDecl",
|
||||
localId,
|
||||
init: { kind: "recordGet", obj: ref("result", tuple), shapeId: "t0", field, type, loc },
|
||||
loc,
|
||||
});
|
||||
|
||||
/** `split(text, at)` returning `[text.slice(0, at), text.slice(at), at > 0]`,
|
||||
* and a `main` destructuring it. */
|
||||
function literalFixture(after: IrStmt[]): IrModule {
|
||||
const slice = (args: IrExpr[]): IrExpr => ({
|
||||
kind: "strIntrinsic",
|
||||
method: "slice",
|
||||
receiver: strRef("text"),
|
||||
args,
|
||||
type: STRING,
|
||||
loc,
|
||||
});
|
||||
return {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "main",
|
||||
records: [
|
||||
{
|
||||
id: "t0",
|
||||
fields: [
|
||||
{ name: "0", type: STRING },
|
||||
{ name: "1", type: STRING },
|
||||
{ name: "2", type: BOOL },
|
||||
],
|
||||
tuple: true,
|
||||
},
|
||||
],
|
||||
functions: [
|
||||
{
|
||||
name: "split",
|
||||
params: [
|
||||
{ localId: "text", name: "text", type: STRING },
|
||||
{ localId: "at", name: "at", type: F64 },
|
||||
],
|
||||
returnType: tuple,
|
||||
locals: [
|
||||
{ id: "text", name: "text", type: STRING, mutable: false },
|
||||
{ id: "at", name: "at", type: F64, mutable: false },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "recordLit",
|
||||
type: tuple,
|
||||
fields: [
|
||||
{ name: "0", value: slice([num(0), ref("at")]) },
|
||||
{ name: "1", value: slice([ref("at")]) },
|
||||
{
|
||||
name: "2",
|
||||
value: { kind: "bin", op: ">", left: ref("at"), right: num(0), type: BOOL, loc },
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
{
|
||||
name: "main",
|
||||
params: [{ localId: "source", name: "source", type: STRING }],
|
||||
returnType: VOID,
|
||||
locals: [
|
||||
{ id: "source", name: "source", type: STRING, mutable: false },
|
||||
{ id: "result", name: "result", type: tuple, mutable: false },
|
||||
{ id: "head", name: "head", type: STRING, mutable: false },
|
||||
{ id: "tail", name: "tail", type: STRING, mutable: true },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "result",
|
||||
init: call("split", [strRef("source"), call("effect", [num(1)])], tuple),
|
||||
loc,
|
||||
},
|
||||
...after,
|
||||
],
|
||||
loc,
|
||||
},
|
||||
{
|
||||
name: "effect",
|
||||
params: [{ localId: "x", name: "x", type: F64 }],
|
||||
returnType: F64,
|
||||
locals: [{ id: "x", name: "x", type: F64, mutable: true }],
|
||||
body: [{ kind: "return", value: ref("x"), loc }],
|
||||
loc,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function literalTransformed(mod: IrModule): IrModule {
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
const out = scalarizeLiteralResults(mod);
|
||||
expect(validateModule(out)).toEqual([]);
|
||||
return out;
|
||||
}
|
||||
|
||||
test("declares destructured elements from a literal producer's fields in evaluation order", () => {
|
||||
const mod = literalFixture([element("head", "0", STRING), element("tail", "1", STRING)]);
|
||||
const snapshot = structuredClone(mod);
|
||||
const out = literalTransformed(mod);
|
||||
expect(mod).toEqual(snapshot);
|
||||
const caller = main(out);
|
||||
expect(nodes(caller.body, "recordGet")).toEqual([]);
|
||||
expect(nodes(caller.body, "call").map((c) => c["callee"])).toEqual(["effect"]);
|
||||
const declared = caller.body.map((s) => (s.kind === "varDecl" ? s.localId : s.kind));
|
||||
// Parameters, then every field in the literal's order: the unread third
|
||||
// field still evaluates, into a binding of its own.
|
||||
expect(declared.slice(2)).toEqual(["head", "tail", declared[4]]);
|
||||
expect(caller.locals.find((l) => l.id === declared[4])?.type).toEqual(BOOL);
|
||||
expect(caller.locals.some((l) => l.id === "result")).toBe(false);
|
||||
// Slices stay slices of the renamed parameter.
|
||||
expect(nodes(caller.body, "strIntrinsic").map((n) => (n["receiver"] as IrExpr).kind)).toEqual([
|
||||
"varRef",
|
||||
"varRef",
|
||||
]);
|
||||
});
|
||||
|
||||
test("keeps the call when the result is read other than by adjacent element declarations", () => {
|
||||
const escaped = literalFixture([
|
||||
element("head", "0", STRING),
|
||||
{ kind: "exprStmt", expr: ref("result", tuple), loc },
|
||||
]);
|
||||
expect(literalTransformed(escaped)).toBe(escaped);
|
||||
const apart = literalFixture([
|
||||
element("head", "0", STRING),
|
||||
{ kind: "exprStmt", expr: num(0), loc },
|
||||
element("tail", "1", STRING),
|
||||
]);
|
||||
expect(literalTransformed(apart)).toBe(apart);
|
||||
});
|
||||
|
||||
test("keeps the call when an argument holds a counted temporary of its own", () => {
|
||||
const mod = literalFixture([element("head", "0", STRING)]);
|
||||
const decl = main(mod).body[0] as IrStmt & { kind: "varDecl" };
|
||||
const length: IrExpr = {
|
||||
kind: "strIntrinsic",
|
||||
method: "length",
|
||||
receiver: { kind: "strLit", value: "abc", type: STRING, loc },
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
(decl.init as IrExpr & { kind: "call" }).args[1] = length;
|
||||
expect(literalTransformed(mod)).toBe(mod);
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "./ir.js";
|
||||
import {
|
||||
everyExpr,
|
||||
transformExpr,
|
||||
everyExprChild,
|
||||
everyStmtChild,
|
||||
everyStmtList,
|
||||
@@ -362,3 +363,323 @@ export function scalarizeNumericRecords(mod: IrModule): IrModule {
|
||||
});
|
||||
return changed ? { ...mod, functions } : mod;
|
||||
}
|
||||
|
||||
/** A producer whose whole body is `return <literal>`, with number, boolean
|
||||
* or string fields, inlined where its result is destructured. */
|
||||
interface LiteralProducer {
|
||||
fn: IrFunction;
|
||||
shape: IrRecordShape;
|
||||
literal: IrExpr & { kind: "recordLit" };
|
||||
size: number;
|
||||
}
|
||||
|
||||
/** Whether `value` holds no counted temporary of its own: every counted
|
||||
* node in it only views a binding, so evaluating it in its own statement
|
||||
* shortens no lifetime. A message argument of a throw counts as nothing: it
|
||||
* lives until the throw, which ends the evaluation anyway. */
|
||||
function viewsOnly(value: IrExpr): boolean {
|
||||
if (!isRefCounted(value.type)) return everyExprChild(value, viewsOnly, () => false);
|
||||
switch (value.kind) {
|
||||
case "varRef":
|
||||
return true;
|
||||
case "unionNarrow":
|
||||
return viewsOnly(value.value);
|
||||
case "ternary":
|
||||
return viewsOnly(value.cond) && viewsOnly(value.then) && viewsOnly(value.else_);
|
||||
case "libCall":
|
||||
return value.fn === "error.nodeThrow";
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** The node's own value may be counted (it moves into the binding that
|
||||
* takes it); everything under it must hold no temporary (viewsOnly), as
|
||||
* scalarTemporaries requires for the numeric pass. */
|
||||
function ownedTemporaries(value: IrExpr): boolean {
|
||||
return everyExprChild(value, viewsOnly, () => false);
|
||||
}
|
||||
|
||||
const LITERAL_FIELD_KINDS: ReadonlySet<string> = new Set(["f64", "bool", "string"]);
|
||||
|
||||
function literalProducer(
|
||||
fn: IrFunction,
|
||||
shapes: ReadonlyMap<string, IrRecordShape>,
|
||||
): LiteralProducer | null {
|
||||
if (
|
||||
fn.async ||
|
||||
fn.generator ||
|
||||
fn.captures?.length ||
|
||||
fn.classCaptures?.length ||
|
||||
fn.returnType.kind !== "record" ||
|
||||
fn.body.length !== 1
|
||||
)
|
||||
return null;
|
||||
const shape = shapes.get(fn.returnType.shapeId);
|
||||
if (
|
||||
!shape ||
|
||||
shape.indexValue ||
|
||||
shape.fields.length === 0 ||
|
||||
shape.fields.length > MAX_FIELDS ||
|
||||
shape.fields.some((f) => !LITERAL_FIELD_KINDS.has(f.type.kind))
|
||||
)
|
||||
return null;
|
||||
// Every local is a parameter: the body declares nothing.
|
||||
const params = new Set(fn.params.map((p) => p.localId));
|
||||
if (fn.locals.some((l) => l.boxed || l.tdz || !params.has(l.id))) return null;
|
||||
const ret = fn.body[0]!;
|
||||
const literal = ret.kind === "return" ? ret.value : null;
|
||||
if (
|
||||
literal?.kind !== "recordLit" ||
|
||||
literal.type.kind !== "record" ||
|
||||
literal.type.shapeId !== shape.id ||
|
||||
literal.fields.length !== shape.fields.length ||
|
||||
!literal.fields.every(
|
||||
(f) =>
|
||||
!f.overflow &&
|
||||
shape.fields.some((sf) => sf.name === f.name && sf.type.kind === f.value.type.kind) &&
|
||||
ownedTemporaries(f.value),
|
||||
)
|
||||
)
|
||||
return null;
|
||||
let size = 0;
|
||||
const small = everyExpr(literal, {
|
||||
expr: (node) =>
|
||||
++size <= MAX_CALLEE_NODES &&
|
||||
node.kind !== "closure" &&
|
||||
node.kind !== "selfRef" &&
|
||||
node.kind !== "awaitExpr" &&
|
||||
node.kind !== "awaitUnionExpr" &&
|
||||
node.kind !== "yieldExpr",
|
||||
stmt: () => false,
|
||||
});
|
||||
return small ? { fn, shape, literal, size } : null;
|
||||
}
|
||||
|
||||
/** Rewrite every statement list under `body`, innermost first. */
|
||||
function mapStatementLists(body: IrStmt[], f: (list: IrStmt[]) => IrStmt[]): IrStmt[] {
|
||||
const lists = (s: IrStmt): IrStmt => {
|
||||
switch (s.kind) {
|
||||
case "if":
|
||||
return {
|
||||
...s,
|
||||
then: mapStatementLists(s.then, f),
|
||||
else_: s.else_ === null ? null : mapStatementLists(s.else_, f),
|
||||
};
|
||||
case "block":
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "for":
|
||||
case "forOf":
|
||||
return { ...s, body: mapStatementLists(s.body, f) };
|
||||
case "switch":
|
||||
return {
|
||||
...s,
|
||||
cases: s.cases.map((c) => ({
|
||||
...c,
|
||||
body: mapStatementLists(c.body, f),
|
||||
})),
|
||||
};
|
||||
case "tryCatch":
|
||||
return {
|
||||
...s,
|
||||
tryBody: mapStatementLists(s.tryBody, f),
|
||||
catchBody: s.catchBody === null ? null : mapStatementLists(s.catchBody, f),
|
||||
finallyBody: s.finallyBody === null ? null : mapStatementLists(s.finallyBody, f),
|
||||
};
|
||||
default:
|
||||
return s;
|
||||
}
|
||||
};
|
||||
return f(body.map(lists));
|
||||
}
|
||||
|
||||
/** How often each local is read, anywhere in `fn`. */
|
||||
function localReads(fn: IrFunction): Map<string, number> {
|
||||
const reads = new Map<string, number>();
|
||||
const count = (id: string): void => {
|
||||
reads.set(id, (reads.get(id) ?? 0) + 1);
|
||||
};
|
||||
everyStmtList(fn.body, {
|
||||
expr: (node) => {
|
||||
switch (node.kind) {
|
||||
case "varRef":
|
||||
case "incDec":
|
||||
case "assignExpr":
|
||||
count(node.localId);
|
||||
break;
|
||||
case "closure":
|
||||
case "classRef":
|
||||
for (const id of node.captures ?? []) count(id);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
stmt: (node) => {
|
||||
if (node.kind === "assign" || node.kind === "forOf" || node.kind === "rethrow")
|
||||
count(node.localId);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return reads;
|
||||
}
|
||||
|
||||
/** Inline small literal producers where the result is only destructured.
|
||||
*
|
||||
* `const [head, tail] = split(text, at)`, where `split` is
|
||||
* `return [text.slice(0, at), text.slice(at)]`, lowers to a result binding
|
||||
* and one declaration per element read from it, right after the call. Each
|
||||
* such declaration now takes its field's value directly: the parameters are
|
||||
* declared from the arguments in order, then the fields in the literal's
|
||||
* order (unread fields go to fresh bindings), then the destructured
|
||||
* bindings, so evaluation order is unchanged and no record is built. A
|
||||
* slice element read only through its length then needs no copy at all
|
||||
* (findScalarStringSlices). The declarations stay in the caller's own
|
||||
* statement list, so every binding keeps its scope; parameters live to the
|
||||
* end of it, which only lengthens the arguments' lifetimes. Every other use
|
||||
* of the result binding keeps the call. */
|
||||
export function scalarizeLiteralResults(mod: IrModule): IrModule {
|
||||
const shapes = new Map((mod.records ?? []).map((s) => [s.id, s]));
|
||||
const producers = new Map<string, LiteralProducer>();
|
||||
for (const fn of mod.functions) {
|
||||
const p = literalProducer(fn, shapes);
|
||||
if (p) producers.set(fn.name, p);
|
||||
}
|
||||
if (producers.size === 0) return mod;
|
||||
let changed = false;
|
||||
const functions = mod.functions.map((fn): IrFunction => {
|
||||
if (fn.async || fn.generator) return fn;
|
||||
const locals = new Map(fn.locals.map((l) => [l.id, l]));
|
||||
const used = new Set([...locals.keys(), ...(mod.globals ?? []).map((g) => g.id)]);
|
||||
collectLabels(fn.body, used);
|
||||
let next = 0;
|
||||
const fresh = (): string => {
|
||||
let id: string;
|
||||
do {
|
||||
id = `%literal.${next++}`;
|
||||
} while (used.has(id));
|
||||
used.add(id);
|
||||
return id;
|
||||
};
|
||||
let reads: Map<string, number> | null = null;
|
||||
const added: IrLocal[] = [];
|
||||
const removed = new Set<string>();
|
||||
let budget = MAX_INLINE_NODES;
|
||||
const rewrite = (list: IrStmt[]): IrStmt[] => {
|
||||
const out: IrStmt[] = [];
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const decl = list[i]!;
|
||||
const call = decl.kind === "varDecl" ? decl.init : null;
|
||||
const p = call?.kind === "call" ? producers.get(call.callee) : undefined;
|
||||
const local = decl.kind === "varDecl" ? locals.get(decl.localId) : undefined;
|
||||
if (
|
||||
decl.kind !== "varDecl" ||
|
||||
call?.kind !== "call" ||
|
||||
!p ||
|
||||
!local ||
|
||||
p.fn.name === fn.name ||
|
||||
p.size > budget ||
|
||||
local.mutable ||
|
||||
local.boxed ||
|
||||
local.tdz ||
|
||||
local.type.kind !== "record" ||
|
||||
local.type.shapeId !== p.shape.id ||
|
||||
call.args.length !== p.fn.params.length ||
|
||||
!call.args.every(ownedTemporaries)
|
||||
) {
|
||||
out.push(decl);
|
||||
continue;
|
||||
}
|
||||
// The element declarations right after the call, one per field.
|
||||
const elements = new Map<string, IrStmt & { kind: "varDecl" }>();
|
||||
let j = i + 1;
|
||||
for (; j < list.length; j++) {
|
||||
const element = list[j]!;
|
||||
const get = element.kind === "varDecl" ? element.init : null;
|
||||
const target = element.kind === "varDecl" ? locals.get(element.localId) : undefined;
|
||||
if (
|
||||
element.kind !== "varDecl" ||
|
||||
get?.kind !== "recordGet" ||
|
||||
get.obj.kind !== "varRef" ||
|
||||
get.obj.localId !== local.id ||
|
||||
get.shapeId !== p.shape.id ||
|
||||
elements.has(get.field) ||
|
||||
!target ||
|
||||
target.boxed ||
|
||||
target.tdz ||
|
||||
target.type.kind !== get.type.kind
|
||||
)
|
||||
break;
|
||||
elements.set(get.field, element);
|
||||
}
|
||||
reads ??= localReads(fn);
|
||||
if (elements.size === 0 || reads.get(local.id) !== elements.size) {
|
||||
out.push(decl);
|
||||
continue;
|
||||
}
|
||||
budget -= p.size;
|
||||
changed = true;
|
||||
removed.add(local.id);
|
||||
const renamed = new Map(p.fn.params.map((param) => [param.localId, fresh()]));
|
||||
for (const l of p.fn.locals) {
|
||||
// Copied field by field: the native compiler does not spread a
|
||||
// record whose type has a union.
|
||||
const copy: IrLocal = {
|
||||
id: renamed.get(l.id)!,
|
||||
name: l.name,
|
||||
type: l.type,
|
||||
mutable: false,
|
||||
};
|
||||
if (l.source !== undefined) copy.source = l.source;
|
||||
added.push(copy);
|
||||
}
|
||||
p.fn.params.forEach((param, index) => {
|
||||
out.push({
|
||||
kind: "varDecl",
|
||||
localId: renamed.get(param.localId)!,
|
||||
init: call.args[index]!,
|
||||
loc: decl.loc,
|
||||
});
|
||||
});
|
||||
const rename = (value: IrExpr): IrExpr =>
|
||||
transformExpr(value, {
|
||||
expr: (node) =>
|
||||
node.kind === "varRef" && renamed.has(node.localId)
|
||||
? { ...node, localId: renamed.get(node.localId)! }
|
||||
: node,
|
||||
stmt: (node) => node,
|
||||
});
|
||||
for (const field of p.literal.fields) {
|
||||
const element = elements.get(field.name);
|
||||
let id = element?.localId;
|
||||
if (id === undefined) {
|
||||
id = fresh();
|
||||
const type = p.shape.fields.find((f) => f.name === field.name)!.type;
|
||||
added.push({
|
||||
id,
|
||||
name: `${local.name}.${field.name}`,
|
||||
type,
|
||||
mutable: false,
|
||||
});
|
||||
}
|
||||
out.push({
|
||||
kind: "varDecl",
|
||||
localId: id,
|
||||
init: rename(field.value),
|
||||
loc: element?.loc ?? decl.loc,
|
||||
});
|
||||
}
|
||||
i = j - 1;
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const body = mapStatementLists(fn.body, rewrite);
|
||||
if (!changed || removed.size === 0) return fn;
|
||||
return {
|
||||
...fn,
|
||||
locals: [...fn.locals.filter((l) => !removed.has(l.id)), ...added],
|
||||
body,
|
||||
};
|
||||
});
|
||||
return changed ? { ...mod, functions } : mod;
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8241-literal-result-slices.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -2210,6 +2210,17 @@ static void scr_str_slice_bounds(ScrStr *s, ScrSidx *e, double start, double end
|
||||
|
||||
void scr_str_slice_range(ScrStr *s, double start, double end, bool substring,
|
||||
ScrStringSlice *out) {
|
||||
if (__builtin_expect(s->ascii, 1)) {
|
||||
/* Units are bytes: no index entry, and no boundary splits a pair. */
|
||||
size_t length = s->len;
|
||||
size_t from = scr_slice_unit(start, length, substring);
|
||||
size_t to = scr_slice_unit(end, length, substring);
|
||||
if (substring && from > to) { size_t swap = from; from = to; to = swap; }
|
||||
out->start = from;
|
||||
out->length = to > from ? to - from : 0;
|
||||
out->split = 0;
|
||||
return;
|
||||
}
|
||||
ScrSidx *e = scr_sidx_for(s);
|
||||
size_t from, to;
|
||||
scr_str_slice_bounds(s, e, start, end, substring, &from, &to);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// Destructured results of small literal producers: string slices, numbers
|
||||
// and booleans, with evaluation order, unread fields, throws and every
|
||||
// slice() and substring() bound kind.
|
||||
let trace = "";
|
||||
function mark<T>(tag: string, value: T): T {
|
||||
trace += tag;
|
||||
return value;
|
||||
}
|
||||
|
||||
function split(text: string, at: number): [string, string] {
|
||||
return [text.slice(0, at), text.slice(at)];
|
||||
}
|
||||
function cut(text: string, from: number, to: number): [string, number, boolean] {
|
||||
return [text.substring(from, to), text.length, from > to];
|
||||
}
|
||||
function divmod(a: number, b: number): [number, number] {
|
||||
return [Math.floor(a / b), a % b];
|
||||
}
|
||||
function pair(text: string): { head: string; size: number } {
|
||||
return { size: mark("s", text.length), head: mark("h", text.slice(1, -1)) };
|
||||
}
|
||||
|
||||
const bounds = [0, 1, 2, 3, -1, -2, -3, -10, 10, 0.5, -0.5, 1.9, -1.9, -0, NaN, Infinity, -Infinity];
|
||||
const texts = ["", "a", "ab", "abcdef", "héllo", "東京ok", "a\u{1F600}b"];
|
||||
let lengths = 0;
|
||||
let units = 0;
|
||||
for (const text of texts) {
|
||||
for (const at of bounds) {
|
||||
const [head, tail] = split(text, at);
|
||||
lengths = lengths * 7 + head.length * 3 + tail.length;
|
||||
lengths %= 1000000007;
|
||||
if (head.length > 0) units += head.charCodeAt(head.length - 1);
|
||||
if (tail.length > 0) units += tail.charCodeAt(0);
|
||||
for (const to of bounds) {
|
||||
const [part, size, swapped] = cut(text, at, to);
|
||||
lengths = (lengths * 5 + part.length + size + (swapped ? 1 : 0)) % 1000000007;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("lengths", lengths, "units", units);
|
||||
|
||||
// The pieces themselves, as strings.
|
||||
const [left, right] = split("scriptc", 3);
|
||||
console.log(left, right, `${left}|${right}`);
|
||||
|
||||
// Arguments, then fields in the literal's order, then the bindings.
|
||||
trace = "";
|
||||
const { head, size } = pair(mark("a", "[inner]"));
|
||||
console.log("order", trace, head.length, size);
|
||||
|
||||
// Unread fields still run.
|
||||
trace = "";
|
||||
const [onlyHead] = split(mark("x", "unread"), mark("y", 2));
|
||||
const { size: onlySize } = pair("<>");
|
||||
console.log("unread", trace, onlyHead, onlySize);
|
||||
|
||||
// A mutable destructured binding.
|
||||
let [first, rest] = split("mutable", 1);
|
||||
first += "!";
|
||||
console.log("let", first, rest.length);
|
||||
|
||||
// Numbers and a throw from an argument.
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const [q, r] = divmod(i * 31, 17);
|
||||
sum += q * 3 + r;
|
||||
}
|
||||
console.log("divmod", sum);
|
||||
const list: (string | undefined)[] = ["xy", undefined];
|
||||
for (const item of list) {
|
||||
try {
|
||||
const [a, b] = split(item!, 1);
|
||||
console.log("ok", a.length, b.length);
|
||||
} catch (error) {
|
||||
console.log("threw", error instanceof TypeError);
|
||||
}
|
||||
}
|
||||
|
||||
// The producer keeps working for every other caller.
|
||||
const whole = split("kept", 2);
|
||||
console.log("kept", whole[0], whole[1], whole.length);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2200, "san": 2900 }
|
||||
Reference in New Issue
Block a user