feat: L1 prototype

This commit is contained in:
Zachery Aaron Shores-Chmielewski 2026-03-02 22:46:50 +07:00
commit f20876af86
20 changed files with 10480 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

300
Cargo.lock generated Normal file
View file

@ -0,0 +1,300 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "crossterm"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [
"bitflags",
"crossterm_winapi",
"mio",
"parking_lot",
"rustix",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
dependencies = [
"winapi",
]
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "l0-cpu-runtime"
version = "0.1.0"
dependencies = [
"crossterm",
]
[[package]]
name = "libc"
version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "mio"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.61.2",
]
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]]
name = "rustix"
version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "signal-hook"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-mio"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
dependencies = [
"libc",
"mio",
"signal-hook",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

7
Cargo.toml Normal file
View file

@ -0,0 +1,7 @@
[package]
name = "l0-cpu-runtime"
version = "0.1.0"
edition = "2021"
[dependencies]
crossterm = "0.28"

36
examples/counter.l1 Normal file
View file

@ -0,0 +1,36 @@
actor counter {
state {
count: u64 = 0
}
window count_view : (count)
readers(display)
on Increment(amount: u64) {
count = count + amount
}
}
leaf ticker {
process {
forward(counter, Increment(1))
}
}
leaf display {
reads counter.count_view
process {
read(counter.count_view.count)
}
}
pipeline main {
ticker -> counter -> display
}
core main {
actors: [counter]
leaves: [ticker, display]
pipelines: [main]
steps: 5
}

50
examples/product.l1 Normal file
View file

@ -0,0 +1,50 @@
// Product tracker: accumulates quantity and total cost from purchase events.
//
// Each step, a purchase of 3 units at price 7 arrives.
// The actor multiplies price * quantity to get the line cost,
// then adds it to the running total.
//
// After 4 steps:
// count = 4 * 3 = 12
// total = 4 * (7 * 3) = 84
// emitted values: [21, 42, 63, 84]
actor ledger {
state {
count: u64 = 0
total: u64 = 0
}
window summary : (count, total)
readers(report)
on Purchase(price: u64, qty: u64) {
count = count + qty
total = total + (price * qty)
}
}
leaf buy {
process {
forward(ledger, Purchase(7, 3))
}
}
leaf report {
reads ledger.summary
process {
read(ledger.summary.total)
emit(total)
}
}
pipeline main {
buy -> ledger -> report
}
core main {
actors: [ledger]
leaves: [buy, report]
pipelines: [main]
steps: 4
}

37
examples/window.l1 Normal file
View file

@ -0,0 +1,37 @@
actor accumulator {
state {
total: u64 = 0
}
window total_view : (total)
readers(observe)
on Add(value: u64) {
total = total + value
}
}
leaf source {
process {
forward(accumulator, Add(10))
}
}
leaf observe {
reads accumulator.total_view
process {
read(accumulator.total_view.total)
emit(total)
}
}
pipeline main {
source -> accumulator -> observe
}
core main {
actors: [accumulator]
leaves: [source, observe]
pipelines: [main]
steps: 3
}

865
spec.md Normal file
View file

@ -0,0 +1,865 @@
# Compiler Spec: L3 → L0
*A four-layer compiler for ML training pipelines, from intent to bare metal.*
Each layer is a closed DSL with finite forms. Each has its own IR suited to the decisions made at that level. Optimization happens *between* layers during lowering and *within* layers as IR→IR rewrites. Never at the DSL surface.
Lowering between layers is **search**: enumerate valid candidates at the target layer, score them, pick the best. If search is slow, the representation is wrong.
```
┌─────────────────────────────────────────────┐
│ L3 Spec what you want │
├───────────────── search ────────────────────┤
│ L2 Method how, algorithmically │
├───────────────── search ────────────────────┤
│ L1 Placement where, on what │
├───────────────── emit ──────────────────────┤
│ L0 Machine explicit execution │
├─────────────────────────────────────────────┤
│ Codegen PTX / LLVM IR / x86 / ... │
└─────────────────────────────────────────────┘
↕ rewrites within each layer
```
---
## Zen
- Read little; write little
- Correctness via DSL model, not assertions
- Every stage as simple as possible; all mutations checkable
- Unbounded execution = specification error
- Optimize between stages, closer to machine; not at DSL level
- Never sacrifice performance for correctness; change the representation
- Specify via properties: liveness, safety, consistency, resource bounds
The last point is structural. Every verification table in this document classifies its checks under exactly these four property kinds. If a check doesn't fit one of them, it doesn't belong.
### Property Definitions
**Safety** — references resolve, types match, nothing dangling. *"You will not read garbage."*
**Consistency** — declared constraints agree with each other and with structure. *"Your spec doesn't contradict itself."*
**Liveness** — every resource produced is consumed; every buffer freed; no dead allocations. *"Nothing is wasted."*
**Resource bound** — memory, compute, time, space are all finite and known. *"You will not run forever or exhaust the machine."*
### Verification Without Solvers
The restricted DSL makes each check a specific algorithm. Conservation is a graph walk. Separation is set disjointness. Boundedness is constant arithmetic. If the DSL allowed recursion, unbounded loops, or dynamic allocation, we'd need SAT solvers. Because it doesn't, we don't.
---
## L3: Spec
### Purpose
Declare *what* you want. No algorithms, no hardware, no shapes beyond the data itself. This is a contract that all lower layers must satisfy.
### DSL Forms
```
data <n> {
<field> : <domain>
...
}
model <n> {
in: <data_ref>
out: <domain>
properties: [<constraint>, ...]
}
loss <n> {
<term_name> : <loss_kind>(<args>), weight: <float>
...
combine: <combinator>
}
goal: minimize(<loss_ref>) | maximize(<ref>) | pareto([<refs>])
```
### Domain Types
```
tensor(dtype, shape) fixed shape
sequence(domain, max_len) variable-length, bounded
table(col: domain, ...) tabular
dist(n) probability distribution over n classes
scalar(dtype) single value
```
### Model Constraints
These are not implementations. They bound what L2 may produce.
```
depth(n) max composed stages
width(n) max hidden dimension
causal output_t depends only on input_≤t
autoregressive output is fed back as input
equivariant(group) symmetry constraint
param_budget(n) max parameter count
latency_budget(ms) max forward pass time (needs L1 info)
```
### Loss Kinds
```
divergence(predicted, target) ce, kl, etc. (choice is L2)
norm(target, p) Lp on params or activations
constraint(predicate) hard constraint → penalty
reconstruction(input, decoded) autoencoders
contrastive(anchor, positive, negative) representation learning
custom(name, reads: [refs]) escape hatch, user-defined
```
### IR
```
L3_IR = {
data: Map<Name, DataDecl>
models: Map<Name, ModelDecl>
losses: Map<Name, LossDecl>
goal: Goal
hints: Map<Path, EscapeHatch>
}
```
All refs resolved. All domains concrete. No optionals. A total description.
### Verification
| Property | Check | Catches |
|----------------|------------------------|-----------------------------------------------------------|
| Safety | Ref resolution | Dangling references to data fields, model outputs |
| Consistency | Domain compatibility | Model input domain ≠ data field domain |
| Safety | Loss signature | Loss term reads something that doesn't exist |
| Consistency | Constraint agreement | `depth(2)` + `autoregressive` on feedforward output |
| Resource bound | Finiteness | Everything bounded, no recursion at this level |
All checks decidable. All linear in program size.
### Optimization
**None.** L3 is a spec. You don't optimize a spec. You may *simplify* it — merge redundant loss terms, canonicalize domains — but no semantic changes.
### Trace — MNIST at L3
```
data mnist {
images : tensor(f32, [28, 28])
labels : tensor(u8, [1])
}
model classify {
in: mnist.images
out: dist(10)
properties: [depth(3), width(512), param_budget(500K)]
}
loss objective {
ce: divergence(classify.out, mnist.labels), weight: 1.0
sparse: norm(classify.params, p=1), weight: 1e-4
combine: weighted_sum
}
goal: minimize(objective)
```
This is the entire program. Everything below exists to make it run.
---
## L3 → L2 Lowering
Given an L3 spec, enumerate valid L2 programs and pick the best.
The search space is combinatorial over (sample) × (architecture) × (backward) × (update). Architecture is the big one — it's a DAG construction problem. The search is structured, not brute force:
From `depth(3)` + `sequential`: 3-layer chain. Width search over powers-of-2 up to `width(n)`. If `causal`, only causal attention. If `autoregressive`, add feedback edges. Backward filtered by architecture compatibility. Update mostly independent. Sample constrained by data domain.
Cost model scores without lowering to L1: param count, FLOPs per step, memory high-water mark, known convergence priors.
If search produces >10K candidates: **representation too loose.** Add constraints at L3 or tighten the enumeration grammar.
---
## L2: Method
### Purpose
Choose *how* to achieve the spec. Algorithms, architecture, training strategy. Still no hardware, no memory, no scheduling.
### DSL Forms
```
methods for <model_ref> {
sample: <sample_method>
infer: <infer_method>
backward: <backward_method>
update: <update_method>
}
```
#### Sample Methods
```
epoch_shuffle(batch: n, drop_last: bool)
stream_window(window: n, stride: n)
curriculum([{ filter: <pred>, duration: n_steps }, ...])
replay(buffer: n, priority: <kind>)
distributed_shard(shards: n, method: <inner>)
```
#### Infer Methods (Architecture)
```
sequential([<layer>, ...])
residual([<block>, ...])
diffusion_chain(steps: n, noise: <schedule>, denoiser: <infer>)
recurrent(cell: <infer>, steps: n | until(<pred>))
graph(nodes: <infer>, edges: <adjacency>)
mixture(experts: [<infer>], gate: <infer>)
```
#### Layer Primitives
```
linear(in, out)
conv(in_ch, out_ch, kernel, stride, padding)
attn(heads, dim, causal: bool)
norm(kind: layer | group(n) | rms)
act(fn: relu | gelu | silu | swish)
embed(vocab, dim)
pool(kind: avg | max, kernel)
dropout(p) training only
```
#### Backward Methods
```
autodiff(mode: reverse | forward, checkpoint: [<layer_ref>])
evolution(pop: n, sigma: f, selection: <kind>)
dfa(feedback_dim: n)
reinforce(baseline: <kind>)
straight_through(estimator: <kind>)
zeroth_order(samples: n, sigma: f)
hybrid(primary: <backward>, aux: <backward>, blend: <schedule>)
```
`autodiff` is an L2 declaration. L2→L1 lowering expands it into an explicit backward DAG in the tensor graph. At L1 and below, forward and backward are both explicit — no compiler magic.
#### Update Methods
```
sgd(lr)
adam(lr, beta1, beta2, eps)
muon(lr, ns_steps)
lion(lr, beta1, beta2)
schedule(warmup: n, decay: <kind>, inner: <update>)
gradient_clip(max_norm: f, inner: <update>)
ema(decay: f, inner: <update>)
```
### IR
```
L2_IR = {
sample: SampleNode
infer: InferGraph DAG of layer nodes
backward: BackwardDecl
update: UpdateChain
shapes: Map<NodeId, Shape> propagated during verification
hints: Map<Path, EscapeHatch>
}
```
The InferGraph is a DAG, not a sequence. `sequential` and `residual` are sugar that lower into the DAG form. This makes fusion and rewrite uniform.
**Key principle:** `recurrent(... steps: n)` is finite. `recurrent(... until(pred))` is a specification error unless `pred` is proven terminating with a maximum bound. We do not allow unbounded execution.
### Verification
| Property | Check | Catches |
|----------------|--------------------------|--------------------------------------------------------|
| Consistency | Shape propagation | Mismatched shapes on infer DAG edges |
| Consistency | Spec constraint check | depth, width, param_budget violated |
| Consistency | Backward compatibility | Backward method can't handle infer graph structure |
| Consistency | Update shape match | Update produces params of wrong shape |
| Consistency | Sample/data match | Batch shape inconsistent with data domains |
| Resource bound | Finiteness | DAG is acyclic (recurrent unrolled to fixed depth) |
| Resource bound | Resource estimation | Rough FLOPs, memory — feeds L1 search cost model |
### Optimization (IR→IR rewrites)
Semantics-preserving transformations on L2 IR: layer fusion (`linear → act` becomes `fused_linear_act`), redundant norm elimination, checkpoint selection for memory/compute tradeoff, architecture search pruning of dominated candidates.
### Trace — MNIST at L2
```
methods for classify {
sample: epoch_shuffle(batch: 128)
infer: sequential [
linear(784, 256), norm(layer), gelu,
linear(256, 256), norm(layer), gelu,
linear(256, 10), softmax
]
backward: autodiff(mode: reverse)
update: schedule(warmup: 1000, decay: cosine,
inner: adam(lr: 3e-4))
}
```
Search chose this from L3 constraints: 3-layer sequential (from `depth(3)`), widths 256 (under `width(512)`), ~200K params (under `param_budget(500K)`). `softmax` output satisfies `dist(10)`.
---
## L2 → L1 Lowering
Given an L2 method and a hardware description, produce a valid L1 placement. Search decides: which compute goes on which node, how to partition across accelerators, pipeline depth, where to stage data, whether to fuse or split stages.
Cost model uses hardware specs: transfer time = data_size / bandwidth, compute time = FLOPs / throughput, memory pressure = peak live tensors, pipeline bubble ratio = (depth − 1) / (depth × stages).
If placement search explodes, either the hardware description is too ambiguous or the L2 method has too many equivalent decompositions.
Three things happen during this lowering that don't happen at other boundaries:
1. **Backward expansion.** If L2 declares `autodiff(mode: reverse)`, the lowering derives the backward pass and writes it as explicit tensor ops in the L1 tensor graph. At L1, no operation is implicit.
2. **Update materialization.** L2's `adam(lr, beta1, beta2, eps)` becomes an explicit actor handler with tensor arithmetic at L1.
3. **Data binding.** L3's abstract data declarations gain physical storage locations (`@ ssd_0, path: "..."`).
---
## L1: Placement
### Purpose
Map the L2 method onto specific hardware. This is the layer where the systems-level DSL lives. It answers two questions: **where** does each computation live (topology + placement), and **when** does it execute (schedule + dataflow).
Six declaration forms: system, data, actor, stage, tensor_graph, pipeline. Plus a schedule block.
### DSL Forms — Topology
```
system <n> {
node <id> { kind: <storage|memory|compute>, <properties> }
...
edges {
<id> -> <id> : bandwidth <rate> // unidirectional
<id> <-> <id> : bandwidth <rate> // bidirectional
}
}
```
Node kinds:
- `storage` — persistent (SSD, disk). Source of data.
- `memory` — volatile (RAM, HBM). Has `capacity`.
- `compute` — processing (CPU, GPU). Has `isa`, and optionally `vram`, `cores`, `sm`, etc.
### DSL Forms — Data Sources
```
data <n> {
<field> : <domain> @ <storage_node>, path: <string>
...
}
```
Binds L3's abstract data declarations to physical storage. The `@` annotation names the topology node where the data lives at rest.
### DSL Forms — Actors
Actors own mutable state. They process messages one at a time — serialized, no concurrent access to their state.
```
actor <n> on <node> {
state {
<field> : <type>, <init>
...
}
window <n> : (<fields>)
readers(<stage_or_graph_refs>)
on <MessageType>(<args>) {
<update logic>
}
}
```
**Windows** grant read-only access to actor state from stages or tensor graphs. The actor's message handler is the only writer. Safety requires that readers and writers never execute concurrently — enforced by data dependency chains within a step and actor serialization across steps.
### DSL Forms — Stages
Stages are stateless dataflow processors. They receive buffers, do work, and forward results. Every buffer received must be consumed exactly once — forwarded or freed. No exceptions.
```
stage <n> on <node> {
reads: <refs> // optional window reads
fn process(<args>) -> forward(<target>, <outputs>)
| Drop(<buf>) {
<body>
}
}
```
Buffer types carry region provenance: `buf<ram_0>`, `buf<gpu_0>`. Every forwarding site is checked: target exists, arity matches, types match, region provenance matches the target's placement.
### DSL Forms — Tensor Graphs
Lazy DAGs of primitive tensor ops. Run on accelerators. No control flow, no mutation, no side effects — pure compute.
```
tensor_graph <n> on <node> {
reads: <window_refs>
fn compute(<inputs>) -> (<outputs>) {
let t0 = tensor_matmul(x, w)
let t1 = tensor_add(t0, tensor_broadcast(b, shape))
...
return (loss, gw, gb)
}
}
```
The `reads` clause grants access to actor state via a window — same mechanism as stages. The body is a sequence of `let` bindings over primitive tensor ops. No autodiff — both forward and backward are explicit. Movement ops (reshape, permute, broadcast) are free — metadata only.
**Primitive tensor ops:**
```
tensor_matmul(a, b) matrix multiply
tensor_add(a, b) elementwise add
tensor_sub(a, b) elementwise subtract
tensor_mul(a, b) elementwise multiply
tensor_scale(a, scalar) scalar multiply
tensor_broadcast(a, shape) expand dimensions
tensor_transpose(a) transpose
tensor_reduce_mean(a) reduce mean (all axes)
tensor_reduce_sum(a, axis: n) reduce sum on axis
tensor_const(v) scalar constant
```
### DSL Forms — Pipelines
Ordered chains of stages and tensor graphs.
```
pipeline <n> {
<stage_or_graph> -> <stage_or_graph> -> ...
}
```
### DSL Forms — Schedule
```
schedule {
pipelines: [<refs>]
actors: [<refs>]
pipeline_depth: <n>
}
```
No explicit phases. Execution order is inferred from `forward()` edges — the dependency DAG. `pipeline_depth` controls overlap: at depth 2, step N+1's early stages can overlap with step N's late stages. The actor is the serialization fence between steps.
### IR
```
L1_IR = {
topology: Graph<NodeId, Edge> hardware graph
data_sources: Map<Name, DataDecl>
placement: Map<L2_NodeId, HW_NodeId> where each computation lives
actors: Map<Name, ActorDecl>
stages: Map<Name, StageDecl>
tensor_graphs: Map<Name, TensorDAG> lazy compute DAGs
pipelines: [Pipeline] ordered stage/graph chains
schedule: ScheduleDecl
hints: Map<Path, EscapeHatch>
}
```
Every cross-reference is resolved during parsing. `demux` reading `connection_manager.routes` becomes a pointer within the tree. If a name is unresolved, the parser rejects. If a syntax rule is violated, the parser rejects. Semantic validity is the verifier's job.
### Verification — Structural (Phase 1)
Reads the IR. Does **not** transform it. If it also rewrote things, we'd need to verify the verifier's transformations. Keeping it read-only keeps it auditable.
| Property | Check | Catches |
|----------------|-----------------------------|---------------------------------------------------------------|
| Safety | Topology consistency | Edges reference nonexistent nodes; no path for transfers |
| Safety | Ref resolution | Dangling window reads, missing forward targets |
| Safety | Forwarding signatures | Arity mismatch, type mismatch, region provenance mismatch |
| Safety | Window separation | Reader and writer may execute concurrently |
| Consistency | Tensor shape propagation | Mismatched shapes in tensor graph DAG |
| Consistency | Region provenance | `buf<ram_0>` forwarded to a `gpu_0` tensor graph |
| Liveness | Buffer conservation | Buffer allocated but never freed or forwarded |
| Liveness | Tensor reachability | DAG node neither consumed nor named as output |
| Resource bound | Persistent memory bounded | Actor state exceeds node capacity |
| Resource bound | DAG finiteness | Tensor graph or pipeline contains a cycle |
**Conservation check per stage:** build the control flow graph, walk every path tracking live `Buffer` resources. Every path must end with an empty live set. If any path ends with a live buffer, the verifier rejects with the specific path and buffer name.
**Separation check:** within a step, the data dependency chain ensures the window reader (tensor graph) completes before the writer (actor handler) fires. At `pipeline_depth > 1`, cross-step safety requires that step N's actor update completes before step N+1's reader begins — the actor serializes this.
### Optimization (IR→IR rewrites)
Stage fusion (merge adjacent stages on same node), pipeline rebalancing, tensor graph rewriting (fuse ops, eliminate copies), communication optimization (overlap compute and transfer), parallelism mapping (data/tensor/pipeline parallel as topology + placement rewrites).
### Trace — MNIST at L1 (abbreviated)
```
system single_gpu {
node ssd_0 { kind: storage }
node ram_0 { kind: memory, capacity: 32GB }
node cpu_0 { kind: compute, isa: x86_64, cores: 8 }
node gpu_0 { kind: compute, isa: cuda, vram: 24GB, sm: 84 }
edges {
ssd_0 -> ram_0 : bandwidth 2GB/s
ram_0 <-> cpu_0 : bandwidth 50GB/s
cpu_0 <-> gpu_0 : bandwidth 32GB/s
}
}
actor model_state on gpu_0 {
state {
w : tensor(f32, [784, 256]), init: kaiming
b : tensor(f32, [256]), init: zeros
}
window weights : (w, b)
readers(forward_backward)
on GradUpdate(gw, gb) {
let lr = 3e-4
w = tensor_sub(w, tensor_scale(gw, lr))
b = tensor_sub(b, tensor_scale(gb, lr))
}
}
stage load_data on cpu_0 { ... }
stage transfer_to_gpu on cpu_0 { ... }
tensor_graph forward_backward on gpu_0 {
reads: model_state.weights
fn compute(x, y) -> (loss, gw, gb) { ... }
}
stage transfer_grads on cpu_0 { ... }
pipeline train { load_data -> transfer_to_gpu -> forward_backward }
pipeline grad_return { transfer_grads }
schedule {
pipelines: [train, grad_return]
actors: [model_state]
pipeline_depth: 2
}
```
---
## L1 → L0 Lowering
Not search — **emission.** The verified L1 IR is lowered into L0 through three compiler passes, then split into two code paths.
### Pass 1: Memory Planning
Compiler-derived, not user-declared. Three strategies selected by lifetime analysis:
| Strategy | Use | Lifetime |
|----------------|--------------------------|----------------------------------|
| **Persistent** | Actor state (w, b) | Init → program end |
| **Arena** | Tensor intermediates | Graph start → graph end, bulk reset |
| **Pool** | Transfer staging buffers | alloc → free, freelist |
### Pass 2: Stream Inference
Each step's GPU work gets a stream. At `pipeline_depth: N`, N streams. Cross-stream dependencies become sync events.
```
stream_0: step N → K0, K1, ..., sgd_kernel
stream_1: step N+1 → K0, K1, ..., sgd_kernel
sync: event recorded after sgd_kernel on stream_0
waited before K0 on stream_1
```
### Pass 3: Kernel Scheduling & Fusion
Tensor graph DAG nodes are grouped into kernels. Fusion rules: elementwise ops fuse freely; reduction forces a kernel boundary; different iteration spaces cannot fuse.
### Dual Emission
The verified L1 IR splits into two code paths:
**Path A — Control.** Stages and actor handlers emit to **Control IR** (Machine IR). This is the CPU-side orchestration: data loading, transfers, message dispatch, pipeline coordination.
**Path B — Compute.** Tensor graphs emit to **Compute IR** (UOps). This is the accelerator-side math: matmuls, reductions, elementwise, fused kernels.
Both paths feed into the Phase 2 (concrete) verifier before codegen.
---
## L0: Machine
### Purpose
Fully explicit execution plan. Every buffer, every kernel, every stream, every byte. No decisions left. This is what actually runs.
### Control IR
Ten operations over named memory regions. SSA registers. No flat address space — every load and store names its region, so the backend knows all aliasing without analysis.
```
load.{sz} rgn[off] read from named region
store.{sz} rgn[off], val write to region (rw only)
hash val → reg deterministic hash for map indexing
arith {op} a, b → reg add, sub, mul, and, or, shift
cmp {op} a, b → reg eq, neq, lt, gt → 1-bit result
branch reg → blk, blk conditional jump
jump → blk unconditional jump
queue_push rgn, val push descriptor to queue
queue_pop rgn → reg pop descriptor from queue
terminate end of execution unit
```
**Regions** are named, sized, access-controlled memory. Not a flat address space — specific bounded things. This preserves aliasing knowledge all the way to codegen. A flat address space throws this away and forces the backend to re-derive it.
Why not target C? C implies abstractions we don't use: a call stack (we don't recurse), a general pointer model (we have named regions), function boundaries (we inline everything). The C compiler then has to *re-discover* structure we already know. It may fail and generate worse code. More importantly, C is a black box: we lose visibility into what actually executes.
### Compute IR (UOps)
Atomic operations inside a kernel. Each is a typed node `(op, dtype, src[], arg)` forming a DAG rooted at STORE nodes.
| Category | Ops |
|--------------|------------------------------------------------------|
| Memory | `LOAD` `STORE` `INDEX` |
| Arithmetic | `ADD` `MUL` `MULACC` `NEG` `RECIPROCAL` `MAX` `MOD` |
| Compare | `CMPLT` `CMPNE` `WHERE` |
| Transcendent | `EXP2` `LOG2` `SIN` `SQRT` |
| Control | `RANGE` `END` `IF` `ENDIF` `BARRIER` |
| Type | `CONST` `CAST` `BITCAST` `VECTORIZE` `GEP` |
| Special | `WMMA` `REDUCE` `CUSTOM` |
~25 ops. Complete for any dense numeric kernel. `CUSTOM` covers sparse, scatter, and target-specific intrinsics.
### IR
```
L0_IR = {
memory_plan: Map<BufferId, MemorySlot>
kernel_plan: [KernelDecl]
stream_plan: Map<StreamId, [KernelId]>
sync_plan: [SyncPoint]
transfer_plan: [Transfer]
init_plan: [InitOp]
control_blocks: [Block] Machine IR blocks
}
KernelDecl = {
id: KernelId
inputs: [BufferId]
outputs: [BufferId]
ops: [UOp] the kernel body
grid: (x, y, z)
block: (x, y, z)
shared_mem: bytes
}
MemorySlot = {
pool: PoolId which physical memory
offset: bytes
size: bytes
lifetime: (first_use, last_use)
}
```
### Verification — Concrete (Phase 2)
After memory planning, stream inference, and kernel scheduling. Pure read-only checks on the concrete plan.
| Property | Check | Catches |
|----------------|----------------------------|--------------------------------------------------------|
| Safety | Kernel correctness | UOp sequences don't type-check, out-of-bounds access |
| Safety | Buffer lifetime safety | Overlapping writes to same memory slot |
| Safety | Stream hazard check | Shared mutable region + different streams, no sync |
| Liveness | Sync completeness | Cross-stream dependency missing a barrier |
| Resource bound | Memory capacity | Sum of live slots > pool capacity at any time t |
| Resource bound | Pool exhaustion | No headroom for transient allocations at pipeline depth |
### Optimization (IR→IR rewrites)
Closest to the metal. Most aggressive. Kernel fusion (merge adjacent kernels sharing buffers), memory packing (minimize peak by reordering lifetimes), launch config tuning (grid/block for occupancy), instruction selection (UOp → target ISA), register pressure management, prefetch insertion.
---
## Codegen
L0 IR → target code. This is the only target-dependent phase in the entire compiler.
### Control Path
Machine IR → target backend. The 10 ops translate mechanically.
| Backend | Characteristics |
|------------|--------------------------------------------------------------------------------|
| **x86-64** | Each op → 1–3 instructions. ~5 live values, no spills. Regions → register pointers. |
| **LLVM** | Regions become pointer arguments with `!noalias` metadata. LLVM gets perfect aliasing. |
| **eBPF** | Machine IR's restrictions (no loops, bounded access, linear resources) already satisfy the eBPF verifier. Nearly 1:1. |
| **FPGA** | Each block → combinational pipeline stage. Regions → BRAM ports. `queue_push` → FIFO write-enable. `terminate` → valid signal. |
### Compute Path
UOps → target ISA. Three passes:
1. **Instruction selection** — maps UOps to target ISA. `MULACC` → `fma.rn.f32` on PTX, `vfmadd` on x86.
2. **Register allocation** — handles spill/fill and manages pressure per-target. GPU has thousands of registers (occupancy-sensitive), CPU has 16 GPRs (spill-heavy).
3. **ISA emission** — produces final text or binary. Warp divergence on GPU means `IF/ENDIF` UOps have different cost profiles than on CPU. Vectorization width is target-dependent.
### Bounded Complexity
Codegen is deferred complexity, not buried complexity. The contract is clear: L0 IR is target-independent. Codegen is the *only* target-dependent phase. The UOp set is small enough that adding a new backend means writing ~25 emission rules plus register allocation for that target. This is a bounded, mechanical task — not an open research problem. The Machine IR is similarly small: 10 ops, each mapping to a handful of target instructions.
---
## Linking & Runtime
### Binary Structure
One binary. CPU object + accelerator blobs + metadata.
```
.text schedule_loop, stage_pipeline, actor_dispatch, platform_primitives
.rodata kernel cubins, memory_layout_table, stream_descriptors, schedule_descriptor
.data pool_metadata, arena_metadata
.bss gpu_context, stream_handles, event_handles
```
### Platform Primitives
Hand-written per platform. The generated code calls into these. The emitter produces control flow + kernels. The runtime provides the operations.
| Primitive | Linux + CUDA |
|---------------------|-----------------------------------|
| `region_alloc` | Pool: atomic pop. Arena: bump. |
| `region_free` | Pool: atomic push. Arena: no-op. |
| `dma_ssd_to_ram` | `io_uring` / mmap+prefault |
| `pcie_dma_h2d` | `cuMemcpyHtoDAsync` |
| `pcie_dma_d2h` | `cuMemcpyDtoHAsync` |
| `gpu_dispatch` | `cuLaunchKernel` |
| `gpu_sync` | `cuStreamSynchronize` |
| `gpu_event_*` | `cuEventRecord / cuStreamWaitEvent` |
| `queue_push/pop` | Ring buffer in shared memory |
Machine IR and kernel descriptors are target-independent. Only backends and primitive implementations change per platform.
### Runtime Error Policy
All correctness errors caught at compile time by the two-phase verifier. The binary assumes all static proofs hold. Hardware faults crash the process — outside scope of generated code.
---
## Escape Hatches
At any layer, annotate with `#[<target_layer>::<directive>]`:
```
// At L3: force a method choice
model classify {
...
#[l2::backward(evolution(pop: 50))]
}
// At L2: force a placement
infer: sequential [
linear(784, 256),
#[l1::on(gpu_1)]
linear(256, 10)
]
// At L2: force a kernel
infer: sequential [
linear(784, 256),
#[l0::kernel("fused_gelu.ptx")]
...
]
```
The escape hatch **must pass verification at its target layer.** You can inline PTX, but it must have the right buffer types. The compiler checks the boundary. If it fails, the error points at the annotation — the user broke the contract.
An escape hatch is **opaque to optimization** at layers above its target. `#[l0::kernel(...)]` cannot be fused with adjacent ops at L1. It's a black box with a typed boundary. This is the price of escaping.
---
## Search Architecture
Every lowering boundary uses the same pattern:
```
lower(source_ir, target_constraints) -> Result<target_ir, SearchFailure>
1. enumerate(source_ir, target_grammar) -> [candidate]
2. filter(candidates, verify_target) -> [valid]
3. score(valid, cost_model_target) -> [(candidate, cost)]
4. select(scored) -> target_ir | SearchFailure
SearchFailure =
NoCandidates(reason) constraints too tight
| TooManyCandidates(count) representation too loose
| Timeout(budget_exhausted) problem is genuinely hard
```
SearchFailure is a diagnostic, not an error. It tells you what to fix: tighten constraints, loosen them, or redesign the layer boundary.
---
## Data Ingestion
Data flows from storage to device memory before any kernel touches it:
```
Storage → decode (CPU) → Host Memory → batch → Staging → DMA → Device
```
The contract boundaries are clean: L2 says *what* to sample (epoch shuffle, curriculum, etc.), L1 says *where* to stage it (which memory pool, which node), L0 says *how* bytes move (DMA ops, transfer plan). Format-specific decoders (JPEG, video, custom binary) are pluggable at the L1/L0 boundary — they're `CUSTOM` UOps or host-side init ops.
---
## Open Questions
**1. Recurrence and variable-length.** Current stance: everything finite, `until(pred)` is spec error without a proven bound. But transformers process variable-length sequences. Is `sequence(domain, max_len)` at L3 enough? Or do we need a richer notion?
**2. Multi-model composition.** GAN = generator + discriminator. Distillation = teacher + student. RL = policy + value + environment. L3 currently has one model per spec. Do we want multiple models with relationships, or a separate composition layer?
**3. Training loop structure.** L3 says `goal: minimize(loss)`. Real training has phases — pretrain, finetune, RLHF. Epochs, checkpointing, evaluation. Probably L2 as a `schedule`, but it needs design.
**4. Cost model grounding.** L2 cost model is heuristic. L1 uses hardware specs. Neither is calibrated. Do we want empirical calibration — run a few kernels, measure, update?
**5. Incremental compilation.** If you change one loss term at L3, do you re-search all of L2? Probably: hash each IR node, re-lower only the subgraph whose inputs changed.
**6. The host language.** Rust for the compiler (algebraic types = IR correctness for free). But the DSL parser, the REPL, the interactive experience — Python bindings? LSP?
---
## What Is Resolved
- Layer count and purpose (4 layers, clear separation of concerns)
- L1 is the systems-level DSL: actors, stages, pipelines, windows, tensor graphs, topology
- Dual emission from L1: stages/actors → Control IR, tensor graphs → Compute IR (UOps)
- Two-phase verification: structural (pre-scheduling, at L1) and concrete (post-scheduling, at L0)
- Verification at every layer, classified by four property kinds
- Named memory regions in Control IR — aliasing known to codegen without analysis
- Memory planning as a compiler pass (persistent, arena, pool), not user-declared
- Stream inference from pipeline_depth + placement
- Escape hatch semantics (typed, opaque, verified at target)
- Codegen contract (target-independent L0 IR, target-dependent emission only)
- Data ingestion contract (L2 what, L1 where, L0 how)
- Search architecture (uniform enumerate → filter → score → select at every boundary)
- Platform primitives as the narrow runtime interface
- No autodiff at L1 — backward is explicit; autodiff is an L2 method expanded during lowering
- Dependency-inferred scheduling — no user-specified phases

