Skip to content

Make AssocTable cache initial capacity configurable (#54)#238

Merged
nojaf merged 7 commits into
fsprojects:masterfrom
larsest1:fix-54-assoctable-grow-on-demand
Jul 6, 2026
Merged

Make AssocTable cache initial capacity configurable (#54)#238
nojaf merged 7 commits into
fsprojects:masterfrom
larsest1:fix-54-assoctable-grow-on-demand

Conversation

@larsest1

@larsest1 larsest1 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Addresses the allocation half of #54.

Problem

AssocTable's lookup cache is created as Dictionary<_,_>(2000). Two AssocTables (action + goto) are constructed inside Tables.Interpret on every parse, so a parser invoked over many small inputs pre-allocates two ~2000-entry dictionaries per parse that never come close to filling — this dominates parse-time allocation.

What changed

This makes the initial cache capacity configurable, defaulting to the historical 2000 so existing behaviour is unchanged. Correctness is unaffected — only the cache's initial capacity changes. It is purely opt-in, at three layers:

  • Runtime — a new public ParseSettings.AssocTableCacheInitialCapacity (default 2000) sets the capacity process-wide, and a new Tables.Interpret overload takes an explicit capacity that overrides the global for a single parser. Setting the capacity to 0 grows the cache on demand, allocating only what a parse actually uses.
  • fsyacc CLI — since most people drive fslex/fsyacc via CLI invocation from MSBuild rather than by setting a runtime global, a new --assoc-cache-capacity <int> option makes the generated parser call the capacity-carrying overload. Omitting the flag emits the existing parameterless overload, so generated output is unchanged by default.
  • Release notes — entries added under 11.4.0 (bumped from 11.3.0, which is already released).

Public API additions (non-breaking)

module ParseSettings =
    val mutable AssocTableCacheInitialCapacity: int

type Tables<'tok> with
    member Interpret: lexer * lexbuf * startState * assocTableCacheInitialCapacity: int -> obj

Both are additive — the existing 3-argument Interpret and the 2000 default are untouched, so current callers and previously generated parsers are unaffected.

Usage

From MSBuild, grow the cache on demand instead of pre-allocating 2000 entries:

<FsYacc Include="Parser.fsy">
  <OtherFlags>--assoc-cache-capacity 0</OtherFlags>
</FsYacc>

Or set it process-wide at startup: ParseSettings.AssocTableCacheInitialCapacity <- 0.

Evidence

Measured on a downstream F#/APL analyzer whose grammar is parsed once per statement (so the per-parse fixed cost is paid heavily), opting in with capacity 0:

  • Full-source run (~131k functions): 97.9 s → 71.9 s (−27%), same-machine A/B.
  • Per-function type-check allocation −80% (the per-parse Dictionary<int,int> was ~95% of allocation, via a GC-AllocationTick profile).
  • Output byte-identical across ~449k result lines; full unit + integration suites green.

Testing

  • Codegen (FsYacc.Core.Tests) — asserts fsyacc emits the 4-argument Interpret overload when --assoc-cache-capacity is given, and the parameterless overload when it is not.
  • Runtime capacity-invariance (JsonLexAndYaccExample) — parses each sample input at the historical default (2000) and at capacities 0, 1, 64 and 100000, and fails if any result differs. This is the key safety property: the capacity is an allocation-tuning knob and must never change what a parser produces.

I kept the default at 2000 so this is non-breaking; happy to instead default to 0 (grow-on-demand) if you'd prefer that as the out-of-the-box behaviour.

larsest1 added 2 commits June 30, 2026 12:01
AssocTable's lookup cache was created as Dictionary<_,_>(2000). Two AssocTables (action + goto) are constructed on every Tables.Interpret call, so a parser invoked over many small inputs pre-allocated two ~2000-entry dictionaries per parse that never came close to filling — dominating parse-time allocation.

Expose ParseSettings.AssocTableCacheInitialCapacity (default 2000, so existing behaviour is unchanged) and use it for the cache's initial capacity. Setting it to 0 grows the cache on demand, allocating only what a parse actually uses.

Addresses the allocation half of fsprojects#54 without changing default behaviour.

@nojaf nojaf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks reasonable, could you give an example of how end-users configure this?

Comment thread RELEASE_NOTES.md
@@ -1,5 +1,6 @@
#### 11.3.0 - Unreleased
* Add Fable support to FsLexYacc.Runtime.
* Make the AssocTable lookup cache initial capacity configurable to avoid pre-allocating 2000 entries per parse #54

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://www.nuget.org/packages/FsLexYacc/11.3.0 does appear to be released, could you make this into 11.4.0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — bumped to 11.4.0

@larsest1

larsest1 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

This looks reasonable, could you give an example of how end-users configure this?

Sure

ParseSettings.AssocTableCacheInitialCapacity is a process-global setting you set once at startup, before any parsing (it's read when each parser's lookup tables are first constructed). The default stays 2000, so it's opt-in.

open FSharp.Text.Parsing

[<EntryPoint>]
let main argv =
    // We parse many small inputs, so grow the AssocTable cache on demand
    // instead of pre-allocating 2000 entries per parse.
    ParseSettings.AssocTableCacheInitialCapacity <- 0
    // ... run the app / parser ...
    0

Typical use is an app that parses many small inputs over its lifetime (editor, linter, a service parsing snippets), where the per-parse 2000-entry pre-allocation dominates. Leaving it unset keeps today's behaviour for grammars that parse a few large inputs.

@nojaf

nojaf commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Makes me wonder if the cli app should have an option to set this as well.
I think most people do use fslex/fsyacc via cli invocation from MSBuild.

@larsest1

Copy link
Copy Markdown
Contributor Author

Makes me wonder if the cli app should have an option to set this as well. I think most people do use fslex/fsyacc via cli invocation from MSBuild.

I will look into it

@nojaf

nojaf commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

@dsyme or @sergey-tihon could you update the key in

run: dotnet nuget push "bin/*.nupkg" -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_KEY }} --skip-duplicate
, pretty sure that will have expired once we merge this PR.

