Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f39d08e
perf(ir): return a BigVal already in JSON form as it is
OmarAlJarrah Oct 27, 2026
820f07d
feat(ir): tell a visitor which field the walk reached a value by
OmarAlJarrah Oct 27, 2026
12df418
perf(irverify): fingerprint a property without building a string
OmarAlJarrah Oct 27, 2026
444ea72
perf(irverify): spell a registry entry's path only to report it
OmarAlJarrah Oct 27, 2026
a6c43f3
perf(irverify): read every string list through one map
OmarAlJarrah Oct 27, 2026
3099dd7
test(ir/irtest): show each visitor the walk it would be shown alone
OmarAlJarrah Oct 27, 2026
93fd0f5
test(irverify): hold every walking check to costing nothing per value
OmarAlJarrah Oct 27, 2026
749d974
Merge main into perf/irverify-clean-values
OmarAlJarrah Oct 27, 2026
0dd9a89
docs(irverify): name the inferred-marker set among a value's costs
OmarAlJarrah Oct 27, 2026
4e223fd
perf(irverify): read every raw payload through one decoder
OmarAlJarrah Oct 27, 2026
1aa8777
perf(irverify): spell a source's path only to report it
OmarAlJarrah Oct 27, 2026
731be69
test(irverify): hold the name exemption off a node inside a listed one
OmarAlJarrah Oct 27, 2026
eeff0e4
docs(ir): say when a race build's pool drops defeat a measurement
OmarAlJarrah Oct 27, 2026
ffa0ea5
docs(ir): state what the cost tests' own claims leave out
OmarAlJarrah Oct 27, 2026
75f538f
test(ir): hold a check to costing nothing on a first meeting
OmarAlJarrah Oct 27, 2026
6d61f09
test(irverify): name the exemption test for the positions it pins
OmarAlJarrah Oct 27, 2026
93fb8fd
Merge main into perf/irverify-clean-values
OmarAlJarrah Oct 29, 2026
4957387
docs(ir): spell out shadowed fields and why a visit needs the walk path
OmarAlJarrah Oct 29, 2026
eea24b7
test(irverify): hold the payload reader either side of the nesting limit
OmarAlJarrah Oct 29, 2026
802c077
perf(irverify): bound what a reused map keeps between collections
OmarAlJarrah Oct 29, 2026
40071a3
perf(ir)!: keep declaration and reference paths unspelled until reported
OmarAlJarrah Oct 29, 2026
a115477
perf(pass): spell, sort and collect only for a diagnostic
OmarAlJarrah Oct 29, 2026
2d69be5
chore(skills): hold a first meeting to a doubling test
OmarAlJarrah Oct 29, 2026
b556811
docs(ir): say how long a replayed path lives and how to keep one
OmarAlJarrah Oct 29, 2026
1ae3192
test(pass): hold a discriminator mapping's reports to sorted order
OmarAlJarrah Oct 29, 2026
c04bbb0
test(pass): reach servers and a kept PropID in the sound-value test
OmarAlJarrah Oct 29, 2026
5c08f25
test(irverify): hold all of Verify to a clean component costing nothing
OmarAlJarrah Oct 29, 2026
8fdefc3
test(irverify): pin where namespace and member-scope reports land
OmarAlJarrah Oct 29, 2026
da1e702
refactor(irverify): drop a check pathsAt's callers already make
OmarAlJarrah Oct 29, 2026
de65106
test(pass): judge each collection a reused map reads on its own
OmarAlJarrah Oct 29, 2026
2b9fed5
test(pass): pin each report's position past the first
OmarAlJarrah Oct 29, 2026
734955e
test(ir): pin a logged step's owner and the declaration list's growth
OmarAlJarrah Oct 29, 2026
3402138
docs(pass): say a family sorts in a kept list, allocating to report
OmarAlJarrah Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,16 @@ gives. A clock does none of that reliably; a counter does.
## Measure a render through a real walk, not a zero path
`allocs-miss-a-free-render`

