Commit Graph

103 Commits

Author SHA1 Message Date
M 138bd21d83 Reject an empty field name
A field named "" was reachable by exactly one spelling, a bare {} token,
and by no dot path: List hid it and Fake could not ask for it. checkName
rejects it now, for the reason it rejects a dot — a name no path can reach.

That also makes the empty-segment rule unconditional. {a.} was rejected
because a field really named "" would otherwise have made it resolve; no
data can carry one, so the rule now guards a shape that cannot exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 07:31:18 +02:00
M e31e77b8a3 Tests for an empty field name
A field named "" loads, and a bare {} token renders it, but no dot path can
reach it — List hides it and Fake cannot ask for it. {a.} is already rejected
for the same reason, so the two spellings disagree about the same mistake.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 07:31:18 +02:00
M 4b3dd011fc Pin the per-head walk, and say where the hold stops
Sharing renders' seen set across heads left the suite green while silently
under-rejecting: the first head's walk marks the only route to the second
head's draw. This PR widens heads from the bound levels to every held name,
so two heads in one template is now the ordinary case; a rejected case whose
violation sits on the later head pins the scoping.

An operand rendering one field twice covers the revisit guard, taking
coverage to 97.8% against main's 97.4%.

The README said the two spellings are rejected alike without saying what the
hold follows, which is not true where the operand reaches its own field
through a reference. It now names the plain {field} tokens the walk follows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M 21e7af6c1f Hold what a calc operand's render settled, up to a reference
The fence pinned only the operand node, so a reference reaching into it drew
that value again: {net.v} was a load error while {..cat.net.v} loaded and
disagreed in 20 of 40 renders. The hold now covers what rendering the operand
settles inside itself, so both spellings of that shape are rejected.

The walk stops at a {..path} edge, where the operand's own value ends and a
shared source begins. cover stops there too, through named, so both halves of
the fence end at the same boundary — and two names drawing from one referenced
category stay two draws.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M 8d03ce34bb Tests for a reference reaching into a calc operand
{net.v} is a load error because the operand is also a path head, but
{..cat.net.v} names the same node by another spelling and loads — and
disagrees in 20 of 40 renders. Same shape, opposite verdicts, which is the
thing this rule exists to remove.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M cf3229cd4d Pin the route an operand fence names
The operand label had no test: every rejected case reported through a token
edge, so flipping the flag that distinguishes the two left the suite green
while the error went back to inventing a {b} the format never wrote. Each
case now asserts the route it names, and one reports through an operand edge,
which is the branch that was uncovered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M 39d59aec23 Pin the prep-time guards, and say why an operand's set is the narrow one
The guards report an invariant break instead of computing around it, and no
data reaches them, so they are called directly: coverage 97.4% -> 97.7%,
which also covers the empty-cover skip through a literal operand.

operandDraw's doc keeps why neither wider set works, since both were tried
here — containment reaches a sibling the operand never renders, the render
closure reaches a source two operands share — and drops the rest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M f949550515 One spelling for an unvalidated builtin arg, and say what the hold buys
float's prep still swallowed ParseFloat's error three lines from the atoi that
had just been changed to report one — two spellings of "parse an arg the check
already validated". atof reports it the same way.

The README paragraph said a reference reaching through an operand is caught,
which the narrowed fence no longer does and should not: it says what the hold
pins instead. Restored the why the trimmed comment in expand had carried.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M 027d5c33fc Name a calc operand as one when the fence reports it
A renderEdge label can be an operand name, and both messages printed it as
{name} — so an author was told a token renders the level when no such token
existed in the format. An edge now carries whether its label is an operand and
names itself the way boundReaders does, so both fences speak one language.

The narrowed operand cover fixed the other half: the render closure pulled the
other operand into the cover, which could report the two sides the wrong way
round ({n} renders "m" where it is m that reaches n).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M 3f46e9c701 Tests for what a calc operand's hold actually pins
A calc renders its operand whole, so the draw it holds is that one node's
value. The render closure was too wide: it pulled in shared descendants, so
two operands drawing from one source were rejected — two dice over one
{..die}, or {w} x {h} over one {..dim} — though each is drawn once, shown
once, and cannot disagree. main accepts all of them.

