Describe each function's cost with a single model - #2
Open
impartialstate wants to merge 2 commits into
Open
impartialstate wants to merge 2 commits into
impartialstate wants to merge 2 commits into
Conversation
Cost tracking reported a cost of zero whenever state tracking or exhaustive evaluation was enabled alongside it. Both features observe every step of an evaluation, and each installs its own decorator over the plan. The second decorator to run found the step already wrapped in a watcher and returned it untouched, so its observer never heard anything. Watchers now hold a set of observers rather than a single one, and a decorator adds itself to the set of a step which is already watched. The set is held behind a pointer because an attribute acquires its qualifiers after it has been decorated, and those qualifiers report through the same set. A step may be decorated more than once by the same decorator, which is what happens as attributes acquire qualifiers during planning, so each observer is registered under the identity of the decorator which introduced it and a repeat registration is ignored. Without that, a select would be counted twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXaZqU8aNuMfJnsxCvYZoJ
The estimator and the tracker each had their own vocabulary and their
own switch statement over the standard library, and every extension
overload needed a hand written estimator and a matching hand written
tracker. Keeping the two halves in agreement was manual, and they had
already drifted: startsWith was estimated from the affix but tracked
from the target, distinct charged for comparing string elements at
runtime but not when estimating, and regex extract charged a base call
cost at runtime that estimation did not.
A cost.Model now declares how the cost of one overload relates to the
sizes of its operands:
Base + Alloc + Traversed(operands) [+ size of the result]
and both halves evaluate it. Estimation supplies operand sizes derived
from ast.Expr, types.Type, and any hint the caller provided; tracking
supplies the exact sizes of the ref.Val values the call received, and
measures the result rather than predicting it. The seam between them is
cost.Operands, which reads operands lazily, so a model only pays to
measure what it names. The standard library is a table of models
consulted by both, which retires the pair of switch statements and the
warning comment that guarded them.
Operands are receiver first on both sides. Estimation used to hand a
function its target separately from its arguments while tracking passed
one flat list with the receiver first, which is what let startsWith
read a different operand in each half.
Estimation:
- cost.Node reports an expression's type, path, size, and the type
and size of the values it holds, resolving hints itself, so a cost
function no longer threads an estimator through to look them up.
- cost.Hints is an Estimator for callers whose knowledge is limited
to "this path, or this type, is at most this large".
- SizeEstimate and CostEstimate become cost.Estimate, since sizes and
costs are converted into one another constantly.
Tracking:
- The shadow stack of evaluated values is gone. Steps are observed
depth first, so a call recovers its arguments by expression id from
values recorded as they are produced, with no stack search and no
heuristics about which entries to drop.
- cost.AggregateSize is the exported size function, and it accounts
for optional values everywhere rather than only in the interpreter.
- cost.Comprehension exposes an evaluated comprehension, so a tracker
can charge for iteration. The default remains free.
The extension libraries declare each overload's cost on one line and
register the models with both halves in one line each, replacing around
400 lines of paired estimator and tracker functions.
Consequences for reported costs:
- startsWith and endsWith cost the shorter of the two operands in
both halves, as the comparison operators already did.
- Comparing values which may be empty has a minimum estimate of zero
rather than one, matching what evaluation charges.
- distinct and sort estimates include the cost of comparing string
and bytes elements, which tracking already charged.
- Regex costs apply their two cost factors together rather than
rounding each up separately, and charge the base call cost when
estimating.
- Sizes of optional values are measured through the optional, so
regex extract costs more than it did.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXaZqU8aNuMfJnsxCvYZoJ
This was referenced Aug 19, 2026
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.
Second of three. Base: #1 — review the two commits on this branch, not the whole diff against master.
Why
The estimator and the tracker each had their own vocabulary and their own switch statement over the standard library, and every extension overload needed a hand-written estimator plus a matching hand-written tracker. Keeping the halves in agreement was manual, and they had already drifted:
startsWithwas estimated from the affix but tracked from the targetdistinctcharged for comparing string elements at runtime but not when estimatingregex.extractcharged a base call cost at runtime that estimation did notWhat replaces it
A
cost.Modeldeclares how the cost of one overload relates to the sizes of its operands:Both halves evaluate the same model. Estimation supplies operand sizes derived from
ast.Expr,types.Type, and any hint the caller gave; tracking supplies the exact sizes of theref.Valvalues the call received, and measures the result rather than predicting it. The seam iscost.Operands, which reads operands lazily —types.String.Size()islen([]rune(s)), so a model only pays to measure what it names. The standard library becomes a table of models consulted by both, retiring the pair of switch statements and the warning comment that guarded them.Operands are receiver-first on both sides. Estimation used to hand a function its target separately from its arguments while tracking passed one flat list with the receiver first, which is exactly what let
startsWithread a different operand in each half.Estimation
cost.Nodereports an expression's type, path, size, and the type and size of the values it holds, resolving hints itself, so a cost function no longer threads an estimator through to look them up.cost.Hintsis a ready-madeEstimatorfor callers whose knowledge is "this path, or this type, is at most this large".SizeEstimateandCostEstimatecollapse intocost.Estimate.Tracking
cost.AggregateSizeis the exported size function, and it accounts for optional values everywhere rather than only in the interpreter.cost.Comprehensionexposes an evaluated comprehension — its range, iteration count, and accumulator — so a tracker can charge for iteration. The default remains free.Extensions
Each overload's cost is declared on one line, and the models are registered with both halves in one line each:
That replaced roughly 400 lines of paired estimator and tracker functions.
Included bug fix (first commit, separately cherry-pickable)
Cost tracking reported zero whenever
OptTrackStateorOptExhaustiveEvalwas enabled alongside it. Both features observe every step and each installs its own decorator; the second decorator found the step already wrapped and returned it untouched, so its observer never heard anything. Watchers now hold a set of observers, registered by the identity of the decorator that introduced it so a step decorated twice during planning is not counted twice.Consequences for reported costs
Test expectations were updated for each; the estimated range still brackets the actual cost everywhere.
startsWith/endsWithcost the shorter of the two operands in both halvesdistinct/sortestimates include the string/bytes element penaltyregex.extractcosts more than it didPerformance
Cost-tracked evaluation on a filter/map benchmark: 14.9µs / 5483 B / 133 allocs versus 13.4µs / 4562 B / 140 allocs before. ~10% slower, fewer allocations. The first cut was ~40% slower; a reusable operand view and id-indexed operand storage closed most of it.
Testing
go test ./...andgo test -racepass. New unit tests incommon/costcover the estimate arithmetic, the size combinators,AggregateSize,Hints, tracker precedence and limits, and that a model evaluated over exact sizes agrees with the same model evaluated over an estimate that knows those sizes. New tests cover comprehension tracking and the observer fix.Generated by Claude Code