diff --git a/AGENTS.md b/AGENTS.md index 613c3c7..8fc3f98 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,7 +55,7 @@ In [docs/decisions.md](docs/decisions.md): - A render shares one reference draw per category, per group - A draw group name is local to its category - The expansion hold and the render's draws are two fences -- A record makes its draw maps up front, a `Fake` on its first read +- A hold set makes its draw maps on the first read - A category never references itself, and a record's fences run at load - A record's column set is fixed before the first draw - Null is a `null` item, not a rate diff --git a/docs/decisions.md b/docs/decisions.md index d66bd23..8358cc6 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -242,14 +242,15 @@ groups. They pin different things — an operand pins the value its own render p and stops at a reference, a path pins every level it passes through — so one walk would carry both rules and both scopes anyway, and tell them apart at every step. -## A record makes its draw maps up front, a `Fake` on its first read +## A hold set makes its draw maps on the first read -A record's columns always read through the render's draws, so making the maps where the -set is declared keeps them on that frame's stack. A `Fake` often reads no reference path -at all, and making them anyway cost about a fifth of the cheapest render, so it makes -them on the first read instead, at two heap allocations for a render that does share a -draw. The allocation gate over a repeat of a reference path and over a named draw group -prices that, and pins what keeps a hold set off the heap. +Every render starts from the zero `holdSet`, per goal 2's one spelling. Making the maps +where the set is declared kept a record's on its frame's stack, but a `Fake`'s set lives +on the `Generator`, so there it cost every render two heap allocations and about half the +cheapest one's time, measured 2026-09-28. Lazily, a render reading a reference path pays +those two allocations, a record or struct included, and one reading none pays nothing; +goal 11 holds either way. The allocation gate over a repeat of a reference path and over +a named draw group prices that, and pins what keeps a hold set off the heap. ## A category never references itself, and a record's fences run at load diff --git a/holdset.go b/holdset.go index b03fb4a..83dbab0 100644 --- a/holdset.go +++ b/holdset.go @@ -20,10 +20,6 @@ type renderScope struct { pins *pinSet } -func eagerHoldSet() holdSet { - return holdSet{unnamed: hold{variant: map[string]node{}, value: map[string]readValue{}}} -} - // renderOnce renders n as one render, over a hold set of its own. func renderOnce(s *session, n node) string { var set holdSet diff --git a/record.go b/record.go index 67ef410..b0c2627 100644 --- a/record.go +++ b/record.go @@ -130,7 +130,7 @@ func (f *Generator) FakeRecord(path string) (*Record, error) { if err != nil { return nil, fmt.Errorf("fejkdata: %s: %w", path, err) } - set := eagerHoldSet() + var set holdSet sc := renderScope{set: &set} if t, isTable := n.(*table); isTable { if n, err = tableRecord(f.rand, t, tail, sc); err != nil { @@ -204,7 +204,7 @@ type RecordTemplate struct { func (t *RecordTemplate) Fake() *Record { t.g.mu.Lock() defer t.g.mu.Unlock() - set := eagerHoldSet() + var set holdSet return renderRecord(t.g.rand, t.template, t.columns, renderScope{set: &set}) } diff --git a/struct.go b/struct.go index 251f199..da98175 100644 --- a/struct.go +++ b/struct.go @@ -351,7 +351,7 @@ func (p *valueProof) checkField(label string, ft reflect.Type, column node) erro // fill draws the record into v's tagged fields, then each nested struct as a record of its own. func (s *structShape) fill(sess *session, v reflect.Value) { if s.record != nil { - set := eagerHoldSet() + var set holdSet for i, c := range renderRecord(sess, s.record, s.columns, renderScope{set: &set}).columns { setColumn(fieldAt(v, s.fieldIndexes[i]), c) } diff --git a/todo.md b/todo.md index f6fbef1..615586a 100644 --- a/todo.md +++ b/todo.md @@ -21,8 +21,6 @@ Self-sufficiency 5.6), every seat capped by Locality, and all nine naming This round, in order: -- Start every render from one spelling of an empty hold set: `Fake` uses - `holdSet{}`, the record and struct paths `eagerHoldSet()`. - Correct the false comments: `funcCall` names `checkFunc` where `parseFormat` reports, `struct.go`'s "t sits at index within c.t" names no field, `recordFormats` calls ndjson "the same column", `Generator` says every other entry point takes `mu`