1246
src/codegen.rs Normal file

File diff suppressed because it is too large Load diff

770
src/emit.rs Normal file
View file

@ -0,0 +1,770 @@
use crate::l0_ir::*;
use crate::l1_ir::*;
use std::collections::HashMap;
/// Errors during L1 → L0 emission.
#[derive(Debug, Clone, PartialEq)]
pub struct EmitError {
pub message: String,
}
impl std::fmt::Display for EmitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "emit error: {}", self.message)
}
}
impl std::error::Error for EmitError {}
/// Tracks SSA register allocation during emission.
struct RegAlloc {
next: u32,
}
impl RegAlloc {
fn new() -> Self {
RegAlloc { next: 0 }
}
fn alloc(&mut self) -> Register {
let r = Register(self.next);
self.next += 1;
r
}
}
/// Context for emission — holds lookup tables derived from L1 IR.
struct EmitContext<'a> {
actors: HashMap<&'a str, &'a ActorDecl>,
leaves: HashMap<&'a str, &'a LeafDecl>,
}
impl<'a> EmitContext<'a> {
fn new(program: &'a L1Program) -> Self {
let actors = program.actors.iter().map(|a| (a.name.as_str(), a)).collect();
let leaves = program.leaves.iter().map(|l| (l.name.as_str(), l)).collect();
EmitContext { actors, leaves }
}
}
/// Lower a verified L1 program to L0 IR.
///
/// For each core, produces an L0Program with:
/// - Named memory regions (persistent state, message queues, step counter, output buffer)
/// - Control blocks (entry, loop_check, step, exit)
///
/// Currently emits for the first core only (single-core MVP).
pub fn emit(program: &L1Program) -> Result<L0Program, EmitError> {
if program.cores.is_empty() {
return Err(EmitError {
message: "no cores defined".to_string(),
});
}
let ctx = EmitContext::new(program);
let core = &program.cores[0];
// Pass 1: Memory planning — allocate regions
let mut regions = Vec::new();
let mut actor_state_regions: HashMap<&str, String> = HashMap::new();
let mut actor_field_offsets: HashMap<(&str, &str), usize> = HashMap::new();
let mut queue_regions: HashMap<&str, String> = HashMap::new();
// Allocate state regions for each actor
for actor_name in &core.actors {
let actor = ctx.actors.get(actor_name.as_str()).ok_or_else(|| EmitError {
message: format!("actor '{}' not found", actor_name),
})?;
let region_name = format!("{}_state", actor.name);
let mut offset = 0usize;
for field in &actor.state {
let size = field_type_size(&field.ty);
actor_field_offsets.insert((actor.name.as_str(), field.name.as_str()), offset);
offset += size;
}
let total_size = offset.max(8); // At least 8 bytes
regions.push(Region {
name: region_name.clone(),
size: total_size,
access: AccessMode::ReadWrite,
kind: RegionKind::State,
});
actor_state_regions.insert(actor.name.as_str(), region_name);
}
// Allocate queue regions for message passing between leaves and actors
for actor_name in &core.actors {
let actor = ctx.actors.get(actor_name.as_str()).unwrap();
for handler in &actor.handlers {
let region_name = format!("{}_{}_queue", actor.name, handler.message_type.to_lowercase());
// Queue region: ring buffer. Each message arg is 8 bytes, plus head/tail pointers (16 bytes).
let msg_size = handler.args.len() * 8;
let queue_capacity = 16; // Support up to 16 messages
let queue_size = 16 + msg_size * queue_capacity; // 16 bytes for head+tail, rest for data
regions.push(Region {
name: region_name.clone(),
size: queue_size,
access: AccessMode::ReadWrite,
kind: RegionKind::Queue,
});
queue_regions.insert(actor.name.as_str(), region_name);
}
}
// Step counter region
let step_counter_region = "step_counter".to_string();
regions.push(Region {
name: step_counter_region.clone(),
size: 8,
access: AccessMode::ReadWrite,
kind: RegionKind::Control,
});
// Check if any leaf uses emit — if so, allocate output region
let has_output = core.leaves.iter().any(|leaf_name| {
if let Some(leaf) = ctx.leaves.get(leaf_name.as_str()) {
leaf.actions.iter().any(|a| matches!(a, LeafAction::Emit(_)))
} else {
false
}
});
let output_region = if has_output {
let name = "output".to_string();
// Output buffer uses queue layout: 16 bytes for head+tail, then 8 bytes per slot
let size = 16 + (core.steps as usize) * 8;
regions.push(Region {
name: name.clone(),
size,
access: AccessMode::ReadWrite,
kind: RegionKind::Output,
});
Some(name)
} else {
None
};
// Pass 2: Control block generation
let mut regs = RegAlloc::new();
let mut blocks = Vec::new();
// --- Entry block: initialize all regions ---
let mut entry_ops = Vec::new();
let zero = regs.alloc();
entry_ops.push(Op::LoadImm { value: 0, dest: zero });
// Initialize actor state fields
for actor_name in &core.actors {
let actor = ctx.actors.get(actor_name.as_str()).unwrap();
let state_region = actor_state_regions.get(actor.name.as_str()).unwrap();
for field in &actor.state {
let offset = actor_field_offsets[&(actor.name.as_str(), field.name.as_str())];
if field.init == 0 {
entry_ops.push(Op::Store {
size: OpSize::B64,
region: state_region.clone(),
offset,
src: zero,
});
} else {
let init_reg = regs.alloc();
entry_ops.push(Op::LoadImm { value: field.init, dest: init_reg });
entry_ops.push(Op::Store {
size: OpSize::B64,
region: state_region.clone(),
offset,
src: init_reg,
});
}
}
}
// Initialize step counter to 0
entry_ops.push(Op::Store {
size: OpSize::B64,
region: step_counter_region.clone(),
offset: 0,
src: zero,
});
// Initialize output count to 0 if output region exists
if let Some(ref out_rgn) = output_region {
entry_ops.push(Op::Store {
size: OpSize::B64,
region: out_rgn.clone(),
offset: 0,
src: zero,
});
}
entry_ops.push(Op::Jump { target: "loop_check".to_string() });
blocks.push(Block { name: "entry".to_string(), ops: entry_ops });
// --- Loop check block: compare step counter to bound ---
let mut check_ops = Vec::new();
let step_val = regs.alloc();
check_ops.push(Op::Load {
size: OpSize::B64,
region: step_counter_region.clone(),
offset: 0,
dest: step_val,
});
let limit = regs.alloc();
check_ops.push(Op::LoadImm { value: core.steps, dest: limit });
let cmp_result = regs.alloc();
check_ops.push(Op::Cmp {
kind: CmpKind::Lt,
lhs: step_val,
rhs: limit,
dest: cmp_result,
});
check_ops.push(Op::Branch {
cond: cmp_result,
true_target: "step".to_string(),
false_target: "exit".to_string(),
});
blocks.push(Block { name: "loop_check".to_string(), ops: check_ops });
// --- Step block: encode pipeline execution ---
let mut step_ops = Vec::new();
// Process pipelines in order. For each pipeline, walk the stages
// and generate ops according to the dataflow.
for pipe_name in &core.pipelines {
let pipeline = program.pipelines.iter()
.find(|p| p.name == *pipe_name)
.ok_or_else(|| EmitError {
message: format!("pipeline '{}' not found", pipe_name),
})?;
for stage_name in &pipeline.stages {
// Is it a leaf?
if let Some(leaf) = ctx.leaves.get(stage_name.as_str()) {
emit_leaf_actions(
leaf,
&ctx,
&mut step_ops,
&mut regs,
&actor_state_regions,
&actor_field_offsets,
&queue_regions,
&output_region,
);
}
// Is it an actor? Process message handlers (pop from queue, dispatch)
else if let Some(actor) = ctx.actors.get(stage_name.as_str()) {
emit_actor_dispatch(
actor,
&mut step_ops,
&mut regs,
&actor_state_regions,
&actor_field_offsets,
&queue_regions,
);
}
}
}
// Increment step counter
let cur_step = regs.alloc();
step_ops.push(Op::Load {
size: OpSize::B64,
region: step_counter_region.clone(),
offset: 0,
dest: cur_step,
});
let next_step = regs.alloc();
step_ops.push(Op::ArithImm {
kind: ArithKind::Add,
lhs: cur_step,
imm: 1,
dest: next_step,
});
step_ops.push(Op::Store {
size: OpSize::B64,
region: step_counter_region.clone(),
offset: 0,
src: next_step,
});
step_ops.push(Op::Jump { target: "loop_check".to_string() });
blocks.push(Block { name: "step".to_string(), ops: step_ops });
// --- Exit block ---
blocks.push(Block {
name: "exit".to_string(),
ops: vec![Op::Terminate],
});
Ok(L0Program { regions, blocks })
}
/// Emit ops for a leaf's actions.
fn emit_leaf_actions(
leaf: &LeafDecl,
_ctx: &EmitContext,
ops: &mut Vec<Op>,
regs: &mut RegAlloc,
actor_state_regions: &HashMap<&str, String>,
actor_field_offsets: &HashMap<(&str, &str), usize>,
queue_regions: &HashMap<&str, String>,
output_region: &Option<String>,
) {
for action in &leaf.actions {
match action {
LeafAction::Forward { target, args, .. } => {
// Emit each argument value and push to the target actor's queue
if let Some(queue_rgn) = queue_regions.get(target.as_str()) {
for arg in args {
let val_reg = emit_expr(arg, ops, regs);
ops.push(Op::QueuePush {
region: queue_rgn.clone(),
src: val_reg,
});
}
}
}
LeafAction::ReadWindow { actor, field, .. } => {
// Load from actor state region through the window
if let Some(state_rgn) = actor_state_regions.get(actor.as_str()) {
let offset = actor_field_offsets
.get(&(actor.as_str(), field.as_str()))
.copied()
.unwrap_or(0);
let _val = regs.alloc();
ops.push(Op::Load {
size: OpSize::B64,
region: state_rgn.clone(),
offset,
dest: _val,
});
}
}
LeafAction::Emit(expr) => {
if let Some(ref out_rgn) = output_region {
// Evaluate the emit expression
let val_reg = emit_leaf_emit_expr(
expr,
ops,
regs,
actor_state_regions,
actor_field_offsets,
&leaf.reads,
);
// Push value to output region (queue layout: head at [0] tracks count)
ops.push(Op::QueuePush {
region: out_rgn.clone(),
src: val_reg,
});
}
}
}
}
}
/// Emit ops for an actor's message handler dispatch.
fn emit_actor_dispatch(
actor: &ActorDecl,
ops: &mut Vec<Op>,
regs: &mut RegAlloc,
actor_state_regions: &HashMap<&str, String>,
actor_field_offsets: &HashMap<(&str, &str), usize>,
queue_regions: &HashMap<&str, String>,
) {
let state_rgn = match actor_state_regions.get(actor.name.as_str()) {
Some(r) => r,
None => return,
};
let queue_rgn = match queue_regions.get(actor.name.as_str()) {
Some(r) => r,
None => return,
};
for handler in &actor.handlers {
// Pop arguments from queue
let mut arg_regs: HashMap<&str, Register> = HashMap::new();
for (arg_name, _) in &handler.args {
let reg = regs.alloc();
ops.push(Op::QueuePop {
region: queue_rgn.clone(),
dest: reg,
});
arg_regs.insert(arg_name.as_str(), reg);
}
// Execute handler body
emit_handler_body(
&handler.body,
ops,
regs,
state_rgn,
actor.name.as_str(),
actor_field_offsets,
&arg_regs,
);
}
}
/// Emit ops for a handler body expression.
fn emit_handler_body(
expr: &Expr,
ops: &mut Vec<Op>,
regs: &mut RegAlloc,
state_region: &str,
actor_name: &str,
field_offsets: &HashMap<(&str, &str), usize>,
arg_regs: &HashMap<&str, Register>,
) {
match expr {
Expr::Assign(target, value) => {
let val_reg = emit_handler_expr(value, ops, regs, state_region, actor_name, field_offsets, arg_regs);
if let Some(&offset) = field_offsets.get(&(actor_name, target.as_str())) {
ops.push(Op::Store {
size: OpSize::B64,
region: state_region.to_string(),
offset,
src: val_reg,
});
}
}
Expr::Block(exprs) => {
for e in exprs {
emit_handler_body(e, ops, regs, state_region, actor_name, field_offsets, arg_regs);
}
}
_ => {
// Expression with side effects — evaluate for value
emit_handler_expr(expr, ops, regs, state_region, actor_name, field_offsets, arg_regs);
}
}
}
/// Emit ops for a handler expression, returning the register holding the result.
fn emit_handler_expr(
expr: &Expr,
ops: &mut Vec<Op>,
regs: &mut RegAlloc,
state_region: &str,
actor_name: &str,
field_offsets: &HashMap<(&str, &str), usize>,
arg_regs: &HashMap<&str, Register>,
) -> Register {
match expr {
Expr::Lit(val) => {
let r = regs.alloc();
ops.push(Op::LoadImm { value: *val, dest: r });
r
}
Expr::Var(name) => {
// Check if it's a handler argument
if let Some(&reg) = arg_regs.get(name.as_str()) {
return reg;
}
// Otherwise it's a state field — load it
if let Some(&offset) = field_offsets.get(&(actor_name, name.as_str())) {
let r = regs.alloc();
ops.push(Op::Load {
size: OpSize::B64,
region: state_region.to_string(),
offset,
dest: r,
});
r
} else {
// Unknown variable — emit a load from offset 0 as fallback
let r = regs.alloc();
ops.push(Op::LoadImm { value: 0, dest: r });
r
}
}
Expr::Arith(op, lhs, rhs) => {
let l = emit_handler_expr(lhs, ops, regs, state_region, actor_name, field_offsets, arg_regs);
let r = emit_handler_expr(rhs, ops, regs, state_region, actor_name, field_offsets, arg_regs);
let dest = regs.alloc();
let kind = match op {
ArithOp::Add => ArithKind::Add,
ArithOp::Sub => ArithKind::Sub,
ArithOp::Mul => ArithKind::Mul,
};
ops.push(Op::Arith { kind, lhs: l, rhs: r, dest });
dest
}
Expr::Assign(target, value) => {
let val_reg = emit_handler_expr(value, ops, regs, state_region, actor_name, field_offsets, arg_regs);
if let Some(&offset) = field_offsets.get(&(actor_name, target.as_str())) {
ops.push(Op::Store {
size: OpSize::B64,
region: state_region.to_string(),
offset,
src: val_reg,
});
}
val_reg
}
Expr::Block(exprs) => {
let mut last_reg = regs.alloc();
ops.push(Op::LoadImm { value: 0, dest: last_reg });
for e in exprs {
last_reg = emit_handler_expr(e, ops, regs, state_region, actor_name, field_offsets, arg_regs);
}
last_reg
}
}
}
/// Emit a simple literal or variable expression for a leaf's forward arguments.
fn emit_expr(expr: &Expr, ops: &mut Vec<Op>, regs: &mut RegAlloc) -> Register {
match expr {
Expr::Lit(val) => {
let r = regs.alloc();
ops.push(Op::LoadImm { value: *val, dest: r });
r
}
Expr::Var(_) => {
// Leaves are stateless — variables in forward args should only be literals
let r = regs.alloc();
ops.push(Op::LoadImm { value: 0, dest: r });
r
}
Expr::Arith(op, lhs, rhs) => {
let l = emit_expr(lhs, ops, regs);
let r = emit_expr(rhs, ops, regs);
let dest = regs.alloc();
let kind = match op {
ArithOp::Add => ArithKind::Add,
ArithOp::Sub => ArithKind::Sub,
ArithOp::Mul => ArithKind::Mul,
};
ops.push(Op::Arith { kind, lhs: l, rhs: r, dest });
dest
}
_ => {
let r = regs.alloc();
ops.push(Op::LoadImm { value: 0, dest: r });
r
}
}
}
/// Emit expression for a leaf's emit action. Handles variable references that
/// may refer to window-read state fields.
fn emit_leaf_emit_expr(
expr: &Expr,
ops: &mut Vec<Op>,
regs: &mut RegAlloc,
actor_state_regions: &HashMap<&str, String>,
actor_field_offsets: &HashMap<(&str, &str), usize>,
reads: &Option<ReadClause>,
) -> Register {
match expr {
Expr::Lit(val) => {
let r = regs.alloc();
ops.push(Op::LoadImm { value: *val, dest: r });
r
}
Expr::Var(name) => {
// If the leaf has a reads clause, the variable might refer to a field
// read through the window. Look it up in the actor's state.
if let Some(reads) = reads {
if let Some(state_rgn) = actor_state_regions.get(reads.actor.as_str()) {
if let Some(&offset) = actor_field_offsets.get(&(reads.actor.as_str(), name.as_str())) {
let r = regs.alloc();
ops.push(Op::Load {
size: OpSize::B64,
region: state_rgn.clone(),
offset,
dest: r,
});
return r;
}
}
}
let r = regs.alloc();
ops.push(Op::LoadImm { value: 0, dest: r });
r
}
Expr::Arith(op, lhs, rhs) => {
let l = emit_leaf_emit_expr(lhs, ops, regs, actor_state_regions, actor_field_offsets, reads);
let r = emit_leaf_emit_expr(rhs, ops, regs, actor_state_regions, actor_field_offsets, reads);
let dest = regs.alloc();
let kind = match op {
ArithOp::Add => ArithKind::Add,
ArithOp::Sub => ArithKind::Sub,
ArithOp::Mul => ArithKind::Mul,
};
ops.push(Op::Arith { kind, lhs: l, rhs: r, dest });
dest
}
_ => {
let r = regs.alloc();
ops.push(Op::LoadImm { value: 0, dest: r });
r
}
}
}
/// Return the size in bytes of a field type.
fn field_type_size(ty: &FieldType) -> usize {
match ty {
FieldType::U64 | FieldType::I64 | FieldType::F64 => 8,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::l1_ir::tests::{build_counter_program, build_window_program};
#[test]
fn emit_counter_produces_regions_and_blocks() {
let program = build_counter_program();
let l0 = emit(&program).expect("emission should succeed");
// Should have regions: counter_state, increment_queue, step_counter
assert!(l0.regions.iter().any(|r| r.name == "counter_state" && r.kind == RegionKind::State));
assert!(l0.regions.iter().any(|r| r.kind == RegionKind::Queue));
assert!(l0.regions.iter().any(|r| r.name == "step_counter" && r.kind == RegionKind::Control));
// Should have 4 blocks: entry, loop_check, step, exit
assert_eq!(l0.blocks.len(), 4);
assert_eq!(l0.blocks[0].name, "entry");
assert_eq!(l0.blocks[1].name, "loop_check");
assert_eq!(l0.blocks[2].name, "step");
assert_eq!(l0.blocks[3].name, "exit");
// Exit block has terminate
assert!(l0.blocks[3].ops.iter().any(|op| matches!(op, Op::Terminate)));
}
#[test]
fn emit_counter_has_explicit_ops() {
let program = build_counter_program();
let l0 = emit(&program).expect("emission should succeed");
let text = format!("{}", l0);
// Verify all expected control IR ops appear
assert!(text.contains("load.64"), "missing load ops");
assert!(text.contains("store.64"), "missing store ops");
assert!(text.contains("cmp.lt"), "missing cmp op");
assert!(text.contains("branch"), "missing branch op");
assert!(text.contains("jump"), "missing jump op");
assert!(text.contains("queue_push"), "missing queue_push op");
assert!(text.contains("queue_pop"), "missing queue_pop op");
assert!(text.contains("terminate"), "missing terminate op");
assert!(text.contains("add"), "missing arith add op");
assert!(text.contains("imm #"), "missing load imm op");
}
#[test]
fn emit_counter_l0_display_is_readable() {
let program = build_counter_program();
let l0 = emit(&program).expect("emission should succeed");
let text = format!("{}", l0);
assert!(text.contains("=== Regions ==="));
assert!(text.contains("=== Blocks ==="));
assert!(text.contains("region counter_state"));
assert!(text.contains("step_counter"));
assert!(text.contains("entry:"));
assert!(text.contains("loop_check:"));
assert!(text.contains("step:"));
assert!(text.contains("exit:"));
}
#[test]
fn emit_window_produces_output_region() {
let program = build_window_program();
let l0 = emit(&program).expect("emission should succeed");
// Window example has emit actions, so output region should exist
assert!(l0.regions.iter().any(|r| r.name == "output" && r.kind == RegionKind::Output));
assert!(l0.regions.iter().any(|r| r.name == "accumulator_state" && r.kind == RegionKind::State));
}
#[test]
fn emit_window_has_queue_push_for_emit() {
let program = build_window_program();
let l0 = emit(&program).expect("emission should succeed");
let text = format!("{}", l0);
// The emit action should produce a queue_push to the output region
assert!(text.contains("queue_push output"), "emit should produce queue_push to output");
}
#[test]
fn emit_counter_matches_expected_structure() {
// Build expected L0 IR by hand and compare against emitted output
let program = build_counter_program();
let l0 = emit(&program).expect("emission should succeed");
// Verify region structure
assert_eq!(l0.regions.len(), 3); // state, queue, step_counter (no output for counter)
// Verify entry block initializes state to 0
let entry = &l0.blocks[0];
assert!(entry.ops.iter().any(|op| matches!(op, Op::LoadImm { value: 0, .. })));
assert!(entry.ops.iter().any(|op| matches!(op, Op::Store { region, .. } if region == "counter_state")));
assert!(entry.ops.iter().any(|op| matches!(op, Op::Store { region, .. } if region == "step_counter")));
assert!(entry.ops.last().unwrap() == &Op::Jump { target: "loop_check".to_string() });
// Verify loop_check compares step counter
let check = &l0.blocks[1];
assert!(check.ops.iter().any(|op| matches!(op, Op::Load { region, .. } if region == "step_counter")));
assert!(check.ops.iter().any(|op| matches!(op, Op::LoadImm { value: 5, .. })));
assert!(check.ops.iter().any(|op| matches!(op, Op::Cmp { kind: CmpKind::Lt, .. })));
assert!(check.ops.iter().any(|op| matches!(op, Op::Branch { true_target, false_target, .. }
if true_target == "step" && false_target == "exit")));
// Verify step block has queue_push (ticker forward), queue_pop (handler), arith, store
let step = &l0.blocks[2];
assert!(step.ops.iter().any(|op| matches!(op, Op::QueuePush { .. })));
assert!(step.ops.iter().any(|op| matches!(op, Op::QueuePop { .. })));
assert!(step.ops.iter().any(|op| matches!(op, Op::Arith { kind: ArithKind::Add, .. })));
// Step counter increment
assert!(step.ops.iter().any(|op| matches!(op, Op::ArithImm { kind: ArithKind::Add, imm: 1, .. })));
assert!(step.ops.last().unwrap() == &Op::Jump { target: "loop_check".to_string() });
// Verify exit block
assert_eq!(l0.blocks[3].ops, vec![Op::Terminate]);
}
#[test]
fn emit_from_parsed_counter() {
let input = include_str!("../examples/counter.l1");
let parsed = crate::parser::parse(input).expect("should parse");
crate::verify_l1::verify(&parsed).expect("should verify");
let l0 = emit(&parsed).expect("should emit");
assert!(l0.regions.iter().any(|r| r.name == "counter_state"));
assert_eq!(l0.blocks.len(), 4);
}
#[test]
fn emit_from_parsed_window() {
let input = include_str!("../examples/window.l1");
let parsed = crate::parser::parse(input).expect("should parse");
crate::verify_l1::verify(&parsed).expect("should verify");
let l0 = emit(&parsed).expect("should emit");
assert!(l0.regions.iter().any(|r| r.name == "accumulator_state"));
assert!(l0.regions.iter().any(|r| r.name == "output"));
assert_eq!(l0.blocks.len(), 4);
}
#[test]
fn emit_error_on_empty_program() {
let program = L1Program {
actors: vec![],
leaves: vec![],
pipelines: vec![],
cores: vec![],
};
let err = emit(&program).unwrap_err();
assert!(err.message.contains("no cores"));
}
}

556
src/interpret_l1.rs Normal file
View file

@ -0,0 +1,556 @@
//! L1 Reference Interpreter — executes L1 IR directly without lowering to L0.
//!
//! This is the *reference semantics* for L1: the specification of what L1 means.
//! The interpreter produces execution traces used by the translation validation
//! engine (Stage 2) to verify that L0 lowering preserves semantics.
use crate::l1_ir::*;
use std::collections::{HashMap, VecDeque};
use std::fmt;
/// A snapshot of one actor's state at a point in time.
#[derive(Debug, Clone, PartialEq)]
pub struct ActorSnapshot {
pub actor_name: String,
pub fields: Vec<(String, u64)>,
}
/// A message that was sent during execution.
#[derive(Debug, Clone, PartialEq)]
pub struct Message {
pub target: String,
pub message_type: String,
pub args: Vec<u64>,
}
/// A window read performed during execution.
#[derive(Debug, Clone, PartialEq)]
pub struct WindowRead {
pub actor: String,
pub window: String,
pub field: String,
pub value: u64,
}
/// A single step in the execution trace.
#[derive(Debug, Clone, PartialEq)]
pub struct TraceStep {
pub step_index: u64,
pub actor_states: Vec<ActorSnapshot>,
pub messages_sent: Vec<Message>,
pub window_reads: Vec<WindowRead>,
pub emitted_outputs: Vec<u64>,
}
/// The complete execution trace produced by the interpreter.
#[derive(Debug, Clone, PartialEq)]
pub struct ExecutionTrace {
pub steps: Vec<TraceStep>,
pub final_actor_states: Vec<ActorSnapshot>,
pub all_outputs: Vec<u64>,
}
/// Errors from the L1 interpreter.
#[derive(Debug, Clone)]
pub struct InterpretError {
pub message: String,
}
impl fmt::Display for InterpretError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "interpret error: {}", self.message)
}
}
impl std::error::Error for InterpretError {}
/// Runtime state for the interpreter.
struct InterpreterState {
/// Current actor field values: actor_name -> field_name -> value
actor_state: HashMap<String, HashMap<String, u64>>,
/// Message queues: actor_name -> queue of (message_type, args)
queues: HashMap<String, VecDeque<(String, Vec<u64>)>>,
/// Accumulated output values
outputs: Vec<u64>,
}
impl InterpreterState {
fn new(program: &L1Program) -> Self {
let mut actor_state = HashMap::new();
for actor in &program.actors {
let mut fields = HashMap::new();
for field in &actor.state {
fields.insert(field.name.clone(), field.init);
}
actor_state.insert(actor.name.clone(), fields);
}
let mut queues = HashMap::new();
for actor in &program.actors {
queues.insert(actor.name.clone(), VecDeque::new());
}
InterpreterState {
actor_state,
queues,
outputs: Vec::new(),
}
}
fn snapshot_actors(&self, program: &L1Program) -> Vec<ActorSnapshot> {
program.actors.iter().map(|actor| {
let fields = actor.state.iter().map(|f| {
let val = self.actor_state
.get(&actor.name)
.and_then(|s| s.get(&f.name))
.copied()
.unwrap_or(0);
(f.name.clone(), val)
}).collect();
ActorSnapshot {
actor_name: actor.name.clone(),
fields,
}
}).collect()
}
}
/// Interpret an L1 program directly, producing an execution trace.
///
/// Executes the first core's pipelines for the configured number of steps.
/// Each step walks pipeline stages in order: leaves forward messages,
/// actors dispatch handlers, windows are read, outputs are emitted.
pub fn interpret(program: &L1Program) -> Result<ExecutionTrace, InterpretError> {
if program.cores.is_empty() {
return Err(InterpretError {
message: "no cores defined".to_string(),
});
}
let core = &program.cores[0];
let actors: HashMap<&str, &ActorDecl> = program.actors.iter()
.map(|a| (a.name.as_str(), a))
.collect();
let leaves: HashMap<&str, &LeafDecl> = program.leaves.iter()
.map(|l| (l.name.as_str(), l))
.collect();
let mut state = InterpreterState::new(program);
let mut trace_steps = Vec::new();
for step_idx in 0..core.steps {
let mut messages_sent = Vec::new();
let mut window_reads = Vec::new();
let mut step_outputs = Vec::new();
// Walk each pipeline's stages in order
for pipe_name in &core.pipelines {
let pipeline = program.pipelines.iter()
.find(|p| p.name == *pipe_name)
.ok_or_else(|| InterpretError {
message: format!("pipeline '{}' not found", pipe_name),
})?;
for stage_name in &pipeline.stages {
if let Some(leaf) = leaves.get(stage_name.as_str()) {
// Execute leaf actions
execute_leaf(
leaf,
&actors,
&mut state,
&mut messages_sent,
&mut window_reads,
&mut step_outputs,
);
} else if let Some(actor) = actors.get(stage_name.as_str()) {
// Dispatch actor messages
execute_actor(actor, &mut state);
}
}
}
state.outputs.extend(&step_outputs);
trace_steps.push(TraceStep {
step_index: step_idx,
actor_states: state.snapshot_actors(program),
messages_sent,
window_reads,
emitted_outputs: step_outputs,
});
}
let final_states = state.snapshot_actors(program);
let all_outputs = state.outputs.clone();
Ok(ExecutionTrace {
steps: trace_steps,
final_actor_states: final_states,
all_outputs,
})
}
/// Execute a leaf's actions.
fn execute_leaf(
leaf: &LeafDecl,
_actors: &HashMap<&str, &ActorDecl>,
state: &mut InterpreterState,
messages_sent: &mut Vec<Message>,
window_reads: &mut Vec<WindowRead>,
step_outputs: &mut Vec<u64>,
) {
// Track values read through windows for use in emit expressions
let mut read_values: HashMap<String, u64> = HashMap::new();
for action in &leaf.actions {
match action {
LeafAction::Forward { target, message_type, args } => {
let arg_vals: Vec<u64> = args.iter()
.map(|e| eval_const_expr(e))
.collect();
// Enqueue message for the target actor
if let Some(queue) = state.queues.get_mut(target.as_str()) {
queue.push_back((message_type.clone(), arg_vals.clone()));
}
messages_sent.push(Message {
target: target.clone(),
message_type: message_type.clone(),
args: arg_vals,
});
}
LeafAction::ReadWindow { actor, window, field } => {
let value = state.actor_state
.get(actor.as_str())
.and_then(|s| s.get(field.as_str()))
.copied()
.unwrap_or(0);
read_values.insert(field.clone(), value);
window_reads.push(WindowRead {
actor: actor.clone(),
window: window.clone(),
field: field.clone(),
value,
});
}
LeafAction::Emit(expr) => {
let value = eval_emit_expr(expr, &read_values, &leaf.reads, &state.actor_state);
step_outputs.push(value);
}
}
}
}
/// Execute an actor's message handlers by popping from its queue.
fn execute_actor(actor: &ActorDecl, state: &mut InterpreterState) {
let queue = match state.queues.get_mut(actor.name.as_str()) {
Some(q) => q,
None => return,
};
// Process each handler — pop messages matching the handler's type
for handler in &actor.handlers {
if let Some((_msg_type, args)) = queue.pop_front() {
// Build variable environment: handler args + current state fields
let mut env: HashMap<String, u64> = HashMap::new();
// Bind handler arguments
for (i, (arg_name, _)) in handler.args.iter().enumerate() {
if i < args.len() {
env.insert(arg_name.clone(), args[i]);
}
}
// Bind current state field values
if let Some(fields) = state.actor_state.get(&actor.name) {
for (name, val) in fields {
env.insert(name.clone(), *val);
}
}
// Evaluate handler body
eval_handler_body(&handler.body, &mut env);
// Write back state fields that were assigned
if let Some(fields) = state.actor_state.get_mut(&actor.name) {
for (name, val) in fields.iter_mut() {
if let Some(&new_val) = env.get(name) {
*val = new_val;
}
}
}
}
}
}
/// Evaluate a constant expression (for leaf forward args — leaves are stateless).
fn eval_const_expr(expr: &Expr) -> u64 {
match expr {
Expr::Lit(v) => *v,
Expr::Arith(op, lhs, rhs) => {
let l = eval_const_expr(lhs);
let r = eval_const_expr(rhs);
apply_arith(*op, l, r)
}
_ => 0,
}
}
/// Evaluate an emit expression. Variables may refer to window-read fields.
fn eval_emit_expr(
expr: &Expr,
read_values: &HashMap<String, u64>,
reads: &Option<ReadClause>,
actor_state: &HashMap<String, HashMap<String, u64>>,
) -> u64 {
match expr {
Expr::Lit(v) => *v,
Expr::Var(name) => {
// First check if we have a value from a prior ReadWindow action
if let Some(&val) = read_values.get(name.as_str()) {
return val;
}
// Fall back to reading through the reads clause
if let Some(rc) = reads {
if let Some(fields) = actor_state.get(&rc.actor) {
if let Some(&val) = fields.get(name.as_str()) {
return val;
}
}
}
0
}
Expr::Arith(op, lhs, rhs) => {
let l = eval_emit_expr(lhs, read_values, reads, actor_state);
let r = eval_emit_expr(rhs, read_values, reads, actor_state);
apply_arith(*op, l, r)
}
_ => 0,
}
}
/// Evaluate a handler body expression, mutating the environment for assignments.
fn eval_handler_body(expr: &Expr, env: &mut HashMap<String, u64>) -> u64 {
match expr {
Expr::Lit(v) => *v,
Expr::Var(name) => env.get(name.as_str()).copied().unwrap_or(0),
Expr::Arith(op, lhs, rhs) => {
let l = eval_handler_body(lhs, env);
let r = eval_handler_body(rhs, env);
apply_arith(*op, l, r)
}
Expr::Assign(target, value) => {
let val = eval_handler_body(value, env);
env.insert(target.clone(), val);
val
}
Expr::Block(exprs) => {
let mut last = 0;
for e in exprs {
last = eval_handler_body(e, env);
}
last
}
}
}
/// Apply an arithmetic operation.
fn apply_arith(op: ArithOp, l: u64, r: u64) -> u64 {
match op {
ArithOp::Add => l.wrapping_add(r),
ArithOp::Sub => l.wrapping_sub(r),
ArithOp::Mul => l.wrapping_mul(r),
}
}
impl fmt::Display for ExecutionTrace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for step in &self.steps {
writeln!(f, "--- Step {} ---", step.step_index)?;
for msg in &step.messages_sent {
writeln!(f, " send {}({:?}) -> {}", msg.message_type, msg.args, msg.target)?;
}
for wr in &step.window_reads {
writeln!(f, " read {}.{}.{} = {}", wr.actor, wr.window, wr.field, wr.value)?;
}
for out in &step.emitted_outputs {
writeln!(f, " emit {}", out)?;
}
for snap in &step.actor_states {
let fields: Vec<String> = snap.fields.iter()
.map(|(n, v)| format!("{}={}", n, v))
.collect();
writeln!(f, " {} {{ {} }}", snap.actor_name, fields.join(", "))?;
}
}
writeln!(f, "--- Final ---")?;
for snap in &self.final_actor_states {
let fields: Vec<String> = snap.fields.iter()
.map(|(n, v)| format!("{}={}", n, v))
.collect();
writeln!(f, " {} {{ {} }}", snap.actor_name, fields.join(", "))?;
}
if !self.all_outputs.is_empty() {
writeln!(f, " outputs: {:?}", self.all_outputs)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::l1_ir::tests::{build_counter_program, build_window_program};
#[test]
fn interpret_counter_matches_l0_execution() {
// Counter: 5 steps of Increment(1), expect count=5
let program = build_counter_program();
let trace = interpret(&program).expect("should interpret");
// Final state should match L0 execution
assert_eq!(trace.final_actor_states.len(), 1);
assert_eq!(trace.final_actor_states[0].actor_name, "counter");
assert_eq!(trace.final_actor_states[0].fields, vec![("count".to_string(), 5)]);
// Should have 5 trace steps
assert_eq!(trace.steps.len(), 5);
// No outputs (counter has no emit)
assert!(trace.all_outputs.is_empty());
// Each step should send one Increment message
for (i, step) in trace.steps.iter().enumerate() {
assert_eq!(step.messages_sent.len(), 1);
assert_eq!(step.messages_sent[0].target, "counter");
assert_eq!(step.messages_sent[0].message_type, "Increment");
assert_eq!(step.messages_sent[0].args, vec![1]);
// State after step i should be count = i+1
assert_eq!(
step.actor_states[0].fields,
vec![("count".to_string(), (i as u64) + 1)]
);
}
}
#[test]
fn interpret_window_matches_l0_execution() {
// Window: 3 steps of Add(10), expect total=30, outputs=[10,20,30]
let program = build_window_program();
let trace = interpret(&program).expect("should interpret");
// Final state
assert_eq!(trace.final_actor_states[0].actor_name, "accumulator");
assert_eq!(trace.final_actor_states[0].fields, vec![("total".to_string(), 30)]);
// Outputs match L0 execution
assert_eq!(trace.all_outputs, vec![10, 20, 30]);
// 3 trace steps
assert_eq!(trace.steps.len(), 3);
// Each step emits one value
for (i, step) in trace.steps.iter().enumerate() {
assert_eq!(step.emitted_outputs, vec![(i as u64 + 1) * 10]);
}
}
#[test]
fn interpret_product_matches_l0_execution() {
// Product: 4 steps of Purchase(7, 3)
// count = 4*3 = 12, total = 4*(7*3) = 84, outputs = [21, 42, 63, 84]
let source = include_str!("../examples/product.l1");
let program = crate::parser::parse(source).expect("should parse");
let trace = interpret(&program).expect("should interpret");
// Final state
let ledger = &trace.final_actor_states[0];
assert_eq!(ledger.actor_name, "ledger");
// Fields should be count=12, total=84
let count = ledger.fields.iter().find(|(n, _)| n == "count").unwrap().1;
let total = ledger.fields.iter().find(|(n, _)| n == "total").unwrap().1;
assert_eq!(count, 12);
assert_eq!(total, 84);
// Outputs
assert_eq!(trace.all_outputs, vec![21, 42, 63, 84]);
}
#[test]
fn interpret_counter_from_file_matches_pipeline() {
// Cross-validate: L1 interpreter vs L0 pipeline execution
let source = include_str!("../examples/counter.l1");
let program = crate::parser::parse(source).expect("should parse");
let trace = interpret(&program).expect("should interpret");
let (result, _) = crate::pipeline::run(source).expect("pipeline should run");
let l0_count = result.read_u64("counter_state", 0).expect("read count");
let l1_count = trace.final_actor_states[0].fields[0].1;
assert_eq!(l1_count, l0_count, "L1 interpreter and L0 pipeline must agree");
}
#[test]
fn interpret_window_from_file_matches_pipeline() {
let source = include_str!("../examples/window.l1");
let program = crate::parser::parse(source).expect("should parse");
let trace = interpret(&program).expect("should interpret");
let (result, _) = crate::pipeline::run(source).expect("pipeline should run");
let l0_total = result.read_u64("accumulator_state", 0).expect("read total");
let l0_outputs = result.read_output_values();
let l1_total = trace.final_actor_states[0].fields[0].1;
assert_eq!(l1_total, l0_total, "L1 and L0 final state must match");
assert_eq!(trace.all_outputs, l0_outputs, "L1 and L0 outputs must match");
}
#[test]
fn interpret_product_from_file_matches_pipeline() {
let source = include_str!("../examples/product.l1");
let program = crate::parser::parse(source).expect("should parse");
let trace = interpret(&program).expect("should interpret");
let (result, _) = crate::pipeline::run(source).expect("pipeline should run");
let l0_count = result.read_u64("ledger_state", 0).expect("read count");
let l0_total = result.read_u64("ledger_state", 8).expect("read total");
let l0_outputs = result.read_output_values();
let l1_count = trace.final_actor_states[0].fields.iter()
.find(|(n, _)| n == "count").unwrap().1;
let l1_total = trace.final_actor_states[0].fields.iter()
.find(|(n, _)| n == "total").unwrap().1;
assert_eq!(l1_count, l0_count, "L1 and L0 count must match");
assert_eq!(l1_total, l0_total, "L1 and L0 total must match");
assert_eq!(trace.all_outputs, l0_outputs, "L1 and L0 outputs must match");
}
#[test]
fn interpret_error_on_empty_program() {
let program = L1Program {
actors: vec![],
leaves: vec![],
pipelines: vec![],
cores: vec![],
};
let err = interpret(&program).unwrap_err();
assert!(err.message.contains("no cores"));
}
#[test]
fn interpret_trace_display() {
let program = build_counter_program();
let trace = interpret(&program).expect("should interpret");
let text = format!("{}", trace);
assert!(text.contains("Step 0"));
assert!(text.contains("Step 4"));
assert!(text.contains("Final"));
assert!(text.contains("counter"));
assert!(text.contains("count=5"));
}
}

