The World Vol 8 · Words
ch 81 / 105
Chapter 81

Pieces of a Word

The sealed language package

This volume puts a machine that reads and writes English beside The World. Nothing in the valley is allowed to move while that happens: the ground rules, the animals, the genome, the creature network and the package that writes the world down all have to end this volume with the bytes they started with.

The new package is internal/lang, every piece of language machinery in this volume lives there, and the five simulation packages plus the store are sealed against it by a test that walks their import lines. The last volume made the same kind of seal around SQL because a tick that waits on a socket has no budget. This volume needs the rule at the slower end: a tick is a hundred milliseconds, while a sentence can take seconds to come back.

Start with the ground, which has no creatures in it at all. A thousand valley-years of terrarium with an empty creature phase hooked into every one of its three million six hundred thousand ticks, folded down to two digests: one for the ground and everything standing on it at the end, one for a census taken every midsummer of all thousand years.

$ go run ./cmd/books -mode keep -years 1000
books: seed 5, 1000 years, an empty roster hooked into every tick

  world    df1c117e69bdf157   the terrarium's own, unmoved
  census   75c77a67f6f430bb   the terrarium's own, unmoved

Those two digests are the oldest figures in this book. They cover seed flight, germination, plant death, moisture, nutrient and where a founding generation lands, and the census half means a valley that arrived at the right ending by a different road would fail. Three and a half minutes of arithmetic for two digests is a fair price for a claim nobody has to take on trust.

Then the animals. Nineteen creatures founded on the herd stream, each stamped out of an identity genome, each handed a brain of its own, and two years of valley with nothing registered into the breeding seam. The right-hand column of that block is not somebody copying the left-hand one: those figures are constants inside the bench, and the word at the end of each line is a comparison the program made.

$ go run ./cmd/stamp -mode herd
stamp: 12x8 valley, tick 901, year 1 summer, 12 plants standing at 1517.5 grams

  19 creatures founded on stream 12, each stamped from an identity
  genome and given a brain of its own off stream 14
  beast.Roster.Born is nil: nothing has registered a breeding pass into it

                                              this run   before the genome   holds
  creatures founded                                 19                  19   yes
  creatures struck off                              17                  17   yes
  still walking after 6750 ticks                     2                   2   yes
  first death                         tick 1044 on 9,5    tick 1044 on 9,5   yes
  last death                         tick 3400 on 11,5   tick 3400 on 11,5   yes
  grams in                                46567.944597        46567.944597   yes
  grams out                               46567.944597        46567.944597   yes

  the two ledgers differ by -1.455e-11, which is the last bits of the adding

Seventeen animals starve, and they starve on the ticks they have always starved on: the first on tick 1044 over cell 9,5 and the last on tick 3400 over 11,5. The grams close in and out at 46567.944597, which is the same number the ledger has closed on since the chapter that first weighed a valley.

The third is neither the ground nor a valley. Sixty-four genomes drawn off their own stream, four trials apiece, scored on grams eaten, every scorecard folded into one digest. A scorecard carries one column per thing a creature could choose to do, so it is the run that would catch a table that quietly grew a column.

$ go run ./cmd/score -mode spread | tail -18
  the best of them                           179.4530
  the genome a quarter of the way down        40.0000
  the middle one                               4.8405
  three quarters of the way down               0.0000
  the worst of them                            0.0000
  the average                                 24.5807
  genomes that ate anything at all                 38
  genomes that scored nothing whatever             26

  grams the generation ate between them     1573.1642
  grams of fodder it was offered          120320.0000
  the share of the larder it found              1.31%
  trials that ended in a store at nothing          129
  creature-ticks over the ring of water          1885
  creature-ticks past the last cell                 0

  digest of the scored generation      e3cd4c94f2fa1612

Best still 179.4530, middle still 4.8405, average still 24.5807, twenty-six of the sixty-four still scoring nothing whatever, and the generation still folding to e3cd4c94f2fa1612. Three runs, three unrelated parts of the world, and not a figure out of place on any of them.

Those three were not picked for being cheap. They were picked because they have no package in common past the module boundary: the first runs the terrarium with the creature phase emptied out, the second runs creatures over a valley with the breeding seam disconnected, and the third runs a scoring harness that is neither. A change that moved one of them and left the other two alone would be a change to one subsystem, and a change that moved all three would be a change to the arithmetic underneath all of them. Between them they leave very little room for a volume to have touched something quietly.

Now the rule that keeps them that way. The last volume's version of this test reads the import lines of five packages and fails if any of them names the store. This one reads six and fails if any of them names internal/lang, and the sixth is the store itself, sealed against language on purpose: a vector arriving at a database is a list of floats and a count of them, and the thing writing it down has no need to know what produced it.

▣ Build · stage 1 — the seal, written as something that fails
// internal/lang/rule_test.go
var sealed = []string{"sim", "terra", "beast", "gene", "mind", "store"}

func TestNoSealedPackageImportsLang(t *testing.T) {
	const forbidden = "theworld/internal/lang"
	fset := token.NewFileSet()
	checked := 0
	for _, pkg := range sealed {
		dir := filepath.Join("..", pkg)
		ents, err := os.ReadDir(dir)
		if err != nil {
			t.Fatalf("reading %s: %v", dir, err)
		}
		for _, e := range ents {
			if e.IsDir() || !strings.HasSuffix(e.Name(), ".go") {
				continue
			}
			path := filepath.Join(dir, e.Name())
			f, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly)
			if err != nil {
				t.Fatalf("parsing %s: %v", path, err)
			}
			checked++
			for _, imp := range f.Imports {
				p, err := strconv.Unquote(imp.Path.Value)
				if err != nil {
					t.Fatalf("%s: unreadable import %s", path, imp.Path.Value)
				}
				if p == forbidden {
					t.Errorf("%s imports %s: a tick that can ask a model a question has no budget", path, forbidden)
				}
			}
		}
	}
	if checked < 80 {
		t.Fatalf("only %d files read across %v: the walk found less than the tree holds", checked, sealed)
	}
	t.Logf("%d files read across %d sealed packages, and not one of them names it", checked, len(sealed))
}
$ go test ./internal/lang/ -run 'NoSealedPackageImportsLang' -v
=== RUN   TestNoSealedPackageImportsLang
    rule_test.go:65: 96 files read across 6 sealed packages, and not one of them names it
