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}