libjay 0.3.0

Independent, modern implementations of the J and APL array languages: parallel and vectorized, embeddable from Rust, Python, and C
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
//! Language frontends. Each parses its own syntax into the shared IR.

pub mod apl;
pub mod j;

use crate::error::{Error, ErrorKind, Result};
use crate::fmt::FmtOpts;
use crate::ir::{ParamSpec, Program};
use crate::verb::{Agreement, Tol};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Lang {
    J,
    Apl,
}

impl Lang {
    pub fn from_name(name: &str) -> Option<Lang> {
        match name.to_ascii_lowercase().as_str() {
            "j" => Some(Lang::J),
            "apl" => Some(Lang::Apl),
            _ => None,
        }
    }
}

/// How a nested array holds a simple scalar.
///
/// APL2 and the ISO standard float: `⊂` on a simple scalar is the scalar
/// itself, because a simple scalar cannot be nested. The other reading
/// grounds it, so `⊂3` is a one-item enclosure distinct from `3`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum NestedModel {
    #[default]
    Floating,
    Grounded,
}

/// What `↑` and `⊃` mean monadically.
///
/// The APL2 line reads `↑` as first and `⊃` as disclose. The other line
/// reads `↑` as mix and `⊃` as first. The dyads (take and pick) agree.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum FirstDisclose {
    #[default]
    UpIsFirst,
    UpIsMix,
}

/// What `⌷` means.
///
/// APL2's `⌷` indexes with one scalar per axis and has no monadic case.
/// The other line reads the left argument as a list of index vectors, one
/// per axis, and gives `⌷` a monadic meaning as well.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum IndexForm {
    #[default]
    ScalarPerAxis,
    AxisVectors,
}

/// What a dyadic `⊂` does.
///
/// The APL2 line reads the left argument as partition flags: a partition
/// begins where the flags rise, and a zero drops its item. The other line
/// reads them as counts — each item says how many partitions to begin
/// before it, so a count above one leaves empty partitions behind — and
/// spells the flag reading `⊆`. Both lines agree about `⊆`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Partition {
    #[default]
    Flags,
    Counts,
}

/// What monadic `≡` answers for an array whose items differ in depth.
///
/// Both lines answer with the depth. The other one negates it where the
/// array is not uniform: where two items of it, at any level, differ in
/// depth or in shape.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum DepthSign {
    #[default]
    Unsigned,
    Signed,
}

/// Which sentence of a dfn body is its result.
///
/// libjay's block model — the value of the last sentence — is what both
/// languages' sequences do. The other reading stops at the first sentence
/// that is not an assignment and answers with its value.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum DfnResult {
    #[default]
    LastSentence,
    FirstNonAssignment,
}

/// When `⍺←v` evaluates `v`.
///
/// Eagerly: the sentence runs and the value is dropped where the left
/// argument already arrived. Lazily: the sentence does not run at all
/// then, which is observable when it has an effect or would fail.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum DefaultArg {
    #[default]
    Eager,
    Lazy,
}

/// How a grade orders complex values.
///
/// Ordering verbs refuse complex operands in either reading — a grade is a
/// permutation, not a claim about size — but a grade still has to be
/// total. By real part then imaginary is one reading; by magnitude then
/// angle is the other.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ComplexOrder {
    #[default]
    RealThenImaginary,
    MagnitudeThenAngle,
}

/// How a grade orders NESTED items.
///
/// The APL2 line, which GNU APL implements and the oracle verifies, orders
/// two items by rank, then by shape, then atom by atom with characters
/// before numbers before nested values. Dyalog's total array ordering is a
/// different comparator throughout: it compares the atoms first, padding
/// the shorter array with an item below every type, extends a lower rank
/// with leading 1s, and orders numbers before characters.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum NestedGrade {
    #[default]
    Apl2,
    TotalOrder,
}

/// What dyadic `⍳` takes on its left.
///
/// The APL2/ISO line, which GNU APL implements, looks a cell up among the
/// items of a left argument of any rank, so `(2 3⍴⍳6)⍳5` answers and a
/// scalar left argument is a one-item table. Dyalog takes a vector alone
/// and gives a RANK ERROR for anything else, scalars included.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum LookupLeft {
    #[default]
    AnyRank,
    VectorOnly,
}

