mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Answer untyped property reads of plain values without the class tables (#2013)
This commit is contained in:
@@ -282,6 +282,11 @@ interface PropertyDispatch {
|
||||
fn: IrFunction;
|
||||
classes: Set<string>;
|
||||
reflect?: boolean;
|
||||
/** The helper's fallthrough statements as created, and the same access
|
||||
* applied to the receiver itself: what the fallthrough reduces to for a
|
||||
* value that is neither a class capsule nor a function, whose bag is the
|
||||
* value. */
|
||||
plain?: { tail: readonly IrStmt[]; access: IrStmt[] };
|
||||
}
|
||||
|
||||
export function classInstanceOf(
|
||||
@@ -2724,7 +2729,28 @@ export class ClassDynamicDispatch {
|
||||
loc,
|
||||
});
|
||||
}
|
||||
dispatch = { name, write: !!write, fn: helper, classes: new Set() };
|
||||
const receiverAccess: IrStmt[] = read
|
||||
? [{ kind: "return", value: { ...read, value: varRef("p.0", DYN, loc) }, loc }]
|
||||
: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
...write!,
|
||||
args: [varRef("p.0", DYN, loc), write!.args[1]!, varRef("p.1", DYN, loc)],
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: null, loc },
|
||||
];
|
||||
dispatch = {
|
||||
name,
|
||||
write: !!write,
|
||||
fn: helper,
|
||||
classes: new Set(),
|
||||
...(name !== "constructor" && name !== "toString"
|
||||
? { plain: { tail: [...helper.body], access: receiverAccess } }
|
||||
: {}),
|
||||
};
|
||||
this.properties.set(key, dispatch);
|
||||
this.generated.add(helper);
|
||||
lowerer.liftedFns.push(helper);
|
||||
@@ -4896,6 +4922,7 @@ export class ClassDynamicDispatch {
|
||||
);
|
||||
}
|
||||
if (lowerer) this.guardClassTables(lowerer);
|
||||
if (lowerer) this.guardPropertyHelpers(lowerer);
|
||||
}
|
||||
|
||||
/** Each tracked class's bag initializer over a typed receiver (`this`),
|
||||
@@ -4985,6 +5012,87 @@ export class ClassDynamicDispatch {
|
||||
}
|
||||
}
|
||||
|
||||
/** Fast exits for the generated property helpers: when every class
|
||||
* branch tests a capsule key or a class key and the fallthrough is still
|
||||
* the bag access the helper was created with, a value of another kind
|
||||
* (a plain object, array or primitive) normalizes to itself, has itself
|
||||
* as its bag, and so gets the plain access directly, without the
|
||||
* normalization, bag and static-bag calls. */
|
||||
private guardPropertyHelpers(lowerer: Lowerer): void {
|
||||
// Every class table the fallthrough or normalization calls must answer
|
||||
// a plain value with the value itself: its kind exit is in place.
|
||||
const exits = (name: string): boolean => {
|
||||
const helper = lowerer.liftedFnNamed(name);
|
||||
const first = helper?.body[0];
|
||||
const param = helper?.params[0]?.localId;
|
||||
return (
|
||||
param !== undefined &&
|
||||
first?.kind === "if" &&
|
||||
first.cond.kind === "dynTest" &&
|
||||
first.cond.negated === true &&
|
||||
first.then.length === 1 &&
|
||||
first.then[0]!.kind === "return" &&
|
||||
first.then[0]!.value !== null &&
|
||||
returnsParam(first.then[0]!.value, param)
|
||||
);
|
||||
};
|
||||
const tables = new Set([
|
||||
"%dyn.class.properties",
|
||||
READ_PROPERTIES,
|
||||
READ_PROPERTIES_OR_EMPTY,
|
||||
"%dyn.class.normalize",
|
||||
"%dyn.class.staticProperties",
|
||||
]);
|
||||
const callsOnlyGuardedTables = (statements: IrStmt[]): boolean =>
|
||||
everyStmtList(statements, {
|
||||
stmt: () => true,
|
||||
expr: (expression) =>
|
||||
expression.kind !== "call" || (tables.has(expression.callee) && exits(expression.callee)),
|
||||
});
|
||||
for (const dispatch of this.properties.values()) {
|
||||
const plain = dispatch.plain;
|
||||
if (!plain) continue;
|
||||
const fn = dispatch.fn;
|
||||
const param = fn.params[0]?.localId;
|
||||
if (param === undefined) continue;
|
||||
const head = fn.body.length - plain.tail.length;
|
||||
if (head < 0 || plain.tail.some((stmt, i) => fn.body[head + i] !== stmt)) continue;
|
||||
let ok = true;
|
||||
for (const stmt of fn.body.slice(0, head)) {
|
||||
if (
|
||||
stmt.kind === "assign" &&
|
||||
stmt.localId === param &&
|
||||
stmt.value.kind === "call" &&
|
||||
stmt.value.callee === "%dyn.class.normalize"
|
||||
)
|
||||
continue;
|
||||
if (stmt.kind === "if" && stmt.else_ === null && branchKind(stmt.cond, param) !== null)
|
||||
continue;
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
// Class branches never run for a plain value; the rest always does.
|
||||
if (!ok || !callsOnlyGuardedTables(fn.body.filter((stmt) => stmt.kind !== "if"))) continue;
|
||||
const loc = fn.loc;
|
||||
const not = (test: "classCapsule" | "funcKind"): IrExpr => ({
|
||||
kind: "dynTest",
|
||||
test,
|
||||
negated: true,
|
||||
value: varRef(param, DYN, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
fn.body.unshift({
|
||||
kind: "if",
|
||||
cond: not("classCapsule"),
|
||||
then: [{ kind: "if", cond: not("funcKind"), then: plain.access, else_: null, loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
delete dispatch.plain;
|
||||
}
|
||||
}
|
||||
|
||||
private normalizedDispatches(): IrFunction[] {
|
||||
return [
|
||||
...(this.propertyBag ? [this.propertyBag] : []),
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7792-untyped-property-reads-mixed-receivers.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7793-untyped-key-writes.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -9369,10 +9369,12 @@ bool scr_dyn_in(const ScrDyn *v, const ScrStr *key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* One lookup of the own property decides the assignment: an own property
|
||||
* is written (or runs its setter, or refuses the write); an absent one
|
||||
* consults the prototype chain and is then added. */
|
||||
static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, const ScrDyn *receiver) {
|
||||
for (size_t i = 0; i < recv->v.obj.len; i++) {
|
||||
ScrDynEntry *entry = &recv->v.obj.entries[i];
|
||||
if (entry->key_len != key->len || memcmp(entry->key, key->data, key->len) != 0) continue;
|
||||
ScrDynEntry *entry = scr_dyn_entry(recv, key);
|
||||
if (entry) {
|
||||
if (entry->accessor && entry->setter) {
|
||||
ScrDyn *args[] = {value};
|
||||
ScrDyn *setter = scr_dyn_retain(entry->setter);
|
||||
@@ -9388,8 +9390,14 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
|
||||
return;
|
||||
}
|
||||
scr_dyn_region_store(recv, value);
|
||||
ScrDyn *old = entry->value;
|
||||
entry->value = scr_dyn_retain(value);
|
||||
scr_dyn_release(old);
|
||||
scr_error_sync_cause(recv, key);
|
||||
return;
|
||||
}
|
||||
if (!scr_dyn_entry(recv, key)) {
|
||||
{
|
||||
for (const ScrDyn *current = scr_dyn_proto_next(recv); current;) {
|
||||
ScrDyn *table = current->kind == SCR_DYN_FUNC ? scr_dyn_fn_properties(current) : NULL;
|
||||
const ScrDyn *object = table ? table : current;
|
||||
@@ -9421,7 +9429,7 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (recv->non_extensible && !scr_dyn_entry(recv, key)) {
|
||||
if (recv->non_extensible) {
|
||||
ScrJsonBuf buffer;
|
||||
scr_jb_init(&buffer);
|
||||
scr_jb_puts(&buffer, "Cannot add property ");
|
||||
@@ -9430,9 +9438,8 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con
|
||||
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer));
|
||||
return;
|
||||
}
|
||||
bool created = scr_dyn_entry(recv, key) == NULL;
|
||||
scr_dyn_obj_set(recv, key->data, key->len, scr_dyn_retain(value));
|
||||
if (created && key->len == 5 && memcmp(key->data, "stack", 5) == 0 &&
|
||||
if (key->len == 5 && memcmp(key->data, "stack", 5) == 0 &&
|
||||
scr_errdyn_find(recv, false) != SIZE_MAX) {
|
||||
/* An error's own `stack` exists from construction as a non-enumerable
|
||||
* data property, so assigning it adds no enumerable key. */
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
"use strict";
|
||||
// One untyped property read site meets class instances (fixed and tracked
|
||||
// layouts, getters, own properties added later), plain objects, arrays,
|
||||
// strings, functions, class constructors with statics, null prototypes and
|
||||
// optional chains. Each receiver kind answers as in Node.
|
||||
|
||||
class Status {
|
||||
constructor() {
|
||||
this.value = "valid";
|
||||
}
|
||||
}
|
||||
|
||||
class Lazy {
|
||||
constructor(key) {
|
||||
this.key = key;
|
||||
}
|
||||
get value() {
|
||||
return `lazy:${this.key}`;
|
||||
}
|
||||
get length() {
|
||||
return 7;
|
||||
}
|
||||
}
|
||||
|
||||
class Tracked {
|
||||
constructor(list) {
|
||||
list.push(this);
|
||||
this.value = list.length;
|
||||
}
|
||||
static value = "static";
|
||||
}
|
||||
|
||||
function plainFunction() {}
|
||||
plainFunction.value = "fn";
|
||||
|
||||
/** @param {any} target */
|
||||
function readValue(target) {
|
||||
return target.value;
|
||||
}
|
||||
/** @param {any} target */
|
||||
function readLength(target) {
|
||||
return target.length;
|
||||
}
|
||||
/** @param {any} target */
|
||||
function readOptional(target) {
|
||||
return target?.value;
|
||||
}
|
||||
/** @param {any} target @param {unknown} value */
|
||||
function writeValue(target, value) {
|
||||
target.value = value;
|
||||
}
|
||||
|
||||
const list = [];
|
||||
const added = new Status();
|
||||
writeValue(added, "dirty");
|
||||
/** @type {any} */
|
||||
const extra = new Lazy("x");
|
||||
extra.other = 1;
|
||||
const nullProto = Object.create(null);
|
||||
nullProto.value = "bare";
|
||||
const inherited = Object.create({ value: "from prototype" });
|
||||
const receivers = [
|
||||
new Status(),
|
||||
added,
|
||||
new Lazy("k"),
|
||||
extra,
|
||||
new Tracked(list),
|
||||
{ value: 1, length: 2 },
|
||||
{ other: true },
|
||||
[1, 2, 3],
|
||||
"text",
|
||||
42,
|
||||
true,
|
||||
plainFunction,
|
||||
Tracked,
|
||||
nullProto,
|
||||
inherited,
|
||||
];
|
||||
for (const receiver of receivers) {
|
||||
console.log(typeof receiver, JSON.stringify(readValue(receiver)), JSON.stringify(readLength(receiver)), JSON.stringify(readOptional(receiver)));
|
||||
}
|
||||
console.log(readOptional(null), readOptional(undefined));
|
||||
for (const bad of [null, undefined]) {
|
||||
try {
|
||||
readValue(bad);
|
||||
} catch (e) {
|
||||
console.log(e instanceof TypeError, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
const plain = { value: 0 };
|
||||
const arr = [];
|
||||
writeValue(plain, 5);
|
||||
writeValue(arr, "on array");
|
||||
writeValue(inherited, "own now");
|
||||
writeValue(plainFunction, "fn2");
|
||||
const status = new Status();
|
||||
writeValue(status, "aborted");
|
||||
console.log(plain.value, arr.value, Object.keys(inherited), plainFunction.value, status.value, JSON.stringify(status));
|
||||
try {
|
||||
writeValue("text", 1);
|
||||
} catch (e) {
|
||||
console.log(e instanceof TypeError, e.message);
|
||||
}
|
||||
const frozen = Object.freeze({ value: "frozen" });
|
||||
try {
|
||||
writeValue(frozen, 2);
|
||||
} catch (e) {
|
||||
console.log(e instanceof TypeError);
|
||||
}
|
||||
console.log(frozen.value);
|
||||
@@ -0,0 +1,83 @@
|
||||
"use strict";
|
||||
// Untyped property writes through one site: new keys append in order,
|
||||
// existing keys keep their position, own and inherited setters run,
|
||||
// getter-only and read-only properties refuse the write, and
|
||||
// non-extensible objects refuse new keys, as in Node.
|
||||
|
||||
/** @param {any} target @param {string} key @param {unknown} value */
|
||||
function put(target, key, value) {
|
||||
target[key] = value;
|
||||
}
|
||||
|
||||
/** @param {() => void} fn */
|
||||
function attempt(label, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(label, "ok");
|
||||
} catch (e) {
|
||||
console.log(label, e instanceof TypeError ? "TypeError" : String(e));
|
||||
}
|
||||
}
|
||||
|
||||
const order = {};
|
||||
for (const key of ["b", "a", "c", "a", "b", "d"]) put(order, key, key.charCodeAt(0));
|
||||
console.log(JSON.stringify(order), Object.keys(order).join(""));
|
||||
|
||||
const log = [];
|
||||
const withSetter = {
|
||||
_v: 0,
|
||||
get v() {
|
||||
return this._v;
|
||||
},
|
||||
set v(value) {
|
||||
log.push(`own ${value}`);
|
||||
this._v = value * 2;
|
||||
},
|
||||
get onlyGetter() {
|
||||
return "g";
|
||||
},
|
||||
};
|
||||
put(withSetter, "v", 4);
|
||||
console.log(withSetter.v, JSON.stringify(log));
|
||||
attempt("getter-only", () => put(withSetter, "onlyGetter", 1));
|
||||
|
||||
const proto = {
|
||||
/** @param {number} value */
|
||||
set inherited(value) {
|
||||
log.push(`inherited ${value} on ${this === child}`);
|
||||
},
|
||||
};
|
||||
Object.defineProperty(proto, "fixed", { value: 1, writable: false });
|
||||
const child = Object.create(proto);
|
||||
put(child, "inherited", 7);
|
||||
attempt("inherited read-only", () => put(child, "fixed", 2));
|
||||
put(child, "own", 3);
|
||||
console.log(JSON.stringify(log), Object.keys(child).join(","), child.fixed);
|
||||
|
||||
/** @type {any} */
|
||||
const sealed = { x: 1 };
|
||||
Object.seal(sealed);
|
||||
put(sealed, "x", 2);
|
||||
attempt("sealed add", () => put(sealed, "y", 3));
|
||||
/** @type {any} */
|
||||
const closed = { x: 1 };
|
||||
Object.preventExtensions(closed);
|
||||
put(closed, "x", 5);
|
||||
attempt("closed add", () => put(closed, "z", 1));
|
||||
const frozen = Object.freeze({ x: 1 });
|
||||
attempt("frozen write", () => put(frozen, "x", 9));
|
||||
console.log(JSON.stringify(sealed), JSON.stringify(closed), JSON.stringify(frozen));
|
||||
|
||||
const readOnly = {};
|
||||
Object.defineProperty(readOnly, "k", { value: "kept", writable: false, enumerable: true });
|
||||
attempt("own read-only", () => put(readOnly, "k", "changed"));
|
||||
console.log(readOnly.k);
|
||||
|
||||
const error = new Error("boom");
|
||||
put(error, "cause", "why");
|
||||
put(error, "code", "E_X");
|
||||
console.log(error.cause, error.code);
|
||||
|
||||
const wide = {};
|
||||
for (let i = 0; i < 40; i++) put(wide, `k${i % 25}`, i);
|
||||
console.log(Object.keys(wide).length, wide.k0, wide.k24, Object.keys(wide).slice(0, 3).join(","));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4500, "san": 5000 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4300, "san": 4800 }
|
||||
Reference in New Issue
Block a user