--- PASS: TestNoSealedPackageImportsLang (0.00s)
PASS
ok  	theworld/internal/lang	0.004s

The floor of eighty files is the part people leave out. A walk that finds nothing passes cheerfully, and a test that passes cheerfully after somebody moves a directory is worse than no test at all, because it is now evidence. Ninety-six files today, and the number only goes up.

With that settled, here is what the rest of the page is for. Nothing that reads text reads letters, and nothing that reads text reads words either. What a reader of language is handed is a list of integers, and there is a table that turns one into the other. A token is a run of bytes the table has a number for. That sentence is the whole subject of this chapter, and by the end of it you will have built the table, used it in both directions, and measured one sentence in the unit it defines.

The chronicle text corpus

A table like that has to be counted off some text, and there is a choice about which text. The usual answer is a scrape of everything: encyclopedias, forums, code, the lot. This world has something better to hand, and it is the only place in this volume where the subject of the book and the subject of the volume are the same object. Since the chapter that gave it a founding document, The World has been keeping a chronicle. Eleven rows written on the tick it was named, and one row a year for as long as it runs, each of them a finished English sentence with the world's own names already in it.

Two registers live in that file and they need telling apart before anything counts them. The eleven founding rows are an operator writing prose: full sentences, first person, a few hundred words of somebody deciding what things are going to be called. The eighty yearly rows are annalistic, which is the register a chronicle has always wanted: what year it is, what the count was, what changed since the last one. One of those is English and the other is a form with numbers in it, and a table counted off both learns both. Where that shows up is at the far end of the table, and it is visible enough that it is one of the runs below.

The chronicle lives in a database, and this chapter has no business starting one. So it is dumped: an eighty-year run of the valley, every line it wrote, one JSON object a line, committed under exemplars/ and embedded the way the founding document is embedded. The mode that cut it is in this page's bench and needs no database either, because a chronicle row is a tick, a kind and a sentence, and the sentence is assembled out of the founding document exactly the way the daemon assembles it. What a database adds is the entry number, which is a counter, and durability, which a fixture does not want.

▣ Build · stage 2 — eighty years, kept
// exemplars/exemplars.go
//go:embed chronicle.jsonl
var Files embed.FS

// Line is one row of the chronicle as the dump carries it: the number
// the database gave it, the tick it belongs to, what sort of entry it
// is, and what it says.
type Line struct {
	Entry int64  `json:"entry"`
	Tick  int    `json:"tick"`
	Kind  string `json:"kind"`
	Text  string `json:"text"`
}
$ go run ./cmd/pieces -mode annals
pieces: 80 years of one valley, and every line it writes down kept

  founded at tick 0: The World, on ground it calls The Hollow, 29 hobbs standing
  11 lines of the founding document, every one of them tick 0

   year     tick  walking     born     gone  chronicle
     10    35550      191     1013      851         21
     20    71550      236     1643     1436         31
     30   107550      238     1731     1522         41
     40   143550      235     1773     1567         51
     50   179550      237     1843     1635         61
     60   215550      236     1902     1695         71
     70   251550      237     1931     1723         81
     80   287550      238     1965     1756         91

  the chronicle it wrote
    rows                                            91
    of those, the founding document                 11
    and the yearly count                            80
    the text column, one entry a line             7123 bytes

  written to chronicle-81.jsonl
    bytes                                        11693
    sha256                            90cdf0222d1d64e2

  against the copy this build carries
    exemplars/chronicle.jsonl          90cdf0222d1d64e2
    and the run just made             90cdf0222d1d64e2
    the same bytes

  287550 ticks in 2m17.118s, 2097 ticks a second (measured here; yours will differ)

The last three lines are the reason the mode is in the bench at all. A committed file is a thing a reader has to believe; a committed file that a run reproduces to the byte is a thing a reader can check, and checking it costs one comparison of two digests. The duration and the rate on the closing line belong to an eight-core Ryzen 7 3700X and will be different on your machine. Everything above them is arithmetic and comes out the same anywhere.

Read that file back and one decision has to be made before any counting starts: which of the four columns is language. The entry number is something Postgres handed out. The tick is a number this world counts in. The kind is a label a program chose. None of those three is anybody's writing, and a table counted off them would spend its first dozen merges learning to spell integers. The corpus is the text column and nothing else, one entry a line.