/// Which line's `∨` and `∧` these are.
///
/// GNU APL's GCD reads three things loosely, all probed against it: a zero
/// argument hands its partner back with the sign (`¯3∨0` is `¯3`, though
/// `¯3.5∨0` is `3.5` — only whole numbers keep it); an argument within
/// `⎕CT` of a whole number is that number (`1.0000000000001∧5` is 5); and
/// one no larger than `⎕CT` beside the other is zero (`1E¯14∨1` is 1).
/// Dyalog does none of the three, and neither does J: `¯3∨0` is 3 there,
/// `1E¯14∨1` is `1E¯14`, and `1.0000000000001∧5` grinds out `1.0008E13`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum GcdRule {
    /// GNU APL: whole-number sign kept, near-whole and vanishing arguments
    /// rounded first.
    #[default]
    Tolerant,
    /// Dyalog and J: the magnitude, and the values as they stand.
    Exact,
}

/// How a float that is merely NEAR a whole number is admitted where a
/// count, a length or an index belongs (`⍳2+9E¯11`, `(2+9E¯11)⍴5`).
///
/// This is not the comparison tolerance — `(2+9E¯11)=2` is 0 under both
/// readings — and the two APL lines part company over it. GNU APL takes an
/// absolute `1E¯10` at every magnitude, so a large count buys no room and
/// `1E¯11` reads as 0. Dyalog's window is relative and follows `⎕CT`, so
/// `⍴⍳1000000+1E¯9` answers there and is refused here, while every
/// `2±9E¯11` case is the other way about. Neither is a superset.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum NearCount {
    /// GNU APL: an absolute `1E¯10`, whatever the magnitude.
    #[default]
    Absolute,
    /// Dyalog: the dialect's own tolerant equality against the whole
    /// number, so `⎕CT` moves it and zero admits nothing.
    Tolerant,
}

/// How `⌊` and `⌈` read a value that is merely near the integer above or
/// below it.
///
/// GNU APL shifts by `⎕CT` outright, so `⌊99.999999999995` is 99 — a gap
/// of 5E¯12 is larger than the tolerance however big the value is — while
/// `⌊¯1E¯13` is 0. Dyalog scales the shift by the magnitude but never
/// below 1, so `⌊9.9999999999999` is 10 and `⌊¯1E¯13` is `¯1`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum FloorRule {
    /// GNU APL: `⌊y+⎕CT`, an absolute shift.
    #[default]
    Shift,
    /// Dyalog: `⌊y+⎕CT×1⌈|y`, a shift that grows with the magnitude.
    Scaled,
}

/// Whether `⊤` reads its digits tolerantly.
///
/// GNU APL takes each digit with the same tolerant residue `|` uses, so
/// `2 2⊤4-1E¯14` is `0 0`. Dyalog takes them exactly, and the difference
/// survives into the digits: the same sentence is `1 2` there, the last
/// digit being 1.99999999999999 rather than a rounded 0.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum EncodeDigits {
    /// GNU APL: the digits are tolerant residues.
    #[default]
    Tolerant,
    /// Dyalog: the digits are exact residues, `⎕CT` unread.
    Exact,
}

/// Dialect settings supplied by the host.
///
/// This is what a host asks for; [`Rules`] is what the compiler and the
/// engine read. Every field's default is the setting libjay implements, so
/// `Dialect::default()` is the language as it ships and a host that names
/// no setting gets exactly that. `Option` fields mean "the language
/// default", which differs between J and APL.
///
/// The enum fields are the points where the APL lineages diverge. libjay
/// implements the APL2/ISO line that GNU APL embodies; the other arm of
/// each is refused by [`Dialect::rules`] as not implemented yet, so
/// selecting it is honest rather than silently wrong. `trains` is the
/// exception: both of its readings are implemented, so it is a choice.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Dialect {
    /// APL `⎕IO`. J's index origin is 0 and is not configurable.
    pub index_origin: Option<i64>,
    /// APL `⎕CT`, J `9!:18`: the relative comparison tolerance.
    pub comparison_tolerance: Option<f64>,
    pub nested_model: NestedModel,
    pub first_disclose: FirstDisclose,
    pub index_form: IndexForm,
    pub partition: Partition,
    pub depth_sign: DepthSign,
    pub dfn_result: DfnResult,
    pub default_arg: DefaultArg,
    pub complex_order: ComplexOrder,
    pub nested_grade: NestedGrade,
    pub lookup_left: LookupLeft,
    pub gcd_rule: GcdRule,
    pub near_count: NearCount,
    pub floor_rule: FloorRule,
    pub encode_digits: EncodeDigits,
    /// Whether a function may stand where a value belongs: a run of
    /// functions is then a train, and `F←+/` names one. Both readings are
    /// implemented, so this is a choice and not a gap. It ships on, as an
    /// extension: GNU APL refuses both spellings, and refusing a feature
    /// the oracle merely lacks serves nobody.
    pub trains: bool,
}

