libjay 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {libjay-0.2.0 → libjay-0.2.1}/Cargo.lock +5 -5
  2. {libjay-0.2.0 → libjay-0.2.1}/Cargo.toml +1 -1
  3. {libjay-0.2.0 → libjay-0.2.1}/PKG-INFO +1 -1
  4. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/array.rs +18 -3
  5. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/dtype.rs +9 -0
  6. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/explain.rs +5 -0
  7. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/fmt.rs +46 -11
  8. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/frontend/apl.rs +150 -2
  9. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/frontend/j.rs +52 -16
  10. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/fuse.rs +214 -71
  11. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/ir.rs +3 -0
  12. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/lib.rs +1 -0
  13. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/par.rs +32 -2
  14. libjay-0.2.1/crates/libjay/src/symbol.rs +131 -0
  15. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/verb.rs +1852 -234
  16. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay-python/src/data.rs +10 -2
  17. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay-python/src/lib.rs +6 -0
  18. {libjay-0.2.0 → libjay-0.2.1}/LICENSE +0 -0
  19. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/Cargo.toml +0 -0
  20. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/LICENSE +0 -0
  21. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/README.md +0 -0
  22. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/complex.rs +0 -0
  23. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/device/codegen.rs +0 -0
  24. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/device/gpu.rs +0 -0
  25. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/device/mod.rs +0 -0
  26. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/error.rs +0 -0
  27. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/exact.rs +0 -0
  28. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/frontend/mod.rs +0 -0
  29. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/gerund.rs +0 -0
  30. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/limits.rs +0 -0
  31. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/rng.rs +0 -0
  32. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay/src/simd.rs +0 -0
  33. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay-python/Cargo.toml +0 -0
  34. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay-testkit/Cargo.toml +0 -0
  35. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay-testkit/src/compare.rs +0 -0
  36. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay-testkit/src/corpus.rs +0 -0
  37. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay-testkit/src/eval.rs +0 -0
  38. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay-testkit/src/lib.rs +0 -0
  39. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay-testkit/src/replay.rs +0 -0
  40. {libjay-0.2.0 → libjay-0.2.1}/crates/libjay-testkit/src/snapshot.rs +0 -0
  41. {libjay-0.2.0 → libjay-0.2.1}/pyproject.toml +0 -0
  42. {libjay-0.2.0 → libjay-0.2.1}/python/README.md +0 -0
  43. {libjay-0.2.0 → libjay-0.2.1}/python/jay/__init__.py +0 -0
  44. {libjay-0.2.0 → libjay-0.2.1}/python/jay/_cli.py +0 -0
  45. {libjay-0.2.0 → libjay-0.2.1}/python/jay/_tstring.py +0 -0
  46. {libjay-0.2.0 → libjay-0.2.1}/python/jay/lang.py +0 -0
@@ -494,7 +494,7 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
494
494
 
495
495
  [[package]]
496
496
  name = "libjay"
