mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
@@ -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];
|
||||
@@ -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(","));
|
||||
@@ -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); }
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 }, {});
|
||||
|
||||
@@ -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("");
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user