▣ Build · stage 3 — the text column, and what is in it
// exemplars/exemplars.go
// Corpus is the text column of a chronicle and nothing else: one
// entry a line, every line ended.
func Corpus(lines []Line) []byte {
	var b bytes.Buffer
	for _, l := range lines {
		b.WriteString(l.Text)
		b.WriteByte('\n')
	}
	return b.Bytes()
}
$ go run ./cmd/pieces -mode corpus
pieces: the history this world kept, read as a corpus

  the file this build carries
    exemplars/chronicle.jsonl             11693 bytes
    sha256                           90cdf0222d1d64e2
    rows                                     91
      genesis                                11
      count                                  80

  the text column, one entry a line, and nothing else
    bytes                                  7123
    sha256                           9adbf2236921cdc7
    characters                             7123
    words, counted on whitespace           1400
    distinct byte values in it               53
    byte values a table starts with         256

  the first rows and the last, cut to fit
    entry     tick  kind     text
        1        0  genesis  This is The World. I built it and I run it, and on the ...
        2        0  genesis  The land is The Hollow. It is one valley with stone rou...
        3        0  genesis  The animals are hobbs. One of them is a hobb. They eat,...
        4        0  genesis  Asteria keeps it. She is not down in the valley and she...
        5        0  genesis  People gather at Firstlight. Nobody is there and nothin...
        6        0  genesis  The way in is The Commons. Anybody who comes here comes...
      ...      ...  ...      ...
       86   269550  count    year 75: 238 hobbs standing in The Hollow, 1962 born an...
       87   273150  count    year 76: 237 hobbs standing in The Hollow, 1962 born an...
       88   276750  count    year 77: 237 hobbs standing in The Hollow, 1962 born an...
       89   280350  count    year 78: 238 hobbs standing in The Hollow, 1963 born an...
       90   283950  count    year 79: 238 hobbs standing in The Hollow, 1963 born an...
       91   287550  count    year 80: 238 hobbs standing in The Hollow, 1965 born an...

  91 entries and not one of them is a word list: this is what the world said

Seven thousand one hundred and twenty-three bytes. That is a small corpus by any standard, and it is the right size for this chapter for a reason that is not about convenience: a table counted off it is small enough that you can print a readable slice of the whole thing and argue with individual rows. Two other numbers on that block matter later. Characters equals bytes exactly, 7,123 both, because every byte this world has ever written down is a plain ASCII one. And fifty-three distinct byte values occur in seven thousand bytes, out of the 256 a byte can hold, which is the first hint that counting bytes one at a time wastes almost all of the room available.

The merge table

Here is the whole algorithm in four sentences. Start with every byte value as a token of its own, so there are 256 tokens before anything has been learned and no input can ever arrive that the table has no number for. Count how often each adjacent pair of tokens occurs across the corpus. Take the commonest pair, give it a number nothing else has, and replace every occurrence of it with that number. Do the whole thing again on the result, as many times as you want tokens.

Do one by hand before running any of it, on eleven bytes taken out of the corpus: standing in. Eleven bytes make ten adjacent pairs, and here they are, in order: st, ta, an, nd, di, in, ng, ,  i, in. Nine of the ten occur once. One of them, in, occurs twice, at bytes six and seven and again at bytes ten and eleven.

So the commonest pair is in, and merging it means minting one number that nothing else uses and putting it wherever those two bytes sat side by side. The eleven tokens become nine: s, t, a, n, d, the new one, g, a space, and the new one again. Two tokens saved, one row added to a table. Count the pairs of that nine-token list and the process starts over, with the new number now eligible to be half of the next pair, which is how a token that stands for four bytes gets built out of a token that stands for two.

Every property this chapter needs is already visible in that eleven-byte example. The table grows by exactly one row a merge. The text shrinks by exactly the number of times the merged pair occurred. And the order matters, because merging in first destroys the pair di as a thing that could ever be merged in this text, while creating a pair that did not exist before.

The counting is the part to run first, because it is the part people assume is complicated. It is one pass and a map.

▣ Build · stage 4 — every adjacent pair, counted
// internal/lang/token.go
// Break is the one byte a merge never touches.
//
// Pairs are counted inside an entry and never across the end of one,
// because two chronicle entries are two things that happened and
// nothing joins them. The consequence is worth being clear about: no
// merge can contain this byte, so an encoder can run straight down a
// whole file and the boundaries look after themselves.
const Break = '\n'

// commonest is the pair to merge next, and how often it occurs.
func commonest(lines [][]int) (Pair, int) {
	tally := map[Pair]int{}
	for _, line := range lines {
		for i := 0; i+1 < len(line); i++ {
			tally[Pair{line[i], line[i+1]}]++
		}
	}
	var best Pair
	most := 0
	for p, n := range tally {
		switch {
		case n > most:
			best, most = p, n
		case n == most && (p.A < best.A || (p.A == best.A && p.B < best.B)):
			best = p
		}
	}
	return best, most
}
$ go run ./cmd/pieces -mode pairs
pieces: every adjacent pair of bytes in the corpus, counted once

  bytes of corpus                        7123
  adjacent pairs, entries apart          6941
  distinct pairs among them               412

  the 12 commonest, which is the order the merges will come in
    rank    a    b  pair        count
       1  110  100  "nd"          192
       2   97  110  "an"          190
       3  105  110  "in"          188
       4  110   32  "n "          181
       5  101   32  "e "          161
       6   32   49  " 1"          160
       7  104  101  "he"          135
       8   32   97  " a"          130
       9  115   32  "s "          130
      10   32  105  " i"          129
      11  100   32  "d "          119
      12  110  103  "ng"           97

  the commonest pair is 192 of 6941, one in 36.2

Two details in that block decide reproducibility. The first is the tie-break, and the fact that there has to be one. Ranks eight and nine are both 130, and a Go map hands its keys back in a different order every run, so a program that took whichever it saw first would train a different table on Tuesday than it trained on Monday. The rule here is the cheapest one available: lowest first half, then lowest second half. Which rule it is barely matters. That it is a rule at all is what makes a token count a number two machines can compare.

The second is that the counting is done inside an entry and never across the end of one. Two chronicle rows are two things that happened years apart, and a pair straddling them is an accident of how the file was written down. Refusing to count those has a consequence that pays for itself: no merge can ever contain a line ending, so an encoder handed a whole file never has to know where the lines are.

Merging is the same loop with a substitution on the end of it. Every merge mints exactly one new number, which is why the table's rows are in a fixed order and why that order is the table: the first row is applied first when anything is encoded, and a table whose rows were shuffled would encode the same sentence into different integers.