497
- version = "0.2.0"
497
+ version = "0.2.1"
498
498
  dependencies = [
499
499
  "libjay-testkit",
500
500
  "multiversion",
@@ -510,14 +510,14 @@ dependencies = [
510
510
 
511
511
  [[package]]
512
512
  name = "libjay-capi"
513
- version = "0.2.0"
513
+ version = "0.2.1"
514
514
  dependencies = [
515
515
  "libjay",
516
516
  ]
517
517
 
518
518
  [[package]]
519
519
  name = "libjay-devtools"
520
- version = "0.2.0"
520
+ version = "0.2.1"
521
521
  dependencies = [
522
522
  "libjay-testkit",
523
523
  "rayon",
@@ -525,7 +525,7 @@ dependencies = [
525
525
 
526
526
  [[package]]
527
527
  name = "libjay-python"
528
- version = "0.2.0"
528
+ version = "0.2.1"
529
529
  dependencies = [
530
530
  "arrow-array",
531
531
  "arrow-buffer",
@@ -537,7 +537,7 @@ dependencies = [
537
537
 
538
538
  [[package]]
539
539
  name = "libjay-testkit"
540
- version = "0.2.0"
540
+ version = "0.2.1"
541
541
  dependencies = [
542
542
  "libjay",
543
543
  ]
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/libjay", "crates/libjay-python", "crates/libjay-testkit"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.2.0"
6
+ version = "0.2.1"
7
7
  edition = "2024"
8
8
  rust-version = "1.89"
9
9
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: libjay
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Intended Audience :: Science/Research
@@ -436,6 +436,10 @@ pub enum Data {
436
436
  /// pair of Arrow float columns all share.
437
437
  Complex(Buf<Cx>),
438
438
  Char(Buf<char>),
439
+ /// Symbols: every element is an index into the process-wide symbol
440
+ /// table (see [`crate::symbol`]), so the buffer is as flat and as
441
+ /// cheap to copy as one of integers and the names live once each.
442
+ Symbol(Buf<crate::symbol::Id>),
439
443
  /// Boxes: every element is a whole array. Foreign memory never holds
440
444
  /// these, so a boxed buffer is always owned and cloning it is a
441
445
  /// refcount bump like any other.
@@ -452,6 +456,7 @@ impl Data {
452
456
  Data::F64(_) => DType::F64,
453
457
  Data::Complex(_) => DType::Complex,
454
458
  Data::Char(_) => DType::Char,
459
+ Data::Symbol(_) => DType::Symbol,
455
460
  Data::Box(_) => DType::Box,
456
461
  }
457
462
  }
@@ -465,6 +470,7 @@ impl Data {
465
470
  Data::F64(v) => v.len(),
466
471
  Data::Complex(v) => v.len(),
467
472
  Data::Char(v) => v.len(),
473
+ Data::Symbol(v) => v.len(),
468
474
  Data::Box(v) => v.len(),
469
475
  }
470
476
  }
@@ -483,6 +489,7 @@ impl Data {
483
489
  Data::F64(v) => v.is_foreign(),
484
490
  Data::Complex(v) => v.is_foreign(),
485
491
  Data::Char(v) => v.is_foreign(),
492
+ Data::Symbol(v) => v.is_foreign(),
486
493
  Data::Box(v) => v.is_foreign(),
487
494
  }
488
495
  }
@@ -498,6 +505,7 @@ impl Data {
498
505
  Data::F64(v) => v.owner(),
499
506
  Data::Complex(v) => v.owner(),
500
507
  Data::Char(v) => v.owner(),
508
+ Data::Symbol(v) => v.owner(),
501
509
  Data::Box(v) => v.owner(),
502
510
  }
503
511
  }
@@ -511,6 +519,7 @@ impl Data {
511
519
  Data::F64(v) => Data::F64(v.slice(start, end)),
512
520
  Data::Complex(v) => Data::Complex(v.slice(start, end)),
513
521
  Data::Char(v) => Data::Char(v.slice(start, end)),
522
+ Data::Symbol(v) => Data::Symbol(v.slice(start, end)),
514
523
  Data::Box(v) => Data::Box(v.slice(start, end)),
515
524
  }
516
525
  }
@@ -524,6 +533,7 @@ impl Data {
524
533
  DType::F64 => Data::F64(Buf::new()),
525
534
  DType::Complex => Data::Complex(Buf::new()),
526
535
  DType::Char => Data::Char(Buf::new()),
536
+ DType::Symbol => Data::Symbol(Buf::new()),
527
537
  DType::Box => Data::Box(Buf::new()),
528
538
  }
529
539
  }
@@ -539,6 +549,7 @@ impl Data {
539
549
  Data::F64(v) => v.push(0.0),
540
550
  Data::Complex(v) => v.push(crate::complex::ZERO),
541
551
  Data::Char(v) => v.push(' '),
552
+ Data::Symbol(v) => v.push(crate::symbol::EMPTY),
542
553
  Data::Box(v) => v.push(Array::box_fill()),
543
554
  }
544
555
  }
