Fused tier-0 typed parsing: one lazy pass for every target type (two-tier make)#473
Open
quinnj wants to merge 10 commits into
Open
Fused tier-0 typed parsing: one lazy pass for every target type (two-tier make)#473quinnj wants to merge 10 commits into
quinnj wants to merge 10 commits into
Conversation
JSON object roots parsed into non-:hot, interpreter-eligible struct types
now materialize through the untyped engine (compiled once, covered by the
package workload) and construct through StructUtils' tier-0 field-table
interpreter. Measured against master on a request-shaped family: marginal
first-parse per new struct family 3.7s -> 0.07s, steady state 51us -> 18us.
Everything else — :hot types, custom dicttype/null, non-object roots,
choosetype-bearing types (recursively, via StructUtils.interptreesafe,
since choosetype functions receive the raw source and are written against
lazy values) — keeps the classic lazy descent with identical semantics.
The classic fallback sits behind an invokelatest boundary in JIT sessions:
with the route decided by a runtime table lookup, a directly-reachable
specialized descent gets compiled per type even when never taken — the
exact first-call cost the tree route exists to eliminate. Trim builds keep
the direct static call (the tree route is compile-time disabled there;
typed parsing under --trim is the :hot tier's job).
__init__ registers the :hot precompile hook: for each :hot-annotated type
defined downstream, parse user-provided samples plus a field-table-
synthesized sample (and "{}") and write the result back out, compiling the
typed lazy descent and write path into the defining package's image.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ntry glue The tree route (untyped materialize, then interpret) becomes one pass: applyobject drives the field-table interpreter's slots directly, with PtrString field matching (no allocation), per-kind scalar materialization through the interpreter's leaf ladder, unknown keys skipped without materializing, and ANY/CUSTOM subtrees materialized via the existing engine (CUSTOM keeps the 4-arg make with the field's tags). The engine is parameterized by style only and sits behind an @noinline boundary — inlined into the per-type entry, the JIT re-infers the whole engine per target type. The route now requires the default read style exactly; custom styles keep classic semantics wholesale. Entry glue is de-specialized (@nospecialize on invalid/checkendpos/ jsonreadstyle/unknownfielderror): interpolating the target type into error strings drags type-show machinery into every type's inference. This is the same defect class the commit-level bisect identified upstream — 53df2b3 'feat(parse): add unknown_fields keyword (#451)' regressed marginal per-family TTFX 0.70s -> 6.3s on 1.5.1 via its per-type error/validation helpers, and 8154d24 'fix(parse): forward type predicates to inner style (#454)' regressed steady state ~31us -> ~66us on 1.6.0; both fixes here apply upstream independently of the tier work. Measured vs master (request-shaped family): marginal first parse per new struct family 3.7s -> 0.15s (24x), steady 51us -> 5.9us (8.7x). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
string(::TimeType) routes through Dates' DateFormat machinery (dynamic lpad/repeat) under juliac --trim; the fixed-width renderings are equal to the string() forms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…get zero Ports the StructUtils JuliaC harness: the temp env devs this checkout plus the resolved StructUtils (dev checkouts are dev'd; registry copies added), pins Parsers#trim-verifier-fixes until #207 releases, exports JULIAC_DISABLE_PRECOMPILE_WORKLOADS to juliac sessions, and sets the trim_build preference (pruning the tier-0 tree route and its invokelatest boundary out of the typed-parse entry). The workload exercises the :hot specialized lazy descent with heterogeneous scalars — dates, UUID, Symbol, unions, nested structs, vectors — plus untyped parse with isa-narrowing, defaults-only parses, and the required-field error path; runtime asserts run in the produced binary. Verifier-driven fixes along the way: error builders use isa-laddered string extraction with per-branch typeasserts (no repr/show of Symbols or types — also removes the per-target-type show inference the 1.5.1 regression introduced); _typenamestr unwraps UnionAlls by hand (Base's nameof(::UnionAll) recurses through an Any-typed body); the #472 hand-formatted ISO date lowers are cherry-picked (Dates.format's padding is not verifier-resolvable). JSON.json of structs remains follow-up work (the writer's BigFloat arm and array-show typeinfo are pre-existing master reachability), so the workload pins the read path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parse.jl returns to entries + classic + untyped engine; the fused lazy interpreter drive, the route memo, the :hot precompile hook, and the sample synthesizer move to tier0.jl with a file-level architecture note. Comment refresh on the invokelatest boundary. No behavior change (suite and trim harness green before and after). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bulk documents lose to a compiled descent under per-element interpretation (fuzz worst case 12x on an 11KB vector-of-structs doc), so object roots at or above 4KiB take the classic specialized route through the existing boundary — a type that parses bulk documents is worth its one-time compile, which is exactly what every type cost before tier-0. The route memo becomes copy-on-write (one atomic load per read): the previous lock dominated sub-microsecond parses of near-empty documents. Fuzz-corpus effect (226 cases vs JSON 1.4.0 + StructUtils 2.6.2): the big-class catastrophes disappear (worst 0.082 -> 0.78 median class ratio); full/minimal classes hold at 2.7x/2.0x geomean faster. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eSpecs The lazy drive walks the spec tree (union arms picked by JSON value type, nullable elements handled in the array closure); dict/custom positions materialize their subtree and reuse the interpreter's boxed arms. Sample synthesis recurses the same specs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The size auto-tier (_FUSED_MAX_BYTES), the route memo, and the classic invokelatest fallback are gone: under the default read configuration every non-:hot typed parse drives the field-table interpreter in one lazy pass — struct objects and element vectors stream directly, custom kinds hand the RAW lazy value to the generic machinery (choosetype/lift/make overloads see exactly what the specialized descent hands them — this dissolves the treesafe gate), and every other (kind, shape) pairing materializes its subtree into the interpreter's boxed arms. Root capability: any T parses through rootspec (scalars, containers, tuples, sets, unions, null); struct-from-array fills positionally (FusedPosClosure — classic's lazy applyeach handed Int keys); fixed-size array roots keep the raw-lazy generic route (dimension discovery). JSONText raw capture routes via the custom-make classification. Explicit nulls take the Missing arm first (classic @_peel order). Alias tuples and raw name tags match through _findspec on the fused key path. Custom dicttype/null and custom inner styles take the fully-specialized descent: per-style lazy lifts and trait overrides are invisible to the engine's materializing ladder, so those hooks keep classic semantics at classic cost. :hot and trim builds are unchanged (specialized static descent; the trim harness compiles under trim_build=true). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… parses Deleting the invokelatest boundary during the fused rewiring re-created the dead-arm compilation cost it existed to remove: the specialized make(style, T, x) arm was JIT-compiled for every target type even when the fused route was always taken (fuzz sum-of-first-parse blew up 7.6x vs base). Types that genuinely route there (:hot, custom styles/dicttype/ null) pay one dynamic dispatch per parse; trim builds keep the static call graph in a dedicated compile-time arm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…style tests
The Any-make fallback interpolated the LazyValue into its error string —
show(::LazyValue) materializes and prints a LazyArray through Base's
matrix-show machinery, ~40 unresolved verifier errors under --trim from
one statement. It now renders position + type through the trim-clean
invalid() helper.
Re-enable the CustomJSONStyle tests (commented since the old choosetype
function hook was removed): per-style lift overloads work through the
existing inner-style forwarding, and @choosetype-emitted make overrides
dispatch on the read wrapper carrying the custom style as its third
parameter (JSONReadStyle{<:Any,<:Any,CustomJSONStyle}) — only traits and
lift forward to the inner style, make methods dispatch on the wrapper.
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.
Fused tier-0 typed parsing: one lazy pass for every target type
Companion to JuliaServices/StructUtils.jl#63 (requires it; the branch dev-pins it locally — release coordination needed before merge).
Under the default read configuration, every non-
:hottypedJSON.parse(str, T)drives the StructUtils field-table interpreter directly from the lazy tokens — no intermediate tree, no per-type compilation beyond a field-table build:FusedObjClosure/FusedArrClosure/FusedPosClosure), scalar leaves parse straight off the token into the interpreter's typed leaf ladder; structs also fill positionally from arrays (classic parity).choosetype,lift, andmakeoverloads (e.g.JSONTextraw capture) see exactly what the specialized descent hands them. This dissolves the old tree-safety gating.:hottypes (specialized descent, precompiled into the annotating package's image via the registered hook + synthesized samples), customdicttype/null(materialization semantics), and custom inner styles (per-style lazylifts and trait overrides are invisible to the engine) — all behind aninvokelatestboundary so the never-taken specialized arm is not JIT-compiled per target type (deleting that boundary measured a 7.6× first-parse-sum regression; restoring it landed at 48.1s vs 77.1s for JSON 1.4.0 across the 216-case fuzz corpus).Any-make error path renders statically (interpolating a LazyValue dragged Base's matrix-show machinery into trimmed binaries as ~44 verifier errors).liftworks via inner-style forwarding;@choosetype/makeoverrides dispatch on the read wrapper (JSONReadStyle{<:Any,<:Any,MyStyle}) — now documented in the tests.Fuzz vs 1.4.0 (80 families / 216 cases, seeded): steady parse geomean 1.80× faster (request-shaped docs 3.6×), write 1.10×, first-parse sum 1.6× faster, zero errors, value-identical output (23 hash diffs are dict key order — this branch writes
AbstractDictkeys sorted). Known regressions: bulk-vector docs ~0.64×, direct container roots ~0.56×, sub-µs empty-doc floors — the interpreter-vs-compiled-descent trade;:hotrecovers known-bulk types.🤖 Generated with Claude Code