603
src/l0_ir.rs Normal file
View file

@ -0,0 +1,603 @@
use std::fmt;
/// Access mode for a memory region.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AccessMode {
ReadOnly,
ReadWrite,
}
impl fmt::Display for AccessMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AccessMode::ReadOnly => write!(f, "ro"),
AccessMode::ReadWrite => write!(f, "rw"),
}
}
}
/// A named, sized memory region with an access mode.
#[derive(Debug, Clone, PartialEq)]
pub struct Region {
pub name: String,
pub size: usize,
pub access: AccessMode,
pub kind: RegionKind,
}
/// The kind of region — distinguishes actor state, queues, and control data.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RegionKind {
/// Persistent actor state.
State,
/// Message queue (ring buffer).
Queue,
/// Step counter / control metadata.
Control,
/// Output/observation buffer.
Output,
}
impl fmt::Display for RegionKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RegionKind::State => write!(f, "state"),
RegionKind::Queue => write!(f, "queue"),
RegionKind::Control => write!(f, "control"),
RegionKind::Output => write!(f, "output"),
}
}
}
impl fmt::Display for Region {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "region {} : {} bytes, {}, {}", self.name, self.size, self.access, self.kind)
}
}
/// A memory slot within a region.
#[derive(Debug, Clone, PartialEq)]
pub struct MemorySlot {
pub region: String,
pub offset: usize,
pub size: usize,
}
impl fmt::Display for MemorySlot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}[{}..{}]", self.region, self.offset, self.offset + self.size)
}
}
/// SSA register.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Register(pub u32);
impl fmt::Display for Register {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "r{}", self.0)
}
}
/// Size of a load/store operation.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum OpSize {
B8,
B16,
B32,
B64,
}
impl OpSize {
pub fn bytes(&self) -> usize {
match self {
OpSize::B8 => 1,
OpSize::B16 => 2,
OpSize::B32 => 4,
OpSize::B64 => 8,
}
}
}
impl fmt::Display for OpSize {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OpSize::B8 => write!(f, "8"),
OpSize::B16 => write!(f, "16"),
OpSize::B32 => write!(f, "32"),
OpSize::B64 => write!(f, "64"),
}
}
}
/// Arithmetic operation kind.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ArithKind {
Add,
Sub,
Mul,
And,
Or,
Shl,
}
impl fmt::Display for ArithKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ArithKind::Add => write!(f, "add"),
ArithKind::Sub => write!(f, "sub"),
ArithKind::Mul => write!(f, "mul"),
ArithKind::And => write!(f, "and"),
ArithKind::Or => write!(f, "or"),
ArithKind::Shl => write!(f, "shl"),
}
}
}
/// Comparison operation kind.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CmpKind {
Eq,
Neq,
Lt,
Gt,
}
impl fmt::Display for CmpKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CmpKind::Eq => write!(f, "eq"),
CmpKind::Neq => write!(f, "neq"),
CmpKind::Lt => write!(f, "lt"),
CmpKind::Gt => write!(f, "gt"),
}
}
}
/// The 10 Control IR operations.
#[derive(Debug, Clone, PartialEq)]
pub enum Op {
/// load.{sz} rgn[off] -> reg
Load {
size: OpSize,
region: String,
offset: usize,
dest: Register,
},
/// store.{sz} rgn[off], val
Store {
size: OpSize,
region: String,
offset: usize,
src: Register,
},
/// hash val -> reg
Hash {
src: Register,
dest: Register,
},
/// arith {op} a, b -> reg
Arith {
kind: ArithKind,
lhs: Register,
rhs: Register,
dest: Register,
},
/// arith {op} a, imm -> reg (immediate variant)
ArithImm {
kind: ArithKind,
lhs: Register,
imm: u64,
dest: Register,
},
/// cmp {op} a, b -> reg
Cmp {
kind: CmpKind,
lhs: Register,
rhs: Register,
dest: Register,
},
/// cmp {op} a, imm -> reg (immediate variant)
CmpImm {
kind: CmpKind,
lhs: Register,
imm: u64,
dest: Register,
},
/// branch reg -> blk_true, blk_false
Branch {
cond: Register,
true_target: String,
false_target: String,
},
/// jump -> blk
Jump {
target: String,
},
/// queue_push rgn, val
QueuePush {
region: String,
src: Register,
},
/// queue_pop rgn -> reg
QueuePop {
region: String,
dest: Register,
},
/// terminate
Terminate,
/// Load immediate value into register
LoadImm {
value: u64,
dest: Register,
},
}
impl fmt::Display for Op {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Op::Load { size, region, offset, dest } =>
write!(f, "{} = load.{} {}[{}]", dest, size, region, offset),
Op::Store { size, region, offset, src } =>
write!(f, "store.{} {}[{}], {}", size, region, offset, src),
Op::Hash { src, dest } =>
write!(f, "{} = hash {}", dest, src),
Op::Arith { kind, lhs, rhs, dest } =>
write!(f, "{} = {} {}, {}", dest, kind, lhs, rhs),
Op::ArithImm { kind, lhs, imm, dest } =>
write!(f, "{} = {} {}, #{}", dest, kind, lhs, imm),
Op::Cmp { kind, lhs, rhs, dest } =>
write!(f, "{} = cmp.{} {}, {}", dest, kind, lhs, rhs),
Op::CmpImm { kind, lhs, imm, dest } =>
write!(f, "{} = cmp.{} {}, #{}", dest, kind, lhs, imm),
Op::Branch { cond, true_target, false_target } =>
write!(f, "branch {} -> {}, {}", cond, true_target, false_target),
Op::Jump { target } =>
write!(f, "jump -> {}", target),
Op::QueuePush { region, src } =>
write!(f, "queue_push {}, {}", region, src),
Op::QueuePop { region, dest } =>
write!(f, "{} = queue_pop {}", dest, region),
Op::Terminate =>
write!(f, "terminate"),
Op::LoadImm { value, dest } =>
write!(f, "{} = imm #{}", dest, value),
}
}
}
/// A basic block — a named sequence of operations.
#[derive(Debug, Clone, PartialEq)]
pub struct Block {
pub name: String,
pub ops: Vec<Op>,
}
impl fmt::Display for Block {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "{}:", self.name)?;
for op in &self.ops {
writeln!(f, " {}", op)?;
}
Ok(())
}
}
/// The complete L0 IR program.
#[derive(Debug, Clone, PartialEq)]
pub struct L0Program {
pub regions: Vec<Region>,
pub blocks: Vec<Block>,
}
impl fmt::Display for L0Program {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "=== Regions ===")?;
for region in &self.regions {
writeln!(f, " {}", region)?;
}
writeln!(f, "=== Blocks ===")?;
for block in &self.blocks {
write!(f, "{}", block)?;
}
Ok(())
}
}
#[cfg(test)]
pub mod tests {
use super::*;
#[test]
fn construct_region() {
let r = Region {
name: "counter_state".to_string(),
size: 8,
access: AccessMode::ReadWrite,
kind: RegionKind::State,
};
assert_eq!(r.name, "counter_state");
assert_eq!(r.size, 8);
assert_eq!(r.access, AccessMode::ReadWrite);
assert_eq!(r.kind, RegionKind::State);
}
#[test]
fn construct_memory_slot() {
let slot = MemorySlot {
region: "counter_state".to_string(),
offset: 0,
size: 8,
};
assert_eq!(format!("{}", slot), "counter_state[0..8]");
}
#[test]
fn construct_ops() {
let load = Op::Load {
size: OpSize::B64,
region: "counter_state".to_string(),
offset: 0,
dest: Register(0),
};
assert_eq!(format!("{}", load), "r0 = load.64 counter_state[0]");
let store = Op::Store {
size: OpSize::B64,
region: "counter_state".to_string(),
offset: 0,
src: Register(1),
};
assert_eq!(format!("{}", store), "store.64 counter_state[0], r1");
let arith = Op::Arith {
kind: ArithKind::Add,
lhs: Register(0),
rhs: Register(1),
dest: Register(2),
};
assert_eq!(format!("{}", arith), "r2 = add r0, r1");
let cmp = Op::Cmp {
kind: CmpKind::Lt,
lhs: Register(0),
rhs: Register(1),
dest: Register(2),
};
assert_eq!(format!("{}", cmp), "r2 = cmp.lt r0, r1");
let branch = Op::Branch {
cond: Register(0),
true_target: "loop".to_string(),
false_target: "exit".to_string(),
};
assert_eq!(format!("{}", branch), "branch r0 -> loop, exit");
let jump = Op::Jump { target: "loop".to_string() };
assert_eq!(format!("{}", jump), "jump -> loop");
let push = Op::QueuePush {
region: "msg_queue".to_string(),
src: Register(0),
};
assert_eq!(format!("{}", push), "queue_push msg_queue, r0");
let pop = Op::QueuePop {
region: "msg_queue".to_string(),
dest: Register(0),
};
assert_eq!(format!("{}", pop), "r0 = queue_pop msg_queue");
let hash = Op::Hash {
src: Register(0),
dest: Register(1),
};
assert_eq!(format!("{}", hash), "r1 = hash r0");
assert_eq!(format!("{}", Op::Terminate), "terminate");
}
#[test]
fn construct_block() {
let block = Block {
name: "entry".to_string(),
ops: vec![
Op::LoadImm { value: 0, dest: Register(0) },
Op::Store {
size: OpSize::B64,
region: "counter_state".to_string(),
offset: 0,
src: Register(0),
},
Op::Jump { target: "loop".to_string() },
],
};
let text = format!("{}", block);
assert!(text.contains("entry:"));
assert!(text.contains("r0 = imm #0"));
assert!(text.contains("store.64 counter_state[0], r0"));
assert!(text.contains("jump -> loop"));
}
#[test]
fn construct_counter_l0_ir() {
// Build L0 IR that represents the counter example.
let program = build_counter_l0();
assert_eq!(program.regions.len(), 3); // state, queue, step counter
assert_eq!(program.blocks.len(), 4); // entry, loop_check, step, exit
let text = format!("{}", program);
assert!(text.contains("=== Regions ==="));
assert!(text.contains("counter_state"));
assert!(text.contains("msg_queue"));
assert!(text.contains("step_counter"));
assert!(text.contains("=== Blocks ==="));
assert!(text.contains("entry:"));
assert!(text.contains("loop_check:"));
assert!(text.contains("step:"));
assert!(text.contains("exit:"));
}
#[test]
fn region_display() {
let r = Region {
name: "test_rgn".to_string(),
size: 64,
access: AccessMode::ReadOnly,
kind: RegionKind::Queue,
};
assert_eq!(format!("{}", r), "region test_rgn : 64 bytes, ro, queue");
}
#[test]
fn l0_program_display_is_human_readable() {
let program = build_counter_l0();
let text = format!("{}", program);
// Verify it's structured and readable, not just Debug output
assert!(text.contains("=== Regions ==="));
assert!(text.contains("=== Blocks ==="));
assert!(text.contains("region counter_state : 8 bytes, rw, state"));
assert!(text.contains("terminate"));
}
pub fn build_counter_l0() -> L0Program {
L0Program {
regions: vec![
Region {
name: "counter_state".to_string(),
size: 8,
access: AccessMode::ReadWrite,
kind: RegionKind::State,
},
Region {
name: "msg_queue".to_string(),
size: 128,
access: AccessMode::ReadWrite,
kind: RegionKind::Queue,
},
Region {
name: "step_counter".to_string(),
size: 8,
access: AccessMode::ReadWrite,
kind: RegionKind::Control,
},
],
blocks: vec![
Block {
name: "entry".to_string(),
ops: vec![
// Initialize counter_state to 0
Op::LoadImm { value: 0, dest: Register(0) },
Op::Store {
size: OpSize::B64,
region: "counter_state".to_string(),
offset: 0,
src: Register(0),
},
// Initialize step counter to 0
Op::Store {
size: OpSize::B64,
region: "step_counter".to_string(),
offset: 0,
src: Register(0),
},
Op::Jump { target: "loop_check".to_string() },
],
},
Block {
name: "loop_check".to_string(),
ops: vec![
// Load step counter
Op::Load {
size: OpSize::B64,
region: "step_counter".to_string(),
offset: 0,
dest: Register(0),
},
// Compare with step limit (5)
Op::LoadImm { value: 5, dest: Register(1) },
Op::Cmp {
kind: CmpKind::Lt,
lhs: Register(0),
rhs: Register(1),
dest: Register(2),
},
Op::Branch {
cond: Register(2),
true_target: "step".to_string(),
false_target: "exit".to_string(),
},
],
},
Block {
name: "step".to_string(),
ops: vec![
// Leaf ticker: push Increment(1) to queue
Op::LoadImm { value: 1, dest: Register(0) },
Op::QueuePush {
region: "msg_queue".to_string(),
src: Register(0),
},
// Actor handler: pop from queue, load state, add, store
Op::QueuePop {
region: "msg_queue".to_string(),
dest: Register(1),
},
Op::Load {
size: OpSize::B64,
region: "counter_state".to_string(),
offset: 0,
dest: Register(2),
},
Op::Arith {
kind: ArithKind::Add,
lhs: Register(2),
rhs: Register(1),
dest: Register(3),
},
Op::Store {
size: OpSize::B64,
region: "counter_state".to_string(),
offset: 0,
src: Register(3),
},
// Window read (display leaf): load counter_state
Op::Load {
size: OpSize::B64,
region: "counter_state".to_string(),
offset: 0,
dest: Register(4),
},
// Increment step counter
Op::Load {
size: OpSize::B64,
region: "step_counter".to_string(),
offset: 0,
dest: Register(5),
},
Op::ArithImm {
kind: ArithKind::Add,
lhs: Register(5),
imm: 1,
dest: Register(6),
},
Op::Store {
size: OpSize::B64,
region: "step_counter".to_string(),
offset: 0,
src: Register(6),
},
Op::Jump { target: "loop_check".to_string() },
],
},
Block {
name: "exit".to_string(),
ops: vec![
Op::Terminate,
],
},
],
}
}
}