So the fence is another route naming the operand's own node, and these pin
both sides: a reference to the operand, one level down, or wrapped in a
choice is rejected; two names drawing from one source, a sibling the operand
never renders, and a literal operand are accepted. That reverses the case
added earlier this round, which read a shared source as a second spelling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M 7e073462c8 Document the fence a calc operand gets, and trim two comments
The one-spelling section described a calc operand only as a route that
renders a level, so a reader could not predict that {..cat.net} beside
{calc(net * 2, 2)} is now a load error with no path token in sight. It says
so, and says what an operand's draw covers and what it leaves free.

Dropped the paragraph in checkBoundLevelsHeld that restated its own first
line and the loop below it, and the escape-analysis note in expand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M 0cee8b2183 Report a prep-time arg that its check should have caught
calcPrep discarded parseCalc's error, so a divergence from checkCalc would
have produced a nil AST and then a nil dereference per render, with no
message. atoi did the same quietly, returning zero for a length, range or
decimal count. Both now panic naming the argument, so the invariant they rest
on fails where it breaks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M 2e187117f8 Cover a calc operand by what it renders
The fence now asks the right question for each kind of hold. A path head keeps
containment, since a path may read into anything the level carries. An operand
head takes the render closure, because a calc renders its field and that draw
fixes only the value the render produces.

That stops rejecting a reference to a sibling the operand never renders, which
had loaded fine before the fence widened, and starts rejecting a reference
that reaches what the operand renders through. A head whose cover is empty —
a fixed string — skips the walk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M 779b89a804 Tests for what an operand's draw actually fixes
Widening the fence used containment for both kinds of hold, which is right
for a level a path reads and wrong for a sibling a calc reads. A path may
read into anything the level contains; an operand's draw fixes only the value
it renders. So containment rejects a reference to a sibling the operand never
renders, which loads fine today and cannot disagree, and misses a reference
reaching what the operand renders through, which disagreed in 20 of 40 draws.

Also pins the repeated reference arm, the one kind that reaches the repeat
check by passing the per-arm checks rather than falling through them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M 909596ec2d Trim comments that restate or argue
The operand order was written out in three places; it now lives at calcVars,
which fixes it, and the other two point there. Dropped the note explaining
why ')' is not reserved, which said what the set is not rather than what it
is, and shortened reservedList's doc to less than its body. The note on held
now states where it reaches instead of arguing against a design this repo no
longer contains.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M 1b5e928d4f Fail loudly where calc says it cannot happen
calcVar.eval returned NaN on a path indexVars makes unreachable, and a name
missing from the index map read operand 0. Both would have printed a wrong
number rather than reporting anything. They panic now, like child() does for
the same class of slip, so drift between calcVars and indexVars surfaces as a
stack trace naming the operand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M 1ba9185edc Pin the calc stream against a moving operand
The calc case drew from single-element lists, so its pinned bytes were
constant wherever the rng sat — it could not have caught a change to how many
draws a calc consumes, which is exactly what the operand hold changed.
Multi-element lists make that stream a pinned fact, and every pinned pair
multiplies correctly, so the hold is pinned with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M 2dbc9b1f9f Drop the unreachable reference-prefix checks
A name starting with ".." also starts with ".", so loadDir's hidden-entry
skip reaches it first and neither isRef branch could ever fire. Removed, and
the comment in named now names the skip that actually holds the invariant it
depends on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M 25a3077925 Say what a dot-prefixed data entry actually does
Two cases claimed to pin that a category or folder named with the reference
prefix is rejected. They passed for the wrong reason: the entry is skipped as
hidden, which left the directory empty, so New failed with "no .json data
found" instead. Beside a valid sibling both load fine and are simply absent.

