This tutorial demonstrates Lace’s central operation: two participants converge the checked records selected by an Interlang policy. Two records move, one valid but unrelated record stays behind, and public Lace operations verify the result.
Both participants start in memory so the boundary is easy to inspect. A local or connected participant occupies the same interlace argument; the final section shows how the second participant becomes a remote endpoint.
| Concern | Responsibility here |
|---|---|
| Runtime | Own participants and stores, construct and store validated records, drive interlace, and list the result. |
| Policy | Select checked records for convergence and determine which local records peer-authored rules may inspect. |
Runtime choice answers where records live and how participants meet. Policy selects checked records for convergence. Changing language, store, or transport does not require another policy model.
The policy selects records under one Group/App/Name shape:
converge {
Item in //first-interlace//demo/item//selected/{*} => include Item
}
It matches records whose fields are:
Group = first-interlace
App = demo/item
Name = selected/<one or more path segments>
converge contributes those records to active exchange
and exposes those same checked local records to peer-origin rules for
this interlace. Exposure permits inspection of record facts; it does not
itself copy a record. A record moves only where both participants’
policy operands select it and the receiver validates its bytes.
Interlang is the normal human authoring surface. Lace lowers it to canonical Datalog. Raw Datalog is the advanced layer for lower-level behavior that Interlang cannot express; it is not a prerequisite here.
The runnable companions load this exact text from policy/items.interlang.
Clone the source checkout first:
git clone https://codeberg.org/rs0/lace.git
cd lace
Choose one panel. Each complete program creates the same records, stores them on the source participant, runs bounded interlace, and checks both the selected and unrelated destination views.
Build the source-checkout JS/WASM bundle, then run the companion:
rustup target add wasm32-unknown-unknown
npm --prefix js/crates/lace-js run build
node examples/first-interlace/js/first-interlace.mjs
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import {
Lace,
PolicyAuthoringSource,
Record,
boundedInterlace,
list,
store,
} from '../../../js/crates/lace-js/dist/lace.bundle.js';
const TAI = '0000000001:000000000';
const policy = PolicyAuthoringSource.interlang(
(await readFile(new URL('../policy/items.interlang', import.meta.url), 'utf8')).trim(),
);
const unrelatedPolicy = PolicyAuthoringSource.interlang(`converge {
Item in //first-interlace//demo/item//unrelated/{*} => include Item
}`);
const item = (name, data) => Record.create({
group: 'first-interlace',
app: 'demo/item',
name,
tai: TAI,
data,
});
const source = await Lace.memory();
const destination = await Lace.memory();
try {
const first = await item('selected/one', 'first selected record');
const second = await item('selected/two', 'second selected record');
const unrelated = await item('unrelated/not-selected', 'this record must stay at the source');
const stored = await store(source, [first, second, unrelated]);
console.log(`source stored: `);
const outcome = await boundedInterlace(source, destination, { policy });
assert.equal(outcome.terminal.type, 'complete');
console.log('interlace outcome: complete');
const selected = await list(destination, policy);
const selectedNames = selected.records.map(record => record.name().text()).sort();
assert.deepEqual(selectedNames, ['selected/one', 'selected/two']);
console.log(`destination selected: `);
const unrelatedResult = await list(destination, unrelatedPolicy);
assert.equal(unrelatedResult.records.length, 0);
console.log(`destination unrelated: `);
} finally {
await Promise.all([source.close(), destination.close()]);
}
The JavaScript package lazily initializes one shared WASM runtime per JavaScript realm, so independently opened local Laces can participate in one direct in-process interlace.
Runnable source: js/first-interlace.mjs.
Build the repository-local extension in its uv
environment, then run the companion:
uv --directory python sync --extra test
uv --directory python run --with 'maturin>=1.7,<2' maturin develop
uv --directory python run python ../examples/first-interlace/python/first_interlace.py
from pathlib import Path
from lace import Complete, Lace, PolicyAuthoringSource, Record, bounded_interlace, list_records, store
TAI = "0000000001:000000000"
POLICY_PATH = Path(__file__).resolve().parent.parent / "policy" / "items.interlang"
UNRELATED_POLICY = (
"converge {\n"
" Item in //first-interlace//demo/item//unrelated/{*} => include Item\n"
"}"
)
def item(name: str, data: bytes) -> Record:
return Record.create(
group="first-interlace",
app="demo/item",
name=name,
tai=TAI,
data=data,
)
first = item("selected/one", b"first selected record")
second = item("selected/two", b"second selected record")
unrelated = item("unrelated/not-selected", b"this record must stay at the source")
with Lace.memory() as source, Lace.memory() as destination:
stored = store(source, [first, second, unrelated])
assert isinstance(stored.terminal, Complete)
print(f"source stored: ")
policy = PolicyAuthoringSource.interlang(POLICY_PATH.read_text().strip())
outcome = bounded_interlace(source, destination, policy)
assert isinstance(outcome.terminal, Complete)
print("interlace outcome: complete")
selected = list_records(destination, policy)
selected_names = sorted(record.name for record in selected.records)
assert selected_names == ["selected/one", "selected/two"]
print(f"destination selected: ")
unrelated_policy = PolicyAuthoringSource.interlang(UNRELATED_POLICY)
unrelated_list = list_records(destination, unrelated_policy)
assert isinstance(unrelated_list.terminal, Complete)
assert not unrelated_list.records
print(f"destination unrelated: ")
Runnable source: python/first_interlace.py.
The Rust companion uses the synchronous lace-single
runtime and requires no daemon or network:
cargo run --manifest-path examples/first-interlace/Cargo.toml
use lace_single::prelude::*;
const TAI: &str = "0000000001:000000000";
const POLICY: &str = include_str!("../policy/items.interlang");
const UNRELATED_POLICY: &str = r#"
converge {
Item in //first-interlace//demo/item//unrelated/{*} => include Item
}
"#;
fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
let source = Lace::empty_memory();
let destination = Lace::empty_memory();
let item = spec!({
group: "first-interlace",
app: "demo/item",
tai: TAI,
});
let first = record!(item, {
name: "selected/one",
data: "first selected record",
})?;
let second = record!(item, {
name: "selected/two",
data: "second selected record",
})?;
let unrelated = record!(item, {
name: "unrelated/not-selected",
data: "this record must stay at the source",
})?;
let stored = store(source.handle(), [first, second, unrelated])?;
println!("source stored: {}", stored.records.len());
let policy = PolicyAuthoringSource::interlang(POLICY.trim());
let outcome = bounded_interlace(
source.handle(),
destination.handle(),
policy.clone(),
)?;
assert_eq!(outcome.terminal, Terminal::Complete);
println!("interlace outcome: complete");
let selected = list(destination.handle(), policy)?;
let mut selected_names = selected
.records
.iter()
.map(|record| record.name().expect("selected records have names").to_string())
.collect::<Vec<_>>();
selected_names.sort();
assert_eq!(selected_names, ["selected/one", "selected/two"]);
println!("destination selected: {}", selected_names.join(", "));
let unrelated = list(
destination.handle(),
PolicyAuthoringSource::interlang(UNRELATED_POLICY.trim()),
)?;
assert!(unrelated.records.is_empty());
println!("destination unrelated: {}", unrelated.records.len());
Ok(())
}
spec! builds the shared Group/App/TAI template.
record! merges each record’s Name and data into it and
returns a validated Record.
Runnable source: src/main.rs.
Every panel prints:
source stored: 3
interlace outcome: complete
destination selected: selected/one, selected/two
destination unrelated: 0
The second destination query is important: listing only
selected/{*} would not prove that
unrelated/{*} was absent. Both checks use public
interlace-backed porcelain rather than raw store inspection.
A bounded interlace completes at the bilateral fixed point, when neither side requests another record in the settled round. It does not claim that the stores are equal.
The operation accepts participant handles, not a special local-store or remote API. The destination argument can therefore come from a memory Lace or a connection:
| Binding | Local destination | Connected destination |
|---|---|---|
| JavaScript | destination from Lace.memory(...) |
await Lace.connectWebSocket('ws:127.0.0.1:4791/interlace') |
| Python | destination from Lace.memory(...) |
Lace.connect_websocket('ws:127.0.0.1:4791/interlace') |
Rust (lace-tokio) |
destination.handle() |
Lace::connect("tcp:127.0.0.1:4790").await? |
Rust (lace-single) |
destination.handle() |
Lace::connect("tcp:127.0.0.1:4790")? |
The synchronous lace-single operation has the same
participant shape without an async executor argument:
let peer = Lace::connect("tcp:127.0.0.1:4790")?;
let outcome = bounded_interlace(source.handle(), peer, policy.clone())?;
assert_eq!(outcome.terminal, Terminal::Complete);
Pass the connected value in the same destination position of
boundedInterlace/bounded_interlace. The local
policy argument remains the local participant’s operand. The remote
endpoint contributes its own configured operand, and records converge
only where both select them; connecting does not let either side impose
its selection alone.
A minimal laced host can expose the same policy to the
TCP and WebSocket bindings:
Config 1
DataDir ./laced-data
Listen tcp:127.0.0.1:4790 policy=./items.interlang
Listen ws:127.0.0.1:4791/interlace policy=./items.interlang
Place the config beside items.interlang, then check and
run it:
laced check-config ./laced.conf
laced --foreground --config ./laced.conf
The runtime integration guide, under
“Operate laced only when a service is needed,” explains the
host boundary and links the configuration reference. The rust-tokio-tcp-fs
companion is a complete connected example with filesystem stores and
TCP.
The local tutorial retains the destination Lace so it can inspect the
result immediately. After a connected bounded session, query the remote
participant through a new connected list operation or
lace --at ... list instead.
laced.examples/shared-todo-list/
shows a browser participant using a WebSocket laced
endpoint.Examples are runnable integration companions. The numbered specifications remain authoritative for the shared contracts.