Behaviour, sameness and dimension
Two programs can look different and be the same. This chapter says what "the same" means in RCE, how the checker can tell, and how to measure how much of a program's state is doing work.
The behaviour of a program
Run any sequence of events through a program and write down what it emits at each step. That table, for every possible sequence, is the program's behaviour. Nothing else about the program is part of its behaviour: not the names of its fields, not the order its steps are written in, not what it keeps in state and never reads. The specification (section 6.10) defines behaviour exactly this way, and defines two programs to be equivalent when their behaviours are equal.
That is the only sense of "same" the Form needs. It is also why the emitters can be checked: a Rust program and a Python program emitted from one Form are correct when they have the same behaviour, and the reference trace of chapter 15 is a prefix of that table.
How the checker decides it
You cannot list every event sequence; there are infinitely many. The specification gives two ways round that.
The first is a bisimulation: a relation between the states of two programs such that related states emit the same thing on every event and land in related states. If the starting states are related, the behaviours are equal (T58). To show two programs are the same, exhibit the relation; to show they differ, exhibit one event sequence where the outputs part.
The second is a distance. Two behaviours that agree on every sequence up
to length k and differ at k + 1 are at distance 2^-(k+1). Trace tests like
those of chapter 15 therefore prove closeness, not equality: rce trace
agreeing on 40 events shows the two programs are within 2^-41 of each
other. That is why a trace never enters a hash in RCE; a hash names one
thing, and a trace names a neighbourhood.
Dimension: how many behaviours can a program be in?
Every RCE program has finite state, so the set of states it can reach is finite, and the number of different behaviours among those states is a number. The specification calls it the program's dimension, dim(P). It is the count of states a minimal machine with the same behaviour would need. State beyond that is redundancy: bits that no future output depends on.
rce dim measures it. It explores the states reachable from the start
through every event, with payloads drawn from each type's edge values, then
groups states that behave alike.
Here is a counter mod 4 that also remembers whether the last event was an increment. Nothing reads that flag:
intent "a counter mod 4 that also records the last event, which nothing reads"
"rce dim (10.11): 8 states explored, 4 behaviours, redundancy 1.00 bit; region DimCounter reports the 1 bit"
example DimInc, DimInc, DimInc, DimInc -> n == 0
example DimInc, DimDec -> last_inc == false
state DimCounter
n: u64 = 0
last_inc: bool = false
event DimInc
event DimDec
step DimCounter on DimInc
n = (n + 1) % 4
last_inc = true
emit n
step DimCounter on DimDec
n = (n + 3) % 4
last_inc = false
emit n
$ rce dim counter_flag.rce
states explored 8 bound reached: no
behaviours 4 dim(P) (exact)
redundancy 1.00 bits: log2(states) - log2(behaviours)
outputs yes some explored step has a λ output (emits: yes, keeps: no)
distinct outputs 4 distinct output sequences over the explored steps (silent steps excluded)
region DimCounter 1.00 (8 values, 4 distinguish behaviours)
exit status: 0
Eight states, four behaviours, one bit of redundancy, and the report says which region carries it. The flag can go.
Redundancy is not always a spare field. This program keeps a count mod 6 but only ever emits its parity:
intent "a counter mod 6 whose only output is its parity"
"rce dim (10.11): 6 states explored, 2 behaviours, redundancy 1.58 bits: the state carries a period of 6 but the behaviour has a period of 2"
example DimStep -> n == 1
example DimStep, DimStep, DimStep, DimStep, DimStep, DimStep -> n == 0
state DimPhase
n: u64 = 0
event DimStep
step DimPhase on DimStep
n = (n + 1) % 6
emit n % 2
$ rce dim phase.rce
states explored 6 bound reached: no
behaviours 2 dim(P) (exact)
redundancy 1.58 bits: log2(states) - log2(behaviours)
outputs yes some explored step has a λ output (emits: yes, keeps: no)
distinct outputs 2 distinct output sequences over the explored steps (silent steps excluded)
region DimPhase 1.58 (6 values, 2 distinguish behaviours)
exit status: 0
Six states, two behaviours: the state has a period of six, the behaviour has a period of two, and log2(6) − log2(2) = 1.58 bits of state change no future output.
And here is the door from chapter 10, where every reachable state answers some future event differently:
intent "a door whose state is exactly its behaviour"
"rce dim (10.11): 3 states explored (open and locked at once is unreachable), 3 behaviours, redundancy 0.00 bits: every reachable state answers some future event differently"
example DimOpen, DimClose, DimLock -> locked
example DimOpen, DimLock -> kept: DimDoorOpen
state DimDoor
open: bool = false
locked: bool = false
event DimOpen
event DimClose
event DimLock
event DimUnlock
error DimDoorOpen
error DimLocked
step DimDoor on DimOpen if locked
fail DimLocked
step DimDoor on DimOpen
open = true
step DimDoor on DimClose
open = false
step DimDoor on DimLock if open
fail DimDoorOpen
step DimDoor on DimLock
locked = true
step DimDoor on DimUnlock
locked = false
$ rce dim door.rce
states explored 3 bound reached: no
behaviours 3 dim(P) (exact)
redundancy 0.00 bits: log2(states) - log2(behaviours)
outputs yes some explored step has a λ output (emits: no, keeps: yes)
distinct outputs 2 distinct output sequences over the explored steps (silent steps excluded)
region DimDoor 0.00 (3 values, 3 distinguish behaviours)
exit status: 0
Three states, not four: a door cannot be open and locked at once, because
Lock keeps the state while open and Open keeps it while locked. Three
behaviours, no redundancy. This program's state is exactly its behaviour.
State nobody can see
The traffic light of chapter 5 has three states and, by this measure, one
behaviour: it never emits and no step of it keeps the state, so no
sequence of events gets a different answer from any state. Its examples check light == Yellow,
but an example reads the state from inside; the Form's behaviour is what
comes out. rce dim on that program reports 3 states, 1 behaviour and 1.58
bits of redundancy, and it is right: as written, the light's colour is in
the state, and in the log, but no output depends on it. Add emit light
to its step and the
three states become three behaviours. When the first run of rce dim over
the specification's own examples was made, most of its state machines
turned out to be of this kind.
The control view
rce dim --control asks a coarser question: not what the program emits,
but which steps fire. Two states are control-equivalent when every event
sequence makes the same steps fire, keep the state or emit the same kinds
of thing from both. The count is never larger than the behaviour count, and its
canonical form is hashed as shape_ctl, a key that does not change when
you rename anything:
$ rce dim --control door.rce
states explored 3 bound reached: no
behaviours 3 dim(P) (exact)
redundancy 0.00 bits: log2(states) - log2(behaviours)
outputs yes some explored step has a λ output (emits: no, keeps: yes)
distinct outputs 2 distinct output sequences over the explored steps (silent steps excluded)
region DimDoor 0.00 (3 values, 3 distinguish behaviours)
control states 3 dim_c(P) (exact)
shape_ctl sha256:cf0bc0895132723a218efd65c63e69596fb0f05e0bb348feeaef5cd85dd1e392
exit status: 0
For the door the two counts agree. For the mod-4 counter they do not: every event fires the same step with the same kind of output, so it has one control state and four behaviours. The gap between the two numbers is how much of the behaviour lives in data rather than in control.
When the count is a lower bound
A program whose state can grow without limit, like the plain counter of
chapter 4, has more reachable states than rce dim will visit. It stops at
--fuel states (100,000 by default), says bound reached: yes, and reports
the behaviours it found as a lower bound. Exploration is deterministic:
the same program and the same fuel give the same report, byte for byte.
Changing a program
Behaviour is also what decides whether a change to a program is safe. Three commands answer the question from different sides.
rce hash names each definition by a hash of its text with every declared
name replaced by its position. Here is a second version of the counter of
the tour, whose Dec is written as two steps instead of one:
intent "a counter that never goes below zero"
example Inc, Inc, Dec -> 1
example Dec -> kept: Underflow
state Counter
n: u64 = 0
event Inc
event Dec
error Underflow
step Counter on Inc -> n + 1
step Counter on Dec if n == 0
fail Underflow
step Counter on Dec -> n - 1
$ rce hash counter_v2.rce
b0562f9794dc5733c70479eefac5879016a9a517b51a082af124c766284395ed state Counter pub
2dc6cdf92460a1f39f996982a11bb4d58e477640faa6c59cd94d7e1d44981ced event Inc pub
2dc6cdf92460a1f39f996982a11bb4d58e477640faa6c59cd94d7e1d44981ced event Dec pub
76507de38a8fce86b67180b9da859c60b592c2cffc85212887300d323b15e40d error Underflow pub
a8374f2c4d3a0420f18870c5dfc0df15adec88465b3d552861d60dbe0f489127 program
exit status: 0
Inc and Dec have the same hash: without their names, two events with no
payload are the same definition. pub marks an export, a definition that
other programs can name. The last line is the hash of the whole program. rce api-diff OLD NEW
compares the exports of two versions, one row per change, each
compatible or breaking.
rce change OLD NEW puts the two together: both program hashes, the
api-diff rows and the check of rce equiv. It decides an erratum when
every export is compatible and every behaviour is kept:
$ rce change --fuel 1000 ../tour/counter.rce counter_v2.rce
program 4631c798c37181944d74d4c6a90fdfc273747787b15df8388f524550327c4cb5
a8374f2c4d3a0420f18870c5dfc0df15adec88465b3d552861d60dbe0f489127
api no export changed
behaviour alphabet 2 event instances explored
behaviour states 1000 / 1000
behaviour verdict equivalent same behaviour on the explored alphabet (to the explored bound)
verdict erratum to the explored bound every export compatible and every behaviour kept as far as explored
exit status: 0
A meaning change when a behaviour changed, with the sequence that shows it:
$ rce change --fuel 1000 ../tour/counter.rce ../tour/sloppy.rce
program 4631c798c37181944d74d4c6a90fdfc273747787b15df8388f524550327c4cb5
fe1f224134b853b16bb3cac10037d20b40584f8e60ce726a02062a59842da59b
api breaking error Underflow export removed or renamed
behaviour alphabet 2 event instances explored
behaviour states 1000 / 1000
behaviour verdict different
behaviour after Dec()
behaviour A kept: Underflow
behaviour B silent
verdict meaning change the behaviour of the program changed
exit status: 1
And an interface change when the behaviour is kept but an export a caller could name is gone. The renamed counter of the tour behaves the same, but every name a caller used has changed:
$ rce change --fuel 1000 ../tour/counter.rce ../tour/tally.rce
program 4631c798c37181944d74d4c6a90fdfc273747787b15df8388f524550327c4cb5
7d16ded6e2398dda981219bd7ade850624b68f4b8b7bbe74115287c9d87e383f
api compatible error BelowZero export added
api breaking state Counter export removed or renamed
api breaking event Dec export removed or renamed
api compatible event Down export added
api breaking event Inc export removed or renamed
api compatible state Tally export added
api breaking error Underflow export removed or renamed
api compatible event Up export added
behaviour alphabet 2 event instances explored
behaviour states 1000 / 1000
behaviour verdict equivalent same behaviour on the explored alphabet (to the explored bound)
verdict interface change behaviour kept, an export broken (F.4.3): callers must change
exit status: 1
The counter grows without end, so each verdict holds "to the explored
bound". When the bound is reached before the two programs are told apart
or shown equal, the verdict is undecided, with exit status 3: raise
--fuel. The decision is the tool's, read off the two programs, not a
version number someone typed. Chapter 16 shows where it is used: a log may
be rebound to a changed program only after an erratum.
What you learned
- A program's behaviour is what it emits on every event sequence; two programs are the same when their behaviours are equal.
- A bisimulation proves sameness; one distinguishing sequence proves difference; a trace proves closeness to within 2^-(k+1).
rce dimcounts the behaviours a program can be in and reports the redundancy of its state per region, in bits;--controlcounts the control states and hashes their canonical form asshape_ctl.rce hash,rce api-diffandrce changesay whether a change keeps the exports and the behaviour: erratum, meaning change or interface change.