Replaced with a test of the real rule — a leading dot means hidden, which is
what lets a data directory be a checkout — and that covers a ".." name too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M 258bce3b98 Let a clearer token error win over the repeated-arm check
The dedup now runs after the per-arm loop, so a missing reference path or a
missing field is reported as itself. A repeated reference arm is still
caught: the loop lets a well-formed reference through without erroring, and
the dedup reads the arms as written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M dea1fcf311 Tests for an arm check that yields to a clearer error
{..|..} reported a repeated arm rather than a reference with no path, and
{|} reported one rather than a missing field. Both are load errors either
way, but the repeat is a consequence of the real mistake, so the real one
should be what the author reads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M 465eb05704 Fence a calc operand against every route that renders it
checkBoundLevelsHeld already performed the right walk; it iterated the wrong
set. Walking t.held covers both kinds of hold — the level a dotted token
reads and the sibling a calc reads — so the guarantee the README states now
holds for every spelling of the operand, not just the bare one.

The path message is unchanged. A calc-only head gets its own, naming the
sibling spelling that reads the held draw.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M d664b1b25c Tests for a reference spelling of a calc operand
A {calc()} operand is drawn once and held, but only a dotted token's level was
fenced against a second route to it. A {..path} naming the same field renders
it afresh, so the value shown is not the value computed — 47 of 60 renders
disagreed on a five-value operand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:58:37 +02:00
M ac0309cf94 Say what a name may contain, and what a calc operand reads
The Computation section carried the caveat this branch removes — that a field
shown and used in a calc is drawn twice — so it now describes the hold
instead. The name rules name all four reserved characters rather than the
dot alone, in both the Data and Data format sections, and the alternation
entry says a repeated arm is a load error and points at weight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:33:05 +02:00
M 9ed0d3bf55 Hold a calc operand's draw for the expansion
A field the format rendered and a calc read was drawn twice, so the operand
shown could differ from the operand computed — the same disagreement the
dotted-path rule already fences, in its plainest spelling. The README
carried it as a caveat, which is what a rule like this exists to remove.

A calc operand now joins the names an expansion holds, so it is drawn once
and every later read of it — the calc, and any bare token spelling it —
sees that draw. The hold stays per expansion: each repeat iteration and each
nested template draws its own.

expand reads a calc's operands before the call and hands over their values,
so a builtin takes (emitted, operands) rather than the sibling fields, and
the evaluator indexes that slice instead of walking the node tree. That is
what keeps the draws a local. Handing them to a builtin instead lets them
escape through an indirect call, which put two maps on the heap for every
held format, calc or not: BenchmarkBound went 56 B/5 allocs -> 744 B/10.

Measured against main: Bound 359 -> 349 ns at 56 B/5 allocs, unchanged;
Calc 416 -> 577 ns and one more alloc, which is what the correlation costs.

Validation is unchanged: t.bound still carries only the levels a dotted
token reads, so the overlap fences fence exactly what they did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:33:05 +02:00
M 4f87a4dd9e Tests for a calc operand reading the expansion's draw
A field the format renders and a calc reads is drawn twice today, so
"{net} x {qty} = {calc(net * qty, 2)}" can print a net that is not the net
it multiplied. The dotted-path rule already holds one draw for a level;
these pin the same hold for a plain sibling a calc reads.

Also pinned: the hold reaches a bare token of the same name, belongs to one
expansion (each repeat iteration and each nested template draws its own),
and leaves a name no calc reads drawing every time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:33:05 +02:00
M b09bc56e87 Reject a repeated alternation arm
An alternation picks its arms evenly, so {a|a|b} was a covert 3:1 skew — a
third spelling of what a choice's weight is for, reachable only by
experiment. The check runs over the arms as written, before a reference arm
is let through, so {..a|..a} is caught too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:33:05 +02:00
M cc76d300e7 Tests for a repeated alternation arm
{a|a|b} skews an alternation three to one, an undocumented third way to
weight beside a choice's weight and a repeated choice entry. These pin its
rejection, and one accepted case fixes the boundary: {x}{x} is two tokens,
so it stays two independent draws.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:33:05 +02:00
M b7c854f2d4 Reject a name the token grammar reserves
The dot rule was spelled twice — once for a category or folder, once for a
field — and covered only the dot. Both now go through checkName, which
rejects the four characters no format can name: . | ( and }.

