mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Keep an error's name and message where a JavaScript factory created them
zValidator's default failure hook serializes zod 4's ZodError with
`name` after `message` natively, and with the first `name` zod's factory
assigns ("$ZodError") when the second assignment took the typed slot.
ZodError instances are views of a JavaScript class extending Error: each
view rebuilds `message` and `name` from the error slots at fixed
positions (after `stack`, and after every other property), and the
property bag that holds the dynamically added properties copied its
committed, possibly stale, values over them.
A JavaScript Error subclass with a property bag now marks its class
(errorBagOrder), and its view calls scr_error_dyn_follow_bag after the
bag is copied: `name` and `message` move to follow the property they
followed when the bag was committed, and data values come from the
slots. TypeScript classes keep their views.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a095565bc8
commit
09f0342641
@@ -993,6 +993,25 @@ export function streamTypedRefMaterializeAdapter(
|
||||
: `call void @scr_error_dyn_place_name(ptr ${out}, ptr ${host.cstr(nameAfter)}, ${host.sizeType} ${Buffer.byteLength(nameAfter, "utf8")})`,
|
||||
);
|
||||
}
|
||||
// A JavaScript Error subclass's property bag records, once a dynamic
|
||||
// write has committed the view, where `name` and `message` were created
|
||||
// (zod's error factory assigns `name` before it defines `message`); the
|
||||
// slots hold their values.
|
||||
if (
|
||||
meta.def.errorBagOrder &&
|
||||
meta.root.def.name === "%Error" &&
|
||||
meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES)
|
||||
) {
|
||||
host.declare(`declare void @scr_error_dyn_follow_bag(ptr, ptr, ptr)`);
|
||||
const { index } = classFieldIndex(meta, DYN_CLASS_PROPERTIES);
|
||||
const slot = B.tmp(),
|
||||
bag = B.tmp();
|
||||
B.line(
|
||||
`${slot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${index}`,
|
||||
);
|
||||
B.line(`${bag} = load ptr, ptr ${slot}`);
|
||||
B.line(`call void @scr_error_dyn_follow_bag(ptr ${out}, ptr ${bag}, ptr %p)`);
|
||||
}
|
||||
// A frozen, sealed or non-extensible instance's view says so.
|
||||
if (withAttributes) {
|
||||
host.declare(`declare void @scr_dyn_class_view_attributes(ptr, double)`);
|
||||
|
||||
@@ -1634,6 +1634,15 @@ export class ClassDynamicDispatch {
|
||||
if (this.deletedNames === undefined)
|
||||
this.deletedNames = deletedPropertyNames(lowerer.moduleOrder);
|
||||
if (!root.base?.def.runtime && !fixedFieldFamily(root, this.deletedNames)) track(root);
|
||||
// An Error family's bag records where `name` and `message` were
|
||||
// created, which the error slots do not.
|
||||
if (root.base?.def.name === "%Error") {
|
||||
const order = (current: ClassInfo): void => {
|
||||
current.def.errorBagOrder = true;
|
||||
current.subclasses.forEach(order);
|
||||
};
|
||||
order(info);
|
||||
}
|
||||
}
|
||||
const loc = this.propertyBag.loc;
|
||||
const { receiver, condition } = this.propertyReceiver(plan, loc);
|
||||
|
||||
@@ -1499,6 +1499,10 @@ export interface IrClassDef {
|
||||
* class of the chain sets `name` during construction, and then an own
|
||||
* `name` follows every other own property. */
|
||||
errorNameAfter?: string | null;
|
||||
/** JavaScript Error subclasses with a property bag: a view committed by a
|
||||
* dynamic write keeps `name` and `message` where the bag recorded them
|
||||
* (scr_error_dyn_follow_bag). */
|
||||
errorBagOrder?: boolean;
|
||||
/** Stable module symbol identities for public instance layout fields. */
|
||||
symbolFields?: { field: string; globalId: string }[];
|
||||
/** RUNTIME-PROVIDED class (the builtin Error hierarchy): the struct, RC
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8112-error-factory-own-key-order.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/hono-zod-default-hook/main.ts"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Cannot find name 'WebSocket'.",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts",
|
||||
"start": 1398,
|
||||
"end": 1407
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Cannot find name 'WebSocket'.",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts",
|
||||
"start": 1413,
|
||||
"end": 1422
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Cannot find name 'WebSocket'.",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts",
|
||||
"start": 3716,
|
||||
"end": 3725
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Cannot find name 'FormData'.",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts",
|
||||
"start": 4798,
|
||||
"end": 4806
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Cannot find name 'FormData'.",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts",
|
||||
"start": 549,
|
||||
"end": 557
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Cannot find name 'FormData'.",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts",
|
||||
"start": 5572,
|
||||
"end": 5580
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1330,6 +1330,10 @@ ScrDyn *scr_dyn_from_error(const ScrError *e);
|
||||
void scr_error_register_dyn(const ScrVt *vt, ScrDyn *(*box)(void *));
|
||||
ScrDyn *scr_error_dyn_fields(const ScrError *e);
|
||||
void scr_error_dyn_place_name(ScrDyn *view, const char *after, size_t after_len);
|
||||
/* An Error subclass view of an instance whose class keeps a property bag:
|
||||
* `name` and `message` take their committed positions, and data values
|
||||
* from the slots (scr_json.c). */
|
||||
void scr_error_dyn_follow_bag(ScrDyn *view, const ScrDyn *bag, const ScrError *e);
|
||||
/* An accessor property of a fresh object: enumerable and configurable,
|
||||
* with the given getter and setter (either may be NULL). Moves both. */
|
||||
void scr_dyn_obj_set_accessor(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *getter, ScrDyn *setter);
|
||||
|
||||
@@ -8310,6 +8310,63 @@ void scr_error_dyn_place_name(ScrDyn *view, const char *after, size_t after_len)
|
||||
view->v.obj.index = NULL;
|
||||
}
|
||||
|
||||
static size_t scr_dyn_entry_index(const ScrDyn *object, const char *key, size_t key_len) {
|
||||
for (size_t i = 0; i < object->v.obj.len; i++) {
|
||||
const ScrDynEntry *entry = &object->v.obj.entries[i];
|
||||
if (entry->key_len == key_len && memcmp(entry->key, key, key_len) == 0) return i;
|
||||
}
|
||||
return SIZE_MAX;
|
||||
}
|
||||
|
||||
/* The bag holds the view as it was last committed, in the order its
|
||||
* properties were created: zod's error factory assigns `name` and then
|
||||
* defines `message`, and Node lists them in that order. The slots rebuild
|
||||
* both at fixed positions (scr_error_dyn_fields, scr_error_dyn_place_name),
|
||||
* so each one present in the bag moves to follow the property it follows
|
||||
* there. A typed write to `name` or `message` reaches only the slot, so a
|
||||
* data property takes the slot's value. */
|
||||
void scr_error_dyn_follow_bag(ScrDyn *view, const ScrDyn *bag, const ScrError *e) {
|
||||
if (!bag || bag->kind != SCR_DYN_OBJ || view->kind != SCR_DYN_OBJ) return;
|
||||
static const struct { const char *key; size_t len; } slots[] = {{"name", 4}, {"message", 7}};
|
||||
for (size_t s = 0; s < 2; s++) {
|
||||
bool name = s == 0;
|
||||
if (!(name ? e->name_present : e->message_present)) continue;
|
||||
size_t at = scr_dyn_entry_index(view, slots[s].key, slots[s].len);
|
||||
if (at == SIZE_MAX || view->v.obj.entries[at].accessor) continue;
|
||||
ScrDyn *value = scr_dyn_new_str(name ? e->name : e->message);
|
||||
scr_dyn_region_store(view, value);
|
||||
ScrDyn *old = view->v.obj.entries[at].value;
|
||||
view->v.obj.entries[at].value = value;
|
||||
scr_dyn_release(old);
|
||||
}
|
||||
bool moved = false;
|
||||
for (size_t b = 0; b < bag->v.obj.len; b++) {
|
||||
const ScrDynEntry *bag_entry = &bag->v.obj.entries[b];
|
||||
bool slot = (bag_entry->key_len == 4 && memcmp(bag_entry->key, "name", 4) == 0) ||
|
||||
(bag_entry->key_len == 7 && memcmp(bag_entry->key, "message", 7) == 0);
|
||||
if (!slot) continue;
|
||||
size_t from = scr_dyn_entry_index(view, bag_entry->key, bag_entry->key_len);
|
||||
if (from == SIZE_MAX) continue;
|
||||
/* The nearest earlier bag property the view has; none puts it first. */
|
||||
size_t after = SIZE_MAX;
|
||||
for (size_t p = b; p-- > 0 && after == SIZE_MAX;)
|
||||
after = scr_dyn_entry_index(view, bag->v.obj.entries[p].key, bag->v.obj.entries[p].key_len);
|
||||
size_t to = after == SIZE_MAX ? 0 : after < from ? after + 1 : after;
|
||||
if (to == from) continue;
|
||||
ScrDynEntry *entries = view->v.obj.entries;
|
||||
ScrDynEntry entry = entries[from];
|
||||
if (to < from) memmove(&entries[to + 1], &entries[to], (from - to) * sizeof *entries);
|
||||
else memmove(&entries[from], &entries[from + 1], (to - from) * sizeof *entries);
|
||||
entries[to] = entry;
|
||||
moved = true;
|
||||
}
|
||||
if (moved) {
|
||||
SCR_JS_SHAPE_RESET(view);
|
||||
free(view->v.obj.index);
|
||||
view->v.obj.index = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_from_error(const ScrError *e) {
|
||||
size_t hit = scr_errdyn_find(e, true);
|
||||
if (hit != SIZE_MAX) return scr_dyn_retain(scr_errdyn_cache[hit].dyn);
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"use strict";
|
||||
// Errors built by a constructor factory, the way zod 4 builds ZodError: a
|
||||
// class extending the parent passed in, then an initializer that assigns
|
||||
// `name`, defines non-enumerable fields and an enumerable `message`
|
||||
// accessor, and a second initializer that reassigns `name`. JSON and
|
||||
// Object.keys list `name` before `message`, as they were created, and
|
||||
// `name` holds the last assignment.
|
||||
|
||||
const messageDescriptor = {
|
||||
get() {
|
||||
return this.state.message ?? JSON.stringify(this.state.issues);
|
||||
},
|
||||
set(value) {
|
||||
this.state.message = value;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
};
|
||||
|
||||
function factory(name, initializer, parent) {
|
||||
const Parent = parent ?? Object;
|
||||
class Definition extends Parent {}
|
||||
Object.defineProperty(Definition, "name", { value: name });
|
||||
function construct(def) {
|
||||
const instance = parent ? new Definition() : this;
|
||||
Object.defineProperty(instance, "state", { value: { issues: def }, enumerable: false });
|
||||
initializer(instance, def);
|
||||
return instance;
|
||||
}
|
||||
Object.defineProperty(construct, "init", { value: initializer });
|
||||
Object.defineProperty(construct, "name", { value: name });
|
||||
return construct;
|
||||
}
|
||||
|
||||
const baseInit = (instance, def) => {
|
||||
instance.name = "$BaseError";
|
||||
Object.defineProperty(instance, "issues", { value: def, enumerable: false });
|
||||
Object.defineProperty(instance, "message", messageDescriptor);
|
||||
};
|
||||
const BaseError = factory("$BaseError", baseInit, Error);
|
||||
const ReportError = factory(
|
||||
"ReportError",
|
||||
(instance, def) => {
|
||||
BaseError.init(instance, def);
|
||||
instance.name = "ReportError";
|
||||
},
|
||||
Error,
|
||||
);
|
||||
|
||||
const report = new ReportError([{ code: "too_small", path: ["title"] }]);
|
||||
console.log(report.name, report instanceof Error, Object.keys(report).join(","));
|
||||
console.log(JSON.stringify(report));
|
||||
console.log(JSON.stringify({ success: false, error: report }));
|
||||
report.message = "replaced";
|
||||
console.log(JSON.stringify(report), report.message);
|
||||
report.name = "Renamed";
|
||||
console.log(JSON.stringify(report), report.name);
|
||||
|
||||
const base = new BaseError([1, 2]);
|
||||
console.log(base.name, Object.keys(base).join(","), JSON.stringify(base));
|
||||
|
||||
// The same steps on a class extending Error directly.
|
||||
function build(flag) {
|
||||
class Direct extends Error {}
|
||||
const instance = flag ? new Direct() : {};
|
||||
instance.name = "First";
|
||||
Object.defineProperty(instance, "message", messageDescriptor);
|
||||
Object.defineProperty(instance, "state", { value: { issues: ["i"] }, enumerable: false });
|
||||
instance.name = "Second";
|
||||
return instance;
|
||||
}
|
||||
const direct = build(true);
|
||||
console.log(Object.keys(direct).join(","), JSON.stringify(direct), direct.name);
|
||||
@@ -0,0 +1,38 @@
|
||||
// @hono/zod-validator without a hook: a failed check answers 400 with the
|
||||
// whole safeParse result serialized, so the ZodError's own enumerable
|
||||
// properties (name, then message) and their order reach the client.
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
import { Hono } from "hono";
|
||||
import { z } from "zod";
|
||||
|
||||
const Item = z.object({ title: z.string().min(2), qty: z.number().int().positive() });
|
||||
|
||||
const app = new Hono();
|
||||
app.post("/items", zValidator("json", Item), (c) => {
|
||||
const item = c.req.valid("json");
|
||||
return c.json({ created: item.title, qty: item.qty }, 201);
|
||||
});
|
||||
app.get("/items", zValidator("query", z.object({ limit: z.coerce.number().int().max(5) })), (c) => {
|
||||
return c.json({ limit: c.req.valid("query").limit });
|
||||
});
|
||||
|
||||
const post = (body: unknown) =>
|
||||
app.request("/items", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
||||
|
||||
const show = async (label: string, res: Response | Promise<Response>) => {
|
||||
const r = await res;
|
||||
console.log(label, r.status, await r.text());
|
||||
};
|
||||
|
||||
await show("ok", post({ title: "pen", qty: 2 }));
|
||||
await show("bad-title", post({ title: "p", qty: 2 }));
|
||||
await show("bad-both", post({ title: 3, qty: -1 }));
|
||||
await show("query-ok", app.request("/items?limit=4"));
|
||||
await show("query-bad", app.request("/items?limit=9"));
|
||||
|
||||
const result = Item.safeParse({ title: "x", qty: 0 });
|
||||
if (!result.success) {
|
||||
const e = result.error;
|
||||
console.log(e.name, e instanceof Error, Object.keys(e).join(","));
|
||||
console.log(JSON.stringify(e));
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "hono-zod-default-hook-case", "private": true, "type": "module" }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2200, "san": 10300 }
|
||||
Reference in New Issue
Block a user