blut-graph-core
blut-graph-core is a domain-neutral, no_std + alloc semantic compiler for
node graphs. You declare what each node means; it kind-checks the graph,
fuses only where the meaning survives, and lowers the result to an execution
realm.
Three properties are declared per node, and each is a lattice rather than a tag:
Determinism : BitExact -> NumericallyEquivalent -> Seeded -> Nondeterministic
Effect : Pure -> Idempotent -> Transactional -> AtMostOnce -> AtLeastOnce
Partiality : Atomic | ExplicitGaps
Those declarations are not documentation. They select the execution protocol:
Transactional drives prepare/commit/abort with invocation-bound idempotency
keys, and a node declared ExplicitGaps must emit a structured gap receipt
rather than quietly succeeding. The compiler verifies typed ports and
capability, proof, policy, fidelity, resource, effect, and target contracts
before producing a deterministic CompiledPlan shared by MCU AOT, host/stream,
and BLUT durable execution realms.
The crate is domain-agnostic in the literal sense: it contains no vocabulary for
any problem domain, and it cannot acquire one, because the only domain-shaped
field on a port is an opaque token it never interprets (see DomainToken).
Node configuration is a sealed exact-value algebra (bool, signed/unsigned
integers, bounded text/choice/bytes) validated against a normalized descriptor
schema. Defaults are materialized before semantic identity is calculated, so
implicit and explicit defaults compile to the same GraphId and PlanId.
Unknown, missing, mistyped, and out-of-range values fail before kernel search.
Every physical port carries a DomainType — a root/view pair of opaque
DomainTokens — plus proof, policy, fidelity, extent, layout, and lease
contracts. root names the artifact, view names the projection of it; both
are domain vocabulary, so the compiler compares and hashes them and never asks
what they mean. The only structural rule it enforces is that a token is
non-empty. These contracts survive fusion, layout conversion, AOT
serialization, and durable-plan adaptation; an edge is admitted only when the
producer contract satisfies the consumer contract.
The crate owns no domain semantics, filesystem, network, async runtime, or plugin process. Those remain implementation concerns behind the kernel, transaction, and process-host traits.
GraphId covers the normalized semantic graph, including capability, proof,
policy, and fidelity contracts. PlanId additionally covers the selected
realm, kernels, layouts, buffers, and fusion regions. Callers set an admission
ceiling with Compiler::with_memory_limit; untrusted AOT readers also enforce
PlanLimits::max_peak_bytes before returning an executable plan.
KernelDescriptor::implements names the exact versioned semantic chain executed
by an implementation. Fusion selects one of those explicit implementations;
the compiler never relabels an ordinary single-node kernel as fused. Physical
layout conversions are likewise registered kernels and appear as StepIds in
the physical topology, attempt receipts, liveness analysis, and memory plan,
while semantic completion receipts contain only the original NodeIds.
Kernel selection is a bounded deterministic whole-plan search. It can reject a cheap local choice in favor of a feasible global layout, insert a shortest typed conversion path, and choose the admitted plan with the smallest arena peak. Explicit fused implementations of arbitrary linear-chain length compete with unfused partitions and with one another; a cheap infeasible fused kernel cannot mask a feasible alternative. Buffer slots are reused only after the prior alias lifetime ends.
Raw CompiledPlan::from_aot_bytes is structural inspection, not authorization.
Execution requires AuthorizedPlan, produced either by local compilation or by
KernelRegistry::decode_authorized_plan after realm, trusted PlanId, kernel
implementation, resource, determinism, lowering, effect, and conversion checks.
Effects distinguish prepare/commit transactions, idempotent work, and
explicitly weaker at-most-once or at-least-once execution. Invocation-bound
idempotency keys and partial failure receipts make retries auditable.
External invocation values bind canonical named input ports; zero-input source
nodes cannot receive undeclared data. Nodes that permit partial output must
declare ExplicitGaps and emit structured, domain-checked gap receipts.
For firmware, AuthorizedPlan::mcu_arena_requirements validates the supported
static subset and returns exact caller-owned arena dimensions without
allocating. The firmware execution loop remains a realm implementation rather
than a disguised use of the generic allocating host executor.
State is explicit and bounded. StateContract distinguishes invocation,
session, and durable state and binds checkpoint mode, snapshot size, and maximum
checkpoint interval. Cross-invocation feedback uses a positive-delay
FeedbackEdge, a dense FeedbackPlan, and a session contract; the plan records
the exact persistent-state arena size. The generic one-shot executor and MCU
subset fail closed on these constructs until their realm supplies a state
store. Hierarchical decompositions are content-identified SubgraphSchemas;
their identity covers repeated local instances, typed config, internal edges,
interface bindings, and nested child identities. Registered lowerings require
descriptor-compatible child interfaces, exact port maps, and type-compatible
outer-to-inner configuration bindings. KernelRegistry::materialize_subgraph
applies one canonical outer instance to a concrete reference graph that callers
can compile with fusion enabled or disabled. Ordinary compilation still keeps
the outer node physical and preserves root subgraph identity in its step; BGP3
does not implicitly inline-expand inner DAGs.
Semantic graphs use schema version 3 and the physical-step/ordered-port contract
is encoded as BGP3. Earlier alpha BGP1 and BGP2 postcard layouts are
intentionally not reinterpreted. BGP3 adds typed canonical configuration,
per-port semantic contracts, explicit bounded state/feedback/session records,
and identity-bound hierarchical lowering.
The process-plugin control plane is BPC2, a bounded canonical postcard wire.
It binds a domain-separated BLAKE3 executable digest, declared capabilities,
a canonical unsigned Ed25519 manifest digest and verifier key identifier,
request and invocation identity, startup/request/heartbeat
deadlines, maximum inflight/frame sizes, and graceful-then-kill or immediate
teardown policy. Its lifecycle only advances from spawn through handshake,
ready, draining, and termination; it cannot re-enter readiness after teardown.
The crate defines the supervisory contract but never spawns a process itself.
The BLUT engine exposes adapt_durable_plan, which maps an authorized
BlutDurable physical plan into the existing kind-checked durable DAG. The
adapter preserves semantic graph/plan identities and per-step implementation
lineage plus ordered physical port identities in the durable recipe record. It
rejects ambiguous multi-port mappings, invocation inputs, explicit gaps,
non-pure effects, resource under-declaration, retry drift, determinism drift,
or missing implementation/checkpoint/policy rechecks.
Running the shipped examples
Both examples are evidence generators rather than tutorials, and they take
different arguments. Neither runs bare — cargo run --example … with no
arguments panics on a missing one, which is worth knowing before you conclude
something is broken:
# compile/lower a fixture graph and emit timing + identity evidence as JSON
# execute a fixture plan in one realm; --inject-fault exercises the failure path
Tests and examples ship inside the published tarball deliberately, so the crate can be verified by someone with no access to its source repository.
Upgrading from 0.2.0-alpha.1
0.3.0 is a breaking change to the API and not to the wire. The three
execution-vocabulary types — Target, ExecutionRealm and Layout — were
fieldless enums and are now opaque #[serde(transparent)] newtypes over u32,
for the same reason DomainToken replaced the ABIR enums in 0.2: a general
compiler should not carry one problem domain's taxonomy in its public API. It
compares these tokens, orders them, and folds them into the plan hash. It has no
opinion about what any of them means.
Every historical name is preserved as an associated constant, so
Target::Host, ExecutionRealm::McuAot and Layout::Canonical still compile,
in expressions and in patterns. What breaks is code that matched exhaustively
over a variant list, or cast with as u32; use token() and from_token(),
which are greppable in a way a cast is not.
The wire does not move, and that is measured rather than argued.
tests/wire_stability.rs pins each realm's graph_id, plan_id and complete
BGP3 byte string as literals, taken at 0.2.0-alpha.1 and unchanged here. This is
the opposite of the 0.2 migration recorded below, which kept the serialized
names and moved every absolute PlanId: here the ordinals are preserved
exactly, #[serde(transparent)] makes postcard emit the same varint a variant
index did, and Debug is hand-written to print the same spellings — which
matters because KernelDescriptor::lowering is conventionally
format!("{target:?}") and lowering is hashed. A derived Debug would have
moved every plan id in the fleet without touching an ordinal.
One behaviour is restored rather than preserved. A fieldless enum's derived
Deserialize rejected an out-of-range variant index for free; a newtype accepts
any u32. from_aot_bytes now range-checks every token it decodes and returns
the new PlanDecodeError::UnknownToken, where 0.2 returned Malformed for the
same input.
Upgrading from 0.1.0-alpha.1
0.2.0-alpha.1 is a breaking change and the only one of consequence is the port's domain field:
| 0.1.0-alpha.1 | 0.2.0-alpha.1 |
|---|---|
port.abir: AbirSemanticType |
port.domain: DomainType |
AbirRootType::Tensor |
DomainToken::new("tensor") |
AbirViewType::Atom |
DomainToken::new("atom") |
AbirRootType::Unknown(s) |
DomainToken::new(s) |
Token strings are byte-identical to the kebab-case names the old enums
serialized to, and DomainToken is #[serde(transparent)], so serialized
plans keep their wire names. Absolute PlanIds do change: the hash previously
folded a numeric discriminant per variant and now folds the token bytes
uniformly, which is what removes the compiler's dependence on any domain's
variant list.
The enums carried one problem domain's taxonomy in a general compiler's public
API. In practice most callers had already routed around it — the majority of
real uses were the Unknown(String) escape — so for those the change is a
rename.
Status and licence
Alpha. The API is not stable, and the version is pre-release for that reason. Single-process compilation and the host/stream realm are exercised hardest; the MCU AOT subset is narrower by construction and fails closed outside it.
Licensed AGPL-3.0-or-later. That is a strong copyleft with a network clause — if you distribute a work built from this crate, or offer it to users over a network, the AGPL's obligations apply to that combined work. Check that this suits you before depending on it.