@@ -554,6 +565,7 @@ impl Data {
554
565
  (Data::F64(a), Data::F64(b)) => a.push(b[i]),
555
566
  (Data::Complex(a), Data::Complex(b)) => a.push(b[i]),
556
567
  (Data::Char(a), Data::Char(b)) => a.push(b[i]),
568
+ (Data::Symbol(a), Data::Symbol(b)) => a.push(b[i]),
557
569
  (Data::Box(a), Data::Box(b)) => a.push(b[i].clone()),
558
570
  _ => {}
559
571
  }
@@ -568,6 +580,7 @@ impl Data {
568
580
  (Data::F64(a), Data::F64(b)) => a.extend_from_slice(b),
569
581
  (Data::Complex(a), Data::Complex(b)) => a.extend_from_slice(b),
570
582
  (Data::Char(a), Data::Char(b)) => a.extend_from_slice(b),
583
+ (Data::Symbol(a), Data::Symbol(b)) => a.extend_from_slice(b),
571
584
  (Data::Box(a), Data::Box(b)) => a.extend_from_slice(b),
572
585
  _ => return false,
573
586
  }
@@ -605,6 +618,7 @@ impl Data {
605
618
  DType::F64 => by!(F64, Buf::join_fast),
606
619
  DType::Complex => by!(Complex, Buf::join_fast),
607
620
  DType::Char => by!(Char, Buf::join),
621
+ DType::Symbol => by!(Symbol, Buf::join_fast),
608
622
  DType::Ext => by!(Ext, Buf::join),
609
623
  DType::Rat => by!(Rat, Buf::join),
610
624
  DType::Box => by!(Box, Buf::join),
@@ -703,6 +717,7 @@ impl Data {
703
717
  DType::F64 => by!(F64, weave),
704
718
  DType::Complex => by!(Complex, weave),
705
719
  DType::Char => by!(Char, weave),
720
+ DType::Symbol => by!(Symbol, weave),
706
721
  DType::Ext => by!(Ext, weave_cloned),
707
722
  DType::Rat => by!(Rat, weave_cloned),
708
723
  DType::Box => by!(Box, weave_cloned),
@@ -1035,7 +1050,7 @@ impl Array {
1035
1050
  Data::Rat(v) => Some(v.iter().map(|x| [x.to_f64(), 0.0]).collect()),
1036
1051
  Data::F64(v) => Some(v.iter().map(|&x| [x, 0.0]).collect()),
1037
1052
  Data::Complex(v) => Some(v.to_vec()),
1038
- Data::Char(_) | Data::Box(_) => None,
1053
+ Data::Char(_) | Data::Symbol(_) | Data::Box(_) => None,
1039
1054
  }
1040
1055
  }
1041
1056
 
@@ -1049,7 +1064,7 @@ impl Array {
1049
1064
  Data::F64(v) => Some(v.to_vec()),
1050
1065
  // A complex value is not a real one, even when its imaginary
1051
1066
  // part is zero: the caller wants a real and must ask for it.
1052
- Data::Complex(_) | Data::Char(_) | Data::Box(_) => None,
1067
+ Data::Complex(_) | Data::Char(_) | Data::Symbol(_) | Data::Box(_) => None,
1053
1068
  }
1054
1069
  }
1055
1070
 
@@ -1074,7 +1089,7 @@ impl Array {
1074
1089
  }
1075
1090
  Some(out)
1076
1091
  }
1077
- Data::Complex(_) | Data::Char(_) | Data::Box(_) => None,
1092
+ Data::Complex(_) | Data::Char(_) | Data::Symbol(_) | Data::Box(_) => None,
1078
1093
  }