▣ Build · stage 5 — merge, and merge again
// internal/lang/token.go
func Train(corpus []byte, want int) *Table {
	lines := split(corpus)
	var merges []Pair
	for len(merges) < want {
		p, n := commonest(lines)
		if n < 2 {
			break
		}
		id := Bytes + len(merges)
		for i := range lines {
			lines[i] = apply(lines[i], p, id)
		}
		merges = append(merges, p)
	}
	t, err := New(merges)
	if err != nil {
		// Unreachable: every merge above is made of ids that exist.
		panic(err)
	}
	return t
}

// apply replaces every occurrence of one pair with one id, left to
// right and without overlapping: in three of the same byte running
// together, the first two merge and the third is left where it is.
func apply(ids []int, p Pair, id int) []int {
	out := ids[:0:0]
	for i := 0; i < len(ids); {
		if i+1 < len(ids) && ids[i] == p.A && ids[i+1] == p.B {
			out = append(out, id)
			i += 2
			continue
		}
		out = append(out, ids[i])
		i++
	}
	return out
}

The stopping condition is the only judgement in there. A pair that occurs once is a pair whose merge costs a row in the table to save a single token, so the loop stops when the commonest pair left is down to one. On this corpus that happens at 354 merges, and this chapter keeps 256 of them, which puts the vocabulary at 512: the byte alphabet, doubled.

$ go run ./cmd/pieces -mode merges
pieces: 256 merges counted off 7123 bytes of chronicle

  byte values, there from the start        256
  merges asked for                        256
  merges learned                          256
  vocabulary                              512

  the first 12, in the order they were learned
    token     a     b  stands for
      256   110   100  "nd"
      257   105   110  "in"
      258    97   256  "and"
      259   101    32  "e "
      260    32    49  " 1"
      261   115    32  "s "
      262   104   259  "he "
      263   257   103  "ing"
      264    32   258  " and"
      265   263    32  "ing "
      266   111   110  "on"
      267    32    98  " b"

  and the last 12
    token     a     b  stands for
      500    57   356  "9 hobbs standing in The Hollow, 16"
      501    57   371  "9: 235 hobbs standing in The Hollow, 18"
      502    58    32  ": "
      503    65   439  "Any"
      504    73    32  "I "
      505    79   110  "On"
      506    84   104  "Th"
      507    87   353  "Way"
      508    97   117  "au"
      509    97   276  "ast"
      510    97   305  "at "
      511    98   111  "bo"

  the longest token in the table
    token                                   458
    bytes it stands for                      39
    which are                        ": 238 hobbs standing in The Hollow, 196"

  written to merges-81.txt
    bytes                                  5892
    sha256                           ed2bfde380f9fa11
    read back, vocabulary                   512
    the same table, and the same corpus in the same tokens

Read the top twelve and the bottom twelve as two different things, because they are. The top twelve are English. nd, in, and, , ing: those are the pieces any English corpus would hand over first, and a table counted off a library would learn most of them in the same order. Token 260 is the odd one,  1, a space followed by a digit, and it is there because eighty of these ninety-one entries are a sentence with four numbers in it.

The bottom twelve are this world and nowhere else. Token 500 stands for thirty-four bytes of one particular sentence about a valley; token 501 for thirty-eight bytes of the same sentence with different numbers in it. The longest row in the table is thirty-nine bytes long. Those are not words and they are not phrases anybody chose; they are the most repeated runs of bytes in a history that says almost the same thing eighty times, and the counting found them without being told what a sentence is.

The tail of the table also shows the tie-break doing its job in public. From token 502 onwards the first column climbs steadily, 58, 65, 73, 79, 84, 87, then three at 97 and one at 98: every one of those pairs occurs exactly twice, so the counts no longer separate them and the ordering is entirely the rule. When a table's rows stop being interesting, that is what it looks like from the outside.

The table has to survive being written down, because a table that only exists inside the process that counted it is not a table anybody can price a prompt against. It goes out as text: one merge a line, four tab-separated columns, and a header naming the corpus it was counted from.

▣ Build · stage 6 — the table on disk, and what reading it refuses
$ head -8 merges-81.txt
# a merge table for The World, one merge a line
# corpus 7123 bytes, sha256 9adbf2236921cdc7
# merges 256, vocabulary 512
256	110	100	"nd"
257	105	110	"in"
258	97	256	"and"
259	101	32	"e "
260	32	49	" 1"

The fourth column can be worked out from the first three, so it carries no information at all, and the loader treats it accordingly: it reads the column and checks it against what the first three make, and takes nothing from it. A file you can open and read is a file whose first merge you can argue with, and checking the readable column means it cannot quietly drift away from the one that matters. The digest in the header is there because a merge table and the text it was counted from are one object in two files: encoding a corpus with a table trained on a different one is not an error anything can catch; it costs more tokens than the matching table, for ever.

// internal/lang/table.go
		want := Bytes + len(merges)
		if got[0] != want {
			return nil, fmt.Errorf("lang: line %d of the table mints token %d where token %d comes next", n, got[0], want)
		}
		merges = append(merges, Pair{got[1], got[2]})
		t, err := New(merges)
		if err != nil {
			return nil, fmt.Errorf("lang: line %d of the table: %w", n, err)
		}
		shown, err := strconv.Unquote(f[3])
		if err != nil {
			return nil, fmt.Errorf("lang: line %d of the table: column 4 is not a quoted string: %q", n, f[3])
		}
		if real := string(t.piece[want]); shown != real {
			return nil, fmt.Errorf("lang: line %d of the table says token %d is %q and its two halves make %q",
				n, want, shown, real)
		}

