Skip to content

Infer a literal fixedSize for struct codecs - #1761

Open
plutohan wants to merge 1 commit into
anza-xyz:mainfrom
plutohan:plutohan/issue-1738-struct-codec-fixed-size
Open

Infer a literal fixedSize for struct codecs#1761
plutohan wants to merge 1 commit into
anza-xyz:mainfrom
plutohan:plutohan/issue-1738-struct-codec-fixed-size

Conversation

@plutohan

Copy link
Copy Markdown
Contributor

Closes #1738.

Made getStructEncoder/Decoder/Codec infer a literal fixedSize from their fields instead of just falling back to number:

getStructEncoder([
  ['a', getU32Encoder()],
  ['b', getU8Encoder()],
]) satisfies FixedSizeEncoder<{ a: number; b: number }, 5>; // used to be `number`

So the size-aware combinators (getUnion*, getPredicate*, getPatternMatch*) can actually tell struct branches of different sizes apart now, which was the point in the issue.

How it works: it's just a type-level sum of each field's fixedSize. The catch is the addition builds a tuple of that length, and TS gives up with TS2589 once a tuple gets near ~1000 elements. So to make sure this never blows up in someone's code, I capped it (MaxInferredStructSize, 512 for now) — within the cap you get the literal sum, anything bigger or a non-literal field just stays number like before. The in-range check walks the cap tuple down instead of building a tuple of the field's size, so a huge single field doesn't trip TS2589 either.

Runtime is untouched — it already summed the sizes, this is purely the types.

Verified with test:typecheck (the acceptance criterion from the issue is in there as a typetest), plus lint/prettier and the unit tests on node + browser.

Couple of things I wasn't sure about:

@changeset-bot

changeset-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 76328e8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 47 packages
Name Type
@solana/codecs-data-structures Patch
@solana/codecs Patch
@solana/offchain-messages Patch
@solana/options Patch
@solana/program-client-core Patch
@solana/sysvars Patch
@solana/transaction-messages Patch
@solana/transactions Patch
@solana/instruction-plans Patch
@solana/kit Patch
@solana/signers Patch
@solana/programs Patch
@solana/react Patch
@solana/rpc-api Patch
@solana/rpc-subscriptions-api Patch
@solana/transaction-confirmation Patch
@solana/wallet-account-signer Patch
@solana/compat Patch
@solana/rpc-graphql Patch
@solana/plugin-interfaces Patch
@solana/rpc Patch
@solana/rpc-subscriptions Patch
@solana/accounts Patch
@solana/addresses Patch
@solana/assertions Patch
@solana/codecs-core Patch
@solana/codecs-numbers Patch
@solana/codecs-strings Patch
@solana/errors Patch
@solana/fast-stable-stringify Patch
@solana/fixed-points Patch
@solana/functional Patch
@solana/instructions Patch
@solana/keys Patch
@solana/nominal-types Patch
@solana/plugin-core Patch
@solana/promises Patch
@solana/rpc-parsed-types Patch
@solana/rpc-spec-types Patch
@solana/rpc-spec Patch
@solana/rpc-subscriptions-channel-websocket Patch
@solana/rpc-subscriptions-spec Patch
@solana/rpc-transformers Patch
@solana/rpc-transport-http Patch
@solana/rpc-types Patch
@solana/subscribable Patch
@solana/webcrypto-ed25519-polyfill Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@bundlemon

bundlemon Bot commented Jun 24, 2026

Copy link
Copy Markdown

BundleMon

Files updated (4)
Status Path Size Limits
@solana/kit production bundle
kit/dist/index.production.min.js
52.71KB (-69B -0.13%) -
rpc-spec-types/dist/index.native.mjs
961B (-214B -18.21%) -
rpc-spec-types/dist/index.browser.mjs
962B (-215B -18.27%) -
rpc-spec-types/dist/index.node.mjs
959B (-215B -18.31%) -
Unchanged files (143)
Status Path Size Limits
errors/dist/index.node.mjs
20.78KB -
errors/dist/index.browser.mjs
20.76KB -
errors/dist/index.native.mjs
20.76KB -
rpc-graphql/dist/index.browser.mjs
18.82KB -
rpc-graphql/dist/index.native.mjs
18.81KB -
rpc-graphql/dist/index.node.mjs
18.81KB -
wallet-account-signer/dist/index.node.mjs
17.58KB -
wallet-account-signer/dist/index.native.mjs
17.56KB -
wallet-account-signer/dist/index.browser.mjs
17.56KB -
transaction-messages/dist/index.browser.mjs
11.32KB -
transaction-messages/dist/index.native.mjs
11.32KB -
transaction-messages/dist/index.node.mjs
11.32KB -
instruction-plans/dist/index.browser.mjs
6.58KB -
instruction-plans/dist/index.native.mjs
6.58KB -
instruction-plans/dist/index.node.mjs
6.58KB -
fixed-points/dist/index.browser.mjs
5.08KB -
fixed-points/dist/index.native.mjs
5.07KB -
fixed-points/dist/index.node.mjs
5.07KB -
offchain-messages/dist/index.browser.mjs
5.06KB -
offchain-messages/dist/index.native.mjs
5.06KB -
offchain-messages/dist/index.node.mjs
5.06KB -
codecs-data-structures/dist/index.browser.mjs
5.04KB -
codecs-data-structures/dist/index.native.mjs
5.03KB -
codecs-data-structures/dist/index.node.mjs
5.03KB -
kit/dist/index.browser.mjs
4.47KB -
kit/dist/index.native.mjs
4.47KB -
kit/dist/index.node.mjs
4.47KB -
transactions/dist/index.browser.mjs
4.07KB -
transactions/dist/index.native.mjs
4.07KB -
transactions/dist/index.node.mjs
4.07KB -
codecs-core/dist/index.browser.mjs
3.62KB -
codecs-core/dist/index.native.mjs
3.62KB -
codecs-core/dist/index.node.mjs
3.62KB -
webcrypto-ed25519-polyfill/dist/index.node.mj
s
3.61KB -
webcrypto-ed25519-polyfill/dist/index.browser
.mjs
3.59KB -
webcrypto-ed25519-polyfill/dist/index.native.
mjs
3.57KB -
rpc-subscriptions/dist/index.browser.mjs
3.37KB -
rpc-subscriptions/dist/index.node.mjs
3.34KB -
rpc-subscriptions/dist/index.native.mjs
3.31KB -
signers/dist/index.browser.mjs
3.26KB -
signers/dist/index.native.mjs
3.26KB -
signers/dist/index.node.mjs
3.26KB -
rpc-transformers/dist/index.browser.mjs
3.16KB -
rpc-transformers/dist/index.native.mjs
3.16KB -
rpc-transformers/dist/index.node.mjs
3.16KB -
react/dist/index.browser.mjs
3.09KB -
react/dist/index.native.mjs
3.09KB -
react/dist/index.node.mjs
3.09KB -
keys/dist/index.node.mjs
3.06KB -
addresses/dist/index.browser.mjs
2.93KB -
addresses/dist/index.native.mjs
2.92KB -
addresses/dist/index.node.mjs
2.92KB -
keys/dist/index.browser.mjs
2.85KB -
keys/dist/index.native.mjs
2.85KB -
subscribable/dist/index.node.mjs
2.68KB -
subscribable/dist/index.native.mjs
2.61KB -
subscribable/dist/index.browser.mjs
2.6KB -
codecs-strings/dist/index.browser.mjs
2.55KB -
codecs-strings/dist/index.node.mjs
2.51KB -
codecs-strings/dist/index.native.mjs
2.47KB -
transaction-confirmation/dist/index.node.mjs
2.42KB -
transaction-confirmation/dist/index.native.mj
s
2.37KB -
sysvars/dist/index.browser.mjs
2.37KB -
sysvars/dist/index.native.mjs
2.37KB -
transaction-confirmation/dist/index.browser.m
js
2.37KB -
sysvars/dist/index.node.mjs
2.37KB -
rpc-subscriptions-spec/dist/index.node.mjs
2.25KB -
rpc-subscriptions-spec/dist/index.native.mjs
2.2KB -
rpc-subscriptions-spec/dist/index.browser.mjs
2.2KB -
rpc/dist/index.node.mjs
1.95KB -
codecs-numbers/dist/index.browser.mjs
1.95KB -
codecs-numbers/dist/index.native.mjs
1.95KB -
codecs-numbers/dist/index.node.mjs
1.94KB -
rpc-transport-http/dist/index.browser.mjs
1.89KB -
rpc-transport-http/dist/index.native.mjs
1.89KB -
rpc/dist/index.native.mjs
1.81KB -
rpc-types/dist/index.browser.mjs
1.8KB -
rpc/dist/index.browser.mjs
1.8KB -
rpc-types/dist/index.native.mjs
1.8KB -
rpc-types/dist/index.node.mjs
1.8KB -
rpc-transport-http/dist/index.node.mjs
1.71KB -
rpc-subscriptions-channel-websocket/dist/inde
x.node.mjs
1.33KB -
rpc-subscriptions-channel-websocket/dist/inde
x.native.mjs
1.27KB -
rpc-subscriptions-channel-websocket/dist/inde
x.browser.mjs
1.26KB -
program-client-core/dist/index.browser.mjs
1.21KB -
program-client-core/dist/index.native.mjs
1.21KB -
program-client-core/dist/index.node.mjs
1.21KB -
options/dist/index.browser.mjs
1.18KB -
options/dist/index.native.mjs
1.18KB -
options/dist/index.node.mjs
1.17KB -
accounts/dist/index.browser.mjs
1.17KB -
accounts/dist/index.native.mjs
1.17KB -
accounts/dist/index.node.mjs
1.16KB -
rpc-api/dist/index.browser.mjs
998B -
rpc-api/dist/index.native.mjs
997B -
rpc-api/dist/index.node.mjs
995B -
compat/dist/index.browser.mjs
969B -
compat/dist/index.native.mjs
968B -
compat/dist/index.node.mjs
966B -
rpc-spec/dist/index.browser.mjs
918B -
rpc-spec/dist/index.native.mjs
918B -
rpc-spec/dist/index.node.mjs
917B -
rpc-subscriptions-api/dist/index.native.mjs
871B -
rpc-subscriptions-api/dist/index.browser.mjs
870B -
rpc-subscriptions-api/dist/index.node.mjs
870B -
promises/dist/index.native.mjs
841B -
promises/dist/index.node.mjs
840B -
promises/dist/index.browser.mjs
839B -
plugin-core/dist/index.browser.mjs
820B -
plugin-core/dist/index.native.mjs
819B -
plugin-core/dist/index.node.mjs
817B -
assertions/dist/index.browser.mjs
783B -
instructions/dist/index.browser.mjs
771B -
instructions/dist/index.native.mjs
770B -
instructions/dist/index.node.mjs
768B -
fast-stable-stringify/dist/index.browser.mjs
726B -
fast-stable-stringify/dist/index.native.mjs
725B -
assertions/dist/index.native.mjs
724B -
fast-stable-stringify/dist/index.node.mjs
724B -
assertions/dist/index.node.mjs
723B -
programs/dist/index.browser.mjs
329B -
programs/dist/index.native.mjs
327B -
programs/dist/index.node.mjs
325B -
fs-impl/dist/index.browser.mjs
245B -
event-target-impl/dist/index.node.mjs
230B -
functional/dist/index.browser.mjs
154B -
functional/dist/index.native.mjs
152B -
text-encoding-impl/dist/index.native.mjs
152B -
functional/dist/index.node.mjs
151B -
codecs/dist/index.browser.mjs
145B -
codecs/dist/index.native.mjs
144B -
codecs/dist/index.node.mjs
142B -
event-target-impl/dist/index.browser.mjs
133B -
ws-impl/dist/index.node.mjs
131B -
text-encoding-impl/dist/index.browser.mjs
122B -
fs-impl/dist/index.node.mjs
120B -
text-encoding-impl/dist/index.node.mjs
119B -
ws-impl/dist/index.browser.mjs
113B -
crypto-impl/dist/index.node.mjs
111B -
crypto-impl/dist/index.browser.mjs
109B -
rpc-parsed-types/dist/index.browser.mjs
66B -
rpc-parsed-types/dist/index.native.mjs
65B -
rpc-parsed-types/dist/index.node.mjs
63B -