impl Default for Dialect {
    fn default() -> Dialect {
        Dialect::gnu_apl()
    }
}

impl Dialect {
    /// The APL libjay implements: the APL2/ISO line GNU APL embodies and
    /// the oracle verifies, plus the extensions listed in
    /// `docs/coverage.md`. Written out rather than derived, so that every
    /// setting's shipped value is stated in one place; it is equal to
    /// `Dialect::default()`, which the tests pin.
    pub fn gnu_apl() -> Dialect {
        Dialect {
            index_origin: None,
            comparison_tolerance: None,
            nested_model: NestedModel::Floating,
            first_disclose: FirstDisclose::UpIsFirst,
            index_form: IndexForm::ScalarPerAxis,
            partition: Partition::Flags,
            depth_sign: DepthSign::Unsigned,
            dfn_result: DfnResult::LastSentence,
            default_arg: DefaultArg::Eager,
            complex_order: ComplexOrder::RealThenImaginary,
            nested_grade: NestedGrade::Apl2,
            lookup_left: LookupLeft::AnyRank,
            gcd_rule: GcdRule::Tolerant,
            near_count: NearCount::Absolute,
            floor_rule: FloorRule::Shift,
            encode_digits: EncodeDigits::Tolerant,
            trains: true,
        }
    }

    /// The Dyalog line, as far as libjay implements it.
    ///
    /// Every setting here is one the recorded Dyalog answers verify
    /// (`docs/testing.md`); the settings left at the GNU/APL2 reading are
    /// the ones libjay has not derived from a Dyalog answer yet, and
    /// `docs/coverage.md` lists what that still costs. `⎕ML` is Dyalog's
    /// own default, 1, which is what the recording ran under: `↑` mixes
    /// and `⊃` takes the first.
    pub fn dyalog() -> Dialect {
        Dialect {
            index_origin: None,
            comparison_tolerance: Some(1e-14),
            nested_model: NestedModel::Floating,
            first_disclose: FirstDisclose::UpIsMix,
            index_form: IndexForm::AxisVectors,
            partition: Partition::Counts,
            depth_sign: DepthSign::Signed,
            dfn_result: DfnResult::FirstNonAssignment,
            default_arg: DefaultArg::Eager,
            complex_order: ComplexOrder::RealThenImaginary,
            nested_grade: NestedGrade::TotalOrder,
            lookup_left: LookupLeft::VectorOnly,
            gcd_rule: GcdRule::Exact,
            near_count: NearCount::Tolerant,
            floor_rule: FloorRule::Scaled,
            encode_digits: EncodeDigits::Exact,
            trains: true,
        }
    }

    /// J. Nothing in J is a dialect setting yet beyond the comparison
    /// tolerance, and the APL settings are not read under `Lang::J`, so
    /// J's dialect is the empty one.
    pub fn j() -> Dialect {
        Dialect::default()
    }

