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}