diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index c3acf766f..80ec73e5a 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -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; +} diff --git a/packages/compiler/src/backend/publication.ts b/packages/compiler/src/backend/publication.ts index 38e429a47..607ee3139 100644 --- a/packages/compiler/src/backend/publication.ts +++ b/packages/compiler/src/backend/publication.ts @@ -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 } { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/published-integrity-queries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/published-integrity-queries.ts.json new file mode 100644 index 000000000..2e5570a24 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/published-integrity-queries.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/published-integrity-queries.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index eb40100cf..93a0bc1ae 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -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); diff --git a/tests/corpus/published-integrity-queries.ts b/tests/corpus/published-integrity-queries.ts new file mode 100644 index 000000000..9ec0c7cf3 --- /dev/null +++ b/tests/corpus/published-integrity-queries.ts @@ -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()); +report("unpublished set", new Set()); +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); +}