1079
1094
  }
1080
1095
  }
@@ -13,6 +13,9 @@ pub enum DType {
13
13
  /// A complex number, held as an interleaved `[re, im]` pair.
14
14
  Complex,
15
15
  Char,
16
+ /// An interned name (J `s:`). The element is an index into the
17
+ /// process-wide symbol table, not the text itself.
18
+ Symbol,
16
19
  /// A box: every element is itself an array (J `<`, APL `⊂`).
17
20
  Box,
18
21
  }
@@ -27,6 +30,7 @@ impl DType {
27
30
  DType::F64 => "float",
28
31
  DType::Complex => "complex",
29
32
  DType::Char => "character",
33
+ DType::Symbol => "symbol",
30
34
  DType::Box => "boxed",
31
35
  }
32
36
  }
@@ -55,6 +59,8 @@ impl DType {
55
59
  (Box, _) | (_, Box) => None,
56
60
  (Char, Char) => Some(Char),
57
61
  (Char, _) | (_, Char) => None,
62
+ (Symbol, Symbol) => Some(Symbol),
63
+ (Symbol, _) | (_, Symbol) => None,
58
64
  (Complex, _) | (_, Complex) => Some(Complex),
59
65
  (F64, _) | (_, F64) => Some(F64),
60
66
  (Rat, _) | (_, Rat) => Some(Rat),
@@ -85,5 +91,8 @@ mod tests {
85
91
  fn characters_and_boxes_mix_with_nothing() {
86
92
  assert_eq!(super::DType::promote(Char, Ext), None);
87
93
  assert_eq!(super::DType::promote(Box, Rat), None);
94
+ assert_eq!(super::DType::promote(Symbol, Char), None);
95
+ assert_eq!(super::DType::promote(Symbol, I64), None);
96
+ assert_eq!(super::DType::promote(Symbol, Symbol), Some(Symbol));
88
97
  }
89
98
  }
@@ -426,6 +426,11 @@ fn verb_lines(v: &Verb, depth: usize, p: &Program, tr: &Trace, out: &mut String)
426
426
  verb_lines(f, depth + 1, p, tr, out);
427
427
  verb_lines(g, depth + 1, p, tr, out);
428
428
  }
429
+ Verb::InnerProduct { u, v, .. } => {
430
+ head(out, "inner product (u . v; the matrix product for +/ . *)");
431
+ verb_lines(u, depth + 1, p, tr, out);
432
+ verb_lines(v, depth + 1, p, tr, out);
433
+ }
429
434
  Verb::UserDerived { def, alpha, omega } => {
430
435
  head(out, "a user-written operator with its operands");
431
436
  verb_lines(def, depth + 1, p, tr, out);
@@ -52,14 +52,35 @@ pub fn format_array(a: &Array, opts: &FmtOpts) -> String {
52
52
  // rather than with a nested display's extra spacing.
53
53
  match mixed_simple_texts(a, opts) {
54
54
  Some(texts) if opts.boxes == BoxStyle::Spaced => {
55
- return laid_out(&a.shape, texts, false)
55
+ return laid_out(&a.shape, texts, Cells::Right)
56
56
  }
57
57
  _ => return format_boxed(a, opts),
58
58
  }
59
59
  }
60
60
  let texts: Vec<String> = (0..a.count()).map(|i| format_atom(&a.data, i, opts)).collect();
61
- // Characters are laid out as text lines; everything else as columns.
62
- laid_out(&a.shape, texts, a.dtype() == DType::Char)
61
+ laid_out(&a.shape, texts, Cells::of(a.dtype()))
62
+ }
63
+
64
+ /// How the formatted elements of one row sit next to each other.
65
+ #[derive(Clone, Copy, PartialEq)]
66
+ enum Cells {
67
+ /// Numbers: one space between columns, each column right-aligned.
68
+ Right,
69
+ /// Characters: no separator at all, because the row IS the text.
70
+ Text,
71
+ /// Symbols: one space between columns, each column left-aligned and
72
+ /// padded on the right, which is how J prints a table of names.
73
+ Left,
74
+ }
75
+
76
+ impl Cells {
77
+ fn of(dtype: DType) -> Cells {
78
+ match dtype {
79
+ DType::Char => Cells::Text,
80
+ DType::Symbol => Cells::Left,
81
+ _ => Cells::Right,
82
+ }
83
+ }
63
84
  }
