Commit Graph

139 Commits

Author SHA1 Message Date
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
M d7c4f43135 Document the path token and the one-draw rule 2026-08-30 22:31:54 +02:00
M a25b42ace2 Read one value when a bound path is read twice 2026-08-30 22:31:54 +02:00
M 12100239f5 Draw a dotted token's head once per expansion 2026-08-30 22:31:54 +02:00
M 5263c866c4 Tests for a dotted sibling token drawing its head once per expansion 2026-08-30 22:31:54 +02:00
Mikael Göransson e371d217f4 Say what the objectid category actually is, and re-flow the registry comment 2026-08-28 13:35:12 +02:00
lilleman 43d3a752b3 Retire the objectid builtin; ship it as data instead 2026-08-28 13:35:12 +02:00
lilleman c11ae5f04f Expect objectid as a data category rather than a builtin 2026-08-28 13:35:12 +02:00
M 16f91503b0 Reject a standalone weight on key presence, whatever its value 2026-08-28 13:27:32 +02:00
M 48cadd6c4d Tests that a standalone weight of any type is rejected 2026-08-28 13:27:32 +02:00
M b14caae3a8 Reject options that cannot take effect, and skip entries that are not data 2026-08-28 13:27:32 +02:00
M 8d0393713c Tests for options that cannot take effect, and entries that are not data 2026-08-28 13:27:32 +02:00
lilleman af09a86e71 Express hex-shaped data with the hex() builtin instead of a hand-rolled choice 2026-08-28 13:25:41 +02:00
M aec0497ce5 Say what the triple-letter test and the escape rule actually cover 2026-08-28 13:24:07 +02:00
M 630f75f556 Remove the duplicated Vasa street prefix 2026-08-28 13:24:07 +02:00