mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Keep the key order of records spread from values JavaScript built (#2024)
This commit is contained in:
@@ -1563,6 +1563,19 @@ export class LlDyn {
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
/** `record.sourceSpread(built, value)`: the built record keeps value's
|
||||
* kept-source order over its own declared names. */
|
||||
emitRecordSourceSpread(B: BlockBuilder, built: string, value: string, shapeId: string): void {
|
||||
const host = this.host;
|
||||
const shape = host.recordsById.get(shapeId)!;
|
||||
this.keepsRecordSources = true;
|
||||
const names = this.recordNameTable(shapeId, shape);
|
||||
host.declare(`declare void @scr_record_source_spread(ptr, ptr, ptr, ptr, ${host.sizeType})`);
|
||||
B.line(
|
||||
`call void @scr_record_source_spread(ptr ${built}, ptr ${value}, ptr ${names.names}, ptr ${names.lens}, ${host.sizeType} ${shape.fields.length})`,
|
||||
);
|
||||
}
|
||||
|
||||
/** The declared-name tables scr_record_source_note scans: parallel
|
||||
* constant arrays of field-name bytes and byte lengths in declaration
|
||||
* order (the order the record serializes), interned per shape. Internal
|
||||
|
||||
@@ -239,6 +239,15 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
host.emitPendingCheck();
|
||||
return output;
|
||||
}
|
||||
if (e.fn === "record.sourceSpread") {
|
||||
const built = host.emitReadReceiver(e.args[0]!);
|
||||
const value = host.emitReadReceiver(e.args[1]!);
|
||||
const type = e.args[0]!.type;
|
||||
if (type.kind !== "record")
|
||||
throw new InternalCompilerError("llvm emitter bug: record.sourceSpread of a non-record");
|
||||
host.dyn.emitRecordSourceSpread(B, built.name, value.name, type.shapeId);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "dyn.typedRefIs" || e.fn === "dyn.typedRefKeyIs") {
|
||||
const literal = e.args[1]!.kind === "strLit" ? e.args[1]!.value : null;
|
||||
if (e.fn === "dyn.typedRefKeyIs" && literal === null)
|
||||
|
||||
@@ -678,6 +678,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"insp.circular": "scr_insp_circular",
|
||||
// Undeclared source properties kept beside a checked record (scr_json.c).
|
||||
"record.sourceHas": "scr_record_source_has",
|
||||
"record.sourceSpread": "scr_record_source_spread",
|
||||
// WHATWG URL + URLSearchParams (scr_url.c / scr_url_params.c):
|
||||
// constructions +1; url.new, the fileURLToPath pair, the win32
|
||||
// pathToFileURL flavor, and sp.fromPairs throw catchably (may-throw).
|
||||
|
||||
@@ -42,6 +42,7 @@ function fixture(): Fixture {
|
||||
unions: new Map([["u0", { id: "u0", arms: [F64, { kind: "undefinedT" }] }]]),
|
||||
classes: new Map(),
|
||||
methodAccessSites: new Map(),
|
||||
dynConvertible: () => true,
|
||||
armTag: (unionId: string, arm: IrType): number =>
|
||||
unionId === "u0" && arm.kind === "undefinedT" ? 1 : -1,
|
||||
recordFieldPresent: (obj: IrExpr, shapeId: string, field: string): IrExpr => ({
|
||||
@@ -873,3 +874,92 @@ test("a generator resume builds only its result record, never the value it carri
|
||||
// The result record itself is built in its fixed layout: today's table.
|
||||
expect(f.order(result)).toEqual(["value", "done"]);
|
||||
});
|
||||
|
||||
/** `{ a, b }` built by a checked conversion and by the literal `{ b, a }`,
|
||||
* copied by `{ ...value, a }` (evaluating the value into a binding, as
|
||||
* spread literals do), observed. Answers the table and the function. */
|
||||
function checkedSelfCopy(): { order: string[]; fn: IrFunction; copy: IrExpr } {
|
||||
const f = fixture();
|
||||
const ab = f.record(
|
||||
[
|
||||
["a", F64],
|
||||
["b", F64],
|
||||
],
|
||||
["a", "b"],
|
||||
);
|
||||
const value = ref("value", ab) as IrExpr & { kind: "varRef" };
|
||||
const self: KeyOrderSegment = { kind: "spread", shapeId: ab.shapeId, maybeAbsent: false, value };
|
||||
const literal = f.literal(ab, ["a", "b"], [self, { kind: "key", name: "a", definite: true }]);
|
||||
const copy: IrExpr = {
|
||||
kind: "seqExpr",
|
||||
stmts: [decl("value", { kind: "dynCheck", value: ref("j", { kind: "dyn" }), type: ab, loc })],
|
||||
result: literal,
|
||||
type: ab,
|
||||
loc,
|
||||
};
|
||||
const fn = f.run([observe(f.literal(ab, ["b", "a"])), observe(copy)]);
|
||||
return { order: f.order(ab)!, fn, copy };
|
||||
}
|
||||
|
||||
test("a spread of a value a checked conversion built carries the value's order over", () => {
|
||||
const { order, fn, copy } = checkedSelfCopy();
|
||||
// Values the literal builds enumerate in its order; copies of converted
|
||||
// values keep the order their record source holds.
|
||||
expect(order).toEqual(["b", "a"]);
|
||||
expect(copy).toMatchObject({
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: "value" },
|
||||
{ kind: "varDecl", localId: "%orderedSpread.0", init: { kind: "recordLit" } },
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "record.sourceSpread",
|
||||
args: [
|
||||
{ kind: "varRef", localId: "%orderedSpread.0" },
|
||||
{ kind: "varRef", localId: "value" },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
result: { kind: "varRef", localId: "%orderedSpread.0" },
|
||||
});
|
||||
expect(fn.locals.map((local) => local.id)).toEqual(["%orderedSpread.0"]);
|
||||
});
|
||||
|
||||
test("a spread of a converted value into a wider shape orders the wider shape", () => {
|
||||
const f = fixture();
|
||||
const ab = f.record(
|
||||
[
|
||||
["a", F64],
|
||||
["b", F64],
|
||||
],
|
||||
["a", "b"],
|
||||
);
|
||||
const abc = f.record(
|
||||
[
|
||||
["a", F64],
|
||||
["b", F64],
|
||||
["c", F64],
|
||||
],
|
||||
["c", "a", "b"],
|
||||
);
|
||||
const value = ref("value", ab) as IrExpr & { kind: "varRef" };
|
||||
const spread: KeyOrderSegment = {
|
||||
kind: "spread",
|
||||
shapeId: ab.shapeId,
|
||||
maybeAbsent: false,
|
||||
value,
|
||||
};
|
||||
const wide: IrExpr = {
|
||||
kind: "seqExpr",
|
||||
stmts: [decl("value", { kind: "dynCheck", value: ref("j", { kind: "dyn" }), type: ab, loc })],
|
||||
result: f.literal(abc, ["a", "b", "c"], [spread, { kind: "key", name: "c", definite: true }]),
|
||||
type: abc,
|
||||
loc,
|
||||
};
|
||||
f.run([observe(f.literal(ab, ["b", "a"])), observe(wide)]);
|
||||
expect(f.order(ab)).toEqual(["b", "a"]);
|
||||
expect(f.order(abc)).toEqual(["b", "a", "c"]);
|
||||
});
|
||||
|
||||
@@ -41,6 +41,12 @@
|
||||
* - a literal that starts by spreading a value of its own shape copies
|
||||
* that value's order. While no checked conversion builds the shape,
|
||||
* every value has the one chosen order, so the copy adds no witness;
|
||||
* - a literal that starts by spreading a value of a shape checked
|
||||
* conversions build (an ordered spread) gives the record it builds the
|
||||
* value's record source, when the value has one (record.sourceSpread).
|
||||
* Every value of a shape enumerates in its table or in the order its
|
||||
* record source holds, so the copy has the value's order either way,
|
||||
* and the literal resolves as if the value were in its table's order;
|
||||
* - a write to an optional key that may be absent appends it in Node. A
|
||||
* key no construction creates ("late-only") goes after every key a
|
||||
* construction can create; any other moving write goes after every key.
|
||||
@@ -61,6 +67,7 @@ import {
|
||||
shapeHasAccessorSlots,
|
||||
typeEquals,
|
||||
UNDEFINED_T,
|
||||
VOID,
|
||||
} from "../../ir/ir.js";
|
||||
import {
|
||||
everyExprChild,
|
||||
@@ -71,6 +78,7 @@ import {
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { ALWAYS_THROW_LIB_FNS } from "../../ir/builtin-effects.js";
|
||||
import { recordMayKeepSource } from "./record-sources.js";
|
||||
|
||||
/** The statements a key-order helper runs for record `ref`: `walks[0]`,
|
||||
* its walk over the shape's table, or, for a shape with orderVariants, a
|
||||
@@ -218,6 +226,14 @@ interface ShapeFacts {
|
||||
|
||||
type Resolved = { order: string[] } | "unknown";
|
||||
|
||||
/** A literal that starts with `...value`: the sequence that evaluates the
|
||||
* value into its binding and then builds the literal, and its function. */
|
||||
interface LeadingSpread {
|
||||
wrapper: IrExpr & { kind: "seqExpr" };
|
||||
literal: IrExpr & { kind: "recordLit" };
|
||||
fn: IrFunction;
|
||||
}
|
||||
|
||||
/** `structuredClone(v)` checked back into v's own type: the copy is built
|
||||
* from v's dyn view, so it creates exactly v's keys in v's order. */
|
||||
function orderPreservingClone(node: IrExpr & { kind: "dynCheck" }): boolean {
|
||||
@@ -412,6 +428,12 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
// ── collection ──────────────────────────────────────────────────────
|
||||
let pos = 0;
|
||||
let fnName = "";
|
||||
let currentFn: IrFunction | null = null;
|
||||
/** Literals that start by spreading a binding, by their witness: where
|
||||
* each one is built. A literal's notes are shared by its rebuilt copies,
|
||||
* so `literalSites` counts every literal of each witness. */
|
||||
const leadingSpreads = new Map<KeyOrderSegment[], LeadingSpread[]>();
|
||||
const literalSites = new Map<KeyOrderSegment[], number>();
|
||||
const loops: number[] = [];
|
||||
let loopIds = 0;
|
||||
const loopKey = (): string => loops.join("/");
|
||||
@@ -463,8 +485,10 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
}
|
||||
const segments = notes.literalOf(e);
|
||||
const copy = notes.copyOf(e);
|
||||
if (segments !== undefined) f.literals.push(segments);
|
||||
else if (copy !== undefined) f.copies.push({ source: copy, keys: presentKeys });
|
||||
if (segments !== undefined) {
|
||||
f.literals.push(segments);
|
||||
literalSites.set(segments, (literalSites.get(segments) ?? 0) + 1);
|
||||
} else if (copy !== undefined) f.copies.push({ source: copy, keys: presentKeys });
|
||||
else f.pinned = true;
|
||||
return;
|
||||
}
|
||||
@@ -479,6 +503,18 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
case "assignExpr":
|
||||
noteInit(e.localId, e.value);
|
||||
return;
|
||||
case "seqExpr": {
|
||||
// A spread literal evaluates its sources into bindings first.
|
||||
const literal = e.result;
|
||||
if (literal.kind !== "recordLit" || currentFn === null) return;
|
||||
const segments = notes.literalOf(literal);
|
||||
const first = segments?.[0];
|
||||
if (segments === undefined || first?.kind !== "spread" || first.value === undefined) return;
|
||||
const list = leadingSpreads.get(segments) ?? [];
|
||||
list.push({ wrapper: e, literal, fn: currentFn });
|
||||
leadingSpreads.set(segments, list);
|
||||
return;
|
||||
}
|
||||
case "jsonStringify":
|
||||
case "dynFrom":
|
||||
observedRoots.push(e.value.type);
|
||||
@@ -582,6 +618,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
};
|
||||
for (const fn of fns) {
|
||||
fnName = fn.name;
|
||||
currentFn = fn;
|
||||
for (const st of fn.body) stmt(st);
|
||||
}
|
||||
for (const id of opaqueShapes) {
|
||||
@@ -698,6 +735,61 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
return edges;
|
||||
};
|
||||
|
||||
// ── ordered spreads ─────────────────────────────────────────────────
|
||||
// A value a checked conversion builds keeps its source's key order in
|
||||
// its record source when that order is not the table's (record-
|
||||
// sources.ts), so every value of a shape enumerates in the table's order
|
||||
// or in the order its record source holds. A literal that starts with
|
||||
// `...value` creates the value's keys first, in the value's order. When
|
||||
// the value's shape is one checked conversions build, the literal
|
||||
// carries that order over: after it builds the record it gives the
|
||||
// record a source holding the value's order (record.sourceSpread), and
|
||||
// the keys the value lacks follow in the table's order, which every
|
||||
// literal sequence of the shape agrees with. Such a literal resolves as
|
||||
// if its source were in the source's table order, and its own shape
|
||||
// counts as built at run time too, since its values may now carry a
|
||||
// source. It qualifies when every value of the source has all of the
|
||||
// source's keys, both shapes can keep a source, and every later spread
|
||||
// whose values may carry a source only overwrites the source's keys
|
||||
// (its own order cannot move them).
|
||||
const orderedSpreads = new Set<readonly KeyOrderSegment[]>();
|
||||
const keepsSource = (shapeId: string): boolean =>
|
||||
recordMayKeepSource(lowerer, { kind: "record", shapeId });
|
||||
const userKeys = (shapeId: string): string[] =>
|
||||
(shapes.get(shapeId)?.fields ?? []).map((x) => x.name).filter((k) => !k.startsWith("%"));
|
||||
const ordersSpread = (segments: KeyOrderSegment[], sites: readonly LeadingSpread[]): boolean => {
|
||||
const first = segments[0];
|
||||
if (first?.kind !== "spread" || first.maybeAbsent || first.value === undefined) return false;
|
||||
const sourceId = first.shapeId;
|
||||
if (facts.get(sourceId)?.runtime !== true || sites.length !== literalSites.get(segments))
|
||||
return false;
|
||||
const target = sites[0]!.literal.type;
|
||||
if (target.kind !== "record" || !keepsSource(sourceId) || !keepsSource(target.shapeId))
|
||||
return false;
|
||||
const localId = first.value.localId;
|
||||
const bound = ({ wrapper, literal }: LeadingSpread): boolean =>
|
||||
typeEquals(literal.type, target) &&
|
||||
wrapper.stmts.some(
|
||||
(st) =>
|
||||
st.kind === "varDecl" &&
|
||||
st.localId === localId &&
|
||||
st.init !== null &&
|
||||
st.init.type.kind === "record" &&
|
||||
st.init.type.shapeId === sourceId,
|
||||
);
|
||||
if (!sites.every(bound)) return false;
|
||||
const sourceKeys = new Set(userKeys(sourceId));
|
||||
if ([...sourceKeys].some((k) => maybeAbsent(sourceId, k))) return false;
|
||||
return segments
|
||||
.slice(1)
|
||||
.every(
|
||||
(s) =>
|
||||
s.kind === "key" ||
|
||||
(s.kind === "spread" &&
|
||||
(facts.get(s.shapeId)?.runtime !== true ||
|
||||
userKeys(s.shapeId).every((k) => sourceKeys.has(k)))),
|
||||
);
|
||||
};
|
||||
// ── witness sequences ───────────────────────────────────────────────
|
||||
/** The key sequences one literal can create, or null when a spread
|
||||
* source's order is unknown. A key sits at its first occurrence that
|
||||
@@ -707,15 +799,19 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
segments: readonly KeyOrderSegment[],
|
||||
keySet: ReadonlySet<string>,
|
||||
resolved: (id: string) => Resolved,
|
||||
raw: (id: string) => Resolved,
|
||||
): string[][] | null => {
|
||||
const occurrences: { key: string; definite: boolean }[] = [];
|
||||
const ordered = orderedSpreads.has(segments);
|
||||
for (const s of segments) {
|
||||
if (s.kind === "opaque") return null;
|
||||
if (s.kind === "key") {
|
||||
if (keySet.has(s.name)) occurrences.push({ key: s.name, definite: s.definite });
|
||||
continue;
|
||||
}
|
||||
const source = resolved(s.shapeId);
|
||||
// An ordered spread's values without a source enumerate in the
|
||||
// source's table; the others carry their order over.
|
||||
const source = ordered && s === segments[0] ? raw(s.shapeId) : resolved(s.shapeId);
|
||||
if (source === "unknown") return null;
|
||||
for (const k of source.order) {
|
||||
if (keySet.has(k))
|
||||
@@ -809,13 +905,17 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
first.kind === "spread" &&
|
||||
first.shapeId === id &&
|
||||
!first.maybeAbsent &&
|
||||
!f.runtime &&
|
||||
(!f.runtime || orderedSpreads.has(segments)) &&
|
||||
segments.every((s) => s.kind !== "opaque") &&
|
||||
table.every((k) => !maybeAbsent(id, k))
|
||||
);
|
||||
};
|
||||
/** One shape's order given its sources' current orders. */
|
||||
const resolveOne = (id: string, current: (source: string) => Resolved): Resolved => {
|
||||
const resolveOne = (
|
||||
id: string,
|
||||
current: (source: string) => Resolved,
|
||||
raw: (source: string) => Resolved,
|
||||
): Resolved => {
|
||||
const shape = shapes.get(id);
|
||||
if (!inScope(shape)) return "unknown";
|
||||
const table = tableOf(shape);
|
||||
@@ -832,7 +932,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
if (f.pinned) chain(table, edges);
|
||||
for (const segments of f.literals) {
|
||||
if (copiesItself(id, f, table, segments)) continue;
|
||||
const sequences = literalSequences(segments, keySet, current);
|
||||
const sequences = literalSequences(segments, keySet, current, raw);
|
||||
if (sequences === null) return "unknown";
|
||||
for (const seq of sequences) chain(seq, edges);
|
||||
}
|
||||
@@ -891,32 +991,57 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
const known = result.get(id);
|
||||
return known === undefined ? "unknown" : asSource(id, known);
|
||||
};
|
||||
for (const scc of sccs) {
|
||||
const members = new Set(scc);
|
||||
const guess = new Map<string, Resolved>();
|
||||
for (const id of scc) {
|
||||
const shape = shapes.get(id);
|
||||
guess.set(id, inScope(shape) ? { order: tableOf(shape) } : "unknown");
|
||||
}
|
||||
const lookup = (id: string): Resolved =>
|
||||
members.has(id) ? asSource(id, guess.get(id)!) : external(id);
|
||||
let stable = false;
|
||||
for (let round = 0; round < scc.length + 3 && !stable; round++) {
|
||||
stable = true;
|
||||
const resolveAll = (): void => {
|
||||
result.clear();
|
||||
late.clear();
|
||||
for (const scc of sccs) {
|
||||
const members = new Set(scc);
|
||||
const guess = new Map<string, Resolved>();
|
||||
for (const id of scc) {
|
||||
const next = resolveOne(id, lookup);
|
||||
const prev = guess.get(id)!;
|
||||
const same =
|
||||
next === "unknown"
|
||||
? prev === "unknown"
|
||||
: prev !== "unknown" && prev.order.join("\0") === next.order.join("\0");
|
||||
if (!same) {
|
||||
stable = false;
|
||||
guess.set(id, next);
|
||||
const shape = shapes.get(id);
|
||||
guess.set(id, inScope(shape) ? { order: tableOf(shape) } : "unknown");
|
||||
}
|
||||
const lookup = (id: string): Resolved =>
|
||||
members.has(id) ? asSource(id, guess.get(id)!) : external(id);
|
||||
const rawLookup = (id: string): Resolved =>
|
||||
members.has(id) ? guess.get(id)! : (result.get(id) ?? "unknown");
|
||||
let stable = false;
|
||||
for (let round = 0; round < scc.length + 3 && !stable; round++) {
|
||||
stable = true;
|
||||
for (const id of scc) {
|
||||
const next = resolveOne(id, lookup, rawLookup);
|
||||
const prev = guess.get(id)!;
|
||||
const same =
|
||||
next === "unknown"
|
||||
? prev === "unknown"
|
||||
: prev !== "unknown" && prev.order.join("\0") === next.order.join("\0");
|
||||
if (!same) {
|
||||
stable = false;
|
||||
guess.set(id, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const id of scc) result.set(id, stable ? guess.get(id)! : "unknown");
|
||||
}
|
||||
for (const id of scc) result.set(id, stable ? guess.get(id)! : "unknown");
|
||||
};
|
||||
// Ordered spreads join once their source resolves; each one can let its
|
||||
// own shape resolve, which a later spread of it needs.
|
||||
resolveAll();
|
||||
for (let grew = true; grew;) {
|
||||
grew = false;
|
||||
for (const [segments, sites] of leadingSpreads) {
|
||||
const first = segments[0];
|
||||
if (first?.kind !== "spread" || orderedSpreads.has(segments)) continue;
|
||||
// A self copy resolves with its own shape.
|
||||
const target = sites[0]!.literal.type;
|
||||
const source = result.get(first.shapeId) ?? "unknown";
|
||||
const self = target.kind === "record" && target.shapeId === first.shapeId;
|
||||
if ((source === "unknown" && !self) || !ordersSpread(segments, sites)) continue;
|
||||
orderedSpreads.add(segments);
|
||||
if (target.kind === "record") factsOf(target.shapeId).runtime = true;
|
||||
grew = true;
|
||||
}
|
||||
if (grew) resolveAll();
|
||||
}
|
||||
|
||||
// ── observation ─────────────────────────────────────────────────────
|
||||
@@ -925,6 +1050,19 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
for (const id of notes.observed)
|
||||
shapesIn({ kind: "record", shapeId: id }, observed, observedSeen);
|
||||
for (const root of observedRoots) shapesIn(root, observed, observedSeen);
|
||||
// An ordered spread's copies of values without a source take the source's
|
||||
// order from its table, which must then hold the resolved order.
|
||||
const orderedTarget = (sites: readonly LeadingSpread[]): string | null => {
|
||||
const target = sites[0]!.literal.type;
|
||||
return target.kind === "record" && (result.get(target.shapeId) ?? "unknown") !== "unknown"
|
||||
? target.shapeId
|
||||
: null;
|
||||
};
|
||||
for (const [segments, sites] of leadingSpreads) {
|
||||
const first = segments[0];
|
||||
if (orderedSpreads.has(segments) && first?.kind === "spread" && orderedTarget(sites) !== null)
|
||||
observed.add(first.shapeId);
|
||||
}
|
||||
|
||||
// ── apply ───────────────────────────────────────────────────────────
|
||||
for (const [id, resolved] of result) {
|
||||
@@ -937,6 +1075,46 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
notes.changed.add(id);
|
||||
}
|
||||
|
||||
// Ordered spreads carry their source's order over (see above).
|
||||
let orderedResults = 0;
|
||||
const rewritten = new Set<IrExpr>();
|
||||
for (const [segments, sites] of leadingSpreads) {
|
||||
const first = segments[0];
|
||||
if (!orderedSpreads.has(segments) || first?.kind !== "spread" || first.value === undefined)
|
||||
continue;
|
||||
// A shape that did not resolve keeps today's table for every value.
|
||||
if (orderedTarget(sites) === null) continue;
|
||||
for (const { wrapper, literal, fn } of sites) {
|
||||
if (rewritten.has(wrapper)) continue;
|
||||
rewritten.add(wrapper);
|
||||
const loc = literal.loc;
|
||||
const localId = `%orderedSpread.${orderedResults++}`;
|
||||
fn.locals.push({ id: localId, name: "%orderedSpread", type: literal.type, mutable: false });
|
||||
const built: IrExpr = { kind: "varRef", localId, type: literal.type, loc };
|
||||
const source: IrExpr = {
|
||||
kind: "varRef",
|
||||
localId: first.value.localId,
|
||||
type: { kind: "record", shapeId: first.shapeId },
|
||||
loc,
|
||||
};
|
||||
wrapper.stmts.push(
|
||||
{ kind: "varDecl", localId, init: literal, loc },
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "record.sourceSpread",
|
||||
args: [built, source],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
);
|
||||
wrapper.result = { kind: "varRef", localId, type: literal.type, loc };
|
||||
}
|
||||
}
|
||||
|
||||
// ── per-instance orders ─────────────────────────────────────────────
|
||||
// A shape whose literals write its keys in more than one order has no
|
||||
// one table. Each value then records which of the orders it was written
|
||||
|
||||
@@ -4458,6 +4458,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* (inspect, Object.keys/values/entries) then answer through its live
|
||||
* checked view. The receiver is any such record type. Never throws. */
|
||||
"record.sourceHas": { argTypes: [null], result: BOOL },
|
||||
/** `record.sourceSpread(built, value)`: the record a literal starting
|
||||
* with `...value` just built keeps the order of value's kept source
|
||||
* (key-order-witness.ts ordered spreads). Both are record types. Never
|
||||
* throws. */
|
||||
"record.sourceSpread": { argTypes: [null, null], result: VOID },
|
||||
"insp.key": { argTypes: [STRING], result: STRING },
|
||||
"insp.moreItems": { argTypes: [F64], result: STRING },
|
||||
"insp.end": { argTypes: [STRING, STRING, STRING, F64, BOOL, BOOL], result: STRING },
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7750-key-order-javascript-spread-sources/lib.js",
|
||||
"<repo>/tests/corpus/7750-key-order-javascript-spread-sources/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -1055,6 +1055,12 @@ void scr_dyn_typed_ref_bind_record(ScrDyn *d, const ScrDynTypedRecordOps *ops);
|
||||
void scr_record_source_note(void *rec, const ScrDyn *src, const char *const *names,
|
||||
const size_t *lens, size_t count);
|
||||
bool scr_record_source_has(const void *rec);
|
||||
/* A literal that starts with `...src` creates src's keys first, in src's
|
||||
* order: when src kept a source, the fresh record `dst` (whose shape's
|
||||
* declared names are `names`) keeps one listing the keys it declares in
|
||||
* that order. Undeclared keys are not copied, as the literal copies none. */
|
||||
void scr_record_source_spread(void *dst, const void *src, const char *const *names,
|
||||
const size_t *lens, size_t count);
|
||||
/* Object.freeze of a record a program may freeze: sets SCR_RC_FROZEN in its
|
||||
* rc word (immortal records are left as they are). */
|
||||
void scr_record_freeze(void *record);
|
||||
|
||||
@@ -3283,6 +3283,45 @@ void scr_record_source_note(void *rec, const ScrDyn *src, const char *const *nam
|
||||
scr_record_source_put(&probe);
|
||||
}
|
||||
|
||||
void scr_record_source_spread(void *dst, const void *src, const char *const *names,
|
||||
const size_t *lens, size_t count) {
|
||||
if (!scr_record_source_count || !dst || !src) return;
|
||||
ScrRecordSource *found = scr_record_source_find(src);
|
||||
if (!found) return;
|
||||
ScrRecordSource probe = { dst, NULL, (const char *)names, (const char *)lens,
|
||||
sizeof *names, sizeof *lens, count };
|
||||
/* Nothing to keep when the declared keys come in the table's order and
|
||||
* the keys src lacks would follow them in it anyway. */
|
||||
const ScrDyn *from = found->view;
|
||||
bool keep = false;
|
||||
size_t next = 0;
|
||||
for (size_t i = 0; i < from->v.obj.len && !keep; i++) {
|
||||
const ScrDynEntry *entry = &from->v.obj.entries[i];
|
||||
size_t index = scr_record_source_index(&probe, entry->key, entry->key_len);
|
||||
if (index == count) continue;
|
||||
keep = index != next;
|
||||
next++;
|
||||
}
|
||||
if (!keep) return;
|
||||
ScrDyn *view = scr_dyn_new_obj();
|
||||
for (size_t i = 0; i < from->v.obj.len; i++) {
|
||||
const ScrDynEntry *entry = &from->v.obj.entries[i];
|
||||
if (scr_record_source_declared(&probe, entry->key, entry->key_len))
|
||||
scr_dyn_obj_set(view, entry->key, entry->key_len, scr_dyn_retain(scr_dyn_undefined()));
|
||||
}
|
||||
/* The keys src lacks follow in the table's order. The view lists them
|
||||
* too, so an integer key among them comes first in its own-key order, as
|
||||
* the literal creates it. */
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (scr_dyn_obj_get(view, names[i], lens[i])) continue;
|
||||
scr_dyn_obj_set(view, names[i], lens[i], scr_dyn_retain(scr_dyn_undefined()));
|
||||
}
|
||||
ScrRecordSource *old = scr_record_source_find(dst);
|
||||
if (old) scr_record_source_drop(old);
|
||||
probe.view = view;
|
||||
scr_record_source_put(&probe);
|
||||
}
|
||||
|
||||
void scr_record_source_move(void *from, void *to) {
|
||||
if (!scr_record_source_count) return;
|
||||
ScrRecordSource *old = scr_record_source_find(to);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Records JavaScript builds in its own key order, handed to TypeScript
|
||||
// through typed slots: a draft from a "validator", a stored row, and a
|
||||
// replacement method installed through a computed key.
|
||||
export function validate(input) {
|
||||
return { year: input.year, title: input.title, tags: input.tags };
|
||||
}
|
||||
|
||||
export function storeRow(rows, id) {
|
||||
rows.set(id, { title: "Stored", id, tags: [], year: 1999, createdAt: "t0", version: 1 });
|
||||
}
|
||||
|
||||
export function replace(target, name) {
|
||||
target[name] = (id) => ({ version: 7, id, year: 2001, title: "Replaced", tags: ["r"], createdAt: "t1" });
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// Literals that start by spreading a record JavaScript built keep that
|
||||
// record's key order, as Node does: a draft from JavaScript spread into a
|
||||
// new record, a self copy of a record JavaScript stored or returned, and
|
||||
// a wider view of either. Native drafts and copies keep the literal order.
|
||||
import { replace, storeRow, validate } from "./lib.js";
|
||||
|
||||
interface Book {
|
||||
id: string;
|
||||
title: string;
|
||||
year: number;
|
||||
tags: readonly string[];
|
||||
createdAt: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
type Draft = Omit<Book, "id" | "createdAt" | "version">;
|
||||
type Patch = Partial<Draft>;
|
||||
|
||||
interface Availability {
|
||||
copies: number;
|
||||
onLoan: number;
|
||||
}
|
||||
|
||||
type BookView = Book & { availability: Availability };
|
||||
|
||||
class Repository {
|
||||
private readonly rows = new Map<string, Book>();
|
||||
private sequence = 0;
|
||||
|
||||
create(draft: Draft): Book {
|
||||
const book: Book = {
|
||||
...draft,
|
||||
tags: [...draft.tags].sort(),
|
||||
id: `bk_${++this.sequence}`,
|
||||
createdAt: "t" + this.sequence,
|
||||
version: 1,
|
||||
};
|
||||
this.rows.set(book.id, book);
|
||||
return book;
|
||||
}
|
||||
|
||||
require(id: string): Book {
|
||||
const row = this.rows.get(id);
|
||||
if (row === undefined) throw new Error("missing " + id);
|
||||
return row;
|
||||
}
|
||||
|
||||
update(id: string, patch: Patch): Book {
|
||||
const current = this.require(id);
|
||||
const next: Book = { ...current, ...patch, version: current.version + 1 };
|
||||
this.rows.set(id, next);
|
||||
return next;
|
||||
}
|
||||
|
||||
view(book: Book): BookView {
|
||||
return { ...book, availability: { copies: 2, onLoan: book.version } };
|
||||
}
|
||||
|
||||
table(): Map<string, Book> {
|
||||
return this.rows;
|
||||
}
|
||||
}
|
||||
|
||||
function show(label: string, value: Book): void {
|
||||
console.log(label, JSON.stringify(value));
|
||||
console.log(" keys", Object.keys(value).join());
|
||||
}
|
||||
|
||||
function showView(label: string, value: BookView): void {
|
||||
console.log(label, JSON.stringify(value));
|
||||
console.log(" keys", Object.keys(value).join());
|
||||
}
|
||||
|
||||
const repo = new Repository();
|
||||
|
||||
// A native draft: the literal's order.
|
||||
const native = repo.create({ title: "Native", year: 1990, tags: ["b", "a"] });
|
||||
show("native", native);
|
||||
showView("native view", repo.view(native));
|
||||
show("native update", repo.update(native.id, { year: 1991 }));
|
||||
|
||||
// A draft JavaScript built in another order.
|
||||
const draft: Draft = validate({ title: "Checked", year: 2020, tags: ["z", "y"] });
|
||||
const checked = repo.create(draft);
|
||||
show("checked", checked);
|
||||
showView("checked view", repo.view(checked));
|
||||
show("checked update", repo.update(checked.id, { title: "Checked 2" }));
|
||||
showView("checked update view", repo.view(repo.require(checked.id)));
|
||||
|
||||
// A row JavaScript stored into the typed map.
|
||||
storeRow(repo.table(), "js_1");
|
||||
show("stored", repo.require("js_1"));
|
||||
show("stored update", repo.update("js_1", { tags: ["s"] }));
|
||||
showView("stored view", repo.view(repo.require("js_1")));
|
||||
|
||||
// A replacement method returning rows JavaScript builds.
|
||||
replace(repo, "require");
|
||||
show("replaced", repo.require("x"));
|
||||
show("replaced update", repo.update("x", { year: 2002 }));
|
||||
showView("replaced view", repo.view(repo.require("y")));
|
||||
|
||||
// The copies are independent records.
|
||||
const again = repo.view(checked);
|
||||
showView("again", again);
|
||||
console.log(native.version, checked.version, again.availability.onLoan);
|
||||
|
||||
// An integer-named key the spread source lacks comes first, as Node
|
||||
// creates integer keys first whatever the literal's order.
|
||||
interface Ranked {
|
||||
year: number;
|
||||
title: string;
|
||||
tags: readonly string[];
|
||||
id: string;
|
||||
"1": string;
|
||||
}
|
||||
function rank(source: Draft, id: string): Ranked {
|
||||
return { ...source, id, "1": "gold" };
|
||||
}
|
||||
for (const ranked of [rank(draft, "r1"), rank({ title: "Native", year: 1990, tags: [] }, "r2")]) {
|
||||
console.log("ranked", JSON.stringify(ranked));
|
||||
console.log(" keys", Object.keys(ranked).join());
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 5300, "san": 9200 }
|
||||
Reference in New Issue
Block a user