499
src/l1_ir.rs Normal file
View file

@ -0,0 +1,499 @@
use std::fmt;
/// A typed field in an actor's state.
#[derive(Debug, Clone, PartialEq)]
pub struct StateField {
pub name: String,
pub ty: FieldType,
pub init: u64,
}
/// Supported field types for actor state.
#[derive(Debug, Clone, PartialEq)]
pub enum FieldType {
U64,
I64,
F64,
}
impl fmt::Display for FieldType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FieldType::U64 => write!(f, "u64"),
FieldType::I64 => write!(f, "i64"),
FieldType::F64 => write!(f, "f64"),
}
}
}
/// A window grants read-only access to actor state fields.
#[derive(Debug, Clone, PartialEq)]
pub struct WindowDecl {
pub name: String,
pub fields: Vec<String>,
pub readers: Vec<String>,
}
/// The body of a message handler expression.
#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
/// A literal integer value.
Lit(u64),
/// Reference to a variable (state field or argument).
Var(String),
/// Arithmetic: op, lhs, rhs.
Arith(ArithOp, Box<Expr>, Box<Expr>),
/// Assignment: target field, value expression.
Assign(String, Box<Expr>),
/// Block of sequential expressions.
Block(Vec<Expr>),
}
/// Arithmetic operations available in handler bodies.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ArithOp {
Add,
Sub,
Mul,
}
impl fmt::Display for ArithOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ArithOp::Add => write!(f, "+"),
ArithOp::Sub => write!(f, "-"),
ArithOp::Mul => write!(f, "*"),
}
}
}
/// A message type that an actor can handle.
#[derive(Debug, Clone, PartialEq)]
pub struct MessageHandler {
pub message_type: String,
pub args: Vec<(String, FieldType)>,
pub body: Expr,
}
/// An actor declaration — owns mutable state, exposes windows, handles messages.
#[derive(Debug, Clone, PartialEq)]
pub struct ActorDecl {
pub name: String,
pub state: Vec<StateField>,
pub windows: Vec<WindowDecl>,
pub handlers: Vec<MessageHandler>,
}
/// Action a leaf can take in its process function.
#[derive(Debug, Clone, PartialEq)]
pub enum LeafAction {
/// Forward a message to a target (actor or leaf).
Forward {
target: String,
message_type: String,
args: Vec<Expr>,
},
/// Read a value through a window.
ReadWindow {
actor: String,
window: String,
field: String,
},
/// Emit a value (for observation/output).
Emit(Expr),
}
/// A leaf declaration — stateless processor with a process function.
#[derive(Debug, Clone, PartialEq)]
pub struct LeafDecl {
pub name: String,
pub reads: Option<ReadClause>,
pub actions: Vec<LeafAction>,
}
/// A reads clause specifying which window a leaf reads from.
#[derive(Debug, Clone, PartialEq)]
pub struct ReadClause {
pub actor: String,
pub window: String,
}
/// A pipeline declaration — ordered chain of leaf/actor references.
#[derive(Debug, Clone, PartialEq)]
pub struct PipelineDecl {
pub name: String,
pub stages: Vec<String>,
}
/// A core declaration — execution context for a single thread.
#[derive(Debug, Clone, PartialEq)]
pub struct CoreDecl {
pub name: String,
pub actors: Vec<String>,
pub leaves: Vec<String>,
pub pipelines: Vec<String>,
pub steps: u64,
}
/// The top-level L1 program.
#[derive(Debug, Clone, PartialEq)]
pub struct L1Program {
pub actors: Vec<ActorDecl>,
pub leaves: Vec<LeafDecl>,
pub pipelines: Vec<PipelineDecl>,
pub cores: Vec<CoreDecl>,
}
// --- Display implementations for human-readable L1 IR text ---
impl fmt::Display for StateField {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {} = {}", self.name, self.ty, self.init)
}
}
impl fmt::Display for WindowDecl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "window {} : ({}) readers({})",
self.name,
self.fields.join(", "),
self.readers.join(", "))
}
}
impl fmt::Display for Expr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Expr::Lit(v) => write!(f, "{}", v),
Expr::Var(name) => write!(f, "{}", name),
Expr::Arith(op, lhs, rhs) => write!(f, "({} {} {})", lhs, op, rhs),
Expr::Assign(target, val) => write!(f, "{} = {}", target, val),
Expr::Block(exprs) => {
for (i, e) in exprs.iter().enumerate() {
if i > 0 { write!(f, "; ")?; }
write!(f, "{}", e)?;
}
Ok(())
}
}
}
}
impl fmt::Display for MessageHandler {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let args: Vec<String> = self.args.iter()
.map(|(name, ty)| format!("{}: {}", name, ty))
.collect();
write!(f, "on {}({}) {{ {} }}", self.message_type, args.join(", "), self.body)
}
}
impl fmt::Display for ActorDecl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "actor {} {{", self.name)?;
writeln!(f, " state {{")?;
for field in &self.state {
writeln!(f, " {}", field)?;
}
writeln!(f, " }}")?;
for window in &self.windows {
writeln!(f, " {}", window)?;
}
for handler in &self.handlers {
writeln!(f, " {}", handler)?;
}
write!(f, "}}")
}
}
impl fmt::Display for LeafAction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LeafAction::Forward { target, message_type, args } => {
let args_str: Vec<String> = args.iter().map(|a| format!("{}", a)).collect();
write!(f, "forward({}, {}({}))", target, message_type, args_str.join(", "))
}
LeafAction::ReadWindow { actor, window, field } => {
write!(f, "read({}.{}.{})", actor, window, field)
}
LeafAction::Emit(expr) => {
write!(f, "emit({})", expr)
}
}
}
}
impl fmt::Display for LeafDecl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "leaf {} {{", self.name)?;
if let Some(reads) = &self.reads {
write!(f, " reads {}.{};", reads.actor, reads.window)?;
}
for action in &self.actions {
write!(f, " {};", action)?;
}
write!(f, " }}")
}
}
impl fmt::Display for PipelineDecl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "pipeline {} {{ {} }}", self.name, self.stages.join(" -> "))
}
}
impl fmt::Display for CoreDecl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "core {} {{", self.name)?;
writeln!(f, " actors: [{}]", self.actors.join(", "))?;
writeln!(f, " leaves: [{}]", self.leaves.join(", "))?;
writeln!(f, " pipelines: [{}]", self.pipelines.join(", "))?;
writeln!(f, " steps: {}", self.steps)?;
write!(f, "}}")
}
}
impl fmt::Display for L1Program {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for actor in &self.actors {
writeln!(f, "{}", actor)?;
}
for leaf in &self.leaves {
writeln!(f, "{}", leaf)?;
}
for pipeline in &self.pipelines {
writeln!(f, "{}", pipeline)?;
}
for core in &self.cores {
writeln!(f, "{}", core)?;
}
Ok(())
}
}
#[cfg(test)]
pub mod tests {
use super::*;
#[test]
fn construct_counter_l1_ir() {
let program = build_counter_program();
assert_eq!(program.actors.len(), 1);
assert_eq!(program.actors[0].name, "counter");
assert_eq!(program.actors[0].state.len(), 1);
assert_eq!(program.actors[0].state[0].name, "count");
assert_eq!(program.actors[0].state[0].ty, FieldType::U64);
assert_eq!(program.actors[0].state[0].init, 0);
assert_eq!(program.actors[0].windows.len(), 1);
assert_eq!(program.actors[0].windows[0].name, "count_view");
assert_eq!(program.actors[0].windows[0].fields, vec!["count"]);
assert_eq!(program.actors[0].windows[0].readers, vec!["display"]);
assert_eq!(program.actors[0].handlers.len(), 1);
assert_eq!(program.actors[0].handlers[0].message_type, "Increment");
assert_eq!(program.leaves.len(), 2);
assert_eq!(program.leaves[0].name, "ticker");
assert_eq!(program.leaves[1].name, "display");
assert_eq!(program.pipelines.len(), 1);
assert_eq!(program.pipelines[0].stages, vec!["ticker", "counter", "display"]);
assert_eq!(program.cores.len(), 1);
assert_eq!(program.cores[0].steps, 5);
}
#[test]
fn construct_window_l1_ir() {
let program = build_window_program();
assert_eq!(program.actors.len(), 1);
assert_eq!(program.actors[0].name, "accumulator");
assert_eq!(program.actors[0].state[0].name, "total");
assert_eq!(program.actors[0].state[0].init, 0);
assert_eq!(program.actors[0].windows[0].readers, vec!["observe"]);
assert_eq!(program.leaves.len(), 2);
assert_eq!(program.leaves[0].name, "source");
assert_eq!(program.leaves[1].name, "observe");
assert_eq!(program.cores[0].steps, 3);
}
#[test]
fn l1_ir_display_counter() {
let program = build_counter_program();
let text = format!("{}", program);
assert!(text.contains("actor counter"));
assert!(text.contains("count: u64 = 0"));
assert!(text.contains("window count_view"));
assert!(text.contains("on Increment"));
assert!(text.contains("leaf ticker"));
assert!(text.contains("leaf display"));
assert!(text.contains("pipeline main"));
assert!(text.contains("core main"));
assert!(text.contains("steps: 5"));
}
#[test]
fn state_field_display() {
let field = StateField {
name: "count".to_string(),
ty: FieldType::U64,
init: 42,
};
assert_eq!(format!("{}", field), "count: u64 = 42");
}
#[test]
fn expr_display() {
let expr = Expr::Arith(
ArithOp::Add,
Box::new(Expr::Var("count".to_string())),
Box::new(Expr::Lit(1)),
);
assert_eq!(format!("{}", expr), "(count + 1)");
}
pub fn build_counter_program() -> L1Program {
L1Program {
actors: vec![ActorDecl {
name: "counter".to_string(),
state: vec![StateField {
name: "count".to_string(),
ty: FieldType::U64,
init: 0,
}],
windows: vec![WindowDecl {
name: "count_view".to_string(),
fields: vec!["count".to_string()],
readers: vec!["display".to_string()],
}],
handlers: vec![MessageHandler {
message_type: "Increment".to_string(),
args: vec![("amount".to_string(), FieldType::U64)],
body: Expr::Assign(
"count".to_string(),
Box::new(Expr::Arith(
ArithOp::Add,
Box::new(Expr::Var("count".to_string())),
Box::new(Expr::Var("amount".to_string())),
)),
),
}],
}],
leaves: vec![
LeafDecl {
name: "ticker".to_string(),
reads: None,
actions: vec![LeafAction::Forward {
target: "counter".to_string(),
message_type: "Increment".to_string(),
args: vec![Expr::Lit(1)],
}],
},
LeafDecl {
name: "display".to_string(),
reads: Some(ReadClause {
actor: "counter".to_string(),
window: "count_view".to_string(),
}),
actions: vec![LeafAction::ReadWindow {
actor: "counter".to_string(),
window: "count_view".to_string(),
field: "count".to_string(),
}],
},
],
pipelines: vec![PipelineDecl {
name: "main".to_string(),
stages: vec![
"ticker".to_string(),
"counter".to_string(),
"display".to_string(),
],
}],
cores: vec![CoreDecl {
name: "main".to_string(),
actors: vec!["counter".to_string()],
leaves: vec!["ticker".to_string(), "display".to_string()],
pipelines: vec!["main".to_string()],
steps: 5,
}],
}
}
pub fn build_window_program() -> L1Program {
L1Program {
actors: vec![ActorDecl {
name: "accumulator".to_string(),
state: vec![StateField {
name: "total".to_string(),
ty: FieldType::U64,
init: 0,
}],
windows: vec![WindowDecl {
name: "total_view".to_string(),
fields: vec!["total".to_string()],
readers: vec!["observe".to_string()],
}],
handlers: vec![MessageHandler {
message_type: "Add".to_string(),
args: vec![("value".to_string(), FieldType::U64)],
body: Expr::Assign(
"total".to_string(),
Box::new(Expr::Arith(
ArithOp::Add,
Box::new(Expr::Var("total".to_string())),
Box::new(Expr::Var("value".to_string())),
)),
),
}],
}],
leaves: vec![
LeafDecl {
name: "source".to_string(),
reads: None,
actions: vec![LeafAction::Forward {
target: "accumulator".to_string(),
message_type: "Add".to_string(),
args: vec![Expr::Lit(10)],
}],
},
LeafDecl {
name: "observe".to_string(),
reads: Some(ReadClause {
actor: "accumulator".to_string(),
window: "total_view".to_string(),
}),
actions: vec![
LeafAction::ReadWindow {
actor: "accumulator".to_string(),
window: "total_view".to_string(),
field: "total".to_string(),
},
LeafAction::Emit(Expr::Var("total".to_string())),
],
},
],
pipelines: vec![PipelineDecl {
name: "main".to_string(),
stages: vec![
"source".to_string(),
"accumulator".to_string(),
"observe".to_string(),
],
}],
cores: vec![CoreDecl {
name: "main".to_string(),
actors: vec!["accumulator".to_string()],
leaves: vec!["source".to_string(), "observe".to_string()],
pipelines: vec!["main".to_string()],
steps: 3,
}],
}
}
}