Three refusals, and each of them is a state a hand-edited file can be in. A row out of order mints the wrong number and everything after it is off by one. A half that has not been minted yet is a table that cannot be built, and it is the check that stops a decoder recursing for ever on a cycle somebody typed in by accident. And a fourth column that disagrees with the first three is a file whose readable part was edited and whose real part was not, which is the exact failure a readable format invites.

Four bytes and three merges A stack of four rows. The bottom row holds four byte tokens: 105 for the letter i, 110 for n, 103 for g and 32 for a space. Merge 257 joins 105 and 110 into the two bytes i-n. Merge 263 joins token 257 and byte 103 into i-n-g. Merge 265 joins token 263 and byte 32 into i-n-g followed by a space, which is one token standing for four bytes. Lines run upward from each byte to the merge that consumes it. FOUR BYTES AND THREE MERGES how token 265 comes to stand for four bytes token 265 "ing " = 263 + 32 token 263 "ing" = 257 + 103 token 257 "in" = 105 + 110 105 "i" 110 "n" 103 "g" 32 a space byte tokens 2nd merge 8th merge 10th merge
Figure 81.1 — a token is a tree of merges with bytes at the leaves, and decoding it is walking the tree down. Nothing above the bottom row existed before the counting; nothing in the bottom row had to be learned.

The byte round trip

Encoding is the training loop with the counting taken out. Every byte becomes its own token, then every merge is applied in the order it was learned, and the result is the same list of numbers the training arrived at. That is not a coincidence to be grateful for, it is the reason encoding is one function and not a second implementation of a first: a test in this package encodes the whole corpus and checks the count against the count training finished on.

▣ Build · stage 7 — both directions
// internal/lang/token.go
func (t *Table) Encode(b []byte) []int {
	ids := make([]int, len(b))
	for i, c := range b {
		ids[i] = int(c)
	}
	for i, p := range t.merges {
		ids = apply(ids, p, Bytes+i)
	}
	return ids
}

func (t *Table) Decode(ids []int) ([]byte, error) {
	out := make([]byte, 0, len(ids)*2)
	for i, id := range ids {
		p, err := t.Piece(id)
		if err != nil {
			return nil, fmt.Errorf("lang: token %d of %d: %w", i+1, len(ids), err)
		}
		out = append(out, p...)
	}
	return out, nil
}

// Piece is the bytes one token stands for.
func (t *Table) Piece(id int) ([]byte, error) {
	if id < 0 || id >= len(t.piece) {
		return nil, fmt.Errorf("lang: no token %d in a table of %d", id, len(t.piece))
	}
	return t.piece[id], nil
}

Piece is a bounds check and a slice index, and the bounds check is what stops this being a panic waiting to happen. An id is an integer that arrived from somewhere: out of a file, off a wire, out of somebody else's tokenizer. A table asked for token 700 when it holds 512 has been handed a number from a different vocabulary, and saying so is the difference between an error a caller can report and a crash halfway through a decode.

Encode makes one pass over the text for every merge in the table, so encoding a sentence with this table walks it 256 times. That is the wrong algorithm for a vocabulary of a hundred thousand and it is the right one here, where the table is 256 rows and the text is a sentence: the whole corpus, all 7,123 bytes of it, goes through in a few milliseconds. The thing to notice is not the speed but that the loop is literally the training loop with commonest deleted, so there is no second definition of what a token is anywhere in this package.

$ go run ./cmd/pieces -mode encode
pieces: one chronicle entry, encoded by a table of 512

  entry 51 of the chronicle
    year 40: 235 hobbs standing in The Hollow, 1773 born and 1567 gone

  bytes                                    66
  tokens                                    7

  every token of it, in order
       n  token  bytes  stands for
       1    331      6  "year 4"
       2     48      1  "0"
       3    383     38  ": 235 hobbs standing in The Hollow, 17"
       4     55      1  "7"
       5     51      1  "3"
       6    311     12  " born and 15"
       7    493      7  "67 gone"

  decoded again
    year 40: 235 hobbs standing in The Hollow, 1773 born and 1567 gone
    the bytes it started as                true

Sixty-six bytes and thirteen words go in, and seven numbers come out. Look at where the boundaries fall, because they fall nowhere a person would put them. Token 3 begins with a colon and ends in the middle of the number 1773. Tokens 4 and 5 are the two digits that particular year happened to need. Token 6 carries the phrase between two of the numbers and the first two digits of the second one. Not one of those seven tokens is a word, and no rule about words was involved in producing them: the counting found the runs that repeat, and in a history that writes the same sentence eighty times with different numbers in it, the runs that repeat are the sentence with holes where the numbers go.

Now the law, which everything above is in service of. Encode then decode returns the input bytes exactly, for every input, including inputs that are not text. It holds for free given a byte alphabet, and the reason to state it out loud is that the obvious variation breaks it.

$ go run ./cmd/pieces -mode round
pieces: encode, decode, compare, over an alphabet of bytes

  vocabulary                              512
  alphabet                              bytes

    input                                   bytes   tokens     back  same
    the whole corpus                         7123     1419     7123  true
    the first entry alone                     323      136      323  true
    a sentence the corpus does not hold        88       46       88  true
    nothing at all                              0        0        0  true
    a pasted line with a dash in it            44       22       44  true
    the same line cut to 26 bytes              26       12       26  true
    4096 seeded random bytes                 4096     4093     4096  true

  7 of 7 inputs came back as the bytes they went in as

The last row is the one that says the law is a law. Four thousand and ninety-six bytes drawn off a seeded generator are not text in any language, and the table encodes them into 4,093 tokens and gets all 4,096 bytes back. It saves three tokens out of four thousand, which is what a table trained on one small history is worth on noise, and it loses nothing, which is what a byte alphabet buys.