    /// Resolve to the settings the compiler and the engine read.
    ///
    /// This is the one place a dialect choice is made. A setting whose
    /// other arm libjay does not implement is refused here, by name, so
    /// that a host selecting it is told rather than quietly given this
    /// dialect's answer.
    pub fn rules(&self, lang: Lang) -> Result<Rules> {
        // A setting is the host's, not the source text's, so these carry
        // no span: there is nothing in the program to point at.
        let refuse = |what: &str| -> Error {
            Error::new(
                ErrorKind::NotYet,
                format!("{what} (the reading of another APL dialect) is not supported yet"),
                None,
            )
            .note("libjay implements the APL2/ISO line, which is the one its oracle verifies")
        };
        if let Some(ct) = self.comparison_tolerance && !(ct.is_finite() && ct >= 0.0) {
            return Err(Error::new(
                ErrorKind::Domain,
                "the comparison tolerance must be a finite value at or above zero",
                None,
            ));
        }
        match self.nested_model {
            NestedModel::Floating => {}
            NestedModel::Grounded => return Err(refuse("a grounded nested array model")),
        }
        match self.default_arg {
            DefaultArg::Eager => {}
            DefaultArg::Lazy => return Err(refuse("a lazy ⍺← default")),
        }
        match self.complex_order {
            ComplexOrder::RealThenImaginary => {}
            ComplexOrder::MagnitudeThenAngle => {
                return Err(refuse("grading complex values by magnitude and angle"))
            }
        }
        let origin = match lang {
            Lang::J => 0,
            Lang::Apl => self.index_origin.unwrap_or(1),
        };
        let ct = self.comparison_tolerance.unwrap_or(match lang {
            Lang::J => Tol::J.ct,
            Lang::Apl => Tol::APL.ct,
        });
        Ok(Rules {
            lang,
            origin,
            ct,
            nested_model: self.nested_model,
            first_disclose: self.first_disclose,
            index_form: self.index_form,
            partition: self.partition,
            depth_sign: self.depth_sign,
            dfn_result: self.dfn_result,
            default_arg: self.default_arg,
            complex_order: self.complex_order,
            nested_grade: self.nested_grade,
            lookup_left: self.lookup_left,
            gcd_rule: self.gcd_rule,
            near_count: self.near_count,
            floor_rule: self.floor_rule,
            encode_digits: self.encode_digits,
            trains: self.trains,
        })
    }
}

/// A dialect resolved against a language: what the parser and the engine
/// read. Copyable, and carried by every evaluation context, so a rule that
/// only bites at run time (the index origin a key answers with, the order
/// a grade puts complex values in) reads the same setting the parser did.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Rules {
    pub lang: Lang,
    /// The index origin in force: APL's `⎕IO`, and 0 for J.
    pub origin: i64,
    /// The comparison tolerance in force. `Rules::tol` pairs it with the
    /// language's scaling rule; a verb-local `u!.n` overrides that copy
    /// and not this one.
    pub ct: f64,
    pub nested_model: NestedModel,
    pub first_disclose: FirstDisclose,
    pub index_form: IndexForm,
    pub partition: Partition,
    pub depth_sign: DepthSign,
    pub dfn_result: DfnResult,
    pub default_arg: DefaultArg,
    pub complex_order: ComplexOrder,
    pub nested_grade: NestedGrade,
    pub lookup_left: LookupLeft,
    pub gcd_rule: GcdRule,
    pub near_count: NearCount,
    pub floor_rule: FloorRule,
    pub encode_digits: EncodeDigits,
    pub trains: bool,
}

impl Rules {
    /// The dialect's comparison tolerance, with the language's scale.
    pub fn tol(&self) -> Tol {
        Tol { ct: self.ct, by_smaller: self.lang == Lang::J, floor_rule: self.floor_rule }
    }

    /// The host-facing form, for a nested compilation (`⍎`, `".`) that has
    /// to run under the same dialect as the program executing it.
    pub fn dialect(&self) -> Dialect {
        Dialect {
            index_origin: Some(self.origin),
            comparison_tolerance: Some(self.ct),
            nested_model: self.nested_model,
            first_disclose: self.first_disclose,
            index_form: self.index_form,
            partition: self.partition,
            depth_sign: self.depth_sign,
            dfn_result: self.dfn_result,
            default_arg: self.default_arg,
            complex_order: self.complex_order,
            nested_grade: self.nested_grade,
            lookup_left: self.lookup_left,
            gcd_rule: self.gcd_rule,
            near_count: self.near_count,
            floor_rule: self.floor_rule,
            encode_digits: self.encode_digits,
            trains: self.trains,
        }
    }
}

impl Default for Rules {
    /// J's rules, which is what a context built without a program uses.
    fn default() -> Rules {
        Dialect::default().rules(Lang::J).expect("J's defaults are implemented")
    }
}

/// A source text with interpolation holes split out. Spans in every token
/// and error refer to `display`, where hole `i` reads `{name_i}`.
#[derive(Clone, Debug)]
pub struct SourceParts {
    pub display: String,
    pub segments: Vec<Segment>,
    pub param_names: Vec<String>,
}

#[derive(Clone, Debug)]
pub enum Segment {
    /// Literal source text starting at `offset` in `display`.
    Text { text: String, offset: usize },
    /// Interpolation hole: parameter `index`, shown as `{name}` in `display`.
    Param { index: usize, offset: usize, len: usize },
}

