Skip to content

fix(avro): resolve field names by NAME when the codec proves it, and reject nested selectors (#95) - #105

Merged
kryptt merged 6 commits into
mainfrom
fix/nominal-resolution-onto-main
Sep 18, 2026
Merged

kryptt merged 6 commits into
mainfrom
fix/nominal-resolution-onto-main

Conversation

@kryptt

@kryptt kryptt commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Why this looks like an already-merged PR

This is #98's content, unchanged, re-based onto main. #98 shows "Merged" on GitHub, but its
six commits never reached main.

#97 (fix/fields-namedtuple-tuplen-96) squash-merged to main at 09:23:45 as 8d0cb29. #98 was
stacked on that branch, and merged 28 seconds later — into fix/fields-namedtuple-tuplen-96,
a branch that had already been collapsed into main. The merge landed on a ref nothing points at.

Verified three ways:

check result
git merge-base --is-ancestor 7fc97fe origin/main not an ancestor
git show origin/main:avro/…/AvroWalk.scala | grep -c caseNames 0 (the nominal rung's own identifier is absent from main)
gh api repos/…/compare/main...7fc97fe --jq .status diverged

GitHub refuses to retarget a merged PR ("Cannot change the base branch of a closed pull request"),
so recovery is this fresh branch off main with #98's six commits cherry-picked — no rebase,
squash, amend or reorder.

The diff is byte-identical to #98's. git diff origin/main..HEAD and
git diff 37940df..origin/fix/nominal-field-resolution-hazard are both 2183 lines and diff -q
reports them equal. 19 files changed, 1698 insertions, 91 deletions.

Follow-up: #102 (hearth/kindlings bump) was retargeted to main and currently carries this
content in its diff. Once this PR merges, #102 must be rebased onto main so it carries only the
bump.


Fixes the silent field mis-targeting surfaced while investigating #95, plus a second, independent
hazard with the same symptom found while reproducing it.

This does not close #95. That issue asks for an efficient whole-record construction primitive,
and nothing here addresses it. What it does close is the defect its reporter's codec shape exposed:
their schema carries computed/derived columns ("no equivalent of a computed/derived field mechanism"
in the issue body), which is exactly the shape that made every .field(_.x) past the divergence
read and write the wrong slot.

The hazard

.field(_.x) resolved x to the schema field at its DECLARATION INDEX and nothing else.
AvroWalk.fieldNameAt's whole body was fields.get(declIdx).name, guarded only against a
non-record parent (AvroWalk.scala:414) and an index past the end (AvroWalk.scala:422). The
field's name, its type and the record's arity were never consulted.

That is sound exactly while the codec's schema is positionally 1:1 with the case class — true by
construction for every kindlings-derived codec, and not true for a hand-written or
vulcan.Codec field list, which can add a computed column, drop one, or reorder:

// vulcan schema {a, computed, b, c}   vs   case class Three(a, b, c)
codecPrism[Three].field(_.b).getOption(bytes)      // Some("A0|B0")  <- read `computed`
codecPrism[Three].field(_.b).replace("B1")(bytes)  // rewrote `computed`

Valid Avro bytes, wrong content. Nothing catches it: get-put, put-get, put-put and modify-fusion
all hold on a mis-targeted optic (it is a perfectly lawful Optional onto the wrong field),
round-trips hold, and every fixture in AvroSpecFixtures is kindlings-derived, hence 1:1, hence
blind. One function, six call sites:

# Site file:line (before)
S1 AvroPrism.widenPath ← .field(_.x) AvroPrism.scala:249
S2 same ← selectDynamic AvroPrismMacro.scala:40
S3 AvroPrism.resolveFieldNames ← .fields(...) AvroPrism.scala:302
S4 AvroTraversal.widenSuffix ← .each.field(_.x) AvroTraversal.scala:158
S5 AvroTraversal.toFieldsTraversal ← .each.fields(...) AvroTraversal.scala:219
S6 every deeper hop after .union[B] / a nested .field AvroWalk.scala:351-380

The mechanism, and why all-or-nothing

One rung above position, firing only when the codec has proved the whole correspondence:

  1. Nominal, all-or-nothing. If EVERY case field maps to a DISTINCT schema field — exactly, or
    uniquely up to _/-/. and case — the map is total and injective, so it is the codec's own
    answer, not a guess. Partial or colliding coverage disqualifies the rung for every field.
  2. Positional (Field navigation must honor the schema's field names, not the Scala field names #35), unchanged. Where a name transform lands, because a transform REMOVES the
    literal Scala name by construction, so rung 1 cannot have fired.

The totality requirement is the load-bearing part, and it is measured, not argued. The per-field
form of rung 1 ("does THIS field's name appear?") INTRODUCES corruption: FpVisit(userId, user)
against legacy columns {uid, user_id} is correct by position today, and a per-field rung re-aims
userId at user_id — a currently-working call site silently moved to the wrong column. Requiring
totality makes the rung abstain there (user matches nothing) and position stays right.

An arity gate was rejected on the same kind of evidence: it refuses 13 of 28 legitimate call
sites — abbreviated wire names plus a trailing ingested_at, a dropped cachedHash, a trailing
checksum on an element codec, a digest in a nested codec — every one correct today because the
extra field is at the tail and shifts nothing. It buys 3 cells.

False-positive scorecard (ResolutionFalsePositiveSpec, 28 legitimate call sites)

Scored against SLOT TRUTH — which schema slot a write actually touched, never copy(...), which
cannot tell a wrong slot from a stale derived one.

CORRECT LOUD-REFUSAL SILENT-WRONG SILENT-MISS
before (published behaviour) 28 0 0 0
after (this PR) 27 1 0 0

The single moved cell is hatch-record-probe-absent — .fieldNamed on a name the reader schema
does not carry, None before and a construction-time refusal now. That is the deliberate change
below. The two TRIPWIRE: examples (zero SILENT-WRONG, zero SILENT-MISS) pass both ways and are
the whole safety argument for touching the resolver.

AvroFieldNamingSpec's other 8 examples — issue #35's own snake_case regression suite — pass
untouched.

.fieldNamed is checked at construction

The escape hatch every error message points at appended the literal with no schema lookup at
all
, although the prism already holds the schema. A typo — or the Scala field name passed where
the schema name was meant — was a runtime SILENT MISS: reads None, writes hand back the payload
unchanged and report success. Sending someone from a loud construction failure to a silent runtime
one is the wrong direction.

Two carve-outs: a MAP parent (.fieldNamed is also how a map KEY is addressed, keys are data,
and feature-detecting an absent key is legitimate — three cells pin it) and an unresolvable parent
path (the walk already reports that, and refusing would change the meaning of a prism deliberately
built against a drifted root schema). Record-level feature detection moves to
codec.schema.getField(name).

Two existing examples change: AvroFieldNamingSpec's "a bad explicit .fieldNamed misses (None),
it does not corrupt" pinned the defect and now asserts the refusal, and AvroBytesSpec keeps its
PathMissing coverage by storing the PathStep.Field("name") through the internal constructor that
example already uses two lines above.

The nested-selector hole (second, independent bug)

MacroSelectors.extractFieldName matched Lambda(_, Select(_, name)) with any receiver. A
nested path _.inner.y is Select(Select(Ident(_), "inner"), "y"), so it matched, yielded the bare
name "y", and every cursor macro resolved y on the parent:

// NOuter(inner: NInner(x, y), y)
codecPrism[NOuter].field(_.inner.y).getOption(bytes)  // Some("OUTER_Y")
codecPrism[NOuter].field(_.inner).field(_.y)          // Some("INNER_Y")

Silent corruption on a perfectly 1:1, kindlings-derived codec, with no schema divergence involved
at all
— and the macros' own "nested paths are not yet supported inside a single call; chain them"
abort was unreachable for exactly the shape it was written for. 100% decidable at compile time.
LensMacro never had this (it uses the strict extractSingleFieldName plus a knownFields.contains
check); the cursor macros simply never got the same treatment.

Both halves land: the extractor now requires the Select receiver to be the lambda parameter
(keeping the wrapper tolerance that is the only reason it exists beside the strict form), and
requireCaseField[A] aborts when a single-hop selector names a non-case-field — those used to pass
through as a literal field name and miss at runtime. Five call sites: AvroPrismMacro.fieldImpl /
fieldTraversalImpl, JsonPrismMacro.fieldImpl / fieldTraversalImpl (eo-circe),
JsoniterPrismMacro.fieldName (eo-jsoniter, shared by prism and traversal). grep finds zero
nested-selector call sites in the repo; the circe, jsoniter, avro and generics suites all pass.

Documented residual limitations (ResolutionResidualSpec, pinned as executable examples)

The memo this PR implements asserted these by inspecting the rule. They are now run, and every
one behaves as predicted:

Cause 1 — no name signal (names unrecoverable AND the list permuted, so the rung abstains and
position decides, wrongly). Reachable only by a differential probe of the codec or an explicit
declaration.

  • 5a {beta_name, alpha_name} vs {alpha, beta} — reads beta for alpha
  • b2 {seq_no, event_ts} vs {occurredAt, seqNo} — reads the sequence number; a
    timestamp-millis logical type annotates the same physical long, so it cannot disambiguate
  • b6 compensating arity (one case field dropped, one computed column added): counts equal, position
    wrong from the insertion point on
  • at3 userId_ and user_id both normalise to userid — ambiguity is no signal, so position
    lands on the stale duplicate

Cause 2 — a MISLEADING name signal (a column bears a case field's name but holds a different
value; the map is total and injective, so it is trusted).

  • at1 {digest, id, raw_ident, payload} — id is a derived public identifier
  • at2 {USER_ID, user_ident, balance} — USER_ID is a stale legacy column

Both cause-2 cells were wrong on 0.15.1 too, with one real cost: the corrupt value is now more
plausible (pub-REAL-ID rather than a digest length). at1's payload is fixed by the change
— it read id before. .fieldNamed reaches all six, and that is asserted too.

Accepted behaviour change: a hand-written codec that permutes the Scala names (writes case
field a into a schema field literally named b, and vice versa) resolved correctly by position and
now resolves by name, i.e. wrongly. No name transform can produce that shape — a transform is a
function of the name alone — but a hand-written field list can.

Cost

Zero per operation, verified from bytecode rather than asserted. javap -p -c over the compiled
avro classes puts every reference to resolveFieldName / fieldNameAt / requireField* in
AvroPrism's widenPath / widenPathNamed / toFieldsPrism / resolveFieldNames and
AvroTraversal's widenSuffix / widenSuffixNamed / toFieldsTraversal. AvroFocus,
AvroFocus$Leaf, AvroFocus$Fields, AvroRecordPrism, AvroRecordTraversal and
AvroBinaryCursor contain none.

Construction time is not free, and the number is reported rather than waved at (best of 5 ×
200 000 builds, same box, same session):

base this PR
identity names .field(_.name) 84.9 98.6 ns/build
snake_case .field(_.clickId) 85.1 264.0 ns/build
nested .field(_.meta).field(_.performanceSourceId) 116.9 430.3 ns/build

The gap is inherent to totality: the rung resolves every case field before trusting any one of
them, so a snake_case parent runs a normalised name compare per (case field × schema field) pair
where position ran none. A first cut using Set[Int] for injectivity cost 316 / 462; the shipped
version scans the filled prefix of the result array instead and adds an early "more case fields than
schema fields ⇒ no total map" precondition. Paid once per drilled prism, strictly off the read/write
path.

Back-compat

Every touched signature is private[avro], but transparent inline bakes the accessor into
caller bytecode (inline$widenPath$i1 gains a List[String] parameter), so downstream must
recompile, not re-jar. MiMa is off build-wide (tlMimaPreviousVersions := Set.empty), so this is
a release note, not a build gate; mimaReportBinaryIssues is green.

Gates (re-run on this branch, on top of main)

JDK 25 (Temurin 25.0.4.1), so the kyo module was in the root aggregate.

gate result
sbt '++ 3' compile test (root aggregate) 862 passed, 0 failed, 0 errors — 409 tests, 212 avro, 92 core, 56 jsoniter, 42 circe, plus generics / schemes / schemesLaws / zio / kyo
sbt avroIntegration/test 212 / 212
sbt benchmarks/compile (outside the aggregate) 44 sources, clean
sbt mimaReportBinaryIssues clean
sbt docs/mdoc docs/laikaSite 20 mdoc files, 18 rendered documents
sbt 'scalafixAll; scalafmtAll' no changes produced (working tree stayed clean)
scalafmtCheckAll + scalafixAll --check + scalafmtSbtCheck + benchmarks/scalafmtCheck + githubWorkflowCheck all [success]

Follow-ups NOT in this PR

  • Rebase build(deps): hearth 0.4.2 / kindlings 0.3.2 — break the 23-element tuple ceiling on .fields #102 onto main once this merges, so the hearth/kindlings bump carries only the bump.
  • TargetingLaws / TargetingTests in cats-eo-laws — an optic vs. an extrinsic reference
    accessor. The only thing that can catch a resolution mechanism's own mistake, and the hazard
    class is carrier-wide (circe/jsoniter resolve by the literal Scala name, zio.schema's
    EoAccessorBuilder and kyo's Record.lens[F]("name") are the same shape). The fixtures land here;
    the reusable ruleset is next.
  • A differential codec probe — encode two sentinels through codec.encode, diff the slots,
    observe which schema field a case field actually lands in. The only mechanism that reaches residual
    cause 1, needs no declaration and no name heuristics, and would turn a wrong hand-written field
    mapping from believed into refused. Must degrade to the ladder rather than refuse.
  • AvroCodec.parseInputUnsafe's fabricated empty record (AvroCodec.scala:329-339) — a parse
    failure yields new GenericData.Record(schema), so modifyUnsafe on 4 junk bytes returns
    {"items": null}: a valid payload with all data erased. Adjacent silent-corruption hole, unrelated
    to resolution, cheap to fix.
  • AvroVulcan.codec's eager val schema; .at(i) has no ARRAY check (silent Miss on the whole byte
    face); AvroVulcan.codecMapped as a user-side escape for residual cause 1.

🤖 Generated with Claude Code

https://claude.ai/code/session_0194EHFR4NamCpTHiqy7B74V

kryptt and others added 6 commits September 18, 2026 13:10
…s it (#95)

`.field(_.x)` resolved `x` to the schema field at its DECLARATION INDEX and
nothing else: `AvroWalk.fieldNameAt`'s whole body was `fields.get(declIdx).name`,
guarded only against a non-record parent and an index past the end. The field's
NAME, its TYPE and the record's ARITY were never consulted.

That is sound exactly while the codec's schema is positionally 1:1 with the case
class — true by construction for every kindlings-derived codec, and NOT true for
a hand-written or `vulcan.Codec` field list, which can add a computed column,
drop one, or reorder. On those the optic silently targets the WRONG SLOT:

    schema {a, computed, b, c}  vs  case class Three(a, b, c)
    codecPrism[Three].field(_.b).getOption(bytes)     // Some("A0|B0")  <- `computed`
    codecPrism[Three].field(_.b).replace("B1")(bytes) // rewrites `computed`

and produces valid Avro bytes with wrong content. Nothing catches it: get-put,
put-get, put-put and modify-fusion all HOLD on a mis-targeted optic (it is a
perfectly lawful Optional onto the wrong field), round-trips hold, and every
fixture in `AvroSpecFixtures` is kindlings-derived, hence 1:1, hence blind.
One function, six call sites: `.field`, `selectDynamic`, `.fields`,
`.each.field`, `.each.fields`, and every deeper hop after a `.union[B]`.

The fix adds one rung ABOVE position, and only fires when the codec has proved
the whole correspondence:

  1. NOMINAL, all-or-nothing. If EVERY case field maps to a DISTINCT schema
     field — exactly, or uniquely up to `_`/`-`/`.` and case — the map is total
     and injective, so it is the codec's own answer, not a guess.
  2. POSITIONAL (issue #35), unchanged. This is where a name transform lands,
     because a transform REMOVES the literal Scala name by construction.

All-or-nothing is the load-bearing part, and it is measured, not assumed. The
per-field form of rung 1 ("does THIS field's name appear?") INTRODUCES
corruption: `FpVisit(userId, user)` against legacy columns `{uid, user_id}` is
correct by position today, and a per-field rung re-aims `userId` at `user_id`.
Requiring totality makes the rung abstain there — `user` matches nothing — and
position stays right. An arity gate was rejected on the same evidence: it
refuses 13 of 28 legitimate call sites (a trailing `ingested_at`, a dropped
`cachedHash`, a trailing checksum) to buy 3 cells.

Per-operation cost is zero: resolution runs once, at prism construction, off
the cached schema. `to`, `from`, `scan`, `spliceAff`, `spliceFoci` and both
erased bridges contain no reference to it on either carrier.

Known residual, pinned as specs rather than prose: a codec that both renames
beyond recognition AND reorders (equal arity, no name hit) still resolves by
position and is still wrong. Known behaviour change: a codec that PERMUTES the
Scala names was right by position and is now wrong by name — no transform can
produce that shape, only a hand-written field list.

Back-compat: every touched signature is `private[avro]`, but `transparent
inline` bakes the accessor into CALLER bytecode, so downstream must recompile,
not re-jar.

Tests:
- `DivergentCodecs` (new) — hand-written vulcan codecs whose schema is not
  positionally 1:1. The standing gap: every existing fixture is derived.
- `AvroNominalResolutionSpec` (new) — the 9 repro cells (computed field on the
  byte and record faces, the leaf past the divergence, `.fields` grouped
  read+write, `.each.field`, a nested record's own divergence, a reversed field
  list, snake_case + a computed field) plus the two false-positive controls.
  Every repro cell failed before this change; both controls passed.
- `ResolutionFalsePositiveSpec` (new) — 28 legitimate, currently-working call
  sites scored against SLOT TRUTH. 28/28 CORRECT before and after: that
  scorecard not moving is the entire safety argument for the mechanism.
- `AvroWalkSpec` — three direct `resolveFieldName` / `fieldNameAt` calls take
  the new case-name list (mechanical, `Nil`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194EHFR4NamCpTHiqy7B74V
…misses

The scaladoc that shipped with issue #35 was wrong in three ways, and each one
is a reason the hazard in #95 went unnoticed for a release:

- It warned only about schema field ORDER divergence. Order is not the shape
  that bit: a COMPUTED/derived schema column with no case-class parameter (or a
  dropped one) shifts every later slot on a codec whose field order is perfectly
  sensible.
- It never said the rule governs `.fields`, `selectDynamic` or `.each.field`.
  They share one resolver, and a grouped `.fields` write on a divergent parent
  damaged two slots and silently no-op'd a third.
- It advertised kebab-case as a supported transform. Kebab cannot produce a
  legal Avro schema at all — `SchemaParseException: Illegal character in:
  click-id` — so the claim was never true.

Rewritten to state the actual ladder (name-when-total, else position), the
precondition the positional rung needs, the three shapes that stay wrong after
it, and the one accepted behaviour change. Same pass over the docs site's
"Field navigation is by SCHEMA name" section and `AvroWalk`'s internal banner,
plus a CHANGELOG entry carrying the recompile-not-rejar note.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194EHFR4NamCpTHiqy7B74V
Three codec shapes are still resolved to the wrong schema field after the
nominal rung, and the decision memo asserted them by INSPECTING the rule rather
than running it. This runs them. All eight cells behave exactly as predicted,
so the ledger below is measured, not argued:

  cause 1, no name signal (the names are unrecoverable AND the list is permuted,
  so the rung abstains and position decides, wrongly)
    5a  {beta_name, alpha_name} vs {alpha, beta}    -> reads beta for alpha
    b2  {seq_no, event_ts} vs {occurredAt, seqNo}   -> reads the sequence number
        (a timestamp-millis logical type annotates the same physical long, so
        it cannot disambiguate either)
    b6  compensating arity: one case field dropped, one computed column added,
        counts equal, position wrong from the insertion point on
    at3 `userId_` and `user_id` both normalise to `userid` — ambiguity is no
        signal, so the rung disqualifies itself and position lands on the stale
        duplicate

  cause 2, a MISLEADING name signal (a column BEARS a case field's name but
  HOLDS a different value; the map is total and injective, so it is trusted)
    at1 {digest, id, raw_ident, payload} — `id` is a derived public identifier
    at2 {USER_ID, user_ident, balance} — `USER_ID` is a stale legacy column

Both cause-2 cells were wrong on 0.15.1 too (they read `digest` / position 0),
with one real cost: the corrupt value is now more PLAUSIBLE, `pub-REAL-ID`
rather than a digest length. Note at1's `payload` is FIXED by the change — it
read `id` before.

Also pins the accepted regression (a PERMUTING rename was right by position and
is now wrong by name) and asserts that `.fieldNamed` reaches every residual,
which is what the error messages and the docs both point at.

Reaching cause 1 needs a differential probe of the codec itself (encode
sentinels, observe which slot they land in) or an explicit declaration; no
amount of name matching helps. Filed as a follow-up, not attempted here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194EHFR4NamCpTHiqy7B74V
`.fieldNamed("schema_name")` appended the literal to the path with NO schema
lookup at all — `widenPathNamed` was one `widenPathStep(PathStep.Field(name))` —
although the prism already holds the schema. A typo, or the Scala field name
passed where the schema name was meant, therefore produced a runtime SILENT
MISS: reads return `None`, writes hand back the payload unchanged and report
success.

That is the same failure class the hatch exists to avoid, and it is the failure
class every error message in this resolver points AT: "navigate by explicit
schema name with .fieldNamed(...)". Sending someone from a loud construction
failure to a silent runtime one is the wrong direction.

Two deliberate carve-outs:
- a MAP parent. `.fieldNamed` is also how a map KEY is addressed, keys are data
  rather than schema fields, and feature-detecting an absent key is legitimate
  (`hatch-map-key-present` / `-absent` / `-write` in the false-positive spec
  pin all three).
- an unresolvable parent path. The walk already reports that at runtime, and
  refusing here would change the meaning of a prism deliberately built against
  a drifted root schema.

Record-level feature detection ("does this schema carry X?") moves to the
schema, where it belongs: `codec.schema.getField(name)`.

Two existing examples change, both deliberately:
- `AvroFieldNamingSpec`'s "a bad explicit .fieldNamed misses (None), it does
  not corrupt" PINNED the defect. Rewritten to assert the refusal.
- `AvroBytesSpec` used `.fieldNamed("name")` on a deliberately narrowed root
  schema to reach the walker's `PathMissing` arm. The coverage is kept by
  storing the `PathStep.Field("name")` through the internal constructor that
  example already uses two lines above.

Also amends the normative laws bullet, which promised "structurally drifted
payloads Miss silently" without distinguishing PAYLOAD drift (still a runtime
miss, still undetectable) from a name absent from the READER schema (now a
construction-time refusal on both `.field` and `.fieldNamed`).

The false-positive scorecard moves by exactly one cell of 28 —
`hatch-record-probe-absent`, None -> refusal — and by no other.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194EHFR4NamCpTHiqy7B74V
#95)

`MacroSelectors.extractFieldName` — the selector parser shared by every cursor
macro — matched `Lambda(_, Select(_, name))` with ANY receiver. A nested path
`_.inner.y` is `Select(Select(Ident(_), "inner"), "y")`, so it matched, yielded
the bare name `"y"`, and the macro resolved `y` on the PARENT record.

Where the parent carries a field of that name — and a record holding a nested
record often does — that is not a miss. It is a well-typed, perfectly lawful
optic aimed at the wrong field:

    NOuter(inner: NInner(x, y), y)
    codecPrism[NOuter].field(_.inner.y).getOption(bytes)   // Some("OUTER_Y")
    codecPrism[NOuter].field(_.inner).field(_.y)           // Some("INNER_Y")

Silent corruption on a perfectly 1:1, derived codec, with no schema divergence
involved at all — a second, independent hazard from the resolution one, and
100% decidable at compile time. The macros' own "nested paths are not yet
supported inside a single call; chain them" abort was UNREACHABLE for exactly
the shape it was written for.

`LensMacro` never had this: it uses the strict `extractSingleFieldName` (which
requires the `Select` receiver to be the lambda parameter) plus a
`knownFields.contains` check. The cursor macros simply never got the same
treatment. Both halves land here:

- `extractFieldName` now unwraps `Inlined` / `Typed` around the lambda, around
  its `Select` body AND around the `Select`'s RECEIVER, then requires that
  receiver to be an `Ident`. That keeps the wrapper tolerance the cursor macros
  need (which is the only reason this function exists beside the strict one)
  and drops the receiver looseness, which was never intentional.
- `requireCaseField[A]` aborts when a single-hop selector names something that
  is not a case field of the parent. Those used to be passed through as a
  literal field name and miss at runtime; the declaration index comes back `-1`
  for them, which is also the legitimate "NamedTuple parent" signal, so the two
  cannot be told apart downstream. They are told apart here. Skipped when `A`
  has no case fields at all, which is the shape the literal fallback exists for.

Five call sites, all covered: `AvroPrismMacro.fieldImpl` / `fieldTraversalImpl`,
`JsonPrismMacro.fieldImpl` / `fieldTraversalImpl` (eo-circe), and
`JsoniterPrismMacro.fieldName` (eo-jsoniter, shared by prism and traversal).
The two traversal messages also gained the "chain them" hint their prism twins
already carried.

`grep` finds zero nested-selector call sites in the repo, and the circe,
jsoniter, avro and generics suites all pass untouched — this closes a hole, it
does not move any working code.

Tests: `NestedSelectorMacroErrorSpec` in avro, circe and jsoniter — the nested
prism selector, the nested traversal selector, the not-a-case-field selector,
and a runtime row asserting the CHAINED form reaches the inner field. Every
compile-error row produced NO error at all before this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194EHFR4NamCpTHiqy7B74V
Resolution is construction-time only, but it is not free, and the first cut
paid more than it needed to. Measured with the prototype's own harness (best of
5 x 200_000 builds, same box, same session, `ConstructionCostProbe`):

                                     base    Set[Int]   this
  identity  .field(_.name)          84.9     115.2      98.6  ns/build
  snake     .field(_.clickId)       85.1     316.0     264.0  ns/build
  nested    .field(_.meta).field(…) 116.9    462.2     430.3  ns/build

Two changes, both free:

- `Set[Int]` -> a linear scan over the filled prefix of the result array. The
  set was allocating a boxed Integer and a new set node per case field, on a
  list that is a handful of entries; the scan is O(n^2) on an n that is the
  case-class arity.
- An early precondition: a total, injective map from case fields into schema
  fields cannot exist when the case class has MORE fields than the record, so
  that shape skips the scan entirely. Same answer, no work.

The remaining gap is inherent to the rule: the total-nominal rung resolves
EVERY case field before it trusts any one of them, so a snake_case parent runs
a normalised name compare per (case field x schema field) pair where position
ran none. That is the price of not re-aiming a working call site at a lucky
single match, it is paid once per drilled prism at construction, and it stays
strictly off the read/write path.

Zero per-operation cost re-verified from bytecode rather than asserted:
`javap -p -c` over the compiled avro classes puts every reference to
`resolveFieldName` / `fieldNameAt` / `requireField*` in `AvroPrism`'s
`widenPath` / `widenPathNamed` / `toFieldsPrism` / `resolveFieldNames` and
`AvroTraversal`'s `widenSuffix` / `widenSuffixNamed` / `toFieldsTraversal`.
`AvroFocus`, `AvroFocus$Leaf`, `AvroFocus$Fields`, `AvroRecordPrism`,
`AvroRecordTraversal` and `AvroBinaryCursor` contain none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194EHFR4NamCpTHiqy7B74V
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Cloudflare Pages preview for fix/nominal-resolution-onto-main is live:

https://35da3fe0.cats-eo-docs.pages.dev

Branch alias: https://fix-nominal-resolution-onto.cats-eo-docs.pages.dev

Built from commit 2e0b6b977da9c9af6b73ef8951650d3242d7d413 · updated on every push.

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark A/B

Allocation (B/op) — authoritative

Benchmark params base head Δ
OrderJsoniterBench.eoReadStreet size=64 128.0 72.0 -43.7%
JsoniterBench.jReadId size=64 72.0 48.0 -33.3%
JsoniterBench.jReadStreet size=512 128.1 152.1 +18.7%
OrderAvroBench.eoModifyNames size=8 2,160.0 1,992.0 -7.8%
AvroVulcanBench.encode_vulcanRaw - 1,272.0 1,208.0 -5.0%
OrderCirceBench.eoNamesIor size=64 85,969.1 89,547.7 +4.2%
OrderCirceBench.eoNames size=512 601,741.0 626,357.5 +4.1%
JsoniterBench.jReplaceId size=8 960.0 936.0 -2.5%
GenericsBench.rawLensGet - 0.0 0.0 -2.4%
JsoniterBench.jModifyId size=8 992.0 968.0 -2.4%
AvroBytesBench.eoSliceGraftPayload - 1,176.0 1,192.0 +1.4%
AvroVulcanBench.encode_bridged - 1,208.0 1,224.0 +1.3%
GenericsBench.genLensGet - 0.0 0.0 -1.2%
GenericsBench.handLensGet - 0.0 0.0 -1.1%
JsoniterBench.jMiss size=8 0.0 0.0 -1.0%
GenericsBench.handPrismGetMiss - 0.0 0.0 -0.8%
GenericsBench.handPrismModifyMiss - 0.0 0.0 +0.8%
JsoniterBench.jMiss size=512 0.0 0.0 +0.5%
AvroJsonBridgeBench.eoClickToAvro - 9,432.0 9,384.0 -0.5%
AvroBytesBench.eoModifyCountry - 3,320.0 3,304.0 -0.5%
198 more benchmarks
Benchmark params base head Δ
GenericsBench.genPrismModifyMiss - 0.0 0.0 -0.5%
GenericsBench.genPrismGetMiss - 0.0 0.0 +0.5%
JsoniterBench.jReplaceId size=64 5,296.0 5,320.0 +0.5%
OrderJsoniterBench.nativeReadStreet size=512 455.2 454.3 -0.2%
JsoniterBench.jMiss size=64 0.0 0.0 +0.1%
JsoniterBench.cReplaceId size=8 20,668.2 20,644.1 -0.1%
OrderAvroBench.naiveModifyStreet size=64 20,856.2 20,880.2 +0.1%
OrderJsoniterBench.nativeSumPrices size=8 1,512.9 1,514.7 +0.1%
OrderJsoniterBench.nativeSumPrices size=64 10,939.7 10,929.6 -0.1%
OrderCirceBench.monocleNames size=8 19,456.1 19,472.1 +0.1%
JsoniterBench.jReplaceId size=512 41,913.1 41,889.1 -0.1%
OrderJsoniterBench.monocleSumPrices size=64 47,369.7 47,390.6 +0.0%
OrderCirceBench.naiveStreet size=64 113,828.4 113,780.2 -0.0%
OrderCirceBench.eoStreet size=512 2,721.9 2,720.9 -0.0%
OrderCirceBench.eoStreetIor size=512 2,736.8 2,737.8 +0.0%
OrderCirceBench.eoStreet size=64 2,720.1 2,721.0 +0.0%
OrderCirceBench.naiveNames size=64 120,838.4 120,870.3 +0.0%
OrderJsoniterBench.eoSumPrices size=512 63,691.9 63,684.7 -0.0%
OrderCirceBench.eoStreetIor size=8 2,737.1 2,736.8 -0.0%
OrderJsoniterBench.monocleModifyStreet size=512 333,595.9 333,571.2 -0.0%
OrderCirceBench.monocleStreet size=64 113,807.4 113,799.2 -0.0%
OrderAvroBench.monocleModifyNames size=512 382,736.5 382,759.8 +0.0%
PlatedBench.mUniverseJson n=4096 6,489,966.9 6,489,678.8 -0.0%
OrderJsoniterBench.nativeSumPrices size=512 86,302.8 86,300.2 -0.0%
OrderJsoniterBench.nativeReadStreet size=64 424.5 424.5 -0.0%
PlatedBench.visitorUniverseJson n=4096 533,757.7 533,769.5 +0.0%
OrderCirceBench.eoStreet size=8 2,720.9 2,720.9 -0.0%
PlatedBench.mUniverseExpr n=4096 4,687,717.6 4,687,635.3 -0.0%
OrderCirceBench.monocleStreet size=512 908,049.0 908,034.2 -0.0%
OrderJsoniterBench.eoReadStreet size=512 128.2 128.2 +0.0%
OrderCirceBench.directNames size=512 614,008.4 613,998.7 -0.0%
JsoniterBench.cSumPrices size=64 156,251.8 156,249.3 -0.0%
JsoniterBench.cModifyId size=8 20,716.2 20,716.5 +0.0%
JsoniterBench.jSumPrices size=512 63,632.6 63,633.4 +0.0%
PlatedBench.eoUniverseDeep n=4096 786,585.0 786,576.8 -0.0%
JsoniterBench.cModifyId size=512 1,066,800.9 1,066,790.1 -0.0%
JsoniterBench.cReadStreet size=512 797,940.4 797,932.3 -0.0%
OrderJsoniterBench.naiveReadStreet size=512 193,232.8 193,234.7 +0.0%
JsoniterBench.cSumPrices size=8 22,704.1 22,704.3 +0.0%
OrderCirceBench.hcursorNames size=64 77,819.6 77,819.0 -0.0%
OrderCirceBench.naiveStreet size=512 908,039.4 908,032.5 -0.0%
OrderJsoniterBench.naiveSumPrices size=512 230,125.3 230,123.9 -0.0%
JsoniterBench.cReadId size=64 101,309.3 101,309.8 +0.0%
OrderCirceBench.hcursorStreet size=512 3,032.9 3,032.9 -0.0%
JsoniterBench.cReadStreet size=64 101,309.1 101,309.6 +0.0%
JsoniterBench.cReplaceId size=512 1,066,755.0 1,066,750.5 -0.0%
OrderAvroBench.monocleModifyStreet size=512 169,066.3 169,067.0 +0.0%
OrderJsoniterBench.monocleReadStreet size=512 193,232.3 193,231.5 -0.0%
JsoniterBench.cSumPrices size=512 1,240,811.0 1,240,815.9 +0.0%
OrderAvroBench.monocleModifyNames size=64 39,867.4 39,867.5 +0.0%
JsoniterBench.cReadId size=512 797,936.6 797,939.6 +0.0%
PlatedBench.visitorUniverseJson n=512 40,954.4 40,954.2 -0.0%
OrderJsoniterBench.monocleSumPrices size=512 1,190,791.5 1,190,787.1 -0.0%
OrderCirceBench.hcursorNames size=512 614,005.3 614,003.4 -0.0%
OrderCirceBench.monocleNames size=512 1,121,846.3 1,121,842.8 -0.0%
OrderCirceBench.naiveNames size=512 965,282.7 965,279.8 -0.0%
OrderAvroBench.monocleReadStreet size=512 69,802.0 69,802.2 +0.0%
PlatedBench.eoTransformDeep n=4096 624,387.7 624,385.9 -0.0%
OrderCirceBench.directNames size=64 77,729.8 77,729.6 -0.0%
OrderJsoniterBench.naiveModifyStreet size=512 333,572.2 333,571.4 -0.0%
OrderAvroBench.naiveModifyNames size=512 226,283.5 226,283.0 -0.0%
OrderJsoniterBench.eoModifyStreet size=512 42,028.0 42,027.9 -0.0%
OrderAvroBench.naiveReadStreet size=512 69,802.2 69,802.0 -0.0%
OrderAvroBench.eoModifyNames size=512 97,404.3 97,404.1 -0.0%
JsoniterBench.jReadStreet size=64 128.0 128.0 +0.0%
OrderAvroBench.naiveModifyStreet size=512 169,057.3 169,057.6 +0.0%
OrderJsoniterBench.eoReadStreet size=8 128.0 128.0 -0.0%
PlatedBench.mUniverseExpr n=512 475,011.5 475,012.2 +0.0%
PlatedBench.visitorUniverseDeep n=4096 196,706.7 196,706.4 -0.0%
OrderCirceBench.eoStreetIor size=64 2,736.1 2,736.1 -0.0%
OrderJsoniterBench.monocleReadStreet size=64 24,705.2 24,705.2 +0.0%
JsoniterBench.cModifyId size=64 136,266.2 136,266.0 -0.0%
OrderAvroBench.eoReadStreet size=512 88.0 88.0 +0.0%
PlatedBench.eoUniverseJson n=4096 786,483.4 786,484.2 +0.0%
JsoniterBench.jSumPrices size=64 8,120.3 8,120.3 -0.0%
OrderJsoniterBench.eoSumPrices size=64 8,121.2 8,121.2 +0.0%
OrderCirceBench.eoNamesIor size=512 683,562.0 683,562.7 +0.0%
OrderCirceBench.monocleNames size=64 132,794.6 132,794.7 +0.0%
OrderCirceBench.directStreet size=512 2,728.8 2,728.8 +0.0%
OrderAvroBench.eoModifyNames size=64 12,584.2 12,584.1 -0.0%
OrderCirceBench.eoNames size=64 79,299.5 79,299.5 +0.0%
JsoniterBench.jReadId size=8 48.0 48.0 -0.0%
PlatedBench.mUniverseJson n=512 699,917.4 699,917.9 +0.0%
OrderJsoniterBench.nativeReadStreet size=8 424.0 424.0 -0.0%
OrderJsoniterBench.eoModifyStreet size=64 5,432.1 5,432.1 -0.0%
OrderAvroBench.eoModifyStreet size=512 328.0 328.0 +0.0%
OrderCirceBench.hcursorStreet size=64 3,032.1 3,032.1 -0.0%
OrderJsoniterBench.monocleModifyStreet size=64 30,081.9 30,081.8 -0.0%
OrderCirceBench.hcursorStreet size=8 3,032.0 3,032.0 +0.0%
OrderCirceBench.directStreet size=8 2,728.0 2,728.0 +0.0%
PlatedBench.eoTransformExpr n=4096 655,359.7 655,359.4 -0.0%
JsoniterBench.jReadId size=512 48.0 48.0 -0.0%
AvroBytesBench.prunedReadPartner - 1,592.0 1,592.0 -0.0%
JsoniterBench.cReplaceId size=64 136,217.5 136,217.5 -0.0%
AvroBytesBench.eoReadPartner - 480.0 480.0 +0.0%
OrderAvroBench.naiveReadStreet size=8 1,208.0 1,208.0 -0.0%
AvroJsonBridgeBench.eoWideToJson - 1,472.0 1,472.0 +0.0%
AvroBytesBench.naiveModifyCountry - 7,616.0 7,616.0 -0.0%
OrderCirceBench.eoNames size=8 10,944.1 10,944.1 +0.0%
OrderJsoniterBench.eoModifyStreet size=8 1,072.0 1,072.0 +0.0%
PlatedBench.visitorUniverseExpr n=4096 196,655.0 196,655.0 +0.0%
OrderCirceBench.eoNamesIor size=8 11,592.1 11,592.1 -0.0%
OrderJsoniterBench.naiveReadStreet size=64 24,705.2 24,705.2 +0.0%
OrderJsoniterBench.monocleModifyStreet size=8 4,664.1 4,664.1 -0.0%
PlatedBench.visitorTransformExpr n=4096 360,471.1 360,471.2 +0.0%
PlatedBench.visitorUniverseDeep n=64 3,128.0 3,128.0 +0.0%
PlatedBench.visitorTransformDeep n=512 20,496.5 20,496.5 +0.0%
PlatedBench.visitorTransformDeep n=4096 163,886.6 163,886.6 -0.0%
OrderCirceBench.directStreet size=64 2,728.1 2,728.1 -0.0%
PlatedBench.visitorUniverseDeep n=512 24,632.8 24,632.8 +0.0%
PlatedBench.visitorTransformExpr n=512 45,032.8 45,032.8 -0.0%
PlatedBench.eoUniverseJson n=512 98,186.9 98,186.9 -0.0%
PlatedBench.mTransformExpr n=512 69,585.6 69,585.6 -0.0%
OrderAvroBench.monocleModifyStreet size=64 20,904.2 20,904.2 +0.0%
PlatedBench.eoUniverseExpr n=4096 786,384.9 786,384.8 -0.0%
JsoniterBench.jSumPrices size=8 1,176.0 1,176.0 +0.0%
AvroBytesBench.naiveModifyPartner - 7,536.0 7,536.0 -0.0%
AvroJsonBridgeBench.naiveClickToJson - 4,696.0 4,696.0 -0.0%
OrderJsoniterBench.monocleReadStreet size=8 3,648.0 3,648.0 -0.0%
PlatedBench.mUniverseJson n=64 73,208.4 73,208.4 -0.0%
OrderJsoniterBench.naiveModifyStreet size=8 4,640.1 4,640.1 -0.0%
OrderCirceBench.naiveStreet size=8 17,040.1 17,040.1 -0.0%
OrderJsoniterBench.naiveReadStreet size=8 3,648.0 3,648.0 +0.0%
OrderAvroBench.monocleReadStreet size=64 8,848.2 8,848.2 -0.0%
OrderJsoniterBench.eoSumPrices size=8 1,176.0 1,176.0 -0.0%
JsoniterBench.jModifyId size=64 5,328.0 5,328.0 -0.0%
PlatedBench.mTransformExpr n=4096 753,738.9 753,738.8 -0.0%
AvroVulcanBench.rootGet_bridged - 1,472.0 1,472.0 +0.0%
PlatedBench.eoUniverseExpr n=512 98,185.5 98,185.5 +0.0%
JsoniterBench.jModifyId size=512 41,921.0 41,921.0 -0.0%
PlatedBench.visitorUniverseExpr n=512 24,584.7 24,584.7 -0.0%
AvroJsonBridgeBench.naiveClickToAvro - 3,928.0 3,928.0 +0.0%
OrderCirceBench.naiveNames size=8 17,808.1 17,808.1 -0.0%
OrderJsoniterBench.naiveModifyStreet size=64 30,058.0 30,058.0 +0.0%
PlatedBench.eoTransformExpr n=512 81,825.7 81,825.7 -0.0%
AvroVulcanBench.decode_native - 48.0 48.0 +0.0%
PlatedBench.eoUniverseJson n=64 12,168.1 12,168.1 -0.0%
OrderAvroBench.eoModifyStreet size=64 328.0 328.0 +0.0%
JsoniterBench.jReadStreet size=8 128.0 128.0 +0.0%
OrderCirceBench.directNames size=8 10,688.1 10,688.1 -0.0%
OpticBuildBench.buildAndUse - 2,816.0 2,816.0 +0.0%
JsoniterBench.cReadStreet size=8 15,568.0 15,568.0 -0.0%
PlatedBench.mUniverseExpr n=64 45,424.3 45,424.3 +0.0%
OrderAvroBench.monocleModifyStreet size=8 2,992.0 2,992.0 +0.0%
OrderAvroBench.eoModifyStreet size=8 328.0 328.0 +0.0%
AvroBytesBench.eoGraftPayload - 720.0 720.0 -0.0%
OrderAvroBench.monocleReadStreet size=8 1,208.0 1,208.0 -0.0%
PlatedBench.eoTransformDeep n=64 7,184.0 7,184.0 +0.0%
OrderAvroBench.eoReadStreet size=64 88.0 88.0 -0.0%
AvroJsonBridgeBench.eoWideToAvro - 6,744.0 6,744.0 -0.0%
AvroBytesBench.eoModifyPartner - 3,360.0 3,360.0 -0.0%
OrderCirceBench.monocleStreet size=8 17,048.1 17,048.1 -0.0%
AvroBytesBench.naivePassthroughPayload - 10,600.1 10,600.1 +0.0%
PlatedBench.eoUniverseDeep n=64 12,360.0 12,360.0 -0.0%
OrderJsoniterBench.naiveSumPrices size=8 4,248.0 4,248.0 +0.0%
AvroVulcanBench.decode_vulcanRaw - 880.0 880.0 +0.0%
PlatedBench.eoUniverseDeep n=512 98,377.5 98,377.5 +0.0%
AvroBytesBench.naiveReadCountry - 4,256.0 4,256.0 -0.0%
PlatedBench.eoTransformDeep n=512 57,361.2 57,361.2 -0.0%
PlatedBench.eoUniverseExpr n=64 12,168.0 12,168.0 -0.0%
OrderJsoniterBench.naiveSumPrices size=64 29,337.3 29,337.3 -0.0%
OrderJsoniterBench.monocleSumPrices size=8 6,640.1 6,640.1 -0.0%
AvroBytesBench.naiveReadPartner - 4,264.0 4,264.0 -0.0%
PlatedBench.eoTransformExpr n=64 10,144.0 10,144.0 +0.0%
OrderCirceBench.hcursorNames size=8 10,768.1 10,768.1 -0.0%
JsoniterBench.cReadId size=8 15,568.0 15,568.0 +0.0%
PlatedBench.visitorTransformExpr n=64 5,608.0 5,608.0 -0.0%
GenericsBench.genLensModify - 24.0 24.0 -0.0%
AvroJsonBridgeBench.naiveWideToJson - 4,376.0 4,376.0 -0.0%
OpticBuildBench.build - 184.0 184.0 -0.0%
AvroVulcanBench.rootGet_native - 600.0 600.0 +0.0%
OpticBuildBench.reuseUse - 2,648.0 2,648.0 +0.0%
PlatedBench.mTransformExpr n=64 11,728.1 11,728.1 +0.0%
AvroVulcanBench.fieldGet_native - 432.0 432.0 -0.0%
OrderAvroBench.naiveModifyStreet size=8 2,968.0 2,968.0 -0.0%
OrderAvroBench.naiveModifyNames size=8 3,752.0 3,752.0 +0.0%
GenericsBench.rawLensModify - 24.0 24.0 -0.0%
OrderAvroBench.eoReadStreet size=8 88.0 88.0 +0.0%
AvroBytesBench.prunedReadCountry - 1,976.0 1,976.0 +0.0%
PlatedBench.visitorTransformDeep n=64 2,576.0 2,576.0 -0.0%
AvroJsonBridgeBench.naiveWideToAvro - 3,504.0 3,504.0 +0.0%
AvroVulcanBench.encode_native - 56.0 56.0 +0.0%
GenericsBench.rawPrismModifyHit - 24.0 24.0 -0.0%
AvroVulcanBench.fieldGet_bridged - 432.0 432.0 -0.0%
PlatedBench.visitorUniverseJson n=64 5,096.0 5,096.0 -0.0%
GenericsBench.handLensModify - 24.0 24.0 -0.0%
OrderAvroBench.naiveModifyNames size=64 27,936.3 27,936.3 +0.0%
GenericsBench.handPrismModifyHit - 24.0 24.0 -0.0%
PlatedBench.visitorUniverseExpr n=64 3,080.0 3,080.0 -0.0%
AvroVulcanBench.decode_bridged - 880.0 880.0 -0.0%
OrderAvroBench.monocleModifyNames size=8 5,400.0 5,400.0 -0.0%
AvroBytesBench.eoReadCountry - 520.0 520.0 -0.0%
GenericsBench.handPrismGetHit - 16.0 16.0 +0.0%
GenericsBench.genPrismGetHit - 16.0 16.0 -0.0%
OrderAvroBench.naiveReadStreet size=64 8,848.2 8,848.2 -0.0%
AvroJsonBridgeBench.eoClickToJson - 4,000.0 4,000.0 -0.0%
GenericsBench.genPrismModifyHit - 24.0 24.0 -0.0%
GenericsBench.rawPrismGetHit - 16.0 16.0 +0.0%
Timing (ns/op) — directional only, same-VM but shared runner
Benchmark params base head Δ
OrderAvroBench.naiveModifyStreet size=64 6,358.6 7,203.6 +13.3%
PlatedBench.mUniverseJson n=4096 3,331,810.4 2,940,697.3 -11.7%
AvroJsonBridgeBench.eoWideToJson - 636.9 709.6 +11.4%
PlatedBench.eoUniverseDeep n=4096 111,316.4 100,020.9 -10.1%
JsoniterBench.jReadId size=8 39.5 35.7 -9.5%
JsoniterBench.jReadStreet size=512 191.0 208.1 +9.0%
PlatedBench.visitorUniverseJson n=4096 183,767.4 199,929.2 +8.8%
OrderAvroBench.naiveReadStreet size=8 554.1 512.1 -7.6%
AvroVulcanBench.encode_vulcanRaw - 266.9 247.6 -7.2%
OrderCirceBench.monocleStreet size=512 200,444.9 185,980.3 -7.2%
OrderAvroBench.eoModifyNames size=64 4,505.3 4,189.7 -7.0%
AvroBytesBench.prunedReadPartner - 582.1 542.3 -6.8%
OrderAvroBench.eoModifyNames size=8 641.8 598.0 -6.8%
OrderCirceBench.directStreet size=8 1,060.4 1,129.4 +6.5%
PlatedBench.visitorUniverseJson n=512 22,776.1 21,326.3 -6.4%
OrderCirceBench.hcursorStreet size=8 1,149.7 1,217.9 +5.9%
AvroBytesBench.eoModifyCountry - 428.7 452.8 +5.6%
JsoniterBench.cSumPrices size=64 43,405.8 45,827.4 +5.6%
AvroBytesBench.eoReadPartner - 204.1 215.1 +5.4%
OrderCirceBench.eoStreetIor size=8 1,061.1 1,116.7 +5.2%
OrderJsoniterBench.eoSumPrices size=512 87,170.3 82,757.2 -5.1%
AvroVulcanBench.encode_bridged - 250.0 262.6 +5.0%
OrderCirceBench.directNames size=512 260,004.0 247,903.6 -4.7%
AvroVulcanBench.rootGet_bridged - 425.5 445.3 +4.6%
OrderCirceBench.eoStreetIor size=512 1,055.1 1,102.9 +4.5%
AvroBytesBench.naiveModifyCountry - 2,815.5 2,688.6 -4.5%
JsoniterBench.cModifyId size=8 9,044.9 9,443.7 +4.4%
OrderJsoniterBench.eoModifyStreet size=8 299.3 311.3 +4.0%
OrderJsoniterBench.eoReadStreet size=8 211.2 202.8 -4.0%
OrderJsoniterBench.eoModifyStreet size=64 688.9 661.4 -4.0%
JsoniterBench.jReplaceId size=8 112.6 108.1 -4.0%
JsoniterBench.jReadStreet size=64 192.3 199.9 +4.0%
PlatedBench.mUniverseExpr n=4096 2,802,837.8 2,693,556.9 -3.9%
PlatedBench.mUniverseExpr n=512 184,825.1 191,763.7 +3.8%
JsoniterBench.jModifyId size=64 495.7 478.1 -3.6%
OrderCirceBench.eoNames size=512 265,751.5 256,330.0 -3.5%
OrderCirceBench.eoStreetIor size=64 1,087.9 1,050.4 -3.5%
OrderJsoniterBench.nativeSumPrices size=64 8,275.9 8,002.1 -3.3%
JsoniterBench.jSumPrices size=512 86,509.0 89,342.8 +3.3%
OrderJsoniterBench.nativeSumPrices size=8 1,184.9 1,222.4 +3.2%
AvroJsonBridgeBench.naiveClickToAvro - 1,509.2 1,555.7 +3.1%
AvroJsonBridgeBench.naiveClickToJson - 2,742.2 2,663.3 -2.9%
JsoniterBench.jSumPrices size=64 10,911.7 10,602.3 -2.8%
JsoniterBench.cReadId size=64 26,487.2 27,235.5 +2.8%
JsoniterBench.jReplaceId size=512 3,825.0 3,718.5 -2.8%
OrderCirceBench.naiveStreet size=8 3,388.6 3,295.1 -2.8%
JsoniterBench.cReplaceId size=8 8,978.1 8,731.3 -2.7%
OrderAvroBench.monocleModifyNames size=512 105,876.6 102,982.7 -2.7%
JsoniterBench.cReadId size=512 215,597.2 209,724.2 -2.7%
AvroVulcanBench.decode_native - 18.6 19.1 +2.7%
OrderCirceBench.eoNamesIor size=8 4,716.2 4,588.4 -2.7%
OrderJsoniterBench.nativeSumPrices size=512 64,603.5 62,875.1 -2.7%
OrderJsoniterBench.monocleReadStreet size=64 11,864.7 12,174.7 +2.6%
PlatedBench.mUniverseJson n=512 203,130.0 207,940.5 +2.4%
AvroJsonBridgeBench.eoWideToAvro - 1,076.8 1,102.2 +2.4%
OrderCirceBench.eoNames size=64 32,314.8 33,070.8 +2.3%
JsoniterBench.jModifyId size=8 113.9 111.2 -2.3%
OrderJsoniterBench.naiveReadStreet size=512 94,393.8 96,591.2 +2.3%
OrderJsoniterBench.eoModifyStreet size=512 4,132.3 4,037.2 -2.3%
JsoniterBench.cReadStreet size=8 4,158.8 4,064.5 -2.3%
OrderCirceBench.eoStreet size=64 1,096.7 1,121.5 +2.3%
JsoniterBench.cReadStreet size=64 26,393.1 26,980.3 +2.2%
OrderCirceBench.directNames size=64 31,491.2 30,796.9 -2.2%
PlatedBench.mUniverseJson n=64 19,507.2 19,083.7 -2.2%
GenericsBench.rawLensGet - 0.6 0.6 -2.1%
OrderCirceBench.naiveNames size=8 3,553.1 3,477.6 -2.1%
OrderJsoniterBench.monocleSumPrices size=64 16,284.8 16,620.3 +2.1%
AvroVulcanBench.decode_vulcanRaw - 214.8 219.2 +2.0%
OrderCirceBench.eoStreet size=8 1,101.0 1,122.9 +2.0%
PlatedBench.eoTransformDeep n=4096 125,974.3 123,519.8 -1.9%
AvroBytesBench.naiveModifyPartner - 2,820.7 2,767.2 -1.9%
JsoniterBench.cSumPrices size=512 354,194.5 360,651.8 +1.8%
GenericsBench.rawLensModify - 2.3 2.2 -1.8%
GenericsBench.genLensModify - 3.7 3.7 -1.8%
OpticBuildBench.buildAndUse - 1,111.5 1,131.0 +1.7%
AvroBytesBench.eoModifyPartner - 533.1 523.9 -1.7%
GenericsBench.genLensGet - 1.1 1.1 -1.7%
OrderCirceBench.eoNamesIor size=64 34,775.2 35,338.7 +1.6%
OrderJsoniterBench.naiveSumPrices size=512 99,607.6 98,019.1 -1.6%
OrderAvroBench.monocleModifyStreet size=8 985.1 1,000.8 +1.6%
OrderCirceBench.hcursorStreet size=512 1,169.9 1,151.3 -1.6%
OrderJsoniterBench.nativeReadStreet size=8 847.4 834.7 -1.5%
OrderJsoniterBench.monocleModifyStreet size=8 3,328.1 3,281.1 -1.4%
OrderCirceBench.hcursorStreet size=64 1,197.0 1,180.3 -1.4%
OrderCirceBench.hcursorNames size=64 31,255.5 30,826.8 -1.4%
PlatedBench.eoUniverseDeep n=64 1,931.6 1,905.1 -1.4%
OrderJsoniterBench.nativeReadStreet size=64 4,540.6 4,479.2 -1.4%
OrderAvroBench.eoModifyStreet size=8 123.0 124.7 +1.3%
GenericsBench.rawPrismModifyHit - 2.2 2.2 -1.3%
JsoniterBench.jReplaceId size=64 481.5 475.5 -1.2%
OrderAvroBench.monocleModifyNames size=64 10,410.7 10,537.7 +1.2%
OrderCirceBench.monocleStreet size=8 3,372.7 3,331.8 -1.2%
OrderJsoniterBench.nativeReadStreet size=512 34,606.1 35,009.6 +1.2%
PlatedBench.eoUniverseJson n=64 3,385.0 3,345.6 -1.2%
OrderAvroBench.monocleReadStreet size=8 518.7 513.0 -1.1%
JsoniterBench.cReplaceId size=64 52,714.3 53,294.6 +1.1%
OrderAvroBench.eoReadStreet size=512 38.3 38.7 +1.1%
OrderJsoniterBench.eoReadStreet size=512 199.5 201.6 +1.1%
OrderJsoniterBench.monocleReadStreet size=8 1,922.9 1,903.1 -1.0%
OrderAvroBench.monocleModifyStreet size=64 7,230.0 7,304.1 +1.0%
JsoniterBench.jSumPrices size=8 1,437.0 1,451.4 +1.0%
JsoniterBench.cReadId size=8 4,149.4 4,189.5 +1.0%
OrderJsoniterBench.monocleReadStreet size=512 93,787.2 92,882.8 -1.0%
JsoniterBench.cModifyId size=512 426,313.5 430,413.4 +1.0%
AvroVulcanBench.fieldGet_native - 97.8 96.9 -0.9%
GenericsBench.handLensGet - 1.1 1.1 -0.9%
OrderJsoniterBench.eoReadStreet size=64 201.7 203.5 +0.9%
OrderJsoniterBench.naiveReadStreet size=8 1,891.0 1,908.4 +0.9%
JsoniterBench.cSumPrices size=8 6,411.2 6,352.3 -0.9%
OrderCirceBench.hcursorNames size=512 256,112.2 253,765.4 -0.9%
OrderCirceBench.naiveNames size=512 201,906.3 200,113.1 -0.9%
PlatedBench.eoUniverseExpr n=64 1,856.9 1,840.8 -0.9%
OrderJsoniterBench.monocleSumPrices size=512 451,122.7 447,259.0 -0.9%
AvroVulcanBench.rootGet_native - 177.6 179.1 +0.9%
OrderJsoniterBench.monocleModifyStreet size=64 20,167.9 19,999.5 -0.8%
OrderAvroBench.monocleReadStreet size=512 34,581.7 34,869.8 +0.8%
GenericsBench.handPrismModifyMiss - 1.1 1.1 +0.8%
PlatedBench.visitorTransformExpr n=512 7,602.0 7,539.0 -0.8%
GenericsBench.handLensModify - 2.8 2.8 -0.8%
OrderCirceBench.directNames size=8 4,236.9 4,203.3 -0.8%
AvroBytesBench.eoSliceGraftPayload - 330.4 327.9 -0.8%
PlatedBench.visitorTransformExpr n=64 992.7 985.2 -0.8%
AvroBytesBench.naivePassthroughPayload - 3,930.8 3,960.0 +0.7%
PlatedBench.visitorTransformDeep n=512 4,903.9 4,940.2 +0.7%
OrderCirceBench.monocleNames size=8 3,867.6 3,839.3 -0.7%
OrderJsoniterBench.naiveModifyStreet size=8 3,296.5 3,272.5 -0.7%
JsoniterBench.jReadId size=64 37.2 37.5 +0.7%
AvroVulcanBench.encode_native - 12.0 12.1 +0.7%
JsoniterBench.jMiss size=8 172.9 171.7 -0.7%
OrderCirceBench.monocleNames size=64 25,292.0 25,473.1 +0.7%
PlatedBench.visitorUniverseDeep n=64 833.7 827.8 -0.7%
GenericsBench.handPrismGetMiss - 1.1 1.1 -0.7%
OrderJsoniterBench.eoSumPrices size=64 10,494.1 10,565.7 +0.7%
OrderCirceBench.naiveNames size=64 23,876.8 23,715.4 -0.7%
PlatedBench.visitorUniverseDeep n=4096 58,681.4 58,285.0 -0.7%
AvroBytesBench.eoGraftPayload - 163.1 162.0 -0.7%
OrderAvroBench.naiveModifyStreet size=512 56,430.0 56,052.3 -0.7%
OrderAvroBench.eoModifyStreet size=512 122.0 122.8 +0.6%
OrderCirceBench.monocleStreet size=64 22,236.1 22,378.8 +0.6%
AvroJsonBridgeBench.eoClickToAvro - 3,184.0 3,163.8 -0.6%
PlatedBench.mTransformExpr n=512 15,553.5 15,458.9 -0.6%
JsoniterBench.cReplaceId size=512 427,719.1 425,165.3 -0.6%
PlatedBench.eoUniverseExpr n=512 14,280.5 14,365.4 +0.6%
JsoniterBench.cReadStreet size=512 209,102.4 207,862.5 -0.6%
OpticBuildBench.reuseUse - 1,055.6 1,061.6 +0.6%
OrderCirceBench.directStreet size=64 1,072.0 1,065.9 -0.6%
OrderJsoniterBench.naiveModifyStreet size=512 166,161.9 165,238.5 -0.6%
OrderAvroBench.monocleReadStreet size=64 4,429.2 4,404.6 -0.6%
AvroJsonBridgeBench.naiveWideToAvro - 1,005.7 1,011.2 +0.5%
PlatedBench.eoTransformExpr n=64 2,036.3 2,047.1 +0.5%
OrderAvroBench.eoModifyStreet size=64 121.5 122.2 +0.5%
JsoniterBench.jMiss size=512 172.2 173.0 +0.5%
PlatedBench.eoUniverseJson n=512 27,607.1 27,468.3 -0.5%
OrderJsoniterBench.monocleModifyStreet size=512 165,789.9 164,966.5 -0.5%
PlatedBench.eoUniverseJson n=4096 235,431.2 236,579.7 +0.5%
AvroJsonBridgeBench.naiveWideToJson - 1,889.8 1,880.7 -0.5%
PlatedBench.visitorUniverseDeep n=512 7,269.9 7,304.6 +0.5%
AvroBytesBench.naiveReadCountry - 1,625.2 1,617.8 -0.5%
OrderCirceBench.monocleNames size=512 236,568.3 235,486.3 -0.5%
GenericsBench.genPrismGetHit - 2.2 2.2 -0.5%
OrderJsoniterBench.naiveReadStreet size=64 11,831.2 11,883.9 +0.4%
GenericsBench.handPrismGetHit - 2.0 2.1 +0.4%
JsoniterBench.jReadStreet size=8 197.7 198.5 +0.4%
OrderCirceBench.eoNamesIor size=512 271,665.0 272,811.1 +0.4%
OrderAvroBench.naiveReadStreet size=512 34,737.9 34,594.3 -0.4%
GenericsBench.rawPrismGetHit - 1.8 1.8 +0.4%
OrderAvroBench.naiveModifyNames size=8 1,166.3 1,170.8 +0.4%
OrderJsoniterBench.naiveSumPrices size=8 1,978.2 1,985.6 +0.4%
GenericsBench.handPrismModifyHit - 2.8 2.8 -0.4%
JsoniterBench.jModifyId size=512 3,679.2 3,666.4 -0.3%
OrderAvroBench.naiveModifyStreet size=8 992.1 988.6 -0.3%
OrderCirceBench.eoStreet size=512 1,102.4 1,098.7 -0.3%
PlatedBench.visitorTransformDeep n=64 432.8 431.3 -0.3%
OrderJsoniterBench.eoSumPrices size=8 1,447.9 1,443.1 -0.3%
PlatedBench.eoTransformExpr n=512 16,454.7 16,400.2 -0.3%
PlatedBench.mTransformExpr n=64 2,597.7 2,605.8 +0.3%
PlatedBench.visitorUniverseExpr n=512 6,756.5 6,735.6 -0.3%
OrderJsoniterBench.monocleSumPrices size=8 2,562.1 2,554.4 -0.3%
AvroVulcanBench.fieldGet_bridged - 97.4 97.1 -0.3%
PlatedBench.eoTransformExpr n=4096 131,517.6 131,134.0 -0.3%
OrderCirceBench.naiveStreet size=512 186,588.5 186,070.9 -0.3%
OrderCirceBench.directStreet size=512 1,069.0 1,071.9 +0.3%
GenericsBench.genPrismModifyHit - 3.0 3.0 -0.2%
AvroBytesBench.prunedReadCountry - 736.6 738.4 +0.2%
GenericsBench.genPrismModifyMiss - 1.3 1.2 -0.2%
OrderCirceBench.hcursorNames size=8 4,287.1 4,277.0 -0.2%
OrderCirceBench.eoNames size=8 4,426.7 4,416.7 -0.2%
OpticBuildBench.build - 46.0 45.9 -0.2%
JsoniterBench.jReadId size=512 39.4 39.4 -0.2%
AvroBytesBench.naiveReadPartner - 1,763.8 1,760.8 -0.2%
OrderAvroBench.eoReadStreet size=8 38.8 38.9 +0.2%
OrderAvroBench.naiveModifyNames size=512 69,865.9 69,977.8 +0.2%
PlatedBench.visitorUniverseExpr n=64 822.0 820.7 -0.2%
OrderAvroBench.eoReadStreet size=64 38.4 38.4 +0.2%
OrderAvroBench.eoModifyNames size=512 34,580.7 34,528.1 -0.2%
PlatedBench.eoTransformDeep n=512 11,842.6 11,825.0 -0.1%
PlatedBench.visitorTransformExpr n=4096 64,714.6 64,806.1 +0.1%
PlatedBench.eoUniverseExpr n=4096 100,164.1 100,025.7 -0.1%
PlatedBench.visitorTransformDeep n=4096 42,110.2 42,060.0 -0.1%
PlatedBench.visitorUniverseExpr n=4096 53,552.4 53,615.4 +0.1%
AvroJsonBridgeBench.eoClickToJson - 2,903.7 2,906.7 +0.1%
OrderAvroBench.monocleModifyNames size=8 1,495.1 1,493.6 -0.1%
OrderJsoniterBench.naiveModifyStreet size=64 20,143.3 20,162.8 +0.1%
PlatedBench.eoUniverseDeep n=512 14,586.1 14,600.0 +0.1%
PlatedBench.eoTransformDeep n=64 1,450.8 1,452.1 +0.1%
PlatedBench.visitorUniverseJson n=64 1,991.8 1,990.1 -0.1%
JsoniterBench.jMiss size=64 172.5 172.6 +0.1%
OrderAvroBench.monocleModifyStreet size=512 56,241.2 56,279.1 +0.1%
AvroBytesBench.eoReadCountry - 169.7 169.6 -0.1%
OrderJsoniterBench.naiveSumPrices size=64 12,557.9 12,549.9 -0.1%
PlatedBench.mTransformExpr n=4096 168,877.3 168,779.7 -0.1%
PlatedBench.mUniverseExpr n=64 14,924.4 14,917.0 -0.0%
JsoniterBench.cModifyId size=64 52,875.3 52,852.2 -0.0%
OrderCirceBench.naiveStreet size=64 22,371.5 22,363.4 -0.0%
AvroVulcanBench.decode_bridged - 222.6 222.7 +0.0%
GenericsBench.genPrismGetMiss - 1.2 1.2 -0.0%
OrderAvroBench.naiveReadStreet size=64 4,413.1 4,411.9 -0.0%
OrderAvroBench.naiveModifyNames size=64 8,891.8 8,891.0 -0.0%

base_sha: 8d0cb291e49b6387aa747d076fc2f932d9746107 · head_sha: 2e0b6b977da9c9af6b73ef8951650d3242d7d413 · jdk: temurin-21 · runner: ubuntu-22.04 · jmh_params: -i 3 -wi 2 -f 1 -t 1 -foe true -prof gc -rf json · profile: pr:-i3-wi2-f1-t1-gc

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No efficient whole-record construction primitive (build-from-scratch beats AvroRecordPrism)

1 participant