64
85
 
65
86
  /// The scalar each element of a boxed array holds, where every one of them
@@ -78,19 +99,23 @@ fn mixed_simple_texts(a: &Array, opts: &FmtOpts) -> Option<Vec<String>> {
78
99
 
79
100
  /// One formatted element per position, laid out for the shape: a vector on
80
101
  /// one line, higher ranks as aligned columns and planes.
81
- fn laid_out(shape: &[usize], texts: Vec<String>, text_layout: bool) -> String {
102
+ fn laid_out(shape: &[usize], texts: Vec<String>, cells: Cells) -> String {
82
103
  let rank = shape.len();
83
104
  let a_shape = shape;
84
105
  match rank {
85
106
  0 => texts.into_iter().next().unwrap_or_default(),
86
- 1 if text_layout => texts.concat(),
107
+ 1 if cells == Cells::Text => texts.concat(),
87
108
  1 => texts.join(" "),
88
109
  _ => {
89
110
  let ncols = a_shape[rank - 1];
90
111
  let nrows = a_shape[rank - 2];
91
112
  // Column widths span every plane, so planes stay aligned with
92
113
  // each other and not just internally.
93
- let widths = if text_layout { vec![0; ncols] } else { column_widths(&texts, ncols) };
114
+ let widths = if cells == Cells::Text {
115
+ vec![0; ncols]
116
+ } else {
117
+ column_widths(&texts, ncols)
118
+ };
94
119
  let frame = &a_shape[..rank - 2];
95
120
  let plane_size = nrows * ncols;
96
121
  let planes: usize = frame.iter().product();
@@ -105,7 +130,7 @@ fn laid_out(shape: &[usize], texts: Vec<String>, text_layout: bool) -> String {
105
130
  out.push('\n');
106
131
  }
107
132
  let start = p * plane_size + r * ncols;
108
- push_row(&mut out, &texts[start..start + ncols], &widths, text_layout);
133
+ push_row(&mut out, &texts[start..start + ncols], &widths, cells);
109
134
  }
110
135
  }
111
136
  out
@@ -257,19 +282,27 @@ fn column_widths(texts: &[String], ncols: usize) -> Vec<usize> {
257
282
  widths
258
283
  }
259
284
 
260
- fn push_row(out: &mut String, row: &[String], widths: &[usize], text_layout: bool) {
285
+ fn push_row(out: &mut String, row: &[String], widths: &[usize], cells: Cells) {
261
286
  for (j, cell) in row.iter().enumerate() {
262
- if text_layout {
287
+ if cells == Cells::Text {
263
288
  out.push_str(cell);
264
289
  continue;
265
290
  }
266
291
  if j > 0 {
267
292
  out.push(' ');
268
293
  }
269
- for _ in 0..widths[j].saturating_sub(width(cell)) {
270
- out.push(' ');
294
+ let pad = widths[j].saturating_sub(width(cell));
295
+ if cells == Cells::Right {
296
+ for _ in 0..pad {
297
+ out.push(' ');
298
+ }
271
299
  }
272
300
  out.push_str(cell);
301
+ if cells == Cells::Left {
302
+ for _ in 0..pad {
303
+ out.push(' ');
304
+ }
305
+ }
273
306
  }
274
307
  }
275
308
 
@@ -287,6 +320,8 @@ fn format_atom(data: &Data, i: usize, opts: &FmtOpts) -> String {
287
320
  Data::F64(v) => format_f64(v[i], opts),
288
321
  Data::Complex(v) => format_complex(v[i], opts),
289
322
  Data::Char(v) => v[i].to_string(),
323
+ // A symbol prints as its name behind the backtick that makes one.
324
+ Data::Symbol(v) => format!("`{}", crate::symbol::name(v[i])),
290
325
  // Boxed data takes the drawing path before reaching here.
291
326
  Data::Box(_) => String::new(),
292
327
  }
@@ -168,6 +168,11 @@ enum OpGlyph {
168
168
  Before,
169
169
  /// `⌸` key (Dyalog): each distinct major cell with what shares it.
170
170
  Key,
171
+ /// `.` between two functions: the inner product, `+.×` above all. The
172
+ /// operand on its right is a function too, as `∘.`'s is.
173
+ Dot,
174
+ /// `⍠` variant: one dialect knob overridden for this application.
175
+ Variant,
171
176
  }
172
177
 
173
178
  impl OpGlyph {
@@ -187,6 +192,8 @@ impl OpGlyph {
187
192
  OpGlyph::Each => '¨',
188
193
  OpGlyph::Before => '⍛',
189
194
  OpGlyph::Key => '⌸',
195
+ OpGlyph::Dot => '.',
196
+ OpGlyph::Variant => '⍠',
190
197
  }
191
198
  }
192
199
  }
@@ -667,7 +674,6 @@ fn quad_name(name: &str, d: Rules, span: Span) -> Result<Tok> {
667
674
  /// it under. These are queue positions, not unknown characters.
668
675
  fn queued_glyph(ch: char) -> Option<&'static str> {
669
676
  Some(match ch {
670
- '⍠' => "the variant operator (f⍠v)",
671
677
  '⌶' => "I-beam (⌶)",
672
678
  // Dyalog's spawn. Named as a queue position rather than reported as
673
679
  // an unknown character; whether libjay's sandbox opens APL's
@@ -693,6 +699,10 @@ fn op_for(ch: char) -> Option<OpGlyph> {
693
699
  '¨' => Some(OpGlyph::Each),
694
700
  '⍛' => Some(OpGlyph::Before),
695
701
  '⌸' => Some(OpGlyph::Key),
702
+ '⍠' => Some(OpGlyph::Variant),
703
+ // A `.` that is not the start of a number and not the tail of `∘.`
704
+ // is the inner-product operator.
705
+ '.' => Some(OpGlyph::Dot),
696
706
  _ => None,
697
707
  }
698
708
  }
@@ -1252,7 +1262,10 @@ fn fold_operators(toks: Vec<Token>, d: Rules) -> Result<Vec<Token>> {
1252
1262
  }
1253
1263
  // `f∘g` and `f⍥g` need a function on both sides; the right one is
1254
1264
  // taken here so the ordinary "operand to the left" path can run.
1255
- if matches!(op, OpGlyph::Jot | OpGlyph::Over | OpGlyph::Before | OpGlyph::Under) {
1265
+ if matches!(
1266
+ op,
1267
+ OpGlyph::Jot | OpGlyph::Over | OpGlyph::Before | OpGlyph::Under | OpGlyph::Dot
1268
+ ) {
1256
1269
  let Some(gtok) = it.peek().filter(|x| matches!(x.kind, Tok::Func(_))) else {
1257
1270
  return Err(Error::not_yet(
1258
1271
  format!("{} with a value operand", op.glyph()),
@@ -1288,6 +1301,13 @@ fn fold_operators(toks: Vec<Token>, d: Rules) -> Result<Vec<Token>> {
1288
1301
  let composed = Verb::Compose(Box::new(f), Box::new(g));
1289
1302
  Verb::Atop(Box::new(back), Box::new(composed))
1290
1303
  }
1304
+ // `f.g` folds with f what g made of every row and column:
1305
+ // `+.×` is the matrix product, `∧.=` asks which rows match.
1306
+ OpGlyph::Dot => Verb::InnerProduct {
1307
+ u: Box::new(Verb::Reduce(Box::new(f))),
1308
+ v: Box::new(g),
1309
+ apl: true,
1310
+ },
1291
1311
  _ => Verb::Compose(Box::new(f), Box::new(g)),
1292
1312
  };
1293
1313
  out.push(Token { kind: Tok::Func(derived), span });
@@ -1315,6 +1335,8 @@ fn fold_operators(toks: Vec<Token>, d: Rules) -> Result<Vec<Token>> {
1315
1335
  | OpGlyph::Stencil
1316
1336
  | OpGlyph::Before
1317
1337
  | OpGlyph::Key
1338
+ | OpGlyph::Dot
1339
+ | OpGlyph::Variant
1318
1340
  | OpGlyph::Each => {
1319
1341
  return Err(Error::parse(
1320
1342
  format!("{} needs a function to its left", op.glyph()),
@@ -1375,6 +1397,19 @@ fn fold_operators(toks: Vec<Token>, d: Rules) -> Result<Vec<Token>> {
1375
1397
  }
1376
1398
  OpGlyph::Commute => Verb::Commute(Box::new(f)),
1377
1399
  OpGlyph::Key => Verb::KeyPairs(Box::new(f)),
1400
+ // `f⍠B`: one setting of the dialect overridden for this
1401
+ // application and no other.
1402
+ OpGlyph::Variant => {
1403
+ let (options, ospan) = variant_options(&mut it, t.span)?;
1404
+ let derived = variant(f, &options, Span::merge(span, ospan))?;
1405
+ out.push(Token { kind: Tok::Func(derived), span: Span::merge(span, ospan) });
1406
+ continue;
1407
+ }
1408
+ // `.` reached with no function on its right: `+.` alone is not
1409
+ // a function in either lineage.
1410
+ OpGlyph::Dot => {
1411
+ return Err(Error::parse("the inner product . needs a function on its right", t.span));
1412
+ }
1378
1413
  // Each: the function runs on the contents of every item and
1379
1414
  // its result goes back into an item. A simple scalar result
1380
1415
  // stays simple, which is APL's enclosure rule.
@@ -1507,6 +1542,119 @@ fn take_axis(
1507
1542
  Ok(Some((k as usize, span)))
1508
1543
  }
1509
1544
 
1545
+ /// The options `⍠` was given, as `(name, value)` pairs.
1546
+ ///
1547
+ /// A bare number is the PRINCIPAL option, which for every function libjay
1548
+ /// gives a variant is the comparison tolerance — so it arrives here named
1549
+ /// `CT`. The other published spelling is one or more parenthesised pairs,
1550
+ /// `⍠('IO' 0)` and `⍠('IO' 0)('CT' 0)`, whose halves are both literals.
1551
+ /// A computed option is a named gap: the variant is settled when the
1552
+ /// program is compiled, as the dialect it overrides is.
1553
+ fn variant_options(
1554
+ it: &mut std::iter::Peekable<std::vec::IntoIter<Token>>,
1555
+ span: Span,
1556
+ ) -> Result<(Vec<(String, Array)>, Span)> {
1557
+ if let Some(tok) = it.peek().filter(|t| literal(&t.kind).is_some()) {
1558
+ let (value, vspan) = (literal(&tok.kind).expect("peeked a literal").clone(), tok.span);
1559
+ it.next();
1560
+ return Ok((vec![("CT".to_string(), value)], vspan));
1561
+ }
1562
+ let mut options = Vec::new();
1563
+ let mut last = span;
1564
+ while it.peek().is_some_and(|t| matches!(t.kind, Tok::LParen)) {
1565
+ it.next();
1566
+ let mut inside: Vec<Array> = Vec::new();
1567
+ loop {
1568
+ let Some(tok) = it.next() else {
1569
+ return Err(Error::parse("unmatched ( after ⍠", span));
1570
+ };
1571
+ last = tok.span;
1572
+ if matches!(tok.kind, Tok::RParen) {
1573
+ break;
1574
+ }
1575
+ match literal(&tok.kind) {
1576
+ Some(a) => inside.push(a.clone()),
1577
+ None => {
1578
+ return Err(Error::not_yet(
1579
+ "a computed variant option (f⍠v with a name or an expression)",
1580
+ tok.span,
1581
+ ));
1582
+ }
1583
+ }
1584
+ }
1585
+ let [name, value] = inside.as_slice() else {
1586
+ return Err(Error::parse("a variant option is a name and a value", last));
1587
+ };
1588
+ let Data::Char(cs) = &name.data else {
1589
+ return Err(Error::parse("a variant option starts with its name", last));
1590
+ };
1591
+ options.push((cs.as_slice().iter().collect::<String>().to_uppercase(), value.clone()));
1592
+ }
1593
+ if options.is_empty() {
1594
+ let where_ = it.peek().map_or(span, |t| t.span);
1595
+ return Err(Error::not_yet(
1596
+ "a computed variant option (f⍠v with a name or an expression)",
1597
+ where_,
1598
+ ));
1599
+ }
1600
+ Ok((options, last))
1601
+ }
1602
+
1603
+ /// `f⍠B`: f with the dialect settings B names overridden for this
1604
+ /// application. `CT` is the comparison tolerance, which is the same
1605
+ /// mechanism J spells `!.`; `IO` is the index origin, which is resolved
1606
+ /// into the primitives when the program is compiled, so overriding it
1607
+ /// derives the verb again.
1608
+ fn variant(f: Verb, options: &[(String, Array)], span: Span) -> Result<Verb> {
1609
+ let mut out = f;
1610
+ for (name, value) in options {
1611
+ out = match name.as_str() {
1612
+ "CT" => {
1613
+ let Some(ct) = value.to_f64_vec().and_then(|v| v.first().copied()) else {
1614
+ return Err(Error::domain("a comparison tolerance is a number", span));
1615
+ };
1616
+ if !out.uses_tolerance() {
1617
+ return Err(Error::domain(
1618
+ format!(
1619
+ "the comparison tolerance is not an option of {}: it consults none",
1620
+ out.name()
1621
+ ),
1622
+ span,
1623
+ ));
1624
+ }
1625
+ if !(0.0..1.0).contains(&ct) {
1626
+ return Err(Error::domain(
1627
+ "a comparison tolerance lies between 0 and 1",
1628
+ span,
1629
+ ));
1630
+ }
1631
+ Verb::Fit(Box::new(out), ct)
1632
+ }
1633
+ "IO" => {
1634
+ let Some(io) = value.to_i64_vec().and_then(|v| v.first().copied()) else {
1635
+ return Err(Error::domain("an index origin is a whole number", span));
1636
+ };
1637
+ if io != 0 && io != 1 {
1638
+ return Err(Error::domain("an index origin is 0 or 1", span));
1639
+ }
1640
+ crate::verb::with_origin(&out, io).ok_or_else(|| {
1641
+ Error::domain(
1642
+ format!("the index origin is not an option of {}", out.name()),
1643
+ span,
1644
+ )
1645
+ })?
1646
+ }
1647
+ other => {
1648
+ return Err(Error::not_yet(
1649
+ format!("the variant option {other} (f⍠v)"),
1650
+ span,
1651
+ ));
1652
+ }
1653
+ };
1654
+ }
1655
+ Ok(out)
1656
+ }
1657
+
1510
1658
  /// `(/)` is `/`: parentheses around a bare operator glyph are transparent,
1511
1659
  /// so what decides the glyph's reading is the token outside them. The pair
1512
1660
  /// has no other meaning — an operator has no operand inside them — and the