Report published values as frozen, sealed and not extensible (#2429)

This commit is contained in:
Chris Tate
2026-10-27 13:45:27 -05:00
committed by GitHub
5 changed files with 178 additions and 4 deletions
@@ -24,7 +24,7 @@ import {
} from "./lib-shared.js";
import { isDynUndefined } from "../../ir/runtime-features.js";
import { DYN_KIND } from "./dyn.js";
import { caBytes } from "./lib-abi.js";
import { caBytes, callRuntime, ptr } from "./lib-abi.js";
import { emitInlineAtomicsOp } from "./expr-bytes.js";
import { regexLiteralCaptures } from "./regex-matchers.js";
import { registerReplay } from "./emit-journal.js";
@@ -1110,14 +1110,44 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
if (e.fn === "record.isFrozen") {
// The frozen bit of the record's rc word (scr_rc.h SCR_RC_FROZEN) is
// its second-highest bit; an immortal word sets the highest one too.
const value = host.emitReadReceiver(e.args[0]!);
// Where the record's shape may be published, an immortal word reads as
// frozen: publish() freezes what it reaches, as Node's deep freeze does.
const receiver = e.args[0]!;
const value = host.emitReadReceiver(receiver);
const S = host.sizeType;
const rc = host.B.tmp(),
top = host.B.tmp(),
t = host.B.tmp();
top = host.B.tmp();
let t = host.B.tmp();
host.B.line(`${rc} = load ${S}, ptr ${value.name}`);
host.B.line(`${top} = lshr ${S} ${rc}, ${S === "i64" ? 62 : 30}`);
host.B.line(`${t} = icmp eq ${S} ${top}, 1`);
if (
receiver.type.kind === "record" &&
host.publishedTypes?.recordGuarded(receiver.type.shapeId) === true
)
t = orPublished(host, t, rc);
return { name: t, type: e.type };
}
if (
(e.fn === "array.isFrozen" || e.fn === "collection.isExtensible") &&
host.publishedTypes?.containerPublished(e.args[0]!.type) === true
) {
// publish() freezes what it reaches, as Node's deep freeze does: a
// published array is frozen, and a published Map or Set is not extensible.
const value = host.emitReadReceiver(e.args[0]!);
const answer = callRuntime(
host,
e.fn === "array.isFrozen" ? "scr_arr_is_frozen" : "scr_map_is_extensible",
"i1",
[ptr(value.name)],
);
const rc = host.B.tmp();
host.B.line(`${rc} = load ${host.sizeType}, ptr ${value.name}`);
if (e.fn === "array.isFrozen") return { name: orPublished(host, answer, rc), type: e.type };
const mortal = host.B.tmp(),
t = host.B.tmp();
host.B.line(`${mortal} = icmp ne ${host.sizeType} ${rc}, -1`);
host.B.line(`${t} = and i1 ${answer}, ${mortal}`);
return { name: t, type: e.type };
}
const prefix = e.fn.slice(0, e.fn.indexOf(".")) as LibCallPrefix;
@@ -1346,3 +1376,14 @@ function emitPromiseAllTuple(host: LlvmEmitterContext, e: LibCallExpr): LlValue
B.line(`call void @scr_promise_release(ptr ${all})`);
return host.own({ name: result, type: e.type });
}
/** `frozen` or'ed with "the object whose rc word is `rc` is published": an
* immortal word (all ones), the test of the published write guards
* (emitter.ts emitPublishedTest). */
function orPublished(host: LlvmEmitterContext, frozen: string, rc: string): string {
const immortal = host.B.tmp(),
t = host.B.tmp();
host.B.line(`${immortal} = icmp eq ${host.sizeType} ${rc}, -1`);
host.B.line(`${t} = or i1 ${frozen}, ${immortal}`);
return t;
}
@@ -73,6 +73,11 @@ export class PublishedTypes {
return this.containers.has(key) || this.frozenArrays.has(key);
}
/** Whether an array, Map or Set of type `t` may be published. */
containerPublished(t: IrType): boolean {
return this.containers.has(typeKey(t));
}
/** Whether an array of type `t` may be published, and whether it may be
* frozen: the two states its write guard tests. */
arrayGuards(t: IrType): { published: boolean; frozen: boolean } {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/published-integrity-queries.ts"
],
"diags": []
}
+15
View File
@@ -2967,6 +2967,18 @@ static bool scr_dyn_typed_ref_view_answer(ScrDyn *object, bool (*query)(const Sc
return true;
}
/* Whether `object` is a published object, array, Map or Set, or the checked
* view of one (its referent's rc word is immortal, the test of the published
* write guards). publish() freezes what it reaches, as Node's deep freeze
* does: such a value is frozen, sealed and not extensible. */
static bool scr_dyn_published_object(const ScrDyn *object) {
if (object->kind == SCR_DYN_TYPED_REF)
return object->v.typed_ref.ptr != NULL && SCR_RC_PUBLISHED(object->v.typed_ref.ptr);
if (object->kind == SCR_DYN_OBJ || object->kind == SCR_DYN_ARR) return object->rc == SIZE_MAX;
const ScrMap *collection = scr_dyn_native_collection(object);
return collection != NULL && SCR_RC_PUBLISHED(collection);
}
static ScrDyn *scr_bytes_properties(const ScrDyn *v);
static bool scr_dyn_is_extensible_view(const ScrDyn *view) {
if (view->kind == SCR_DYN_BYTES) return !view->v.bytes->properties || !view->v.bytes->properties->non_extensible;
@@ -3043,6 +3055,7 @@ ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object) {
bool scr_dyn_is_extensible(ScrDyn *object) {
if (scr_dyn_class_reflection_fence(object)) return false;
if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.isExtensible"); return false; }
if (scr_dyn_published_object(object)) return false;
bool answer;
if (scr_dyn_typed_ref_view_answer(object, scr_dyn_is_extensible_view, &answer)) return answer;
ScrMap *collection = scr_dyn_native_collection(object);
@@ -3094,6 +3107,7 @@ ScrDyn *scr_dyn_seal(ScrDyn *object) {
bool scr_dyn_is_sealed(const ScrDyn *object) {
if (scr_dyn_class_reflection_fence(object)) return false;
if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.isSealed"); return false; }
if (scr_dyn_published_object(object)) return true;
bool answer;
if (scr_dyn_typed_ref_view_answer((ScrDyn *)object, scr_dyn_is_sealed, &answer)) return answer;
ScrMap *collection = scr_dyn_native_collection(object);
@@ -19103,6 +19117,7 @@ ScrDyn *scr_dyn_freeze_data(ScrDyn *value) {
bool scr_dyn_is_frozen(const ScrDyn *value) {
if (scr_dyn_class_reflection_fence(value)) return false;
if (scr_dyn_published_object(value)) return true;
bool answer;
if (scr_dyn_typed_ref_view_answer((ScrDyn *)value, scr_dyn_is_frozen, &answer)) return answer;
ScrMap *collection = scr_dyn_native_collection(value);
+107
View File
@@ -0,0 +1,107 @@
import { publish } from "@scriptc/threads";
// Object.isFrozen, Object.isSealed and Object.isExtensible on published
// values. Under Node.js, publish() deep-freezes the graph, so every object it
// reaches is frozen, sealed and not extensible. Unpublished values of the same
// types keep their own state.
class Box {
items: number[];
constructor(items: number[]) {
this.items = items;
}
}
class Link {
next: Link | undefined = undefined;
label = "link";
}
interface Point {
x: number;
inner: { y: number };
}
function report(name: string, value: object): void {
console.log(name, Object.isFrozen(value), Object.isSealed(value), Object.isExtensible(value));
}
const box = publish(new Box([]));
report("class", box);
report("class field array", box.items);
report("numbers", publish([1, 2]));
report("strings", publish(["a", "b"]));
const point: Point = publish({ x: 1, inner: { y: 2 } });
report("record", point);
report("nested record", point.inner);
report("map", publish(new Map([["a", 1]])));
report("set", publish(new Set([1])));
const chain = new Link();
chain.next = new Link();
publish(chain);
report("chain", chain);
report("chain next", chain.next!);
const loose: unknown = publish({ z: [3] });
report("unknown", loose as object);
report("unknown field", (loose as { z: number[] }).z);
// The same queries on statically typed values.
const numbers = publish([4, 5]);
const pair = publish({ x: 3, inner: { y: 4 } });
const table = publish(new Map([["b", 2]]));
const members = publish(new Set(["s"]));
console.log(
"typed",
Object.isFrozen(numbers),
Object.isFrozen(pair),
Object.isFrozen(pair.inner),
Object.isFrozen(table),
Object.isFrozen(members),
Object.isFrozen(box),
Object.isFrozen(box.items),
);
console.log("typed sealed", Object.isSealed(table), Object.isSealed(members), Object.isSealed(box));
console.log(
"typed extensible",
Object.isExtensible(table),
Object.isExtensible(members),
Object.isExtensible(box),
);
const otherNumbers = [4, 5];
const otherPair = { x: 3, inner: { y: 4 } };
const otherTable = new Map([["b", 2]]);
console.log(
"typed unpublished",
Object.isFrozen(otherNumbers),
Object.isFrozen(otherPair),
Object.isFrozen(otherTable),
Object.isExtensible(otherTable),
Object.isFrozen(new Box([])),
);
// Unpublished values of the same types.
report("unpublished class", new Box([1]));
report("unpublished numbers", [1, 2]);
report("unpublished record", { x: 1, inner: { y: 2 } });
report("unpublished map", new Map<string, number>());
report("unpublished set", new Set<number>());
const fresh = new Link();
report("unpublished chain", fresh);
fresh.next = new Link();
report("unpublished chain next", fresh.next);
// Frozen without publishing.
report("frozen record", Object.freeze({ x: 1, inner: { y: 2 } }));
report("frozen numbers", Object.freeze([1, 2]));
// Writes into published values still throw.
try {
box.items.push(1);
} catch (e) {
console.log((e as Error).name, (e as Error).message);
}
try {
(chain as { label: string }).label = "changed";
} catch (e) {
console.log((e as Error).name, (e as Error).message);
}