Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Short names in a space

A space that changes while it runs put a declaration resource in front of a dynamic space and changed the space by writing it. This chapter uses the same mechanism for a smaller job: names. A composition that reads a dozen resources spells out a dozen full names, urn:json:query and urn:iki:tutorial:shop:stock again and again. Short names would make it easier to write and to read. The question is where short names may exist without changing what any other name means.

The answer here is the one Scope and alias gave for renaming: a name is resolved in a space, and an alias rule is a declaration in one. A prefix table is an alias whose rules rewrite json: to urn:json: and shop: to urn:iki:tutorial:shop:. It lives in a dynamic space, and the space is carried as a corridor around the requests that compose. Inside, shop:stock is a name. Outside, there is no such name.

The data is the shop’s stock from Transreption: a CSV table, read as JSON by urn:json:query. The code is crates/shop. Nothing here needs a server or a REPL. The live host on the page of A space that changes while it runs, if it is still open, is a different page’s kernel and changes nothing here.

The prefix table is data

The table is an arrangement, in the s-expression form A space that changes while it runs wrote:

; The shop's prefix table: two short names, each a prefix rule that resolves from the root under
; the caller's own capability (`:resolve-at root`), over an empty space. `json:query` is
; `urn:json:query`, and `shop:stock` is `urn:iki:tutorial:shop:stock`. ikigai-fn's
; `prefix_check` refuses any other shape, and any prefix under a scheme the host's own names use.
(alias
  (prefix "json:" "urn:json:" :resolve-at root)
  (prefix "shop:" "urn:iki:tutorial:shop:" :resolve-at root)
  (endpoints))

Each rule is :resolve-at root, which that chapter’s view introduced: the rewritten name is asked again from the root of the caller’s chain, under the caller’s own capability. A short name is a view of a full name, so it grants nothing. A caller who may not read urn:iki:tutorial:shop:stock may not read shop:stock either.

The table lives at urn:iki:tutorial:shop:prefixes, a declaration resource of the same kind as that chapter’s (the crate reuses live_space::declaration): Sink writes an arrangement, and Source answers the ik: Turtle core builds a space from. The space is ikigai-fn’s prefix_space, which is a DynamicSpace with a check of its own:

/// The shop's names: the stock, the prefix table, the three templates, and the two views that
/// fill them. Nothing here knows that the templates' names are short: they are filled by
/// whatever the request's resolution chain makes of them.
pub fn space(stock_state: Arc<StockState>, prefixes: Arc<DeclarationState>) -> EndpointSpace {
    let mut space = EndpointSpace::new()
        .bind(Exact::new(STOCK), stock(stock_state))
        .bind(Exact::new(PREFIXES), live_space::declaration(prefixes));
    for (name, text) in TEMPLATES {
        space = space.bind(
            Exact::new(format!("urn:iki:tutorial:shop:template:{name}")),
            template(name, text),
        );
    }
    space
        .bind(
            Exact::new(SUMMARY),
            ikigai_fn::compose_over(iri("urn:iki:tutorial:shop:template:summary"))
                .named("shop-summary"),
        )
        .bind(
            Exact::new(RECORDS),
            ikigai_fn::compose_over(iri("urn:iki:tutorial:shop:template:records"))
                .named("shop-records"),
        )
}

/// The prefix space called `name`: an empty dynamic space built from [`PREFIXES`], which binds
/// only a prefix table (ikigai-fn's `prefix_space` checks it), and is named
/// `urn:iki:space:dynamic:{name}`.
pub fn prefixes(name: &str) -> DynamicSpace {
    ikigai_fn::prefix_space(name, iri(PREFIXES))
}

/// The host's root: the shop's names, then ikigai-json's (`urn:json:*`), then ikigai-fn's
/// (`urn:iki:fn:*`). The prefix space is not in it: it is a corridor a request carries.
pub fn root(stock_state: Arc<StockState>, prefixes: Arc<DeclarationState>) -> Fallback {
    Fallback::new(vec![
        Arc::new(space(stock_state, prefixes)) as Arc<dyn Space>,
        Arc::new(ikigai_json::space()),
        Arc::new(ikigai_fn::space()),
    ])
}

The check refuses a table that could do anything but rename. It accepts one alias, of rules that resolve from the root, over an empty space with no doors. A prefix under a scheme the host’s own names use is refused too: urn, http, https, file, data and the rest. The corridor is consulted ahead of the root, so a prefix urn: could stand in for every name the host has. Under the reserved schemes it never can.

Carried as a corridor

