Skip to main content

earthsci_ast/simulate_array/range/
lanes.rs

1//! `lanes` (API_SPEC §5.8, RFC U3 §6): a pointwise or column operator over a
2//! run-time list of cells or columns.
3//!
4//! A lane program is the operator's rules rewritten over one lane axis of
5//! `capacity` lanes (and, for a column operator, the whole of every other
6//! axis), and re-lowered: a state read at the lane's own cell becomes a read
7//! of the lane block, and every value that depends on the cell's index — a
8//! coordinate, a level gate, an affine read of an array parameter, a forcing
9//! buffer or a state-free observed — is computed from the lane's entry of
10//! `cells`, which the call copies into the program's state each time. One
11//! build serves every batch of up to `capacity` lanes; a lane's result
12//! depends only on its own inputs, so lane order, duplicates and padding
13//! cannot change a value.
14//!
15//! The pre-gather plan ([`LaneProgram::lane_inputs`]) hoists every read of a
16//! non-state array whose subscripts depend on the lane's cell alone into a
17//! per-lane buffer: [`LaneProgram::gather_lane_inputs`] fills the buffers once
18//! per batch, and [`LaneProgram::eval_gathered_into`] runs the program with
19//! those reads replaced by reads of the buffers.
20
21use super::forcing::{ForcingSet, ForcingSlot, RhsError, shape};
22use super::tile::{declined_error, plain_precision, refused, shell};
23use super::walk::AxisSpec;
24use super::{
25    Blocks, Decomposition, LaneKind, LaneLayout, ProgramModel, RhsProgram, RhsWorkspace, TileInput,
26    WsExec, build_error, check_forcing, check_lengths, check_owner, index_set_sizes,
27    next_program_id, slot_map, tape_fault, var_infos,
28};
29use crate::simulate::SimulateError;
30use crate::simulate_array::tape::{
31    CallInputs, ForcingView, TapeCtx, TapeProgram, TileLowering, run_tape_inputs,
32};
33use crate::simulate_array::{AlgebraicRule, ArrayCompiled, RhsRule, VarShape};
34use crate::types::{Expr, ExpressionNode, RegionBound};
35use indexmap::IndexMap;
36use rustc_hash::FxHashMap;
37use std::collections::{HashMap, HashSet};
38use std::sync::Arc;
39
40/// The lane symbol every rewritten rule runs over.
41const LANE: &str = "__lane";
42
43fn cell_var(set: &str) -> String {
44    format!("__cell_{set}")
45}
46
47fn input_var(k: usize) -> String {
48    format!("__lane_input_{k}")
49}
50
51/// A lane program (RFC U3 §6). `Send + Sync + Clone`; it reads the forcing
52/// sets of the program it was built from.
53#[derive(Clone)]
54pub struct LaneProgram {
55    inner: Arc<LaneInner>,
56}
57
58struct LaneInner {
59    id: u64,
60    main: Arc<TapeProgram>,
61    pre: Arc<TapeProgram>,
62    gather: Arc<TapeProgram>,
63    /// The forcing slot each forcing load of `main`, `pre` and `gather`
64    /// reads.
65    slot_of: [Vec<u32>; 3],
66    blocks: Blocks,
67    layout: LaneLayout,
68    slots: Arc<[ForcingSlot]>,
69    capacity: usize,
70    sets: Vec<String>,
71    /// The global extent of each lane set, for validating `cells`.
72    extents: Vec<i64>,
73    n_inputs: usize,
74    n_params: usize,
75    declared: Arc<HashSet<String>>,
76    precision: crate::precision::Precision,
77}
78
79impl std::fmt::Debug for LaneProgram {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        f.debug_struct("LaneProgram")
82            .field("capacity", &self.inner.capacity)
83            .field("sets", &self.inner.sets)
84            .field("layout", &self.inner.layout)
85            .finish()
86    }
87}
88
89/// The per-lane buffers of a lane program's pre-gather plan: one run of
90/// `capacity` values per hoisted read. Filled by
91/// [`LaneProgram::gather_lane_inputs`], read by
92/// [`LaneProgram::eval_gathered_into`], which reads the forcing set they
93/// were gathered from. `Send`, like a workspace.
94pub struct LaneInputs {
95    owner: u64,
96    forcing: Option<ForcingSet>,
97    values: Vec<f64>,
98    state: Vec<f64>,
99    ctx: Box<TapeCtx>,
100}
101
102impl std::fmt::Debug for LaneInputs {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        f.debug_struct("LaneInputs")
105            .field("len", &self.values.len())
106            .finish()
107    }
108}
109
110impl LaneInputs {
111    /// The gathered values: hoisted read `k`'s lanes at `k * capacity ..`.
112    pub fn values(&self) -> &[f64] {
113        &self.values
114    }
115}
116
117const MAIN: usize = 0;
118const PRE: usize = 1;
119const GATHER: usize = 2;
120
121impl LaneProgram {
122    /// The lane layout (RFC U3 §6): each variable a column-major block whose
123    /// first axis is the lane (`1..=capacity`) and whose other axes are the
124    /// variable's axes outside the lane's sets, in order.
125    pub fn layout(&self) -> &LaneLayout {
126        &self.inner.layout
127    }
128
129    /// The number of lanes the program was built for.
130    pub fn capacity(&self) -> usize {
131        self.inner.capacity
132    }
133
134    /// The number of hoisted reads the pre-gather plan fills.
135    pub fn n_lane_inputs(&self) -> usize {
136        self.inner.n_inputs
137    }
138
139    /// The forcing slots a set for this program holds: the whole program's.
140    pub fn forcing_slots(&self) -> &[ForcingSlot] {
141        &self.inner.slots
142    }
143
144    /// Buffers for [`Self::eval_into`] and [`Self::eval_gathered_into`].
145    pub fn new_workspace(&self) -> RhsWorkspace {
146        let inner = &*self.inner;
147        let n = inner.blocks.u_len
148            + inner.blocks.aux_len
149            + inner.capacity * (inner.sets.len() + inner.n_inputs);
150        let serial = |t: &Arc<TapeProgram>| {
151            let mut ctx = TapeCtx::new(Arc::clone(t));
152            ctx.set_serial();
153            Box::new(ctx)
154        };
155        RhsWorkspace {
156            owner: inner.id,
157            state: vec![0.0; n],
158            dy: Vec::new(),
159            exec: WsExec::Lanes {
160                main: serial(&inner.main),
161                pre: serial(&inner.pre),
162            },
163            _not_sync: std::marker::PhantomData,
164        }
165    }
166
167    /// Copy `cells` (`[D, L]`, column-major, 1-based global indices) into the
168    /// lane rows of `state` starting at `at`, as `f64`. Only lanes `1..=n` are
169    /// read from `cells` (and range-checked); every lane past `n` repeats lane
170    /// 1 (or cell 1 when `n = 0`), so the padding lanes the tape still
171    /// computes read in-range indices whatever the tail of `cells` holds.
172    fn write_cells(
173        &self,
174        state: &mut [f64],
175        at: usize,
176        cells: &[i32],
177        n: usize,
178    ) -> Result<(), RhsError> {
179        let inner = &*self.inner;
180        let d = inner.sets.len();
181        if n > inner.capacity {
182            return Err(shape("n (lanes past the capacity)", inner.capacity, n));
183        }
184        if !cells.len().is_multiple_of(d.max(1))
185            || cells.len() / d.max(1) < n
186            || cells.len() / d.max(1) > inner.capacity
187        {
188            return Err(shape(
189                "cells (D times a lane count between n and the capacity)",
190                d * n.max(1),
191                cells.len(),
192            ));
193        }
194        for (a, &ext) in inner.extents.iter().enumerate() {
195            let rows = &mut state[at + a * inner.capacity..at + (a + 1) * inner.capacity];
196            for (l, row) in rows.iter_mut().enumerate() {
197                let c = match (l < n, n > 0) {
198                    (true, _) => cells[l * d + a],
199                    (false, true) => cells[a],
200                    (false, false) => 1,
201                };
202                if l < n && (i64::from(c) < 1 || i64::from(c) > ext) {
203                    return Err(shape(
204                        "cells (a lane's cell outside its index set)",
205                        ext as usize,
206                        c.max(0) as usize,
207                    ));
208                }
209                *row = f64::from(c);
210            }
211        }
212        Ok(())
213    }
214
215    /// Evaluate lanes `1..=n` (RFC U3 §6): for each lane `l` and each written
216    /// variable, `du` holds the whole-domain program's value at `cells[:, l]`.
217    #[allow(clippy::too_many_arguments)]
218    pub fn eval_into(
219        &self,
220        u: &[f64],
221        aux: &[f64],
222        p: &[f64],
223        t: f64,
224        forcing: &ForcingSet,
225        cells: &[i32],
226        n: usize,
227        du: &mut [f64],
228        ws: &mut RhsWorkspace,
229    ) -> Result<(), RhsError> {
230        self.run(u, aux, None, p, t, forcing, cells, n, du, ws)
231    }
232
233    /// The pre-gather plan's buffers, unfilled.
234    pub fn lane_inputs(&self) -> LaneInputs {
235        let inner = &*self.inner;
236        let mut ctx = TapeCtx::new(Arc::clone(&inner.gather));
237        ctx.set_serial();
238        LaneInputs {
239            owner: inner.id,
240            forcing: None,
241            values: vec![0.0; inner.capacity * inner.n_inputs],
242            state: vec![0.0; inner.capacity * (inner.n_inputs + inner.sets.len())],
243            ctx: Box::new(ctx),
244        }
245    }
246
247    /// Fill `inputs` for lanes `1..=n` of `cells`: every hoisted read of a
248    /// non-state input, at each lane's cell.
249    pub fn gather_lane_inputs(
250        &self,
251        inputs: &mut LaneInputs,
252        p: &[f64],
253        t: f64,
254        forcing: &ForcingSet,
255        cells: &[i32],
256        n: usize,
257    ) -> Result<(), RhsError> {
258        let inner = &*self.inner;
259        if inputs.owner != inner.id {
260            return Err(RhsError::ForeignProgram {
261                what: "lane inputs",
262            });
263        }
264        // Cleared until this gather succeeds: a failed gather leaves the
265        // buffers half-written, and they must not evaluate.
266        inputs.forcing = None;
267        if p.len() != inner.n_params {
268            return Err(shape("p", inner.n_params, p.len()));
269        }
270        check_forcing(forcing, &inner.slots)?;
271        self.write_cells(&mut inputs.state, inner.capacity * inner.n_inputs, cells, n)?;
272        let _precision = crate::precision::enter(inner.precision);
273        let scope = crate::simulate_array::FaultScope::enter();
274        let call = CallInputs {
275            state: &inputs.state,
276            params: p,
277            forcing: ForcingView::Slots {
278                bufs: forcing.bufs(),
279                slot_of: &inner.slot_of[GATHER],
280            },
281            forcing_epoch: Some(forcing.epoch()),
282            t,
283            declared: &inner.declared,
284        };
285        run_tape_inputs(&mut inputs.ctx, &call, &mut inputs.values, false);
286        tape_fault(scope, &mut inputs.ctx)?;
287        // Kept so the evaluation reads the set these inputs came from (a
288        // clone is reference counts, not an allocation).
289        inputs.forcing = Some(forcing.clone());
290        Ok(())
291    }
292
293    /// [`Self::eval_into`] with every hoisted read taken from `inputs`, and
294    /// the forcing set they were gathered from.
295    #[allow(clippy::too_many_arguments)]
296    pub fn eval_gathered_into(
297        &self,
298        u: &[f64],
299        aux: &[f64],
300        inputs: &LaneInputs,
301        p: &[f64],
302        t: f64,
303        cells: &[i32],
304        n: usize,
305        du: &mut [f64],
306        ws: &mut RhsWorkspace,
307    ) -> Result<(), RhsError> {
308        if inputs.owner != self.inner.id {
309            return Err(RhsError::ForeignProgram {
310                what: "lane inputs",
311            });
312        }
313        let Some(forcing) = &inputs.forcing else {
314            return Err(RhsError::NotGathered);
315        };
316        self.run(
317            u,
318            aux,
319            Some(&inputs.values),
320            p,
321            t,
322            forcing,
323            cells,
324            n,
325            du,
326            ws,
327        )
328    }
329
330    #[allow(clippy::too_many_arguments)]
331    fn run(
332        &self,
333        u: &[f64],
334        aux: &[f64],
335        gathered: Option<&[f64]>,
336        p: &[f64],
337        t: f64,
338        forcing: &ForcingSet,
339        cells: &[i32],
340        n: usize,
341        du: &mut [f64],
342        ws: &mut RhsWorkspace,
343    ) -> Result<(), RhsError> {
344        let inner = &*self.inner;
345        check_lengths(&inner.blocks, u, aux, du, p, inner.n_params)?;
346        check_owner(ws, inner.id)?;
347        check_forcing(forcing, &inner.slots)?;
348        let (nu, na) = (inner.blocks.u_len, inner.blocks.aux_len);
349        ws.state[..nu].copy_from_slice(u);
350        ws.state[nu..nu + na].copy_from_slice(aux);
351        let mut state = std::mem::take(&mut ws.state);
352        let res = self.write_cells(&mut state, nu + na, cells, n);
353        let at = nu + na + inner.capacity * inner.sets.len();
354        if let Some(g) = gathered {
355            state[at..at + g.len()].copy_from_slice(g);
356        }
357        ws.state = state;
358        res?;
359        let WsExec::Lanes { main, pre } = &mut ws.exec else {
360            unreachable!("a lane workspace runs lane tapes");
361        };
362        let (ctx, which) = if gathered.is_some() {
363            (pre, PRE)
364        } else {
365            (main, MAIN)
366        };
367        let _precision = crate::precision::enter(inner.precision);
368        let scope = crate::simulate_array::FaultScope::enter();
369        let call = CallInputs {
370            state: &ws.state,
371            params: p,
372            forcing: ForcingView::Slots {
373                bufs: forcing.bufs(),
374                slot_of: &inner.slot_of[which],
375            },
376            forcing_epoch: Some(forcing.epoch()),
377            t,
378            declared: &inner.declared,
379        };
380        run_tape_inputs(ctx, &call, du, false);
381        tape_fault(scope, ctx)
382    }
383}
384
385/// The rewrite of one program's rules over the lane axis.
386struct Rewriter<'a> {
387    /// The index-set name of each axis of each state and array observed.
388    axes: &'a HashMap<String, Vec<Option<String>>>,
389    /// The states and state-dependent observeds that become lane blocks.
390    lane_vars: &'a HashSet<String>,
391    sets: &'a [String],
392    sizes: &'a HashMap<String, i64>,
393    cells: bool,
394    capacity: i64,
395    rule: String,
396    /// The hoisted non-state reads of the pre-gather plan.
397    gathers: Vec<Expr>,
398    /// Replace each hoisted read by its lane buffer.
399    pre: bool,
400}
401
402/// The symbols in force while rewriting: the original output symbols that
403/// run along a lane set (each mapped to that set), and every other symbol.
404#[derive(Clone, Default)]
405struct LScope {
406    lane_syms: Vec<(String, String)>,
407    inner: Vec<String>,
408}
409
410impl LScope {
411    fn lane_set(&self, sym: &str) -> Option<&str> {
412        self.lane_syms
413            .iter()
414            .find(|(s, _)| s == sym)
415            .map(|(_, set)| set.as_str())
416    }
417}
418
419type RResult<T> = Result<T, SimulateError>;
420
421fn lane_read(set: &str) -> Expr {
422    Expr::operator(ExpressionNode {
423        op: "index".to_string(),
424        args: vec![
425            Expr::Variable(cell_var(set)),
426            Expr::Variable(LANE.to_string()),
427        ],
428        ..Default::default()
429    })
430}
431
432fn mentions(e: &Expr, pred: &mut dyn FnMut(&str) -> bool) -> bool {
433    match e {
434        Expr::Variable(v) => pred(v),
435        Expr::Operator(n) => n.any_child(&mut |c| mentions(c, pred)),
436        _ => false,
437    }
438}
439
440impl Rewriter<'_> {
441    fn refuse<T>(&self, what: impl std::fmt::Display) -> RResult<T> {
442        Err(refused(format!("lane: rule `{}` {what}", self.rule)))
443    }
444
445    fn expr(&mut self, e: &Expr, sc: &LScope) -> RResult<Expr> {
446        match e {
447            Expr::Number(_) | Expr::Integer(_) => Ok(e.clone()),
448            Expr::Variable(v) => {
449                if let Some(set) = sc.lane_set(v) {
450                    return Ok(lane_read(set));
451                }
452                if sc.inner.contains(v) {
453                    return Ok(e.clone());
454                }
455                if self.lane_vars.contains(v) && self.axes.get(v).is_some_and(|a| !a.is_empty()) {
456                    return self.refuse(format_args!("reads the array `{v}` whole"));
457                }
458                Ok(e.clone())
459            }
460            Expr::Operator(node) => match node.op.as_str() {
461                "index" if !node.args.is_empty() => self.index(node, sc),
462                "makearray" => self.refuse("holds a makearray outside a subscripted read"),
463                "faq" | "aggregate" => {
464                    if node.output_idx.as_ref().is_some_and(|o| !o.is_empty()) {
465                        return self.refuse("holds an array-valued faq in an elementwise position");
466                    }
467                    let mut inner = sc.clone();
468                    if let Some(r) = &node.ranges {
469                        for k in r.keys() {
470                            inner.lane_syms.retain(|(s, _)| s != k);
471                            if !inner.inner.contains(k) {
472                                inner.inner.push(k.clone());
473                            }
474                        }
475                    }
476                    let mut n = ExpressionNode::clone(node);
477                    if let Some(b) = node.expr.as_deref() {
478                        n.expr = Some(Box::new(self.expr(b, &inner)?));
479                    }
480                    if let Some(f) = node.filter.as_deref() {
481                        n.filter = Some(Box::new(self.expr(f, &inner)?));
482                    }
483                    Ok(Expr::operator(n))
484                }
485                _ => {
486                    let mut err = None;
487                    let out = node.map_children(&mut |c| match self.expr(c, sc) {
488                        Ok(x) => x,
489                        Err(r) => {
490                            err.get_or_insert(r);
491                            c.clone()
492                        }
493                    });
494                    match err {
495                        Some(r) => Err(r),
496                        None => Ok(Expr::operator(out)),
497                    }
498                }
499            },
500        }
501    }
502
503    fn index(&mut self, node: &Arc<ExpressionNode>, sc: &LScope) -> RResult<Expr> {
504        let base = &node.args[0];
505        let subs = &node.args[1..];
506        match base {
507            Expr::Variable(v) if self.lane_vars.contains(v) => {
508                let axes = self.axes.get(v).cloned().unwrap_or_default();
509                if axes.len() != subs.len() {
510                    return self.refuse(format_args!("reads `{v}` at the wrong rank"));
511                }
512                let mut new_args = vec![base.clone(), Expr::Variable(LANE.to_string())];
513                for (d, s) in subs.iter().enumerate() {
514                    match axes[d]
515                        .as_deref()
516                        .filter(|set| self.sets.iter().any(|x| x == set))
517                    {
518                        Some(set) => {
519                            let own = matches!(s, Expr::Variable(x) if sc.lane_set(x) == Some(set));
520                            if !own {
521                                return self.refuse(format_args!(
522                                    "reads `{v}` off its own lane's cell along `{set}`, which is \
523                                     not {} locality",
524                                    if self.cells { "pointwise" } else { "column" }
525                                ));
526                            }
527                        }
528                        None => new_args.push(self.expr(s, sc)?),
529                    }
530                }
531                let mut n = ExpressionNode::clone(node);
532                n.args = new_args;
533                Ok(Expr::operator(n))
534            }
535            Expr::Variable(_) => {
536                let mut new_subs = Vec::with_capacity(subs.len());
537                for s in subs {
538                    new_subs.push(self.expr(s, sc)?);
539                }
540                let mut n = ExpressionNode::clone(node);
541                n.args = std::iter::once(base.clone()).chain(new_subs).collect();
542                let read = Expr::operator(n);
543                // A read whose subscripts depend on the lane's cell alone
544                // (and on no other symbol) is one value per lane: hoisted.
545                let by_cell = read_mentions_lane(&read)
546                    && !mentions(&read, &mut |x| sc.inner.iter().any(|s| s == x));
547                if !by_cell {
548                    return Ok(read);
549                }
550                let k = match self.gathers.iter().position(|g| *g == read) {
551                    Some(k) => k,
552                    None => {
553                        self.gathers.push(read.clone());
554                        self.gathers.len() - 1
555                    }
556                };
557                if self.pre {
558                    return Ok(Expr::operator(ExpressionNode {
559                        op: "index".to_string(),
560                        args: vec![
561                            Expr::Variable(input_var(k)),
562                            Expr::Variable(LANE.to_string()),
563                        ],
564                        ..Default::default()
565                    }));
566                }
567                Ok(read)
568            }
569            Expr::Operator(m) if m.op == "makearray" => self.makearray_read(node, m, sc),
570            _ => self.refuse("subscripts an array expression a lane cannot restrict"),
571        }
572    }
573
574    /// `index(makearray(…), syms…)` with the box's own symbols in order: a
575    /// makearray whose regions span the whole of every lane set, rewritten
576    /// over the lane axis and the other axes.
577    fn makearray_read(
578        &mut self,
579        node: &Arc<ExpressionNode>,
580        m: &Arc<ExpressionNode>,
581        sc: &LScope,
582    ) -> RResult<Expr> {
583        let subs = &node.args[1..];
584        let (Some(regions), Some(values)) = (m.regions.as_ref(), m.values.as_ref()) else {
585            return self.refuse("holds a makearray with no regions");
586        };
587        // Per axis: the lane set it runs along, if any.
588        let mut lane_axes: Vec<Option<String>> = Vec::with_capacity(subs.len());
589        let mut inner_subs = Vec::new();
590        for s in subs {
591            match s {
592                Expr::Variable(x) if sc.lane_set(x).is_some() => {
593                    lane_axes.push(sc.lane_set(x).map(str::to_string));
594                }
595                Expr::Variable(x) if sc.inner.contains(x) => {
596                    lane_axes.push(None);
597                    inner_subs.push(s.clone());
598                }
599                _ => {
600                    return self.refuse("subscripts a makearray at a shifted or computed position");
601                }
602            }
603        }
604        let mut new_regions = Vec::with_capacity(regions.len());
605        let mut new_values = Vec::with_capacity(values.len());
606        for (region, value) in regions.iter().zip(values) {
607            if region.len() != subs.len() {
608                return self.refuse("reads a makearray at the wrong rank");
609            }
610            let mut r = vec![[RegionBound::Int(1), RegionBound::Int(self.capacity)]];
611            let inner_sc = sc.clone();
612            for (d, b) in region.iter().enumerate() {
613                let Some([lo, hi]) = crate::types::region_bounds(b) else {
614                    return self.refuse("holds a makearray with a symbolic region bound");
615                };
616                match &lane_axes[d] {
617                    Some(set) => {
618                        let n = self.sizes.get(set).copied().unwrap_or(-1);
619                        if lo != 1 || hi != n {
620                            return self.refuse(format_args!(
621                                "holds a makearray region {lo}..{hi} along the lane set `{set}`, \
622                                 which depends on the lane's cell"
623                            ));
624                        }
625                    }
626                    None => r.push(b.clone()),
627                }
628            }
629            if let Expr::Operator(v) = value
630                && (v.op == "faq" || v.op == "aggregate")
631                && v.output_idx.as_ref().is_some_and(|o| !o.is_empty())
632            {
633                return self.refuse("holds a faq region value");
634            }
635            new_values.push(self.expr(value, &inner_sc)?);
636            new_regions.push(r);
637        }
638        let mut nm = ExpressionNode::clone(m);
639        nm.regions = Some(new_regions);
640        nm.values = Some(new_values);
641        let mut n = ExpressionNode::clone(node);
642        n.args = std::iter::once(Expr::operator(nm))
643            .chain(std::iter::once(Expr::Variable(LANE.to_string())))
644            .chain(inner_subs)
645            .collect();
646        Ok(Expr::operator(n))
647    }
648}
649
650fn read_mentions_lane(e: &Expr) -> bool {
651    mentions(e, &mut |x| x.starts_with("__cell_"))
652}
653
654struct Rewritten {
655    rhs: Vec<RhsRule>,
656    observed: Vec<AlgebraicRule>,
657    gathers: Vec<Expr>,
658}
659
660#[allow(clippy::too_many_arguments)]
661fn rewrite(
662    compiled: &ProgramModel,
663    axes: &HashMap<String, Vec<Option<String>>>,
664    lane_vars: &HashSet<String>,
665    dependent: &HashSet<String>,
666    sets: &[String],
667    sizes: &HashMap<String, i64>,
668    cells: bool,
669    capacity: i64,
670    pre: bool,
671) -> RResult<Rewritten> {
672    let mut rw = Rewriter {
673        axes,
674        lane_vars,
675        sets,
676        sizes,
677        cells,
678        capacity,
679        rule: String::new(),
680        gathers: Vec::new(),
681        pre,
682    };
683    // The lane box of a rule writing `var` over `syms`/`ranges`.
684    let lane_box = |rw: &Rewriter,
685                    var: &str,
686                    syms: &[String],
687                    ranges: &[(i64, i64)]|
688     -> RResult<(LScope, Vec<String>, Vec<(i64, i64)>)> {
689        let var_axes = axes.get(var).cloned().unwrap_or_default();
690        if var_axes.len() != syms.len() {
691            return rw.refuse(format_args!(
692                "writes `{var}` through a reshaped left-hand side"
693            ));
694        }
695        let mut sc = LScope::default();
696        let mut out_syms = vec![LANE.to_string()];
697        let mut out_ranges = vec![(1, capacity)];
698        for (d, s) in syms.iter().enumerate() {
699            match var_axes[d]
700                .as_deref()
701                .filter(|set| sets.iter().any(|x| x == set))
702            {
703                Some(set) => {
704                    let n = sizes.get(set).copied().unwrap_or(-1);
705                    if ranges[d] != (1, n) {
706                        return rw.refuse(format_args!(
707                            "writes `{var}` over {}..{} of the lane set `{set}`, not the whole of it",
708                            ranges[d].0, ranges[d].1
709                        ));
710                    }
711                    sc.lane_syms.push((s.clone(), set.to_string()));
712                }
713                None => {
714                    if cells {
715                        return rw.refuse(format_args!(
716                            "writes `{var}` along `{}`, which is not one of the lane's sets",
717                            var_axes[d].as_deref().unwrap_or("an unnamed axis")
718                        ));
719                    }
720                    sc.inner.push(s.clone());
721                    out_syms.push(s.clone());
722                    out_ranges.push(ranges[d]);
723                }
724            }
725        }
726        Ok((sc, out_syms, out_ranges))
727    };
728    let mut rhs = Vec::new();
729    for rule in &compiled.rhs_rules {
730        let RhsRule::ArrayLoop {
731            var_name,
732            output_idx_names,
733            output_ranges,
734            lhs_idx_exprs,
735            body,
736            contract_names,
737            contract_dims,
738            reduce,
739            filter,
740        } = rule
741        else {
742            return Err(refused(
743                "lane: a scalar or per-cell rule has no lane form".to_string(),
744            ));
745        };
746        rw.rule = format!("D({var_name})");
747        let identity = lhs_idx_exprs.len() == output_idx_names.len()
748            && lhs_idx_exprs
749                .iter()
750                .zip(output_idx_names)
751                .all(|(e, s)| matches!(e, Expr::Variable(x) if x == s));
752        if !identity {
753            return rw.refuse("writes through a shifted or computed left-hand side");
754        }
755        let (mut sc, syms, ranges) = lane_box(&rw, var_name, output_idx_names, output_ranges)?;
756        sc.inner.extend(contract_names.iter().cloned());
757        let body = rw.expr(body, &sc)?;
758        let filter = match filter {
759            Some(f) => Some(Box::new(rw.expr(f, &sc)?)),
760            None => None,
761        };
762        rhs.push(RhsRule::ArrayLoop {
763            var_name: var_name.clone(),
764            lhs_idx_exprs: syms.iter().map(|s| Expr::Variable(s.clone())).collect(),
765            output_idx_names: syms,
766            output_ranges: ranges,
767            body: Box::new(body),
768            contract_names: contract_names.clone(),
769            contract_dims: contract_dims.clone(),
770            reduce: *reduce,
771            filter,
772        });
773    }
774    let mut observed = Vec::new();
775    for rule in &compiled.observed_rules {
776        let var = match rule {
777            AlgebraicRule::Scalar { var, .. }
778            | AlgebraicRule::ArrayLoop { var, .. }
779            | AlgebraicRule::Recurrence { var, .. } => var,
780        };
781        if !dependent.contains(var) {
782            observed.push(rule.clone());
783            continue;
784        }
785        rw.rule = var.clone();
786        match rule {
787            AlgebraicRule::ArrayLoop {
788                output_idx_names,
789                output_ranges,
790                body,
791                ..
792            } if lane_vars.contains(var) => {
793                let (sc, syms, ranges) = lane_box(&rw, var, output_idx_names, output_ranges)?;
794                let body = rw.expr(body, &sc)?;
795                observed.push(AlgebraicRule::ArrayLoop {
796                    var: var.clone(),
797                    output_idx_names: syms,
798                    output_ranges: ranges,
799                    body: std::sync::Arc::new(body),
800                });
801            }
802            AlgebraicRule::Scalar { body, .. } if axes.get(var).is_none_or(Vec::is_empty) => {
803                let body = rw.expr(body, &LScope::default())?;
804                if mentions(&body, &mut |x| x.starts_with("__cell_") || x == LANE) {
805                    return rw.refuse("is a scalar observed that reads the lane state");
806                }
807                observed.push(rule.clone());
808            }
809            _ => return rw.refuse("is a state-dependent observed with no lane form"),
810        }
811    }
812    Ok(Rewritten {
813        rhs,
814        observed,
815        gathers: rw.gathers,
816    })
817}
818
819pub(super) fn build(
820    prog: &RhsProgram,
821    kind: &LaneKind,
822    capacity: usize,
823) -> Result<LaneProgram, SimulateError> {
824    let compiled = &*prog.inner.model;
825    let precision = plain_precision(compiled, "lane")?;
826    if capacity == 0 {
827        return Err(build_error(
828            "`lanes`: capacity must be at least 1".to_string(),
829        ));
830    }
831    let sets: Vec<String> = kind.sets().to_vec();
832    let sizes = index_set_sizes(prog.inner.flat());
833    let box_invalid =
834        |m: String| super::range_error(crate::diagnostic::codes::RANGE_BOX_INVALID, m);
835    for s in &sets {
836        if !sizes.contains_key(s) {
837            return Err(box_invalid(format!(
838                "`lanes`: `{s}` is not an interval index set of this program"
839            )));
840        }
841    }
842    let cells_mode = matches!(kind, LaneKind::Cells { .. });
843    // Every axis's index-set name (the decomposition is empty: lanes cut
844    // nothing, they only name sets).
845    let infos = var_infos(prog, &Decomposition::default());
846    let axes: HashMap<String, Vec<Option<String>>> = infos
847        .iter()
848        .map(|(k, v)| {
849            (
850                k.clone(),
851                v.axes
852                    .iter()
853                    .map(|a| match a {
854                        AxisSpec::Whole { set } => set.clone(),
855                        AxisSpec::Cut { set, .. } => Some(set.clone()),
856                    })
857                    .collect(),
858            )
859        })
860        .collect();
861    // Every state in `u` and `aux` carries every lane set; for `cells` the
862    // lane sets are exactly the written variables' index sets.
863    for name in prog.inner.writes.iter().chain(&prog.inner.aux) {
864        let var_sets: Vec<String> = axes[name].iter().flatten().cloned().collect();
865        if let Some(s) = sets.iter().find(|s| !var_sets.contains(s)) {
866            return Err(box_invalid(format!(
867                "`lanes`: the state `{name}` does not carry the lane set `{s}`"
868            )));
869        }
870        if cells_mode
871            && prog.inner.writes.contains(name)
872            && let Some(s) = var_sets.iter().find(|s| !sets.contains(s))
873        {
874            return Err(box_invalid(format!(
875                "`lanes`: `cells` lanes name every index set of the written variables, and \
876                 `{name}` also has `{s}`"
877            )));
878        }
879    }
880    let dependent = super::tile::state_dependent(compiled);
881    let mut lane_vars: HashSet<String> = compiled.var_shapes.keys().cloned().collect();
882    for rule in &compiled.observed_rules {
883        if let AlgebraicRule::ArrayLoop { var, .. } = rule
884            && dependent.contains(var)
885        {
886            lane_vars.insert(var.clone());
887        }
888    }
889    let cap = capacity as i64;
890    let main = rewrite(
891        compiled, &axes, &lane_vars, &dependent, &sets, &sizes, cells_mode, cap, false,
892    )?;
893    let pre = rewrite(
894        compiled, &axes, &lane_vars, &dependent, &sets, &sizes, cells_mode, cap, true,
895    )?;
896
897    // The lane layout: u, then aux, each block `[capacity, inner…]`.
898    let mut entries = Vec::new();
899    for (input, names) in [
900        (TileInput::U, &prog.inner.writes),
901        (TileInput::Aux, &prog.inner.aux),
902    ] {
903        for name in names {
904            let vs = &compiled.var_shapes[name];
905            let var_axes = &axes[name];
906            if cells_mode
907                && var_axes
908                    .iter()
909                    .any(|a| a.as_deref().is_none_or(|s| !sets.iter().any(|x| x == s)))
910                && !vs.shape.is_empty()
911            {
912                return Err(refused(format!(
913                    "lane: `{name}` has an axis outside the lane's sets {sets:?}"
914                )));
915            }
916            let mut storage = vec![(1, cap)];
917            let mut index_sets = vec!["lane".to_string()];
918            for (d, a) in var_axes.iter().enumerate() {
919                if a.as_deref().is_some_and(|s| sets.iter().any(|x| x == s)) {
920                    continue;
921                }
922                let o = vs.origin[d];
923                storage.push((o, o + vs.shape[d] as i64 - 1));
924                index_sets.push(a.clone().unwrap_or_default());
925            }
926            if vs.shape.is_empty() {
927                return Err(refused(format!(
928                    "lane: `{name}` is a 0-d state, which no lane holds"
929                )));
930            }
931            entries.push((name.clone(), input, index_sets, storage.clone(), storage));
932        }
933    }
934    let layout = Blocks::build(entries);
935
936    // The internal state: u, aux, the lane cells, then the hoisted inputs.
937    let mut var_shapes: IndexMap<String, VarShape> = IndexMap::new();
938    for v in &layout.vars {
939        let flat_offset = match v.input {
940            TileInput::U => v.base,
941            TileInput::Aux => layout.u_len + v.base,
942        };
943        var_shapes.insert(
944            v.name.clone(),
945            VarShape {
946                shape: v
947                    .storage
948                    .iter()
949                    .map(|(lo, hi)| (hi - lo + 1) as usize)
950                    .collect(),
951                origin: v.storage.iter().map(|r| r.0).collect(),
952                flat_offset,
953            },
954        );
955    }
956    let add = |shapes: &mut IndexMap<String, VarShape>, name: String, at: &mut usize| {
957        shapes.insert(
958            name,
959            VarShape {
960                shape: vec![capacity],
961                origin: vec![1],
962                flat_offset: *at,
963            },
964        );
965        *at += capacity;
966    };
967    let mut at = layout.u_len + layout.aux_len;
968    for s in &sets {
969        add(&mut var_shapes, cell_var(s), &mut at);
970    }
971    let mut pre_shapes = var_shapes.clone();
972    let mut pre_at = at;
973    for k in 0..pre.gathers.len() {
974        add(&mut pre_shapes, input_var(k), &mut pre_at);
975    }
976    let held: HashSet<String> = prog
977        .inner
978        .aux
979        .iter()
980        .cloned()
981        .chain(sets.iter().map(|s| cell_var(s)))
982        .collect();
983    let empty = TileLowering {
984        states: Vec::new(),
985        observed: FxHashMap::default(),
986    };
987    let build_tape = |c: &ArrayCompiled| -> Result<TapeProgram, SimulateError> {
988        let _env = compiled.precision.enter();
989        c.build_tile_tape(&empty)
990            .map_err(|d| declined_error(&d, "lane"))
991    };
992    let main_tape = build_tape(&shell(
993        compiled,
994        var_shapes.clone(),
995        main.rhs,
996        main.observed,
997        held.clone(),
998    ))?;
999    let mut pre_held = held.clone();
1000    pre_held.extend((0..pre.gathers.len()).map(input_var));
1001    let pre_tape = build_tape(&shell(
1002        compiled,
1003        pre_shapes,
1004        pre.rhs,
1005        pre.observed,
1006        pre_held,
1007    ))?;
1008
1009    // The gather program: one rule per hoisted read, writing its lanes.
1010    let mut gshapes: IndexMap<String, VarShape> = IndexMap::new();
1011    let mut gat = 0usize;
1012    for k in 0..main.gathers.len() {
1013        add(&mut gshapes, input_var(k), &mut gat);
1014    }
1015    for s in &sets {
1016        add(&mut gshapes, cell_var(s), &mut gat);
1017    }
1018    // The gather program's `du` is the inputs alone, so the cells follow
1019    // them in its state; the call passes the cells there.
1020    let n_inputs = main.gathers.len();
1021    let grules: Vec<RhsRule> = main
1022        .gathers
1023        .iter()
1024        .enumerate()
1025        .map(|(k, g)| RhsRule::ArrayLoop {
1026            var_name: input_var(k),
1027            output_idx_names: vec![LANE.to_string()],
1028            output_ranges: vec![(1, cap)],
1029            lhs_idx_exprs: vec![Expr::Variable(LANE.to_string())],
1030            body: Box::new(g.clone()),
1031            contract_names: Vec::new(),
1032            contract_dims: Vec::new(),
1033            reduce: crate::faq::ReduceKind::Sum,
1034            filter: None,
1035        })
1036        .collect();
1037    let gobserved: Vec<AlgebraicRule> = compiled
1038        .observed_rules
1039        .iter()
1040        .filter(|r| {
1041            let var = match r {
1042                AlgebraicRule::Scalar { var, .. }
1043                | AlgebraicRule::ArrayLoop { var, .. }
1044                | AlgebraicRule::Recurrence { var, .. } => var,
1045            };
1046            !dependent.contains(var)
1047        })
1048        .cloned()
1049        .collect();
1050    let gheld: HashSet<String> = sets.iter().map(|s| cell_var(s)).collect();
1051    let gather_tape = if n_inputs == 0 {
1052        build_tape(&shell(compiled, gshapes, Vec::new(), Vec::new(), gheld))?
1053    } else {
1054        build_tape(&shell(compiled, gshapes, grules, gobserved, gheld))?
1055    };
1056    let slot_of = [
1057        slot_map(&main_tape, &prog.inner.slots)?,
1058        slot_map(&pre_tape, &prog.inner.slots)?,
1059        slot_map(&gather_tape, &prog.inner.slots)?,
1060    ];
1061    Ok(LaneProgram {
1062        inner: Arc::new(LaneInner {
1063            id: next_program_id(),
1064            main: Arc::new(main_tape),
1065            pre: Arc::new(pre_tape),
1066            gather: Arc::new(gather_tape),
1067            slot_of,
1068            layout: layout.lane_public(&sets, capacity),
1069            blocks: layout,
1070            slots: Arc::clone(&prog.inner.slots),
1071            capacity,
1072            extents: sets.iter().map(|s| sizes[s]).collect(),
1073            sets,
1074            n_inputs,
1075            n_params: compiled.param_names.len(),
1076            declared: Arc::clone(&prog.inner.declared),
1077            precision,
1078        }),
1079    })
1080}