mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-04 08:04:37 +08:00
Report published values as frozen, sealed and not extensible (#2429)
This commit is contained in:
@@ -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 } {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/published-integrity-queries.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user