Compile native class caches and environment proxies

- Keep cached native class instances callable, including inherited methods and JavaScript arguments.
- Support plain-object Proxy traps with live receivers, enumeration, and data-property invariants.
- Compile untyped addition, computed environment keys, and text-codec startup storage natively.
This commit is contained in:
Chris Tate
2026-09-28 12:25:30 -05:00
committed by GitHub
parent 385f0fb80c
commit fc6408a662
35 changed files with 1254 additions and 145 deletions
+13 -20
View File
@@ -1425,16 +1425,16 @@ function emitStringExpr(
// The ARM value's ToString via the per-union interned helper
// (unit arms are interned literals, string arms retain the
// payload, f64/bool arms format). Box borrowed; result +1.
return emitter.newTemp(e.type, `${emitter.unionToStrHelper(v.type.unionId)}(${v.name})`);
return emitter.fallibleTemp(e.type, `${emitter.unionToStrHelper(v.type.unionId)}(${v.name})`);
}
if (v.type.kind === "caught") {
// String(e) over the exception snapshot. Box borrowed; result +1.
return emitter.newTemp(e.type, `scr_caught_to_string(${v.name})`);
return emitter.fallibleTemp(e.type, `scr_caught_to_string(${v.name})`);
}
if (v.type.kind === "dyn") {
// String(unknown): dispatch over the dyn kind (dynToStrHelper —
// Node's String() incl. arrays-join and "[object Object]").
return emitter.newTemp(e.type, `${emitter.dynToStrHelper()}(${v.name})`);
return emitter.fallibleTemp(e.type, `${emitter.dynToStrHelper()}(${v.name})`);
}
if (v.type.kind === "record") {
// String(record) / `${record}`: Object.prototype.toString's
@@ -3199,22 +3199,9 @@ function emitDynamicExpr(
return emitter.fallibleTemp(e.type, call);
}
case "dynHasKey": {
// `"k" in pkg`: a kind-guarded presence answer, computed against
// the literal key at compile time — no allocation, borrowed box.
// An ISLAND-held receiver fences loudly (Node asks the real
// engine object — `false` would be a silent wrong answer), so
// the temp rides the fallible path.
const d = emitter.emitExpr(e.value);
const keyBytes = Buffer.from(e.key, "utf8");
const keyLit = cStringLiteral(keyBytes);
const objTest = `scr_dyn_obj_get(${d.name}, ${keyLit}, ${keyBytes.length}) != NULL`;
const arrTest =
e.key === "length"
? "true"
: /^(0|[1-9][0-9]*)$/.test(e.key) && Number(e.key) <= Number.MAX_SAFE_INTEGER
? `${d.name}->v.arr.len > ${e.key}`
: "false";
const test = `(${d.name}->kind == SCR_DYN_OBJ ? (${objTest}) : ${d.name}->kind == SCR_DYN_ARR ? (${arrTest}) : scr_dyn_isl_fence(${d.name}, "'in'"))`;
const value = emitter.emitExpr(e.value);
const key = emitter.emitExpr({ kind: "strLit", value: e.key, type: STRING, loc: e.loc });
const test = `scr_dyn_has_key(${value.name}, ${key.name})`;
return emitter.fallibleTemp(e.type, e.negated ? `!${test}` : test);
}
case "dynScalarEq": {
@@ -3256,7 +3243,7 @@ function emitDynamicExpr(
? // `typeof v === "object"`: objects, arrays, bytes, native
// handles, promises, AND null — engine-held objects by the
// engine's own typeof.
`(${d.name}->kind == SCR_DYN_OBJ || ${d.name}->kind == SCR_DYN_ARR || ${d.name}->kind == SCR_DYN_BYTES || ${d.name}->kind == SCR_DYN_HANDLE || ${d.name}->kind == SCR_DYN_PROMISE || ${d.name}->kind == SCR_DYN_NULL || scr_dyn_isl_typeof_is(${d.name}, "object"))`
`(${d.name}->kind == SCR_DYN_OBJ || ${d.name}->kind == SCR_DYN_ARR || ${d.name}->kind == SCR_DYN_BYTES || ${d.name}->kind == SCR_DYN_HANDLE || ${d.name}->kind == SCR_DYN_PROMISE || ${d.name}->kind == SCR_DYN_PROXY || ${d.name}->kind == SCR_DYN_NULL || scr_dyn_isl_typeof_is(${d.name}, "object"))`
: e.test === "truthy"
? // Runtime ToBoolean includes typed-reference capsules and
// keeps this backend in lockstep with LLVM.
@@ -4460,6 +4447,8 @@ function emitDynamicLibCall(state: LibCallState): Temp {
return finish(`scr_dyn_global_symbol_has(${arg(0)})`);
case "dyn.globalSymbolDelete":
return finish(`scr_dyn_global_symbol_delete(${arg(0)})`);
case "dyn.typedRefIs":
return finish(`scr_dyn_typed_ref_is_key(${arg(0)}, ${arg(1)})`);
case "dyn.iterPack":
// Destructuring/for-of pack over a dyn source: both borrowed,
// fresh array +1; throws V8's not-iterable TypeError on
@@ -4493,6 +4482,10 @@ function emitDynamicLibCall(state: LibCallState): Temp {
// thrown hook leaves the exception pending; the may-throw
// epilogue abandons the NaN dummy.
return finish(`scr_dyn_number_coerce(${arg(0)})`);
case "dyn.add":
return finish(`scr_dyn_add(${arg(0)}, ${arg(1)})`);
case "dyn.proxyNew":
return finish(`scr_dyn_proxy_new(${arg(0)}, ${arg(1)})`);
case "global.undefRead":
// A declare-d const nothing defines: Node's catchable
// ReferenceError at the access (always throws — the typed
+5 -1
View File
@@ -101,10 +101,11 @@ export function computeMayThrow(mod: IrModule): { fns: Set<string>; indirect: bo
f.throws = true;
break;
case "dynKeyGet":
case "dynHasKey":
// The keyed read throws JS's TypeError on an undefined/null
// receiver (the `?.` form answers undefined instead), and
// HANDLE receivers can throw the loud unmodeled-property
// ladder on either form — seed both.
// ladder; Proxy get/has traps can throw too.
f.throws = true;
break;
case "dynDestrCheck":
@@ -160,6 +161,9 @@ export function computeMayThrow(mod: IrModule): { fns: Set<string>; indirect: bo
if (vt === "dyn" || vt === "record" || vt === "array" || vt === "union") f.throws = true;
break;
}
case "toString":
if (rec.operand.type.kind === "dyn" || rec.operand.type.kind === "union" || rec.operand.type.kind === "caught") f.throws = true;
break;
case "jsOp":
case "jsExit":
case "jsMarshal":
@@ -384,6 +384,9 @@ export function unionWidenHelper(emitter: CEmitter, fromId: string, toId: string
` scr_dyn_release(sc_materialized);`,
` break;`,
` }`,
` case SCR_DYN_PROXY:`,
` scr_dyn_proxy_unsupported("string conversion");`,
` break;`,
` }`,
`}`,
`static ScrStr *${name}(const ScrDyn *d) { /* String(unknown) -> owned (+1) */`,
@@ -1068,6 +1071,7 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
d.push(` if (d->kind == SCR_DYN_OBJ) {`);
d.push(` return scr_dyn_obj_read(d, k->data, k->len);`);
d.push(` }`);
d.push(` if (d->kind == SCR_DYN_PROXY) return scr_dyn_proxy_get(d, k);`);
d.push(` if (d->kind == SCR_DYN_JSVAL) {`);
d.push(` /* Island-held: o[k] reads the REAL engine property (getters`);
d.push(` * included, throws bridged catchably) and the result wraps`);
+22 -3
View File
@@ -19,7 +19,8 @@ import { InternalCompilerError } from "../../errors.js";
* v.arr { len +16, cap +24, items +32 };
* v.obj { len +16, cap +24, entries +32 };
* v.fn { clo +16, thunk +24, sig +32, name +40, arity +48 }.
* ScrDynEntry { char *key; size_t key_len; ScrDyn *value; } — 24 bytes.
* ScrDynEntry { char *key; size_t key_len; ScrDyn *value; bool writable,
* enumerable, configurable; } — 32 bytes on 64-bit.
* ScrDynKind: NULL=0 BOOL=1 NUM=2 STR=3 ARR=4 OBJ=5 UNDEF=6 BYTES=7
* FUNC=8 HANDLE=9.
* ScrBytes { rc +0; len +8; elem +16; data +24 }.
@@ -61,6 +62,7 @@ export const DYN_KIND = {
PROMISE: 10,
JSVAL: 11, /* SCR_DYN_JSVAL — island values held by reference */
TYPED_REF: 12, /* SCR_DYN_TYPED_REF — static Web-stream transit capsule */
PROXY: 13,
} as const;
/** What the dyn helpers need beyond the walker host: interned immortal
@@ -189,7 +191,7 @@ export class LlDyn {
private entryAt(B: BlockBuilder, entries: string, i: string): { key: string; keyLen: string; value: string } {
const off = B.tmp();
const base = B.tmp();
B.line(`${off} = mul ${this.S} ${i}, ${this.abiOffset(24, 12)} ; sizeof(ScrDynEntry)`);
B.line(`${off} = mul ${this.S} ${i}, ${this.abiOffset(32, 16)} ; sizeof(ScrDynEntry)`);
B.line(`${base} = getelementptr inbounds i8, ptr ${entries}, ${this.S} ${off}`);
const key = B.tmp();
B.line(`${key} = load ptr, ptr ${base}`);
@@ -1753,12 +1755,16 @@ export class LlDyn {
const kd = this.kindOf(B, "%d");
const done = B.newLabel("ds.d");
const labels = new Map<number, string>();
for (const k of [DYN_KIND.NULL, DYN_KIND.BOOL, DYN_KIND.NUM, DYN_KIND.STR, DYN_KIND.ARR, DYN_KIND.OBJ, DYN_KIND.UNDEF, DYN_KIND.BYTES, DYN_KIND.FUNC, DYN_KIND.HANDLE, DYN_KIND.PROMISE, DYN_KIND.JSVAL, DYN_KIND.TYPED_REF]) {
for (const k of [DYN_KIND.NULL, DYN_KIND.BOOL, DYN_KIND.NUM, DYN_KIND.STR, DYN_KIND.ARR, DYN_KIND.OBJ, DYN_KIND.UNDEF, DYN_KIND.BYTES, DYN_KIND.FUNC, DYN_KIND.HANDLE, DYN_KIND.PROMISE, DYN_KIND.JSVAL, DYN_KIND.TYPED_REF, DYN_KIND.PROXY]) {
labels.set(k, B.newLabel(`ds.k${k}`));
}
const branches: string[] = [];
for (const [kind, label] of labels) branches.push(`i32 ${kind}, label %${label}`);
B.terminate(`switch i32 ${kd}, label %${done} [ ${branches.join(" ")} ]`);
B.startBlock(labels.get(DYN_KIND.PROXY)!);
host.declare(`declare void @scr_dyn_proxy_unsupported(ptr)`);
B.line(`call void @scr_dyn_proxy_unsupported(ptr ${host.cstr("string conversion")})`);
B.br(done);
B.startBlock(labels.get(DYN_KIND.JSVAL)!);
{
// Island-held: the engine's own ToString (a bridged failure
@@ -2259,6 +2265,19 @@ export class LlDyn {
// ISLAND-held receivers: o[k] reads the REAL engine property (getters
// included, throws bridged catchably) and the result wraps back
// scalar-normalized — the routed keyed read that retired the fence.
{
const isProxy = B.tmp();
B.line(`${isProxy} = icmp eq i32 ${kd}, ${DYN_KIND.PROXY}`);
const lProxy = B.newLabel("kg.proxy");
const lNext = B.newLabel("kg.n");
B.condBr(isProxy, lProxy, lNext);
B.startBlock(lProxy);
host.declare(`declare ptr @scr_dyn_proxy_get(ptr, ptr)`);
const r = B.tmp();
B.line(`${r} = call ptr @scr_dyn_proxy_get(ptr %d, ptr %k)`);
B.terminate(`ret ptr ${r}`);
B.startBlock(lNext);
}
{
const isJv = B.tmp();
B.line(`${isJv} = icmp eq i32 ${kd}, ${DYN_KIND.JSVAL}`);
@@ -414,60 +414,11 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" |
return out;
}
case "dynHasKey": {
// `"k" in pkg`: a kind-guarded presence answer, computed against
// the literal key at compile time — no allocation, borrowed box.
const d = host.emitExpr(e.value);
const kd = host.dynKind(d.name);
const slot = B.slot();
B.entryAllocas.push(`${slot} = alloca i1`);
B.line(`store i1 false, ptr ${slot}`);
const lObj = B.newLabel("dhk.o");
const lArr = B.newLabel("dhk.a");
const lNotObj = B.newLabel("dhk.no");
const lj = B.newLabel("dhk.j");
const isObj = B.tmp();
B.line(`${isObj} = icmp eq i32 ${kd}, ${DYN_KIND.OBJ}`);
B.condBr(isObj, lObj, lNotObj);
B.startBlock(lObj);
host.declare(`declare ptr @scr_dyn_obj_get(ptr, ptr, ${host.sizeType})`);
const keyBytes = Buffer.byteLength(e.key, "utf8");
const m = B.tmp();
const has = B.tmp();
B.line(`${m} = call ptr @scr_dyn_obj_get(ptr ${d.name}, ptr ${host.cstr(e.key)}, ${host.sizeType} ${keyBytes})`);
B.line(`${has} = icmp ne ptr ${m}, null`);
B.line(`store i1 ${has}, ptr ${slot}`);
B.br(lj);
B.startBlock(lNotObj);
const isArr = B.tmp();
B.line(`${isArr} = icmp eq i32 ${kd}, ${DYN_KIND.ARR}`);
const lNotArr = B.newLabel("dhk.na");
B.condBr(isArr, lArr, lNotArr);
B.startBlock(lArr);
if (e.key === "length") {
B.line(`store i1 true, ptr ${slot}`);
} else if (/^(0|[1-9][0-9]*)$/.test(e.key) && Number(e.key) <= Number.MAX_SAFE_INTEGER) {
const lenp = B.tmp();
const len = B.tmp();
const inR = B.tmp();
B.line(`${lenp} = getelementptr inbounds i8, ptr ${d.name}, i64 16 ; ->v.arr.len`);
B.line(`${len} = load ${host.sizeType}, ptr ${lenp}`);
B.line(`${inR} = icmp ugt ${host.sizeType} ${len}, ${e.key}`);
B.line(`store i1 ${inR}, ptr ${slot}`);
}
B.br(lj);
// An ISLAND-held receiver fences loudly (Node asks the real
// engine object — `false` would be a silent wrong answer); the
// helper answers false for every other kind, so this arm is a
// plain unconditional call.
B.startBlock(lNotArr);
host.declare(`declare zeroext i1 @scr_dyn_isl_fence(ptr, ptr)`);
const fenced = B.tmp();
B.line(`${fenced} = call zeroext i1 @scr_dyn_isl_fence(ptr ${d.name}, ptr ${host.cstr("'in'")})`);
B.line(`store i1 ${fenced}, ptr ${slot}`);
B.br(lj);
B.startBlock(lj);
const value = host.emitExpr(e.value);
const key = host.emitExpr({ kind: "strLit", value: e.key, type: { kind: "string" }, loc: e.loc });
host.declare(`declare zeroext i1 @scr_dyn_has_key(ptr, ptr)`);
const raw = B.tmp();
B.line(`${raw} = load i1, ptr ${slot}`);
B.line(`${raw} = call zeroext i1 @scr_dyn_has_key(ptr ${value.name}, ptr ${key.name})`);
host.emitPendingCheck();
if (!e.negated) return { name: raw, type: e.type };
const neg = B.tmp();
@@ -612,7 +563,7 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" |
// `typeof v === "object"`: objects, arrays, bytes, native
// handles, promises, AND null — engine-held objects by the
// engine's own typeof.
test = orIsl(oneOf([DYN_KIND.OBJ, DYN_KIND.ARR, DYN_KIND.BYTES, DYN_KIND.HANDLE, DYN_KIND.PROMISE, DYN_KIND.NULL]), "scr_dyn_isl_typeof_is", host.cstr("object"));
test = orIsl(oneOf([DYN_KIND.OBJ, DYN_KIND.ARR, DYN_KIND.BYTES, DYN_KIND.HANDLE, DYN_KIND.PROMISE, DYN_KIND.PROXY, DYN_KIND.NULL]), "scr_dyn_isl_typeof_is", host.cstr("object"));
} else if (e.test === "array") {
// Array.isArray: the checked-dynamic tree's array kind, or the engine's own
// answer for an engine-held value.
@@ -350,7 +350,9 @@ export function emitStringExpr(host: LlvmEmitterContext, e: ExprOf<"strConcat" |
B.startBlock(join);
const t = B.tmp();
B.line(`${t} = load ptr, ptr ${slot}`);
return host.own({ name: t, type: e.type });
const result = host.own({ name: t, type: e.type });
host.emitPendingCheck();
return result;
}
if (v.type.kind === "record") {
// String(record) / `${record}`: Object.prototype.toString's
@@ -364,7 +366,9 @@ export function emitStringExpr(host: LlvmEmitterContext, e: ExprOf<"strConcat" |
host.declare(`declare ptr @scr_caught_to_string(ptr)`);
const t = B.tmp();
B.line(`${t} = call ptr @scr_caught_to_string(ptr ${v.name})`);
return host.own({ name: t, type: e.type });
const result = host.own({ name: t, type: e.type });
host.emitPendingCheck();
return result;
}
if (v.type.kind === "dyn") {
// String(unknown): dispatch over the dyn kind (dyn.ts's sc_ds —
@@ -372,7 +376,9 @@ export function emitStringExpr(host: LlvmEmitterContext, e: ExprOf<"strConcat" |
const helper = host.dyn.dynToStrHelper();
const t = B.tmp();
B.line(`${t} = call ptr @${helper}(ptr ${v.name})`);
return host.own({ name: t, type: e.type });
const result = host.own({ name: t, type: e.type });
host.emitPendingCheck();
return result;
}
const t = B.tmp();
if (v.type.kind === "f64") {
@@ -254,6 +254,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
// Node's argument ladders throw catchably (MAY_THROW_LIB_FNS).
"dyn.toStringCoerce": "scr_dyn_string_coerce_js",
"dyn.toNumberCoerce": "scr_dyn_number_coerce",
"dyn.add": "scr_dyn_add",
"dyn.proxyNew": "scr_dyn_proxy_new",
"buffer.compareChk": "scr_buffer_compare_chk",
"bytes.equalsChk": "scr_bytes_equals_chk",
"bytes.compareChk": "scr_bytes_compare_chk",
@@ -512,6 +514,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.globalSymbolSet": "scr_dyn_global_symbol_set",
"dyn.globalSymbolHas": "scr_dyn_global_symbol_has",
"dyn.globalSymbolDelete": "scr_dyn_global_symbol_delete",
"dyn.typedRefIs": "scr_dyn_typed_ref_is_key",
"dyn.iterPack": "scr_dyn_iter_pack",
"dyn.arrLen": "scr_dyn_arr_len",
"dyn.arrAt": "scr_dyn_arr_at",
@@ -0,0 +1,184 @@
import { BOOL, DYN, STRING, VOID, isDynTypedRefType, typeEquals, typeKey, type IrExpr, type IrFunction, type IrStmt, type IrType } from "../../ir/ir.js";
import { varRef } from "../../ir/build.js";
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
import { dynUndefinedExpr, PoisonError, type Lowerer } from "./lowerer.js";
import { implicitDefaultInstance, type ParamShape } from "./lower-calls.js";
import { findGenericMethodOn, findMethodOn, upcastTo, type ClassInfo } from "./lower-classes.js";
type Invoke = Extract<IrExpr, { kind: "dynInvoke" }>;
interface Dispatch {
source: Invoke;
fn: IrFunction;
classes: Set<string>;
}
/** Calls on native class capsules keep the instance's compiled methods. The
* reachable-body fixed point discovers both the boxed classes and method
* names before generating checked native dispatch; ordinary dyn receivers
* continue through their existing runtime implementation. */
export class ClassDynamicDispatch {
private readonly boxed = new Set<string>();
private readonly dispatches = new Map<string, Dispatch>();
private readonly generated = new Set<IrFunction>();
process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean {
const seenTypes = new Set<string>();
const discover = (type: IrType): void => {
const key = typeKey(type);
if (seenTypes.has(key)) return;
seenTypes.add(key);
if (isDynTypedRefType(type)) this.boxed.add(type.className);
else if (type.kind === "array") discover(type.elem);
else if (type.kind === "record") {
const shape = lowerer.shapes.get(type.shapeId);
shape?.fields.forEach((field) => discover(field.type));
if (shape?.indexValue) discover(shape.indexValue);
} else if (type.kind === "union") lowerer.unions.get(type.unionId)?.arms.forEach(discover);
else if (type.kind === "func") discover(type.ret);
else if (type.kind === "promise") discover(type.inner);
};
for (const fn of functions) everyStmtList(fn.body, {
stmt: () => true,
expr: (expr) => {
if (expr.kind === "dynFrom") discover(expr.value.type);
return true;
},
});
if (this.boxed.size === 0) return false;
let changed = false;
const byMethod = new Map<string, ClassInfo[]>();
const candidates = (method: string): ClassInfo[] => {
const found = byMethod.get(method);
if (found) return found;
const matching = [...this.boxed].flatMap((name) => {
const info = lowerer.classes.get(name);
if (!info || info.fields.has(method) || info.builtinEmitter || info.builtinStream || info.builtinError) return [];
return findMethodOn(lowerer, info, method) || findGenericMethodOn(lowerer, info, method) ? [info] : [];
});
byMethod.set(method, matching);
return matching;
};
for (const fn of functions) {
if (this.generated.has(fn)) continue;
fn.body = transformStmtList(fn.body, {
stmt: (stmt) => stmt,
expr: (expr) => {
if (expr.kind !== "dynInvoke" || candidates(expr.method).length === 0) return expr;
const key = JSON.stringify([expr.method, expr.calleeName, expr.args.length]);
let dispatch = this.dispatches.get(key);
if (!dispatch) {
const params = [expr.recv, ...expr.args].map((_, i) => ({ localId: `p.${i}`, name: `p${i}`, type: DYN }));
const helper: IrFunction = {
name: `%dyn.class.call.${this.dispatches.size}`, params, returnType: DYN,
locals: params.map((p) => ({ id: p.localId, name: p.name, type: DYN, mutable: false })),
body: [{ kind: "return", value: { ...expr, recv: varRef("p.0", DYN, expr.loc), args: expr.args.map((_, i) => varRef(`p.${i + 1}`, DYN, expr.loc)) }, loc: expr.loc }],
loc: expr.loc,
};
dispatch = { source: expr, fn: helper, classes: new Set() };
this.dispatches.set(key, dispatch);
this.generated.add(helper);
lowerer.liftedFns.push(helper);
changed = true;
}
return { kind: "call", callee: dispatch.fn.name, args: [expr.recv, ...expr.args], type: DYN, loc: expr.loc };
},
});
}
for (const dispatch of this.dispatches.values()) {
for (const info of candidates(dispatch.source.method)) {
if (dispatch.classes.has(info.def.name)) continue;
dispatch.classes.add(info.def.name);
const loc = dispatch.source.loc;
const type: IrType = { kind: "object", className: info.def.name };
const receiver = varRef("p.0", DYN, loc);
const before = lowerer.diags.length;
let branch: IrStmt[];
try {
branch = this.methodBody(lowerer, dispatch, info, { kind: "dynCheck", value: receiver, type, loc });
} catch (error) {
if (!(error instanceof PoisonError) || !info.decl) throw error;
const fence = lowerer.deferToRuntimeFence(before, info.decl, { kind: "statement" });
if (!fence) throw error;
branch = [fence];
}
dispatch.fn.body.unshift({
kind: "if", cond: {
kind: "libCall", fn: "dyn.typedRefIs", args: [receiver, { kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc,
}, then: branch, else_: null, loc,
});
changed = true;
}
}
return changed;
}
private methodBody(lowerer: Lowerer, dispatch: Dispatch, info: ClassInfo, receiver: IrExpr): IrStmt[] {
const { method, loc } = dispatch.source;
const fence = (): IrStmt[] => [{ kind: "runtimeFence", code: "SC2020", message: `calling '${method}' on this native class through an untyped value is not supported yet`, loc }];
const methodInfo = findMethodOn(lowerer, info, method);
const generic = methodInfo ? null : findGenericMethodOn(lowerer, info, method);
let params: ParamShape[];
let result: IrType;
let callee: string;
let virtual = false;
let owner: ClassInfo;
if (methodInfo) {
owner = methodInfo.declarer;
params = methodInfo.sig.params;
result = methodInfo.sig.ret;
callee = `%${owner.def.name}.${method}`;
virtual = lowerer.overrideBelow(owner, method) || methodInfo.sig.abstract === true;
} else if (generic?.info.implicitParams && generic.declarer.decl && !lowerer.inHierarchy(info)) {
owner = generic.declarer;
// A failed eager specialization leaves a signature in the instance
// cache, but no body. Later dispatch arities must retain the fence
// instead of turning that cached signature into an unresolved call.
const instance = implicitDefaultInstance(lowerer, generic.declarer.decl, generic.info);
if (!lowerer.implicitFns.some((fn) => fn.name === instance.name)) return fence();
params = instance.params;
result = instance.returnType;
callee = instance.name;
} else return fence();
const incoming = dispatch.source.args.map((_, i) => varRef(`p.${i + 1}`, DYN, loc));
const args: IrExpr[] = [];
let index = 0;
for (const param of params) {
if ((param.mode === "dynRest" || param.mode === "arguments") && param.type.kind === "dyn") {
args.push({ kind: "dynArrLit", elems: param.mode === "arguments" ? incoming : incoming.slice(index), type: DYN, loc });
index = incoming.length;
continue;
}
if (param.mode === "rest" && param.type.kind === "array") {
const element = param.type.elem;
const elems = incoming.slice(index).map((value) => lowerer.coerceToExpected(value, element));
if (elems.some((value) => !typeEquals(value.type, element))) return fence();
args.push({ kind: "arrayLit", elems, type: param.type, loc });
index = incoming.length;
continue;
}
if (param.mode !== "required" && param.mode !== "omittable") return fence();
const value = incoming[index++] ?? param.callDefault ?? dynUndefinedExpr(loc);
const converted = lowerer.coerceToExpected(value, param.type);
if (!typeEquals(converted.type, param.type)) return fence();
args.push(converted);
}
const call: IrExpr = virtual
? { kind: "virtualCall", className: owner.def.name, method, args: [upcastTo(lowerer, receiver, owner.def.name), ...args], type: result, loc }
: { kind: "call", callee, args: [upcastTo(lowerer, receiver, owner.def.name), ...args], type: result, loc };
let body: IrStmt[];
if (result.kind === "void") body = [
{ kind: "exprStmt", expr: { ...call, type: VOID }, loc },
{ kind: "return", value: dynUndefinedExpr(loc), loc },
];
else {
const boxed = lowerer.coerceToExpected(call, DYN);
if (boxed.type.kind !== "dyn") return fence();
body = [{ kind: "return", value: boxed, loc }];
}
if (methodInfo) {
if (virtual) lowerer.noteVirtualEdge(owner, method);
else lowerer.noteEdge(callee);
}
return body;
}
}
@@ -0,0 +1,45 @@
import * as ts from "../../ts7/adapter.js";
import { isJsSourceFile, locOf } from "../../program.js";
import { DYN, type IrExpr } from "../../../ir/ir.js";
import type { Lowerer } from "../lowerer.js";
/** These values must stay checked-native instead of being copied into the
* target's inferred record layout, including hoisted and module bindings. */
export function isNativeProxyInitializer(lowerer: Lowerer, expression: ts.Expression | undefined): boolean {
if (lowerer.dynamic || !expression) return false;
while (ts.isParenthesizedExpression(expression)) expression = expression.expression;
return ts.isNewExpression(expression) && ts.isIdentifier(expression.expression) &&
expression.expression.text === "Proxy" &&
lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(expression.expression) ?? undefined);
}
export function lowerNativeProxy(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
const loc = locOf(expr);
const args = expr.arguments ?? [];
if (args.length !== 2 || args.some(ts.isSpreadElement)) lowerer.noLowering("new Proxy with this argument list", expr);
const handler = args[1]!;
if (isJsSourceFile(expr.getSourceFile()) && ts.isObjectLiteralExpression(handler)) {
for (const prop of handler.properties) {
const callback = ts.isMethodDeclaration(prop) ? prop
: ts.isPropertyAssignment(prop) && (ts.isFunctionExpression(prop.initializer) || ts.isArrowFunction(prop.initializer)) ? prop.initializer : null;
if (!callback) continue;
if (!callback.type) lowerer.runtimeOptionalFunctionReturns.set(callback, DYN);
// The native trap ABI passes live checked values. Contextual
// ProxyHandler types include symbols, which this boundary
// explicitly refuses; they must not force a copying record
// parameter or an unboxable string|symbol closure signature.
for (const param of callback.parameters) {
if (ts.isIdentifier(param.name) && !param.type && !param.dotDotDotToken) lowerer.checkedCallbackParams.add(param);
}
}
}
const inputs = args.map((arg) => {
const value = ts.isObjectLiteralExpression(arg)
? lowerer.lowerExprExpecting(arg, DYN) : lowerer.lowerExpr(arg);
if (value.type.kind !== "dyn") {
lowerer.unsupported("SC1090", arg, "Proxy targets and handlers outside checked-native object storage");
}
return value;
});
return { kind: "libCall", fn: "dyn.proxyNew", args: inputs, type: DYN, loc };
}
@@ -2023,11 +2023,10 @@ export function lowerShorthandValue(lowerer: Lowerer, prop: ts.ShorthandProperty
);
}
/** Rejects any `this` inside an object-literal method body — including in
* nested arrows, which inherit the method's `this` (nested function
* expressions reset it, but their bare `this` is already a tsc error
* under noImplicitThis, so over-rejecting them here changes nothing). */
/** Rejects the method's own receiver, including lexical captures in
* arrows. Nested ordinary functions bind their own ambient receiver. */
export function rejectThisInObjectMethod(lowerer: Lowerer, node: ts.Node): void {
if (ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isAccessor(node)) return;
if (node.kind === ts.SyntaxKind.ThisKeyword) {
lowerer.unsupported("SC1090", node, "references to 'this' in object literal methods");
}
@@ -257,6 +257,11 @@ export interface GenericInstance {
* - `...xs: T[]`: the ABI type is the array; call sites pack the surplus.
*/
export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): ParamShape {
if (lowerer.checkedCallbackParams.has(param)) {
return param.initializer
? { type: DYN, mode: "omittable", bodyType: DYN }
: { type: DYN, mode: "required" };
}
const moduleNs = lowerer.moduleNsParamOverrides.get(param);
if (moduleNs !== undefined) {
return { type: moduleNs, mode: param.questionToken ? "omittable" : "required" };
@@ -2275,7 +2280,7 @@ export function implicitMethodCallInfersReturn(lowerer: Lowerer, call: ts.CallEx
/** The all-dyn DEFAULT instance — today's compiled body exactly: what a
* VALUE reference of an implicit-any function names (indirect calls
* carry no per-site types to bind). */
function implicitDefaultInstance(lowerer: Lowerer, blame: ts.Node, info: GenericFnInfo): GenericInstance {
export function implicitDefaultInstance(lowerer: Lowerer, blame: ts.Node, info: GenericFnInfo): GenericInstance {
const shapes: ParamShape[] = info.decl.parameters.map((param, i) =>
info.implicitParams![i] ? { type: DYN, mode: "required" as const } : lowerer.paramShape(param),
);
@@ -5937,7 +5942,7 @@ export function lowerDynDispatchMethodCall(
arrayReceiver: boolean,
): IrExpr | null {
const method = access.name.text;
if (!DYN_DISPATCH_METHODS.has(method) || call.questionDotToken || access.questionDotToken) return null;
if ((!DYN_DISPATCH_METHODS.has(method) && !isJsSourceFile(call.getSourceFile())) || call.questionDotToken || access.questionDotToken) return null;
if (call.arguments.some((arg) => ts.isSpreadElement(arg))) {
lowerer.unsupported("SC1090", call, "spread arguments in calls through 'unknown' values");
}
@@ -6943,11 +6948,13 @@ function loweredTemplateStrings(
const hasRest = shapes.some((s) => s.mode === "rest" || s.mode === "dynRest" || s.mode === "islandRest");
const usesArguments =
!hasRest &&
(ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node)) &&
(ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) &&
isJsSourceFile(node.getSourceFile()) &&
bodyReadsArguments(node);
if (usesArguments && node.parameters.length > 0) {
if (!isNodeEsmFile(node.getSourceFile())) {
if (usesArguments && (node.parameters.length > 0 || ts.isMethodDeclaration(node))) {
const classMethod = ts.isMethodDeclaration(node) &&
(ts.isClassDeclaration(node.parent) || ts.isClassExpression(node.parent));
if (!classMethod && !isNodeEsmFile(node.getSourceFile())) {
lowerer.unsupported("SC1090", node, "parameterized 'arguments' outside an ES module (sloppy-mode parameter aliases)");
}
shapes.push({ type: DYN, mode: "arguments" });
@@ -26,6 +26,7 @@ import { ambientNsRootOf, ambientUndefReadType, ambientUndefVarRootOf, ambientUn
import { mixinResultBindingClassOf, type MixinInstanceInfo } from "./lower-mixins.js";
import { rejectStaticThis } from "./static-this.js";
import { staticForkString } from "../fork-target.js";
import { isNativeProxyInitializer, lowerNativeProxy } from "./expressions/native-proxy.js";
export interface ClassInfo {
def: IrClassDef;
@@ -4342,8 +4343,10 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction
// A shorter JS override still implements every inherited ABI slot.
// These tail parameters have no source binding or default prologue.
for (const shape of sig.params.slice(declared.params.length)) {
const ignored = lowerer.declareHiddenLocal("%ignored", shape.type);
params.push({ localId: ignored.id, name: "%ignored", type: shape.type });
const name = shape.mode === "arguments" ? "%arguments" : "%ignored";
const local = lowerer.declareHiddenLocal(name, shape.type);
params.push({ localId: local.id, name, type: shape.type });
if (shape.mode === "arguments") fnCtx.argumentsLocal = local;
}
const body = [...declared.prologue, ...lowerer.lowerStmts(fnLike.body.statements)];
appendImplicitUndefinedReturn(lowerer, body, bodyReturn, locOf(fnLike));
@@ -4397,6 +4400,11 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction
(keyword) => `'${keyword}' in static methods (it names the RECEIVER class — a dynamic value; reference the class by name instead)`,
);
const declared = lowerer.declareParams(entry.member.parameters, entry.params);
if (entry.params.at(-1)?.mode === "arguments") {
const local = lowerer.declareHiddenLocal("%arguments", DYN);
declared.params.push({ localId: local.id, name: "%arguments", type: DYN });
fnCtx.argumentsLocal = local;
}
const body = [...declared.prologue, ...lowerer.lowerStmts(entry.member.body.statements)];
appendImplicitUndefinedReturn(lowerer, body, bodyReturn, locOf(entry.member));
const fn: IrFunction = {
@@ -5147,6 +5155,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
// (a no-op for every non-import= binding).
fenceEarlyAliasUse(lowerer, expr.expression, expr);
const symbol = lowerer.resolveValueSymbol(expr.expression);
if (isNativeProxyInitializer(lowerer, expr)) return lowerNativeProxy(lowerer, expr);
// `new Error(msg?)` and its standard subclasses: the
// runtime-provided classes construct through one libCall — the result
// TYPE names which builtin, and the message completes to "" exactly
@@ -5752,7 +5761,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
FinalizationRegistry: "finalization callbacks expose GC timing — genuinely dynamic; release resources explicitly instead",
SharedArrayBuffer: "no shared-memory threads exist in a compiled program — Uint8Array is the byte storage",
ArrayBuffer: "no free-standing ArrayBuffer value exists — typed arrays own their storage: allocate the view directly (new Uint8Array(n)), or erase a fresh buffer into one (new Uint8Array(new ArrayBuffer(n)), new DataView(new ArrayBuffer(n), ...))",
Proxy: "property-access metaprogramming has no static lowering (every property read must resolve at compile time)",
Proxy: "native Proxy construction requires checked-native plain targets and handlers",
Function: "runtime code generation cannot be compiled ahead of time (the eval stance) — write the function",
};
lowerer.noLowering(
@@ -865,6 +865,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
if (
!arithmeticUnion && g.type.kind === "union" && narrowed && narrowed.kind !== "union" &&
!isUnitType(narrowed) &&
!(narrowed.kind === "record" && recordTextCodecClass(lowerer.shapes.get(narrowed.shapeId)!) !== null) &&
narrowed.kind !== "f64" && narrowed.kind !== "string" && narrowed.kind !== "bool" &&
lowerer.armTag(g.type.unionId, narrowed) >= 0
) {
@@ -1122,7 +1123,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// The families with a WHY: each hint states what makes the
// surface genuinely non-static (or what to use instead).
const globalHints: Record<string, string | undefined> = {
Proxy: "property-access metaprogramming has no static lowering (every property read must resolve at compile time)",
Proxy: "Proxy constructor values have no native lowering; use a direct new Proxy with checked-native plain objects",
Reflect: "reflective property access has no static lowering — read and call members directly",
Intl: "locale- and ICU-backed behavior lives outside the static runtime (the localeCompare stance: code-unit order, no collation/locale data) — what lowers: the composed new Intl.NumberFormat(\"en-US\").format(x) and x.toLocaleString(\"en-US\") with default options",
SharedArrayBuffer: "no shared-memory threads exist in a compiled program — Uint8Array is the byte storage",
@@ -4331,7 +4332,7 @@ export function lowerOptionalNumber(
// lower to the ONE process.envGet intrinsic. The read narrows like any
// union-typed expression when the checker narrowed this occurrence.
if (lowerer.isProcessEnv(expr.expression)) {
const key = lowerer.lowerExpr(expr.argumentExpression);
const key = lowerEnvironmentKey(lowerer, expr.argumentExpression);
if (key.type.kind !== "string") {
lowerer.unsupported(
"SC1090",
@@ -5169,7 +5170,7 @@ export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression
// setenv(3), string keys and string values only.
if (lowerer.isProcessEnv(target.expression) && !target.questionDotToken) {
const loc = locOf(expr);
const key = lowerer.lowerExpr(target.argumentExpression);
const key = lowerEnvironmentKey(lowerer, target.argumentExpression);
if (key.type.kind !== "string") {
lowerer.unsupported("SC1090", target.argumentExpression, "indexing process.env with non-string keys");
}
@@ -5525,6 +5526,14 @@ export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression
return arrayValueStore(lowerer, arr, index, value, receiverIr.elem, locOf(expr));
}
/** Environment property names use ToPrimitive with the string hint, so
* checked objects must execute their own conversion hooks. */
export function lowerEnvironmentKey(lowerer: Lowerer, node: ts.Expression): IrExpr {
const key = lowerer.lowerExpr(node);
if (key.type.kind === "dyn") return { kind: "libCall", fn: "dyn.toStringCoerce", args: [key], type: STRING, loc: key.loc };
return lowerRecordPropertyKey(lowerer, key, node);
}
export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr {
if (e.type.kind === "string") return e;
if (e.type.kind === "bigint") {
@@ -6757,20 +6766,15 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
if (arith) return { kind: "bin", op: arith, left: l, right: r, type: F64, loc };
return { kind: "bin", op: cmp!, left: l, right: r, type: BOOL, loc };
}
// `+`: number when the OTHER side is a number, string concat when
// it is a string — the two static homes; dyn+dyn stays a number.
// Untyped addition chooses concatenation or numeric addition only
// after both operands have undergone ToPrimitive at runtime.
if (op === ts.SyntaxKind.PlusToken) {
if (other.type.kind === "f64" || other.type.kind === "dyn") {
return { kind: "bin", op: "+", left: checkNum(left), right: checkNum(right), type: F64, loc };
}
if (other.type.kind === "string") {
// String-context `+`: JS's answer is String(unknown) — the
// JS-exact dyn walker (numbers format, arrays join, objects
// print [object Object], handles the same) — never a checked
// cast: `'status ' + res.statusCode` concatenates like Node.
const strOf = (e: IrExpr): IrExpr =>
e.type.kind === "dyn" ? { kind: "toString", operand: e, type: STRING, loc: e.loc } : e;
return { kind: "strConcat", left: strOf(left), right: strOf(right), type: STRING, loc };
const l = lowerer.coerceToExpected(left, DYN);
const r = lowerer.coerceToExpected(right, DYN);
if (l.type.kind === "dyn" && r.type.kind === "dyn") {
const value: IrExpr = { kind: "libCall", fn: "dyn.add", args: [l, r], type: DYN, loc };
// A statically known string operand guarantees a string result.
return other.type.kind === "string" ? { kind: "dynCheck", value, type: STRING, loc } : value;
}
}
}
@@ -29,6 +29,7 @@ import { collectVirtualJsMethods, decoratorNodesOf, genericIfaceBindingKeepsClas
import { isMixinFnBinding, mixinResultBindingClassOf } from "./lower-mixins.js";
import { cjsModuleRef, cjsModuleRegistryPrelude } from "./lower-node-module.js";
import { forkTargetPaths } from "../fork-target.js";
import { isNativeProxyInitializer } from "./expressions/native-proxy.js";
/** One file's declarations, split for collection and init-body lowering. */
export interface FileParts {
@@ -814,6 +815,7 @@ function cjsScalarLiteral(e: ts.Expression): boolean {
* typed-but-unmappable initializers keep the %init-local adoption). */
function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefined): boolean {
if (init === undefined) return true;
if (isNativeProxyInitializer(lowerer, init)) return true;
let e: ts.Expression = init;
while (ts.isParenthesizedExpression(e)) e = e.expression;
if (e.kind === ts.SyntaxKind.NullKeyword) return true;
@@ -1545,7 +1547,8 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts
// it).
const handleT =
ts.isIdentifier(decl.name) && nameNode === decl.name
? (lowerer.dynamic ? importCallHandleType(decl.initializer) : staticImportNamespaceType(lowerer, decl.initializer)) ??
? (isNativeProxyInitializer(lowerer, decl.initializer) ? DYN : null) ??
(lowerer.dynamic ? importCallHandleType(decl.initializer) : staticImportNamespaceType(lowerer, decl.initializer)) ??
(lowerer.dynamic ?
// An unchecked-overload call result stores the handle,
// exactly the local rule (uncheckedOverloadHandleCall).
@@ -1632,13 +1635,15 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts
const shape = lowerer.shapes.get(type.shapeId);
if (shape && shape.fields.length === 0 && !shape.indexValue && !shape.tuple) type = DYN;
}
// A module var starts as undefined, even when its declared type
// excludes it. Do not silently treat a NULL codec as a usable
// receiver during a closure's pre-initialization read.
// Module vars hold real undefined until their declaration runs.
// Codec records keep that state in the ordinary optional union;
// lexical codec declarations retain their separate TDZ guard.
const codec = type.kind === "record" && recordTextCodecClass(lowerer.shapes.get(type.shapeId)!) !== null;
if (codec && (isVarDeclared(decl) || !decl.initializer)) {
lowerer.noLowering("module-scope codec bindings that can hold undefined before initialization", decl,
"use let or const with an initializer for TextEncoder/TextDecoder instances");
if (codec && isVarDeclared(decl)) {
type = { kind: "union", unionId: lowerer.unions.intern([type, { kind: "undefinedT" }]) };
} else if (codec && !decl.initializer) {
lowerer.noLowering("uninitialized lexical codec bindings", decl,
"initialize let or const TextEncoder/TextDecoder bindings at their declaration");
}
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
// Merged `var` redeclarations (`var y = 1; ...; var y = 2;` —
@@ -1654,7 +1659,7 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts
// A function pointer must not be dereferenced before a lexical
// declaration assigns it. The shared pointer TDZ guard handles
// these bindings like stored codec records.
...((codec || (type.kind === "func" && !isVarDeclared(decl))) ? { tdz: true as const } : {}),
...(((codec || type.kind === "func") && !isVarDeclared(decl)) ? { tdz: true as const } : {}),
};
lowerer.globalsBySymbol.set(symbol, g);
lowerer.globalsList.push(g);
@@ -1775,6 +1780,9 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts
const shape = lowerer.shapes.get(type.shapeId);
if (shape && shape.fields.length === 0 && !shape.indexValue && !shape.tuple) type = DYN;
}
if (type.kind === "record" && recordTextCodecClass(lowerer.shapes.get(type.shapeId)!) !== null) {
type = { kind: "union", unionId: lowerer.unions.intern([type, { kind: "undefinedT" }]) };
}
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
if (!symbol || lowerer.globalsBySymbol.has(symbol)) continue;
const g: IrGlobal = { id: `%g.${tag}${nameNode.text}`, name: nameNode.text, type, mutable: true, source: bindingSource(nameNode) };
@@ -30,6 +30,8 @@ import { abstractPropertyDeclOf, aliasTypeofNarrows, isMatchSliceType, lowerAbse
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
import { globalSymbolKey } from "./expressions/global-symbols.js";
import { lowerNullishAssignment } from "./expressions/nullish-assignment.js";
import { lowerEnvironmentKey } from "./lower-exprs.js";
import { isNativeProxyInitializer } from "./expressions/native-proxy.js";
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
import { lowerUnionFieldWrite } from "./expressions/union-field-write.js";
import { UNSUPPORTED, checkerPanicDiag, isCheckerPanic, requiresDynamicDiag } from "../../diagnostics/diagnostic.js";
@@ -677,6 +679,7 @@ export function provenanceElidedConstDecl(lowerer: Lowerer, decl: ts.VariableDec
* file rides the dyn fallback). Null when no static type can hold the
* binding. */
function varBindingType(lowerer: Lowerer, nameNode: ts.Identifier): IrType | null {
if (ts.isVariableDeclaration(nameNode.parent) && isNativeProxyInitializer(lowerer, nameNode.parent.initializer)) return DYN;
let type = lowerer.mapTypeOf(lowerer.typeOf(nameNode));
if (type?.kind === "record" && isJsSourceFile(nameNode.getSourceFile())) {
const shape = lowerer.shapes.get(type.shapeId);
@@ -4391,7 +4394,10 @@ function lowerBranchSwitch(
return { kind: "strLit", value: target.name.text, type: STRING, loc: locOf(target.name) };
}
const keyNode = (target as ts.ElementAccessExpression).argumentExpression;
let key = lowerer.lowerExpr(keyNode);
let key = lowerer.isProcessEnv(target.expression) ? lowerEnvironmentKey(lowerer, keyNode) : lowerer.lowerExpr(keyNode);
if (key.type.kind === "dyn") {
key = { kind: "libCall", fn: "dyn.toStringCoerce", args: [key], type: STRING, loc: key.loc };
}
if (lowerer.runtimeOptionalWidening(key.type, STRING) !== null) {
key = lowerer.ensureString(key, keyNode);
}
@@ -1,4 +1,5 @@
import { InternalCompilerError } from "../../errors.js";
import { ClassDynamicDispatch } from "./class-dynamic-dispatch.js";
/* AST + checker → IR.
*
* Invariants:
@@ -1744,6 +1745,7 @@ export class Lowerer {
* engine handle (a dynamic import's namespace object) — paramShape's
* early-out. */
readonly jsvalParamOverrides = new Set<ts.ParameterDeclaration>();
readonly checkedCallbackParams = new Set<ts.ParameterDeclaration>();
/** Inline Promise.then parameters whose contextual type is a builtin
* module namespace anonymous object. The settled native token is the
* ABI truth even when the checker type has no stable module symbol. */
@@ -3603,7 +3605,10 @@ export class Lowerer {
let gc = 0;
let gi = 0;
let es = 0;
const classDispatch = new ClassDynamicDispatch();
let dispatchChanged = !this.remainder;
while (
dispatchChanged ||
ec < this.exprClasses.length ||
gc < this.genericClassInstances.length ||
gi < this.instantiationQueue.length ||
@@ -3638,6 +3643,9 @@ export class Lowerer {
if (!(e instanceof PoisonError)) throw e;
}
}
dispatchChanged = !this.remainder && classDispatch.process(this, [
...functions, ...this.liftedFns, ...this.implicitFns,
]);
}
}
// Lambdas lifted while lowering any of the above (plus synthetic
@@ -4218,9 +4226,13 @@ export class Lowerer {
// turn queue more instances. Continue to the joint fixpoint; the initial
// queue above is the only portion whose discovery order differed from
// historical emit order.
const classDispatch = new ClassDynamicDispatch();
for (;;) {
drainInstances();
if (queue.length === 0) break;
const dispatchChanged = classDispatch.process(this, [
...loweredUnits.values(), ...initFunctions, ...instanceFunctions, ...this.liftedFns, ...this.implicitFns,
]);
if (queue.length === 0 && !dispatchChanged) break;
drainUnits();
this.restoreGenericInstanceOrder(instLowered);
this.restoreGenericClassInstanceOrder(clsInstLowered);
@@ -8092,6 +8104,15 @@ export class Lowerer {
* nothing evaluates; unit-typed non-literals keep the fences. */
lowerReturnValue(node: ts.Expression): IrExpr | null {
const expected = this.ctx.returnType;
// Fresh JS objects returned through a checked-native ABI use that
// storage directly, just like arguments and variable initializers.
// Contextual types (such as Proxy descriptors' symbol-valued fields)
// must not force an intermediate record with incompatible field slots.
let literal = node;
while (ts.isParenthesizedExpression(literal)) literal = literal.expression;
if (expected.kind === "dyn" && isJsSourceFile(node.getSourceFile()) && ts.isObjectLiteralExpression(literal)) {
return this.lowerExprExpecting(node, expected);
}
const e = this.lowerExpr(node);
if (expected.kind === "void" && e.kind === "unitLit") return null;
if (this.ctx.isAsync && e.type.kind === "promise" && expected.kind !== "promise") {
+8 -1
View File
@@ -2040,6 +2040,8 @@ export type IrLibFn =
| "dyn.globalSymbolSet"
| "dyn.globalSymbolHas"
| "dyn.globalSymbolDelete"
/** Exact native class capsule identity, without materializing its fields. */
| "dyn.typedRefIs"
/** Destructuring pack over a dyn source — `const [a, b] = d`, a
* destructured dyn callback param (args: the source and the STATIC
* TypeError spelling, "" when the source has none — both borrowed;
@@ -2060,7 +2062,7 @@ export type IrLibFn =
/** `key in v` with a RUNTIME (string) key on a checked-dynamic
* receiver (args: value dyn, key string; result bool): OBJ answers
* own-member presence, ARR answers 'length'/a valid index — exactly
* the compile-time dynHasKey fold, per value. Never throws. */
* the literal-key dynHasKey path. Proxy has traps may throw. */
| "dyn.hasKey"
/** Object.defineProperties over dyn values (args: target, descriptors —
* both borrowed dyn; result: the target, +1 — JS's return value).
@@ -3803,6 +3805,8 @@ export type IrLibFn =
* f64 result, or a throw. Used by statically lowered numeric coercions
* whose checker type remained any. */
| "dyn.toNumberCoerce"
| "dyn.add"
| "dyn.proxyNew"
/** A read of a `declare`d const NOTHING defines (the bundler-define
* pattern — __VERSION__): always throws the catchable ReferenceError
* Node raises at the access ("<name> is not defined"). args[0] is the
@@ -7728,6 +7732,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dyn.objectTag",
// Numeric coercion runs user valueOf/toString — throws propagate.
"dyn.toNumberCoerce",
"dyn.add",
"dyn.proxyNew",
"dyn.hasKey",
"child.kill",
// The caller's lookup runs synchronously inside the connect call — a
// throw there propagates like Node's.
+3
View File
@@ -117,6 +117,7 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"dyn.globalSymbolSet": { argTypes: [SYMBOL_T, DYN], result: VOID },
"dyn.globalSymbolHas": { argTypes: [SYMBOL_T], result: BOOL },
"dyn.globalSymbolDelete": { argTypes: [SYMBOL_T], result: VOID },
"dyn.typedRefIs": { argTypes: [DYN, STRING], result: BOOL },
"dyn.iterPack": { argTypes: [DYN, STRING], result: DYN },
"dyn.arrLen": { argTypes: [DYN], result: F64 },
"dyn.arrAt": { argTypes: [DYN, F64], result: DYN },
@@ -1070,6 +1071,8 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"error.nodeThrow": { argTypes: [F64, STRING, STRING], result: VOID },
"dyn.toStringCoerce": { argTypes: [DYN], result: STRING },
"dyn.toNumberCoerce": { argTypes: [DYN], result: F64 },
"dyn.add": { argTypes: [DYN, DYN], result: DYN },
"dyn.proxyNew": { argTypes: [DYN, DYN], result: DYN },
// Always throws; the result is the READ's declared type (a typed dummy
// the unwind abandons) — the libCall case skips the result check.
"global.undefRead": { argTypes: [STRING], result: VOID },
@@ -10716,6 +10716,36 @@
"<repo>/tests/fixtures/node-types/url-getters.ts"
],
"diags": []
},
"<repo>/tests/corpus/checked-addition.js": {
"order": [
"<repo>/tests/corpus/checked-addition.js"
],
"diags": []
},
"<repo>/tests/corpus/native-class-cache.js": {
"order": [
"<repo>/tests/corpus/native-class-cache.js"
],
"diags": []
},
"<repo>/tests/corpus/native-proxy-env.js": {
"order": [
"<repo>/tests/corpus/native-proxy-env.js"
],
"diags": []
},
"<repo>/tests/corpus/process-env-computed-keys.js": {
"order": [
"<repo>/tests/corpus/process-env-computed-keys.js"
],
"diags": []
},
"<repo>/tests/corpus/text-codec-var.ts": {
"order": [
"<repo>/tests/corpus/text-codec-var.ts"
],
"diags": []
}
}
}
+8 -1
View File
@@ -480,6 +480,10 @@ static bool scr_assert_dyn_same_value(const ScrDyn *a, const ScrDyn *b) {
* memo here where Node carries one (documented divergence). */
static bool scr_assert_dyn_deep_eq(const ScrDyn *a, const ScrDyn *b) {
if (a == b) return true;
if (a->kind == SCR_DYN_PROXY || b->kind == SCR_DYN_PROXY) {
scr_dyn_proxy_unsupported("deep equality");
return false;
}
if (a->kind == SCR_DYN_TYPED_REF || b->kind == SCR_DYN_TYPED_REF) {
ScrDyn *ma = a->kind == SCR_DYN_TYPED_REF
? scr_dyn_typed_ref_materialize(a)
@@ -725,6 +729,9 @@ static void scr_assert_cf_value(ScrAssertBuf *b, const ScrDyn *d, size_t indent,
scr_dyn_release(materialized);
return;
}
case SCR_DYN_PROXY:
scr_assert_cf_value(b, d->v.proxy.target, indent, depth);
return;
case SCR_DYN_OBJ: {
/* The null-proto dictionary renders with Node's prefix in the
* failure diff too (assertion_error.js inspects both sides). */
@@ -969,7 +976,7 @@ static bool scr_assert_print_myers(ScrAssertBuf *b, const ScrDiffOp *diff, size_
static bool scr_assert_dyn_is_object(const ScrDyn *d) {
return d->kind == SCR_DYN_ARR || d->kind == SCR_DYN_OBJ || d->kind == SCR_DYN_BYTES ||
d->kind == SCR_DYN_HANDLE || d->kind == SCR_DYN_PROMISE ||
d->kind == SCR_DYN_TYPED_REF ||
d->kind == SCR_DYN_TYPED_REF || d->kind == SCR_DYN_PROXY ||
scr_dyn_isl_typeof_is(d, "object"); /* engine-held: the engine's typeof */
}
+18
View File
@@ -125,6 +125,9 @@ static void scr_dyn_display_buf(ScrJsonBuf *b, const ScrDyn *d) {
scr_dyn_release(materialized);
return;
}
case SCR_DYN_PROXY:
scr_dyn_proxy_unsupported("string conversion");
return;
}
}
@@ -371,6 +374,17 @@ static ScrDyn *scr_dyn_invoke_impl(
if (recv->kind == SCR_DYN_JSVAL) {
return scr_dyn_jsval_ops()->invoke(recv->v.jsval.cell, method, args, argc, what);
}
if (recv->kind == SCR_DYN_PROXY) {
ScrStr *key = scr_str_new(method, strlen(method));
ScrDyn *callable = scr_dyn_proxy_get(recv, key);
scr_str_release(key);
if (!callable) return NULL;
scr_dyn_this_push_dyn(recv);
ScrDyn *result = scr_dyn_call(callable, args, argc, what);
scr_dyn_this_pop();
scr_dyn_release(callable);
return result;
}
/* Live Web-boundary capsules expose the prototype of their materialized
* value. Run the ordinary dispatch against the stable snapshot, then
@@ -379,6 +393,10 @@ static ScrDyn *scr_dyn_invoke_impl(
* snapshot identity back to the externally visible capsule. */
if (recv->kind == SCR_DYN_TYPED_REF) {
ScrDyn *materialized = scr_dyn_typed_ref_materialize(recv);
if (scr_exc_pending()) {
scr_dyn_release(materialized);
return NULL;
}
bool mutates = materialized->kind == SCR_DYN_ARR &&
dyn_arr_proto_mutates(method);
ScrDyn *result = scr_dyn_invoke_impl(
+1
View File
@@ -737,6 +737,7 @@ ScrStr *scr_insp_key(ScrStr *k) {
* synthesized helper. Same engine, same defaults. dyn-boxed bytes render
* in the checked-dynamic tree's documented Uint8Array identity (SEMANTICS.md). */
ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
if (d->kind == SCR_DYN_PROXY) return scr_insp_dyn(d->v.proxy.target, recurse, depth);
switch (d->kind) {
case SCR_DYN_NULL:
return scr_str_new("null", 4);
+2
View File
@@ -998,6 +998,8 @@ static const char *isl_dyn_unmarshalable(const ScrDyn *d) {
return "a runtime handle";
case SCR_DYN_PROMISE:
return "a promise";
case SCR_DYN_PROXY:
return "a native Proxy";
case SCR_DYN_ARR:
for (size_t i = 0; i < d->v.arr.len; i++) {
const char *r = isl_dyn_unmarshalable(d->v.arr.items[i]);
+453 -10
View File
@@ -356,6 +356,7 @@ static ScrDyn *scr_dyn_alloc(ScrDynKind kind) {
}
static void scr_dyn_handle_release(void *h, ScrDynHandleTag tag);
static bool scr_dyn_to_primitive_result_is_object(const ScrDyn *d);
void scr_dyn_release(ScrDyn *d) {
if (!d || d->rc == SIZE_MAX) return; /* NULL: an uninitialized `let` local */
@@ -406,6 +407,10 @@ void scr_dyn_release(ScrDyn *d) {
}
d->v.typed_ref.release(d->v.typed_ref.ptr);
break;
case SCR_DYN_PROXY:
scr_dyn_release(d->v.proxy.target);
scr_dyn_release(d->v.proxy.handler);
break;
default:
break; /* null/bool/num have no children */
}
@@ -429,6 +434,11 @@ void scr_dyn_release(ScrDyn *d) {
}
ScrDyn *scr_dyn_obj_get(const ScrDyn *d, const char *key, size_t key_len) {
if (d->kind == SCR_DYN_PROXY) {
scr_dyn_proxy_unsupported("raw object inspection");
return NULL;
}
if (d->kind != SCR_DYN_OBJ) return NULL;
for (size_t i = 0; i < d->v.obj.len; i++) {
const ScrDynEntry *e = &d->v.obj.entries[i];
if (e->key_len == key_len && memcmp(e->key, key, key_len) == 0) return e->value;
@@ -702,6 +712,239 @@ ScrDyn *scr_dyn_new_obj_null_proto(void) {
return d;
}
void scr_dyn_proxy_unsupported(const char *operation) {
ScrJsonBuf msg;
scr_jb_init(&msg);
scr_jb_puts(&msg, operation);
scr_jb_puts(&msg, " on a native Proxy is not supported yet");
scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&msg));
}
static ScrDynEntry *scr_dyn_entry(ScrDyn *obj, const ScrStr *key);
static bool scr_dyn_property_same_value(const ScrDyn *a, const ScrDyn *b);
static void scr_dyn_proxy_invariant(void) {
static const char msg[] = "Proxy trap violated a target property invariant";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
}
ScrDyn *scr_dyn_proxy_new(const ScrDyn *target, const ScrDyn *handler) {
if (!scr_dyn_to_primitive_result_is_object(target) || !scr_dyn_to_primitive_result_is_object(handler)) {
static const char msg[] = "Cannot create proxy with a non-object as target or handler";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
return NULL;
}
/* Plain dyn objects expose their live data-property descriptors. Other
* representations cannot promise the Proxy invariants or live identity. */
if (target->kind != SCR_DYN_OBJ || handler->kind != SCR_DYN_OBJ) {
scr_dyn_proxy_unsupported("non-plain targets or handlers");
return NULL;
}
ScrDyn *out = scr_dyn_alloc(SCR_DYN_PROXY);
out->v.proxy.target = scr_dyn_retain((ScrDyn *)target);
out->v.proxy.handler = scr_dyn_retain((ScrDyn *)handler);
return out;
}
/* GetMethod observes handler mutations on every operation. Retain the
* callable across invocation, since a trap may replace itself. */
static ScrDyn *scr_dyn_proxy_trap(const ScrDyn *proxy, const char *name) {
ScrDyn *method = scr_dyn_obj_get(proxy->v.proxy.handler, name, strlen(name));
if (!method || method->kind == SCR_DYN_UNDEF || method->kind == SCR_DYN_NULL) return NULL;
if (method->kind != SCR_DYN_FUNC) {
ScrJsonBuf msg;
scr_jb_init(&msg);
scr_jb_puts(&msg, "Proxy handler '");
scr_jb_puts(&msg, name);
scr_jb_puts(&msg, "' is not callable");
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&msg));
return NULL;
}
return scr_dyn_retain(method);
}
static ScrDyn *scr_dyn_proxy_call(const ScrDyn *proxy, ScrDyn *trap, ScrDyn *const *args, size_t argc) {
scr_dyn_this_push_dyn(proxy->v.proxy.handler);
ScrDyn *result = scr_dyn_call(trap, args, argc, "Proxy trap");
scr_dyn_this_pop();
scr_dyn_release(trap);
return result;
}
static bool scr_dyn_object_proto_key(const ScrDyn *target, const ScrStr *key) {
if (target->null_proto) return false;
static const char *const names[] = {
"constructor", "__defineGetter__", "__defineSetter__", "hasOwnProperty",
"__lookupGetter__", "__lookupSetter__", "isPrototypeOf", "propertyIsEnumerable",
"toString", "valueOf", "__proto__", "toLocaleString"
};
for (size_t i = 0; i < sizeof names / sizeof names[0]; i++) {
if (key->len == strlen(names[i]) && memcmp(key->data, names[i], key->len) == 0) return true;
}
return false;
}
ScrDyn *scr_dyn_proxy_get(const ScrDyn *proxy, const ScrStr *key) {
ScrDyn *trap = scr_dyn_proxy_trap(proxy, "get");
if (scr_exc_pending()) return NULL;
if (!trap) {
ScrDyn *target = proxy->v.proxy.target;
if (!scr_dyn_obj_get(target, key->data, key->len) && scr_dyn_object_proto_key(target, key)) {
scr_dyn_proxy_unsupported("inherited Object.prototype member reads");
return NULL;
}
return scr_dyn_obj_read(target, key->data, key->len);
}
ScrDyn *property = scr_dyn_new_str((ScrStr *)key);
ScrDyn *args[] = { proxy->v.proxy.target, property, (ScrDyn *)proxy };
ScrDyn *result = scr_dyn_proxy_call(proxy, trap, args, 3);
scr_dyn_release(property);
ScrDynEntry *entry = scr_dyn_entry(proxy->v.proxy.target, key);
if (result && entry && !entry->configurable && !entry->writable && !scr_dyn_property_same_value(entry->value, result)) {
scr_dyn_release(result);
scr_dyn_proxy_invariant();
return NULL;
}
return result;
}
bool scr_dyn_proxy_has(const ScrDyn *proxy, const ScrStr *key) {
ScrDyn *trap = scr_dyn_proxy_trap(proxy, "has");
if (scr_exc_pending()) return false;
if (!trap) return scr_dyn_has_key(proxy->v.proxy.target, key) || scr_dyn_object_proto_key(proxy->v.proxy.target, key);
ScrDyn *property = scr_dyn_new_str((ScrStr *)key);
ScrDyn *args[] = { proxy->v.proxy.target, property };
ScrDyn *result = scr_dyn_proxy_call(proxy, trap, args, 2);
scr_dyn_release(property);
bool has = result && scr_dyn_truthy(result);
scr_dyn_release(result);
ScrDynEntry *entry = scr_dyn_entry(proxy->v.proxy.target, key);
if (!scr_exc_pending() && !has && entry && !entry->configurable) scr_dyn_proxy_invariant();
return has;
}
ScrDyn *scr_dyn_own_descriptor(const ScrDyn *value, const ScrStr *key) {
if (value->kind == SCR_DYN_OBJ) {
ScrDynEntry *entry = scr_dyn_entry((ScrDyn *)value, key);
if (!entry) return scr_dyn_retain(scr_dyn_undefined());
ScrDyn *out = scr_dyn_new_obj();
scr_dyn_obj_set(out, "value", 5, scr_dyn_retain(entry->value));
scr_dyn_obj_set(out, "writable", 8, scr_dyn_new_bool(entry->writable));
scr_dyn_obj_set(out, "enumerable", 10, scr_dyn_new_bool(entry->enumerable));
scr_dyn_obj_set(out, "configurable", 12, scr_dyn_new_bool(entry->configurable));
return out;
}
if (value->kind != SCR_DYN_PROXY) {
scr_dyn_proxy_unsupported("property descriptors for this receiver kind");
return NULL;
}
ScrDyn *trap = scr_dyn_proxy_trap(value, "getOwnPropertyDescriptor");
if (scr_exc_pending()) return NULL;
if (!trap) return scr_dyn_own_descriptor(value->v.proxy.target, key);
ScrDyn *property = scr_dyn_new_str((ScrStr *)key);
ScrDyn *args[] = { value->v.proxy.target, property };
ScrDyn *desc = scr_dyn_proxy_call(value, trap, args, 2);
scr_dyn_release(property);
if (!desc) return NULL;
ScrDynEntry *entry = scr_dyn_entry(value->v.proxy.target, key);
if (desc->kind == SCR_DYN_UNDEF) {
if (entry && !entry->configurable) { scr_dyn_release(desc); scr_dyn_proxy_invariant(); return NULL; }
return desc;
}
if (desc->kind != SCR_DYN_OBJ) {
if (scr_dyn_to_primitive_result_is_object(desc)) scr_dyn_proxy_unsupported("non-plain descriptors");
else {
static const char msg[] = "'getOwnPropertyDescriptor' on proxy: trap returned neither object nor undefined";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
}
scr_dyn_release(desc);
return NULL;
}
ScrDyn *getter = scr_dyn_obj_get(desc, "get", 3);
ScrDyn *setter = scr_dyn_obj_get(desc, "set", 3);
ScrDyn *data = scr_dyn_obj_get(desc, "value", 5);
ScrDyn *writable = scr_dyn_obj_get(desc, "writable", 8);
ScrDyn *enumerable = scr_dyn_obj_get(desc, "enumerable", 10);
ScrDyn *configurable = scr_dyn_obj_get(desc, "configurable", 12);
const char *error = NULL;
if (getter && getter->kind != SCR_DYN_FUNC && getter->kind != SCR_DYN_UNDEF) error = "Getter must be a function";
else if (setter && setter->kind != SCR_DYN_FUNC && setter->kind != SCR_DYN_UNDEF) error = "Setter must be a function";
else if ((getter || setter) && (data || writable)) error = "Invalid property descriptor. Cannot both specify accessors and a value or writable attribute";
bool c = configurable && scr_dyn_truthy(configurable);
bool e = enumerable && scr_dyn_truthy(enumerable);
bool w = writable && scr_dyn_truthy(writable);
if (!error && ((!c && (!entry || entry->configurable)) ||
(entry && !entry->configurable &&
(c || e != entry->enumerable || getter || setter || w != entry->writable ||
(!w && !scr_dyn_property_same_value(entry->value, data ? data : scr_dyn_undefined())))))) {
scr_dyn_release(desc);
scr_dyn_proxy_invariant();
return NULL;
}
if (error) {
scr_throw_error_msg(SCR_ERR_TYPE, error, strlen(error));
scr_dyn_release(desc);
return NULL;
}
ScrDyn *out = scr_dyn_new_obj();
if (getter || setter) {
scr_dyn_obj_set(out, "get", 3, scr_dyn_retain(getter ? getter : scr_dyn_undefined()));
scr_dyn_obj_set(out, "set", 3, scr_dyn_retain(setter ? setter : scr_dyn_undefined()));
} else {
scr_dyn_obj_set(out, "value", 5, scr_dyn_retain(data ? data : scr_dyn_undefined()));
scr_dyn_obj_set(out, "writable", 8, scr_dyn_new_bool(writable && scr_dyn_truthy(writable)));
}
scr_dyn_obj_set(out, "enumerable", 10, scr_dyn_new_bool(enumerable && scr_dyn_truthy(enumerable)));
scr_dyn_obj_set(out, "configurable", 12, scr_dyn_new_bool(c));
scr_dyn_release(desc);
return out;
}
void scr_dyn_proxy_set(ScrDyn *proxy, ScrStr *key, ScrDyn *value) {
ScrDyn *trap = scr_dyn_proxy_trap(proxy, "set");
if (scr_exc_pending()) return;
if (!trap) {
/* OrdinarySet uses the Proxy receiver's [[GetOwnProperty]] and
* [[DefineOwnProperty]]. Keep that boundary explicit for now. */
scr_dyn_proxy_unsupported("assignment without a set trap");
return;
}
ScrDyn *property = scr_dyn_new_str(key);
ScrDyn *args[] = { proxy->v.proxy.target, property, value, proxy };
ScrDyn *result = scr_dyn_proxy_call(proxy, trap, args, 4);
scr_dyn_release(property);
if (result && !scr_dyn_truthy(result)) {
/* The keyed-write ABI does not carry lexical strictness yet. A
* falsish trap is a silent failure in sloppy code and a TypeError
* in strict code; retain an explicit boundary instead of guessing. */
scr_dyn_proxy_unsupported("falsish set traps");
}
ScrDynEntry *entry = scr_dyn_entry(proxy->v.proxy.target, key);
if (!scr_exc_pending() && entry && !entry->configurable && !entry->writable && !scr_dyn_property_same_value(entry->value, value)) scr_dyn_proxy_invariant();
scr_dyn_release(result);
}
void scr_dyn_proxy_delete(ScrDyn *proxy, const ScrStr *key) {
ScrDyn *trap = scr_dyn_proxy_trap(proxy, "deleteProperty");
if (scr_exc_pending()) return;
if (!trap) {
ScrDynEntry *entry = scr_dyn_entry(proxy->v.proxy.target, key);
if (entry && !entry->configurable) { scr_dyn_proxy_unsupported("deletion of non-configurable properties"); return; }
scr_dyn_key_delete(proxy->v.proxy.target, key);
return;
}
ScrDyn *property = scr_dyn_new_str((ScrStr *)key);
ScrDyn *args[] = { proxy->v.proxy.target, property };
ScrDyn *result = scr_dyn_proxy_call(proxy, trap, args, 2);
scr_dyn_release(property);
if (result && !scr_dyn_truthy(result)) {
scr_dyn_proxy_unsupported("falsish deleteProperty traps");
}
ScrDynEntry *entry = scr_dyn_entry(proxy->v.proxy.target, key);
if (!scr_exc_pending() && entry && !entry->configurable) scr_dyn_proxy_invariant();
scr_dyn_release(result);
}
ScrDyn *scr_dyn_new_bytes_copy(const ScrBytes *b) {
ScrDyn *d = scr_dyn_alloc(SCR_DYN_BYTES);
d->v.bytes = scr_bytes_copy(b); /* the static→dyn boundary copies */
@@ -739,6 +982,10 @@ bool scr_dyn_typed_ref_is(
memcmp(d->v.typed_ref.type_key, type_key, type_key_len) == 0;
}
bool scr_dyn_typed_ref_is_key(const ScrDyn *d, const ScrStr *type_key) {
return scr_dyn_typed_ref_is(d, type_key->data, type_key->len);
}
void *scr_dyn_typed_ref_unbox(const ScrDyn *d) {
return d->v.typed_ref.retain(d->v.typed_ref.ptr);
}
@@ -1397,6 +1644,10 @@ static bool scr_dyn_property_same_value(const ScrDyn *a, const ScrDyn *b) {
}
ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) {
if (target->kind == SCR_DYN_PROXY || descriptor->kind == SCR_DYN_PROXY) {
scr_dyn_proxy_unsupported("Object.defineProperty");
return NULL;
}
scr_dyn_isl_fence(target, "Object.defineProperty");
if (!scr_exc_pending()) scr_dyn_isl_fence(key, "Object.defineProperty key");
if (!scr_exc_pending()) scr_dyn_isl_fence(descriptor, "Object.defineProperty descriptor");
@@ -1464,21 +1715,15 @@ ScrDyn *scr_dyn_get_own_property_descriptor(ScrDyn *target, ScrDyn *key) {
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
return NULL;
}
if (target->kind != SCR_DYN_OBJ) {
if (target->kind != SCR_DYN_OBJ && target->kind != SCR_DYN_PROXY) {
static const char msg[] = "Object.getOwnPropertyDescriptor on this value is not supported yet";
scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1);
return NULL;
}
ScrStr *name = scr_dyn_property_key(key);
if (!name) return NULL;
ScrDynEntry *entry = scr_dyn_entry(target, name);
ScrDyn *out = scr_dyn_own_descriptor(target, name);
scr_str_release(name);
if (!entry) return scr_dyn_retain(scr_dyn_undefined());
ScrDyn *out = scr_dyn_new_obj();
scr_dyn_obj_set(out, "value", 5, scr_dyn_retain(entry->value));
scr_dyn_obj_set(out, "writable", 8, scr_dyn_new_bool(entry->writable));
scr_dyn_obj_set(out, "enumerable", 10, scr_dyn_new_bool(entry->enumerable));
scr_dyn_obj_set(out, "configurable", 12, scr_dyn_new_bool(entry->configurable));
return out;
}
@@ -1498,6 +1743,7 @@ bool scr_dyn_truthy(const ScrDyn *d) {
case SCR_DYN_HANDLE:
case SCR_DYN_PROMISE:
case SCR_DYN_TYPED_REF: return true;
case SCR_DYN_PROXY: return true;
case SCR_DYN_JSVAL:
/* Route to the engine's ToBoolean: objects/arrays/functions are
* true, but the symbol/bigint edge (0n is falsy) needs the engine. */
@@ -1528,6 +1774,7 @@ ScrStr *scr_dyn_typeof(const ScrDyn *d) {
case SCR_DYN_BYTES:
case SCR_DYN_HANDLE:
case SCR_DYN_PROMISE:
case SCR_DYN_PROXY:
case SCR_DYN_TYPED_REF: s = "object"; break; /* handled above */
case SCR_DYN_BOOL: s = "boolean"; break;
case SCR_DYN_NUM: s = "number"; break;
@@ -1546,6 +1793,10 @@ bool scr_dyn_is_object(const ScrDyn *d) {
}
ScrStr *scr_dyn_object_tag(const ScrDyn *d) {
if (d->kind == SCR_DYN_PROXY) {
scr_dyn_proxy_unsupported("Symbol.toStringTag lookup");
return NULL;
}
if (d->kind == SCR_DYN_TYPED_REF) {
if (scr_dyn_isl_is_error(d)) return scr_str_new("[object Error]", 14);
ScrDyn *materialized = scr_dyn_typed_ref_materialize(d);
@@ -1602,6 +1853,10 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) {
* absent under stringify (root undefined/function — %j prints
* "undefined" there, Node's tryStringify tail). */
static bool scr_dyn_json_write(ScrJsonBuf *b, const ScrDyn *d) {
if (d->kind == SCR_DYN_PROXY) {
scr_dyn_proxy_unsupported("JSON.stringify");
return false;
}
switch (d->kind) {
case SCR_DYN_UNDEF:
case SCR_DYN_FUNC:
@@ -1857,6 +2112,12 @@ void scr_errdyn_put(ScrError *e, ScrDyn *d) {
* "[object Object]", and undefined/null throw Node's TypeError. Borrows
* both; +1 result. */
ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) {
if (d->kind == SCR_DYN_PROXY) {
scr_dyn_proxy_unsupported("string conversion");
/* Match this helper's existing exception-plus-dummy contract; its
* recursive array walker and native callers still consume a string. */
return scr_str_new("", 0);
}
switch (d->kind) {
case SCR_DYN_BYTES:
if (d->buffer) return scr_bytes_to_str(d->v.bytes, enc);
@@ -1985,6 +2246,7 @@ static bool scr_dyn_to_primitive_result_is_object(const ScrDyn *d) {
case SCR_DYN_HANDLE:
case SCR_DYN_PROMISE:
case SCR_DYN_TYPED_REF:
case SCR_DYN_PROXY:
return true;
case SCR_DYN_JSVAL:
/* Engine scalars normally normalize into their native dyn kinds on
@@ -2009,6 +2271,10 @@ static bool scr_dyn_to_primitive_result_is_object(const ScrDyn *d) {
* kind matches scr_dyn_string_coerce (units RENDER — ToString(null) is
* "null"). Borrows; +1, or NULL with the exception pending. */
ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d) {
if (d->kind == SCR_DYN_PROXY) {
scr_dyn_proxy_unsupported("Symbol.toPrimitive lookup");
return NULL;
}
if (d->kind == SCR_DYN_TYPED_REF) {
ScrDyn *materialized = scr_dyn_typed_ref_materialize(d);
ScrStr *out = scr_dyn_string_coerce_js(materialized);
@@ -2051,6 +2317,10 @@ ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d) {
* and the inherited toString fallback supplies "[object Object]". Borrows d;
* false means an object hook threw or no primitive value could be produced. */
bool scr_dyn_number_coerce_js(const ScrDyn *d, double *out) {
if (d->kind == SCR_DYN_PROXY) {
scr_dyn_proxy_unsupported("Symbol.toPrimitive lookup");
return false;
}
if (d->kind == SCR_DYN_TYPED_REF) {
ScrDyn *materialized = scr_dyn_typed_ref_materialize(d);
bool ok = scr_dyn_number_coerce_js(materialized, out);
@@ -2125,6 +2395,86 @@ double scr_dyn_number_coerce(const ScrDyn *d) {
return out;
}
/* OrdinaryToPrimitive's default hint is numeric for the represented
* ordinary objects. Retain the primitive itself: addition must choose
* string concatenation before either side is converted to a number. */
static ScrDyn *scr_dyn_add_primitive(const ScrDyn *d) {
if (d->kind == SCR_DYN_PROXY) {
scr_dyn_proxy_unsupported("Symbol.toPrimitive lookup");
return NULL;
}
if (d->kind == SCR_DYN_TYPED_REF) {
ScrDyn *view = scr_dyn_typed_ref_materialize(d);
ScrDyn *out = scr_exc_pending() ? NULL : scr_dyn_add_primitive(view);
scr_dyn_release(view);
return out;
}
if (d->kind == SCR_DYN_JSVAL) {
scr_dyn_isl_fence(d, "native addition");
return NULL;
}
if (d->kind == SCR_DYN_OBJ) {
static const char *const methods[] = { "valueOf", "toString" };
for (size_t i = 0; i < 2; i++) {
ScrDyn *method = scr_dyn_obj_get(d, methods[i], strlen(methods[i]));
if (!method) {
if (i == 1 && !d->null_proto) {
ScrStr *str = scr_str_new("[object Object]", 15);
ScrDyn *out = scr_dyn_new_str(str);
scr_str_release(str);
return out;
}
continue;
}
if (method->kind != SCR_DYN_FUNC) continue;
scr_dyn_retain(method);
scr_dyn_this_push_dyn(d);
ScrDyn *out = scr_dyn_call(method, NULL, 0, methods[i]);
scr_dyn_this_pop();
scr_dyn_release(method);
if (!out) return NULL;
if (!scr_dyn_to_primitive_result_is_object(out)) return out;
scr_dyn_release(out);
}
static const char msg[] = "Cannot convert object to primitive value";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
return NULL;
}
if (!scr_dyn_to_primitive_result_is_object(d)) return scr_dyn_retain((ScrDyn *)d);
ScrStr *str = scr_dyn_string_coerce(d);
if (!str) return NULL;
ScrDyn *out = scr_dyn_new_str(str);
scr_str_release(str);
return out;
}
ScrDyn *scr_dyn_add(const ScrDyn *left, const ScrDyn *right) {
ScrDyn *l = scr_dyn_add_primitive(left);
if (!l || scr_exc_pending()) { scr_dyn_release(l); return NULL; }
ScrDyn *r = scr_dyn_add_primitive(right);
if (!r || scr_exc_pending()) { scr_dyn_release(l); scr_dyn_release(r); return NULL; }
ScrDyn *out = NULL;
if (l->kind == SCR_DYN_STR || r->kind == SCR_DYN_STR) {
ScrStr *ls = scr_dyn_string_coerce(l);
ScrStr *rs = ls ? scr_dyn_string_coerce(r) : NULL;
if (ls && rs) {
ScrStr *joined = scr_str_concat(ls, rs);
out = scr_dyn_new_str(joined);
scr_str_release(joined);
}
scr_str_release(ls);
scr_str_release(rs);
} else {
double ln, rn;
if (scr_dyn_number_coerce_js(l, &ln) && scr_dyn_number_coerce_js(r, &rn)) {
out = scr_dyn_new_num(ln + rn);
}
}
scr_dyn_release(l);
scr_dyn_release(r);
return out;
}
/* The checked-dynamic keyed WRITE (`h.k = v` on a dyn receiver): OBJ sets
* the member (later writes win, insertion order — JS); undefined/null
* throws Node's "Cannot set properties of ..."; every other kind throws
@@ -2135,8 +2485,10 @@ static const char *scr_dyn_kind_name(const ScrDyn *d);
/* `key in v` with a RUNTIME key (the compile-time dynHasKey fold, per
* value): OBJ answers own-member presence, ARR answers 'length' or a
* valid dense index, every other kind false (tsc admits `in` only on
* object-typed operands). Borrows both; never throws. */
* object-typed operands). Proxy traps may throw. Borrows both. */
bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key) {
if (v->kind == SCR_DYN_PROXY) return scr_dyn_proxy_has(v, key);
if (v->kind == SCR_DYN_JSVAL) return scr_dyn_isl_fence(v, "'in'");
if (v->kind == SCR_DYN_TYPED_REF) {
ScrDyn *materialized = scr_dyn_typed_ref_materialize(v);
bool out = scr_dyn_has_key(materialized, key);
@@ -2160,6 +2512,7 @@ bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key) {
}
void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_set(recv, key, value); return; }
if (recv->kind == SCR_DYN_TYPED_REF) {
ScrDyn *materialized = scr_dyn_typed_ref_materialize(recv);
scr_dyn_key_set(materialized, key, value);
@@ -2247,6 +2600,7 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
* array slots holding undefined print null. A bare undefined never arrives
* (the record serializer drops the entry first); print null defensively. */
void scr_jb_put_dyn(ScrJsonBuf *b, const ScrDyn *d) {
if (d->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("JSON.stringify"); return; }
switch (d->kind) {
case SCR_DYN_NULL:
case SCR_DYN_UNDEF:
@@ -2380,7 +2734,8 @@ static const char *scr_dyn_kind_name(const ScrDyn *d) {
case SCR_DYN_JSVAL: return "an island value"; /* "got an island value" — validated
* extraction of engine-held values has no armed route yet (lane
* dom-jsval-long-tail); the failure names the world honestly. */
case SCR_DYN_TYPED_REF: return "object";
case SCR_DYN_TYPED_REF:
case SCR_DYN_PROXY: return "object";
}
return "unknown";
}
@@ -2971,6 +3326,7 @@ static void scr_json_delete_member(ScrDyn *object, const ScrStr *key) {
* arrays, typed references and handles keep their explicit boundary;
* deleting from a materialized snapshot would lose the mutation. */
void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key) {
if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_delete(recv, key); return; }
if (recv->kind == SCR_DYN_OBJ) {
scr_json_delete_member(recv, key);
return;
@@ -3296,6 +3652,10 @@ typedef struct ScrScParent {
} ScrScParent;
static ScrDyn *scr_sc_clone(const ScrDyn *v, const ScrScParent *up) {
if (v->kind == SCR_DYN_PROXY) {
scr_throw_domex("DataCloneError", "#<Object> could not be cloned.");
return NULL;
}
switch (v->kind) {
case SCR_DYN_UNDEF:
return scr_dyn_retain(scr_dyn_undefined());
@@ -3495,6 +3855,79 @@ static void scr_dyn_objwalk_push(ScrDyn *out, ScrObjWalk mode, const char *key,
}
static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) {
if (v->kind == SCR_DYN_PROXY) {
ScrDyn *trap = scr_dyn_proxy_trap(v, "ownKeys");
if (scr_exc_pending()) return NULL;
ScrDyn *raw;
if (trap) {
ScrDyn *args[] = { v->v.proxy.target };
raw = scr_dyn_proxy_call(v, trap, args, 1);
} else {
/* [[OwnPropertyKeys]] includes non-enumerable properties too. Use
* the shared numeric/insertion ordering over a names-only table. */
ScrDyn *names = scr_dyn_new_obj();
for (size_t i = 0; i < v->v.proxy.target->v.obj.len; i++) {
const ScrDynEntry *entry = &v->v.proxy.target->v.obj.entries[i];
scr_dyn_obj_set(names, entry->key, entry->key_len, scr_dyn_retain(scr_dyn_undefined()));
}
raw = scr_dyn_obj_keys(names);
scr_dyn_release(names);
}
if (!raw) return NULL;
if (raw->kind != SCR_DYN_ARR) {
if (scr_dyn_to_primitive_result_is_object(raw)) scr_dyn_proxy_unsupported("array-like ownKeys results");
else {
static const char msg[] = "CreateListFromArrayLike called on non-object";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
}
scr_dyn_release(raw);
return NULL;
}
/* Snapshot and validate the key list before invoking any descriptor
* or get trap. Those callbacks may mutate the array returned above. */
ScrDyn *keys = scr_dyn_new_arr();
for (size_t i = 0; i < raw->v.arr.len; i++) {
ScrDyn *key = raw->v.arr.items[i];
const char *error = key->kind != SCR_DYN_STR ? "Proxy ownKeys result contains a non-string key" : NULL;
for (size_t j = 0; !error && j < keys->v.arr.len; j++) {
if (scr_str_eq(key->v.str, keys->v.arr.items[j]->v.str)) error = "'ownKeys' on proxy: trap returned duplicate entries";
}
if (error) {
scr_throw_error_msg(SCR_ERR_TYPE, error, strlen(error));
scr_dyn_release(keys);
scr_dyn_release(raw);
return NULL;
}
scr_dyn_arr_push(keys, scr_dyn_retain(key));
}
scr_dyn_release(raw);
for (size_t i = 0; i < v->v.proxy.target->v.obj.len; i++) {
const ScrDynEntry *entry = &v->v.proxy.target->v.obj.entries[i];
if (entry->configurable) continue;
bool found = false;
for (size_t j = 0; j < keys->v.arr.len; j++) {
const ScrStr *key = keys->v.arr.items[j]->v.str;
if (key->len == entry->key_len && memcmp(key->data, entry->key, key->len) == 0) { found = true; break; }
}
if (!found) { scr_dyn_release(keys); scr_dyn_proxy_invariant(); return NULL; }
}
ScrDyn *out = scr_dyn_new_arr();
for (size_t i = 0; i < keys->v.arr.len; i++) {
ScrStr *key = keys->v.arr.items[i]->v.str;
ScrDyn *desc = scr_dyn_own_descriptor(v, key);
if (!desc) { scr_dyn_release(keys); scr_dyn_release(out); return NULL; }
ScrDyn *enumerable = desc->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(desc, "enumerable", 10) : NULL;
bool include = enumerable && scr_dyn_truthy(enumerable);
scr_dyn_release(desc);
if (!include) continue;
ScrDyn *value = mode == SCR_OBJWALK_KEYS ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_proxy_get(v, key);
if (!value) { scr_dyn_release(keys); scr_dyn_release(out); return NULL; }
scr_dyn_objwalk_push(out, mode, key->data, key->len, value);
scr_dyn_release(value);
}
scr_dyn_release(keys);
return out;
}
if (v->kind == SCR_DYN_TYPED_REF) {
ScrDyn *materialized = scr_dyn_typed_ref_materialize(v);
ScrDyn *out = scr_dyn_objwalk(materialized, mode);
@@ -3623,6 +4056,10 @@ ScrDyn *scr_dyn_obj_keys(const ScrDyn *v) { return scr_dyn_objwalk(v, SCR_OBJWAL
* have no own enumerable string keys. Non-OBJ targets copy nothing (the
* existing two-arg stance — a dyn array target has no property table). */
static void scr_dyn_assign_from(ScrDyn *target, const ScrDyn *src) {
if (target->kind == SCR_DYN_PROXY || src->kind == SCR_DYN_PROXY) {
scr_dyn_proxy_unsupported("Object.assign");
return;
}
if (target->kind != SCR_DYN_OBJ) return;
if (src->kind == SCR_DYN_TYPED_REF) {
ScrDyn *materialized = scr_dyn_typed_ref_materialize(src);
@@ -3803,6 +4240,12 @@ static bool scr_dyn_canonical_own_index(const ScrStr *key, size_t length) {
}
bool scr_dyn_has_own(const ScrDyn *v, const ScrStr *key) {
if (v->kind == SCR_DYN_PROXY) {
ScrDyn *desc = scr_dyn_own_descriptor(v, key);
bool has = desc && desc->kind != SCR_DYN_UNDEF;
scr_dyn_release(desc);
return has;
}
if (v->kind == SCR_DYN_UNDEF || v->kind == SCR_DYN_NULL) {
const char *m = "Cannot convert undefined or null to object";
scr_throw_error_msg(SCR_ERR_TYPE, m, strlen(m));
+15 -4
View File
@@ -3332,9 +3332,10 @@ typedef enum {
* contract must expose the exact input reference again. The capsule
* retains the original value so an exact checked cast preserves identity;
* `materialize` supplies the ordinary own-field/data view to generic dyn
* consumers. Enum position: LAST — LLVM hardcodes every preceding kind
* number. */
* consumers. LLVM hardcodes these kind numbers; append new kinds. */
SCR_DYN_TYPED_REF,
/* Native Proxy over a checked plain object; keep new tags at the end. */
SCR_DYN_PROXY,
} ScrDynKind;
/* The handle-type tags the checked-dynamic tree can carry. The set is deliberately the
@@ -3477,6 +3478,7 @@ struct ScrDyn {
* comment). Released through the installed ops so this always-linked
* core never references the gated island unit. */
struct { ScrJsval *cell; } jsval;
struct { ScrDyn *target; ScrDyn *handler; } proxy; /* both owned */
} v;
};
@@ -3563,6 +3565,13 @@ bool scr_dyn_obj_same_source(const ScrDyn *a, const ScrDyn *b);
/* Object.create(null): the fresh null-prototype dictionary (see the
* null_proto flavor flag above). */
ScrDyn *scr_dyn_new_obj_null_proto(void);
ScrDyn *scr_dyn_proxy_new(const ScrDyn *target, const ScrDyn *handler);
ScrDyn *scr_dyn_proxy_get(const ScrDyn *proxy, const ScrStr *key);
bool scr_dyn_proxy_has(const ScrDyn *proxy, const ScrStr *key);
ScrDyn *scr_dyn_own_descriptor(const ScrDyn *value, const ScrStr *key);
void scr_dyn_proxy_set(ScrDyn *proxy, ScrStr *key, ScrDyn *value);
void scr_dyn_proxy_delete(ScrDyn *proxy, const ScrStr *key);
void scr_dyn_proxy_unsupported(const char *operation);
/* Wraps a fresh COPY of the u8 payload (the static→dyn boundary copies —
* DataView-backed sources copy their aliased window). Borrows b. */
ScrDyn *scr_dyn_new_bytes_copy(const ScrBytes *b);
@@ -3581,6 +3590,7 @@ ScrDyn *scr_dyn_new_typed_ref(
void (*commit)(void *, const ScrDyn *));
bool scr_dyn_typed_ref_is(
const ScrDyn *d, const char *type_key, size_t type_key_len);
bool scr_dyn_typed_ref_is_key(const ScrDyn *d, const ScrStr *type_key);
void *scr_dyn_typed_ref_unbox(const ScrDyn *d); /* +1 */
ScrDyn *scr_dyn_class_view_unavailable(void *ptr);
ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d); /* +1 */
@@ -3628,8 +3638,8 @@ ScrDyn *scr_dyn_global_symbol_get(ScrSym *key); /* borrowed key; +1 value */
void scr_dyn_global_symbol_set(ScrSym *key, ScrDyn *value); /* both borrowed */
bool scr_dyn_global_symbol_has(ScrSym *key);
void scr_dyn_global_symbol_delete(ScrSym *key);
/* `key in v` with a runtime key — the dynHasKey fold per value (OBJ own
* members, ARR length/valid indices, false elsewhere). Never throws. */
/* `key in v` with a runtime key (OBJ own members, ARR length/valid indices,
* Proxy has traps). Traps and unsupported representations may throw. */
bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key);
/* Bare `typeof v` on a dyn value: the dyn kind's JS answer (+1 string;
* null answers "object"). Never throws. */
@@ -3655,6 +3665,7 @@ bool scr_dyn_number_coerce_js(const ScrDyn *d, double *out);
/* Direct-return ABI wrapper for compiler libCalls: JS ToNumber, or NaN
* with the exception pending when an object hook throws/refuses. */
double scr_dyn_number_coerce(const ScrDyn *d);
ScrDyn *scr_dyn_add(const ScrDyn *left, const ScrDyn *right); /* borrowed; +1 or NULL/pending */
/* `d instanceof TypeError` (and the other builtin error classes) on a
* checked-dynamic value: the from_error cache resolves the dyn encoding
+20
View File
@@ -0,0 +1,20 @@
function add(a, b) { return a + b; }
console.log(add(2, 3), add("2", 3), add(2, "3"), add(true, null));
console.log(add(undefined, 3), add([1, 2], [3]), add({}, 1));
let trace = "";
const left = { value: "left", valueOf: function() { trace += "L"; return this.value; } };
const right = { valueOf() { trace += "R"; return 2; } };
console.log(add(left, right), trace);
console.log(add({ valueOf() { return {}; }, toString() { return "fallback"; } }, 4));
console.log(add({ valueOf() { return false; } }, true));
try { add({ valueOf() { throw new Error("conversion"); } }, right); }
catch (error) { console.log(error.message, trace); }
try { add({ valueOf() { return {}; }, toString() { return {}; } }, 1); }
catch (error) { console.log(error.name, error.message); }
const empty = Object.create(null);
try { add(empty, "x"); } catch (error) { console.log(error.name); }
function stringLeft(value) { return "prefix:" + value; }
console.log(stringLeft({ valueOf() { return 8; }, toString() { return "wrong"; } }));
function removeHook(object) { delete object.valueOf; }
const changing = { valueOf: function() { removeHook(this); return 6; } };
console.log(add(changing, 1), add(changing, 1));
+54
View File
@@ -0,0 +1,54 @@
class Store {
values = new Map();
count = 0;
put(key, value) { this.values.set(key, value); this.count++; return this; }
get(key) { return this.values.get(key); }
has(key) { return this.values.has(key); }
clearCache() { this.values.clear(); }
join(prefix, ...values) { return prefix + values.join(":"); }
/** @param {...number} numbers */
total(...numbers) { let result = 0; for (const number of numbers) result += number; return result; }
countArgs(first) { return arguments.length; }
noParams() { return arguments.length; }
argumentAlias(first) { arguments[0] = 9; return first; }
static countArgs(first) { return arguments.length; }
describe(prefix = "store") { return `${prefix}:${this.count}`; }
fail() { throw new Error("store failed"); }
}
const cacheKey = Symbol.for("scriptc.class-cache");
globalThis[cacheKey] = new Store();
const store = globalThis[cacheKey];
console.log(store.put("one", 3) === store, store.get("one"), store.has("one"));
console.log(store.describe(), store.describe("cached"));
console.log(store.join("items:", 1, "two", true), store.join("empty:"), store.countArgs(1, 2, 3));
console.log(store.noParams(1, 2), store.argumentAlias(4), Store.countArgs(1, 2, 3, 4));
console.log(store.total(1, 2, 3), store.total());
store.clearCache();
console.log(store.get("one") === undefined, store.has("one"));
try { store.fail(); } catch (error) { console.log(error.message); }
class Base {
value = 4;
describe() { return `base:${this.value}`; }
}
class Derived extends Base {
describe() { return `derived:${this.value}`; }
}
globalThis[cacheKey] = new Derived();
console.log(globalThis[cacheKey].describe());
/** @type {Base} */
const base = new Derived();
globalThis[cacheKey] = base;
console.log(globalThis[cacheKey].describe());
class Factory {
make() { return new Store(); }
}
globalThis[cacheKey] = new Factory();
const next = globalThis[cacheKey].make();
console.log(next.put("next", 8).get("next"));
let effects = "";
function receiver() { effects += "receiver;"; return next; }
function replace() { effects += "argument;"; globalThis[cacheKey] = new Store(); return "last"; }
console.log(receiver().put(replace(), 9) === next, next.get("last"), effects);
globalThis[cacheKey] = { describe() { return "plain"; } };
console.log(globalThis[cacheKey].describe());
delete globalThis[cacheKey];
+109
View File
@@ -0,0 +1,109 @@
const values = { one: 1, two: 2 };
function erase(object, key) { delete object[key]; }
let trace = "";
const proxy = new Proxy({}, {
get(target, key, receiver) {
trace += `get:${key};`;
if (key === "identity") return receiver;
if (key === "method") return function(value) { return this.one + value; };
return values[key];
},
has(target, key) { trace += `has:${key};`; return key in values; },
ownKeys() { trace += "keys;"; return ["two", "hidden", "one", "absent"]; },
getOwnPropertyDescriptor(target, key) {
trace += `desc:${key};`;
if (key === "absent") return;
return { configurable: true, enumerable: key !== "hidden", get: () => values[key] };
}
});
console.log(proxy.one, proxy.identity === proxy, proxy.method(4));
console.log(typeof proxy, !!proxy, Array.isArray(proxy), proxy !== new Proxy({}, {}));
console.log("one" in proxy, "no" in proxy, trace);
trace = "";
console.log(Object.keys(proxy).join(","), trace);
trace = "";
console.log(Object.values(proxy).join(","), trace);
trace = "";
console.log(Object.entries(proxy).join("|"), trace);
trace = "";
console.log(Object.hasOwn(proxy, "one"), Object.hasOwn(proxy, "absent"), trace);
const descriptor = Object.getOwnPropertyDescriptor(proxy, "one");
console.log(descriptor.enumerable, descriptor.configurable, descriptor.set === undefined, descriptor.get());
values.one = 8;
console.log(proxy.one, descriptor.get());
function readGlobalProxy() { return proxy.one; }
console.log(readGlobalProxy());
const target = { count: 1 };
const handler = { get: function(obj, key, receiver) { return obj[key] + (this === handler ? 10 : 0); } };
const live = new Proxy(target, handler);
console.log(live.count);
target.count = 3;
handler.get = function(obj, key) { return obj[key] + 20; };
console.log(live.count);
erase(handler, "get");
console.log(live.count, Object.keys(live).join(","));
const mutator = new Proxy(target, {
set(obj, key, value, receiver) { obj[key] = value; return true; },
deleteProperty(obj, key) { delete obj[key]; return true; }
});
mutator.count = 7;
console.log(target.count);
erase(mutator, "count");
console.log(Object.keys(target).length);
function membership(value, key) { return key in value; }
const throws = new Proxy({}, {
get() { throw new Error("get failed"); },
has() { throw new Error("has failed"); },
ownKeys() { throw new Error("keys failed"); },
getOwnPropertyDescriptor() { throw new Error("descriptor failed"); }
});
try { console.log(throws.x); } catch (error) { console.log(error.message); }
try { console.log("x" in throws); } catch (error) { console.log(error.message); }
try { console.log(membership(throws, "x")); } catch (error) { console.log(error.message); }
try { console.log(Object.keys(throws)); } catch (error) { console.log(error.message); }
try { console.log(Object.hasOwn(throws, "x")); } catch (error) { console.log(error.message); }
const duplicates = new Proxy({}, { ownKeys() { return ["x", "x"]; } });
try { Object.keys(duplicates); } catch (error) { console.log(error.name); }
const badKey = new Proxy({}, { ownKeys() { return [3]; } });
try { Object.keys(badKey); } catch (error) { console.log(error.name); }
const fixed = new Proxy({}, { getOwnPropertyDescriptor() { return { value: 1 }; } });
try { Object.hasOwn(fixed, "x"); } catch (error) { console.log(error.name); }
const plain = new Proxy({}, {});
console.log("toString" in plain, "absent" in plain, plain.absent === undefined);
const bare = new Proxy(Object.create(null), {});
console.log("toString" in bare, bare.toString === undefined);
const keys = ["first", "second"];
const snapshot = new Proxy({}, {
ownKeys() { return keys; },
getOwnPropertyDescriptor(target, key) {
keys[1] = "changed";
return { enumerable: true, configurable: true, value: key };
}
});
console.log(Object.keys(snapshot).join(","));
const protectedTarget = { visible: 2 };
Object.defineProperty(protectedTarget, "fixed", { value: -0 });
const protectedProxy = new Proxy(protectedTarget, {});
const protectedDescriptor = Object.getOwnPropertyDescriptor(protectedProxy, "fixed");
console.log(Object.keys(protectedProxy).join(","), protectedDescriptor.enumerable, protectedDescriptor.writable, protectedDescriptor.configurable, 1 / protectedDescriptor.value);
const readonlyGet = new Proxy(protectedTarget, { get() { return 0; } });
try { console.log(readonlyGet.fixed); } catch (error) { console.log(error.name); }
const readonlyHas = new Proxy(protectedTarget, { has() { return false; } });
try { console.log("fixed" in readonlyHas); } catch (error) { console.log(error.name); }
const readonlyKeys = new Proxy(protectedTarget, { ownKeys() { return ["visible"]; } });
try { Object.keys(readonlyKeys); } catch (error) { console.log(error.name); }
const readonlyDescriptor = new Proxy(protectedTarget, { getOwnPropertyDescriptor() { return; } });
try { Object.hasOwn(readonlyDescriptor, "fixed"); } catch (error) { console.log(error.name); }
const readonlySet = new Proxy(protectedTarget, { set() { return true; } });
try { readonlySet.fixed = 1; } catch (error) { console.log(error.name); }
const readonlyDelete = new Proxy(protectedTarget, { deleteProperty() { return true; } });
try { erase(readonlyDelete, "fixed"); } catch (error) { console.log(error.name); }
const exactDescriptor = new Proxy(protectedTarget, { getOwnPropertyDescriptor() { return { value: -0 }; } });
console.log(Object.hasOwn(exactDescriptor, "fixed"), 1 / Object.getOwnPropertyDescriptor(exactDescriptor, "fixed").value);
const showHidden = new Proxy(protectedTarget, {
getOwnPropertyDescriptor(target, key) {
if (key === "fixed") return { value: -0 };
return { value: 2, enumerable: true, configurable: true };
}
});
console.log(Object.keys(showHidden).join(","));
+27
View File
@@ -0,0 +1,27 @@
const name = JSON.parse('"SCRIPTC_COMPUTED_ENV"');
process.env[name] = "first";
console.log(process.env[name]);
process.env[name] = "second";
console.log(process.env.SCRIPTC_COMPUTED_ENV);
delete process.env[name];
console.log(process.env[name] === undefined);
process.env[918273] = "number";
process.env[true] = "boolean";
console.log(process.env[918273], process.env[true]);
delete process.env[918273];
delete process.env[true];
console.log(process.env[918273] === undefined, process.env[true] === undefined);
let calls = 0;
function key() { calls++; return name; }
process.env[key()] = "once";
console.log(process.env[key()], calls);
delete process.env[key()];
console.log(calls);
const objectKey = JSON.parse('{}');
objectKey.toString = () => { calls++; return "SCRIPTC_COERCED_ENV"; };
process.env[objectKey] = "coerced";
console.log(process.env[objectKey], calls);
delete process.env[objectKey];
objectKey.toString = () => { throw new Error("key failed"); };
try { console.log(process.env[objectKey]); }
catch (error) { console.log(error.message, calls); }
+27
View File
@@ -0,0 +1,27 @@
function encode(): Uint8Array { return encoder.encode(input()); }
let calls = 0;
function input(): string { calls++; return "café"; }
function encoderState(): void { console.log(encoder === undefined, typeof encoder); }
encoderState();
try { encode(); }
catch (error) { if (error instanceof Error) console.log(error.name, error.message, calls); }
var encoder = new TextEncoder();
console.log(encode().join(","), calls);
const saved = encoder;
encoder = new TextEncoder();
console.log(saved === encoder, saved.encode("saved").length);
function decode(): string { return decoder.decode(new Uint8Array([0x80])); }
try { decode(); }
catch (error) { if (error instanceof Error) console.log(error.name, error.message); }
var decoder: TextDecoder;
function decoderState(): void { console.log(decoder === undefined); }
decoderState();
decoder = new TextDecoder("windows-1252");
console.log(decode());
decoder = new TextDecoder();
console.log(decoder.decode(encoder.encode("shared ✓")));
function nestedRead(): number { return nested.encode("nested").length; }
try { nestedRead(); }
catch (error) { if (error instanceof Error) console.log(error.name, error.message); }
if (calls > 0) { var nested = new TextEncoder(); }
console.log(nestedRead());
+3 -1
View File
@@ -1,4 +1,4 @@
import { Base, FactoryInput, Input, Metrics, View, describeOptions } from "bundled-methods";
import { Base, FactoryInput, Input, Metrics, View, describeOptions, exerciseCachedStore, exerciseEnvironmentProxy } from "bundled-methods";
const view = new View("selected");
const base: Base = view;
@@ -53,3 +53,5 @@ factory.open("replacement");
console.log(factory.read(), factory.save() === saved);
console.log(describeOptions({ name: "omitted" }));
console.log(describeOptions({ name: "present", required: false, default: 0, type: "number" }));
console.log(exerciseCachedStore());
console.log(exerciseEnvironmentProxy());
+2
View File
@@ -35,3 +35,5 @@ export class FactoryInput {
save(): { describe(): string } | null;
}
export function describeOptions(config: { name: string; required?: boolean; default?: number; type?: string }): string;
export function exerciseCachedStore(): string;
export function exerciseEnvironmentProxy(): string;
+39
View File
@@ -25,3 +25,42 @@ export class FactoryInput {
export function describeOptions(config) {
return [config.name, String(config.required), String(config.default), typeof config.type].join(":");
}
const storeKey = Symbol.for("bundled-methods.store");
class Store {
values = new Map();
set(key, value) { this.values.set(key, value); return this; }
get(key) { return this.values.get(key); }
clearCache() { this.values.clear(); }
}
function cachedStore() { return globalThis[storeKey] ??= new Store(); }
export function exerciseCachedStore() {
const store = cachedStore();
const same = store.set("value", "native") === cachedStore();
const value = store.get("value");
store.clearCache();
const cleared = store.get("value") === undefined;
delete globalThis[storeKey];
return `${same}:${value}:${cleared}`;
}
export function exerciseEnvironmentProxy() {
const store = cachedStore();
store.set("value", 4);
const env = new Proxy({}, {
get(target, key) { return store.get(key); },
has(target, key) { return store.get(key) !== undefined; },
ownKeys() { return ["value"]; },
getOwnPropertyDescriptor(target, key) {
if (key === "value") return { enumerable: true, configurable: true, get: () => store.get(key) };
}
});
const first = env.value;
const keys = Object.keys(env).join(",");
const own = Object.hasOwn(env, "value");
store.set("value", 8);
const value = env.value;
const present = "value" in env;
delete globalThis[storeKey];
return `${first}:${keys}:${own}:${value}:${present}`;
}
@@ -100,15 +100,6 @@ stdlib-fence.ts:27:12 - error SC2020: 'new WeakMap' is part of the standard libr
hint: weak collections observe garbage collection, which reference counting never exposes — a strong Map behaves identically in-language: use Map
stdlib-fence.ts:28:12 - error SC2020: 'new Proxy' is part of the standard library types but has no scriptc lowering yet
27 | const wm = new WeakMap();
28 | const px = new Proxy({ a: 1 }, {});
| ^~~~~~~~~~~~~~~~~~~~~~~
29 | const buf = new ArrayBuffer(8);
hint: property-access metaprogramming has no static lowering (every property read must resolve at compile time)
stdlib-fence.ts:29:7 - error SC2020: 'ArrayBuffer' is part of the standard library types but has no scriptc lowering yet
28 | const px = new Proxy({ a: 1 }, {});
+53
View File
@@ -0,0 +1,53 @@
import { execFile } from "node:child_process";
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import { expect, test } from "vitest";
import { compile } from "@scriptc/compiler";
const execFileAsync = promisify(execFile);
for (const backend of ["c", "llvm"] as const) {
test(`native Proxy boundaries unwind catchably (${backend})`, async () => {
const outDir = mkdtempSync(join(tmpdir(), "scriptc-proxy-boundaries-"));
const entry = join(outDir, "main.mjs");
writeFileSync(entry, `
const proxy = new Proxy({}, {});
function stringify(value) { return String(value); }
function add(left, right) { return left + right; }
function wrap(value) { return [value]; }
try { stringify(proxy); } catch (error) { console.log(error.message); }
try { stringify(wrap(proxy)); } catch (error) { console.log(error.message); }
try { add(wrap(proxy), 1); } catch (error) { console.log(error.message); }
try { proxy.value = 1; } catch (error) { console.log(error.message); }
try { Object.assign({}, proxy); } catch (error) { console.log(error.message); }
try { JSON.stringify(proxy); } catch (error) { console.log(error.message); }
try { structuredClone(proxy); } catch (error) { console.log(error.name); }
const falsish = new Proxy({}, { set() { return false; }, deleteProperty() { return false; } });
try { falsish.value = 1; } catch (error) { console.log(error.message); }
try { delete falsish.value; } catch (error) { console.log(error.message); }
console.log("after");
`);
const result = await compile(entry, {
backend, dynamic: false, sanitize: process.env["SCRIPTC_SAN"] === "1",
outDir, outPath: join(outDir, "program"),
});
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
if (!result.ok) return;
const { stdout, stderr } = await execFileAsync(result.binaryPath, [], { encoding: "utf8" });
expect(stdout).toBe([
"string conversion on a native Proxy is not supported yet",
"string conversion on a native Proxy is not supported yet",
"string conversion on a native Proxy is not supported yet",
"assignment without a set trap on a native Proxy is not supported yet",
"Object.assign on a native Proxy is not supported yet",
"JSON.stringify on a native Proxy is not supported yet",
"DataCloneError",
"falsish set traps on a native Proxy is not supported yet",
"falsish deleteProperty traps on a native Proxy is not supported yet",
"after", "",
].join("\n"));
expect(stderr).toBe("");
});
}