impl SourceParts {
    /// Build from pre-split literal parts with holes between them
    /// (the t-string path). `names[i]` sits between `parts[i]` and
    /// `parts[i+1]`; repeated names share one parameter.
    pub fn from_parts(parts: &[&str], names: &[&str]) -> SourceParts {
        assert_eq!(parts.len(), names.len() + 1, "N parts need N-1 holes");
        let mut display = String::new();
        let mut segments = Vec::new();
        let mut param_names: Vec<String> = Vec::new();
        for (i, part) in parts.iter().enumerate() {
            if !part.is_empty() {
                segments.push(Segment::Text { text: (*part).to_string(), offset: display.len() });
                display.push_str(part);
            }
            if i < names.len() {
                let name = names[i];
                let index = param_names
                    .iter()
                    .position(|n| n == name)
                    .unwrap_or_else(|| {
                        param_names.push(name.to_string());
                        param_names.len() - 1
                    });
                let shown = format!("{{{name}}}");
                segments.push(Segment::Param { index, offset: display.len(), len: shown.len() });
                display.push_str(&shown);
            }
        }
        SourceParts { display, segments, param_names }
    }

    /// Build from a plain string where `{identifier}` outside quotes is an
    /// interpolation hole (the pre-3.14 and Rust runtime path).
    pub fn from_source(src: &str) -> Result<SourceParts> {
        let bytes = src.as_bytes();
        let mut parts: Vec<String> = vec![String::new()];
        let mut names: Vec<String> = Vec::new();
        let mut in_quote = false;
        let mut i = 0;
        while i < src.len() {
            let ch = src[i..].chars().next().unwrap();
            if ch == '\'' {
                in_quote = !in_quote;
                parts.last_mut().unwrap().push(ch);
                i += 1;
                continue;
            }
            if ch == '{' && !in_quote {
                // Exactly `{identifier}` is an interpolation hole. Any other
                // `{` is literal program text: J spells take as `{.`, drop as
                // `}.`, so the brace itself belongs to the language.
                let rest = &src[i + 1..];
                if let Some(end) = rest.find('}') {
                    let name = &rest[..end];
                    if is_identifier(name) {
                        names.push(name.to_string());
                        parts.push(String::new());
                        i += 2 + end;
                        continue;
                    }
                }
            }
            parts.last_mut().unwrap().push(ch);
            i += ch.len_utf8();
        }
        let _ = bytes;
        let part_refs: Vec<&str> = parts.iter().map(|s| s.as_str()).collect();
        let name_refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
        Ok(SourceParts::from_parts(&part_refs, &name_refs))
    }
}

fn is_identifier(s: &str) -> bool {
    let mut chars = s.chars();
    match chars.next() {
        Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
        _ => return false,
    }
    chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}

/// Compile a plain source string (with `{name}` holes) in the given language.
pub fn compile(lang: Lang, source: &str, dialect: &Dialect) -> Result<Program> {
    let sp = SourceParts::from_source(source)?;
    compile_source_parts(lang, sp, dialect)
}

/// Compile pre-split parts (the t-string path).
pub fn compile_parts(
    lang: Lang,
    parts: &[&str],
    names: &[&str],
    dialect: &Dialect,
) -> Result<Program> {
    compile_source_parts(lang, SourceParts::from_parts(parts, names), dialect)
}

fn compile_source_parts(lang: Lang, sp: SourceParts, dialect: &Dialect) -> Result<Program> {
    let rules = dialect.rules(lang)?;
    let tol = rules.tol();
    let (mut stmts, agreement, fmt) = match lang {
        Lang::J => (j::parse(&sp)?, Agreement::LeadingPrefix, FmtOpts::J),
        Lang::Apl => (apl::parse(&sp, rules)?, Agreement::ExactOrScalar, FmtOpts::APL),
    };
    // Everything after this point walks the tree recursively, so a
    // sentence nested past what a stack holds is refused here rather than
    // taking the process down. The measurement itself does not recurse.
    for stmt in &stmts {
        crate::verb::check_nesting(stmt.depth(), stmt.span())?;
    }
    crate::fuse::pass(&mut stmts, tol);
    let params = sp.param_names.into_iter().map(|name| ParamSpec { name }).collect();
    Ok(Program { stmts, params, display_src: sp.display, agreement, fmt, rules })
}