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