From f20876af8620c3cc8129a18e9903ed548d364aa1 Mon Sep 17 00:00:00 2001 From: Zachery Aaron Shores-Chmielewski Date: Mon, 2 Mar 2026 22:46:50 +0700 Subject: [PATCH] feat: L1 prototype --- .gitignore | 1 + Cargo.lock | 300 ++++++ Cargo.toml | 7 + examples/counter.l1 | 36 + examples/product.l1 | 50 + examples/window.l1 | 37 + spec.md | 865 +++++++++++++++++ src/codegen.rs | 1246 +++++++++++++++++++++++++ src/emit.rs | 770 +++++++++++++++ src/interpret_l1.rs | 556 +++++++++++ src/l0_ir.rs | 603 ++++++++++++ src/l1_ir.rs | 499 ++++++++++ src/lib.rs | 11 + src/main.rs | 78 ++ src/parser.rs | 816 ++++++++++++++++ src/pipeline.rs | 307 ++++++ src/repl.rs | 1849 +++++++++++++++++++++++++++++++++++++ src/translate_validate.rs | 1127 ++++++++++++++++++++++ src/verify_l0.rs | 642 +++++++++++++ src/verify_l1.rs | 680 ++++++++++++++ 20 files changed, 10480 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 examples/counter.l1 create mode 100644 examples/product.l1 create mode 100644 examples/window.l1 create mode 100644 spec.md create mode 100644 src/codegen.rs create mode 100644 src/emit.rs create mode 100644 src/interpret_l1.rs create mode 100644 src/l0_ir.rs create mode 100644 src/l1_ir.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/parser.rs create mode 100644 src/pipeline.rs create mode 100644 src/repl.rs create mode 100644 src/translate_validate.rs create mode 100644 src/verify_l0.rs create mode 100644 src/verify_l1.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f9b8a29 --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d0f2365 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "l0-cpu-runtime" +version = "0.1.0" +edition = "2021" + +[dependencies] +crossterm = "0.28" diff --git a/examples/counter.l1 b/examples/counter.l1 new file mode 100644 index 0000000..7440bb5 --- /dev/null +++ b/examples/counter.l1 @@ -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 +} diff --git a/examples/product.l1 b/examples/product.l1 new file mode 100644 index 0000000..59125a1 --- /dev/null +++ b/examples/product.l1 @@ -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 +} diff --git a/examples/window.l1 b/examples/window.l1 new file mode 100644 index 0000000..f0a0a70 --- /dev/null +++ b/examples/window.l1 @@ -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 +} diff --git a/spec.md b/spec.md new file mode 100644 index 0000000..7bde21a --- /dev/null +++ b/spec.md @@ -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 { + : + ... +} + +model { + in: + out: + properties: [, ...] +} + +loss { + : (), weight: + ... + combine: +} + +goal: minimize() | maximize() | pareto([]) +``` + +### 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 + models: Map + losses: Map + goal: Goal + hints: Map +} +``` + +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 { + sample: + infer: + backward: + update: +} +``` + +#### Sample Methods + +``` +epoch_shuffle(batch: n, drop_last: bool) +stream_window(window: n, stride: n) +curriculum([{ filter: , duration: n_steps }, ...]) +replay(buffer: n, priority: ) +distributed_shard(shards: n, method: ) +``` + +#### Infer Methods (Architecture) + +``` +sequential([, ...]) +residual([, ...]) +diffusion_chain(steps: n, noise: , denoiser: ) +recurrent(cell: , steps: n | until()) +graph(nodes: , edges: ) +mixture(experts: [], gate: ) +``` + +#### 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: []) +evolution(pop: n, sigma: f, selection: ) +dfa(feedback_dim: n) +reinforce(baseline: ) +straight_through(estimator: ) +zeroth_order(samples: n, sigma: f) +hybrid(primary: , aux: , blend: ) +``` + +`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: , inner: ) +gradient_clip(max_norm: f, inner: ) +ema(decay: f, inner: ) +``` + +### IR + +``` +L2_IR = { + sample: SampleNode + infer: InferGraph DAG of layer nodes + backward: BackwardDecl + update: UpdateChain + shapes: Map propagated during verification + hints: Map +} +``` + +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 { + node { kind: , } + ... + edges { + -> : bandwidth // unidirectional + <-> : bandwidth // 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 { + : @ , path: + ... +} +``` + +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 on { + state { + : , + ... + } + + window : () + readers() + + on () { + + } +} +``` + +**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 on { + reads: // optional window reads + + fn process() -> forward(, ) + | Drop() { + + } +} +``` + +Buffer types carry region provenance: `buf`, `buf`. 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 on { + reads: + + fn compute() -> () { + 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 { + -> -> ... +} +``` + +### DSL Forms — Schedule + +``` +schedule { + pipelines: [] + actors: [] + pipeline_depth: +} +``` + +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 hardware graph + data_sources: Map + placement: Map where each computation lives + actors: Map + stages: Map + tensor_graphs: Map lazy compute DAGs + pipelines: [Pipeline] ordered stage/graph chains + schedule: ScheduleDecl + hints: Map +} +``` + +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` 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 + kernel_plan: [KernelDecl] + stream_plan: Map + 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 `#[::]`: + +``` +// 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 + + 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 diff --git a/src/codegen.rs b/src/codegen.rs new file mode 100644 index 0000000..69aad46 --- /dev/null +++ b/src/codegen.rs @@ -0,0 +1,1246 @@ +use crate::l0_ir::*; +use std::collections::HashMap; + +/// Errors during x86-64 code generation. +#[derive(Debug, Clone, PartialEq)] +pub struct CodegenError { + pub message: String, +} + +impl std::fmt::Display for CodegenError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "codegen error: {}", self.message) + } +} + +impl std::error::Error for CodegenError {} + +/// Physical x86-64 registers used for register allocation. +/// We use callee-saved and caller-saved registers that don't conflict +/// with the region base pointer passing convention. +/// +/// Convention: +/// - rdi = pointer to region base addresses array (passed by caller) +/// - We load region base addresses into dedicated registers at entry. +/// - SSA registers map to physical registers from a pool. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum PhysReg { + Rax = 0, + Rcx = 1, + Rdx = 2, + Rbx = 3, + Rsp = 4, // not used for allocation + Rbp = 5, // not used for allocation + Rsi = 6, + Rdi = 7, + R8 = 8, + R9 = 9, + R10 = 10, + R11 = 11, + R12 = 12, + R13 = 13, + R14 = 14, + R15 = 15, +} + +impl PhysReg { + /// REX.B or REX.R bit needed for this register (registers r8-r15). + fn needs_rex_ext(self) -> bool { + (self as u8) >= 8 + } + + /// The 3-bit register encoding (low 3 bits of register number). + fn encoding(self) -> u8 { + (self as u8) & 0x7 + } +} + +/// The pool of registers available for SSA register allocation. +/// Excludes rsp, rbp (frame), rdi (region array pointer at entry). +const ALLOC_REGS: &[PhysReg] = &[ + PhysReg::Rax, + PhysReg::Rcx, + PhysReg::Rdx, + PhysReg::Rbx, + PhysReg::Rsi, + PhysReg::R8, + PhysReg::R9, + PhysReg::R10, + PhysReg::R11, + PhysReg::R12, + PhysReg::R13, + PhysReg::R14, + PhysReg::R15, +]; + +/// Maps SSA registers to physical registers. Simple linear allocation — +/// the spec says ~5 live values, so no spills needed. +#[allow(dead_code)] +struct RegMap { + map: HashMap, + next_idx: usize, +} + +#[allow(dead_code)] +impl RegMap { + fn new() -> Self { + RegMap { + map: HashMap::new(), + next_idx: 0, + } + } + + fn get_or_alloc(&mut self, ssa: Register) -> Result { + if let Some(&phys) = self.map.get(&ssa) { + return Ok(phys); + } + if self.next_idx >= ALLOC_REGS.len() { + return Err(CodegenError { + message: format!("register pressure exceeded: cannot allocate {}", ssa), + }); + } + let phys = ALLOC_REGS[self.next_idx]; + self.next_idx += 1; + self.map.insert(ssa, phys); + Ok(phys) + } +} + +/// Assembled x86-64 machine code with metadata for execution. +pub struct MachineCode { + /// The raw x86-64 bytes. + pub code: Vec, + /// Region names in order — caller must provide region memory pointers + /// in this order via the regions_array argument. + pub region_order: Vec, + /// Region sizes for allocation. + pub region_sizes: Vec, +} + +impl MachineCode { + /// Hex dump of the machine code bytes. + pub fn hex_dump(&self) -> String { + self.code + .iter() + .map(|b| format!("{:02x}", b)) + .collect::>() + .join(" ") + } +} + +/// Result of executing machine code — region memory after execution. +pub struct ExecutionResult { + /// Region memory buffers after execution, indexed by region name. + pub regions: HashMap>, +} + +impl ExecutionResult { + /// Read a u64 from a region at a given byte offset. + pub fn read_u64(&self, region: &str, offset: usize) -> Option { + let buf = self.regions.get(region)?; + if offset + 8 > buf.len() { + return None; + } + Some(u64::from_le_bytes(buf[offset..offset + 8].try_into().unwrap())) + } + + /// Read all output values from the output region. + /// The output region uses queue layout: [head: u64, tail: u64, data...] + /// Head tracks how many values were pushed. Data starts at offset 16. + pub fn read_output_values(&self) -> Vec { + if let Some(buf) = self.regions.get("output") { + if buf.len() < 16 { + return vec![]; + } + let count = u64::from_le_bytes(buf[0..8].try_into().unwrap()) as usize; + let mut vals = Vec::with_capacity(count); + for i in 0..count { + let off = 16 + i * 8; + if off + 8 <= buf.len() { + vals.push(u64::from_le_bytes(buf[off..off + 8].try_into().unwrap())); + } + } + vals + } else { + vec![] + } + } +} + +// ── x86-64 Encoding Helpers ──────────────────────────────────────────── + +/// Emit REX prefix for 64-bit operations. +/// REX.W = 1 (64-bit operand size), plus optional R, X, B extension bits. +fn rex_w(reg: PhysReg, rm: PhysReg) -> u8 { + let mut rex: u8 = 0x48; // REX.W + if reg.needs_rex_ext() { + rex |= 0x04; // REX.R + } + if rm.needs_rex_ext() { + rex |= 0x01; // REX.B + } + rex +} + +/// REX prefix for single-operand instructions. +fn rex_w_rm(rm: PhysReg) -> u8 { + let mut rex: u8 = 0x48; + if rm.needs_rex_ext() { + rex |= 0x01; + } + rex +} + +/// ModR/M byte: mod=11 (register direct), reg, r/m. +fn modrm_rr(reg: PhysReg, rm: PhysReg) -> u8 { + 0xC0 | (reg.encoding() << 3) | rm.encoding() +} + +/// ModR/M byte: mod=10 (register + disp32), reg, r/m. +fn modrm_mem_disp32(reg: PhysReg, base: PhysReg) -> u8 { + 0x80 | (reg.encoding() << 3) | base.encoding() +} + +/// ModR/M byte: mod=00 (register indirect), reg, r/m. +#[allow(dead_code)] +fn modrm_mem(reg: PhysReg, base: PhysReg) -> u8 { + (reg.encoding() << 3) | base.encoding() +} + +// ── Single Instruction Encoders ──────────────────────────────────────── + +/// Encode: mov reg, imm64 (REX.W + B8+rd, imm64) +pub fn encode_mov_imm64(dest: PhysReg, imm: u64) -> Vec { + let mut buf = Vec::with_capacity(10); + buf.push(rex_w_rm(dest)); + buf.push(0xB8 + dest.encoding()); + buf.extend_from_slice(&imm.to_le_bytes()); + buf +} + +/// Encode: mov [base+disp32], reg (store 64-bit) +pub fn encode_store64(base: PhysReg, disp: i32, src: PhysReg) -> Vec { + let mut buf = Vec::with_capacity(7); + buf.push(rex_w(src, base)); + buf.push(0x89); // MOV r/m64, r64 + if base == PhysReg::Rsp || base == PhysReg::R12 { + // RSP/R12 as base needs SIB byte + buf.push(modrm_mem_disp32(src, base)); + buf.push(0x24); // SIB: base=RSP, index=none + } else { + buf.push(modrm_mem_disp32(src, base)); + } + buf.extend_from_slice(&disp.to_le_bytes()); + buf +} + +/// Encode: mov reg, [base+disp32] (load 64-bit) +pub fn encode_load64(dest: PhysReg, base: PhysReg, disp: i32) -> Vec { + let mut buf = Vec::with_capacity(7); + buf.push(rex_w(dest, base)); + buf.push(0x8B); // MOV r64, r/m64 + if base == PhysReg::Rsp || base == PhysReg::R12 { + buf.push(modrm_mem_disp32(dest, base)); + buf.push(0x24); + } else { + buf.push(modrm_mem_disp32(dest, base)); + } + buf.extend_from_slice(&disp.to_le_bytes()); + buf +} + +/// Encode: add dest, src (64-bit) +pub fn encode_add_rr(dest: PhysReg, src: PhysReg) -> Vec { + vec![rex_w(src, dest), 0x01, modrm_rr(src, dest)] +} + +/// Encode: sub dest, src (64-bit) +pub fn encode_sub_rr(dest: PhysReg, src: PhysReg) -> Vec { + vec![rex_w(src, dest), 0x29, modrm_rr(src, dest)] +} + +/// Encode: imul dest, src (64-bit) +pub fn encode_imul_rr(dest: PhysReg, src: PhysReg) -> Vec { + vec![rex_w(dest, src), 0x0F, 0xAF, modrm_rr(dest, src)] +} + +/// Encode: and dest, src (64-bit) +pub fn encode_and_rr(dest: PhysReg, src: PhysReg) -> Vec { + vec![rex_w(src, dest), 0x21, modrm_rr(src, dest)] +} + +/// Encode: or dest, src (64-bit) +pub fn encode_or_rr(dest: PhysReg, src: PhysReg) -> Vec { + vec![rex_w(src, dest), 0x09, modrm_rr(src, dest)] +} + +/// Encode: shl dest, cl (64-bit, shift by CL register) +pub fn encode_shl_cl(dest: PhysReg) -> Vec { + vec![rex_w_rm(dest), 0xD3, 0xC0 | (4 << 3) | dest.encoding()] +} + +/// Encode: cmp a, b (64-bit) +pub fn encode_cmp_rr(a: PhysReg, b: PhysReg) -> Vec { + vec![rex_w(b, a), 0x39, modrm_rr(b, a)] +} + +/// Encode: setl dest (set byte if less than — signed) +pub fn encode_setl(dest: PhysReg) -> Vec { + let mut buf = Vec::new(); + if dest.needs_rex_ext() { + buf.push(0x41); // REX.B + } + buf.push(0x0F); + buf.push(0x9C); + buf.push(0xC0 | dest.encoding()); + buf +} + +/// Encode: sete dest (set byte if equal) +pub fn encode_sete(dest: PhysReg) -> Vec { + let mut buf = Vec::new(); + if dest.needs_rex_ext() { + buf.push(0x41); + } + buf.push(0x0F); + buf.push(0x94); + buf.push(0xC0 | dest.encoding()); + buf +} + +/// Encode: setne dest (set byte if not equal) +pub fn encode_setne(dest: PhysReg) -> Vec { + let mut buf = Vec::new(); + if dest.needs_rex_ext() { + buf.push(0x41); + } + buf.push(0x0F); + buf.push(0x95); + buf.push(0xC0 | dest.encoding()); + buf +} + +/// Encode: setg dest (set byte if greater — signed) +pub fn encode_setg(dest: PhysReg) -> Vec { + let mut buf = Vec::new(); + if dest.needs_rex_ext() { + buf.push(0x41); + } + buf.push(0x0F); + buf.push(0x9F); + buf.push(0xC0 | dest.encoding()); + buf +} + +/// Encode: movzx dest, dest_byte (zero-extend byte to 64-bit) +pub fn encode_movzx_byte(dest: PhysReg) -> Vec { + vec![rex_w(dest, dest), 0x0F, 0xB6, modrm_rr(dest, dest)] +} + +/// Encode: jne rel32 (conditional jump, not equal / nonzero) +pub fn encode_jne_rel32(rel: i32) -> Vec { + let mut buf = vec![0x0F, 0x85]; + buf.extend_from_slice(&rel.to_le_bytes()); + buf +} + +/// Encode: je rel32 (conditional jump, equal / zero) +pub fn encode_je_rel32(rel: i32) -> Vec { + let mut buf = vec![0x0F, 0x84]; + buf.extend_from_slice(&rel.to_le_bytes()); + buf +} + +/// Encode: jmp rel32 (unconditional jump) +pub fn encode_jmp_rel32(rel: i32) -> Vec { + let mut buf = vec![0xE9]; + buf.extend_from_slice(&rel.to_le_bytes()); + buf +} + +/// Encode: ret +pub fn encode_ret() -> Vec { + vec![0xC3] +} + +/// Encode: mov dest, src (register to register, 64-bit) +pub fn encode_mov_rr(dest: PhysReg, src: PhysReg) -> Vec { + vec![rex_w(src, dest), 0x89, modrm_rr(src, dest)] +} + +/// Encode: add reg, imm32 (64-bit, sign-extended) +pub fn encode_add_imm32(dest: PhysReg, imm: i32) -> Vec { + let mut buf = Vec::new(); + buf.push(rex_w_rm(dest)); + buf.push(0x81); + buf.push(0xC0 | dest.encoding()); // ModR/M: mod=11, reg=0 (ADD), r/m=dest + buf.extend_from_slice(&imm.to_le_bytes()); + buf +} + +// ── Full Program Code Generation ─────────────────────────────────────── + +/// Generate x86-64 machine code from a verified L0 program. +/// +/// Calling convention: the generated function takes a single argument (rdi) +/// which points to an array of pointers, one per region, in the order +/// specified by `region_order`. +/// +/// fn generated(regions: *const *mut u8) +pub fn codegen(program: &L0Program) -> Result { + let region_order: Vec = program.regions.iter().map(|r| r.name.clone()).collect(); + let region_sizes: Vec = program.regions.iter().map(|r| r.size).collect(); + + // Build region index: region name → index in the regions array + // Build block index: block name → block index + let block_idx: HashMap<&str, usize> = program + .blocks + .iter() + .enumerate() + .map(|(i, b)| (b.name.as_str(), i)) + .collect(); + + // First pass: generate code for each block, collecting fixups for jumps. + // We'll use a two-pass approach: + // 1. Generate code for each block, recording jump/branch locations that need fixup. + // 2. Patch relative offsets. + + let mut code = Vec::new(); + let mut block_offsets: Vec = Vec::new(); + // We reserve rdi for the initial region array pointer. + // At function entry, load each region's base address from the array into + // dedicated registers. But we only have ~13 allocatable registers, so for + // programs with many regions, we reload from the array as needed. + // + // Strategy: for each region, we store its base pointer on the stack or + // in a register. Since we have few regions (typically 3-5), we use + // a simple approach: load region base addresses into a set of dedicated + // registers at function entry, before we start mapping SSA registers. + + // Reserve registers for region base addresses. + // Use R12-R15 and RBX for region bases (callee-saved, so we should + // push/pop them, but for simplicity in this mmap'd function we skip that). + let region_base_regs: Vec = vec![ + PhysReg::R12, + PhysReg::R13, + PhysReg::R14, + PhysReg::R15, + PhysReg::Rbx, + PhysReg::Rsi, + ]; + + if program.regions.len() > region_base_regs.len() { + return Err(CodegenError { + message: format!( + "too many regions ({}) for available base registers ({})", + program.regions.len(), + region_base_regs.len() + ), + }); + } + + let region_base: HashMap<&str, PhysReg> = program + .regions + .iter() + .enumerate() + .map(|(i, r)| (r.name.as_str(), region_base_regs[i])) + .collect(); + + // Mark region base registers as used so SSA allocation avoids them + let ssa_alloc_regs: Vec = ALLOC_REGS + .iter() + .copied() + .filter(|r| !region_base.values().any(|rb| rb == r)) + .collect(); + + // Custom SSA allocator that only uses non-region registers + let mut ssa_map: HashMap = HashMap::new(); + let mut ssa_next = 0usize; + + let mut alloc_ssa = |ssa: Register| -> Result { + if let Some(&phys) = ssa_map.get(&ssa) { + return Ok(phys); + } + if ssa_next >= ssa_alloc_regs.len() { + // Reuse from the beginning — simple round-robin for high-numbered SSA regs + ssa_next = 0; + } + let phys = ssa_alloc_regs[ssa_next]; + ssa_next += 1; + ssa_map.insert(ssa, phys); + Ok(phys) + }; + + // Prologue: save callee-saved registers we use, then load region base addresses. + // Callee-saved in System V AMD64: rbx, rbp, r12-r15 + let callee_saved_used: Vec = vec![ + PhysReg::Rbx, PhysReg::Rbp, + PhysReg::R12, PhysReg::R13, PhysReg::R14, PhysReg::R15, + ]; + for ® in &callee_saved_used { + // push reg: REX? + 0x50+rd + if reg.needs_rex_ext() { + code.push(0x41); + } + code.push(0x50 + reg.encoding()); + } + + // Load region base addresses from the array pointed to by rdi. + // rdi points to: [ptr0, ptr1, ptr2, ...] (each 8 bytes) + for (i, region) in program.regions.iter().enumerate() { + let base_reg = region_base[region.name.as_str()]; + // mov base_reg, [rdi + i*8] + code.extend_from_slice(&encode_load64(base_reg, PhysReg::Rdi, (i * 8) as i32)); + } + + // Track jump fixups: (code_offset_of_rel32, target_block_name) + struct JumpFixup { + /// Offset in `code` where the rel32 displacement starts. + code_offset: usize, + /// The block name this jump targets. + target_block: String, + /// Size of the entire jump instruction (for calculating IP after instruction). + instr_end_offset: usize, + } + let mut fixups: Vec = Vec::new(); + + // Generate code for each block + for (_bi, block) in program.blocks.iter().enumerate() { + block_offsets.push(code.len()); + + for op in &block.ops { + match op { + Op::LoadImm { value, dest } => { + let d = alloc_ssa(*dest)?; + code.extend_from_slice(&encode_mov_imm64(d, *value)); + } + Op::Load { size, region, offset, dest } => { + let base = *region_base.get(region.as_str()).ok_or_else(|| { + CodegenError { + message: format!("unknown region '{}'", region), + } + })?; + let d = alloc_ssa(*dest)?; + match size { + OpSize::B64 => { + code.extend_from_slice(&encode_load64(d, base, *offset as i32)); + } + _ => { + // For simplicity, treat all sizes as 64-bit loads + code.extend_from_slice(&encode_load64(d, base, *offset as i32)); + } + } + } + Op::Store { size, region, offset, src } => { + let base = *region_base.get(region.as_str()).ok_or_else(|| { + CodegenError { + message: format!("unknown region '{}'", region), + } + })?; + let s = alloc_ssa(*src)?; + match size { + OpSize::B64 => { + code.extend_from_slice(&encode_store64(base, *offset as i32, s)); + } + _ => { + code.extend_from_slice(&encode_store64(base, *offset as i32, s)); + } + } + } + Op::Arith { kind, lhs, rhs, dest } => { + let l = alloc_ssa(*lhs)?; + let r = alloc_ssa(*rhs)?; + let d = alloc_ssa(*dest)?; + // mov dest, lhs + if d != l { + code.extend_from_slice(&encode_mov_rr(d, l)); + } + match kind { + ArithKind::Add => code.extend_from_slice(&encode_add_rr(d, r)), + ArithKind::Sub => code.extend_from_slice(&encode_sub_rr(d, r)), + ArithKind::Mul => code.extend_from_slice(&encode_imul_rr(d, r)), + ArithKind::And => code.extend_from_slice(&encode_and_rr(d, r)), + ArithKind::Or => code.extend_from_slice(&encode_or_rr(d, r)), + ArithKind::Shl => { + // shl requires count in CL. Move rhs to RCX first. + if r != PhysReg::Rcx { + code.extend_from_slice(&encode_mov_rr(PhysReg::Rcx, r)); + } + code.extend_from_slice(&encode_shl_cl(d)); + } + } + } + Op::ArithImm { kind, lhs, imm, dest } => { + let l = alloc_ssa(*lhs)?; + let d = alloc_ssa(*dest)?; + // mov dest, lhs + if d != l { + code.extend_from_slice(&encode_mov_rr(d, l)); + } + match kind { + ArithKind::Add => { + code.extend_from_slice(&encode_add_imm32(d, *imm as i32)); + } + ArithKind::Sub => { + // sub reg, imm32: opcode 0x81 /5 + let mut buf = Vec::new(); + buf.push(rex_w_rm(d)); + buf.push(0x81); + buf.push(0xE8 | d.encoding()); // ModR/M: mod=11, reg=5 (SUB), r/m=dest + buf.extend_from_slice(&(*imm as i32).to_le_bytes()); + code.extend_from_slice(&buf); + } + _ => { + // For other immediate arith, load imm into a temp register and use reg-reg + // Use rdi as temp since we're past the prologue + code.extend_from_slice(&encode_mov_imm64(PhysReg::Rdi, *imm)); + match kind { + ArithKind::Mul => code.extend_from_slice(&encode_imul_rr(d, PhysReg::Rdi)), + ArithKind::And => code.extend_from_slice(&encode_and_rr(d, PhysReg::Rdi)), + ArithKind::Or => code.extend_from_slice(&encode_or_rr(d, PhysReg::Rdi)), + ArithKind::Shl => { + code.extend_from_slice(&encode_mov_rr(PhysReg::Rcx, PhysReg::Rdi)); + code.extend_from_slice(&encode_shl_cl(d)); + } + _ => unreachable!(), + } + } + } + } + Op::Cmp { kind, lhs, rhs, dest } => { + let l = alloc_ssa(*lhs)?; + let r = alloc_ssa(*rhs)?; + let d = alloc_ssa(*dest)?; + code.extend_from_slice(&encode_cmp_rr(l, r)); + match kind { + CmpKind::Lt => code.extend_from_slice(&encode_setl(d)), + CmpKind::Eq => code.extend_from_slice(&encode_sete(d)), + CmpKind::Neq => code.extend_from_slice(&encode_setne(d)), + CmpKind::Gt => code.extend_from_slice(&encode_setg(d)), + } + code.extend_from_slice(&encode_movzx_byte(d)); + } + Op::CmpImm { kind, lhs, imm, dest } => { + let l = alloc_ssa(*lhs)?; + let d = alloc_ssa(*dest)?; + // Load imm into rdi (temp), then cmp + code.extend_from_slice(&encode_mov_imm64(PhysReg::Rdi, *imm)); + code.extend_from_slice(&encode_cmp_rr(l, PhysReg::Rdi)); + match kind { + CmpKind::Lt => code.extend_from_slice(&encode_setl(d)), + CmpKind::Eq => code.extend_from_slice(&encode_sete(d)), + CmpKind::Neq => code.extend_from_slice(&encode_setne(d)), + CmpKind::Gt => code.extend_from_slice(&encode_setg(d)), + } + code.extend_from_slice(&encode_movzx_byte(d)); + } + Op::Branch { cond, true_target, false_target } => { + let c = alloc_ssa(*cond)?; + // test cond, cond + code.push(rex_w(c, c)); + code.push(0x85); + code.push(modrm_rr(c, c)); + // jne true_target (6 bytes: 0F 85 rel32) + let jne_start = code.len(); + code.extend_from_slice(&encode_jne_rel32(0)); // placeholder + let jne_end = code.len(); + fixups.push(JumpFixup { + code_offset: jne_start + 2, // rel32 starts after opcode bytes + target_block: true_target.clone(), + instr_end_offset: jne_end, + }); + // jmp false_target (5 bytes: E9 rel32) + let jmp_start = code.len(); + code.extend_from_slice(&encode_jmp_rel32(0)); // placeholder + let jmp_end = code.len(); + fixups.push(JumpFixup { + code_offset: jmp_start + 1, + target_block: false_target.clone(), + instr_end_offset: jmp_end, + }); + } + Op::Jump { target } => { + let jmp_start = code.len(); + code.extend_from_slice(&encode_jmp_rel32(0)); // placeholder + let jmp_end = code.len(); + fixups.push(JumpFixup { + code_offset: jmp_start + 1, + target_block: target.clone(), + instr_end_offset: jmp_end, + }); + } + Op::QueuePush { region, src } => { + // Queue layout (ring buffer): + // [0..8]: head (write position) + // [8..16]: tail (read position) + // [16..]: data slots (each 8 bytes) + // + // Push: store value at [base + 16 + head*8], increment head + let base = *region_base.get(region.as_str()).ok_or_else(|| { + CodegenError { + message: format!("unknown region '{}'", region), + } + })?; + let s = alloc_ssa(*src)?; + + // Load head into rdi (temp) + code.extend_from_slice(&encode_load64(PhysReg::Rdi, base, 0)); + // Compute offset: head * 8 + 16. Use rdi for head value. + // Save src value if it happens to be in rdi — but we don't use rdi for SSA + // rdi = head; compute rdi*8+16 for the store offset + // We need: mov [base + 16 + head*8], src + // Approach: compute address = base + 16 + head*8 into a scratch reg + // Use rbp as scratch (we're not using a frame pointer) + code.extend_from_slice(&encode_mov_rr(PhysReg::Rbp, PhysReg::Rdi)); // rbp = head + // rbp = rbp * 8 (shl rbp, 3) + code.extend_from_slice(&encode_mov_imm64(PhysReg::Rcx, 3)); // cl = 3 for shift + // But we can't use Rcx if it's allocated to an SSA reg. + // Simpler: use imul rbp, rbp, 8 — but x86 imul r64, r/m64, imm8 is 0x6B + // Let's use shl with immediate instead. + // shl rbp, 3: REX.W 0xC1 /4 ib + code.push(rex_w_rm(PhysReg::Rbp)); + code.push(0xC1); + code.push(0xC0 | (4 << 3) | PhysReg::Rbp.encoding()); // /4 = SHL + code.push(3); // shift by 3 = multiply by 8 + + // rbp = rbp + 16 + code.extend_from_slice(&encode_add_imm32(PhysReg::Rbp, 16)); + // rbp = rbp + base + code.extend_from_slice(&encode_add_rr(PhysReg::Rbp, base)); + // mov [rbp], src — store at computed address + // mov [rbp+0], src + code.push(rex_w(s, PhysReg::Rbp)); + code.push(0x89); + // Use mod=01 disp8=0 for rbp (which normally means disp32 in mod=00) + code.push(0x40 | (s.encoding() << 3) | PhysReg::Rbp.encoding()); // mod=01, disp8 + code.push(0x00); // disp8 = 0 + + // Increment head: head += 1 + code.extend_from_slice(&encode_add_imm32(PhysReg::Rdi, 1)); + // Store head back + code.extend_from_slice(&encode_store64(base, 0, PhysReg::Rdi)); + } + Op::QueuePop { region, dest } => { + // Pop: load value from [base + 16 + tail*8], increment tail + let base = *region_base.get(region.as_str()).ok_or_else(|| { + CodegenError { + message: format!("unknown region '{}'", region), + } + })?; + let d = alloc_ssa(*dest)?; + + // Load tail into rdi + code.extend_from_slice(&encode_load64(PhysReg::Rdi, base, 8)); + // Compute offset: tail*8 + 16 + code.extend_from_slice(&encode_mov_rr(PhysReg::Rbp, PhysReg::Rdi)); + // shl rbp, 3 + code.push(rex_w_rm(PhysReg::Rbp)); + code.push(0xC1); + code.push(0xC0 | (4 << 3) | PhysReg::Rbp.encoding()); + code.push(3); + // rbp += 16 + code.extend_from_slice(&encode_add_imm32(PhysReg::Rbp, 16)); + // rbp += base + code.extend_from_slice(&encode_add_rr(PhysReg::Rbp, base)); + // load value: mov d, [rbp+0] + code.push(rex_w(d, PhysReg::Rbp)); + code.push(0x8B); + code.push(0x40 | (d.encoding() << 3) | PhysReg::Rbp.encoding()); + code.push(0x00); + + // Increment tail + code.extend_from_slice(&encode_add_imm32(PhysReg::Rdi, 1)); + code.extend_from_slice(&encode_store64(base, 8, PhysReg::Rdi)); + } + Op::Hash { src, dest } => { + // Simple hash: dest = src * 2654435761 (Knuth multiplicative hash) + let s = alloc_ssa(*src)?; + let d = alloc_ssa(*dest)?; + code.extend_from_slice(&encode_mov_imm64(PhysReg::Rdi, 2654435761)); + if d != s { + code.extend_from_slice(&encode_mov_rr(d, s)); + } + code.extend_from_slice(&encode_imul_rr(d, PhysReg::Rdi)); + } + Op::Terminate => { + // Epilogue: restore callee-saved registers in reverse order + for ® in callee_saved_used.iter().rev() { + // pop reg: REX? + 0x58+rd + if reg.needs_rex_ext() { + code.push(0x41); + } + code.push(0x58 + reg.encoding()); + } + code.extend_from_slice(&encode_ret()); + } + } + } + } + + // Patch jump fixups + for fixup in &fixups { + let target_idx = *block_idx.get(fixup.target_block.as_str()).ok_or_else(|| { + CodegenError { + message: format!("unknown jump target block '{}'", fixup.target_block), + } + })?; + let target_offset = block_offsets[target_idx]; + let rel = (target_offset as i64) - (fixup.instr_end_offset as i64); + let rel32 = rel as i32; + code[fixup.code_offset..fixup.code_offset + 4] + .copy_from_slice(&rel32.to_le_bytes()); + } + + Ok(MachineCode { + code, + region_order, + region_sizes, + }) +} + +/// Execute generated machine code via mmap. +/// +/// Allocates region memory, maps the code as executable, calls it, +/// and returns the resulting region memory. +pub fn execute(mc: &MachineCode) -> Result { + if mc.code.is_empty() { + return Err(CodegenError { + message: "empty machine code".to_string(), + }); + } + + // Allocate region memory + let mut region_bufs: Vec> = mc + .region_sizes + .iter() + .map(|&sz| vec![0u8; sz]) + .collect(); + + // Build the array of region pointers + let region_ptrs: Vec<*mut u8> = region_bufs + .iter_mut() + .map(|buf| buf.as_mut_ptr()) + .collect(); + + // mmap anonymous RWX memory for the code — using raw syscall (no libc dependency) + let code_len = mc.code.len(); + let page_size = 4096usize; + let alloc_size = (code_len + page_size - 1) & !(page_size - 1); + + // syscall numbers for x86-64 Linux: + // mmap = 9, munmap = 11 + // mmap(addr, length, prot, flags, fd, offset) + // PROT_READ=1, PROT_WRITE=2, PROT_EXEC=4 + // MAP_PRIVATE=0x02, MAP_ANONYMOUS=0x20 + let prot: usize = 1 | 2 | 4; // PROT_READ | PROT_WRITE | PROT_EXEC + let flags: usize = 0x02 | 0x20; // MAP_PRIVATE | MAP_ANONYMOUS + + let code_ptr: usize; + unsafe { + std::arch::asm!( + "syscall", + in("rax") 9usize, // SYS_mmap + in("rdi") 0usize, // addr = NULL + in("rsi") alloc_size, // length + in("rdx") prot, // prot + in("r10") flags, // flags + in("r8") !0usize, // fd = -1 + in("r9") 0usize, // offset = 0 + lateout("rax") code_ptr, + lateout("rcx") _, + lateout("r11") _, + ); + } + + // Check for mmap failure (returns -errno on error, i.e. a value > 0xffff...f000) + if code_ptr > 0xFFFF_FFFF_FFFF_F000 { + return Err(CodegenError { + message: "mmap failed".to_string(), + }); + } + + // Copy code into the mapped region + unsafe { + std::ptr::copy_nonoverlapping(mc.code.as_ptr(), code_ptr as *mut u8, code_len); + } + + // Cast to function pointer and call + let func: extern "C" fn(*const *mut u8) = unsafe { std::mem::transmute(code_ptr) }; + func(region_ptrs.as_ptr()); + + // munmap to clean up + unsafe { + let _ret: usize; + std::arch::asm!( + "syscall", + in("rax") 11usize, // SYS_munmap + in("rdi") code_ptr, + in("rsi") alloc_size, + lateout("rax") _ret, + lateout("rcx") _, + lateout("r11") _, + ); + } + + // Build result + let mut regions = HashMap::new(); + for (i, name) in mc.region_order.iter().enumerate() { + regions.insert(name.clone(), region_bufs[i].clone()); + } + + Ok(ExecutionResult { regions }) +} + +#[cfg(test)] +mod tests { + use super::*; + + // ── Individual instruction encoding tests ── + + #[test] + fn encode_mov_imm64_rax() { + let bytes = encode_mov_imm64(PhysReg::Rax, 42); + // REX.W=0x48, B8+0=0xB8, then 42 as le u64 + assert_eq!(bytes[0], 0x48); + assert_eq!(bytes[1], 0xB8); + assert_eq!(u64::from_le_bytes(bytes[2..10].try_into().unwrap()), 42); + assert_eq!(bytes.len(), 10); + } + + #[test] + fn encode_mov_imm64_r8() { + let bytes = encode_mov_imm64(PhysReg::R8, 100); + // REX.W + REX.B = 0x49, B8+0=0xB8 + assert_eq!(bytes[0], 0x49); + assert_eq!(bytes[1], 0xB8); + assert_eq!(u64::from_le_bytes(bytes[2..10].try_into().unwrap()), 100); + } + + #[test] + fn encode_add_rr_rax_rcx() { + let bytes = encode_add_rr(PhysReg::Rax, PhysReg::Rcx); + // REX.W=0x48, ADD=0x01, ModRM(11, rcx, rax) = 0xC8 + assert_eq!(bytes, vec![0x48, 0x01, 0xC8]); + } + + #[test] + fn encode_sub_rr_rax_rcx() { + let bytes = encode_sub_rr(PhysReg::Rax, PhysReg::Rcx); + assert_eq!(bytes, vec![0x48, 0x29, 0xC8]); + } + + #[test] + fn encode_store64_basic() { + let bytes = encode_store64(PhysReg::Rax, 0, PhysReg::Rcx); + // REX.W, MOV r/m64,r64 (0x89), ModRM(10, rcx, rax) + disp32 + assert_eq!(bytes[0], 0x48); // REX.W + assert_eq!(bytes[1], 0x89); // MOV opcode + assert_eq!(bytes.len(), 7); // REX + opcode + ModRM + 4 bytes disp32 + } + + #[test] + fn encode_load64_basic() { + let bytes = encode_load64(PhysReg::Rcx, PhysReg::Rax, 8); + assert_eq!(bytes[0], 0x48); + assert_eq!(bytes[1], 0x8B); + assert_eq!(bytes.len(), 7); + // Check displacement + let disp = i32::from_le_bytes(bytes[3..7].try_into().unwrap()); + assert_eq!(disp, 8); + } + + #[test] + fn encode_cmp_and_setl() { + let cmp = encode_cmp_rr(PhysReg::Rax, PhysReg::Rcx); + assert_eq!(cmp, vec![0x48, 0x39, 0xC8]); + + let setl = encode_setl(PhysReg::Rdx); + assert_eq!(setl, vec![0x0F, 0x9C, 0xC2]); + } + + #[test] + fn encode_jmp_rel32_basic() { + let bytes = encode_jmp_rel32(0); + assert_eq!(bytes[0], 0xE9); + assert_eq!(bytes.len(), 5); + } + + #[test] + fn encode_jne_rel32_basic() { + let bytes = encode_jne_rel32(0); + assert_eq!(bytes[0], 0x0F); + assert_eq!(bytes[1], 0x85); + assert_eq!(bytes.len(), 6); + } + + #[test] + fn encode_ret_basic() { + assert_eq!(encode_ret(), vec![0xC3]); + } + + #[test] + fn encode_mov_rr_basic() { + let bytes = encode_mov_rr(PhysReg::Rdx, PhysReg::Rax); + assert_eq!(bytes, vec![0x48, 0x89, 0xC2]); + } + + #[test] + fn encode_imul_rr_basic() { + let bytes = encode_imul_rr(PhysReg::Rax, PhysReg::Rcx); + // REX.W + 0F AF + ModRM + assert_eq!(bytes[0], 0x48); + assert_eq!(bytes[1], 0x0F); + assert_eq!(bytes[2], 0xAF); + } + + #[test] + fn encode_add_imm32_basic() { + let bytes = encode_add_imm32(PhysReg::Rax, 42); + assert_eq!(bytes[0], 0x48); // REX.W + assert_eq!(bytes[1], 0x81); // opcode + assert_eq!(bytes[2], 0xC0); // ModR/M: mod=11, reg=0 (ADD), r/m=rax + assert_eq!(i32::from_le_bytes(bytes[3..7].try_into().unwrap()), 42); + } + + // ── Minimal program: load, add 1, store, terminate ── + + #[test] + fn minimal_program_executes() { + // Build a minimal L0 program: load value from state, add 1, store back, terminate. + let program = L0Program { + regions: vec![Region { + name: "state".to_string(), + size: 8, + access: AccessMode::ReadWrite, + kind: RegionKind::State, + }], + blocks: vec![Block { + name: "main".to_string(), + ops: vec![ + Op::Load { + size: OpSize::B64, + region: "state".to_string(), + offset: 0, + dest: Register(0), + }, + Op::LoadImm { + value: 1, + dest: Register(1), + }, + Op::Arith { + kind: ArithKind::Add, + lhs: Register(0), + rhs: Register(1), + dest: Register(2), + }, + Op::Store { + size: OpSize::B64, + region: "state".to_string(), + offset: 0, + src: Register(2), + }, + Op::Terminate, + ], + }], + }; + + let mc = codegen(&program).expect("codegen should succeed"); + assert!(!mc.code.is_empty()); + + let result = execute(&mc).expect("execution should succeed"); + let val = result.read_u64("state", 0).expect("should read state"); + assert_eq!(val, 1, "state[0] should be 1 after adding 1 to initial 0"); + } + + #[test] + fn minimal_loop_program() { + // A loop that adds 1 to state five times. + let program = L0Program { + regions: vec![ + Region { + name: "state".to_string(), + size: 8, + access: AccessMode::ReadWrite, + kind: RegionKind::State, + }, + Region { + name: "counter".to_string(), + size: 8, + access: AccessMode::ReadWrite, + kind: RegionKind::Control, + }, + ], + blocks: vec![ + Block { + name: "entry".to_string(), + ops: vec![ + Op::LoadImm { value: 0, dest: Register(0) }, + Op::Store { + size: OpSize::B64, + region: "state".to_string(), + offset: 0, + src: Register(0), + }, + Op::Store { + size: OpSize::B64, + region: "counter".to_string(), + offset: 0, + src: Register(0), + }, + Op::Jump { target: "check".to_string() }, + ], + }, + Block { + name: "check".to_string(), + ops: vec![ + Op::Load { + size: OpSize::B64, + region: "counter".to_string(), + offset: 0, + dest: Register(1), + }, + Op::LoadImm { value: 5, dest: Register(2) }, + Op::Cmp { + kind: CmpKind::Lt, + lhs: Register(1), + rhs: Register(2), + dest: Register(3), + }, + Op::Branch { + cond: Register(3), + true_target: "body".to_string(), + false_target: "exit".to_string(), + }, + ], + }, + Block { + name: "body".to_string(), + ops: vec![ + // state += 1 + Op::Load { + size: OpSize::B64, + region: "state".to_string(), + offset: 0, + dest: Register(4), + }, + Op::ArithImm { + kind: ArithKind::Add, + lhs: Register(4), + imm: 1, + dest: Register(5), + }, + Op::Store { + size: OpSize::B64, + region: "state".to_string(), + offset: 0, + src: Register(5), + }, + // counter += 1 + Op::Load { + size: OpSize::B64, + region: "counter".to_string(), + offset: 0, + dest: Register(6), + }, + Op::ArithImm { + kind: ArithKind::Add, + lhs: Register(6), + imm: 1, + dest: Register(7), + }, + Op::Store { + size: OpSize::B64, + region: "counter".to_string(), + offset: 0, + src: Register(7), + }, + Op::Jump { target: "check".to_string() }, + ], + }, + Block { + name: "exit".to_string(), + ops: vec![Op::Terminate], + }, + ], + }; + + let mc = codegen(&program).expect("codegen should succeed"); + let result = execute(&mc).expect("execution should succeed"); + + let state_val = result.read_u64("state", 0).unwrap(); + assert_eq!(state_val, 5, "state should be 5 after 5 iterations"); + + let counter_val = result.read_u64("counter", 0).unwrap(); + assert_eq!(counter_val, 5, "counter should be 5"); + } + + #[test] + fn queue_push_pop_roundtrip() { + // Push a value to a queue, pop it, store in state. + let program = L0Program { + regions: vec![ + Region { + name: "state".to_string(), + size: 8, + access: AccessMode::ReadWrite, + kind: RegionKind::State, + }, + Region { + name: "q".to_string(), + size: 128, + access: AccessMode::ReadWrite, + kind: RegionKind::Queue, + }, + ], + blocks: vec![Block { + name: "main".to_string(), + ops: vec![ + Op::LoadImm { value: 42, dest: Register(0) }, + Op::QueuePush { + region: "q".to_string(), + src: Register(0), + }, + Op::QueuePop { + region: "q".to_string(), + dest: Register(1), + }, + Op::Store { + size: OpSize::B64, + region: "state".to_string(), + offset: 0, + src: Register(1), + }, + Op::Terminate, + ], + }], + }; + + let mc = codegen(&program).expect("codegen should succeed"); + let result = execute(&mc).expect("execution should succeed"); + + let val = result.read_u64("state", 0).unwrap(); + assert_eq!(val, 42, "popped value should be 42"); + } + + #[test] + fn codegen_hex_dump_is_nonempty() { + let program = L0Program { + regions: vec![Region { + name: "s".to_string(), + size: 8, + access: AccessMode::ReadWrite, + kind: RegionKind::State, + }], + blocks: vec![Block { + name: "main".to_string(), + ops: vec![Op::Terminate], + }], + }; + let mc = codegen(&program).expect("codegen should succeed"); + let hex = mc.hex_dump(); + assert!(!hex.is_empty(), "hex dump should not be empty"); + // Should contain the ret byte (c3) at the end + assert!(hex.ends_with("c3"), "should end with ret instruction"); + } +} diff --git a/src/emit.rs b/src/emit.rs new file mode 100644 index 0000000..fb2a6d4 --- /dev/null +++ b/src/emit.rs @@ -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 { + 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, + regs: &mut RegAlloc, + actor_state_regions: &HashMap<&str, String>, + actor_field_offsets: &HashMap<(&str, &str), usize>, + queue_regions: &HashMap<&str, String>, + output_region: &Option, +) { + 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, + 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, + 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, + 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(®) = 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, 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, + regs: &mut RegAlloc, + actor_state_regions: &HashMap<&str, String>, + actor_field_offsets: &HashMap<(&str, &str), usize>, + reads: &Option, +) -> 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")); + } +} diff --git a/src/interpret_l1.rs b/src/interpret_l1.rs new file mode 100644 index 0000000..c0e7ed1 --- /dev/null +++ b/src/interpret_l1.rs @@ -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, +} + +/// 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, + pub messages_sent: Vec, + pub window_reads: Vec, + pub emitted_outputs: Vec, +} + +/// The complete execution trace produced by the interpreter. +#[derive(Debug, Clone, PartialEq)] +pub struct ExecutionTrace { + pub steps: Vec, + pub final_actor_states: Vec, + pub all_outputs: Vec, +} + +/// 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>, + /// Message queues: actor_name -> queue of (message_type, args) + queues: HashMap)>>, + /// Accumulated output values + outputs: Vec, +} + +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 { + 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 { + 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, + window_reads: &mut Vec, + step_outputs: &mut Vec, +) { + // Track values read through windows for use in emit expressions + let mut read_values: HashMap = HashMap::new(); + + for action in &leaf.actions { + match action { + LeafAction::Forward { target, message_type, args } => { + let arg_vals: Vec = 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 = 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, + reads: &Option, + actor_state: &HashMap>, +) -> 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) -> 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 = 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 = 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")); + } +} diff --git a/src/l0_ir.rs b/src/l0_ir.rs new file mode 100644 index 0000000..d166c78 --- /dev/null +++ b/src/l0_ir.rs @@ -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, +} + +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, + pub blocks: Vec, +} + +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, + ], + }, + ], + } + } +} diff --git a/src/l1_ir.rs b/src/l1_ir.rs new file mode 100644 index 0000000..ca337c0 --- /dev/null +++ b/src/l1_ir.rs @@ -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, + pub readers: Vec, +} + +/// 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, Box), + /// Assignment: target field, value expression. + Assign(String, Box), + /// Block of sequential expressions. + Block(Vec), +} + +/// 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, + pub windows: Vec, + pub handlers: Vec, +} + +/// 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, + }, + /// 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, + pub actions: Vec, +} + +/// 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, +} + +/// A core declaration — execution context for a single thread. +#[derive(Debug, Clone, PartialEq)] +pub struct CoreDecl { + pub name: String, + pub actors: Vec, + pub leaves: Vec, + pub pipelines: Vec, + pub steps: u64, +} + +/// The top-level L1 program. +#[derive(Debug, Clone, PartialEq)] +pub struct L1Program { + pub actors: Vec, + pub leaves: Vec, + pub pipelines: Vec, + pub cores: Vec, +} + +// --- 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 = 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 = 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, + }], + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d1c615e --- /dev/null +++ b/src/lib.rs @@ -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; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..4ab1cc0 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,78 @@ +use l0_cpu_runtime::{pipeline, repl}; + +fn main() { + let args: Vec = std::env::args().collect(); + if args.len() < 2 { + eprintln!("Usage: l0-cpu-runtime [--dump-ir]"); + eprintln!(" l0-cpu-runtime verify "); + 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 "); + 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); + } + } + } +} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..9454549 --- /dev/null +++ b/src/parser.rs @@ -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 { + 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::() + .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 { + 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 { + 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 { + 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 { + 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 { + 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, 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 { + // "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 { + 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 { + 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 { + // "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 { + // "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, 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 { + // "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 { + // "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, 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 { + // "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 { + 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 { + 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); + } +} diff --git a/src/pipeline.rs b/src/pipeline.rs new file mode 100644 index 0000000..cb42de3 --- /dev/null +++ b/src/pipeline.rs @@ -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), + 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 { + 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 { + // 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()); + } +} diff --git a/src/repl.rs b/src/repl.rs new file mode 100644 index 0000000..1f885c1 --- /dev/null +++ b/src/repl.rs @@ -0,0 +1,1849 @@ +//! Interactive REPL for L1 programs. +//! +//! Supports loading files, typing declarations interactively, running programs, +//! inspecting IR, and running formal verification. Rich terminal output with +//! colors, box-drawing, ASCII pipeline diagrams, and execution trace diffs. + +use crate::interpret_l1; +use crate::l1_ir::{L1Program, LeafAction}; +use crate::translate_validate::{Severity, ToolName}; +use crate::{emit, parser, pipeline, verify_l1}; +use crossterm::style::{Attribute, Color, SetAttribute, SetForegroundColor, ResetColor}; +use std::io::{self, BufRead, Write}; + +// ─── Visualization helpers ────────────────────────────────────────── + +/// Write colored text to the output. +fn write_colored(out: &mut W, color: Color, text: &str) { + write!(out, "{}{}{}", SetForegroundColor(color), text, ResetColor).ok(); +} + +/// Write bold colored text to the output. +fn write_bold_colored(out: &mut W, color: Color, text: &str) { + write!( + out, + "{}{}{}{}{}", + SetAttribute(Attribute::Bold), + SetForegroundColor(color), + text, + ResetColor, + SetAttribute(Attribute::Reset) + ) + .ok(); +} + +/// Display verification results with box-drawing and pass/fail indicators. +fn display_verification_report( + out: &mut W, + report: &crate::translate_validate::VerificationReport, +) { + writeln!(out).ok(); + write_bold_colored(out, Color::Cyan, "┌─────────────────────────────────────────────┐\n"); + write_bold_colored(out, Color::Cyan, "│"); + write_bold_colored(out, Color::White, " Translation Validation Report "); + write_bold_colored(out, Color::Cyan, "│\n"); + write_bold_colored(out, Color::Cyan, "├─────────────────────────────────────────────┤\n"); + + for result in &report.results { + write_bold_colored(out, Color::Cyan, "│ "); + if result.passed { + write_colored(out, Color::Green, "[PASS]"); + } else { + write_colored(out, Color::Red, "[FAIL]"); + } + let tool_name = match result.tool { + ToolName::TraceEquivalence => "Trace Equivalence ", + ToolName::StructuralCorrespondence => "Structural Correspondence", + ToolName::BoundedModelCheck => "Bounded Model Check ", + }; + write!(out, " {}", tool_name).ok(); + write_colored(out, Color::DarkGrey, &format!(" {:>6}us", result.elapsed_us)); + write_bold_colored(out, Color::Cyan, " │\n"); + + for diag in &result.diagnostics { + write_bold_colored(out, Color::Cyan, "│ "); + match diag.severity { + Severity::Error => write_colored(out, Color::Red, &format!(" {}", diag.message)), + Severity::Warning => write_colored(out, Color::Yellow, &format!(" {}", diag.message)), + Severity::Info => write_colored(out, Color::DarkGrey, &format!(" {}", diag.message)), + } + // Pad to close box + writeln!(out).ok(); + } + } + + write_bold_colored(out, Color::Cyan, "├─────────────────────────────────────────────┤\n"); + + // Field mappings + if !report.field_mappings.is_empty() { + write_bold_colored(out, Color::Cyan, "│ "); + write_bold_colored(out, Color::White, "Field Mappings (L1 -> L0)"); + writeln!(out).ok(); + for m in &report.field_mappings { + write_bold_colored(out, Color::Cyan, "│ "); + write_colored(out, Color::Yellow, &format!("{}.{}", m.actor, m.field)); + write_colored(out, Color::DarkGrey, " -> "); + write_colored(out, Color::Blue, &format!("{}[{}]", m.l0_region, m.l0_offset)); + writeln!(out).ok(); + } + } + + // Exploration stats + if let Some(stats) = &report.exploration_stats { + write_bold_colored(out, Color::Cyan, "│ "); + write_bold_colored(out, Color::White, "Exploration Stats"); + writeln!(out).ok(); + write_bold_colored(out, Color::Cyan, "│ "); + write_colored( + out, + Color::DarkGrey, + &format!( + "states: {} blocks: {}/{} regions: {}/{}", + stats.states_explored, + stats.l0_blocks_reached, + stats.l0_blocks_total, + stats.l0_regions_used, + stats.l0_regions_total + ), + ); + writeln!(out).ok(); + } + + write_bold_colored(out, Color::Cyan, "├─────────────────────────────────────────────┤\n"); + write_bold_colored(out, Color::Cyan, "│ "); + write_bold_colored(out, Color::White, "Overall: "); + if report.all_passed() { + write_colored(out, Color::Green, "PASS"); + } else { + write_colored(out, Color::Red, "FAIL"); + } + writeln!(out).ok(); + write_bold_colored(out, Color::Cyan, "└─────────────────────────────────────────────┘\n"); + writeln!(out).ok(); +} + +/// Generate an ASCII pipeline diagram from L1 IR. +fn display_pipeline_diagram(out: &mut W, program: &L1Program) { + writeln!(out).ok(); + write_bold_colored(out, Color::Cyan, "Pipeline Topology\n"); + write_colored(out, Color::DarkGrey, "─────────────────\n"); + + for pipe in &program.pipelines { + write_bold_colored(out, Color::White, &format!(" pipeline: {}\n", pipe.name)); + + for (i, stage_name) in pipe.stages.iter().enumerate() { + // Determine if stage is an actor or a leaf + let is_actor = program.actors.iter().any(|a| a.name == *stage_name); + let is_leaf = program.leaves.iter().any(|l| l.name == *stage_name); + + if is_actor { + let actor = program.actors.iter().find(|a| a.name == *stage_name).unwrap(); + let fields: Vec = + actor.state.iter().map(|f| format!("{}: {}", f.name, f.ty)).collect(); + write_colored(out, Color::DarkGrey, " "); + write_bold_colored(out, Color::Magenta, &format!("┌─ actor: {} ─┐\n", stage_name)); + write_colored(out, Color::DarkGrey, " "); + write_colored(out, Color::Magenta, &format!("│ state: {{{}}} │\n", fields.join(", "))); + for win in &actor.windows { + write_colored(out, Color::DarkGrey, " "); + write_colored( + out, + Color::Magenta, + &format!("│ window: {} ({}) │\n", win.name, win.fields.join(", ")), + ); + } + for handler in &actor.handlers { + let args: Vec = + handler.args.iter().map(|(n, t)| format!("{}: {}", n, t)).collect(); + write_colored(out, Color::DarkGrey, " "); + write_colored( + out, + Color::Magenta, + &format!("│ on {}({}) │\n", handler.message_type, args.join(", ")), + ); + } + write_colored(out, Color::DarkGrey, " "); + write_bold_colored(out, Color::Magenta, "└──────────────┘\n"); + } else if is_leaf { + let leaf = program.leaves.iter().find(|l| l.name == *stage_name).unwrap(); + write_colored(out, Color::DarkGrey, " "); + write_bold_colored(out, Color::Green, &format!("[ leaf: {} ]\n", stage_name)); + for action in &leaf.actions { + let desc = match action { + LeafAction::Forward { target, message_type, .. } => { + format!("forward -> {}.{}", target, message_type) + } + LeafAction::ReadWindow { actor, window, field } => { + format!("read {}.{}.{}", actor, window, field) + } + LeafAction::Emit(expr) => { + format!("emit({})", expr) + } + }; + write_colored(out, Color::DarkGrey, " "); + write_colored(out, Color::Green, &format!(" {}\n", desc)); + } + } else { + write_colored(out, Color::DarkGrey, " "); + write_colored(out, Color::White, &format!("[{}]\n", stage_name)); + } + + // Draw arrow to next stage + if i < pipe.stages.len() - 1 { + // Figure out what message goes between stages + let next = &pipe.stages[i + 1]; + let mut edge_label = String::new(); + if is_leaf { + let leaf = program.leaves.iter().find(|l| l.name == *stage_name).unwrap(); + for action in &leaf.actions { + if let LeafAction::Forward { target, message_type, .. } = action { + if target == next { + edge_label = message_type.clone(); + } + } + } + } + write_colored(out, Color::DarkGrey, " "); + if edge_label.is_empty() { + write_colored(out, Color::DarkGrey, " │\n"); + write_colored(out, Color::DarkGrey, " ▼\n"); + } else { + write_colored(out, Color::Yellow, &format!(" │ {}\n", edge_label)); + write_colored(out, Color::DarkGrey, " ▼\n"); + } + } + } + } + + // Core summary + for core in &program.cores { + writeln!(out).ok(); + write_bold_colored(out, Color::White, &format!(" core: {}\n", core.name)); + write_colored( + out, + Color::DarkGrey, + &format!( + " actors: [{}] leaves: [{}] steps: {}\n", + core.actors.join(", "), + core.leaves.join(", "), + core.steps + ), + ); + } + writeln!(out).ok(); +} + +/// Display execution trace with step-by-step state changes and diffs. +fn display_execution_trace( + out: &mut W, + trace: &interpret_l1::ExecutionTrace, +) { + writeln!(out).ok(); + write_bold_colored(out, Color::Cyan, "Execution Trace\n"); + write_colored(out, Color::DarkGrey, "───────────────\n"); + + let mut prev_states: Option<&Vec> = None; + + for step in &trace.steps { + write_bold_colored(out, Color::White, &format!(" Step {}\n", step.step_index)); + + // Messages sent + for msg in &step.messages_sent { + write_colored(out, Color::DarkGrey, " "); + write_colored(out, Color::Yellow, "send "); + write_colored( + out, + Color::White, + &format!("{}({:?})", msg.message_type, msg.args), + ); + write_colored(out, Color::DarkGrey, " -> "); + write_colored(out, Color::Magenta, &msg.target); + writeln!(out).ok(); + } + + // Window reads + for wr in &step.window_reads { + write_colored(out, Color::DarkGrey, " "); + write_colored(out, Color::Blue, "read "); + write_colored( + out, + Color::White, + &format!("{}.{}.{}", wr.actor, wr.window, wr.field), + ); + write_colored(out, Color::DarkGrey, " = "); + write_colored(out, Color::Cyan, &format!("{}", wr.value)); + writeln!(out).ok(); + } + + // Emitted outputs + for val in &step.emitted_outputs { + write_colored(out, Color::DarkGrey, " "); + write_colored(out, Color::Green, "emit "); + write_colored(out, Color::White, &format!("{}", val)); + writeln!(out).ok(); + } + + // Actor state with diffs + for snap in &step.actor_states { + write_colored(out, Color::DarkGrey, " "); + write_colored(out, Color::Magenta, &format!("{} ", snap.actor_name)); + write_colored(out, Color::DarkGrey, "{ "); + + for (fi, (name, val)) in snap.fields.iter().enumerate() { + if fi > 0 { + write_colored(out, Color::DarkGrey, ", "); + } + // Check if value changed from previous step + let changed = prev_states + .and_then(|prev| { + prev.iter() + .find(|ps| ps.actor_name == snap.actor_name) + .and_then(|ps| { + ps.fields.iter().find(|(n, _)| n == name).map(|(_, v)| *v != *val) + }) + }) + .unwrap_or(true); // First step — all values are "new" + + if changed { + // Show old -> new in green + let old_val = prev_states.and_then(|prev| { + prev.iter() + .find(|ps| ps.actor_name == snap.actor_name) + .and_then(|ps| ps.fields.iter().find(|(n, _)| n == name).map(|(_, v)| *v)) + }); + write_colored(out, Color::White, name); + write_colored(out, Color::DarkGrey, "="); + if let Some(old) = old_val { + write_colored(out, Color::DarkGrey, &format!("{}", old)); + write_colored(out, Color::Green, &format!("->{}", val)); + } else { + write_colored(out, Color::Green, &format!("{}", val)); + } + } else { + write_colored(out, Color::DarkGrey, &format!("{}={}", name, val)); + } + } + write_colored(out, Color::DarkGrey, " }"); + writeln!(out).ok(); + } + + prev_states = Some(&step.actor_states); + writeln!(out).ok(); + } + + // Final state + write_bold_colored(out, Color::White, " Final State\n"); + for snap in &trace.final_actor_states { + write_colored(out, Color::DarkGrey, " "); + write_bold_colored(out, Color::Magenta, &format!("{} ", snap.actor_name)); + write_colored(out, Color::DarkGrey, "{ "); + for (fi, (name, val)) in snap.fields.iter().enumerate() { + if fi > 0 { + write_colored(out, Color::DarkGrey, ", "); + } + write_colored(out, Color::White, &format!("{}=", name)); + write_bold_colored(out, Color::Cyan, &format!("{}", val)); + } + write_colored(out, Color::DarkGrey, " }"); + writeln!(out).ok(); + } + + if !trace.all_outputs.is_empty() { + write_colored(out, Color::DarkGrey, " "); + write_bold_colored(out, Color::White, "outputs: "); + write_colored(out, Color::Cyan, &format!("{:?}", trace.all_outputs)); + writeln!(out).ok(); + } + writeln!(out).ok(); +} + +/// Display syntax-highlighted L1 IR. +fn display_l1_ir(out: &mut W, program: &L1Program) { + writeln!(out).ok(); + write_bold_colored(out, Color::Cyan, "L1 IR\n"); + write_colored(out, Color::DarkGrey, "─────\n"); + + for actor in &program.actors { + write_bold_colored(out, Color::Blue, "actor "); + write_bold_colored(out, Color::White, &actor.name); + writeln!(out, " {{").ok(); + write_colored(out, Color::Blue, " state "); + writeln!(out, "{{").ok(); + for field in &actor.state { + write_colored(out, Color::DarkGrey, " "); + write_colored(out, Color::White, &field.name); + write_colored(out, Color::DarkGrey, ": "); + write_colored(out, Color::Yellow, &format!("{}", field.ty)); + write_colored(out, Color::DarkGrey, " = "); + write_colored(out, Color::Cyan, &format!("{}", field.init)); + writeln!(out).ok(); + } + writeln!(out, " }}").ok(); + for win in &actor.windows { + write_colored(out, Color::Blue, " window "); + write_colored(out, Color::White, &win.name); + write_colored(out, Color::DarkGrey, " : ("); + write_colored(out, Color::White, &win.fields.join(", ")); + write_colored(out, Color::DarkGrey, ") readers("); + write_colored(out, Color::White, &win.readers.join(", ")); + writeln!(out, ")").ok(); + } + for handler in &actor.handlers { + let args: Vec = handler + .args + .iter() + .map(|(n, t)| format!("{}: {}", n, t)) + .collect(); + write_colored(out, Color::Magenta, " on "); + write_colored(out, Color::White, &handler.message_type); + write_colored(out, Color::DarkGrey, "("); + write_colored(out, Color::White, &args.join(", ")); + write_colored(out, Color::DarkGrey, ") { "); + write_colored(out, Color::White, &format!("{}", handler.body)); + writeln!(out, " }}").ok(); + } + writeln!(out, "}}").ok(); + } + + for leaf in &program.leaves { + write_bold_colored(out, Color::Blue, "leaf "); + write_bold_colored(out, Color::White, &leaf.name); + write!(out, " {{").ok(); + if let Some(reads) = &leaf.reads { + write_colored(out, Color::Blue, " reads "); + write_colored(out, Color::White, &format!("{}.{}", reads.actor, reads.window)); + write_colored(out, Color::DarkGrey, ";"); + } + for action in &leaf.actions { + write_colored(out, Color::DarkGrey, " "); + write_colored(out, Color::White, &format!("{}", action)); + write_colored(out, Color::DarkGrey, ";"); + } + writeln!(out, " }}").ok(); + } + + for pipe in &program.pipelines { + write_bold_colored(out, Color::Blue, "pipeline "); + write_bold_colored(out, Color::White, &pipe.name); + write_colored(out, Color::DarkGrey, " { "); + for (i, stage) in pipe.stages.iter().enumerate() { + if i > 0 { + write_colored(out, Color::Yellow, " -> "); + } + write_colored(out, Color::White, stage); + } + writeln!(out, " }}").ok(); + } + + for core in &program.cores { + write_bold_colored(out, Color::Blue, "core "); + write_bold_colored(out, Color::White, &core.name); + writeln!(out, " {{").ok(); + write_colored(out, Color::DarkGrey, " actors: ["); + write_colored(out, Color::White, &core.actors.join(", ")); + writeln!(out, "]").ok(); + write_colored(out, Color::DarkGrey, " leaves: ["); + write_colored(out, Color::White, &core.leaves.join(", ")); + writeln!(out, "]").ok(); + write_colored(out, Color::DarkGrey, " pipelines: ["); + write_colored(out, Color::White, &core.pipelines.join(", ")); + writeln!(out, "]").ok(); + write_colored(out, Color::DarkGrey, " steps: "); + write_colored(out, Color::Cyan, &format!("{}", core.steps)); + writeln!(out).ok(); + writeln!(out, "}}").ok(); + } + writeln!(out).ok(); +} + +/// Display syntax-highlighted L0 IR. +fn display_l0_ir(out: &mut W, source: &str) -> Result<(), String> { + let l1 = parser::parse(source).map_err(|e| format!("{}", e))?; + verify_l1::verify(&l1).map_err(|e| format!("{}", e))?; + let l0 = emit::emit(&l1).map_err(|e| format!("{}", e))?; + + writeln!(out).ok(); + write_bold_colored(out, Color::Cyan, "L0 IR\n"); + write_colored(out, Color::DarkGrey, "─────\n"); + + // Regions + write_bold_colored(out, Color::White, " Regions\n"); + for region in &l0.regions { + write_colored(out, Color::DarkGrey, " "); + write_colored(out, Color::Blue, "region "); + write_bold_colored(out, Color::White, ®ion.name); + write_colored( + out, + Color::DarkGrey, + &format!(" : {} bytes, {}, ", region.size, region.access), + ); + let kind_color = match region.kind { + crate::l0_ir::RegionKind::State => Color::Magenta, + crate::l0_ir::RegionKind::Queue => Color::Yellow, + crate::l0_ir::RegionKind::Control => Color::Cyan, + crate::l0_ir::RegionKind::Output => Color::Green, + }; + write_colored(out, kind_color, &format!("{}", region.kind)); + writeln!(out).ok(); + } + + writeln!(out).ok(); + write_bold_colored(out, Color::White, " Blocks\n"); + for block in &l0.blocks { + write_bold_colored(out, Color::Yellow, &format!(" {}:\n", block.name)); + for op in &block.ops { + write_colored(out, Color::DarkGrey, " "); + let op_str = format!("{}", op); + // Color different parts of the op + if op_str.contains("load.") || op_str.contains("store.") { + write_colored(out, Color::Blue, &op_str); + } else if op_str.contains("queue_push") || op_str.contains("queue_pop") { + write_colored(out, Color::Yellow, &op_str); + } else if op_str.contains("cmp.") || op_str.contains("branch") { + write_colored(out, Color::Magenta, &op_str); + } else if op_str.contains("add") + || op_str.contains("sub") + || op_str.contains("mul") + { + write_colored(out, Color::Green, &op_str); + } else if op_str.contains("jump") { + write_colored(out, Color::Cyan, &op_str); + } else if op_str.contains("terminate") { + write_colored(out, Color::Red, &op_str); + } else if op_str.contains("imm") { + write_colored(out, Color::Cyan, &op_str); + } else { + write_colored(out, Color::White, &op_str); + } + writeln!(out).ok(); + } + } + writeln!(out).ok(); + Ok(()) +} + +/// REPL state accumulates declarations typed interactively. +pub struct ReplState { + /// Raw source text accumulated from interactive declarations. + pub source: String, + /// Last successfully parsed program (may be partial — no core yet). + pub program: Option, +} + +impl ReplState { + pub fn new() -> Self { + ReplState { + source: String::new(), + program: None, + } + } + + /// Try to parse the current accumulated source. + pub fn reparse(&mut self) -> Result<(), String> { + match parser::parse(&self.source) { + Ok(prog) => { + self.program = Some(prog); + Ok(()) + } + Err(e) => Err(format!("{}", e)), + } + } + + pub fn reset(&mut self) { + self.source.clear(); + self.program = None; + } +} + +/// Parse a REPL command from a line of input. +/// Returns (command, argument) where argument may be empty. +pub fn parse_command(line: &str) -> Option<(&str, &str)> { + let trimmed = line.trim(); + if !trimmed.starts_with(':') { + return None; + } + let without_colon = &trimmed[1..]; + let (cmd, arg) = match without_colon.find(char::is_whitespace) { + Some(pos) => (&without_colon[..pos], without_colon[pos..].trim()), + None => (without_colon, ""), + }; + Some((cmd, arg)) +} + +/// Check if input has unmatched open braces (for multi-line continuation). +pub fn has_unmatched_braces(input: &str) -> bool { + let mut depth: i32 = 0; + for ch in input.chars() { + match ch { + '{' => depth += 1, + '}' => depth -= 1, + _ => {} + } + } + depth > 0 +} + +/// Execute a REPL command, writing output to the given writer. +/// Returns `false` if the REPL should exit. +pub fn execute_command( + state: &mut ReplState, + cmd: &str, + arg: &str, + out: &mut W, +) -> bool { + match cmd { + "quit" | "q" => return false, + "reset" => { + state.reset(); + writeln!(out, "State cleared.").ok(); + } + "load" => { + if arg.is_empty() { + writeln!(out, "Usage: :load ").ok(); + } else { + match std::fs::read_to_string(arg) { + Ok(contents) => { + state.source = contents; + match state.reparse() { + Ok(()) => { + let prog = state.program.as_ref().unwrap(); + writeln!( + out, + "Loaded {} ({} actors, {} leaves, {} pipelines, {} cores)", + arg, + prog.actors.len(), + prog.leaves.len(), + prog.pipelines.len(), + prog.cores.len(), + ) + .ok(); + } + Err(e) => { + writeln!(out, "Parse error: {}", e).ok(); + } + } + } + Err(e) => { + writeln!(out, "Error reading '{}': {}", arg, e).ok(); + } + } + } + } + "run" => { + if state.source.is_empty() { + writeln!(out, "No program loaded. Use :load or type declarations.").ok(); + } else { + // If arg is a number, override core steps + let mut source = state.source.clone(); + if !arg.is_empty() { + if let Ok(n) = arg.parse::() { + source = override_steps(&source, n); + } + } + // Run L1 interpreter for trace display + if let Ok(program) = parser::parse(&source) { + if let Ok(trace) = interpret_l1::interpret(&program) { + display_execution_trace(out, &trace); + } + } + match pipeline::run(&source) { + Ok((result, _ir)) => { + write_bold_colored(out, Color::Green, "Execution complete.\n"); + for (name, buf) in &result.regions { + if buf.len() >= 8 { + let val = + u64::from_le_bytes(buf[0..8].try_into().unwrap()); + write_colored(out, Color::DarkGrey, " "); + write_colored(out, Color::White, &format!("{}[0]", name)); + write_colored(out, Color::DarkGrey, " = "); + write_bold_colored(out, Color::Cyan, &format!("{}", val)); + writeln!(out).ok(); + } + } + let output = result.read_output_values(); + if !output.is_empty() { + write_colored(out, Color::DarkGrey, " "); + write_colored(out, Color::White, "output values: "); + write_bold_colored(out, Color::Cyan, &format!("{:?}", output)); + writeln!(out).ok(); + } + } + Err(e) => { + write_colored(out, Color::Red, &format!("Error: {}\n", e)); + } + } + } + } + "ir" => { + if let Some(prog) = &state.program { + display_l1_ir(out, prog); + display_pipeline_diagram(out, prog); + } else { + writeln!(out, "No program loaded.").ok(); + } + } + "l0" => { + if state.source.is_empty() { + writeln!(out, "No program loaded.").ok(); + } else { + if let Err(e) = display_l0_ir(out, &state.source) { + write_colored(out, Color::Red, &format!("Error: {}\n", e)); + } + } + } + "verify" => { + if state.source.is_empty() { + writeln!(out, "No program loaded.").ok(); + } else { + // Parse and validate directly so we can display even on failure + let validate_result = crate::translate_validate::validate_source(&state.source); + match validate_result { + Ok(report) => { + display_verification_report(out, &report); + } + Err(e) => { + write_colored(out, Color::Red, &format!("Verification failed: {}\n", e)); + } + } + } + } + "help" => { + print_help(out); + } + "examples" => { + handle_examples(arg, state, out); + } + _ => { + writeln!(out, "Unknown command: :{}", cmd).ok(); + writeln!(out, "Type :help for available commands.").ok(); + } + } + true +} + +/// Add a declaration to the REPL state. +pub fn add_declaration(state: &mut ReplState, input: &str, out: &mut W) { + state.source.push_str(input); + state.source.push('\n'); + match state.reparse() { + Ok(()) => { + // Determine what was added by checking the program + let prog = state.program.as_ref().unwrap(); + let kind = detect_declaration_kind(input); + writeln!(out, "Added {}.", kind).ok(); + + // Run partial verification if possible + if let Err(e) = verify_l1::verify(prog) { + writeln!(out, " (partial verification: {})", e).ok(); + } else { + writeln!(out, " (verification: ok)").ok(); + } + } + Err(e) => { + // Revert the addition + let len = state.source.len() - input.len() - 1; + state.source.truncate(len); + let _ = state.reparse(); // try to restore + writeln!(out, "Parse error: {}", e).ok(); + } + } +} + +/// Detect what kind of declaration was typed. +fn detect_declaration_kind(input: &str) -> &str { + let trimmed = input.trim(); + if trimmed.starts_with("actor ") { + "actor" + } else if trimmed.starts_with("leaf ") { + "leaf" + } else if trimmed.starts_with("pipeline ") { + "pipeline" + } else if trimmed.starts_with("core ") { + "core" + } else { + "declaration" + } +} + +/// Override the steps count in a source string. +fn override_steps(source: &str, n: u64) -> String { + // Replace "steps: " with "steps: " + let mut result = String::new(); + let mut rest = source; + while let Some(pos) = rest.find("steps:") { + result.push_str(&rest[..pos]); + result.push_str("steps: "); + let after = &rest[pos + 6..]; + // Skip whitespace + let trimmed = after.trim_start(); + // Skip the old number + let num_end = trimmed.find(|c: char| !c.is_ascii_digit()).unwrap_or(trimmed.len()); + result.push_str(&format!("{}", n)); + rest = &trimmed[num_end..]; + } + result.push_str(rest); + if result.is_empty() { + source.to_string() + } else { + result + } +} + +fn print_help(out: &mut W) { + writeln!(out).ok(); + write_bold_colored(out, Color::Cyan, "L1 REPL Commands\n"); + write_colored(out, Color::DarkGrey, "────────────────\n"); + let commands = [ + (":load ", "Load a .l1 file, replacing current state"), + (":reset", "Clear all declarations"), + (":run [N]", "Compile and execute (N overrides step count)"), + (":ir", "Show current L1 IR with pipeline diagram"), + (":l0", "Show emitted L0 IR (syntax-highlighted)"), + (":verify", "Run formal verification pipeline"), + (":help", "Show this help and DSL reference"), + (":examples", "List built-in examples"), + (":examples ", "Load example with annotated walkthrough"), + (":quit", "Exit the REPL"), + ]; + for (cmd, desc) in &commands { + write_colored(out, Color::DarkGrey, " "); + write_colored(out, Color::Yellow, &format!("{:<18}", cmd)); + write_colored(out, Color::White, &format!(" {}", desc)); + writeln!(out).ok(); + } + + // ── Declaration Forms ── + writeln!(out).ok(); + write_bold_colored(out, Color::Cyan, "Declaration Forms\n"); + write_colored(out, Color::DarkGrey, "─────────────────\n"); + + // actor + writeln!(out).ok(); + write_bold_colored(out, Color::Blue, " actor "); + write_colored(out, Color::White, " {\n"); + write_colored(out, Color::Blue, " state "); + write_colored(out, Color::White, "{ : = , ... }\n"); + write_colored(out, Color::DarkGrey, " Mutable fields owned by this actor. Updated in handlers.\n"); + write_colored(out, Color::Blue, " window "); + write_colored(out, Color::White, " : (, ...) readers(, ...)\n"); + write_colored(out, Color::DarkGrey, " Read-only snapshot of selected fields. Leaves listed in\n"); + write_colored(out, Color::DarkGrey, " readers() may read this window. Provides safe concurrent access.\n"); + write_colored(out, Color::Blue, " on "); + write_colored(out, Color::White, "(: , ...) { }\n"); + write_colored(out, Color::DarkGrey, " Message handler. Body is a sequence of field assignments.\n"); + write_colored(out, Color::DarkGrey, " Expressions: field refs, args, literals, arithmetic (+, -, *).\n"); + write_colored(out, Color::White, " }\n"); + + // leaf + writeln!(out).ok(); + write_bold_colored(out, Color::Blue, " leaf "); + write_colored(out, Color::White, " {\n"); + write_colored(out, Color::Blue, " reads "); + write_colored(out, Color::White, ".\n"); + write_colored(out, Color::DarkGrey, " (optional) Declares which actor window this leaf may read.\n"); + write_colored(out, Color::Blue, " process "); + write_colored(out, Color::White, "{\n"); + write_colored(out, Color::Yellow, " forward"); + write_colored(out, Color::White, "(, (, ...))\n"); + write_colored(out, Color::DarkGrey, " Send a message to an actor's handler.\n"); + write_colored(out, Color::Yellow, " read"); + write_colored(out, Color::White, "(..)\n"); + write_colored(out, Color::DarkGrey, " Read a field from a window snapshot.\n"); + write_colored(out, Color::Yellow, " emit"); + write_colored(out, Color::White, "()\n"); + write_colored(out, Color::DarkGrey, " Emit a value to the output region (observable output).\n"); + write_colored(out, Color::White, " }\n"); + write_colored(out, Color::White, " }\n"); + + // pipeline + writeln!(out).ok(); + write_bold_colored(out, Color::Blue, " pipeline "); + write_colored(out, Color::White, " { -> -> ... }\n"); + write_colored(out, Color::DarkGrey, " Defines execution order. Stages are actor or leaf names.\n"); + write_colored(out, Color::DarkGrey, " Each core step walks the pipeline left to right.\n"); + + // core + writeln!(out).ok(); + write_bold_colored(out, Color::Blue, " core "); + write_colored(out, Color::White, " {\n"); + write_colored(out, Color::White, " actors: [, ...] "); + write_colored(out, Color::DarkGrey, "Actors in this core\n"); + write_colored(out, Color::White, " leaves: [, ...] "); + write_colored(out, Color::DarkGrey, "Leaves in this core\n"); + write_colored(out, Color::White, " pipelines: [, ...] "); + write_colored(out, Color::DarkGrey, "Pipelines to execute\n"); + write_colored(out, Color::White, " steps: "); + write_colored(out, Color::DarkGrey, "Number of execution steps (bounded)\n"); + write_colored(out, Color::White, " }\n"); + + // ── Types and Expressions ── + writeln!(out).ok(); + write_bold_colored(out, Color::Cyan, "Types & Expressions\n"); + write_colored(out, Color::DarkGrey, "───────────────────\n"); + write_colored(out, Color::DarkGrey, " Field types: "); + write_colored(out, Color::Yellow, "u64"); + write_colored(out, Color::DarkGrey, ", "); + write_colored(out, Color::Yellow, "i64"); + write_colored(out, Color::DarkGrey, ", "); + write_colored(out, Color::Yellow, "f64"); + writeln!(out).ok(); + write_colored(out, Color::DarkGrey, " Operators: "); + write_colored(out, Color::Yellow, "+ - *"); + write_colored(out, Color::DarkGrey, " (binary arithmetic)"); + writeln!(out).ok(); + write_colored(out, Color::DarkGrey, " Literals: "); + write_colored(out, Color::Cyan, "0, 1, 42, ..."); + write_colored(out, Color::DarkGrey, " (unsigned integers)"); + writeln!(out).ok(); + write_colored(out, Color::DarkGrey, " Expressions: "); + write_colored(out, Color::White, "field, arg, literal, (expr op expr)"); + writeln!(out).ok(); + + // ── Semantics ── + writeln!(out).ok(); + write_bold_colored(out, Color::Cyan, "Execution Semantics\n"); + write_colored(out, Color::DarkGrey, "───────────────────\n"); + write_colored(out, Color::DarkGrey, " Each core step:\n"); + write_colored(out, Color::DarkGrey, " 1. Walk pipeline stages left to right\n"); + write_colored(out, Color::DarkGrey, " 2. Leaves execute: forward messages, read windows, emit outputs\n"); + write_colored(out, Color::DarkGrey, " 3. Messages dispatched to actor handlers, updating state\n"); + write_colored(out, Color::DarkGrey, " 4. Window snapshots updated for next step\n"); + write_colored(out, Color::DarkGrey, " Execution is bounded (finite steps) — no unbounded loops.\n"); + write_colored(out, Color::DarkGrey, " Multi-line input: open braces auto-continue to the next line.\n"); + writeln!(out).ok(); +} + +fn handle_examples(arg: &str, state: &mut ReplState, out: &mut W) { + if arg.is_empty() { + writeln!(out).ok(); + write_bold_colored(out, Color::Cyan, "Built-in Examples\n"); + write_colored(out, Color::DarkGrey, "─────────────────\n"); + write_colored(out, Color::DarkGrey, " "); + write_bold_colored(out, Color::Yellow, "counter "); + write_colored(out, Color::White, "Simplest program: one actor, one message, one state field\n"); + write_colored(out, Color::DarkGrey, " "); + write_bold_colored(out, Color::Yellow, "window "); + write_colored(out, Color::White, "Introduces window reads and output emission via emit()\n"); + write_colored(out, Color::DarkGrey, " "); + write_bold_colored(out, Color::Yellow, "product "); + write_colored(out, Color::White, "Multi-field state, compound arithmetic, accumulation pattern\n"); + writeln!(out).ok(); + write_colored(out, Color::DarkGrey, " Usage: "); + write_colored(out, Color::Yellow, ":examples "); + write_colored(out, Color::DarkGrey, " — loads the example with an annotated walkthrough\n"); + writeln!(out).ok(); + } else { + let source = match arg { + "counter" => include_str!("../examples/counter.l1"), + "window" => include_str!("../examples/window.l1"), + "product" => include_str!("../examples/product.l1"), + _ => { + writeln!(out, "Unknown example: '{}'. Try: counter, window, product", arg).ok(); + return; + } + }; + state.source = source.to_string(); + match state.reparse() { + Ok(()) => { + writeln!(out).ok(); + write_bold_colored(out, Color::Cyan, &format!("Example: {}\n", arg)); + write_colored(out, Color::DarkGrey, &"─".repeat(10 + arg.len())); + writeln!(out).ok(); + display_annotated_example(out, arg, source); + writeln!(out).ok(); + write_colored(out, Color::DarkGrey, " Try: "); + write_colored(out, Color::Yellow, ":run"); + write_colored(out, Color::DarkGrey, " to execute, "); + write_colored(out, Color::Yellow, ":ir"); + write_colored(out, Color::DarkGrey, " for IR, "); + write_colored(out, Color::Yellow, ":verify"); + write_colored(out, Color::DarkGrey, " for formal verification\n"); + writeln!(out).ok(); + } + Err(e) => { + writeln!(out, "Error loading example: {}", e).ok(); + } + } + } +} + +/// Display an example with inline annotations explaining each construct. +fn display_annotated_example(out: &mut W, name: &str, _source: &str) { + match name { + "counter" => display_annotated_counter(out), + "window" => display_annotated_window(out), + "product" => display_annotated_product(out), + _ => {} + } +} + +/// Annotated walkthrough: counter example +/// The simplest L1 program — one actor, one message type, one state field. +fn display_annotated_counter(out: &mut W) { + writeln!(out).ok(); + write_colored(out, Color::DarkGrey, " ┌─ The simplest L1 program. One actor receives Increment messages\n"); + write_colored(out, Color::DarkGrey, " │ and counts up. A leaf sends the message each step.\n"); + writeln!(out).ok(); + + // actor counter { + write_colored(out, Color::Blue, " actor "); + write_bold_colored(out, Color::White, "counter"); + writeln!(out, " {{").ok(); + + // state + write_colored(out, Color::Blue, " state "); + writeln!(out, "{{").ok(); + write_colored(out, Color::White, " count"); + write_colored(out, Color::DarkGrey, ": "); + write_colored(out, Color::Yellow, "u64"); + write_colored(out, Color::DarkGrey, " = "); + write_colored(out, Color::Cyan, "0"); + writeln!(out).ok(); + writeln!(out, " }}").ok(); + write_colored(out, Color::DarkGrey, " ▲ One mutable field: count, starts at 0.\n"); + + // window + writeln!(out).ok(); + write_colored(out, Color::Blue, " window "); + write_colored(out, Color::White, "count_view"); + write_colored(out, Color::DarkGrey, " : ("); + write_colored(out, Color::White, "count"); + write_colored(out, Color::DarkGrey, ") readers("); + write_colored(out, Color::White, "display"); + writeln!(out, ")").ok(); + write_colored(out, Color::DarkGrey, " ▲ Exposes 'count' as a read-only window. The 'display' leaf\n"); + write_colored(out, Color::DarkGrey, " can read it safely without blocking the actor.\n"); + + // handler + writeln!(out).ok(); + write_colored(out, Color::Magenta, " on "); + write_colored(out, Color::White, "Increment"); + write_colored(out, Color::DarkGrey, "("); + write_colored(out, Color::White, "amount"); + write_colored(out, Color::DarkGrey, ": "); + write_colored(out, Color::Yellow, "u64"); + writeln!(out, ") {{").ok(); + write_colored(out, Color::White, " count = count + amount\n"); + writeln!(out, " }}").ok(); + write_colored(out, Color::DarkGrey, " ▲ Handler: when an Increment(amount) message arrives,\n"); + write_colored(out, Color::DarkGrey, " add amount to count. This is the only way state changes.\n"); + + writeln!(out, " }}").ok(); + + // leaf ticker + writeln!(out).ok(); + write_colored(out, Color::Blue, " leaf "); + write_bold_colored(out, Color::White, "ticker"); + writeln!(out, " {{").ok(); + write_colored(out, Color::Blue, " process "); + writeln!(out, "{{").ok(); + write_colored(out, Color::Yellow, " forward"); + write_colored(out, Color::DarkGrey, "(counter, "); + write_colored(out, Color::White, "Increment(1)"); + writeln!(out, ")").ok(); + writeln!(out, " }}").ok(); + writeln!(out, " }}").ok(); + write_colored(out, Color::DarkGrey, " ▲ Each step, ticker sends Increment(1) to counter.\n"); + write_colored(out, Color::DarkGrey, " Leaves are the edges of the system — they produce and consume.\n"); + + // leaf display + writeln!(out).ok(); + write_colored(out, Color::Blue, " leaf "); + write_bold_colored(out, Color::White, "display"); + writeln!(out, " {{").ok(); + write_colored(out, Color::Blue, " reads "); + write_colored(out, Color::White, "counter.count_view\n"); + write_colored(out, Color::Blue, " process "); + writeln!(out, "{{").ok(); + write_colored(out, Color::Yellow, " read"); + write_colored(out, Color::DarkGrey, "(counter.count_view."); + write_colored(out, Color::White, "count"); + writeln!(out, ")").ok(); + writeln!(out, " }}").ok(); + writeln!(out, " }}").ok(); + write_colored(out, Color::DarkGrey, " ▲ display reads the count window. 'reads' declares the dependency;\n"); + write_colored(out, Color::DarkGrey, " read() in process actually fetches the value.\n"); + + // pipeline + writeln!(out).ok(); + write_colored(out, Color::Blue, " pipeline "); + write_bold_colored(out, Color::White, "main"); + write_colored(out, Color::DarkGrey, " { "); + write_colored(out, Color::White, "ticker"); + write_colored(out, Color::Yellow, " -> "); + write_colored(out, Color::White, "counter"); + write_colored(out, Color::Yellow, " -> "); + write_colored(out, Color::White, "display"); + writeln!(out, " }}").ok(); + write_colored(out, Color::DarkGrey, " ▲ Execution order: ticker sends, counter processes, display reads.\n"); + + // core + writeln!(out).ok(); + write_colored(out, Color::Blue, " core "); + write_bold_colored(out, Color::White, "main"); + writeln!(out, " {{").ok(); + write_colored(out, Color::White, " actors: [counter] leaves: [ticker, display]\n"); + write_colored(out, Color::White, " pipelines: [main] steps: "); + write_colored(out, Color::Cyan, "5\n"); + writeln!(out, " }}").ok(); + write_colored(out, Color::DarkGrey, " ▲ Runs the pipeline for 5 steps. After: count = 5.\n"); + write_colored(out, Color::DarkGrey, " Execution is bounded — no infinite loops possible.\n"); +} + +/// Annotated walkthrough: window example +/// Introduces window reads and output emission. +fn display_annotated_window(out: &mut W) { + writeln!(out).ok(); + write_colored(out, Color::DarkGrey, " ┌─ Builds on counter by adding output emission. The 'observe' leaf\n"); + write_colored(out, Color::DarkGrey, " │ reads a window and emits values to the output region — making\n"); + write_colored(out, Color::DarkGrey, " │ results observable outside the system.\n"); + writeln!(out).ok(); + + // actor accumulator + write_colored(out, Color::Blue, " actor "); + write_bold_colored(out, Color::White, "accumulator"); + writeln!(out, " {{").ok(); + write_colored(out, Color::Blue, " state "); + writeln!(out, "{{").ok(); + write_colored(out, Color::White, " total"); + write_colored(out, Color::DarkGrey, ": "); + write_colored(out, Color::Yellow, "u64"); + write_colored(out, Color::DarkGrey, " = "); + write_colored(out, Color::Cyan, "0"); + writeln!(out).ok(); + writeln!(out, " }}").ok(); + write_colored(out, Color::DarkGrey, " ▲ Like counter, but named 'total' to emphasize accumulation.\n"); + + writeln!(out).ok(); + write_colored(out, Color::Blue, " window "); + write_colored(out, Color::White, "total_view"); + write_colored(out, Color::DarkGrey, " : ("); + write_colored(out, Color::White, "total"); + write_colored(out, Color::DarkGrey, ") readers("); + write_colored(out, Color::White, "observe"); + writeln!(out, ")").ok(); + write_colored(out, Color::DarkGrey, " ▲ Window exposes 'total' to the 'observe' leaf.\n"); + + writeln!(out).ok(); + write_colored(out, Color::Magenta, " on "); + write_colored(out, Color::White, "Add"); + write_colored(out, Color::DarkGrey, "("); + write_colored(out, Color::White, "value"); + write_colored(out, Color::DarkGrey, ": "); + write_colored(out, Color::Yellow, "u64"); + writeln!(out, ") {{").ok(); + write_colored(out, Color::White, " total = total + value\n"); + writeln!(out, " }}").ok(); + writeln!(out, " }}").ok(); + + // leaf source + writeln!(out).ok(); + write_colored(out, Color::Blue, " leaf "); + write_bold_colored(out, Color::White, "source"); + writeln!(out, " {{").ok(); + write_colored(out, Color::Blue, " process "); + writeln!(out, "{{").ok(); + write_colored(out, Color::Yellow, " forward"); + write_colored(out, Color::DarkGrey, "(accumulator, "); + write_colored(out, Color::White, "Add(10)"); + writeln!(out, ")").ok(); + writeln!(out, " }}").ok(); + writeln!(out, " }}").ok(); + write_colored(out, Color::DarkGrey, " ▲ Sends Add(10) each step. After 3 steps: total = 30.\n"); + + // leaf observe + writeln!(out).ok(); + write_colored(out, Color::Blue, " leaf "); + write_bold_colored(out, Color::White, "observe"); + writeln!(out, " {{").ok(); + write_colored(out, Color::Blue, " reads "); + write_colored(out, Color::White, "accumulator.total_view\n"); + write_colored(out, Color::Blue, " process "); + writeln!(out, "{{").ok(); + write_colored(out, Color::Yellow, " read"); + write_colored(out, Color::DarkGrey, "(accumulator.total_view."); + write_colored(out, Color::White, "total"); + writeln!(out, ")").ok(); + write_colored(out, Color::Yellow, " emit"); + write_colored(out, Color::DarkGrey, "("); + write_colored(out, Color::White, "total"); + writeln!(out, ")").ok(); + writeln!(out, " }}").ok(); + writeln!(out, " }}").ok(); + write_colored(out, Color::DarkGrey, " ▲ KEY DIFFERENCE: emit(total) writes the value to the output region.\n"); + write_colored(out, Color::DarkGrey, " This is how data leaves the system. read() fetches from the window;\n"); + write_colored(out, Color::DarkGrey, " emit() publishes the result. Emitted values: [10, 20, 30].\n"); + + // pipeline + writeln!(out).ok(); + write_colored(out, Color::Blue, " pipeline "); + write_bold_colored(out, Color::White, "main"); + write_colored(out, Color::DarkGrey, " { "); + write_colored(out, Color::White, "source"); + write_colored(out, Color::Yellow, " -> "); + write_colored(out, Color::White, "accumulator"); + write_colored(out, Color::Yellow, " -> "); + write_colored(out, Color::White, "observe"); + writeln!(out, " }}").ok(); + + // core + writeln!(out).ok(); + write_colored(out, Color::Blue, " core "); + write_bold_colored(out, Color::White, "main"); + writeln!(out, " {{").ok(); + write_colored(out, Color::White, " actors: [accumulator] leaves: [source, observe]\n"); + write_colored(out, Color::White, " pipelines: [main] steps: "); + write_colored(out, Color::Cyan, "3\n"); + writeln!(out, " }}").ok(); + write_colored(out, Color::DarkGrey, " ▲ 3 steps. Final total = 30. Output values = [10, 20, 30].\n"); +} + +/// Annotated walkthrough: product example +/// Multi-field state, compound arithmetic, accumulation pattern. +fn display_annotated_product(out: &mut W) { + writeln!(out).ok(); + write_colored(out, Color::DarkGrey, " ┌─ The most complex example. Demonstrates multi-field state,\n"); + write_colored(out, Color::DarkGrey, " │ compound arithmetic (price * qty), and multi-field windows.\n"); + write_colored(out, Color::DarkGrey, " │ Models a product ledger tracking purchases.\n"); + writeln!(out).ok(); + + // actor ledger + write_colored(out, Color::Blue, " actor "); + write_bold_colored(out, Color::White, "ledger"); + writeln!(out, " {{").ok(); + write_colored(out, Color::Blue, " state "); + writeln!(out, "{{").ok(); + write_colored(out, Color::White, " count"); + write_colored(out, Color::DarkGrey, ": "); + write_colored(out, Color::Yellow, "u64"); + write_colored(out, Color::DarkGrey, " = "); + write_colored(out, Color::Cyan, "0"); + writeln!(out).ok(); + write_colored(out, Color::White, " total"); + write_colored(out, Color::DarkGrey, ": "); + write_colored(out, Color::Yellow, "u64"); + write_colored(out, Color::DarkGrey, " = "); + write_colored(out, Color::Cyan, "0"); + writeln!(out).ok(); + writeln!(out, " }}").ok(); + write_colored(out, Color::DarkGrey, " ▲ Two fields: count tracks quantity, total tracks cost.\n"); + write_colored(out, Color::DarkGrey, " Both start at 0 and are updated by the Purchase handler.\n"); + + // window + writeln!(out).ok(); + write_colored(out, Color::Blue, " window "); + write_colored(out, Color::White, "summary"); + write_colored(out, Color::DarkGrey, " : ("); + write_colored(out, Color::White, "count, total"); + write_colored(out, Color::DarkGrey, ") readers("); + write_colored(out, Color::White, "report"); + writeln!(out, ")").ok(); + write_colored(out, Color::DarkGrey, " ▲ Multi-field window: exposes both count AND total.\n"); + write_colored(out, Color::DarkGrey, " The report leaf can read either field.\n"); + + // handler + writeln!(out).ok(); + write_colored(out, Color::Magenta, " on "); + write_colored(out, Color::White, "Purchase"); + write_colored(out, Color::DarkGrey, "("); + write_colored(out, Color::White, "price"); + write_colored(out, Color::DarkGrey, ": "); + write_colored(out, Color::Yellow, "u64"); + write_colored(out, Color::DarkGrey, ", "); + write_colored(out, Color::White, "qty"); + write_colored(out, Color::DarkGrey, ": "); + write_colored(out, Color::Yellow, "u64"); + writeln!(out, ") {{").ok(); + write_colored(out, Color::White, " count = count + qty\n"); + write_colored(out, Color::White, " total = total + (price * qty)\n"); + writeln!(out, " }}").ok(); + write_colored(out, Color::DarkGrey, " ▲ Multi-arg handler with compound expression: (price * qty).\n"); + write_colored(out, Color::DarkGrey, " Parenthesized sub-expressions compute intermediate values.\n"); + write_colored(out, Color::DarkGrey, " Each Purchase(7, 3) adds 3 to count, 21 to total.\n"); + + writeln!(out, " }}").ok(); + + // leaf buy + writeln!(out).ok(); + write_colored(out, Color::Blue, " leaf "); + write_bold_colored(out, Color::White, "buy"); + writeln!(out, " {{").ok(); + write_colored(out, Color::Blue, " process "); + writeln!(out, "{{").ok(); + write_colored(out, Color::Yellow, " forward"); + write_colored(out, Color::DarkGrey, "(ledger, "); + write_colored(out, Color::White, "Purchase(7, 3)"); + writeln!(out, ")").ok(); + writeln!(out, " }}").ok(); + writeln!(out, " }}").ok(); + write_colored(out, Color::DarkGrey, " ▲ Sends Purchase(price=7, qty=3) each step.\n"); + + // leaf report + writeln!(out).ok(); + write_colored(out, Color::Blue, " leaf "); + write_bold_colored(out, Color::White, "report"); + writeln!(out, " {{").ok(); + write_colored(out, Color::Blue, " reads "); + write_colored(out, Color::White, "ledger.summary\n"); + write_colored(out, Color::Blue, " process "); + writeln!(out, "{{").ok(); + write_colored(out, Color::Yellow, " read"); + write_colored(out, Color::DarkGrey, "(ledger.summary."); + write_colored(out, Color::White, "total"); + writeln!(out, ")").ok(); + write_colored(out, Color::Yellow, " emit"); + write_colored(out, Color::DarkGrey, "("); + write_colored(out, Color::White, "total"); + writeln!(out, ")").ok(); + writeln!(out, " }}").ok(); + writeln!(out, " }}").ok(); + write_colored(out, Color::DarkGrey, " ▲ Reads the 'total' field from the summary window, then emits it.\n"); + write_colored(out, Color::DarkGrey, " Could also read 'count' — the window exposes both.\n"); + + // pipeline + writeln!(out).ok(); + write_colored(out, Color::Blue, " pipeline "); + write_bold_colored(out, Color::White, "main"); + write_colored(out, Color::DarkGrey, " { "); + write_colored(out, Color::White, "buy"); + write_colored(out, Color::Yellow, " -> "); + write_colored(out, Color::White, "ledger"); + write_colored(out, Color::Yellow, " -> "); + write_colored(out, Color::White, "report"); + writeln!(out, " }}").ok(); + + // core + writeln!(out).ok(); + write_colored(out, Color::Blue, " core "); + write_bold_colored(out, Color::White, "main"); + writeln!(out, " {{").ok(); + write_colored(out, Color::White, " actors: [ledger] leaves: [buy, report]\n"); + write_colored(out, Color::White, " pipelines: [main] steps: "); + write_colored(out, Color::Cyan, "4\n"); + writeln!(out, " }}").ok(); + write_colored(out, Color::DarkGrey, " ▲ 4 steps. Each step: count += 3, total += 21.\n"); + write_colored(out, Color::DarkGrey, " Final: count = 12, total = 84.\n"); + write_colored(out, Color::DarkGrey, " Emitted values: [21, 42, 63, 84] (running total each step).\n"); +} + +/// Run the interactive REPL loop reading from stdin. +pub fn run_repl() { + let stdin = io::stdin(); + let mut stdout = io::stdout(); + let mut state = ReplState::new(); + let mut multi_line_buf = String::new(); + + write_bold_colored(&mut stdout, Color::Cyan, "L1 REPL"); + writeln!(stdout, " — type declarations or :help for commands.").ok(); + + loop { + if multi_line_buf.is_empty() { + write_colored(&mut stdout, Color::Green, "l1> "); + } else { + write_colored(&mut stdout, Color::Green, "... "); + } + stdout.flush().ok(); + + let mut line = String::new(); + match stdin.lock().read_line(&mut line) { + Ok(0) => break, // EOF + Ok(_) => {} + Err(_) => break, + } + + let line = line.trim_end_matches('\n').trim_end_matches('\r'); + + // If we're accumulating a multi-line input + if !multi_line_buf.is_empty() { + multi_line_buf.push('\n'); + multi_line_buf.push_str(line); + if !has_unmatched_braces(&multi_line_buf) { + let input = multi_line_buf.clone(); + multi_line_buf.clear(); + add_declaration(&mut state, &input, &mut stdout); + } + continue; + } + + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + + // Check for commands + if let Some((cmd, arg)) = parse_command(trimmed) { + if !execute_command(&mut state, cmd, arg, &mut stdout) { + break; + } + continue; + } + + // Otherwise it's a declaration — check for multi-line + if has_unmatched_braces(trimmed) { + multi_line_buf = trimmed.to_string(); + continue; + } + + // Single-line declaration + add_declaration(&mut state, trimmed, &mut stdout); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_command_basic() { + assert_eq!(parse_command(":quit"), Some(("quit", ""))); + assert_eq!(parse_command(":load foo.l1"), Some(("load", "foo.l1"))); + assert_eq!(parse_command(":run 10"), Some(("run", "10"))); + assert_eq!(parse_command(":help"), Some(("help", ""))); + assert_eq!(parse_command("actor foo {}"), None); + } + + #[test] + fn unmatched_braces_detection() { + assert!(has_unmatched_braces("actor foo {")); + assert!(!has_unmatched_braces("actor foo { }")); + assert!(has_unmatched_braces("actor foo { state {")); + assert!(!has_unmatched_braces("actor foo { state { } }")); + assert!(!has_unmatched_braces("")); + } + + #[test] + fn repl_state_lifecycle() { + let mut state = ReplState::new(); + assert!(state.source.is_empty()); + assert!(state.program.is_none()); + + state.source = include_str!("../examples/counter.l1").to_string(); + state.reparse().unwrap(); + assert!(state.program.is_some()); + let prog = state.program.as_ref().unwrap(); + assert_eq!(prog.actors.len(), 1); + + state.reset(); + assert!(state.source.is_empty()); + assert!(state.program.is_none()); + } + + #[test] + fn execute_help_command() { + let mut state = ReplState::new(); + let mut buf = Vec::new(); + let cont = execute_command(&mut state, "help", "", &mut buf); + assert!(cont); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("L1 REPL Commands")); + assert!(output.contains(":load")); + assert!(output.contains(":quit")); + // Check comprehensive DSL reference sections + assert!(output.contains("Declaration Forms"), "should have declaration forms section"); + assert!(output.contains("Types"), "should have types section"); + assert!(output.contains("Execution Semantics"), "should have semantics section"); + assert!(output.contains("window"), "should describe window construct"); + assert!(output.contains("forward"), "should describe forward action"); + assert!(output.contains("emit"), "should describe emit action"); + } + + #[test] + fn execute_quit_command() { + let mut state = ReplState::new(); + let mut buf = Vec::new(); + let cont = execute_command(&mut state, "quit", "", &mut buf); + assert!(!cont); + } + + #[test] + fn execute_reset_command() { + let mut state = ReplState::new(); + state.source = "something".to_string(); + let mut buf = Vec::new(); + execute_command(&mut state, "reset", "", &mut buf); + assert!(state.source.is_empty()); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("cleared")); + } + + #[test] + fn execute_examples_list() { + let mut state = ReplState::new(); + let mut buf = Vec::new(); + execute_command(&mut state, "examples", "", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("counter")); + assert!(output.contains("window")); + assert!(output.contains("product")); + } + + #[test] + fn execute_examples_load_counter() { + let mut state = ReplState::new(); + let mut buf = Vec::new(); + execute_command(&mut state, "examples", "counter", &mut buf); + let output = String::from_utf8(buf).unwrap(); + // Annotated walkthrough output contains "Example: counter" and annotations + assert!(output.contains("Example: counter")); + assert!(output.contains("Increment"), "should show annotated handler"); + assert!(output.contains("count"), "should show state field"); + assert!(state.program.is_some()); + assert_eq!(state.program.as_ref().unwrap().actors[0].name, "counter"); + } + + #[test] + fn execute_examples_load_product() { + let mut state = ReplState::new(); + let mut buf = Vec::new(); + execute_command(&mut state, "examples", "product", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("Example: product")); + assert!(output.contains("Purchase"), "should show annotated handler"); + assert!(output.contains("ledger"), "should show actor name"); + assert!(state.program.is_some()); + } + + #[test] + fn execute_ir_with_program() { + let mut state = ReplState::new(); + state.source = include_str!("../examples/counter.l1").to_string(); + state.reparse().unwrap(); + let mut buf = Vec::new(); + execute_command(&mut state, "ir", "", &mut buf); + let output = String::from_utf8(buf).unwrap(); + // Output includes ANSI color codes; check that key names appear + assert!(output.contains("counter"), "should mention counter actor"); + assert!(output.contains("pipeline"), "should mention pipeline section"); + assert!(output.contains("main"), "should mention pipeline name"); + } + + #[test] + fn execute_ir_without_program() { + let mut state = ReplState::new(); + let mut buf = Vec::new(); + execute_command(&mut state, "ir", "", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("No program loaded")); + } + + #[test] + fn execute_l0_with_program() { + let mut state = ReplState::new(); + state.source = include_str!("../examples/counter.l1").to_string(); + state.reparse().unwrap(); + let mut buf = Vec::new(); + execute_command(&mut state, "l0", "", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("Regions") || output.contains("region")); + } + + #[test] + fn execute_run_with_program() { + let mut state = ReplState::new(); + state.source = include_str!("../examples/counter.l1").to_string(); + state.reparse().unwrap(); + let mut buf = Vec::new(); + execute_command(&mut state, "run", "", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("Execution complete")); + } + + #[test] + fn execute_run_with_step_override() { + let mut state = ReplState::new(); + state.source = include_str!("../examples/counter.l1").to_string(); + state.reparse().unwrap(); + let mut buf = Vec::new(); + execute_command(&mut state, "run", "3", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("Execution complete")); + } + + #[test] + fn execute_verify_with_program() { + let mut state = ReplState::new(); + state.source = include_str!("../examples/counter.l1").to_string(); + state.reparse().unwrap(); + let mut buf = Vec::new(); + execute_command(&mut state, "verify", "", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("PASS") || output.contains("pass")); + } + + #[test] + fn execute_run_without_program() { + let mut state = ReplState::new(); + let mut buf = Vec::new(); + execute_command(&mut state, "run", "", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("No program loaded")); + } + + #[test] + fn execute_unknown_command() { + let mut state = ReplState::new(); + let mut buf = Vec::new(); + execute_command(&mut state, "foobar", "", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("Unknown command")); + } + + #[test] + fn add_declaration_incremental() { + let mut state = ReplState::new(); + let mut buf = Vec::new(); + + // Add an actor + let actor_src = r#"actor counter { + state { + count: u64 = 0 + } + window count_view : (count) readers(display) + on Increment(amount: u64) { + count = count + amount + } +}"#; + add_declaration(&mut state, actor_src, &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("Added actor")); + assert!(state.program.is_some()); + assert_eq!(state.program.as_ref().unwrap().actors.len(), 1); + } + + #[test] + fn add_declaration_parse_error() { + let mut state = ReplState::new(); + let mut buf = Vec::new(); + add_declaration(&mut state, "invalid garbage {{{", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("Parse error") || output.contains("error")); + // State should be empty (reverted) + assert!(state.source.is_empty() || state.source.trim().is_empty()); + } + + #[test] + fn override_steps_in_source() { + let source = "core main { actors: [a] leaves: [b] pipelines: [c] steps: 5 }"; + let modified = override_steps(source, 10); + assert!(modified.contains("steps: 10")); + assert!(!modified.contains("steps: 5")); + } + + #[test] + fn load_file_command() { + let mut state = ReplState::new(); + let mut buf = Vec::new(); + execute_command(&mut state, "load", "examples/counter.l1", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("Loaded")); + assert!(state.program.is_some()); + } + + #[test] + fn load_file_not_found() { + let mut state = ReplState::new(); + let mut buf = Vec::new(); + execute_command(&mut state, "load", "nonexistent.l1", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("Error reading")); + } + + #[test] + fn load_no_arg() { + let mut state = ReplState::new(); + let mut buf = Vec::new(); + execute_command(&mut state, "load", "", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("Usage")); + } + + #[test] + fn examples_unknown_name() { + let mut state = ReplState::new(); + let mut buf = Vec::new(); + execute_command(&mut state, "examples", "nonexistent", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("Unknown example")); + } + + #[test] + fn execute_examples_load_window() { + let mut state = ReplState::new(); + let mut buf = Vec::new(); + execute_command(&mut state, "examples", "window", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("Example: window")); + assert!(output.contains("emit"), "should explain emit in annotations"); + assert!(output.contains("accumulator"), "should show actor name"); + assert!(state.program.is_some()); + } + + #[test] + fn examples_list_shows_descriptions() { + let mut state = ReplState::new(); + let mut buf = Vec::new(); + execute_command(&mut state, "examples", "", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("counter")); + assert!(output.contains("window")); + assert!(output.contains("product")); + assert!(output.contains("Built-in Examples"), "should have header"); + } + + #[test] + fn help_dsl_reference_completeness() { + let mut state = ReplState::new(); + let mut buf = Vec::new(); + execute_command(&mut state, "help", "", &mut buf); + let output = String::from_utf8(buf).unwrap(); + // All four declaration forms documented + assert!(output.contains("actor")); + assert!(output.contains("leaf")); + assert!(output.contains("pipeline")); + assert!(output.contains("core")); + // Leaf actions documented + assert!(output.contains("forward")); + assert!(output.contains("read")); + assert!(output.contains("emit")); + // Types documented + assert!(output.contains("u64")); + assert!(output.contains("i64")); + assert!(output.contains("f64")); + } + + #[test] + fn q_alias_for_quit() { + let mut state = ReplState::new(); + let mut buf = Vec::new(); + let cont = execute_command(&mut state, "q", "", &mut buf); + assert!(!cont); + } + + #[test] + fn partial_program_run_gives_error() { + // Actor only — no core — should give a meaningful error on :run + let mut state = ReplState::new(); + let actor_src = r#"actor counter { state { count: u64 = 0 } on Increment(amount: u64) { count = count + amount } }"#; + state.source = actor_src.to_string(); + state.reparse().unwrap(); + + let mut buf = Vec::new(); + execute_command(&mut state, "run", "", &mut buf); + let output = String::from_utf8(buf).unwrap(); + // Should show an error, not crash + assert!(output.contains("Error") || output.contains("error")); + } + + #[test] + fn partial_program_verify_gives_error() { + // Actor only — no core — should give a meaningful error on :verify + let mut state = ReplState::new(); + let actor_src = r#"actor counter { state { count: u64 = 0 } on Increment(amount: u64) { count = count + amount } }"#; + state.source = actor_src.to_string(); + state.reparse().unwrap(); + + let mut buf = Vec::new(); + execute_command(&mut state, "verify", "", &mut buf); + let output = String::from_utf8(buf).unwrap(); + // Should show a verification failure message, not crash + assert!(output.contains("failed") || output.contains("Error") || output.contains("error")); + } + + #[test] + fn partial_program_ir_shows_what_exists() { + // Actor only — :ir should still display the partial IR + let mut state = ReplState::new(); + let actor_src = r#"actor counter { state { count: u64 = 0 } on Increment(amount: u64) { count = count + amount } }"#; + state.source = actor_src.to_string(); + state.reparse().unwrap(); + + let mut buf = Vec::new(); + execute_command(&mut state, "ir", "", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("counter"), "should show partial program IR"); + } + + #[test] + fn l0_without_program() { + let mut state = ReplState::new(); + let mut buf = Vec::new(); + execute_command(&mut state, "l0", "", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("No program loaded")); + } + + #[test] + fn verify_without_program() { + let mut state = ReplState::new(); + let mut buf = Vec::new(); + execute_command(&mut state, "verify", "", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("No program loaded")); + } + + #[test] + fn run_window_example_shows_outputs() { + let mut state = ReplState::new(); + state.source = include_str!("../examples/window.l1").to_string(); + state.reparse().unwrap(); + let mut buf = Vec::new(); + execute_command(&mut state, "run", "", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("Execution complete")); + assert!(output.contains("output values")); + } + + #[test] + fn run_product_example_shows_outputs() { + let mut state = ReplState::new(); + state.source = include_str!("../examples/product.l1").to_string(); + state.reparse().unwrap(); + let mut buf = Vec::new(); + execute_command(&mut state, "run", "", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("Execution complete")); + assert!(output.contains("output values")); + } + + #[test] + fn verify_all_examples() { + for (name, source) in [ + ("counter", include_str!("../examples/counter.l1")), + ("window", include_str!("../examples/window.l1")), + ("product", include_str!("../examples/product.l1")), + ] { + let mut state = ReplState::new(); + state.source = source.to_string(); + state.reparse().unwrap(); + let mut buf = Vec::new(); + execute_command(&mut state, "verify", "", &mut buf); + let output = String::from_utf8(buf).unwrap(); + assert!( + output.contains("PASS"), + "{} verification should show PASS, got: {}", + name, + output + ); + } + } + + #[test] + fn detect_declaration_kinds() { + assert_eq!(detect_declaration_kind("actor foo {}"), "actor"); + assert_eq!(detect_declaration_kind("leaf bar {}"), "leaf"); + assert_eq!(detect_declaration_kind("pipeline p { a -> b }"), "pipeline"); + assert_eq!(detect_declaration_kind("core c {}"), "core"); + assert_eq!(detect_declaration_kind("something else"), "declaration"); + } + + #[test] + fn parse_command_edge_cases() { + // Whitespace handling + assert_eq!(parse_command(" :quit "), Some(("quit", ""))); + assert_eq!(parse_command(":load foo.l1 "), Some(("load", "foo.l1"))); + // Not a command + assert_eq!(parse_command(""), None); + assert_eq!(parse_command(" "), None); + assert_eq!(parse_command("hello"), None); + } +} diff --git a/src/translate_validate.rs b/src/translate_validate.rs new file mode 100644 index 0000000..8b0ae09 --- /dev/null +++ b/src/translate_validate.rs @@ -0,0 +1,1127 @@ +//! Translation Validation Engine — formal verification of L1→L0 lowering correctness. +//! +//! Three verification tools, inspired by real verification techniques: +//! +//! 1. **Trace Equivalence Checker** (CompCert-style translation validation): +//! Run L1 interpreter and L0 execution on the same program, compare traces. +//! +//! 2. **Structural Correspondence Checker** (refinement type system-inspired): +//! Walk L1 IR and L0 IR in parallel, verify structural invariants. +//! +//! 3. **Bounded State Space Explorer** (CBMC-style bounded model checking): +//! Enumerate all reachable states, verify no dead code or invalid states. + +use crate::codegen; +use crate::emit; +use crate::interpret_l1; +use crate::l0_ir::{L0Program, Op, RegionKind}; +use crate::l1_ir::*; +use crate::parser; +use crate::verify_l0; +use crate::verify_l1; +use std::collections::HashSet; +use std::fmt; +use std::time::Instant; + +// ─── Result types ─────────────────────────────────────────────────── + +/// A single diagnostic from a verification tool. +#[derive(Debug, Clone)] +pub struct Diagnostic { + pub tool: ToolName, + pub severity: Severity, + pub message: String, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Severity { + Error, + Warning, + Info, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ToolName { + TraceEquivalence, + StructuralCorrespondence, + BoundedModelCheck, +} + +impl fmt::Display for ToolName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ToolName::TraceEquivalence => write!(f, "Trace Equivalence"), + ToolName::StructuralCorrespondence => write!(f, "Structural Correspondence"), + ToolName::BoundedModelCheck => write!(f, "Bounded Model Check"), + } + } +} + +impl fmt::Display for Severity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Severity::Error => write!(f, "ERROR"), + Severity::Warning => write!(f, "WARN"), + Severity::Info => write!(f, "INFO"), + } + } +} + +impl fmt::Display for Diagnostic { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[{}] {}: {}", self.tool, self.severity, self.message) + } +} + +/// Result from a single verification tool. +#[derive(Debug, Clone)] +pub struct ToolResult { + pub tool: ToolName, + pub passed: bool, + pub diagnostics: Vec, + pub elapsed_us: u64, +} + +/// A mapping between an L1 state field and its L0 region location. +#[derive(Debug, Clone)] +pub struct FieldMapping { + pub actor: String, + pub field: String, + pub l0_region: String, + pub l0_offset: usize, +} + +/// Statistics from bounded state space exploration. +#[derive(Debug, Clone)] +pub struct ExplorationStats { + pub states_explored: usize, + pub l0_blocks_reached: usize, + pub l0_blocks_total: usize, + pub l0_regions_used: usize, + pub l0_regions_total: usize, +} + +/// Complete verification report from all three tools. +#[derive(Debug, Clone)] +pub struct VerificationReport { + pub results: Vec, + pub field_mappings: Vec, + pub exploration_stats: Option, +} + +impl VerificationReport { + pub fn all_passed(&self) -> bool { + self.results.iter().all(|r| r.passed) + } +} + +impl fmt::Display for VerificationReport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "=== Translation Validation Report ===")?; + for result in &self.results { + let status = if result.passed { "PASS" } else { "FAIL" }; + writeln!(f, " [{}] {} ({}us)", status, result.tool, result.elapsed_us)?; + for diag in &result.diagnostics { + writeln!(f, " {}", diag)?; + } + } + if !self.field_mappings.is_empty() { + writeln!(f, " --- Field Mappings ---")?; + for m in &self.field_mappings { + writeln!(f, " {}.{} -> {}[{}]", m.actor, m.field, m.l0_region, m.l0_offset)?; + } + } + if let Some(stats) = &self.exploration_stats { + writeln!(f, " --- Exploration Stats ---")?; + writeln!(f, " states explored: {}", stats.states_explored)?; + writeln!(f, " L0 blocks reached: {}/{}", stats.l0_blocks_reached, stats.l0_blocks_total)?; + writeln!(f, " L0 regions used: {}/{}", stats.l0_regions_used, stats.l0_regions_total)?; + } + let overall = if self.all_passed() { "PASS" } else { "FAIL" }; + writeln!(f, " Overall: {}", overall)?; + Ok(()) + } +} + +// ─── Main entry point ─────────────────────────────────────────────── + +/// Run all three verification tools on an L1 program and its L0 emission. +pub fn validate(l1: &L1Program, l0: &L0Program) -> VerificationReport { + let t1 = check_trace_equivalence(l1, l0); + let t2 = check_structural_correspondence(l1, l0); + let t3 = check_bounded_state_space(l1, l0); + + let field_mappings = build_field_mappings(l1); + let exploration_stats = Some(build_exploration_stats(l1, l0)); + + VerificationReport { + results: vec![t1, t2, t3], + field_mappings, + exploration_stats, + } +} + +/// Convenience: parse source, emit, then validate. +pub fn validate_source(source: &str) -> Result { + let l1 = parser::parse(source).map_err(|e| format!("parse: {}", e))?; + verify_l1::verify(&l1).map_err(|e| format!("L1 verify: {}", e))?; + let l0 = emit::emit(&l1).map_err(|e| format!("emit: {}", e))?; + verify_l0::verify(&l0).map_err(|e| format!("L0 verify: {:?}", e))?; + Ok(validate(&l1, &l0)) +} + +// ─── Tool 1: Trace Equivalence Checker ────────────────────────────── +// +// Inspired by CompCert's translation validation: run both the reference +// (L1 interpreter) and the compiled (L0 codegen) on the same program, +// then compare observable outputs step by step. + +fn check_trace_equivalence(l1: &L1Program, l0: &L0Program) -> ToolResult { + let start = Instant::now(); + let mut diagnostics = Vec::new(); + let mut passed = true; + + // Run L1 interpreter + let l1_trace = match interpret_l1::interpret(l1) { + Ok(t) => t, + Err(e) => { + diagnostics.push(Diagnostic { + tool: ToolName::TraceEquivalence, + severity: Severity::Error, + message: format!("L1 interpreter failed: {}", e), + }); + return ToolResult { + tool: ToolName::TraceEquivalence, + passed: false, + diagnostics, + elapsed_us: start.elapsed().as_micros() as u64, + }; + } + }; + + // Run L0 execution + let mc = match codegen::codegen(l0) { + Ok(mc) => mc, + Err(e) => { + diagnostics.push(Diagnostic { + tool: ToolName::TraceEquivalence, + severity: Severity::Error, + message: format!("L0 codegen failed: {}", e), + }); + return ToolResult { + tool: ToolName::TraceEquivalence, + passed: false, + diagnostics, + elapsed_us: start.elapsed().as_micros() as u64, + }; + } + }; + + let l0_result = match codegen::execute(&mc) { + Ok(r) => r, + Err(e) => { + diagnostics.push(Diagnostic { + tool: ToolName::TraceEquivalence, + severity: Severity::Error, + message: format!("L0 execution failed: {}", e), + }); + return ToolResult { + tool: ToolName::TraceEquivalence, + passed: false, + diagnostics, + elapsed_us: start.elapsed().as_micros() as u64, + }; + } + }; + + // Compare final actor states + for actor_snap in &l1_trace.final_actor_states { + let region_name = format!("{}_state", actor_snap.actor_name); + let actor_decl = l1.actors.iter().find(|a| a.name == actor_snap.actor_name); + let mut offset = 0usize; + for (field_name, l1_val) in &actor_snap.fields { + match l0_result.read_u64(®ion_name, offset) { + Some(l0_val) => { + if *l1_val != l0_val { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::TraceEquivalence, + severity: Severity::Error, + message: format!( + "final state mismatch: {}.{} = {} (L1) vs {} (L0)", + actor_snap.actor_name, field_name, l1_val, l0_val + ), + }); + } + } + None => { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::TraceEquivalence, + severity: Severity::Error, + message: format!( + "cannot read L0 region {} at offset {} for {}.{}", + region_name, offset, actor_snap.actor_name, field_name + ), + }); + } + } + // Advance offset by field size (all fields are 8 bytes) + if let Some(decl) = actor_decl { + if let Some(f) = decl.state.iter().find(|f| f.name == *field_name) { + offset += field_type_size(&f.ty); + } else { + offset += 8; + } + } else { + offset += 8; + } + } + } + + // Compare outputs + let l0_outputs = l0_result.read_output_values(); + if l1_trace.all_outputs != l0_outputs { + passed = false; + let max_len = l1_trace.all_outputs.len().max(l0_outputs.len()); + for i in 0..max_len { + let l1_v = l1_trace.all_outputs.get(i); + let l0_v = l0_outputs.get(i); + match (l1_v, l0_v) { + (Some(a), Some(b)) if a != b => { + diagnostics.push(Diagnostic { + tool: ToolName::TraceEquivalence, + severity: Severity::Error, + message: format!("output[{}] mismatch: {} (L1) vs {} (L0)", i, a, b), + }); + } + (Some(a), None) => { + diagnostics.push(Diagnostic { + tool: ToolName::TraceEquivalence, + severity: Severity::Error, + message: format!("output[{}]: L1 produced {} but L0 has no output", i, a), + }); + } + (None, Some(b)) => { + diagnostics.push(Diagnostic { + tool: ToolName::TraceEquivalence, + severity: Severity::Error, + message: format!("output[{}]: L0 produced {} but L1 has no output", i, b), + }); + } + _ => {} + } + } + } + + // Compare output count + if l1_trace.all_outputs.len() == l0_outputs.len() && l1_trace.all_outputs == l0_outputs { + diagnostics.push(Diagnostic { + tool: ToolName::TraceEquivalence, + severity: Severity::Info, + message: format!( + "outputs match: {} values, final states agree across {} actors", + l1_trace.all_outputs.len(), + l1_trace.final_actor_states.len() + ), + }); + } + + ToolResult { + tool: ToolName::TraceEquivalence, + passed, + diagnostics, + elapsed_us: start.elapsed().as_micros() as u64, + } +} + +// ─── Tool 2: Structural Correspondence Checker ────────────────────── +// +// Inspired by refinement type systems / Liquid Haskell: walk L1 IR and L0 IR +// in parallel and verify structural invariants. Catches *classes* of bugs, +// not just specific inputs. + +fn check_structural_correspondence(l1: &L1Program, l0: &L0Program) -> ToolResult { + let start = Instant::now(); + let mut diagnostics = Vec::new(); + let mut passed = true; + + let core = match l1.cores.first() { + Some(c) => c, + None => { + diagnostics.push(Diagnostic { + tool: ToolName::StructuralCorrespondence, + severity: Severity::Error, + message: "no core defined in L1 program".to_string(), + }); + return ToolResult { + tool: ToolName::StructuralCorrespondence, + passed: false, + diagnostics, + elapsed_us: start.elapsed().as_micros() as u64, + }; + } + }; + + // Check 1: Every L1 actor state field maps to an L0 State region slot + for actor_name in &core.actors { + let actor = match l1.actors.iter().find(|a| a.name == *actor_name) { + Some(a) => a, + None => continue, + }; + let expected_region = format!("{}_state", actor.name); + let l0_region = l0.regions.iter().find(|r| r.name == expected_region); + + match l0_region { + Some(region) => { + if region.kind != RegionKind::State { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::StructuralCorrespondence, + severity: Severity::Error, + message: format!( + "region '{}' should be State kind, got {:?}", + expected_region, region.kind + ), + }); + } + + // Verify region is large enough for all fields + let mut total_size = 0usize; + for field in &actor.state { + total_size += field_type_size(&field.ty); + } + if region.size < total_size { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::StructuralCorrespondence, + severity: Severity::Error, + message: format!( + "region '{}' is {} bytes but actor '{}' needs {} bytes for {} fields", + expected_region, region.size, actor.name, total_size, actor.state.len() + ), + }); + } + + // Check that L0 blocks contain loads/stores at correct offsets for each field + let mut expected_offset = 0usize; + for field in &actor.state { + let has_init_store = l0.blocks.iter().any(|b| { + b.name == "entry" && b.ops.iter().any(|op| { + matches!(op, Op::Store { region, offset, .. } + if *region == expected_region && *offset == expected_offset) + }) + }); + if !has_init_store { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::StructuralCorrespondence, + severity: Severity::Error, + message: format!( + "missing init store for {}.{} at {}[{}]", + actor.name, field.name, expected_region, expected_offset + ), + }); + } + expected_offset += field_type_size(&field.ty); + } + } + None => { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::StructuralCorrespondence, + severity: Severity::Error, + message: format!( + "L1 actor '{}' has no corresponding L0 region '{}'", + actor.name, expected_region + ), + }); + } + } + } + + // Check 2: Every L1 handler maps to queue_pop + arithmetic + store in L0 + for actor_name in &core.actors { + let actor = match l1.actors.iter().find(|a| a.name == *actor_name) { + Some(a) => a, + None => continue, + }; + for handler in &actor.handlers { + let expected_queue = format!("{}_{}_queue", actor.name, handler.message_type.to_lowercase()); + let has_queue_region = l0.regions.iter().any(|r| r.name == expected_queue && r.kind == RegionKind::Queue); + if !has_queue_region { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::StructuralCorrespondence, + severity: Severity::Error, + message: format!( + "L1 handler '{}.on {}' has no L0 queue region '{}'", + actor.name, handler.message_type, expected_queue + ), + }); + continue; + } + + // Check step block has queue_pop from this queue (one per arg) + let step_block = l0.blocks.iter().find(|b| b.name == "step"); + if let Some(step) = step_block { + let pop_count = step.ops.iter().filter(|op| { + matches!(op, Op::QueuePop { region, .. } if *region == expected_queue) + }).count(); + if pop_count != handler.args.len() { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::StructuralCorrespondence, + severity: Severity::Error, + message: format!( + "handler '{}.on {}' has {} args but L0 has {} queue_pop ops for '{}'", + actor.name, handler.message_type, handler.args.len(), pop_count, expected_queue + ), + }); + } + } + + // Check handler body arithmetic operations are reflected in L0 + let arith_count_l1 = count_arith_ops(&handler.body); + if let Some(step) = step_block { + let arith_count_l0 = step.ops.iter().filter(|op| { + matches!(op, Op::Arith { .. }) + }).count(); + // L0 should have at least as many arith ops as L1 handler + // (may have more due to step counter increment, which uses ArithImm) + if arith_count_l0 < arith_count_l1 { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::StructuralCorrespondence, + severity: Severity::Error, + message: format!( + "handler '{}.on {}' has {} arithmetic ops but L0 step block only has {}", + actor.name, handler.message_type, arith_count_l1, arith_count_l0 + ), + }); + } + } + } + } + + // Check 3: Every L1 leaf forward maps to queue_push in L0 + for leaf_name in &core.leaves { + let leaf = match l1.leaves.iter().find(|l| l.name == *leaf_name) { + Some(l) => l, + None => continue, + }; + for action in &leaf.actions { + if let LeafAction::Forward { target, .. } = action { + let expected_queue = format!("{}_", target); + let step_block = l0.blocks.iter().find(|b| b.name == "step"); + if let Some(step) = step_block { + let has_push = step.ops.iter().any(|op| { + matches!(op, Op::QueuePush { region, .. } if region.starts_with(&expected_queue)) + }); + if !has_push { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::StructuralCorrespondence, + severity: Severity::Error, + message: format!( + "leaf '{}' forwards to '{}' but L0 has no queue_push to a {}_* queue", + leaf.name, target, target + ), + }); + } + } + } + } + } + + // Check 4: Every L1 window read maps to a Load from the actor's state region + for leaf_name in &core.leaves { + let leaf = match l1.leaves.iter().find(|l| l.name == *leaf_name) { + Some(l) => l, + None => continue, + }; + for action in &leaf.actions { + if let LeafAction::ReadWindow { actor, field, .. } = action { + let expected_region = format!("{}_state", actor); + let actor_decl = l1.actors.iter().find(|a| a.name == *actor); + let expected_offset = actor_decl.map(|a| { + let mut off = 0usize; + for f in &a.state { + if f.name == *field { + return off; + } + off += field_type_size(&f.ty); + } + off + }).unwrap_or(0); + + let step_block = l0.blocks.iter().find(|b| b.name == "step"); + if let Some(step) = step_block { + let has_load = step.ops.iter().any(|op| { + matches!(op, Op::Load { region, offset, .. } + if *region == expected_region && *offset == expected_offset) + }); + if !has_load { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::StructuralCorrespondence, + severity: Severity::Error, + message: format!( + "leaf '{}' reads {}.{} but L0 has no load from {}[{}]", + leaf.name, actor, field, expected_region, expected_offset + ), + }); + } + } + } + } + } + + // Check 5: If any leaf has Emit, L0 must have an Output region + let has_emit = core.leaves.iter().any(|leaf_name| { + l1.leaves.iter() + .find(|l| l.name == *leaf_name) + .map(|l| l.actions.iter().any(|a| matches!(a, LeafAction::Emit(_)))) + .unwrap_or(false) + }); + if has_emit { + let has_output_region = l0.regions.iter().any(|r| r.kind == RegionKind::Output); + if !has_output_region { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::StructuralCorrespondence, + severity: Severity::Error, + message: "L1 has emit actions but L0 has no Output region".to_string(), + }); + } + } + + // Check 6: L0 control structure: must have entry, loop_check, step, exit blocks + let required_blocks = ["entry", "loop_check", "step", "exit"]; + for name in &required_blocks { + if !l0.blocks.iter().any(|b| b.name == *name) { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::StructuralCorrespondence, + severity: Severity::Error, + message: format!("missing required L0 block '{}'", name), + }); + } + } + + // Check 7: step counter region exists and is used + let has_step_counter = l0.regions.iter().any(|r| r.name == "step_counter" && r.kind == RegionKind::Control); + if !has_step_counter { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::StructuralCorrespondence, + severity: Severity::Error, + message: "missing step_counter Control region in L0".to_string(), + }); + } + + if passed { + diagnostics.push(Diagnostic { + tool: ToolName::StructuralCorrespondence, + severity: Severity::Info, + message: "all structural invariants verified".to_string(), + }); + } + + ToolResult { + tool: ToolName::StructuralCorrespondence, + passed, + diagnostics, + elapsed_us: start.elapsed().as_micros() as u64, + } +} + +// ─── Tool 3: Bounded State Space Explorer ─────────────────────────── +// +// Inspired by CBMC / bounded model checking: enumerate all reachable states +// in the finite state space (bounded by step count × actor state domain). +// Verify: no unreachable L0 blocks, no dead regions, all L0 states correspond +// to valid L1 states. + +fn check_bounded_state_space(l1: &L1Program, l0: &L0Program) -> ToolResult { + let start = Instant::now(); + let mut diagnostics = Vec::new(); + let mut passed = true; + + let core = match l1.cores.first() { + Some(c) => c, + None => { + diagnostics.push(Diagnostic { + tool: ToolName::BoundedModelCheck, + severity: Severity::Error, + message: "no core defined".to_string(), + }); + return ToolResult { + tool: ToolName::BoundedModelCheck, + passed: false, + diagnostics, + elapsed_us: start.elapsed().as_micros() as u64, + }; + } + }; + + // Run the L1 interpreter to get the full trace (all reachable states) + let trace = match interpret_l1::interpret(l1) { + Ok(t) => t, + Err(e) => { + diagnostics.push(Diagnostic { + tool: ToolName::BoundedModelCheck, + severity: Severity::Error, + message: format!("cannot explore states: {}", e), + }); + return ToolResult { + tool: ToolName::BoundedModelCheck, + passed: false, + diagnostics, + elapsed_us: start.elapsed().as_micros() as u64, + }; + } + }; + + // Collect all unique states observed during execution + let mut unique_states: HashSet)>> = HashSet::new(); + // Initial state (all zeros or init values) + let init_state: Vec<(String, Vec<(String, u64)>)> = l1.actors.iter() + .filter(|a| core.actors.contains(&a.name)) + .map(|a| { + let fields: Vec<(String, u64)> = a.state.iter() + .map(|f| (f.name.clone(), f.init)) + .collect(); + (a.name.clone(), fields) + }) + .collect(); + unique_states.insert(init_state); + + for step in &trace.steps { + let state: Vec<(String, Vec<(String, u64)>)> = step.actor_states.iter() + .map(|snap| (snap.actor_name.clone(), snap.fields.clone())) + .collect(); + unique_states.insert(state); + } + + let states_explored = unique_states.len(); + + diagnostics.push(Diagnostic { + tool: ToolName::BoundedModelCheck, + severity: Severity::Info, + message: format!( + "explored {} unique states across {} steps", + states_explored, core.steps + ), + }); + + // Check 1: All L0 blocks are reachable (no dead blocks) + let reachable_blocks = compute_reachable_blocks(l0); + let total_blocks = l0.blocks.len(); + let unreachable: Vec<&str> = l0.blocks.iter() + .filter(|b| !reachable_blocks.contains(b.name.as_str())) + .map(|b| b.name.as_str()) + .collect(); + + if !unreachable.is_empty() { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::BoundedModelCheck, + severity: Severity::Error, + message: format!("unreachable L0 blocks: {}", unreachable.join(", ")), + }); + } + + // Check 2: All L0 regions are accessed (no dead regions) + let accessed_regions = compute_accessed_regions(l0); + let dead_regions: Vec<&str> = l0.regions.iter() + .filter(|r| !accessed_regions.contains(r.name.as_str())) + .map(|r| r.name.as_str()) + .collect(); + + if !dead_regions.is_empty() { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::BoundedModelCheck, + severity: Severity::Error, + message: format!("dead L0 regions (never accessed): {}", dead_regions.join(", ")), + }); + } + + // Check 3: Step count is bounded and matches L0 loop limit + let loop_check = l0.blocks.iter().find(|b| b.name == "loop_check"); + if let Some(lc) = loop_check { + let loop_limit = lc.ops.iter().find_map(|op| { + if let Op::LoadImm { value, .. } = op { + Some(*value) + } else { + None + } + }); + match loop_limit { + Some(limit) if limit == core.steps => { + diagnostics.push(Diagnostic { + tool: ToolName::BoundedModelCheck, + severity: Severity::Info, + message: format!("L0 loop limit ({}) matches L1 core steps", limit), + }); + } + Some(limit) => { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::BoundedModelCheck, + severity: Severity::Error, + message: format!( + "L0 loop limit ({}) does not match L1 core steps ({})", + limit, core.steps + ), + }); + } + None => { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::BoundedModelCheck, + severity: Severity::Error, + message: "cannot find loop limit in L0 loop_check block".to_string(), + }); + } + } + } + + // Check 4: Termination — exit block contains Terminate + let exit_block = l0.blocks.iter().find(|b| b.name == "exit"); + match exit_block { + Some(eb) => { + if !eb.ops.iter().any(|op| matches!(op, Op::Terminate)) { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::BoundedModelCheck, + severity: Severity::Error, + message: "exit block does not contain Terminate instruction".to_string(), + }); + } + } + None => { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::BoundedModelCheck, + severity: Severity::Error, + message: "no exit block in L0 program".to_string(), + }); + } + } + + // Check 5: The control flow is well-formed — entry jumps to loop_check, + // loop_check branches to step/exit, step jumps back to loop_check + if let Some(entry) = l0.blocks.iter().find(|b| b.name == "entry") { + let jumps_to_loop = entry.ops.iter().any(|op| { + matches!(op, Op::Jump { target } if target == "loop_check") + }); + if !jumps_to_loop { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::BoundedModelCheck, + severity: Severity::Error, + message: "entry block does not jump to loop_check".to_string(), + }); + } + } + + if let Some(step) = l0.blocks.iter().find(|b| b.name == "step") { + let jumps_back = step.ops.iter().any(|op| { + matches!(op, Op::Jump { target } if target == "loop_check") + }); + if !jumps_back { + passed = false; + diagnostics.push(Diagnostic { + tool: ToolName::BoundedModelCheck, + severity: Severity::Error, + message: "step block does not jump back to loop_check".to_string(), + }); + } + } + + if passed { + diagnostics.push(Diagnostic { + tool: ToolName::BoundedModelCheck, + severity: Severity::Info, + message: format!( + "all {} L0 blocks reachable, all {} regions accessed, bounded execution verified", + total_blocks, l0.regions.len() + ), + }); + } + + ToolResult { + tool: ToolName::BoundedModelCheck, + passed, + diagnostics, + elapsed_us: start.elapsed().as_micros() as u64, + } +} + +// ─── Helpers ──────────────────────────────────────────────────────── + +fn field_type_size(ty: &FieldType) -> usize { + match ty { + FieldType::U64 | FieldType::I64 | FieldType::F64 => 8, + } +} + +/// Count the number of arithmetic operations in an L1 expression. +fn count_arith_ops(expr: &Expr) -> usize { + match expr { + Expr::Arith(_, lhs, rhs) => 1 + count_arith_ops(lhs) + count_arith_ops(rhs), + Expr::Assign(_, val) => count_arith_ops(val), + Expr::Block(exprs) => exprs.iter().map(count_arith_ops).sum(), + Expr::Lit(_) | Expr::Var(_) => 0, + } +} + +/// Compute the set of reachable blocks via control flow from "entry". +fn compute_reachable_blocks(l0: &L0Program) -> HashSet { + let mut reachable = HashSet::new(); + let mut worklist = vec!["entry".to_string()]; + + while let Some(name) = worklist.pop() { + if !reachable.insert(name.clone()) { + continue; + } + if let Some(block) = l0.blocks.iter().find(|b| b.name == name) { + for op in &block.ops { + match op { + Op::Jump { target } => { + worklist.push(target.clone()); + } + Op::Branch { true_target, false_target, .. } => { + worklist.push(true_target.clone()); + worklist.push(false_target.clone()); + } + _ => {} + } + } + } + } + + reachable +} + +/// Compute the set of regions that are accessed (loaded from, stored to, +/// or used with queue operations) across all blocks. +fn compute_accessed_regions(l0: &L0Program) -> HashSet { + let mut accessed = HashSet::new(); + + for block in &l0.blocks { + for op in &block.ops { + match op { + Op::Load { region, .. } | Op::Store { region, .. } => { + accessed.insert(region.clone()); + } + Op::QueuePush { region, .. } | Op::QueuePop { region, .. } => { + accessed.insert(region.clone()); + } + _ => {} + } + } + } + + accessed +} + +/// Build the field mappings between L1 actor fields and L0 region slots. +fn build_field_mappings(l1: &L1Program) -> Vec { + let mut mappings = Vec::new(); + let core = match l1.cores.first() { + Some(c) => c, + None => return mappings, + }; + + for actor_name in &core.actors { + let actor = match l1.actors.iter().find(|a| a.name == *actor_name) { + Some(a) => a, + None => continue, + }; + let region_name = format!("{}_state", actor.name); + let mut offset = 0usize; + for field in &actor.state { + mappings.push(FieldMapping { + actor: actor.name.clone(), + field: field.name.clone(), + l0_region: region_name.clone(), + l0_offset: offset, + }); + offset += field_type_size(&field.ty); + } + } + + mappings +} + +/// Build exploration statistics from L1 trace and L0 structure. +fn build_exploration_stats(l1: &L1Program, l0: &L0Program) -> ExplorationStats { + let reachable = compute_reachable_blocks(l0); + let accessed = compute_accessed_regions(l0); + let core = l1.cores.first(); + let steps = core.map(|c| c.steps).unwrap_or(0); + + // Count unique states: initial state + one per step + let states_explored = (steps + 1) as usize; + + ExplorationStats { + states_explored, + l0_blocks_reached: reachable.len(), + l0_blocks_total: l0.blocks.len(), + l0_regions_used: accessed.len(), + l0_regions_total: l0.regions.len(), + } +} + +// ─── Tests ────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + fn load_and_validate(source: &str) -> VerificationReport { + let l1 = parser::parse(source).expect("parse"); + verify_l1::verify(&l1).expect("verify L1"); + let l0 = emit::emit(&l1).expect("emit"); + verify_l0::verify(&l0).expect("verify L0"); + validate(&l1, &l0) + } + + #[test] + fn counter_passes_all_tools() { + let source = include_str!("../examples/counter.l1"); + let report = load_and_validate(source); + assert!(report.all_passed(), "counter should pass: {}", report); + assert_eq!(report.results.len(), 3); + for result in &report.results { + assert!(result.passed, "{} should pass: {:?}", result.tool, result.diagnostics); + } + } + + #[test] + fn window_passes_all_tools() { + let source = include_str!("../examples/window.l1"); + let report = load_and_validate(source); + assert!(report.all_passed(), "window should pass: {}", report); + } + + #[test] + fn product_passes_all_tools() { + let source = include_str!("../examples/product.l1"); + let report = load_and_validate(source); + assert!(report.all_passed(), "product should pass: {}", report); + } + + #[test] + fn field_mappings_correct_for_product() { + let source = include_str!("../examples/product.l1"); + let report = load_and_validate(source); + // Product has actor "ledger" with fields count (offset 0) and total (offset 8) + assert!(report.field_mappings.len() >= 2); + let count_mapping = report.field_mappings.iter() + .find(|m| m.actor == "ledger" && m.field == "count") + .expect("should have count mapping"); + assert_eq!(count_mapping.l0_region, "ledger_state"); + assert_eq!(count_mapping.l0_offset, 0); + + let total_mapping = report.field_mappings.iter() + .find(|m| m.actor == "ledger" && m.field == "total") + .expect("should have total mapping"); + assert_eq!(total_mapping.l0_region, "ledger_state"); + assert_eq!(total_mapping.l0_offset, 8); + } + + #[test] + fn exploration_stats_present() { + let source = include_str!("../examples/counter.l1"); + let report = load_and_validate(source); + let stats = report.exploration_stats.as_ref().expect("should have stats"); + assert!(stats.states_explored > 0); + assert_eq!(stats.l0_blocks_reached, stats.l0_blocks_total); + assert_eq!(stats.l0_regions_used, stats.l0_regions_total); + } + + #[test] + fn validate_source_convenience() { + let source = include_str!("../examples/counter.l1"); + let report = validate_source(source).expect("should validate"); + assert!(report.all_passed()); + } + + #[test] + fn deliberate_bug_caught_by_trace_checker() { + // Create a program where we deliberately corrupt the L0 emission + // by modifying the emitted L0 IR to use wrong arithmetic. + let source = include_str!("../examples/counter.l1"); + let l1 = parser::parse(source).expect("parse"); + verify_l1::verify(&l1).expect("verify L1"); + let mut l0 = emit::emit(&l1).expect("emit"); + + // Corrupt: change the Add in the step block to Sub + // This means instead of count = count + amount, it does count = count - amount + let step_block = l0.blocks.iter_mut().find(|b| b.name == "step").unwrap(); + for op in step_block.ops.iter_mut() { + if let Op::Arith { kind, .. } = op { + if *kind == crate::l0_ir::ArithKind::Add { + *kind = crate::l0_ir::ArithKind::Sub; + break; // Only corrupt the first add (the handler arith, not step counter) + } + } + } + + let report = validate(&l1, &l0); + + // Trace equivalence should catch this + let trace_result = report.results.iter() + .find(|r| r.tool == ToolName::TraceEquivalence) + .expect("should have trace result"); + assert!(!trace_result.passed, "trace checker should catch the corruption"); + + // Should have a diagnostic about the mismatch + let has_mismatch = trace_result.diagnostics.iter().any(|d| { + d.severity == Severity::Error && + (d.message.contains("mismatch") || d.message.contains("output")) + }); + assert!(has_mismatch, "should have a mismatch diagnostic: {:?}", trace_result.diagnostics); + } + + #[test] + fn deliberate_missing_region_caught_by_structural_checker() { + // Remove a state region from L0 to test structural checker + let source = include_str!("../examples/counter.l1"); + let l1 = parser::parse(source).expect("parse"); + verify_l1::verify(&l1).expect("verify L1"); + let mut l0 = emit::emit(&l1).expect("emit"); + + // Remove the counter_state region + l0.regions.retain(|r| r.name != "counter_state"); + + let report = validate(&l1, &l0); + + let structural_result = report.results.iter() + .find(|r| r.tool == ToolName::StructuralCorrespondence) + .expect("should have structural result"); + assert!(!structural_result.passed, "structural checker should catch missing region"); + assert!(structural_result.diagnostics.iter().any(|d| { + d.message.contains("counter_state") + })); + } + + #[test] + fn report_display_formatting() { + let source = include_str!("../examples/counter.l1"); + let report = load_and_validate(source); + let text = format!("{}", report); + assert!(text.contains("Translation Validation Report")); + assert!(text.contains("PASS")); + assert!(text.contains("Trace Equivalence")); + assert!(text.contains("Structural Correspondence")); + assert!(text.contains("Bounded Model Check")); + assert!(text.contains("Field Mappings")); + assert!(text.contains("counter.count")); + assert!(text.contains("Exploration Stats")); + } +} diff --git a/src/verify_l0.rs b/src/verify_l0.rs new file mode 100644 index 0000000..9cf3626 --- /dev/null +++ b/src/verify_l0.rs @@ -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> { + let mut errors = Vec::new(); + + if program.blocks.is_empty() { + errors.push(L0VerifyError { + block: "".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(), ®ions, &mut errors); + } + Op::Store { size, region, offset, .. } => { + check_region_write(block, op, region, *offset, size.bytes(), ®ions, &mut errors); + } + Op::QueuePush { region, .. } => { + check_queue_region(block, op, region, ®ions, &mut errors); + } + Op::QueuePop { region, .. } => { + check_queue_region(block, op, region, ®ions, &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, +) { + 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, +) { + 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, +) { + 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"); + } +} diff --git a/src/verify_l1.rs b/src/verify_l1.rs new file mode 100644 index 0000000..fd01e44 --- /dev/null +++ b/src/verify_l1.rs @@ -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")); + } +}