⚠ Worked failure — the tokenizer that reads characters

The reasonable alternative is to say that the alphabet is characters. A byte is a storage detail; text is made of characters; work in characters and the whole thing is tidier. In Go that is one conversion, right at the top of the encoder, before anything else has happened:

// the one line that makes a rune tokenizer a rune tokenizer:
// decide that the input is characters before deciding anything
// else about it.
in = []byte(string([]rune(string(c.in))))

Run the same seven inputs through it. Five of them come back unharmed, which is the trouble with this mistake: it works on everything you are likely to try first.

$ go run ./cmd/pieces -mode round -runes
pieces: encode, decode, compare, over an alphabet of runes

  vocabulary                              512
  alphabet                              runes

    input                                   bytes   tokens     back  same
    the whole corpus                         7123     1419     7123  true
    the first entry alone                     323      136      323  true
    a sentence the corpus does not hold        88       46       88  true
    nothing at all                              0        0        0  true
    a pasted line with a dash in it            44       22       44  true
    the same line cut to 26 bytes              26       16       30  false
    4096 seeded random bytes                 4096     7663     7666  false

  5 of 7 inputs came back as the bytes they went in as

  the first one that did not
    in                                       26 bytes  61636b20e280
    out                                      30 bytes  efbfbdefbfbd
    first byte that differs                  24

Start at the symptom. Twenty-six bytes went in and thirty came out, and the first byte that differs is byte 24. The input is an ordinary line somebody pasted out of an editor that turned two hyphens into a dash, and a dash of that kind is three bytes in UTF-8: e2 80 94. Cutting the line at twenty-six bytes, which is the sort of thing a preview column does without asking, leaves two of those three behind. So the last two bytes of the input are e2 80, which is the beginning of a character and not a character.

Now work back. []rune(string(b)) has to hand back something for those two bytes, and what Go hands back is the replacement character, once for each byte it could not make sense of. That is ef bf bd each time, six bytes where there were two, which is the thirty. The conversion has not failed and has not returned an error; it has quietly substituted, which is what it is documented to do and is exactly wrong here. Nothing downstream can tell. The decoder gets valid tokens, produces valid text, and the text is not what arrived.

The bottom row is the same cause with the volume turned up. Random bytes are almost never valid UTF-8, so nearly every one of them becomes a replacement character, and 4,096 bytes come back as 7,666. The byte tokenizer got that case exactly right without anybody thinking about it, because it never asked what a byte meant.

One limit of the byte alphabet needs to stay explicit: nothing stops a token boundary falling inside a character. The three bytes of that dash go through as three separate tokens, because this corpus has never held one, and a table counted off a corpus that had would happily merge two of the three and leave the third on its own. It does not matter. Decoding concatenates the pieces and the character reassembles itself, and that is the whole difference between the two designs: one of them can cut a character in half and put it back together, the other cannot represent one it did not expect.

▣ Build · stage 8 — the law, pinned
$ go test ./exemplars/ ./internal/lang/ -run 'TheDumpIsOneRowALine|TheCorpusIsTheTextColumnAndNothingElse|AHalfWrittenDumpIsAnErrorAndNotAShortHistory|EncodeThenDecodeIsTheInput|EncodingTheCorpusLandsOnTheTrainedCount|NoMergeCrossesTheEndOfAnEntry|TheSameCorpusTrainsTheSameTable|AMergeTableSurvivesBeingWrittenDown|ATokenFromAnotherVocabularyIsAnError' -v
=== RUN   TestTheDumpIsOneRowALine
    exemplars_test.go:29: 91 rows, entry 1 to entry 91, no gaps
--- PASS: TestTheDumpIsOneRowALine (0.00s)
=== RUN   TestTheCorpusIsTheTextColumnAndNothingElse
--- PASS: TestTheCorpusIsTheTextColumnAndNothingElse (0.00s)
=== RUN   TestAHalfWrittenDumpIsAnErrorAndNotAShortHistory
--- PASS: TestAHalfWrittenDumpIsAnErrorAndNotAShortHistory (0.00s)
PASS
ok  	theworld/exemplars	0.003s
=== RUN   TestEncodeThenDecodeIsTheInput
--- PASS: TestEncodeThenDecodeIsTheInput (0.00s)
=== RUN   TestEncodingTheCorpusLandsOnTheTrainedCount
--- PASS: TestEncodingTheCorpusLandsOnTheTrainedCount (0.00s)
=== RUN   TestNoMergeCrossesTheEndOfAnEntry
--- PASS: TestNoMergeCrossesTheEndOfAnEntry (0.00s)
=== RUN   TestTheSameCorpusTrainsTheSameTable
--- PASS: TestTheSameCorpusTrainsTheSameTable (0.00s)
=== RUN   TestAMergeTableSurvivesBeingWrittenDown
--- PASS: TestAMergeTableSurvivesBeingWrittenDown (0.00s)
=== RUN   TestATokenFromAnotherVocabularyIsAnError
--- PASS: TestATokenFromAnotherVocabularyIsAnError (0.00s)
PASS
ok  	theworld/internal/lang	0.008s

Nine tests and every one of them is a sentence from this page made checkable. The round trip over eight inputs including 512 seeded random bytes. Encoding landing on the count training finished with. No merge containing a line ending. The same corpus training the same table twice. The file format surviving a round trip and refusing three kinds of edited file. A token from another vocabulary coming back as an error. And, in the fixture package, that the committed chronicle parses to the last line, is numbered from one without a gap, and that a half-written dump is refused rather than read as a short history.

Pricing one sentence

