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/// Dialect settings supplied by the host.
223///
224/// This is what a host asks for; [`Rules`] is what the compiler and the
225/// engine read. Every field's default is the setting libjay implements, so
226/// `Dialect::default()` is the language as it ships and a host that names
227/// no setting gets exactly that. `Option` fields mean "the language
228/// default", which differs between J and APL.
229///
230/// The enum fields are the points where the APL lineages diverge. libjay
231/// implements the APL2/ISO line that GNU APL embodies; the other arm of
232/// each is refused by [`Dialect::rules`] as not implemented yet, so
233/// selecting it is honest rather than silently wrong. `trains` is the
234/// exception: both of its readings are implemented, so it is a choice.
235#[derive(Clone, Copy, Debug, PartialEq)]
236pub struct Dialect {
237    /// APL `⎕IO`. J's index origin is 0 and is not configurable.
238    pub index_origin: Option<i64>,
239    /// APL `⎕CT`, J `9!:18`: the relative comparison tolerance.
240    pub comparison_tolerance: Option<f64>,
241    pub nested_model: NestedModel,
242    pub first_disclose: FirstDisclose,
243    pub index_form: IndexForm,
244    pub partition: Partition,
245    pub depth_sign: DepthSign,
246    pub dfn_result: DfnResult,
247    pub default_arg: DefaultArg,
248    pub complex_order: ComplexOrder,
249    pub nested_grade: NestedGrade,
250    pub lookup_left: LookupLeft,
251    pub gcd_rule: GcdRule,
252    pub near_count: NearCount,
253    pub floor_rule: FloorRule,
254    pub encode_digits: EncodeDigits,
255    /// Whether a function may stand where a value belongs: a run of
256    /// functions is then a train, and `F←+/` names one. Both readings are
257    /// implemented, so this is a choice and not a gap. It ships on, as an
258    /// extension: GNU APL refuses both spellings, and refusing a feature
259    /// the oracle merely lacks serves nobody.
260    pub trains: bool,
261}
262
263impl Default for Dialect {
264    fn default() -> Dialect {
265        Dialect::gnu_apl()
266    }
267}
268
269impl Dialect {
270    /// The APL libjay implements: the APL2/ISO line GNU APL embodies and
271    /// the oracle verifies, plus the extensions listed in
272    /// `docs/coverage.md`. Written out rather than derived, so that every
273    /// setting's shipped value is stated in one place; it is equal to
274    /// `Dialect::default()`, which the tests pin.
275    pub fn gnu_apl() -> Dialect {
276        Dialect {
277            index_origin: None,
278            comparison_tolerance: None,
279            nested_model: NestedModel::Floating,
280            first_disclose: FirstDisclose::UpIsFirst,
281            index_form: IndexForm::ScalarPerAxis,
282            partition: Partition::Flags,
283            depth_sign: DepthSign::Unsigned,
284            dfn_result: DfnResult::LastSentence,
285            default_arg: DefaultArg::Eager,
286            complex_order: ComplexOrder::RealThenImaginary,
287            nested_grade: NestedGrade::Apl2,
288            lookup_left: LookupLeft::AnyRank,
289            gcd_rule: GcdRule::Tolerant,
290            near_count: NearCount::Absolute,
291            floor_rule: FloorRule::Shift,
292            encode_digits: EncodeDigits::Tolerant,
293            trains: true,
294        }
295    }
296
297    /// The Dyalog line, as far as libjay implements it.
298    ///
299    /// Every setting here is one the recorded Dyalog answers verify
300    /// (`docs/testing.md`); the settings left at the GNU/APL2 reading are
301    /// the ones libjay has not derived from a Dyalog answer yet, and
302    /// `docs/coverage.md` lists what that still costs. `⎕ML` is Dyalog's
303    /// own default, 1, which is what the recording ran under: `↑` mixes
304    /// and `⊃` takes the first.
305    pub fn dyalog() -> Dialect {
306        Dialect {
307            index_origin: None,
308            comparison_tolerance: Some(1e-14),
309            nested_model: NestedModel::Floating,
310            first_disclose: FirstDisclose::UpIsMix,
311            index_form: IndexForm::AxisVectors,
312            partition: Partition::Counts,
313            depth_sign: DepthSign::Signed,
314            dfn_result: DfnResult::FirstNonAssignment,
315            default_arg: DefaultArg::Eager,
316            complex_order: ComplexOrder::RealThenImaginary,
317            nested_grade: NestedGrade::TotalOrder,
318            lookup_left: LookupLeft::VectorOnly,
319            gcd_rule: GcdRule::Exact,
320            near_count: NearCount::Tolerant,
321            floor_rule: FloorRule::Scaled,
322            encode_digits: EncodeDigits::Exact,
323            trains: true,
324        }
325    }
326
327    /// J. Nothing in J is a dialect setting yet beyond the comparison
328    /// tolerance, and the APL settings are not read under `Lang::J`, so
329    /// J's dialect is the empty one.
330    pub fn j() -> Dialect {
331        Dialect::default()
332    }
333
334    /// Resolve to the settings the compiler and the engine read.
335    ///
336    /// This is the one place a dialect choice is made. A setting whose
337    /// other arm libjay does not implement is refused here, by name, so
338    /// that a host selecting it is told rather than quietly given this
339    /// dialect's answer.
340    pub fn rules(&self, lang: Lang) -> Result<Rules> {
341        // A setting is the host's, not the source text's, so these carry
342        // no span: there is nothing in the program to point at.
343        let refuse = |what: &str| -> Error {
344            Error::new(
345                ErrorKind::NotYet,
346                format!("{what} (the reading of another APL dialect) is not supported yet"),
347                None,
348            )
349            .note("libjay implements the APL2/ISO line, which is the one its oracle verifies")
350        };
351        if let Some(ct) = self.comparison_tolerance && !(ct.is_finite() && ct >= 0.0) {
352            return Err(Error::new(
353                ErrorKind::Domain,
354                "the comparison tolerance must be a finite value at or above zero",
355                None,
356            ));
357        }
358        match self.nested_model {
359            NestedModel::Floating => {}
360            NestedModel::Grounded => return Err(refuse("a grounded nested array model")),
361        }
362        match self.default_arg {
363            DefaultArg::Eager => {}
364            DefaultArg::Lazy => return Err(refuse("a lazy ⍺← default")),
365        }
366        match self.complex_order {
367            ComplexOrder::RealThenImaginary => {}
368            ComplexOrder::MagnitudeThenAngle => {
369                return Err(refuse("grading complex values by magnitude and angle"))
370            }
371        }
372        let origin = match lang {
373            Lang::J => 0,
374            Lang::Apl => self.index_origin.unwrap_or(1),
375        };
376        let ct = self.comparison_tolerance.unwrap_or(match lang {
377            Lang::J => Tol::J.ct,
378            Lang::Apl => Tol::APL.ct,
379        });
380        Ok(Rules {
381            lang,
382            origin,
383            ct,
384            nested_model: self.nested_model,
385            first_disclose: self.first_disclose,
386            index_form: self.index_form,
387            partition: self.partition,
388            depth_sign: self.depth_sign,
389            dfn_result: self.dfn_result,
390            default_arg: self.default_arg,
391            complex_order: self.complex_order,
392            nested_grade: self.nested_grade,
393            lookup_left: self.lookup_left,
394            gcd_rule: self.gcd_rule,
395            near_count: self.near_count,
396            floor_rule: self.floor_rule,
397            encode_digits: self.encode_digits,
398            trains: self.trains,
399        })
400    }
401}
402
403/// A dialect resolved against a language: what the parser and the engine
404/// read. Copyable, and carried by every evaluation context, so a rule that
405/// only bites at run time (the index origin a key answers with, the order
406/// a grade puts complex values in) reads the same setting the parser did.
407#[derive(Clone, Copy, Debug, PartialEq)]
408pub struct Rules {
409    pub lang: Lang,
410    /// The index origin in force: APL's `⎕IO`, and 0 for J.
411    pub origin: i64,
412    /// The comparison tolerance in force. `Rules::tol` pairs it with the
413    /// language's scaling rule; a verb-local `u!.n` overrides that copy
414    /// and not this one.
415    pub ct: f64,
416    pub nested_model: NestedModel,
417    pub first_disclose: FirstDisclose,
418    pub index_form: IndexForm,
419    pub partition: Partition,
420    pub depth_sign: DepthSign,
421    pub dfn_result: DfnResult,
422    pub default_arg: DefaultArg,
423    pub complex_order: ComplexOrder,
424    pub nested_grade: NestedGrade,
425    pub lookup_left: LookupLeft,
426    pub gcd_rule: GcdRule,
427    pub near_count: NearCount,
428    pub floor_rule: FloorRule,
429    pub encode_digits: EncodeDigits,
430    pub trains: bool,
431}
432
433impl Rules {
434    /// The dialect's comparison tolerance, with the language's scale.
435    pub fn tol(&self) -> Tol {
436        Tol { ct: self.ct, by_smaller: self.lang == Lang::J, floor_rule: self.floor_rule }
437    }
438
439    /// The host-facing form, for a nested compilation (`⍎`, `".`) that has
440    /// to run under the same dialect as the program executing it.
441    pub fn dialect(&self) -> Dialect {
442        Dialect {
443            index_origin: Some(self.origin),
444            comparison_tolerance: Some(self.ct),
445            nested_model: self.nested_model,
446            first_disclose: self.first_disclose,
447            index_form: self.index_form,
448            partition: self.partition,
449            depth_sign: self.depth_sign,
450            dfn_result: self.dfn_result,
451            default_arg: self.default_arg,
452            complex_order: self.complex_order,
453            nested_grade: self.nested_grade,
454            lookup_left: self.lookup_left,
455            gcd_rule: self.gcd_rule,
456            near_count: self.near_count,
457            floor_rule: self.floor_rule,
458            encode_digits: self.encode_digits,
459            trains: self.trains,
460        }
461    }
462}
463
464impl Default for Rules {
465    /// J's rules, which is what a context built without a program uses.
466    fn default() -> Rules {
467        Dialect::default().rules(Lang::J).expect("J's defaults are implemented")
468    }
469}
470
471/// A source text with interpolation holes split out. Spans in every token
472/// and error refer to `display`, where hole `i` reads `{name_i}`.
473#[derive(Clone, Debug)]
474pub struct SourceParts {
475    pub display: String,
476    pub segments: Vec<Segment>,
477    pub param_names: Vec<String>,
478}
479
480#[derive(Clone, Debug)]
481pub enum Segment {
482    /// Literal source text starting at `offset` in `display`.
483    Text { text: String, offset: usize },
484    /// Interpolation hole: parameter `index`, shown as `{name}` in `display`.
485    Param { index: usize, offset: usize, len: usize },
486}
487
488impl SourceParts {
489    /// Build from pre-split literal parts with holes between them
490    /// (the t-string path). `names[i]` sits between `parts[i]` and
491    /// `parts[i+1]`; repeated names share one parameter.
492    pub fn from_parts(parts: &[&str], names: &[&str]) -> SourceParts {
493        assert_eq!(parts.len(), names.len() + 1, "N parts need N-1 holes");
494        let mut display = String::new();
495        let mut segments = Vec::new();
496        let mut param_names: Vec<String> = Vec::new();
497        for (i, part) in parts.iter().enumerate() {
498            if !part.is_empty() {
499                segments.push(Segment::Text { text: (*part).to_string(), offset: display.len() });
500                display.push_str(part);
501            }
502            if i < names.len() {
503                let name = names[i];
504                let index = param_names
505                    .iter()
506                    .position(|n| n == name)
507                    .unwrap_or_else(|| {
508                        param_names.push(name.to_string());
509                        param_names.len() - 1
510                    });
511                let shown = format!("{{{name}}}");
512                segments.push(Segment::Param { index, offset: display.len(), len: shown.len() });
513                display.push_str(&shown);
514            }
515        }
516        SourceParts { display, segments, param_names }
517    }
518
519    /// Build from a plain string where `{identifier}` outside quotes is an
520    /// interpolation hole (the pre-3.14 and Rust runtime path).
521    pub fn from_source(src: &str) -> Result<SourceParts> {
522        let bytes = src.as_bytes();
523        let mut parts: Vec<String> = vec![String::new()];
524        let mut names: Vec<String> = Vec::new();
525        let mut in_quote = false;
526        let mut i = 0;
527        while i < src.len() {
528            let ch = src[i..].chars().next().unwrap();
529            if ch == '\'' {
530                in_quote = !in_quote;
531                parts.last_mut().unwrap().push(ch);
532                i += 1;
533                continue;
534            }
535            if ch == '{' && !in_quote {
536                // Exactly `{identifier}` is an interpolation hole. Any other
537                // `{` is literal program text: J spells take as `{.`, drop as
538                // `}.`, so the brace itself belongs to the language.
539                let rest = &src[i + 1..];
540                if let Some(end) = rest.find('}') {
541                    let name = &rest[..end];
542                    if is_identifier(name) {
543                        names.push(name.to_string());
544                        parts.push(String::new());
545                        i += 2 + end;
546                        continue;
547                    }
548                }
549            }
550            parts.last_mut().unwrap().push(ch);
551            i += ch.len_utf8();
552        }
553        let _ = bytes;
554        let part_refs: Vec<&str> = parts.iter().map(|s| s.as_str()).collect();
555        let name_refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
556        Ok(SourceParts::from_parts(&part_refs, &name_refs))
557    }
558}
559
560fn is_identifier(s: &str) -> bool {
561    let mut chars = s.chars();
562    match chars.next() {
563        Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
564        _ => return false,
565    }
566    chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
567}
568
569/// Compile a plain source string (with `{name}` holes) in the given language.
570pub fn compile(lang: Lang, source: &str, dialect: &Dialect) -> Result<Program> {
571    let sp = SourceParts::from_source(source)?;
572    compile_source_parts(lang, sp, dialect)
573}
574
575/// Compile pre-split parts (the t-string path).
576pub fn compile_parts(
577    lang: Lang,
578    parts: &[&str],
579    names: &[&str],
580    dialect: &Dialect,
581) -> Result<Program> {
582    compile_source_parts(lang, SourceParts::from_parts(parts, names), dialect)
583}
584
585fn compile_source_parts(lang: Lang, sp: SourceParts, dialect: &Dialect) -> Result<Program> {
586    let rules = dialect.rules(lang)?;
587    let tol = rules.tol();
588    let (mut stmts, agreement, fmt) = match lang {
589        Lang::J => (j::parse(&sp)?, Agreement::LeadingPrefix, FmtOpts::J),
590        Lang::Apl => (apl::parse(&sp, rules)?, Agreement::ExactOrScalar, FmtOpts::APL),
591    };
592    // Everything after this point walks the tree recursively, so a
593    // sentence nested past what a stack holds is refused here rather than
594    // taking the process down. The measurement itself does not recurse.
595    for stmt in &stmts {
596        crate::verb::check_nesting(stmt.depth(), stmt.span())?;
597    }
598    crate::fuse::pass(&mut stmts, tol);
599    let params = sp.param_names.into_iter().map(|name| ParamSpec { name }).collect();
600    Ok(Program { stmts, params, display_src: sp.display, agreement, fmt, rules })
601}