Nothing Waits for a Thought
An answer longer than a hundred milliseconds
The World has run at ten ticks a second since its first volume. Ten ticks in one second means one tick is a tenth of a second, and a hundred milliseconds is the whole budget every part of this world has ever had to work inside. The ground gets its share of it. Every plant standing on the ground gets its share. Every animal walking on the plants gets a share, and inside that share it casts nine rays, fills twenty-four numbers, is asked what it wants and does it. All of that finishes with room to spare, and it finishes with room to spare because everything in it is arithmetic on numbers that are already in memory.
A language model works nothing like that. Asking one for anything is a request to another process, over a socket, which comes back when it comes back. The measurement this volume opened on put a stopwatch on three kinds of that request at this world's own pace, and not one of the three fitted inside a tick; the dearest of them was tens of ticks long. Those are figures about one machine on one afternoon and yours will differ. The ordering will not, and the ordering is all this page needs. A tick is a hundred milliseconds and a thought is seconds, so the two of them can never be in the same call stack. Everything in this volume is a consequence of arranging that properly, and this chapter is the arrangement.
Start from what a villager has to satisfy, because it has not changed since the fifth
volume and it is one method wide. Every creature in this world is steered through
beast.Mind, which takes one reading of the world and returns one entry of the
action table. Twenty-four numbers in, one action out, nothing else crossing, and no way
for the thing on the far side to reach the valley, spend anything, or take longer than the
caller has. The people founded in the last chapter answer that method with a struct of
settings instead of an evolved network, and the valley never found out.
Pick is called from inside a tick. It is called once per creature per tick,
in the middle of the creature phase, with the world's index open and everybody else's turn
still to come. There is no version of this where Pick opens a socket. What
there is instead is a place to put an answer that was worked out somewhere else, and a
rule for what to do on the overwhelming majority of ticks where there is no such answer
waiting.
So three things get built here, and the first two are small enough to read in one sitting. A mailbox: one slot a villager, carrying at most one already-made decision. A habit: a plain policy in Go that produces an action every tick, never fails, and asks nothing of anybody. And a console, which is a program that founds the village, ticks the world, stands one more body up for whoever is at the keyboard, and reads one command a line. By the last page there are three people keeping a sensible day in The Hollow with no model anywhere near them, and a number saying how much of that day the habit ran. That number is the floor every later measurement in this volume is taken against.
One slot with three waiting decisions
A decision is a small thing. It is a verb out of the table, the body it is about, how many grams it is about, and the tick somebody settled on it. Four numbers, and none of them is a pointer:
// internal/village/mail.go
// Intent is one decision somebody has already made, written down as a
// value: which entry of the action table, the body it is about, how
// many grams it is about, and the world tick it was decided at.
//
// Every field is a number, and that is the whole of the type. Nothing
// in here is a pointer, because the side that decides and the side
// that acts run at the same time as each other: a pointer would let
// the deciding side go on editing a decision the acting side is
// already holding, and neither of them would ever find out.
// ...
type Intent struct {
Act beast.Act // which entry of the table
Whom sim.EntityID // the body it is about, or 0 for nobody
Grams float64 // how many grams it is about, or 0
At int // the world tick it was decided at
}
At is the field that will look unnecessary and is not. A decision made on
one tick is carried out on a later one, and the difference between those two numbers is
a real quantity that somebody is going to want. Writing it down when the decision is
made costs one integer. Working it out afterwards is impossible, because by then the
only clock anybody has says now.
Now the slot. Go has one obvious thing to reach for here, and reaching for it is right: a channel is the language's own answer to "one goroutine has something another goroutine wants". What is not obvious is the three decisions that go with it, and every one of the three is about waiting.
// internal/village/mail.go
// Box is a mailbox: one slot, holding at most one Intent, with three
// properties that are all about waiting and none about carrying.
//
// It holds VALUES. What crosses is a copy, so once an intent is in the
// slot nobody can change it, including whoever wrote it.
//
// A send REPLACES. Two intents cannot queue, because the second one
// was decided later about a world the first one no longer describes.
// Keeping the older one and making the newer one wait would be keeping
// the worse of the two on purpose.
//
// A receive GIVES UP. The tick side asks whether there is anything
// here and carries on either way, so nothing about a tick's cost
// depends on whether anybody has finished thinking.
//
// A Box nobody opened is an empty mailbox rather than a crash: the
// slot is a nil channel, both selects below fall to their default, and
// a villager with no mailbox keeps to its habit forever.
// ...
type Box struct {
slot chan Intent
// ...
}
// NewBox opens a mailbox: one slot, and both ends give up rather than
// wait for the other.
func NewBox() Box { return Box{slot: make(chan Intent, 1)} }
// Send puts an intent in the slot, throwing away whatever was there,
// and never waits for anybody.
//
// The two selects are one operation in two halves. The first empties
// the slot if it has anything in it; the second fills it. Both give up
// at once, so a Send costs the same whether the slot was full, empty,
// or filled by somebody else between the two halves. In that last case
// this intent is the one dropped, which is what replacement means from
// the other side of the slot.
func (b Box) Send(i Intent) {
// ...
select {
case <-b.slot:
default:
}
select {
case b.slot <- i:
default:
}
}
// Take empties the slot if there is anything in it and gives up at
// once if there is not. It is the only read there is: nothing peeks,
// because a slot that can be looked at without being emptied is a slot
// two ticks can act on.
func (b Box) Take() (Intent, bool) {
select {
case i := <-b.slot:
return i, true
default:
return Intent{}, false
}
}
$ go run ./cmd/slot -mode box
slot: one slot, three properties, and neither end ever waits
the slot one intent, 32 bytes of it, held by value
a send throws away what is there, fills the slot, returns
a receive empties the slot if it is full, returns either way
what happens slot what came out
a mailbox nobody has written to yet 0 -
send: walk, decided at tick 100 1 -
send: hand to creature 2, decided at tick 140 1 -
take 0 hand to creature 2, decided at tick 140
take 0 nothing
the intent decided at tick 100 is gone and nothing reported it.
Two sends and one take is not a lost message: the second decision
was made 40 ticks after the first, about a world the first one had
stopped describing, and a queue would have handed over the worse
of the two and then handed over the better one a tick later.
a decision crosses as a value, so posting one is copying it
posted hand to creature 2, decided at tick 300
the poster then edits take from creature 3, decided at tick 900
taken out of the slot hand to creature 2, decided at tick 300
the same two moves through a slot that carries a pointer
posted hand to creature 2, decided at tick 300
the poster then edits take from creature 3, decided at tick 900
taken out of the slot take from creature 3, decided at tick 900
the decision changed after it was posted. What crossed was 8 bytes
of address, both sides are looking at the one intent, and neither of
them can find out that the other one moved it
a mailbox nobody opened is an empty mailbox and not a stop
sent, then taken nothing
the slot is a channel nobody made, so both selects fall to their
default and a villager with no mailbox keeps to its habit
a thousand receives on an empty slot, which is what a tick does
receives 0 of 1000 found anything, and not one of them waited
Read the middle two blocks together, because they are the same two moves run twice. Post a decision, then edit the copy you still have. Through a slot carrying values the posted decision does not move: what went in was a copy of thirty-two bytes and your edits land on your own. Through a slot carrying pointers, eight bytes of address crossed, both ends are looking at the one struct, and the decision the tick eventually acts on is the one the deciding side happened to have finished editing by then. Nothing reports it. There is no error, no panic, no line in a log; the villager does something nobody chose.
That bug is easy to write and hard to see, and the reason is that it is invisible in a single-threaded test. Post, edit, take, and you can see it happen in a straight line, as the run above does. Post from a goroutine that is still working while the tick side reads, and the same bug is a race whose symptom is one wrong action every few thousand ticks. The type is what stops it, and the type stops it everywhere at once.
The last two blocks of that run are about the degenerate cases, and both of them are
deliberate. A Box nobody opened has a nil channel in it, and a send or a
receive on a nil channel blocks forever; inside a select with a
default, neither of them ever gets the chance. So the zero value of a mailbox
is an empty mailbox that quietly refuses everything, and a villager holding one keeps to
its habit for the rest of its life. That is the right failure for this world: it is one
villager going quiet, and not a tick that never returns.
A thousand receives on an empty slot found nothing a thousand times, and the run says the
thing that matters about them: not one of them waited. That is the operation a tick
performs. It is what a villager's Pick does before anything else, on every
one of the ten ticks a second this world runs at, whether or not there is a worker on the
far side and whether or not that worker is halfway through anything.
// internal/village/mail.go
// Mind is what answers beast.Mind for a villager: a mailbox and a
// habit, and nothing else at all. It holds no database handle, no
// client, no valley and no clock.
//
// Pick is the tick side of the seam. Neither of its two paths can
// wait: Take gives up at once, and Habit is a value rather than an
// interface, so there is no nil here to check and no failure to
// handle. A villager always has an answer.
type Mind struct {
Box Box
Habit Habit
// What the tick side did, kept for a bench to print.
Took Intent // the last intent taken out of the slot
Read int // ticks that found one there
Ran int // ticks that found nothing and ran the habit
}
// Pick takes whatever is in the slot, and finding nothing, runs the
// habit.
func (m *Mind) Pick(row *beast.Senses) beast.Act {
if i, ok := m.Box.Take(); ok {
m.Took, m.Read = i, m.Read+1
return i.Act
}
m.Ran++
return m.Habit.Pick(row)
}
$ go test ./internal/village/ -run 'TestASendReplacesWhatIsInTheSlot|TestATakeOnAnEmptySlotGivesUpAtOnce|TestAnIntentCrossesAsAValueAndNotAsAPointer|TestAMailboxNobodyOpenedIsAnEmptyMailbox|TestAnEmptySlotRunsTheHabitAndAFullOneDoesNot' -v
=== RUN TestASendReplacesWhatIsInTheSlot
--- PASS: TestASendReplacesWhatIsInTheSlot (0.00s)
=== RUN TestATakeOnAnEmptySlotGivesUpAtOnce
--- PASS: TestATakeOnAnEmptySlotGivesUpAtOnce (0.00s)
=== RUN TestAnIntentCrossesAsAValueAndNotAsAPointer
--- PASS: TestAnIntentCrossesAsAValueAndNotAsAPointer (0.00s)
=== RUN TestAMailboxNobodyOpenedIsAnEmptyMailbox
--- PASS: TestAMailboxNobodyOpenedIsAnEmptyMailbox (0.00s)
=== RUN TestAnEmptySlotRunsTheHabitAndAFullOneDoesNot
--- PASS: TestAnEmptySlotRunsTheHabitAndAFullOneDoesNot (0.00s)
PASS
ok theworld/internal/village 0.003s
Habit is a struct value on that type and not an interface, and the
difference is the whole of why Pick has no error path. An interface field
can be nil, so a Pick written around one has to decide what to do about
that, and whatever it decides is a branch somebody has to test. A value cannot be nil.
The zero value of this one answers every reading of the world with a rest, which is a
real answer and a legal action, so the worst thing an unfilled Mind can do
is stand still.
The second test in that list is the one that cannot be written any other way.
TestATakeOnAnEmptySlotGivesUpAtOnce asks an empty slot for an intent a
thousand times and asserts that it got nothing each time. If the receive waited, the
test would not fail: it would never finish, and the package's test binary would hang
until the toolchain killed it ten minutes later. That is the only way to test "does not
block" without putting a clock in a test, and a clock in a test is a test that fails on
a loaded machine.
The rule underneath, finished
The errand the last chapter drove three people with is a policy of about thirty lines that
reads the fan, walks toward the nearest body it can see, and puts a parcel in their hands
when it gets close enough. It has one drive in it, and a field on the struct called
Hungry that names what to do when that drive has nothing to say. Nothing ever
filled that field. A giver standing next to a neighbour with a full store hands over
grain until there is nothing left to hand over, and then rests, and then dies
of it, on ground with plants standing in plain sight.
The rule that fills it is a total order over a handful of drives, asked in one fixed order, where the first drive with something to say wins and the last one always has something to say. Four drives is enough for a village to keep a day:
// internal/village/habit.go
// Habit is the utility rule: what a villager does on every tick
// nothing has told it to do anything else, which in this world is
// nearly all of them.
//
// It is four drives asked in one fixed order, and the first one with
// something to say wins:
//
// 1 hunger below Feed, with a plant in sight eat
// 2 errand Trade is on, the store is on the far side
// of Deal, and a body is in sight hand or take
// 3 forage below Fill, with a plant in sight eat
// 4 idle none of the above Idle
//
// The order is the whole design. It is not a score with weights on it
// and there is nothing to tune: drive 1 sits above drive 2 because a
// villager that hands its store away while it is starving is a
// villager that starves, and drive 3 sits below drive 2 because a full
// store is worth less than a neighbour who needs something.
// ...
type Habit struct {
Feed float64 // eat below this share of a full store
Deal float64 // trade while the store is on the far side of this share
Fill float64 // stop topping up at this share
Trade bool // whether drive 2 is asked at all
Idle beast.Act // the last resort, and the zero value of it is a rest
Eat beast.Hunt // drives 1 and 3
Give Errand // drive 2
}
// Pick asks the four drives in order and hands back one entry of the
// table.
func (h Habit) Pick(row *beast.Senses) beast.Act {
store := row[beast.StoreAt]
switch {
case store < h.Feed && sighted(row, beast.Plant):
return h.Eat.Pick(row)
case h.trading(store) && sighted(row, beast.Creature):
return h.Give.Pick(row)
case store < h.Fill && sighted(row, beast.Plant):
return h.Eat.Pick(row)
}
return h.Idle
}
$ go run ./cmd/slot -mode rule
slot: the rule that runs on every tick nothing has been posted
the settings feed below 25%, trade past 40%, top up below 90%
the row 24 numbers, of which this rule reads 19: the fan and the store
this one a giver, eating within 0.5000 cells and handing within 1.4142,
over a sight span of 12 cells and a fan of 9 rays
the four drives, asked in this order, first one with something to say wins
1 hunger below 25% of a full store, with a plant in sight
2 errand past 40% of one the trading way, with a body in sight
3 forage below 90% of one, with a plant in sight
4 idle none of the above, and there is no condition on this one
one reading at a time: nine rays, one store, and the answer
store what the fan met drive the answer
10% a plant ahead, a person beside it hunger bite
30% a plant ahead, a person beside it forage bite
60% a plant ahead, a person beside it errand hand
95% a plant ahead, a person beside it errand hand
60% a plant ahead, nobody about forage bite
95% a plant ahead, nobody about idle turn right
10% a plant well off to the left hunger turn left
10% a plant ahead and six cells off hunger walk
10% nothing at all idle turn right
and every way the fan can come back: 9 rays, 5 things a ray can meet
readings handed to the rule 5859375
readings it answered 5859375
readings it did not 0
which drive answered, and with what
drive readings walk turn left turn right bite hand
hunger 1690981 433470 563695 563608 130208 0
errand 3381962 658564 1127322 1127326 0 468750
forage 242461 58942 80862 80811 21846 0
idle 543971 0 0 543971 0 0
it named 5 of the 9 entries in the table, and never named these 4:
rest, sprint, strike, take
not one of those 5859375 readings came back without an entry of the table,
and it is the last arm of the switch that says so: nothing is asked of
the row there, so there is no way out of the bottom of this rule. The
two fallback fields inside the drives were never reached either, because
the order asks each drive only where its own fallback cannot fire
The nine hand-made readings in the middle are the order working. The same fan, the same neighbour standing in the same place, and one number moved: a tenth of a store and the villager eats, three tenths and it eats, six tenths and it hands over. The first two rows are answered by different drives and come back with the same action, and that is not redundancy. Drive 1 is need and drive 3 is opportunity, and they sit on either side of the errand because a person who is hungry feeds themselves first and a person who is comfortable helps somebody else first. Change nothing but which side of drive 2 the eating sits on and you have written a different person.
The bottom half is the claim this rule has to be able to make. There are nine rays and five things a ray can come back with, so there are 1,953,125 ways the fan can be filled in; at three store levels that is 5,859,375 readings, and the bench hands the rule every one of them with the distances shuffled a different way each time so the search for the nearest ray is asked every question it can be asked. Every reading came back with an entry of the table. Nothing failed, because there is nothing in this function that can fail: no error return, no allocation, no index computed from a number the world supplied, and a final arm on the switch with no condition attached to it.
The two fallback fields are worth one more sentence, because they are the reason this is a
finish and not a rewrite. Eat carries a field for what to do with no plant in
sight and Give carries the Hungry field that started this
section, and both of them are still nil. The order is what makes them unreachable: the
eating drives are asked only when a plant is in the row, and the errand is asked only when
a body is in the row and the store is on the trading side of the threshold, which are
exactly the two conditions those fallbacks exist to handle. A field nothing fills is a
defect when something can reach it, and a fact about the caller when nothing can.
// internal/village/habit_test.go
for _, c := range []struct {
what string
hits []beast.Hit
store float64
drive int
}{
{"starving, with both in sight", both, 0.10, 0},
{"comfortable, with both in sight", both, 0.60, 1},
{"comfortable, with nobody in sight", plants, 0.60, 2},
{"between Feed and Deal, with both in sight", both, 0.30, 2},
{"full, with a plant in sight and nobody near", plants, 0.95, 3},
{"starving, with nothing in sight", none, 0.10, 3},
} {
// ...
$ go test ./internal/village/ -run 'TestTheHabitAsksItsFourDrivesInOneOrder|TestTheHabitAnswersEveryWayTheFanCanComeBack|TestTheHabitAnswersNumbersNoValleyWouldEverProduce|TestAHabitWithEveryDriveOffStandsStill' -v
=== RUN TestTheHabitAsksItsFourDrivesInOneOrder
--- PASS: TestTheHabitAsksItsFourDrivesInOneOrder (0.00s)
=== RUN TestTheHabitAnswersEveryWayTheFanCanComeBack
habit_test.go:99: 1953125 readings, every one answered, 5 entries of the table named
--- PASS: TestTheHabitAnswersEveryWayTheFanCanComeBack (0.25s)
=== RUN TestTheHabitAnswersNumbersNoValleyWouldEverProduce
habit_test.go:129: 729 readings of numbers a valley cannot make, every one answered
--- PASS: TestTheHabitAnswersNumbersNoValleyWouldEverProduce (0.00s)
=== RUN TestAHabitWithEveryDriveOffStandsStill
--- PASS: TestAHabitWithEveryDriveOffStandsStill (0.00s)
PASS
ok theworld/internal/village 0.252s
The third of those four is the one people leave out. It fills rows with numbers no
valley in this book has ever produced: both infinities, a NaN, and the two ends of what
a float64 holds, in the distance slots and in the store slot. None of those
is a reading this world makes. All of them are readings this function has to survive,
because "never fails" is a claim about the function and not about its callers, and the
day somebody hands it a row from somewhere new is the day the claim gets tested for
real. It survives them for a plain reason: every number in the row is compared and never
indexed by, and a comparison against a NaN is false, so a ray reporting nonsense loses
every contest it enters.
One distinction before this rule goes near a world, because it will otherwise get muddled the first time a run prints a refusal. The habit never fails to produce an action. The world can still refuse the action it produced. Walking into open water is refused by the ground rule; handing a parcel to somebody who has drifted a cell too far away is refused by the transfer rule. A refused action costs the tick and is spent standing there, exactly as it does for every animal in this world. Those are two different properties with two different owners, and the rule owning the first one is what keeps the tick's cost fixed.
A decision that arrives thirty ticks late
Now the far side, and the first question is where it hangs off. The daemon that ran a
valley unattended for weeks in the seventh volume has a comment on one line of its loop
calling that line the one place where no phase is part way through, no view is open and no
creature is half stepped. Everything in that loop which is not a tick happens there: the
snapshot, the signal check, the stop. The eighth volume hung a between-ticks job on the
same line for the same reason. The far side of the seam hangs there too, and it is
not in terra.Valley.Phase, not in beast.Roster.Seen, not in
Born, and above all not in Pick. Those are all inside a tick.
No run on this page calls a model. There is no server, no container and no socket anywhere in these runs, and that is the point of them: what is being measured is the crossing, so the far side is a goroutine that answers a stated number of ticks after it is asked, and how many ticks is a flag the bench prints. The answers it gives are five verbs written down in a list. Nothing here decides anything, and a bench that pretended to decide would be measuring its own pretending.
// cmd/slot/late.go
for t := 0; t < s.ticks; t++ {
// The boundary. No phase is part way through here, no view is
// open and no creature is half stepped, so this is where a
// finished answer is handed over and where the next request is
// made. Neither of those two things is inside a tick.
if pending != nil && v.Now >= pending.due {
close(pending.release)
// This waits for the hand-over and not for the answer.
// The answer was finished at the top of this boundary;
// what is being waited on is one channel operation, and
// it is here so that this run is the same run on a fast
// machine and a slow one.
<-pending.landed
// ...
}
if pending == nil && t%s.every == 0 {
// ...
go func(j *job, box village.Box) {
<-j.release
box.Send(j.what)
close(j.landed)
}(pending, minds[who].Box)
}
v.Tick()
// ...
}
$ go run ./cmd/slot -mode late
slot: 3 people, one worker, and an answer that takes 30 ticks
the world seed 5, 12x8 cells, founded at tick 901
the run 600 ticks, ungoverned
the pace 10 ticks a second, so a tick is 100ms
the worker one, asked every 40 ticks, answering after 30
the mailbox one slot, a send that replaces and a receive that gives up
the habit feed below 25%, trade past 40%, top up below 90%
# villager posted at landed at taken at ticks old the intent
1 Halla 901 931 932 31 hand to creature 2
2 Ander 941 971 972 31 turn left
3 Mose 981 1011 1012 31 walk
4 Halla 1021 1051 1052 31 take from creature 3
5 Ander 1061 1091 1092 31 rest
6 Mose 1101 1131 1132 31 hand to creature 1
7 Halla 1141 1171 1172 31 turn left
8 Ander 1181 1211 1212 31 walk
9 Mose 1221 1251 1252 31 take from creature 2
10 Halla 1261 1291 1292 31 rest
11 Ander 1301 1331 1332 31 hand to creature 1
12 Mose 1341 1371 1372 31 turn left
13 Halla 1381 1411 1412 31 walk
14 Ander 1421 1451 1452 31 take from creature 1
15 Mose 1461 1491 1492 31 rest
what drove each tick
villager ticks from an intent from the habit
Halla 600 5 595
Ander 600 5 595
Mose 600 5 595
together 1800 15 1785
every intent in this run was 31 ticks old when it was carried out:
30 ticks on the far side of the seam and one more to be taken out of
the slot. A villager acting on it is acting on a world that has moved
on 31 ticks since anybody looked at it.
and no tick in this run waited for anybody: 1785 of the 1800 ticks found
an empty slot, gave up on it at once and ran the habit instead
600 ticks in 12ms, 48247 ticks a second (measured here; yours will differ)
The column to read twice is the last number in each row. Every intent in that run was thirty-one ticks old when the villager acted on it: thirty on the far side, and one more to be taken out of the slot. Thirty-one ticks at ten ticks a second is a little over three seconds of world, in which three people and a valley full of plants carried on without waiting for anybody. Halla's first intent was to hand a parcel to creature 2. By the time it was carried out, creature 2 had had thirty-one turns of its own and might be somewhere else.
Every decision a villager makes is a decision about a world that has already
moved on. That is the design and not a defect of it. There is no arrangement
where it is otherwise: an answer that takes time is an answer about the past, and the
only way to make it an answer about the present would be to stop the world while it was
worked out, which is the thing this whole chapter exists to avoid. What makes it safe is
the field on Intent that looked unnecessary two sections ago. A decision
arrives carrying the tick it was made at, so how stale it is is a number the engine
holds, and premises that have gone away can be checked against the world before anything
is done about them.
Take the run above and count. It went for six hundred ticks with three people in it, so eighteen hundred creature-ticks were handed out. Fifteen intents were delivered. Fifteen out of eighteen hundred leaves 1,785, and the run prints all three numbers.
Turn that into a share. 15 divided by 1,800 is 0.00833, which is a third of one per cent. Put the other way round, 1,785 divided by 1,800 is 0.99167, so the habit answered 99.2% of the ticks in that village. The rule built in the last section is not the degraded case that runs when something has gone wrong. It is what a villager does with nearly the whole of its life, and the thing on the far side of the seam is the rare event.
Now the arithmetic that predicts it, so the number can be worked out before a run instead of counted afterwards. One worker serves one villager at a time and takes R ticks over each, so with n villagers, each of them is served once every n times R ticks. Each answer drives exactly one tick, because the slot is emptied by the first receive that finds it. So the share of a villager's ticks that come from the far side is one over n times R:
share = 1 ÷ (n × R)
With three people and thirty ticks an answer, that is 1 divided by 90, which is 0.0111, or 1.1%. The run measured 0.83%, and the gap is the cadence: the bench was told to ask every 40 ticks and the worker only needs 30, so it idles for ten ticks out of every forty. Take the idling away, as the first exercise at the end of this chapter does, and the count goes to 19 out of 1,800, which is 1.06%. The last sliver between that and 1.11% is one request still out on the far side when the run stopped.
The useful thing about that formula is which way its two letters point. Both are on the bottom, so both of them make the share smaller. More people means each one waits longer. A dearer answer means the same. There is no arrangement of a village where a slow far side drives most of the ticks, and that is true before anybody has chosen how many people to found or what to ask them.
A mailbox is a channel and a channel already has a send. Writing
b.slot <- i is one line where the version above is eight, it needs no
select and no default, and it does something a reasonable
person would call the right thing: it puts the intent in the slot, and if the slot is
full it waits until the tick side has taken what is there. Nothing is ever dropped. It
compiles, it vets clean, and it is behind a flag so it can be run.
// internal/village/mail.go
// Waiting opens the mailbox as it is usually written the first time:
// one slot and a plain send, which stops the sender until somebody
// takes what is there. Nothing this book ships opens one.
func Waiting() Box { return Box{slot: make(chan Intent), waits: true} }
func (b Box) Send(i Intent) {
if b.waits {
b.slot <- i
return
}
// ...
}
$ go run ./cmd/slot -mode late -blocking
slot: 3 people, one worker, and an answer that takes 30 ticks
the world seed 5, 12x8 cells, founded at tick 901
the run 600 ticks, ungoverned
the pace 10 ticks a second, so a tick is 100ms
the worker one, asked every 40 ticks, answering after 30
the mailbox one slot, a send that WAITS for a reader
the habit feed below 25%, trade past 40%, top up below 90%
fatal error: all goroutines are asleep - deadlock!
The run prints its settings and stops dead. Under that line the runtime prints a dump
naming every parked goroutine, which is not quoted here because the numbers in it belong
to one process; the two lines that matter say goroutine 1 [chan receive] in
the loop and [chan send] in village.Box.Send. The Go runtime
noticed that every goroutine in the program was waiting for another one and gave up on
the whole process. Not one intent was ever delivered.
Work back from the symptom. The boundary released the worker and then waited for the
hand-over, because that is what makes the run reproducible: the answer is finished, so
handing it across should be one channel operation and take no time at all. With a
replacing send it is exactly that. With a plain send it is a rendezvous, and a
rendezvous needs somebody on the other end. The only thing that ever reads that slot is
Pick, Pick runs inside v.Tick(), and
v.Tick() is the line after the boundary. The reader cannot arrive
until the boundary lets go, and the boundary cannot let go until the reader arrives.
The tempting reading of that is "the boundary should not have waited", and it is the wrong one. Rearrange the loop so the boundary does not wait and the deadlock goes away, but the property that caused it does not: a send that waits is a send whose cost depends on somebody else's schedule. The reader of a villager's slot stops arriving the moment that villager stops being stepped, which happens when it starves and is struck off the roster, and there is exactly one worker in this design. One person dying would take the whole village's thinking with it, at some tick nobody can predict, weeks into an unattended run. A deadlock on the first delivery is the cheapest possible version of that bug, and it is only that cheap because the boundary waits.
The cause is one word in the design rather than one line in the code. A send that waits waits for a reader, and nothing anywhere in this world promises that a reader will come back. Replacement is what turns that promise into something the sender never needs.
Somebody to walk over and look at
A village with nobody in it is a village nobody can watch. Everything so far has been read
out of a bench's own tables, which is the right way to check arithmetic and a poor way to
find out whether a place should hold a body. So there is a second program:
cmd/village, which founds the ground, founds the people out of
configs/village.json, stands one more body up for whoever is at the keyboard,
and then reads one command a line and writes one event a line back. The world ticks while
it reads.
It has five verbs and they are closed: look, who,
carry, go and wait. It is not a player and not a
client. It opens no socket, speaks no protocol, has no window and draws nothing. What it
has is a body on the same roster as everybody else, stamped from the same row, paying the
same prices for the same actions and starving on the same clock.
And that body reaches the world exactly the way a villager does. Typing go 4
does not move anything: it puts four intents, one a tick, into a mailbox, at the boundary,
between two ticks. The tick side takes them or does not. Your own body is driven by the
same Mind the villagers are, with the same Take in front of it
and the same rule underneath it, and the only difference is which rule:
// cmd/village/main.go
c.you = k.Spawn(beast.Centre(cell), v.Now)
c.you.Store = k.Full / 2
// Your own body runs the same rule the villagers do, with every
// drive turned off. The zero value of the last resort is a rest, so
// a console nobody is typing at stands where it was put.
c.yours = &village.Mind{Box: village.NewBox()}
c.you.Mind = c.yours
r.Add(c.you)
// cmd/village/main.go — the posting, which is at the boundary
func (c *console) walk(n int, act beast.Act, post bool) (took, refused int, why error) {
was, nos := c.yours.Read, c.nos
for i := 0; i < n; i++ {
if post {
c.yours.Box.Send(village.Intent{Act: act, At: c.v.Now})
c.posted++
}
c.v.Tick()
c.ran++
// ...
}
// ...
}
village.Mind{Box: village.NewBox()} leaves the habit at its zero value, and
the zero value of a habit has every threshold at nothing, no trading, and a last resort
of beast.Rest, which happens to be the first entry of the action table.
Every drive's condition is a comparison against nought that no store can satisfy, so
every reading falls through to the last arm, and a console nobody is typing at stands
where it was put. There is no special case in the code for "this body is the operator's".
It is the same rule with the numbers turned down.
# cmd/village/walk-91.txt
# Chapter 91's walk. Nothing in this file is sent to anybody but your
# own body: the villagers are running their own rule the whole time.
who
carry
look
go left 6
look
go 4
who
wait 300
who
carry
$ go run ./cmd/village -script cmd/village/walk-91.txt
village: a console beside the world, reading cmd/village/walk-91.txt
the world 12x8 cells, tick 901, 12 plants standing at 1517.5 grams
the village 3 people, each on a mailbox and a habit
you creature 4 on 10,5, one more body on the same roster,
paying the same prices, on a mailbox and no habit at all
the pace ungoverned: as fast as this machine will carry it
the verbs look, who, carry, go [left|right] N, wait N
the fan . nothing * a plant o a body ~ water # rock
tick 901 the world is running; nothing you have not typed is yours
> who
tick 901 Halla creature 1 on 8,5, 2.0000 cells off, carrying 200.0000 grams, settled law
tick 901 Ander creature 2 on 9,5, 1.0000 cells off, carrying 40.0000 grams, settled law
tick 901 Mose creature 3 on 8,6, 2.2361 cells off, carrying 120.0000 grams, settled law
> carry
tick 901 you are carrying 100.0000 grams, with room for 100.0000 more, out of a full 200.0000
> look
tick 901 the fan, left to right: # # # # # # # # *
tick 901 nearest plant 0.7500 cells, on ray 9 of 9
tick 901 nearest rock 0.5000 cells, on ray 5 of 9
> go left 6
tick 907 you posted turn left 6 times and 6 of them were taken out of your slot
tick 907 you are on 10,5 facing west
> look
tick 907 the fan, left to right: * * o o o o o * *
tick 907 nearest plant 0.7500 cells, on ray 1 of 9
tick 907 nearest body 0.7500 cells, on ray 3 of 9
> go 4
tick 911 you posted walk 4 times and 4 of them were taken out of your slot
tick 911 you are on 9,5 facing west
> who
tick 911 Halla creature 1 on 8,5, 1.0000 cells off, carrying 196.3250 grams, settled law
tick 911 Ander creature 2 on 9,5, 0.0000 cells off, carrying 42.7925 grams, settled law
tick 911 Mose creature 3 on 8,6, 1.4142 cells off, carrying 121.3250 grams, settled law
> wait 300
tick 1211 you waited 300 ticks and did nothing with any of them
> who
tick 1211 Halla creature 1 on 8,5, 1.0000 cells off, carrying 75.6150 grams, settled law
tick 1211 Ander creature 2 on 9,5, 0.0000 cells off, carrying 112.7225 grams, settled law
tick 1211 Mose creature 3 on 8,6, 1.4142 cells off, carrying 161.0750 grams, settled law
> carry
tick 1211 you are carrying 104.3588 grams, with room for 95.6412 more, out of a full 200.0000
ticks run 310
intents you posted 10
of those, taken out of your slot 10
of those, refused by the check 0
ticks your body found the slot empty 300
intents anybody posted to a villager 0
ticks the villagers ran their own rule 930
310 ticks in 7ms, 46881 ticks a second (measured here; yours will differ)
The first look is a wall. The body was stood up on 10,5 facing east and the
whole eastern column of this valley is rock, so eight of the nine rays came back with a
rock and the ninth found a plant three quarters of a cell away. Six turns to the left is
half a turn, because one turn action swings the heading by a twelfth of a whole one, and
the second look is the village: two plants, five rays that ended on a body,
two more plants. There are only three people out there and one of you, so more than one
of those five rays found the same person. That is what somebody standing there sees, cast
by the ray caster every animal in this world has used since the volume that built it.
The interesting part is the three hundred ticks in the middle where nothing was typed. Halla went from 196 grams to 75; Ander went from 42 to 112; Mose went from 121 to 161. Nobody sent any of them anything, and the run says so on its last block: no intent was posted to a villager at all. What took a hundred and twenty grams off Halla and put most of them somewhere else, in half a minute of world, was three copies of the rule from the last section, asking four questions each, ten times a second.
And your own store went up. You arrived carrying a hundred grams, did nothing at all for three hundred ticks, and came away with 104.3588. That figure is net of what three hundred ticks charged your body for standing there, so rather more than four grams arrived. Halla's errand hands a parcel to whoever has the most room inside her reach, and once Ander had filled up past you, that was you. The action table does not know what a person is and has never been told what a console is; it is two bodies and a parcel, and you were standing there.
Which leaves the run the rest of this volume gets measured against. Three people, a live valley, two thousand ticks, and nothing on the far side of any mailbox at all.
$ go run ./cmd/slot -mode day -ticks 2000
slot: a day with nothing at all on the far side of the seam
the world seed 5, 12x8 cells, tick 901, 12 plants at 1517.5 grams
the run 2000 ticks, which is 200 seconds of world at 10 ticks a second
the village 3 people out of configs/village.json
the mailboxes opened and never written to: nothing posts anything here
the habit feed below 25%, trade past 40%, top up below 90%
who trades and who does not, worked out from the roll and not written in it
name founded holding the second drive
Halla 200.0000 hands over
Mose 120.0000 neither
Ander 40.0000 takes
the errand on its own, which is the rule as it was first written:
one drive, and a fallback field with nothing in it
name at first at last after the run
Halla 200.0000 -0.0300 starved on tick 1471
Ander 40.0000 -0.0800 starved on tick 1476
Mose 120.0000 78.6350 standing
1 of 3 standing after 2000 ticks
the habit, which is four drives asked in one order
name at first at last after the run
Halla 200.0000 91.1613 standing
Ander 40.0000 180.0603 standing
Mose 120.0000 102.7601 standing
3 of 3 standing after 2000 ticks
what the habit asked for over 6000 creature-ticks, and what the check allowed
action asked
walk 233
turn left 344
turn right 1114
bite 3398
hand 832
take 79
together 6000
the rule was asked 6000 times and answered 6000 times
refused 158 times: the cell ahead is open water or off the grid
refused 426 times: no other living creature inside reach
a refused action costs the tick and is spent standing there, and the
rule failing and the world refusing are two different things
what the ground carried, in grams
standing at the founding 1517.4652
taken off standing plants 901.8115
standing at the end 301.0757
the village burned being alive in it 480.0000
plants standing at the founding 12
plants standing at the end 5
the ground is carrying less than it was. 3 people eating for 2000 ticks
take more off this valley than it grows back, which is a fact about
12 plants and not about the rule: run it long enough and the last of
them is eaten and everybody starves whatever they are being driven by
this is the floor: 3 people, 6000 creature-ticks, every one of them
answered, no mailbox written to, no worker running, nothing outside
this process asked for anything, and 3 of 3 still standing
2000 ticks in 24ms, 82762 ticks a second (measured here; yours will differ)
Two runs over the same ground from the same seed, and the only difference between them
is what answers Pick. The errand on its own loses two people inside five
hundred ticks of each other: the giver hands everything away and starves at 1471, and
the taker starves five ticks later because there is nobody left with a parcel to take.
The one who trades with nobody is the one who lives, which is as clear a statement of
what a missing drive costs as this world is going to produce. Four drives in one order,
and all three are standing at the end with between 91 and 180 grams in the store.
The middle table is the claim that matters for everything after this page. Six thousand creature-ticks were handed out and six thousand actions came back, so the rule was asked on every tick every person got and it answered every time. Five hundred and eighty-four of those actions were refused by the world, and the run keeps the two facts in separate columns on purpose: the check refusing an action is the world's business, and it costs the tick either way. What the rule guarantees is narrower and more useful. There is never a tick where nobody has an answer.
The last block is the honest part. Twelve plants at the founding and five at the end; 902 grams taken off the ground and 480 burned by three bodies standing on it. This valley does not grow enough to keep three people eating forever, and no rule can fix that, because the rule decides what a person does with a tick and not how much grows. The number to hold on to is what happened across those two thousand ticks: three people kept themselves alive, moved 900 grams of tissue about between themselves and the ground, and did not once ask anything outside this process for anything.
Why fast loops and slow loops share values
Take the valley away and what is left is a pattern that turns up wherever a program has a loop with a deadline in it and a job without one. A game's frame. An audio callback. A control loop on a motor. A packet handler. Every one of them is a piece of code that has to finish inside a fixed budget, sitting in the same process as something that cannot promise to finish at all, and the two of them have something to say to each other.
The reflex is to make the join a queue, because a queue is what a join usually is, and then to make the deadline side drain the queue. That works until the day the slow side gets slower than the fast side, and then the queue grows, and everything in it is an answer to a question about a world that no longer exists. The deadline side then spends its budget working through decisions in the order they became stale. A one-slot mailbox is the same join with that failure removed by construction: there is nowhere for a backlog to accumulate, so the deadline side always acts on the newest thing the slow side finished, and the older ones are dropped by the only participant who knows they are old.
The second thing that generalizes is which of the two sides is allowed to fail. Between a fast loop and a slow job, exactly one of them can be made unable to fail, and it has to be the fast one, because the slow one is slow precisely because it is doing something that depends on the outside world. So the deadline side gets a total function with no error return and no dependency, and every uncertainty in the design is pushed across the join. The habit here reads twenty-four numbers and returns; it cannot fail because there is nothing in it that could. That is the property a fallback has to have, and "fallback" is a poor word for it, because the run above says it answers 99.2% of the ticks. It is the normal case, and the far side is the exception.
The third is about what a value costs and what a pointer costs. Sending thirty-two bytes across a channel copies thirty-two bytes. Sending a pointer copies eight, and then hands the receiver a promise that nobody will touch what it points at, which is a promise made in a comment and enforced by nothing. The eight bytes look cheaper and they are, by twenty-four bytes, once per decision, a handful of times a minute. Against that, a value removes a class of bug that does not show up in a test, does not show up in a log, and shows up in production as one wrong action in ten thousand. Anywhere a struct is small and crosses a boundary between two things that run at once, copying it is not the compromise. It is the design, and the size of the struct is a thing to keep small on purpose so it stays available.
And the last one is the sentence this chapter's title is about. A tick that can wait is a tick with no budget. It does not matter what it waits for: a disk, a socket, a lock, a channel with nobody on the other end. The rule this book adopted when it grew a database was that nothing inside a tick opens a transaction, and this page is that rule met a second time with something far slower on the other side of it. The way to keep it is to make waiting unrepresentable at the join, which is what a send that replaces and a receive that gives up amount to. Neither of them has a code path that can block, so no amount of pressure from the far side can produce one.
- Say what happens to the first of two intents posted into one slot before either is taken, and give the reason a queue would be the worse answer here rather than the safer one.
- A worker posts an intent and then edits the struct it built it from. Say what the tick side sees when the slot carries a value and when it carries a pointer, and which of the two produces an error message.
- Work out how old, in ticks, a decision is when it is carried out, given that the far
side takes R ticks and the receive that finds it runs in the tick after it lands. Then
say why
Intent.Athas to be written when the decision is made and cannot be worked out afterwards. - Name the four drives in the order they are asked, and say what changes about a villager if drives 1 and 2 swap places.
- Explain how a habit whose every field is left at its zero value produces a legal action for every one of the 5,859,375 readings the fan can return.
- Given one worker, twelve villagers and an answer that takes 30 ticks, work out the share of a villager's ticks that an intent drives, and say which of the two numbers you would change to raise it.
Exercise 1 — ask four times as often. The seam run asks for a decision every 40 ticks and the worker answers in 30. Drop the cadence to 10 and predict two things before you run it: how many answers arrive over 600 ticks, and how old each one is when it is carried out.
Nineteen answers instead of fifteen, and every one of them still thirty-one ticks old. Asking four times as often bought four extra decisions and did not make a single one of them arrive sooner:
$ go run ./cmd/slot -mode late -every 10 | tail -20 | head -18
villager ticks from an intent from the habit
Halla 600 7 593
Ander 600 6 594
Mose 600 6 594
together 1800 19 1781
1 request was still on the far side when the run stopped. Its answer
went into a slot no tick will read again and the villager it was for
never found out, because a villager that is not told anything keeps
to its habit. That is the whole of what is lost when this stops.
every intent in this run was 31 ticks old when it was carried out:
30 ticks on the far side of the seam and one more to be taken out of
the slot. A villager acting on it is acting on a world that has moved
on 31 ticks since anybody looked at it.
and no tick in this run waited for anybody: 1781 of the 1800 ticks found
an empty slot, gave up on it at once and ran the habit instead
The reason is that there is one worker, and asking it for something while it is busy is not asking. The cadence stopped being the constraint the moment it went below the answer's own cost: at 40 ticks the worker idles for ten out of every forty, and at 10 it is asked again the instant it is free, so requests go out every 30 ticks and 600 divided by 30 is 20. Nineteen of those came back inside the run and the twentieth was still out when it stopped.
That last one is the interesting line in the block. Its answer was handed into a slot nothing will ever read, and the villager it was for never found out that anybody had been asked. Nothing anywhere is damaged by that, and the reason is the same reason the receive gives up: a villager that is told nothing keeps to its habit. Everything a villager is lives in the roster and in files, and what is in flight is one decision nobody will miss.
Exercise 2 — put hunger above generosity, and then above everything. The giver eats below 25% of a full store and hands over above 40%. Raise the eating threshold to 90%, so it sits above the trading one, and predict what happens to the number of parcels handed over in a two thousand tick run.
Hands go from 832 to 55, and takes go from 79 to none at all:
$ go run ./cmd/slot -mode day -ticks 2000 -feed 0.9 | tail -37 | head -19
the habit, which is four drives asked in one order
name at first at last after the run
Halla 200.0000 169.5403 standing
Ander 40.0000 157.2588 standing
Mose 120.0000 145.7315 standing
3 of 3 standing after 2000 ticks
what the habit asked for over 6000 creature-ticks, and what the check allowed
action asked
walk 799
turn left 239
turn right 366
bite 4541
hand 55
together 6000
the rule was asked 6000 times and answered 6000 times
refused 751 times: the cell ahead is open water or off the grid
a refused action costs the tick and is spent standing there, and the
rule failing and the world refusing are two different things
Nothing was disabled. Drive 2 is still there, still switched on, still the second thing asked. A villager below 90% of a full store with a plant in sight is answered by drive 1 first, and a villager in this valley is nearly always below 90% of a full store with a plant in sight. Drive 2 now only speaks in the narrow window where somebody is nearly full and standing next to a plant they cannot see, which happened 55 times in six thousand ticks.
Everybody ends the run richer, which is the trap in the result. Three people who never help each other and eat all day do very well for two thousand ticks, and the thing they have stopped doing is the only thing that distinguishes them from the animals in the same roster. A threshold moved by one number turned a village into three browsers, and no test failed.
Exercise 3 — take the third drive out. Drive 3 is the one that tops a store up when there is nothing better to do, and it looks like the most expendable of the four. Set its threshold to nothing so it can never fire, run the day again, and say what you expect the program to print and what you expect it to exit with.
Two of the three starve, and the four-drive rule ends the run with exactly as many people standing as the one-drive errand did:
$ go run ./cmd/slot -mode day -ticks 2000 -fill 0 2>&1 | tail -36 | head -6
the habit, which is four drives asked in one order
name at first at last after the run
Halla 200.0000 -0.0032 starved on tick 2264
Ander 40.0000 22.4335 standing
Mose 120.0000 -0.0321 starved on tick 2804
1 of 3 standing after 2000 ticks
Drive 1 only fires below a quarter of a full store, so with drive 3 gone a villager between a quarter and the trading threshold has nothing to say about a plant standing in front of it and turns on the spot instead. It coasts down to the quarter mark, eats back up above it, coasts down again, and spends the whole run inside the narrowest band its rent allows, until one stretch of turning costs more than the band had in it. The ground is barely touched by comparison: 528.5047 grams eaten against the 901.8115 the full rule ate, on the same twelve plants. Two people starved with food standing all around them.
And the run stops, which is the other half of the exercise. The audit at the bottom asks whether four drives kept more people standing than one drive did, gets 1 against 1, and says so on standard error with a non-zero status:
$ go run ./cmd/slot -mode day -ticks 2000 -fill 0 2>&1 | tail -3
NO: the habit kept 1 people standing and the errand alone kept 1
slot: 1 thing(s) did not add up
exit status 1
A table of numbers for a person to read is a report. A check has to be able to stop the run it is watching, and the only way to find out whether a particular check can is to give it something to catch. Put the threshold back and the same comparison passes silently, which is what it does on every other run on this page.
So there are three people at Firstlight keeping to a sensible day, a slot each for somebody to tell them otherwise, and a console to walk over and look at them from. What none of them has is any idea that any of it happened. Halla asked to hand a parcel eight hundred and thirty-two times in that last run and could not tell you about one of them; the console's own body was handed grain by a neighbour it will never be able to name. Every tick of that village is computed from the world as it stands and then thrown away. A person who cannot say what happened to them yesterday has nothing for a decision to be made out of, and writing that down is the next thing this village needs.