Skip to content

docs: the tutorial, zero to every feature (#447) - #520

Merged
gafferongames merged 43 commits into
mainfrom
tutorial
Sep 4, 2026
Merged

docs: the tutorial, zero to every feature (#447)#520
gafferongames merged 43 commits into
mainfrom
tutorial

Conversation

@gafferongames

@gafferongames gafferongames commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

docs/TUTORIAL.md: fourteen parts, from an empty directory to a program that uses every feature the C++ reference implements. The narrative path through the material USAGE.md presents feature by feature, linked from the README hub above it.

Scope, and the commit the outputs come from

Every command, output, id, header excerpt and byte count on this page was produced by bin/schema built from bafdb69 (schema v2.4.0-148-gbafdb69), with the serialize siblings at the tags this build pins: serialize v1.16.2 and serialize.c v1.9.2. Every C++ and C program on the page was compiled and run, one at a time.

Re-verified unchanged at 64325e1, this branch's base: every protocol id the page prints, the build version, the baseline projection version and Part 6's byte counts all reproduce. #530, #531, #532 and #534 moved the spec and the certification legs, not the emitted bytes.

The tutorial's scope is the feature set the C++ reference implements at that commit. A specification page without an implementation is not taught, because a reader who follows the page must be able to run it. Three features are in the spec at bafdb69 and refused by the compiler, so they are parts this page owes their implementation PRs:

owed part what the compiler says at bafdb69
wstring(N), the wide string type (#510) Bad.schema:3:19: expected newline after field, found "("
field-level tags and doc columns (#529) Bad.schema:3:21: unknown attribution "tags" — the vocabulary is typed and closed per compiler version (SPEC §4.2)
retain-unknown (#527) Bad.schema:3:9: a table declaration takes no qualification (docs/SPEC-TABLES.md)

When each lands, it gets its section, run the same way as everything else.

One unit, cumulative

The page grows one schema unit, starlight, from four constants to nine files, and every name it uses is declared in the part that introduces it. Every pasted output belongs to that unit as it stands at that point, and Part 1 says so once and names schema id and schema projection as the way a reader compares.

file joins in
Game.schema 1 (constants), 2 (enums, flags), 3 (ShipState), 4 (Vector3, the grown ShipState)
Vectors.schema 4, the cpp_native mapping and game_vector2.h
Net.schema 5, Contact through Packet and PacketHeader
Config.schema 6 (ShipConfig), 7 (GunnerSettings, WeaponConfig, GameConfig, optionals, the keyed array), 10 (systems)
Scene.schema 8, Waypoint and Route
Render.schema 12, RenderShip, RenderLaser, RenderFrame

A second unit, starlight/tools, holds the C++-only table forms, introduced in Part 7 by the refusal that prescribes exactly that move (... or move the union and its tables to their own unit). It holds: a union a table closure holds, with a table arm, a scalar arm and a payload-free arm (ToolBody); an array of unions (ToolLog); *string and *bytes (Attachment, AttachmentSet); and Part 8's maps (Fleet). Because they live there, the starlight unit generates for all nine targets, which the page shows in Parts 6, 7, 8 and 12, and Parts 12 and 13 run.

Ruling 1: present state

The rule now lands in Part 6, where tables first appear and the promise is made: the table wire is C++'s within one build, the other eight targets are named with their issues #511 to #518, and the packet wire, the cook and the block cross all nine.

Part 13 is present state. It runs the three crossings that work, each from the starlight unit into C: the packet wire (C reads a ShipState C++ wrote), the cook (C opens Game.cook C++ cooked), and the block (Part 12's produce.cpp to consume.c). No failed handoff as a centerpiece, no rewrite narrative, and no in-flight vocabulary in the page's own voice. Where a refusal appears it is quoted as the tool's own text.

Prerequisites, and programs that build

Part 1 states the Go requirement, the clone, make, the PATH step, and both sibling checkouts with their tags, before Part 3 and Part 13 need them.

Every ./program the page runs is a program the page shows, with its compile line, and every file it reads is created on the page first: ship.bin, forged.bin, dart.bin, config.bin, packed.bin, frame.block, packet.bin, Game.cook and Mine.cook.

Notable corrections from the cold read

  • The Vec2/Vec3/GameVec2 muddle is one Vector2 in Vectors.schema with game_vector2.h shown in full and exercised by the program.
  • TableReport is shown and printed with all six members after tables: the id-table wire (#435) #507, duplicate included.
  • The truncation claim is demonstrated at three cut points, and the form-byte refusal has a forged.bin the page writes.
  • The check notice is explained in Part 6 with this unit's output and its exit status, answered in Part 10, and reconciled with "silent on success" in Part 14.
  • The retired [<= N] sentence is gone; the [2..8] count hazard is shown through the generated write instead of asserted.
  • The "a forged wire with a reference loop is refused" claim is removed, because it is not true. Part 8 replaces it with an exhaustive single-bit sweep of an 80-byte pointered wire and tells the reader to bound their own walks. See below.

Findings

#521 carries every finding, in #447's six categories, with a reproduction command each.

G-19 is the one worth reading first. A single flipped bit in an 80-byte pointered wire loads with an all-zero report (malformed=0, refused=0, unknown=0) and hands the caller a node whose next points at itself. The obvious walk over it never terminates. The sweep is on the page and in the issue.

Also new in this pass: the table wire not crossing languages (G-15) and the refusal verdict living only in C++ (G-16), both the ports work #507 declared; two refusals still describing the wire as "field-tagged TLV" (G-17); and #447's F-16 re-observed now that the local and UTC dates disagree (G-18).

🤖 Generated with Claude Code

@gafferongames

Copy link
Copy Markdown
Contributor Author

Rulings from the cold read, under the owner's word of 2026-09-04:

  1. The tutorial ships as a present-state page before the table ports land. The rule that the table wire is C++'s within one build today, with the other eight targets refusing a table unit by name behind Go: the table wire's id-table form (#435) #511 to Rust: the table wire's id-table form (#435) #518 while the packet wire, the cook and the block cross all nine, is stated in part 6 where the promise is made; part 13 is present state with no rewrite narrative.
  2. The tutorial's scope is the feature set the C++ reference implements at the commit its outputs come from, named in this PR's body. Pages without implementations (doc comments and tags, retain-unknown, wstring) are not taught until they land; each lands its own part with its implementation PR.

Structural fixes from the read land on this branch before it leaves draft: one cumulative unit with every name declared where introduced; the C++-only table forms in a second unit named as such in part 7; the sibling checkouts stated with tags; every program and file shown before it is used; the foreign-unit outputs replaced.

gafferongames and others added 23 commits September 5, 2026 02:14
…nts (#447)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ers (#447)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rmat (#447)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
#447)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ng the compiler (#447)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…gs (#447)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
)

Every command and program in parts 1 through 5 re-run against bin/schema built
from b74a7d4. The packet wire is untouched by the id-table wire: both protocol
ids, every generated header excerpt, every diagnostic and every program output
are byte-identical to what the page already carried. Only the build stamp in
part 1's install block moved.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Re-run against bin/schema built from b74a7d4 (#507). What moved: a saved
ShipConfig is 89 bytes where it was 42, an all-default one is 10 where it was
2, the report carries a sixth member (the refusal verdict), and a truncated
file now yields nothing at all rather than a good prefix — the id table is the
trailer, located from the end, so a cut file has no vocabulary to name its
fields with. The page now teaches the form byte and the refusal verdict beside
the five counters. What did not move: every drift result and every counter
value, in both directions, and all nine targets still generate the surface.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Re-run against b74a7d4 (#507). Every Measure moved: the fighter is 10 and 22
where it was 2 and 11, GameConfig is 142 where it was 99, and the three tool
messages are 79, 49 and 45 where they were 42, 22 and 18. What did not move:
the optional storage layout, all four ? refusals word for word, the None-key
abort in a release build, the keyed array's survival of a mid-enum insertion
with unknown=0, and the message union's tag enum and arms.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Re-run against b74a7d4 (#507). What moved: the route wire is 163 bytes where
it was 172, the fleet is 145 where it was 165, and the four-thousand-node
parallel build is 51,904 where it was 111,989 — the id table in the trailer
carries each field id once, so a pointer-heavy graph is where the new form is
cheapest. The page now says so beside the number. What did not move: the region
size, sharing and null preserved through the round trip, the builder's 8,264
bytes, the map's ascending-key iteration and Find, and all three pointer
refusals.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…#447)

Re-run against b74a7d4 (#507). Nothing in part 9 moved: the JSON a table
writes and reads is text, and the wire's framing does not reach it. Every
document, every report line, the &node graph form with sharing preserved, the
unpacked tree's file list, --one-file, and the strict-mode refusal are all
byte-identical to what the page carries. The packed file is larger for the
reason part 6 now explains, and the page never printed its size.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Re-run against b74a7d4 (#507). What moved: the baseline projection is version
7 where it was 6, and its field ids are the wire's new 64-bit hashes where they
were 16-bit. What did not move: every one of the three verdicts word for word,
both was guard rails, the --update refusal without a reason, the history block
the override appends, and the was round trip reading a v1 file into the renamed
field with unknown=0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames and others added 20 commits September 5, 2026 02:14
Re-run against b74a7d4 (#507). What moved: both build versions, and the facts
projection is form 3 with 64-bit field ids where it was form 2 with 16-bit ones.
What did not move: the cook's own sizes — 408 bytes, 328 data, 16 attribution —
because a cook is a laid-out region and not the wire, so the id-table form does
not reach it. Nor did Open, cook-check, the uncook byte-for-byte proof, the
big-endian refusal on a little-endian build, the target-neutral build version
across byte orders, or the protocol id's independence from a table edit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Re-run against b74a7d4 (#507). What moved: the build version stamped in the
block prologue, in both the C++ and the C halves. What did not move: the block
is 56,064 bytes of a 196,672 maximum, exactly as before — the block form is a
laid-out extent and the id-table wire does not reach it — and the count refusal,
the C read through the aligned base, and the same-build refusal after a
one-sided rebuild are all unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Re-run against b74a7d4 (#507). The packet-wire handoff is unchanged: C reads
the C++ packet bit for bit. The table-wire handoff is NOT, and the page now
says so rather than claiming a crossing that does not happen today. C++ reads
its own 89-byte save cleanly and both C and Go answer malformed on the same
bytes, because the reference is on the new form and the eight ports have not
moved to it. The page shows all three outputs, names what the ports do with
bytes they cannot read (report malformed, misdecode nothing), notes that only
C++ carries the refusal verdict, and points a reader who needs another language
today at the packet wire, the cook or the block. Filed as G-15 in #521.

Also moved: the reflection walk's ids are 64-bit and the enum field's kind is
30 where it was 7. The embedding demo is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
#447)

Re-run against b74a7d4 (#507). The command map, the misplaced-flag failure, the
baseline notice, all four edge refusals and the absent UnitView are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One cumulative unit named starlight, and the prerequisites a stranger needs
before parts 3 and 13: the Go toolchain, the clone, make, the PATH step, and
the two sibling serialize checkouts at the tags this build pins (v1.16.2 and
v1.9.2). Every diagnostic now shows its Bad.schema before its output, with the
error trailer. New: a paragraph saying once that every id and byte count on the
page belongs to the unit as it stands at that point, and naming schema id and
schema projection as the way to compare.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Enums and flags land in the cumulative starlight unit, with the whole file shown
after the edit. Pending is declared where it is discussed. Every diagnostic
shows its Bad.schema and its error trailer. SystemFlagsNamesMax is shown as
generated rather than asserted, the ADL note now points at the program's
using-directive, and the program is shown entire and runs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ShipState joins the cumulative unit with the constant it needs shown in place.
The program is shown entire, with the forged-packet half in it rather than as a
loose fragment, and the compile line names -I gen -I ../serialize, the checkout
part 1 now makes. The five range refusals show the Bad.schema shape they share
and each carries its error trailer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Vector2/GameVector2 replaces the Vec2/Vec3/GameVec2 muddle: one declaration in
its own file of the same unit, one native header shown in full, one field on
ShipState, and the program exercises the operator. The retired [<= N] sentence
is gone and the [2..8] count hazard is demonstrated with the generated write
rather than asserted. Game.schema is shown entire at the end of the part, and
the program compiles as shown with -I gen -I . -I ../serialize.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ChatLine, Snapshot and MaxPayloadsPerPacket are declared where the page uses
them, in Net.schema of the same unit, and the program builds and reads a real
two-payload Packet rather than a loose FireCommand. The scalar-arm refusal is
shown here, where a reader first wants it, and it names the table closure part 7
builds. The id demonstration adds an arm and takes it back out, showing the id
return.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ruling 1's rule lands here, where tables first appear: the table wire is C++'s
within one build, the eight ports are named with their issues #511 to #518, and
the packet wire, cook and block cross all nine. The check notice is explained
with this unit's own output and its exit status. The evolution demo copies the
whole unit to starlight-2.0 and starlight-3.0, so both programs are shown entire
and every file they read is created on the page. TableReport is shown and printed
with all six members, duplicate included, and the truncation claim is
demonstrated at three cut points rather than asserted. The flags law now uses
this unit's own SystemFlags.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GunnerSettings, WeaponConfig and MaxWeapons are declared where the page uses
them. The second unit lands here as the ruling asks: starlight/tools, its own
package, holding the C++-only table forms (a union a table closure holds, with
table, scalar and payload-free arms; an array of unions; *string and *bytes),
introduced by the refusal that names the move. The main unit is shown still
generating for all nine. config.cpp, none.cpp, keyed.cpp and tools.cpp are each
shown entire with their compile lines, and config.bin is created here for parts
9 and 11.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Pointers stay in the starlight unit, which still generates for all nine, and
maps move to the tools unit where the C++-only forms live. SetName and heads are
declared in the programs that use them, and route.cpp, damage.cpp, wide.cpp and
fleet.cpp are each shown entire with their compile lines.

The 'a forged wire with a reference loop is refused' claim is replaced by what
the loader actually does: an exhaustive single-bit sweep of an 80-byte pointered
wire, showing 295 stopped, 345 opened, and one that hands back a self-referential
node under an all-zero report. The page tells the reader to bound their own
walks. Filed as G-19 in #521.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
json.cpp and loadpacked.cpp are shown entire with their compile lines, and the
graph half is in the same program rather than a loose fragment. The level_name
edit is shown before the load prints it, the Corvette edit is shown as the file
the designer saves, and the 'very strong' edit is reverted and repacked on the
page before part 11 cooks the tree. Absent optionals not appearing in ToJson is
stated where the output shows it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The baseline is created over the real cumulative unit, so the notice part 6
introduced is answered here and check goes silent again on the page. ShipConfig
gains a systems SystemFlags field so the flags law is demonstrated on this unit's
own flags rather than an invented one. The was diagnostic is shown beside the
declaration that produced it, and the was round trip reads ship.bin, written in
part 6 under the old field name.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
cook.cpp is shown entire with its compile line, and Mine.cook is produced by the
program on the page before cmp compares it. The build-version demonstration adds
a real field to this unit's GameConfig, shows the baseline passing it, the
protocol id holding and the build version moving in one place, and takes it back
out. Every cook command runs against the tree part 9 packed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
RenderShip, RenderLaser and RenderFrame join the cumulative unit with the two
constants they need, and the unit is shown still generating for all nine.
produce.cpp and consume.c are shown entire with their compile lines, frame.block
is created by the program on the page before the consumer reads it, and
TableBlockRefusal is shown as generated so the field names in the program match.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ruling 1: the part now shows the three crossings that work — the packet wire,
the cook and the block, each from the starlight unit into C, each run — instead
of staging a failed table handoff. No rewrite narrative and no in-flight
vocabulary in the page's own voice; the C++-only rule is stated once in part 6
where the promise is made. writepacket.cpp, readpacket.c, opencook.c and
reflect.cpp are shown entire with their compile lines.

The reflection walk now distinguishes an enum's vocabulary from a flags field's
by the variant_id tell, so Shields is no longer dropped, and the Go embedding
example has its go.mod, its imports and a sorted iteration order with the reason
that determinism is a generator's contract.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The 'silent on success' rule is reconciled with the notice part 6 introduced and
part 10 answers, with the rule stated as success is silent except while a table
format is unguarded. Every edge refusal shows its Bad.schema and its error
trailer, the bare-bytes case shows all four errors it really prints, and the
UnitView gap is shown against this unit's own generate listing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
)

save2.cpp, load3.cpp, keyed.cpp and j.cpp now appear with their sources and
their compile lines, so every ./program the page runs is a program the page
built. Verified against the real files: load3.cpp was rewritten in the working
tree to match the printf the page shows, and re-run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The cold reader's last item. Only the check branch calls the unguarded-format
nudge; generate prints nothing on success, verified:

  $ schema check .
  notice: nt declares 1 table and . holds no tables.baseline — ...
  $ schema generate --lang cpp --out gen .
  $

Corrected in part 14 and in the identical sentence part 6 carried. Part 10's
separate claim, that generate diffs the closure against a committed baseline,
stands: generate prints the same warnings and the same refusal as check and
exits 1 on a refused edit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gafferongames
gafferongames marked this pull request as ready for review September 4, 2026 16:15
@gafferongames
gafferongames merged commit 06cab85 into main Sep 4, 2026
20 checks passed
@gafferongames
gafferongames deleted the tutorial branch September 4, 2026 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant