Skip to main content

logicaffeine_compile/spatial/lower/
rdu_lanes.rs

1//! The RDU front door's three lanes — how ANY LOGOS program reaches the SambaNova descriptor.
2//!
3//! LOGOS already lowers to RISC-V, C, WASM, CUDA, Tensix, the spatial fabric (the 117-op ISA) and
4//! Plasticine. `largo emit rdu` lowers a program onto the SN40L / SN50 descriptor through ONE of three
5//! lanes, chosen by the program's SHAPE ([`lane_of`]) and printed by name:
6//!
7//! * **tensor** — the program declares a tensor or the tensor recognizer reads a matmul / elementwise /
8//!   swiglu / fused-row loop nest: the spatial IR front end (`pipeline::compile_to_spatial`), placed and
9//!   scheduled on the mesh, the certified neutral placement (`lower::rdu`).
10//! * **loop** ([`lower_loop_lane`]) — the first function is a counted range loop with scalar carried state:
11//!   the Plasticine lane on the RDU mesh descriptor (`PlasticineTarget::rdu`) — metapipeline II under the
12//!   Tungsten-calibrated PCU model, checkerboard placement + static routing, the SAT certificate of the
13//!   routed body, the VM anchor, Spatial source for the public PIR chain.
14//! * **whole-program** ([`lower_whole_program_lane`]) — everything else the fabric's decision datapath
15//!   covers: the first function (if/else, collections on the state plane, structs, enums, helper calls
16//!   inlined) as a `Scalar` stage applied lane-wise over an input vector (`y[i] = f(x_0[i], …)`), or a
17//!   `## Main` of `Show`s as the tape-select stage (lane i emits Show i) — verified, certified, and handed
18//!   to `RduSpatialBackend` as the neutral placement.
19//!
20//! What the dataflow lanes REFUSE, by name (never a wrong stage): recursion (the decision front end would
21//! otherwise take the first-return path only — a spatial call stack is what it needs; the whole-program
22//! call-stack engine runs it ≡VM meanwhile), loops outside the loop lane's single-stage counted shape, early
23//! returns, a `Show` of Text on the tape, and every op on the fabric frontier (`fabric_tile::frontier_reason`).
24
25use std::collections::BTreeSet;
26use std::panic::{catch_unwind, AssertUnwindSafe};
27
28use serde::{Deserialize, Serialize};
29use serde_json::json;
30
31use super::super::ir::*;
32use super::super::passes::{SpatialReject, LV_ITEM_X, LV_ITEM_Y};
33use super::super::pipeline::run_passes;
34use super::super::rdu_chip::RduChip;
35use super::super::target::SpatialTarget;
36use crate::codegen_sva::fabric_rtl::{
37    decision_fabric_from_source, whole_program_fabric_from_source, CmpKind, DecisionFabric,
38    LoopCell, Src,
39};
40use crate::codegen_sva::fabric_target::Precision;
41use crate::codegen_sva::fabric_tile::{fabric_tile, frontier_reason};
42use crate::codegen_sva::plasticine::PlasticineTarget;
43use crate::codegen_sva::plasticine_report::{
44    counted_loop_fabric, lower_counted_loop, PlasticineArtifact, CERTIFICATE_WIDTH,
45};
46use crate::tensor_recognize::{
47    recognize_elementwise, recognize_fused_row, recognize_matmul, recognize_swiglu,
48};
49use crate::vm::{CompiledProgram, Compiler, Op};
50use logicaffeine_language::ast::stmt::Stmt;
51
52/// Which lane a program takes — a function of its shape, printed by `largo emit rdu` as `lane: <name>`.
53#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
54#[serde(rename_all = "kebab-case")]
55pub enum RduLane {
56    Tensor,
57    Loop,
58    WholeProgram,
59}
60
61impl RduLane {
62    pub const ALL: [RduLane; 3] = [RduLane::Tensor, RduLane::Loop, RduLane::WholeProgram];
63
64    /// The printed spelling.
65    pub fn as_str(self) -> &'static str {
66        match self {
67            RduLane::Tensor => "tensor",
68            RduLane::Loop => "loop",
69            RduLane::WholeProgram => "whole-program",
70        }
71    }
72
73    pub fn parse(s: &str) -> Option<RduLane> {
74        RduLane::ALL.into_iter().find(|l| l.as_str() == s.trim())
75    }
76}
77
78/// The lanes an input vector of the whole-program lane carries: four PCUs' worth of the unit model's
79/// sixteen SIMD lanes — enough to exercise every path of a decision body at the VM anchor's sample inputs.
80pub const WHOLE_PROGRAM_LANES: usize = 64;
81
82/// The VM anchor's sample inputs for a function of `arity` parameters: small, distinct, positive — the
83/// same samples `compile_to_plasticine` anchors on.
84pub fn anchor_params(arity: usize) -> Vec<i64> {
85    (0..arity).map(|i| 8 + i as i64).collect()
86}
87
88/// The function names `source` ITSELF declares (`## To [native] [private] <name>`), read off the text.
89///
90/// The parsed statement list is NOT the program's own: `loader::apply_prelude` demand-imports every stdlib
91/// module the program references and splices its definitions AHEAD of the user's, so the first parsed
92/// `FunctionDef` of `Let c be tcpConnect(host, port).` is tcp.lg's `tcpConnect`, not the function the
93/// program is about. The lane's unit is the program's own first function; this set is what says which.
94fn own_function_names(source: &str) -> BTreeSet<String> {
95    source
96        .lines()
97        .filter_map(|line| {
98            let mut rest = line.trim().strip_prefix("## To ")?;
99            loop {
100                match rest
101                    .strip_prefix("native ")
102                    .or_else(|| rest.strip_prefix("private "))
103                {
104                    Some(r) => rest = r,
105                    None => break,
106                }
107            }
108            let name = rest.split(|c: char| c == '(' || c.is_whitespace()).next()?;
109            (!name.is_empty()).then(|| name.to_string())
110        })
111        .collect()
112}
113
114/// The first function `source` itself declares — the unit the loop and whole-program lanes lower — as
115/// `(name, arity)`; `None` for a `## Main`-only program or one that does not parse. A prelude module
116/// spliced in by demand (`own_function_names`) is never the unit.
117pub fn first_function(source: &str) -> Option<(String, usize)> {
118    let own = own_function_names(source);
119    if own.is_empty() {
120        return None;
121    }
122    crate::ui_bridge::with_parsed_program(source, |parsed, interner| {
123        let (stmts, _, _) = parsed.ok()?;
124        stmts.iter().find_map(|s| match s {
125            Stmt::FunctionDef { name, params, .. } => {
126                let name = interner.resolve(*name).to_string();
127                own.contains(&name).then_some((name, params.len()))
128            }
129            _ => None,
130        })
131    })
132}
133
134/// Whether `source` declares a tensor — `Let X be a R by C tensor of …`, the tensor sugar's own opening
135/// (the parser's `try_parse_tensor_new`), read off the text so the lane is known before any op is recognized.
136pub fn has_tensor_sugar(source: &str) -> bool {
137    source.lines().any(|line| {
138        let line = line.trim();
139        if line.starts_with('#') || !line.starts_with("Let ") || !line.contains(" be a ") {
140            return false;
141        }
142        let words: Vec<&str> = line.split_whitespace().collect();
143        words.windows(4).any(|w| {
144            w[0] == "by"
145                && w[1].parse::<u64>().is_ok()
146                && w[2] == "tensor"
147                && w[3].starts_with("of")
148        })
149    })
150}
151
152/// Whether the tensor recognizer reads a matmul / elementwise / swiglu / fused-row loop nest in `source`
153/// (the plain-loop tensor programs the spatial front end lowers without sugar).
154pub fn recognizes_a_tensor_op(source: &str) -> bool {
155    crate::ui_bridge::with_parsed_program(source, |parsed, interner| {
156        let Ok((stmts, _, _)) = parsed else {
157            return false;
158        };
159        recognize_matmul(stmts, interner).is_ok()
160            || recognize_elementwise(stmts, interner).is_ok()
161            || recognize_swiglu(stmts, interner).is_ok()
162            || recognize_fused_row(stmts, interner).is_ok()
163    })
164}
165
166/// The lane `source` takes: tensor when it declares a tensor or a tensor op is recognized; loop when its
167/// first function is the counted range loop the metapipeline model takes; whole-program otherwise (a
168/// source that does not parse falls there too — its lowering reports the parse error typed).
169pub fn lane_of(source: &str) -> RduLane {
170    if has_tensor_sugar(source) || recognizes_a_tensor_op(source) {
171        return RduLane::Tensor;
172    }
173    if let Some((name, _)) = first_function(source) {
174        if counted_loop_fabric(source, &name).is_ok() {
175            return RduLane::Loop;
176        }
177    }
178    RduLane::WholeProgram
179}
180
181// ── The loop lane ────────────────────────────────────────────────────────────────────────────────
182
183/// What the loop lane hands `largo emit rdu`: the Plasticine artifact of the first function on the RDU
184/// mesh descriptor, with its pieces rendered — the placement, the modeled report row, the Spatial source,
185/// the certificate summary bound to the certificate by its digest, and the text an engineer reads first.
186#[derive(Debug)]
187pub struct LoopLaneArtifact {
188    /// The checkerboard placement (pretty JSON).
189    pub placement_json: String,
190    /// The modeled report row: II, latencies, utilization, the VM anchor, the certificate row (pretty JSON).
191    pub report_json: String,
192    /// Spatial source for the counted loop (a standalone `SpatialApp`).
193    pub spatial_source: String,
194    /// The SAT certificate summary — kind, what it proves, steps, variables, width, rechecked, digest — or
195    /// the reason it was omitted.
196    pub certificate_json: String,
197    /// One screen of text: the lane, the placement, the II, the anchor, the certificate.
198    pub pretty: String,
199    /// The Plasticine artifact itself (the loop fabric, the metapipeline, the placement object, the
200    /// certificate object, the regression-harness Spatial app).
201    pub plasticine: PlasticineArtifact,
202}
203
204/// Lower the first function of `source` — a counted range loop — onto `chip`'s mesh descriptor through the
205/// Plasticine lane, anchored to the VM at [`anchor_params`]. A program whose first function is not that
206/// loop is refused with the lane's own reason.
207pub fn lower_loop_lane(source: &str, chip: RduChip) -> Result<LoopLaneArtifact, String> {
208    let (fn_name, arity) = first_function(source).ok_or_else(|| {
209        "no region function found (the loop lane's unit is a counted-loop function)".to_string()
210    })?;
211    let params = anchor_params(arity);
212    let target = PlasticineTarget::rdu(chip);
213    let art = lower_counted_loop(source, &fn_name, &params, &target)?;
214    let certificate = match (
215        &art.certificate,
216        &art.row.certificate,
217        &art.row.certificate_omitted,
218    ) {
219        (Some(pc), Some(row), _) => json!({
220            "kind": "sat-placement",
221            "proves": row.proves,
222            "steps": row.steps,
223            "vars": row.vars,
224            "width": CERTIFICATE_WIDTH,
225            "rechecked": pc.recheck(),
226            "digest": format!("{:016x}", pc.digest()),
227        }),
228        (_, _, Some(why)) => json!({ "kind": "omitted", "reason": why }),
229        _ => json!({ "kind": "omitted", "reason": "no certificate was produced" }),
230    };
231    let certificate_json = format!(
232        "{}\n",
233        serde_json::to_string_pretty(&certificate).expect("a certificate summary serializes")
234    );
235    let row = &art.row;
236    let mut pretty = String::new();
237    pretty.push_str("lane: loop\n");
238    pretty.push_str(&format!(
239        "program: `{fn_name}` ({arity} parameter{}) on {}\n",
240        if arity == 1 { "" } else { "s" },
241        target.name
242    ));
243    pretty.push_str(&format!(
244        "placement: pcu {}/{} pmu {}/{} ({}x{} checkerboard) router {} routed statically {}\n",
245        row.pcu_used,
246        row.pcu_total,
247        row.pmu_used,
248        row.pmu_total,
249        target.pcu_rows,
250        target.pcu_cols,
251        row.router,
252        row.routed_statically
253    ));
254    for s in &row.stages {
255        pretty.push_str(&format!(
256            "stage `{}`: II {} (resource {} recurrence {} memory {}) latency {} cells {} pcus {}\n",
257            s.name, s.ii, s.resource_ii, s.recurrence_ii, s.memory_ii, s.latency, s.cells, s.pcus
258        ));
259    }
260    pretty.push_str(&format!(
261        "metapipeline: II {} iterations {} fill {} compute {} modeled cycles {} (provenance {})\n",
262        row.pipeline_ii,
263        row.iterations,
264        row.fill_cycles,
265        row.compute_cycles,
266        row.modeled_cycles,
267        row.provenance
268    ));
269    if let Some(a) = &row.vm_anchor {
270        pretty.push_str(&format!(
271            "vm anchor: {fn_name}({}) = {} (VM) = {} (fabric) = {} (pipelined at II {}, hazards {})\n",
272            a.params.iter().map(|p| p.to_string()).collect::<Vec<_>>().join(", "),
273            a.vm,
274            a.fabric,
275            a.pipelined_at_ii,
276            row.pipeline_ii,
277            a.hazards_at_ii
278        ));
279    }
280    match (&art.certificate, &row.certificate) {
281        (Some(pc), Some(c)) => pretty.push_str(&format!(
282            "certificate: SAT (PR/RUP) {} steps {} vars at {} bits, rechecked {}, digest {:016x}\n",
283            c.steps,
284            c.vars,
285            CERTIFICATE_WIDTH,
286            c.rechecked,
287            pc.digest()
288        )),
289        _ => pretty.push_str(&format!(
290            "certificate: omitted — {}\n",
291            row.certificate_omitted
292                .as_deref()
293                .unwrap_or("no certificate")
294        )),
295    }
296    pretty.push_str(&format!("spatial: Logos_{fn_name} (SpatialApp) + the Logos_{fn_name} regression app for the public PIR chain\n"));
297    Ok(LoopLaneArtifact {
298        placement_json: art.placement_json(),
299        report_json: art.row_json(),
300        spatial_source: art.spatial.clone(),
301        certificate_json,
302        pretty,
303        plasticine: art,
304    })
305}
306
307// ── The whole-program lane ──────────────────────────────────────────────────────────────────────
308
309/// Run `f`, turning a panic into an `Err` carrying its message, with the default panic printer silenced —
310/// the fabric front ends panic on shapes outside their family (their honest boundary); here that becomes a
311/// typed refusal.
312fn quietly<T>(f: impl FnOnce() -> T) -> Result<T, String> {
313    let prev = std::panic::take_hook();
314    std::panic::set_hook(Box::new(|_| {}));
315    let r = catch_unwind(AssertUnwindSafe(f));
316    std::panic::set_hook(prev);
317    r.map_err(|p| {
318        p.downcast_ref::<String>()
319            .cloned()
320            .or_else(|| p.downcast_ref::<&str>().map(|s| s.to_string()))
321            .unwrap_or_else(|| "the lowering panicked".to_string())
322    })
323}
324
325/// The unit the whole-program lane lowers, with the bytecode facts the gates need.
326enum Unit {
327    /// The first function: its name, arity, and the ops of its body plus every function it reaches.
328    Function { name: String, arity: usize },
329    /// A `## Main` of `Show`s.
330    Main,
331}
332
333struct Scan {
334    unit: Unit,
335    /// A back-edge (a loop) in the unit or a function it reaches, with where.
336    loop_in: Option<String>,
337    /// A cycle in the call graph reachable from the unit, with the function that closes it.
338    recursion: Option<String>,
339    /// A branch at the top level of `Main` (a conditional `Show` has no tape form).
340    branch_in_main: bool,
341    /// The first op with no fabric tile, with its frontier reason.
342    frontier: Option<&'static str>,
343}
344
345/// The range of `f`'s ops: its entry up to the next function's entry (or the end of the code).
346fn function_range(program: &CompiledProgram, entry: usize) -> std::ops::Range<usize> {
347    let end = program
348        .functions
349        .iter()
350        .map(|g| g.entry_pc)
351        .filter(|&e| e > entry)
352        .min()
353        .unwrap_or(program.code.len());
354    entry..end
355}
356
357/// The ops of Main: the code before the first function (or the whole code), up to its `Halt`.
358fn main_range(program: &CompiledProgram) -> std::ops::Range<usize> {
359    let first_fn = program
360        .functions
361        .iter()
362        .map(|f| f.entry_pc)
363        .min()
364        .unwrap_or(program.code.len());
365    let halt = program.code[..first_fn]
366        .iter()
367        .position(|op| matches!(op, Op::Halt))
368        .map(|i| i + 1)
369        .unwrap_or(first_fn);
370    0..halt
371}
372
373fn callees(program: &CompiledProgram, range: std::ops::Range<usize>) -> Vec<usize> {
374    program.code[range]
375        .iter()
376        .filter_map(|op| match op {
377            Op::Call { func, .. }
378            | Op::Spawn { func, .. }
379            | Op::SpawnHandle { func, .. }
380            | Op::MakeClosure { func, .. } => Some(*func as usize),
381            _ => None,
382        })
383        .collect()
384}
385
386fn has_back_edge(program: &CompiledProgram, range: std::ops::Range<usize>) -> bool {
387    let start = range.start;
388    program.code[range]
389        .iter()
390        .enumerate()
391        .any(|(i, op)| match op {
392            Op::Jump { target }
393            | Op::JumpIfFalse { target, .. }
394            | Op::JumpIfTrue { target, .. } => *target <= start + i,
395            _ => false,
396        })
397}
398
399/// Compile `source` and read the facts the lane's gates decide on.
400fn scan(source: &str) -> Result<Scan, SpatialReject> {
401    let own = own_function_names(source);
402    crate::ui_bridge::with_parsed_program(source, |parsed, interner| {
403        let (stmts, types, _policies) = parsed.map_err(SpatialReject::Parse)?;
404        let program = Compiler::compile_with_types(stmts, interner, Some(types))
405            .map_err(SpatialReject::Parse)?;
406        let first = stmts.iter().find_map(|s| match s {
407            Stmt::FunctionDef { name, params, .. } => {
408                let name = interner.resolve(*name).to_string();
409                own.contains(&name).then_some((name, params.len()))
410            }
411            _ => None,
412        });
413        let name_of = |idx: usize| {
414            program
415                .functions
416                .get(idx)
417                .map(|f| interner.resolve(f.name).to_string())
418                .unwrap_or_else(|| format!("#{idx}"))
419        };
420        // The unit's own range, and the roots of its call graph.
421        let (unit, own, roots): (Unit, std::ops::Range<usize>, Vec<usize>) = match &first {
422            Some((name, arity)) => {
423                let idx = program
424                    .functions
425                    .iter()
426                    .position(|f| interner.resolve(f.name) == name)
427                    .expect("the first function compiles");
428                let range = function_range(&program, program.functions[idx].entry_pc);
429                (
430                    Unit::Function {
431                        name: name.clone(),
432                        arity: *arity,
433                    },
434                    range,
435                    vec![idx],
436                )
437            }
438            None => {
439                let range = main_range(&program);
440                let roots = callees(&program, range.clone());
441                (Unit::Main, range, roots)
442            }
443        };
444        let branch_in_main = matches!(unit, Unit::Main)
445            && program.code[own.clone()].iter().any(|op| {
446                matches!(
447                    op,
448                    Op::Jump { .. } | Op::JumpIfFalse { .. } | Op::JumpIfTrue { .. }
449                )
450            });
451        // Loops: in the unit itself, then in every function it reaches.
452        let mut loop_in =
453            if matches!(unit, Unit::Function { .. }) && has_back_edge(&program, own.clone()) {
454                match &unit {
455                    Unit::Function { name, .. } => Some(name.clone()),
456                    Unit::Main => None,
457                }
458            } else {
459                None
460            };
461        // Recursion: a cycle in the call graph reachable from the roots (DFS with the active path).
462        let mut recursion = None;
463        let mut visited: BTreeSet<usize> = BTreeSet::new();
464        let mut on_path: Vec<usize> = Vec::new();
465        fn dfs(
466            program: &CompiledProgram,
467            f: usize,
468            visited: &mut BTreeSet<usize>,
469            on_path: &mut Vec<usize>,
470            recursion: &mut Option<String>,
471            loop_in: &mut Option<String>,
472            name_of: &dyn Fn(usize) -> String,
473        ) {
474            if on_path.contains(&f) {
475                if recursion.is_none() {
476                    *recursion = Some(name_of(f));
477                }
478                return;
479            }
480            if !visited.insert(f) {
481                return;
482            }
483            let Some(func) = program.functions.get(f) else {
484                return;
485            };
486            let range = function_range(program, func.entry_pc);
487            if loop_in.is_none() && has_back_edge(program, range.clone()) {
488                *loop_in = Some(name_of(f));
489            }
490            on_path.push(f);
491            for c in callees(program, range) {
492                dfs(program, c, visited, on_path, recursion, loop_in, name_of);
493            }
494            on_path.pop();
495        }
496        for r in roots {
497            dfs(
498                &program,
499                r,
500                &mut visited,
501                &mut on_path,
502                &mut recursion,
503                &mut loop_in,
504                &name_of,
505            );
506        }
507        // The frontier: any op of the unit or a reached function with no fabric tile.
508        let mut ranges = vec![own];
509        for f in &visited {
510            ranges.push(function_range(&program, program.functions[*f].entry_pc));
511        }
512        let frontier = ranges
513            .into_iter()
514            .flat_map(|r| program.code[r].iter())
515            .find_map(|op| {
516                if fabric_tile(op).is_none() {
517                    Some(
518                        frontier_reason(op)
519                            .unwrap_or("an op with no fabric tile and no stated frontier reason"),
520                    )
521                } else {
522                    None
523                }
524            });
525        Ok(Scan {
526            unit,
527            loop_in,
528            recursion,
529            branch_in_main,
530            frontier,
531        })
532    })
533}
534
535/// A decision fabric as one `Scalar` stage over `n`-lane input vectors — one Input tensor, load and FIFO
536/// per parameter, the body, one Output tensor (the general form of `from_scalar_body`'s single input).
537fn scalar_program(
538    df: &DecisionFabric,
539    n: usize,
540    target: &SpatialTarget,
541    name: String,
542) -> Result<SpatialProgram, SpatialReject> {
543    if target.scalar_target().is_none() {
544        return Err(SpatialReject::MissingCompute {
545            needed: "scalar datapath",
546            target: target.name,
547        });
548    }
549    let k = df.n_in;
550    // The stage's result is a cell value: an identity function or a constant tape lane drives its result
551    // straight from an input or a constant, which the per-PE certificate has no output cell to bind to.
552    let mut cells = df.cells.clone();
553    let result = match df.result {
554        Src::Val(_) => df.result,
555        other => {
556            cells.push(LoopCell::Arith {
557                op: crate::codegen_sva::fabric_rtl::AluOp::Add,
558                a: other,
559                b: Src::Const(0),
560            });
561            Src::Val(cells.len() - 1)
562        }
563    };
564    let local = Handshake::Fifo;
565    let tile = target.tile;
566    let grid = |rows: usize, cols: usize| TileGrid {
567        rows_t: rows.div_ceil(tile.rows),
568        cols_t: cols.div_ceil(tile.cols),
569    };
570    let decl = |id: usize, name: String, role: TensorRole, init: TensorInit| TensorDecl {
571        id: TensorId::new(id),
572        name,
573        role,
574        elems: ElemShape { rows: 1, cols: n },
575        tiles: grid(1, n),
576        dtype: Precision::Int64,
577        layout: Layout::RowMajorTiles,
578        space: TensorSpace::Dram,
579        init,
580        range: None,
581        chip: None,
582    };
583    let mut tensors: Vec<TensorDecl> = (0..k)
584        .map(|i| decl(i, format!("x{i}"), TensorRole::Input, TensorInit::External))
585        .collect();
586    tensors.push(decl(k, "y".into(), TensorRole::Output, TensorInit::Zero));
587    let nt = tensors[k].tiles.cols_t;
588    // ★ ONE PAGE CARRIES THE WHOLE VECTOR. The decision body is ONE dataflow program over a 1xN lane
589    // vector; splitting it per tile made `place` clone the entire Load/Scalar/Store triple per tile, so a
590    // 64-lane vector over 32-wide tiles reported TWO identical 75-cell Scalar stages for one program. The
591    // page spans the tensor's tiles, the block spans them, and there is one work item — so the program
592    // says what it is: one body, applied across the lanes.
593    let fifo = |id: usize, name: String| Buffer {
594        id: BufferId::new(id),
595        name,
596        kind: BufferKind::Fifo {
597            depth_pages: 2,
598            page_tiles: nt,
599        },
600        dtype: Precision::Int64,
601        space: MemSpace::Local,
602        site: None,
603        origin: None,
604        alias_of: None,
605    };
606    let mut buffers: Vec<Buffer> = (0..k).map(|i| fifo(i, format!("in{i}"))).collect();
607    buffers.push(fifo(k, "out".into()));
608    let pattern = || AccessPattern {
609        origin: TileAddr {
610            base: 0,
611            steps: vec![(LV_ITEM_Y, nt), (LV_ITEM_X, nt)],
612            indirect: None,
613        },
614        block_h: 1,
615        block_w: nt,
616        stride_w: 1,
617        stride_h: nt,
618    };
619    let mut stages: Vec<Stage> = (0..k)
620        .map(|i| Stage {
621            id: StageId::new(i),
622            name: format!("load_x{i}"),
623            kind: StageKind::Load {
624                tensor: TensorId::new(i),
625                pattern: pattern(),
626                num_blocks: 1,
627                multicast: None,
628            },
629            role: SiteRole::Movement,
630            ins: vec![],
631            outs: vec![BufferId::new(i)],
632            site: None,
633            item: None,
634            origin: None,
635        })
636        .collect();
637    let body = StageId::new(k);
638    stages.push(Stage {
639        id: body,
640        name: "body".into(),
641        kind: StageKind::Scalar(ScalarBody {
642            n_in: df.n_in,
643            cells,
644            result,
645        }),
646        role: SiteRole::Compute,
647        ins: (0..k).map(BufferId::new).collect(),
648        outs: vec![BufferId::new(k)],
649        site: None,
650        item: None,
651        origin: None,
652    });
653    let store = StageId::new(k + 1);
654    stages.push(Stage {
655        id: store,
656        name: "store_y".into(),
657        kind: StageKind::Store {
658            tensor: TensorId::new(k),
659            pattern: pattern(),
660            sub_blocks: (1, 1),
661        },
662        role: SiteRole::Movement,
663        ins: vec![BufferId::new(k)],
664        outs: vec![],
665        site: None,
666        item: None,
667        origin: None,
668    });
669    let mut edges: Vec<Edge> = (0..k)
670        .map(|i| Edge {
671            id: EdgeId::new(i),
672            from: StageId::new(i),
673            buffer: BufferId::new(i),
674            to: vec![body],
675            handshake: local.clone(),
676        })
677        .collect();
678    edges.push(Edge {
679        id: EdgeId::new(k),
680        from: body,
681        buffer: BufferId::new(k),
682        to: vec![store],
683        handshake: local.clone(),
684    });
685    let items = vec![WorkItem {
686        id: WorkItemId::new(0),
687        coord: (0, 0),
688        out: TileRect {
689            row0_t: 0,
690            col0_t: 0,
691            h_t: 1,
692            w_t: nt,
693        },
694    }];
695    let mut requires = Requirements::default();
696    requires.handshakes.insert(local.kind());
697    let p = SpatialProgram {
698        name,
699        tile,
700        tensors,
701        stages,
702        buffers,
703        edges,
704        semaphores: Vec::new(),
705        sections: Vec::new(),
706        trips: Vec::new(),
707        links: Vec::new(),
708        work: WorkList {
709            items,
710            k_tiles: 1,
711            plan: None,
712            k_trip: None,
713        },
714        numerics: NumericsPolicy::Exact,
715        claim: NumericsClaim::Exact,
716        acc_tiles_used: target.site.acc_tiles.max(1),
717        mem_banks: target.site.mem_banks,
718        requires,
719        placement: None,
720        memory: None,
721        schedule: None,
722        certificate: None,
723    };
724    p.validate()?;
725    Ok(p)
726}
727
728/// Lower `source` onto `chip` through the whole-program lane: the first function as a `Scalar` stage over
729/// [`WHOLE_PROGRAM_LANES`]-lane input vectors, or a `## Main` of `Show`s as the tape-select stage; the
730/// program is run through the pinned passes to `Verified` (with its choreography certificate and the
731/// per-PE certificate where the body is ALU + select). Every shape the dataflow lane cannot carry is
732/// refused by name.
733pub fn lower_whole_program_lane(
734    source: &str,
735    chip: RduChip,
736) -> Result<SpatialProgram, SpatialReject> {
737    let target = chip.facts().spatial_target();
738    let scan = scan(source)?;
739    if let Some(reason) = scan.frontier {
740        return Err(SpatialReject::MissingCompute {
741            needed: reason,
742            target: target.name,
743        });
744    }
745    if scan.recursion.is_some() {
746        return Err(SpatialReject::NotLowered(
747            "recursion: the first function reaches itself through the call graph — the decision front end inlines calls and would take the first-return path only; a spatial call stack is what a data-dependent recursion needs (the whole-program call-stack engine, `program_calls_fabric`, runs it ≡VM; a sequenced-program stage in the spatial IR is the expansion that lands it on the mesh)",
748        ));
749    }
750    if scan.loop_in.is_some() {
751        return Err(match &scan.unit {
752            Unit::Function { name, .. } if counted_loop_fabric(source, name).is_ok() => SpatialReject::NotLowered(
753                "a counted range loop is the loop lane's unit (`lower_loop_lane`: the Plasticine metapipeline on the RDU mesh descriptor), not a Scalar stage",
754            ),
755            _ => SpatialReject::NotLowered(
756                "a loop outside the loop lane's single-stage counted shape (a `While`, a nested loop, a loop over a sequence, a loop in a called function): the loop fabrics model it and have no spatial-IR stage form yet",
757            ),
758        });
759    }
760    if scan.branch_in_main {
761        return Err(SpatialReject::NotLowered("a branch at the top level of `Main` would make a `Show` conditional — the tape has no conditional lane; move the decision into a function"));
762    }
763    match scan.unit {
764        Unit::Function { name, arity } => {
765            if arity == 0 {
766                return Err(SpatialReject::NotLowered("a parameterless first function has no input vector to stream lane-wise — write its value as a `Show` in `Main` (the tape form) or give it a parameter"));
767            }
768            let df = quietly(|| decision_fabric_from_source(source, &name))
769                .map_err(SpatialReject::ScalarFrontEnd)?;
770            if df.escaped_first_return {
771                return Err(SpatialReject::NotLowered(
772                    "an early return: a branch jumps past the first `Return`, and the decision front end would keep the first-return path only — write the function with one final `Return` and conditional `Set`s",
773                ));
774            }
775            let p = scalar_program(
776                &df,
777                WHOLE_PROGRAM_LANES,
778                &target,
779                format!("rdu_whole_program:{name}"),
780            )?;
781            run_passes(p, &target)
782        }
783        Unit::Main => {
784            let wpf = quietly(|| whole_program_fabric_from_source(source))
785                .map_err(SpatialReject::ScalarFrontEnd)?;
786            if wpf.outputs.is_empty() {
787                return Err(SpatialReject::NotLowered(
788                    "a `Main` without a `Show` has no tape to lower",
789                ));
790            }
791            if wpf.outputs.iter().any(|(_, is_text, _)| *is_text) {
792                return Err(SpatialReject::NotLowered("a `Show` of Text has no integer lane on the tape — the tape-select stage carries Int values"));
793            }
794            if wpf.outputs.iter().any(|(_, _, guard)| guard.is_some()) {
795                return Err(SpatialReject::NotLowered("a policy `Check` guards the tape (a denied check halts it) — the tape-select stage has no conditional lane"));
796            }
797            // The tape-select body: lane i reads `In(0) == i ? show_i : …`, the program's cells shared.
798            let mut cells = wpf.cells.clone();
799            let k = wpf.outputs.len();
800            let mut acc = wpf.outputs[k - 1].0;
801            for i in (0..k - 1).rev() {
802                let idx = cells.len();
803                cells.push(LoopCell::Select {
804                    cmp: CmpKind::Eq,
805                    ca: Src::In(0),
806                    cb: Src::Const(i as i64),
807                    t: wpf.outputs[i].0,
808                    e: acc,
809                });
810                acc = Src::Val(idx);
811            }
812            let df = DecisionFabric {
813                n_in: 1,
814                cells,
815                result: acc,
816                escaped_first_return: false,
817            };
818            let p = scalar_program(&df, k, &target, "rdu_whole_program:main".into())?;
819            run_passes(p, &target)
820        }
821    }
822}
823
824/// The lane's input vectors for a program the whole-program lane produced, so a caller can run it: for a
825/// function unit, [`anchor_params`]-seeded samples per parameter; for the tape, the Show indices.
826pub fn whole_program_inputs(p: &SpatialProgram) -> super::super::eval::Inputs {
827    use super::super::eval::{Inputs, Value};
828    let mut inputs = Inputs::default();
829    let n = p
830        .tensors
831        .iter()
832        .find(|t| t.role == TensorRole::Output)
833        .map(|t| t.elems.cols)
834        .unwrap_or(0);
835    let is_tape = p.name == "rdu_whole_program:main";
836    for (k, t) in p
837        .tensors
838        .iter()
839        .filter(|t| t.role == TensorRole::Input)
840        .enumerate()
841    {
842        let xs: Vec<i64> = if is_tape {
843            (0..n as i64).collect()
844        } else {
845            (0..n).map(|i| sample(k, i)).collect()
846        };
847        inputs.tensors.insert(t.id, Value::Int(xs));
848    }
849    inputs
850}
851
852/// Lane `i` of parameter `k`'s sample vector: the anchor value on lane 0, then a deterministic spread of
853/// small signed integers around it (every path of a decision body sees both signs and a run of magnitudes).
854pub fn sample(k: usize, i: usize) -> i64 {
855    let base = anchor_params(k + 1)[k];
856    if i == 0 {
857        return base;
858    }
859    let mut x = (i as u64)
860        .wrapping_mul(0x9E37_79B9_7F4A_7C15)
861        .wrapping_add(k as u64 + 1);
862    x ^= x >> 29;
863    x = x.wrapping_mul(0xBF58_476D_1CE4_E5B9);
864    x ^= x >> 32;
865    (x % 401) as i64 - 200
866}
867
868/// The lane's VM anchor for a function unit: `fn_name(args)` shown by the VM, `Main` replaced.
869pub fn vm_anchor(source: &str, fn_name: &str, args: &[i64]) -> Result<i64, String> {
870    let call = args
871        .iter()
872        .map(|p| {
873            if *p < 0 {
874                format!("(0 - {})", -p)
875            } else {
876                p.to_string()
877            }
878        })
879        .collect::<Vec<_>>()
880        .join(", ");
881    let funcs = match source.find("## Main") {
882        Some(idx) => &source[..idx],
883        None => source,
884    };
885    let prog = format!("{funcs}## Main\n    Show {fn_name}({call}).\n");
886    let out = crate::compile::vm_outcome(&prog);
887    if let Some(e) = out.error {
888        return Err(format!("the VM errored running `{fn_name}({call})`: {e}"));
889    }
890    out.output
891        .split_whitespace()
892        .next()
893        .and_then(|s| s.parse::<i64>().ok())
894        .ok_or_else(|| {
895            format!(
896                "the VM printed no integer for `{fn_name}({call})`: {:?}",
897                out.output
898            )
899        })
900}