Enumerate each record in the key order its object literal wrote (#1802)

This commit is contained in:
Chris Tate
2026-10-22 18:23:26 -05:00
committed by GitHub
50 changed files with 1390 additions and 223 deletions
+1 -1
View File
@@ -97,7 +97,7 @@ Coverage and `scriptc build` report the same diagnostic codes. Common ones inclu
Some code compiles but can behave differently than under Node.js. Coverage reports the cases it can detect as `SC6xxx` warnings, and `scriptc build` prints the same warnings after a successful build. They never fail a build, but `--fail-on=divergences` can fail a CI job on them.
- `SC6001`: a record passed to, stored as, asserted as, or stored as an array element of a record type with different properties is copied, and the program then writes through the copy or compares it by identity.
- `SC6002`: values of one record type are built in different key orders, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output.
- `SC6002`: values of one record type are built in key orders scriptc cannot track per value, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output.
- `SC6003`: `localeCompare` compares a string literal outside the built-in Latin-script collation.
- `SC6004`: a call passes a class instance to a function that casts it to an unrelated class.
@@ -10,9 +10,10 @@ A type whose values are only read field by field, from fields the class does not
Object spread of a class instance is refused.
Dense array literals created directly in checked storage preserve identity, and supported native handles pass by reference.
`Object.keys`, `Object.values`, `Object.entries`, `for...in`, `JSON.stringify`, and `util.inspect` enumerate a record's keys in one order per record type rather than per object. scriptc chooses that order from the order in which the program's object literals create the keys, follows object spread, rest destructuring, and record conversions back to the values they copy, and places an optional key that the program assigns after construction after the keys the object was built with.
The order matches Node.js when every value of the type is created in the same order.
When values of one record type are created in different orders, including values of two types with the same properties and property types, or when a key is deleted and assigned again, the type keeps the declaration order of its first type instead, and `SC6002` reports object literals whose order differs where the program observes it.
`Object.keys`, `Object.values`, `Object.entries`, `for...in`, `JSON.stringify`, and `util.inspect` enumerate a record's keys in the order the object created them, as in Node.js.
scriptc chooses one order per record type from the order in which the program's object literals create the keys, follows object spread, rest destructuring, and record conversions back to the values they copy, and places an optional key that the program assigns after construction after the keys the object was built with.
When the object literals of one record type create its keys in different orders, including literals of two types with the same properties and property types, each value records the order its literal used: copies made by object spread, rest destructuring, and record conversions keep their source's order, and assigning an optional key the object lacks, including after `delete`, moves the key last.
A type keeps the declaration order of its first type, and `SC6002` reports object literals whose order differs where the program observes it, when one of its values gets its keys from a spread whose source order is not known, from a spread that also sets an optional key the source lacks, or from a copy of a value scriptc cannot trace, or when the type's values can take more than 16 orders.
A record converted from a checked value, such as `const rows: { score: number }[] = JSON.parse(text)`, keeps the properties its type does not declare.
`JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `util.inspect`, and untyped views of the record see them, together with the declared fields, in the source object's key order.
+2 -2
View File
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
minimum_os: "14.0",
},
program: { object, entry_symbol: "main" },
runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" },
runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" },
runtime_pack: {
kind: "precompiled",
package: "@scriptc/runtime-darwin-arm64",
@@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
() => null,
(failure: { stderr?: string }) => failure,
);
expect(error?.stderr).toContain("scr_runtime_abi_v27");
expect(error?.stderr).toContain("scr_runtime_abi_v28");
});
});
+84 -65
View File
@@ -1,3 +1,4 @@
import { emitPerKeyOrder } from "./key-orders.js";
import { typedRefConstructor } from "./shapes.js";
import { bindLiveArrayOps } from "./expr-stream-bridges.js";
import { BYTES_ELEM_NUM, SCR_ARR_FIELD, SCR_ARR_STATES, f64Lit, sizeToDouble } from "./common.js";
@@ -37,6 +38,8 @@ import {
accessorSlotProp,
DYN_HANDLE_KINDS,
isDirentShape,
KEY_ORDER_FIELD,
shapeKeyOrders,
isDynTypedRefType,
isRefCounted,
isUnitType,
@@ -1107,9 +1110,10 @@ export class LlDyn {
}
B.condBr(readable, l0, fail);
B.startBlock(l0);
// dyn ('unknown') fields match ANY value, present or missing.
// dyn ('unknown') fields match ANY value, present or missing. The
// order index is no key.
for (const f of shape.fields) {
if (f.type.kind === "dyn") continue;
if (f.type.kind === "dyn" || f.name === KEY_ORDER_FIELD) continue;
const { value: m, release } = this.memberRead(B, f.name, f.type);
const lNext = B.newLabel("dm.n");
this.pendingBail(
@@ -1292,9 +1296,10 @@ export class LlDyn {
return (
shape.fields.every(
(f) =>
!f.name.startsWith("%") &&
f.name !== "__proto__" &&
this.jsonSchemaSupported(f.type, visiting, true),
f.name === KEY_ORDER_FIELD ||
(!f.name.startsWith("%") &&
f.name !== "__proto__" &&
this.jsonSchemaSupported(f.type, visiting, true)),
) &&
(!index || this.jsonSchemaSupported(index, visiting))
);
@@ -1380,7 +1385,9 @@ export class LlDyn {
const base = host.nullableUnions.fieldBase(t.shapeId);
const index = new Map(shape.fields.map((f, i) => [f.name, i + base]));
// The runtime names kept-source keys by this index (record-sources.ts).
const order = recordSourceFieldOrder(shape);
// The order index is no key: parsed values keep the table's order
// and their source's own order.
const order = recordSourceFieldOrder(shape).filter((name) => name !== KEY_ORDER_FIELD);
if (recordMayKeepSource(shape)) this.keepsRecordSources = true;
const fieldTy = `{ ptr, ${S}, ${S}, ptr }`;
const entries = order.map((fieldName) => {
@@ -2156,7 +2163,10 @@ export class LlDyn {
}
B.line(`%r0 = call ptr @${mangleRecordNew(t.shapeId)}()`);
if (recursive) this.recordBuilt(B, name, "%r0");
// The order index stays 0: the source's own order is kept below
// when it differs from the table.
for (const f of shape.fields) {
if (f.name === KEY_ORDER_FIELD) continue;
const { value: m, release } = this.memberRead(B, f.name, f.type);
this.pendingBail(
B,
@@ -2889,70 +2899,79 @@ export class LlDyn {
host.declare(`declare ptr @scr_dyn_new_obj()`);
B.line(`${d} = call ptr @scr_dyn_new_obj()`);
}
// Keys insert in DECLARED order (JS insertion order); internal
// '%'-fields follow so a record→dyn→record round trip keeps them.
// Keys insert in DECLARED order (JS insertion order), or in the
// value's own order for a shape with orderVariants; internal
// '%'-fields follow so a record→dyn→record round trip keeps them
// (all but the order index, which is no key).
const byName = new Map(shape.fields.map((f) => [f.name, f]));
// An accessor property is one key whose getter and setter are the
// slot closures (see accessorSlotProp), in accessorOrder's place.
const order = shape.accessorOrder ?? shape.declaredOrder ?? shape.fields.map((f) => f.name);
const inOrder = new Set(order);
const accessors = new Set(
shape.accessorOrder === undefined
? []
: shape.fields.flatMap((f) => {
const slot = accessorSlotProp(f.name);
return slot === null ? [] : [slot.prop];
}),
);
type DynField = { name: string; type: IrType } | { accessor: string };
const dynFields: DynField[] = [
...order.flatMap((n) => {
if (accessors.has(n)) return [{ accessor: n } as DynField];
const f = byName.get(n);
return f === undefined ? [] : [f as DynField];
}),
...shape.fields.filter(
(f) =>
!inOrder.has(f.name) && !(accessors.size > 0 && accessorSlotProp(f.name) !== null),
),
];
for (const entry of dynFields) {
if ("accessor" in entry) {
host.declare(
`declare void @scr_dyn_obj_set_accessor(ptr, ptr, ${host.sizeType}, ptr, ptr)`,
);
const boxed = (kind: "get" | "set"): string => {
const slot = byName.get(`%${kind}:${entry.accessor}`);
if (slot === undefined) return "null";
const fv = loadFieldOf(slot.name, slot.type);
const conv = B.tmp();
B.line(
`${conv} = call ptr @${this.toDynHelper(slot.type)}(${this.valTy(slot.type)} ${fv})`,
);
return conv;
};
const getter = boxed("get");
const setter = boxed("set");
B.line(
`call void @scr_dyn_obj_set_accessor(ptr ${d}, ptr ${host.cstr(entry.accessor)}, ${host.sizeType} ${Buffer.byteLength(entry.accessor, "utf8")}, ptr ${getter}, ptr ${setter}) ; ${llvmCommentText(entry.accessor)}`,
);
continue;
}
const f = entry;
const klen = Buffer.byteLength(f.name, "utf8");
const fv = loadFieldOf(f.name, f.type);
// An ABSENT optional field contributes no key.
const lSkip = f.type.kind === "union" ? this.skipIfAbsent(B, fv, f.type) : null;
const conv = B.tmp();
B.line(`${conv} = call ptr @${this.toDynHelper(f.type)}(${this.valTy(f.type)} ${fv})`);
B.line(
`call void @scr_dyn_obj_set(ptr ${d}, ptr ${host.cstr(f.name)}, ${host.sizeType} ${klen}, ptr ${conv}) ; ${llvmCommentText(f.name)}`,
const orders =
shape.accessorOrder !== undefined ? [shape.accessorOrder] : shapeKeyOrders(shape);
const perOrder = (order: readonly string[]): void => {
const inOrder = new Set(order);
const accessors = new Set(
shape.accessorOrder === undefined
? []
: shape.fields.flatMap((f) => {
const slot = accessorSlotProp(f.name);
return slot === null ? [] : [slot.prop];
}),
);
if (lSkip !== null) {
B.br(lSkip);
B.startBlock(lSkip);
type DynField = { name: string; type: IrType } | { accessor: string };
const dynFields: DynField[] = [
...order.flatMap((n) => {
if (accessors.has(n)) return [{ accessor: n } as DynField];
const f = byName.get(n);
return f === undefined ? [] : [f as DynField];
}),
...shape.fields.filter(
(f) =>
!inOrder.has(f.name) &&
f.name !== KEY_ORDER_FIELD &&
!(accessors.size > 0 && accessorSlotProp(f.name) !== null),
),
];
for (const entry of dynFields) {
if ("accessor" in entry) {
host.declare(
`declare void @scr_dyn_obj_set_accessor(ptr, ptr, ${host.sizeType}, ptr, ptr)`,
);
const boxed = (kind: "get" | "set"): string => {
const slot = byName.get(`%${kind}:${entry.accessor}`);
if (slot === undefined) return "null";
const fv = loadFieldOf(slot.name, slot.type);
const conv = B.tmp();
B.line(
`${conv} = call ptr @${this.toDynHelper(slot.type)}(${this.valTy(slot.type)} ${fv})`,
);
return conv;
};
const getter = boxed("get");
const setter = boxed("set");
B.line(
`call void @scr_dyn_obj_set_accessor(ptr ${d}, ptr ${host.cstr(entry.accessor)}, ${host.sizeType} ${Buffer.byteLength(entry.accessor, "utf8")}, ptr ${getter}, ptr ${setter}) ; ${llvmCommentText(entry.accessor)}`,
);
continue;
}
const f = entry;
const klen = Buffer.byteLength(f.name, "utf8");
const fv = loadFieldOf(f.name, f.type);
// An ABSENT optional field contributes no key.
const lSkip = f.type.kind === "union" ? this.skipIfAbsent(B, fv, f.type) : null;
const conv = B.tmp();
B.line(`${conv} = call ptr @${this.toDynHelper(f.type)}(${this.valTy(f.type)} ${fv})`);
B.line(
`call void @scr_dyn_obj_set(ptr ${d}, ptr ${host.cstr(f.name)}, ${host.sizeType} ${klen}, ptr ${conv}) ; ${llvmCommentText(f.name)}`,
);
if (lSkip !== null) {
B.br(lSkip);
B.startBlock(lSkip);
}
}
}
};
if (orders.length === 1) perOrder(orders[0]!);
else emitPerKeyOrder(B, shape, base, "%v", perOrder);
// A Dirent row takes Node's Dirent prototype (the type probes) and
// its Symbol(type) key; %dtype stays readable but non-enumerable.
if (isDirentShape(shape)) {
@@ -141,7 +141,7 @@ test("live record capsules bind field hooks in snapshot order", () => {
const llvm = emitLlvmModule(module, { pointerBits: 64 });
const tables = [
...llvm.matchAll(
/^@(\S+)_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr \} \{ i64 (\d+), /gm,
/^@(\S+)_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr, ptr \} \{ i64 (\d+), /gm,
),
];
// root and leaf get field hooks; the record with an index-like field name
@@ -13,6 +13,8 @@ import {
classFieldAttributesName,
isClassOwnEnumerableFieldName,
isDirentShape,
KEY_ORDER_FIELD,
shapeKeyOrders,
isDynTypedRefType,
isRefCounted,
shapeHasAccessorSlots,
@@ -44,6 +46,7 @@ import type { NullableRefField } from "./nullable-fields.js";
import { emitUnionPeek, emitUnionTag } from "./union-repr.js";
import { inlineUnionCopyLines } from "./inline-unions.js";
import { recordMayKeepSource } from "./record-sources.js";
import { emitPerKeyOrder } from "./key-orders.js";
import { registerReplay } from "./emit-journal.js";
/** An owned (+1) union value for a nullable-pointer class field slot `p`
@@ -1065,52 +1068,68 @@ export function streamTypedRefMaterializeAdapter(
const out = B.tmp();
B.line(`${out} = call ptr @scr_dyn_new_obj()`);
const byName = new Map(shape.fields.map((field) => [field.name, field]));
const order = shape.declaredOrder ?? shape.fields.map((field) => field.name);
const inOrder = new Set(order);
const fields = [
...order.map((name) => byName.get(name)).filter((field) => field !== undefined),
...shape.fields.filter((field) => !inOrder.has(field.name)),
];
// A shape with orderVariants has one field list per key order; its
// order index is no key.
const orders = shapeKeyOrders(shape);
const fieldsOf = (order: readonly string[]): IrRecordShape["fields"][number][] => {
const inOrder = new Set(order);
return [
...order.map((name) => byName.get(name)).filter((field) => field !== undefined),
...shape.fields.filter(
(field) => !inOrder.has(field.name) && field.name !== KEY_ORDER_FIELD,
),
];
};
// Published before the field converters, like the array hooks below.
// A Dirent row reads through its finished view (the prototype and
// the non-enumerable %dtype).
const dirent = isDirentShape(shape);
if (!dirent && liveRecordOpsEligible(fields)) {
adapter.recordOps = `@${snapshot}_rops`;
liveRecordOps(host, t, snapshot, fields);
}
for (const field of fields) {
const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
const fieldPtr = B.tmp();
B.line(
`${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`,
);
const fieldValue = loadStructField(
B,
host.sizeType,
fieldPtr,
field.type,
host.inlineUnions.record(field.type),
);
// An ABSENT optional field contributes no key.
const undefinedTag = undefinedArmTag(field.type, host.unionsById);
let skip: string | null = null;
if (undefinedTag >= 0 && field.type.kind === "union") {
const absent = host.fieldAbsentTestIn(B, fieldValue, field.type.unionId);
skip = B.newLabel("live.record.absent");
const set = B.newLabel("live.record.set");
B.condBr(absent, skip, set);
B.startBlock(set);
}
const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue);
B.line(
`call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`,
);
if (skip !== null) {
B.br(skip);
B.startBlock(skip);
if (!dirent && orders.every((order) => liveRecordOpsEligible(fieldsOf(order)))) {
if (orders.length === 1) {
adapter.recordOps = `@${snapshot}_rops`;
liveRecordOps(host, t, snapshot, fieldsOf(orders[0]!));
} else {
orders.forEach((order, i) =>
liveRecordOps(host, t, `${snapshot}_o${i}`, fieldsOf(order)),
);
adapter.recordOps = `@${snapshot}_rops`;
liveRecordOrderSelect(host, shape, snapshot);
}
}
emitPerKeyOrder(B, shape, host.nullableUnions.fieldBase(t.shapeId), "%p", (order) => {
for (const field of fieldsOf(order)) {
const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
const fieldPtr = B.tmp();
B.line(
`${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`,
);
const fieldValue = loadStructField(
B,
host.sizeType,
fieldPtr,
field.type,
host.inlineUnions.record(field.type),
);
// An ABSENT optional field contributes no key.
const undefinedTag = undefinedArmTag(field.type, host.unionsById);
let skip: string | null = null;
if (undefinedTag >= 0 && field.type.kind === "union") {
const absent = host.fieldAbsentTestIn(B, fieldValue, field.type.unionId);
skip = B.newLabel("live.record.absent");
const set = B.newLabel("live.record.set");
B.condBr(absent, skip, set);
B.startBlock(set);
}
const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue);
B.line(
`call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`,
);
if (skip !== null) {
B.br(skip);
B.startBlock(skip);
}
}
});
if (dirent) {
host.declare(`declare void @scr_dyn_dirent_finish(ptr)`);
B.line(`call void @scr_dyn_dirent_finish(ptr ${out})`);
@@ -1373,7 +1392,35 @@ function liveRecordOps(
``,
`@${names} = internal constant [${fields.length} x ptr] [${literals.join(", ")}]`,
// Mutable: the runtime fills the trailing key-array slot on first use.
`@${ops} = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr } { ${host.sizeType} ${fields.length}, ptr @${names}, ptr @${has}, ptr @${get}, i8 ${optional.length === 0 ? 1 : 0}, ptr null }`,
`@${ops} = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr, ptr } { ${host.sizeType} ${fields.length}, ptr @${names}, ptr @${has}, ptr @${get}, i8 ${optional.length === 0 ? 1 : 0}, ptr null, ptr null }`,
``,
);
}
/** `@<snapshot>_rops` for a shape with orderVariants: a record-ops table
* whose `select` hook answers the `_o<i>_rops` table (liveRecordOps) of the
* record's current key order, which a write of an optional key can change
* after the record is boxed. */
function liveRecordOrderSelect(
host: LlvmEmitterContext,
shape: IrRecordShape,
snapshot: string,
): void {
const B = new BlockBuilder();
const out = B.slot();
B.entryAllocas.push(`${out} = alloca ptr`);
emitPerKeyOrder(B, shape, host.nullableUnions.fieldBase(shape.id), "%p", (_, i) => {
B.line(`store ptr @${snapshot}_o${i}_rops, ptr ${out}`);
});
const ops = B.tmp();
B.line(`${ops} = load ptr, ptr ${out}`);
B.terminate(`ret ptr ${ops}`);
host.resolveThunkDefs.push(
`define internal ptr @${snapshot}_select(ptr %p) ${FN_ATTRS} { ; live record ops by key order`,
B.render(),
`}`,
``,
`@${snapshot}_rops = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr, ptr } { ${host.sizeType} 0, ptr null, ptr null, ptr null, i8 0, ptr null, ptr @${snapshot}_select }`,
``,
);
}
@@ -0,0 +1,49 @@
/* Key-order dispatch for shapes whose values record the order their keys
* were written in (IrRecordShape.orderVariants): an emitter that walks a
* record's keys emits one walk per order and branches on the value's
* KEY_ORDER_FIELD. */
import type { IrRecordShape } from "../../ir/ir.js";
import { KEY_ORDER_FIELD, shapeKeyOrders } from "../../ir/ir.js";
import { mangleRecordStruct } from "../mangle.js";
import type { BlockBuilder } from "./blocks.js";
/** Emits `walk(order, index)` for record `rec` of `shape`, once per order behind a
* switch on the value's order index when the shape has several. Each walk
* starts in its own block, and the code after this call continues in the
* block they all branch to. */
export function emitPerKeyOrder(
B: BlockBuilder,
shape: IrRecordShape,
fieldBase: number,
rec: string,
walk: (order: readonly string[], index: number) => void,
): void {
const orders = shapeKeyOrders(shape);
if (orders.length === 1) {
walk(orders[0]!, 0);
return;
}
const index = shape.fields.findIndex((f) => f.name === KEY_ORDER_FIELD) + fieldBase;
const p = B.tmp();
const n = B.tmp();
const i = B.tmp();
B.line(
`${p} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr ${rec}, i64 0, i32 ${index} ; .${KEY_ORDER_FIELD}`,
);
B.line(`${n} = load double, ptr ${p}`);
B.line(`${i} = fptosi double ${n} to i32`);
const labels = orders.map(() => B.newLabel("ko"));
const join = B.newLabel("ko.join");
B.terminate(
`switch i32 ${i}, label %${labels[0]} [ ${labels
.slice(1)
.map((label, k) => `i32 ${k + 1}, label %${label}`)
.join(" ")} ]`,
);
orders.forEach((order, k) => {
B.startBlock(labels[k]!);
walk(order, k);
B.br(join);
});
B.startBlock(join);
}
+71 -68
View File
@@ -1,3 +1,4 @@
import { emitPerKeyOrder } from "./key-orders.js";
import { f64Lit } from "./common.js";
import { InternalCompilerError } from "../../errors.js";
/* Structure-walking helper EMITTERS for the LLVM backend — the .ll mirror
@@ -328,78 +329,80 @@ export class LlWalkers {
if (cyclic) this.jbLeave(B);
return;
}
// Fields serialize in DECLARED order (JS insertion order); internal
// Fields serialize in DECLARED order (JS insertion order), or in the
// value's own order for a shape with orderVariants; internal
// '%'-fields stay hidden — jsonWriteHelper's contract, ported comment
// and all.
const order = shape.declaredOrder ?? shape.fields.map((f) => f.name);
const inOrder = new Set(order);
if (shape.fields.some((f) => !inOrder.has(f.name) && !f.name.startsWith("%"))) {
throw new InternalCompilerError(
`llvm emitter bug: declaredOrder of shape ${shapeId} omits a non-internal field`,
);
}
const byName = new Map(shape.fields.map((f) => [f.name, f]));
// Function-valued properties are omitted, as JSON.stringify omits them.
const emitFields = order
.map((n) => byName.get(n))
.filter((f) => f !== undefined)
.filter((f) => f.type.kind !== "func");
const droppable =
emitFields.some((f) => undefinedArmTag(f.type, this.host.unionsById) >= 0) ||
!!shape.indexValue;
this.putc(B, "%b", "123"); // '{'
if (!droppable) {
emitFields.forEach((f, i) => {
this.puts(B, "%b", `${i > 0 ? "," : ""}${jsonObjectKeyLabel(f.name)}`);
if (edgeable(f.type)) this.jbEdgeProp(B, f.name);
const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type);
B.line(
`call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`,
emitPerKeyOrder(B, shape, fieldBase, "%v", (order) => {
const inOrder = new Set(order);
if (shape.fields.some((f) => !inOrder.has(f.name) && !f.name.startsWith("%"))) {
throw new InternalCompilerError(
`llvm emitter bug: declaredOrder of shape ${shapeId} omits a non-internal field`,
);
});
} else {
const first = B.slot();
B.entryAllocas.push(`${first} = alloca i1`);
B.line(`store i1 true, ptr ${first}`);
const comma = (): void => {
const isf = B.tmp();
const lc = B.newLabel("jwc.c");
const lj = B.newLabel("jwc.j");
B.line(`${isf} = load i1, ptr ${first}`);
B.condBr(isf, lj, lc);
B.startBlock(lc);
this.putc(B, "%b", "44"); // ','
B.br(lj);
B.startBlock(lj);
B.line(`store i1 false, ptr ${first}`);
};
for (const f of emitFields) {
const utag = undefinedArmTag(f.type, this.host.unionsById);
const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type);
let skip: string | null = null;
if (utag >= 0 && f.type.kind === "union") {
// Undefined-valued field: dropped, like Node.
const tag = this.unionTag(B, v, f.type.unionId);
const isu = B.tmp();
B.line(`${isu} = icmp eq i32 ${tag}, ${utag}`);
const lw = B.newLabel("jwf.w");
skip = B.newLabel("jwf.s");
B.condBr(isu, skip, lw);
B.startBlock(lw);
}
comma();
this.puts(B, "%b", jsonObjectKeyLabel(f.name));
if (edgeable(f.type)) this.jbEdgeProp(B, f.name);
B.line(
`call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`,
);
if (skip !== null) {
B.br(skip);
B.startBlock(skip);
}
}
if (shape.indexValue) this.emitOverflowEntries(B, shape, first, edgeable(shape.indexValue));
}
const byName = new Map(shape.fields.map((f) => [f.name, f]));
// Function-valued properties are omitted, as JSON.stringify omits them.
const emitFields = order
.map((n) => byName.get(n))
.filter((f) => f !== undefined)
.filter((f) => f.type.kind !== "func");
const droppable =
emitFields.some((f) => undefinedArmTag(f.type, this.host.unionsById) >= 0) ||
!!shape.indexValue;
this.putc(B, "%b", "123"); // '{'
if (!droppable) {
emitFields.forEach((f, i) => {
this.puts(B, "%b", `${i > 0 ? "," : ""}${jsonObjectKeyLabel(f.name)}`);
if (edgeable(f.type)) this.jbEdgeProp(B, f.name);
const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type);
B.line(
`call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`,
);
});
} else {
const first = B.slot();
B.entryAllocas.push(`${first} = alloca i1`);
B.line(`store i1 true, ptr ${first}`);
const comma = (): void => {
const isf = B.tmp();
const lc = B.newLabel("jwc.c");
const lj = B.newLabel("jwc.j");
B.line(`${isf} = load i1, ptr ${first}`);
B.condBr(isf, lj, lc);
B.startBlock(lc);
this.putc(B, "%b", "44"); // ','
B.br(lj);
B.startBlock(lj);
B.line(`store i1 false, ptr ${first}`);
};
for (const f of emitFields) {
const utag = undefinedArmTag(f.type, this.host.unionsById);
const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type);
let skip: string | null = null;
if (utag >= 0 && f.type.kind === "union") {
// Undefined-valued field: dropped, like Node.
const tag = this.unionTag(B, v, f.type.unionId);
const isu = B.tmp();
B.line(`${isu} = icmp eq i32 ${tag}, ${utag}`);
const lw = B.newLabel("jwf.w");
skip = B.newLabel("jwf.s");
B.condBr(isu, skip, lw);
B.startBlock(lw);
}
comma();
this.puts(B, "%b", jsonObjectKeyLabel(f.name));
if (edgeable(f.type)) this.jbEdgeProp(B, f.name);
B.line(
`call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`,
);
if (skip !== null) {
B.br(skip);
B.startBlock(skip);
}
}
if (shape.indexValue) this.emitOverflowEntries(B, shape, first, edgeable(shape.indexValue));
}
});
this.putc(B, "%b", "125"); // '}'
if (cyclic) this.jbLeave(B);
}
+2 -2
View File
@@ -2,8 +2,8 @@
* of the link contract: a program object keeps this symbol undefined and the
* matching runtime defines it, so incompatible manual links fail before the
* program can start. */
export const RUNTIME_ABI_VERSION = 27 as const;
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v27" as const;
export const RUNTIME_ABI_VERSION = 28 as const;
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v28" as const;
/** Object consumption is useful today, but the complete scr_* surface may
* still change before 1.0. The versioned marker prevents accidental mixing;
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) {
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" },
runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" },
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
flavors: {
@@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" },
runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" },
compiler: {
command: "clang",
identity: "fixture clang",
@@ -724,6 +724,11 @@ function keyOrderFinding(
written: readonly string[],
assignedNames: ReadonlySet<string>,
): { native: string; node: string; detail: "reordered" | "added-later" } | null {
// Values of a shape with orderVariants enumerate in the order their
// literal wrote (key-order-witness.ts).
const mapped = safely(() => lowerer.mapTypeOf(contextual));
if (mapped?.kind === "record" && lowerer.shapes.get(mapped.shapeId)?.orderVariants !== undefined)
return null;
// A shape whose table the witness pass re-chose (key-order-witness.ts)
// enumerates that table, not the checker's declaration order.
const table = witnessTable(lowerer, contextual);
@@ -1,5 +1,12 @@
import { expect, test } from "vitest";
import { F64, type IrExpr, type IrFunction, type IrStmt, type IrType } from "../../ir/ir.js";
import {
F64,
KEY_ORDER_FIELD,
type IrExpr,
type IrFunction,
type IrStmt,
type IrType,
} from "../../ir/ir.js";
import { observeRecordLiteralRebuilds, transformExpr } from "../../ir/traverse.js";
import { ShapeRegistry } from "../type-mapper.js";
import {
@@ -21,7 +28,7 @@ interface Fixture {
notes: KeyOrderNotes;
record(fields: [string, IrType][], order: string[]): IrType & { kind: "record" };
literal(type: IrType, keys: string[], segments?: KeyOrderSegment[]): IrExpr;
run(body: IrStmt[]): void;
run(body: IrStmt[]): IrFunction;
order(type: IrType & { kind: "record" }): string[] | undefined;
}
@@ -35,6 +42,14 @@ function fixture(): Fixture {
classes: new Map(),
armTag: (unionId: string, arm: IrType): number =>
unionId === "u0" && arm.kind === "undefinedT" ? 1 : -1,
recordFieldPresent: (obj: IrExpr, shapeId: string, field: string): IrExpr => ({
kind: "recordHas",
obj,
shapeId,
field,
type: { kind: "bool" },
loc,
}),
} as unknown as Lowerer;
const record = (fields: [string, IrType][], order: string[]): IrType & { kind: "record" } => ({
kind: "record",
@@ -55,15 +70,20 @@ function fixture(): Fixture {
segments ?? keys.map((name) => ({ kind: "key", name, definite: true })),
);
};
const run = (body: IrStmt[]): void => {
const run = (body: IrStmt[]): IrFunction => {
const fn: IrFunction = { name: "main", params: [], returnType: F64, locals: [], body, loc };
applyKeyOrderWitnesses(lowerer, [fn]);
return fn;
};
const order = (type: IrType & { kind: "record" }): string[] | undefined =>
shapes.get(type.shapeId)?.declaredOrder;
return { lowerer, shapes, notes, record, literal, run, order };
}
/** The order index a literal stores (KEY_ORDER_FIELD), when it stores one. */
const orderIndexOf = (lit: IrExpr): IrExpr | undefined =>
lit.kind === "recordLit" ? lit.fields.find((x) => x.name === KEY_ORDER_FIELD)?.value : undefined;
const observe = (value: IrExpr): IrStmt => ({
kind: "exprStmt",
expr: { kind: "jsonStringify", value, type: { kind: "string" }, loc },
@@ -109,7 +129,7 @@ test("literals that agree with the table leave the same array in place", () => {
expect(f.notes.changed.size).toBe(0);
});
test("values built in two orders keep today's table", () => {
test("values built in two orders each record their literal's order", () => {
const f = fixture();
const ab = f.record(
[
@@ -118,8 +138,71 @@ test("values built in two orders keep today's table", () => {
],
["a", "b"],
);
f.run([observe(f.literal(ab, ["b", "a"])), observe(f.literal(ab, ["a", "b"]))]);
const ba = f.literal(ab, ["b", "a"]);
const aOrder = f.literal(ab, ["a", "b"]);
f.run([observe(ba), observe(aOrder)]);
expect(f.order(ab)).toEqual(["a", "b"]);
const shape = f.shapes.get(ab.shapeId)!;
expect(shape.orderVariants).toEqual([
["a", "b"],
["b", "a"],
]);
expect(shape.fields.map((x) => x.name)).toEqual([KEY_ORDER_FIELD, "a", "b"]);
expect(orderIndexOf(ba)).toMatchObject({ kind: "numLit", value: 1 });
expect(orderIndexOf(aOrder)).toMatchObject({ kind: "numLit", value: 0 });
});
test("a copy of a value with orders stores its source's index", () => {
const f = fixture();
const abc = f.record(
[
["a", F64],
["b", F64],
["c", F64],
],
["a", "b", "c"],
);
const ab = f.record(
[
["a", F64],
["b", F64],
],
["a", "b"],
);
const read = (field: string): IrExpr => ({
kind: "recordGet",
obj: ref("x", abc),
shapeId: abc.shapeId,
field,
type: F64,
loc,
});
const rest = f.notes.copy(
{
kind: "recordLit",
fields: [
{ name: "a", value: read("a") },
{ name: "b", value: read("b") },
],
type: ab,
loc,
},
abc.shapeId,
);
f.run([
decl("x", f.literal(abc, ["c", "b", "a"])),
observe(f.literal(abc, ["a", "b", "c"])),
observe(rest),
]);
expect(f.shapes.get(ab.shapeId)!.orderVariants).toEqual([
["a", "b"],
["b", "a"],
]);
expect(orderIndexOf(rest)).toMatchObject({
kind: "recordGet",
obj: { kind: "varRef", localId: "x" },
field: KEY_ORDER_FIELD,
});
});
test("an unobserved shape keeps today's table", () => {
@@ -337,7 +420,7 @@ test("straight-line writes of late-only keys keep program order", () => {
expect(f.order(o)).toEqual(["id", "y", "x"]);
});
test("late-only keys written through different bindings keep today's table", () => {
test("late-only keys written through different bindings move to the end per value", () => {
const f = fixture();
const o = f.record(
[
@@ -354,9 +437,13 @@ test("late-only keys written through different bindings keep today's table", ()
observe(ref("o", o)),
]);
expect(f.order(o)).toEqual(["id", "x", "y"]);
expect(f.shapes.get(o.shapeId)!.orderVariants).toEqual([
["id", "x", "y"],
["id", "y", "x"],
]);
});
test("a late-only key written in a loop over an outer binding keeps today's table", () => {
test("a late-only key written in a loop guards the write with its move", () => {
const f = fixture();
const o = f.record(
[
@@ -366,7 +453,7 @@ test("a late-only key written in a loop over an outer binding keeps today's tabl
],
["id", "x", "y"],
);
f.run([
const body: IrStmt[] = [
decl("o", f.literal(o, ["id"])),
set(ref("o", o), "y"),
{
@@ -376,11 +463,18 @@ test("a late-only key written in a loop over an outer binding keeps today's tabl
loc,
},
observe(ref("o", o)),
]);
];
const fn = f.run(body);
expect(f.order(o)).toEqual(["id", "x", "y"]);
const loop = fn.body.find((st) => st.kind === "while") as IrStmt & { kind: "while" };
const guarded = loop.body[0]!;
expect(guarded.kind).toBe("block");
const [move, write] = (guarded as IrStmt & { kind: "block" }).body;
expect(move).toMatchObject({ kind: "if", cond: { kind: "unary", op: "!" } });
expect(write).toMatchObject({ kind: "recordSet", field: "x" });
});
test("re-adding a key a literal creates earlier keeps today's table", () => {
test("re-adding a key a literal creates earlier moves it to the end of that value", () => {
const f = fixture();
const o = f.record(
[
@@ -399,4 +493,119 @@ test("re-adding a key a literal creates earlier keeps today's table", () => {
};
f.run([decl("o", f.literal(o, ["a", "b"])), del, set(ref("o", o), "a"), observe(ref("o", o))]);
expect(f.order(o)).toEqual(["a", "b"]);
expect(f.shapes.get(o.shapeId)!.orderVariants).toEqual([
["a", "b"],
["b", "a"],
]);
});
test("a moving write evaluates its value before it tests and moves the key", () => {
const f = fixture();
const o = f.record(
[
["id", F64],
["x", OPT],
["y", OPT],
],
["id", "x", "y"],
);
// `o.x = ((o.y = 2), 3)`: y is created first.
const value: IrExpr = {
kind: "seqExpr",
stmts: [set(ref("o", o), "y")],
result: num(3),
type: F64,
loc,
};
const write: IrStmt = {
kind: "recordSet",
obj: ref("o", o),
shapeId: o.shapeId,
field: "x",
value,
loc,
};
const fn = f.run([
decl("o", f.literal(o, ["id"])),
observe(f.literal(o, ["x", "id"])),
write,
observe(ref("o", o)),
]);
const shape = f.shapes.get(o.shapeId)!;
expect(shape.orderVariants).toBeDefined();
const block = fn.body[2] as IrStmt & { kind: "block" };
expect(block.kind).toBe("block");
// The receiver evaluates first, since the value may assign its binding.
const [receiver, first, guard, written] = block.body;
expect(receiver).toMatchObject({ kind: "varDecl", init: { kind: "varRef", localId: "o" } });
const target = (receiver as IrStmt & { kind: "varDecl" }).localId;
expect(first).toMatchObject({ kind: "varDecl", init: { kind: "seqExpr" } });
const local = (first as IrStmt & { kind: "varDecl" }).localId;
expect(guard).toMatchObject({ kind: "if", cond: { kind: "unary", op: "!" } });
expect(written).toMatchObject({
kind: "recordSet",
obj: { kind: "varRef", localId: target },
field: "x",
value: { kind: "varRef", localId: local },
});
// The write inside the value moves y in its own block.
const init = (first as IrStmt & { kind: "varDecl" }).init as IrExpr & { kind: "seqExpr" };
expect(init.stmts[0]).toMatchObject({ kind: "block" });
});
test("a key after a spread that the source may lack picks its order from the source's keys", () => {
const f = fixture();
const s = f.record(
[
["a", F64],
["b", OPT],
["c", OPT],
],
["a", "b", "c"],
);
const t = f.record(
[
["a", F64],
["b", F64],
["c", OPT],
],
["a", "b", "c"],
);
const read = (field: string): IrExpr => ({
kind: "recordGet",
obj: ref("x", s),
shapeId: s.shapeId,
field,
type: OPT,
loc,
});
// `{ ...x, b: 1 }`
const merged = f.notes.literal(
{
kind: "recordLit",
fields: [
{ name: "a", value: read("a") },
{ name: "c", value: read("c") },
{ name: "b", value: num(1) },
],
type: t,
loc,
},
[
{ kind: "spread", shapeId: s.shapeId, maybeAbsent: false },
{ kind: "key", name: "b", definite: true },
],
);
f.run([decl("x", f.literal(s, ["c", "a"])), observe(f.literal(s, ["a", "c"])), observe(merged)]);
// b keeps its place in the source's order when the source has it, and
// follows the source's keys when it does not.
expect(f.shapes.get(t.shapeId)!.orderVariants).toEqual([
["a", "c", "b"],
["c", "a", "b"],
["a", "b", "c"],
]);
expect(orderIndexOf(merged)).toMatchObject({
kind: "ternary",
cond: { kind: "recordHas", obj: { kind: "varRef", localId: "x" }, field: "b" },
});
});
@@ -41,12 +41,57 @@
* value: that shape, and every shape copied from it, keeps today's table
* (and today's SC6002 warnings). Shapes that no order surface observes
* keep today's table too. */
import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType } from "../../ir/ir.js";
import { shapeHasAccessorSlots, typeEquals, UNDEFINED_T } from "../../ir/ir.js";
import { everyExprChild, everyStmtChild, everyTypeChild } from "../../ir/traverse.js";
import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType, SrcLoc } from "../../ir/ir.js";
import {
BOOL,
F64,
KEY_ORDER_FIELD,
shapeHasAccessorSlots,
typeEquals,
UNDEFINED_T,
} from "../../ir/ir.js";
import {
everyExprChild,
everyStmtChild,
everyTypeChild,
transformStmtList,
} from "../../ir/traverse.js";
import type { Lowerer } from "./lowerer.js";
import { ALWAYS_THROW_LIB_FNS } from "../../ir/builtin-effects.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
* branch per order (walks[i] for order i) on the value's KEY_ORDER_FIELD.
* `walks` follows shapeKeyOrders. */
export function perKeyOrder(
shape: IrRecordShape,
ref: IrExpr,
walks: readonly IrStmt[][],
loc: SrcLoc,
): IrStmt[] {
let tail = walks[0]!;
for (let i = walks.length - 1; i >= 1; i--) {
const index: IrExpr = {
kind: "recordGet",
obj: ref,
shapeId: shape.id,
field: KEY_ORDER_FIELD,
type: F64,
loc,
};
const cond: IrExpr = {
kind: "bin",
op: "===",
left: index,
right: { kind: "numLit", value: i, type: F64, loc },
type: BOOL,
loc,
};
tail = [{ kind: "if", cond, then: walks[i]!, else_: tail, loc }];
}
return tail;
}
/** One property of an object literal, in source order. */
export type KeyOrderSegment =
/** An explicit key (`definite`), or a conditional spread's key, which
@@ -713,4 +758,496 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
shape.declaredOrder = [...shape.declaredOrder!.filter(isArrayIndex), ...resolved.order];
notes.changed.add(id);
}
// ── 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
// in (KEY_ORDER_FIELD), when every construction's order is known
// statically or copied from a value that records its own:
// - a literal of explicit keys writes its order;
// - a copy from a shape with one resolved order writes that order;
// - a copy from a value of a shape with orders writes the source's
// order, filtered to the copy's keys (rest bindings, width copies);
// - clones copy the field, and every other construction (synthesized
// literals, checked conversions, runtime producers) leaves 0, today's
// table, as before.
// A write of an optional key a value lacks moves the key to the end of
// the value's order, as in Node: the orders include every order such
// writes reach, and each write site updates the index after it evaluates
// the value and before it writes the key.
const MAX_ORDERS = 16;
const unresolved = (id: string): boolean => result.get(id) === "unknown";
/** `order` over every key of `table`: keys the order lacks follow it. */
const complete = (order: readonly string[], table: readonly string[]): string[] => {
const keys = new Set(table);
const out = order.filter((k) => keys.has(k));
const seen = new Set(out);
return [...out, ...table.filter((k) => !seen.has(k))];
};
const literalOrder = (segments: readonly KeyOrderSegment[]): string[] | null => {
const out: string[] = [];
for (const segment of segments) {
if (segment.kind !== "key" || !segment.definite) return null;
if (!isArrayIndex(segment.name)) out.push(segment.name);
}
return out;
};
/** `{ ...value }` of a value of the literal's own shape: a copy of the
* value's order. */
const selfSpread = (segments: readonly KeyOrderSegment[], id: string): boolean =>
segments.length === 1 &&
segments[0]!.kind === "spread" &&
segments[0]!.shapeId === id &&
!segments[0]!.maybeAbsent;
const sameOrder = (a: readonly string[], b: readonly string[]): boolean =>
a.join("\0") === b.join("\0");
const indexOfOrder = (orders: readonly string[][], order: readonly string[]): number =>
orders.findIndex((o) => sameOrder(o, order));
/** The keys writes can add to a value of the shape, or null when a
* clone override can (the clone's order is its source's). */
const movingKeys = (id: string, f: ShapeFacts, table: readonly string[]): string[] | null => {
const keys = new Set(table);
const moving = new Set<string>();
for (const w of f.writes) {
if (w.kind === "delete" || !keys.has(w.field) || !maybeAbsent(id, w.field)) continue;
if (w.kind === "clone") return null;
moving.add(w.field);
}
return [...moving];
};
/** `values[i]` for the order index `current` holds, as a chain of
* tests. */
const indexChain = (current: IrExpr, values: readonly number[], loc: SrcLoc): IrExpr => {
let out: IrExpr = { kind: "numLit", value: values[values.length - 1]!, type: F64, loc };
for (let i = values.length - 2; i >= 0; i--) {
if (out.kind === "numLit" && out.value === values[i]) continue;
out = {
kind: "ternary",
cond: {
kind: "bin",
op: "===",
left: current,
right: { kind: "numLit", value: i, type: F64, loc },
type: BOOL,
loc,
},
then: { kind: "numLit", value: values[i]!, type: F64, loc },
else_: out,
type: F64,
loc,
};
}
return out;
};
const moves = new Map<string, Map<string, number[]>>();
/** Adds to `orders` every order writes of `moving` keys reach from them
* and records each key's transitions; false past MAX_ORDERS. */
const closeUnderMoves = (id: string, orders: string[][], moving: readonly string[]): boolean => {
const transitions = new Map<string, number[]>(moving.map((k) => [k, []]));
for (let i = 0; i < orders.length; i++) {
for (const key of moving) {
const moved = [...orders[i]!.filter((k) => k !== key), key];
let index = indexOfOrder(orders, moved);
if (index < 0) {
if (orders.length === MAX_ORDERS) return false;
index = orders.push(moved) - 1;
}
transitions.get(key)![i] = index;
}
}
if (moving.length > 0) moves.set(id, transitions);
return true;
};
// Observed shapes, and the shapes their values copy their orders from.
const wanted = new Set(observed);
const pending = [...observed];
while (pending.length > 0) {
const f = facts.get(pending.pop()!);
if (f === undefined) continue;
const sources = [
...f.copies.map((c) => c.source),
...f.literals.flatMap((segments) =>
segments.flatMap((s) => (s.kind === "spread" ? [s.shapeId] : [])),
),
];
for (const source of sources) {
if (wanted.has(source)) continue;
wanted.add(source);
pending.push(source);
}
}
const orderable = (
id: string,
f: ShapeFacts,
): { shape: IrRecordShape; moving: string[] } | null => {
const shape = shapes.get(id);
if (!wanted.has(id) || !unresolved(id) || !inScope(shape)) return null;
const moving = movingKeys(id, f, tableOf(shape));
return moving === null ? null : { shape, moving };
};
const variants = new Map<string, string[][]>();
for (const [id, f] of facts) {
const candidate = orderable(id, f);
if (candidate === null || f.literals.length === 0) continue;
const table = tableOf(candidate.shape);
const orders: string[][] = [table];
let ok = true;
const add = (order: readonly string[]): void => {
const full = complete(order, table);
if (indexOfOrder(orders, full) < 0) orders.push(full);
};
for (const segments of f.literals) {
if (selfSpread(segments, id)) continue;
const order = literalOrder(segments);
if (order === null) ok = false;
else add(order);
}
for (const c of f.copies) {
if (c.source === id) continue;
const source = result.get(c.source);
if (source === undefined || source === "unknown") ok = false;
else add(source.order.filter((k) => c.keys.has(k)));
}
if (!ok || orders.length > MAX_ORDERS || !closeUnderMoves(id, orders, candidate.moving))
continue;
if (orders.length > 1) variants.set(id, orders);
else moves.delete(id);
}
// Shapes built only from values of one shape with orders, all in one
// way: copies (rest bindings, width copies), or literals spreading the
// value between the same explicit keys. Order i is the source's order i
// over the shape's keys, after the keys before the spread and before
// the keys after it. A key after the spread that the source value may
// lack goes where the source has it, or after the source's keys when it
// has not: such a literal's orders are one per source order and set of
// those keys the source has, and `select.index[present][i]` is the order
// for source order i with the keys of bit set `present`.
type Template = {
source: string;
prefix: string[];
suffix: string[];
select?: { keys: string[]; index: number[][] };
};
const templateOf = (segments: readonly KeyOrderSegment[], id: string): Template | null => {
const prefix: string[] = [];
const suffix: string[] = [];
let source: string | null = null;
for (const segment of segments) {
if (segment.kind === "opaque") return null;
if (segment.kind === "spread") {
if (source !== null || segment.maybeAbsent || segment.shapeId === id) return null;
source = segment.shapeId;
continue;
}
if (!segment.definite) return null;
(source === null ? prefix : suffix).push(segment.name);
}
return source === null ? null : { source, prefix, suffix };
};
const templateKey = (t: Template): string => JSON.stringify([t.source, t.prefix, t.suffix]);
const copyTemplate = (source: string): Template => ({ source, prefix: [], suffix: [] });
const mirrors = new Map<string, Template>();
for (const [id, f] of facts) {
if (variants.has(id) || f.pinned || f.runtime || f.opaque) continue;
const candidate = orderable(id, f);
if (candidate === null || f.literals.length + f.copies.length === 0) continue;
const templates: (Template | null)[] = f.literals.map((segments) => templateOf(segments, id));
for (const c of f.copies) templates.push(copyTemplate(c.source));
const first = templates[0];
if (
first === null ||
first === undefined ||
templates.some((t) => t === null || templateKey(t) !== templateKey(first))
)
continue;
const orders = variants.get(first.source);
if (orders === undefined || mirrors.has(first.source)) continue;
const table = tableOf(candidate.shape);
const tested = first.suffix.filter((k) => maybeAbsent(first.source, k));
if (tested.length > 4) continue;
const mirrorOf = (o: readonly string[], lacks: ReadonlySet<string>): string[] => {
const order = [...first.prefix];
for (const k of o) if (!lacks.has(k) && !order.includes(k)) order.push(k);
for (const k of first.suffix) if (!order.includes(k)) order.push(k);
return complete(order, table);
};
let mirrored: string[][];
let template: Template = first;
if (tested.length === 0) mirrored = orders.map((o) => mirrorOf(o, new Set()));
else {
mirrored = [];
const index: number[][] = [];
for (let present = 0; present < 1 << tested.length; present++) {
const lacks = new Set(tested.filter((_, j) => (present & (1 << j)) === 0));
const row: number[] = [];
for (const o of orders) {
const order = mirrorOf(o, lacks);
let i = indexOfOrder(mirrored, order);
if (i < 0) i = mirrored.push(order) - 1;
row.push(i);
}
index.push(row);
}
if (mirrored.length > MAX_ORDERS) continue;
template = {
source: first.source,
prefix: first.prefix,
suffix: first.suffix,
select: { keys: tested, index },
};
}
if (!closeUnderMoves(id, mirrored, candidate.moving)) continue;
if (mirrored.every((o) => sameOrder(o, mirrored[0]!))) {
// Every order of the source agrees over these keys: one table.
moves.delete(id);
const shape = candidate.shape;
const only: string[] = mirrored[0] ?? table;
if (!sameOrder(only, table))
shape.declaredOrder = [...shape.declaredOrder!.filter(isArrayIndex), ...only];
continue;
}
variants.set(id, mirrored);
mirrors.set(id, template);
}
if (variants.size === 0) return;
// Every construction of those shapes, to give each its order index. A
// copy reads its source's index, so its source must be a plain binding.
const sites = new Map<string, (IrExpr & { kind: "recordLit" })[]>();
/** The statements a spread literal's sequence evaluates first: the
* spread value's binding, then its fields. */
const prologues = new Map<IrExpr, readonly IrStmt[]>();
const siteOf = (e: IrExpr): boolean => {
if (e.kind === "seqExpr" && e.result.kind === "recordLit") prologues.set(e.result, e.stmts);
if (e.kind === "recordLit" && e.type.kind === "record" && variants.has(e.type.shapeId)) {
const list = sites.get(e.type.shapeId) ?? [];
list.push(e);
sites.set(e.type.shapeId, list);
}
return everyExprChild(e, siteOf, siteStmt);
};
const siteStmt = (st: IrStmt): boolean => everyStmtChild(st, siteOf, siteStmt);
for (const fn of fns) for (const st of fn.body) siteStmt(st);
/** The binding a copy or spread reads its fields from. */
const copySource = (lit: IrExpr & { kind: "recordLit" }, source: string): IrExpr | null => {
for (const field of lit.fields) {
const value = field.value;
if (value.kind === "recordGet" && value.shapeId === source && value.obj.kind === "varRef")
return value.obj;
}
for (const st of prologues.get(lit) ?? []) {
if (st.kind !== "varDecl" || st.init === null) continue;
const t = st.init.type;
if (t.kind === "record" && t.shapeId === source)
return { kind: "varRef", localId: st.localId, type: t, loc: lit.loc };
}
return null;
};
const orderIndex = (id: string, lit: IrExpr & { kind: "recordLit" }): IrExpr | number | null => {
const orders = variants.get(id)!;
const segments = notes.literalOf(lit);
if (segments !== undefined && selfSpread(segments, id)) {
const obj = copySource(lit, id);
if (obj === null) return null;
return {
kind: "recordGet",
obj,
shapeId: id,
field: KEY_ORDER_FIELD,
type: F64,
loc: lit.loc,
};
}
const mirror = mirrors.get(id);
if (segments !== undefined && mirror !== undefined) {
const obj = copySource(lit, mirror.source);
if (obj === null) return null;
const sourceIndex: IrExpr = {
kind: "recordGet",
obj,
shapeId: mirror.source,
field: KEY_ORDER_FIELD,
type: F64,
loc: lit.loc,
};
const select = mirror.select;
if (select === undefined) return sourceIndex;
// Tests the source for each key in turn, then picks by its index.
const pick = (j: number, present: number): IrExpr => {
if (j === select.keys.length)
return indexChain(sourceIndex, select.index[present]!, lit.loc);
return {
kind: "ternary",
cond: lowerer.recordFieldPresent(obj, mirror.source, select.keys[j]!, lit.loc),
then: pick(j + 1, present | (1 << j)),
else_: pick(j + 1, present),
type: F64,
loc: lit.loc,
};
};
return pick(0, 0);
}
if (segments !== undefined) {
const order = literalOrder(segments);
return order === null ? null : indexOfOrder(orders, complete(order, orders[0]!));
}
const source = notes.copyOf(lit);
if (source === undefined) return 0;
if (variants.has(source) && (source === id || mirrors.get(id)?.source === source)) {
const obj = copySource(lit, source);
if (obj === null) return null;
return {
kind: "recordGet",
obj,
shapeId: source,
field: KEY_ORDER_FIELD,
type: F64,
loc: lit.loc,
};
}
const resolved = result.get(source);
if (resolved === undefined || resolved === "unknown") return null;
const keys = new Set(
lit.fields.filter((x) => x.value.kind !== "fieldAbsent").map((x) => x.name),
);
return indexOfOrder(
orders,
complete(
resolved.order.filter((k) => keys.has(k)),
orders[0]!,
),
);
};
const plan = new Map<IrExpr & { kind: "recordLit" }, IrExpr | number>();
const dropped = new Set<string>();
for (const [id, list] of sites) {
for (const lit of list) {
const index = orderIndex(id, lit);
if (index === null || (typeof index === "number" && index < 0)) dropped.add(id);
else plan.set(lit, index);
}
}
// Write sites of keys that move: each reads and updates the value's
// index before the write.
const writeSites: { st: IrStmt & { kind: "recordSet" }; fn: IrFunction }[] = [];
let writeFn: IrFunction | null = null;
const writeExpr = (e: IrExpr): boolean => everyExprChild(e, writeExpr, writeOf);
const writeOf = (st: IrStmt): boolean => {
if (
st.kind === "recordSet" &&
st.value.kind !== "fieldAbsent" &&
moves.get(st.shapeId)?.has(st.field)
)
writeSites.push({ st, fn: writeFn! });
return everyStmtChild(st, writeExpr, writeOf);
};
for (const fn of fns) {
writeFn = fn;
for (const st of fn.body) writeOf(st);
}
// A shape whose copies cannot read their source keeps today's table, and
// so do the shapes that mirror it.
for (const [id, mirror] of mirrors) if (dropped.has(mirror.source)) dropped.add(id);
for (const [lit, index] of plan) {
const id = (lit.type as IrType & { kind: "record" }).shapeId;
if (dropped.has(id)) continue;
const value: IrExpr =
typeof index === "number" ? { kind: "numLit", value: index, type: F64, loc: lit.loc } : index;
lit.fields.push({ name: KEY_ORDER_FIELD, value });
}
let temporaries = 0;
const replacements = new Map<IrFunction, Map<IrStmt, IrStmt>>();
for (const { st, fn } of writeSites) {
if (dropped.has(st.shapeId)) continue;
const transitions = moves.get(st.shapeId)!.get(st.field)!;
const loc = st.loc;
// A receiver other than a binding evaluates once, into a local. So does
// a binding when the value evaluates first (below): the value may
// assign the binding, and the write still goes to the object the
// binding held before.
const pure =
st.value.kind === "numLit" ||
st.value.kind === "strLit" ||
st.value.kind === "boolLit" ||
st.value.kind === "varRef";
let plain = st.obj;
while (plain.kind === "unionNarrow" || plain.kind === "downcast") plain = plain.value;
const prologue: IrStmt[] = [];
let obj = st.obj;
if (plain.kind !== "varRef" || !pure) {
const localId = `%keyOrderReceiver.${temporaries++}`;
fn.locals.push({ id: localId, name: "%keyOrderReceiver", type: st.obj.type, mutable: false });
prologue.push({ kind: "varDecl", localId, init: st.obj, loc });
obj = { kind: "varRef", localId, type: st.obj.type, loc };
}
const current: IrExpr = {
kind: "recordGet",
obj,
shapeId: st.shapeId,
field: KEY_ORDER_FIELD,
type: F64,
loc,
};
const next = indexChain(current, transitions, loc);
// The value evaluates before the key is tested and moved: it may add
// other keys to the same value, which then come first.
let value = st.value;
if (!pure) {
const localId = `%keyOrderValue.${temporaries++}`;
fn.locals.push({ id: localId, name: "%keyOrderValue", type: value.type, mutable: false });
prologue.push({ kind: "varDecl", localId, init: value, loc });
value = { kind: "varRef", localId, type: value.type, loc };
}
const absent: IrExpr = {
kind: "unary",
op: "!",
operand: lowerer.recordFieldPresent(obj, st.shapeId, st.field, loc),
type: BOOL,
loc,
};
const update: IrStmt = {
kind: "recordSet",
obj,
shapeId: st.shapeId,
field: KEY_ORDER_FIELD,
value: next,
loc,
};
// The write becomes a block that moves the key first.
const write: IrStmt = {
kind: "recordSet",
obj,
shapeId: st.shapeId,
field: st.field,
value,
loc,
};
const guard: IrStmt = { kind: "if", cond: absent, then: [update], else_: null, loc };
const block: IrStmt = { kind: "block", body: [...prologue, guard, write], loc };
let replaced = replacements.get(fn);
if (replaced === undefined) {
replaced = new Map();
replacements.set(fn, replaced);
}
replaced.set(st, block);
}
for (const [fn, replaced] of replacements) {
fn.body = transformStmtList(fn.body, {
expr: (e) => e,
stmt: (st) => replaced.get(st) ?? st,
});
}
for (const [id, orders] of variants) {
if (dropped.has(id)) continue;
const shape = shapes.get(id)!;
const indexKeys: string[] = (shape.declaredOrder ?? []).filter(isArrayIndex);
shapes.addKeyOrderField(id);
const tables: string[][] = [];
for (const order of orders) {
const table: string[] = [...indexKeys];
for (const key of order) table.push(key);
tables.push(table);
}
shape.orderVariants = tables;
}
}
@@ -1,4 +1,5 @@
import { keyedHelperName } from "./stable-names.js";
import { perKeyOrder } from "./key-order-witness.js";
import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js";
import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js";
import { lowerDynStreamMemberCall } from "./dyn-streams.js";
@@ -73,6 +74,7 @@ import {
isDynTypedRefType,
isUnitType,
shapeHasAccessorSlots,
shapeKeyOrders,
typeEquals,
RUNTIME_EMITTER_CLASS,
} from "../../ir/ir.js";
@@ -7090,8 +7092,12 @@ export function recordKeysArrayCall(
loc,
},
];
const order = current.declaredOrder ?? current.fields.map((f) => f.name);
for (const name of order) {
// One walk per key order (see perKeyOrder).
const orders = shapeKeyOrders(current);
const walks = orders.map((): IrStmt[] => []);
for (const [k, name] of orders.flatMap((order, k) =>
order.map((name) => [k, name] as const),
)) {
const f = current.fields.find((x) => x.name === name)!;
const pushStmt: IrStmt = {
kind: "exprStmt",
@@ -7108,12 +7114,13 @@ export function recordKeysArrayCall(
// Fields that can be absent push behind their presence test (the
// key exists exactly when Object.keys would list it).
const present = lowerer.recordFieldPresent(ref, argIr.shapeId, f.name, loc);
body.push(
walks[k]!.push(
present.kind === "recordHas"
? { kind: "if", cond: present, then: [pushStmt], else_: null, loc }
: pushStmt,
);
}
body.push(...perKeyOrder(current, ref, walks, loc));
body.push({ kind: "return", value: outRef, loc });
fn.body = body;
};
@@ -8799,8 +8806,12 @@ function lowerObjectStaticCall(
loc,
},
];
const order = current.declaredOrder ?? current.fields.map((f) => f.name);
for (const name of order) {
// One walk per key order (see perKeyOrder).
const orders = shapeKeyOrders(current);
const walks = orders.map((): IrStmt[] => []);
for (const [k, name] of orders.flatMap((order, k) =>
order.map((name) => [k, name] as const),
)) {
const f = current.fields.find((x) => x.name === name)!;
const raw: IrExpr = {
kind: "recordGet",
@@ -8962,8 +8973,9 @@ function lowerObjectStaticCall(
loc,
};
}
body.push(stmt);
walks[k]!.push(stmt);
}
body.push(...perKeyOrder(current, ref, walks, loc));
body.push({ kind: "return", value: outRef, loc });
fn.body = body;
};
@@ -1,4 +1,5 @@
import { keyedHelperName } from "./stable-names.js";
import { perKeyOrder } from "./key-order-witness.js";
import {
dynUndefinedExpr,
boolLit,
@@ -40,6 +41,7 @@ import {
isDynTypedRefType,
recordTextCodecClass,
shapeHasAccessorSlots,
shapeKeyOrders,
typeKey,
} from "../../ir/ir.js";
import type { ClassInfo } from "./lower-classes.js";
@@ -1647,7 +1649,12 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
});
}
body.push(depthGate("[Object]"), ...begin());
for (const fname of order) {
// One walk per key order (see perKeyOrder).
const orders = shapeKeyOrders(current);
const walks = orders.map((): IrStmt[] => []);
for (const [k, fname] of orders.flatMap((order, k) =>
order.map((name) => [k, name] as const),
)) {
const ft = byName.get(fname);
if (!ft) continue;
const show = entry(
@@ -1655,12 +1662,13 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
boolLit(false, loc),
);
const present = lowerer.recordFieldPresent(v(), t.shapeId, fname, loc);
body.push(
walks[k]!.push(
present.kind === "recordHas"
? { kind: "if", cond: present, then: [show], else_: null, loc }
: show,
);
}
body.push(...perKeyOrder(current, v(), walks, loc));
body.push(
ret(end(strLit("", loc), strLit("{", loc), strLit("}", loc), false, boolLit(false, loc))),
);
+16 -1
View File
@@ -42,7 +42,7 @@ import {
UNDEFINED_T,
VOID,
} from "../ir/ir.js";
import { BIGINT_T } from "../ir/ir.js";
import { BIGINT_T, KEY_ORDER_FIELD } from "../ir/ir.js";
import { literalValues } from "./literal-values.js";
import { remapUnionDiscriminant } from "./union-discriminants.js";
@@ -554,6 +554,21 @@ export class ShapeRegistry {
return missing.length === 0 ? order : [...order, ...missing];
}
/** Gives a shape with orderVariants its KEY_ORDER_FIELD, in place,
* after the bodies lowered (key-order-witness.ts adds the field to each
* of the shape's literals). Allocation zeroes it, which every other
* construction keeps. */
addKeyOrderField(shapeId: string): void {
const shape = this.byId.get(shapeId);
if (!shape || shape.fields.some((f) => f.name === KEY_ORDER_FIELD)) return;
shape.fields = [...shape.fields, { name: KEY_ORDER_FIELD, type: F64 }].sort((a, b) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
);
this.revision++;
const key = this.keyOf(shape.fields, false, shape.indexValue);
if (!this.byKey.has(key)) this.byKey.set(key, shapeId);
}
/** Adds optional fields to an interned shape in place: members a type
* assertion attaches to the shape's values (`r as R & { tag: string }`
* followed by `view.tag = ...`; see assertion-members.ts). Every value
@@ -172,6 +172,7 @@ const MODULE_FIELDS: Record<StringFields<Omit<IrModule, "functions">>, Role> = {
"records[].fields[].name": "other",
"records[].declaredOrder[]": "other",
"records[].accessorOrder[]": "other",
"records[].orderVariants[][]": "other",
"unions[].id": "other",
"unions[].discriminant.field": "other",
"unions[].discriminant.cases[].values[]": "other",
+18
View File
@@ -1665,6 +1665,24 @@ export interface IrRecordShape {
* an accessor property named once (see accessorSlotProp). The
* checked-dynamic copy lists its keys in this order. */
accessorOrder?: string[];
/** Every order the shape's values enumerate their keys in, when its
* object literals write the keys in more than one order (see
* key-order-witness.ts); declaredOrder is the first. Each value holds the
* index of its order in the hidden number field KEY_ORDER_FIELD: the
* literal that wrote it stores its order's index, a copy its source's,
* and every other construction leaves 0. Each key-order surface
* dispatches on that field. */
orderVariants?: string[][];
}
/** The hidden field of a shape with orderVariants: the index of the value's
* key order. */
export const KEY_ORDER_FIELD = "%order";
/** The orders a record shape's values enumerate their keys in: each order
* of a shape with orderVariants, or its one table. */
export function shapeKeyOrders(shape: IrRecordShape): readonly (readonly string[])[] {
return shape.orderVariants ?? [shape.declaredOrder ?? shape.fields.map((f) => f.name)];
}
/** fs.Dirent rows: the hidden %dtype entry kind (libuv's UV_DIRENT
@@ -378,7 +378,7 @@ done:
expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"]));
expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"]));
expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]);
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v27");
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v28");
const clangExe = join(dir, "clang-program");
const linkDriver = join(dir, "clang-driver.c");
@@ -712,7 +712,7 @@ done:
.split("\n");
expect(undefinedSymbols).toContain("_sf_scale");
expect(undefinedSymbols).toContain("_sf_callback_mix");
expect(undefinedSymbols).toContain("_scr_runtime_abi_v27");
expect(undefinedSymbols).toContain("_scr_runtime_abi_v28");
});
test("helper and clang object paths retain the same outbound FFI C ABI", async () => {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7132-key-order-json.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7133-key-order-keys-values-entries.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7134-key-order-for-in.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7135-key-order-inspect.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7136-key-order-copies.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7137-key-order-dynamic.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7138-key-order-late-writes.ts"
],
"diags": []
}
@@ -340,7 +340,7 @@ async function build() {
package: packageManifest.name,
version: packageManifest.version,
target: RUNTIME_PACK_MATRIX.target,
runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" },
runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" },
compiler: {
command: compiler,
identity: compilerVersion,
@@ -437,7 +437,7 @@ async function build() {
package: packageManifest.name,
version: packageManifest.version,
target: matrix.target,
runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" },
runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" },
compiler: {
command: compiler,
identity: compilerVersion,
+1 -1
View File
@@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) {
}
#endif
void scr_runtime_abi_v27(void) {}
void scr_runtime_abi_v28(void) {}
void scr_init(void) {
#ifdef SCR_WORKERS
+1 -1
View File
@@ -81,7 +81,7 @@ void scr_init(void);
/* Program objects emitted by the bundled LLVM helper reference this symbol.
* Its versioned spelling makes a mismatched manual runtime link fail before
* the program can start. */
void scr_runtime_abi_v27(void);
void scr_runtime_abi_v28(void);
void scr_runtime_workers_v8(void);
/* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ──────
+6
View File
@@ -363,6 +363,12 @@ typedef struct ScrDynTypedRecordOps {
/* The field names as an immortal string[], built on first use by
* for-in over a record with every field present (atomic slot). */
ScrArr *keys;
/* A shape whose values enumerate their keys in more than one order
* (IrRecordShape.orderVariants) binds a table with only this hook: it
* answers the table of the record's current order, which a write of an
* optional key can change after the record is boxed. NULL in every other
* table. */
const struct ScrDynTypedRecordOps *(*select)(void *rec);
} ScrDynTypedRecordOps;
/* A record capsule's ops while it answers from the native record (no view
* created yet), or NULL. */
+2 -1
View File
@@ -3595,7 +3595,8 @@ bool scr_dyn_typed_ref_class_has(const ScrDyn *d, const ScrStr *key) {
static const ScrDynTypedRecordOps *scr_typed_ref_record_ops(const ScrDyn *d) {
if (!d->v.typed_ref.record || d->v.typed_ref.materialized) return NULL;
return (const ScrDynTypedRecordOps *)(const void *)d->v.typed_ref.array;
const ScrDynTypedRecordOps *ops = (const ScrDynTypedRecordOps *)(const void *)d->v.typed_ref.array;
return ops && ops->select ? ops->select(d->v.typed_ref.ptr) : ops;
}
const ScrDynTypedRecordOps *scr_dyn_typed_ref_live_record(const ScrDyn *d) {
+31
View File
@@ -0,0 +1,31 @@
// JSON.stringify of object literals that write the same keys in different
// orders: each value serializes in the order its literal wrote, as in Node.
type Point = { x: number; y: number; label?: string };
const a = { id: 1, name: "a", ok: true };
const b = { name: "b", ok: false, id: 2 };
const c = { ok: true, id: 3, name: "c" };
for (const value of [a, b, c]) console.log(JSON.stringify(value));
console.log(JSON.stringify([a, b, c]));
console.log(JSON.stringify({ first: b, rest: [c, a] }, null, 2));
// Optional keys the literal leaves out, and writes to keys it has.
const p: Point = { y: 2, x: 1 };
const q: Point = { label: "q", x: 3, y: 4 };
const r: Point = { x: 5, y: 6, label: "r" };
q.x = 30;
r.label = "r2";
for (const point of [p, q, r]) console.log(JSON.stringify(point));
// A function typed by one literal's shape receiving the others.
function describe(value: { id: number; name: string; ok: boolean }): string {
return JSON.stringify(value);
}
console.log(describe(c), describe(a));
// JSON.parse into the same record type keeps each text's own key order.
type Entry = { id: number; name: string; ok: boolean };
for (const text of ['{"ok":true,"name":"p","id":7}', '{"id":8,"name":"q","ok":false}']) {
const parsed = JSON.parse(text) as Entry;
console.log(JSON.stringify(parsed), Object.keys(parsed).join(","), describe(parsed));
}
@@ -0,0 +1,19 @@
// Object.keys, Object.values, Object.entries and Object.fromEntries over
// object literals that write the same keys in different orders.
const a = { id: 1, name: "a", ok: true };
const b = { name: "b", ok: false, id: 2 };
const c = { ok: true, id: 3, name: "c" };
for (const value of [a, b, c]) {
console.log(Object.keys(value).join(","));
console.log(Object.values(value).join(","));
console.log(Object.entries(value).map(([key, item]) => `${key}=${String(item)}`).join(" "));
console.log(JSON.stringify(Object.fromEntries(Object.entries(value))));
}
type Row = { year: number; title?: string; score: number };
const rows: Row[] = [
{ score: 1, year: 2001 },
{ title: "t", year: 2002, score: 2 },
{ year: 2003, score: 3, title: "u" },
];
for (const row of rows) console.log(Object.keys(row).join(","), Object.values(row).length);
+15
View File
@@ -0,0 +1,15 @@
// for-in over object literals that write the same keys in different
// orders, and over values a function received.
const a = { id: 1, name: "a", ok: true };
const b = { name: "b", ok: false, id: 2 };
const c = { ok: true, id: 3, name: "c" };
function keysOf(value: { id: number; name: string; ok: boolean }): string {
const keys: string[] = [];
for (const key in value) keys.push(key);
return keys.join(",");
}
for (const value of [a, b, c]) {
const seen: string[] = [];
for (const key in value) seen.push(key.toUpperCase());
console.log(seen.join(" "), keysOf(value));
}
+12
View File
@@ -0,0 +1,12 @@
import { inspect } from "node:util";
// console.log and util.inspect of object literals that write the same keys
// in different orders, alone and nested.
const a = { id: 1, name: "a", ok: true };
const b = { name: "b", ok: false, id: 2 };
const c = { ok: true, id: 3, name: "c" };
console.log(a, b, c);
console.log([a, b, c]);
console.log({ outer: { inner: b }, list: [c] });
console.log(inspect(b, { depth: 0 }), inspect({ deep: { deeper: c } }, { depth: 0 }));
console.log(`${inspect(c)} / ${inspect(a)}`);
+32
View File
@@ -0,0 +1,32 @@
// Copies of object literals that write the same keys in different orders:
// spreads, spreads with other keys before and after, rest bindings,
// Object.assign and structuredClone keep each value's own order.
const a = { id: 1, name: "a", ok: true };
const b = { name: "b", ok: false, id: 2 };
const c = { ok: true, id: 3, name: "c" };
for (const value of [a, b, c]) {
const copy = { ...value };
const changed = { ...value, id: 10 };
const after = { ...value, extra: "e" };
const before = { first: 0, ...value };
const { name, ...rest } = value;
const assigned = Object.assign({}, value);
const cloned = structuredClone(value);
console.log(JSON.stringify(copy), JSON.stringify(changed));
console.log(JSON.stringify(after), Object.keys(before).join(","));
console.log(name, JSON.stringify(rest), Object.keys(rest).join(","));
console.log(JSON.stringify(assigned), JSON.stringify(cloned));
}
// Keys after a spread that the spread value may lack: a key the value has
// keeps its place, and a key it lacks goes after the value's keys.
type Options = { id: number; label?: string; size?: number; tag?: string };
const options: Options[] = [
{ id: 1, size: 2 },
{ size: 3, id: 2 },
{ label: "c", id: 3, tag: "t" },
{ tag: "u", size: 4, id: 4 },
];
for (const value of options) {
const merged = { ...value, tag: "x", label: "y", done: true };
console.log(Object.keys(merged).join(","), JSON.stringify(merged));
}
+30
View File
@@ -0,0 +1,30 @@
import { MessageChannel } from "node:worker_threads";
// Object literals that write the same keys in different orders, seen
// through untyped values: their keys, JSON text, own property names, and a
// MessageChannel message.
const a = { id: 1, name: "a", ok: true };
const b = { name: "b", ok: false, id: 2 };
const c = { ok: true, id: 3, name: "c" };
const values: unknown[] = [a, b, c];
for (const value of values) {
const object = value as Record<string, unknown>;
console.log(Object.keys(object).join(","), JSON.stringify(object));
console.log(Object.getOwnPropertyNames(object).join(","));
}
const { port1, port2 } = new MessageChannel();
let received = 0;
port1.on("message", (message: unknown) => {
console.log("message", JSON.stringify(message), Object.keys(message as object).join(","));
if (++received === 3) port1.close();
});
for (const value of [a, b, c]) port2.postMessage(value);
// A nested value seen through an untyped view keeps its identity and its
// own key order, and writes through the view reach it.
const holder = { inner: b, tag: "h" };
const view = holder as unknown as Record<string, unknown>;
const inner = view.inner as Record<string, unknown>;
console.log(view.inner === view.inner, Object.keys(inner).join(","));
inner.id = 20;
console.log(b.id, JSON.stringify(b), JSON.stringify(view));
@@ -0,0 +1,44 @@
// Optional keys added after construction go last, as in Node, also for
// shapes whose literals write their keys in different orders: a write of a
// missing key, a delete and a later write, and writes in a loop.
type Item = { id: number; name?: string; note?: string; count: number };
const items: Item[] = [
{ id: 1, count: 0 },
{ count: 1, name: "b", id: 2 },
{ note: "c", id: 3, count: 2 },
];
items[0]!.note = "added";
items[0]!.name = "later";
items[1]!.name = "kept";
delete items[2]!.note;
items[2]!.note = "again";
for (const item of items) {
if (item.count > 0) item.name ??= "filled";
console.log(JSON.stringify(item), Object.keys(item).join(","));
}
const built: Item = { count: 9, id: 9 };
for (let i = 0; i < 2; i++) {
if (i === 1) built.note = `n${i}`;
console.log(Object.keys(built).join(","));
}
console.log(built);
// A write's value runs first, so a key it adds comes before the written
// key.
const nested: Item = { count: 3, id: 4 };
nested.name = ((nested.note = "first"), "second");
console.log(Object.keys(nested).join(","), JSON.stringify(nested));
// A value seen as a JavaScript value enumerates later writes in order.
const seen: Item = { id: 5, count: 4 };
const view: unknown = seen;
console.log(Object.keys(view as object).join(","));
seen.note = "n";
seen.name = "m";
console.log(Object.keys(view as object).join(","), JSON.stringify(view));
// The write goes to the object the receiver held before the value ran,
// even when the value assigns the receiver's binding.
let target: Item = { id: 6, count: 5 };
const before = target;
const after: Item = { count: 6, id: 7 };
target.name = ((target = after), "moved");
console.log(Object.keys(before).join(","), Object.keys(after).join(","), JSON.stringify(target));
+7 -3
View File
@@ -20,14 +20,18 @@ console.log(tagged.label);
const view: Labeled = tagged;
console.log(view === tagged);
// Key order: values of one type are built in two orders and the program
// observes them, so the type keeps its declaration order.
// Key order: values of one type are built in two orders, and a spread sets
// an optional key its source lacks, so the values cannot record their own
// orders and the type keeps its declaration order.
interface Point {
x: number;
y: number;
label?: string;
}
const point: Point = { y: 1, x: 2 };
console.log(JSON.stringify(point), JSON.stringify({ x: 3, y: 4 }));
const other: Point = { x: 3, y: 4 };
const labeled: Point = { ...point, label: "p" };
console.log(JSON.stringify(point), JSON.stringify(other), JSON.stringify(labeled));
// Text outside the built-in collation compares by code point natively.
console.log(["б", "а"].sort((p, q) => p.localeCompare(q)).join(","), "яблоко".localeCompare("груша"));
@@ -1,7 +1,7 @@
scriptc coverage tests/coverage-fixtures/divergences.ts
statements analyzed 18
compile statically 18 (100%)
statements analyzed 20
compile statically 20 (100%)
in dynamic tier 1 (operate on values typed only at runtime; --explain-dynamic lists them)
fully static — all analyzed statements compile to native code.
@@ -14,11 +14,11 @@ scriptc coverage tests/coverage-fixtures/divergences.ts
at tests/coverage-fixtures/divergences.ts:20:23
hint: declare the parameter or binding with the value's full type ('Tagged'), or make the shape a class — class instances keep their identity when passed as a base class
×1 SC6002 this object's keys enumerate as 'x, y' natively (the declaration order of 'Point') but as 'y, x' under Node (insertion order), and the program observes the order (JSON.stringify, Object.keys, for-in …
at tests/coverage-fixtures/divergences.ts:29:22
at tests/coverage-fixtures/divergences.ts:31:22
hint: write the properties in the declaration order of 'Point' (or reorder the declaration to match how objects are built)
×1 SC6003 localeCompare over 'яблоко' compares characters outside the built-in collation (Latin letters, digits, punctuation and symbols) by code point natively, while Node applies ICU's full collation
at tests/coverage-fixtures/divergences.ts:33:70
at tests/coverage-fixtures/divergences.ts:37:70
hint: results agree with Node for Latin-script text; for other scripts compare explicitly (for example by code point with < and >) so both runtimes agree
×1 SC6004 this 'Circle' reaches a cast to the unrelated class 'Square' in the callee: natively the checked cast throws a TypeError when it executes, while Node reads the members structurally
at tests/coverage-fixtures/divergences.ts:48:20
at tests/coverage-fixtures/divergences.ts:52:20
hint: narrow with 'instanceof Square' before casting, or cast to a base class or interface both classes share
@@ -0,0 +1 @@
{ "plain": 2200, "san": 3600 }
@@ -0,0 +1 @@
{ "plain": 1500, "san": 2800 }
@@ -0,0 +1 @@
{ "plain": 1600, "san": 2100 }
@@ -0,0 +1 @@
{ "plain": 1600, "san": 2500 }
@@ -0,0 +1 @@
{ "plain": 1900, "san": 3500 }
@@ -0,0 +1 @@
{ "plain": 1800, "san": 2700 }
@@ -0,0 +1 @@
{ "plain": 1700, "san": 2800 }
@@ -108,7 +108,7 @@ for (const backend of ["llvm"] as const) {
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" },
runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" },
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" },
flavors: {