The host starts an empty prefix space called shop and the loop that rebinds it, exactly as that chapter’s host did:

    /// Make the root, an empty prefix space called `name`, and the loop that rebinds it from
    /// [`PREFIXES`]; the corridor is the space, alone.
    pub fn start(name: &str) -> Host {
        let kernel = Arc::new(Kernel::with_meta_renderer(
            Arc::new(root(Arc::default(), Arc::default())),
            Arc::new(ikigai_vocab::TurtleRenderer),
        ));
        let space = Arc::new(prefixes(name));
        let corridor = Scope::empty().with(Arc::clone(&space) as Arc<dyn Space>);
        let pool = LocalPool::new();
        let (looping, on) = (Arc::clone(&space), Arc::clone(&kernel));
        pool.spawner()
            .spawn_local(async move {
                let stopped = looping.drive(&on, &Capability::root()).await;
                panic!("the prefix space's loop stopped: {stopped}");
            })
            .expect("spawn the rebuild loop");
        let mut host = Host {
            kernel,
            space,
            corridor,
            pool,
        };
        host.settle();
        host
    }

What is new is how a request gets into it. The prefix space is not a layer of the root. It is a corridor, injected around one request:

    /// `request` under root, issued IN the prefix space: the space rides the request as a
    /// corridor, so every sub-request it gives rise to, at any depth, resolves the short names.
    pub fn read_in(&self, request: Request) -> Result<String, Error> {
        let root = Capability::root();
        let answer = block_on(self.kernel.issue_in(request, &root, self.corridor.clone()))?;
        Ok(String::from_utf8_lossy(&answer.bytes).into_owned())
    }

A corridor rides the request it was injected for, and every sub-request that request gives rise to, at any depth, inherits it. That matters here. A marker’s shop:stock is not asked by the caller: it is asked by urn:json:query, an endpoint that lives in the root, which asks for its src while it runs. A Level scopes only the sub-requests of endpoints found inside it, and urn:json:query is not inside anything, so a Level would not reach that read. The corridor does.

In the page, the cells below that say “in the prefix space shop” run on the page’s kernel with that space carried as a corridor. The others run at the page’s root. Run them in order: each one changes what the next one reads.

Empty, then a table

Until a table is written, the space holds nothing, and the summary, whose template is written with short names, cannot be composed:

space: urn:iki:space:dynamic:shop
declaration: urn:iki:tutorial:shop:prefixes
in force: nothing (the space is empty)
installs: 0
refused: the declaration could not be read: not found: urn:iki:tutorial:shop:prefixes
[uncacheable]
error: no endpoint resolved for shop:stock
       did you mean `urn:iki:tutorial:shop:stock`? (bound here)

Write the table, on one line, and ask again. The summary composes, and shop:summary, the summary’s own short name, answers the same: a name in a cell’s line is resolved in the cell’s chain like any other. The last line passes a short name as a value, src=shop:stock, and the query reads it in the corridor it inherited:

ok
[uncacheable]
space: urn:iki:space:dynamic:shop
declaration: urn:iki:tutorial:shop:prefixes
in force: b3:24a60324ce76a9284a2c82696491fea287d9f88b1f304992fbe4dc480d4cba74
installs: 1
refused: nothing since the last bind
[uncacheable]
4 items, 49 on the shelves; out of stock: sugar.
[computed]
4 items, 49 on the shelves; out of stock: sugar.
[computed]
flour
[computed]

shop:summary is computed, not served from the line before: a name is a cache key, so the short name has an answer of its own, which depends on the full name’s.

The summary: CSV, JSON, jq and $j

Here is the summary’s template. Each $j is a jq selection of the stock, spliced as text (Tic-tac-toe, V showed that $j is shorthand for a $h of urn:json:query):

$j{shop:stock length} items, $j{shop:stock map(.count | tonumber) | add} on the shelves; out of stock: $j{shop:stock [.[] | select(.count == "0") | .item] | if length == 0 then "nothing" else join(", ") end}.

Follow one marker down. $j{shop:stock length} asks urn:json:query (its own name, fixed) for src=shop:stock and q=length. The query endpoint reads shop:stock, which the corridor rewrites to urn:iki:tutorial:shop:stock and asks again from the root. That answers text/csv, so the query plans the CSV transreptor, urn:json:from-csv, and runs length over the array of records it makes. Four resources, two of them reached by short names, and none of them written for this summary.

The same, in Rust:

#![allow(unused)]
fn main() {
extern crate shop;
extern crate ikigai_core;
use ikigai_core::Error;
use shop::{source, Host, PREFIXES, PREFIX_TABLE, SUMMARY};

let mut host = Host::start("shop");
host.write(PREFIXES, PREFIX_TABLE);

let summary = || source(SUMMARY, &[]);
assert_eq!(
    host.read_in(summary()).unwrap(),
    "4 items, 49 on the shelves; out of stock: sugar."
);
assert!(host.is_cached_in(&summary()));

// Outside the space, the template's short names do not exist.
assert!(matches!(host.read(summary()), Err(Error::Unresolved(_))));
assert!(matches!(host.read(source("shop:stock", &[])), Err(Error::Unresolved(_))));
}

Outside, no such name

At the page’s root, with no corridor, the same two names:

