Give errors held as any their own stack accessor

In Node every Error has an own, non-enumerable `stack` accessor, shared by
all errors, that formats the stack on first access. Errors that reached
checked-dynamic code read `stack` as undefined: the error encoding carried
a stack only when a typed read had already formatted one. Reads on
instances of Error subclasses found no class arm for it, and assigning
`stack` on a subclass instance, typed or through `any`, did not change
what a typed read answered.

The encoding now lists `stack` first among its own properties as a
non-enumerable accessor. Its getter formats the native error's stack on
first access, so the heading is the name and message at that point, and
its setter stores a string in the native error, which typed reads share.
Any other value replaces the accessor with a data property. Objects that
inherit from an error read the error's stack, and a getter called on any
other receiver answers undefined.

A `stack` read on a checked-dynamic value counts as a stack observation,
so such programs capture frames as they do for typed reads. Dynamic reads
and writes of `stack` on Error subclass instances use the native stack;
non-string values are kept with the instance's own properties.
This commit is contained in:
Chris Tate
2026-10-19 22:26:32 -05:00
parent 62fb17bf8c
commit 80355af8ea
16 changed files with 511 additions and 6 deletions
@@ -827,6 +827,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.sameValue": "scr_dyn_same_value",
"error.toString": "scr_error_to_string",
"error.stack": "scr_error_stack",
"error.setStack": "scr_error_set_stack",
"error.stackLimitGet": "scr_stack_limit_get",
"error.stackLimitSet": "scr_stack_limit_set",
"class.name": "scr_classobj_name",
@@ -1,5 +1,6 @@
import { expect, test } from "vitest";
import {
DYN,
F64,
STRING,
VOID,
@@ -49,6 +50,23 @@ test("a reachable stack read is observed", () => {
expect(observesStacks(mod([fn("%main", [call("parse")]), fn("parse", [stackRead])]))).toBe(true);
});
test("a stack read on a checked-dynamic value is observed", () => {
const value: IrExpr = { loc, kind: "varRef", localId: "d", type: DYN };
const read = (key: string): IrStmt => ({
loc,
kind: "exprStmt",
expr: {
loc,
kind: "dynKeyGet",
value,
key: { loc, kind: "strLit", value: key, type: STRING },
type: DYN,
},
});
expect(observesStacks(mod([fn("%main", [read("stack")])]))).toBe(true);
expect(observesStacks(mod([fn("%main", [read("message")])]))).toBe(false);
});
test("programs without stack reads are not", () => {
expect(observesStacks(mod([fn("%main", [call("helper")]), fn("helper", [ret])]))).toBe(false);
});
@@ -13,8 +13,11 @@ import { writesPublished } from "./publication.js";
/** Lib calls that read a captured stack trace. */
const STACK_READS: ReadonlySet<string> = new Set(["error.stack"]);
/** A typed `error.stack` read, or a `stack` read on a checked-dynamic
* value, which reaches an error's own `stack` accessor. */
function readsStack(expr: IrExpr): boolean {
return expr.kind === "libCall" && STACK_READS.has(expr.fn);
if (expr.kind === "libCall") return STACK_READS.has(expr.fn);
return expr.kind === "dynKeyGet" && expr.key.kind === "strLit" && expr.key.value === "stack";
}
function containsStackRead(fn: IrFunction): boolean {
@@ -2694,6 +2694,7 @@ export class ClassDynamicDispatch {
!findGenericMethodOn(lowerer, info, dispatch.name) &&
dispatch.name !== "constructor" &&
!info.fields.has(dispatch.name) &&
!(dispatch.name === "stack" && lowerer.isSubclassOf(info.def.name, "%Error")) &&
!findMethodOn(lowerer, info, `get:${dispatch.name}`) &&
!findMethodOn(lowerer, info, `set:${dispatch.name}`))
)
@@ -4498,6 +4499,110 @@ export class ClassDynamicDispatch {
},
];
}
// An Error's own `stack`: a string is the native error's stack, which
// typed reads share; any other value is kept with the instance's own
// properties, which a read checks first. A class whose chain declares
// a `stack` accessor answers through it (the accessor code below).
if (
name === "stack" &&
!info.fields.has(name) &&
lowerer.isSubclassOf(info.def.name, "%Error") &&
!findMethodOn(lowerer, info, "get:stack") &&
!findMethodOn(lowerer, info, "set:stack")
) {
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
const error = lowerer.upcastTo(receiver, "%Error");
if (write) {
const helper = classPropertiesHelper(lowerer, loc);
this.generated.add(helper);
const store: IrStmt = {
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "dyn.keySet",
args: [
{
kind: "call",
callee: helper.name,
args: [lowerer.coerceToExpected(receiver, DYN)],
type: DYN,
loc,
},
key,
varRef("p.1", DYN, loc),
],
type: VOID,
loc,
},
loc,
};
return [
{
kind: "if",
cond: {
kind: "dynTest",
test: "string",
value: varRef("p.1", DYN, loc),
type: BOOL,
loc,
},
then: [
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "error.setStack",
args: [error, lowerer.coerceToExpected(varRef("p.1", DYN, loc), STRING)],
type: VOID,
loc,
},
loc,
},
{
kind: "if",
cond: this.readBagHas(
lowerer,
"dyn.hasKey",
this.readPropertyBag(lowerer, receiver, loc),
key,
loc,
),
then: [store],
else_: null,
loc,
},
],
else_: [store],
loc,
},
{ kind: "return", value: null, loc },
];
}
const bag = this.readPropertyBag(lowerer, receiver, loc);
return [
{
kind: "return",
value: {
kind: "ternary",
cond: this.readBagHas(lowerer, "dyn.hasKey", bag, key, loc),
then: {
kind: "libCall",
fn: "dyn.bagGet",
args: [bag, key, lowerer.coerceToExpected(receiver, DYN)],
type: DYN,
loc,
},
else_: lowerer.coerceToExpected(
{ kind: "libCall", fn: "error.stack", args: [error], type: STRING, loc },
DYN,
),
type: DYN,
loc,
},
loc,
},
];
}
if ((name === "message" || name === "name") && lowerer.isSubclassOf(info.def.name, "%Error"))
return write
? [
@@ -3231,6 +3231,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
/** SameValue over checked values, including NaN, signed zero and reference identity. */
"dyn.sameValue": { argTypes: [DYN, DYN], result: BOOL },
"error.stack": { argTypes: [null], result: STRING },
"error.setStack": { argTypes: [null, STRING], result: VOID },
"error.stackLimitGet": { argTypes: [], result: F64 },
"error.stackLimitSet": { argTypes: [F64], result: VOID },
// Receiver (any error-hierarchy object) and the program-dependent
+1
View File
@@ -4109,6 +4109,7 @@ function validateFunction(
if (checkLibEmitterCall(e, sig.result)) return;
if (
e.fn === "error.stack" ||
e.fn === "error.setStack" ||
e.fn === "error.cause" ||
e.fn === "error.hasCause" ||
e.fn === "error.setCause" ||
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5916-dyn-error-stack-property.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6400-dyn-error-stack-property-js.js"
],
"diags": []
}
+6
View File
@@ -354,6 +354,12 @@ ScrStr *scr_error_stack(ScrError *e) {
return scr_str_retain(e->stack);
}
void scr_error_set_stack(ScrError *e, ScrStr *stack) {
ScrStr *replacement = scr_str_retain(stack);
scr_str_release(e->stack);
e->stack = replacement;
}
bool scr_error_is(const void *obj) {
const ScrVt *vt = ((const ScrError *)obj)->vt;
return scr_error_vts[SCR_ERR_ERROR].pre <= vt->pre &&
+3
View File
@@ -252,6 +252,9 @@ void scr_error_install_message_reader(ScrStr *(*reader)(ScrError *));
void scr_error_install_name_reader(ScrStr *(*reader)(ScrError *));
ScrStr *scr_error_default_name(const ScrError *e);
ScrStr *scr_error_stack(ScrError *e);
/* Replaces the error's stack string, as an assignment to `stack` does.
* Borrowed operands. */
void scr_error_set_stack(ScrError *e, ScrStr *stack);
ScrError *scr_error_retain(ScrError *e);
void scr_error_release(ScrError *e); /* dispatches through e->vt */
+100 -5
View File
@@ -6719,10 +6719,109 @@ static void scr_errdyn_teardown(void) {
free(cache);
}
/* An error's own `stack`: a non-enumerable accessor shared by every error,
* as V8 defines it. The getter formats the stack on first access, so its
* heading is the name and message at that point, and later reads answer
* the same string. Both functions find the error through the receiver or
* its prototype chain; on any other receiver the getter answers undefined
* and the setter changes nothing but the receiver's own accessor. */
static SCR_TL ScrDyn *scr_error_stack_accessors[2];
static SCR_TL bool scr_error_stack_accessors_registered;
static void scr_error_stack_accessors_cleanup(void) {
for (size_t i = 0; i < 2; i++) {
scr_dyn_release(scr_error_stack_accessors[i]);
scr_error_stack_accessors[i] = NULL;
}
scr_error_stack_accessors_registered = false;
}
/* The native error behind an accessor receiver. +1 or NULL. */
static ScrError *scr_error_stack_holder(const ScrDyn *receiver) {
for (const ScrDyn *current = receiver; current; current = scr_dyn_proto_next(current)) {
ScrError *error = scr_errdyn_err_of(current);
if (error) return error;
if (current->kind != SCR_DYN_OBJ) return NULL;
}
return NULL;
}
static ScrDyn *scr_error_stack_get_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
(void)closure; (void)args; (void)argc;
ScrDyn *self = scr_dyn_this_get();
ScrError *error = scr_error_stack_holder(self);
scr_dyn_release(self);
if (!error) return scr_dyn_retain(scr_dyn_undefined());
ScrStr *stack = scr_error_stack(error);
scr_error_release(error);
ScrDyn *out = scr_dyn_new_str(stack);
scr_str_release(stack);
return out;
}
static ScrDyn *scr_error_stack_set_call(ScrClosure *closure, ScrDyn *const *args, size_t argc);
/* Whether an own property is an error's `stack` accessor. */
static bool scr_dyn_entry_is_error_stack(const ScrDynEntry *entry) {
return entry->accessor && entry->setter && entry->setter->kind == SCR_DYN_FUNC &&
entry->setter->v.fn.thunk == scr_error_stack_set_call;
}
static ScrDyn *scr_error_stack_set_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
(void)closure;
ScrDyn *self = scr_dyn_this_get();
ScrError *error = scr_error_stack_holder(self);
ScrDyn *value = argc > 0 ? args[0] : scr_dyn_undefined();
if (error && value->kind == SCR_DYN_STR) {
scr_error_set_stack(error, value->v.str);
} else if (self->kind == SCR_DYN_OBJ) {
/* The native slot holds strings only, and a typed reference's
* snapshot names no error: the receiver's own accessor becomes a data
* property holding the value, which a snapshot's commit stores. */
ScrStr *key = scr_str_new("stack", 5);
ScrDynEntry *entry = scr_dyn_entry(self, key);
scr_str_release(key);
if (entry && scr_dyn_entry_is_error_stack(entry)) {
ScrDyn *old_value = entry->value, *old_getter = entry->getter, *old_setter = entry->setter;
entry->value = scr_dyn_retain(value);
entry->getter = entry->setter = NULL;
entry->accessor = false;
entry->writable = true;
scr_dyn_release(old_value);
scr_dyn_release(old_getter);
scr_dyn_release(old_setter);
}
}
scr_error_release(error);
scr_dyn_release(self);
return scr_dyn_retain(scr_dyn_undefined());
}
static void scr_error_dyn_add_stack(ScrDyn *d) {
if (!scr_error_stack_accessors_registered) {
scr_error_stack_accessors_registered = true;
scr_atexit(scr_error_stack_accessors_cleanup);
}
if (!scr_error_stack_accessors[0]) {
scr_error_stack_accessors[0] =
scr_dyn_new_func(scr_closure_new(NULL, 0), scr_error_stack_get_call, 0, "native:error.stack", NULL);
scr_error_stack_accessors[1] =
scr_dyn_new_func(scr_closure_new(NULL, 0), scr_error_stack_set_call, 1, "native:error.stack", NULL);
}
scr_dyn_obj_set(d, "stack", 5, scr_dyn_retain(scr_dyn_undefined()));
ScrDynEntry *entry = &d->v.obj.entries[d->v.obj.len - 1];
entry->accessor = true;
entry->getter = scr_dyn_retain(scr_error_stack_accessors[0]);
entry->setter = scr_dyn_retain(scr_error_stack_accessors[1]);
entry->enumerable = false;
}
ScrDyn *scr_error_dyn_fields(const ScrError *e) {
ScrDyn *d = scr_dyn_new_obj();
scr_dyn_obj_set(d, "%error", 6, scr_dyn_new_bool(true)); /* the checked-dynamic tree's error marker */
d->v.obj.entries[d->v.obj.len - 1].enumerable = false;
/* Node lists `stack` first among an error's own properties. */
scr_error_dyn_add_stack(d);
if (e->name_present) {
scr_dyn_obj_set(d, "name", 4, scr_dyn_new_str(e->name));
d->v.obj.entries[d->v.obj.len - 1].enumerable = e->name_enumerable;
@@ -6744,11 +6843,6 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) {
scr_dyn_obj_set(d, "message", 7, scr_dyn_new_str(e->message));
d->v.obj.entries[d->v.obj.len - 1].enumerable = e->message_enumerable;
}
if (e->stack) {
/* Node's own `stack` is non-enumerable. */
scr_dyn_obj_set(d, "stack", 5, scr_dyn_new_str(e->stack));
d->v.obj.entries[d->v.obj.len - 1].enumerable = false;
}
if (e->system_message) {
/* SystemError: code, the read-only `info` context ({ message, path,
* syscall, errno, code }), then errno, syscall, path and dest. */
@@ -10860,6 +10954,7 @@ static ScrDyn *scr_sc_clone(const ScrDyn *v, const ScrScParent *up, ScrDynRegion
ScrDyn *out = scr_dyn_new_obj();
for (size_t i = 0; i < v->v.obj.len; i++) {
const ScrDynEntry *e = &v->v.obj.entries[i];
if (scr_dyn_entry_is_error_stack(e)) continue;
ScrDyn *c = scr_sc_clone(e->value, &self, region);
if (c == NULL) {
scr_dyn_release(out);
+6
View File
@@ -801,6 +801,12 @@ ScrDyn *scr_message_decode(const ScrMessage *message) {
}
if (!values[i]->v.arr.properties) values[i]->v.arr.properties = scr_dyn_new_obj();
scr_dyn_obj_set(values[i]->v.arr.properties, property->key, property->length, value);
} else if (node->kind == SCR_MESSAGE_ERROR && property->length == 5 && !memcmp(property->key, "stack", 5)) {
/* The new error's own `stack` accessor stores the value. */
ScrStr *key = scr_str_new(property->key, property->length);
scr_dyn_key_set(values[i], key, value);
scr_str_release(key);
scr_dyn_release(value);
} else scr_dyn_obj_set(values[i], property->key, property->length, value);
}
if (node->kind == SCR_MESSAGE_ERROR) {
@@ -0,0 +1,126 @@
// Errors held as `any` or `unknown` have an own `stack` like Node's: a
// non-enumerable accessor that every error shares. The first read formats
// "Name: message" from the error's name and message at that point, and
// later reads answer the same string. Assigning a value replaces it,
// `delete` removes it, Object.keys, spread, Object.assign and
// JSON.stringify leave it out, `in` and Object.hasOwn find it, objects
// that inherit from an error read the error's stack. Typed and dynamic
// reads answer the same string, and a stack read through `any` captures
// frames. Only the first line is printed: frame lines differ between
// runtimes.
class ParseError extends Error {
constructor(message: string) {
super(message);
this.name = "ParseError";
}
}
class PlainSubclass extends Error {}
function dyn(value: unknown): any {
return value;
}
function firstLine(value: unknown): string {
return typeof value === "string" ? JSON.stringify(value.split("\n")[0]) : `(${typeof value} ${String(value)})`;
}
function describe(label: string, error: any): void {
console.log(
label,
"|",
typeof error.stack,
firstLine(error.stack),
"|",
Object.keys(error).join(","),
JSON.stringify(error),
"|",
"stack" in error,
Object.hasOwn(error, "stack"),
error.propertyIsEnumerable("stack"),
);
}
try {
throw new RangeError("thrown");
} catch (error) {
describe("caught", error);
}
const unknownError: unknown = new TypeError("bad thing");
describe("unknown", unknownError);
describe("plain", dyn(new Error("plain")));
describe("no message", dyn(new SyntaxError()));
describe("subclass", dyn(new ParseError("line 3")));
describe("plain subclass", dyn(new PlainSubclass("inherited name")));
const coded = dyn(new Error("with code"));
coded.code = "E_CODE";
describe("own property", coded);
const descriptor = Object.getOwnPropertyDescriptor(dyn(new Error("a")), "stack")!;
const other = Object.getOwnPropertyDescriptor(dyn(new TypeError("b")), "stack")!;
console.log("descriptor", Object.keys(descriptor).join(","), descriptor.enumerable, descriptor.configurable);
console.log("shared accessors", typeof descriptor.get, descriptor.get === other.get, descriptor.set === other.set);
console.log("foreign receiver", descriptor.get!.call({}), firstLine(descriptor.get!.call(dyn(new TypeError("b")))));
const subclassDescriptor = Object.getOwnPropertyDescriptor(dyn(new PlainSubclass("s")), "stack")!;
console.log("subclass descriptor", Object.keys(subclassDescriptor).join(","), subclassDescriptor.get === descriptor.get);
const renamed = dyn(new Error("before"));
renamed.message = "after";
renamed.name = "Renamed";
console.log("first read", firstLine(renamed.stack));
renamed.message = "later";
console.log("later read", firstLine(renamed.stack));
const subclassRenamed = dyn(new PlainSubclass("before"));
subclassRenamed.message = "after";
console.log("subclass first read", firstLine(subclassRenamed.stack));
const replaced = dyn(new Error("replaced"));
replaced.stack = "custom stack\n at nowhere";
describe("assigned", replaced);
console.log("assigned descriptor", Object.keys(Object.getOwnPropertyDescriptor(replaced, "stack")!).join(","));
const subclassReplaced = dyn(new ParseError("replaced"));
subclassReplaced.stack = "subclass stack";
console.log("subclass assigned", subclassReplaced.stack);
const typedReplaced = new ParseError("typed write");
typedReplaced.stack = "typed subclass stack";
console.log("typed subclass assigned", typedReplaced.stack, dyn(typedReplaced).stack);
const subclassNumbered = dyn(new PlainSubclass("numbered"));
subclassNumbered.stack = 7;
console.log("subclass number", subclassNumbered.stack, typeof subclassNumbered.stack);
subclassNumbered.stack = "string again";
console.log("subclass string again", subclassNumbered.stack);
const numbered = dyn(new Error("numbered"));
numbered.stack = 5;
console.log("number", numbered.stack, typeof numbered.stack);
const removed = dyn(new Error("removed"));
delete removed.stack;
console.log("deleted", removed.stack, "stack" in removed, Object.hasOwn(removed, "stack"));
const spreadSource = dyn(new Error("spread"));
console.log("spread", Object.keys({ ...spreadSource }).length, Object.keys(Object.assign({}, spreadSource)).length);
console.log("inherited", firstLine(Object.create(dyn(new RangeError("prototype"))).stack));
const typed = new Error("typed");
const view = dyn(typed);
console.log("typed and dynamic", view.stack === typed.stack, firstLine(typed.stack));
const typedSubclass = new ParseError("typed subclass");
console.log("typed subclass", firstLine(dyn(typedSubclass).stack), dyn(typedSubclass).stack === typedSubclass.stack);
const assignedThroughView = new Error("typed");
dyn(assignedThroughView).stack = "through the view";
console.log("assigned through view", assignedThroughView.stack);
function inner(): any {
return dyn(new Error("deep"));
}
function outer(): any {
return inner();
}
const deep: string = outer().stack;
console.log("frames", deep.split("\n").length > 1, deep.includes("at inner"), deep.includes("at outer"));
function makeSubclass(): any {
return dyn(new PlainSubclass("deep"));
}
console.log("subclass frames", (makeSubclass().stack as string).includes("at makeSubclass"));
@@ -0,0 +1,126 @@
// In JavaScript sources, errors have an own `stack` like Node's: a
// non-enumerable accessor that every error shares. The first read formats
// "Name: message" from the error's name and message at that point, and
// later reads answer the same string. Assigning a value replaces it,
// `delete` removes it, Object.keys, spread, Object.assign and
// JSON.stringify leave it out, `in` and Object.hasOwn find it, objects
// that inherit from an error read the error's stack. Typed and dynamic
// reads answer the same string, and a stack read through `any` captures
// frames. Only the first line is printed: frame lines differ between
// runtimes.
class ParseError extends Error {
constructor(message) {
super(message);
this.name = "ParseError";
}
}
class PlainSubclass extends Error {}
function dyn(value) {
return value;
}
function firstLine(value) {
return typeof value === "string" ? JSON.stringify(value.split("\n")[0]) : `(${typeof value} ${String(value)})`;
}
function describe(label, error) {
console.log(
label,
"|",
typeof error.stack,
firstLine(error.stack),
"|",
Object.keys(error).join(","),
JSON.stringify(error),
"|",
"stack" in error,
Object.hasOwn(error, "stack"),
error.propertyIsEnumerable("stack"),
);
}
try {
throw new RangeError("thrown");
} catch (error) {
describe("caught", error);
}
const unknownError = new TypeError("bad thing");
describe("unknown", unknownError);
describe("plain", dyn(new Error("plain")));
describe("no message", dyn(new SyntaxError()));
describe("subclass", dyn(new ParseError("line 3")));
describe("plain subclass", dyn(new PlainSubclass("inherited name")));
const coded = dyn(new Error("with code"));
coded.code = "E_CODE";
describe("own property", coded);
const descriptor = dyn(Object.getOwnPropertyDescriptor(dyn(new Error("a")), "stack"));
const other = dyn(Object.getOwnPropertyDescriptor(dyn(new TypeError("b")), "stack"));
console.log("descriptor", Object.keys(descriptor).join(","), descriptor.enumerable, descriptor.configurable);
console.log("shared accessors", typeof descriptor.get, descriptor.get === other.get, descriptor.set === other.set);
console.log("foreign receiver", descriptor.get.call({}), firstLine(descriptor.get.call(dyn(new TypeError("b")))));
const subclassDescriptor = dyn(Object.getOwnPropertyDescriptor(dyn(new PlainSubclass("s")), "stack"));
console.log("subclass descriptor", Object.keys(subclassDescriptor).join(","), subclassDescriptor.get === descriptor.get);
const renamed = dyn(new Error("before"));
renamed.message = "after";
renamed.name = "Renamed";
console.log("first read", firstLine(renamed.stack));
renamed.message = "later";
console.log("later read", firstLine(renamed.stack));
const subclassRenamed = dyn(new PlainSubclass("before"));
subclassRenamed.message = "after";
console.log("subclass first read", firstLine(subclassRenamed.stack));
const replaced = dyn(new Error("replaced"));
replaced.stack = "custom stack\n at nowhere";
describe("assigned", replaced);
console.log("assigned descriptor", Object.keys(Object.getOwnPropertyDescriptor(replaced, "stack")).join(","));
const subclassReplaced = dyn(new ParseError("replaced"));
subclassReplaced.stack = "subclass stack";
console.log("subclass assigned", subclassReplaced.stack);
const typedReplaced = new ParseError("typed write");
typedReplaced.stack = "typed subclass stack";
console.log("typed subclass assigned", typedReplaced.stack, dyn(typedReplaced).stack);
const subclassNumbered = dyn(new PlainSubclass("numbered"));
subclassNumbered.stack = 7;
console.log("subclass number", subclassNumbered.stack, typeof subclassNumbered.stack);
subclassNumbered.stack = "string again";
console.log("subclass string again", subclassNumbered.stack);
const numbered = dyn(new Error("numbered"));
numbered.stack = 5;
console.log("number", numbered.stack, typeof numbered.stack);
const removed = dyn(new Error("removed"));
delete removed.stack;
console.log("deleted", removed.stack, "stack" in removed, Object.hasOwn(removed, "stack"));
const spreadSource = dyn(new Error("spread"));
console.log("spread", Object.keys({ ...spreadSource }).length, Object.keys(Object.assign({}, spreadSource)).length);
console.log("inherited", firstLine(Object.create(dyn(new RangeError("prototype"))).stack));
const typed = new Error("typed");
const view = dyn(typed);
console.log("typed and dynamic", view.stack === typed.stack, firstLine(typed.stack));
const typedSubclass = new ParseError("typed subclass");
console.log("typed subclass", firstLine(dyn(typedSubclass).stack), dyn(typedSubclass).stack === typedSubclass.stack);
const assignedThroughView = new Error("typed");
dyn(assignedThroughView).stack = "through the view";
console.log("assigned through view", assignedThroughView.stack);
function inner() {
return dyn(new Error("deep"));
}
function outer() {
return inner();
}
const deep = outer().stack;
console.log("frames", deep.split("\n").length > 1, deep.includes("at inner"), deep.includes("at outer"));
function makeSubclass() {
return dyn(new PlainSubclass("deep"));
}
console.log("subclass frames", makeSubclass().stack.includes("at makeSubclass"));
@@ -0,0 +1 @@
{ "plain": 3232, "san": 4298 }
@@ -0,0 +1 @@
{ "plain": 3172, "san": 4044 }