Skip to main content

jay/frontend/
mod.rs

1//! Language frontends. Each parses its own syntax into the shared IR.
2
3pub mod apl;
4pub mod j;
5
6use crate::error::{Error, ErrorKind, Result};
7use crate::fmt::FmtOpts;
8use crate::ir::{ParamSpec, Program};
9use crate::verb::{Agreement, Tol};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum Lang {
13    J,
14    Apl,
15}
16
17impl Lang {
18    pub fn from_name(name: &str) -> Option<Lang> {
19        match name.to_ascii_lowercase().as_str() {
20            "j" => Some(Lang::J),
21            "apl" => Some(Lang::Apl),
22            _ => None,
23        }
24    }
25}
26
27/// How a nested array holds a simple scalar.
28///
29/// APL2 and the ISO standard float: `⊂` on a simple scalar is the scalar
30/// itself, because a simple scalar cannot be nested. The other reading
31/// grounds it, so `⊂3` is a one-item enclosure distinct from `3`.
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
33pub enum NestedModel {
34    #[default]
35    Floating,
36    Grounded,
37}
38
39/// What `↑` and `⊃` mean monadically.
40///
41/// The APL2 line reads `↑` as first and `⊃` as disclose. The other line
42/// reads `↑` as mix and `⊃` as first. The dyads (take and pick) agree.
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
44pub enum FirstDisclose {
45    #[default]
46    UpIsFirst,
47    UpIsMix,
48}
49
50/// What `⌷` means.
51///
52/// APL2's `⌷` indexes with one scalar per axis and has no monadic case.
53/// The other line reads the left argument as a list of index vectors, one
54/// per axis, and gives `⌷` a monadic meaning as well.
55#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
56pub enum IndexForm {
57    #[default]
58    ScalarPerAxis,
59    AxisVectors,
60}
61
62/// What a dyadic `⊂` does.
63///
64/// The APL2 line reads the left argument as partition flags: a partition
65/// begins where the flags rise, and a zero drops its item. The other line
66/// reads them as counts — each item says how many partitions to begin
67/// before it, so a count above one leaves empty partitions behind — and
68/// spells the flag reading `⊆`. Both lines agree about `⊆`.
69#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
70pub enum Partition {
71    #[default]
72    Flags,
73    Counts,
74}
75
76/// What monadic `≡` answers for an array whose items differ in depth.
77///
78/// Both lines answer with the depth. The other one negates it where the
79/// array is not uniform: where two items of it, at any level, differ in
80/// depth or in shape.
81#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
82pub enum DepthSign {
83    #[default]
84    Unsigned,
85    Signed,
86}
87
88/// Which sentence of a dfn body is its result.
89///
90/// libjay's block model — the value of the last sentence — is what both
91/// languages' sequences do. The other reading stops at the first sentence
92/// that is not an assignment and answers with its value.
93#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
94pub enum DfnResult {
95    #[default]
96    LastSentence,
97    FirstNonAssignment,
98}
99
100/// When `⍺←v` evaluates `v`.
101///
102/// Eagerly: the sentence runs and the value is dropped where the left
103/// argument already arrived. Lazily: the sentence does not run at all
104/// then, which is observable when it has an effect or would fail.
105#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
106pub enum DefaultArg {
107    #[default]
108    Eager,
109    Lazy,
110}
111
112/// How a grade orders complex values.
113///
114/// Ordering verbs refuse complex operands in either reading — a grade is a
115/// permutation, not a claim about size — but a grade still has to be
116/// total. By real part then imaginary is one reading; by magnitude then
117/// angle is the other.
118#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
119pub enum ComplexOrder {
120    #[default]
121    RealThenImaginary,
122    MagnitudeThenAngle,
123}
124
125/// How a grade orders NESTED items.
126///
127/// The APL2 line, which GNU APL implements and the oracle verifies, orders
128/// two items by rank, then by shape, then atom by atom with characters
129/// before numbers before nested values. Dyalog's total array ordering is a
130/// different comparator throughout: it compares the atoms first, padding
131/// the shorter array with an item below every type, extends a lower rank
132/// with leading 1s, and orders numbers before characters.
133#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
134pub enum NestedGrade {
135    #[default]
136    Apl2,
137    TotalOrder,
138}
139
140/// What dyadic `⍳` takes on its left.
141///
142/// The APL2/ISO line, which GNU APL implements, looks a cell up among the
143/// items of a left argument of any rank, so `(2 3⍴⍳6)⍳5` answers and a
144/// scalar left argument is a one-item table. Dyalog takes a vector alone
145/// and gives a RANK ERROR for anything else, scalars included.
146#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
147pub enum LookupLeft {
148    #[default]
149    AnyRank,
150    VectorOnly,
151}
152
153/// Which line's `∨` and `∧` these are.
154///
155/// GNU APL's GCD reads three things loosely, all probed against it: a zero
156/// argument hands its partner back with the sign (`¯3∨0` is `¯3`, though
157/// `¯3.5∨0` is `3.5` — only whole numbers keep it); an argument within
158/// `⎕CT` of a whole number is that number (`1.0000000000001∧5` is 5); and
159/// one no larger than `⎕CT` beside the other is zero (`1E¯14∨1` is 1).
160/// Dyalog does none of the three, and neither does J: `¯3∨0` is 3 there,
161/// `1E¯14∨1` is `1E¯14`, and `1.0000000000001∧5` grinds out `1.0008E13`.
162#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
163pub enum GcdRule {
164    /// GNU APL: whole-number sign kept, near-whole and vanishing arguments
165    /// rounded first.
166    #[default]
167    Tolerant,
168    /// Dyalog and J: the magnitude, and the values as they stand.
169    Exact,
170}
171
172/// How a float that is merely NEAR a whole number is admitted where a
173/// count, a length or an index belongs (`⍳2+9E¯11`, `(2+9E¯11)⍴5`).
174///
175/// This is not the comparison tolerance — `(2+9E¯11)=2` is 0 under both
176/// readings — and the two APL lines part company over it. GNU APL takes an
177/// absolute `1E¯10` at every magnitude, so a large count buys no room and
178/// `1E¯11` reads as 0. Dyalog's window is relative and follows `⎕CT`, so
179/// `⍴⍳1000000+1E¯9` answers there and is refused here, while every
180/// `2±9E¯11` case is the other way about. Neither is a superset.
181#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
182pub enum NearCount {
183    /// GNU APL: an absolute `1E¯10`, whatever the magnitude.
184    #[default]
185    Absolute,
186    /// Dyalog: the dialect's own tolerant equality against the whole
187    /// number, so `⎕CT` moves it and zero admits nothing.
188    Tolerant,
189}
190
191/// How `⌊` and `⌈` read a value that is merely near the integer above or
192/// below it.
193///
194/// GNU APL shifts by `⎕CT` outright, so `⌊99.999999999995` is 99 — a gap
195/// of 5E¯12 is larger than the tolerance however big the value is — while
196/// `⌊¯1E¯13` is 0. Dyalog scales the shift by the magnitude but never
197/// below 1, so `⌊9.9999999999999` is 10 and `⌊¯1E¯13` is `¯1`.
198#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
199pub enum FloorRule {
200    /// GNU APL: `⌊y+⎕CT`, an absolute shift.
201    #[default]
202    Shift,
203    /// Dyalog: `⌊y+⎕CT×1⌈|y`, a shift that grows with the magnitude.
204    Scaled,
205}
206
207/// Whether `⊤` reads its digits tolerantly.
208///
209/// GNU APL takes each digit with the same tolerant residue `|` uses, so
210/// `2 2⊤4-1E¯14` is `0 0`. Dyalog takes them exactly, and the difference
211/// survives into the digits: the same sentence is `1 2` there, the last
212/// digit being 1.99999999999999 rather than a rounded 0.
213#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
214pub enum EncodeDigits {
215    /// GNU APL: the digits are tolerant residues.
216    #[default]
217    Tolerant,
218    /// Dyalog: the digits are exact residues, `⎕CT` unread.
219    Exact,
220}
221
222/// How strictly a control structure reads what it is given.
223///
224/// The lenient reading is the one both languages ship: a condition is true
225/// where its first atom is, whatever else it holds, and a `:Leave` outside
226/// a loop leaves the definition. Dyalog reads both strictly — a condition
227/// is one element and no more, and `:Leave` belongs to a loop — and says so
228/// rather than answering. GNU APL has no control structures at all, so
229/// nothing it records turns on this.
230#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
231pub enum ControlStrictness {
232    /// The first atom decides, and a stray `:Leave` is a return.
233    #[default]
234    Lenient,
235    /// Dyalog: a condition is a single value, and `:Leave` needs its loop.
236    Strict,
237}
238
239/// Where the each in the inner product's definition sits.
240///
241/// `f.g` is a fold over a pairing, and the two lines put the each on
242/// different halves of it. GNU APL puts it on the FOLD — `f/¨ (⊂[last]x)
243/// ∘.g (⊂[first]y)` — so `g` meets one whole vector from each side and what
244/// the fold makes of a pair is enclosed once more. Dyalog puts it on the
245/// PAIRING — `f/ row g¨ column` — so `g` meets one element from each side
246/// and the fold's own value stands as the cell. `1 2+.,3 4` is `10` under
247/// the first and an enclosed `3 7` under the second. The two agree wherever
248/// `g` is a scalar function and the fold ends in a number, which is every
249/// published use, so `+.×` and the Life idiom's `∨.∧` differ only in depth.
250#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
251pub enum InnerEach {
252    /// GNU APL: `f/¨` over the outer product.
253    #[default]
254    OnFold,
255    /// Dyalog: `f/` over `g¨`.
256    OnPair,
257}
258
259/// Dialect settings supplied by the host.
260///
261/// This is what a host asks for; [`Rules`] is what the compiler and the
262/// engine read. Every field's default is the setting libjay implements, so
263/// `Dialect::default()` is the language as it ships and a host that names
264/// no setting gets exactly that. `Option` fields mean "the language
265/// default", which differs between J and APL.
266///
267/// The enum fields are the points where the APL lineages diverge. libjay
268/// implements the APL2/ISO line that GNU APL embodies; the other arm of
269/// each is refused by [`Dialect::rules`] as not implemented yet, so
270/// selecting it is honest rather than silently wrong. `trains` is the
271/// exception: both of its readings are implemented, so it is a choice.
272#[derive(Clone, Copy, Debug, PartialEq)]
273pub struct Dialect {
274    /// APL `⎕IO`. J's index origin is 0 and is not configurable.
275    pub index_origin: Option<i64>,
276    /// APL `⎕CT`, J `9!:18`: the relative comparison tolerance.
277    pub comparison_tolerance: Option<f64>,
278    pub nested_model: NestedModel,
279    pub first_disclose: FirstDisclose,
280    pub index_form: IndexForm,
281    pub partition: Partition,
282    pub depth_sign: DepthSign,
283    pub dfn_result: DfnResult,
284    pub default_arg: DefaultArg,
285    pub complex_order: ComplexOrder,
286    pub nested_grade: NestedGrade,
287    pub lookup_left: LookupLeft,
288    pub gcd_rule: GcdRule,
289    pub near_count: NearCount,
290    pub floor_rule: FloorRule,
291    pub encode_digits: EncodeDigits,
292    pub inner_each: InnerEach,
293    pub control_strictness: ControlStrictness,
294    /// Whether a function may stand where a value belongs: a run of
295    /// functions is then a train, and `F←+/` names one. Both readings are
296    /// implemented, so this is a choice and not a gap. It ships on, as an
297    /// extension: GNU APL refuses both spellings, and refusing a feature
298    /// the oracle merely lacks serves nobody.
299    pub trains: bool,
300}
301
302impl Default for Dialect {
303    fn default() -> Dialect {
304        Dialect::gnu_apl()
305    }
306}
307
308impl Dialect {
309    /// The APL libjay implements: the APL2/ISO line GNU APL embodies and
310    /// the oracle verifies, plus the extensions listed in
311    /// `docs/coverage.md`. Written out rather than derived, so that every
312    /// setting's shipped value is stated in one place; it is equal to
313    /// `Dialect::default()`, which the tests pin.
314    pub fn gnu_apl() -> Dialect {
315        Dialect {
316            index_origin: None,
317            comparison_tolerance: None,
318            nested_model: NestedModel::Floating,
319            first_disclose: FirstDisclose::UpIsFirst,
320            index_form: IndexForm::ScalarPerAxis,
321            partition: Partition::Flags,
322            depth_sign: DepthSign::Unsigned,
323            dfn_result: DfnResult::LastSentence,
324            default_arg: DefaultArg::Eager,
325            complex_order: ComplexOrder::RealThenImaginary,
326            nested_grade: NestedGrade::Apl2,
327            lookup_left: LookupLeft::AnyRank,
328            gcd_rule: GcdRule::Tolerant,
329            near_count: NearCount::Absolute,
330            floor_rule: FloorRule::Shift,
331            encode_digits: EncodeDigits::Tolerant,
332            inner_each: InnerEach::OnFold,
333            control_strictness: ControlStrictness::Lenient,
334            trains: true,
335        }
336    }
337
338    /// The Dyalog line, as far as libjay implements it.
339    ///
340    /// Every setting here is one the recorded Dyalog answers verify
341    /// (`docs/testing.md`); the settings left at the GNU/APL2 reading are
342    /// the ones libjay has not derived from a Dyalog answer yet, and
343    /// `docs/coverage.md` lists what that still costs. `⎕ML` is Dyalog's
344    /// own default, 1, which is what the recording ran under: `↑` mixes
345    /// and `⊃` takes the first.
346    pub fn dyalog() -> Dialect {
347        Dialect {
348            index_origin: None,
349            comparison_tolerance: Some(1e-14),
350            nested_model: NestedModel::Floating,
351            first_disclose: FirstDisclose::UpIsMix,
352            index_form: IndexForm::AxisVectors,
353            partition: Partition::Counts,
354            depth_sign: DepthSign::Signed,
355            dfn_result: DfnResult::FirstNonAssignment,
356            default_arg: DefaultArg::Eager,
357            complex_order: ComplexOrder::RealThenImaginary,
358            nested_grade: NestedGrade::TotalOrder,
359            lookup_left: LookupLeft::VectorOnly,
360            gcd_rule: GcdRule::Exact,
361            near_count: NearCount::Tolerant,
362            floor_rule: FloorRule::Scaled,
363            encode_digits: EncodeDigits::Exact,
364            inner_each: InnerEach::OnPair,
365            control_strictness: ControlStrictness::Strict,
366            trains: true,
367        }
368    }
369
370    /// J. Nothing in J is a dialect setting yet beyond the comparison
371    /// tolerance, and the APL settings are not read under `Lang::J`, so
372    /// J's dialect is the empty one.
373    pub fn j() -> Dialect {
374        Dialect::default()
375    }
376
377    /// Resolve to the settings the compiler and the engine read.
378    ///
379    /// This is the one place a dialect choice is made. A setting whose
380    /// other arm libjay does not implement is refused here, by name, so
381    /// that a host selecting it is told rather than quietly given this
382    /// dialect's answer.
383    pub fn rules(&self, lang: Lang) -> Result<Rules> {
384        // A setting is the host's, not the source text's, so these carry
385        // no span: there is nothing in the program to point at.
386        let refuse = |what: &str| -> Error {
387            Error::new(
388                ErrorKind::NotYet,
389                format!("{what} (the reading of another APL dialect) is not supported yet"),
390                None,
391            )
392            .note("libjay implements the APL2/ISO line, which is the one its oracle verifies")
393        };
394        if let Some(ct) = self.comparison_tolerance && !(ct.is_finite() && ct >= 0.0) {
395            return Err(Error::new(
396                ErrorKind::Domain,
397                "the comparison tolerance must be a finite value at or above zero",
398                None,
399            ));
400        }
401        match self.nested_model {
402            NestedModel::Floating => {}
403            NestedModel::Grounded => return Err(refuse("a grounded nested array model")),
404        }
405        match self.default_arg {
406            DefaultArg::Eager => {}
407            DefaultArg::Lazy => return Err(refuse("a lazy ⍺← default")),
408        }
409        match self.complex_order {
410            ComplexOrder::RealThenImaginary => {}
411            ComplexOrder::MagnitudeThenAngle => {
412                return Err(refuse("grading complex values by magnitude and angle"))
413            }
414        }
415        let origin = match lang {
416            Lang::J => 0,
417            Lang::Apl => self.index_origin.unwrap_or(1),
418        };
419        let ct = self.comparison_tolerance.unwrap_or(match lang {
420            Lang::J => Tol::J.ct,
421            Lang::Apl => Tol::APL.ct,
422        });
423        Ok(Rules {
424            lang,
425            origin,
426            ct,
427            nested_model: self.nested_model,
428            first_disclose: self.first_disclose,
429            index_form: self.index_form,
430            partition: self.partition,
431            depth_sign: self.depth_sign,
432            dfn_result: self.dfn_result,
433            default_arg: self.default_arg,
434            complex_order: self.complex_order,
435            nested_grade: self.nested_grade,
436            lookup_left: self.lookup_left,
437            gcd_rule: self.gcd_rule,
438            near_count: self.near_count,
439            floor_rule: self.floor_rule,
440            encode_digits: self.encode_digits,
441            inner_each: self.inner_each,
442            control_strictness: self.control_strictness,
443            trains: self.trains,
444        })
445    }
446}
447
448/// A dialect resolved against a language: what the parser and the engine
449/// read. Copyable, and carried by every evaluation context, so a rule that
450/// only bites at run time (the index origin a key answers with, the order
451/// a grade puts complex values in) reads the same setting the parser did.
452#[derive(Clone, Copy, Debug, PartialEq)]
453pub struct Rules {
454    pub lang: Lang,
455    /// The index origin in force: APL's `⎕IO`, and 0 for J.
456    pub origin: i64,
457    /// The comparison tolerance in force. `Rules::tol` pairs it with the
458    /// language's scaling rule; a verb-local `u!.n` overrides that copy
459    /// and not this one.
460    pub ct: f64,
461    pub nested_model: NestedModel,
462    pub first_disclose: FirstDisclose,
463    pub index_form: IndexForm,
464    pub partition: Partition,
465    pub depth_sign: DepthSign,
466    pub dfn_result: DfnResult,
467    pub default_arg: DefaultArg,
468    pub complex_order: ComplexOrder,
469    pub nested_grade: NestedGrade,
470    pub lookup_left: LookupLeft,
471    pub gcd_rule: GcdRule,
472    pub near_count: NearCount,
473    pub floor_rule: FloorRule,
474    pub encode_digits: EncodeDigits,
475    pub inner_each: InnerEach,
476    pub control_strictness: ControlStrictness,
477    pub trains: bool,
478}
479
480impl Rules {
481    /// The dialect's comparison tolerance, with the language's scale.
482    pub fn tol(&self) -> Tol {
483        Tol { ct: self.ct, by_smaller: self.lang == Lang::J, floor_rule: self.floor_rule }
484    }
485
486    /// The host-facing form, for a nested compilation (`⍎`, `".`) that has
487    /// to run under the same dialect as the program executing it.
488    pub fn dialect(&self) -> Dialect {
489        Dialect {
490            index_origin: Some(self.origin),
491            comparison_tolerance: Some(self.ct),
492            nested_model: self.nested_model,
493            first_disclose: self.first_disclose,
494            index_form: self.index_form,
495            partition: self.partition,
496            depth_sign: self.depth_sign,
497            dfn_result: self.dfn_result,
498            default_arg: self.default_arg,
499            complex_order: self.complex_order,
500            nested_grade: self.nested_grade,
501            lookup_left: self.lookup_left,
502            gcd_rule: self.gcd_rule,
503            near_count: self.near_count,
504            floor_rule: self.floor_rule,
505            encode_digits: self.encode_digits,
506            inner_each: self.inner_each,
507            control_strictness: self.control_strictness,
508            trains: self.trains,
509        }
510    }
511}
512
513impl Default for Rules {
514    /// J's rules, which is what a context built without a program uses.
515    fn default() -> Rules {
516        Dialect::default().rules(Lang::J).expect("J's defaults are implemented")
517    }
518}
519
520/// A source text with interpolation holes split out. Spans in every token
521/// and error refer to `display`, where hole `i` reads `{name_i}`.
522#[derive(Clone, Debug)]
523pub struct SourceParts {
524    pub display: String,
525    pub segments: Vec<Segment>,
526    pub param_names: Vec<String>,
527}
528
529#[derive(Clone, Debug)]
530pub enum Segment {
531    /// Literal source text starting at `offset` in `display`.
532    Text { text: String, offset: usize },
533    /// Interpolation hole: parameter `index`, shown as `{name}` in `display`.
534    Param { index: usize, offset: usize, len: usize },
535}
536
537impl SourceParts {
538    /// Build from pre-split literal parts with holes between them
539    /// (the t-string path). `names[i]` sits between `parts[i]` and
540    /// `parts[i+1]`; repeated names share one parameter.
541    pub fn from_parts(parts: &[&str], names: &[&str]) -> SourceParts {
542        assert_eq!(parts.len(), names.len() + 1, "N parts need N-1 holes");
543        let mut display = String::new();
544        let mut segments = Vec::new();
545        let mut param_names: Vec<String> = Vec::new();
546        for (i, part) in parts.iter().enumerate() {
547            if !part.is_empty() {
548                segments.push(Segment::Text { text: (*part).to_string(), offset: display.len() });
549                display.push_str(part);
550            }
551            if i < names.len() {
552                let name = names[i];
553                let index = param_names
554                    .iter()
555                    .position(|n| n == name)
556                    .unwrap_or_else(|| {
557                        param_names.push(name.to_string());
558                        param_names.len() - 1
559                    });
560                let shown = format!("{{{name}}}");
561                segments.push(Segment::Param { index, offset: display.len(), len: shown.len() });
562                display.push_str(&shown);
563            }
564        }
565        SourceParts { display, segments, param_names }
566    }
567
568    /// Build from a plain string where `{identifier}` outside quotes is an
569    /// interpolation hole (the pre-3.14 and Rust runtime path).
570    pub fn from_source(src: &str) -> Result<SourceParts> {
571        let bytes = src.as_bytes();
572        let mut parts: Vec<String> = vec![String::new()];
573        let mut names: Vec<String> = Vec::new();
574        let mut in_quote = false;
575        let mut i = 0;
576        while i < src.len() {
577            let ch = src[i..].chars().next().unwrap();
578            if ch == '\'' {
579                in_quote = !in_quote;
580                parts.last_mut().unwrap().push(ch);
581                i += 1;
582                continue;
583            }
584            if ch == '{' && !in_quote {
585                // Exactly `{identifier}` is an interpolation hole. Any other
586                // `{` is literal program text: J spells take as `{.`, drop as
587                // `}.`, so the brace itself belongs to the language.
588                let rest = &src[i + 1..];
589                if let Some(end) = rest.find('}') {
590                    let name = &rest[..end];
591                    if is_identifier(name) {
592                        names.push(name.to_string());
593                        parts.push(String::new());
594                        i += 2 + end;
595                        continue;
596                    }
597                }
598            }
599            parts.last_mut().unwrap().push(ch);
600            i += ch.len_utf8();
601        }
602        let _ = bytes;
603        let part_refs: Vec<&str> = parts.iter().map(|s| s.as_str()).collect();
604        let name_refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
605        Ok(SourceParts::from_parts(&part_refs, &name_refs))
606    }
607}
608
609fn is_identifier(s: &str) -> bool {
610    let mut chars = s.chars();
611    match chars.next() {
612        Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
613        _ => return false,
614    }
615    chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
616}
617
618/// Compile a plain source string (with `{name}` holes) in the given language.
619pub fn compile(lang: Lang, source: &str, dialect: &Dialect) -> Result<Program> {
620    let sp = SourceParts::from_source(source)?;
621    compile_source_parts(lang, sp, dialect)
622}
623
624/// Compile pre-split parts (the t-string path).
625pub fn compile_parts(
626    lang: Lang,
627    parts: &[&str],
628    names: &[&str],
629    dialect: &Dialect,
630) -> Result<Program> {
631    compile_source_parts(lang, SourceParts::from_parts(parts, names), dialect)
632}
633
634fn compile_source_parts(lang: Lang, sp: SourceParts, dialect: &Dialect) -> Result<Program> {
635    let rules = dialect.rules(lang)?;
636    let tol = rules.tol();
637    let (mut stmts, agreement, fmt) = match lang {
638        Lang::J => (j::parse(&sp)?, Agreement::LeadingPrefix, FmtOpts::J),
639        Lang::Apl => (apl::parse(&sp, rules)?, Agreement::ExactOrScalar, FmtOpts::APL),
640    };
641    // Everything after this point walks the tree recursively, so a
642    // sentence nested past what a stack holds is refused here rather than
643    // taking the process down. The measurement itself does not recurse.
644    for stmt in &stmts {
645        crate::verb::check_nesting(stmt.depth(), stmt.span())?;
646    }
647    crate::fuse::pass(&mut stmts, tol);
648    let params = sp.param_names.into_iter().map(|name| ParamSpec { name }).collect();
649    Ok(Program { stmts, params, display_src: sp.display, agreement, fmt, rules })
650}