11
src/lib.rs Normal file
View file

@ -0,0 +1,11 @@
pub mod l1_ir;
pub mod l0_ir;
pub mod parser;
pub mod verify_l1;
pub mod emit;
pub mod verify_l0;
pub mod codegen;
pub mod pipeline;
pub mod interpret_l1;
pub mod translate_validate;
pub mod repl;

78
src/main.rs Normal file
View file

@ -0,0 +1,78 @@
use l0_cpu_runtime::{pipeline, repl};
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
eprintln!("Usage: l0-cpu-runtime <file.l1> [--dump-ir]");
eprintln!(" l0-cpu-runtime verify <file.l1>");
eprintln!(" l0-cpu-runtime repl");
std::process::exit(1);
}
// Handle "repl" subcommand
if args[1] == "repl" {
repl::run_repl();
return;
}
// Handle "verify" subcommand
if args[1] == "verify" {
if args.len() < 3 {
eprintln!("Usage: l0-cpu-runtime verify <file.l1>");
std::process::exit(1);
}
let path = &args[2];
let source = std::fs::read_to_string(path).unwrap_or_else(|e| {
eprintln!("Error reading '{}': {}", path, e);
std::process::exit(1);
});
match pipeline::verify_translation(&source) {
Ok(report) => {
print!("{}", report);
}
Err(e) => {
eprintln!("Verification failed: {}", e);
std::process::exit(1);
}
}
return;
}
let path = &args[1];
let source = std::fs::read_to_string(path).unwrap_or_else(|e| {
eprintln!("Error reading '{}': {}", path, e);
std::process::exit(1);
});
let dump = args.iter().any(|a| a == "--dump-ir");
if dump {
match pipeline::dump_ir(&source) {
Ok(text) => print!("{}", text),
Err(e) => {
eprintln!("Pipeline error: {}", e);
std::process::exit(1);
}
}
} else {
match pipeline::run(&source) {
Ok((result, _ir)) => {
println!("Execution complete.");
for (name, buf) in &result.regions {
if buf.len() >= 8 {
let val = u64::from_le_bytes(buf[0..8].try_into().unwrap());
println!(" {}[0] = {}", name, val);
}
}
let output = result.read_output_values();
if !output.is_empty() {
println!(" output values: {:?}", output);
}
}
Err(e) => {
eprintln!("Pipeline error: {}", e);
std::process::exit(1);
}
}
}
}

816
src/parser.rs Normal file
View file