error: no endpoint resolved for shop:stock
       did you mean `urn:iki:tutorial:shop:stock`? (bound here)
error: no endpoint resolved for shop:stock
       did you mean `urn:iki:tutorial:shop:stock`? (bound here)

shop:stock is not a name the page’s kernel has. The summary is, but its template asks for shop:stock, so outside the space it fails the same way. The prefix table changed what names exist for the requests that carry it, and for nothing else on the page.

A collection: each

urn:iki:fn:each fills one template once per element of a JSON array. The array is shop:records, a template that is one $r of json:query over the stock with the program ., and the row is $h{{item}} ($h{{count}}):

flour (12), sugar (0), rice (30), lentils (7)
[computed]
error: no endpoint resolved for tmpl:row
       did you mean `urn:iki:tutorial:shop:template:row`? (bound here)

The first line fills the row once per record. The second asks for tmpl:row, and the table has no tmpl: prefix.

A new prefix, while it runs

Writing a new prefix is a Sink on the declaration. The host’s loop hears the write as a cut of the declaration’s thread, rebinds the space, and the names inside it change. Nothing restarts. Add tmpl: for the shop’s templates, and ask again. Then ask about the summary, which the first table composed:

ok
[uncacheable]
$h{{item}} ($h{{count}})
[computed]
not cached
4 items, 49 on the shelves; out of stock: sugar.
[computed]

The summary was cached, and the new table cut it, though the summary uses no tmpl: name. That is that chapter’s coarse cut: every answer served through the space hangs from the space’s one thread, and every successful bind cuts it. It is never wrong, only sometimes wasteful: what was composed with the old table recomputes once.

The data changes too

A write to the stock cuts the stock’s thread, and the summary hangs from it through every $j. This line keeps only what is on a shelf, the same round trip Transreption ran, with the stock named short where it is a value:

ok
[2 computed · 1 uncacheable]
3 items, 49 on the shelves; out of stock: nothing.
[computed]

A table refused

A table that declares a prefix under a scheme the host’s names use is refused before it binds. Here it claims file::

ok
[uncacheable]
space: urn:iki:space:dynamic:shop
declaration: urn:iki:tutorial:shop:prefixes
in force: b3:680caaa80e550dcaf0c1c73445e5c5afd4a8e8a6cdd326c9b034921867d3e8ac
installs: 2
refused: the host's check refused it: prefix `file:` is under the reserved scheme `file:` — the host's own names live there, and a prefix may never stand in for one
refused digest: b3:5c613c265b34d7e9f9166633782a60a66117656c2ebbe07601ba97f5b99636fd
[uncacheable]
cached
3 items, 49 on the shelves; out of stock: nothing.
[cached]

As in A space that changes while it runs, a refused change cuts nothing, and the table that bound last stays in force. The status says what was refused and why.

What it does not do

  • The cut is coarse, as in that chapter (ledger #2034): any new table recomputes everything composed through the space.
  • The reserved schemes are the whole guarantee. A host that serves names under a scheme of its own (page:, say) and also declares a prefix page: would have that prefix stand in for its names inside the corridor. Core has no placement for “answer only if the root misses”, so ikigai-fn refuses the schemes every ikigai host uses, and every ikigai host’s names are urn:.
  • A short name is not a line’s target when it takes arguments. Inside a composition every short name works, arguments and all: a marker’s json:query?src=shop:stock&q=. builds its request directly. A REPL line is different. The engine recognizes key=value by the target’s contract, which it reads from the target’s Meta, and the Meta of a short name describes the forwarding endpoint the rule binds, which declares no inputs. So source json:query src=shop:stock q=. sends src=… as positional text and is refused for a missing q. The cells above name the endpoint in full and pass short names as values.
  • A sandbox bound in the page is an abort. jq runs metered and bounded. Natively a bound reached is a typed refusal. In this page, WebAssembly cannot unwind, so it traps the page’s kernel until a reload. The cells here are far inside the bounds; keep an edited program small.

What was built

whatwherewhat it is
the stockurn:iki:tutorial:shop:stockan atom: CSV, written by a Sink, cached and cut
the CSV as JSONurn:json:query, urn:json:from-csvikigai-json: jq over the stock, through the CSV transreptor
the prefix tableurn:iki:tutorial:shop:prefixesdata: an alias of root-resolving prefix rules, written by a Sink
the prefix spaceurn:iki:space:dynamic:shopikigai-fn’s prefix_space: the table, bound, and checked
the corridorKernel::issue_inthe space carried by a request and every sub-request it makes
the summaryurn:iki:tutorial:shop:summarya template of three $j markers, filled
the listurn:iki:fn:eacha row template filled once per record

The only code is the two atoms. The CSV reader, the query language, the template language, the iteration and the short names are published resources, composed. A prefix is not a feature of the template language: it is a name, declared in a space, and the space decides where it exists.