larsest1 added 3 commits June 30, 2026 14:28
…jects#54)

Add a Tables.Interpret overload that takes an explicit initial capacity for the per-parse AssocTable lookup caches, threading it through the interpreter (AssocTable now receives the capacity as a constructor argument). The existing 3-argument Interpret keeps using ParseSettings.AssocTableCacheInitialCapacity, so this is non-breaking — existing generated parsers are unaffected.

This is the runtime support for letting fsyacc bake a per-parser capacity into generated code (follows in a separate commit).
Wire the runtime AssocTable cache-capacity control (issue fsprojects#54) through to the fsyacc command line, since fslex/fsyacc are most commonly driven via CLI from MSBuild rather than by setting the runtime knob in code.

- fsyacc.fs: new `--assoc-cache-capacity <int>` arg, forwarded into GeneratorState.assocCacheCapacity.
- fsyaccdriver.fs: when the capacity is set, emit the 4-arg Interpret overload; otherwise emit the parameterless overload (unchanged default behaviour).
- FsYacc.Core.Tests: codegen tests asserting the generated engine line uses the capacity-carrying overload when the flag is given and the default overload when it is not. Sequenced, since fsyacc's spec compiler uses process-global state.

Usage from MSBuild: <OtherFlags>--assoc-cache-capacity 0</OtherFlags>
The AssocTable cache initial capacity (fsprojects#54) is a pure allocation-tuning knob and must never change what a parser produces. Extend the JSON example smoke test to parse each input at the historical default (2000) and at capacities 0, 1, 64 and 100000, asserting identical results and failing with a nonzero exit otherwise.

This exercises the runtime interpreter that both the default path and the --assoc-cache-capacity path funnel into (Parser.start reads ParseSettings.AssocTableCacheInitialCapacity), complementing the codegen-only assertions in FsYacc.Core.Tests.
@larsest1

larsest1 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

CLI option added

@nojaf nojaf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!
Can you run dotnet fantomas . again please.

@larsest1

larsest1 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@kkm000
@dsyme

What is the release cycle on this project?

@dsyme

dsyme commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

We should set up trusted publishing here, I'll work on it

@sergey-tihon

Copy link
Copy Markdown
Member

@nojaf i've replaces nuget key for a fresh one
@dsyme are you sure about trusted publishing? i don't see globe setting there, so we will let any trusted repos to push any packages under fsprojects.

@nojaf

nojaf commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

@sergey-tihon thanks, I'm going to merge this and hope for the best.

@nojaf nojaf merged commit 7e5f997 into fsprojects:master Jul 6, 2026
3 checks passed
@nojaf

nojaf commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

@sergey-tihon no luck

error: Response status code does not indicate success: 403 (The specified API key is invalid, has expired, or does not have permission to access the specified package.).

@sergey-tihon

Copy link
Copy Markdown
Member

@nojaf Sorry, I wasn’t aware of the FsYacc.Core package. The API key has now been updated, and all packages have been successfully published using the workflow.

image

@nojaf

nojaf commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Thx Sergey!

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.

4 participants