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:
Chris Tate
2026-10-24 17:35:25 -05:00
parent bac7a80377
commit 43ff8a1122
9 changed files with 696 additions and 17 deletions
+15 -13
View File
@@ -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 };
}
+159 -1
View File
@@ -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);
});
+321
View File
@@ -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;
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8241-literal-result-slices.ts"
],
"diags": []
}
+11
View File
@@ -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 }