Total files change -713B -0.13%

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

@plutohan

Copy link
Copy Markdown
Contributor Author

Hey @mcintyre94, this one is green and ready whenever you get a chance. I left a couple of open questions in the description (the 512 cap, and whether the size helpers should stay local or move into codecs-core for the union / predicate / pattern-match work) and I'm happy to go whichever way you prefer. No rush!

@mcintyre94 mcintyre94 added the do-not-close Add this tag to exempt an issue/PR from being closed by the stalebot label Jun 29, 2026
@mcintyre94

Copy link
Copy Markdown
Member

@plutohan Thanks for this, and sorry I didn't get back to you quicker. Part of the reason I opened this as an issue is because I think there's some research/feasibility/stress testing to do before committing to a solution here. I appreciate your PR as a demonstration of how the implementation will look if we land it, but I'm a bit unsure about committing to this Typescript complexity. We need to think a bit more about the cost/benefit here - I do think there's a real benefit for small structs and unions/predicates.

I've marked this do-not-close for now so it won't get closed before we make a decision.

WRT to your open questions, I think just having it in struct for now and a patch changeset are right, so no immediate changes. I think the approach is good too, it's just a decision of whether it's worth it.

@plutohan

Copy link
Copy Markdown
Contributor Author

Thanks @mcintyre94, no worries on the timing, and totally fair to weigh the complexity.

On the cost side: the type machinery is self-contained in struct.ts (about 40 lines) and the cap is the safety net. Because it is gated at 512 bytes and the range check walks a fixed tuple rather than building one from the field size, it can never produce a TS2589 in a consumer's build. Anything over the cap, or any non-literal field, just stays number exactly like today. So the downside is bounded, worst case it is a no-op and it cannot break anyone.

On the benefit: it is mainly the unions / predicates / pattern-match work (#1443, #1683) being able to tell struct branches of different sizes apart instead of widening, plus the v1 tx config case.

Happy to help with the stress testing you mentioned if useful, for example running it across the real struct codecs in the repo and checking the tsc compile-time impact. No rush either way, and feel free to park it.

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

Labels

do-not-close Add this tag to exempt an issue/PR from being closed by the stalebot

Projects

None yet

Development

Successfully merging this pull request may close these issues.

getStructEncoder/Decoder/Codec do not infer a literal fixedSize from their fields

2 participants