')' stays legal: on its own it is spellable, and rejecting it would refuse a
name that works.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:33:05 +02:00
M cc8f8db83f Tests for a name no token can spell
A dot is already rejected in a category, folder or field name. The token
grammar reserves three more characters: '|' separates a token's arms, '('
opens a function call, and '}' ends the token. A name carrying one resolves
by dot path and is advertised by List, yet no format can name it.

These pin the rejection, and one accepted case fixes the boundary: ')' on
its own is spellable, so it stays legal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:33:05 +02:00
M 403d68de27 Write every builtin one way
A builtin could be spelled as prep (args parsed once, at compile) or call
(args parsed per render), reaching the same closure by two routes. Nothing
enforced the "exactly one of" invariant: supplying both silently dropped
call, supplying neither compiled and then panicked inside a render, and a
test existed only to police it.

prep does everything call did, so it is now the one spelling. The zero-arg
shapes the README already names — a derivation reads the output emitted so
far, a generator reads only the rng — lift through derive and generate, so
the registry reads as that taxonomy. iban stops looking its country up per
render.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:33:05 +02:00
M 24084fc0cf List every node's children one way
authored and named differed only in whether a "..path" binding was skipped,
which made two spellings of one question: what does this node contain? They
could never disagree — loadDir rejects a category or folder carrying the
prefix, so a group's children are never bindings, and TestReferenceErrors
pins both rejections.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:33:05 +02:00
M 5a69b02ee0 Find a format's bound readers in one ordered scan 2026-08-30 22:31:54 +02:00
M c1222eaf72 Label the error fences, and settle a tie by where the format writes it 2026-08-30 22:31:54 +02:00
M 4a1073f018 Test that a path reaches every variant it might draw 2026-08-30 22:31:54 +02:00
M 32b9aa5c81 Aim a path token's render edge at what it renders 2026-08-30 22:31:54 +02:00
M 1987deb14c Tests for a path token rendering its leaf, not the level it starts from 2026-08-30 22:31:54 +02:00
M 447cfae50a Hold a bound level against every route that renders it, and let a matching string be 2026-08-30 22:31:54 +02:00
M c755d085a9 Tests for a reference reaching a bound level from anywhere, and a literal that merely matches 2026-08-30 22:31:54 +02:00
M 3ebaa5958d Close the calc and reference spellings of an overlap, and reach the repeat rule behind a choice 2026-08-30 22:31:54 +02:00
M 9b5e34332b Tests for overlap via calc and references, repeat behind a choice, and nested choice draws 2026-08-30 22:31:54 +02:00
M f7165274c7 Accept one spelling per draw, and reject a path into a repeating level 2026-08-30 22:31:54 +02:00
M 97ecbe208f Tests for one spelling per draw, and a path into a repeating level 2026-08-30 22:31:54 +02:00
M e05fed1190 Hold a path's last level too, and reject an unfinished path 2026-08-30 22:31:54 +02:00
M 8accca54b1 Tests for a path sharing its prefix's draw and rejecting an empty segment 2026-08-30 22:31:54 +02:00
M e4b37091e4 Say that a path is held at every level 2026-08-30 22:31:54 +02:00
M f512c1663d Hold a path's draw at every level it passes through 2026-08-30 22:31:54 +02:00
M eb0c5dc066 Tests for a path holding its draw at every level 2026-08-30 22:31:54 +02:00
M 644f3ed9d9 Benchmark what a bound draw costs 2026-08-30 22:31:54 +02:00
M b8da6fdbc0 See a path token's head when walking for cycles 2026-08-30 22:31:54 +02:00