Tokens are the unit this volume prices in, so the unit has to be settled before anything is quoted in it. The count is the number of entries the encoder emits, whitespace and punctuation included. It is never a word count and it is never a character count, and the way to make that a measurement instead of a claim is to print all three for one sentence.

The sentence is one of this world's own, out of the founding note about where whatever stops moving ends up.

∑ Math Interlude — the size of a table, and three counts of one sentence

Start with the table rather than the sentence, because two of its numbers are fixed by arithmetic and nothing else. A table that has learned m merges holds V = 256 + m tokens: the 256 byte values it began with, plus one new number a merge. With 256 merges that is 512, and the choice of 256 was made off the curve further down this page rather than off any property of language. The other fixed number is the floor. However many merges you learn, a text of b bytes can never encode to fewer than one token, and it can never encode to more than b, because every byte is already a token before any merge is applied. Every count in this volume sits between those two.

Take the sentence Bodies, husks, whatever falls: it ends in one place and the ground takes it back. Count it three ways by hand first.

Characters: every letter, every space, the two commas, the colon and the full stop. Eighty-one of them, and because every byte in this corpus is plain ASCII, eighty-one bytes as well. Words, if a word is a run of non-space characters: fifteen, counting falls: as one and back. as one. Tokens: whatever the table says, and the table has no opinion about spaces or full stops, so the answer cannot be guessed from either of the other two.

The ratios are what the rest of the volume actually uses. Write c for characters, w for words and t for tokens. Then c/t is how many characters one token is worth on this text, and t/w is how many tokens a word costs. Neither is a constant. Both are properties of the table and the text together, which is the single most useful thing to know about them: a figure measured on one text with one table is not a figure about another.

ccharacters in the text, counted one per character
wwords, counted as runs of characters with no space in them
ttokens, the number of entries the encoder emits
c/tcharacters a token, the compression the table is getting
t/wtokens a word, the number a price in tokens is usually guessed from
Vvocabulary: 256 byte values plus one entry per merge learned
mmerges the table has learned, one row of the file each
bbytes of text, which is also the largest token count it can have
$ go run ./cmd/pieces -mode price
pieces: one sentence, priced three ways

  the sentence, and where the chronicle keeps it
    Bodies, husks, whatever falls: it ends in one place and the ground takes it back.
    inside entry 10

  characters                               81
  words, counted on whitespace             15
  tokens, by a table of 512                41

  characters a token                     1.98
  tokens a word                          2.73

  and the same three over the whole corpus
    characters                           7123
    words                                1400
    tokens                               1419
    characters a token                   5.02

Eighty-one characters, fifteen words, forty-one tokens. The three numbers are not close to each other and no ratio between two of them survives being carried to the third. And the same table on the same corpus is getting 5.02 characters a token overall, two and a half times better than the 1.98 it manages on this sentence, which needs an explanation rather than a shrug.

The explanation is that eighty of the ninety-one entries are one sentence written eighty times with different numbers in it, and the merges went where the repetition was. That sentence encodes to seven tokens for sixty-six bytes. The founding note is prose that occurs once, so it gets the ordinary English merges and nothing else, and it costs 2.73 tokens a word. A reader who took the corpus-wide 5.02 and priced their own writing with it would be out by a factor of two and a half in the direction that costs money.

The trade behind the vocabulary size is the other half of the same picture, and the curve does something people do not expect.

$ go run ./cmd/pieces -mode grow
pieces: what each merge buys, over 7123 bytes of chronicle

     asked  learned vocabulary     tokens  bytes/token    saved
         0        0        256       7123         1.00         
         1        1        257       6931         1.03      192
         2        2        258       6743         1.06      188
         4        4        260       6400         1.11      343
         8        8        264       5901         1.21      499
        16       16        272       5171         1.38      730
        32       32        288       3836         1.86     1335
        64       64        320       2292         3.11     1544
       128      128        384       1783         3.99      509
       256      256        512       1419         5.02      364
       512      354        610       1223         5.82      196

  asking for more than 354 gets 354: no pair is left that occurs twice

  the table this chapter keeps
    merges                                  256
    vocabulary                              512
    tokens for the whole chronicle         1419

The saved column has to be read against the step it covers, because the rows are not evenly spaced. The very first merge takes 192 tokens off the corpus on its own. Merges 33 to 64, thirty-two of them, take 1,544 between them, about 48 apiece. Merges 65 to 128 take 509, under 8 apiece. The 128 after those take 364, under 3 apiece. Every one of them costs the same thing: one row in a table that has to be carried, shipped and held in memory beside everything else, and every one buys less than the one before it. That is the whole of what choosing a vocabulary size is, and there is no correct answer to it, only a curve and a budget.

Which leaves the design most people would have reached for first: split on spaces and call each word a token. It is one line of Go, so the page runs it instead of dismissing it.

$ go run ./cmd/pieces -mode price -split | tail -23
  the same sentence, by a tokenizer that splits on whitespace
    vocabulary, off this corpus           431
    tokens for the sentence                15
    words it has no id for                  0

  and the entry it would write next year, which is not in the corpus
    year 81: 240 hobbs standing in The Hollow, 1970 born and 1760 gone
    already in the chronicle            false
    tokens, by the merge table             11
    tokens, by the word list               10
    words it has no id for at all           3
      "81:"
      "1970"
      "1760"

  and the whole corpus, taken apart into words and put back
    words the list knows                  431
    bytes in                             7123
    bytes out                            7122
    entry boundaries in                    91
    entry boundaries out                    0
    the bytes it started as             false

On the sentence it was built from, the word list wins: fifteen tokens against forty-one, and a vocabulary of 431 against 512. Then the valley finishes another year and writes the entry it writes every year, and three of its words have no number at all. Not a rare number, not an unusual number: 81:, 1970, 1760. The merge table encodes the same sentence in eleven tokens without being consulted about it, because a number it has never seen is still a run of bytes and every byte has an id.

