Repository navigation
openapi: the compile's work and size bounds are written as a dozen separate counters #785
Description
Problem
Every bound in the compiler is a counter with a limit, and each is written out where it is used. At 210283f, among others:
| Counter | Where | Direction | Past the limit |
|---|---|---|---|
budget{spent, limit} |
load/reach_tree.go |
up | caller reads exhausted() |
resumeWork{spent, limit} |
load/resume.go |
up | BudgetExceeded error |
mappings.work/limit |
load/mappings.go |
up | stops reading |
loops.work against maxLoopWork |
load/loops.go |
up | CycleScanFailed warning |
tally{steps, limit} |
load/treereads.go |
up | stops counting, limit held below math.MaxInt |
reached.spent (per document) |
load/reached.go |
up | BudgetExceeded error; limit <= 0 means unbounded |
*int budgets |
overlay/overlay.go (substituteGrafts, expandAlias), schema/schema.go (schemaNullVerdict and two callers) |
down | return false / read as silent |
weigher{ceiling} |
overlay/sequence.go |
saturating | answers stop at limit+1 |
rawConv.nodes |
annotation/rawjson.go |
up | error |
View.cachedPairs |
nodeview/nodeview.go |
up | declines to memoize, silently |
sourceindex.Build(root, maxNodes) |
sourceindex/sourceindex.go |
up, int64 |
Truncated() |
grep -rnE 'spent|budget|work +int' --include='*.go' compilers/ re-derives the list.
They differ in ways that look incidental: whether the count goes up or down, how "unbounded" is spelled (limit <= 0 in reached, a negative Limits value projected to zero by bounded, no spelling at all for a constant), whether the arithmetic can overflow (only weigher saturates), and what crossing the limit does. Some of the last are deliberate; the rest look like history. A fix in one has no way to know what the others do; #571, a charging error in one of them, is the kind of defect that results.
Proposed direction
One small type in compilers/openapi/internal with Spend(n) bool, Exhausted() bool, Remaining() int and a constructor that applies the Limits convention in one place (zero takes the default, negative is unbounded, saturating add). Each site keeps its own diagnostic and its own decision about what crossing means; only the counting moves.
It starts internal rather than in compilers/compile: promotion needs evidence from all three compilers (docs/micro-compiler-design.md §3).
No bound changes value. This is a refactor with unchanged output.
Acceptance
- No private
spent/limitpair remains where the type fits; the table above is reduced to the sites that genuinely differ, each with a comment saying why. - Every existing bound test still reddens when its limit is lowered by one (check by mutation, not by reading).
- Goldens and the harness sweep are byte-identical.
- The type has a test for saturation at
math.MaxIntand for the negative-unbounded constructor.
Activity
OmarAlJarrah commentedon Oct 20, 2026
#1016 changes this list. At 83ab508:
budgetinload/reach_tree.go,maxReachWorkinload/reach.go, the mapping read's work and limit inload/mappings.go,loops.workandmaxLoopWorkinload/loops.go, and the work counter inscan/chains.go.budgetincompilers/openapi/internal/bind/table.go, one counter for all binding work, withDefaultLimitand no unbounded spelling, reported asopenapi/budget-exceeded.bind/documents.goadds two caps,maxPointerTokensandmaxPasses.resumeWork,treeReads.tally,reached.spentandView.cachedPairs.*intbudget of theschemanull verdicts is now a field ofschema.nulls, and is still a down-counter.