@ -0,0 +1,816 @@
use crate::l1_ir::*;
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub struct ParseError {
pub message: String,
pub line: usize,
pub col: usize,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "parse error at {}:{}: {}", self.line, self.col, self.message)
}
}
impl std::error::Error for ParseError {}
struct Lexer<'a> {
input: &'a str,
pos: usize,
line: usize,
col: usize,
}
#[derive(Debug, Clone, PartialEq)]
enum Token {
Ident(String),
Number(u64),
LBrace,
RBrace,
LParen,
RParen,
LBracket,
RBracket,
Colon,
Comma,
Eq,
Plus,
Minus,
Star,
Arrow, // ->
Dot,
Eof,
}
impl fmt::Display for Token {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Token::Ident(s) => write!(f, "'{}'", s),
Token::Number(n) => write!(f, "{}", n),
Token::LBrace => write!(f, "'{{'"),
Token::RBrace => write!(f, "'}}'"),
Token::LParen => write!(f, "'('"),
Token::RParen => write!(f, "')'"),
Token::LBracket => write!(f, "'['"),
Token::RBracket => write!(f, "']'"),
Token::Colon => write!(f, "':'"),
Token::Comma => write!(f, "','"),
Token::Eq => write!(f, "'='"),
Token::Plus => write!(f, "'+'"),
Token::Minus => write!(f, "'-'"),
Token::Star => write!(f, "'*'"),
Token::Arrow => write!(f, "'->'"),
Token::Dot => write!(f, "'.'"),
Token::Eof => write!(f, "EOF"),
}
}
}
impl<'a> Lexer<'a> {
fn new(input: &'a str) -> Self {
Lexer { input, pos: 0, line: 1, col: 1 }
}
fn skip_whitespace_and_comments(&mut self) {
let bytes = self.input.as_bytes();
while self.pos < bytes.len() {
if bytes[self.pos] == b'\n' {
self.pos += 1;
self.line += 1;
self.col = 1;
} else if bytes[self.pos].is_ascii_whitespace() {
self.pos += 1;
self.col += 1;
} else if self.pos + 1 < bytes.len() && bytes[self.pos] == b'/' && bytes[self.pos + 1] == b'/' {
// Line comment
while self.pos < bytes.len() && bytes[self.pos] != b'\n' {
self.pos += 1;
}
} else {
break;
}
}
}
fn error(&self, msg: &str) -> ParseError {
ParseError { message: msg.to_string(), line: self.line, col: self.col }
}
fn next_token(&mut self) -> Result<Token, ParseError> {
self.skip_whitespace_and_comments();
let bytes = self.input.as_bytes();
if self.pos >= bytes.len() {
return Ok(Token::Eof);
}
let ch = bytes[self.pos];
// Identifiers and keywords
if ch.is_ascii_alphabetic() || ch == b'_' {
let start = self.pos;
while self.pos < bytes.len() && (bytes[self.pos].is_ascii_alphanumeric() || bytes[self.pos] == b'_') {
self.pos += 1;
self.col += 1;
}
let word = &self.input[start..self.pos];
return Ok(Token::Ident(word.to_string()));
}
// Numbers
if ch.is_ascii_digit() {
let start = self.pos;
while self.pos < bytes.len() && bytes[self.pos].is_ascii_digit() {
self.pos += 1;
self.col += 1;
}
let num_str = &self.input[start..self.pos];
let value = num_str.parse::<u64>()
.map_err(|_| self.error(&format!("invalid number: {}", num_str)))?;
return Ok(Token::Number(value));
}
self.pos += 1;
self.col += 1;
match ch {
b'{' => Ok(Token::LBrace),
b'}' => Ok(Token::RBrace),
b'(' => Ok(Token::LParen),
b')' => Ok(Token::RParen),
b'[' => Ok(Token::LBracket),
b']' => Ok(Token::RBracket),
b':' => Ok(Token::Colon),
b',' => Ok(Token::Comma),
b'+' => Ok(Token::Plus),
b'*' => Ok(Token::Star),
b'.' => Ok(Token::Dot),
b'=' => Ok(Token::Eq),
b'-' => {
if self.pos < bytes.len() && bytes[self.pos] == b'>' {
self.pos += 1;
self.col += 1;
Ok(Token::Arrow)
} else {
Ok(Token::Minus)
}
}
_ => Err(self.error(&format!("unexpected character: '{}'", ch as char))),
}
}
fn peek_token(&mut self) -> Result<Token, ParseError> {
let saved_pos = self.pos;
let saved_line = self.line;
let saved_col = self.col;
let tok = self.next_token()?;
self.pos = saved_pos;
self.line = saved_line;
self.col = saved_col;
Ok(tok)
}
}
pub struct Parser<'a> {
lexer: Lexer<'a>,
}
impl<'a> Parser<'a> {
pub fn new(input: &'a str) -> Self {
Parser { lexer: Lexer::new(input) }
}
fn error(&self, msg: &str) -> ParseError {
self.lexer.error(msg)
}
fn expect_ident(&mut self) -> Result<String, ParseError> {
match self.lexer.next_token()? {
Token::Ident(s) => Ok(s),
other => Err(self.error(&format!("expected identifier, got {}", other))),
}
}
fn expect_token(&mut self, expected: &Token) -> Result<(), ParseError> {
let tok = self.lexer.next_token()?;
if std::mem::discriminant(&tok) == std::mem::discriminant(expected) {
Ok(())
} else {
Err(self.error(&format!("expected {}, got {}", expected, tok)))
}
}
fn expect_number(&mut self) -> Result<u64, ParseError> {
match self.lexer.next_token()? {
Token::Number(n) => Ok(n),
other => Err(self.error(&format!("expected number, got {}", other))),
}
}
fn parse_field_type(&mut self) -> Result<FieldType, ParseError> {
let name = self.expect_ident()?;
match name.as_str() {
"u64" => Ok(FieldType::U64),
"i64" => Ok(FieldType::I64),
"f64" => Ok(FieldType::F64),
_ => Err(self.error(&format!("unknown type: {}", name))),
}
}
fn parse_state_field(&mut self) -> Result<StateField, ParseError> {
let name = self.expect_ident()?;
self.expect_token(&Token::Colon)?;
let ty = self.parse_field_type()?;
self.expect_token(&Token::Eq)?;
let init = self.expect_number()?;
Ok(StateField { name, ty, init })
}
fn parse_state_block(&mut self) -> Result<Vec<StateField>, ParseError> {
// "state" keyword already consumed
self.expect_token(&Token::LBrace)?;
let mut fields = Vec::new();
loop {
match self.lexer.peek_token()? {
Token::RBrace => {
self.lexer.next_token()?;
break;
}
_ => fields.push(self.parse_state_field()?),
}
}
Ok(fields)
}
fn parse_window_decl(&mut self) -> Result<WindowDecl, ParseError> {
// "window" keyword already consumed
let name = self.expect_ident()?;
self.expect_token(&Token::Colon)?;
self.expect_token(&Token::LParen)?;
let mut fields = Vec::new();
loop {
match self.lexer.peek_token()? {
Token::RParen => {
self.lexer.next_token()?;
break;
}
Token::Comma => {
self.lexer.next_token()?;
}
_ => fields.push(self.expect_ident()?),
}
}
// readers(...)
let readers_kw = self.expect_ident()?;
if readers_kw != "readers" {
return Err(self.error(&format!("expected 'readers', got '{}'", readers_kw)));
}
self.expect_token(&Token::LParen)?;
let mut readers = Vec::new();
loop {
match self.lexer.peek_token()? {
Token::RParen => {
self.lexer.next_token()?;
break;
}
Token::Comma => {
self.lexer.next_token()?;
}
_ => readers.push(self.expect_ident()?),
}
}
Ok(WindowDecl { name, fields, readers })
}
fn parse_expr(&mut self) -> Result<Expr, ParseError> {
let lhs = self.parse_expr_atom()?;
// Check for assignment: ident = expr
// or arithmetic: expr op expr
match self.lexer.peek_token()? {
Token::Eq => {
// Assignment
if let Expr::Var(name) = lhs {
self.lexer.next_token()?;
let rhs = self.parse_expr()?;
Ok(Expr::Assign(name, Box::new(rhs)))
} else {
Err(self.error("left side of assignment must be an identifier"))
}
}
Token::Plus | Token::Minus | Token::Star => {
let op_tok = self.lexer.next_token()?;
let op = match op_tok {
Token::Plus => ArithOp::Add,
Token::Minus => ArithOp::Sub,
Token::Star => ArithOp::Mul,
_ => unreachable!(),
};
let rhs = self.parse_expr_atom()?;
Ok(Expr::Arith(op, Box::new(lhs), Box::new(rhs)))
}
_ => Ok(lhs),
}
}
fn parse_expr_atom(&mut self) -> Result<Expr, ParseError> {
match self.lexer.peek_token()? {
Token::Number(_) => {
let n = self.expect_number()?;
Ok(Expr::Lit(n))
}
Token::Ident(_) => {
let name = self.expect_ident()?;
Ok(Expr::Var(name))
}
Token::LParen => {
self.lexer.next_token()?;
let expr = self.parse_expr()?;
self.expect_token(&Token::RParen)?;
Ok(expr)
}
other => Err(self.error(&format!("expected expression, got {}", other))),
}
}
fn parse_handler(&mut self) -> Result<MessageHandler, ParseError> {
// "on" keyword already consumed
let message_type = self.expect_ident()?;
self.expect_token(&Token::LParen)?;
let mut args = Vec::new();
loop {
match self.lexer.peek_token()? {
Token::RParen => {
self.lexer.next_token()?;
break;
}
Token::Comma => {
self.lexer.next_token()?;
}
_ => {
let arg_name = self.expect_ident()?;
self.expect_token(&Token::Colon)?;
let arg_ty = self.parse_field_type()?;
args.push((arg_name, arg_ty));
}
}
}
self.expect_token(&Token::LBrace)?;
let mut exprs = Vec::new();
loop {
match self.lexer.peek_token()? {
Token::RBrace => {
self.lexer.next_token()?;
break;
}
_ => exprs.push(self.parse_expr()?),
}
}
let body = if exprs.len() == 1 {
exprs.into_iter().next().unwrap()
} else {
Expr::Block(exprs)
};
Ok(MessageHandler { message_type, args, body })
}
fn parse_actor(&mut self) -> Result<ActorDecl, ParseError> {
// "actor" keyword already consumed
let name = self.expect_ident()?;
self.expect_token(&Token::LBrace)?;
let mut state = Vec::new();
let mut windows = Vec::new();
let mut handlers = Vec::new();
loop {
match self.lexer.peek_token()? {
Token::RBrace => {
self.lexer.next_token()?;
break;
}
Token::Ident(ref kw) => {
let kw = kw.clone();
match kw.as_str() {
"state" => {
self.lexer.next_token()?;
state = self.parse_state_block()?;
}
"window" => {
self.lexer.next_token()?;
windows.push(self.parse_window_decl()?);
}
"on" => {
self.lexer.next_token()?;
handlers.push(self.parse_handler()?);
}
_ => return Err(self.error(&format!(
"unexpected keyword '{}' in actor body", kw
))),
}
}
other => return Err(self.error(&format!(
"unexpected token {} in actor body", other
))),
}
}
Ok(ActorDecl { name, state, windows, handlers })
}
fn parse_leaf_actions(&mut self) -> Result<Vec<LeafAction>, ParseError> {
// "process" keyword already consumed
self.expect_token(&Token::LBrace)?;
let mut actions = Vec::new();
loop {
match self.lexer.peek_token()? {
Token::RBrace => {
self.lexer.next_token()?;
break;
}
Token::Ident(ref kw) => {
let kw = kw.clone();
match kw.as_str() {
"forward" => {
self.lexer.next_token()?;
self.expect_token(&Token::LParen)?;
let target = self.expect_ident()?;
self.expect_token(&Token::Comma)?;
let msg_type = self.expect_ident()?;
self.expect_token(&Token::LParen)?;
let mut args = Vec::new();
loop {
match self.lexer.peek_token()? {
Token::RParen => {
self.lexer.next_token()?;
break;
}
Token::Comma => {
self.lexer.next_token()?;
}
_ => args.push(self.parse_expr()?),
}
}
self.expect_token(&Token::RParen)?; // outer close
actions.push(LeafAction::Forward {
target,
message_type: msg_type,
args,
});
}
"read" => {
self.lexer.next_token()?;
self.expect_token(&Token::LParen)?;
let actor = self.expect_ident()?;
self.expect_token(&Token::Dot)?;
let window = self.expect_ident()?;
self.expect_token(&Token::Dot)?;
let field = self.expect_ident()?;
self.expect_token(&Token::RParen)?;
actions.push(LeafAction::ReadWindow {
actor,
window,
field,
});
}
"emit" => {
self.lexer.next_token()?;
self.expect_token(&Token::LParen)?;
let expr = self.parse_expr()?;
self.expect_token(&Token::RParen)?;
actions.push(LeafAction::Emit(expr));
}
_ => return Err(self.error(&format!(
"unexpected action '{}' in process body", kw
))),
}
}
other => return Err(self.error(&format!(
"unexpected token {} in process body", other
))),
}
}
Ok(actions)
}
fn parse_leaf(&mut self) -> Result<LeafDecl, ParseError> {
// "leaf" keyword already consumed
let name = self.expect_ident()?;
self.expect_token(&Token::LBrace)?;
let mut reads = None;
let mut actions = Vec::new();
loop {
match self.lexer.peek_token()? {
Token::RBrace => {
self.lexer.next_token()?;
break;
}
Token::Ident(ref kw) => {
let kw = kw.clone();
match kw.as_str() {
"reads" => {
self.lexer.next_token()?;
let actor = self.expect_ident()?;
self.expect_token(&Token::Dot)?;
let window = self.expect_ident()?;
reads = Some(ReadClause { actor, window });
}
"process" => {
self.lexer.next_token()?;
actions = self.parse_leaf_actions()?;
}
_ => return Err(self.error(&format!(
"unexpected keyword '{}' in leaf body", kw
))),
}
}
other => return Err(self.error(&format!(
"unexpected token {} in leaf body", other
))),
}
}
Ok(LeafDecl { name, reads, actions })
}
fn parse_pipeline(&mut self) -> Result<PipelineDecl, ParseError> {
// "pipeline" keyword already consumed
let name = self.expect_ident()?;
self.expect_token(&Token::LBrace)?;
let mut stages = Vec::new();
stages.push(self.expect_ident()?);
loop {
match self.lexer.peek_token()? {
Token::Arrow => {
self.lexer.next_token()?;
stages.push(self.expect_ident()?);
}
Token::RBrace => {
self.lexer.next_token()?;
break;
}
other => return Err(self.error(&format!(
"expected '->' or '}}' in pipeline, got {}", other
))),
}
}
Ok(PipelineDecl { name, stages })
}
fn parse_ident_list(&mut self) -> Result<Vec<String>, ParseError> {
self.expect_token(&Token::LBracket)?;
let mut items = Vec::new();
loop {
match self.lexer.peek_token()? {
Token::RBracket => {
self.lexer.next_token()?;
break;
}
Token::Comma => {
self.lexer.next_token()?;
}
_ => items.push(self.expect_ident()?),
}
}
Ok(items)
}
fn parse_core(&mut self) -> Result<CoreDecl, ParseError> {
// "core" keyword already consumed
let name = self.expect_ident()?;
self.expect_token(&Token::LBrace)?;
let mut actors = Vec::new();
let mut leaves = Vec::new();
let mut pipelines = Vec::new();
let mut steps = 0u64;
loop {
match self.lexer.peek_token()? {
Token::RBrace => {
self.lexer.next_token()?;
break;
}
Token::Ident(ref kw) => {
let kw = kw.clone();
match kw.as_str() {
"actors" => {
self.lexer.next_token()?;
self.expect_token(&Token::Colon)?;
actors = self.parse_ident_list()?;
}
"leaves" => {
self.lexer.next_token()?;
self.expect_token(&Token::Colon)?;
leaves = self.parse_ident_list()?;
}
"pipelines" => {
self.lexer.next_token()?;
self.expect_token(&Token::Colon)?;
pipelines = self.parse_ident_list()?;
}
"steps" => {
self.lexer.next_token()?;
self.expect_token(&Token::Colon)?;
steps = self.expect_number()?;
}
_ => return Err(self.error(&format!(
"unexpected field '{}' in core body", kw
))),
}
}
other => return Err(self.error(&format!(
"unexpected token {} in core body", other
))),
}
}
Ok(CoreDecl { name, actors, leaves, pipelines, steps })
}
pub fn parse_program(&mut self) -> Result<L1Program, ParseError> {
let mut actors = Vec::new();
let mut leaves = Vec::new();
let mut pipelines = Vec::new();
let mut cores = Vec::new();
loop {
match self.lexer.peek_token()? {
Token::Eof => break,
Token::Ident(ref kw) => {
let kw = kw.clone();
match kw.as_str() {
"actor" => {
self.lexer.next_token()?;
actors.push(self.parse_actor()?);
}
"leaf" => {
self.lexer.next_token()?;
leaves.push(self.parse_leaf()?);
}
"pipeline" => {
self.lexer.next_token()?;
pipelines.push(self.parse_pipeline()?);
}
"core" => {
self.lexer.next_token()?;
cores.push(self.parse_core()?);
}
_ => return Err(self.error(&format!(
"unexpected top-level keyword '{}'", kw
))),
}
}
other => return Err(self.error(&format!(
"unexpected token {} at top level", other
))),
}
}
Ok(L1Program { actors, leaves, pipelines, cores })
}
}
/// Parse L1 DSL text into an L1 program.
pub fn parse(input: &str) -> Result<L1Program, ParseError> {
Parser::new(input).parse_program()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_counter_example() {
let input = include_str!("../examples/counter.l1");
let program = parse(input).expect("counter.l1 should parse");
assert_eq!(program.actors.len(), 1);
assert_eq!(program.actors[0].name, "counter");
assert_eq!(program.actors[0].state.len(), 1);
assert_eq!(program.actors[0].state[0].name, "count");
assert_eq!(program.actors[0].state[0].ty, FieldType::U64);
assert_eq!(program.actors[0].state[0].init, 0);
assert_eq!(program.actors[0].windows.len(), 1);
assert_eq!(program.actors[0].windows[0].name, "count_view");
assert_eq!(program.actors[0].windows[0].fields, vec!["count"]);
assert_eq!(program.actors[0].windows[0].readers, vec!["display"]);
assert_eq!(program.actors[0].handlers.len(), 1);
assert_eq!(program.actors[0].handlers[0].message_type, "Increment");
assert_eq!(program.actors[0].handlers[0].args.len(), 1);
assert_eq!(program.actors[0].handlers[0].args[0].0, "amount");
assert_eq!(program.leaves.len(), 2);
assert_eq!(program.leaves[0].name, "ticker");
assert_eq!(program.leaves[1].name, "display");
assert!(program.leaves[1].reads.is_some());
assert_eq!(program.pipelines.len(), 1);
assert_eq!(program.pipelines[0].stages, vec!["ticker", "counter", "display"]);
assert_eq!(program.cores.len(), 1);
assert_eq!(program.cores[0].steps, 5);
}
#[test]
fn parse_window_example() {
let input = include_str!("../examples/window.l1");
let program = parse(input).expect("window.l1 should parse");
assert_eq!(program.actors.len(), 1);
assert_eq!(program.actors[0].name, "accumulator");
assert_eq!(program.actors[0].state[0].name, "total");
assert_eq!(program.actors[0].state[0].init, 0);
assert_eq!(program.actors[0].windows[0].name, "total_view");
assert_eq!(program.actors[0].windows[0].readers, vec!["observe"]);
assert_eq!(program.leaves.len(), 2);
assert_eq!(program.leaves[0].name, "source");
assert_eq!(program.leaves[1].name, "observe");
assert!(program.leaves[1].reads.is_some());
assert_eq!(program.leaves[1].actions.len(), 2);
assert_eq!(program.cores[0].steps, 3);
}
#[test]
fn parse_error_unexpected_top_level() {
let input = "foobar { }";
let err = parse(input).unwrap_err();
assert!(err.message.contains("unexpected top-level keyword 'foobar'"));
}
#[test]
fn parse_error_missing_brace() {
let input = "actor counter state { count: u64 = 0 } }";
let err = parse(input).unwrap_err();
assert!(err.message.contains("expected '{'"));
}
#[test]
fn parse_error_unknown_type() {
let input = "actor a { state { x: bool = 0 } }";
let err = parse(input).unwrap_err();
assert!(err.message.contains("unknown type: bool"));
}
#[test]
fn parse_error_malformed_pipeline() {
let input = "pipeline p { a -> }";
let err = parse(input).unwrap_err();
assert!(err.message.contains("expected identifier"));
}
#[test]
fn parsed_matches_hand_built_counter() {
let input = include_str!("../examples/counter.l1");
let parsed = parse(input).expect("should parse");
let hand_built = crate::l1_ir::tests::build_counter_program();
assert_eq!(parsed.actors.len(), hand_built.actors.len());
assert_eq!(parsed.actors[0].name, hand_built.actors[0].name);
assert_eq!(parsed.actors[0].state, hand_built.actors[0].state);
assert_eq!(parsed.actors[0].windows, hand_built.actors[0].windows);
assert_eq!(parsed.actors[0].handlers[0].message_type, hand_built.actors[0].handlers[0].message_type);
assert_eq!(parsed.leaves.len(), hand_built.leaves.len());
assert_eq!(parsed.pipelines, hand_built.pipelines);
assert_eq!(parsed.cores, hand_built.cores);
}
#[test]
fn parsed_matches_hand_built_window() {
let input = include_str!("../examples/window.l1");
let parsed = parse(input).expect("should parse");
let hand_built = crate::l1_ir::tests::build_window_program();
assert_eq!(parsed.actors[0].name, hand_built.actors[0].name);
assert_eq!(parsed.actors[0].state, hand_built.actors[0].state);
assert_eq!(parsed.actors[0].windows, hand_built.actors[0].windows);
assert_eq!(parsed.leaves.len(), hand_built.leaves.len());
assert_eq!(parsed.pipelines, hand_built.pipelines);
assert_eq!(parsed.cores, hand_built.cores);
}
}

307
src/pipeline.rs Normal file
View file