- **Rule.** `testing.AllocsPerRun` cannot see a render the compiler turns into a no-copy map lookup,
nor one of the zero `ir.WalkPath`, which renders "" without allocating. Measure each visitor in a
call of its own to `irtest.AllocatingVisits`, which visits inside a real walk with the walk's own
path, so no other visitor's pruning hides a visit, and assert the exact set of allocating paths.
Check `go build -gcflags=-m` before trusting a count to see a render at all.
- **Why.** An eager-render mutant survived a test indexing a map with the rendered path (#1125),
and four survived visit tests run on the zero path (#1133).
- **Probe.** Plant an eager render in the visitor; the test reddens.
- **Rule.** `testing.AllocsPerRun` sees neither a render the compiler turns into a no-copy map
lookup nor one of the zero `ir.WalkPath`, which renders "" without allocating. Measure visitors
through `irtest.AllocatingVisits`, which walks each as it would walk alone with the walk's own
path, and assert the exact allocating paths. It measures only a visit's repeat, so hold a first
meeting with a test that doubles the document. Check `go build -gcflags=-m` before trusting a
count.
- **Why.** Eager-render mutants survived a test indexing a map with the rendered path (#1125) and
visit tests on the zero path (#1133); a path kept on each first meeting survived every visit test
(#1147).
- **Probe.** Plant an eager render, and a path kept on a first meeting; each test reddens.

## Remove a process-wide confound from the measured window
`process-counter-window`
Expand Down
16 changes: 15 additions & 1 deletion ir/bigval.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,29 @@ type BigVal string
// value whose leading digit carries a power of ten past [MaxDecimalExp] with
// [ErrExponentRange]. It never rounds or reformats the significant digits: a
// high-precision decimal and an exponential literal are preserved verbatim,
// and only JSON-invalid affixes are normalized.
// and only JSON-invalid affixes are normalized. A literal in JSON form already
// is returned as it is, allocating nothing.
func NewBigVal(s string) (BigVal, error) {
l, _, err := readDecimal(s)
if err != nil {
return "", fmt.Errorf("bigval: %q: %w", s, err)
}
if l.spelledForJSON(s) {
return BigVal(s), nil
}
return BigVal(jsonSpelling(l)), nil
}

// spelledForJSON reports whether s, the text l was scanned from, is what
// jsonSpelling makes of l already: no leading "+", an integer part that is "0"
// or opens with another digit, and a point only where digits follow it. Those
// are the only affixes jsonSpelling rewrites, and it keeps every other part as
// written.
func (l decimalLiteral) spelledForJSON(s string) bool {
whole := l.intPart == "0" || (l.intPart != "" && l.intPart[0] != '0')
return whole && s[0] != '+' && l.point == (l.frac != "")
}

// jsonSpelling rewrites the JSON-invalid affixes of a scanned literal into JSON
// form without altering its value: it drops a leading "+", gives the integer
// part its one leading digit (".5" → "0.5", "09" → "9"), and drops a trailing
Expand Down
66 changes: 66 additions & 0 deletions ir/bigval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,72 @@ func TestNewBigVal_CanonicalizesToJSONForm(t *testing.T) {
}
}

// TestNewBigVal_ReturnsAJSONSpelledLiteralAsItIs holds the shortcut that keeps
// a literal already in JSON form to exactly the literals jsonSpelling leaves
// unchanged, over every string of up to six runes drawn from the runes the
// grammar treats differently: a sign, a zero, another digit, a point and an
// exponent marker. A literal it takes wrongly would be stored un-canonicalized,
// and one it misses only costs a string.
func TestNewBigVal_ReturnsAJSONSpelledLiteralAsItIs(t *testing.T) {
t.Parallel()
const runes, longest = "+-01.e", 6
size, width := 1, 1
for range longest {
width *= len(runes)
size += width
}
literals, shorter := make([]string, 1, size), []string{""}
for range longest {
longer := make([]string, 0, len(runes)*len(shorter))
for _, s := range shorter {
for _, r := range runes {
longer = append(longer, s+string(r))
}
}
literals, shorter = append(literals, longer...), longer
}
var read, asIs int
for _, s := range literals {
spelling, spelled, ok := ir.JSONSpelling(s)
if !ok {
continue
}
read++
if spelled {
asIs++
}
require.Equal(t, spelling == s, spelled, "%q spells as %q", s, spelling)
v, err := ir.NewBigVal(s)
require.NoError(t, err, s)
require.Equal(t, spelling, v.String(), s)
}
assert.Positive(t, asIs, "some literal is in JSON form already")
assert.Less(t, asIs, read, "some literal is rewritten")
}

// TestNewBigVal_AllocatesOnlyToRewrite pins NewBigVal's cost: nothing for a
// literal in JSON form, whatever its parts, and the one string a rewrite
// builds otherwise. Reading the range joins no digits, so a literal with digits
// either side of its point costs no more, even past the length a join could be
// built on the stack. It is not run in parallel: AllocsPerRun measures the
// process.
func TestNewBigVal_AllocatesOnlyToRewrite(t *testing.T) {
for _, tc := range []struct {
in string
allocs float64
}{
{"0", 0}, {"-0", 0}, {"42", 0}, {"3.14", 0}, {"-0.5", 0}, {"0.0", 0}, {"1.5e+3", 0},
{"2.5E-3", 0}, {"12345678901234567890.12345678901234567890", 0},
{"+3.14", 1}, {".5", 1}, {"5.", 1}, {"007.5", 1}, {"-00.5e3", 1},
} {
var err error
allocs := testing.AllocsPerRun(10, func() { _, err = ir.NewBigVal(tc.in) })

require.NoError(t, err, tc.in)
assert.Equal(t, tc.allocs, allocs, tc.in)
}
}

func TestBigVal_JSONRoundTrip(t *testing.T) {
t.Parallel()
v, err := ir.NewBigVal("123456789012345678901234567890.5")
Expand Down
72 changes: 49 additions & 23 deletions ir/decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,14 @@ type Decimal struct {
}

// decimalLiteral is a numeric literal split as written: its sign, the digit
// runs either side of the point, and the exponent with its e or E and sign, or
// "" when it has none. It keeps what [BigVal] spells and [Decimal] normalizes.
// runs either side of the point, whether a point was written at all, which a
// point with no digits after it leaves no other trace of, and the exponent with
// its e or E and sign, or "" when it has none. It keeps what [BigVal] spells and
// [Decimal] normalizes.
type decimalLiteral struct {
neg bool
intPart, frac string
point bool
exponent string
}

Expand All @@ -88,26 +91,27 @@ type decimalLiteral struct {
// Zero is accepted at any written exponent. The result is the zero Decimal on
// error, which reads as the number zero, so a caller must check the error.
func ParseDecimal(s string) (Decimal, error) {
_, d, err := readDecimal(s)
l, exp, err := readDecimal(s)
if err != nil {
return Decimal{}, fmt.Errorf("decimal: %q: %w", s, err)
}
return d, nil
return l.decimal(exp), nil
}

// readDecimal splits and reads s, so [NewBigVal] spells the literal from the
// split that [ParseDecimal] reads the value from. Its error is bare, for each
// of them to name itself in.
func readDecimal(s string) (decimalLiteral, Decimal, error) {
// readDecimal splits s and reads the power of ten its leading digit carries, so
// [NewBigVal] spells the literal from the split that [ParseDecimal] reads the
// value from. The split is substrings of s, so reading it allocates nothing.
// Its error is bare, for each of them to name itself in.
func readDecimal(s string) (decimalLiteral, int64, error) {
l, ok := scanDecimal(s)
if !ok {
return decimalLiteral{}, Decimal{}, ErrNotDecimal
return decimalLiteral{}, 0, ErrNotDecimal
}
d, err := l.decimal()
exp, err := l.leadExponent()
if err != nil {
return decimalLiteral{}, Decimal{}, err
return decimalLiteral{}, 0, err
}
return l, d, nil
return l, exp, nil
}

// scanDecimal splits s by the grammar sign? (digits ("." digits*)? | "." digits)
Expand All @@ -125,7 +129,7 @@ func scanDecimal(s string) (decimalLiteral, bool) {
l.intPart, i = s[i:end], end
if i < len(s) && s[i] == '.' {
end = scanDigits(s, i+1)
l.frac, i = s[i+1:end], end
l.frac, l.point, i = s[i+1:end], true, end
}
if l.intPart == "" && l.frac == "" {
return decimalLiteral{}, false // no digit anywhere: "", "+", ".", "Inf"
Expand Down Expand Up @@ -188,24 +192,46 @@ func exponentValue(exponent string) int64 {
return e
}

// decimal reads l as a [Decimal]. Zero is accepted at any written exponent; any
// other value must carry its leading digit's power of ten within ±[MaxDecimalExp].
// leadExponent returns the power of ten l's leading significant digit carries,
// or 0 for zero, which is accepted at any written exponent; any other value
// must carry it within ±[MaxDecimalExp]. It counts the digits where they lie
// rather than joining the two runs, so it allocates nothing.
//
// The sum below stays inside int64: exponentValue is at most 4·10¹⁸ in
// magnitude and the digit term at most len(s), which is below Go's largest
// allocation. Saturating an exponent never changes a verdict, since a saturated
// exponent plus any string's digit count is still past the bound.
func (l decimalLiteral) decimal() (Decimal, error) {
digits := l.intPart + l.frac
lead := strings.TrimLeft(digits, "0")
if lead == "" {
return Decimal{}, nil // zero, at any written exponent
func (l decimalLiteral) leadExponent() (int64, error) {
zeros := l.leadingZeros()
if zeros == len(l.intPart)+len(l.frac) {
return 0, nil // zero, at any written exponent
}
exp := exponentValue(l.exponent) + int64(len(lead)-len(l.frac)-1)
exp := exponentValue(l.exponent) + int64(len(l.intPart)-zeros-1)
if exp > MaxDecimalExp || exp < -MaxDecimalExp {
return Decimal{}, &ExponentRangeError{neg: l.neg, small: exp < 0}
return 0, &ExponentRangeError{neg: l.neg, small: exp < 0}
}
return exp, nil
}

// leadingZeros returns how many zeros open l's digits: the integer part's, and
// when those are all zeros, the fraction's after them.
func (l decimalLiteral) leadingZeros() int {
zeros := len(l.intPart) - len(strings.TrimLeft(l.intPart, "0"))
if zeros == len(l.intPart) {
zeros += len(l.frac) - len(strings.TrimLeft(l.frac, "0"))
}
return zeros
}

// decimal reads l, whose leading digit carries the power of ten exp, as a
// [Decimal]. Its digits are the two runs joined, which costs an allocation when
// both hold one, so only [ParseDecimal] asks for them.
func (l decimalLiteral) decimal(exp int64) Decimal {
lead := strings.TrimLeft(l.intPart+l.frac, "0")
if lead == "" {
return Decimal{} // zero, at any written exponent
}
return Decimal{neg: l.neg, digits: strings.TrimRight(lead, "0"), exp: exp}, nil
return Decimal{neg: l.neg, digits: strings.TrimRight(lead, "0"), exp: exp}
}

// Sign returns -1, 0 or +1 as d is negative, zero or positive. Having no digits
Expand Down
30 changes: 19 additions & 11 deletions ir/declarations.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ir

import (
"iter"
"reflect"
)

Expand All @@ -9,37 +10,37 @@ import (
// the Go compiler cannot check.
const idFieldName = "ID"

// IDDeclaration is one node's declaration of its own identity: the class of ID,
// the ID itself, and the path of the node that declares it.
// IDDeclaration is one node's declaration of its own identity: the class of ID
// and the ID itself. Where the node sits is [IDDeclarations.Paths]'s to spell.
type IDDeclaration struct {
// Class is the Go type of the ID, which names the reference class the
// declaration settles.
Class reflect.Type
// ID is the declared identity.
ID string
// Path locates the declaring node, as [WalkValues] spells it.
Path string
}

// IDDeclarations collects, as a [Visitor], the identities the values its walk
// reaches declare, in walk order and each with its path, for a check reporting
// where one is declared, such as twice. Reading them off the value graph covers
// a new ID-bearing node the moment it exists. [IDReferences] reads those it
// resolves references against, rendering no path.
// reaches declare, in walk order, for a check reporting where one is declared,
// such as twice, each with its path kept unspelled for the check to spell
// only those it reports. Reading them off the value graph covers a new
// ID-bearing node the moment it exists.
//
// An empty ID is skipped, since nothing can reference one and several nodes
// carrying one are not duplicates. Reporting it is irverify.checkRegistryKeys's
// job for a map-keyed class and irverify.checkDeclaredIDs's for the rest. The
// zero IDDeclarations is ready to walk with.
type IDDeclarations struct {
ids IDReader
list []IDDeclaration
ids IDReader
list []IDDeclaration
paths PathLog
}

// Visit records the identity v declares, if any, and asks for v's children.
func (d *IDDeclarations) Visit(v reflect.Value, at WalkPath) bool {
if class, id, declares := d.ids.DeclaredIDOf(v); declares && id != "" {
d.list = append(d.list, IDDeclaration{Class: class, ID: id, Path: at.String()})
d.list = appendGrowing(d.list, IDDeclaration{Class: class, ID: id})
d.paths.Record(at)
}
return true
}
Expand All @@ -50,6 +51,13 @@ func (d *IDDeclarations) List() []IDDeclaration {
return d.list
}

// Paths yields the path of each declaration List holds, with its position
// there, in walk order, as a WalkPath valid until the next is yielded: a check
// keeps the String of those it reports and spells no other.
func (d *IDDeclarations) Paths() iter.Seq2[int, WalkPath] {
return d.paths.All()
}

// IDReader reads the identity a value declares for itself, reading where a
// struct type declares one only the first time it meets the type: a visitor
// shown every value of a walk meets a few dozen struct types many times over,
Expand Down
Loading
Loading