The last block is the quieter failure and the worse one. Take the whole corpus apart into words and put it back together and you get 7,122 bytes where there were 7,123, which looks like nothing at all until you read the line under it. Ninety-one entry boundaries went in and none came out. The eighty years of history have become one line, and the byte count barely moved because a line ending and a space are both one byte. A tokenizer that loses the difference between two entries and a space in the middle of one has not lost a byte; it has lost the structure, and it did it without changing a number anybody was watching.

Why token counts name a table

The mechanism generalises past this chapter and past this world. Any system that charges for text charges for it in units of some table, and the table is not a natural fact about language. It was counted off a corpus somebody chose, at a size somebody picked off a curve like the one above, with a tie-break somebody had to write down. Three tables counted off three corpora will give three different answers for the same sentence, and all three will be right.

That is why the count is defined here as the number of entries the encoder emits and not as anything more intuitive. It is the only definition that can be checked: hand a table and a string to two programs and they agree to the integer, whereas words depends on what you think a word is and characters depends on whether you are counting characters or bytes. A figure quoted in that unit names the table it was measured with, for the same reason a weight names its scale.

The three properties that make the table usable, stated once so nothing later has to argue them again. It is total: the byte alphabet means no input can arrive that has no representation, which is what the round trip over random bytes demonstrates and what the character alphabet gives away. It is lossless: decode of encode is the input, proved on the corpus, on text the corpus does not hold, on a cut character and on noise. And it is deterministic: the same corpus and the same merge count produce the same table, on any machine, in any order the map felt like today, because the tie-break is a rule and not an accident.

One thing this chapter did not do, said plainly because the word gets used loosely. Nothing here was trained in the sense that word usually carries. There is no gradient in this package, no backward pass, no loss and no learning rate. Counting how often two bytes sit next to each other is counting, and calling it training would put this page on the wrong side of a distinction the rest of the volume depends on.

What the chapter leaves standing is small and deliberately dull: a package with a tokenizer in it, a fixture with a history in it, a digest tying the two together, and a file format that can be written out and read back with three ways to be refused. Anything that wants tokens asks Train for a table over the committed corpus and gets the same 512 entries every time, on any machine, or reads one back off disk with ReadTable and gets a table that encodes identically. Nothing in the valley knows any of it exists, which is the seal at the top of this page doing exactly the job it was put there for.

✓ Checkpoint — the table and its units
  • Why the alphabet is the 256 byte values and not the characters of any encoding, and what specifically goes wrong at byte 24 of a line cut in the middle of a dash.
  • What the tie-break in commonest is for, and why ranks eight and nine of the pair count both reading 130 is the reason it exists.
  • Why counting stops at the end of a chronicle entry, and what that buys an encoder handed a whole file.
  • Why the fourth column of merges-81.txt is checked on load rather than read, given that it carries no information.
  • Why one sentence of this corpus costs 41 tokens at 1.98 characters a token while the corpus as a whole runs at 5.02, and which of those two numbers you would use to price text of your own.
  • What the word list has no number for the moment the valley finishes its eighty-first year, and why the merge table does.
⚡ Exercises — try first, then reveal
Exercise 1 — find the point of no return. The merge table stops at 354 because no pair is left occurring twice. Find the merge count at which the table stops learning English and starts learning this valley's boilerplate, and say how you decided.

Run the listing at a few sizes and read the tail of each one:

go run ./cmd/pieces -mode merges -merges 32 -top 6
go run ./cmd/pieces -mode merges -merges 48 -top 6
go run ./cmd/pieces -mode merges -merges 64 -top 6

The longest-token line the mode prints at the end is the quickest instrument. At 32 merges the longest row in the table stands for seven bytes and is a fragment any English text would produce. By 48 it stands for thirty-two, and what it stands for has standing in in the middle of it. So the turn happens between those two, and by 256 the longest row is thirty-nine bytes of one particular sentence. The honest answer to "where" is a range and not a number. A second way to ask the same question is -mode grow and its saved column: the merges that buy the most are the ones learning English, and the fall-off starts as the table begins spending rows on the sentence this valley writes every year.

Exercise 2 — price your own writing. Encode a paragraph you wrote yourself with this table and work out its characters a token. Explain the number you get before you look at anything else.

The bench takes the sentence on a flag, so no code is needed:

go run ./cmd/pieces -mode price -line "whatever you want to price, in quotes"

Expect something near the 1.98 the founding note gets and nowhere near 5.02, and expect it to fall further the less your text looks like this valley. The line saying which entry holds the sentence will report that no entry does, which is the honest answer and not an error. The general lesson is the one the volume needs: a characters-a-token figure is a property of a table and a text together, so the corpus-wide number is the least useful of the two for pricing anything that is not the corpus.

Exercise 3 — break the table on purpose. Write a merge table by hand that makes ReadTable refuse, once for each of the three things it refuses, and check that the error names the line.

Write the table out first so you have a real one to damage:

go run ./cmd/pieces -mode merges -merges 8 -table hand-81.txt
cat hand-81.txt

Three edits, one at a time. Swap two rows: the reader stops at the first of them and says it mints a token where a different one comes next. Change the second column of the first merge to 9999: the reader says a token can only be made of tokens that already exist, because 9,999 has not been minted at line one and never will be. And change only the quoted fourth column of any row: the reader says what the row claims and what its two halves actually make, and refuses. That third one is the whole reason the column is checked. Its information content is zero, so nothing would be lost by trusting it, and trusting it is how a readable file ends up disagreeing with itself. A test in the package asserts all three, pinned: go test ./internal/lang/ -run TestAMergeTableSurvivesBeingWrittenDown.