mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
Compile renderer values and callbacks natively
- Preserve unbound class methods, call-time receivers, and bound callback arguments. - Keep native factory results, fallback functions, and mutable JavaScript renderer state intact. - Support live Error.cause assignment, deletion, and property descriptors across aliases.
This commit is contained in:
@@ -12,7 +12,8 @@ These are rejected at compile time with an `SC` code, a code frame, and usually
|
||||
|
||||
**Language edges**
|
||||
|
||||
- `Error`, `TypeError`, `RangeError`, and `SyntaxError` accept `{ cause }` options, including through inherited constructors. The options must expose `cause` as a data property; accessor options and subclass redeclarations of `cause` remain unsupported. Reading `error.cause` and testing `"cause" in error` preserve the distinction between an absent cause and an explicitly undefined cause.
|
||||
- `Error`, `TypeError`, `RangeError`, and `SyntaxError` accept `{ cause }` options, including through inherited constructors. Reading, assigning, deleting, and testing `"cause" in error` preserve the distinction between an absent cause and an explicitly undefined cause. Constructor causes are non-enumerable; assignment creates an enumerable property when the cause is absent. Property descriptors and checked-value aliases share the same cause, including getters and setters installed after construction. Accessor constructor options and explicit subclass redeclarations of `cause` remain unsupported.
|
||||
- Native class methods can be stored and passed as unbound function values. A field call supplies the containing instance as `this`; `call`, `apply`, and `bind` work when the function signature can cross the checked-value boundary. Extracted overrides keep the selected method even when called with another receiver. Receiver validation accepts the declaring class and its subclasses; borrowing a method onto an unrelated object remains unsupported. Detached methods that use `this`, builtin method values, abstract method declarations, unspecialized generic methods, and incompatible override signatures retain checked errors or compile-time refusals.
|
||||
- Loose `==`/`!=` comparisons compile across statically represented primitive and primitive-union operands, including number/string/boolean/BigInt coercions and the `x == null`/`x != null` nullish idiom. Object-to-primitive comparisons remain fenced because custom `valueOf`/`toString` methods can execute arbitrary code; convert the object explicitly first.
|
||||
- Block- and function-scoped `using` and `await using` compile for statically represented classes with zero-parameter disposal methods and for the supported FileHandle, timer, immediate, child-process, and readline handles. Array `for (using ... of ...)` loops compile. Top-level and switch-clause declarations, disposal through an engine-held package value, and the `DisposableStack` constructors remain fenced; a caught `SuppressedError` exposes its Node-compatible `name` and `message`, while its `error` and `suppressed` payload properties remain outside the static catch surface.
|
||||
- Typed async generator functions and methods compile with lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`. `for await` also consumes Node `Readable` streams, Web `ReadableStream` values, and statically represented class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record; abrupt completion runs the iterator's cleanup, including literal `destroyOnReturn` and `preventCancel` options. Async `yield*` delegation, stored Web/Node iterator handles, and other structural async-iterator objects remain fenced. The `node:timers/promises` `setInterval(delay, value)` async iterator compiles, while its AbortSignal options remain fenced.
|
||||
|
||||
@@ -2616,18 +2616,22 @@ function emitCallExpr(
|
||||
}
|
||||
case "callValue": {
|
||||
const callee = emitter.emitExpr(e.callee);
|
||||
const receiver = e.receiver === undefined ? null : emitter.emitExpr(e.receiver);
|
||||
const args = e.args.map((a) => emitter.emitExpr(a));
|
||||
for (const a of args) emitter.moveTemp(a); // callee owns its params
|
||||
if (e.callee.type.kind !== "func") throw new InternalCompilerError("emitter bug: callValue on non-func");
|
||||
const cast = cFnPtrCast(e.callee.type);
|
||||
const argList = [callee.name, ...args.map((a) => a.name)].join(", ");
|
||||
const call = `(${cast}${callee.name}->fn)(${argList})`;
|
||||
emitter.line(`scr_dyn_this_push_dyn(${receiver?.name ?? "NULL"});`);
|
||||
if (e.type.kind === "void") {
|
||||
emitter.line(`${call};${emitter.srcComment(e.loc)}`);
|
||||
emitter.line("scr_dyn_this_pop();");
|
||||
if (emitter.indirectMayThrow) emitter.emitPendingCheck();
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
const t = emitter.newTemp(e.type, call);
|
||||
emitter.line("scr_dyn_this_pop();");
|
||||
if (emitter.indirectMayThrow) emitter.emitPendingCheck();
|
||||
return t;
|
||||
}
|
||||
@@ -2920,6 +2924,14 @@ function emitDynamicExpr(
|
||||
// with the frame as usual. The callee's source spelling rides
|
||||
// along for Node's "<name> is not a function" TypeError.
|
||||
const callee = emitter.emitExpr(e.callee);
|
||||
const receiver = e.receiver === undefined ? null : emitter.emitExpr(e.receiver);
|
||||
const invoke = (call: string): Temp => {
|
||||
emitter.line(`scr_dyn_this_push_dyn(${receiver?.name ?? "NULL"});`);
|
||||
const result = emitter.newTemp(e.type, call);
|
||||
emitter.line("scr_dyn_this_pop();");
|
||||
emitter.emitPendingCheck();
|
||||
return result;
|
||||
};
|
||||
const what = cStringLiteral(Buffer.from(e.calleeName, "utf8"));
|
||||
if (e.spreads !== undefined && e.spreads.length > 0) {
|
||||
// The RUNTIME-ARITY form (`f(...args)`): one fresh dyn array
|
||||
@@ -2943,7 +2955,7 @@ function emitDynamicExpr(
|
||||
emitter.line(`scr_dyn_arr_push(${pack.name}, ${v.name});`);
|
||||
}
|
||||
});
|
||||
return emitter.fallibleTemp(e.type, `scr_dyn_apply(${callee.name}, ${pack.name}, ${what})`);
|
||||
return invoke(`scr_dyn_apply(${callee.name}, ${pack.name}, ${what})`);
|
||||
}
|
||||
const args = e.args.map((a) => emitter.emitExpr(a));
|
||||
let argsExpr = "NULL";
|
||||
@@ -2952,10 +2964,7 @@ function emitDynamicExpr(
|
||||
emitter.line(`ScrDyn *${arr}[${args.length}] = { ${args.map((a) => a.name).join(", ")} };`);
|
||||
argsExpr = arr;
|
||||
}
|
||||
return emitter.fallibleTemp(
|
||||
e.type,
|
||||
`scr_dyn_call(${callee.name}, ${argsExpr}, ${args.length}, ${what})`,
|
||||
);
|
||||
return invoke(`scr_dyn_call(${callee.name}, ${argsExpr}, ${args.length}, ${what})`);
|
||||
}
|
||||
case "dynInvoke": {
|
||||
// Prototype-method dispatch on a dyn receiver: everything is
|
||||
@@ -8258,6 +8267,10 @@ function emitErrorsEventsLibCall(state: LibCallState): Temp {
|
||||
return finish(`scr_error_cause((ScrError *)${arg(0)})`);
|
||||
case "error.hasCause":
|
||||
return finish(`scr_error_has_cause((ScrError *)${arg(0)})`);
|
||||
case "error.setCause":
|
||||
return finish(`scr_error_set_cause((ScrError *)${arg(0)}, ${arg(1)})`);
|
||||
case "error.deleteCause":
|
||||
return finish(`scr_error_delete_cause((ScrError *)${arg(0)})`);
|
||||
case "error.toString":
|
||||
// Borrowed receiver; +1 "name: message" (Node's toString rules).
|
||||
return finish(`scr_error_to_string((ScrError *)${arg(0)})`);
|
||||
|
||||
@@ -419,6 +419,7 @@ export function emitCallExpr(host: LlvmEmitterContext, e: ExprOf<"call" | "ffiCa
|
||||
const ft = e.callee.type;
|
||||
if (e.args.length !== ft.params.length) throw new LlvmUnsupportedError("callValue:arity", e.loc);
|
||||
const callee = host.emitExpr(e.callee);
|
||||
const receiver = e.receiver === undefined ? null : host.emitExpr(e.receiver);
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
for (const a of args) host.moveTemp(a);
|
||||
const fnp = B.tmp();
|
||||
@@ -429,13 +430,18 @@ export function emitCallExpr(host: LlvmEmitterContext, e: ExprOf<"call" | "ffiCa
|
||||
`ptr ${callee.name}`,
|
||||
...args.map((a, i) => `${host.llType(ft.params[i]!)} ${a.name}`),
|
||||
].join(", ");
|
||||
host.declare("declare void @scr_dyn_this_push_dyn(ptr)");
|
||||
host.declare("declare void @scr_dyn_this_pop()");
|
||||
B.line(`call void @scr_dyn_this_push_dyn(ptr ${receiver?.name ?? "null"})`);
|
||||
if (e.type.kind === "void") {
|
||||
B.line(`call void ${fn}(${argList})`);
|
||||
B.line("call void @scr_dyn_this_pop()");
|
||||
if (host.indirectMayThrow) host.emitPendingCheck();
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call ${host.llType(e.type)} ${fn}(${argList})`);
|
||||
B.line("call void @scr_dyn_this_pop()");
|
||||
// The check runs AFTER the result temp joins the frame: an unwind
|
||||
// releases it (the dummy is NULL for refcounted returns).
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
|
||||
@@ -99,6 +99,9 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" |
|
||||
// boxed thunk builds its own typed copies. The callee's source
|
||||
// spelling rides along for Node's "<name> is not a function".
|
||||
const callee = host.emitExpr(e.callee);
|
||||
const receiver = e.receiver === undefined ? null : host.emitExpr(e.receiver);
|
||||
host.declare("declare void @scr_dyn_this_push_dyn(ptr)");
|
||||
host.declare("declare void @scr_dyn_this_pop()");
|
||||
if (e.spreads !== undefined && e.spreads.length > 0) {
|
||||
// The RUNTIME-ARITY form (`f(...args)`): one fresh dyn array
|
||||
// collects the arguments left-to-right — plain args move in
|
||||
@@ -127,7 +130,9 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" |
|
||||
}
|
||||
});
|
||||
const t = B.tmp();
|
||||
B.line(`call void @scr_dyn_this_push_dyn(ptr ${receiver?.name ?? "null"})`);
|
||||
B.line(`${t} = call ptr @scr_dyn_apply(ptr ${callee.name}, ptr ${pack}, ptr ${host.cstr(e.calleeName)})`);
|
||||
B.line("call void @scr_dyn_this_pop()");
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return out;
|
||||
@@ -146,7 +151,9 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" |
|
||||
}
|
||||
host.declare(`declare ptr @scr_dyn_call(ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
const t = B.tmp();
|
||||
B.line(`call void @scr_dyn_this_push_dyn(ptr ${receiver?.name ?? "null"})`);
|
||||
B.line(`${t} = call ptr @scr_dyn_call(ptr ${callee.name}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${host.cstr(e.calleeName)})`);
|
||||
B.line("call void @scr_dyn_this_pop()");
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return out;
|
||||
|
||||
@@ -61,6 +61,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
// refuses by name like the rest of the throwing tier.
|
||||
"str.encodeUriComponent": "scr_str_encode_uri_component",
|
||||
"error.cause": "scr_error_cause",
|
||||
"error.setCause": "scr_error_set_cause",
|
||||
"error.deleteCause": "scr_error_delete_cause",
|
||||
"error.hasCause": "scr_error_has_cause",
|
||||
// DOMException: construction and the read surface never throw; the
|
||||
// WebIDL clone's option validation throws (may-throw pending check).
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { BOOL, DYN, STRING, 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 { locOf } from "../program.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { findGenericMethodOn, findMethodOn, type ClassInfo } from "./lower-classes.js";
|
||||
import { funcTypeFromParamShapes, implicitDefaultInstance, type ParamShape } from "./lower-calls.js";
|
||||
|
||||
/** A method value retains its declaration's identity, not the receiver from
|
||||
* extraction. Its native thunk validates the receiver supplied at call time. */
|
||||
export function lowerClassMethodValue(lowerer: Lowerer, expr: ts.PropertyAccessExpression, info: ClassInfo): IrExpr | null {
|
||||
const method = expr.name.text;
|
||||
const value = methodValue(lowerer, expr, info);
|
||||
if (!value) return null;
|
||||
const loc = locOf(expr);
|
||||
const overrides = [...lowerer.classes.values()].filter((candidate) =>
|
||||
candidate !== info && lowerer.isSubclassOf(candidate.def.name, info.def.name) && candidate.methods.has(method));
|
||||
if (overrides.length === 0) {
|
||||
return { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(expr.expression), loc }], result: value, type: value.type, loc };
|
||||
}
|
||||
// Select the declaration when extracting the value. Calling the value
|
||||
// later must not redispatch the method name on a different receiver.
|
||||
const name = `%method.select:${info.def.name}.${method}`;
|
||||
const receiverType: IrType = { kind: "object", className: info.def.name };
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
|
||||
const receiver = varRef("this.0", receiverType, loc);
|
||||
const body: IrStmt[] = [];
|
||||
overrides.sort((a, b) => lowerer.isSubclassOf(a.def.name, b.def.name) ? -1 : lowerer.isSubclassOf(b.def.name, a.def.name) ? 1 : 0);
|
||||
for (const candidate of overrides) {
|
||||
const selected = methodValue(lowerer, expr, candidate);
|
||||
if (!selected || !typeEquals(selected.type, value.type)) {
|
||||
lowerer.unsupported("SC1090", expr, "method values with incompatible override signatures");
|
||||
}
|
||||
body.push({ kind: "if", cond: { kind: "instanceOf", value: receiver, className: candidate.def.name, type: BOOL, loc }, then: [{ kind: "return", value: selected, loc }], else_: null, loc });
|
||||
}
|
||||
body.push({ kind: "return", value, loc });
|
||||
lowerer.liftedFns.push({ name, params: [{ localId: "this.0", name: "this", type: receiverType }], returnType: value.type,
|
||||
locals: [{ id: "this.0", name: "this", type: receiverType, mutable: false }], body, loc });
|
||||
}
|
||||
return { kind: "call", callee: name, args: [lowerer.lowerExpr(expr.expression)], type: value.type, loc };
|
||||
}
|
||||
|
||||
function methodValue(lowerer: Lowerer, expr: ts.PropertyAccessExpression, info: ClassInfo): IrExpr | null {
|
||||
const method = expr.name.text;
|
||||
const found = findMethodOn(lowerer, info, method);
|
||||
const generic = found ? null : findGenericMethodOn(lowerer, info, method);
|
||||
if (!found && !generic) return null;
|
||||
const loc = locOf(expr);
|
||||
let owner: ClassInfo;
|
||||
let params: ParamShape[];
|
||||
let ret: IrType;
|
||||
let callee: string;
|
||||
if (found && !found.declarer.builtinError) {
|
||||
owner = found.declarer;
|
||||
if (found.sig.abstract) lowerer.unsupported("SC1090", expr, "values of abstract method declarations");
|
||||
params = found.sig.params;
|
||||
ret = found.sig.ret;
|
||||
callee = `%${owner.def.name}.${method}`;
|
||||
lowerer.noteEdge(callee);
|
||||
} else if (generic?.info.implicitParams && generic.declarer.decl && !lowerer.inHierarchy(info)) {
|
||||
owner = generic.declarer;
|
||||
const instance = implicitDefaultInstance(lowerer, owner.decl!, generic.info);
|
||||
params = instance.params;
|
||||
ret = instance.returnType;
|
||||
callee = instance.name;
|
||||
} else {
|
||||
lowerer.unsupported("SC1090", expr, "values of builtin or unspecialized generic methods");
|
||||
}
|
||||
const type = funcTypeFromParamShapes(params, ret);
|
||||
const name = `%method.value:${callee}`;
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
|
||||
const thunkParams = params.map((param, i) => ({ localId: `p.${i}`, name: `p${i}`, type: param.type }));
|
||||
const receiverType: IrType = { kind: "object", className: owner.def.name };
|
||||
const receiverName = `%method.receiver:${owner.def.name}`;
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === receiverName)) {
|
||||
lowerer.liftedFns.push({ name: receiverName, params: [{ localId: "this.0", name: "this", type: DYN }], returnType: receiverType,
|
||||
locals: [{ id: "this.0", name: "this", type: DYN, mutable: false }],
|
||||
body: [{ kind: "return", value: { kind: "dynCheck", value: varRef("this.0", DYN, loc), type: receiverType, loc }, loc }], loc });
|
||||
}
|
||||
const receiver: IrExpr = { kind: "call", callee: receiverName,
|
||||
args: [{ kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc }], type: receiverType, loc };
|
||||
const call: IrExpr = {
|
||||
kind: "call", callee, args: [receiver, ...thunkParams.map((p) => varRef(p.localId, p.type, loc))], type: ret, loc,
|
||||
};
|
||||
const fn: IrFunction = {
|
||||
name, params: thunkParams, returnType: ret,
|
||||
locals: thunkParams.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
|
||||
body: ret.kind === "void"
|
||||
? [{ kind: "exprStmt", expr: call, loc }, { kind: "return", value: null, loc }]
|
||||
: [{ kind: "return", value: call, loc }], loc,
|
||||
};
|
||||
lowerer.liftedFns.push(fn);
|
||||
}
|
||||
return { kind: "closure", fnName: name, captures: [], type, loc };
|
||||
}
|
||||
|
||||
/** Complete adapters after all native class bodies and instantiations exist. */
|
||||
export function finalizeClassMethodValues(lowerer: Lowerer, functions: IrFunction[]): void {
|
||||
const byName = new Map(functions.map((fn) => [fn.name, fn]));
|
||||
for (const fn of [...functions]) {
|
||||
if (fn.name.startsWith("%method.receiver:") && fn.returnType.kind === "object") {
|
||||
const owner = fn.returnType.className;
|
||||
const loc = fn.loc;
|
||||
const receiver = varRef("this.0", DYN, loc);
|
||||
const branches: IrStmt[] = [];
|
||||
for (const info of lowerer.classes.values()) {
|
||||
const name = info.def.name;
|
||||
if (!lowerer.isSubclassOf(name, owner) && !lowerer.isSubclassOf(owner, name) && name !== owner) continue;
|
||||
const type: IrType = { kind: "object", className: name };
|
||||
const checked: IrExpr = { kind: "dynCheck", value: receiver, type, loc };
|
||||
const value: IrExpr = name === owner ? checked : lowerer.isSubclassOf(name, owner)
|
||||
? { kind: "upcast", value: checked, type: fn.returnType, loc }
|
||||
: { kind: "downcast", value: checked, type: fn.returnType, loc };
|
||||
const returned: IrStmt = { kind: "return", value, loc };
|
||||
const then: IrStmt[] = name === owner || lowerer.isSubclassOf(name, owner) ? [returned] : [{
|
||||
kind: "if", cond: { kind: "instanceOf", value: checked, className: owner, type: BOOL, loc }, then: [returned], else_: null, loc,
|
||||
}];
|
||||
branches.push({ kind: "if", cond: { kind: "libCall", fn: "dyn.typedRefIs", args: [receiver, { kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc }, then, else_: null, loc });
|
||||
}
|
||||
fn.body.unshift(...branches);
|
||||
}
|
||||
if (!fn.name.startsWith("%method.value:")) continue;
|
||||
const target = byName.get(fn.name.slice("%method.value:".length));
|
||||
const thisParam = target?.params[0];
|
||||
if (!target || !thisParam || target.async || target.generator) continue;
|
||||
const usesThis = !everyStmtList(target.body, { stmt: () => true, expr: (expr) =>
|
||||
!(expr.kind === "varRef" && expr.localId === thisParam.localId) &&
|
||||
!(expr.kind === "closure" && expr.captures.includes(thisParam.localId)) });
|
||||
if (usesThis) continue;
|
||||
// A method that never uses this remains callable when detached.
|
||||
const name = `%method.receiverless:${target.name}`;
|
||||
functions.push({ ...target, name, params: target.params.slice(1), locals: target.locals.filter((local) => local.id !== thisParam.localId) });
|
||||
fn.body = transformStmtList(fn.body, { stmt: (stmt) => stmt, expr: (expr) =>
|
||||
expr.kind === "call" && expr.callee === target.name ? { ...expr, callee: name, args: expr.args.slice(1) } : expr });
|
||||
}
|
||||
}
|
||||
@@ -1059,10 +1059,13 @@ function completeFuncValueArgs(
|
||||
// checked-dynamic (or none): a lambda with RECORD-typed parameters
|
||||
// (a reduce reducer over a typed array) legitimately returns its
|
||||
// parameters' records and keeps the static type.
|
||||
// Native-only members (such as Int32Array layout offsets) cannot cross
|
||||
// that boundary; their factories keep the native record ABI.
|
||||
if (
|
||||
isJsSourceFile(decl.getSourceFile()) &&
|
||||
decl.type === undefined &&
|
||||
lowerer.mapTypeOf(retTsType)?.kind === "record" &&
|
||||
mappedReturn?.kind === "record" &&
|
||||
lowerer.dynConvertible(mappedReturn) &&
|
||||
decl.parameters.every((p) => {
|
||||
const mt = lowerer.mapTypeOf(lowerer.typeOf(p));
|
||||
return mt === null || mt.kind === "dyn";
|
||||
@@ -2221,6 +2224,20 @@ export function implicitMethodCallInfersReturn(lowerer: Lowerer, call: ts.CallEx
|
||||
);
|
||||
}
|
||||
|
||||
/** A represented binding or direct callable result can be wider than the
|
||||
* checker's inferred shape. Preserve that boundary when specializing JS. */
|
||||
function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrType | null {
|
||||
while (ts.isParenthesizedExpression(arg)) arg = arg.expression;
|
||||
if (ts.isIdentifier(arg)) return lowerer.peekLocal(arg)?.type ?? lowerer.globalOf(arg)?.type ?? null;
|
||||
if (ts.isCallExpression(arg) && ts.isIdentifier(arg.expression)) {
|
||||
const callee = storedImplicitArgumentType(lowerer, arg.expression);
|
||||
if (callee?.kind === "dyn") return DYN;
|
||||
if (callee?.kind === "func") return callee.ret;
|
||||
return lowerer.fnSigOf(arg.expression)?.returnType ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The instance a CALL of an implicit-any function-like names: each
|
||||
* bindable implicit param takes the call's WIDENED argument checker type
|
||||
* when it maps statically (DYN otherwise — today's slot), typed params
|
||||
@@ -2242,8 +2259,7 @@ export function implicitMethodCallInfersReturn(lowerer: Lowerer, call: ts.CallEx
|
||||
if ((lowerer.typeOf(param.name).flags & ts.TypeFlags.Any) === 0) bound = declared.type;
|
||||
const arg = call.arguments[i];
|
||||
if (arg && !ts.isSpreadElement(arg)) {
|
||||
const runtimeLocal = ts.isIdentifier(arg) ? lowerer.peekLocal(arg) : null;
|
||||
if (runtimeLocal?.type.kind === "dyn") {
|
||||
if (storedImplicitArgumentType(lowerer, arg)?.kind === "dyn") {
|
||||
bound = DYN;
|
||||
argTypes.set(sym, lowerer.checker.getUnknownType());
|
||||
shapes.push({ type: bound, mode: "required" });
|
||||
@@ -2663,6 +2679,7 @@ export function islandPrimitiveExit(lowerer: Lowerer, call: ts.CallExpression, r
|
||||
kind: "dynCall",
|
||||
callee: { kind: "varRef", localId: "f.0", type: DYN, loc },
|
||||
calleeName: "callback",
|
||||
receiver: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc },
|
||||
args: args.map((_, i) => ({ kind: "varRef", localId: `a${i}.0`, type: DYN, loc }) as IrExpr),
|
||||
type: DYN,
|
||||
loc,
|
||||
@@ -5842,15 +5859,20 @@ function lowerOptionalStringNumber(
|
||||
if (call.arguments.some((a) => ts.isSpreadElement(a))) {
|
||||
lowerer.unsupported("SC1090", call, "spread arguments in calls through 'unknown' values");
|
||||
}
|
||||
const receiverLocal = lowerer.declareHiddenLocal("%dynCallReceiver", DYN);
|
||||
const receiver: IrExpr = { kind: "varRef", localId: receiverLocal.id, type: DYN, loc };
|
||||
const member: IrExpr = {
|
||||
kind: "dynKeyGet",
|
||||
key: { kind: "strLit", value: access.name.text, type: STRING, loc: locOf(access) },
|
||||
value: recv,
|
||||
value: receiver,
|
||||
type: DYN,
|
||||
loc: locOf(access),
|
||||
};
|
||||
const args = call.arguments.map((a) => lowerer.lowerExprExpecting(a, DYN));
|
||||
return { kind: "dynCall", callee: member, calleeName: access.getText(), args, type: DYN, loc };
|
||||
return {
|
||||
kind: "seqExpr", stmts: [{ kind: "varDecl", localId: receiverLocal.id, init: recv, loc }],
|
||||
result: { kind: "dynCall", callee: member, receiver, calleeName: access.getText(), args, type: DYN, loc }, type: DYN, loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Prototype method names of the checked-dynamic tree-representable kinds (String, Array,
|
||||
@@ -5879,7 +5901,7 @@ const DYN_PROTO_METHOD_NAMES = new Set([
|
||||
* is-not-a-function where the kind's prototype lacks the name, and
|
||||
* fences LOUDLY on real-but-unimplemented pairs. */
|
||||
const DYN_DISPATCH_METHODS = new Set([
|
||||
"apply", "call",
|
||||
"apply", "bind", "call",
|
||||
"push", "pop", "shift", "unshift", "slice", "at",
|
||||
"indexOf", "lastIndexOf", "includes", "join", "concat", "reverse", "sort",
|
||||
"forEach", "map", "filter", "some", "every", "find", "findIndex",
|
||||
@@ -8976,6 +8998,13 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
|
||||
(member === "getOwnPropertyDescriptor" && call.arguments.length === 2)) {
|
||||
if (call.arguments.some((a) => ts.isSpreadElement(a))) return null;
|
||||
let target = tryLowerExpression(lowerer, call.arguments[0]!);
|
||||
// Error.cause owns a live data-property slot shared with its checked
|
||||
// view. Other native fields do not yet synchronize descriptor edits.
|
||||
const keyNode = call.arguments[1]!;
|
||||
if (target?.type.kind === "object" && target.type.className !== "%DOMException" && lowerer.errorHierarchyClassOf(target.type.className) &&
|
||||
ts.isStringLiteral(keyNode) && keyNode.text === "cause") {
|
||||
target = lowerer.coerceToExpected(target, DYN);
|
||||
}
|
||||
if (!target || (target.type.kind !== "dyn" && !isUnitType(target.type))) return null;
|
||||
if (target.type.kind !== "dyn") target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments[0]!) };
|
||||
const key = lowerer.lowerExprExpecting(call.arguments[1]!, DYN);
|
||||
@@ -10434,21 +10463,24 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I
|
||||
if (arrowBind !== null) return arrowBind;
|
||||
const arrowApply = lowerArrowFunctionApply(lowerer, call, access);
|
||||
if (arrowApply !== null) return arrowApply;
|
||||
// Function.prototype.apply/call/bind spelled through a FUNCTION
|
||||
// receiver: compiled functions are direct calls with no runtime
|
||||
// `this`/arguments object to re-route — name the working spelling
|
||||
// instead of a class-receiver hint (or an SC2020 recitation) no
|
||||
// function value can follow. Before the stdlib decline: these ARE
|
||||
// stdlib members (CallableFunction), but the pointed message is
|
||||
// the honest one.
|
||||
// Convertible native callables use the checked invocation boundary
|
||||
// for Function.prototype methods, including their call-time receiver.
|
||||
if (
|
||||
(name === "apply" || name === "call" || name === "bind") &&
|
||||
lowerer.checker.getCallSignatures(recvT).length > 0
|
||||
) {
|
||||
const value = tryLowerExpression(lowerer, access.expression);
|
||||
if (value?.type.kind === "func" && lowerer.dynConvertible(value.type) && !call.arguments.some(ts.isSpreadElement)) {
|
||||
const loc = locOf(call);
|
||||
return {
|
||||
kind: "dynInvoke", recv: { kind: "dynFrom", value, type: DYN, loc }, method: name,
|
||||
calleeName: access.getText(), args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc,
|
||||
};
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
`Function.prototype.${name} on a compiled function value (compiled calls are direct — no runtime 'this' or arguments object exists to re-route; spell the call directly: '${access.expression.getText()}(...)')`,
|
||||
`Function.prototype.${name} on a function whose signature cannot cross the checked-value boundary (call '${access.expression.getText()}(...)' directly)`,
|
||||
);
|
||||
}
|
||||
// STANDARD-LIBRARY generic members (Promise.then, Object.
|
||||
@@ -10573,6 +10605,13 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I
|
||||
: null;
|
||||
if (fieldType && (callableType !== null || fieldType.kind === "dyn")) {
|
||||
const target = lowerer.fieldTarget(access);
|
||||
if (!target) return null;
|
||||
const loc = locOf(call);
|
||||
const receiverLocal = lowerer.declareHiddenLocal("%callReceiver", target.obj.type);
|
||||
const init: IrStmt = { kind: "varDecl", localId: receiverLocal.id, init: target.obj, loc };
|
||||
target.obj = { kind: "varRef", localId: receiverLocal.id, type: receiverLocal.type, loc };
|
||||
const receiver = lowerer.coerceToExpected(target.obj, DYN);
|
||||
const finish = (result: IrExpr): IrExpr => ({ kind: "seqExpr", stmts: [init], result, type: result.type, loc });
|
||||
let callee = target ? lowerer.fieldGetExpr(target, locOf(access), access) : null;
|
||||
if (callee?.type.kind === "union" && callableType !== null) {
|
||||
const helper = lowerer.narrowedArmHelper(callee.type.unionId, callableType, locOf(access));
|
||||
@@ -10581,15 +10620,23 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I
|
||||
}
|
||||
}
|
||||
if (callee?.type.kind === "func") {
|
||||
if ((callee.type.rest === true && callee.type.restAbi === undefined || call.arguments.length > callee.type.params.length) &&
|
||||
isJsSourceFile(call.getSourceFile()) && lowerer.dynConvertible(callee.type)) {
|
||||
const boxed: IrExpr = { kind: "dynFrom", value: callee, type: DYN, loc };
|
||||
const spread = lowerSpreadArgsCall(lowerer, call, boxed, loc);
|
||||
if (spread?.kind === "dynCall") return finish({ ...spread, receiver });
|
||||
const args = call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN));
|
||||
return finish({ kind: "dynCall", callee: boxed, receiver, calleeName: access.getText(), args, type: DYN, loc });
|
||||
}
|
||||
const args = completeFuncValueArgs(lowerer, call, callee.type, locOf(call));
|
||||
return { kind: "callValue", callee, args, type: callee.type.ret, loc: locOf(call) };
|
||||
return finish({ kind: "callValue", callee, receiver, args, type: callee.type.ret, loc });
|
||||
}
|
||||
if (callee?.type.kind === "dyn") {
|
||||
if (call.arguments.some((a) => ts.isSpreadElement(a))) {
|
||||
lowerer.unsupported("SC1090", call, "spread arguments in calls through 'unknown' values");
|
||||
}
|
||||
const args = call.arguments.map((a) => lowerer.lowerExprExpecting(a, DYN));
|
||||
return { kind: "dynCall", callee, calleeName: access.getText(), args, type: DYN, loc: locOf(call) };
|
||||
return finish({ kind: "dynCall", callee, receiver, calleeName: access.getText(), args, type: DYN, loc });
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -10717,6 +10764,8 @@ function lowerUnionObjectDynFieldCall(
|
||||
const loc = locOf(call);
|
||||
const receiver = lowerer.coerceInto(access.expression, lowerer.lowerExpr(access.expression), receiverT);
|
||||
const key = `${receiverT.unionId}:dyn-field:${field}`;
|
||||
const receiverLocal = lowerer.declareHiddenLocal("%unionCallReceiver", receiverT);
|
||||
const receiverRef = varRef(receiverLocal.id, receiverT, loc);
|
||||
let helper = lowerer.unionCallHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = `%union.call.${lowerer.unionCallHelpers.size}`;
|
||||
@@ -10769,9 +10818,12 @@ function lowerUnionObjectDynFieldCall(
|
||||
const locals: IrLocal[] = params.map((param) => ({ id: param.localId, name: param.name, type: param.type, mutable: false }));
|
||||
lowerer.liftedFns.push({ name: helper, params, returnType: DYN, locals, body, loc });
|
||||
}
|
||||
const callee: IrExpr = { kind: "call", callee: helper, args: [receiver], type: DYN, loc };
|
||||
const callee: IrExpr = { kind: "call", callee: helper, args: [receiverRef], type: DYN, loc };
|
||||
const args = call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN));
|
||||
return { kind: "dynCall", callee, calleeName: access.getText(), args, type: DYN, loc };
|
||||
return {
|
||||
kind: "seqExpr", stmts: [{ kind: "varDecl", localId: receiverLocal.id, init: receiver, loc }],
|
||||
result: { kind: "dynCall", callee, receiver: lowerer.coerceToExpected(receiverRef, DYN), calleeName: access.getText(), args, type: DYN, loc }, type: DYN, loc,
|
||||
};
|
||||
}
|
||||
|
||||
function unionObjectMethodHelper(
|
||||
|
||||
@@ -363,6 +363,7 @@ export interface GenericClassInfo {
|
||||
{ name: "message", type: STRING },
|
||||
{ name: "%code", type: STRING },
|
||||
{ name: "%cause", type: DYN },
|
||||
{ name: "%causeEnumerable", type: BOOL },
|
||||
],
|
||||
loc,
|
||||
},
|
||||
@@ -2192,6 +2193,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
) {
|
||||
const assign = lhs;
|
||||
const name = assign.name.text;
|
||||
if (errorRooted && name === "cause") continue;
|
||||
// Later assignments to an already-declared field (own or
|
||||
// inherited) are writes, not declarations.
|
||||
if (fields.has(name) || hasAccessor(name)) continue;
|
||||
@@ -2295,6 +2297,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
// static layouts cannot represent. Named fence, at the first
|
||||
// assignment site.
|
||||
for (const p of instType ? lowerer.checker.getPropertiesOfType(instType) : []) {
|
||||
if (errorRooted && p.name === "cause") continue;
|
||||
if (fields.has(p.name) || methods.has(p.name)) continue;
|
||||
if (hasAccessor(p.name)) continue;
|
||||
if (base && (base.fields.has(p.name) || lowerer.findMethodOn(base, p.name))) continue;
|
||||
|
||||
@@ -44,11 +44,13 @@ import { lowerAbstractEquality } from "./abstract-equality.js";
|
||||
import { coerceStringSearchValue, defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "./optional-arguments.js";
|
||||
import { recordTextCodecClass } from "../../ir/ir.js";
|
||||
import { classSymbolKeyOf } from "./symbol-fields.js";
|
||||
import { lowerClassMethodValue } from "./class-method-values.js";
|
||||
|
||||
/** An assignable `obj.field` target — a class field, a record field, or a
|
||||
* class ACCESSOR property (reads become getter calls, writes setter calls;
|
||||
* fieldType is the property's one type). */
|
||||
export type FieldTarget =
|
||||
| { container: "errorCause"; obj: IrExpr; field: "cause"; fieldType: IrType }
|
||||
| { container: "class"; obj: IrExpr; className: string; field: string; fieldType: IrType }
|
||||
| { container: "record"; obj: IrExpr; shapeId: string; field: string; fieldType: IrType }
|
||||
// An UNDECLARED key of an index-signature shape in dot spelling
|
||||
@@ -1858,6 +1860,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
if (
|
||||
(isJsSourceFile(expr.getSourceFile()) &&
|
||||
(lowerer.mapTypeOf(lowerer.typeOf(expr.expression)) === null ||
|
||||
(lowerer.typeOf(expr.expression).flags & ts.TypeFlags.Never) !== 0 ||
|
||||
// A never-tainted receiver type maps (never rides as f64) but
|
||||
// its VALUE lowered checked-dynamic — same dyn read.
|
||||
neverTaintedJsType(lowerer, expr.expression, lowerer.typeOf(expr.expression)))) ||
|
||||
@@ -2457,6 +2460,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
// instead of misreading the payload (the dyn boundary's usual stance).
|
||||
// Object/array narrowings stay dyn-typed and keep their fences.
|
||||
if (expr.type.kind === "dyn") {
|
||||
// A null-initialized JS field can later receive an object even when
|
||||
// checker flow calls its non-null branch never. The live checked
|
||||
// value remains authoritative; never is not a numeric guard.
|
||||
if (isJsSourceFile(node.getSourceFile()) && (lowerer.typeOf(node).flags & ts.TypeFlags.Never) !== 0) return expr;
|
||||
const narrowed = lowerer.mapTypeOf(lowerer.typeOf(node));
|
||||
if (
|
||||
narrowed &&
|
||||
@@ -9091,11 +9098,8 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
return { kind: "call", callee: helper, args: [recv], type: recordT, loc };
|
||||
}
|
||||
|
||||
/** Field read `obj.f` on class-instance and record receivers, through the
|
||||
* shared FieldTarget union (fieldGet / recordGet). Bound method references
|
||||
* on classes are rejected specifically; func-typed record fields are
|
||||
* ordinary closure values, so bare references to them work (unlike class
|
||||
* methods, which have no bound-value form). */
|
||||
/** Field reads use the shared FieldTarget union. Native method reads select
|
||||
* an interned unbound callable; invocation supplies its receiver separately. */
|
||||
export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null {
|
||||
const target = lowerer.fieldTarget(expr);
|
||||
if (target) return lowerer.fieldGetExpr(target, locOf(expr), expr);
|
||||
@@ -9112,7 +9116,8 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
(lowerer.findMethodOn(lowerer.classes.get(receiverIr.className) ?? null, expr.name.text) ||
|
||||
findGenericMethodOn(lowerer, lowerer.classes.get(receiverIr.className) ?? null, expr.name.text))
|
||||
) {
|
||||
lowerer.unsupported("SC1090", expr, `bound method references (call '${expr.name.text}' directly)`);
|
||||
const info = lowerer.classes.get(receiverIr.className);
|
||||
if (info) return lowerClassMethodValue(lowerer, expr, info);
|
||||
}
|
||||
// An object-literal GENERIC method as a VALUE (`o.m` — the member is
|
||||
// excluded from the record shape): the pinned-value rule verbatim when
|
||||
@@ -9454,6 +9459,13 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
if (access.name.text === "cause" && receiverIr.className !== "%DOMException") {
|
||||
let root = info;
|
||||
while (root.base) root = root.base;
|
||||
if (root.def.name === "%Error") {
|
||||
return { container: "errorCause", obj: lowerObjectReceiver(), field: "cause", fieldType: DYN };
|
||||
}
|
||||
}
|
||||
if (fieldType) {
|
||||
const obj = lowerObjectReceiver();
|
||||
return { container: "class", obj, className: receiverIr.className, field: access.name.text, fieldType };
|
||||
@@ -9651,6 +9663,9 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
* tsc-clean (the property types as the setter's param), but Node yields
|
||||
* undefined, which these property types cannot represent. */
|
||||
export function fieldGetExpr(lowerer: Lowerer, target: FieldTarget, loc: SrcLoc, blame: ts.Node): IrExpr {
|
||||
if (target.container === "errorCause") {
|
||||
return { kind: "libCall", fn: "error.cause", args: [target.obj], type: DYN, loc };
|
||||
}
|
||||
// A record-shaped CHECKER target whose receiver VALUE lives in the checked-dynamic tree
|
||||
// (a JS file-scope object-literal global): the checked-dynamic keyed
|
||||
// read — dynKeyGet (a missing key answers the dyn undefined, exactly
|
||||
@@ -9762,6 +9777,9 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
* call). A write to a getter-only property never gets here in a clean
|
||||
* program (tsc's TS2540 is the fence); the rejection is the backstop. */
|
||||
export function fieldSetStmt(lowerer: Lowerer, target: FieldTarget, value: IrExpr, loc: SrcLoc, blame: ts.Node): IrStmt {
|
||||
if (target.container === "errorCause") {
|
||||
return { kind: "exprStmt", expr: { kind: "libCall", fn: "error.setCause", args: [target.obj, value], type: VOID, loc }, loc };
|
||||
}
|
||||
// A record-shaped CHECKER target whose receiver VALUE lives in the checked-dynamic tree:
|
||||
// the checked-dynamic keyed write — dyn.keySet (later writes win,
|
||||
// insertion order; Node's TypeErrors on non-object receivers), the
|
||||
|
||||
@@ -17,7 +17,7 @@ import { BOOL, DYN, F64, IrClassDef, IrExpr, IrFunction, IrGlobal, IrRecordShape
|
||||
import { ENTRY_NAME, PoisonError, boundIdentifiersOf, dynFallbackType, dynUndefinedExpr, importCallHandleType, newFnCtx, staticImportNamespaceType, uncheckedOverloadHandleCall } from "./lowerer.js";
|
||||
import { builtinMemberRequireDecl, builtinNamespaceDestructureModuleOf, createRequireBindingDecl, createRequireNamespaceDecl, createRequireProgramModuleDecl, createRequireSpecOf, isPromisifyCall, registerBuiltinCallableAlias } from "./lower-builtins.js";
|
||||
import { bindingContextualGenericFnNodeOf, bindingGenericFnAliasInfoOf, bindingGenericFnInfoOf, bindingGenericFnNodeOf, bindingNeverReassigned, deadUnmappableBinding, implicitLocalFnInfoOf, implicitLocalFnNodeOf, nullishGenericBindingUnitOf, registerOverloadedCallableAlias } from "./lower-calls.js";
|
||||
import { isVarDeclared, numericIteratorSourceOf, provenanceElidedConstDecl } from "./lower-stmts.js";
|
||||
import { hasJsTypeAnnotation, isVarDeclared, numericIteratorSourceOf, provenanceElidedConstDecl } from "./lower-stmts.js";
|
||||
import { streamClassAliasDecl } from "./lower-stream.js";
|
||||
import { stdlibGlobalAliasDecl, stdlibGlobalAliasNameOf } from "./surfaces.js";
|
||||
import { collectNamespaceStmt, nsPathPrefix, trapDeclRootOf } from "./lower-namespaces.js";
|
||||
@@ -822,6 +822,17 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (e.kind === ts.SyntaxKind.NullKeyword) return true;
|
||||
if (ts.isIdentifier(e) && e.text === "undefined") return true;
|
||||
if (ts.isConditionalExpression(e)) {
|
||||
return jsDynHoldableInitializer(lowerer, e.whenTrue) && jsDynHoldableInitializer(lowerer, e.whenFalse);
|
||||
}
|
||||
if (ts.isBinaryExpression(e) && (e.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken ||
|
||||
e.operatorToken.kind === ts.SyntaxKind.BarBarToken || e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)) {
|
||||
return jsDynHoldableInitializer(lowerer, e.left) && jsDynHoldableInitializer(lowerer, e.right);
|
||||
}
|
||||
if (ts.isIdentifier(e) || ts.isArrowFunction(e) || ts.isFunctionExpression(e)) {
|
||||
const type = lowerer.mapTypeOf(lowerer.typeOf(e)) ?? dynFallbackType(lowerer, e, lowerer.typeOf(e));
|
||||
if (type?.kind === "func" && canBoxFuncIntoDyn(type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) return true;
|
||||
}
|
||||
if (
|
||||
(ts.isCallExpression(e) || ts.isPropertyAccessExpression(e) || ts.isIdentifier(e)) &&
|
||||
(lowerer.typeOf(e).flags & ts.TypeFlags.Any) !== 0
|
||||
@@ -1515,7 +1526,7 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts
|
||||
if (
|
||||
isJsSourceFile(sf) && !lowerer.mapTypeOf(lowerer.typeOf(nameNode)) &&
|
||||
ts.isIdentifier(decl.name) && nameNode === decl.name && decl.initializer &&
|
||||
(ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer))
|
||||
jsDynHoldableInitializer(lowerer, decl.initializer)
|
||||
) {
|
||||
const fallback = dynFallbackType(lowerer, nameNode, lowerer.typeOf(nameNode));
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
|
||||
@@ -1557,6 +1568,14 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts
|
||||
(uncheckedOverloadHandleCall(lowerer, decl.initializer) ? JSVAL : null) : null)
|
||||
: null;
|
||||
let type = handleT ?? lowerer.irTypeOf(nameNode);
|
||||
// JavaScript bind inference can leave an artificial tuple-rest
|
||||
// signature. Keep the returned callable's runtime arity intact.
|
||||
if (isJsSourceFile(sf) && type.kind === "func" && decl.initializer &&
|
||||
ts.isCallExpression(decl.initializer) && ts.isPropertyAccessExpression(decl.initializer.expression) &&
|
||||
decl.initializer.expression.name.text === "bind" && lowerer.isStdlibMember(decl.initializer.expression) &&
|
||||
decl.type === undefined && !hasJsTypeAnnotation(decl)) {
|
||||
type = DYN;
|
||||
}
|
||||
// An evolving-`any` array's DERIVED file-scope binding under
|
||||
// --dynamic (`const kept = fns.filter(...)` where `fns`
|
||||
// registered array<jsval> at its `any[]` declaration): the
|
||||
@@ -1622,6 +1641,7 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts
|
||||
isJsSourceFile(sf) &&
|
||||
type.kind === "func" &&
|
||||
type.ret.kind === "record" &&
|
||||
lowerer.dynConvertible(type.ret) &&
|
||||
decl.initializer !== undefined &&
|
||||
(ts.isFunctionExpression(decl.initializer) || ts.isArrowFunction(decl.initializer)) &&
|
||||
decl.initializer.type === undefined &&
|
||||
|
||||
@@ -3792,7 +3792,11 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL
|
||||
) {
|
||||
type = DYN;
|
||||
}
|
||||
const preservesObjectIdentity = init.type.kind === "dyn" && inferredObjectType?.kind === "record" &&
|
||||
const preservesObjectIdentity = init.type.kind === "dyn" &&
|
||||
(inferredObjectType?.kind === "record" ||
|
||||
(isJsSourceFile(decl.getSourceFile()) && inferredObjectType?.kind === "func") ||
|
||||
(isJsSourceFile(decl.getSourceFile()) && inferredObjectType !== null &&
|
||||
(isUnitType(inferredObjectType) || inferredObjectType.kind === "union" && lowerer.unions.get(inferredObjectType.unionId)?.arms.every(isUnitType)))) &&
|
||||
!decl.type && !hasJsTypeAnnotation(decl);
|
||||
if (expandsObject || preservesObjectIdentity) {
|
||||
type = DYN;
|
||||
@@ -4148,7 +4152,7 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL
|
||||
return expands;
|
||||
}
|
||||
|
||||
function hasJsTypeAnnotation(decl: ts.VariableDeclaration): boolean {
|
||||
export function hasJsTypeAnnotation(decl: ts.VariableDeclaration): boolean {
|
||||
const statement = decl.parent?.parent;
|
||||
return !!statement && /@type\b/.test(decl.getSourceFile().text.slice(statement.pos, decl.getStart()));
|
||||
}
|
||||
@@ -4391,6 +4395,12 @@ function lowerBranchSwitch(
|
||||
lowerer.unsupported("SC1090", expr, "'delete' of non-property expressions");
|
||||
}
|
||||
fenceNodeModuleMutation(lowerer, target, "delete");
|
||||
if (ts.isPropertyAccessExpression(target) && target.name.text === "cause") {
|
||||
const field = lowerer.fieldTarget(target);
|
||||
if (field?.container === "errorCause") {
|
||||
return { kind: "exprStmt", expr: { kind: "libCall", fn: "error.deleteCause", args: [field.obj], type: VOID, loc }, loc };
|
||||
}
|
||||
}
|
||||
const globalKey = ts.isElementAccessExpression(target) ? globalSymbolKey(lowerer, target.expression, target.argumentExpression) : null;
|
||||
if (globalKey) return { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.globalSymbolDelete", args: [globalKey], type: VOID, loc }, loc };
|
||||
const lowerKey = (): IrExpr => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { FrontendServices } from "../services.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { ClassDynamicDispatch } from "./class-dynamic-dispatch.js";
|
||||
import { finalizeClassMethodValues } from "./class-method-values.js";
|
||||
/* AST + checker → IR.
|
||||
*
|
||||
* Invariants:
|
||||
@@ -3690,6 +3691,7 @@ export class Lowerer {
|
||||
/** Final retention, pruning, and module assembly shared by ordinary emit
|
||||
* and the retained reachability worklist. */
|
||||
finishModule(functions: IrFunction[]): LowerResult {
|
||||
finalizeClassMethodValues(this, functions);
|
||||
|
||||
// Globals typed by a class that never REGISTERED (a JS class whose
|
||||
// collection fenced — Symbol-keyed fields, an unsupported base): the
|
||||
@@ -6582,6 +6584,7 @@ export class Lowerer {
|
||||
{
|
||||
kind: "callValue",
|
||||
callee: { kind: "varRef", localId: "f.0", type: fromT, loc },
|
||||
receiver: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc },
|
||||
args: [],
|
||||
type: fromT.ret,
|
||||
loc,
|
||||
@@ -6764,6 +6767,7 @@ export class Lowerer {
|
||||
const call: IrExpr = {
|
||||
kind: "callValue",
|
||||
callee: { kind: "varRef", localId: "f.0", type: fromT, loc },
|
||||
receiver: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc },
|
||||
args,
|
||||
type: fromT.ret,
|
||||
loc,
|
||||
@@ -6918,6 +6922,7 @@ export class Lowerer {
|
||||
init: {
|
||||
kind: "callValue",
|
||||
callee: { kind: "varRef", localId: "f.0", type: fromT, loc },
|
||||
receiver: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc },
|
||||
args: params.map((p): IrExpr => ({ kind: "varRef", localId: p.localId, type: p.type, loc })),
|
||||
type: fromT.ret,
|
||||
loc,
|
||||
|
||||
@@ -3786,6 +3786,9 @@ export type IrLibFn =
|
||||
* when absent); hasCause distinguishes absence from present undefined. */
|
||||
| "error.cause"
|
||||
| "error.hasCause"
|
||||
/** Assignment borrows both operands and retains the new cause. */
|
||||
| "error.setCause"
|
||||
| "error.deleteCause"
|
||||
/** The compiler-resolved Node-parity throw for always-throwing lowered
|
||||
* arms (ERR_INVALID_THIS receivers, ERR_MISSING_ARGS arity ladders,
|
||||
* the symbol-to-string TypeError): args are [error-kind f64 (the
|
||||
@@ -5041,8 +5044,10 @@ export type IrExpr =
|
||||
| { kind: "closure"; fnName: string; captures: string[]; type: IrType; loc: SrcLoc }
|
||||
/** Indirect call of a func-typed value. Args follow `call`'s convention
|
||||
* (callee owns its params, callers pass +1). The callee expression is an
|
||||
* ordinary owned temp, released at statement end. */
|
||||
| { kind: "callValue"; callee: IrExpr; args: IrExpr[]; type: IrType; loc: SrcLoc }
|
||||
* ordinary owned temp, released at statement end. receiver supplies the
|
||||
* call-time this value; absence means undefined. Evaluate callee, receiver,
|
||||
* then args, and restore the ambient receiver before unwinding. */
|
||||
| { kind: "callValue"; callee: IrExpr; receiver?: IrExpr; args: IrExpr[]; type: IrType; loc: SrcLoc }
|
||||
/** The currently-executing closure, as a value (+1). Valid only inside a
|
||||
* lifted function. Exists so a named nested function can recurse on itself
|
||||
* WITHOUT capturing its own binding — a box holding its own closure would
|
||||
@@ -5281,7 +5286,7 @@ export type IrExpr =
|
||||
* the nullish text spells), evaluated and flattened left-to-right (JS's
|
||||
* ArgumentListEvaluation). The emitters build one fresh argument array
|
||||
* and apply through it. */
|
||||
| { kind: "dynCall"; callee: IrExpr; calleeName: string; args: IrExpr[]; spreads?: { arg: number; what: string }[]; type: IrType; loc: SrcLoc }
|
||||
| { kind: "dynCall"; callee: IrExpr; receiver?: IrExpr; calleeName: string; args: IrExpr[]; spreads?: { arg: number; what: string }[]; type: IrType; loc: SrcLoc }
|
||||
/** Prototype-method DISPATCH on a dyn receiver — `recv.m(...)` where `m`
|
||||
* is a name a dyn-representable prototype declares (Array/String/
|
||||
* Function shared names: push, slice, join, forEach, map, apply, ...),
|
||||
@@ -7729,6 +7734,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
// Error messages use the same coercion protocol before installing cause.
|
||||
"error.newOptions",
|
||||
"error.ctorOptions",
|
||||
"error.cause",
|
||||
"error.setCause",
|
||||
"error.deleteCause",
|
||||
"dyn.objectTag",
|
||||
// Numeric coercion runs user valueOf/toString — throws propagate.
|
||||
"dyn.toNumberCoerce",
|
||||
|
||||
@@ -101,7 +101,7 @@ export function everyExprChild(node: IrExpr, expr: (expr: IrExpr) => boolean, st
|
||||
case "closure":
|
||||
return true;
|
||||
case "callValue":
|
||||
return expr(node.callee) && node.args.every((child) => expr(child));
|
||||
return expr(node.callee) && (node.receiver === undefined || expr(node.receiver)) && node.args.every((child) => expr(child));
|
||||
case "selfRef":
|
||||
return true;
|
||||
case "yieldExpr":
|
||||
@@ -149,7 +149,7 @@ export function everyExprChild(node: IrExpr, expr: (expr: IrExpr) => boolean, st
|
||||
case "dynFromJsval":
|
||||
return expr(node.value);
|
||||
case "dynCall":
|
||||
return expr(node.callee) && node.args.every((child) => expr(child));
|
||||
return expr(node.callee) && (node.receiver === undefined || expr(node.receiver)) && node.args.every((child) => expr(child));
|
||||
case "dynInvoke":
|
||||
return expr(node.recv) && node.args.every((child) => expr(child));
|
||||
case "dynArrLit":
|
||||
@@ -363,7 +363,7 @@ export function mapExprChildren(node: IrExpr, expr: (expr: IrExpr) => IrExpr, st
|
||||
case "closure":
|
||||
return node;
|
||||
case "callValue":
|
||||
return { ...node, callee: expr(node.callee), args: node.args.map((child) => expr(child)) };
|
||||
return { ...node, callee: expr(node.callee), ...(node.receiver === undefined ? {} : { receiver: expr(node.receiver) }), args: node.args.map((child) => expr(child)) };
|
||||
case "selfRef":
|
||||
return node;
|
||||
case "yieldExpr":
|
||||
@@ -411,7 +411,7 @@ export function mapExprChildren(node: IrExpr, expr: (expr: IrExpr) => IrExpr, st
|
||||
case "dynFromJsval":
|
||||
return { ...node, value: expr(node.value) };
|
||||
case "dynCall":
|
||||
return { ...node, callee: expr(node.callee), args: node.args.map((child) => expr(child)) };
|
||||
return { ...node, callee: expr(node.callee), ...(node.receiver === undefined ? {} : { receiver: expr(node.receiver) }), args: node.args.map((child) => expr(child)) };
|
||||
case "dynInvoke":
|
||||
return { ...node, recv: expr(node.recv), args: node.args.map((child) => expr(child)) };
|
||||
case "dynArrLit":
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { BOOL, F64, NULL_T, STRING, UNDEFINED_T, VOID, arrayOf, mapOf, setOf, type IrExpr, type IrModule, type IrType, type IrUnionDef } from "./ir.js";
|
||||
import { BOOL, DYN, F64, NULL_T, STRING, UNDEFINED_T, VOID, arrayOf, mapOf, setOf, type IrExpr, type IrModule, type IrType, type IrUnionDef } from "./ir.js";
|
||||
import { deserializeModule, serializeModule } from "./serialize.js";
|
||||
import { validateModule } from "./validate.js";
|
||||
|
||||
@@ -47,6 +47,23 @@ function expressionModule(expr: IrExpr, unions: IrUnionDef[]): IrModule {
|
||||
};
|
||||
}
|
||||
|
||||
test.each(["callValue", "dynCall"] as const)("%s requires a checked-value receiver and preserves it in serialization", (kind) => {
|
||||
const funcType: IrType = { kind: "func", params: [], ret: DYN };
|
||||
const closure: IrExpr = { kind: "closure", fnName: "callback", captures: [], type: funcType, loc };
|
||||
const receiver: IrExpr = { kind: "dynFrom", value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, type: DYN, loc };
|
||||
const call: IrExpr = kind === "callValue"
|
||||
? { kind, callee: closure, receiver, args: [], type: DYN, loc }
|
||||
: { kind, callee: { kind: "dynFrom", value: closure, type: DYN, loc }, receiver, calleeName: "callback", args: [], type: DYN, loc };
|
||||
const mod = expressionModule(call, []);
|
||||
mod.functions.push({ name: "callback", params: [], locals: [], returnType: DYN, body: [{ kind: "return", value: receiver, loc }], loc });
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
expect(deserializeModule(serializeModule(mod))).toEqual(mod);
|
||||
call.receiver = { kind: "numLit", value: 1, type: F64, loc };
|
||||
expect(validateModule(mod).some((error) => error.message.includes(`${kind} receiver`))).toBe(true);
|
||||
call.receiver = { kind: "varRef", localId: "missing", type: DYN, loc };
|
||||
expect(validateModule(mod).some((error) => error.message.includes("missing"))).toBe(true);
|
||||
});
|
||||
|
||||
function optionalUnionModule(arms: IrType[] = [BOOL, F64, UNDEFINED_T]): IrModule {
|
||||
const type: IrType = { kind: "union", unionId: "receiver" };
|
||||
const tag = arms.findIndex((arm) => arm.kind === "f64");
|
||||
|
||||
@@ -1068,6 +1068,8 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
|
||||
"error.ctorOptions": { argTypes: [null, DYN, DYN], result: VOID },
|
||||
"error.cause": { argTypes: [null], result: DYN },
|
||||
"error.hasCause": { argTypes: [null], result: BOOL },
|
||||
"error.setCause": { argTypes: [null, DYN], result: VOID },
|
||||
"error.deleteCause": { argTypes: [null], result: VOID },
|
||||
"error.nodeThrow": { argTypes: [F64, STRING, STRING], result: VOID },
|
||||
"dyn.toStringCoerce": { argTypes: [DYN], result: STRING },
|
||||
"dyn.toNumberCoerce": { argTypes: [DYN], result: F64 },
|
||||
@@ -3180,6 +3182,10 @@ function validateFunction(
|
||||
}
|
||||
case "callValue": {
|
||||
checkExpr(e.callee);
|
||||
if (e.receiver !== undefined) {
|
||||
checkExpr(e.receiver);
|
||||
expectType(e.receiver, DYN, "callValue receiver");
|
||||
}
|
||||
for (const a of e.args) checkExpr(a);
|
||||
if (e.callee.type.kind !== "func") {
|
||||
err(`callValue callee is ${e.callee.type.kind}, not func`, e.loc);
|
||||
@@ -3630,6 +3636,10 @@ function validateFunction(
|
||||
}
|
||||
case "dynCall": {
|
||||
checkExpr(e.callee);
|
||||
if (e.receiver !== undefined) {
|
||||
checkExpr(e.receiver);
|
||||
expectType(e.receiver, DYN, "dynCall receiver");
|
||||
}
|
||||
expectType(e.callee, DYN, "dynCall callee");
|
||||
if (e.type.kind !== "dyn") err(`dynCall must be dyn-typed, got ${e.type.kind}`, e.loc);
|
||||
for (const a of e.args) {
|
||||
@@ -5205,7 +5215,7 @@ function validateFunction(
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (e.fn === "error.cause" || e.fn === "error.hasCause") {
|
||||
if (e.fn === "error.cause" || e.fn === "error.hasCause" || e.fn === "error.setCause" || e.fn === "error.deleteCause") {
|
||||
const recv = e.args[0];
|
||||
let cls = recv?.type.kind === "object" ? classes.get(recv.type.className) : undefined;
|
||||
while (cls?.base) cls = classes.get(cls.base);
|
||||
|
||||
@@ -10758,6 +10758,43 @@
|
||||
"<repo>/tests/fixtures/node-types/url-getters.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/native-factory-records.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/native-factory-records.js"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/native-method-values.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/native-method-values.js"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/native-method-values.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/native-method-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/error-cause-writes.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/error-cause-writes.js"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/renderer-null-state.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/renderer-null-state.js"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/module-fallback-functions/main.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/module-fallback-functions/platform.js",
|
||||
"<repo>/tests/corpus/module-fallback-functions/main.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,7 +458,8 @@ static ScrDyn *scr_dyn_invoke_impl(
|
||||
scr_dyn_this_pop();
|
||||
return r;
|
||||
}
|
||||
if (dyn_name_is(method, "bind") || dyn_name_is(method, "toString")) {
|
||||
if (dyn_name_is(method, "bind")) return scr_dyn_bind(recv, args, argc);
|
||||
if (dyn_name_is(method, "toString")) {
|
||||
dyn_throw_unsupported("Function", method);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -1206,6 +1206,44 @@ ScrDyn *scr_dyn_apply(const ScrDyn *d, const ScrDyn *args, const char *what) {
|
||||
return scr_dyn_call(d, args->v.arr.items, args->v.arr.len, what);
|
||||
}
|
||||
|
||||
static ScrDyn *scr_dyn_bound_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
|
||||
ScrDyn *target = scr_box_get_ref(closure->caps[0]);
|
||||
ScrDyn *receiver = scr_box_get_ref(closure->caps[1]);
|
||||
ScrDyn *bound = scr_box_get_ref(closure->caps[2]);
|
||||
ScrDyn *all = scr_dyn_new_arr();
|
||||
for (size_t i = 0; i < bound->v.arr.len; i++) scr_dyn_arr_push(all, scr_dyn_retain(bound->v.arr.items[i]));
|
||||
for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(all, scr_dyn_retain(args[i]));
|
||||
scr_dyn_this_push_dyn(receiver);
|
||||
ScrDyn *result = scr_dyn_apply(target, all, "bound function");
|
||||
scr_dyn_this_pop();
|
||||
scr_dyn_release(all);
|
||||
scr_dyn_release(bound);
|
||||
scr_dyn_release(receiver);
|
||||
scr_dyn_release(target);
|
||||
return result;
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_bind(ScrDyn *target, ScrDyn *const *args, size_t argc) {
|
||||
ScrDyn *bound = scr_dyn_new_arr();
|
||||
for (size_t i = 1; i < argc; i++) scr_dyn_arr_push(bound, scr_dyn_retain(args[i]));
|
||||
ScrStr *prefix = scr_str_new("bound ", 6);
|
||||
const char *original = target->v.fn.name ? target->v.fn.name : "";
|
||||
ScrStr *suffix = scr_str_new(original, strlen(original));
|
||||
ScrStr *name = scr_str_concat(prefix, suffix);
|
||||
scr_str_release(prefix);
|
||||
scr_str_release(suffix);
|
||||
ScrClosure *closure = scr_closure_new(NULL, 4);
|
||||
for (size_t i = 0; i < 3; i++) closure->caps[i] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL);
|
||||
closure->caps[3] = scr_box_new(SCR_BOX_STR);
|
||||
scr_box_set_ref(closure->caps[0], scr_dyn_retain(target));
|
||||
scr_box_set_ref(closure->caps[1], scr_dyn_retain(argc ? args[0] : scr_dyn_undefined()));
|
||||
scr_box_set_ref(closure->caps[2], bound);
|
||||
scr_box_set_ref(closure->caps[3], name);
|
||||
size_t bound_count = argc ? argc - 1 : 0;
|
||||
uint32_t arity = bound_count < target->v.fn.arity ? target->v.fn.arity - (uint32_t)bound_count : 0;
|
||||
return scr_dyn_new_func(closure, &scr_dyn_bound_call, arity, "%bound", name->data);
|
||||
}
|
||||
|
||||
/* ── native handles in the checked-dynamic tree (SCR_DYN_HANDLE) ───────────────────────
|
||||
* Per-tag ops stamped by the owning units at main() (scr_http_dyn_install
|
||||
* / scr_net_dyn_install — the scr_net_install hook story), so this
|
||||
@@ -1611,7 +1649,7 @@ void scr_dyn_this_push_dyn(const ScrDyn *v) {
|
||||
ScrDynThisEnt *e = scr_dyn_this_grow();
|
||||
e->ptr = NULL;
|
||||
e->tag = 0;
|
||||
e->dv = scr_dyn_retain((ScrDyn *)v);
|
||||
e->dv = v ? scr_dyn_retain((ScrDyn *)v) : NULL;
|
||||
}
|
||||
|
||||
void scr_dyn_this_pop(void) {
|
||||
@@ -1685,6 +1723,7 @@ static bool scr_dyn_property_same_value(const ScrDyn *a, const ScrDyn *b) {
|
||||
static void scr_dyn_descriptor_fields_drop(ScrDyn **fields) {
|
||||
for (size_t i = 0; i < 6; i++) scr_dyn_release(fields[i]);
|
||||
}
|
||||
static void scr_error_sync_cause(ScrDyn *view, const ScrStr *key);
|
||||
|
||||
ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) {
|
||||
if (target->kind == SCR_DYN_PROXY || descriptor->kind == SCR_DYN_PROXY) {
|
||||
@@ -1795,6 +1834,7 @@ ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor)
|
||||
entry->configurable = configurable;
|
||||
}
|
||||
scr_dyn_descriptor_fields_drop(fields);
|
||||
scr_error_sync_cause(target, name);
|
||||
scr_str_release(name);
|
||||
return scr_dyn_retain(target);
|
||||
}
|
||||
@@ -2110,7 +2150,10 @@ ScrDyn *scr_dyn_from_error(const ScrError *e) {
|
||||
scr_dyn_obj_set(d, "name", 4, scr_dyn_new_str(e->name));
|
||||
scr_dyn_obj_set(d, "message", 7, scr_dyn_new_str(e->message));
|
||||
if (e->code) scr_dyn_obj_set(d, "code", 4, scr_dyn_new_str(e->code));
|
||||
if (e->error_cause) scr_dyn_obj_set(d, "cause", 5, scr_dyn_retain(e->error_cause));
|
||||
if (e->error_cause) {
|
||||
scr_dyn_obj_set(d, "cause", 5, scr_dyn_retain(e->error_cause));
|
||||
d->v.obj.entries[d->v.obj.len - 1].enumerable = e->cause_enumerable;
|
||||
}
|
||||
/* DOMException: `code` is the WebIDL legacy NUMBER (never the errno
|
||||
* string slot), and the options form's cause crosses as itself. */
|
||||
if (e->vt == &scr_error_vts[SCR_ERR_DOMEX]) {
|
||||
@@ -2173,6 +2216,10 @@ ScrError *scr_error_from_dyn(const ScrDyn *d) {
|
||||
if (cause) {
|
||||
scr_error_install_cause_drop(&scr_error_cause_drop_impl);
|
||||
e->error_cause = scr_dyn_retain(cause);
|
||||
for (size_t i = 0; i < d->v.obj.len; i++) {
|
||||
const ScrDynEntry *entry = &d->v.obj.entries[i];
|
||||
if (entry->key_len == 5 && memcmp(entry->key, "cause", 5) == 0) e->cause_enumerable = entry->enumerable;
|
||||
}
|
||||
}
|
||||
scr_errdyn_put(e, (ScrDyn *)d);
|
||||
return e;
|
||||
@@ -2205,6 +2252,22 @@ void scr_errdyn_put(ScrError *e, ScrDyn *d) {
|
||||
scr_errdyn_n++;
|
||||
}
|
||||
|
||||
/* Error.cause has one live value even when the Error already crossed a
|
||||
* checked-value boundary. Reflective data-property edits update its slot. */
|
||||
static void scr_error_sync_cause(ScrDyn *view, const ScrStr *key) {
|
||||
if (key->len != 5 || memcmp(key->data, "cause", 5) != 0) return;
|
||||
ScrDynEntry *entry = scr_dyn_entry(view, key);
|
||||
for (size_t i = 0; i < scr_errdyn_n; i++) {
|
||||
if (scr_errdyn_cache[i].dyn != view) continue;
|
||||
ScrError *e = scr_errdyn_cache[i].err;
|
||||
if (e->vt == &scr_error_vts[SCR_ERR_DOMEX]) continue;
|
||||
ScrDyn *replacement = entry ? scr_dyn_retain(entry->value) : NULL;
|
||||
scr_error_install_cause_drop(&scr_error_cause_drop_impl);
|
||||
scr_dyn_release(e->error_cause);
|
||||
e->error_cause = replacement;
|
||||
e->cause_enumerable = entry && entry->enumerable;
|
||||
}
|
||||
}
|
||||
|
||||
/* Receiver-kind-dispatched toString() on a checked-dynamic value (the
|
||||
* dyn method surface — a stream's 'data'/for-await chunk is the common
|
||||
@@ -2644,6 +2707,7 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
|
||||
}
|
||||
}
|
||||
scr_dyn_obj_set(recv, key->data, key->len, scr_dyn_retain(value));
|
||||
scr_error_sync_cause(recv, key);
|
||||
return;
|
||||
}
|
||||
if (recv->kind == SCR_DYN_ARR) {
|
||||
@@ -3443,7 +3507,14 @@ static void scr_json_delete_member(ScrDyn *object, const ScrStr *key) {
|
||||
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) {
|
||||
ScrDynEntry *entry = scr_dyn_entry(recv, key);
|
||||
if (entry && !entry->configurable) {
|
||||
static const char msg[] = "Cannot delete property";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
|
||||
return;
|
||||
}
|
||||
scr_json_delete_member(recv, key);
|
||||
scr_error_sync_cause(recv, key);
|
||||
return;
|
||||
}
|
||||
if (recv->kind == SCR_DYN_UNDEF || recv->kind == SCR_DYN_NULL) {
|
||||
@@ -4488,9 +4559,40 @@ ScrError *scr_error_new_options(int kind, const ScrDyn *message, const ScrDyn *o
|
||||
}
|
||||
|
||||
ScrDyn *scr_error_cause(ScrError *e) {
|
||||
for (size_t i = 0; i < scr_errdyn_n; i++) {
|
||||
if (scr_errdyn_cache[i].err == e) return scr_dyn_obj_read(scr_errdyn_cache[i].dyn, "cause", 5);
|
||||
}
|
||||
return e->error_cause ? scr_dyn_retain(e->error_cause) : scr_dyn_undefined();
|
||||
}
|
||||
|
||||
void scr_error_set_cause(ScrError *e, ScrDyn *value) {
|
||||
for (size_t i = 0; i < scr_errdyn_n; i++) {
|
||||
if (scr_errdyn_cache[i].err != e) continue;
|
||||
ScrStr *key = scr_str_new("cause", 5);
|
||||
scr_dyn_key_set(scr_errdyn_cache[i].dyn, key, value);
|
||||
scr_str_release(key);
|
||||
return;
|
||||
}
|
||||
scr_error_install_cause_drop(&scr_error_cause_drop_impl);
|
||||
ScrDyn *replacement = scr_dyn_retain(value);
|
||||
if (!e->error_cause) e->cause_enumerable = true;
|
||||
scr_dyn_release(e->error_cause);
|
||||
e->error_cause = replacement;
|
||||
}
|
||||
|
||||
void scr_error_delete_cause(ScrError *e) {
|
||||
for (size_t i = 0; i < scr_errdyn_n; i++) {
|
||||
if (scr_errdyn_cache[i].err != e) continue;
|
||||
ScrStr *key = scr_str_new("cause", 5);
|
||||
scr_dyn_key_delete(scr_errdyn_cache[i].dyn, key);
|
||||
scr_str_release(key);
|
||||
return;
|
||||
}
|
||||
scr_dyn_release(e->error_cause);
|
||||
e->error_cause = NULL;
|
||||
e->cause_enumerable = false;
|
||||
}
|
||||
|
||||
static void scr_domex_cause_drop_impl(void *obj) {
|
||||
ScrDomException *d = (ScrDomException *)obj;
|
||||
scr_dyn_release(d->cause);
|
||||
|
||||
@@ -520,6 +520,7 @@ typedef struct ScrError {
|
||||
* carry a matching third field, so user subclasses
|
||||
* embed the slot and release it NULL-guarded. */
|
||||
struct ScrDyn *error_cause; /* NULL = absent; dyn undefined = present */
|
||||
bool cause_enumerable; /* assignment creates an enumerable own property */
|
||||
} ScrError;
|
||||
|
||||
enum {
|
||||
@@ -550,6 +551,7 @@ typedef struct ScrDomException {
|
||||
ScrStr *message; /* "" when constructed without one */
|
||||
ScrStr *code; /* the Node string-code slot (stays NULL here) */
|
||||
struct ScrDyn *error_cause; /* shared ScrError prefix; unused by DOMException */
|
||||
bool cause_enumerable;
|
||||
double dom_code; /* the WebIDL legacy code (0 when the name is off-table) */
|
||||
bool has_cause; /* the options form carried a `cause` member */
|
||||
struct ScrDyn *cause; /* owned; NULL when has_cause is false */
|
||||
@@ -601,6 +603,8 @@ void scr_error_init_options(void *obj, int kind, const struct ScrDyn *message, c
|
||||
void scr_error_install_cause_drop(void (*fn)(void *obj));
|
||||
bool scr_error_has_cause(ScrError *e);
|
||||
struct ScrDyn *scr_error_cause(ScrError *e); /* +1, undefined when absent */
|
||||
void scr_error_set_cause(ScrError *e, struct ScrDyn *value); /* borrowed operands */
|
||||
void scr_error_delete_cause(ScrError *e);
|
||||
/* ECMA Error.prototype.toString: "", name, message, or "name: message".
|
||||
* Borrows e, returns +1. */
|
||||
ScrStr *scr_error_to_string(ScrError *e);
|
||||
@@ -3753,6 +3757,8 @@ ScrDyn *scr_dyn_call(const ScrDyn *d, ScrDyn *const *args, size_t argc, const ch
|
||||
* `f(...args)` after the emitted argument array is built): argv IS the
|
||||
* array's items. Borrows both; result owned (+1), or NULL pending. */
|
||||
ScrDyn *scr_dyn_apply(const ScrDyn *d, const ScrDyn *args, const char *what);
|
||||
/* Function.bind borrows its target and arguments, returns a fresh callable. */
|
||||
ScrDyn *scr_dyn_bind(ScrDyn *target, ScrDyn *const *args, size_t argc);
|
||||
|
||||
/* Path spine for dynCheck error messages — a compile-time-shaped linked
|
||||
* list the emitted builders stack-allocate per recursion level: `key`
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
class RenderError extends Error {
|
||||
constructor(message, cause) {
|
||||
super(message);
|
||||
if (cause !== undefined) this.cause = cause;
|
||||
}
|
||||
update(cause) { this.cause = cause; }
|
||||
}
|
||||
const absent = new RenderError("a");
|
||||
const caused = new RenderError("b", 4);
|
||||
console.log(absent.cause, "cause" in absent, caused.cause, "cause" in caused);
|
||||
caused.update(undefined);
|
||||
console.log(caused.cause, "cause" in caused);
|
||||
delete caused.cause;
|
||||
console.log(caused.cause, "cause" in caused);
|
||||
caused.update("again");
|
||||
console.log(caused.cause, "cause" in caused);
|
||||
const error = new Error("options", { cause: "first" });
|
||||
function descriptor(value) {
|
||||
const d = Object.getOwnPropertyDescriptor(value, "cause");
|
||||
console.log(d.value, d.enumerable, d.writable, d.configurable);
|
||||
}
|
||||
descriptor(error);
|
||||
error.cause = "second";
|
||||
descriptor(error);
|
||||
delete error.cause;
|
||||
console.log("cause" in error);
|
||||
error.cause = undefined;
|
||||
descriptor(error);
|
||||
function overwrite(value) { value.cause = "through alias"; }
|
||||
overwrite(error);
|
||||
console.log(error.cause);
|
||||
function remove(value) { delete value.cause; }
|
||||
remove(error);
|
||||
console.log(error.cause, "cause" in error);
|
||||
function freeze(value) { Object.defineProperty(value, "cause", { value: "fixed", writable: false, configurable: false }); }
|
||||
freeze(error);
|
||||
try { error.cause = 7; } catch (error) { console.log(error.name); }
|
||||
try { delete error.cause; } catch (error) { console.log(error.name); }
|
||||
console.log(error.cause, "cause" in error);
|
||||
const original = new Error("original");
|
||||
const retained = new RenderError("retained", original);
|
||||
retained.update(retained.cause);
|
||||
const retainedCause = retained.cause;
|
||||
if (retainedCause instanceof Error) console.log(retainedCause === original);
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const item = new RenderError("item", { number: i });
|
||||
item.update(new Error("replacement"));
|
||||
delete item.cause;
|
||||
}
|
||||
console.log("released");
|
||||
|
||||
const accessor = new Error("accessor", { cause: 1 });
|
||||
let getterCalls = 0;
|
||||
let storedCause = 3;
|
||||
function installAccessor(value) {
|
||||
Object.defineProperty(value, "cause", {
|
||||
configurable: true,
|
||||
get() { getterCalls++; return storedCause; },
|
||||
set(next) { storedCause = next; }
|
||||
});
|
||||
}
|
||||
installAccessor(accessor);
|
||||
console.log("cause" in accessor, getterCalls);
|
||||
console.log(accessor.cause, getterCalls);
|
||||
accessor.cause = 9;
|
||||
console.log(accessor.cause, storedCause, getterCalls);
|
||||
delete accessor.cause;
|
||||
console.log("cause" in accessor, accessor.cause, getterCalls);
|
||||
Object.defineProperty(accessor, "cause", { configurable: true, get() { throw new Error("cause getter"); } });
|
||||
try { console.log(accessor.cause); } catch (error) { console.log(error.message); }
|
||||
Object.defineProperty(accessor, "cause", { value: "restored", writable: true });
|
||||
console.log(accessor.cause, "cause" in accessor);
|
||||
@@ -0,0 +1,8 @@
|
||||
import { sleep, format, selected, replace } from "./platform.js";
|
||||
class Renderer {
|
||||
async render() { console.log(await sleep(4), format("frame"), selected("frame")); }
|
||||
}
|
||||
const renderer = new Renderer();
|
||||
await renderer.render();
|
||||
replace();
|
||||
await renderer.render();
|
||||
@@ -0,0 +1,13 @@
|
||||
const runtime = globalThis[Symbol.for("scriptc.test.runtime")];
|
||||
beforeInitialization();
|
||||
export var sleep = runtime?.sleep ?? standardSleep;
|
||||
export var format = runtime?.format || standardFormat;
|
||||
export var selected = runtime ? standardFormat : alternateFormat;
|
||||
function beforeInitialization() {
|
||||
console.log(typeof sleep);
|
||||
try { sleep(0); } catch (error) { console.log(error.name); }
|
||||
}
|
||||
function standardSleep(value) { return Promise.resolve(value); }
|
||||
function standardFormat(value) { return `standard:${value}`; }
|
||||
function alternateFormat(value) { return `alternate:${value}`; }
|
||||
export function replace() { sleep = (value) => Promise.resolve(value + 1); }
|
||||
@@ -0,0 +1,24 @@
|
||||
function createLayout() {
|
||||
return { columns: [], offsets: [0], packed: new Int32Array([0, 2]), width: 0 };
|
||||
}
|
||||
const factory = () => ({ packed: new Float64Array([1.5, 2.5]), size: 2 });
|
||||
class Layout {
|
||||
layout = this.createEmptyLayout();
|
||||
createEmptyLayout() { return createLayout(); }
|
||||
}
|
||||
const layout = new Layout();
|
||||
layout.layout.packed[0] = 7;
|
||||
layout.layout.offsets[0] = 3;
|
||||
console.log(layout.layout.packed[0], layout.layout.offsets[0], layout.layout.width);
|
||||
const result = factory();
|
||||
const alias = result;
|
||||
alias.packed[1] = 4.5;
|
||||
console.log(result === alias, result.packed[1], result.size);
|
||||
const first = createLayout();
|
||||
const second = createLayout();
|
||||
first.packed[0] = 8;
|
||||
console.log(first === second, first.packed === second.packed, second.packed[0]);
|
||||
const expected = { label: "same" };
|
||||
function wrapper() { return expected; }
|
||||
function identity(value) { return value; }
|
||||
console.log(wrapper() === expected, identity(wrapper()) === expected);
|
||||
@@ -0,0 +1,91 @@
|
||||
class Parser {
|
||||
value = 3;
|
||||
options = this.setOptions;
|
||||
setOptions(value) { this.value = value; return this; }
|
||||
otherOptions(value) { this.value = value + 10; return this; }
|
||||
constant() { return 12; }
|
||||
describe(prefix = "parser") { return `${prefix}:${this.value}`; }
|
||||
countArgs(first) { return arguments.length; }
|
||||
join(prefix, ...parts) { return prefix + parts.join(":"); }
|
||||
fail() { throw new Error("method failed"); }
|
||||
}
|
||||
const parser = new Parser();
|
||||
console.log(parser.options(4) === parser, parser.value);
|
||||
const second = new Parser();
|
||||
second.options = parser.options;
|
||||
console.log(second.options(7) === second, second.value, parser.value);
|
||||
console.log(parser.options === parser.setOptions, parser.options === second.options);
|
||||
const detached = parser.options;
|
||||
try { detached(5); } catch (error) { console.log(error.name); }
|
||||
const constant = parser.constant;
|
||||
console.log(constant());
|
||||
function call(method, receiver, ...args) { return method.apply(receiver, args); }
|
||||
console.log(call(parser.describe, second), call(parser.describe, second, "other"));
|
||||
console.log(call(parser.countArgs, undefined, 1, 2, 3), call(parser.join, parser, "parts:", 1, "two"));
|
||||
try { call(parser.fail, second); } catch (error) { console.log(error.message); }
|
||||
console.log(call(parser.describe, parser));
|
||||
console.log(parser.describe.call(second, "called"));
|
||||
console.log(parser.describe.apply(second, ["applied"]));
|
||||
const bound = parser.describe.bind(second, "bound");
|
||||
console.log(bound(), bound.call(parser), bound.bind(parser)());
|
||||
const restBound = parser.join.bind(parser, "bound:", 1);
|
||||
console.log(restBound("two", 3));
|
||||
try { parser.fail.bind(parser)(); } catch (error) { console.log(error.message); }
|
||||
console.log(bound());
|
||||
function bindAgain(method, receiver) { return method.bind(receiver, "indirect"); }
|
||||
console.log(bindAgain(parser.describe, second)());
|
||||
let effects = "";
|
||||
function receiver() { effects += "receiver;"; return parser; }
|
||||
function argument() { effects += "argument;"; parser.options = second.otherOptions; return 9; }
|
||||
console.log(receiver().options(argument()) === parser, effects, parser.value);
|
||||
const dynamic = { first: 17, method: function() { return this.first; }, nested: function() { return detached(1); } };
|
||||
function invoke(object) { return object.method(); }
|
||||
console.log(invoke(dynamic));
|
||||
try { dynamic.nested(); } catch (error) { console.log(error.name); }
|
||||
console.log(invoke(dynamic));
|
||||
|
||||
class Base {
|
||||
value = 10;
|
||||
method() { return `base:${this.value}`; }
|
||||
inherited() { return this.value; }
|
||||
}
|
||||
class Derived extends Base {
|
||||
method() { return `derived:${this.value}`; }
|
||||
}
|
||||
class Further extends Derived {
|
||||
method() { return `further:${this.value}`; }
|
||||
}
|
||||
/** @type {Base} */
|
||||
const baseView = new Derived();
|
||||
const derived = new Derived();
|
||||
const base = new Base();
|
||||
const selected = baseView.method;
|
||||
console.log(selected === derived.method, selected === base.method);
|
||||
console.log(call(selected, derived), call(base.method, derived), call(baseView.inherited, derived));
|
||||
const further = new Further();
|
||||
console.log(call(selected, further), further.method());
|
||||
|
||||
class Left {
|
||||
value = "left";
|
||||
leftOnly = true;
|
||||
/** @type {*} */
|
||||
callback;
|
||||
read(suffix) { return this.value + suffix; }
|
||||
constructor() { this.callback = this.read; }
|
||||
}
|
||||
class Right {
|
||||
value = "right";
|
||||
rightOnly = true;
|
||||
/** @type {*} */
|
||||
callback;
|
||||
read(suffix) { return this.value + suffix; }
|
||||
constructor() { this.callback = this.read; }
|
||||
}
|
||||
const left = new Left();
|
||||
const right = new Right();
|
||||
let selectedCalls = 0;
|
||||
function select(rightArm) { selectedCalls++; return rightArm ? right : left; }
|
||||
function replace() { left.callback = () => "replacement"; return "!"; }
|
||||
console.log(select(false).callback(replace()), selectedCalls);
|
||||
console.log(select(true).callback("?"), selectedCalls);
|
||||
console.log(select(false).callback("ignored"), selectedCalls);
|
||||
@@ -0,0 +1,27 @@
|
||||
class Counter {
|
||||
value: number;
|
||||
callback: (increment: number) => number;
|
||||
constructor(value: number) { this.value = value; this.callback = this.add; }
|
||||
add(increment: number): number { this.value += increment; return this.value; }
|
||||
constant(): number { return 11; }
|
||||
fail(): number { throw new Error("callback failed"); }
|
||||
}
|
||||
const first = new Counter(1);
|
||||
const second = new Counter(10);
|
||||
console.log(first.callback(2));
|
||||
second.callback = first.callback;
|
||||
console.log(second.callback(3), first.value);
|
||||
console.log(first.callback === first.add, first.add === second.add);
|
||||
const detached = first.callback;
|
||||
try { detached(1); } catch (error) { if (error instanceof Error) console.log(error.name); }
|
||||
const constant = first.constant;
|
||||
console.log(constant());
|
||||
const arrow = (value: number): number => value + 1;
|
||||
const alias = arrow.bind(null);
|
||||
const ordinary = (function(value: number): number { return value + 2; }).bind(null);
|
||||
console.log(alias(4), ordinary(4));
|
||||
const bound = first.add.bind(second);
|
||||
console.log(bound(2), second.value, first.value);
|
||||
console.log(first.add.call(second, 1), first.add.apply(first, [4]));
|
||||
try { first.fail.bind(first)(); } catch (error) { if (error instanceof Error) console.log(error.message); }
|
||||
console.log(bound(1));
|
||||
@@ -0,0 +1,19 @@
|
||||
class Renderer {
|
||||
pending = null;
|
||||
setPending(transition) { this.pending = transition; }
|
||||
resize(width) {
|
||||
const pending = this.pending;
|
||||
const height = pending?.sourceHeight ?? 4;
|
||||
let clearStart = null;
|
||||
if (width < 4 && height > 0) clearStart = 9;
|
||||
if (pending !== null) clearStart = clearStart === null ? pending.sourceTopLine : Math.min(clearStart, pending.sourceTopLine);
|
||||
console.log(clearStart, height);
|
||||
}
|
||||
}
|
||||
const renderer = new Renderer();
|
||||
renderer.resize(5);
|
||||
renderer.setPending({ sourceTopLine: 3, sourceHeight: 5 });
|
||||
renderer.resize(5);
|
||||
renderer.resize(2);
|
||||
renderer.setPending(null);
|
||||
renderer.resize(3);
|
||||
@@ -1,7 +1,4 @@
|
||||
// Preset arguments, rest arrows, non-inline arrows and ordinary functions
|
||||
// retain their explicit Function.prototype.bind refusal.
|
||||
// Typed preset and rest signatures still exceed the callable adapter.
|
||||
const preset = ((value: number) => value).bind(null, 1);
|
||||
const rest = ((...values: number[]) => values.length).bind(null);
|
||||
const arrow = (value: number) => value + 1;
|
||||
const alias = arrow.bind(null);
|
||||
const ordinary = (function (value: number) { return value + 2; }).bind(null);
|
||||
export {};
|
||||
|
||||
@@ -1,27 +1,15 @@
|
||||
arrow-bind.ts:3:16 - error SC1090: Function.prototype.bind on a compiled function value (compiled calls are direct — no runtime 'this' or arguments object exists to re-route; spell the call directly: '((value: number) => value)(...)') is not supported yet
|
||||
arrow-bind.ts:2:16 - error SC1100: passing 'unknown' values where '((null | undefined)[]) => number' is expected is not supported yet
|
||||
|
||||
2 | // retain their explicit Function.prototype.bind refusal.
|
||||
3 | const preset = ((value: number) => value).bind(null, 1);
|
||||
1 | // Typed preset and rest signatures still exceed the callable adapter.
|
||||
2 | const preset = ((value: number) => value).bind(null, 1);
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
4 | const rest = ((...values: number[]) => values.length).bind(null);
|
||||
3 | const rest = ((...values: number[]) => values.length).bind(null);
|
||||
|
||||
arrow-bind.ts:4:14 - error SC1090: Function.prototype.bind on a compiled function value (compiled calls are direct — no runtime 'this' or arguments object exists to re-route; spell the call directly: '((...values: number[]) => values.length)(...)') is not supported yet
|
||||
hint: validate with 'as <type>' first — the cast checks the dynamic value at runtime and throws on mismatch
|
||||
|
||||
3 | const preset = ((value: number) => value).bind(null, 1);
|
||||
4 | const rest = ((...values: number[]) => values.length).bind(null);
|
||||
arrow-bind.ts:3:14 - error SC1090: Function.prototype.bind on a function whose signature cannot cross the checked-value boundary (call '((...values: number[]) => values.length)(...)' directly) is not supported yet
|
||||
|
||||
2 | const preset = ((value: number) => value).bind(null, 1);
|
||||
3 | const rest = ((...values: number[]) => values.length).bind(null);
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
5 | const arrow = (value: number) => value + 1;
|
||||
|
||||
arrow-bind.ts:6:15 - error SC1090: Function.prototype.bind on a compiled function value (compiled calls are direct — no runtime 'this' or arguments object exists to re-route; spell the call directly: 'arrow(...)') is not supported yet
|
||||
|
||||
5 | const arrow = (value: number) => value + 1;
|
||||
6 | const alias = arrow.bind(null);
|
||||
| ^~~~~~~~~~~~~~~~
|
||||
7 | const ordinary = (function (value: number) { return value + 2; }).bind(null);
|
||||
|
||||
arrow-bind.ts:7:18 - error SC1090: Function.prototype.bind on a compiled function value (compiled calls are direct — no runtime 'this' or arguments object exists to re-route; spell the call directly: '(function (value: number) { return value + 2; })(...)') is not supported yet
|
||||
|
||||
6 | const alias = arrow.bind(null);
|
||||
7 | const ordinary = (function (value: number) { return value + 2; }).bind(null);
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
8 |
|
||||
4 | export {};
|
||||
@@ -99,6 +99,35 @@ async function buildStatic(entry: string, npmStatic: string[] | "auto"): Promise
|
||||
}
|
||||
|
||||
describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => {
|
||||
test.each(["c", "llvm"] as const)("renderer values and callbacks compile from shipped JavaScript (%s)", async (backend) => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-renderer-values-"));
|
||||
try {
|
||||
const pkg = join(dir, "node_modules", "renderer-values");
|
||||
mkdirSync(pkg, { recursive: true });
|
||||
writeFileSync(join(dir, "package.json"), '{"type":"module"}');
|
||||
writeFileSync(join(pkg, "package.json"), '{"name":"renderer-values","version":"1.0.0","type":"module","main":"index.js","types":"index.d.ts"}');
|
||||
writeFileSync(join(pkg, "index.d.ts"), "export {};\n");
|
||||
const cases = ["native-factory-records.js", "native-method-values.js", "error-cause-writes.js", "renderer-null-state.js"];
|
||||
for (const name of cases) cpSync(join(repoRoot, "tests/corpus", name), join(pkg, name));
|
||||
cpSync(join(repoRoot, "tests/corpus/module-fallback-functions"), join(pkg, "module-fallback-functions"), { recursive: true });
|
||||
writeFileSync(join(pkg, "index.js"), [...cases.map((name) => `import "./${name}";`), 'import "./module-fallback-functions/main.js";'].join("\n"));
|
||||
const entry = join(dir, "main.js");
|
||||
writeFileSync(entry, 'import "renderer-values";');
|
||||
const { coverage } = analyze(entry, { npmStatic: "auto" });
|
||||
expect(coverage.npmStatic).toEqual([{ package: "renderer-values", status: "static" }]);
|
||||
expect(coverage.diagnostics).toHaveLength(0);
|
||||
expect(coverage.runtimeFences ?? [], JSON.stringify(coverage.runtimeFences, null, 2)).toHaveLength(0);
|
||||
const result = await compile(entry, { backend, dynamic: false, npmStatic: "auto", sanitize, outDir: join(dir, "out"), outPath: join(dir, "program") });
|
||||
if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const [nodeRes, nativeRes] = await Promise.all([runBinary(process.execPath, [entry]), runBinary(result.binaryPath, [])]);
|
||||
expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8"));
|
||||
expect(comparableStderr(nativeRes.stderr)).toEqual(nodeRes.stderr);
|
||||
expect(nativeRes.exitCode).toBe(nodeRes.exitCode);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test.each(["c", "llvm"] as const)("bundled class aliases preserve declared methods and callback fields (%s)", async (backend) => {
|
||||
const entry = join(pilotRoot, "bundled-methods-cli.ts");
|
||||
const { coverage } = analyze(entry, { npmStatic: "auto" });
|
||||
|
||||
Reference in New Issue
Block a user