perf: hot-path overhaul, zero runtime deps, full quality/docs pass - #46
Merged
Conversation
… directory Running bench/class-validator/*.cv.bench.ts from the repo root crashes: bun resolves tsconfig from the working directory, so the root tsconfig (native TC39 decorators) applies and class-validator's legacy-decorator API throws in ValidateBy. Add `bench` / `bench:cv` scripts — the cv runner cds into bench/class-validator so its own tsconfig (experimentalDecorators) applies — and a bench/README.md documenting the two suites and the cwd caveat. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…reallocated element loops Replace the @zipbul/result err()/isErr() protocol in generated deserialize executors with the raw BakerIssue[] array as the failure value, discriminated by Array.isArray — a success is always a class instance and seal now rejects Array-exotic DTO classes (new BakerError at seal: "DTO classes must not extend Array"), making the sentinel sound including under allowClassDefaults' `new Cls()` path. err() cost 48.5ns per wrap (alloc + Object.freeze) — 55% of the whole invalid path; validate executors already used a null-sentinel, deserialize now matches. @zipbul/result is removed entirely (zero runtime dependencies). Element-loop machinery, via centralized emit helpers (emitErrPush/emitMarkDecl/ emitMarkCheck — no raw push/length emission sites remain): - lazy error lists: `errors = null`, allocated on first push — no allocation on the valid path, and no per-element allocation in nested loops - nested-[Dto] output arrays preallocated with a write cursor instead of push-growth - nested executor objects hoisted out of element loops (object hoist, preserving the method receiver) Behavior-preserving: issue payloads, codes, paths, and ordering are identical; only generated-code text changed (snapshots updated and hunk-verified). Adversarially reviewed (Codex + Grok, spec and implementation rounds), empirically probed on all-valid/all-invalid/mixed/empty/stopAtFirstError/autoConvert/circular/cache-reuse paths. Benchmarks (Bun 1.3.14, mitata, median): - simple valid 44.5→32.9ns, invalid 102→55.9ns - nested valid 41.7→20.4ns, invalid 306→157.6ns - array-1000 deserialize 16.4→9.4µs - error-collection 102→59.4ns Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With a groups object the per-call options guard cost ~62ns — dominated by the Object.keys() array allocation and the groups.some() closure — on the documented per-request groups pattern. Rewrite the loop as `for...in` guarded by Object.hasOwn (inherited enumerables from Object.prototype pollution are filtered, preserving exact Object.keys membership and order) and validate groups with an indexed loop whose `i in groups` hole-check matches Array.prototype.some's HasProperty semantics, so sparse arrays stay accepted. Semantics and every BakerError message are byte-identical; the undefined fast path is untouched. ~1.7x faster (6.9→4.1ns in a same-process A/B), zero allocation. Regression tests cover prototype pollution (own-key behavior preserved both ways) and sparse groups arrays. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rated code Delete FieldMetaApplier's #wrapTransform runtime closure: TransformDef now stores the user's raw transform fn (isAsync still detected at decoration via isAsyncFunction), and the "sync transform returned Promise" BakerError guard is emitted as inline generated code after each sync transform call — async transforms emit no guard at all. This removes two extra call frames (refs[N] → wrapper → user fn) plus an isPromiseLike() call per transform per direction. BakerError joins the deserialize executor's closure params (serialize-builder already had it). Codegen simplifications that fall out: the hand-unrolled 1-/2-transform special cases collapse into the general statement-per-transform loop, and serialize's buildTransformExpr emits statement-per-transform (with per-sync-step guards into a reused per-field temp var) whenever a chain contains a sync transform — pure-async chains keep the nested-expression form. The per-transform await/guard decision now uses `td.isAsync ?? isAsyncFunction(td.fn)`, matching AsyncAnalyzer's established fallback — previously the codegen read the bare flag, silently skipping `await` for off-decorator metadata that omitted isAsync. Behavior-preserving: guard predicate ≡ isPromiseLike, messages byte-identical, throw timing identical at every chain position in both directions (new mid-chain e2e tests). Adversarially reviewed (Codex + Grok): zero surviving findings, including old-vs-new async-detection equivalence and serialize's reversed execution order. Benchmarks: deserialize 1/2 sync transforms 13.7→12.4 / 17.1→13.9ns; serialize 1/2 sync transforms 16.9→9.4 / 25.2→14.2ns. Plain fields unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… causes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…c type exports Full-repo quality audit follow-up (three parallel audits: per-directory code sweep, docs-vs-source, public-surface consistency). All changes are behavior-preserving — generated code is byte-identical (zero snapshot changes). Deduplication: - makeRegexRule() in rules/string-shared.ts replaces 38 copy-pasted single-regex "test-then-fail" rule bodies across 8 string-rule files - resolveNestedExecutor / resolveFieldSkip (direction-parameterized) / emitPromiseGuard extracted to seal/codegen-utils.ts — each was duplicated between the deserialize and serialize builders - loadPeerDependency shared by the luxon/moment transformers; its thunk-based shape finally makes the missing-peer catch testable (new 1:1 spec, previously a documented untestable branch) Type strictness / SSOT: - rules/public.ts now exports EmittableRule and WidenLiteral (verified consumer repros: TS2305 importing EmittableRule from @zipbul/baker/rules; TS2742 naming WidenLiteral in a generic wrapper around equals/isIn); WidenLiteral also exported from the root - typedBuckets typed via a single TYPE_GATE_KINDS tuple (drops two non-null assertions) - FieldOptions.message reuses MessageArgs; TypeThunk shared by TypeDef.fn and FieldOptions.type (drops a cast); redundant casts removed (binary.ts, type-normalizer); meta-store avoids re-reading [Symbol.metadata] with a non-null assertion Docs-in-code: - errors.ts reserved-code list gains the emitted-but-undocumented 'circular' and 'isDefined'; isBakerIssueSet example uses a Baker instance; stale/misleading comments reworded (seal-time vs runtime, dead-guard phrasing); resolvedClass invariant documented Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rError causes README accuracy pass from the docs-vs-source audit — every added claim verified against the source: - Rules: add the missing Combinators (oneOf/arrayEvery), Object (isNotEmptyObject/ isInstance), and Binary (isUint8Array/isByteSize) sections; complete the Type Checkers list (isRegExp/isFunction/isStatelessRegExp); add a curated-highlights caveat under the "114 built-in rules" heading pointing at @zipbul/baker/rules as the full surface - Error handling: expand the BakerError causes to match errors.ts (including the new extends-Array seal-time rejection and why), and note that the luxon/moment transformers throw BakerError at await when the peer is missing - Zero runtime dependencies: claimed in the README intro, the Why-baker table, and package.json's description. The comparison table's stale "reflect-metadata: Required" row for class-validator was replaced with an accurate Dependencies row — class-validator 0.15 depends on validator + libphonenumber-js, not reflect-metadata (the FAQ line repeating that claim is fixed too) - Config example gains the missing `debug` key; the @field options table gains the `rules` row; Exports lists ArrayOfMarker and the /symbols subpath; Map wording and union-domain rules (isLatitude/isLongitude) clarified; Luxon/MomentTransformerOptions mentioned Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…owing assertions Final pass from the deep codegen-builder audit. Generated code is byte-identical — all 15 codegen snapshots unchanged. Emission single-sourcing (deserialize-codegen.ts): - emitFailStmt — the collectErrors push-vs-return split, previously hand-rolled at ~10 sites, completing the error-emission helper family - emitInvalidDiscriminatorDefault (4 near-identical default arms), emitValidateElement (3 copy-pasted validate-only element blocks), emitMarkedAssignTail (3 duplicated mark/assign tails), emitGroupsGuardPair - generateDiscriminatorEachCode and its ValidateOnly twin merged into one method-parameterized core; resolveTypeGate moved beside its analysis-phase siblings as a pure function Structure/strictness: - serialize-builder's 234-line generateFieldCode split into collection/discriminator/ nested methods, mirroring the deserialize side - provably-dead `if (!meta.type)` branches removed (the sole call site guards the narrowed TypeDef, now passed as a parameter) - lost-narrowing non-null assertions eliminated by threading narrowed values (meta.type/collection/discriminator params, an InlineTarget struct instead of a derived boolean); generateConversionCode derives its mode from skipVar, closing a skipVar=null+collectErrors=true footgun; lazy Set init via ??= - change-history narration comments replaced with current-contract statements Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uous The rejects assertion was unawaited and the .catch-based assertions would pass vacuously if the promise resolved. Await the outcome explicitly and fail loud when the loader unexpectedly resolves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Performance overhaul of the deserialize hot paths plus a full-repo quality and documentation audit (minor, 6.0.0 → 6.1.0). 10 commits, each adversarially reviewed (Codex + Grok; 10 review rounds total across spec/implementation/quality) with all findings fixed pre-commit.
Performance (measured, mitata medians)
groupsvalidation ~15×; sync transforms 1.3-1.8×BakerIssue[]internal failure protocol (Array.isArray sentinel), lazy error-list allocation, preallocated cursor output arrays, executor-object hoisting, allocation-freecheckCallOptions, inlined transform Promise guardConsumer-facing
@zipbul/resultremoved — was internal-only)ArraythrowBakerErrorat seal (protocol soundness; noted in the changeset as a behavior change)EmittableRule/WidenLiteralimportable from@zipbul/baker/rules(fixes real TS2305/TS2742 consumer repros)BakerErrorcauses, zero-deps claim, error-code list completed (circular,isDefined)Quality
makeRegexRule; 10+ builder duplications extracted into shared emit helpers; serializegenerateFieldCodeSRP split; provably-dead branches and lost-narrowing non-null assertions removed — all with byte-identical generated code (15 codegen snapshots unchanged throughout)loadPeerDependency+ spec)Verification
🤖 Generated with Claude Code