Tic-tac-toe, VII: the game’s space is a file
Six parts built tic-tac-toe as resources. The atom is code, because it holds the marks, and
the rules and views are small functions and templates. But one thing about the game was still
Rust in every part: its arrangement. Which doors exist, in what order they are tried, which
names are only other names (the eight lines of Part II), and what a host puts beside the game
(conditional, for the views) were all calls in space_with_store and kernel_over (the
crate’s function that builds the coded game’s kernel and puts conditional behind it).
A kernel already reports its arrangement. urn:kernel:topology answers it as Turtle, and the
catalog and the tracing chapter read it. This part runs that the
other way: the arrangement is written down as data, one file, and the kernel builds the game
from it. The endpoints stay in Rust. Everything around them moves into the file.
The file
Here is the game’s whole arrangement, crates/tic-tac-toe/tic-tac-toe.arrangement:
; Tic-tac-toe's space: which doors exist, in what order, and which names are only other
; names. Every endpoint named here is Rust, registered by the host under that name. This
; file arranges them and cannot make one.
(fallback
(alias
; The board's eight lines, each an exact alias onto the line of cells it holds.
(exact "urn:iki:tutorial:ttt:row:0" "urn:iki:tutorial:ttt:cells:0.0,1.0,2.0")
(exact "urn:iki:tutorial:ttt:row:1" "urn:iki:tutorial:ttt:cells:0.1,1.1,2.1")
(exact "urn:iki:tutorial:ttt:row:2" "urn:iki:tutorial:ttt:cells:0.2,1.2,2.2")
(exact "urn:iki:tutorial:ttt:column:0" "urn:iki:tutorial:ttt:cells:0.0,0.1,0.2")
(exact "urn:iki:tutorial:ttt:column:1" "urn:iki:tutorial:ttt:cells:1.0,1.1,1.2")
(exact "urn:iki:tutorial:ttt:column:2" "urn:iki:tutorial:ttt:cells:2.0,2.1,2.2")
(exact "urn:iki:tutorial:ttt:diagonal:0" "urn:iki:tutorial:ttt:cells:0.0,1.1,2.2")
(exact "urn:iki:tutorial:ttt:diagonal:1" "urn:iki:tutorial:ttt:cells:2.0,1.1,0.2")
(fallback
; Everything above the atom: the cell, the lines, the rules, the templates and views.
(endpoints
(door "urn:iki:tutorial:ttt:cell:{x}:{y}" ttt-cell)
(door "urn:iki:tutorial:ttt:cells:{list}" ttt-cells)
(door "urn:iki:tutorial:ttt:board" ttt-board :match template)
(door "urn:iki:tutorial:ttt:checkset:{x}:{y}" ttt-checkset)
(door "urn:iki:tutorial:ttt:winner" ttt-winner :match template)
(door "urn:iki:tutorial:ttt:turn" ttt-turn :match template)
(door "urn:iki:tutorial:ttt:move:{x}:{y}" ttt-move)
(door "urn:iki:tutorial:ttt:reset" ttt-reset :match template)
(door "urn:iki:tutorial:ttt:template:{name}" ttt-template)
(door "urn:iki:tutorial:ttt:view:board" ttt-view-board :match template)
(door "urn:iki:tutorial:ttt:view:square:{x}:{y}" ttt-view-square)
(door "urn:iki:tutorial:ttt:view:status" ttt-view-status :match template)
(door "urn:iki:tutorial:ttt:view:reply" ttt-view-reply :match template)
(door "urn:iki:tutorial:ttt:view:game:{game}" ttt-view-game)
(door "urn:iki:tutorial:ttt:view:play:{x}:{y}" ttt-view-play)
(door "urn:iki:tutorial:ttt:view:reset" ttt-view-reset :match template))
; The one atom: the stored cell, the only state a game has.
(endpoints
(door "urn:iki:tutorial:ttt:stored:{x}:{y}" ttt-stored))))
; The one resource the views use that the game does not bind.
(endpoints
(door "urn:iki:fn:conditional" conditional)))
It is an s-expression, read by ikigai-sexpr, which converts it losslessly to and from the
Turtle that core builds a space from. Read it from the outside in:
(fallback A B)is an ordered list of spaces. A name is tried in the first, then the second, and the first one that answers wins. The game is first. The second layer holds the one thing the views use that the game does not bind,ikigai-fn’sconditional, whichkernel_overadded in code.(alias rules… space)is Part II’s table. Eachexactrule renames one name to another before the space inside sees it, sourn:iki:tutorial:ttt:row:0reaches the line of cells it holds. The rules are the whole of Part II’sLINES, and no code is behind them.- The inner
fallbackis Part IV’s seam: every composite first, then the store, in a space of its own. That is what lets a game corridor, or a store in another process, answer for the stored cell alone. (endpoints (door pattern endpoint) …)is an endpoint space. Each door binds a pattern to an endpoint, by the endpoint’s name, and the doors are tried in the order written.
Eight doors say :match template. A door’s pattern is text, and text alone cannot say how it
matches: urn:iki:tutorial:ttt:board could be one exact name, or a URI template that happens
to have no variables. Both match the same single name. The reader takes a pattern with no
braces as exact, but the game’s Rust binds every door through UriTemplate::parse, so those
eight are templates, and the file says so to declare the arrangement the code builds. Left
out, the game would answer the same, but it would be a different arrangement. This part
checks that it is the same one, so the file says it.
What stays Rust: the endpoints, in a registry
A declaration binds a door to an endpoint by name, and never makes one. So the host’s half is a registry: every endpoint the game has, each under the name it gives itself.
/// Every endpoint the game has, registered under the name each one gives itself — the host's
/// half of a declaration. The file says which door binds which name; this says what each
/// name IS. Nothing else in the game is Rust any more.
///
/// The five views are one kind of endpoint, `composeOver`, over five templates, so each is
/// [`named`](ikigai_fn::ComposeOver::named) here: a registry holds one endpoint per name.
/// `lines` is the table the rules read (the CheckSet, the winner and the reset), taken from
/// the declaration by [`lines_of`] so the board's geometry is written once.
pub fn registry(
store: Arc<CellStore>,
lines: Arc<ikigai_core::AliasTable>,
) -> std::result::Result<Registry, DeclarationError> {
let endpoints: [Arc<dyn Endpoint>; 18] = [
Arc::new(stored_cell(store)),
Arc::new(platonic_cell()),
Arc::new(line_of_cells()),
Arc::new(board()),
Arc::new(checkset(Arc::clone(&lines))),
Arc::new(winner(Arc::clone(&lines))),
Arc::new(turn()),
Arc::new(play_move()),
Arc::new(reset(lines)),
Arc::new(templates()),
Arc::new(view("board").named("ttt-view-board")),
Arc::new(view("square").named("ttt-view-square")),
Arc::new(view("status").named("ttt-view-status")),
Arc::new(view("reply").named("ttt-view-reply")),
Arc::new(view("game").named("ttt-view-game")),
Arc::new(view_play()),
Arc::new(view_reset()),
Arc::new(ikigai_fn::conditional()),
];
let mut registry = Registry::new();
for endpoint in endpoints {
registry.register(endpoint)?;
}
Ok(registry)
}
Eighteen endpoints, each the same function earlier parts wrote: the atom, the cell, the line,
the board, the rules, the templates, the views, the two view writes, and conditional. None
of them changed to be declared.
The five views need one thing here. Each is composeOver bound to a different template, and
an unnamed composeOver answers the name composeOver. A registry holds one endpoint per
name, because a door that said “composeOver” could otherwise mean any of the five:
/// A declaration arranges registered endpoints and never mints one: a door naming an endpoint
/// nobody registered is refused at build, naming the door and the name.
#[test]
fn a_declaration_cannot_mint_an_endpoint() {
let undo = ARRANGEMENTS
.iter()
.find(|(name, _)| *name == "undo")
.map(|(_, text)| *text)
.expect("the undo arrangement");
let tree = arrangement_to_topology(undo).expect("it reads");
let refused = build(
&tree,
®istry(Arc::default(), Arc::new(lines())).expect("registers"),
)
.err()
.expect("refused");
assert!(
matches!(&refused, DeclarationError::UnknownEndpoint { id, .. } if id == "ttt-undo"),
"{refused}"
);
}
/// A registry holds one endpoint per name, so two views left unnamed — both `composeOver` —
/// cannot both be registered. That is why each view is named.
#[test]
fn two_unnamed_views_are_one_name_too_many() {
let mut registry = Registry::new();
registry
.register(Arc::new(view("board")))
.expect("the first");
let refused = registry
.register(Arc::new(view("square")))
.expect_err("refused");
assert_eq!(
refused,
DeclarationError::DuplicateEndpoint {
id: "composeOver".to_string()
}
);
}
So the registry names each view (ttt-view-board, and so on) where it registers it. Part V’s
view is unchanged, and so is the coded game, whose views are still unnamed.
Three endpoints are configured with a value rather than just registered. The CheckSet, the
winner and the reset read the line table, to know which lines pass through a cell and which
squares a reset clears. In the coded game that table is LINES. In the declared game it comes
from the file, so the board’s geometry is written once:
/// The game `tree` declares, over `store`: its line table read from the declaration, every
/// endpoint registered, and core's [`build`](ikigai_core::build). Refused, naming where, if
/// the declaration binds a name the registry does not hold, or anything else core refuses.
pub fn build_game(
tree: &Topology,
store: Arc<CellStore>,
) -> std::result::Result<Arc<dyn Space>, DeclarationError> {
let lines = Arc::new(lines_of(tree)?);
ikigai_core::build(tree, ®istry(store, lines)?)
}
lines_of finds the file’s one alias and turns its rules into a table. Everything else is
core’s build: it walks the declaration, rebuilds each space with its public constructor,
binds each door to the registered endpoint of that name, and hands back a live space. A host
for the declared game is then one line:
/// A host for the declared game alone: a kernel over the space the file declares, with the
/// Meta renderer [`kernel`](crate::kernel) uses. The coded kernel's twin, with no arrangement
/// in Rust.
pub fn kernel_over(store: Arc<CellStore>) -> std::result::Result<Kernel, DeclarationError> {
Ok(Kernel::with_meta_renderer(
space_over(store)?,
Arc::new(TurtleRenderer),
))
}
The round trip
A declaration is also a resource. The file is bound at
urn:iki:tutorial:ttt:arrangement:tic-tac-toe, and urn:iki:tutorial:ttt:build reads the
declaration it is given through the kernel, builds it against the game’s registry, and
answers what it built, printed back as an arrangement:
/// `ttt-build`: read the declaration `of` names through the kernel — as Turtle, by whatever
/// transreptor the kernel has for its type (ikigai-sexpr's, for an `.arrangement`) — build
/// it against the game's registry over a store of its own, and answer the space that was
/// built, printed as an arrangement.
///
/// The answer is the built space describing itself, not the file: comments gone, the alias
/// rules in the order the table tries them. A declaration the build refuses is refused here,
/// in core's words, as an invalid `of`. Cacheable: a pure function of what `of` answered,
/// so it hangs from `of`'s thread, and an edit to the file rebuilds it.
pub fn build_endpoint() -> AsyncFnEndpoint {
AsyncFnEndpoint::new("ttt-build", |inv: &Invocation<'_>| -> InvokeFuture<'_> {
Box::pin(async move {
let raw = inv.inline_str("of")?.trim();
let of = Iri::parse(raw).map_err(|e| Error::InvalidArgument {
name: "of".to_string(),
detail: format!("not a resource name ({e}): `of` names the declaration"),
})?;
let turtle = turtle_of(inv, &of).await?;
let refused = |detail: String| Error::InvalidArgument {
name: "of".to_string(),
detail,
};
let tree = Topology::from_turtle(&turtle).map_err(|e| refused(e.to_string()))?;
let built = build_game(&tree, Arc::default()).map_err(|e| refused(e.to_string()))?;
let text = topology_to_arrangement(&built.topology())
.map_err(|e| Error::Endpoint(format!("printing the built space: {e}")))?;
Ok(Representation::new(arrangement_type(), text.into_bytes()).cacheable())
})
})
.with_description(
Description::new("ttt-build")
.title("Build a declaration")
.summary(
"Read the declaration `of` names, build it against the game's endpoints, and \
answer the space that was built as an arrangement; refused, naming where, if \
it binds an endpoint the game does not have.",
)
.verb(Verb::Source)
.verb(Verb::Meta)
.input(
ArgSpec::new("of")
.summary(
"the name of a declaration, e.g. \
urn:iki:tutorial:ttt:arrangement:tic-tac-toe; the one input, so a \
positional value lands here",
)
.class(XSD_ANY_URI),
)
.output(MEDIA_ARRANGEMENT),
)
}
/// Source `of` and bring it to Turtle through the kernel's own transreptors: a plan from the
/// type it answered as, each step a request, so every step is a sub-request like any other.
async fn turtle_of(inv: &Invocation<'_>, of: &Iri) -> Result<String> {
let answered = inv.source(of).await?;
let media = answered.repr_type.media_type.clone();
let mut bytes = answered.bytes;
if media != TURTLE {
let plan =
inv.select_transreptor(&media, TURTLE)
.ok_or_else(|| Error::InvalidArgument {
name: "of".to_string(),
detail: format!(
"{} answers {media}, and nothing here reads that as Turtle",
of.as_str()
),
})?;
for step in plan {
let transreptor = Iri::parse(&step.endpoint)
.map_err(|e| Error::Endpoint(format!("a transreptor's name: {e}")))?;
let request = Request::new(Verb::Source, transreptor)
.with_arg("content", ArgRef::Inline(bytes))
.with_arg("as", ArgRef::Inline(step.to.into_bytes()));
bytes = inv.issue(request).await?.bytes;
}
}
String::from_utf8(bytes).map_err(|_| Error::InvalidArgument {
name: "of".to_string(),
detail: format!("{} is not UTF-8 text", of.as_str()),
})
}
The file is served as text/x-ikigai-arrangement, and nothing in the build endpoint parses
that. It asks the kernel for a transreption to text/turtle, and the kernel finds
ikigai-sexpr’s. Run it:
(fallback
(alias
(exact "urn:iki:tutorial:ttt:diagonal:0" "urn:iki:tutorial:ttt:cells:0.0,1.1,2.2")
(exact "urn:iki:tutorial:ttt:diagonal:1" "urn:iki:tutorial:ttt:cells:2.0,1.1,0.2")
(exact "urn:iki:tutorial:ttt:column:0" "urn:iki:tutorial:ttt:cells:0.0,0.1,0.2")
(exact "urn:iki:tutorial:ttt:column:1" "urn:iki:tutorial:ttt:cells:1.0,1.1,1.2")
(exact "urn:iki:tutorial:ttt:column:2" "urn:iki:tutorial:ttt:cells:2.0,2.1,2.2")
(exact "urn:iki:tutorial:ttt:row:0" "urn:iki:tutorial:ttt:cells:0.0,1.0,2.0")
(exact "urn:iki:tutorial:ttt:row:1" "urn:iki:tutorial:ttt:cells:0.1,1.1,2.1")
(exact "urn:iki:tutorial:ttt:row:2" "urn:iki:tutorial:ttt:cells:0.2,1.2,2.2")
(fallback
(endpoints
(door "urn:iki:tutorial:ttt:cell:{x}:{y}" ttt-cell)
(door "urn:iki:tutorial:ttt:cells:{list}" ttt-cells)
(door "urn:iki:tutorial:ttt:board" ttt-board :match template)
(door "urn:iki:tutorial:ttt:checkset:{x}:{y}" ttt-checkset)
(door "urn:iki:tutorial:ttt:winner" ttt-winner :match template)
(door "urn:iki:tutorial:ttt:turn" ttt-turn :match template)
(door "urn:iki:tutorial:ttt:move:{x}:{y}" ttt-move)
(door "urn:iki:tutorial:ttt:reset" ttt-reset :match template)
(door "urn:iki:tutorial:ttt:template:{name}" ttt-template)
(door "urn:iki:tutorial:ttt:view:board" ttt-view-board :match template)
(door "urn:iki:tutorial:ttt:view:square:{x}:{y}" ttt-view-square)
(door "urn:iki:tutorial:ttt:view:status" ttt-view-status :match template)
(door "urn:iki:tutorial:ttt:view:reply" ttt-view-reply :match template)
(door "urn:iki:tutorial:ttt:view:game:{game}" ttt-view-game)
(door "urn:iki:tutorial:ttt:view:play:{x}:{y}" ttt-view-play)
(door "urn:iki:tutorial:ttt:view:reset" ttt-view-reset :match template))
(endpoints
(door "urn:iki:tutorial:ttt:stored:{x}:{y}" ttt-stored))))
(endpoints
(door "urn:iki:fn:conditional" conditional)))
[computed]
(fallback
(alias
(exact "urn:iki:tutorial:ttt:diagonal:0" "urn:iki:tutorial:ttt:cells:0.0,1.1,2.2")
(exact "urn:iki:tutorial:ttt:diagonal:1" "urn:iki:tutorial:ttt:cells:2.0,1.1,0.2")
(exact "urn:iki:tutorial:ttt:column:0" "urn:iki:tutorial:ttt:cells:0.0,0.1,0.2")
(exact "urn:iki:tutorial:ttt:column:1" "urn:iki:tutorial:ttt:cells:1.0,1.1,1.2")
(exact "urn:iki:tutorial:ttt:column:2" "urn:iki:tutorial:ttt:cells:2.0,2.1,2.2")
(exact "urn:iki:tutorial:ttt:row:0" "urn:iki:tutorial:ttt:cells:0.0,1.0,2.0")
(exact "urn:iki:tutorial:ttt:row:1" "urn:iki:tutorial:ttt:cells:0.1,1.1,2.1")
(exact "urn:iki:tutorial:ttt:row:2" "urn:iki:tutorial:ttt:cells:0.2,1.2,2.2")
(fallback
(endpoints
(door "urn:iki:tutorial:ttt:cell:{x}:{y}" ttt-cell)
(door "urn:iki:tutorial:ttt:cells:{list}" ttt-cells)
(door "urn:iki:tutorial:ttt:board" ttt-board :match template)
(door "urn:iki:tutorial:ttt:checkset:{x}:{y}" ttt-checkset)
(door "urn:iki:tutorial:ttt:winner" ttt-winner :match template)
(door "urn:iki:tutorial:ttt:turn" ttt-turn :match template)
(door "urn:iki:tutorial:ttt:move:{x}:{y}" ttt-move)
(door "urn:iki:tutorial:ttt:reset" ttt-reset :match template)
(door "urn:iki:tutorial:ttt:template:{name}" ttt-template)
(door "urn:iki:tutorial:ttt:view:board" ttt-view-board :match template)
(door "urn:iki:tutorial:ttt:view:square:{x}:{y}" ttt-view-square)
(door "urn:iki:tutorial:ttt:view:status" ttt-view-status :match template)
(door "urn:iki:tutorial:ttt:view:reply" ttt-view-reply :match template)
(door "urn:iki:tutorial:ttt:view:game:{game}" ttt-view-game)
(door "urn:iki:tutorial:ttt:view:play:{x}:{y}" ttt-view-play)
(door "urn:iki:tutorial:ttt:view:reset" ttt-view-reset :match template))
(endpoints
(door "urn:iki:tutorial:ttt:stored:{x}:{y}" ttt-stored))))
(endpoints
(door "urn:iki:fn:conditional" conditional)))
[cached]
That answer is the built space describing itself, not the file echoed back. The comments are gone, because a space has none to report. The eight rules come out diagonals first, in the order the alias table tries them (longest name first), not the order the file lists them in. Everything else is the file. Building it and asking the result for its arrangement gives the arrangement you started with: the round trip is a fixpoint, and a test holds it to that.
The second line was served from the cache. The build is a pure function of what it read, so it hangs from the file’s golden thread, and a change to the file would rebuild it.
The same game, twice
The page’s kernel now holds both games: the coded one, which every earlier part played, and
one built from the file. A cell that says data-game='a' plays the coded game a, as in
Part IV. A cell marked as a declared game a runs in a corridor that holds the whole game
built from the file, ahead of the root’s coded one, with a store of its own.
Play the same moves in each. X takes the center, O a corner, and then X tries the center again. First in the coded game:
X plays 1,1 [uncacheable] O plays 0,0 [uncacheable] error: invalid argument `x, y`: 1,1 is taken — X played there O-- -X- --- [computed] X [computed]
Then in the game built from the file:
X plays 1,1 [uncacheable] O plays 0,0 [uncacheable] error: invalid argument `x, y`: 1,1 is taken — X played there O-- -X- --- [computed] X [computed]
The same answers, the same refusal in the same words, and the same verdicts from the cache.
The refusal is still InvalidArgument, as Part VI explained: the move
has not switched to Conflict yet (ledger item 585). When it does, both games will say
conflict: together, because a refusal belongs to the endpoint, and the file only says where
the endpoint is bound.
A trace shows which space answered. One more move in the declared game, then a trace of the square it took:
X plays 2,2 [uncacheable] trace urn:iki:tutorial:ttt:cell:2:2 client ikigai repl · capability: root (full authority) transport embedded · in-process · chain urn:iki:tutorial:ttt:declared:a root urn:iki:tutorial:ttt:cell:2:2 ttt-cell · computed · ThreadId(1) · — · answered-by=urn:iki:tutorial:ttt:declared:a scope=urn:iki:tutorial:ttt:declared:a root → 1b X └─ urn:iki:tutorial:ttt:stored:2:2 ttt-stored · computed · ThreadId(1) · — · answered-by=urn:iki:tutorial:ttt:declared:a scope=urn:iki:tutorial:ttt:declared:a root
Both nodes were answered by the corridor urn:iki:tutorial:ttt:declared:a: the cell, and
the stored cell it read, which is the declared game’s own store. Nothing fell through to the
coded game behind it.
Two cells are one sample. The crate plays a whole game both ways, reads nineteen names after every step (the board, the rules, the lines, the views and the store), and requires the same bytes, the same cacheability and the same refusals from both, and the same number of reads of the atom:
/// The game built from the file answers every step of a whole game exactly as the coded game
/// does — every write, every refusal, and every read after each — and reads its atom exactly
/// as often, so it caches and recomputes the same.
#[test]
fn the_declared_game_answers_exactly_as_the_coded_one() {
let (coded_store, declared_store) = (
Arc::new(CellStore::default()),
Arc::new(CellStore::default()),
);
let coded = kernel_over(Arc::clone(&coded_store));
let declared = declared::kernel_over(Arc::clone(&declared_store)).expect("the file builds");
let mut steps = 0;
let mut compare = |verb: Verb, name: &str, content: Option<&str>| {
let name = format!("{T}{name}");
let (a, b) = (
answer(&coded, verb, &name, content),
answer(&declared, verb, &name, content),
);
assert_eq!(b, a, "{verb:?} {name}");
steps += 1;
};
for name in READS {
compare(Verb::Source, name, None);
}
for (verb, name, content) in SCRIPT {
compare(verb, name, content);
for name in READS {
compare(Verb::Source, name, None);
}
}
assert_eq!(steps, READS.len() * (SCRIPT.len() + 1) + SCRIPT.len());
assert_eq!(
declared_store.reads.load(Ordering::SeqCst),
coded_store.reads.load(Ordering::SeqCst),
"the same reads of the atom: the same cache, the same golden threads"
);
}
The last assertion is the strongest one. Both kernels read their store the same number of times over the whole game, so the declared game caches, and recomputes on a move, exactly as the coded one does. Nothing about golden threads was declared. They come from the endpoints, which are the same.
That test compares behavior, not arrangements, and on purpose. The two arrangements are not
identical: the coded game’s five views are all named composeOver, and the declared ones are
named. Rename those five back and they are the same tree, which another test in the same file
checks.
What a declaration may and may not do
A declaration arranges endpoints the host registered. It can order doors, stack spaces, alias names, mount, limit and seal. It can never make an endpoint. Here is a declaration that tries to add an undo by naming one:
; A declaration that tries to add a feature by naming it: an undo that nobody wrote.
(endpoints
(door "urn:iki:tutorial:ttt:board" ttt-board :match template)
(door "urn:iki:tutorial:ttt:undo" ttt-undo))
error: invalid argument `of`: <urn:ikigai:space:_:1:door:2> binds `ttt-undo`, which the host did not register: a declaration arranges registered endpoints and never mints one
`of`: the name of a declaration, e.g. urn:iki:tutorial:ttt:arrangement:tic-tac-toe; the one input, so a positional value lands here
The refusal names where: the second door of the first anonymous space, by the name that door has in the declaration’s Turtle. An undo would be a new endpoint, written in Rust and registered, and then a file could bind it.
Core refuses everything it cannot rebuild, and never skips it, because a kernel that answered less than its declaration says would look fine until someone asked for the missing part. The crate checks each case against the game’s own registry:
| refused at build | why |
|---|---|
| an endpoint name the registry does not hold | a declaration arranges what the host registered, and mints nothing |
| two different endpoints under one name | refused when the second is registered: a door binds by name, so the name must say which |
| an opaque space | a remote peer or a hand-written resolver has nothing to rebuild from |
| a closure rewrite | its rule is code, not a table. The table-driven form is an alias |
| a seal on a name the host sealed | seals are checked when the space is built, against the host’s own |
Each refusal names the node it found, as the declaration’s Turtle names it.
The picture
An arrangement is a tree, so it can be drawn. ikigai-diagram renders one as nested boxes,
and this is the file’s:
tic-tac-toe.arrangement by
ikigai_diagram::render. Each door row names its pattern, the endpoint that answers
it, and how the pattern matches. Select the picture to open it at full size.The picture is committed beside this chapter, and a test holds it to the file:
/// The picture the chapter shows is `ikigai_diagram::render` of the file's arrangement, byte
/// for byte. On a mismatch the picture the code draws is written beside the build and the
/// failure names the command that commits it; nothing rewrites it from inside a test.
#[test]
fn the_committed_picture_is_the_files_arrangement() {
let committed = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../books/ikigai/src/applied/tic-tac-toe-arrangement.svg"
);
let drawn = ikigai_diagram::render(&declared::topology().expect("the file reads"));
if std::fs::read_to_string(committed).ok().as_deref() == Some(drawn.as_str()) {
return;
}
let out = format!(
"{}/tic-tac-toe-arrangement.svg",
env!("CARGO_TARGET_TMPDIR")
);
std::fs::write(&out, &drawn).expect("write the picture the code draws");
panic!(
"the committed picture no longer matches tic-tac-toe.arrangement.\n\
If the change is intended: cp {out} {committed}"
);
}
Edit the file, and the test fails until the picture is redrawn.
What changed, measured
The devtools’ resource-ratio report counts, from a kernel’s topology, the names bound to code
(doors) and the names composed by a rule over another name (aliases). Before this part, for
the coded game under urn:iki:tutorial:: 17 coded, 8 composed. For the declared game: 17
coded, 8 composed. The same, and it should be. Declaring does not change how many doors are
code. The same eighteen endpoints (seventeen under the game’s names, and conditional) answer
the same doors.
What moved is the arrangement:
| coded game | declared game | |
|---|---|---|
| where the doors, their order and the aliases are | Rust: space_with_store, stored_space, LINES, and the fallback in kernel_over | tic-tac-toe.arrangement: 32 lines, not counting comments |
| Rust that arranges anything | all of the above | none |
| Rust the host adds | none | registry, one line per endpoint, and lines_of to read the line table from the file |
| endpoints | 18 | the same 18, five of them named |
The registry is new code, and it is a list: the endpoints the coded game held implicitly, in its binds. That is the line this part draws. An endpoint is behavior, and behavior is code. Where things go, and under which names, is data, and now it is a file that any tool can read, draw, diff or check.
Where this goes
A host can read an arrangement too. Since ikigai-cli 0.1.33 the ikigai host prints the
root it is running as a declaration, draws it, and starts its local root from one:
ikigai --plain -c 'source urn:iki:host:arrangement as=text/x-ikigai-arrangement' > host.arrangement
ikigai --plain -c 'source urn:diagram:kernel' > host.svg
ikigai --arrangement mine.arrangement
The first line is the s-expression face; without as= the same root comes back as Turtle. The
second is an SVG, and needs urn:cap:kernel:inspect, which the local REPL holds. The third
reads a declaration (here mine.arrangement, your edited copy of host.arrangement), Turtle or
*.arrangement, relative to the working directory, and
arrangement = "<path>" in the host’s config does the same. A file that does not build stops
the start and names the node that failed. One thing to know before you try it: a host arranges
only the endpoints it registered. This chapter’s tic-tac-toe.arrangement names ttt-cell
and the rest, which only the game registers, so it is not a host arrangement and
--arrangement refuses it. Start from the host’s own printed root instead, and edit that. As
printed, it does not yet start unchanged, because a few of the host’s endpoints share a name
(ledger item 639); its header says which.
This part declared one space, the game’s root. A game is still a corridor built in Rust
(Part IV’s game), and so are the temporal and personal corridors the spreadsheet arc builds
later (A spreadsheet, VI).
Declaring corridors as templates, filled in per request from a game id, an instant or a
person, is the next step (ledger item 632).
What was built
The game’s space is one file. The kernel reads it, as Turtle, through a transreptor, and core builds the game from it against a registry of the endpoints the earlier parts wrote. The declared game plays exactly as the coded one, move for move, refusal for refusal and cache hit for cache hit, and a test holds it to that. Its picture is drawn from the same file, and a test holds that too. What stays Rust is the part that has to be: the endpoints, which hold the state and compute the answers. Where they are bound is now data.