Skip to main content

earthsci_ast/simulate_array/range/
tile.rs

1//! `tile` (API_SPEC §5.8, RFC U3 §4–§5): a `native` program restricted to an
2//! owned box that reads a storage box.
3//!
4//! A tile is a re-lowering of the tape. The compiled rules are rewritten over
5//! their need boxes ([`super::walk`]): each derivative over its owned rows, each
6//! state-dependent array observed over the union of what its readers read, and
7//! every makearray clipped to what is read of it. The state layout is the
8//! storage box, at its global origin, so the existing slot addressing is
9//! `base + Σ (g − S_lo)·stride`; the lowering classifies every state read
10//! against the global domain (RFC U3 §4 rule 2). No document is parsed or
11//! flattened.
12
13use super::forcing::{ForcingSet, ForcingSlot, RhsError};
14use super::walk::{AxisSpec, Read, Refusal, Scope, VarInfo, Walker};
15use super::{
16    Blocks, Decomposition, ProgramModel, RhsProgram, RhsWorkspace, TileBox, TileInput, TileLayout,
17    WsExec, check_forcing, check_lengths, check_owner, index_set_sizes, next_program_id,
18    range_error, slot_map, tape_fault, var_infos,
19};
20use crate::diagnostic::codes;
21use crate::simulate::SimulateError;
22use crate::simulate_array::tape::{
23    CallInputs, DeclinedTape, ForcingView, TapeCtx, TapeProgram, TileAxisDom, TileLowering,
24    TileObserved, TilePiece, run_tape_inputs,
25};
26use crate::simulate_array::vectorized::lhs_constant_shifts;
27use crate::simulate_array::{AlgebraicRule, ArrayCompiled, RhsRule, VarShape};
28use crate::types::Expr;
29use indexmap::IndexMap;
30use rustc_hash::FxHashMap;
31use std::collections::{HashMap, HashSet};
32use std::sync::Arc;
33
34/// One tile of a [`RhsProgram`] (RFC U3 §4): it reads `u` and `aux` over their
35/// storage boxes and writes only the owned rows of `du`.
36///
37/// `Send + Sync + Clone`: it holds the program and plain layout data, so
38/// tiles of one rank can share it across threads, each with its own
39/// [`RhsWorkspace`]. It reads the forcing sets of the program it was cut
40/// from.
41#[derive(Clone)]
42pub struct TileProgram {
43    inner: Arc<TileInner>,
44}
45
46struct TileInner {
47    id: u64,
48    prog: Arc<TapeProgram>,
49    blocks: Blocks,
50    layout: TileLayout,
51    slots: Arc<[ForcingSlot]>,
52    slot_of: Vec<u32>,
53    n_params: usize,
54    declared: Arc<HashSet<String>>,
55    precision: crate::precision::Precision,
56}
57
58impl std::fmt::Debug for TileProgram {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        f.debug_struct("TileProgram")
61            .field("layout", &self.inner.layout)
62            .finish()
63    }
64}
65
66impl TileProgram {
67    /// The tile's input layout: every variable's storage and owned boxes.
68    pub fn layout(&self) -> &TileLayout {
69        &self.inner.layout
70    }
71
72    /// The forcing slots a set for this tile holds: the whole program's.
73    pub fn forcing_slots(&self) -> &[ForcingSlot] {
74        &self.inner.slots
75    }
76
77    /// Buffers for [`Self::eval_into`].
78    pub fn new_workspace(&self) -> RhsWorkspace {
79        let n = self.inner.blocks.u_len + self.inner.blocks.aux_len;
80        let mut ctx = TapeCtx::new(Arc::clone(&self.inner.prog));
81        ctx.set_serial();
82        RhsWorkspace {
83            owner: self.inner.id,
84            state: vec![0.0; n],
85            dy: Vec::new(),
86            exec: WsExec::Tape(Box::new(ctx)),
87            _not_sync: std::marker::PhantomData,
88        }
89    }
90
91    /// Write the tile's owned rows of `du` (RFC U3 §4): `u` and `aux` hold
92    /// every in-domain cell of their storage boxes (and, on a periodic axis,
93    /// the wrapped values of the cells outside `1..N`); every other element of
94    /// `du` is left as the caller holds it.
95    #[allow(clippy::too_many_arguments)]
96    pub fn eval_into(
97        &self,
98        u: &[f64],
99        aux: &[f64],
100        p: &[f64],
101        t: f64,
102        forcing: &ForcingSet,
103        du: &mut [f64],
104        ws: &mut RhsWorkspace,
105    ) -> Result<(), RhsError> {
106        let inner = &*self.inner;
107        check_lengths(&inner.blocks, u, aux, du, p, inner.n_params)?;
108        check_owner(ws, inner.id)?;
109        check_forcing(forcing, &inner.slots)?;
110        let nu = inner.blocks.u_len;
111        ws.state[..nu].copy_from_slice(u);
112        ws.state[nu..].copy_from_slice(aux);
113        let WsExec::Tape(ctx) = &mut ws.exec else {
114            unreachable!("a tile workspace runs a tape");
115        };
116        let _precision = crate::precision::enter(inner.precision);
117        let scope = crate::simulate_array::FaultScope::enter();
118        let inputs = CallInputs {
119            state: &ws.state,
120            params: p,
121            forcing: ForcingView::Slots {
122                bufs: forcing.bufs(),
123                slot_of: &inner.slot_of,
124            },
125            forcing_epoch: Some(forcing.epoch()),
126            t,
127            declared: &inner.declared,
128        };
129        run_tape_inputs(ctx, &inputs, du, false);
130        tape_fault(scope, ctx)
131    }
132}
133
134fn box_invalid(message: String) -> SimulateError {
135    range_error(codes::RANGE_BOX_INVALID, message)
136}
137
138pub(super) fn refusal_error(r: Refusal) -> SimulateError {
139    if r.code == codes::COMPILER_REFUSED_RULE {
140        refused(r.message)
141    } else {
142        range_error(r.code, r.message)
143    }
144}
145
146/// `compiler_refused_rule` under `native` for a range restriction.
147pub(super) fn refused(reason: String) -> SimulateError {
148    SimulateError::Compile(crate::compile_error::CompileError::CompilerRefusedRule {
149        compiler: "native",
150        kind: "state derivative",
151        rule: reason
152            .split('`')
153            .nth(1)
154            .unwrap_or("(range restriction)")
155            .to_string(),
156        tier: "continuous",
157        reason,
158    })
159}
160
161/// A declined restricted tape, as the error its first decline names: a
162/// range diagnostic when the lowering raised one, else `compiler_refused_rule`.
163pub(super) fn declined_error(d: &DeclinedTape, what: &str) -> SimulateError {
164    let declined: Vec<(usize, &String)> = d
165        .reasons
166        .iter()
167        .enumerate()
168        .filter_map(|(k, r)| r.as_ref().map(|r| (k, r)))
169        .collect();
170    for code in [codes::RANGE_HALO_INSUFFICIENT, codes::RANGE_GLOBAL_LOCALITY] {
171        if let Some((k, r)) = declined.iter().find(|(_, r)| r.starts_with(code)) {
172            let msg = r.trim_start_matches(code).trim_start_matches(':').trim();
173            return range_error(code, format!("{} (rule `{}`)", msg, d.rules[*k].name));
174        }
175    }
176    let (k, reason) = declined[0];
177    let rule = &d.rules[k];
178    let reason = if reason.contains(what) {
179        reason.clone()
180    } else {
181        format!("{what}: {reason}")
182    };
183    SimulateError::Compile(crate::compile_error::CompileError::CompilerRefusedRule {
184        compiler: "native",
185        kind: rule.kind_name(),
186        rule: rule.name.clone(),
187        tier: rule.cadence_name(),
188        reason,
189    })
190}
191
192/// The per-cut-set owned and storage ranges of a validated box.
193struct Boxes {
194    owned: HashMap<String, (i64, i64)>,
195    storage: HashMap<String, (i64, i64)>,
196}
197
198fn validate(prog: &RhsProgram, dec: &Decomposition, bx: &TileBox) -> Result<Boxes, SimulateError> {
199    let sizes = index_set_sizes(prog.inner.flat());
200    for a in &dec.axes {
201        if !sizes.contains_key(&a.index_set) {
202            return Err(box_invalid(format!(
203                "the decomposition cuts `{}`, which is not an interval index set of this program",
204                a.index_set
205            )));
206        }
207        if let Some(p) = a.period
208            && p != sizes[&a.index_set]
209        {
210            return Err(box_invalid(format!(
211                "the decomposition gives `{}` period {p}, and it has {} cells",
212                a.index_set, sizes[&a.index_set]
213            )));
214        }
215    }
216    for (face, cell) in &dec.faces {
217        if !dec.axes.iter().any(|a| &a.index_set == cell) {
218            return Err(box_invalid(format!(
219                "face set `{face}` is declared against `{cell}`, which the decomposition does \
220                 not cut"
221            )));
222        }
223        if !sizes.contains_key(face) {
224            return Err(box_invalid(format!(
225                "face set `{face}` is not an interval index set of this program"
226            )));
227        }
228    }
229    let collect = |list: &[(String, std::ops::RangeInclusive<i64>)], what: &str| {
230        let mut out: HashMap<String, (i64, i64)> = HashMap::new();
231        for (set, r) in list {
232            if !dec.axes.iter().any(|a| &a.index_set == set) {
233                return Err(box_invalid(format!(
234                    "the {what} box names `{set}`, which the decomposition does not cut"
235                )));
236            }
237            if out.insert(set.clone(), (*r.start(), *r.end())).is_some() {
238                return Err(box_invalid(format!("the {what} box names `{set}` twice")));
239            }
240        }
241        for a in &dec.axes {
242            if !out.contains_key(&a.index_set) {
243                return Err(box_invalid(format!(
244                    "the {what} box has no range for the decomposed set `{}`",
245                    a.index_set
246                )));
247            }
248        }
249        Ok(out)
250    };
251    let owned = collect(&bx.owned, "owned")?;
252    let storage = collect(&bx.storage, "storage")?;
253    for a in &dec.axes {
254        let set = &a.index_set;
255        let n = sizes[set];
256        let (olo, ohi) = owned[set];
257        let (slo, shi) = storage[set];
258        if olo > ohi || olo < 1 || ohi > n {
259            return Err(box_invalid(format!(
260                "the owned range {olo}..{ohi} of `{set}` is not a non-empty range inside 1..{n}"
261            )));
262        }
263        if slo > olo || shi < ohi {
264            return Err(box_invalid(format!(
265                "the owned range {olo}..{ohi} of `{set}` is not inside its storage range \
266                 {slo}..{shi}"
267            )));
268        }
269        if a.period.is_some() && shi - slo + 1 > 2 * n {
270            return Err(box_invalid(format!(
271                "the storage range {slo}..{shi} of the periodic set `{set}` spans more than two \
272                 periods"
273            )));
274        }
275    }
276    Ok(Boxes { owned, storage })
277}
278
279/// The storage and owned range of one axis of a variable on this tile.
280fn axis_ranges(spec: &AxisSpec, extent: (i64, i64), boxes: &Boxes) -> ((i64, i64), (i64, i64)) {
281    match spec {
282        AxisSpec::Whole { .. } => (extent, extent),
283        AxisSpec::Cut { cell, face, hi, .. } => {
284            let (slo, shi) = boxes.storage[cell];
285            let (olo, ohi) = boxes.owned[cell];
286            if *face {
287                let n_cells = hi - 1;
288                (
289                    (slo, shi + 1),
290                    (olo, if ohi == n_cells { ohi + 1 } else { ohi }),
291                )
292            } else {
293                ((slo, shi), (olo, ohi))
294            }
295        }
296    }
297}
298
299/// The free names of every rule body that reads `name`, transitively: whether
300/// an observed depends on the state.
301pub(super) fn state_dependent(compiled: &ProgramModel) -> HashSet<String> {
302    let states: HashSet<&str> = compiled.var_shapes.keys().map(String::as_str).collect();
303    let mut dep: HashSet<String> = HashSet::new();
304    for rule in &compiled.observed_rules {
305        let (var, body) = observed_parts(rule);
306        let free = crate::expression::free_variables(body);
307        if free
308            .iter()
309            .any(|n| states.contains(n.as_str()) || dep.contains(n))
310        {
311            dep.insert(var.clone());
312        }
313    }
314    dep
315}
316
317fn observed_parts(rule: &AlgebraicRule) -> (&String, &Expr) {
318    match rule {
319        AlgebraicRule::Scalar { var, body, .. }
320        | AlgebraicRule::ArrayLoop { var, body, .. }
321        | AlgebraicRule::Recurrence { var, body, .. } => (var, body),
322    }
323}
324
325/// An observed whose wholesale body is one array-valued `faq` with no
326/// reduction index, over ranges `1..=n` matching its declared shape, as the
327/// array loop over those ranges: the same cells, computed by the same
328/// per-element kernels. `None` for every other rule.
329pub(super) fn as_loop(rule: &AlgebraicRule) -> Option<AlgebraicRule> {
330    let AlgebraicRule::Scalar {
331        var,
332        body,
333        declared_shape: Some(shape),
334    } = rule
335    else {
336        return None;
337    };
338    let Expr::Operator(node) = &**body else {
339        return None;
340    };
341    if !matches!(node.op.as_str(), "faq" | "aggregate")
342        || node.filter.is_some()
343        || node.join.is_some()
344    {
345        return None;
346    }
347    let out = node.output_idx.as_ref().filter(|o| !o.is_empty())?;
348    let ranges = node.ranges.as_ref()?;
349    if out.len() != shape.len() || ranges.keys().any(|k| !out.contains(k)) {
350        return None;
351    }
352    let mut bounds = Vec::with_capacity(out.len());
353    for (d, name) in out.iter().enumerate() {
354        let [lo, hi] = ranges.get(name)?.bounds()?;
355        if lo != 1 || hi != shape[d] as i64 {
356            return None;
357        }
358        bounds.push((lo, hi));
359    }
360    Some(AlgebraicRule::ArrayLoop {
361        var: var.clone(),
362        output_idx_names: out.clone(),
363        output_ranges: bounds,
364        body: std::sync::Arc::new(node.expr.as_deref()?.clone()),
365    })
366}
367
368/// The read intervals of each name, per axis.
369type AxisReads = HashMap<String, Vec<Vec<(i64, i64)>>>;
370
371/// Collect the reads of each name per axis.
372fn collect_reads(reads: &[Read], into: &mut AxisReads, vars: &HashMap<String, VarInfo>) {
373    for r in reads {
374        let Some(info) = vars.get(&r.var) else {
375            continue;
376        };
377        let entry = into
378            .entry(r.var.clone())
379            .or_insert_with(|| vec![Vec::new(); info.axes.len()]);
380        if let Some(e) = entry.get_mut(r.axis) {
381            e.push((r.lo, r.hi));
382        }
383    }
384}
385
386/// One axis of an observed's need: its slot range `lo..=hi`, unwrapped (on a
387/// periodic axis `hi` may pass the period), and the pieces of the global
388/// domain it holds, each `(global lo, global hi, 0-based slot offset)`.
389#[derive(Debug, PartialEq)]
390struct AxisNeed {
391    lo: i64,
392    hi: i64,
393    pieces: Vec<(i64, i64, i64)>,
394}
395
396/// The cells of an observed axis of global extent `1..=hi` that the reads
397/// `reads` need. Reads past the extent are the zero ghost and need nothing.
398/// Along a periodic axis of `period == hi` cells the need is the shortest arc
399/// of the circle that covers every read, so reads on both sides of the seam
400/// need two pieces, not the cells between them; otherwise it is the hull.
401fn axis_need(reads: &[(i64, i64)], hi: i64, period: Option<i64>) -> AxisNeed {
402    let mut iv: Vec<(i64, i64)> = reads
403        .iter()
404        .map(|&(a, b)| (a.max(1), b.min(hi)))
405        .filter(|(a, b)| a <= b)
406        .collect();
407    if iv.is_empty() {
408        return AxisNeed {
409            lo: 1,
410            hi,
411            pieces: vec![(1, hi, 0)],
412        };
413    }
414    iv.sort_unstable();
415    let mut merged: Vec<(i64, i64)> = Vec::with_capacity(iv.len());
416    for (a, b) in iv {
417        match merged.last_mut() {
418            Some(m) if a <= m.1 + 1 => m.1 = m.1.max(b),
419            _ => merged.push((a, b)),
420        }
421    }
422    let first = merged[0].0;
423    let last = merged[merged.len() - 1].1;
424    let hull = AxisNeed {
425        lo: first,
426        hi: last,
427        pieces: vec![(first, last, 0)],
428    };
429    if period != Some(hi) || merged.len() < 2 {
430        return hull;
431    }
432    // The gap across the seam, against the widest gap between two runs.
433    let seam_gap = (hi - last) + (first - 1);
434    let (k, gap) = merged
435        .windows(2)
436        .enumerate()
437        .map(|(k, w)| (k, w[1].0 - w[0].1 - 1))
438        .max_by_key(|&(k, g)| (g, std::cmp::Reverse(k)))
439        .expect("two runs have a gap");
440    if gap <= seam_gap {
441        return hull;
442    }
443    // Need `merged[k+1].0 ..= hi`, then `1 ..= merged[k].1` past the seam.
444    let a = merged[k + 1].0;
445    let b = merged[k].1;
446    AxisNeed {
447        lo: a,
448        hi: b + hi,
449        pieces: vec![(a, hi, 0), (1, b, hi - a + 1)],
450    }
451}
452
453/// The rules a tile runs, and the need of every array observed it restricts.
454pub(super) struct ClippedRules {
455    pub(super) rhs: Vec<RhsRule>,
456    pub(super) observed: Vec<AlgebraicRule>,
457    pub(super) need: FxHashMap<String, TileObserved>,
458}
459
460/// Rewrite `compiled`'s rules for a tile whose derivative of each written
461/// state covers `owned[var]` (one inclusive range per axis).
462pub(super) fn clip_rules(
463    compiled: &ProgramModel,
464    vars: &HashMap<String, VarInfo>,
465    owned: &HashMap<String, Vec<(i64, i64)>>,
466) -> Result<ClippedRules, SimulateError> {
467    let mut reads: AxisReads = HashMap::new();
468    let mut needed: HashSet<String> = HashSet::new();
469    let mut rhs = Vec::new();
470    for rule in &compiled.rhs_rules {
471        match rule {
472            RhsRule::Scalar { slot, .. } => {
473                let name = super::super::layout::slot_name(&compiled.var_shapes, *slot)
474                    .unwrap_or_else(|| format!("slot {slot}"));
475                return Err(range_error(
476                    codes::RANGE_GLOBAL_LOCALITY,
477                    format!("rule `D({name})` writes a 0-d state, which no tile owns"),
478                ));
479            }
480            RhsRule::IndexedScalar { slot, .. } => {
481                let name = super::super::layout::slot_name(&compiled.var_shapes, *slot)
482                    .unwrap_or_else(|| format!("slot {slot}"));
483                return Err(refused(format!(
484                    "tile restriction: rule `D({name})` is a per-cell scalar rule, which a tile \
485                     does not restrict"
486                )));
487            }
488            RhsRule::ArrayLoop {
489                var_name,
490                output_idx_names,
491                output_ranges,
492                lhs_idx_exprs,
493                body,
494                contract_names,
495                contract_dims,
496                reduce,
497                filter,
498            } => {
499                let rule_name = format!("D({var_name})");
500                let info = &vars[var_name];
501                let Some(shifts) = lhs_constant_shifts(lhs_idx_exprs, output_idx_names) else {
502                    return Err(refused(format!(
503                        "tile restriction: rule `{rule_name}` writes through a computed \
504                         left-hand side"
505                    )));
506                };
507                let own = &owned[var_name];
508                let mut ranges = Vec::with_capacity(output_ranges.len());
509                let mut sets = Vec::with_capacity(output_ranges.len());
510                for (d, &(lo, hi)) in output_ranges.iter().enumerate() {
511                    let (olo, ohi) = own[d];
512                    ranges.push((lo.max(olo - shifts[d]), hi.min(ohi - shifts[d])));
513                    sets.push(info.axes[d].cut_cell().map(str::to_string));
514                }
515                if ranges.iter().any(|(lo, hi)| lo > hi) {
516                    continue;
517                }
518                let sc = Scope {
519                    syms: output_idx_names.clone(),
520                    lo: ranges.iter().map(|r| r.0).collect(),
521                    hi: ranges.iter().map(|r| r.1).collect(),
522                    sets,
523                    contracted: contract_names.clone(),
524                };
525                let mut w = Walker::new(vars, rule_name);
526                let body = w.expr(body, &sc).map_err(refusal_error)?;
527                let filter = match filter {
528                    Some(f) => Some(Box::new(w.expr(f, &sc).map_err(refusal_error)?)),
529                    None => None,
530                };
531                collect_reads(&w.reads, &mut reads, vars);
532                needed.extend(crate::expression::free_variables(&body));
533                if let Some(f) = &filter {
534                    needed.extend(crate::expression::free_variables(f));
535                }
536                rhs.push(RhsRule::ArrayLoop {
537                    var_name: var_name.clone(),
538                    output_idx_names: output_idx_names.clone(),
539                    output_ranges: ranges,
540                    lhs_idx_exprs: lhs_idx_exprs.clone(),
541                    body: Box::new(body),
542                    contract_names: contract_names.clone(),
543                    contract_dims: contract_dims.clone(),
544                    reduce: *reduce,
545                    filter,
546                });
547            }
548        }
549    }
550
551    let dependent = state_dependent(compiled);
552    let mut need = FxHashMap::default();
553    let mut kept: Vec<Option<AlgebraicRule>> = vec![None; compiled.observed_rules.len()];
554    for (k, rule) in compiled.observed_rules.iter().enumerate().rev() {
555        let as_array = as_loop(rule);
556        let rule = as_array.as_ref().unwrap_or(rule);
557        let (var, body) = observed_parts(rule);
558        if !needed.contains(var) {
559            continue;
560        }
561        let info = &vars[var];
562        match rule {
563            AlgebraicRule::ArrayLoop {
564                output_idx_names,
565                output_ranges,
566                ..
567            } if dependent.contains(var) && info.is_cut() => {
568                let read = reads.get(var).cloned().unwrap_or_default();
569                let axes: Vec<AxisNeed> = output_ranges
570                    .iter()
571                    .enumerate()
572                    .map(|(d, &(_, hi))| match &info.axes[d] {
573                        AxisSpec::Cut { period, face, .. } => {
574                            let r = read.get(d).map(Vec::as_slice).unwrap_or(&[]);
575                            axis_need(r, hi, if *face { None } else { *period })
576                        }
577                        AxisSpec::Whole { .. } => AxisNeed {
578                            lo: 1,
579                            hi,
580                            pieces: vec![(1, hi, 0)],
581                        },
582                    })
583                    .collect();
584                let sets: Vec<Option<String>> = info
585                    .axes
586                    .iter()
587                    .map(|a| a.cut_cell().map(str::to_string))
588                    .collect();
589                // One piece per combination of the axes' pieces, each the
590                // observed's own cells there, walked over its own box.
591                let mut pieces = Vec::new();
592                let mut combo = vec![0usize; axes.len()];
593                'combos: loop {
594                    let mut ranges = Vec::with_capacity(axes.len());
595                    let mut dest_lo = Vec::with_capacity(axes.len());
596                    for (d, ax) in axes.iter().enumerate() {
597                        let (glo, ghi, off) = ax.pieces[combo[d]];
598                        let (lo, hi) = (output_ranges[d].0.max(glo), output_ranges[d].1.min(ghi));
599                        ranges.push((lo, hi));
600                        dest_lo.push((off + lo - glo).max(0) as usize);
601                    }
602                    if ranges.iter().all(|(lo, hi)| lo <= hi) {
603                        let sc = Scope {
604                            syms: output_idx_names.clone(),
605                            lo: ranges.iter().map(|r| r.0).collect(),
606                            hi: ranges.iter().map(|r| r.1).collect(),
607                            sets: sets.clone(),
608                            contracted: Vec::new(),
609                        };
610                        let mut w = Walker::new(vars, var.clone());
611                        let b = w.expr(body, &sc).map_err(refusal_error)?;
612                        collect_reads(&w.reads, &mut reads, vars);
613                        needed.extend(crate::expression::free_variables(&b));
614                        pieces.push(TilePiece {
615                            ranges,
616                            dest_lo,
617                            body: std::rc::Rc::new(b),
618                        });
619                    }
620                    for d in (0..axes.len()).rev() {
621                        combo[d] += 1;
622                        if combo[d] < axes[d].pieces.len() {
623                            continue 'combos;
624                        }
625                        combo[d] = 0;
626                    }
627                    break;
628                }
629                need.insert(
630                    var.clone(),
631                    TileObserved {
632                        lo: axes.iter().map(|a| a.lo).collect(),
633                        hi: axes.iter().map(|a| a.hi).collect(),
634                        period: axes
635                            .iter()
636                            .map(|a| (a.pieces.len() > 1).then(|| a.pieces[0].1))
637                            .collect(),
638                        pieces,
639                    },
640                );
641                kept[k] = Some(rule.clone());
642            }
643            AlgebraicRule::Recurrence { axis, .. } if dependent.contains(var) && info.is_cut() => {
644                if let Some(AxisSpec::Cut { set, .. }) = info.axes.get(*axis) {
645                    return Err(range_error(
646                        codes::RANGE_GLOBAL_LOCALITY,
647                        format!(
648                            "observed `{var}` is a recurrence along the decomposed axis `{set}`"
649                        ),
650                    ));
651                }
652                return Err(refused(format!(
653                    "tile restriction: observed `{var}` is a recurrence over a decomposed array"
654                )));
655            }
656            _ => {
657                if dependent.contains(var) && info.is_cut() {
658                    return Err(refused(format!(
659                        "tile restriction: observed `{var}` is a whole-array expression of the \
660                         state"
661                    )));
662                }
663                needed.extend(crate::expression::free_variables(body));
664                kept[k] = Some(rule.clone());
665            }
666        }
667    }
668    Ok(ClippedRules {
669        rhs,
670        observed: kept.into_iter().flatten().collect(),
671        need,
672    })
673}
674
675/// A shell model around restricted rules and a restricted state layout: the
676/// input a tape build reads. Everything else is the program's.
677pub(super) fn shell(
678    compiled: &ProgramModel,
679    var_shapes: IndexMap<String, VarShape>,
680    rhs_rules: Vec<RhsRule>,
681    observed_rules: Vec<AlgebraicRule>,
682    held: HashSet<String>,
683) -> ArrayCompiled {
684    compiled.shell(var_shapes, rhs_rules, observed_rules, held)
685}
686
687/// The document precision a restricted program runs at, refusing a document
688/// with per-variable element types (whose environment is not `Send`).
689pub(super) fn plain_precision(
690    compiled: &ProgramModel,
691    what: &str,
692) -> Result<crate::precision::Precision, SimulateError> {
693    let _env = compiled.precision.enter();
694    if crate::precision::has_variable_overrides() {
695        return Err(refused(format!(
696            "{what}: the document declares per-variable element types, which a range-restricted \
697             program does not carry"
698        )));
699    }
700    Ok(compiled.precision.document)
701}
702
703pub(super) fn build(
704    prog: &RhsProgram,
705    dec: &Decomposition,
706    bx: &TileBox,
707) -> Result<TileProgram, SimulateError> {
708    let boxes = validate(prog, dec, bx)?;
709    let compiled = &*prog.inner.model;
710    let precision = plain_precision(compiled, "tile restriction")?;
711    let vars = var_infos(prog, dec);
712    for w in &prog.inner.writes {
713        if !vars[w].is_cut() {
714            return Err(range_error(
715                codes::RANGE_GLOBAL_LOCALITY,
716                format!(
717                    "the program writes `{w}`, which has no decomposed axis, so no tile owns its \
718                     rows (rule `D({w})`)"
719                ),
720            ));
721        }
722    }
723    let mut owned: HashMap<String, Vec<(i64, i64)>> = HashMap::new();
724    let mut entries = Vec::new();
725    let set_names = super::state_set_names(prog.inner.flat(), compiled);
726    for (input, names) in [
727        (TileInput::U, &prog.inner.writes),
728        (TileInput::Aux, &prog.inner.aux),
729    ] {
730        for name in names {
731            let info = &vars[name];
732            let mut storage = Vec::new();
733            let mut own = Vec::new();
734            for (d, spec) in info.axes.iter().enumerate() {
735                let (s, o) = axis_ranges(spec, info.extent[d], &boxes);
736                storage.push(s);
737                own.push(o);
738            }
739            owned.insert(name.clone(), own.clone());
740            entries.push((name.clone(), input, set_names[name].clone(), storage, own));
741        }
742    }
743    let layout = Blocks::build(entries);
744    let clipped = clip_rules(compiled, &vars, &owned)?;
745
746    let mut var_shapes: IndexMap<String, VarShape> = IndexMap::new();
747    let mut doms = Vec::new();
748    for v in &layout.vars {
749        let flat_offset = match v.input {
750            TileInput::U => v.base,
751            TileInput::Aux => layout.u_len + v.base,
752        };
753        var_shapes.insert(
754            v.name.clone(),
755            VarShape {
756                shape: v
757                    .storage
758                    .iter()
759                    .map(|(lo, hi)| (hi - lo + 1) as usize)
760                    .collect(),
761                origin: v.storage.iter().map(|r| r.0).collect(),
762                flat_offset,
763            },
764        );
765        let info = &vars[&v.name];
766        doms.push(
767            info.axes
768                .iter()
769                .enumerate()
770                .map(|(d, a)| match a {
771                    AxisSpec::Whole { set } => TileAxisDom {
772                        lo: info.extent[d].0,
773                        hi: info.extent[d].1,
774                        period: None,
775                        set: set.clone().unwrap_or_else(|| format!("axis {}", d + 1)),
776                    },
777                    AxisSpec::Cut {
778                        set, hi, period, ..
779                    } => TileAxisDom {
780                        lo: 1,
781                        hi: *hi,
782                        period: *period,
783                        set: set.clone(),
784                    },
785                })
786                .collect(),
787        );
788    }
789    let lowering = TileLowering {
790        states: doms,
791        observed: clipped.need,
792    };
793    let held: HashSet<String> = prog.inner.aux.iter().cloned().collect();
794    let shell = shell(compiled, var_shapes, clipped.rhs, clipped.observed, held);
795    let tape = {
796        let _env = compiled.precision.enter();
797        shell
798            .build_tile_tape(&lowering)
799            .map_err(|d| declined_error(&d, "tile restriction"))?
800    };
801    let slot_of = slot_map(&tape, &prog.inner.slots)?;
802    Ok(TileProgram {
803        inner: Arc::new(TileInner {
804            id: next_program_id(),
805            prog: Arc::new(tape),
806            layout: layout.public(),
807            blocks: layout,
808            slots: Arc::clone(&prog.inner.slots),
809            slot_of,
810            n_params: compiled.param_names.len(),
811            declared: Arc::clone(&prog.inner.declared),
812            precision,
813        }),
814    })
815}
816
817#[cfg(test)]
818mod tests {
819    use super::{AxisNeed, axis_need};
820
821    #[test]
822    fn a_need_across_a_periodic_seam_is_two_pieces() {
823        // Cells 1..4 and the wrapped cell 64.
824        assert_eq!(
825            axis_need(&[(1, 4), (64, 64), (0, 3)], 64, Some(64)),
826            AxisNeed {
827                lo: 64,
828                hi: 68,
829                pieces: vec![(64, 64, 0), (1, 4, 1)],
830            }
831        );
832        // Not periodic: the hull.
833        assert_eq!(
834            axis_need(&[(1, 4), (64, 64)], 64, None),
835            AxisNeed {
836                lo: 1,
837                hi: 64,
838                pieces: vec![(1, 64, 0)],
839            }
840        );
841        // Periodic, but the reads sit nearer each other than across the seam.
842        assert_eq!(
843            axis_need(&[(10, 12), (20, 22)], 64, Some(64)),
844            AxisNeed {
845                lo: 10,
846                hi: 22,
847                pieces: vec![(10, 22, 0)],
848            }
849        );
850        // No read: the whole axis.
851        assert_eq!(axis_need(&[], 8, Some(8)).pieces, vec![(1, 8, 0)]);
852    }
853}