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/// Which sentence of a dfn body is its result.
63///
64/// libjay's block model — the value of the last sentence — is what both
65/// languages' sequences do. The other reading stops at the first sentence
66/// that is not an assignment and answers with its value.
67#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
68pub enum DfnResult {
69    #[default]
70    LastSentence,
71    FirstNonAssignment,
72}
73
74/// When `⍺←v` evaluates `v`.
75///
76/// Eagerly: the sentence runs and the value is dropped where the left
77/// argument already arrived. Lazily: the sentence does not run at all
78/// then, which is observable when it has an effect or would fail.
79#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
80pub enum DefaultArg {
81    #[default]
82    Eager,
83    Lazy,
84}
85
86/// How a grade orders complex values.
87///
88/// Ordering verbs refuse complex operands in either reading — a grade is a
89/// permutation, not a claim about size — but a grade still has to be
90/// total. By real part then imaginary is one reading; by magnitude then
91/// angle is the other.
92#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
93pub enum ComplexOrder {
94    #[default]
95    RealThenImaginary,
96    MagnitudeThenAngle,
97}
98
99/// How a grade orders NESTED items.
100///
101/// The APL2 line, which GNU APL implements and the oracle verifies, orders
102/// two items by rank, then by shape, then atom by atom with characters
103/// before numbers before nested values. Dyalog's total array ordering is a
104/// different comparator throughout: it compares the atoms first, padding
105/// the shorter array with an item below every type, extends a lower rank
106/// with leading 1s, and orders numbers before characters.
107#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
108pub enum NestedGrade {
109    #[default]
110    Apl2,
111    TotalOrder,
112}
113
114/// Dialect settings supplied by the host.
115///
116/// This is what a host asks for; [`Rules`] is what the compiler and the
117/// engine read. Every field's default is the setting libjay implements, so
118/// `Dialect::default()` is the language as it ships and a host that names
119/// no setting gets exactly that. `Option` fields mean "the language
120/// default", which differs between J and APL.
121///
122/// The enum fields are the points where the APL lineages diverge. libjay
123/// implements the APL2/ISO line that GNU APL embodies; the other arm of
124/// each is refused by [`Dialect::rules`] as not implemented yet, so
125/// selecting it is honest rather than silently wrong. `trains` is the
126/// exception: both of its readings are implemented, so it is a choice.
127#[derive(Clone, Copy, Debug, PartialEq)]
128pub struct Dialect {
129    /// APL `⎕IO`. J's index origin is 0 and is not configurable.
130    pub index_origin: Option<i64>,
131    /// APL `⎕CT`, J `9!:18`: the relative comparison tolerance.
132    pub comparison_tolerance: Option<f64>,
133    pub nested_model: NestedModel,
134    pub first_disclose: FirstDisclose,
135    pub index_form: IndexForm,
136    pub dfn_result: DfnResult,
137    pub default_arg: DefaultArg,
138    pub complex_order: ComplexOrder,
139    pub nested_grade: NestedGrade,
140    /// Whether a function may stand where a value belongs: a run of
141    /// functions is then a train, and `F←+/` names one. Both readings are
142    /// implemented, so this is a choice and not a gap. It ships on, as an
143    /// extension: GNU APL refuses both spellings, and refusing a feature
144    /// the oracle merely lacks serves nobody.
145    pub trains: bool,
146}
147
148impl Default for Dialect {
149    fn default() -> Dialect {
150        Dialect::gnu_apl()
151    }
152}
153
154impl Dialect {
155    /// The APL libjay implements: the APL2/ISO line GNU APL embodies and
156    /// the oracle verifies, plus the extensions listed in
157    /// `docs/coverage.md`. Written out rather than derived, so that every
158    /// setting's shipped value is stated in one place; it is equal to
159    /// `Dialect::default()`, which the tests pin.
160    pub fn gnu_apl() -> Dialect {
161        Dialect {
162            index_origin: None,
163            comparison_tolerance: None,
164            nested_model: NestedModel::Floating,
165            first_disclose: FirstDisclose::UpIsFirst,
166            index_form: IndexForm::ScalarPerAxis,
167            dfn_result: DfnResult::LastSentence,
168            default_arg: DefaultArg::Eager,
169            complex_order: ComplexOrder::RealThenImaginary,
170            nested_grade: NestedGrade::Apl2,
171            trains: true,
172        }
173    }
174
175    /// J. Nothing in J is a dialect setting yet beyond the comparison
176    /// tolerance, and the APL settings are not read under `Lang::J`, so
177    /// J's dialect is the empty one.
178    pub fn j() -> Dialect {
179        Dialect::default()
180    }
181
182    /// Resolve to the settings the compiler and the engine read.
183    ///
184    /// This is the one place a dialect choice is made. A setting whose
185    /// other arm libjay does not implement is refused here, by name, so
186    /// that a host selecting it is told rather than quietly given this
187    /// dialect's answer.
188    pub fn rules(&self, lang: Lang) -> Result<Rules> {
189        // A setting is the host's, not the source text's, so these carry
190        // no span: there is nothing in the program to point at.
191        let refuse = |what: &str| -> Error {
192            Error::new(
193                ErrorKind::NotYet,
194                format!("{what} (the reading of another APL dialect) is not supported yet"),
195                None,
196            )
197            .note("libjay implements the APL2/ISO line, which is the one its oracle verifies")
198        };
199        if let Some(ct) = self.comparison_tolerance && !(ct.is_finite() && ct >= 0.0) {
200            return Err(Error::new(
201                ErrorKind::Domain,
202                "the comparison tolerance must be a finite value at or above zero",
203                None,
204            ));
205        }
206        match self.nested_model {
207            NestedModel::Floating => {}
208            NestedModel::Grounded => return Err(refuse("a grounded nested array model")),
209        }
210        match self.first_disclose {
211            FirstDisclose::UpIsFirst => {}
212            FirstDisclose::UpIsMix => return Err(refuse("↑ as mix and ⊃ as first")),
213        }
214        match self.index_form {
215            IndexForm::ScalarPerAxis => {}
216            IndexForm::AxisVectors => return Err(refuse("⌷ over index vectors")),
217        }
218        match self.dfn_result {
219            DfnResult::LastSentence => {}
220            DfnResult::FirstNonAssignment => {
221                return Err(refuse("a dfn that answers with its first non-assignment sentence"))
222            }
223        }
224        match self.default_arg {
225            DefaultArg::Eager => {}
226            DefaultArg::Lazy => return Err(refuse("a lazy ⍺← default")),
227        }
228        match self.complex_order {
229            ComplexOrder::RealThenImaginary => {}
230            ComplexOrder::MagnitudeThenAngle => {
231                return Err(refuse("grading complex values by magnitude and angle"))
232            }
233        }
234        match self.nested_grade {
235            NestedGrade::Apl2 => {}
236            NestedGrade::TotalOrder => {
237                return Err(refuse("a total array ordering for a nested grade"))
238            }
239        }
240        let origin = match lang {
241            Lang::J => 0,
242            Lang::Apl => self.index_origin.unwrap_or(1),
243        };
244        let ct = self.comparison_tolerance.unwrap_or(match lang {
245            Lang::J => Tol::J.ct,
246            Lang::Apl => Tol::APL.ct,
247        });
248        Ok(Rules {
249            lang,
250            origin,
251            ct,
252            nested_model: self.nested_model,
253            first_disclose: self.first_disclose,
254            index_form: self.index_form,
255            dfn_result: self.dfn_result,
256            default_arg: self.default_arg,
257            complex_order: self.complex_order,
258            nested_grade: self.nested_grade,
259            trains: self.trains,
260        })
261    }
262}
263
264/// A dialect resolved against a language: what the parser and the engine
265/// read. Copyable, and carried by every evaluation context, so a rule that
266/// only bites at run time (the index origin a key answers with, the order
267/// a grade puts complex values in) reads the same setting the parser did.
268#[derive(Clone, Copy, Debug, PartialEq)]
269pub struct Rules {
270    pub lang: Lang,
271    /// The index origin in force: APL's `⎕IO`, and 0 for J.
272    pub origin: i64,
273    /// The comparison tolerance in force. `Rules::tol` pairs it with the
274    /// language's scaling rule; a verb-local `u!.n` overrides that copy
275    /// and not this one.
276    pub ct: f64,
277    pub nested_model: NestedModel,
278    pub first_disclose: FirstDisclose,
279    pub index_form: IndexForm,
280    pub dfn_result: DfnResult,
281    pub default_arg: DefaultArg,
282    pub complex_order: ComplexOrder,
283    pub nested_grade: NestedGrade,
284    pub trains: bool,
285}
286
287impl Rules {
288    /// The dialect's comparison tolerance, with the language's scale.
289    pub fn tol(&self) -> Tol {
290        Tol { ct: self.ct, by_smaller: self.lang == Lang::J }
291    }
292
293    /// The host-facing form, for a nested compilation (`⍎`, `".`) that has
294    /// to run under the same dialect as the program executing it.
295    pub fn dialect(&self) -> Dialect {
296        Dialect {
297            index_origin: Some(self.origin),
298            comparison_tolerance: Some(self.ct),
299            nested_model: self.nested_model,
300            first_disclose: self.first_disclose,
301            index_form: self.index_form,
302            dfn_result: self.dfn_result,
303            default_arg: self.default_arg,
304            complex_order: self.complex_order,
305            nested_grade: self.nested_grade,
306            trains: self.trains,
307        }
308    }
309}
310
311impl Default for Rules {
312    /// J's rules, which is what a context built without a program uses.
313    fn default() -> Rules {
314        Dialect::default().rules(Lang::J).expect("J's defaults are implemented")
315    }
316}
317
318/// A source text with interpolation holes split out. Spans in every token
319/// and error refer to `display`, where hole `i` reads `{name_i}`.
320#[derive(Clone, Debug)]
321pub struct SourceParts {
322    pub display: String,
323    pub segments: Vec<Segment>,
324    pub param_names: Vec<String>,
325}
326
327#[derive(Clone, Debug)]
328pub enum Segment {
329    /// Literal source text starting at `offset` in `display`.
330    Text { text: String, offset: usize },
331    /// Interpolation hole: parameter `index`, shown as `{name}` in `display`.
332    Param { index: usize, offset: usize, len: usize },
333}
334
335impl SourceParts {
336    /// Build from pre-split literal parts with holes between them
337    /// (the t-string path). `names[i]` sits between `parts[i]` and
338    /// `parts[i+1]`; repeated names share one parameter.
339    pub fn from_parts(parts: &[&str], names: &[&str]) -> SourceParts {
340        assert_eq!(parts.len(), names.len() + 1, "N parts need N-1 holes");
341        let mut display = String::new();
342        let mut segments = Vec::new();
343        let mut param_names: Vec<String> = Vec::new();
344        for (i, part) in parts.iter().enumerate() {
345            if !part.is_empty() {
346                segments.push(Segment::Text { text: (*part).to_string(), offset: display.len() });
347                display.push_str(part);
348            }
349            if i < names.len() {
350                let name = names[i];
351                let index = param_names
352                    .iter()
353                    .position(|n| n == name)
354                    .unwrap_or_else(|| {
355                        param_names.push(name.to_string());
356                        param_names.len() - 1
357                    });
358                let shown = format!("{{{name}}}");
359                segments.push(Segment::Param { index, offset: display.len(), len: shown.len() });
360                display.push_str(&shown);
361            }
362        }
363        SourceParts { display, segments, param_names }
364    }
365
366    /// Build from a plain string where `{identifier}` outside quotes is an
367    /// interpolation hole (the pre-3.14 and Rust runtime path).
368    pub fn from_source(src: &str) -> Result<SourceParts> {
369        let bytes = src.as_bytes();
370        let mut parts: Vec<String> = vec![String::new()];
371        let mut names: Vec<String> = Vec::new();
372        let mut in_quote = false;
373        let mut i = 0;
374        while i < src.len() {
375            let ch = src[i..].chars().next().unwrap();
376            if ch == '\'' {
377                in_quote = !in_quote;
378                parts.last_mut().unwrap().push(ch);
379                i += 1;
380                continue;
381            }
382            if ch == '{' && !in_quote {
383                // Exactly `{identifier}` is an interpolation hole. Any other
384                // `{` is literal program text: J spells take as `{.`, drop as
385                // `}.`, so the brace itself belongs to the language.
386                let rest = &src[i + 1..];
387                if let Some(end) = rest.find('}') {
388                    let name = &rest[..end];
389                    if is_identifier(name) {
390                        names.push(name.to_string());
391                        parts.push(String::new());
392                        i += 2 + end;
393                        continue;
394                    }
395                }
396            }
397            parts.last_mut().unwrap().push(ch);
398            i += ch.len_utf8();
399        }
400        let _ = bytes;
401        let part_refs: Vec<&str> = parts.iter().map(|s| s.as_str()).collect();
402        let name_refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
403        Ok(SourceParts::from_parts(&part_refs, &name_refs))
404    }
405}
406
407fn is_identifier(s: &str) -> bool {
408    let mut chars = s.chars();
409    match chars.next() {
410        Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
411        _ => return false,
412    }
413    chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
414}
415
416/// Compile a plain source string (with `{name}` holes) in the given language.
417pub fn compile(lang: Lang, source: &str, dialect: &Dialect) -> Result<Program> {
418    let sp = SourceParts::from_source(source)?;
419    compile_source_parts(lang, sp, dialect)
420}
421
422/// Compile pre-split parts (the t-string path).
423pub fn compile_parts(
424    lang: Lang,
425    parts: &[&str],
426    names: &[&str],
427    dialect: &Dialect,
428) -> Result<Program> {
429    compile_source_parts(lang, SourceParts::from_parts(parts, names), dialect)
430}
431
432fn compile_source_parts(lang: Lang, sp: SourceParts, dialect: &Dialect) -> Result<Program> {
433    let rules = dialect.rules(lang)?;
434    let tol = rules.tol();
435    let (mut stmts, agreement, fmt) = match lang {
436        Lang::J => (j::parse(&sp)?, Agreement::LeadingPrefix, FmtOpts::J),
437        Lang::Apl => (apl::parse(&sp, rules)?, Agreement::ExactOrScalar, FmtOpts::APL),
438    };
439    // Everything after this point walks the tree recursively, so a
440    // sentence nested past what a stack holds is refused here rather than
441    // taking the process down. The measurement itself does not recurse.
442    for stmt in &stmts {
443        crate::verb::check_nesting(stmt.depth(), stmt.span())?;
444    }
445    crate::fuse::pass(&mut stmts, tol);
446    let params = sp.param_names.into_iter().map(|name| ParamSpec { name }).collect();
447    Ok(Program { stmts, params, display_src: sp.display, agreement, fmt, rules })
448}