@ -0,0 +1,307 @@
use crate::codegen::{self, CodegenError, ExecutionResult, MachineCode};
use crate::emit::{self, EmitError};
use crate::l0_ir::L0Program;
use crate::l1_ir::L1Program;
use crate::parser::{self, ParseError};
use crate::translate_validate::{self, VerificationReport};
use crate::verify_l0;
use crate::verify_l1::{self, VerifyError};
use std::fmt;
/// Errors from any stage of the pipeline.
#[derive(Debug)]
pub enum PipelineError {
Parse(ParseError),
VerifyL1(VerifyError),
Emit(EmitError),
VerifyL0(Vec<verify_l0::L0VerifyError>),
Codegen(CodegenError),
TranslationValidation(String),
}
impl fmt::Display for PipelineError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PipelineError::Parse(e) => write!(f, "parse: {}", e),
PipelineError::VerifyL1(e) => write!(f, "L1 verify: {}", e),
PipelineError::Emit(e) => write!(f, "emit: {}", e),
PipelineError::VerifyL0(errs) => {
write!(f, "L0 verify:")?;
for e in errs {
write!(f, " {}", e)?;
}
Ok(())
}
PipelineError::Codegen(e) => write!(f, "codegen: {}", e),
PipelineError::TranslationValidation(msg) => write!(f, "translation validation: {}", msg),
}
}
}
impl std::error::Error for PipelineError {}
/// All intermediate representations captured during pipeline execution.
pub struct PipelineIR {
pub l1: L1Program,
pub l0: L0Program,
pub machine_code: MachineCode,
}
/// Run the full pipeline: DSL text -> parse -> L1 IR -> verify -> emit L0 IR
/// -> verify -> codegen -> execute.
///
/// Returns both the execution result and captured intermediate representations.
pub fn run(source: &str) -> Result<(ExecutionResult, PipelineIR), PipelineError> {
// Parse
let l1 = parser::parse(source).map_err(PipelineError::Parse)?;
// Verify L1
verify_l1::verify(&l1).map_err(PipelineError::VerifyL1)?;
// Emit L0 IR
let l0 = emit::emit(&l1).map_err(PipelineError::Emit)?;
// Verify L0
verify_l0::verify(&l0).map_err(PipelineError::VerifyL0)?;
// Codegen
let mc = codegen::codegen(&l0).map_err(PipelineError::Codegen)?;
// Execute
let result = codegen::execute(&mc).map_err(PipelineError::Codegen)?;
let ir = PipelineIR {
l1,
l0,
machine_code: mc,
};
Ok((result, ir))
}
/// Dump all intermediate representations for a given DSL input.
/// Returns a human-readable string showing L1 IR, L0 IR, and x86-64 hex.
pub fn dump_ir(source: &str) -> Result<String, PipelineError> {
let l1 = parser::parse(source).map_err(PipelineError::Parse)?;
verify_l1::verify(&l1).map_err(PipelineError::VerifyL1)?;
let l0 = emit::emit(&l1).map_err(PipelineError::Emit)?;
verify_l0::verify(&l0).map_err(PipelineError::VerifyL0)?;
let mc = codegen::codegen(&l0).map_err(PipelineError::Codegen)?;
let mut out = String::new();
out.push_str("========== L1 IR ==========\n");
out.push_str(&format!("{}", l1));
out.push('\n');
out.push_str("========== L0 IR ==========\n");
out.push_str(&format!("{}", l0));
out.push('\n');
out.push_str("========== x86-64 Machine Code ==========\n");
out.push_str(&mc.hex_dump());
out.push('\n');
Ok(out)
}
/// Run the full verification pipeline: parse → L1 verify → emit → L0 verify → translate validate → report.
///
/// Returns the verification report containing results from all three validation tools.
pub fn verify_translation(source: &str) -> Result<VerificationReport, PipelineError> {
// Parse
let l1 = parser::parse(source).map_err(PipelineError::Parse)?;
// Verify L1
verify_l1::verify(&l1).map_err(PipelineError::VerifyL1)?;
// Emit L0 IR
let l0 = emit::emit(&l1).map_err(PipelineError::Emit)?;
// Verify L0
verify_l0::verify(&l0).map_err(PipelineError::VerifyL0)?;
// Translation validation
let report = translate_validate::validate(&l1, &l0);
if !report.all_passed() {
return Err(PipelineError::TranslationValidation(format!("{}", report)));
}
Ok(report)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counter_end_to_end() {
let source = include_str!("../examples/counter.l1");
let (result, _ir) = run(source).expect("counter pipeline should succeed");
let count = result.read_u64("counter_state", 0).expect("should read counter_state");
assert_eq!(count, 5, "counter should be 5 after 5 steps");
}
#[test]
fn window_end_to_end() {
let source = include_str!("../examples/window.l1");
let (result, _ir) = run(source).expect("window pipeline should succeed");
let total = result.read_u64("accumulator_state", 0).expect("should read accumulator_state");
assert_eq!(total, 30, "total should be 30 after 3 steps of Add(10)");
let observed = result.read_output_values();
assert_eq!(observed, vec![10, 20, 30], "observed values should be [10, 20, 30]");
}
#[test]
fn counter_ir_dump() {
let source = include_str!("../examples/counter.l1");
let dump = dump_ir(source).expect("dump should succeed");
assert!(dump.contains("========== L1 IR =========="));
assert!(dump.contains("========== L0 IR =========="));
assert!(dump.contains("========== x86-64 Machine Code =========="));
assert!(dump.contains("actor counter"));
assert!(dump.contains("=== Regions ==="));
assert!(dump.contains("counter_state"));
}
#[test]
fn window_ir_dump() {
let source = include_str!("../examples/window.l1");
let dump = dump_ir(source).expect("dump should succeed");
assert!(dump.contains("========== L1 IR =========="));
assert!(dump.contains("accumulator"));
assert!(dump.contains("output"));
}
#[test]
fn counter_test_vector_full_pipeline() {
// Full test vector: DSL source -> L1 IR -> L0 IR -> x86-64 -> result
let source = include_str!("../examples/counter.l1");
let (result, ir) = run(source).expect("pipeline should succeed");
// 1. DSL source is the counter.l1 file (inlined via include_str!)
assert!(source.contains("actor counter"));
assert!(source.contains("on Increment(amount: u64)"));
assert!(source.contains("steps: 5"));
// 2. Expected L1 IR text
let l1_text = format!("{}", ir.l1);
assert!(l1_text.contains("actor counter {"));
assert!(l1_text.contains("count: u64 = 0"));
assert!(l1_text.contains("window count_view : (count) readers(display)"));
assert!(l1_text.contains("on Increment(amount: u64) { count = (count + amount) }"));
assert!(l1_text.contains("leaf ticker {"));
assert!(l1_text.contains("forward(counter, Increment(1))"));
assert!(l1_text.contains("leaf display {"));
assert!(l1_text.contains("pipeline main { ticker -> counter -> display }"));
assert!(l1_text.contains("steps: 5"));
// 3. Expected L0 IR text
let l0_text = format!("{}", ir.l0);
assert!(l0_text.contains("=== Regions ==="));
assert!(l0_text.contains("region counter_state : 8 bytes, rw, state"));
assert!(l0_text.contains("queue"));
assert!(l0_text.contains("region step_counter : 8 bytes, rw, control"));
assert!(l0_text.contains("=== Blocks ==="));
assert!(l0_text.contains("entry:"));
assert!(l0_text.contains("loop_check:"));
assert!(l0_text.contains("step:"));
assert!(l0_text.contains("exit:"));
assert!(l0_text.contains("load.64"));
assert!(l0_text.contains("store.64"));
assert!(l0_text.contains("cmp.lt"));
assert!(l0_text.contains("branch"));
assert!(l0_text.contains("queue_push"));
assert!(l0_text.contains("queue_pop"));
assert!(l0_text.contains("terminate"));
// 4. Expected x86-64 bytes (non-empty hex string)
let hex = ir.machine_code.hex_dump();
assert!(!hex.is_empty(), "machine code should not be empty");
// Verify it ends with ret (c3) preceded by the epilogue pops
assert!(hex.contains("c3"), "machine code should contain ret instruction");
// 5. Expected execution result
let count = result.read_u64("counter_state", 0).unwrap();
assert_eq!(count, 5, "counter should equal step count (5)");
}
#[test]
fn window_test_vector_full_pipeline() {
// Full test vector: DSL source -> L1 IR -> L0 IR -> x86-64 -> result
let source = include_str!("../examples/window.l1");
let (result, ir) = run(source).expect("pipeline should succeed");
// 1. DSL source
assert!(source.contains("actor accumulator"));
assert!(source.contains("on Add(value: u64)"));
assert!(source.contains("steps: 3"));
// 2. Expected L1 IR text
let l1_text = format!("{}", ir.l1);
assert!(l1_text.contains("actor accumulator {"));
assert!(l1_text.contains("total: u64 = 0"));
assert!(l1_text.contains("window total_view : (total) readers(observe)"));
assert!(l1_text.contains("on Add(value: u64) { total = (total + value) }"));
assert!(l1_text.contains("leaf source {"));
assert!(l1_text.contains("forward(accumulator, Add(10))"));
assert!(l1_text.contains("leaf observe {"));
assert!(l1_text.contains("emit(total)"));
assert!(l1_text.contains("pipeline main { source -> accumulator -> observe }"));
assert!(l1_text.contains("steps: 3"));
// 3. Expected L0 IR text
let l0_text = format!("{}", ir.l0);
assert!(l0_text.contains("region accumulator_state : 8 bytes, rw, state"));
assert!(l0_text.contains("region output :"));
assert!(l0_text.contains("output"));
assert!(l0_text.contains("entry:"));
assert!(l0_text.contains("queue_push output"));
// 4. x86-64 bytes
let hex = ir.machine_code.hex_dump();
assert!(!hex.is_empty());
assert!(hex.contains("c3"));
// 5. Expected execution result
let total = result.read_u64("accumulator_state", 0).unwrap();
assert_eq!(total, 30, "total should be 30");
let observed = result.read_output_values();
assert_eq!(observed, vec![10, 20, 30], "observed values should be [10, 20, 30]");
}
#[test]
fn pipeline_error_on_invalid_input() {
let result = run("invalid garbage input");
assert!(result.is_err());
}
#[test]
fn verify_translation_counter() {
let source = include_str!("../examples/counter.l1");
let report = verify_translation(source).expect("counter verification should pass");
assert!(report.all_passed());
assert_eq!(report.results.len(), 3);
}
#[test]
fn verify_translation_window() {
let source = include_str!("../examples/window.l1");
let report = verify_translation(source).expect("window verification should pass");
assert!(report.all_passed());
}
#[test]
fn verify_translation_product() {
let source = include_str!("../examples/product.l1");
let report = verify_translation(source).expect("product verification should pass");
assert!(report.all_passed());
}
#[test]
fn verify_translation_invalid_input() {
let result = verify_translation("invalid garbage");
assert!(result.is_err());
}
}

1849
src/repl.rs Normal file

File diff suppressed because it is too large Load diff

1127
src/translate_validate.rs Normal file

File diff suppressed because it is too large Load diff

642
src/verify_l0.rs Normal file
View file

@ -0,0 +1,642 @@
use crate::l0_ir::*;
use std::collections::{HashMap, HashSet};
use std::fmt;
/// Error kinds for L0 verification failures.
#[derive(Debug, Clone, PartialEq)]
pub enum L0ErrorKind {
/// A branch or jump targets a block that does not exist.
DanglingBlockTarget { op_desc: String, target: String },
/// A block is unreachable from the entry block.
UnreachableBlock { block: String },
/// A load/store references a region that is not declared.
UndeclaredRegion { op_desc: String, region: String },
/// A load/store access exceeds region bounds (offset + size > region size).
OutOfBoundsAccess {
op_desc: String,
region: String,
offset: usize,
access_size: usize,
region_size: usize,
},
/// A store targets a read-only region.
WriteToReadOnly { op_desc: String, region: String },
/// queue_push or queue_pop references a non-queue region.
QueueOpOnNonQueue { op_desc: String, region: String, actual_kind: RegionKind },
/// No blocks defined.
NoBlocks,
/// No entry block (first block is the entry point).
NoEntryBlock,
}
/// A single L0 verification error with location context.
#[derive(Debug, Clone, PartialEq)]
pub struct L0VerifyError {
pub block: String,
pub kind: L0ErrorKind,
}
impl fmt::Display for L0VerifyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
L0ErrorKind::DanglingBlockTarget { op_desc, target } =>
write!(f, "in block '{}': {} targets non-existent block '{}'", self.block, op_desc, target),
L0ErrorKind::UnreachableBlock { block } =>
write!(f, "block '{}' is unreachable from entry", block),
L0ErrorKind::UndeclaredRegion { op_desc, region } =>
write!(f, "in block '{}': {} references undeclared region '{}'", self.block, op_desc, region),
L0ErrorKind::OutOfBoundsAccess { op_desc, region, offset, access_size, region_size } =>
write!(f, "in block '{}': {} accesses {}[{}..{}] but region is only {} bytes",
self.block, op_desc, region, offset, offset + access_size, region_size),
L0ErrorKind::WriteToReadOnly { op_desc, region } =>
write!(f, "in block '{}': {} writes to read-only region '{}'", self.block, op_desc, region),
L0ErrorKind::QueueOpOnNonQueue { op_desc, region, actual_kind } =>
write!(f, "in block '{}': {} uses region '{}' which is {} (not queue)",
self.block, op_desc, region, actual_kind),
L0ErrorKind::NoBlocks =>
write!(f, "program has no blocks"),
L0ErrorKind::NoEntryBlock =>
write!(f, "program has no entry block"),
}
}
}
impl std::error::Error for L0VerifyError {}
/// Verify a concrete L0 IR program.
///
/// Checks:
/// 1. Control flow: all branch/jump targets exist, no unreachable blocks
/// 2. Region access: all load/store reference declared regions, within bounds, respecting access modes
/// 3. Memory bounds: total region bytes are finite (implicit — all sizes are usize)
/// 4. Queue safety: queue_push/queue_pop only on queue-typed regions
pub fn verify(program: &L0Program) -> Result<(), Vec<L0VerifyError>> {
let mut errors = Vec::new();
if program.blocks.is_empty() {
errors.push(L0VerifyError {
block: "<program>".to_string(),
kind: L0ErrorKind::NoBlocks,
});
return Err(errors);
}
// Build lookup tables
let block_names: HashSet<&str> = program.blocks.iter().map(|b| b.name.as_str()).collect();
let regions: HashMap<&str, &Region> = program.regions.iter().map(|r| (r.name.as_str(), r)).collect();
// Check 1: Control flow — all branch/jump targets exist
for block in &program.blocks {
for op in &block.ops {
match op {
Op::Branch { true_target, false_target, .. } => {
if !block_names.contains(true_target.as_str()) {
errors.push(L0VerifyError {
block: block.name.clone(),
kind: L0ErrorKind::DanglingBlockTarget {
op_desc: format!("{}", op),
target: true_target.clone(),
},
});
}
if !block_names.contains(false_target.as_str()) {
errors.push(L0VerifyError {
block: block.name.clone(),
kind: L0ErrorKind::DanglingBlockTarget {
op_desc: format!("{}", op),
target: false_target.clone(),
},
});
}
}
Op::Jump { target } => {
if !block_names.contains(target.as_str()) {
errors.push(L0VerifyError {
block: block.name.clone(),
kind: L0ErrorKind::DanglingBlockTarget {
op_desc: format!("{}", op),
target: target.clone(),
},
});
}
}
_ => {}
}
}
}
// Check 1b: Reachability — all blocks reachable from entry (first block)
let mut reachable: HashSet<&str> = HashSet::new();
let mut worklist: Vec<&str> = vec![program.blocks[0].name.as_str()];
while let Some(name) = worklist.pop() {
if !reachable.insert(name) {
continue;
}
// Find this block and collect successors
if let Some(block) = program.blocks.iter().find(|b| b.name == name) {
for op in &block.ops {
match op {
Op::Branch { true_target, false_target, .. } => {
if block_names.contains(true_target.as_str()) {
worklist.push(true_target.as_str());
}
if block_names.contains(false_target.as_str()) {
worklist.push(false_target.as_str());
}
}
Op::Jump { target } => {
if block_names.contains(target.as_str()) {
worklist.push(target.as_str());
}
}
_ => {}
}
}
}
}
for block in &program.blocks {
if !reachable.contains(block.name.as_str()) {
errors.push(L0VerifyError {
block: block.name.clone(),
kind: L0ErrorKind::UnreachableBlock {
block: block.name.clone(),
},
});
}
}
// Check 2 & 3 & 4: Region access, bounds, queue safety
for block in &program.blocks {
for op in &block.ops {
match op {
Op::Load { size, region, offset, .. } => {
check_region_read(block, op, region, *offset, size.bytes(), &regions, &mut errors);
}
Op::Store { size, region, offset, .. } => {
check_region_write(block, op, region, *offset, size.bytes(), &regions, &mut errors);
}
Op::QueuePush { region, .. } => {
check_queue_region(block, op, region, &regions, &mut errors);
}
Op::QueuePop { region, .. } => {
check_queue_region(block, op, region, &regions, &mut errors);
}
_ => {}
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
/// Check that a region exists and the read access is within bounds.
fn check_region_read(
block: &Block,
op: &Op,
region_name: &str,
offset: usize,
access_size: usize,
regions: &HashMap<&str, &Region>,
errors: &mut Vec<L0VerifyError>,
) {
match regions.get(region_name) {
None => {
errors.push(L0VerifyError {
block: block.name.clone(),
kind: L0ErrorKind::UndeclaredRegion {
op_desc: format!("{}", op),
region: region_name.to_string(),
},
});
}
Some(rgn) => {
if offset + access_size > rgn.size {
errors.push(L0VerifyError {
block: block.name.clone(),
kind: L0ErrorKind::OutOfBoundsAccess {
op_desc: format!("{}", op),
region: region_name.to_string(),
offset,
access_size,
region_size: rgn.size,
},
});
}
}
}
}
/// Check that a region exists, is read-write, and the write access is within bounds.
fn check_region_write(
block: &Block,
op: &Op,
region_name: &str,
offset: usize,
access_size: usize,
regions: &HashMap<&str, &Region>,
errors: &mut Vec<L0VerifyError>,
) {
match regions.get(region_name) {
None => {
errors.push(L0VerifyError {
block: block.name.clone(),
kind: L0ErrorKind::UndeclaredRegion {
op_desc: format!("{}", op),
region: region_name.to_string(),
},
});
}
Some(rgn) => {
if rgn.access == AccessMode::ReadOnly {
errors.push(L0VerifyError {
block: block.name.clone(),
kind: L0ErrorKind::WriteToReadOnly {
op_desc: format!("{}", op),
region: region_name.to_string(),
},
});
}
if offset + access_size > rgn.size {
errors.push(L0VerifyError {
block: block.name.clone(),
kind: L0ErrorKind::OutOfBoundsAccess {
op_desc: format!("{}", op),
region: region_name.to_string(),
offset,
access_size,
region_size: rgn.size,
},
});
}
}
}
}
/// Check that a queue operation references a queue-typed region.
fn check_queue_region(
block: &Block,
op: &Op,
region_name: &str,
regions: &HashMap<&str, &Region>,
errors: &mut Vec<L0VerifyError>,
) {
match regions.get(region_name) {
None => {
errors.push(L0VerifyError {
block: block.name.clone(),
kind: L0ErrorKind::UndeclaredRegion {
op_desc: format!("{}", op),
region: region_name.to_string(),
},
});
}
Some(rgn) => {
// Queue ops on output regions are allowed (emit uses queue_push on output)
if rgn.kind != RegionKind::Queue && rgn.kind != RegionKind::Output {
errors.push(L0VerifyError {
block: block.name.clone(),
kind: L0ErrorKind::QueueOpOnNonQueue {
op_desc: format!("{}", op),
region: region_name.to_string(),
actual_kind: rgn.kind,
},
});
}
}
}
}
/// Compute total memory usage across all regions.
pub fn total_memory(program: &L0Program) -> usize {
program.regions.iter().map(|r| r.size).sum()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::l0_ir::tests::build_counter_l0;
#[test]
fn valid_counter_passes_verification() {
let program = build_counter_l0();
assert!(verify(&program).is_ok());
}
#[test]
fn valid_emitted_counter_passes() {
let input = include_str!("../examples/counter.l1");
let parsed = crate::parser::parse(input).expect("should parse");
crate::verify_l1::verify(&parsed).expect("should verify L1");
let l0 = crate::emit::emit(&parsed).expect("should emit");
assert!(verify(&l0).is_ok());
}
#[test]
fn valid_emitted_window_passes() {
let input = include_str!("../examples/window.l1");
let parsed = crate::parser::parse(input).expect("should parse");
crate::verify_l1::verify(&parsed).expect("should verify L1");
let l0 = crate::emit::emit(&parsed).expect("should emit");
assert!(verify(&l0).is_ok());
}
#[test]
fn total_memory_for_counter() {
let program = build_counter_l0();
let mem = total_memory(&program);
// counter_state(8) + msg_queue(128) + step_counter(8) = 144
assert_eq!(mem, 144);
}
// --- Negative tests ---
#[test]
fn reject_dangling_branch_target() {
let program = L0Program {
regions: vec![],
blocks: vec![
Block {
name: "entry".to_string(),
ops: vec![
Op::LoadImm { value: 1, dest: Register(0) },
Op::Branch {
cond: Register(0),
true_target: "exists".to_string(),
false_target: "nowhere".to_string(),
},
],
},
Block {
name: "exists".to_string(),
ops: vec![Op::Terminate],
},
],
};
let errs = verify(&program).unwrap_err();
assert!(errs.iter().any(|e| matches!(&e.kind,
L0ErrorKind::DanglingBlockTarget { target, .. } if target == "nowhere"
)));
}
#[test]
fn reject_dangling_jump_target() {
let program = L0Program {
regions: vec![],
blocks: vec![
Block {
name: "entry".to_string(),
ops: vec![
Op::Jump { target: "ghost".to_string() },
],
},
],
};
let errs = verify(&program).unwrap_err();
assert!(errs.iter().any(|e| matches!(&e.kind,
L0ErrorKind::DanglingBlockTarget { target, .. } if target == "ghost"
)));
}
#[test]
fn reject_unreachable_block() {
let program = L0Program {
regions: vec![],
blocks: vec![
Block {
name: "entry".to_string(),
ops: vec![Op::Terminate],
},
Block {
name: "orphan".to_string(),
ops: vec![Op::Terminate],
},
],
};
let errs = verify(&program).unwrap_err();
assert!(errs.iter().any(|e| matches!(&e.kind,
L0ErrorKind::UnreachableBlock { block } if block == "orphan"
)));
}
#[test]
fn reject_undeclared_region_in_load() {
let program = L0Program {
regions: vec![],
blocks: vec![
Block {
name: "entry".to_string(),
ops: vec![
Op::Load {
size: OpSize::B64,
region: "nonexistent".to_string(),
offset: 0,
dest: Register(0),
},
Op::Terminate,
],
},
],
};
let errs = verify(&program).unwrap_err();
assert!(errs.iter().any(|e| matches!(&e.kind,
L0ErrorKind::UndeclaredRegion { region, .. } if region == "nonexistent"
)));
}
#[test]
fn reject_out_of_bounds_access() {
let program = L0Program {
regions: vec![
Region {
name: "small".to_string(),
size: 4,
access: AccessMode::ReadWrite,
kind: RegionKind::State,
},
],
blocks: vec![
Block {
name: "entry".to_string(),
ops: vec![
Op::Load {
size: OpSize::B64, // 8 bytes from a 4-byte region
region: "small".to_string(),
offset: 0,
dest: Register(0),
},
Op::Terminate,
],
},
],
};
let errs = verify(&program).unwrap_err();
assert!(errs.iter().any(|e| matches!(&e.kind,
L0ErrorKind::OutOfBoundsAccess { region, offset, access_size, region_size, .. }
if region == "small" && *offset == 0 && *access_size == 8 && *region_size == 4
)));
}
#[test]
fn reject_write_to_readonly_region() {
let program = L0Program {
regions: vec![
Region {
name: "constants".to_string(),
size: 64,
access: AccessMode::ReadOnly,
kind: RegionKind::State,
},
],
blocks: vec![
Block {
name: "entry".to_string(),
ops: vec![
Op::LoadImm { value: 42, dest: Register(0) },
Op::Store {
size: OpSize::B64,
region: "constants".to_string(),
offset: 0,
src: Register(0),
},
Op::Terminate,
],
},
],
};
let errs = verify(&program).unwrap_err();
assert!(errs.iter().any(|e| matches!(&e.kind,
L0ErrorKind::WriteToReadOnly { region, .. } if region == "constants"
)));
}
#[test]
fn reject_queue_push_on_state_region() {
let program = L0Program {
regions: vec![
Region {
name: "actor_state".to_string(),
size: 64,
access: AccessMode::ReadWrite,
kind: RegionKind::State,
},
],
blocks: vec![
Block {
name: "entry".to_string(),
ops: vec![
Op::LoadImm { value: 1, dest: Register(0) },
Op::QueuePush {
region: "actor_state".to_string(),
src: Register(0),
},
Op::Terminate,
],
},
],
};
let errs = verify(&program).unwrap_err();
assert!(errs.iter().any(|e| matches!(&e.kind,
L0ErrorKind::QueueOpOnNonQueue { region, actual_kind: RegionKind::State, .. }
if region == "actor_state"
)));
}
#[test]
fn reject_queue_pop_on_control_region() {
let program = L0Program {
regions: vec![
Region {
name: "step_counter".to_string(),
size: 8,
access: AccessMode::ReadWrite,
kind: RegionKind::Control,
},
],
blocks: vec![
Block {
name: "entry".to_string(),
ops: vec![
Op::QueuePop {
region: "step_counter".to_string(),
dest: Register(0),
},
Op::Terminate,
],
},
],
};
let errs = verify(&program).unwrap_err();
assert!(errs.iter().any(|e| matches!(&e.kind,
L0ErrorKind::QueueOpOnNonQueue { region, actual_kind: RegionKind::Control, .. }
if region == "step_counter"
)));
}
#[test]
fn reject_no_blocks() {
let program = L0Program {
regions: vec![],
blocks: vec![],
};
let errs = verify(&program).unwrap_err();
assert!(errs.iter().any(|e| matches!(&e.kind, L0ErrorKind::NoBlocks)));
}
#[test]
fn reject_store_out_of_bounds_with_offset() {
let program = L0Program {
regions: vec![
Region {
name: "data".to_string(),
size: 16,
access: AccessMode::ReadWrite,
kind: RegionKind::State,
},
],
blocks: vec![
Block {
name: "entry".to_string(),
ops: vec![
Op::LoadImm { value: 99, dest: Register(0) },
Op::Store {
size: OpSize::B64,
region: "data".to_string(),
offset: 12, // 12 + 8 = 20 > 16
src: Register(0),
},
Op::Terminate,
],
},
],
};
let errs = verify(&program).unwrap_err();
assert!(errs.iter().any(|e| matches!(&e.kind,
L0ErrorKind::OutOfBoundsAccess { region, offset, access_size, region_size, .. }
if region == "data" && *offset == 12 && *access_size == 8 && *region_size == 16
)));
}
#[test]
fn error_messages_are_descriptive() {
let program = L0Program {
regions: vec![],
blocks: vec![
Block {
name: "entry".to_string(),
ops: vec![
Op::Jump { target: "missing".to_string() },
],
},
],
};
let errs = verify(&program).unwrap_err();
let msg = format!("{}", errs[0]);
assert!(msg.contains("entry"), "error should name the block");
assert!(msg.contains("missing"), "error should name the target");
}
}

680
src/verify_l1.rs Normal file
View file

@ -0,0 +1,680 @@
use crate::l1_ir::*;
use std::collections::{HashMap, HashSet};
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub struct VerifyError {
pub kind: ErrorKind,
pub message: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ErrorKind {
DanglingRef,
DuplicateOwnership,
InvalidWindowReader,
MalformedPipeline,
BufferNotConsumed,
InvalidSteps,
PipelineCycle,
WindowWriterConflict,
}
impl fmt::Display for VerifyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{:?}] {}", self.kind, self.message)
}
}
impl std::error::Error for VerifyError {}
/// Verify an L1 program for structural correctness.
/// Returns Ok(()) if the program passes all checks, or Err with the first violation found.
pub fn verify(program: &L1Program) -> Result<(), VerifyError> {
verify_ref_resolution(program)?;
verify_exclusive_ownership(program)?;
verify_window_separation(program)?;
verify_pipeline_wellformedness(program)?;
verify_buffer_conservation(program)?;
verify_finiteness(program)?;
Ok(())
}
/// Safety: all names referenced in cores, pipelines, windows, and leaf actions resolve
/// to declared actors/leaves/pipelines.
fn verify_ref_resolution(program: &L1Program) -> Result<(), VerifyError> {
let actor_names: HashSet<&str> = program.actors.iter().map(|a| a.name.as_str()).collect();
let leaf_names: HashSet<&str> = program.leaves.iter().map(|l| l.name.as_str()).collect();
let pipeline_names: HashSet<&str> = program.pipelines.iter().map(|p| p.name.as_str()).collect();
let all_entity_names: HashSet<&str> = actor_names.iter().chain(leaf_names.iter()).copied().collect();
// Check core references
for core in &program.cores {
for actor_ref in &core.actors {
if !actor_names.contains(actor_ref.as_str()) {
return Err(VerifyError {
kind: ErrorKind::DanglingRef,
message: format!(
"core '{}' references undeclared actor '{}'",
core.name, actor_ref
),
});
}
}
for leaf_ref in &core.leaves {
if !leaf_names.contains(leaf_ref.as_str()) {
return Err(VerifyError {
kind: ErrorKind::DanglingRef,
message: format!(
"core '{}' references undeclared leaf '{}'",
core.name, leaf_ref
),
});
}
}
for pipe_ref in &core.pipelines {
if !pipeline_names.contains(pipe_ref.as_str()) {
return Err(VerifyError {
kind: ErrorKind::DanglingRef,
message: format!(
"core '{}' references undeclared pipeline '{}'",
core.name, pipe_ref
),
});
}
}
}
// Check pipeline stage references
for pipeline in &program.pipelines {
for stage in &pipeline.stages {
if !all_entity_names.contains(stage.as_str()) {
return Err(VerifyError {
kind: ErrorKind::DanglingRef,
message: format!(
"pipeline '{}' references undeclared entity '{}'",
pipeline.name, stage
),
});
}
}
}
// Check window reader references
for actor in &program.actors {
for window in &actor.windows {
for reader in &window.readers {
if !leaf_names.contains(reader.as_str()) {
return Err(VerifyError {
kind: ErrorKind::DanglingRef,
message: format!(
"window '{}' on actor '{}' names undeclared leaf reader '{}'",
window.name, actor.name, reader
),
});
}
}
// Check that window fields reference actual state fields
let state_field_names: HashSet<&str> =
actor.state.iter().map(|f| f.name.as_str()).collect();
for field in &window.fields {
if !state_field_names.contains(field.as_str()) {
return Err(VerifyError {
kind: ErrorKind::DanglingRef,
message: format!(
"window '{}' on actor '{}' references undeclared state field '{}'",
window.name, actor.name, field
),
});
}
}
}
}
// Check leaf forward targets and read references
for leaf in &program.leaves {
for action in &leaf.actions {
match action {
LeafAction::Forward { target, .. } => {
if !all_entity_names.contains(target.as_str()) {
return Err(VerifyError {
kind: ErrorKind::DanglingRef,
message: format!(
"leaf '{}' forwards to undeclared target '{}'",
leaf.name, target
),
});
}
}
LeafAction::ReadWindow { actor, window, field } => {
if !actor_names.contains(actor.as_str()) {
return Err(VerifyError {
kind: ErrorKind::DanglingRef,
message: format!(
"leaf '{}' reads from undeclared actor '{}'",
leaf.name, actor
),
});
}
// Check the window exists on the actor
if let Some(actor_decl) = program.actors.iter().find(|a| a.name == *actor) {
if !actor_decl.windows.iter().any(|w| w.name == *window) {
return Err(VerifyError {
kind: ErrorKind::DanglingRef,
message: format!(
"leaf '{}' reads from undeclared window '{}' on actor '{}'",
leaf.name, window, actor
),
});
}
// Check the field exists in the window
if let Some(win) = actor_decl.windows.iter().find(|w| w.name == *window) {
if !win.fields.contains(field) {
return Err(VerifyError {
kind: ErrorKind::DanglingRef,
message: format!(
"leaf '{}' reads field '{}' not exposed by window '{}' on actor '{}'",
leaf.name, field, window, actor
),
});
}
}
}
}
LeafAction::Emit(_) => {}
}
}
// Check reads clause
if let Some(reads) = &leaf.reads {
if !actor_names.contains(reads.actor.as_str()) {
return Err(VerifyError {
kind: ErrorKind::DanglingRef,
message: format!(
"leaf '{}' reads clause references undeclared actor '{}'",
leaf.name, reads.actor
),
});
}
if let Some(actor_decl) = program.actors.iter().find(|a| a.name == reads.actor) {
if !actor_decl.windows.iter().any(|w| w.name == reads.window) {
return Err(VerifyError {
kind: ErrorKind::DanglingRef,
message: format!(
"leaf '{}' reads clause references undeclared window '{}' on actor '{}'",
leaf.name, reads.window, reads.actor
),
});
}
}
}
}
Ok(())
}
/// Safety: every actor and leaf belongs to exactly one core.
fn verify_exclusive_ownership(program: &L1Program) -> Result<(), VerifyError> {
let mut actor_owners: HashMap<&str, &str> = HashMap::new();
let mut leaf_owners: HashMap<&str, &str> = HashMap::new();
for core in &program.cores {
for actor_ref in &core.actors {
if let Some(prev_core) = actor_owners.insert(actor_ref.as_str(), core.name.as_str()) {
return Err(VerifyError {
kind: ErrorKind::DuplicateOwnership,
message: format!(
"actor '{}' is owned by both core '{}' and core '{}'",
actor_ref, prev_core, core.name
),
});
}
}
for leaf_ref in &core.leaves {
if let Some(prev_core) = leaf_owners.insert(leaf_ref.as_str(), core.name.as_str()) {
return Err(VerifyError {
kind: ErrorKind::DuplicateOwnership,
message: format!(
"leaf '{}' is owned by both core '{}' and core '{}'",
leaf_ref, prev_core, core.name
),
});
}
}
}
Ok(())
}
/// Safety: window readers are leaves in the same core as the actor;
/// no leaf both reads a window on an actor AND forwards messages to that same actor.
fn verify_window_separation(program: &L1Program) -> Result<(), VerifyError> {
// Build a map: entity_name -> core_name
let mut entity_core: HashMap<&str, &str> = HashMap::new();
for core in &program.cores {
for a in &core.actors {
entity_core.insert(a.as_str(), core.name.as_str());
}
for l in &core.leaves {
entity_core.insert(l.as_str(), core.name.as_str());
}
}
for actor in &program.actors {
let actor_core = entity_core.get(actor.name.as_str());
for window in &actor.windows {
for reader in &window.readers {
// Reader must be a leaf
let leaf_names: HashSet<&str> =
program.leaves.iter().map(|l| l.name.as_str()).collect();
if !leaf_names.contains(reader.as_str()) {
return Err(VerifyError {
kind: ErrorKind::InvalidWindowReader,
message: format!(
"window '{}' on actor '{}' names reader '{}' which is not a leaf",
window.name, actor.name, reader
),
});
}
// Reader must be in the same core as the actor
let reader_core = entity_core.get(reader.as_str());
if actor_core != reader_core {
return Err(VerifyError {
kind: ErrorKind::InvalidWindowReader,
message: format!(
"window '{}' reader '{}' is not in the same core as actor '{}'",
window.name, reader, actor.name
),
});
}
// Check the reader leaf doesn't also forward messages to this actor
if let Some(leaf) = program.leaves.iter().find(|l| l.name == *reader) {
for action in &leaf.actions {
if let LeafAction::Forward { target, .. } = action {
if target == &actor.name {
return Err(VerifyError {
kind: ErrorKind::WindowWriterConflict,
message: format!(
"leaf '{}' both reads window '{}' on actor '{}' and forwards messages to it",
reader, window.name, actor.name
),
});
}
}
}
}
}
}
}
Ok(())
}
/// Consistency: pipeline chain elements exist, stages connect validly.
fn verify_pipeline_wellformedness(program: &L1Program) -> Result<(), VerifyError> {
for pipeline in &program.pipelines {
if pipeline.stages.len() < 2 {
return Err(VerifyError {
kind: ErrorKind::MalformedPipeline,
message: format!(
"pipeline '{}' has fewer than 2 stages",
pipeline.name
),
});
}
// Check for duplicate stages (which would indicate a cycle)
let mut seen: HashSet<&str> = HashSet::new();
for stage in &pipeline.stages {
if !seen.insert(stage.as_str()) {
return Err(VerifyError {
kind: ErrorKind::PipelineCycle,
message: format!(
"pipeline '{}' contains duplicate stage '{}' (cycle detected)",
pipeline.name, stage
),
});
}
}
}
Ok(())
}
/// Liveness: every leaf input is consumed — forwarded or explicitly dropped.
/// A leaf with a process body must have at least one action (forward, read, or emit).
fn verify_buffer_conservation(program: &L1Program) -> Result<(), VerifyError> {
for leaf in &program.leaves {
if leaf.actions.is_empty() {
return Err(VerifyError {
kind: ErrorKind::BufferNotConsumed,
message: format!(
"leaf '{}' has no actions — input is not consumed",
leaf.name
),
});
}
}
Ok(())
}
/// Resource bound: steps is a positive integer, pipelines are acyclic.
fn verify_finiteness(program: &L1Program) -> Result<(), VerifyError> {
for core in &program.cores {
if core.steps == 0 {
return Err(VerifyError {
kind: ErrorKind::InvalidSteps,
message: format!(
"core '{}' has steps = 0, must be positive",
core.name
),
});
}
}
// Pipeline acyclicity: check that no entity appears as a stage across pipelines
// forming a cycle. In a single pipeline this is caught by duplicate detection above.
// Across pipelines, build a graph of entity ordering and check for cycles.
let mut edges: Vec<(&str, &str)> = Vec::new();
for pipeline in &program.pipelines {
for pair in pipeline.stages.windows(2) {
edges.push((pair[0].as_str(), pair[1].as_str()));
}
}
// Topological sort to detect cycles
let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
let mut in_degree: HashMap<&str, usize> = HashMap::new();
let mut all_nodes: HashSet<&str> = HashSet::new();
for (from, to) in &edges {
adj.entry(*from).or_default().push(*to);
in_degree.entry(*to).or_insert(0);
*in_degree.entry(*to).or_default() += 1;
in_degree.entry(*from).or_insert(0);
all_nodes.insert(*from);
all_nodes.insert(*to);
}
let mut queue: Vec<&str> = in_degree
.iter()
.filter(|(_, deg)| **deg == 0)
.map(|(node, _)| *node)
.collect();
let mut visited = 0usize;
while let Some(node) = queue.pop() {
visited += 1;
if let Some(neighbors) = adj.get(node) {
for next in neighbors {
if let Some(deg) = in_degree.get_mut(next) {
*deg -= 1;
if *deg == 0 {
queue.push(*next);
}
}
}
}
}
if visited < all_nodes.len() {
return Err(VerifyError {
kind: ErrorKind::PipelineCycle,
message: "pipeline graph contains a cycle".to_string(),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::l1_ir::tests::{build_counter_program, build_window_program};
#[test]
fn counter_passes_verification() {
let program = build_counter_program();
assert!(verify(&program).is_ok());
}
#[test]
fn window_passes_verification() {
let program = build_window_program();
assert!(verify(&program).is_ok());
}
#[test]
fn parsed_counter_passes_verification() {
let input = include_str!("../examples/counter.l1");
let program = crate::parser::parse(input).expect("should parse");
assert!(verify(&program).is_ok());
}
#[test]
fn parsed_window_passes_verification() {
let input = include_str!("../examples/window.l1");
let program = crate::parser::parse(input).expect("should parse");
assert!(verify(&program).is_ok());
}
// --- Negative test cases ---
#[test]
fn reject_dangling_actor_ref_in_core() {
let mut program = build_counter_program();
program.cores[0].actors = vec!["nonexistent_actor".to_string()];
let err = verify(&program).unwrap_err();
assert_eq!(err.kind, ErrorKind::DanglingRef);
assert!(err.message.contains("nonexistent_actor"));
}
#[test]
fn reject_duplicate_ownership() {
let mut program = build_counter_program();
// Add a second core that also claims the counter actor
program.cores.push(CoreDecl {
name: "second_core".to_string(),
actors: vec!["counter".to_string()],
leaves: vec![],
pipelines: vec![],
steps: 1,
});
let err = verify(&program).unwrap_err();
assert_eq!(err.kind, ErrorKind::DuplicateOwnership);
assert!(err.message.contains("counter"));
assert!(err.message.contains("main"));
assert!(err.message.contains("second_core"));
}
#[test]
fn reject_invalid_window_reader() {
let mut program = build_counter_program();
// Set the window reader to a non-existent leaf
program.actors[0].windows[0].readers = vec!["ghost_leaf".to_string()];
let err = verify(&program).unwrap_err();
assert_eq!(err.kind, ErrorKind::DanglingRef);
assert!(err.message.contains("ghost_leaf"));
}
#[test]
fn reject_window_reader_in_different_core() {
let mut program = build_counter_program();
// Add a new leaf and put it in a different core, then make it a window reader
program.leaves.push(LeafDecl {
name: "remote_leaf".to_string(),
reads: None,
actions: vec![LeafAction::Emit(Expr::Lit(0))],
});
program.actors[0].windows[0].readers = vec!["remote_leaf".to_string()];
// remote_leaf is NOT in the main core, put it in a second core
program.cores.push(CoreDecl {
name: "other_core".to_string(),
actors: vec![],
leaves: vec!["remote_leaf".to_string()],
pipelines: vec![],
steps: 1,
});
let err = verify(&program).unwrap_err();
assert_eq!(err.kind, ErrorKind::InvalidWindowReader);
assert!(err.message.contains("remote_leaf"));
assert!(err.message.contains("not in the same core"));
}
#[test]
fn reject_malformed_pipeline_single_stage() {
let mut program = build_counter_program();
program.pipelines[0].stages = vec!["ticker".to_string()];
let err = verify(&program).unwrap_err();
assert_eq!(err.kind, ErrorKind::MalformedPipeline);
assert!(err.message.contains("fewer than 2"));
}
#[test]
fn reject_empty_leaf_actions() {
let mut program = build_counter_program();
program.leaves[0].actions = vec![];
let err = verify(&program).unwrap_err();
assert_eq!(err.kind, ErrorKind::BufferNotConsumed);
assert!(err.message.contains("ticker"));
}
#[test]
fn reject_zero_steps() {
let mut program = build_counter_program();
program.cores[0].steps = 0;
let err = verify(&program).unwrap_err();
assert_eq!(err.kind, ErrorKind::InvalidSteps);
assert!(err.message.contains("steps = 0"));
}
#[test]
fn reject_pipeline_cycle() {
let mut program = build_counter_program();
// Create a cycle across two pipelines: a -> b and b -> a
program.leaves.push(LeafDecl {
name: "leaf_a".to_string(),
reads: None,
actions: vec![LeafAction::Emit(Expr::Lit(0))],
});
program.leaves.push(LeafDecl {
name: "leaf_b".to_string(),
reads: None,
actions: vec![LeafAction::Emit(Expr::Lit(0))],
});
program.cores[0].leaves.push("leaf_a".to_string());
program.cores[0].leaves.push("leaf_b".to_string());
program.pipelines = vec![
PipelineDecl {
name: "p1".to_string(),
stages: vec!["leaf_a".to_string(), "leaf_b".to_string()],
},
PipelineDecl {
name: "p2".to_string(),
stages: vec!["leaf_b".to_string(), "leaf_a".to_string()],
},
];
program.cores[0].pipelines = vec!["p1".to_string(), "p2".to_string()];
let err = verify(&program).unwrap_err();
assert_eq!(err.kind, ErrorKind::PipelineCycle);
assert!(err.message.contains("cycle"));
}
#[test]
fn reject_window_writer_conflict() {
// A leaf that both reads a window on an actor AND forwards messages to it
let program = L1Program {
actors: vec![ActorDecl {
name: "my_actor".to_string(),
state: vec![StateField {
name: "val".to_string(),
ty: FieldType::U64,
init: 0,
}],
windows: vec![WindowDecl {
name: "view".to_string(),
fields: vec!["val".to_string()],
readers: vec!["bad_leaf".to_string()],
}],
handlers: vec![MessageHandler {
message_type: "Update".to_string(),
args: vec![("x".to_string(), FieldType::U64)],
body: Expr::Assign(
"val".to_string(),
Box::new(Expr::Var("x".to_string())),
),
}],
}],
leaves: vec![
LeafDecl {
name: "bad_leaf".to_string(),
reads: Some(ReadClause {
actor: "my_actor".to_string(),
window: "view".to_string(),
}),
actions: vec![
LeafAction::ReadWindow {
actor: "my_actor".to_string(),
window: "view".to_string(),
field: "val".to_string(),
},
LeafAction::Forward {
target: "my_actor".to_string(),
message_type: "Update".to_string(),
args: vec![Expr::Lit(1)],
},
],
},
LeafDecl {
name: "other_leaf".to_string(),
reads: None,
actions: vec![LeafAction::Emit(Expr::Lit(0))],
},
],
pipelines: vec![PipelineDecl {
name: "main".to_string(),
stages: vec!["bad_leaf".to_string(), "my_actor".to_string()],
}],
cores: vec![CoreDecl {
name: "main".to_string(),
actors: vec!["my_actor".to_string()],
leaves: vec!["bad_leaf".to_string(), "other_leaf".to_string()],
pipelines: vec!["main".to_string()],
steps: 1,
}],
};
let err = verify(&program).unwrap_err();
assert_eq!(err.kind, ErrorKind::WindowWriterConflict);
assert!(err.message.contains("bad_leaf"));
assert!(err.message.contains("reads window"));
assert!(err.message.contains("forwards messages"));
}
#[test]
fn reject_dangling_pipeline_entity() {
let mut program = build_counter_program();
program.pipelines[0].stages = vec![
"ticker".to_string(),
"phantom".to_string(),
"display".to_string(),
];
let err = verify(&program).unwrap_err();
assert_eq!(err.kind, ErrorKind::DanglingRef);
assert!(err.message.contains("phantom"));
}
#[test]
fn reject_dangling_leaf_forward_target() {
let mut program = build_counter_program();
program.leaves[0].actions = vec![LeafAction::Forward {
target: "void".to_string(),
message_type: "Msg".to_string(),
args: vec![Expr::Lit(1)],
}];
let err = verify(&program).unwrap_err();
assert_eq!(err.kind, ErrorKind::DanglingRef);
assert!(err.message.contains("void"));
}
}